# -*- 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)