1006 lines
119 KiB
JavaScript
1006 lines
119 KiB
JavaScript
|
||
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 => ({
|
||
"&":"&","<":"<",">":">",'"':""","'":"'"
|
||
}[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">Ⅱ 暂停监听</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"));
|
||
}
|