This commit is contained in:
Your Name
2026-08-18 17:25:22 +08:00
parent 1048b9ba29
commit f8c78739e7
261 changed files with 16253 additions and 7399 deletions
+237 -65
View File
@@ -100,8 +100,10 @@ TOGGLE_OFF = "#C8D5CE"
CAPSULE_SHADOW = "#A9B8B0"
CAPSULE_HIGHLIGHT = "#EEF3F0"
CAPSULE_TRANSPARENT = "#02050B"
CAPSULE_WIDTH = 372
CAPSULE_HEIGHT = 68
# 监听胶囊除了状态和计时,还要给每一道自动化步骤留出独立的一整行。
# 436×96 在 200% DPI 下约为 872×192,仍然紧凑,但不会再把进程文字裁掉。
CAPSULE_WIDTH = 436
CAPSULE_HEIGHT = 96
WECOM_WAITING_MESSAGE = (
"企业微信主界面未显示、未在前台或已最小化,正在尝试切到前台;"
"本轮失败时下一轮将自动重试。"
@@ -432,6 +434,8 @@ class App(tk.Tk):
self._capsule_position = None
self._capsule_drag_offset = (0, 0)
self._capsule_dpi = None
self._capsule_progress_text = ""
self._capsule_progress_step = 0
self._console_geometry = (
f"{window_width}x{window_height}{window_x:+d}{window_y:+d}"
)
@@ -461,8 +465,6 @@ class App(tk.Tk):
self._refresh_ai_status()
self._append("控制台已就绪。请先确认企业微信已登录,再开始监听。", "notify")
self.after(150, self._process_queue)
if os.environ.get("WECOM_RPA_DISABLE_PORTAL") != "1":
self.after(350, self._open_customer_service_portal)
def _start_backend_sync(self):
"""经典界面启动后也会自动获取固定云端配置。"""
@@ -766,6 +768,12 @@ class App(tk.Tk):
return windows
def _focus_customer_service_portal(self):
if hasattr(self, "_desk_input"):
try:
self._desk_input.focus_set()
return True
except tk.TclError:
return False
if (
not self._portal_hwnd
or not self._portal_embedded
@@ -1193,6 +1201,10 @@ class App(tk.Tk):
pass
def _open_customer_service_portal(self):
if hasattr(self, "_desk_reload"):
self._desk_reload()
self._focus_customer_service_portal()
return
if (
hasattr(self, "_page_frames")
and "AI 客服" in self._page_frames
@@ -1391,19 +1403,24 @@ class App(tk.Tk):
width=1,
)
content = tk.Frame(canvas, bg=PANEL, height=48)
content = tk.Frame(canvas, bg=PANEL, height=74)
self._capsule_content = content
self._capsule_content_window_id = canvas.create_window(
18,
10,
9,
anchor="nw",
width=336,
height=48,
width=400,
height=74,
window=content,
)
top_row = tk.Frame(content, bg=PANEL, height=46)
self._capsule_top_row = top_row
top_row.pack(side="top", fill="x")
top_row.pack_propagate(False)
handle = tk.Label(
content,
top_row,
text=":::",
bg=PANEL,
fg=TEXT_FAINT,
@@ -1414,7 +1431,7 @@ class App(tk.Tk):
handle.pack(side="left", padx=(0, 9))
dot = tk.Canvas(
content,
top_row,
width=12,
height=12,
bg=PANEL,
@@ -1424,7 +1441,7 @@ class App(tk.Tk):
self._capsule_dot = dot
self._capsule_dot_id = dot.create_oval(1, 1, 11, 11, fill=WARNING, outline="")
status_box = tk.Frame(content, bg=PANEL, width=108)
status_box = tk.Frame(top_row, bg=PANEL, width=126)
self._capsule_status_box = status_box
status_box.pack(side="left", fill="y")
status_box.pack_propagate(False)
@@ -1448,7 +1465,7 @@ class App(tk.Tk):
self._capsule_hint.pack(anchor="w", pady=(1, 0))
self._capsule_timer = tk.Label(
content,
top_row,
text="00:00:00",
bg=PANEL,
fg=TEXT_MUTED,
@@ -1459,7 +1476,7 @@ class App(tk.Tk):
self._capsule_timer.pack(side="left", padx=(6, 7))
expand_button = self._capsule_button(
content,
top_row,
"",
"展开控制台",
self._expand_console,
@@ -1467,7 +1484,7 @@ class App(tk.Tk):
self._capsule_expand_button = expand_button
expand_button.pack(side="left", padx=(0, 5))
stop_button = self._capsule_button(
content,
top_row,
"×",
"停止监听",
self._on_stop,
@@ -1476,15 +1493,30 @@ class App(tk.Tk):
self._capsule_stop_button = stop_button
stop_button.pack(side="left")
# 这行是真正的执行进程,不再借用“安全监听中”那句静态提示。
# 进程编号每变化一次递增,肉眼可以确认后台是否仍在向前走。
self._capsule_progress = tk.Label(
content,
text="步骤 000 · 等待启动",
bg=PANEL_ALT,
fg=TEXT_MUTED,
font=self._capsule_fonts["hint"],
anchor="w",
padx=9,
)
self._capsule_progress.pack(side="bottom", fill="x", pady=(3, 0))
drag_widgets = (
canvas,
content,
top_row,
handle,
dot,
status_box,
self._capsule_status,
self._capsule_hint,
self._capsule_timer,
self._capsule_progress,
)
for widget in drag_widgets:
widget.bind("<ButtonPress-1>", self._capsule_drag_start)
@@ -1546,17 +1578,18 @@ class App(tk.Tk):
fill=CAPSULE_HIGHLIGHT,
width=px(1),
)
canvas.coords(self._capsule_content_window_id, px(18), px(10))
canvas.coords(self._capsule_content_window_id, px(18), px(9))
canvas.itemconfigure(
self._capsule_content_window_id,
width=px(336),
height=px(48),
width=px(400),
height=px(74),
)
canvas.tag_raise(self._capsule_body_id, self._capsule_shadow_id)
canvas.tag_raise(self._capsule_line_id, self._capsule_body_id)
canvas.tag_raise(self._capsule_content_window_id, self._capsule_line_id)
self._capsule_content.configure(height=px(48))
self._capsule_content.configure(height=px(74))
self._capsule_top_row.configure(height=px(46))
self._capsule_handle.pack_configure(padx=(0, px(9)))
dot_color = self._capsule_dot.itemcget(self._capsule_dot_id, "fill")
self._capsule_dot.configure(width=px(12), height=px(12))
@@ -1570,9 +1603,11 @@ class App(tk.Tk):
outline="",
)
self._capsule_dot.pack_configure(padx=(0, px(8)))
self._capsule_status_box.configure(width=px(108))
self._capsule_status_box.configure(width=px(126))
self._capsule_hint.pack_configure(pady=(px(1), 0))
self._capsule_timer.pack_configure(padx=(px(6), px(7)))
self._capsule_progress.configure(padx=px(9))
self._capsule_progress.pack_configure(pady=(px(3), 0))
self._capsule_expand_button.configure(padx=px(3), pady=px(4))
self._capsule_expand_button.pack_configure(padx=(0, px(5)))
self._capsule_stop_button.configure(padx=px(3), pady=px(4))
@@ -2035,6 +2070,37 @@ class App(tk.Tk):
self._capsule_dot.itemconfigure(self._capsule_dot_id, fill=color)
self._capsule_status.configure(text=text, fg=color)
self._capsule_hint.configure(text=hints.get(state, "安全监听"))
defaults = {
"connecting": "正在查找并挂载企业微信窗口",
"waiting": "等待企业微信窗口可见",
"running": "等待新消息",
"stopping": "正在停止监听线程",
"error": "运行失败,请展开控制台查看",
"verification": "检测到安全验证,自动操作已停止",
"stopped": "监听已停止",
}
if state in defaults:
self._update_capsule_progress(defaults[state])
def _update_capsule_progress(self, text):
"""在经典界面的胶囊里显示真实操作步骤,而不是只显示静态状态。"""
if not hasattr(self, "_capsule_progress"):
return
message = " ".join(str(text or "").split())
if not message:
message = "等待新消息" if self._running else "监听已停止"
if message == getattr(self, "_capsule_progress_text", ""):
return
self._capsule_progress_text = message
self._capsule_progress_step = int(
getattr(self, "_capsule_progress_step", 0) or 0
) + 1
rendered = f"步骤 {self._capsule_progress_step:03d} · {message}"
# Tk Label 在固定画布里不会自动省略;主动裁剪,完整内容仍保存在属性中,
# 双击胶囊展开控制台后可从运行日志查看。
if len(rendered) > 54:
rendered = rendered[:53] + ""
self._capsule_progress.configure(text=rendered)
def _build_sidebar(self, parent):
sidebar = tk.Frame(
@@ -2259,12 +2325,8 @@ class App(tk.Tk):
code = dict(PAGES).get(name, "--")
self._page_code.configure(text=code)
self._page_title.configure(text=name)
self._set_customer_service_portal_visible(is_customer_service)
if is_customer_service:
if self._portal_embedded:
self.after(80, self._focus_customer_service_portal)
else:
self.after_idle(self._open_customer_service_portal)
self.after_idle(self._focus_customer_service_portal)
def _metric_card(self, parent, column, label, attr_name, note):
card = tk.Frame(
@@ -2299,56 +2361,161 @@ class App(tk.Tk):
setattr(self, attr_name, value)
def _build_customer_service_page(self, page):
self._portal_status_label = tk.Label(
page,
text="正在加载客服系统…",
page.configure(bg=CONTENT_BG)
header = tk.Frame(page, bg=CONTENT_BG)
header.pack(fill="x", padx=18, pady=(16, 8))
tk.Label(
header,
text="AI 客服工作台",
bg=CONTENT_BG,
fg=TEXT,
font=TYPE_BODY_BOLD,
).pack(anchor="w")
self._desk_backend_label = tk.Label(
header,
text="",
bg=CONTENT_BG,
fg=TEXT_MUTED,
font=TYPE_SMALL,
font=TYPE_CAPTION,
)
self._portal_host = tk.Frame(page, bg=CONTENT_BG)
self._portal_host.pack(fill="both", expand=True)
self._portal_embed_header = tk.Frame(
self._portal_host,
bg=PANEL,
highlightthickness=0,
self._desk_backend_label.pack(anchor="w", pady=(2, 0))
toolbar = tk.Frame(page, bg=CONTENT_BG)
toolbar.pack(fill="x", padx=18, pady=(0, 8))
tk.Button(
toolbar,
text="+ 开始新对话",
command=self._desk_new_session,
bg=ACCENT,
fg="#FFFFFF",
relief="flat",
font=TYPE_SMALL_BOLD,
padx=12,
pady=6,
).pack(side="left")
self._desk_status = tk.Label(
toolbar,
text="Enter 发送",
bg=CONTENT_BG,
fg=TEXT_MUTED,
font=TYPE_CAPTION,
)
tk.Frame(
self._portal_embed_header,
bg=BORDER_SOFT,
height=1,
).pack(side="bottom", fill="x")
tk.Label(
self._portal_embed_header,
text="甄养堂 AI 客服",
self._desk_status.pack(side="left", padx=12)
self._desk_transcript = tk.Text(
page,
bg=PANEL,
fg=TEXT,
font=TYPE_SMALL_BOLD,
).pack(side="left", padx=18)
tk.Label(
self._portal_embed_header,
text="● 网页已连接",
relief="flat",
wrap="word",
font=TYPE_BODY,
padx=12,
pady=12,
state="disabled",
)
self._desk_transcript.pack(fill="both", expand=True, padx=18, pady=(0, 8))
composer = tk.Frame(page, bg=CONTENT_BG)
composer.pack(fill="x", padx=18, pady=(0, 16))
self._desk_input = tk.Text(
composer,
height=4,
bg=PANEL,
fg=SUCCESS,
font=TYPE_CAPTION,
).pack(side="right", padx=18)
self._portal_loading_label = tk.Label(
self._portal_host,
text="正在接入 AI 客服页面…",
bg=CONTENT_BG,
fg=TEXT_MUTED,
fg=TEXT,
relief="flat",
wrap="word",
font=TYPE_BODY,
)
self._portal_loading_label.place(relx=0.5, rely=0.5, anchor="center")
self._portal_host.bind(
"<Configure>",
self._schedule_customer_service_portal_resize,
)
self.bind(
"<Configure>",
self._schedule_customer_service_portal_resize,
add="+",
)
self._desk_input.pack(side="left", fill="both", expand=True, padx=(0, 8))
self._desk_input.bind("<Return>", self._desk_return)
tk.Button(
composer,
text="发送",
command=self._desk_send,
bg=ACCENT,
fg="#FFFFFF",
relief="flat",
font=TYPE_SMALL_BOLD,
padx=16,
pady=8,
).pack(side="right")
self._desk_session_id = ""
self._desk_busy = False
self.after_idle(self._desk_reload)
def _desk_return(self, event):
if event.state & 0x0001:
return None
self._desk_send()
return "break"
def _desk_reload(self):
from dsh_agent import get_desk
desk = get_desk()
self._desk_backend_label.configure(text=desk.backend_label())
session_id = self._desk_session_id or desk.current_session_id()
session = desk.set_current_session(session_id) or desk.get_session(session_id)
if session is None:
session = desk.create_session()
self._desk_session_id = session.id
self._desk_render(session.messages)
def _desk_render(self, messages):
widget = self._desk_transcript
widget.configure(state="normal")
widget.delete("1.0", "end")
if not messages:
widget.insert("end", "一个输入框,完成客服起草与对话。\n复用同一会话会保留上下文。\n")
else:
for message in messages:
role = {"user": "坐席", "assistant": "Agent", "system": "系统"}.get(
str(message.get("role") or ""),
str(message.get("role") or ""),
)
widget.insert("end", f"{role}\n{message.get('content') or ''}\n\n")
widget.configure(state="disabled")
widget.see("end")
def _desk_new_session(self):
if self._desk_busy:
return
from dsh_agent import get_desk
session = get_desk().create_session()
self._desk_session_id = session.id
self._desk_reload()
def _desk_send(self):
if self._desk_busy:
return
prompt = self._desk_input.get("1.0", "end").strip()
if not prompt:
return
from dsh_agent import get_desk
desk = get_desk()
session_id = self._desk_session_id or desk.current_session_id()
self._desk_input.delete("1.0", "end")
self._desk_busy = True
self._desk_status.configure(text="Agent 正在处理…")
session = desk.get_session(session_id)
pending = list(session.messages if session is not None else [])
pending.append({"role": "user", "content": prompt})
pending.append({"role": "system", "content": "Agent 正在处理当前任务…"})
self._desk_render(pending)
def worker():
try:
result = desk.run(prompt, session_id=session_id)
self._queue.put(("desk_done", (session_id, result, "")))
except Exception as exc:
self._queue.put(("desk_done", (session_id, None, str(exc))))
threading.Thread(target=worker, daemon=True, name="dsh-desk-tk").start()
def _desk_finished(self, session_id, _result, error):
self._desk_busy = False
self._desk_session_id = session_id
self._desk_status.configure(text=error or "Enter 发送")
self._desk_reload()
def _build_dashboard_page(self, page):
wrap = tk.Frame(page, bg=CONTENT_BG)
@@ -3777,6 +3944,8 @@ class App(tk.Tk):
self._dashboard_state.configure(text="需要扫码验证", fg=DANGER)
elif data == "stopped":
self._finish_thread("stopped")
elif kind == "progress":
self._update_capsule_progress(data)
elif kind == "info":
self._hwnd_label.configure(text=data.get("hwnd", "--"))
self._window_size_label.configure(text=data.get("size", "--"))
@@ -3784,6 +3953,9 @@ class App(tk.Tk):
elif kind == "stats":
self._replied_label.configure(text=str(data.get("replied", 0)))
self._false_label.configure(text=str(data.get("false_pos", 0)))
elif kind == "desk_done":
session_id, result, error = data
self._desk_finished(session_id, result, error)
elif kind == "backend_config":
try:
import ai_config