This commit is contained in:
Your Name
2026-07-29 09:34:02 +08:00
parent 0ff8943ee2
commit f913a57529
54 changed files with 2453 additions and 378 deletions
+101 -23
View File
@@ -51,8 +51,11 @@ import tkinter as tk
import tkinter.font as tkfont
from tkinter import messagebox, scrolledtext, ttk
from app_version import APP_VERSION, release_status
from runtime_paths import application_data_dir, resource_path
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SCRIPT_DIR = str(application_data_dir())
APP_SETTINGS_FILE = os.path.join(SCRIPT_DIR, "app_settings.json")
CUSTOMER_SERVICE_URL = "http://kf.zhenyangtang.com.cn/"
EDGE_PROFILE_DIR = os.path.join(
@@ -60,10 +63,12 @@ EDGE_PROFILE_DIR = os.path.join(
"ZhenYangTangRPA",
"CustomerServiceProfile",
)
EDGE_LIGHT_THEME_DIR = os.path.join(SCRIPT_DIR, "edge_light_theme")
WIN_TITLE = "甄养堂 · 企微客服助手"
EDGE_LIGHT_THEME_DIR = str(resource_path("edge_light_theme"))
APP_ICON_PNG_PATH = resource_path("assets", "brand", "zhenyangtang-icon.png")
APP_ICON_ICO_PATH = resource_path("assets", "brand", "zhenyangtang-icon.ico")
WIN_TITLE = f"甄养堂 · 企微客服助手 v{APP_VERSION}"
AUTO_REPLY_TEXT = "你好"
AUTO_REPLY_TEXT = "在的,您慢慢说,我这边看着呢。"
POLL_INTERVAL = 2.0
MOUSE_IDLE_ENABLED = True
MOUSE_IDLE_SECONDS = 20.0
@@ -253,6 +258,11 @@ class BotThread(threading.Thread):
while not self.stop_event.is_set():
bot._poll_once()
if bot.security_verification_required:
failed = True
self.target_queue.put(("status", "verification"))
self.stop_event.set()
break
ready = bool(bot._window_ready)
if ready != last_ready:
last_ready = ready
@@ -264,7 +274,7 @@ class BotThread(threading.Thread):
"input": f"({bot.input_x}, {bot.input_y})",
}))
self.target_queue.put(("stats", {
"replied": len(bot.replied),
"replied": bot.reply_count,
"false_pos": len(bot.false_pos_rows),
}))
self.stop_event.wait(self.poll_seconds)
@@ -455,11 +465,27 @@ class Card(tk.Frame):
self.body.pack(fill="both", expand=True, padx=pad, pady=(12, 18))
def _set_window_icon(window):
try:
if sys.platform == "win32":
window.iconbitmap(default=str(APP_ICON_ICO_PATH))
return
except tk.TclError:
pass
try:
icon = tk.PhotoImage(file=str(APP_ICON_PNG_PATH))
window.iconphoto(True, icon)
window._app_icon_image = icon
except tk.TclError:
pass
class App(tk.Tk):
def __init__(self):
super().__init__()
self._configure_default_fonts()
self.title(WIN_TITLE)
_set_window_icon(self)
initial_dpi = round(float(self.tk.call("tk", "scaling")) * 72)
self._layout_scale = max(1.0, initial_dpi / 96.0)
initial_monitor = self._monitor_info_for(self)
@@ -524,26 +550,19 @@ class App(tk.Tk):
self._refresh_ai_status()
self._append("控制台已就绪。请先确认企业微信已登录,再开始监听。", "notify")
self.after(150, self._process_queue)
self.after(300, self._start_backend_sync)
if os.environ.get("WECOM_RPA_DISABLE_PORTAL") != "1":
self.after(350, self._open_customer_service_portal)
def _start_backend_sync(self):
"""经典界面也会沿用 Qt 界面保存的后台登录并自动同步"""
try:
import backend_client
if not backend_client.is_configured():
return
except Exception as exc:
self._queue.put(("log", f"后台连接配置读取失败:{exc}"))
return
"""经典界面启动后也会自动获取固定云端配置"""
def worker():
try:
result = backend_client.sync_config()
import backend_client
result = backend_client.startup_sync_config(timeout=3.0)
except Exception as exc:
self._queue.put(("log", f"后台配置同步失败:{exc}"))
self._queue.put(("log", f"云端配置同步失败:{exc}"))
else:
if result.get("synced"):
self._queue.put(("backend_config", result))
@@ -2069,6 +2088,7 @@ class App(tk.Tk):
"running": SUCCESS,
"stopping": WARNING,
"error": DANGER,
"verification": DANGER,
"stopped": TEXT_FAINT,
}
hints = {
@@ -2077,6 +2097,7 @@ class App(tk.Tk):
"running": "安全监听中",
"stopping": "正在停止",
"error": "打开控制台查看",
"verification": "请先用手机扫码验证",
"stopped": "监听已停止",
}
color = colors.get(state, TEXT_FAINT)
@@ -3324,11 +3345,7 @@ class App(tk.Tk):
body = card.body
fields = (
("API 地址", "AI_API_BASE", str, False),
("API Key", "AI_API_KEY", str, True),
("模型名称", "AI_MODEL", str, False),
("客服名称", "AI_AGENT_NAME", str, False),
("机构名称", "AI_HOSPITAL_NAME", str, False),
("本机客服名称(留空跟随云端)", "AI_AGENT_NAME", str, False),
("上下文记忆轮数", "AI_CONTEXT_MAX_ROUNDS", int, False),
("最大回复 tokens", "AI_MAX_TOKENS", int, False),
("温度", "AI_TEMPERATURE", float, False),
@@ -3343,7 +3360,12 @@ class App(tk.Tk):
fg=TEXT_MUTED,
font=TYPE_SMALL,
).grid(row=row_index, column=0, sticky="w", pady=5)
variable = tk.StringVar(value=str(getattr(ai_config, key, "")))
initial = (
ai_config.local_agent_name_override()
if key == "AI_AGENT_NAME"
else str(getattr(ai_config, key, ""))
)
variable = tk.StringVar(value=initial)
shell, _entry = self._entry(body, variable, show="*" if secret else None)
shell.grid(row=row_index, column=1, sticky="ew", padx=(16, 0), pady=5)
variables[key] = variable
@@ -3372,6 +3394,9 @@ class App(tk.Tk):
def save_config():
for label, key, value_type, _secret in fields:
raw = variables[key].get().strip()
if key == "AI_AGENT_NAME":
ai_config.set_local_agent_name_override(raw)
continue
try:
value = value_type(raw)
except ValueError:
@@ -3685,6 +3710,7 @@ class App(tk.Tk):
"running": SUCCESS,
"stopping": WARNING,
"error": DANGER,
"verification": DANGER,
"stopped": TEXT_FAINT,
}
color = colors.get(state, TEXT_FAINT)
@@ -3696,6 +3722,7 @@ class App(tk.Tk):
"running": "安全模式:切到企业微信后监听",
"stopping": "等待后台任务退出",
"error": "请查看运行日志",
"verification": "请用手机企业微信扫码验证,完成后重新开始监听",
"stopped": "等待连接企业微信",
}
self._status_hint.configure(text=hints.get(state, text))
@@ -3761,6 +3788,10 @@ class App(tk.Tk):
self._set_status("waiting", "等待企业微信")
elif data == "error":
self._finish_thread("error")
elif data == "verification":
self._finish_thread("verification")
self._set_status("verification", "需要扫码验证")
self._dashboard_state.configure(text="需要扫码验证", fg=DANGER)
elif data == "stopped":
self._finish_thread("stopped")
elif kind == "info":
@@ -3811,6 +3842,43 @@ class App(tk.Tk):
self.destroy()
def handle_startup_update(release):
"""经典界面的启动升级提示;False 表示本次应退出。"""
status = release_status(release)
if not status["update_available"]:
return True
forced = status["force_upgrade"]
root = tk.Tk()
_set_window_icon(root)
root.withdraw()
try:
text = (
f"当前版本:v{status['local_version']}\n"
f"云端版本:v{status['latest_version']}\n\n"
)
if status["release_notes"]:
text += f"更新内容:\n{status['release_notes'][:1200]}\n\n"
text += (
"管理员已设置强制升级,升级前无法继续使用。\n\n"
"选择“是”前往下载,选择“否”退出软件。"
if forced
else "是否现在前往下载新版本?选择“否”可继续使用当前版本。"
)
upgrade = messagebox.askyesno(
"必须升级" if forced else "发现新版本", text, parent=root
)
if upgrade:
if not status["download_url"]:
messagebox.showwarning(
"无法打开下载地址", "管理员尚未配置升级下载地址。", parent=root
)
else:
webbrowser.open(status["download_url"])
return not forced
finally:
root.destroy()
def main():
if "--classic-ui" not in sys.argv and os.environ.get("WECOM_RPA_CLASSIC_UI") != "1":
try:
@@ -3822,6 +3890,16 @@ def main():
qt_main()
return
startup_result = {}
try:
import backend_client
startup_result = backend_client.startup_sync_config(timeout=3.0)
except Exception:
# 云端暂时不可用时仍执行上次成功同步的强制升级策略。
startup_result = {"release": backend_client.cached_release_status()}
if not handle_startup_update(startup_result.get("release")):
return
app = App()
app.protocol("WM_DELETE_WINDOW", app.on_close)
app.mainloop()