diff --git a/wechat_rpa/UI_COMPONENT_SPEC.md b/wechat_rpa/UI_COMPONENT_SPEC.md new file mode 100644 index 0000000..61b63c8 --- /dev/null +++ b/wechat_rpa/UI_COMPONENT_SPEC.md @@ -0,0 +1,146 @@ +# 甄养堂企业微信 AI 自动回复 v2.0 — UI 与交互规格 + +## 1. 全局规范 + +- 参考画布:窗口内容区固定基准为 `1630 × 920`;Windows 原生标题栏约 `45px`,完整窗口对应 `1630 × 965`。小屏幕只做等比可用性收缩,不改变模块顺序。 +- 字体:页面标题 `HarmonyOS Sans SC Bold / 700`;菜单和模块标题 `Medium / 500–600`;正文 `Regular / 400`;数字指标 `Inter Semibold / 600`;Windows 回退 `Microsoft YaHei UI`。 +- 字体文件随程序打包注册;没有安装系统字体时仍使用同一字体资源。 +- 左侧导航:点击切换页面;选中态为蓝紫发光圆形图标和浅蓝背景;hover 为浅蓝底;底部监控球在停止时启动监听,在运行时停止监听。 +- 数据原则:设计稿里的数值和会话文字是构图示例;运行时显示真实企业微信、队列、档案、AI 与日志数据,不以假数据覆盖业务状态。 +- 页面内容区在基准窗口内不显示滚动条;会话正文、任务列表等局部数据区域按需内部滚动。 + +## 2. 桌面自动化控制台 + +### 顶部状态与任务概览 + +- 企业微信、AI 服务、发送模式和监听按钮:均展示真实运行状态;`自动发送` 启动监听,`暂停监听 / 恢复监听` 安全停止或恢复同一后台线程。 +- `监听状态 / 排队 / 今日回复 / 失败`:从当前线程、待回复队列和执行日志动态汇总;只读展示,不允许直接改数。 +- 任务队列:加载真实待处理会话;筛选和排序图标按既定顺序循环,不改变参考稿的图标、按钮数量或占位尺寸;刷新按钮立即重读队列与档案。 + +### 当前任务、执行链路与事件 + +- 当前处理任务:只读显示客户、已读取消息数、来源消息、回复预览、置信度和模型耗时;没有任务时保持同一卡片区域并显示空状态。 +- `终止本任务`:二次确认后取消对应未完成回复;`转人工`:二次确认后停止自动发送并保留档案。 +- 自动化执行状态:按真实阶段展示扫描、读取、合并、AI、回填和发送进度;企业微信窗口状态展示窗口识别、当前会话、DPI、人工保护和最小化恢复状态。 +- 实时事件:显示最近四条真实事件;`查看完整日志` 跳转运行日志页。 + +## 3. 任务队列 + +### 顶部操作 + +- `自动发送`:点击后启动真实监听线程;线程已运行时给出运行中日志,不重复创建线程。 +- `暂停监听`:点击后安全停止监听;未完成任务保留,恢复监听后继续处理。 +- 企业微信、AI 状态胶囊:动态状态展示,不可编辑。 + +### 状态带与任务列表 + +- `处理中 / 等待中 / 待重试 / 平均等待`:每 2 秒从真实待回复文件更新。 +- `全部 / 处理中 / 等待中 / 待重试`:单选筛选;只隐藏不匹配行,不删除任务。 +- 任务列表:动态加载、内部滚动、不分页;选择任务后右侧详情同步更新。 +- 刷新按钮:立即重读队列和执行记录。 +- 空队列:正常运行显示“当前没有待处理任务”,不会伪造设计稿任务;只有 `--qt-smoke-test` 截图模式在真实队列不足时注入五条隔离的参考构图数据,演示项不能执行删除、转人工或导出。 + +### 任务详情 + +- 会话 ID、等待时间、重试次数、来源消息、当前阶段:只读动态数据。 +- `取消任务`:要求二次确认;取消未完成自动回复,保留会话档案和执行记录。 +- `立即重试`:只对所选 `retry_wait` 任务清除退避并重新排队;发送中、回执不确定、等待人工审核和正常任务受保护。监听未运行时会先启动监听,并在机器人初始化后执行重试。 +- `转人工`:要求二次确认;取消该任务的后续自动发送并记录转人工日志,会话档案保留。 +- `查看完整日志`:跳转运行日志页。 + +## 4. 会话归档 + +### 顶部筛选 + +- 搜索框:占位文字“搜索客户或会话 ID”,最大 80 字;按客户名、会话 ID 和摘要即时过滤。 +- `全部会话 / 今日 / AI 已回复 / 已转人工 / 日期`:互斥筛选;筛选仅影响左侧可见列表。 +- `导出记录`:打开保存对话框,导出全部真实会话及消息为 UTF-8 JSON;取消对话框不写文件。 + +### 数据区域 + +- 最近会话:最多加载 500 份档案,列表内部滚动,不分页;头像、名称、摘要、时间和状态来自档案数据。 +- 会话记录:选择左侧会话后加载最近 8 条用于页面展示;区域内部滚动;完整记录仍可从详情和导出读取。 +- 客户记忆、业务沉淀:展示长期上下文、挂号登记、回访和人工跟进状态。 +- `复制全部记录`:打开所选会话的完整记录窗口,可复制完整文本。 +- `标记已联系`:对选中的真实登记记录更新状态;没有选择时提示。 +- `删除所选`:二次确认后删除所选会话档案;不可撤销。 + +## 5. AI 设置 + +### 顶部与分页锚点 + +- `基础设置 / 知识与工具 / 安全策略`:互斥选中;点击滚动定位到对应模块,不重排页面。 +- `测试 AI`:先做配置预检,再异步调用真实 `ai_chat.call_ai_text`;只有收到非空响应才显示成功,模型未保存或请求失败会返回真实错误。 +- `保存并发布`:校验 MCP JSON 后保存客服名、主模型、上下文轮数、最大回复、温度、工具开关和工具轮数;成功或失败给出短时状态反馈。 + +### 表单限制 + +- 客服名称:可编辑,最大 40 字;空值回退云端客服名。 +- 人格提示词:明确标注为只读安全模板摘要,避免在 UI 层绕过现有 `build_system_prompt` 安全拼装逻辑。 +- 当前模型:点击可输入模型标识并持久化为 `AI_MODEL`;备用模型由云端策略管理,本地点击只显示说明,不伪装成已切换。 +- 上下文:`1–50` 轮;最大回复:`50–32000 tokens`;温度:`0.00–2.00`。 +- MCP 最大轮数:`1–20`;高级 JSON 根节点必须为数组,否则禁止保存并显示错误。 +- `立即同步`:执行真实云端配置同步;`管理知识`:打开本地数据目录。 +- 数据区为动态状态展示;知识库、工具和安全策略卡片不分页。 + +## 6. 自动化设置 + +### 模式与保存 + +- `自动发送`:AI 回复通过全部安全校验后自动按 Enter,并按真实聊天证据核对回执。 +- `审核后发送`:完成识别、AI 生成和全部发送前校验,只把回复填入企业微信输入框,绝不按 Enter;任务持久化为“等待人工审核”,人工发送后再按真实聊天证据完成归档。 +- 两个模式按钮互斥,写入 `send_mode`;运行中修改会热更新,下一条回复起生效,经典界面共享同一配置。 +- `保存设置`:立即保存;输入控件修改后 500ms 自动保存,成功或失败短时显示状态。 + +### 输入与开关 + +- 扫描间隔:`0.2–300.0 秒`。 +- 连续消息等待:使用运行时允许的消息合并最小值和最大值。 +- 发送前等待:独立参数 `0–30 秒`,发生在任何剪贴板/输入操作之前,可被停止或删除任务打断;等待结束重新执行任务与安全检查。 +- 人工操作保护恢复:独立参数 `0–3600 秒`,默认 `5 秒`,不与发送前等待共用。 +- 固定回复:最大 120 字;空值保存时使用安全默认回复。 +- 人工操作保护开关:开启后只在鼠标达到静止时间后自动操作。 +- `重新检测`:调用真实企业微信窗口检测并报告句柄或失败原因。 +- 诊断、用药调整、投诉退款是内置只读风险类别,使用禁用状态而非空点击;`规则管理` 跳转到“AI 设置 → 安全策略”。 + +## 7. 运行日志 + +### 顶部操作 + +- `实时刷新`:默认开启;点击暂停可见事件流更新,再次点击恢复。暂停不影响后台日志落盘。 +- `导出日志`:打开保存对话框,导出当前内存日志为 UTF-8 文本。 +- `清空日志`:清空内存日志、五行事件流和警告计数;不删除已经落盘的历史日志文件。 +- `监控中`:只读状态胶囊。 + +### 事件流 + +- `全部 / 连接 / 识别 / AI / 发送 / 警告`:互斥筛选。 +- 搜索框:最大 80 字,按标题、详情和状态即时过滤。 +- 页面保留最近 5 条结构化事件;完整内存日志最多 2000 个文本块,后台磁盘日志独立保留。 +- 系统健康、回复链路、异常追踪和最近活动均为动态或运行状态展示,不分页。 + +## 8. 系统设置 + +### 基础、数据和通知 + +- 开机启动、启动后监听、最小化到托盘、关闭窗口后台运行、四类通知:可切换;点击“保存设置”后写入本地设置并跨重启保留。 +- 最小化到托盘关闭时,运行中的窗口保持在任务栏;开启时进入监控胶囊。关闭窗口后台运行关闭时会正常停止并退出,开启时运行中关闭主窗进入监控胶囊。 +- 界面语言当前固定为简体中文;缩放比例选项保存为 Qt 启动偏好,默认“自动”,重启后生效。 +- 日志保留:`7–365 天`。 +- `打开目录`:打开应用数据目录。 +- `立即备份`:新建时间戳备份目录,复制存在的设置、档案、队列和登记数据。 +- `清理缓存`:二次确认后只删除视觉状态和误判缓存,不删除会话及队列。 + +### 维护与诊断 + +- `连接测试`:显示本地服务状态。 +- `检查更新`:读取缓存的发布状态并显示版本信息。 +- `导出诊断`:保存版本、设置、数据目录和文件清单为 JSON。 +- `恢复默认设置`:二次确认后恢复系统选项,不删除业务数据。 +- `查看许可信息`:显示软件版本及随包字体许可说明。 +- 系统状态带为只读结果展示,不分页。 + +## 9. 原有功能兼容层 + +- 原 PySide6 页面继续作为真实数据和业务动作的后端,不再作为可见页面重复占位;HTML 七页只负责按参考稿呈现。 +- 队列、档案、AI、自动化、日志和系统设置仍复用既有存储、线程、安全校验与导出逻辑,换肤不会绕过原有安全检查或丢失已有数据。 diff --git a/wechat_rpa/__pycache__/ai_chat.cpython-311.pyc b/wechat_rpa/__pycache__/ai_chat.cpython-311.pyc index 33f5de0..961dc56 100644 Binary files a/wechat_rpa/__pycache__/ai_chat.cpython-311.pyc and b/wechat_rpa/__pycache__/ai_chat.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/ai_config.cpython-311.pyc b/wechat_rpa/__pycache__/ai_config.cpython-311.pyc index 7460831..f27409e 100644 Binary files a/wechat_rpa/__pycache__/ai_config.cpython-311.pyc and b/wechat_rpa/__pycache__/ai_config.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc b/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc index 9c0f888..d0494eb 100644 Binary files a/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc and b/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/registration_store.cpython-311.pyc b/wechat_rpa/__pycache__/registration_store.cpython-311.pyc index c764dbd..bd3103c 100644 Binary files a/wechat_rpa/__pycache__/registration_store.cpython-311.pyc and b/wechat_rpa/__pycache__/registration_store.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc b/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc index 25fafb3..ac356d2 100644 Binary files a/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc and b/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc b/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc index a396a33..99e7105 100644 Binary files a/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc and b/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc differ diff --git a/wechat_rpa/ai_settings.json b/wechat_rpa/ai_settings.json index 642d646..b41a5e5 100644 --- a/wechat_rpa/ai_settings.json +++ b/wechat_rpa/ai_settings.json @@ -9,14 +9,14 @@ "AI_FORCE_REMOTE_VISION_EVERY_MESSAGE": false, "AI_UI_GUARD_ENABLED": true, "AI_CONTEXT_ENABLED": true, - "AI_CONTEXT_MAX_ROUNDS": 5, + "AI_CONTEXT_MAX_ROUNDS": 8, "AI_COUNTER_INSULT_ENABLED": false, "AI_AGENT_NAME": "贴心管家", "AI_HOSPITAL_NAME": "甄养堂互联网医院", "AI_MAX_TOKENS": 500, "AI_TEMPERATURE": 0.35, "AI_TIMEOUT": 120, - "AI_MCP_ENABLED": false, + "AI_MCP_ENABLED": true, "AI_MCP_MAX_ROUNDS": 5, "AI_MCP_SERVERS": [] } \ No newline at end of file diff --git a/wechat_rpa/app_main.py b/wechat_rpa/app_main.py index dc5bd50..f191035 100644 --- a/wechat_rpa/app_main.py +++ b/wechat_rpa/app_main.py @@ -1,30 +1,30 @@ -# -*- coding: utf-8 -*- -"""打包与源码共用的启动入口。 - -关键点:默认(Qt 界面)路径下绝不 import wechat_gui——那个模块顶层就会加载 -tkinter/tcl,冷启动平白多出一大截;tk 经典界面只在显式要求或 PySide6 缺失 -时才加载。 -""" - -import os -import sys - - -def main() -> None: - if "--classic-ui" not in sys.argv and os.environ.get("WECOM_RPA_CLASSIC_UI") != "1": - try: - from wechat_gui_qt import main as qt_main - except ImportError as exc: - if not (exc.name or "").startswith("PySide6"): - raise - else: - qt_main() - return - - from wechat_gui import run_classic_ui - - run_classic_ui() - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +"""打包与源码共用的启动入口。 + +关键点:默认(Qt 界面)路径下绝不 import wechat_gui——那个模块顶层就会加载 +tkinter/tcl,冷启动平白多出一大截;tk 经典界面只在显式要求或 PySide6 缺失 +时才加载。 +""" + +import os +import sys + + +def main() -> None: + if "--classic-ui" not in sys.argv and os.environ.get("WECOM_RPA_CLASSIC_UI") != "1": + try: + from wechat_gui_qt import main as qt_main + except ImportError as exc: + if not (exc.name or "").startswith("PySide6"): + raise + else: + qt_main() + return + + from wechat_gui import run_classic_ui + + run_classic_ui() + + +if __name__ == "__main__": + main() diff --git a/wechat_rpa/app_settings.json b/wechat_rpa/app_settings.json index 81ac074..7702120 100644 --- a/wechat_rpa/app_settings.json +++ b/wechat_rpa/app_settings.json @@ -3,5 +3,22 @@ "poll_interval": 2.0, "mouse_idle_enabled": true, "mouse_idle_seconds": 5.0, - "message_batch_window_seconds": 2.0 + "send_delay_seconds": 1.0, + "send_mode": "auto", + "message_batch_window_seconds": 20.0, + "auto_launch": false, + "auto_monitor": true, + "minimize_on_close": true, + "keep_background": true, + "interface_language": "简体中文", + "scale_ratio": "自动", + "log_retention_days": 90, + "mask_sensitive": true, + "encrypt_local": false, + "hide_chat_in_logs": false, + "clear_clipboard_on_exit": true, + "notify_send_failed": true, + "notify_timeout": true, + "notify_manual": true, + "notify_disconnect": true } \ No newline at end of file diff --git a/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Bold.ttf b/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Bold.ttf new file mode 100644 index 0000000..534ddf9 Binary files /dev/null and b/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Bold.ttf differ diff --git a/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf b/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf new file mode 100644 index 0000000..6814cce Binary files /dev/null and b/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf differ diff --git a/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf b/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf new file mode 100644 index 0000000..25069da Binary files /dev/null and b/wechat_rpa/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf differ diff --git a/wechat_rpa/assets/fonts/Inter-SemiBold.ttf b/wechat_rpa/assets/fonts/Inter-SemiBold.ttf new file mode 100644 index 0000000..47f8ab1 Binary files /dev/null and b/wechat_rpa/assets/fonts/Inter-SemiBold.ttf differ diff --git a/wechat_rpa/assets/fonts/LICENSE_HarmonyOS_Sans.txt b/wechat_rpa/assets/fonts/LICENSE_HarmonyOS_Sans.txt new file mode 100644 index 0000000..64ab8ad --- /dev/null +++ b/wechat_rpa/assets/fonts/LICENSE_HarmonyOS_Sans.txt @@ -0,0 +1,34 @@ +License Notice +Copyright 2021 Huawei Device Co., Ltd. +HarmonyOS Sans Fonts Software is licensed under HarmonyOS Sans Fonts License Agreement. + +-------------------------------------------------------------------------------- +HarmonyOS Sans Fonts License Agreement + +THIS HARMONYOS SANS FONTS LICENSE AGREEMENT ("AGREEMENT") IS A LEGAL AGREEMENT BETWEEN YOU (EITHER A SINGLE INDIVIDUAL, OR SINGLE LEGAL ENTITY) AND HUAWEI DEVICE CO., LTD.( "LICENSOR")FOR THE USE OF THE HARMONYOS SANS FONTS ACCOMPANYING THIS AGREEMENT.BY DOWNLOADING,COPYING OR OTHERWISE USING HARMONYOS SANS FONTS YOU INDICATE THAT YOU AGREE TO BE BOUND BY ALL OF THE TERMS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO THE TERMS OF THIS AGREEMENT, YOU MAY NOT DOWNLOAD, COPY OR OTHERWISE USE THE HARMONYOS SANS FONTS, AND YOU SHALL PROMPTLY DESTROY AND DELETE ALL THE HARMONYOS SANS FONTS. + +1.Definition +"HarmonyOS Sans Fonts" shall mean the collection of fonts software components provided by Licensor under this Agreement and clearly marked as "HarmonyOS¬ Sans". + +"YOU" shall mean an individual or legal entity exercising permissions granted by this Agreement. + +2.GRANT OF LICENSE + +Subject to the terms and conditions of this Agreement, Licensor hereby grant YOU a non-transferable, non-exclusive, royalty-free, revocable, worldwide copyright license to use,copy, merge, embed, bundle, redistribute and/or sell unmodified copies of HarmonyOS Sans Fonts with any software except for fonts software,subject to the following conditions: +1)YOU shall make a prominent notice in the software to state that HarmonyOS Sans Fonts are used. +2)YOU may not make any modifications to HarmonyOS Sans Fonts or any of their individual components. +3)Neither HarmonyOS Sans Fonts nor any of their individual components may be redistributed or sold in a stand-alone base. This limitation does not apply to any work created by using HarmonyOS Sans Fonts. You can freely distribute or sell your work, such as materials, logos, application software etc. created by using HarmonyOS Sans Fonts. +4)YOU shall retain the copyright notice and this Agreement in any copies of HarmonyOS Sans Fonts. + +3.Termination +This Agreement will be automatically terminated if YOU breach any of this Agreement. + +4.NO WARRANTY +YOU AGREE THAT THE HARMONYOS SANS FONTS ARE PROVIDED BY LICENSOR ON AN "AS IS" BASIS. LICENSOR MAKES NO WARRANTY, EXPRESSED OR IMPLIED OR STATUTORY, WITH RESPECT TO ANY OF THE HARMONYOS SANS FONTS, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT. + +5.NO LIABILITY + +IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY DIRECT OR INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE OF OR INABILITY TO USE HARMONYOS SANS FONTS, WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHER LEGAL THEORY, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +YOU EXPRESSLY ASSUME ALL LIABILITIES AND RISKS FOR USE OF HARMONYOS SANS FONTS. SHOULD THE HARMONYOS SANS FONTS PROVE DEFECTIVE, YOU ASSUME THE ENTIRE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + diff --git a/wechat_rpa/assets/fonts/LICENSE_Inter.txt b/wechat_rpa/assets/fonts/LICENSE_Inter.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/wechat_rpa/assets/fonts/LICENSE_Inter.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/wechat_rpa/assets/ui/_link_scripts.py b/wechat_rpa/assets/ui/_link_scripts.py new file mode 100644 index 0000000..f61ad6b --- /dev/null +++ b/wechat_rpa/assets/ui/_link_scripts.py @@ -0,0 +1,12 @@ +from pathlib import Path + +p = Path(__file__).resolve().parent / "zhenyang-ai-console.html" +text = p.read_text(encoding="utf-8") +start = text.index(" ", start) + len("") +new = ( + ' \n' + ' ' +) +p.write_text(text[:start] + new + text[end:], encoding="utf-8") +print("patched html", p.stat().st_size) diff --git a/wechat_rpa/assets/ui/_orig_script.js b/wechat_rpa/assets/ui/_orig_script.js new file mode 100644 index 0000000..26656a3 --- /dev/null +++ b/wechat_rpa/assets/ui/_orig_script.js @@ -0,0 +1,33 @@ + + const icon=n=>``; + const people=[["高","高瑞@微信","处理中","00:00:12",""],["小","一个小迷糊@微信","等待中","00:01:45","green"],["李","李先生@微信","等待中","00:02:10",""] ,["陈","陈女士@微信","待重试","—","violet"],["张","张小明@微信","等待中","00:03:22","cyan"]]; + let listening=true; + const head=(title,sub,actions)=>`

${title}

${sub}

${actions}
`; + const connected=(extra="",primary="")=>`企业微信已连接AI 服务正常${extra}`; + const ribbon=items=>`
${items.map(x=>`
${icon(x[2])}
${x[0]}${x[1]}
`).join("")}
`; + const queueList=(archive=false)=>`
${people.map((p,i)=>`
${p[0]}
${p[1]}${archive?(i===0?"请问你们的灵芝孢子粉怎么服用…":i===1?"每天吃几次比较合适?":i===2?"有不良反应吗?":"会话已归档"):`${p[2]}`}
${archive?["16:31","16:18","15:42","14:20","13:35"][i]:p[3]}${archive?`${i===2?"已转人工":"AI 已回复"}`:`${icon("message")} ${i===3?1:0}`}
`).join("")}
`; + const activities=(more="查看完整日志")=>`
${[["16:33:27","扫描未读","检测到客户未读消息","activity","成功",""],["16:33:28","提取聊天记录","已提取最近 6 条聊天记录","log","成功",""],["16:33:29","AI 服务","正在请求 AI 生成回复","brain","进行中","blue"],["16:33:31","人工保护","检测到人工鼠标操作,暂停 5 秒","user","警告","orange"]].map(x=>`
${icon(x[3])}
${x[1]}

${x[2]}

● ${x[4]}
`).join("")}
`; + const journey=()=>`
${[["扫描未读","已完成","0.3s"],["提取聊天记录","已完成","1.2s"],["合并连续消息","已完成","0.4s"],["发送至 AI","已完成","0.6s"],["生成回复","进行中","2.4s"],["回填企业微信","等待","—"],["自动发送与归档","等待","—"]].map((x,i)=>`
4?"wait":""}">
${i<4?"✓":i+1}
${x[0]}${x[1]}
${x[2]}
`).join("")}
`; + const statusCells=items=>`
${items.map(x=>`
${icon(x[0])}
${x[1]}${x[2]}
`).join("")}
`; + function autoView(){return `
${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`${icon("send")}自动发送`,"danger"))}${ribbon([["监听状态","运行中","activity","green"],["排队","3","users"],["今日回复","128","message"],["失败","1","alert","red"]])}

任务队列

3${icon("filter")}${icon("sort")}
${queueList()}

当前处理任务

${icon("user")}高瑞@微信已读取 6 条消息AI 生成中
复制的聊天内容
请问你们的灵芝孢子粉怎么服用?
每天吃几次比较合适?
有什么注意事项吗?
AI 回复预览
您好!灵芝孢子粉建议每日 1–2 次,每次 2–3 克,温水冲服,饭前半小时或睡前服用效果更佳。
如有特殊体质或正在服药,建议咨询专业医生后再服用哦。
${icon("shield")}置信度 96%${icon("clock")}模型耗时 2.4s

自动化执行状态

总耗时 5.01s
${journey()}

企业微信窗口状态

${[["folder","窗口","已识别"],["message","当前会话","高瑞@微信"],["monitor","DPI","200%"],["shield","人工保护","5 秒"],["refresh","最小化恢复","已开启"]].map(x=>`
${icon(x[0])}${x[1]}${x[2]}
`).join("")}

实时事件

${activities()}
`} + function queueView(){return `
${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`${icon("send")}自动发送`,"danger"))}${ribbon([["处理中","1","check","","blue"],["等待中","3","clock"],["待重试","1","refresh","orange"],["平均等待","12s","clock","green"]])}

全部任务

${queueList()}

任务详情

客户高瑞@微信
会话 ID7bc26546… ${icon("copy")}
已等待12 秒
重试0 次
来源消息
请问你们的灵芝孢子粉怎么服用?
✦ AI 生成回复中
${[["入队","16:32:11"],["读取消息","16:32:11"],["请求 AI","16:32:12"],["生成回复","进行中"],["回填发送","等待中"],["完成","等待中"]].map((x,i)=>`
${i<3?"✓":i+1}
${x[0]}${x[1]}
`).join("")}

调度信息

优先级普通
${icon("clock")}
预计完成3 秒
${icon("shield")}
人工保护已开启
${icon("refresh")}
下次重试

任务执行记录

${activities()}
`} + function archiveView(){return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`企业微信已连接AI 服务正常`)}

最近会话

${queueList(true)}

会话记录

${icon("user")}高瑞@微信6 条消息AI 已回复${icon("clock")}16:31 更新
${icon("user")}
请问你们的灵芝孢子粉怎么服用?
${icon("bot")}
每天吃几次比较合适?
${icon("user")}
有什么注意事项吗?
${icon("bot")}
温水冲服,饭前半小时或睡前服用效果更佳。
如有特殊体质或正在服药,建议咨询专业医生后再服用哦。
${icon("user")}
好的,谢谢!
${icon("bot")}
不客气!如有其他问题,随时为您服务~

客户记忆

饮食控制体重管理长期回访
客户关注灵芝孢子粉的服用方法与注意事项,希望优化服用时间与剂量,同时注重产品安全性与体质适配。
◔ 上下文 8 轮

业务沉淀

挂号登记已登记潜在客户
1 次 ✓
回访线索创建回访计划
1 条 ✓
${icon("user")}
人工跟进未分配客服
0 次 ·

最近更新

${activities("复制全部记录")}
`} + function aiView(){return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`配置已同步`)}

客服人格

AI
服务风格:温和专业简洁
✧ 实时预览

您好!我是甄养堂贴心管家,
很高兴为您服务 😊

请问有什么可以帮您的吗?

✣ 模型引擎

▰ 企业知识库

1,284个知识片段
● 刚刚同步✓ 已连接

◇ MCP 工具

单次最多调用 5 个工具 ⓘ
挂号登记
已启用
客户查询
已启用
回访记录
已启用

${icon("shield")} 安全边界

🛡 不做医疗诊断
💊 不修改用药方案
♙ 高风险转人工
当前模式:${icon("shield")}严格模式

配置状态

${statusCells([["cloud","云端模型正常","最后检查:刚刚"],["folder","知识库已同步","1,284 个知识片段"],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])}
`} + function automationView(){return `
${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`企业微信已识别规则已生效`)}

监听与发送策略

${icon("activity")} ● 规则运行中
${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`
${icon(x[0])}
${x[1]}
`).join("")}
${icon("clock")}扫描间隔 2 秒
⌛ 连续消息等待 20 秒
${icon("clock")}发送前等待 1 秒

企业微信窗口

▣ 主窗口:● 已识别
${icon("user")} 当前账号:甄养堂客服
◎ DPI:200%
${icon("monitor")} 最小化恢复:● 已开启

人机共存

启用人工操作保护
鼠标静止 5 秒 后恢复
${icon("bot")}
AI 运行中

检测到人工操作
AI 暂停

消息处理

✓ 同一客户连续消息自动合并
✓ 保留最近 8 轮上下文8 轮
✓ 一次只处理 1 个会话1 个

异常与兜底

${icon("clock")}AI 超时:重试 2 次2 次
${icon("alert")}发送失败:自动重试已开启
${icon("message")}固定回复:在的,请稍等编辑

高风险转人工

● 已开启
⚕ 诊断◇ 用药调整¥ 投诉退款

启动前检查

${statusCells([["check","企业微信已登录","检查完成"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准","检查完成"],["shield","规则配置有效","检查完成"]])}
`} + function logsView(){return `
${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`监控中`)}${ribbon([["企业微信","99.9%","message","green"],["AI 成功率","98.6%","brain","","blue"],["平均响应","2.4s","clock","","blue"],["警告","1","alert","red"]])}

实时事件流

${[["16:33:27","未读扫描","检测到客户未读消息","activity","成功","green"],["16:33:28","提取聊天记录","已读取最近 6 条消息","message","成功","green"],["16:33:29","AI 服务","正在生成回复","brain","进行中",""],["16:33:31","人工保护","检测到鼠标操作,暂停 5 秒","user","警告","orange"],["16:33:36","自动发送","回复发送成功","send","成功","green"]].map(x=>`
${icon(x[3])}
${x[1]}${x[2]}
${x[4]}
`).join("")}

系统健康

${[["企业微信连接","正常"],["AI 服务","正常"],["窗口识别","正常"]].map(x=>`
${icon("check")}
${x[0]}${x[1]}
`).join("")}
${icon("clock")}运行时长 02:18:46

当前回复链路

${[["activity","扫描未读","0.3s"],["message","读取消息","1.2s"],["brain","请求 AI","2.4s"],["send","回填发送","0.6s"]].map(x=>`
${icon(x[0])}
${x[1]}${x[2]}
`).join("")}

事件详情

${icon("user")}
人工保护

检测到人工鼠标操作,自动回复暂停 5 秒

模块:人机共存会话:高瑞@微信已自动恢复

异常追踪

${icon("alert")}
发送失败1
${icon("clock")}
模型超时0
${icon("monitor")}
窗口丢失0

最近活动

${activities("查看完整记录")}
`} + function systemView(){return `
${head("系统设置","管理软件启动、数据存储、通知与系统维护",`当前版本 v2.0 ⓘ`)}

基础设置

${icon("power")}开机自动启动
${icon("activity")}启动后自动监听
最小化到系统托盘
${icon("log")}关闭窗口时保持后台运行
界面语言: 简体中文
${icon("monitor")}缩放比例: 自动

数据与存储

286MB 已使用
● 会话归档128 MB
● 运行日志86 MB
● 缓存数据72 MB
数据保留90 天 ⌄

通知提醒

${icon("send")}发送失败提醒
模型超时提醒
${icon("user")}需要人工跟进
企业微信连接中断
发送失败提醒消息发送失败,已加入重试队列

安全与隐私

${icon("shield")}敏感信息脱敏:已开启 ●
${icon("lock")}本地数据加密:已开启 ●
${icon("lock")}日志隐藏聊天正文:已开启 ●
${icon("log")}退出时清理剪贴板:已开启 ●

网络与服务

${icon("cloud")}云端 AI:已连接 ●
${icon("folder")}企业知识库:已连接 ●
网络延迟:28ms ●

维护与诊断

${icon("refresh")}
检查更新已是最新版本
导出诊断导出系统诊断报告
恢复默认设置重置所有设置项
${icon("shield")}
查看许可信息查看软件许可详情

系统状态

${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","数据已备份","最近备份:16:33:28"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务运行中","系统运行状态良好"]])}
`} + const views={auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView}; + const glyphIcons=[["✦","sparkles"],["✧","sparkles"],["✣","brain"],["⚑","flag"],["▣","clipboard"],["☎","phone"],["⌕","search"],["▤","clipboard"],["◇","package"],["🛡","shield"],["💊","pill"],["♙","users"],["⌛","hourglass"],["◎","globe"],["⚕","shield"],["¥","coins"],["⌁","wifi"],["♟","bell"],["▰","tray"],["▷","play"],["↻","refresh"],["➤","cursor"],["Ⅱ","pause"],["◔","clock"],["ⓘ","info"],["⌄","chevron"]]; + function polishedIcons(html){return glyphIcons.reduce((value,[glyph,name])=>value.replaceAll(glyph,icon(name)),html)} + function render(name){ + if(!views[name])name="auto"; + document.querySelectorAll("[data-view]").forEach(b=>b.classList.toggle("active",b.dataset.view===name)); + document.getElementById("main").innerHTML=polishedIcons(views[name]()); + const listen=document.querySelector('[data-action="toggle-listen"]'); + if(listen)listen.innerHTML=listening?`${icon("pause")}暂停监听`:`${icon("play")}恢复监听`; + location.hash=name; + } + function toast(msg="操作已完成"){const t=document.getElementById("toast");t.textContent=msg;t.classList.add("show");clearTimeout(window.__toast);window.__toast=setTimeout(()=>t.classList.remove("show"),1600)} + document.addEventListener("click",e=>{const nav=e.target.closest("[data-view]");if(nav){render(nav.dataset.view);return}const action=e.target.closest("[data-action]");if(action){const a=action.dataset.action;if(a==="toggle-listen"){listening=!listening;document.getElementById("orb").classList.toggle("paused",!listening);document.getElementById("orb-label").textContent=listening?"监听中":"已暂停";render(location.hash.slice(1));toast(listening?"已恢复监听":"监听已暂停")}else if(a==="logs")render("logs");else if(a==="clear-logs"){const list=document.getElementById("log-list");if(list)list.innerHTML='
日志已清空
';toast("日志已清空")}else toast()}const sw=e.target.closest(".switch");if(sw)sw.classList.toggle("on");const pill=e.target.closest(".filter-pill,.tab");if(pill){[...pill.parentElement.children].forEach(x=>x.classList.remove("active"));pill.classList.add("active")}}); + function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById("stage").style.setProperty("--scale",String(s))}addEventListener("resize",fit);fit();render((location.hash||"#auto").slice(1)); + \ No newline at end of file diff --git a/wechat_rpa/assets/ui/_patch_console.py b/wechat_rpa/assets/ui/_patch_console.py new file mode 100644 index 0000000..674c919 --- /dev/null +++ b/wechat_rpa/assets/ui/_patch_console.py @@ -0,0 +1,351 @@ +# -*- coding: utf-8 -*- +"""Build console-app.js from the original mock script plus live-data hooks.""" +from pathlib import Path + +root = Path(__file__).resolve().parent +script = (root / "_orig_script.js").read_text(encoding="utf-8") +for old, new in [ + ("const people=", "var people="), + ("let listening=true", "var listening=true"), + ("const head=", "var head="), + ("const connected=", "var connected="), + ("const ribbon=", "var ribbon="), + ("const queueList=", "var queueList="), + ("const activities=", "var activities="), + ("const journey=", "var journey="), + ("const statusCells=", "var statusCells="), + ("const views=", "var views="), + ("const glyphIcons=", "var glyphIcons="), +]: + script = script.replace(old, new, 1) + +script = script.replace( + "function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById(\"stage\").style.setProperty(\"--scale\",String(s))}addEventListener(\"resize\",fit);fit();render((location.hash||\"#auto\").slice(1));", + "function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById(\"stage\").style.setProperty(\"--scale\",String(s))}", + 1, +) + +header = r''' +const icon=n=>``; +window.STATE = window.STATE || { view: "auto", demo: true, ready: false, listening: true }; +function esc(value){ + return String(value ?? "").replace(/[&<>"']/g, ch => ({ + "&":"&","<":"<",">":">",'"':""","'":"'" + }[ch])); +} +function bridge(){ return window.qtBridge || null; } +function callBridge(name, ...args){ + const api = bridge(); + if (!api || typeof api[name] !== "function") return false; + api[name](...args); + return true; +} +''' + +# Original script starts with icon=...; drop that duplicate. +if script.lstrip().startswith("const icon="): + script = script.lstrip() + script = script[script.index(";") + 1 :] + +footer = r''' +const _queueList = queueList; +const _activities = activities; +const _connected = connected; +const _journey = journey; + +connected = function(extra="", primary=""){ + const wecom = STATE.wecomLabel || (STATE.wecomConnected ? "企业微信已连接" : "企业微信未连接"); + const ai = STATE.aiLabel || (STATE.aiOk === false ? "AI 服务异常" : "AI 服务正常"); + const listenLabel = STATE.listening ? `${icon("pause")}暂停监听` : `${icon("play")}恢复监听`; + return `${esc(wecom)}${esc(ai)}${extra}`; +}; + +queueList = function(archive=false){ + if (STATE.demo) return _queueList(archive); + const rows = archive ? (STATE.archives || []) : (STATE.people || []); + if (!rows.length){ + const title = archive ? "暂无会话档案" : "当前没有待处理任务"; + const hint = archive ? "新的客户会话会自动出现在这里" : "所有客户消息均已完成"; + return `
${icon("check")}
${title}${hint}
`; + } + return `
${rows.map((p,i)=>{ + const active = p.active || p.key === STATE.selectedKey || (!STATE.selectedKey && i===0); + const preview = archive ? (p.preview || "会话已归档") : `${esc(p.status || "")}`; + const meta = archive + ? `${esc(p.time || "")}${esc(p.badge || "AI 已回复")}` + : `${esc(p.wait || "")}${icon("message")} ${p.bubbles || 0}`; + return `
${esc(p.avatar || "客")}
${esc(p.name || "")}${preview}
${meta}
`; + }).join("")}
`; +}; + +activities = function(more="查看完整日志"){ + const items = STATE.demo ? null : (STATE.activities || []); + if (STATE.demo || !items) return _activities(more); + const cards = items.length ? items.map(x=>{ + const tone = x.tone || ""; + const color = tone==="orange"?"var(--orange)":tone==="blue"?"var(--blue)":"var(--green)"; + return `
${icon(x.icon || "activity")}
${esc(x.title || "")}

${esc(x.detail || "")}

● ${esc(x.state || "")}
`; + }).join("") : `
暂无执行记录
`; + const action = more === "复制全部记录" ? "copy-archive" : (more === "查看完整记录" ? "full-log" : "logs"); + return `
${cards}
`; +}; + +journey = function(){ + if (STATE.demo || !(STATE.journey && STATE.journey.length)) return _journey(); + return `
${STATE.journey.map((x,i)=>{ + const mode = x.mode || (x.active ? "active" : x.wait ? "wait" : ""); + const token = x.done ? "✓" : String(i+1); + return `
${token}
${esc(x.title)}${esc(x.status)}
${esc(x.time)}
`; + }).join("")}
`; +}; + +const _autoView = autoView; +const _queueView = queueView; +const _archiveView = archiveView; +const _aiView = aiView; +const _automationView = automationView; +const _logsView = logsView; +const _systemView = systemView; + +function liveRibbonAuto(){ + const m = STATE.metrics || {}; + return ribbon([ + ["监听状态", m.listen || (STATE.listening ? "运行中" : "已停止"), "activity", STATE.listening ? "green" : ""], + ["排队", m.queue || "0", "users"], + ["今日回复", m.today || "0", "message"], + ["失败", m.failed || "0", "alert", Number(m.failed) > 0 ? "red" : ""], + ]); +} +function liveRibbonQueue(){ + const m = STATE.metrics || {}; + return ribbon([ + ["处理中", m.processing || "0", "check", "", "blue"], + ["等待中", m.waiting || "0", "clock"], + ["待重试", m.retry || "0", "refresh", "orange"], + ["平均等待", m.avgWait || "0s", "clock", "green"], + ]); +} +function liveRibbonLogs(){ + const m = STATE.metrics || {}; + return ribbon([ + ["企业微信", m.wecomHealth || "待连接", "message", STATE.wecomConnected ? "green" : ""], + ["AI 成功率", m.aiRate || "—", "brain", "", "blue"], + ["平均响应", m.avgResp || "—", "clock", "", "blue"], + ["警告", m.warnings || "0", "alert", Number(m.warnings) > 0 ? "red" : ""], + ]); +} + +function currentTaskCard(){ + const task = STATE.task; + if (!task){ + return `

当前处理任务

暂无处理中的任务新的客户消息入队后会显示在这里
`; + } + const messages = (task.messages || []).map(x=>`
${esc(x.text || "")}
`).join("") || `
等待读取客户消息
`; + const answer = task.answer ? `
${esc(task.answer).replaceAll("\\n","
")}
` : `
尚未生成回复
`; + return `

当前处理任务

${icon("user")}${esc(task.name || "")}${esc(task.messageCount || "已读取消息")}${esc(task.action || "")}
复制的聊天内容
${messages}
AI 回复预览
${answer}
${icon("shield")}置信度 ${esc(task.confidence || "—")}${icon("clock")}模型耗时 ${esc(task.latency || "—")}
`; +} + +function windowStatusCard(){ + const stats = STATE.windowStats || [["folder","窗口", STATE.wecomConnected ? "已识别" : "未识别"],["message","当前会话", (STATE.task && STATE.task.name) || "—"],["monitor","DPI", STATE.dpi || "—"],["shield","人工保护", STATE.idleLabel || "—"],["refresh","最小化恢复", "已开启"]]; + return `

企业微信窗口状态

${stats.map(x=>`
${icon(x[0])}${esc(x[1])}${esc(x[2])}
`).join("")}
`; +} + +function queueDetailPanel(){ + const task = STATE.task; + if (!task){ + return `

任务详情

暂无选中任务选择左侧任务后查看来源消息与处理进度
`; + } + const steps = task.steps || [["入队","等待中"],["读取消息","等待中"],["请求 AI","等待中"],["生成回复","等待中"],["回填发送","等待中"],["完成","等待中"]]; + const active = Number(task.stepIndex ?? -1); + const process = steps.map((x,i)=>`
${i${esc(x[0])}${esc(x[1])}
`).join(""); + const message = (task.messages && task.messages[0]) || {time:"", text:"队列为空时,选择任务后会在这里显示来源消息。"}; + return `

任务详情

客户${esc(task.name || "暂无选中任务")}
会话 ID${esc(task.sessionId || "--")} 
已等待${esc(task.wait || "--")}
重试${esc(task.retries || "0 次")}
来源消息
${esc(message.text || "")}
${icon("sparkles")} ${esc(task.action || "等待新任务")}
${process}
`; +} + +function schedulePanel(){ + const s = (STATE.task && STATE.task.schedule) || {priority:"—", eta:"—", protect:"—", retry:"—"}; + return `

调度信息

${icon("flag")}
优先级${esc(s.priority)}
${icon("clock")}
预计完成${esc(s.eta)}
${icon("shield")}
人工保护${esc(s.protect)}
${icon("refresh")}
下次重试${esc(s.retry)}
`; +} + +autoView = function(){ + if (STATE.demo) return _autoView(); + const count = (STATE.people || []).length; + return `
${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(``,"danger"))}${liveRibbonAuto()}

任务队列

${count}${icon("filter")}${icon("sort")}
${queueList()}
${currentTaskCard()}

自动化执行状态

${esc(STATE.journeyTotal || "")}
${journey()}
${windowStatusCard()}

实时事件

${activities()}
`; +}; + +queueView = function(){ + if (STATE.demo) return _queueView(); + const count = (STATE.people || []).length; + const filter = STATE.queueFilter || "全部"; + const pills = ["全部","处理中","等待中","待重试"].map(name=>``).join(""); + return `
${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(``,"danger"))}${liveRibbonQueue()}

全部任务

${pills}
${queueList()}
${queueDetailPanel()}${schedulePanel()}

任务执行记录

${activities()}
`; +}; + +archiveView = function(){ + if (STATE.demo) return _archiveView(); + const rows = STATE.archives || []; + const chat = STATE.chat || []; + const selected = rows.find(x=>x.key===STATE.selectedArchive) || rows[0] || {}; + const chatHtml = chat.length ? chat.map(x=>`
${icon(x.role==="assistant"?"bot":"user")}
${esc(x.text || "")}
`).join("") : `
选择会话后显示聊天记录
`; + const tags = (STATE.memory && STATE.memory.tags || []).map((t,i)=>`${esc(t)}`).join("") || `暂无标签`; + const settle = (STATE.settle || []).map(x=>`
${x.iconHtml || icon("clipboard")}
${esc(x.title)}${esc(x.hint)}
${esc(x.value)}
`).join(""); + const filter = STATE.archiveFilter || "全部会话"; + return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`${esc(STATE.wecomLabel || "企业微信")}${esc(STATE.aiLabel || "AI 服务")}`)}
${["全部会话","今日","AI 已回复","已转人工"].map(name=>``).join("")}

最近会话

${queueList(true)}

会话记录

${icon("user")}${esc(selected.name || "未选择")}${esc(selected.count || "0")} 条消息${esc(selected.badge || "")}${icon("clock")}${esc(selected.time || "")}
${chatHtml}

客户记忆

${tags}
${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。")}
${icon("clock")} 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮

业务沉淀

${settle || "
暂无业务沉淀
"}

最近更新

${activities("复制全部记录")}
`; +}; + +aiView = function(){ + if (STATE.demo) return _aiView(); + const ai = STATE.ai || {}; + const toolsOn = ai.tools || [true, true, true]; + const names = ["挂号登记","客户查询","回访记录"]; + const toolIcons = ["clipboard","search","clipboard"]; + return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`配置已同步`)}

客服人格

AI
服务风格:温和专业简洁
${icon("sparkles")} 实时预览

您好!我是甄养堂${esc(ai.name || "贴心管家")},
很高兴为您服务 😊

请问有什么可以帮您的吗?

${icon("brain")} 模型引擎

${esc(ai.backup || "由云端策略管理")}
${esc(ai.context || 8)} 轮
${esc(ai.maxTokens || 500)} tokens

${icon("tray")} 企业知识库

${esc(ai.knowledge || "0")}个知识片段
● 已同步✓ 已连接

${icon("package")} MCP 工具

单次最多调用 ${esc(ai.mcpRounds || 5)} 个工具 ⓘ
${names.map((n,i)=>`
${icon(toolIcons[i])}
${n}
${toolsOn[i]?"已启用":"未启用"}
`).join("")}

${icon("shield")} 安全边界

${icon("shield")} 不做医疗诊断
${icon("pill")} 不修改用药方案
${icon("users")} 高风险转人工
当前模式:${icon("shield")}严格模式

配置状态

${statusCells([["cloud","云端模型正常","最后检查:刚刚"],["folder","知识库已同步",`${ai.knowledge || 0} 个知识片段`],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])}
`; +}; + +automationView = function(){ + if (STATE.demo) return _automationView(); + const a = STATE.automation || {}; + const autoOn = (STATE.sendMode || "auto") === "auto"; + return `
${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`${esc(a.wecom || "企业微信未识别")}规则已生效`)}

监听与发送策略

${icon("activity")} ● 规则运行中
${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`
${icon(x[0])}
${x[1]}
`).join("")}
${icon("clock")}扫描间隔 ${esc(a.poll || "2")} 秒
${icon("hourglass")} 连续消息等待 ${esc(a.batch || "20")} 秒
${icon("clock")}发送前等待 ${esc(a.delay || "1")} 秒

企业微信窗口

${icon("clipboard")} 主窗口:● ${esc(a.wecom || "未识别")}
${icon("user")} 当前账号:${esc(a.account || "—")}
${icon("globe")} DPI:${esc(STATE.dpi || "—")}
${icon("monitor")} 最小化恢复:● 已开启

人机共存

启用人工操作保护
鼠标静止 ${esc(a.idle || "5")} 秒 后恢复
${icon("bot")}
AI 运行中
${icon("cursor")}
${icon("pause")}
检测到人工操作
AI 暂停

消息处理

✓ 同一客户连续消息自动合并
✓ 保留最近 ${esc(a.context || 8)} 轮上下文${esc(a.context || 8)} 轮
✓ 一次只处理 1 个会话1 个

异常与兜底

${icon("clock")}AI 超时:重试 2 次2 次
${icon("alert")}发送失败:自动重试已开启
${icon("message")}固定回复:${esc(a.reply || "在的,请稍等")}编辑

高风险转人工

● 已开启
${icon("shield")} 诊断${icon("package")} 用药调整${icon("coins")} 投诉退款

启动前检查

${statusCells([["check","企业微信已登录", STATE.wecomConnected ? "检查完成" : "等待连接"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准", STATE.wecomConnected ? "检查完成" : "待校准"],["shield","规则配置有效","检查完成"]])}
`; +}; + +logsView = function(){ + if (STATE.demo) return _logsView(); + const events = STATE.logs || []; + const list = events.length ? events.map(x=>`
${icon(x.icon || "activity")}
${esc(x.title)}${esc(x.detail)}
${esc(x.state || "")}
`).join("") : `
当前没有运行记录
`; + const ex = STATE.exceptions || {}; + const detail = STATE.logDetail || {title:"等待事件", text:"开始监听后,这里会显示最近一条运行事件。", tags:[]}; + return `
${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`${esc(STATE.statusLabel || "监控中")}`)}${liveRibbonLogs()}

实时事件流

${["全部","连接","识别","AI","发送","警告"].map(n=>``).join("")}
${list}

系统健康

${[["企业微信连接", STATE.wecomConnected ? "正常" : "待连接"],["AI 服务","正常"],["窗口识别", STATE.wecomConnected ? "正常" : "待监听"]].map(x=>`
${icon("check")}
${x[0]}${x[1]}
`).join("")}
${icon("clock")}运行时长 ${esc(STATE.runtime || "00:00:00")}

当前回复链路

${[["activity","扫描未读",""],["message","读取消息",""],["brain","请求 AI",""],["send","回填发送",""]].map(x=>`
${icon(x[0])}
${x[1]}${x[2]}
`).join("")}

事件详情

${icon("user")}
${esc(detail.title)}

${esc(detail.text)}

${(detail.tags||[]).map(t=>`${esc(t)}`).join("")}

异常追踪

${icon("alert")}
发送失败${esc(ex.sendFail || 0)}
${icon("clock")}
模型超时${esc(ex.timeout || 0)}
${icon("monitor")}
窗口丢失${esc(ex.windowLost || 0)}

最近活动

${activities("查看完整记录")}
`; +}; + +systemView = function(){ + if (STATE.demo) return _systemView(); + const s = STATE.system || {}; + const on = k => s[k] ? "on" : ""; + return `
${head("系统设置","管理软件启动、数据存储、通知与系统维护",`当前版本 v${esc(s.version || "2.0")} ${icon("info")}`)}

基础设置

${icon("power")}开机自动启动
${icon("activity")}启动后自动监听
${icon("tray")}最小化到系统托盘
${icon("log")}关闭窗口时保持后台运行
${icon("globe")}界面语言: 简体中文${icon("chevron")}
${icon("monitor")}缩放比例: 自动${icon("chevron")}

数据与存储

${esc(s.storage || "—")}已使用
● 会话归档${esc(s.archiveSize || "—")}
● 运行日志${esc(s.logSize || "—")}
● 缓存数据${esc(s.cacheSize || "—")}
数据保留${esc(s.retention || 90)} 天

通知提醒

${icon("send")}发送失败提醒
${icon("hourglass")}模型超时提醒
${icon("user")}需要人工跟进
${icon("wifi")}企业微信连接中断
${icon("bell")}
发送失败提醒消息发送失败,已加入重试队列

安全与隐私

${icon("shield")}敏感信息脱敏:已开启 ●
${icon("lock")}本地数据加密:已开启 ●
${icon("lock")}日志隐藏聊天正文:已开启 ●
${icon("log")}退出时清理剪贴板:已开启 ●

网络与服务

${icon("cloud")}云端 AI:已连接 ●
${icon("folder")}企业知识库:已连接 ●
${icon("wifi")}网络延迟:— ●

维护与诊断

${icon("refresh")}
检查更新已是最新版本
${icon("clipboard")}
导出诊断导出系统诊断报告
${icon("refresh")}
恢复默认设置重置所有设置项
${icon("shield")}
查看许可信息查看软件许可详情

系统状态

${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","数据目录正常","本地数据可写"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务", STATE.listening ? "运行中" : "已停止"]])}
`; +}; + +views = {auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView}; + +const _render = render; +render = function(name){ + if(!views[name]) name = STATE.view || "auto"; + STATE.view = name; + listening = !!STATE.listening; + _render(name); + const orb = document.getElementById("orb"); + const label = document.getElementById("orb-label"); + if (orb) orb.classList.toggle("paused", !STATE.listening); + if (label) label.textContent = STATE.statusLabel || (STATE.listening ? "监听中" : "已停止"); +}; + +window.applyState = function(payload){ + const data = typeof payload === "string" ? JSON.parse(payload) : (payload || {}); + const prev = STATE.view; + Object.assign(STATE, data); + STATE.ready = true; + if (typeof data.listening === "boolean") listening = data.listening; + const active = document.activeElement; + const typing = active && /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName); + if (typing && (STATE.view || prev) === prev && ["ai","automation","system","archive","logs"].includes(prev)) { + return; + } + render(STATE.view || prev || "auto"); +}; + +window.showConsoleView = function(name){ render(name); }; + +document.addEventListener("click", e => { + const nav = e.target.closest("[data-view]"); + if (nav){ + const name = nav.dataset.view; + render(name); + callBridge("navigate", name); + e.stopImmediatePropagation(); + return; + } + const action = e.target.closest("[data-action]"); + if (!action) return; + const a = action.dataset.action; + const key = action.dataset.key || ""; + if (a === "toggle-listen" || a === "start-listen"){ + if (!callBridge("toggleListen")) { + listening = !listening; + STATE.listening = listening; + render(STATE.view); + } + e.stopImmediatePropagation(); + return; + } + const map = { + logs: () => callBridge("navigate", "logs") || render("logs"), + refresh: () => callBridge("refresh") || toast("已刷新"), + "clear-logs": () => callBridge("clearLogs") || toast("日志已清空"), + "cancel-task": () => callBridge("cancelTask", key), + "retry-task": () => callBridge("retryTask", key), + "handoff-task": () => callBridge("handoffTask", key), + "select-task": () => callBridge("selectTask", key), + "select-archive": () => callBridge("selectArchive", key), + "queue-filter": () => callBridge("setQueueFilter", key), + "archive-filter": () => callBridge("setArchiveFilter", key), + "event-filter": () => callBridge("setEventFilter", key), + "copy-session": () => callBridge("copyText", key), + "copy-archive": () => callBridge("copyArchive"), + "export-archive": () => callBridge("exportArchive"), + "export-logs": () => callBridge("exportLogs"), + "full-log": () => callBridge("showFullLog"), + "save-ai": () => callBridge("saveAi", JSON.stringify({name: (document.getElementById("ai-name")||{}).value || "", temperature: Number((document.getElementById("ai-temp")||{}).value || 35)/100})), + "test-ai": () => callBridge("testAi"), + "pick-model": () => callBridge("pickModel"), + "sync-knowledge": () => callBridge("syncKnowledge"), + "open-data": () => callBridge("openDataDir"), + "save-automation": () => callBridge("saveAutomation"), + "send-mode": () => callBridge("setSendMode", key), + "toggle-idle": () => callBridge("toggleIdle"), + "redetect": () => callBridge("redetectWecom"), + "security-rules": () => callBridge("showSecurityRules"), + "save-system": () => callBridge("saveSystem"), + "sys-toggle": () => callBridge("toggleSetting", key), + "check-update": () => callBridge("checkUpdate"), + "backup": () => callBridge("backupData"), + "clear-cache": () => callBridge("clearCache"), + "reset-settings": () => callBridge("resetSettings"), + license: () => callBridge("showLicense"), + "toggle-tool": () => callBridge("toggleTool", key), + }; + if (map[a]){ + map[a](); + e.stopImmediatePropagation(); + } +}, true); + +document.addEventListener("change", e => { + if (e.target && e.target.id === "archive-search") callBridge("setArchiveQuery", e.target.value || ""); + if (e.target && e.target.id === "event-search") callBridge("setEventQuery", e.target.value || ""); +}); + +const _fit = fit; +fit = function(){ + const embedded = document.documentElement.classList.contains("embedded"); + const w = embedded ? 1630 : 1630; + const h = embedded ? 920 : 965; + const s = Math.min(innerWidth / w, innerHeight / h); + document.getElementById("stage").style.setProperty("--scale", String(s)); +}; + +function connectQt(){ + if (typeof qt === "undefined" || !qt.webChannelTransport || typeof QWebChannel === "undefined") return; + new QWebChannel(qt.webChannelTransport, channel => { + window.qtBridge = channel.objects.bridge; + if (window.qtBridge.stateChanged) window.qtBridge.stateChanged.connect(window.applyState); + if (window.qtBridge.requestState) window.qtBridge.requestState(); + }); +} +connectQt(); +addEventListener("resize", fit); +fit(); +render((STATE.view || (location.hash || "#auto").slice(1) || "auto")); +''' + +(root / "console-app.js").write_text(header + script + footer, encoding="utf-8") +print("wrote", root / "console-app.js", (root / "console-app.js").stat().st_size) diff --git a/wechat_rpa/assets/ui/console-app.js b/wechat_rpa/assets/ui/console-app.js new file mode 100644 index 0000000..4d9faed --- /dev/null +++ b/wechat_rpa/assets/ui/console-app.js @@ -0,0 +1,1005 @@ + +const icon=n=>``; +if (/\bQt/i.test(navigator.userAgent) || window.qtBridge || typeof qt !== "undefined") { + document.documentElement.classList.add("embedded"); +} +window.STATE = window.STATE || { + view: "auto", + demo: false, + ready: false, + listening: false +}; +function esc(value){ + return String(value ?? "").replace(/[&<>"']/g, ch => ({ + "&":"&","<":"<",">":">",'"':""","'":"'" + }[ch])); +} +function bridge(){ return window.qtBridge || null; } +function callBridge(name, ...args){ + const api = bridge(); + if (!api || typeof api[name] !== "function") return false; + api[name](...args); + return true; +} +function fieldNum(id, fallback){ + const el = document.getElementById(id); + if (!el || el.value === "" || el.value == null) return fallback; + const n = Number(el.value); + return Number.isFinite(n) ? n : fallback; +} +function collectAiPayload(extra){ + const ai = STATE.ai || {}; + const rawLength = fieldNum("ai-length", [10,55,90][Number(ai.replyLengthIndex ?? 1)] || 55); + const replyLength = rawLength <= 33 ? 0 : (rawLength < 75 ? 1 : 2); + const payload = { + name: (document.getElementById("ai-name") || {}).value || ai.name || "", + temperature: fieldNum("ai-temp", Math.round(Number(ai.temperature || 0.35) * 100)) / 100, + context: fieldNum("ai-context", fieldNum("auto-context", Number(ai.context || 8))), + maxTokens: fieldNum("ai-tokens", Number(ai.maxTokens || 500)), + mcpRounds: fieldNum("ai-mcp-rounds", Number(ai.mcpRounds || 5)), + replyLength: replyLength, + }; + return JSON.stringify(Object.assign(payload, extra || {})); +} +function markAiDirty(){ + const status = document.querySelector("[data-config-status]"); + if (status) status.innerHTML = '有未保存更改'; +} +function collectAutomationPayload(){ + const a = STATE.automation || {}; + return JSON.stringify({ + poll: fieldNum("auto-poll", Number(a.poll || 2)), + batch: fieldNum("auto-batch", Number(a.batch || 20)), + delay: fieldNum("auto-delay", Number(a.delay || 1)), + idle: fieldNum("auto-idle", Number(a.idle || 5)), + context: fieldNum("auto-context", Number(a.context || 8)), + }); +} +function collectSystemPayload(){ + const s = STATE.system || {}; + return JSON.stringify({ + language: (document.getElementById("sys-language") || {}).value || s.language || "简体中文", + scale: (document.getElementById("sys-scale") || {}).value || s.scale || "自动", + retention: fieldNum("sys-retention", Number(s.retention || 90)), + }); +} +function sortLabel(mode){ + return ({queue:"入队顺序", wait:"等待时长", name:"客户名称"})[mode] || "入队顺序"; +} + + var people=[["高","高瑞@微信","处理中","00:00:12",""],["小","一个小迷糊@微信","等待中","00:01:45","green"],["李","李先生@微信","等待中","00:02:10",""] ,["陈","陈女士@微信","待重试","—","violet"],["张","张小明@微信","等待中","00:03:22","cyan"]]; + var listening=false; + var head=(title,sub,actions)=>`

${title}

${sub}

${actions}
`; + var connected=(extra="",primary="")=>`企业微信已连接AI 服务正常${extra}`; + var ribbon=items=>`
${items.map(x=>`
${icon(x[2])}
${x[0]}${x[1]}
`).join("")}
`; + var queueList=(archive=false)=>`
${people.map((p,i)=>`
${p[0]}
${p[1]}${archive?(i===0?"请问你们的灵芝孢子粉怎么服用…":i===1?"每天吃几次比较合适?":i===2?"有不良反应吗?":"会话已归档"):`${p[2]}`}
${archive?["16:31","16:18","15:42","14:20","13:35"][i]:p[3]}${archive?`${i===2?"已转人工":"AI 已回复"}`:`${icon("message")} ${i===3?1:0}`}
`).join("")}
`; + var activities=(more="查看完整日志")=>`
${[["16:33:27","扫描未读","检测到客户未读消息","activity","成功",""],["16:33:28","提取聊天记录","已提取最近 6 条聊天记录","log","成功",""],["16:33:29","AI 服务","正在请求 AI 生成回复","brain","进行中","blue"],["16:33:31","人工保护","检测到人工鼠标操作,暂停 5 秒","user","警告","orange"]].map(x=>`
${icon(x[3])}
${x[1]}

${x[2]}

● ${x[4]}
`).join("")}
`; + var journey=()=>`
${[["扫描未读","已完成","0.3s"],["提取聊天记录","已完成","1.2s"],["合并连续消息","已完成","0.4s"],["发送至 AI","已完成","0.6s"],["生成回复","进行中","2.4s"],["回填企业微信","等待","—"],["自动发送与归档","等待","—"]].map((x,i)=>`
4?"wait":""}">
${i<4?"✓":i+1}
${x[0]}${x[1]}
${x[2]}
`).join("")}
`; + var statusCells=items=>`
${items.map(x=>`
${icon(x[0])}
${x[1]}${x[2]}
`).join("")}
`; + function autoView(){return `
${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`${icon("send")}自动发送`,"danger"))}${ribbon([["监听状态","运行中","activity","green"],["排队","3","users"],["今日回复","128","message"],["失败","1","alert","red"]])}

任务队列

3${icon("filter")}${icon("sort")}
${queueList()}

当前处理任务

${icon("user")}高瑞@微信已读取 6 条消息AI 生成中
复制的聊天内容
请问你们的灵芝孢子粉怎么服用?
每天吃几次比较合适?
有什么注意事项吗?
AI 回复预览
您好!灵芝孢子粉建议每日 1–2 次,每次 2–3 克,温水冲服,饭前半小时或睡前服用效果更佳。
如有特殊体质或正在服药,建议咨询专业医生后再服用哦。
${icon("shield")}置信度 96%${icon("clock")}模型耗时 2.4s

自动化执行状态

总耗时 5.01s
${journey()}

企业微信窗口状态

${[["folder","窗口","已识别"],["message","当前会话","高瑞@微信"],["monitor","DPI","200%"],["shield","人工保护","5 秒"],["refresh","最小化恢复","已开启"]].map(x=>`
${icon(x[0])}${x[1]}${x[2]}
`).join("")}

实时事件

${activities()}
`} + function queueView(){return `
${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`${icon("send")}自动发送`,"danger"))}${ribbon([["处理中","1","check","","blue"],["等待中","3","clock"],["待重试","1","refresh","orange"],["平均等待","12s","clock","green"]])}

全部任务

${queueList()}

任务详情

客户高瑞@微信
会话 ID7bc26546… ${icon("copy")}
已等待12 秒
重试0 次
来源消息
请问你们的灵芝孢子粉怎么服用?
✦ AI 生成回复中
${[["入队","16:32:11"],["读取消息","16:32:11"],["请求 AI","16:32:12"],["生成回复","进行中"],["回填发送","等待中"],["完成","等待中"]].map((x,i)=>`
${i<3?"✓":i+1}
${x[0]}${x[1]}
`).join("")}

调度信息

优先级普通
${icon("clock")}
预计完成3 秒
${icon("shield")}
人工保护已开启
${icon("refresh")}
下次重试

任务执行记录

${activities()}
`} + function archiveView(){return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`企业微信已连接AI 服务正常`)}

最近会话

${queueList(true)}

会话记录

${icon("user")}高瑞@微信6 条消息AI 已回复${icon("clock")}16:31 更新
${icon("user")}
请问你们的灵芝孢子粉怎么服用?
${icon("bot")}
每天吃几次比较合适?
${icon("user")}
有什么注意事项吗?
${icon("bot")}
温水冲服,饭前半小时或睡前服用效果更佳。
如有特殊体质或正在服药,建议咨询专业医生后再服用哦。
${icon("user")}
好的,谢谢!
${icon("bot")}
不客气!如有其他问题,随时为您服务~

客户记忆

饮食控制体重管理长期回访
客户关注灵芝孢子粉的服用方法与注意事项,希望优化服用时间与剂量,同时注重产品安全性与体质适配。
◔ 上下文 8 轮

业务沉淀

挂号登记已登记潜在客户
1 次 ✓
回访线索创建回访计划
1 条 ✓
${icon("user")}
人工跟进未分配客服
0 次 ·

最近更新

${activities("复制全部记录")}
`} + function aiView(){return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`配置已同步`)}

客服人格

AI
服务风格:温和专业简洁
✧ 实时预览

您好!我是甄养堂贴心管家,
很高兴为您服务 😊

请问有什么可以帮您的吗?

✣ 模型引擎

▰ 企业知识库

1,284个知识片段
● 刚刚同步✓ 已连接

◇ MCP 工具

单次最多调用 5 个工具 ⓘ
挂号登记
已启用
客户查询
已启用
回访记录
已启用

${icon("shield")} 安全边界

🛡 不做医疗诊断
💊 不修改用药方案
♙ 高风险转人工
当前模式:${icon("shield")}严格模式

配置状态

${statusCells([["cloud","云端模型正常","最后检查:刚刚"],["folder","知识库已同步","1,284 个知识片段"],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])}
`} + function automationView(){return `
${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`企业微信已识别规则已生效`)}

监听与发送策略

${icon("activity")} ● 规则运行中
${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`
${icon(x[0])}
${x[1]}
`).join("")}
${icon("clock")}扫描间隔 2 秒
⌛ 连续消息等待 20 秒
${icon("clock")}发送前等待 1 秒

企业微信窗口

▣ 主窗口:● 已识别
${icon("user")} 当前账号:甄养堂客服
◎ DPI:200%
${icon("monitor")} 最小化恢复:● 已开启

人机共存

启用人工操作保护
鼠标静止 5 秒 后恢复
${icon("bot")}
AI 运行中

检测到人工操作
AI 暂停

消息处理

✓ 同一客户连续消息自动合并
✓ 保留最近 8 轮上下文8 轮
✓ 一次只处理 1 个会话1 个

异常与兜底

${icon("clock")}AI 超时:重试 2 次2 次
${icon("alert")}发送失败:自动重试已开启
${icon("message")}固定回复:在的,请稍等编辑

高风险转人工

● 已开启
⚕ 诊断◇ 用药调整¥ 投诉退款

启动前检查

${statusCells([["check","企业微信已登录","检查完成"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准","检查完成"],["shield","规则配置有效","检查完成"]])}
`} + function logsView(){return `
${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`监控中`)}${ribbon([["企业微信","99.9%","message","green"],["AI 成功率","98.6%","brain","","blue"],["平均响应","2.4s","clock","","blue"],["警告","1","alert","red"]])}

实时事件流

${[["16:33:27","未读扫描","检测到客户未读消息","activity","成功","green"],["16:33:28","提取聊天记录","已读取最近 6 条消息","message","成功","green"],["16:33:29","AI 服务","正在生成回复","brain","进行中",""],["16:33:31","人工保护","检测到鼠标操作,暂停 5 秒","user","警告","orange"],["16:33:36","自动发送","回复发送成功","send","成功","green"]].map(x=>`
${icon(x[3])}
${x[1]}${x[2]}
${x[4]}
`).join("")}

系统健康

${[["企业微信连接","正常"],["AI 服务","正常"],["窗口识别","正常"]].map(x=>`
${icon("check")}
${x[0]}${x[1]}
`).join("")}
${icon("clock")}运行时长 02:18:46

当前回复链路

${[["activity","扫描未读","0.3s"],["message","读取消息","1.2s"],["brain","请求 AI","2.4s"],["send","回填发送","0.6s"]].map(x=>`
${icon(x[0])}
${x[1]}${x[2]}
`).join("")}

事件详情

${icon("user")}
人工保护

检测到人工鼠标操作,自动回复暂停 5 秒

模块:人机共存会话:高瑞@微信已自动恢复

异常追踪

${icon("alert")}
发送失败1
${icon("clock")}
模型超时0
${icon("monitor")}
窗口丢失0

最近活动

${activities("查看完整记录")}
`} + function systemView(){return `
${head("系统设置","管理软件启动、数据存储、通知与系统维护",`当前版本 v2.0 ⓘ`)}

基础设置

${icon("power")}开机自动启动
${icon("activity")}启动后自动监听
最小化到系统托盘
${icon("log")}关闭窗口时保持后台运行
界面语言: 简体中文
${icon("monitor")}缩放比例: 自动

数据与存储

286MB 已使用
● 会话归档128 MB
● 运行日志86 MB
● 缓存数据72 MB
数据保留90 天 ⌄

通知提醒

${icon("send")}发送失败提醒
模型超时提醒
${icon("user")}需要人工跟进
企业微信连接中断
发送失败提醒消息发送失败,已加入重试队列

安全与隐私

${icon("shield")}敏感信息脱敏:已开启 ●
${icon("lock")}本地数据加密:已开启 ●
${icon("lock")}日志隐藏聊天正文:已开启 ●
${icon("log")}退出时清理剪贴板:已开启 ●

网络与服务

${icon("cloud")}云端 AI:已连接 ●
${icon("folder")}企业知识库:已连接 ●
网络延迟:28ms ●

维护与诊断

${icon("refresh")}
检查更新已是最新版本
导出诊断导出系统诊断报告
恢复默认设置重置所有设置项
${icon("shield")}
查看许可信息查看软件许可详情

系统状态

${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","数据已备份","最近备份:16:33:28"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务运行中","系统运行状态良好"]])}
`} + var views={auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView}; + var glyphIcons=[["✦","sparkles"],["✧","sparkles"],["✣","brain"],["⚑","flag"],["▣","clipboard"],["☎","phone"],["⌕","search"],["▤","clipboard"],["◇","package"],["🛡","shield"],["💊","pill"],["♙","users"],["⌛","hourglass"],["◎","globe"],["⚕","shield"],["¥","coins"],["⌁","wifi"],["♟","bell"],["▰","tray"],["▷","play"],["↻","refresh"],["➤","cursor"],["Ⅱ","pause"],["◔","clock"],["ⓘ","info"],["⌄","chevron"]]; + function polishedIcons(html){return glyphIcons.reduce((value,[glyph,name])=>value.replaceAll(glyph,icon(name)),html)} + function render(name){ + if(!views[name])name="auto"; + document.querySelectorAll("[data-view]").forEach(b=>b.classList.toggle("active",b.dataset.view===name)); + document.getElementById("main").innerHTML=polishedIcons(views[name]()); + const listen=document.querySelector('[data-action="toggle-listen"]'); + if(listen)listen.innerHTML=listening?`${icon("pause")}暂停监听`:`${icon("play")}恢复监听`; + location.hash=name; + } + function toast(msg="操作已完成"){const t=document.getElementById("toast");t.textContent=msg;t.classList.add("show");clearTimeout(window.__toast);window.__toast=setTimeout(()=>t.classList.remove("show"),1600)} + document.addEventListener("click",e=>{if(window.STATE&&window.STATE.ready&&!window.STATE.demo&&bridge())return;const nav=e.target.closest("[data-view]");if(nav){render(nav.dataset.view);return}const action=e.target.closest("[data-action]");if(action){const a=action.dataset.action;if(a==="toggle-listen"){listening=!listening;document.getElementById("orb").classList.toggle("paused",!listening);document.getElementById("orb-label").textContent=listening?"监听中":"已暂停";render(location.hash.slice(1));toast(listening?"已恢复监听":"监听已暂停")}else if(a==="logs")render("logs");else if(a==="clear-logs"){const list=document.getElementById("log-list");if(list)list.innerHTML='
日志已清空
';toast("日志已清空")}else toast()}const sw=e.target.closest(".switch");if(sw)sw.classList.toggle("on");const pill=e.target.closest(".filter-pill,.tab");if(pill){[...pill.parentElement.children].forEach(x=>x.classList.remove("active"));pill.classList.add("active")}}); + function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById("stage").style.setProperty("--scale",String(s))} + +const _queueList = queueList; +const _activities = activities; +const _connected = connected; +const _journey = journey; + +connected = function(extra="", primary=""){ + const wecom = STATE.wecomLabel || (STATE.wecomConnected ? "企业微信已连接" : "企业微信未连接"); + const ai = STATE.aiLabel || (STATE.aiOk === false ? "AI 服务异常" : "AI 服务正常"); + const listenLabel = STATE.listening ? `${icon("pause")}暂停监听` : `${icon("play")}恢复监听`; + const wecomLed = STATE.wecomConnected ? "" : "off"; + const aiLed = STATE.aiOk === false ? "off" : ""; + return `${esc(wecom)}${esc(ai)}${extra}`; +}; + +queueList = function(archive=false){ + if (STATE.demo) return _queueList(archive); + const rows = archive ? (STATE.archives || []) : (STATE.people || []); + if (!rows.length){ + const title = archive ? "暂无会话档案" : "当前没有待处理任务"; + const hint = archive ? "新的客户会话会自动出现在这里" : "所有客户消息均已完成"; + return `
${icon("check")}
${title}${hint}
`; + } + return `
${rows.map((p,i)=>{ + const selectedKey = archive ? STATE.selectedArchive : STATE.selectedKey; + const active = p.active || p.key === selectedKey || (!selectedKey && i===0); + const preview = archive ? (p.preview || "会话已归档") : `${esc(p.status || "")}`; + const meta = archive + ? `${esc(p.time || "")}${esc(p.badge || "AI 已回复")}` + : `${esc(p.wait || "")}${icon("message")} ${p.bubbles || 0}`; + return `
${esc(p.avatar || "客")}
${esc(p.name || "")}${preview}
${meta}
`; + }).join("")}
`; +}; + +activities = function(more="查看完整日志"){ + const items = STATE.demo ? null : (STATE.activities || []); + if (STATE.demo || !items) return _activities(more); + const cards = items.length ? items.map(x=>{ + const tone = x.tone || ""; + const color = tone==="orange"?"var(--orange)":tone==="blue"?"var(--blue)":"var(--green)"; + return `
${icon(x.icon || "activity")}
${esc(x.title || "")}

${esc(x.detail || "")}

● ${esc(x.state || "")}
`; + }).join("") : `
暂无执行记录
`; + const action = more === "复制全部记录" ? "copy-archive" : (more === "查看完整记录" ? "full-log" : "logs"); + return `
${cards}
`; +}; + +journey = function(){ + if (STATE.demo) return _journey(); + const items = STATE.journey || []; + if (!items.length){ + return `
等待新的自动回复任务
`; + } + return `
${STATE.journey.map((x,i)=>{ + const mode = x.mode || (x.active ? "active" : x.wait ? "wait" : ""); + const token = x.done ? "✓" : String(i+1); + return `
${token}
${esc(x.title)}${esc(x.status)}
${esc(x.time)}
`; + }).join("")}
`; +}; + +const _autoView = autoView; +const _queueView = queueView; +const _archiveView = archiveView; +const _aiView = aiView; +const _automationView = automationView; +const _logsView = logsView; +const _systemView = systemView; + +function liveRibbonAuto(){ + const m = STATE.metrics || {}; + return ribbon([ + ["监听状态", m.listen || (STATE.listening ? "运行中" : "已停止"), "activity", STATE.listening ? "green" : "", "", "listen"], + ["排队", m.queue || "0", "users", "", "", "queue"], + ["今日回复", m.today || "0", "message", "", "", "today"], + ["失败", m.failed || "0", "alert", Number(m.failed) > 0 ? "red" : "", "", "failed"], + ]); +} +function liveRibbonQueue(){ + const m = STATE.metrics || {}; + return ribbon([ + ["处理中", m.processing || "0", "check", "", "blue", "processing"], + ["等待中", m.waiting || "0", "clock", "", "", "waiting"], + ["待重试", m.retry || "0", "refresh", "orange", "", "retry"], + ["平均等待", m.avgWait || "0s", "clock", "green", "", "avgWait"], + ]); +} +function liveRibbonLogs(){ + const m = STATE.metrics || {}; + return ribbon([ + ["企业微信", m.wecomHealth || "待连接", "message", STATE.wecomConnected ? "green" : "", "", "wecomHealth"], + ["AI 成功率", m.aiRate || "—", "brain", "", "blue", "aiRate"], + ["平均响应", m.avgResp || "—", "clock", "", "blue", "avgResp"], + ["警告", m.warnings || "0", "alert", Number(m.warnings) > 0 ? "red" : "", "", "warnings"], + ]); +} + +function currentTaskCard(){ + const task = STATE.task; + if (!task){ + return `

当前处理任务

暂无处理中的任务新的客户消息入队后会显示在这里
`; + } + const messages = (task.messages || []).map(x=>`
${esc(x.text || "")}
`).join("") || `
等待读取客户消息
`; + const answer = task.answer ? `
${esc(task.answer).replace(/\n/g,"
")}
` : `
尚未生成回复
`; + return `

当前处理任务

${icon("user")}${esc(task.name || "")}${esc(task.messageCount || "已读取消息")}${esc(task.action || "")}
复制的聊天内容
${messages}
AI 回复预览
${answer}
${icon("shield")}置信度 ${esc(task.confidence || "—")}${icon("clock")}模型耗时 ${esc(task.latency || "—")}
`; +} + +function windowStatusCard(){ + const stats = STATE.windowStats || [["folder","窗口", STATE.wecomConnected ? "已识别" : "未识别"],["message","当前会话", (STATE.task && STATE.task.name) || "—"],["monitor","DPI", STATE.dpi || "—"],["shield","人工保护", STATE.idleLabel || "—"],["refresh","最小化恢复", "已开启"]]; + return `

企业微信窗口状态

${stats.map(x=>`
${icon(x[0])}${esc(x[1])}${esc(x[2])}
`).join("")}
`; +} + +function queueDetailPanel(){ + const task = STATE.task; + if (!task){ + return `

任务详情

暂无选中任务选择左侧任务后查看来源消息与处理进度
`; + } + const steps = task.steps || [["入队","等待中"],["读取消息","等待中"],["请求 AI","等待中"],["生成回复","等待中"],["回填发送","等待中"],["完成","等待中"]]; + const active = Number(task.stepIndex ?? -1); + const process = steps.map((x,i)=>`
${i${esc(x[0])}${esc(x[1])}
`).join(""); + const message = (task.messages && task.messages[0]) || {time:"", text:"队列为空时,选择任务后会在这里显示来源消息。"}; + return `

任务详情

客户${esc(task.name || "暂无选中任务")}
会话 ID${esc(task.sessionDisplay || task.sessionId || "--")} ${icon("copy")}
已等待${esc(task.wait || "--")}
重试${esc(task.retries || "0 次")}
来源消息
${esc(message.text || "")}
${icon("sparkles")} ${esc(task.action || "等待新任务")}
${process}
`; +} + +function schedulePanel(){ + const s = (STATE.task && STATE.task.schedule) || {priority:"—", eta:"—", protect:"—", retry:"—"}; + return `

调度信息

${icon("flag")}
优先级${esc(s.priority)}
${icon("clock")}
预计完成${esc(s.eta)}
${icon("shield")}
人工保护${esc(s.protect)}
${icon("refresh")}
下次重试${esc(s.retry)}
`; +} + +autoView = function(){ + if (STATE.demo) return _autoView(); + const count = (STATE.people || []).length; + const waiting = (STATE.metrics && STATE.metrics.waiting) || "0"; + return `
${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(``,"danger"))}${liveRibbonAuto()}

任务队列

${esc(waiting)}${icon("filter")}${icon("sort")}
${queueList()}
${currentTaskCard()}

自动化执行状态

${esc(STATE.journeyTotal || "")}
${journey()}
${windowStatusCard()}

实时事件

${activities()}
`; +}; + +queueView = function(){ + if (STATE.demo) return _queueView(); + const count = (STATE.people || []).length; + const filter = STATE.queueFilter || "全部"; + const pills = ["全部","处理中","等待中","待重试"].map(name=>``).join(""); + return `
${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(``,"danger"))}${liveRibbonQueue()}

全部任务

${pills}
${queueList()}
${queueDetailPanel()}${schedulePanel()}

任务执行记录

${activities()}
`; +}; + +archiveView = function(){ + if (STATE.demo) return _archiveView(); + const rows = STATE.archives || []; + const chat = STATE.chat || []; + const selected = rows.find(x=>x.key===STATE.selectedArchive) || rows[0] || {}; + const chatHtml = chat.length ? chat.map(x=>`
${icon(x.role==="assistant"?"bot":"user")}
${esc(x.text || "")}
`).join("") : `
选择会话后显示聊天记录
`; + const tags = (STATE.memory && STATE.memory.tags || []).map((t,i)=>`${esc(t)}`).join("") || `暂无标签`; + const settle = (STATE.settle || []).map(x=>`
${x.iconHtml || icon("clipboard")}
${esc(x.title)}${esc(x.hint)}
${esc(x.value)}
`).join(""); + const filter = STATE.archiveFilter || "全部会话"; + const dateValue = STATE.archiveDate || ""; + return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`${esc(STATE.wecomLabel || "企业微信")}${esc(STATE.aiLabel || "AI 服务")}`)}
${["全部会话","今日","AI 已回复","已转人工"].map(name=>``).join("")}

最近会话

${queueList(true)}

会话记录

${icon("user")}${esc(selected.name || "未选择")}${esc(selected.count || "0")} 条消息${esc(selected.badge || "")}${icon("clock")}${esc(selected.time || "")}
${chatHtml}

客户记忆

${tags}
${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。")}
${icon("clock")} 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮

业务沉淀

${settle || "
暂无业务沉淀
"}

最近更新

${activities("复制全部记录")}
`; +}; + +aiView = function(){ + if (STATE.demo) return _aiView(); + const ai = STATE.ai || {}; + const toolsOn = ai.tools || [true, true, true]; + const names = ["挂号登记","客户查询","回访记录"]; + const toolIcons = ["clipboard","search","clipboard"]; + const tab = STATE.aiTab || "基础设置"; + const tabs = ["基础设置","知识与工具","安全策略"].map(n=>``).join(""); + const lengthIndex = Number(ai.replyLengthIndex ?? 1); + const lengthLabel = ai.replyLength || ["短","中等","长"][lengthIndex] || "中等"; + const tempVal = Number(ai.temperature || 0.35); + return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`配置已同步`)}
${tabs}

客服人格

AI
服务风格:温和专业简洁
${icon("sparkles")} 实时预览

您好!我是甄养堂${esc(ai.name || "贴心管家")},
很高兴为您服务 😊

请问有什么可以帮您的吗?

${icon("brain")} 模型引擎

tokens

${icon("tray")} 企业知识库

${esc(ai.knowledge || "0")}个 MCP 服务
● ${esc(ai.syncAt || "尚未同步")}✓ ${Number(ai.knowledge||0)?"已连接":"未配置"}

${icon("package")} MCP 工具

单次最多调用 个工具
${names.map((n,i)=>`
${icon(toolIcons[i])}
${n}
${toolsOn[i]?"已启用":"未启用"}
`).join("")}

${icon("link")} MCP 配置

服务数量${esc(ai.knowledge || "0")}
工具开关${toolsOn.filter(Boolean).length ? "已启用" : "未启用"}

${icon("shield")} 安全边界

${icon("shield")} 不做医疗诊断
${icon("pill")} 不修改用药方案
${icon("users")} 高风险转人工

转人工规则

${icon("shield")} 诊断${icon("package")} 用药调整${icon("coins")} 投诉退款

当前模式

${icon("shield")}严格模式

配置状态

${statusCells([["cloud", STATE.aiOk ? "云端模型正常" : "AI 服务异常", `最后同步:${esc(ai.syncAt || "尚未同步")}`],["folder","MCP 工具配置",`${esc(ai.knowledge || "0")} 个服务`],["folder",`${esc(ai.toolCount || 0)} 个工具可用`, toolsOn.filter(Boolean).length ? "已启用" : "未启用"],["shield","安全策略已生效","严格模式"]])}
`; +}; + +// Keep the live WebChannel pages on the exact HTML reference structure. Data +// and actions are dynamic, but no extra cards or alternate tab layouts are +// introduced here. +archiveView = function(){ + if (STATE.demo) return wireDemoArchive(_archiveView()); + const rows = STATE.archives || []; + const chat = STATE.chat || []; + const selected = rows.find(x=>x.key===STATE.selectedArchive) || rows[0] || {}; + const chatHtml = chat.length + ? chat.map(x=>`
${icon(x.role==="assistant"?"bot":"user")}
${esc(x.text || "").replace(/\n/g,"
")}
`).join("") + : `
选择会话后显示聊天记录
`; + const tags = (STATE.memory && STATE.memory.tags || []).map((tag,index)=>`${esc(tag)}`).join("") || `暂无标签`; + const settleTokens = ["▣","☎",icon("user")]; + const settleStyles = ["","background:linear-gradient(145deg,#9d7cff,#6e5be5)","background:linear-gradient(145deg,#ffc65a,#ff941f)"]; + const settle = (STATE.settle || []).map((item,index)=>`
${settleTokens[index] || icon("clipboard")}
${esc(item.title)}${esc(item.hint)}
${esc(item.value)}
`).join(""); + const filter = STATE.archiveFilter || "全部会话"; + const now = new Date(); + const currentDate = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`; + const dateLabel = STATE.archiveDate || currentDate; + const filterButtons = ["全部会话","今日","AI 已回复","已转人工"].map(name=>``).join(""); + return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`${esc(STATE.wecomLabel || "企业微信")}${esc(STATE.aiLabel || "AI 服务")}`)}
${filterButtons}

最近会话

${queueList(true)}

会话记录

${icon("user")}${esc(selected.name || "未选择")}${esc(selected.count || "0")} 条消息${esc(selected.badge || "")}${icon("clock")}${esc(selected.time || "")} 更新
${chatHtml}

客户记忆

${tags}
${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。").replace(/\n/g,"
")}
◔ 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮

业务沉淀

${settle || "
暂无业务沉淀
"}

最近更新

${activities("复制全部记录")}
`; +}; + +aiView = function(){ + if (STATE.demo) return wireDemoAi(_aiView()); + const ai = STATE.ai || {}; + const toolsOn = ai.tools || [true,true,true]; + const names = ["挂号登记","客户查询","回访记录"]; + const toolTokens = ["▣","⌕","▤"]; + const tab = STATE.aiTab || "基础设置"; + const tabs = ["基础设置","知识与工具","安全策略"].map(name=>``).join(""); + const contextValue = Math.max(1,Number(ai.context || 8)); + const contextValues = [...new Set([4,8,12,16,20,contextValue])].sort((a,b)=>a-b); + const contextOptions = contextValues.map(value=>``).join(""); + const tokenValue = Math.max(50,Number(ai.maxTokens || 500)); + const tokenValues = [...new Set([300,500,1000,tokenValue])].sort((a,b)=>a-b); + const tokenOptions = tokenValues.map(value=>``).join(""); + const lengthIndex = Math.max(0,Math.min(2,Number(ai.replyLengthIndex ?? 1))); + const lengthLabel = ["短","中等","长"][lengthIndex]; + const lengthValue = [10,55,90][lengthIndex]; + const temperature = Math.max(0,Math.min(1,Number(ai.temperature || 0.35))); + const knowledgeCount = ai.knowledgeCount || ai.knowledge || "0"; + const connectedKnowledge = ai.knowledgeConnected !== undefined ? Boolean(ai.knowledgeConnected) : Number(ai.knowledge || 0) > 0; + const syncLabel = ai.syncLabel || ai.syncAt || "尚未同步"; + const toolCount = Number(ai.toolCount ?? toolsOn.filter(Boolean).length); + const configLabel = ai.synced === false ? "配置未同步" : "配置已同步"; + const knowledgeStatus = connectedKnowledge ? "知识库已同步" : "知识库未连接"; + const toolStatus = toolCount === 3 ? "全部运行正常" : `${toolCount} 个已启用`; + return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`${configLabel}`)}
${tabs}

客服人格

AI
服务风格:温和专业简洁
✧ 实时预览

您好!我是甄养堂${esc(ai.name || "贴心管家")},
很高兴为您服务 😊

请问有什么可以帮您的吗?

✣ 模型引擎

▰ 企业知识库

${esc(knowledgeCount)}个知识片段
● ${esc(syncLabel)}✓ ${connectedKnowledge?"已连接":"未连接"}

◇ MCP 工具

单次最多调用 ${esc(ai.mcpRounds || 5)} 个工具 ⓘ
${names.map((name,index)=>`
${toolTokens[index]}
${name}
${toolsOn[index]?"已启用":"未启用"}
`).join("")}

${icon("shield")} 安全边界

🛡 不做医疗诊断
💊 不修改用药方案
♙ 高风险转人工
当前模式:${icon("shield")}严格模式

配置状态

${statusCells([["cloud",STATE.aiOk===false?"AI 服务异常":"云端模型正常","最后检查:刚刚"],["folder",knowledgeStatus,`${esc(knowledgeCount)} 个知识片段`],["folder",`${toolCount} 个工具可用`,toolStatus],["shield","安全策略已生效","严格模式"]])}
`; +}; + +automationView = function(){ + if (STATE.demo) return _automationView(); + const a = STATE.automation || {}; + const autoOn = (STATE.sendMode || "auto") === "auto"; + return `
${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`${esc(a.wecom || "企业微信未识别")}规则已生效`)}

监听与发送策略

${icon("activity")} ● 规则运行中
${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`
${icon(x[0])}
${x[1]}
`).join("")}
${icon("clock")}扫描间隔 
${icon("hourglass")} 连续消息等待 
${icon("clock")}发送前等待 

企业微信窗口

${icon("clipboard")} 主窗口:● ${esc(a.wecom || "未识别")}
${icon("user")} 当前账号:${esc(a.account || "—")}
${icon("globe")} DPI:${esc(STATE.dpi || "—")}
${icon("monitor")} 最小化恢复:● 已开启

人机共存

启用人工操作保护
鼠标静止  秒 后恢复
${icon("bot")}
AI 运行中
${icon("cursor")}
${icon("pause")}
检测到人工操作
AI 暂停

消息处理

✓ 同一客户连续消息自动合并
✓ 保留最近 轮上下文${esc(a.context || 8)} 轮
✓ 一次只处理 1 个会话1 个

异常与兜底

${icon("clock")}AI 超时:${esc(a.timeout || 120)} 秒后重试2 次
${icon("alert")}发送失败:自动重试已开启
${icon("message")}固定回复:${esc(a.reply || "在的,请稍等")}

高风险转人工

● 已开启
${icon("shield")} 诊断${icon("package")} 用药调整${icon("coins")} 投诉退款

启动前检查

${statusCells([["check","企业微信已登录", STATE.wecomConnected ? "检查完成" : "等待连接"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准", STATE.wecomConnected ? "检查完成" : "待校准"],["shield","规则配置有效","检查完成"]])}
`; +}; + +logsView = function(){ + if (STATE.demo) return _logsView(); + const events = STATE.logs || []; + const list = events.length ? events.map(x=>`
${icon(x.icon || "activity")}
${esc(x.title)}${esc(x.detail)}
${esc(x.state || "")}
`).join("") : `
当前没有运行记录
`; + const ex = STATE.exceptions || {}; + const detail = STATE.logDetail || {title:"等待事件", text:"开始监听后,这里会显示最近一条运行事件。", tags:[]}; + const chain = (STATE.chain && STATE.chain.length ? STATE.chain : [{icon:"activity",title:"扫描未读",time:""},{icon:"message",title:"读取消息",time:""},{icon:"brain",title:"请求 AI",time:""},{icon:"send",title:"回填发送",time:""}]); + return `
${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`${esc(STATE.statusLabel || "监控中")}`)}${liveRibbonLogs()}

实时事件流

${["全部","连接","识别","AI","发送","警告"].map(n=>``).join("")}
${list}

系统健康

${[["企业微信连接", STATE.wecomConnected ? "正常" : "待连接"],["AI 服务", STATE.aiOk ? "正常" : "异常"],["窗口识别", STATE.wecomConnected ? "正常" : "待监听"]].map(x=>`
${icon("check")}
${x[0]}${x[1]}
`).join("")}
${icon("clock")}运行时长 ${esc(STATE.runtime || "00:00:00")}

当前回复链路

${chain.map((x,i)=>`
${icon(x.icon || "activity")}
${esc(x.title)}${esc(x.time || "")}
`).join("")}

事件详情

${icon("user")}
${esc(detail.title)}

${esc(detail.text)}

${(detail.tags||[]).map(t=>`${esc(t)}`).join("")}

异常追踪

${icon("alert")}
发送失败${esc(ex.sendFail || 0)}
${icon("clock")}
模型超时${esc(ex.timeout || 0)}
${icon("monitor")}
窗口丢失${esc(ex.windowLost || 0)}

最近活动

${activities("查看完整记录")}
`; +}; + +systemView = function(){ + if (STATE.demo) return _systemView(); + const s = STATE.system || {}; + const on = k => s[k] ? "on" : ""; + return `
${head("系统设置","管理软件启动、数据存储、通知与系统维护",`当前版本 v${esc(s.version || "2.0")} ${icon("info")}`)}

基础设置

${icon("power")}开机自动启动
${icon("activity")}启动后自动监听
${icon("tray")}最小化到系统托盘
${icon("log")}关闭窗口时保持后台运行
${icon("globe")}界面语言
${icon("monitor")}缩放比例

数据与存储

${esc(s.storage || "—")}已使用
● 会话归档${esc(s.archiveSize || "—")}
● 运行日志${esc(s.logSize || "—")}
● 缓存数据${esc(s.cacheSize || "—")}
数据保留

通知提醒

${icon("send")}发送失败提醒
${icon("hourglass")}模型超时提醒
${icon("user")}需要人工跟进
${icon("wifi")}企业微信连接中断
${icon("bell")}
发送失败提醒消息发送失败,已加入重试队列

安全与隐私

${icon("shield")}敏感信息脱敏
${icon("lock")}本地数据加密
${icon("lock")}日志隐藏聊天正文
${icon("log")}退出时清理剪贴板

网络与服务

${icon("cloud")}云端 AI:${esc(s.cloudAi || "未检测")}
${icon("folder")}企业知识库:${esc(s.knowledgeNet || "未检测")}
${icon("wifi")}网络延迟:${esc(s.latency || "—")}

维护与诊断

${icon("refresh")}
检查更新${esc(s.updateLabel || "已是最新版本")}
${icon("clipboard")}
导出诊断导出系统诊断报告
${icon("refresh")}
恢复默认设置重置所有设置项
${icon("shield")}
查看许可信息查看软件许可详情

系统状态

${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","最近备份", esc(s.lastBackup || "尚未备份")],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务", STATE.listening ? "运行中" : "已停止"]])}
`; +}; + +views = {auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView}; + +function syncOrb(){ + const orb = document.getElementById("orb"); + const label = document.getElementById("orb-label"); + if (orb) orb.classList.toggle("paused", !STATE.listening); + if (label) label.textContent = STATE.statusLabel || (STATE.listening ? "监听中" : "已停止"); +} + +function coreFingerprint(state){ + const sys = state.system || {}; + const ai = state.ai || {}; + const auto = state.automation || {}; + const metrics = state.metrics || {}; + const view = state.view || "auto"; + const common = { + view: state.view, + demo: state.demo, + listening: state.listening, + wecomConnected: state.wecomConnected, + aiOk: state.aiOk, + wecomLabel: state.wecomLabel, + aiLabel: state.aiLabel, + }; + const people = (state.people || []).map(p => [ + p.key, p.name, p.status, p.bubbles, p.avatar, p.tone, p.dot + ]); + const task = state.task ? { + key: state.task.key, + name: state.task.name, + sessionId: state.task.sessionId, + sessionDisplay: state.task.sessionDisplay, + action: state.task.action, + stepIndex: state.task.stepIndex, + answer: state.task.answer, + messageCount: state.task.messageCount, + confidence: state.task.confidence, + messages: (state.task.messages || []).map(message => [message.time, message.text]), + steps: (state.task.steps || []).map(step => [step[0], step[1]]), + } : null; + const metricModes = [Number(metrics.failed) > 0, Number(metrics.warnings) > 0]; + let page; + if (view === "auto") { + page = { + selectedKey: state.selectedKey, + queueFilter: state.queueFilter, + queueSort: state.queueSort, + people, + task, + journey: (state.journey || []).map(j => [j.title, j.status, j.mode, j.done, j.active]), + windowStats: state.windowStats, + metricModes, + }; + } else if (view === "queue") { + page = { + selectedKey: state.selectedKey, + queueFilter: state.queueFilter, + queueSort: state.queueSort, + people, + task, + metricModes, + }; + } else if (view === "archive") { + page = { + archiveFilter: state.archiveFilter, + archiveQuery: state.archiveQuery, + archiveDate: state.archiveDate, + selectedArchive: state.selectedArchive, + archives: (state.archives || []).map(a => [ + a.key, a.badge, a.count, a.name, a.time, a.preview, a.avatar, a.tone + ]), + chat: (state.chat || []).map(message => [message.role, message.time, message.text]), + memory: state.memory, + settle: state.settle, + }; + } else if (view === "ai") { + page = {aiTab: state.aiTab, ai}; + } else if (view === "automation") { + page = {sendMode: state.sendMode, dpi: state.dpi, auto, tray: sys.tray}; + } else if (view === "logs") { + page = { + statusLabel: state.statusLabel, + eventFilter: state.eventFilter, + eventQuery: state.eventQuery, + exceptions: state.exceptions, + logDetail: state.logDetail, + chain: (state.chain || []).map(item => [item.icon, item.title]), + metricModes, + }; + } else { + const { + storage: _storage, + archiveSize: _archiveSize, + logSize: _logSize, + cacheSize: _cacheSize, + noticeTime: _noticeTime, + ...systemStructure + } = sys; + page = {system: systemStructure}; + } + return JSON.stringify({common, page}); +} + +function logsFingerprint(state){ + return JSON.stringify((state.logs || []).map(x => [x.time, x.title, x.detail, x.state])); +} + +function logRowsHtml(events){ + events = (events || []).slice(-5); + if (!events.length){ + return `
当前没有运行记录
`; + } + return events.map(x=>`
${icon(x.icon || "activity")}
${esc(x.title)}${esc(x.detail)}
${esc(x.state || "")}
`).join(""); +} + +function wireDemoArchive(html){ + let rowIndex = 0; + return html + .replace('data-action="toast">' + icon("download") + '导出记录', 'data-action="export-archive">' + icon("download") + '导出记录') + .replace('', '') + .replace('', '') + .replace('', '') + .replace('', '') + .replace('', '') + .replace('', '') + .replace(/
/g, (_match, classes) => `
`) + .replace('', '') + .replace('data-action="logs">复制全部记录', 'data-action="copy-archive">复制全部记录'); +} + +function wireDemoAi(html){ + const activeTab = STATE.aiTab || "基础设置"; + let toolIndex = 0; + return html + .replace('配置已同步', '配置已同步') + .replace('', '') + .replace('', '') + .replace('
', `
${["基础设置","知识与工具","安全策略"].map(name=>``).join("")}
`) + .replace('', '') + .replace('