更新
This commit is contained in:
+237
-65
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user