新增
This commit is contained in:
+101
-23
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user