Files
kefu/wechat_rpa/assets/ui/console-app.js
T
2026-08-19 17:35:59 +08:00

1006 lines
119 KiB
JavaScript
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.
const icon=n=>`<svg class="ico"><use href="#i-${n}"/></svg>`;
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 => ({
"&":"&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;
}
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 = '<i class="led"></i>有未保存更改';
}
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)=>`<header class="page-head"><div><h1>${title}</h1><p>${sub}</p></div><div class="head-actions">${actions}</div></header>`;
var connected=(extra="",primary="")=>`<span class="btn"><i class="led"></i>企业微信已连接</span><span class="btn"><i class="led"></i>AI 服务正常</span>${extra}<button class="btn ${primary}" data-action="toggle-listen">Ⅱ&nbsp; 暂停监听</button>`;
var ribbon=items=>`<section class="panel wave-ribbon">${items.map(x=>`<div class="metric"><div class="metric-icon ${x[3]||""}">${icon(x[2])}</div><div class="metric-copy"><span>${x[0]}</span><strong class="${x[4]||x[3]||""}"${x[5]?` data-live-metric="${esc(x[5])}"`:""}>${x[1]}</strong></div></div>`).join("")}</section>`;
var queueList=(archive=false)=>`<div class="queue-list">${people.map((p,i)=>`<div class="queue-row ${i===0?"active":""}"><div class="avatar ${p[4]}">${p[0]}</div><div class="q-copy"><strong>${p[1]}</strong><span>${archive?(i===0?"请问你们的灵芝孢子粉怎么服用…":i===1?"每天吃几次比较合适?":i===2?"有不良反应吗?":"会话已归档"):`<i class="${i===3?"orange":i?"gray":""}"></i>${p[2]}`}</span></div><div class="q-meta"><span>${archive?["16:31","16:18","15:42","14:20","13:35"][i]:p[3]}</span>${archive?`<span class="badge ${i===2?"orange":""}">${i===2?"已转人工":"AI 已回复"}</span>`:`<span class="bubble-count">${icon("message")} ${i===3?1:0}</span>`}</div></div>`).join("")}</div>`;
var activities=(more="查看完整日志")=>`<div class="activity-strip">${[["16:33:27","扫描未读","检测到客户未读消息","activity","成功",""],["16:33:28","提取聊天记录","已提取最近 6 条聊天记录","log","成功",""],["16:33:29","AI 服务","正在请求 AI 生成回复","brain","进行中","blue"],["16:33:31","人工保护","检测到人工鼠标操作,暂停 5 秒","user","警告","orange"]].map(x=>`<div class="activity-card"><div class="activity-icon ${x[5]}">${icon(x[3])}</div><time>${x[0]}</time><strong>${x[1]}</strong><p>${x[2]}</p><span class="activity-state" style="color:${x[5]==="orange"?"var(--orange)":x[5]==="blue"?"var(--blue)":"var(--green)"}">● ${x[4]}</span></div>`).join("")}<button class="btn blue activity-more" data-action="logs">${more} </button></div>`;
var journey=()=>`<div class="journey">${[["扫描未读","已完成","0.3s"],["提取聊天记录","已完成","1.2s"],["合并连续消息","已完成","0.4s"],["发送至 AI","已完成","0.6s"],["生成回复","进行中","2.4s"],["回填企业微信","等待","—"],["自动发送与归档","等待","—"]].map((x,i)=>`<div class="j-step ${i===4?"active":i>4?"wait":""}"><div class="j-dot">${i<4?"✓":i+1}</div><div class="j-copy"><strong>${x[0]}</strong><span>${x[1]}</span></div><div class="j-time">${x[2]}</div></div>`).join("")}</div>`;
var statusCells=items=>`<div class="status-cells">${items.map(x=>`<div class="status-cell"><div class="metric-icon">${icon(x[0])}</div><div><strong>${x[1]}</strong><span>${x[2]}</span></div></div>`).join("")}</div>`;
function autoView(){return `<div class="screen">${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`<span class="btn blue">${icon("send")}自动发送</span>`,"danger"))}${ribbon([["监听状态","运行中","activity","green"],["排队","3","users"],["今日回复","128","message"],["失败","1","alert","red"]])}<div class="auto-grid"><section class="panel panel-pad queue-card"><div class="panel-title"><h2>任务队列</h2><div class="toolbar"><span class="badge">3</span>${icon("filter")}${icon("sort")}</div></div>${queueList()}<div class="queue-footer"><span>共 5 个会话</span><button class="round-btn" data-action="toast">${icon("refresh")}</button></div></section><section class="panel panel-pad task-card"><div class="panel-title"><h2>当前处理任务</h2><div class="task-head-meta"><span class="badge">${icon("user")}高瑞@微信</span><span class="badge">已读取 6 条消息</span><span class="badge">AI 生成中</span></div></div><div class="subhead">复制的聊天内容</div><div class="copy-block"><div class="message-line"><time>16:32:11</time><span>请问你们的灵芝孢子粉怎么服用?</span></div><div class="message-line"><time>16:32:18</time><span>每天吃几次比较合适?</span></div><div class="message-line"><time>16:32:26</time><span>有什么注意事项吗?</span></div></div><div class="subhead">AI 回复预览</div><div class="answer-block">您好!灵芝孢子粉建议每日 1–2 次,每次 2–3 克,温水冲服,饭前半小时或睡前服用效果更佳。<br>如有特殊体质或正在服药,建议咨询专业医生后再服用哦。</div><div class="answer-meta"><span>${icon("shield")}置信度 <b>96%</b></span><span>${icon("clock")}模型耗时 2.4s</span></div><div class="task-actions"><button class="btn small danger" data-action="toast">${icon("stop")}终止本任务</button><button class="btn small orange" data-action="toast">${icon("user")}转人工</button></div></section><section class="panel panel-pad journey-card"><div class="panel-title"><h2>自动化执行状态</h2><small>总耗时 5.01s</small></div>${journey()}</section><section class="panel panel-pad window-card"><div class="panel-title"><h2>企业微信窗口状态</h2></div><div class="window-status">${[["folder","窗口","已识别"],["message","当前会话","高瑞@微信"],["monitor","DPI","200%"],["shield","人工保护","5 秒"],["refresh","最小化恢复","已开启"]].map(x=>`<div class="window-stat">${icon(x[0])}<strong>${x[1]}</strong><span>${x[2]}</span></div>`).join("")}</div></section></div><section class="panel activity-panel"><div class="panel-title"><h2>实时事件</h2></div>${activities()}</section></div>`}
function queueView(){return `<div class="screen">${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`<span class="btn blue">${icon("send")}自动发送</span>`,"danger"))}${ribbon([["处理中","1","check","","blue"],["等待中","3","clock"],["待重试","1","refresh","orange"],["平均等待","12s","clock","green"]])}<div class="queue-view-grid"><section class="panel panel-pad"><div class="panel-title"><h2>全部任务</h2></div><div class="queue-filters"><button class="filter-pill active">全部</button><button class="filter-pill">处理中</button><button class="filter-pill">等待中</button><button class="filter-pill">待重试</button></div>${queueList()}<div class="queue-footer"><span>共 5 个任务</span>${icon("refresh")}</div></section><section class="panel panel-pad"><div class="panel-title"><h2>任务详情</h2></div><div class="detail-stats"><div class="detail-stat"><span>客户</span><b>高瑞@微信</b></div><div class="detail-stat"><span>会话 ID</span><b>7bc26546… ${icon("copy")}</b></div><div class="detail-stat"><span>已等待</span><b>12 秒</b></div><div class="detail-stat"><span>重试</span><b>0 次</b></div></div><div class="subhead">来源消息</div><div class="copy-block"><div class="message-line"><time>16:32:11</time><span>请问你们的灵芝孢子粉怎么服用?</span></div></div><div class="process-title">✦ AI 生成回复中</div><div class="process-line">${[["入队","16:32:11"],["读取消息","16:32:11"],["请求 AI","16:32:12"],["生成回复","进行中"],["回填发送","等待中"],["完成","等待中"]].map((x,i)=>`<div class="process-step ${i<3?"done":i===3?"active":""}"><div class="process-dot">${i<3?"✓":i+1}</div><strong>${x[0]}</strong><span>${x[1]}</span></div>`).join("")}</div><div class="task-actions" style="margin-top:17px;justify-content:center"><button class="btn danger" data-action="toast">× 取消任务</button><button class="btn blue" data-action="toast">${icon("refresh")}立即重试</button><button class="btn orange" data-action="toast">${icon("users")}转人工</button></div></section><section class="panel panel-pad"><div class="panel-title"><h2>调度信息</h2></div><div class="stack"><div class="side-stat"><div class="metric-icon">⚑</div><div><span>优先级</span><b>普通</b></div></div><div class="side-stat"><div class="metric-icon">${icon("clock")}</div><div><span>预计完成</span><b>3 秒</b></div></div><div class="side-stat"><div class="metric-icon green">${icon("shield")}</div><div><span>人工保护</span><b style="color:var(--green)">已开启</b></div></div><div class="side-stat"><div class="metric-icon">${icon("refresh")}</div><div><span>下次重试</span><b>—</b></div></div></div></section></div><section class="panel activity-panel"><div class="panel-title"><h2>任务执行记录</h2></div>${activities()}</section></div>`}
function archiveView(){return `<div class="screen">${head("会话归档","保存客户上下文、自动回复与回访线索",`<span class="btn"><i class="led"></i>企业微信已连接</span><span class="btn"><i class="led"></i>AI 服务正常</span><button class="btn blue" data-action="toast">${icon("download")}导出记录</button>`)}<section class="panel filter-row"><label class="search">${icon("search")}<input placeholder="搜索客户或会话 ID"></label><button class="filter-pill active">全部会话⌄</button><button class="filter-pill">今日</button><button class="filter-pill">AI 已回复</button><button class="filter-pill">已转人工</button><button class="filter-pill">2026-08-18 □</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>共 21 个会话</span><button class="round-btn">${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")}高瑞@微信</span><span class="badge">6 条消息</span><span class="badge">AI 已回复</span><span class="badge">${icon("clock")}16:31 更新</span></div><div class="chat"><div class="chat-line"><span class="mini-avatar">${icon("user")}</span><div class="chat-bubble"><time>16:32:11</time>请问你们的灵芝孢子粉怎么服用?</div></div><div class="chat-line ai"><span class="mini-avatar">${icon("bot")}</span><div class="chat-bubble"><time>16:32:18</time>每天吃几次比较合适?</div></div><div class="chat-line"><span class="mini-avatar">${icon("user")}</span><div class="chat-bubble"><time>16:32:26</time>有什么注意事项吗?</div></div><div class="chat-line ai"><span class="mini-avatar">${icon("bot")}</span><div class="chat-bubble"><time>16:32:42</time>温水冲服,饭前半小时或睡前服用效果更佳。<br>如有特殊体质或正在服药,建议咨询专业医生后再服用哦。</div></div><div class="chat-line"><span class="mini-avatar">${icon("user")}</span><div class="chat-bubble"><time>16:33:05</time>好的,谢谢!</div></div><div class="chat-line ai"><span class="mini-avatar">${icon("bot")}</span><div class="chat-bubble"><time>16:33:12</time>不客气!如有其他问题,随时为您服务~</div></div></div></section><div class="stack"><section class="panel panel-pad memory-card"><div class="panel-title"><h2>客户记忆</h2></div><div class="toolbar"><span class="tag">饮食控制</span><span class="tag violet">体重管理</span><span class="tag blue">长期回访</span></div><div class="memory-copy">客户关注灵芝孢子粉的服用方法与注意事项,希望优化服用时间与剂量,同时注重产品安全性与体质适配。</div><span class="badge" style="margin-top:10px">◔ 上下文 8 轮</span></section><section class="panel panel-pad settle-card"><div class="panel-title"><h2>业务沉淀</h2></div><div class="stack" style="gap:8px"><div class="settle-row"><div class="settle-icon">▣</div><div><strong>挂号登记</strong><small>已登记潜在客户</small></div><b>1 次 ✓</b></div><div class="settle-row"><div class="settle-icon" style="background:linear-gradient(145deg,#9d7cff,#6e5be5)">☎</div><div><strong>回访线索</strong><small>创建回访计划</small></div><b>1 条 ✓</b></div><div class="settle-row"><div class="settle-icon" style="background:linear-gradient(145deg,#ffc65a,#ff941f)">${icon("user")}</div><div><strong>人工跟进</strong><small>未分配客服</small></div><b>0 次 ·</b></div></div></section></div></div><section class="panel panel-pad archive-bottom"><div class="panel-title"><h2>最近更新</h2></div>${activities("复制全部记录")}</section></div>`}
function aiView(){return `<div class="screen">${head("AI 设置","配置模型、客服人格、知识库与工具能力",`<span class="btn"><i class="led"></i>配置已同步</span><button class="btn" data-action="toast">▷ 测试 AI</button><button class="btn primary" data-action="toast">${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" value="贴心管家"></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">你是甄养堂的贴心管家,温和专业、耐心解答用户关于健康管理与产品服务的问题。表达清晰简洁,关注用户需求,提供实用建议,必要时引导用户转人工客服。</textarea></div></div><div class="preview-box"><b>✧ 实时预览</b><p>您好!我是甄养堂贴心管家,<br>很高兴为您服务 😊</p><p>请问有什么可以帮您的吗?</p></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>✣ 模型引擎</h2></div><div class="field"><label>当前模型</label><select class="select model"><option>✦ Qwen 3.6 35B</option></select></div><div class="form-grid" style="margin-top:17px"><div class="field"><label>备用模型</label><select class="select"><option>Qwen 3.1 14B</option></select></div><div class="field"><label>上下文</label><select class="select"><option>8 轮</option></select></div><div class="field"><label>最大回复</label><select class="select"><option>500 tokens</option></select></div></div><div class="form-grid two" style="margin-top:18px"><div class="field"><label>温度 0.35</label><input class="range" type="range" value="35"></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>▰ 企业知识库</h2></div><div class="knowledge"><div class="donut"><strong>1,284<small>个知识片段</small></strong></div><div class="stack"><span class="badge green">● 刚刚同步</span><span class="badge green">✓ 已连接</span><button class="btn small primary" data-action="toast">${icon("refresh")}立即同步</button><button class="btn small" data-action="toast">${icon("folder")}管理知识</button></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>◇ MCP 工具</h2><small>单次最多调用 5 个工具 ⓘ</small></div><div class="tool-grid"><div class="tool-card"><div class="tool-icon">▣</div><b>挂号登记</b><div class="row"><small>已启用</small><div class="switch on"></div></div></div><div class="tool-card"><div class="tool-icon">⌕</div><b>客户查询</b><div class="row"><small>已启用</small><div class="switch on"></div></div></div><div class="tool-card"><div class="tool-icon">▤</div><b>回访记录</b><div class="row"><small>已启用</small><div class="switch on"></div></div></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("shield")} 安全边界</h2></div><div class="stack"><div class="setting-line"><span>🛡 不做医疗诊断</span></div><div class="setting-line"><span>💊 不修改用药方案</span></div><div class="setting-line"><span>♙ 高风险转人工</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","知识库已同步","1,284 个知识片段"],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])}</section></div>`}
function automationView(){return `<div class="screen">${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`<span class="btn"><i class="led"></i>企业微信已识别</span><span class="btn"><i class="led"></i>规则已生效</span><button class="btn primary" data-action="toast">${icon("save")}保存设置</button>`)}<div class="tabs"><button class="tab active">${icon("send")} 自动发送</button><button class="tab">${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">2 秒</b></div><div class="flow-pill">⌛ 连续消息等待 <b style="color:#2d65f0">20 秒</b></div><div class="flow-pill">${icon("clock")}发送前等待 <b style="color:#2d65f0">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>▣ 主窗口:</span><b style="color:var(--green)">● 已识别</b></div><div class="row"><span>${icon("user")} 当前账号:</span><b>甄养堂客服</b></div><div class="row"><span>◎ DPI</span><b>200%</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="toast">${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 on"></div></div><div style="text-align:center;margin-top:8px">鼠标静止 <b style="color:#2562ed">5 秒</b> 后恢复</div><div class="mouse-orb"><span style="color:#315fec">${icon("bot")}<br><small>AI 运行中</small></span><div class="mouse-pointer">➤</div><span style="color:var(--orange)">Ⅱ<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>✓ 保留最近 8 轮上下文</span><span class="badge">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">固定回复:在的,请稍等</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">⚕ 诊断</span><span class="risk-tag">◇ 用药调整</span><span class="risk-tag">¥ 投诉退款</span></div><button class="btn small blue" style="margin:11px auto 0;display:flex" data-action="toast">${icon("settings")}规则管理</button></section></div><section class="panel panel-pad check-strip"><div class="panel-title"><h2>启动前检查</h2></div>${statusCells([["check","企业微信已登录","检查完成"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准","检查完成"],["shield","规则配置有效","检查完成"]])}</section></div>`}
function logsView(){return `<div class="screen">${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`<button class="btn" data-action="toast">${icon("refresh")}实时刷新</button><button class="btn" data-action="toast">${icon("download")}导出日志</button><button class="btn danger" data-action="clear-logs">▱ 清空日志</button><span class="btn" style="color:var(--green)"><span class="orb" style="width:31px;height:31px"></span>监控中</span>`)}${ribbon([["企业微信","99.9%","message","green"],["AI 成功率","98.6%","brain","","blue"],["平均响应","2.4s","clock","","blue"],["警告","1","alert","red"]])}<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"><button class="filter-pill active" style="height:34px;padding:0 17px">全部</button><button class="filter-pill" style="height:34px;padding:0 17px">连接</button><button class="filter-pill" style="height:34px;padding:0 17px">识别</button><button class="filter-pill" style="height:34px;padding:0 17px">AI</button><button class="filter-pill" style="height:34px;padding:0 17px">发送</button><button class="filter-pill" style="height:34px;padding:0 17px">警告</button><label class="search" style="height:34px;width:180px;margin-left:auto">${icon("search")}<input placeholder="搜索事件内容"></label></div><div class="event-list" id="log-list">${[["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=>`<div class="event-row"><div class="event-node">✓</div><time>${x[0]} ●</time><div class="event-kind" style="color:${x[5]==="orange"?"var(--orange)":x[5]==="green"?"var(--green)":"var(--blue)"}">${icon(x[3])}</div><div class="event-copy"><strong>${x[1]}</strong><span>${x[2]}</span></div><span class="badge ${x[5]}">${x[4]}</span></div>`).join("")}</div></section><section class="panel panel-pad"><div class="panel-title"><h2>系统健康</h2></div><div class="health-rings">${[["企业微信连接","正常"],["AI 服务","正常"],["窗口识别","正常"]].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">02:18:46</b></div></section><section class="panel panel-pad"><div class="panel-title"><h2>当前回复链路</h2></div><div class="chain">${[["activity","扫描未读","0.3s"],["message","读取消息","1.2s"],["brain","请求 AI","2.4s"],["send","回填发送","0.6s"]].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>人工保护</b><p style="margin:4px 0 7px;font-size:12px">检测到人工鼠标操作,自动回复暂停 5 秒</p><div class="toolbar"><span class="badge orange">模块:人机共存</span><span class="badge orange">会话:高瑞@微信</span><span class="badge green">已自动恢复</span></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>1</b></div></div><div class="exception"><div class="metric-icon" style="color:#785cf1">${icon("clock")}</div><div>模型超时<b>0</b></div></div><div class="exception"><div class="metric-icon orange">${icon("monitor")}</div><div>窗口丢失<b>0</b></div></div><button class="btn blue" data-action="toast">查看异常 </button></div></section></div><section class="panel activity-panel recent-activity"><div class="panel-title"><h2>最近活动</h2></div>${activities("查看完整记录")}</section></div>`}
function systemView(){return `<div class="screen">${head("系统设置","管理软件启动、数据存储、通知与系统维护",`<span class="btn">当前版本 v2.0 ⓘ</span><button class="btn" data-action="toast">${icon("refresh")}检查更新</button><button class="btn primary" data-action="toast">${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"></div></div><div class="system-line">${icon("activity")}<span>启动后自动监听</span><div class="switch on"></div></div><div class="system-line">▰<span>最小化到系统托盘</span><div class="switch on"></div></div><div class="system-line">${icon("log")}<span>关闭窗口时保持后台运行</span><div class="switch on"></div></div><div class="system-line">◎<span>界面语言: 简体中文</span>⌄</div><div class="system-line">${icon("monitor")}<span>缩放比例: 自动</span>⌄</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>286<small>MB 已使用</small></strong></div><div class="stack"><div class="row"><span>● 会话归档</span><span>128 MB</span></div><div class="row"><span>● 运行日志</span><span>86 MB</span></div><div class="row"><span>● 缓存数据</span><span>72 MB</span></div></div></div><div class="row" style="justify-content:center;margin-top:7px"><span>数据保留</span><span class="btn small">90 天 ⌄</span></div><div class="storage-actions"><button class="btn small blue" data-action="toast">${icon("folder")}打开目录</button><button class="btn small blue" data-action="toast">${icon("cloud")}立即备份</button><button class="btn small blue" data-action="toast">⌁ 清理缓存</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"></div></div><div class="system-line">⌛<span>模型超时提醒</span><div class="switch on"></div></div><div class="system-line">${icon("user")}<span>需要人工跟进</span><div class="switch on"></div></div><div class="system-line">⌁<span>企业微信连接中断</span><div class="switch on"></div></div><div class="notice-preview"><div class="metric-icon">♟</div><div><b>发送失败提醒</b><small style="display:block">消息发送失败,已加入重试队列</small></div><time>16:33<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">⌁<span>网络延迟:</span><b>28ms ●</b></div></div><button class="btn small blue" style="margin:10px auto 0;display:flex" data-action="toast">⌁ 连接测试</button></section><section class="panel panel-pad"><div class="panel-title"><h2>维护与诊断</h2></div><div class="maintenance"><div class="maint-card"><div class="metric-icon">${icon("refresh")}</div><div><strong>检查更新</strong><small>已是最新版本</small></div></div><div class="maint-card"><div class="metric-icon">▣</div><div><strong>导出诊断</strong><small>导出系统诊断报告</small></div></div><div class="maint-card"><div class="metric-icon">↻</div><div><strong>恢复默认设置</strong><small>重置所有设置项</small></div></div><div class="maint-card"><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","数据已备份","最近备份:16:33:28"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务运行中","系统运行状态良好"]])}</section></div>`}
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='<div class="soft" style="height:170px;display:grid;place-items:center;color:var(--muted)">日志已清空</div>';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 `<span class="btn"><i class="led ${wecomLed}"></i>${esc(wecom)}</span><span class="btn"><i class="led ${aiLed}"></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 selectedKey = archive ? STATE.selectedArchive : STATE.selectedKey;
const active = p.active || p.key === selectedKey || (!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 data-live-wait="${esc(p.key || "")}">${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" data-live-activity="1">${cards}<button class="btn blue activity-more" data-action="${action}">${esc(more)} </button></div>`;
};
journey = function(){
if (STATE.demo) return _journey();
const items = STATE.journey || [];
if (!items.length){
return `<div class="empty-board" style="height:220px"><span>等待新的自动回复任务</span></div>`;
}
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" data-live-jtime="${i}">${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" : "", "", "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 `<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).replace(/\n/g,"<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")}模型耗时 <span data-live-latency="1">${esc(task.latency || "—")}</span></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.sessionDisplay || task.sessionId || "--")} <span data-action="copy-session" data-key="${esc(task.sessionId || "")}" style="display:inline-flex;width:21px;height:21px;cursor:pointer">${icon("copy")}</span></b></div><div class="detail-stat"><span>已等待</span><b data-live-task-wait="1">${esc(task.wait || "--")}</b></div><div class="detail-stat"><span>重试</span><b data-live-task-retries="1">${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 data-live-schedule="priority">${esc(s.priority)}</b></div></div><div class="side-stat"><div class="metric-icon">${icon("clock")}</div><div><span>预计完成</span><b data-live-schedule="eta">${esc(s.eta)}</b></div></div><div class="side-stat"><div class="metric-icon green">${icon("shield")}</div><div><span>人工保护</span><b data-live-schedule="protect" 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 data-live-schedule="retry">${esc(s.retry)}</b></div></div></div></section>`;
}
autoView = function(){
if (STATE.demo) return _autoView();
const count = (STATE.people || []).length;
const waiting = (STATE.metrics && STATE.metrics.waiting) || "0";
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" data-live-metric="waiting">${esc(waiting)}</span><span data-action="cycle-filter" title="筛选任务" style="display:inline-flex;width:21px;height:21px;cursor:pointer">${icon("filter")}</span><span data-action="cycle-sort" title="排序任务" style="display:inline-flex;width:21px;height:21px;cursor:pointer">${icon("sort")}</span></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 data-live-journey-total="1">${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><span data-action="refresh" style="display:inline-flex;width:21px;height:21px;cursor:pointer">${icon("refresh")}</span></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 || "全部会话";
const dateValue = STATE.archiveDate || "";
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===name || (name==="全部会话" && filter==="全部")?"active":""}" data-action="archive-filter" data-key="${name}">${name}${name==="全部会话"?"⌄":""}</button>`).join("")}<label class="filter-pill date-pill ${filter==="日期"?"active":""}">${icon("clock")}<input id="archive-date" type="date" value="${esc(dateValue)}"></label></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"];
const tab = STATE.aiTab || "基础设置";
const tabs = ["基础设置","知识与工具","安全策略"].map(n=>`<button class="tab ${tab===n?"active":""}" data-action="ai-tab" data-key="${n}">${n}</button>`).join("");
const lengthIndex = Number(ai.replyLengthIndex ?? 1);
const lengthLabel = ai.replyLength || ["短","中等","长"][lengthIndex] || "中等";
const tempVal = Number(ai.temperature || 0.35);
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">${tabs}</div><div class="ai-top ai-pane"${tab!=="基础设置"?" hidden":""}><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">${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 action" data-action="pick-model">${icon("sparkles")} ${esc(ai.model || "云端模型")}</button></div><div class="form-grid" style="margin-top:17px"><div class="field"><label>备用模型</label><button class="select action" data-action="pick-backup">${esc(ai.backup || "由云端策略管理")}</button></div><div class="field"><label>上下文</label><input class="select ai-num" id="ai-context" type="number" min="1" max="50" value="${esc(ai.context || 8)}"> 轮</div><div class="field"><label>最大回复</label><input class="select ai-num" id="ai-tokens" type="number" min="50" max="32000" value="${esc(ai.maxTokens || 500)}"> tokens</div></div><div class="form-grid two" style="margin-top:18px"><div class="field"><label data-temp-label="1">温度 ${esc(tempVal.toFixed(2))}</label><input class="range" id="ai-temp" type="range" min="0" max="200" value="${Math.round(tempVal*100)}"></div><div class="field"><label data-length-label="1">回复长度 ${esc(lengthLabel)}</label><input class="range" id="ai-length" type="range" min="0" max="2" step="1" value="${lengthIndex}"></div></div></section></div><div class="ai-bottom ai-pane"${tab!=="知识与工具"?" hidden":""}><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>个 MCP 服务</small></strong></div><div class="stack"><span class="badge green">● ${esc(ai.syncAt || "尚未同步")}</span><span class="badge green">✓ ${Number(ai.knowledge||0)?"已连接":"未配置"}</span><button class="btn small primary" data-action="sync-knowledge">${icon("refresh")}立即同步</button><button class="btn small" data-action="edit-knowledge">${icon("folder")}管理知识</button></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("package")} MCP 工具</h2><small>单次最多调用 <input class="ai-num" id="ai-mcp-rounds" type="number" min="1" max="20" value="${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("link")} MCP 配置</h2></div><div class="stack"><div class="setting-line"><span>服务数量</span><b>${esc(ai.knowledge || "0")}</b></div><div class="setting-line"><span>工具开关</span><b>${toolsOn.filter(Boolean).length ? "已启用" : "未启用"}</b></div><button class="btn small blue" data-action="edit-knowledge">${icon("settings")}编辑服务器 JSON</button></div></section></div><div class="ai-bottom ai-pane"${tab!=="安全策略"?" hidden":""}><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></section><section class="panel panel-pad"><div class="panel-title"><h2>转人工规则</h2></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><section class="panel panel-pad"><div class="panel-title"><h2>当前模式</h2></div><div class="row" style="height:100%;align-items:center"><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", STATE.aiOk ? "云端模型正常" : "AI 服务异常", `最后同步:${esc(ai.syncAt || "尚未同步")}`],["folder","MCP 工具配置",`${esc(ai.knowledge || "0")} 个服务`],["folder",`${esc(ai.toolCount || 0)} 个工具可用`, toolsOn.filter(Boolean).length ? "已启用" : "未启用"],["shield","安全策略已生效","严格模式"]])}</section></div>`;
};
// 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=>`<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 || "").replace(/\n/g,"<br>")}</div></div>`).join("")
: `<div class="empty-board" style="height:220px"><span>选择会话后显示聊天记录</span></div>`;
const tags = (STATE.memory && STATE.memory.tags || []).map((tag,index)=>`<span class="tag ${index===1?"violet":index===2?"blue":""}">${esc(tag)}</span>`).join("") || `<span class="tag">暂无标签</span>`;
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)=>`<div class="settle-row"><div class="settle-icon"${settleStyles[index]?` style="${settleStyles[index]}"`:""}>${settleTokens[index] || icon("clipboard")}</div><div><strong>${esc(item.title)}</strong><small>${esc(item.hint)}</small></div><b>${esc(item.value)}</b></div>`).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=>`<button class="filter-pill ${filter===name || (name==="全部会话" && filter==="全部")?"active":""}" data-action="archive-filter" data-key="${name}">${name}${name==="全部会话"?"⌄":""}</button>`).join("");
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" maxlength="80" placeholder="搜索客户或会话 ID" value="${esc(STATE.archiveQuery || "")}"></label>${filterButtons}<button class="filter-pill ${filter==="日期"?"active":""}" data-action="archive-date">${esc(dateLabel)} □</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) || "选择会话后显示长期上下文。").replace(/\n/g,"<br>")}</div><span class="badge" style="margin-top:10px">◔ 上下文 ${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 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=>`<button class="tab ${tab===name?"active":""}" data-action="ai-tab" data-key="${name}">${name}</button>`).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=>`<option value="${value}" ${value===contextValue?"selected":""}>${value} 轮</option>`).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=>`<option value="${value}" ${value===tokenValue?"selected":""}>${value} tokens</option>`).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 `<div class="screen">${head("AI 设置","配置模型、客服人格、知识库与工具能力",`<span class="btn" data-config-status="1"><i class="led"></i>${configLabel}</span><button class="btn" data-action="test-ai">▷ 测试 AI</button><button class="btn primary" data-action="save-ai">${icon("send")}保存并发布</button>`)}<div class="tabs">${tabs}</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 aria-readonly="true">${esc(ai.prompt || "")}</textarea></div></div><div class="preview-box"><b>✧ 实时预览</b><p>您好!我是甄养堂${esc(ai.name || "贴心管家")}<br>很高兴为您服务 😊</p><p>请问有什么可以帮您的吗?</p></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>✣ 模型引擎</h2></div><div class="field"><label>当前模型</label><select class="select model" data-action="pick-model"><option>✦ ${esc(ai.model || "云端模型")}</option></select></div><div class="form-grid" style="margin-top:17px"><div class="field"><label>备用模型</label><select class="select" data-action="pick-backup"><option>${esc(ai.backup || "由云端策略管理")}</option></select></div><div class="field"><label>上下文</label><select class="select" id="ai-context">${contextOptions}</select></div><div class="field"><label>最大回复</label><select class="select" id="ai-tokens">${tokenOptions}</select></div></div><div class="form-grid two" style="margin-top:18px"><div class="field"><label data-temp-label="1">温度 ${temperature.toFixed(2)}</label><input class="range" id="ai-temp" type="range" min="0" max="100" value="${Math.round(temperature*100)}"></div><div class="field"><label data-length-label="1">回复长度 ${lengthLabel}</label><input class="range" id="ai-length" type="range" min="0" max="100" value="${lengthValue}"></div></div></section></div><div class="ai-bottom"><section class="panel panel-pad"><div class="panel-title"><h2>▰ 企业知识库</h2></div><div class="knowledge"><div class="donut"><strong>${esc(knowledgeCount)}<small>个知识片段</small></strong></div><div class="stack"><span class="badge green">● ${esc(syncLabel)}</span><span class="badge green">✓ ${connectedKnowledge?"已连接":"未连接"}</span><button class="btn small primary" data-action="sync-knowledge">${icon("refresh")}立即同步</button><button class="btn small" data-action="edit-knowledge">${icon("folder")}管理知识</button></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>◇ MCP 工具</h2><small>单次最多调用 ${esc(ai.mcpRounds || 5)} 个工具 ⓘ</small></div><div class="tool-grid">${names.map((name,index)=>`<div class="tool-card"><div class="tool-icon">${toolTokens[index]}</div><b>${name}</b><div class="row"><small>${toolsOn[index]?"已启用":"未启用"}</small><div class="switch ${toolsOn[index]?"on":""}" data-action="toggle-tool" data-key="${index}"></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>🛡 不做医疗诊断</span></div><div class="setting-line"><span>💊 不修改用药方案</span></div><div class="setting-line"><span>♙ 高风险转人工</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",STATE.aiOk===false?"AI 服务异常":"云端模型正常","最后检查:刚刚"],["folder",knowledgeStatus,`${esc(knowledgeCount)} 个知识片段`],["folder",`${toolCount} 个工具可用`,toolStatus],["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")}扫描间隔 <input class="num" id="auto-poll" type="number" min="0.2" max="300" step="0.1" value="${esc(a.poll || "2")}"> 秒</div><div class="flow-pill">${icon("hourglass")} 连续消息等待 <input class="num" id="auto-batch" type="number" min="1" max="120" step="1" value="${esc(a.batch || "20")}"> 秒</div><div class="flow-pill">${icon("clock")}发送前等待 <input class="num" id="auto-delay" type="number" min="0" max="30" step="0.5" value="${esc(a.delay || "1")}"> 秒</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">鼠标静止 <input class="idle-num" id="auto-idle" type="number" min="0" max="3600" step="0.5" value="${esc(a.idle || "5")}"> 秒 后恢复</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>✓ 保留最近 <input class="ai-num" id="auto-context" type="number" min="1" max="50" value="${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 超时:${esc(a.timeout || 120)} 秒后重试</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><button class="badge" data-action="edit-reply">编辑</button></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:[]};
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 `<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" maxlength="80" 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.aiOk ? "正常" : "异常"],["窗口识别", 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" data-live-runtime="1">${esc(STATE.runtime || "00:00:00")}</b></div></section><section class="panel panel-pad"><div class="panel-title"><h2>当前回复链路</h2></div><div class="chain">${chain.map((x,i)=>`<div class="chain-step"><div class="flow-circle">${icon(x.icon || "activity")}</div><strong>${esc(x.title)}</strong><span data-live-chain="${i}">${esc(x.time || "")}</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><select id="sys-language"><option ${(s.language||"简体中文")==="简体中文"?"selected":""}>简体中文</option></select></div><div class="system-line">${icon("monitor")}<span>缩放比例</span><select id="sys-scale">${["自动","100%","125%","150%","200%"].map(n=>`<option ${ (s.scale||"自动")===n?"selected":""}>${n}</option>`).join("")}</select></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><input class="retain-num" id="sys-retention" type="number" min="7" max="365" value="${esc(s.retention || 90)}"> 天</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>${esc(s.noticeTime || "—")}<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><div class="switch ${on("maskSensitive")}" data-action="sys-toggle" data-key="mask_sensitive"></div></div><div class="system-line">${icon("lock")}<span>本地数据加密</span><div class="switch ${on("encryptLocal")}" data-action="sys-toggle" data-key="encrypt_local"></div></div><div class="system-line">${icon("lock")}<span>日志隐藏聊天正文</span><div class="switch ${on("hideChat")}" data-action="sys-toggle" data-key="hide_chat_in_logs"></div></div><div class="system-line">${icon("log")}<span>退出时清理剪贴板</span><div class="switch ${on("clearClipboard")}" data-action="sys-toggle" data-key="clear_clipboard_on_exit"></div></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>${esc(s.cloudAi || "未检测")}</b></div><div class="system-line">${icon("folder")}<span>企业知识库:</span><b>${esc(s.knowledgeNet || "未检测")}</b></div><div class="system-line">${icon("wifi")}<span>网络延迟:</span><b>${esc(s.latency || "—")}</b></div></div><button class="btn small blue" style="margin:10px auto 0;display:flex" data-action="test-connection">${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>${esc(s.updateLabel || "已是最新版本")}</small></div></div><div class="maint-card" data-action="export-diagnostics"><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","最近备份", esc(s.lastBackup || "尚未备份")],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务", STATE.listening ? "运行中" : "已停止"]])}</section></div>`;
};
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 `<div class="soft" style="height:170px;display:grid;place-items:center;color:var(--muted)">当前没有运行记录</div>`;
}
return 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("");
}
function wireDemoArchive(html){
let rowIndex = 0;
return html
.replace('data-action="toast">' + icon("download") + '导出记录', 'data-action="export-archive">' + icon("download") + '导出记录')
.replace('<input placeholder="搜索客户或会话 ID">', '<input id="archive-search" maxlength="80" placeholder="搜索客户或会话 ID">')
.replace('<button class="filter-pill active">全部会话⌄</button>', '<button class="filter-pill active" data-action="archive-filter" data-key="全部会话">全部会话⌄</button>')
.replace('<button class="filter-pill">今日</button>', '<button class="filter-pill" data-action="archive-filter" data-key="今日">今日</button>')
.replace('<button class="filter-pill">AI 已回复</button>', '<button class="filter-pill" data-action="archive-filter" data-key="AI 已回复">AI 已回复</button>')
.replace('<button class="filter-pill">已转人工</button>', '<button class="filter-pill" data-action="archive-filter" data-key="已转人工">已转人工</button>')
.replace('<button class="filter-pill">2026-08-18 □</button>', '<button class="filter-pill" data-action="archive-date">2026-08-18 □</button>')
.replace(/<div class="queue-row ([^"]*)">/g, (_match, classes) => `<div class="queue-row ${classes}" data-action="select-archive" data-key="demo-archive-${rowIndex++}">`)
.replace('<button class="round-btn">' + icon("refresh") + '</button>', '<button class="round-btn" data-action="refresh">' + icon("refresh") + '</button>')
.replace('data-action="logs">复制全部记录', 'data-action="copy-archive">复制全部记录');
}
function wireDemoAi(html){
const activeTab = STATE.aiTab || "基础设置";
let toolIndex = 0;
return html
.replace('<span class="btn"><i class="led"></i>配置已同步</span>', '<span class="btn" data-config-status="1"><i class="led"></i>配置已同步</span>')
.replace('<button class="btn" data-action="toast">▷ 测试 AI</button>', '<button class="btn" data-action="test-ai">▷ 测试 AI</button>')
.replace('<button class="btn primary" data-action="toast">' + icon("send") + '保存并发布</button>', '<button class="btn primary" data-action="save-ai">' + icon("send") + '保存并发布</button>')
.replace('<div class="tabs"><button class="tab active">基础设置</button><button class="tab">知识与工具</button><button class="tab">安全策略</button></div>', `<div class="tabs">${["基础设置","知识与工具","安全策略"].map(name=>`<button class="tab ${activeTab===name?"active":""}" data-action="ai-tab" data-key="${name}">${name}</button>`).join("")}</div>`)
.replace('<input class="input" value="贴心管家">', '<input class="input" id="ai-name" value="贴心管家" maxlength="40">')
.replace('<textarea class="textarea">', '<textarea class="textarea" id="ai-prompt" readonly aria-readonly="true">')
.replace('<select class="select model">', '<select class="select model" data-action="pick-model">')
.replace('<select class="select"><option>Qwen 3.1 14B</option></select>', '<select class="select" data-action="pick-backup"><option>Qwen 3.1 14B</option></select>')
.replace('<select class="select"><option>8 轮</option></select>', '<select class="select" id="ai-context"><option value="8">8 轮</option></select>')
.replace('<select class="select"><option>500 tokens</option></select>', '<select class="select" id="ai-tokens"><option value="500">500 tokens</option></select>')
.replace('<label>温度 0.35</label><input class="range" type="range" value="35">', '<label data-temp-label="1">温度 0.35</label><input class="range" id="ai-temp" type="range" value="35">')
.replace('<label>回复长度 中等</label><input class="range" type="range" value="55">', '<label data-length-label="1">回复长度 中等</label><input class="range" id="ai-length" type="range" value="55">')
.replace('<button class="btn small primary" data-action="toast">' + icon("refresh") + '立即同步</button>', '<button class="btn small primary" data-action="sync-knowledge">' + icon("refresh") + '立即同步</button>')
.replace('<button class="btn small" data-action="toast">' + icon("folder") + '管理知识</button>', '<button class="btn small" data-action="edit-knowledge">' + icon("folder") + '管理知识</button>')
.replace(/<div class="switch on"><\/div>/g, () => `<div class="switch on" data-action="toggle-tool" data-key="${toolIndex++}"></div>`);
}
function liveLogActivities(more="查看完整记录"){
const items = (STATE.activities && STATE.activities.length ? STATE.activities : (STATE.logs || [])).slice(-4);
const cards = items.map(x => {
const tone = x.tone === "orange" || x.category === "警告" ? "orange" : (x.tone === "blue" ? "blue" : "");
const color = tone === "orange" ? "var(--orange)" : tone === "blue" ? "var(--blue)" : "var(--green)";
return `<div class="activity-card"><div class="activity-icon ${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("");
return `<div class="activity-strip">${cards}<button class="btn blue activity-more" data-action="full-log">${more} </button></div>`;
}
function patchSystemStorage(){
if (STATE.view !== "system") return;
const s = STATE.system || {};
const storage = document.querySelector(".system-top > section:nth-child(2) .donut strong");
if (storage) {
let value = storage.querySelector("[data-live-system-storage]");
let unit = storage.querySelector("small");
if (!value || !unit) {
storage.textContent = "";
value = document.createElement("span");
value.dataset.liveSystemStorage = "1";
unit = document.createElement("small");
storage.append(value, unit);
}
const raw = String(s.storage || "—").trim();
const match = raw.match(/^([\d,.]+)\s*(.*)$/);
value.textContent = match ? match[1] : raw;
unit.textContent = `${match && match[2] ? `${match[2]} ` : ""}已使用`;
}
const sizeValues = [s.archiveSize, s.logSize, s.cacheSize];
document.querySelectorAll(
".system-top > section:nth-child(2) .stack .row > span:last-child"
).forEach((node, index) => {
const next = sizeValues[index] || "—";
if (node.textContent !== String(next)) node.textContent = next;
});
const notice = document.querySelector(".system-top > section:nth-child(3) .notice-preview time");
if (notice) {
let stamp = notice.querySelector("[data-live-system-notice]");
if (!stamp) {
notice.textContent = "";
stamp = document.createElement("span");
stamp.dataset.liveSystemNotice = "1";
notice.append(stamp, document.createElement("br"));
notice.insertAdjacentHTML("beforeend", '<span style="color:#3772ff">●</span>');
}
stamp.textContent = s.noticeTime || "—";
}
}
function normalizeReferenceView(){
if (STATE.demo) return;
if (STATE.view === "automation") {
const recognition = document.querySelector(".page-head .head-actions > span.btn:first-child");
if (recognition) {
const recognized = !!STATE.wecomConnected;
recognition.innerHTML = `<i class="led ${recognized ? "" : "off"}"></i>企业微信${recognized ? "已识别" : "未识别"}`;
}
document.querySelectorAll(".automation-top .flow-pill").forEach(pill => pill.classList.add("live-flow-pill"));
const ruleBadge = document.querySelector(".automation-top > section:first-child > .badge");
if (ruleBadge) {
ruleBadge.innerHTML = `${icon("activity")}${STATE.listening ? "规则运行中" : "规则待运行"}`;
ruleBadge.classList.toggle("green", !!STATE.listening);
ruleBadge.classList.toggle("orange", !STATE.listening);
}
const check = document.querySelector(".automation-top .fake-check");
if (check) {
check.textContent = STATE.wecomConnected ? "✓" : "!";
check.classList.toggle("off", !STATE.wecomConnected);
}
const windowRows = document.querySelectorAll(".automation-top > section:nth-child(2) .stack .row");
const mainWindow = windowRows[0] && windowRows[0].querySelector("b");
if (mainWindow) {
mainWindow.textContent = `● ${STATE.wecomConnected ? "已识别" : "未识别"}`;
mainWindow.style.color = STATE.wecomConnected ? "var(--green)" : "var(--red)";
}
const trayState = windowRows[3] && windowRows[3].querySelector("b");
if (trayState) {
const enabled = !!((STATE.system || {}).tray);
trayState.textContent = `● ${enabled ? "已开启" : "已关闭"}`;
trayState.style.color = enabled ? "var(--green)" : "var(--red)";
}
const runner = document.querySelector(".automation-mid > section:first-child .mouse-orb > span:first-child small");
if (runner) runner.textContent = STATE.listening ? "AI 运行中" : "AI 已停止";
const checks = document.querySelectorAll(".automation-mid + .check-strip .status-cell");
if (checks[0]) {
checks[0].querySelector("strong").textContent = STATE.wecomConnected ? "企业微信已登录" : "企业微信未登录";
checks[0].querySelector("span").textContent = STATE.wecomConnected ? "检查完成" : "等待连接";
}
if (checks[1]) {
checks[1].querySelector("strong").textContent = STATE.aiOk ? "AI 服务正常" : "AI 服务异常";
checks[1].querySelector("span").textContent = STATE.aiOk ? "检查完成" : "待检查";
}
if (checks[2]) {
checks[2].querySelector("strong").textContent = STATE.wecomConnected ? "窗口坐标已校准" : "窗口坐标待校准";
checks[2].querySelector("span").textContent = STATE.wecomConnected ? "检查完成" : "待校准";
}
const timeout = document.querySelector(".automation-mid > section:nth-child(3) .setting-line:first-child > span:nth-child(2)");
if (timeout) timeout.textContent = "AI 超时:重试 2 次";
return;
}
if (STATE.view === "logs") {
const list = document.getElementById("log-list");
if (list) {
const rows = [...list.querySelectorAll(":scope > .event-row")];
rows.slice(0, Math.max(0, rows.length - 5)).forEach(row => row.remove());
}
const health = [!!STATE.wecomConnected, !!STATE.aiOk, !!STATE.wecomConnected];
document.querySelectorAll(".health-rings .health-ring").forEach((ring, index) => ring.classList.toggle("bad", !health[index]));
const monitorOrb = document.querySelector(".page-head .head-actions > span.btn .orb");
if (monitorOrb) monitorOrb.classList.toggle("paused", !STATE.listening);
const tags = document.querySelectorAll(".log-mid > section:first-child .toolbar .badge");
tags.forEach(tag => {
const value = tag.textContent || "";
tag.classList.toggle("green", /成功|恢复|正常|完成/.test(value));
tag.classList.toggle("orange", /警告|失败|人工/.test(value));
});
const strip = document.querySelector(".recent-activity .activity-strip");
if (strip) strip.outerHTML = liveLogActivities("查看完整记录");
return;
}
if (STATE.view === "system") {
const s = STATE.system || {};
const base = document.querySelectorAll(".system-top > section:first-child .system-line");
if (base[4]) base[4].querySelector("span").textContent = "界面语言:";
if (base[5]) base[5].querySelector("span").textContent = "缩放比例:";
patchSystemStorage();
const retention = document.getElementById("sys-retention");
if (retention && !retention.parentElement.classList.contains("retention-control")) {
const row = retention.parentElement;
[...row.childNodes].forEach(node => {
if (node.nodeType === Node.TEXT_NODE && /天/.test(node.textContent || "")) node.remove();
});
const control = document.createElement("span");
control.className = "btn small retention-control";
row.insertBefore(control, retention);
control.append(retention, document.createTextNode(" 天"));
control.insertAdjacentHTML("beforeend", icon("chevron"));
}
const securityKeys = ["maskSensitive", "encryptLocal", "hideChat", "clearClipboard"];
const securityBridgeKeys = ["mask_sensitive", "encrypt_local", "hide_chat_in_logs", "clear_clipboard_on_exit"];
document.querySelectorAll(".system-mid > section:first-child .system-line").forEach((line, index) => {
const active = !!s[securityKeys[index]];
line.dataset.action = "sys-toggle";
line.dataset.key = securityBridgeKeys[index];
line.setAttribute("role", "button");
line.setAttribute("tabindex", "0");
const control = line.querySelector(".switch");
if (control) {
const status = document.createElement("b");
status.className = `system-security-state ${active ? "on" : "off"}`;
status.textContent = `${active ? "已开启" : "已关闭"} ●`;
control.replaceWith(status);
}
const label = line.querySelector("span");
if (label && !label.textContent.endsWith("")) label.textContent += "";
});
document.querySelectorAll(".system-mid > section:nth-child(2) .system-line b").forEach(value => {
const text = (value.textContent || "").replace(/\s*●\s*$/, "");
if (text && text !== "—" && !/未连接|未检测|失败|异常/.test(text)) value.textContent = `${text} ●`;
});
document.querySelectorAll(".maintenance .maint-card[data-action]").forEach(card => {
card.setAttribute("role", "button");
card.setAttribute("tabindex", "0");
});
const cells = document.querySelectorAll(".system-status .status-cell");
if (cells[1]) {
const backed = s.lastBackup && s.lastBackup !== "尚未备份";
cells[1].querySelector("strong").textContent = backed ? "数据已备份" : "数据未备份";
const stamp = backed ? String(s.lastBackup).slice(-8) : "尚未备份";
cells[1].querySelector("span").textContent = backed ? `最近备份:${stamp}` : stamp;
}
if (cells[2]) {
const enabledCount = securityKeys.reduce((count, key) => count + (s[key] ? 1 : 0), 0);
cells[2].querySelector("strong").textContent = enabledCount === securityKeys.length ? "安全策略正常" : "安全策略已配置";
cells[2].querySelector("span").textContent = enabledCount === securityKeys.length ? "所有安全策略已启用" : `${enabledCount}/${securityKeys.length} 项安全策略已启用`;
}
if (cells[3]) {
cells[3].querySelector("strong").textContent = STATE.listening ? "后台服务运行中" : "后台服务已停止";
cells[3].querySelector("span").textContent = STATE.listening ? "系统运行状态良好" : "当前未启动监听";
}
}
}
function patchVolatile(){
const metrics = STATE.metrics || {};
document.querySelectorAll("[data-live-metric]").forEach(el => {
const key = el.getAttribute("data-live-metric");
const next = metrics[key];
if (next != null && el.textContent !== String(next)) el.textContent = next;
});
const waits = document.querySelectorAll("[data-live-wait]");
if (waits.length){
const byKey = Object.fromEntries((STATE.people || []).map(p => [p.key, p.wait || ""]));
waits.forEach(el => {
const next = byKey[el.getAttribute("data-live-wait")];
if (next != null && el.textContent !== next) el.textContent = next;
});
}
document.querySelectorAll("[data-live-runtime]").forEach(el => {
el.textContent = STATE.runtime || "00:00:00";
});
(STATE.journey || []).forEach((step, i) => {
const el = document.querySelector(`[data-live-jtime="${i}"]`);
if (el && step && step.time != null && el.textContent !== String(step.time)) {
el.textContent = step.time;
}
});
(STATE.chain || []).forEach((step, i) => {
const el = document.querySelector(`[data-live-chain="${i}"]`);
if (el && step && step.time != null && el.textContent !== String(step.time)) {
el.textContent = step.time;
}
});
const latency = document.querySelector("[data-live-latency]");
if (latency && STATE.task) latency.textContent = STATE.task.latency || "—";
const taskWait = document.querySelector("[data-live-task-wait]");
if (taskWait && STATE.task) taskWait.textContent = STATE.task.wait || "--";
const taskRetries = document.querySelector("[data-live-task-retries]");
if (taskRetries && STATE.task) taskRetries.textContent = STATE.task.retries || "0 次";
const schedule = (STATE.task && STATE.task.schedule) || {};
document.querySelectorAll("[data-live-schedule]").forEach(el => {
const key = el.getAttribute("data-live-schedule");
const next = schedule[key];
if (next != null && el.textContent !== String(next)) el.textContent = next;
if (key === "protect" && next != null) {
el.style.color = String(next) === "已开启" ? "var(--green)" : "inherit";
}
});
const journeyTotal = document.querySelector("[data-live-journey-total]");
if (journeyTotal) journeyTotal.textContent = STATE.journeyTotal || "";
patchSystemStorage();
syncOrb();
}
function patchLogs(){
const list = document.getElementById("log-list");
if (list) list.innerHTML = logRowsHtml(STATE.logs || []);
const strip = STATE.view === "logs" ? document.querySelector(".recent-activity .activity-strip") : document.querySelector("[data-live-activity]");
if (strip){
const more = STATE.view === "archive" ? "复制全部记录" : (STATE.view === "logs" ? "查看完整记录" : "查看完整日志");
strip.outerHTML = STATE.view === "logs" ? liveLogActivities(more) : activities(more);
}
// Log rows need their status normalization; unrelated pages must not have
// their controls touched merely because a background log entry arrived.
if (STATE.view === "logs") normalizeReferenceView();
}
const _render = render;
render = function(name){
if(!views[name]) name = STATE.view || "auto";
const sameView = STATE.view === name;
STATE.view = name;
listening = !!STATE.listening;
const html = polishedIcons(views[name]());
if (sameView && html === window.__lastHtml){
window.__coreFp = coreFingerprint(STATE);
window.__logsFp = logsFingerprint(STATE);
normalizeReferenceView();
syncOrb();
return;
}
window.__lastHtml = html;
window.__coreFp = coreFingerprint(STATE);
window.__logsFp = logsFingerprint(STATE);
const main = document.getElementById("main");
if (main){
document.querySelectorAll("[data-view]").forEach(b=>b.classList.toggle("active", b.dataset.view===name));
main.innerHTML = html;
normalizeReferenceView();
} else {
_render(name);
}
syncOrb();
};
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;
}
const core = coreFingerprint(STATE);
const logs = logsFingerprint(STATE);
if (core === window.__coreFp){
patchVolatile();
if (logs !== window.__logsFp){
window.__logsFp = logs;
patchLogs();
}
return;
}
render(STATE.view || prev || "auto");
};
window.showConsoleView = function(name){
if (name && name === STATE.view && window.__lastHtml) return;
render(name);
};
document.addEventListener("click", e => {
const nav = e.target.closest("[data-view]");
if (nav){
const name = nav.dataset.view;
// Render once immediately so every view remains available even when its
// host-side navigation does not need a state refresh. A matching state
// payload is then absorbed by render()'s same-HTML identity guard.
if (name !== STATE.view) render(name);
callBridge("navigate", name);
nav.blur();
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"){
if (!callBridge("toggleListen")) {
listening = !listening;
STATE.listening = listening;
syncOrb();
}
e.stopImmediatePropagation();
return;
}
if (a === "start-listen"){
if (!callBridge("startListen")) {
listening = true;
STATE.listening = true;
syncOrb();
}
e.stopImmediatePropagation();
return;
}
const map = {
logs: () => { if (STATE.view !== "logs") render("logs"); callBridge("navigate", "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),
"archive-date": () => callBridge("chooseArchiveDate"),
"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", collectAiPayload()),
"test-ai": () => callBridge("testAi"),
"pick-model": () => callBridge("pickModel"),
"pick-backup": () => callBridge("pickBackupModel"),
"edit-knowledge": () => callBridge("editKnowledge"),
"sync-knowledge": () => callBridge("syncKnowledge"),
"open-data": () => callBridge("openDataDir"),
"save-automation": () => callBridge("saveAutomationSettings", collectAutomationPayload()),
"send-mode": () => callBridge("setSendMode", key),
"toggle-idle": () => callBridge("toggleIdle"),
"redetect": () => callBridge("redetectWecom"),
"security-rules": () => callBridge("showSecurityRules"),
"save-system": () => callBridge("setSystemField", collectSystemPayload()),
"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),
"ai-tab": () => callBridge("setAiTab", key),
"edit-reply": () => callBridge("editReply"),
"test-connection": () => callBridge("testConnection"),
"export-diagnostics": () => callBridge("exportDiagnostics"),
"cycle-filter": () => callBridge("cycleQueueFilter"),
"cycle-sort": () => callBridge("cycleQueueSort"),
};
if (map[a]){
map[a]();
e.stopImmediatePropagation();
}
}, true);
document.addEventListener("keydown", e => {
if (e.key !== "Enter" && e.key !== " ") return;
const action = e.target && e.target.closest ? e.target.closest('[role="button"][data-action]') : null;
if (!action) return;
e.preventDefault();
action.click();
});
let searchTimer = 0;
document.addEventListener("input", e => {
if (!e.target) return;
const id = e.target.id || "";
const value = e.target.value || "";
if (["ai-name","ai-temp","ai-length","ai-context","ai-tokens"].includes(id)) {
markAiDirty();
}
if (id === "archive-search" || id === "event-search") {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
if (id === "archive-search") callBridge("setArchiveQuery", value);
else callBridge("setEventQuery", value);
}, 280);
return;
}
if (id === "ai-temp") {
const label = document.querySelector("[data-temp-label]");
if (label) label.textContent = "温度 " + (Number(value) / 100).toFixed(2);
return;
}
if (id === "ai-length") {
const index = Number(value) <= 33 ? 0 : (Number(value) < 75 ? 1 : 2);
const label = document.querySelector("[data-length-label]");
if (label) label.textContent = "回复长度 " + ["短","中等","长"][index];
const tokens = document.getElementById("ai-tokens");
if (tokens && [...tokens.options].some(option=>Number(option.value)===[300,500,1000][index])) {
tokens.value = String([300,500,1000][index]);
}
return;
}
if (id === "ai-name") {
const preview = document.querySelector(".preview-box p");
if (preview) preview.innerHTML = `您好!我是甄养堂${esc(value || "贴心管家")}<br>很高兴为您服务 😊`;
return;
}
if (id === "archive-date") {
callBridge("setArchiveDate", value);
return;
}
if (id === "auto-poll" || id === "auto-batch" || id === "auto-delay" || id === "auto-idle" || id === "auto-context") {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
callBridge("saveAutomationSettings", collectAutomationPayload());
}, 400);
return;
}
if (id === "sys-language" || id === "sys-scale" || id === "sys-retention") {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
callBridge("setSystemField", collectSystemPayload());
}, 280);
}
});
document.addEventListener("change", e => {
if (!e.target) return;
const id = e.target.id || "";
if (id === "ai-tokens") {
const value = Number(e.target.value || 500);
const index = value <= 300 ? 0 : (value <= 800 ? 1 : 2);
const range = document.getElementById("ai-length");
if (range) range.value = String([10,55,90][index]);
const label = document.querySelector("[data-length-label]");
if (label) label.textContent = "回复长度 " + ["短","中等","长"][index];
return;
}
if (id === "sys-language" || id === "sys-scale") {
callBridge("setSystemField", collectSystemPayload());
}
});
fit = function(){
document.documentElement.classList.add("embedded");
const stage = document.getElementById("stage");
if (!stage) return;
const w = 1630;
const h = 920;
let s = Math.min(innerWidth / w, innerHeight / h);
if (!isFinite(s) || s <= 0) s = 1;
s = Math.round(s * 1000) / 1000;
stage.style.left = "0";
stage.style.top = "0";
stage.style.width = w + "px";
stage.style.height = h + "px";
stage.style.transformOrigin = "top left";
stage.style.transform = "scale(" + s + ")";
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();
let fitTimer = 0;
addEventListener("resize", () => {
clearTimeout(fitTimer);
fitTimer = setTimeout(fit, 60);
});
fit();
if (document.documentElement.classList.contains("embedded")) {
setTimeout(() => {
if (!STATE.ready) render(STATE.view || "auto");
}, 400);
} else {
render((STATE.view || (location.hash || "#auto").slice(1) || "auto"));
}