Files
kefu/wechat_rpa/assets/ui/_patch_console.py
T
2026-08-19 17:35:59 +08:00

352 lines
43 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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=>`<svg class="ico"><use href="#i-${n}"/></svg>`;
window.STATE = window.STATE || { view: "auto", demo: true, ready: false, listening: true };
function esc(value){
return String(value ?? "").replace(/[&<>"']/g, ch => ({
"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"
}[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 `<span class="btn"><i class="led"></i>${esc(wecom)}</span><span class="btn"><i class="led"></i>${esc(ai)}</span>${extra}<button class="btn ${primary}" data-action="toggle-listen">${listenLabel}</button>`;
};
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 `<div class="empty-board"><div><div class="metric-icon green" style="margin:0 auto 12px">${icon("check")}</div><strong style="color:var(--navy);display:block;margin-bottom:6px">${title}</strong><span>${hint}</span></div></div>`;
}
return `<div class="queue-list">${rows.map((p,i)=>{
const active = p.active || p.key === STATE.selectedKey || (!STATE.selectedKey && i===0);
const preview = archive ? (p.preview || "会话已归档") : `<i class="${p.dot || (p.status==="待重试"?"orange": i?"gray":"")}"></i>${esc(p.status || "")}`;
const meta = archive
? `<span>${esc(p.time || "")}</span><span class="badge ${p.badge==="已转人工"?"orange":""}">${esc(p.badge || "AI 已回复")}</span>`
: `<span>${esc(p.wait || "")}</span><span class="bubble-count">${icon("message")} ${p.bubbles || 0}</span>`;
return `<div class="queue-row ${active?"active":""}" data-action="${archive?"select-archive":"select-task"}" data-key="${esc(p.key || "")}"><div class="avatar ${esc(p.tone || "")}">${esc(p.avatar || "客")}</div><div class="q-copy"><strong>${esc(p.name || "")}</strong><span>${preview}</span></div><div class="q-meta">${meta}</div></div>`;
}).join("")}</div>`;
};
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 `<div class="activity-card"><div class="activity-icon ${esc(tone)}">${icon(x.icon || "activity")}</div><time>${esc(x.time || "")}</time><strong>${esc(x.title || "")}</strong><p>${esc(x.detail || "")}</p><span class="activity-state" style="color:${color}">● ${esc(x.state || "")}</span></div>`;
}).join("") : `<div class="empty-board" style="height:82px"><span>暂无执行记录</span></div>`;
const action = more === "复制全部记录" ? "copy-archive" : (more === "查看完整记录" ? "full-log" : "logs");
return `<div class="activity-strip">${cards}<button class="btn blue activity-more" data-action="${action}">${esc(more)} </button></div>`;
};
journey = function(){
if (STATE.demo || !(STATE.journey && STATE.journey.length)) return _journey();
return `<div class="journey">${STATE.journey.map((x,i)=>{
const mode = x.mode || (x.active ? "active" : x.wait ? "wait" : "");
const token = x.done ? "✓" : String(i+1);
return `<div class="j-step ${mode}"><div class="j-dot">${token}</div><div class="j-copy"><strong>${esc(x.title)}</strong><span>${esc(x.status)}</span></div><div class="j-time">${esc(x.time)}</div></div>`;
}).join("")}</div>`;
};
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 `<section class="panel panel-pad task-card"><div class="panel-title"><h2>当前处理任务</h2></div><div class="empty-board" style="height:280px"><div><strong style="color:var(--navy);display:block;margin-bottom:6px">暂无处理中的任务</strong><span>新的客户消息入队后会显示在这里</span></div></div></section>`;
}
const messages = (task.messages || []).map(x=>`<div class="message-line"><time>${esc(x.time || "")}</time><span>${esc(x.text || "")}</span></div>`).join("") || `<div class="message-line"><span>等待读取客户消息</span></div>`;
const answer = task.answer ? `<div class="answer-block">${esc(task.answer).replaceAll("\\n","<br>")}</div>` : `<div class="answer-block" style="color:var(--muted)">尚未生成回复</div>`;
return `<section class="panel panel-pad task-card"><div class="panel-title"><h2>当前处理任务</h2><div class="task-head-meta"><span class="badge">${icon("user")}${esc(task.name || "")}</span><span class="badge">${esc(task.messageCount || "已读取消息")}</span><span class="badge">${esc(task.action || "")}</span></div></div><div class="subhead">复制的聊天内容</div><div class="copy-block">${messages}</div><div class="subhead">AI 回复预览</div>${answer}<div class="answer-meta"><span>${icon("shield")}置信度 <b>${esc(task.confidence || "—")}</b></span><span>${icon("clock")}模型耗时 ${esc(task.latency || "—")}</span></div><div class="task-actions"><button class="btn small danger" data-action="cancel-task" data-key="${esc(task.key || "")}">${icon("stop")}终止本任务</button><button class="btn small orange" data-action="handoff-task" data-key="${esc(task.key || "")}">${icon("user")}转人工</button></div></section>`;
}
function windowStatusCard(){
const stats = STATE.windowStats || [["folder","窗口", STATE.wecomConnected ? "已识别" : "未识别"],["message","当前会话", (STATE.task && STATE.task.name) || "—"],["monitor","DPI", STATE.dpi || "—"],["shield","人工保护", STATE.idleLabel || "—"],["refresh","最小化恢复", "已开启"]];
return `<section class="panel panel-pad window-card"><div class="panel-title"><h2>企业微信窗口状态</h2></div><div class="window-status">${stats.map(x=>`<div class="window-stat">${icon(x[0])}<strong>${esc(x[1])}</strong><span>${esc(x[2])}</span></div>`).join("")}</div></section>`;
}
function queueDetailPanel(){
const task = STATE.task;
if (!task){
return `<section class="panel panel-pad"><div class="panel-title"><h2>任务详情</h2></div><div class="empty-board"><div><strong style="color:var(--navy);display:block;margin-bottom:6px">暂无选中任务</strong><span>选择左侧任务后查看来源消息与处理进度</span></div></div></section>`;
}
const steps = task.steps || [["入队","等待中"],["读取消息","等待中"],["请求 AI","等待中"],["生成回复","等待中"],["回填发送","等待中"],["完成","等待中"]];
const active = Number(task.stepIndex ?? -1);
const process = steps.map((x,i)=>`<div class="process-step ${active<0?"":i<active?"done":i===active?"active":""}"><div class="process-dot">${i<active?"✓":i+1}</div><strong>${esc(x[0])}</strong><span>${esc(x[1])}</span></div>`).join("");
const message = (task.messages && task.messages[0]) || {time:"", text:"队列为空时,选择任务后会在这里显示来源消息。"};
return `<section class="panel panel-pad"><div class="panel-title"><h2>任务详情</h2></div><div class="detail-stats"><div class="detail-stat"><span>客户</span><b>${esc(task.name || "暂无选中任务")}</b></div><div class="detail-stat"><span>会话 ID</span><b>${esc(task.sessionId || "--")} <button class="btn small" data-action="copy-session" data-key="${esc(task.sessionId || "")}" style="height:24px;padding:0 8px">${icon("copy")}</button></b></div><div class="detail-stat"><span>已等待</span><b>${esc(task.wait || "--")}</b></div><div class="detail-stat"><span>重试</span><b>${esc(task.retries || "0 次")}</b></div></div><div class="subhead">来源消息</div><div class="copy-block"><div class="message-line"><time>${esc(message.time || "")}</time><span>${esc(message.text || "")}</span></div></div><div class="process-title">${icon("sparkles")} ${esc(task.action || "等待新任务")}</div><div class="process-line">${process}</div><div class="task-actions" style="margin-top:17px;justify-content:center"><button class="btn danger" data-action="cancel-task" data-key="${esc(task.key || "")}">× 取消任务</button><button class="btn blue" data-action="retry-task" data-key="${esc(task.key || "")}">${icon("refresh")}立即重试</button><button class="btn orange" data-action="handoff-task" data-key="${esc(task.key || "")}">${icon("users")}转人工</button></div></section>`;
}
function schedulePanel(){
const s = (STATE.task && STATE.task.schedule) || {priority:"—", eta:"—", protect:"—", retry:"—"};
return `<section class="panel panel-pad"><div class="panel-title"><h2>调度信息</h2></div><div class="stack"><div class="side-stat"><div class="metric-icon">${icon("flag")}</div><div><span>优先级</span><b>${esc(s.priority)}</b></div></div><div class="side-stat"><div class="metric-icon">${icon("clock")}</div><div><span>预计完成</span><b>${esc(s.eta)}</b></div></div><div class="side-stat"><div class="metric-icon green">${icon("shield")}</div><div><span>人工保护</span><b style="color:${s.protect==="已开启"?"var(--green)":"inherit"}">${esc(s.protect)}</b></div></div><div class="side-stat"><div class="metric-icon">${icon("refresh")}</div><div><span>下次重试</span><b>${esc(s.retry)}</b></div></div></div></section>`;
}
autoView = function(){
if (STATE.demo) return _autoView();
const count = (STATE.people || []).length;
return `<div class="screen">${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`<button class="btn blue" data-action="start-listen">${icon("send")}自动发送</button>`,"danger"))}${liveRibbonAuto()}<div class="auto-grid"><section class="panel panel-pad queue-card"><div class="panel-title"><h2>任务队列</h2><div class="toolbar"><span class="badge">${count}</span>${icon("filter")}${icon("sort")}</div></div>${queueList()}<div class="queue-footer"><span>共 ${count} 个会话</span><button class="round-btn" data-action="refresh">${icon("refresh")}</button></div></section>${currentTaskCard()}<section class="panel panel-pad journey-card"><div class="panel-title"><h2>自动化执行状态</h2><small>${esc(STATE.journeyTotal || "")}</small></div>${journey()}</section>${windowStatusCard()}</div><section class="panel activity-panel"><div class="panel-title"><h2>实时事件</h2></div>${activities()}</section></div>`;
};
queueView = function(){
if (STATE.demo) return _queueView();
const count = (STATE.people || []).length;
const filter = STATE.queueFilter || "全部";
const pills = ["全部","处理中","等待中","待重试"].map(name=>`<button class="filter-pill ${filter===name?"active":""}" data-action="queue-filter" data-key="${name}">${name}</button>`).join("");
return `<div class="screen">${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`<button class="btn blue" data-action="start-listen">${icon("send")}自动发送</button>`,"danger"))}${liveRibbonQueue()}<div class="queue-view-grid"><section class="panel panel-pad"><div class="panel-title"><h2>全部任务</h2></div><div class="queue-filters">${pills}</div>${queueList()}<div class="queue-footer"><span>共 ${count} 个任务</span><button class="round-btn" data-action="refresh">${icon("refresh")}</button></div></section>${queueDetailPanel()}${schedulePanel()}</div><section class="panel activity-panel"><div class="panel-title"><h2>任务执行记录</h2></div>${activities()}</section></div>`;
};
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=>`<div class="chat-line ${x.role==="assistant"?"ai":""}"><span class="mini-avatar">${icon(x.role==="assistant"?"bot":"user")}</span><div class="chat-bubble"><time>${esc(x.time || "")}</time>${esc(x.text || "")}</div></div>`).join("") : `<div class="empty-board" style="height:220px"><span>选择会话后显示聊天记录</span></div>`;
const tags = (STATE.memory && STATE.memory.tags || []).map((t,i)=>`<span class="tag ${i===1?"violet":i===2?"blue":""}">${esc(t)}</span>`).join("") || `<span class="tag">暂无标签</span>`;
const settle = (STATE.settle || []).map(x=>`<div class="settle-row"><div class="settle-icon">${x.iconHtml || icon("clipboard")}</div><div><strong>${esc(x.title)}</strong><small>${esc(x.hint)}</small></div><b>${esc(x.value)}</b></div>`).join("");
const filter = STATE.archiveFilter || "全部会话";
return `<div class="screen">${head("会话归档","保存客户上下文、自动回复与回访线索",`<span class="btn"><i class="led"></i>${esc(STATE.wecomLabel || "企业微信")}</span><span class="btn"><i class="led"></i>${esc(STATE.aiLabel || "AI 服务")}</span><button class="btn blue" data-action="export-archive">${icon("download")}导出记录</button>`)}<section class="panel filter-row"><label class="search">${icon("search")}<input id="archive-search" placeholder="搜索客户或会话 ID" value="${esc(STATE.archiveQuery || "")}"></label>${["全部会话","今日","AI 已回复","已转人工"].map(name=>`<button class="filter-pill ${filter.startsWith(name)?"active":""}" data-action="archive-filter" data-key="${name}">${name}${name==="全部会话"?"⌄":""}</button>`).join("")}<button class="filter-pill" data-action="archive-date">${esc(STATE.archiveDate || "日期")} □</button></section><div class="archive-grid"><section class="panel panel-pad"><div class="panel-title"><h2>最近会话</h2></div>${queueList(true)}<div class="queue-footer"><span>共 ${rows.length} 个会话</span><button class="round-btn" data-action="refresh">${icon("refresh")}</button></div></section><section class="panel panel-pad"><div class="panel-title"><h2>会话记录</h2></div><div class="toolbar" style="margin-bottom:12px"><span class="badge">${icon("user")}${esc(selected.name || "未选择")}</span><span class="badge">${esc(selected.count || "0")} 条消息</span><span class="badge">${esc(selected.badge || "")}</span><span class="badge">${icon("clock")}${esc(selected.time || "")}</span></div><div class="chat">${chatHtml}</div></section><div class="stack"><section class="panel panel-pad memory-card"><div class="panel-title"><h2>客户记忆</h2></div><div class="toolbar">${tags}</div><div class="memory-copy">${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。")}</div><span class="badge" style="margin-top:10px">${icon("clock")} 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮</span></section><section class="panel panel-pad settle-card"><div class="panel-title"><h2>业务沉淀</h2></div><div class="stack" style="gap:8px">${settle || "<div class='empty-board' style='height:120px'><span>暂无业务沉淀</span></div>"}</div></section></div></div><section class="panel panel-pad archive-bottom"><div class="panel-title"><h2>最近更新</h2></div>${activities("复制全部记录")}</section></div>`;
};
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 `<div class="screen">${head("AI 设置","配置模型、客服人格、知识库与工具能力",`<span class="btn"><i class="led"></i>配置已同步</span><button class="btn" data-action="test-ai">${icon("play")} 测试 AI</button><button class="btn primary" data-action="save-ai">${icon("send")}保存并发布</button>`)}<div class="tabs"><button class="tab active">基础设置</button><button class="tab">知识与工具</button><button class="tab">安全策略</button></div><div class="ai-top"><section class="panel panel-pad"><div class="panel-title"><h2>客服人格</h2></div><div class="ai-profile"><div class="ai-ball">AI</div><div class="stack"><div class="field"><label>客服名称:</label><input class="input" id="ai-name" value="${esc(ai.name || "贴心管家")}" maxlength="40"></div><div class="toolbar"><b style="font-size:13px">服务风格:</b><span class="badge green">温和</span><span class="badge">专业</span><span class="badge" style="color:#7657e7;background:rgba(125,91,236,.08)">简洁</span></div><div class="field"><label>人格提示词:</label><textarea class="textarea" id="ai-prompt" readonly>${esc(ai.prompt || "")}</textarea></div></div><div class="preview-box"><b>${icon("sparkles")} 实时预览</b><p>您好!我是甄养堂${esc(ai.name || "贴心管家")}<br>很高兴为您服务 😊</p><p>请问有什么可以帮您的吗?</p></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("brain")} 模型引擎</h2></div><div class="field"><label>当前模型</label><button class="select model" data-action="pick-model" style="text-align:left">${icon("sparkles")} ${esc(ai.model || "云端模型")}</button></div><div class="form-grid" style="margin-top:17px"><div class="field"><label>备用模型</label><div class="select">${esc(ai.backup || "由云端策略管理")}</div></div><div class="field"><label>上下文</label><div class="select">${esc(ai.context || 8)} 轮</div></div><div class="field"><label>最大回复</label><div class="select">${esc(ai.maxTokens || 500)} tokens</div></div></div><div class="form-grid two" style="margin-top:18px"><div class="field"><label>温度 ${esc(ai.temperature || "0.35")}</label><input class="range" id="ai-temp" type="range" min="0" max="200" value="${Math.round(Number(ai.temperature || 0.35)*100)}"></div><div class="field"><label>回复长度 中等</label><input class="range" type="range" value="55"></div></div></section></div><div class="ai-bottom"><section class="panel panel-pad"><div class="panel-title"><h2>${icon("tray")} 企业知识库</h2></div><div class="knowledge"><div class="donut"><strong>${esc(ai.knowledge || "0")}<small>个知识片段</small></strong></div><div class="stack"><span class="badge green">● 已同步</span><span class="badge green">✓ 已连接</span><button class="btn small primary" data-action="sync-knowledge">${icon("refresh")}立即同步</button><button class="btn small" data-action="open-data">${icon("folder")}管理知识</button></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("package")} MCP 工具</h2><small>单次最多调用 ${esc(ai.mcpRounds || 5)} 个工具 ⓘ</small></div><div class="tool-grid">${names.map((n,i)=>`<div class="tool-card"><div class="tool-icon">${icon(toolIcons[i])}</div><b>${n}</b><div class="row"><small>${toolsOn[i]?"已启用":"未启用"}</small><div class="switch ${toolsOn[i]?"on":""}" data-action="toggle-tool" data-key="${i}"></div></div></div>`).join("")}</div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("shield")} 安全边界</h2></div><div class="stack"><div class="setting-line"><span>${icon("shield")} 不做医疗诊断</span></div><div class="setting-line"><span>${icon("pill")} 不修改用药方案</span></div><div class="setting-line"><span>${icon("users")} 高风险转人工</span></div></div><div class="row" style="margin-top:10px;justify-content:flex-end"><span>当前模式:</span><span class="badge green">${icon("shield")}严格模式</span></div></section></div><section class="panel panel-pad config-strip"><div class="panel-title"><h2>配置状态</h2></div>${statusCells([["cloud","云端模型正常","最后检查:刚刚"],["folder","知识库已同步",`${ai.knowledge || 0} 个知识片段`],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])}</section></div>`;
};
automationView = function(){
if (STATE.demo) return _automationView();
const a = STATE.automation || {};
const autoOn = (STATE.sendMode || "auto") === "auto";
return `<div class="screen">${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`<span class="btn"><i class="led"></i>${esc(a.wecom || "企业微信未识别")}</span><span class="btn"><i class="led"></i>规则已生效</span><button class="btn primary" data-action="save-automation">${icon("save")}保存设置</button>`)}<div class="tabs"><button class="tab ${autoOn?"active":""}" data-action="send-mode" data-key="auto">${icon("send")} 自动发送</button><button class="tab ${autoOn?"":"active"}" data-action="send-mode" data-key="review">${icon("users")} 审核后发送</button></div><div class="automation-top"><section class="panel panel-pad"><div class="panel-title"><h2>监听与发送策略</h2></div><span class="badge green">${icon("activity")} ● 规则运行中</span><div class="auto-flow">${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`<div class="flow-step"><div class="flow-circle">${icon(x[0])}</div><strong>${x[1]}</strong></div>`).join("")}</div><div class="form-grid"><div class="flow-pill">${icon("clock")}扫描间隔 <b style="color:#2d65f0">${esc(a.poll || "2")} 秒</b></div><div class="flow-pill">${icon("hourglass")} 连续消息等待 <b style="color:#2d65f0">${esc(a.batch || "20")} 秒</b></div><div class="flow-pill">${icon("clock")}发送前等待 <b style="color:#2d65f0">${esc(a.delay || "1")} 秒</b></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>企业微信窗口</h2></div><div class="wechat-graphic"><div class="fake-window"><div class="fake-check">✓</div></div><div class="stack"><div class="row"><span>${icon("clipboard")} 主窗口:</span><b style="color:var(--green)">● ${esc(a.wecom || "未识别")}</b></div><div class="row"><span>${icon("user")} 当前账号:</span><b>${esc(a.account || "—")}</b></div><div class="row"><span>${icon("globe")} DPI</span><b>${esc(STATE.dpi || "—")}</b></div><div class="row"><span>${icon("monitor")} 最小化恢复:</span><b style="color:var(--green)">● 已开启</b></div></div></div><div class="task-actions"><button class="btn small blue" data-action="redetect">${icon("refresh")}重新检测</button></div></section></div><div class="automation-mid"><section class="panel panel-pad"><div class="panel-title"><h2>人机共存</h2></div><div class="setting-line"><span>启用人工操作保护</span><div class="switch ${a.mouseProtect?"on":""}" data-action="toggle-idle"></div></div><div style="text-align:center;margin-top:8px">鼠标静止 <b style="color:#2562ed">${esc(a.idle || "5")} 秒</b> 后恢复</div><div class="mouse-orb"><span style="color:#315fec">${icon("bot")}<br><small>AI 运行中</small></span><div class="mouse-pointer">${icon("cursor")}</div><span style="color:var(--orange)">${icon("pause")}<br><small>检测到人工操作<br>AI 暂停</small></span></div></section><section class="panel panel-pad"><div class="panel-title"><h2>消息处理</h2></div><div class="stack"><div class="setting-line"><span>✓ 同一客户连续消息自动合并</span></div><div class="setting-line"><span>✓ 保留最近 ${esc(a.context || 8)} 轮上下文</span><span class="badge">${esc(a.context || 8)} 轮</span></div><div class="setting-line"><span>✓ 一次只处理 1 个会话</span><span class="badge">1 个</span></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>异常与兜底</h2></div><div class="stack"><div class="setting-line"><span style="color:var(--orange)">${icon("clock")}</span><span style="flex:1">AI 超时:重试 2 次</span><span class="badge">2 次</span></div><div class="setting-line"><span style="color:var(--orange)">${icon("alert")}</span><span style="flex:1">发送失败:自动重试</span><span class="badge green">已开启</span></div><div class="setting-line"><span style="color:#3c69f4">${icon("message")}</span><span style="flex:1">固定回复:${esc(a.reply || "在的,请稍等")}</span><span class="badge">编辑</span></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>高风险转人工</h2><span class="badge green">● 已开启</span></div><div class="risk-tags"><span class="risk-tag">${icon("shield")} 诊断</span><span class="risk-tag">${icon("package")} 用药调整</span><span class="risk-tag">${icon("coins")} 投诉退款</span></div><button class="btn small blue" style="margin:11px auto 0;display:flex" data-action="security-rules">${icon("settings")}规则管理</button></section></div><section class="panel panel-pad check-strip"><div class="panel-title"><h2>启动前检查</h2></div>${statusCells([["check","企业微信已登录", STATE.wecomConnected ? "检查完成" : "等待连接"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准", STATE.wecomConnected ? "检查完成" : "待校准"],["shield","规则配置有效","检查完成"]])}</section></div>`;
};
logsView = function(){
if (STATE.demo) return _logsView();
const events = STATE.logs || [];
const list = events.length ? events.map(x=>`<div class="event-row"><div class="event-node">✓</div><time>${esc(x.time)} ●</time><div class="event-kind" style="color:${x.tone==="orange"?"var(--orange)":x.tone==="green"?"var(--green)":"var(--blue)"}">${icon(x.icon || "activity")}</div><div class="event-copy"><strong>${esc(x.title)}</strong><span>${esc(x.detail)}</span></div><span class="badge ${esc(x.tone || "")}">${esc(x.state || "")}</span></div>`).join("") : `<div class="soft" style="height:170px;display:grid;place-items:center;color:var(--muted)">当前没有运行记录</div>`;
const ex = STATE.exceptions || {};
const detail = STATE.logDetail || {title:"等待事件", text:"开始监听后,这里会显示最近一条运行事件。", tags:[]};
return `<div class="screen">${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`<button class="btn" data-action="refresh">${icon("refresh")}实时刷新</button><button class="btn" data-action="export-logs">${icon("download")}导出日志</button><button class="btn danger" data-action="clear-logs">${icon("tray")} 清空日志</button><span class="btn" style="color:var(--green)"><span class="orb" style="width:31px;height:31px"></span>${esc(STATE.statusLabel || "监控中")}</span>`)}${liveRibbonLogs()}<div class="logs-grid"><section class="panel panel-pad event-flow"><div class="panel-title"><h2>实时事件流</h2></div><div class="toolbar" style="margin-bottom:7px">${["全部","连接","识别","AI","发送","警告"].map(n=>`<button class="filter-pill ${(STATE.eventFilter||"全部")===n?"active":""}" style="height:34px;padding:0 17px" data-action="event-filter" data-key="${n}">${n}</button>`).join("")}<label class="search" style="height:34px;width:180px;margin-left:auto">${icon("search")}<input id="event-search" placeholder="搜索事件内容" value="${esc(STATE.eventQuery || "")}"></label></div><div class="event-list" id="log-list">${list}</div></section><section class="panel panel-pad"><div class="panel-title"><h2>系统健康</h2></div><div class="health-rings">${[["企业微信连接", STATE.wecomConnected ? "正常" : "待连接"],["AI 服务","正常"],["窗口识别", STATE.wecomConnected ? "正常" : "待监听"]].map(x=>`<div><div class="health-ring">${icon("check")}</div><strong>${x[0]}</strong><span>${x[1]}</span></div>`).join("")}</div><div class="btn small" style="display:flex;margin:10px auto 0;width:350px">${icon("clock")}运行时长 <b style="color:#2b62f2">${esc(STATE.runtime || "00:00:00")}</b></div></section><section class="panel panel-pad"><div class="panel-title"><h2>当前回复链路</h2></div><div class="chain">${[["activity","扫描未读",""],["message","读取消息",""],["brain","请求 AI",""],["send","回填发送",""]].map(x=>`<div class="chain-step"><div class="flow-circle">${icon(x[0])}</div><strong>${x[1]}</strong><span>${x[2]}</span></div>`).join("")}</div></section></div><div class="log-mid"><section class="panel panel-pad" style="background:linear-gradient(130deg,rgba(255,253,250,.82),rgba(255,245,239,.72))"><div class="panel-title"><h2>事件详情</h2></div><div class="row" style="justify-content:flex-start;gap:16px"><div class="metric-icon orange">${icon("user")}</div><div><b>${esc(detail.title)}</b><p style="margin:4px 0 7px;font-size:12px">${esc(detail.text)}</p><div class="toolbar">${(detail.tags||[]).map(t=>`<span class="badge">${esc(t)}</span>`).join("")}</div></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>异常追踪</h2></div><div class="exception-grid"><div class="exception"><div class="metric-icon red">${icon("alert")}</div><div>发送失败<b>${esc(ex.sendFail || 0)}</b></div></div><div class="exception"><div class="metric-icon" style="color:#785cf1">${icon("clock")}</div><div>模型超时<b>${esc(ex.timeout || 0)}</b></div></div><div class="exception"><div class="metric-icon orange">${icon("monitor")}</div><div>窗口丢失<b>${esc(ex.windowLost || 0)}</b></div></div><button class="btn blue" data-action="event-filter" data-key="警告">查看异常 </button></div></section></div><section class="panel activity-panel recent-activity"><div class="panel-title"><h2>最近活动</h2></div>${activities("查看完整记录")}</section></div>`;
};
systemView = function(){
if (STATE.demo) return _systemView();
const s = STATE.system || {};
const on = k => s[k] ? "on" : "";
return `<div class="screen">${head("系统设置","管理软件启动、数据存储、通知与系统维护",`<span class="btn">当前版本 v${esc(s.version || "2.0")} ${icon("info")}</span><button class="btn" data-action="check-update">${icon("refresh")}检查更新</button><button class="btn primary" data-action="save-system">${icon("save")}保存设置</button>`)}<div class="system-top"><section class="panel panel-pad"><div class="panel-title"><h2>基础设置</h2></div><div class="stack" style="gap:8px"><div class="system-line">${icon("power")}<span>开机自动启动</span><div class="switch ${on("autoLaunch")}" data-action="sys-toggle" data-key="auto_launch"></div></div><div class="system-line">${icon("activity")}<span>启动后自动监听</span><div class="switch ${on("autoMonitor")}" data-action="sys-toggle" data-key="auto_monitor"></div></div><div class="system-line">${icon("tray")}<span>最小化到系统托盘</span><div class="switch ${on("tray")}" data-action="sys-toggle" data-key="minimize_on_close"></div></div><div class="system-line">${icon("log")}<span>关闭窗口时保持后台运行</span><div class="switch ${on("background")}" data-action="sys-toggle" data-key="keep_background"></div></div><div class="system-line">${icon("globe")}<span>界面语言: 简体中文</span>${icon("chevron")}</div><div class="system-line">${icon("monitor")}<span>缩放比例: 自动</span>${icon("chevron")}</div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>数据与存储</h2></div><div class="storage-grid"><div class="donut" style="width:145px;height:145px"><strong>${esc(s.storage || "—")}<small>已使用</small></strong></div><div class="stack"><div class="row"><span>● 会话归档</span><span>${esc(s.archiveSize || "—")}</span></div><div class="row"><span>● 运行日志</span><span>${esc(s.logSize || "—")}</span></div><div class="row"><span>● 缓存数据</span><span>${esc(s.cacheSize || "—")}</span></div></div></div><div class="row" style="justify-content:center;margin-top:7px"><span>数据保留</span><span class="btn small">${esc(s.retention || 90)} 天</span></div><div class="storage-actions"><button class="btn small blue" data-action="open-data">${icon("folder")}打开目录</button><button class="btn small blue" data-action="backup">${icon("cloud")}立即备份</button><button class="btn small blue" data-action="clear-cache">${icon("wifi")} 清理缓存</button></div></section><section class="panel panel-pad"><div class="panel-title"><h2>通知提醒</h2></div><div class="stack" style="gap:8px"><div class="system-line">${icon("send")}<span>发送失败提醒</span><div class="switch ${on("notifyFail")}" data-action="sys-toggle" data-key="notify_send_failed"></div></div><div class="system-line">${icon("hourglass")}<span>模型超时提醒</span><div class="switch ${on("notifyTimeout")}" data-action="sys-toggle" data-key="notify_timeout"></div></div><div class="system-line">${icon("user")}<span>需要人工跟进</span><div class="switch ${on("notifyManual")}" data-action="sys-toggle" data-key="notify_manual"></div></div><div class="system-line">${icon("wifi")}<span>企业微信连接中断</span><div class="switch ${on("notifyDisconnect")}" data-action="sys-toggle" data-key="notify_disconnect"></div></div><div class="notice-preview"><div class="metric-icon">${icon("bell")}</div><div><b>发送失败提醒</b><small style="display:block">消息发送失败,已加入重试队列</small></div><time>—<br><span style="color:#3772ff">●</span></time></div></div></section></div><div class="system-mid"><section class="panel panel-pad"><div class="panel-title"><h2>安全与隐私</h2></div><div class="stack" style="gap:6px"><div class="system-line">${icon("shield")}<span>敏感信息脱敏:</span><b>已开启 ●</b></div><div class="system-line">${icon("lock")}<span>本地数据加密:</span><b>已开启 ●</b></div><div class="system-line">${icon("lock")}<span>日志隐藏聊天正文:</span><b>已开启 ●</b></div><div class="system-line">${icon("log")}<span>退出时清理剪贴板:</span><b>已开启 ●</b></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>网络与服务</h2></div><div class="stack" style="gap:7px"><div class="system-line">${icon("cloud")}<span>云端 AI</span><b>已连接 ●</b></div><div class="system-line">${icon("folder")}<span>企业知识库:</span><b>已连接 ●</b></div><div class="system-line">${icon("wifi")}<span>网络延迟:</span><b>— ●</b></div></div><button class="btn small blue" style="margin:10px auto 0;display:flex" data-action="test-ai">${icon("wifi")} 连接测试</button></section><section class="panel panel-pad"><div class="panel-title"><h2>维护与诊断</h2></div><div class="maintenance"><div class="maint-card" data-action="check-update"><div class="metric-icon">${icon("refresh")}</div><div><strong>检查更新</strong><small>已是最新版本</small></div></div><div class="maint-card" data-action="export-logs"><div class="metric-icon">${icon("clipboard")}</div><div><strong>导出诊断</strong><small>导出系统诊断报告</small></div></div><div class="maint-card" data-action="reset-settings"><div class="metric-icon">${icon("refresh")}</div><div><strong>恢复默认设置</strong><small>重置所有设置项</small></div></div><div class="maint-card" data-action="license"><div class="metric-icon">${icon("shield")}</div><div><strong>查看许可信息</strong><small>查看软件许可详情</small></div></div></div></section></div><section class="panel panel-pad system-status"><div class="panel-title"><h2>系统状态</h2></div>${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","数据目录正常","本地数据可写"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务", STATE.listening ? "运行中" : "已停止"]])}</section></div>`;
};
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)