Download center: copy code, online/offline status; remove client offline mode.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+24
-3
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
|
from datetime import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
@@ -323,7 +324,8 @@ def register_routes(app: Flask) -> None:
|
|||||||
if u["role"] == "admin":
|
if u["role"] == "admin":
|
||||||
acts = conn.execute(
|
acts = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT act.*, a.name AS account_name, m.name AS machine_name
|
SELECT act.*, a.name AS account_name, m.name AS machine_name,
|
||||||
|
m.last_seen_at AS machine_last_seen
|
||||||
FROM activations act
|
FROM activations act
|
||||||
LEFT JOIN accounts a ON a.id = act.account_id
|
LEFT JOIN accounts a ON a.id = act.account_id
|
||||||
LEFT JOIN machines m ON m.id = act.machine_id
|
LEFT JOIN machines m ON m.id = act.machine_id
|
||||||
@@ -333,7 +335,8 @@ def register_routes(app: Flask) -> None:
|
|||||||
else:
|
else:
|
||||||
acts = conn.execute(
|
acts = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT act.*, a.name AS account_name, m.name AS machine_name
|
SELECT act.*, a.name AS account_name, m.name AS machine_name,
|
||||||
|
m.last_seen_at AS machine_last_seen
|
||||||
FROM activations act
|
FROM activations act
|
||||||
LEFT JOIN accounts a ON a.id = act.account_id
|
LEFT JOIN accounts a ON a.id = act.account_id
|
||||||
LEFT JOIN machines m ON m.id = act.machine_id
|
LEFT JOIN machines m ON m.id = act.machine_id
|
||||||
@@ -342,7 +345,25 @@ def register_routes(app: Flask) -> None:
|
|||||||
""",
|
""",
|
||||||
(u.get("account_id"),),
|
(u.get("account_id"),),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
activations = [database.row_to_dict(r) for r in acts]
|
activations = []
|
||||||
|
now_ts = database.now()
|
||||||
|
online_sec = 180 # 3 分钟内有心跳视为在线
|
||||||
|
for r in acts:
|
||||||
|
item = database.row_to_dict(r)
|
||||||
|
last = item.get("machine_last_seen")
|
||||||
|
if item.get("status") == "active" and last and (now_ts - float(last)) <= online_sec:
|
||||||
|
item["online_label"] = "在线"
|
||||||
|
item["online"] = True
|
||||||
|
else:
|
||||||
|
item["online_label"] = "离线"
|
||||||
|
item["online"] = False
|
||||||
|
try:
|
||||||
|
item["created_at_fmt"] = datetime.fromtimestamp(float(item["created_at"])).strftime(
|
||||||
|
"%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError, OSError):
|
||||||
|
item["created_at_fmt"] = str(item.get("created_at") or "—")
|
||||||
|
activations.append(item)
|
||||||
exe_name = "黄金市价辅助.exe"
|
exe_name = "黄金市价辅助.exe"
|
||||||
exe_path = RELEASES_DIR / exe_name
|
exe_path = RELEASES_DIR / exe_name
|
||||||
return render_template(
|
return render_template(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="grid cols-2">
|
<div class="grid cols-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>通用辅助包</h3>
|
<h3>通用辅助包</h3>
|
||||||
<p class="muted">所有机器下载同一 exe。首次启动输入激活码,绑定账户与机器标识。</p>
|
<p class="muted">所有机器下载同一 exe。首次启动输入激活码,绑定账户与机器标识(须在线,不允许离线运行)。</p>
|
||||||
{% if exe_ready %}
|
{% if exe_ready %}
|
||||||
<p><span class="tag green">已上传 {{ exe_name }}</span></p>
|
<p><span class="tag green">已上传 {{ exe_name }}</span></p>
|
||||||
<a class="btn" href="{{ url_for('download_file') }}">下载 exe</a>
|
<a class="btn" href="{{ url_for('download_file') }}">下载 exe</a>
|
||||||
@@ -48,11 +48,21 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for a in activations %}
|
{% for a in activations %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>{{ a.code }}</code></td>
|
<td>
|
||||||
|
<code id="code-{{ loop.index }}">{{ a.code }}</code>
|
||||||
|
<button type="button" class="btn secondary btn-copy" style="margin-left:8px;padding:4px 10px;font-size:12px"
|
||||||
|
data-code="{{ a.code }}">复制</button>
|
||||||
|
</td>
|
||||||
<td>{{ a.account_name or a.account_id }}</td>
|
<td>{{ a.account_name or a.account_id }}</td>
|
||||||
<td>{{ a.machine_name or a.machine_id or '—' }}</td>
|
<td>{{ a.machine_name or a.machine_id or '—' }}</td>
|
||||||
<td><span class="tag {% if a.status=='active' %}green{% else %}gray{% endif %}">{{ a.status }}</span></td>
|
<td>
|
||||||
<td class="muted">{{ a.created_at }}</td>
|
{% if a.online %}
|
||||||
|
<span class="tag green">在线</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="tag gray">离线</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="muted">{{ a.created_at_fmt }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="5" class="muted">暂无激活码</td></tr>
|
<tr><td colspan="5" class="muted">暂无激活码</td></tr>
|
||||||
@@ -61,3 +71,27 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.btn-copy').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var code = btn.getAttribute('data-code') || '';
|
||||||
|
function done(ok) {
|
||||||
|
var old = btn.textContent;
|
||||||
|
btn.textContent = ok ? '已复制' : '失败';
|
||||||
|
setTimeout(function () { btn.textContent = old; }, 1200);
|
||||||
|
}
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(code).then(function () { done(true); }).catch(function () { done(false); });
|
||||||
|
} else {
|
||||||
|
var ta = document.createElement('textarea');
|
||||||
|
ta.value = code;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
try { document.execCommand('copy'); done(true); } catch (e) { done(false); }
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
+7
-10
@@ -3,14 +3,15 @@
|
|||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 连接本机已登录 MT5,市价开平、ATR 止损止盈、保本、满仓灰显等(由原 `MT5黄金方案/Desktop` 迁入)
|
- 连接本机已登录 MT5,市价开平、ATR 止损止盈、保本、满仓灰显等(由原 `MT5黄金方案/Desktop` 迁入)
|
||||||
- **通用包 + 首次激活**:仅第一次填写中控地址与激活码,写入本机后以后自动连接
|
- **通用包 + 首次激活**:仅第一次填写中控地址与激活码;**不允许离线运行**
|
||||||
- **打开复盘**:用已保存的中控地址打开 `/login`,无需再输入
|
- **打开复盘**:用已保存的中控地址打开 `/login`
|
||||||
- **自动上报**:开仓/平仓成功后 `POST /api/v1/trades`,并上传 M1/M5/M15/H1 K 线供网页复盘
|
- **心跳**:运行中每分钟 ping 中控,网页下载中心显示在线/离线
|
||||||
|
- **自动上报**:开仓/平仓成功后上报成交与四周期 K 线
|
||||||
|
|
||||||
## 本地配置
|
## 本地配置
|
||||||
|
|
||||||
激活信息保存在:`%USERPROFILE%\.gold_monitor\device.json`(含 `server`、`token` 等)。
|
激活信息:`%USERPROFILE%\.gold_monitor\device.json`(`server`、`token` 等)。
|
||||||
已激活后启动不再弹窗;删掉该文件才会再次要求激活。
|
已激活且中控可达则直接进主界面;中控不可达会提示重试,不能跳过。
|
||||||
|
|
||||||
## 开发运行
|
## 开发运行
|
||||||
|
|
||||||
@@ -27,8 +28,4 @@ py -3 gold_order_app.py
|
|||||||
build_exe.bat
|
build_exe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
产物:`dist\黄金市价辅助.exe`,复制到仓库 `releases\黄金市价辅助.exe` 后由网页下载中心分发。
|
产物同步到仓库 `releases\黄金市价辅助.exe` 供下载中心分发。
|
||||||
|
|
||||||
## 离线模式
|
|
||||||
|
|
||||||
激活窗可点「离线使用」:正常下单,但不上报复盘中心。
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""首次激活对话框:仅第一次填写中控地址 + 激活码;之后自动读取本地配置。"""
|
"""首次激活:必须填写中控地址 + 激活码,不允许离线运行。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -24,32 +24,51 @@ def _bring_front(win: tk.Misc) -> None:
|
|||||||
|
|
||||||
def ensure_activated(parent: Optional[tk.Misc] = None) -> bool:
|
def ensure_activated(parent: Optional[tk.Misc] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
- 已激活(本地有 server + token):直接放行,不再弹窗;中控地址固定复用。
|
- 已激活:探测中控,成功则放行;失败则提示重试/退出(不允许离线)。
|
||||||
- 未激活:弹出首次激活窗,成功后写入 device.json。
|
- 未激活:必须激活成功才能进入。
|
||||||
"""
|
"""
|
||||||
if is_activated():
|
if is_activated():
|
||||||
# 后台轻量探测(失败也不打扰,主界面仍可用,上报会静默失败)
|
if hub_client.ping():
|
||||||
|
return True
|
||||||
|
return _ask_retry_or_exit(parent)
|
||||||
|
|
||||||
|
return _show_activation_form(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_retry_or_exit(parent: Optional[tk.Misc]) -> bool:
|
||||||
|
owns = parent is None
|
||||||
|
root = parent or tk.Tk()
|
||||||
|
if owns:
|
||||||
|
root.withdraw()
|
||||||
try:
|
try:
|
||||||
hub_client.ping()
|
while True:
|
||||||
|
_bring_front(root)
|
||||||
|
retry = messagebox.askretrycancel(
|
||||||
|
"无法连接中控",
|
||||||
|
"客户端不允许离线运行。\n请确认中控已启动且本机网络正常后重试。\n\n点「重试」再次连接,点「取消」退出。",
|
||||||
|
parent=root,
|
||||||
|
)
|
||||||
|
if not retry:
|
||||||
|
return False
|
||||||
|
if hub_client.ping():
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
if owns:
|
||||||
|
try:
|
||||||
|
root.destroy()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return True
|
|
||||||
|
|
||||||
owns_root = False
|
|
||||||
if parent is not None:
|
def _show_activation_form(parent: Optional[tk.Misc]) -> bool:
|
||||||
root = parent
|
owns_root = parent is None
|
||||||
else:
|
root = parent or tk.Tk()
|
||||||
root = tk.Tk()
|
|
||||||
owns_root = True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if owns_root:
|
win = root if owns_root else tk.Toplevel(root)
|
||||||
win = root
|
|
||||||
else:
|
|
||||||
win = tk.Toplevel(root)
|
|
||||||
win.title("首次激活 · 黄金辅助")
|
win.title("首次激活 · 黄金辅助")
|
||||||
win.minsize(520, 420)
|
win.minsize(520, 400)
|
||||||
win.geometry("520x420")
|
win.geometry("520x400")
|
||||||
win.resizable(True, True)
|
win.resizable(True, True)
|
||||||
result = {"ok": False}
|
result = {"ok": False}
|
||||||
device = load_device()
|
device = load_device()
|
||||||
@@ -62,7 +81,7 @@ def ensure_activated(parent: Optional[tk.Misc] = None) -> bool:
|
|||||||
|
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
body,
|
body,
|
||||||
text="首次使用:填写中控地址与激活码。激活成功后会保存在本机,以后打开无需再填。",
|
text="必须激活后才能使用。填写中控地址与激活码,成功后本机保存,以后自动连接。",
|
||||||
wraplength=470,
|
wraplength=470,
|
||||||
).pack(anchor="w")
|
).pack(anchor="w")
|
||||||
|
|
||||||
@@ -83,9 +102,8 @@ def ensure_activated(parent: Optional[tk.Misc] = None) -> bool:
|
|||||||
status = ttk.Label(body, text="", foreground="#888")
|
status = ttk.Label(body, text="", foreground="#888")
|
||||||
status.pack(anchor="w", pady=(14, 0))
|
status.pack(anchor="w", pady=(14, 0))
|
||||||
|
|
||||||
def do_skip():
|
def do_exit():
|
||||||
# 离线:不写 token,下次仍会提示首次激活
|
result["ok"] = False
|
||||||
result["ok"] = True
|
|
||||||
win.destroy()
|
win.destroy()
|
||||||
|
|
||||||
def do_ok():
|
def do_ok():
|
||||||
@@ -103,20 +121,17 @@ def ensure_activated(parent: Optional[tk.Misc] = None) -> bool:
|
|||||||
result["ok"] = True
|
result["ok"] = True
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"激活成功",
|
"激活成功",
|
||||||
f"已绑定中控:{server}\n以后打开将自动连接,无需再输入。",
|
f"已绑定中控:{server}\n以后打开将自动连接。",
|
||||||
parent=win,
|
parent=win,
|
||||||
)
|
)
|
||||||
win.destroy()
|
win.destroy()
|
||||||
except hub_client.HubError as exc:
|
except hub_client.HubError as exc:
|
||||||
status.configure(text=str(exc), foreground="#f6465d")
|
status.configure(text=str(exc), foreground="#f6465d")
|
||||||
|
|
||||||
ttk.Button(btn_bar, text="离线使用(下次仍会提示激活)", command=do_skip).pack(
|
ttk.Button(btn_bar, text="退出", command=do_exit).pack(side=tk.LEFT, ipadx=8, ipady=4)
|
||||||
side=tk.LEFT, ipadx=6, ipady=4
|
|
||||||
)
|
|
||||||
ttk.Button(btn_bar, text="确认激活", command=do_ok).pack(side=tk.RIGHT, ipadx=12, ipady=4)
|
ttk.Button(btn_bar, text="确认激活", command=do_ok).pack(side=tk.RIGHT, ipadx=12, ipady=4)
|
||||||
win.bind("<Return>", lambda _e: do_ok())
|
win.bind("<Return>", lambda _e: do_ok())
|
||||||
|
win.protocol("WM_DELETE_WINDOW", do_exit)
|
||||||
win.protocol("WM_DELETE_WINDOW", do_skip)
|
|
||||||
_bring_front(win)
|
_bring_front(win)
|
||||||
try:
|
try:
|
||||||
win.grab_set()
|
win.grab_set()
|
||||||
|
|||||||
@@ -621,6 +621,7 @@ class OrderApp(tk.Tk):
|
|||||||
self._pos_profit_labels: dict = {}
|
self._pos_profit_labels: dict = {}
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self.after(200, self._try_connect_and_refresh)
|
self.after(200, self._try_connect_and_refresh)
|
||||||
|
self.after(3000, self._hub_heartbeat)
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
style = ttk.Style(self)
|
style = ttk.Style(self)
|
||||||
@@ -1445,16 +1446,40 @@ class OrderApp(tk.Tk):
|
|||||||
|
|
||||||
self._run_async(work)
|
self._run_async(work)
|
||||||
|
|
||||||
|
def _hub_heartbeat(self) -> None:
|
||||||
|
"""定期 ping 中控,供网页显示在线;断线则提示(不允许离线运行)。"""
|
||||||
|
if hub_client is None or not is_activated():
|
||||||
|
self.after(60000, self._hub_heartbeat)
|
||||||
|
return
|
||||||
|
|
||||||
|
def work():
|
||||||
|
ok = False
|
||||||
|
try:
|
||||||
|
ok = bool(hub_client.ping())
|
||||||
|
except Exception:
|
||||||
|
ok = False
|
||||||
|
return ok
|
||||||
|
|
||||||
|
def done(ok: bool):
|
||||||
|
if not ok:
|
||||||
|
self.set_status("中控离线:请检查服务器,客户端需保持在线", ok=False)
|
||||||
|
self.after(60000, self._hub_heartbeat)
|
||||||
|
|
||||||
|
def run():
|
||||||
|
ok = work()
|
||||||
|
self.after(0, lambda: done(ok))
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
def on_open_review(self) -> None:
|
def on_open_review(self) -> None:
|
||||||
"""打开中控复盘登录页(浏览器)。"""
|
"""打开中控复盘登录页(浏览器),使用已保存地址。"""
|
||||||
server = ""
|
server = ""
|
||||||
try:
|
try:
|
||||||
server = (load_device().get("server") or "").rstrip("/")
|
server = (load_device().get("server") or "").rstrip("/")
|
||||||
except Exception:
|
except Exception:
|
||||||
server = ""
|
server = ""
|
||||||
if not server:
|
if not server:
|
||||||
server = simpledialog_ask_server(self)
|
self.set_status("未激活:请先完成中控激活", ok=False)
|
||||||
if not server:
|
|
||||||
return
|
return
|
||||||
url = server.rstrip("/") + "/login"
|
url = server.rstrip("/") + "/login"
|
||||||
try:
|
try:
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user