const icon=n=>` `;
if (/\bQt/i.test(navigator.userAgent) || window.qtBridge || typeof qt !== "undefined") {
document.documentElement.classList.add("embedded");
}
window.STATE = window.STATE || {
view: "auto",
demo: false,
ready: false,
listening: false
};
function esc(value){
return String(value ?? "").replace(/[&<>"']/g, ch => ({
"&":"&","<":"<",">":">",'"':""","'":"'"
}[ch]));
}
function bridge(){ return window.qtBridge || null; }
function callBridge(name, ...args){
const api = bridge();
if (!api || typeof api[name] !== "function") return false;
api[name](...args);
return true;
}
/**
* 带反馈地调用后台:点下去立刻有回执,做完再报真实结果。
*
* 以前几个保存按钮都是"调完就完"——点了界面一动不动,成功没提示、失败更没提示,
* 只能等下一次同步才发现改动根本没生效。人自然会以为按钮坏了,或者反复点。
*
* 三段反馈缺一不可:
* 1) 按钮本身立刻变成"处理中"并禁用 —— 告诉人"收到了",也防止连点;
* 2) 立刻弹一条"正在…" —— 慢操作时不至于一片安静;
* 3) 后台把真实结果回传后再弹结果 —— 成功说成功,失败说清为什么。
* 老版本后台的槽不返回值,拿不到回调,那就退回"已提交",不假装成功。
*/
function callBridgeWithFeedback(name, args, opts){
const options = opts || {};
const api = bridge();
const button = options.button || null;
const restore = button ? button.innerHTML : "";
if (button){
button.classList.add("is-busy");
button.disabled = true;
button.innerHTML = icon("refresh") + (options.busyLabel || "处理中…");
}
const done = (ok, message) => {
if (button){
button.classList.remove("is-busy");
button.disabled = false;
button.innerHTML = restore;
}
toast(message);
if (!ok) console.warn("[控制台]", name, message);
};
if (!api || typeof api[name] !== "function"){
done(false, "操作失败:界面没有连上后台");
return false;
}
toast(options.pending || "正在处理…");
let settled = false;
const finish = (ok, message) => { if (!settled){ settled = true; done(ok, message); } };
try {
api[name](...(args || []), reply => {
let data = null;
try { data = typeof reply === "string" ? JSON.parse(reply) : reply; } catch (e) { data = null; }
if (data && typeof data === "object"){
finish(!!data.ok, String(data.message || (data.ok ? "已完成" : "操作失败")));
} else {
finish(true, options.okLabel || "已完成");
}
});
} catch (e){
finish(true, options.submittedLabel || "已提交");
}
// 兜底:后台没有回调能力(版本不匹配、老槽不返回值)时,别让按钮一直卡在
// "处理中"。等得久一点,好让真实结果先到——真结果一到就 settled,兜底不会再响。
// 措辞也不能假装成功:没收到确认就说没收到,别让人以为存好了。
setTimeout(
() => finish(true, options.submittedLabel || "已提交,但没收到后台确认"),
10000,
);
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){
// 这一页现在只有客服昵称是真正的本机设置。
//
// 温度、最大 tokens、当前模型搬去了后台的「模型清单 / 角色编排」;上下文轮数和
// MCP 随后台配置下发。继续在这里提交它们的话,界面会提示"已保存并生效",而下
// 一次同步(每 5 分钟一次)就把它们盖回去——改了、提示成功了、什么都没发生。
const ai = STATE.ai || {};
const payload = {
name: (document.getElementById("ai-name") || {}).value || ai.name || "",
};
return JSON.stringify(Object.assign(payload, extra || {}));
}
function markAiDirty(){
const status = document.querySelector("[data-config-status]");
if (status) status.innerHTML = ' 有未保存更改';
}
function collectAutomationPayload(){
const a = STATE.automation || {};
return JSON.stringify({
poll: fieldNum("auto-poll", Number(a.poll || 2)),
batch: fieldNum("auto-batch", Number(a.batch || 20)),
delay: fieldNum("auto-delay", Number(a.delay || 1)),
idle: fieldNum("auto-idle", Number(a.idle || 5)),
context: fieldNum("auto-context", Number(a.context || 8)),
// 开关关掉就送 0(= 关闭);开着才送分钟数。
draftAutosend: (document.getElementById("auto-draft-autosend") || {}).disabled
? 0
: fieldNum("auto-draft-autosend", Number(a.draftAutosend || 0)),
});
}
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)=>``;
var connected=(extra="",primary="")=>` 企业微信已连接 AI 服务正常 ${extra}Ⅱ 暂停监听 `;
var ribbon=items=>`${items.map(x=>`${icon(x[2])}
${x[0]} ${x[1]}
`).join("")} `;
var queueList=(archive=false)=>`
${people.map((p,i)=>`
${p[0]}
${p[1]} ${archive?(i===0?"请问你们的灵芝孢子粉怎么服用…":i===1?"每天吃几次比较合适?":i===2?"有不良反应吗?":"会话已归档"):` ${p[2]}`}
${archive?["16:31","16:18","15:42","14:20","13:35"][i]:p[3]} ${archive?`${i===2?"已转人工":"AI 已回复"} `:`${icon("message")} ${i===3?1:0} `}
`).join("")}
`;
var activities=(more="查看完整日志")=>`${[["16:33:27","扫描未读","检测到客户未读消息","activity","成功",""],["16:33:28","提取聊天记录","已提取最近 6 条聊天记录","log","成功",""],["16:33:29","AI 服务","正在请求 AI 生成回复","brain","进行中","blue"],["16:33:31","人工保护","检测到人工鼠标操作,暂停 5 秒","user","警告","orange"]].map(x=>`
${icon(x[3])}
${x[0]} ${x[1]} ${x[2]}
● ${x[4]} `).join("")}
${more} › `;
var journey=()=>`${[["扫描未读","已完成","0.3s"],["提取聊天记录","已完成","1.2s"],["合并连续消息","已完成","0.4s"],["发送至 AI","已完成","0.6s"],["生成回复","进行中","2.4s"],["回填企业微信","等待","—"],["自动发送与归档","等待","—"]].map((x,i)=>`
4?"wait":""}">
${i<4?"✓":i+1}
${x[0]} ${x[1]}
${x[2]}
`).join("")}
`;
var statusCells=items=>`${items.map(x=>`
${icon(x[0])}
${x[1]} ${x[2]}
`).join("")}
`;
function autoView(){return `${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`
${icon("send")}自动发送 `,"danger"))}${ribbon([["监听状态","运行中","activity","green"],["排队","3","users"],["今日回复","128","message"],["失败","1","alert","red"]])}
任务队列 3 ${icon("filter")}${icon("sort")}
${queueList()}当前处理任务 ${icon("user")}高瑞@微信 已读取 6 条消息 AI 生成中
复制的聊天内容
16:32:11 请问你们的灵芝孢子粉怎么服用?
16:32:18 每天吃几次比较合适?
16:32:26 有什么注意事项吗?
AI 回复预览
您好!灵芝孢子粉建议每日 1–2 次,每次 2–3 克,温水冲服,饭前半小时或睡前服用效果更佳。 如有特殊体质或正在服药,建议咨询专业医生后再服用哦。
${icon("shield")}置信度 96% ${icon("clock")}模型耗时 2.4s
${icon("stop")}终止本任务 ${icon("user")}转人工
自动化执行状态 总耗时 5.01s ${journey()}
企业微信窗口状态 ${[["folder","窗口","已识别"],["message","当前会话","高瑞@微信"],["monitor","DPI","200%"],["shield","人工保护","5 秒"],["refresh","最小化恢复","已开启"]].map(x=>`
${icon(x[0])}${x[1]} ${x[2]}
`).join("")}
`}
function queueView(){return `${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`
${icon("send")}自动发送 `,"danger"))}${ribbon([["处理中","1","check","","blue"],["等待中","3","clock"],["待重试","1","refresh","orange"],["平均等待","12s","clock","green"]])}
全部任务 全部 处理中 等待中 待重试
${queueList()}
任务详情 客户 高瑞@微信
会话 ID 7bc26546… ${icon("copy")}
已等待 12 秒
重试 0 次
来源消息
✦ AI 生成回复中
${[["入队","16:32:11"],["读取消息","16:32:11"],["请求 AI","16:32:12"],["生成回复","进行中"],["回填发送","等待中"],["完成","等待中"]].map((x,i)=>`
${i<3?"✓":i+1}
${x[0]} ${x[1]} `).join("")}
× 取消任务 ${icon("refresh")}立即重试 ${icon("users")}转人工
`}
function archiveView(){return `${head("会话归档","保存客户上下文、自动回复与回访线索",`
企业微信已连接 AI 服务正常${icon("download")}导出记录 `)}
会话记录 ${icon("user")}高瑞@微信 6 条消息 AI 已回复 ${icon("clock")}16:31 更新
${icon("user")} 16:32:11 请问你们的灵芝孢子粉怎么服用?
${icon("bot")} 16:32:18 每天吃几次比较合适?
${icon("user")} 16:32:26 有什么注意事项吗?
${icon("bot")} 16:32:42 温水冲服,饭前半小时或睡前服用效果更佳。 如有特殊体质或正在服药,建议咨询专业医生后再服用哦。
${icon("user")} 16:33:05 好的,谢谢!
${icon("bot")} 16:33:12 不客气!如有其他问题,随时为您服务~
客户记忆 饮食控制 体重管理 长期回访
客户关注灵芝孢子粉的服用方法与注意事项,希望优化服用时间与剂量,同时注重产品安全性与体质适配。
◔ 上下文 8 轮
业务沉淀 ${icon("user")}
人工跟进 未分配客服
0 次 ·
最近更新 ${activities("复制全部记录")} `}
function aiView(){return `${head("AI 设置","配置模型、客服人格、知识库与工具能力",`
配置已同步▷ 测试 AI ${icon("send")}保存并发布 `)}
基础设置 知识与工具 安全策略
客服人格 AI
✧ 实时预览 您好!我是甄养堂贴心管家, 很高兴为您服务 😊
请问有什么可以帮您的吗?
▰ 企业知识库 1,284个知识片段
● 刚刚同步 ✓ 已连接 ${icon("refresh")}立即同步 ${icon("folder")}管理知识
${icon("shield")} 安全边界 🛡 不做医疗诊断
💊 不修改用药方案
♙ 高风险转人工
当前模式: ${icon("shield")}严格模式
配置状态 ${statusCells([["cloud","云端模型正常","最后检查:刚刚"],["folder","知识库已同步","1,284 个知识片段"],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])} `}
function automationView(){return `${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`
企业微信已识别 规则已生效${icon("save")}保存设置 `)}
${icon("send")} 自动发送 ${icon("users")} 审核后发送
监听与发送策略 ${icon("activity")} ● 规则运行中 ${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`
`).join("")}
企业微信窗口 ▣ 主窗口: ● 已识别
${icon("user")} 当前账号: 甄养堂客服
◎ DPI: 200%
${icon("monitor")} 最小化恢复: ● 已开启
${icon("refresh")}重新检测
人机共存 鼠标静止 5 秒 后恢复
${icon("bot")}AI 运行中 ➤
Ⅱ检测到人工操作 AI 暂停
消息处理 ✓ 同一客户连续消息自动合并
✓ 保留最近 8 轮上下文 8 轮
✓ 一次只处理 1 个会话 1 个
异常与兜底 ${icon("clock")} AI 超时:重试 2 次 2 次
${icon("alert")} 发送失败:自动重试 已开启
${icon("message")} 固定回复:在的,请稍等 编辑
高风险转人工 ● 已开启 ⚕ 诊断 ◇ 用药调整 ¥ 投诉退款
${icon("settings")}规则管理
启动前检查 ${statusCells([["check","企业微信已登录","检查完成"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准","检查完成"],["shield","规则配置有效","检查完成"]])} `}
function logsView(){return `${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`
${icon("refresh")}实时刷新 ${icon("download")}导出日志 ▱ 清空日志 监控中`)}${ribbon([["企业微信","99.9%","message","green"],["AI 成功率","98.6%","brain","","blue"],["平均响应","2.4s","clock","","blue"],["警告","1","alert","red"]])}
实时事件流 全部 连接 识别 AI 发送 警告 ${icon("search")}
${[["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=>`
✓
${x[0]} ● ${icon(x[3])}
${x[1]} ${x[2]}
${x[4]} `).join("")}
系统健康 ${[["企业微信连接","正常"],["AI 服务","正常"],["窗口识别","正常"]].map(x=>`
${icon("check")}
${x[0]} ${x[1]} `).join("")}
${icon("clock")}运行时长 02:18:46
当前回复链路 ${[["activity","扫描未读","0.3s"],["message","读取消息","1.2s"],["brain","请求 AI","2.4s"],["send","回填发送","0.6s"]].map(x=>`
${icon(x[0])}
${x[1]} ${x[2]} `).join("")}
事件详情 ${icon("user")}
人工保护 检测到人工鼠标操作,自动回复暂停 5 秒
模块:人机共存 会话:高瑞@微信 已自动恢复
最近活动 ${activities("查看完整记录")} `}
function systemView(){return `${head("系统设置","管理软件启动、数据存储、通知与系统维护",`
当前版本 v2.0 ⓘ ${icon("refresh")}检查更新 ${icon("save")}保存设置 `)}
基础设置 ${icon("activity")}
启动后自动监听
${icon("log")}
关闭窗口时保持后台运行
◎界面语言: 简体中文 ⌄
${icon("monitor")}缩放比例: 自动 ⌄
数据与存储 286MB 已使用
● 会话归档 128 MB
● 运行日志 86 MB
● 缓存数据 72 MB
数据保留 90 天 ⌄
${icon("folder")}打开目录 ${icon("cloud")}立即备份 ⌁ 清理缓存
通知提醒 ♟
发送失败提醒 消息发送失败,已加入重试队列
16:33●
安全与隐私 ${icon("shield")}敏感信息脱敏: 已开启 ●
${icon("lock")}本地数据加密: 已开启 ●
${icon("lock")}日志隐藏聊天正文: 已开启 ●
${icon("log")}退出时清理剪贴板: 已开启 ●
网络与服务 ${icon("cloud")}云端 AI: 已连接 ●
${icon("folder")}企业知识库: 已连接 ●
⌁网络延迟: 28ms ●
⌁ 连接测试
维护与诊断 ${icon("refresh")}
检查更新 已是最新版本
${icon("shield")}
查看许可信息 查看软件许可详情
系统状态 ${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","数据已备份","最近备份:16:33:28"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务运行中","系统运行状态良好"]])} `}
var views={auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView};
var glyphIcons=[["✦","sparkles"],["✧","sparkles"],["✣","brain"],["⚑","flag"],["▣","clipboard"],["☎","phone"],["⌕","search"],["▤","clipboard"],["◇","package"],["🛡","shield"],["💊","pill"],["♙","users"],["⌛","hourglass"],["◎","globe"],["⚕","shield"],["¥","coins"],["⌁","wifi"],["♟","bell"],["▰","tray"],["▷","play"],["↻","refresh"],["➤","cursor"],["Ⅱ","pause"],["◔","clock"],["ⓘ","info"],["⌄","chevron"]];
function polishedIcons(html){return glyphIcons.reduce((value,[glyph,name])=>value.replaceAll(glyph,icon(name)),html)}
function render(name){
if(!views[name])name="auto";
document.querySelectorAll("[data-view]").forEach(b=>b.classList.toggle("active",b.dataset.view===name));
document.getElementById("main").innerHTML=polishedIcons(views[name]());
const listen=document.querySelector('[data-action="toggle-listen"]');
if(listen)listen.innerHTML=listening?`${icon("pause")}暂停监听`:`${icon("play")}恢复监听`;
location.hash=name;
}
function toast(msg="操作已完成"){const t=document.getElementById("toast");t.textContent=msg;t.classList.add("show");clearTimeout(window.__toast);window.__toast=setTimeout(()=>t.classList.remove("show"),1600)}
document.addEventListener("click",e=>{if(window.STATE&&window.STATE.ready&&!window.STATE.demo&&bridge())return;const nav=e.target.closest("[data-view]");if(nav){render(nav.dataset.view);return}const action=e.target.closest("[data-action]");if(action){const a=action.dataset.action;if(a==="toggle-listen"){listening=!listening;document.getElementById("orb").classList.toggle("paused",!listening);document.getElementById("orb-label").textContent=listening?"监听中":"已暂停";render(location.hash.slice(1));toast(listening?"已恢复监听":"监听已暂停")}else if(a==="logs")render("logs");else if(a==="clear-logs"){const list=document.getElementById("log-list");if(list)list.innerHTML='日志已清空
';toast("日志已清空")}else toast()}const sw=e.target.closest(".switch");if(sw)sw.classList.toggle("on");const pill=e.target.closest(".filter-pill,.tab");if(pill){[...pill.parentElement.children].forEach(x=>x.classList.remove("active"));pill.classList.add("active")}});
function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById("stage").style.setProperty("--scale",String(s))}
const _queueList = queueList;
const _activities = activities;
const _connected = connected;
const _journey = journey;
connected = function(extra="", primary=""){
const wecom = STATE.wecomLabel || (STATE.wecomConnected ? "企业微信已连接" : "企业微信未连接");
const ai = STATE.aiLabel || (STATE.aiOk === false ? "AI 服务异常" : "AI 服务正常");
const listenLabel = STATE.listening ? `${icon("pause")}暂停监听` : `${icon("play")}恢复监听`;
const wecomLed = STATE.wecomConnected ? "" : "off";
const aiLed = STATE.aiOk === false ? "off" : "";
return ` ${esc(wecom)} ${esc(ai)} ${extra}${listenLabel} `;
};
queueList = function(archive=false){
if (STATE.demo) return _queueList(archive);
const rows = archive ? (STATE.archives || []) : (STATE.people || []);
if (!rows.length){
const title = archive ? "暂无会话档案" : "当前没有待处理任务";
const hint = archive ? "新的客户会话会自动出现在这里" : "所有客户消息均已完成";
return `${icon("check")}
${title} ${hint} `;
}
return `${rows.map((p,i)=>{
const selectedKey = archive ? STATE.selectedArchive : STATE.selectedKey;
const active = p.active || p.key === selectedKey || (!selectedKey && i===0);
const preview = archive ? (p.preview || "会话已归档") : `
${esc(p.status || "")}`;
const meta = archive
? `
${esc(p.time || "")} ${esc(p.badge || "AI 已回复")} `
: `
${esc(p.wait || "")} ${icon("message")} ${p.bubbles || 0} `;
return `
${esc(p.avatar || "客")}
${esc(p.name || "")} ${preview}
${meta}
`;
}).join("")}
`;
};
activities = function(more="查看完整日志"){
const items = STATE.demo ? null : (STATE.activities || []);
if (STATE.demo || !items) return _activities(more);
const cards = items.length ? items.map(x=>{
const tone = x.tone || "";
const color = tone==="orange"?"var(--orange)":tone==="blue"?"var(--blue)":"var(--green)";
return `${icon(x.icon || "activity")}
${esc(x.time || "")} ${esc(x.title || "")} ${esc(x.detail || "")}
● ${esc(x.state || "")} `;
}).join("") : `暂无执行记录
`;
const action = more === "复制全部记录" ? "copy-archive" : (more === "查看完整记录" ? "full-log" : "logs");
return `${cards}${esc(more)} ›
`;
};
journey = function(){
if (STATE.demo) return _journey();
const items = STATE.journey || [];
if (!items.length){
return `等待新的自动回复任务
`;
}
return `${STATE.journey.map((x,i)=>{
const mode = x.mode || (x.active ? "active" : x.wait ? "wait" : "");
const token = x.done ? "✓" : String(i+1);
return `
${token}
${esc(x.title)} ${esc(x.status)}
${esc(x.time)}
`;
}).join("")}
`;
};
const _autoView = autoView;
const _queueView = queueView;
const _archiveView = archiveView;
const _aiView = aiView;
const _automationView = automationView;
const _logsView = logsView;
const _systemView = systemView;
function liveRibbonAuto(){
const m = STATE.metrics || {};
return ribbon([
["监听状态", m.listen || (STATE.listening ? "运行中" : "已停止"), "activity", STATE.listening ? "green" : "", "", "listen"],
["排队", m.queue || "0", "users", "", "", "queue"],
["今日回复", m.today || "0", "message", "", "", "today"],
["失败", m.failed || "0", "alert", Number(m.failed) > 0 ? "red" : "", "", "failed"],
]);
}
function liveRibbonQueue(){
const m = STATE.metrics || {};
return ribbon([
["处理中", m.processing || "0", "check", "", "blue", "processing"],
["等待中", m.waiting || "0", "clock", "", "", "waiting"],
["待重试", m.retry || "0", "refresh", "orange", "", "retry"],
["平均等待", m.avgWait || "0s", "clock", "green", "", "avgWait"],
]);
}
function liveRibbonLogs(){
const m = STATE.metrics || {};
return ribbon([
["企业微信", m.wecomHealth || "待连接", "message", STATE.wecomConnected ? "green" : "", "", "wecomHealth"],
["AI 成功率", m.aiRate || "—", "brain", "", "blue", "aiRate"],
["平均响应", m.avgResp || "—", "clock", "", "blue", "avgResp"],
["警告", m.warnings || "0", "alert", Number(m.warnings) > 0 ? "red" : "", "", "warnings"],
]);
}
function currentTaskCard(){
const task = STATE.task;
if (!task){
return ``;
}
const messages = (task.messages || []).map(x=>`${esc(x.time || "")} ${esc(x.text || "")}
`).join("") || `等待读取客户消息
`;
const answer = task.answer ? `${esc(task.answer).replace(/\n/g," ")}
` : `尚未生成回复
`;
// 审核模式下回复只贴进输入框、不按回车。没有这个按钮的话,草稿就永远停在
// 那儿——界面显示"等待人工审核",但界面上根本没有"通过"这个动作,只能切到
// 企业微信里自己按回车。用户看到的就是"AI 回复了、框里也有字,就是不发"。
const approveBtn = task.awaitingReview
? `${icon("send")}通过并发送 `
: "";
// reviewReason 有值 = 自动发送模式下这一条单独触发了选择性审核规则(或被
// 裁判判成高风险),不是全局审核模式——文案要说清楚"为什么偏偏是这一条",
// 不然运营会以为审核开关又被打开了,去systemwide 排查一圈却什么也查不到。
const reviewHint = task.awaitingReview
? task.reviewReason
? `${icon("shield")}${esc(task.reviewReason)},已自动停下等待人工确认(其余会话仍在自动发送):确认无误后点「通过并发送」;也可以直接在企业微信里改完自己按回车。
`
: `${icon("shield")}当前是审核后发送 模式:回复已填入企业微信输入框,确认无误后点「通过并发送」;也可以直接在企业微信里改完自己按回车。
`
: "";
// 输入框里有人打的字:机器人一个字都不会动它,也不会在后面接着打(两个人的
// 话会拼成一句发出去)。这一条会被跳过,不占用轮次。不写出来的话,界面上
// 只是"这个会话不动了",没人会想到是自己在企微里留了半句没发。
const foreignDraftHint = task.foreignDraft
? `${icon("user")}企业微信输入框里有人工草稿「${esc(String(task.foreignDraft).slice(0, 20))}」,已暂停 自动回复并跳过这个会话,不会覆盖你写的内容。把草稿发出去或清空,就会自动恢复。
`
: "";
return `当前处理任务 ${icon("user")}${esc(task.name || "")} ${esc(task.messageCount || "已读取消息")} ${esc(task.action || "")}
复制的聊天内容
${messages}
AI 回复预览
${answer}${icon("shield")}置信度 ${esc(task.confidence || "—")} ${icon("clock")}模型耗时 ${esc(task.latency || "—")}
${approveBtn}${icon("stop")}终止本任务 ${icon("user")}转人工
${reviewHint}${foreignDraftHint} `;
}
function windowStatusCard(){
const stats = STATE.windowStats || [["folder","窗口", STATE.wecomConnected ? "已识别" : "未识别"],["message","当前会话", (STATE.task && STATE.task.name) || "—"],["monitor","DPI", STATE.dpi || "—"],["shield","人工保护", STATE.idleLabel || "—"],["refresh","最小化恢复", "已开启"]];
return `
企业微信窗口状态 ${stats.map(x=>`
${icon(x[0])}${esc(x[1])} ${esc(x[2])}
`).join("")}
`;
}
function queueDetailPanel(){
const task = STATE.task;
if (!task){
return ``;
}
const steps = task.steps || [["入队","等待中"],["读取消息","等待中"],["请求 AI","等待中"],["生成回复","等待中"],["回填发送","等待中"],["完成","等待中"]];
const active = Number(task.stepIndex ?? -1);
const process = steps.map((x,i)=>`${i
${esc(x[0])} ${esc(x[1])} `).join("");
const message = (task.messages && task.messages[0]) || {time:"", text:"队列为空时,选择任务后会在这里显示来源消息。"};
return `
任务详情 客户 ${esc(task.name || "暂无选中任务")}
会话 ID ${esc(task.sessionDisplay || task.sessionId || "--")} ${icon("copy")}
已等待 ${esc(task.wait || "--")}
重试 ${esc(task.retries || "0 次")}
来源消息
${esc(message.time || "")} ${esc(message.text || "")}
${icon("sparkles")} ${esc(task.action || "等待新任务")}
${process}
× 取消任务 ${icon("refresh")}立即重试 ${icon("users")}转人工
`;
}
function schedulePanel(){
const s = (STATE.task && STATE.task.schedule) || {priority:"—", eta:"—", protect:"—", retry:"—"};
return `
调度信息 ${icon("flag")}
优先级 ${esc(s.priority)}
${icon("clock")}
预计完成 ${esc(s.eta)}
${icon("shield")}
人工保护 ${esc(s.protect)}
${icon("refresh")}
下次重试 ${esc(s.retry)}
`;
}
autoView = function(){
if (STATE.demo) return _autoView();
const count = (STATE.people || []).length;
const waiting = (STATE.metrics && STATE.metrics.waiting) || "0";
return `
${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`
${icon("send")}自动发送 `,"danger"))}${liveRibbonAuto()}
任务队列 ${esc(waiting)} ${icon("filter")} ${icon("sort")}
${queueList()}${currentTaskCard()}
自动化执行状态 ${esc(STATE.journeyTotal || "")} ${journey()}${windowStatusCard()}
`;
};
queueView = function(){
if (STATE.demo) return _queueView();
const count = (STATE.people || []).length;
const filter = STATE.queueFilter || "全部";
const pills = ["全部","处理中","等待中","待重试"].map(name=>`
${name} `).join("");
return `
${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`
${icon("send")}自动发送 `,"danger"))}${liveRibbonQueue()}
全部任务 ${pills}
${queueList()}${queueDetailPanel()}${schedulePanel()}
`;
};
archiveView = function(){
if (STATE.demo) return _archiveView();
const rows = STATE.archives || [];
const chat = STATE.chat || [];
const selected = rows.find(x=>x.key===STATE.selectedArchive) || rows[0] || {};
const chatHtml = chat.length ? chat.map(x=>`
${icon(x.role==="assistant"?"bot":"user")} ${esc(x.time || "")} ${esc(x.text || "")}
`).join("") : `
选择会话后显示聊天记录
`;
const tags = (STATE.memory && STATE.memory.tags || []).map((t,i)=>`
${esc(t)} `).join("") || `
暂无标签 `;
const settle = (STATE.settle || []).map(x=>`
${x.iconHtml || icon("clipboard")}
${esc(x.title)} ${esc(x.hint)}
${esc(x.value)} `).join("");
const filter = STATE.archiveFilter || "全部会话";
const dateValue = STATE.archiveDate || "";
return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`
${esc(STATE.wecomLabel || "企业微信")} ${esc(STATE.aiLabel || "AI 服务")}${icon("download")}导出记录 `)}
会话记录 ${icon("user")}${esc(selected.name || "未选择")} ${esc(selected.count || "0")} 条消息 ${esc(selected.badge || "")} ${icon("clock")}${esc(selected.time || "")}
${chatHtml}
客户记忆 ${tags}
${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。")}
${icon("clock")} 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮
最近更新 ${activities("复制全部记录")}`;
};
aiView = function(){
if (STATE.demo) return _aiView();
const ai = STATE.ai || {};
const toolsOn = ai.tools || [true, true, true];
const names = ["挂号登记","客户查询","回访记录"];
const toolIcons = ["clipboard","search","clipboard"];
const tab = STATE.aiTab || "基础设置";
const tabs = ["基础设置","知识与工具","安全策略"].map(n=>`
${n} `).join("");
const lengthIndex = Number(ai.replyLengthIndex ?? 1);
const lengthLabel = ai.replyLength || ["短","中等","长"][lengthIndex] || "中等";
const tempVal = Number(ai.temperature || 0.35);
return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`
配置已同步${icon("play")} 测试 AI ${icon("send")}保存并发布 `)}
${tabs}
客服人格 AI
${icon("sparkles")} 实时预览 您好!我是甄养堂${esc(ai.name || "贴心管家")}, 很高兴为您服务 😊
请问有什么可以帮您的吗?
${icon("brain")} 模型引擎 当前模型 ${icon("sparkles")} ${esc(ai.model || "云端模型")}
${icon("tray")} 企业知识库 ${esc(ai.knowledge || "0")}个 MCP 服务
● ${esc(ai.syncAt || "尚未同步")} ✓ ${Number(ai.knowledge||0)?"已连接":"未配置"} ${icon("refresh")}立即同步 ${icon("folder")}管理知识
${icon("link")} MCP 配置 服务数量 ${esc(ai.knowledge || "0")}
工具开关 ${toolsOn.filter(Boolean).length ? "已启用" : "未启用"}
${icon("settings")}编辑服务器 JSON
${icon("shield")} 安全边界 ${icon("shield")} 不做医疗诊断
${icon("pill")} 不修改用药方案
${icon("users")} 高风险转人工
转人工规则 ${icon("shield")} 诊断 ${icon("package")} 用药调整 ${icon("coins")} 投诉退款
${icon("settings")}规则管理
当前模式 ${icon("shield")}严格模式
配置状态 ${statusCells([["cloud", STATE.aiOk ? "云端模型正常" : "AI 服务异常", `最后同步:${esc(ai.syncAt || "尚未同步")}`],["folder","MCP 工具配置",`${esc(ai.knowledge || "0")} 个服务`],["folder",`${esc(ai.toolCount || 0)} 个工具可用`, toolsOn.filter(Boolean).length ? "已启用" : "未启用"],["shield","安全策略已生效","严格模式"]])}`;
};
// Keep the live WebChannel pages on the exact HTML reference structure. Data
// and actions are dynamic, but no extra cards or alternate tab layouts are
// introduced here.
archiveView = function(){
if (STATE.demo) return wireDemoArchive(_archiveView());
const rows = STATE.archives || [];
const chat = STATE.chat || [];
const selected = rows.find(x=>x.key===STATE.selectedArchive) || rows[0] || {};
const chatHtml = chat.length
? chat.map(x=>`
${icon(x.role==="assistant"?"bot":"user")} ${esc(x.time || "")} ${esc(x.text || "").replace(/\n/g," ")}
`).join("")
: `
选择会话后显示聊天记录
`;
const tags = (STATE.memory && STATE.memory.tags || []).map((tag,index)=>`
${esc(tag)} `).join("") || `
暂无标签 `;
const settleTokens = ["▣","☎",icon("user")];
const settleStyles = ["","background:linear-gradient(145deg,#9d7cff,#6e5be5)","background:linear-gradient(145deg,#ffc65a,#ff941f)"];
const settle = (STATE.settle || []).map((item,index)=>`
${settleTokens[index] || icon("clipboard")}
${esc(item.title)} ${esc(item.hint)}
${esc(item.value)} `).join("");
const filter = STATE.archiveFilter || "全部会话";
const now = new Date();
const currentDate = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`;
const dateLabel = STATE.archiveDate || currentDate;
const filterButtons = ["全部会话","今日","AI 已回复","已转人工"].map(name=>`
${name}${name==="全部会话"?"⌄":""} `).join("");
return `
${head("会话归档","保存客户上下文、自动回复与回访线索",`
${esc(STATE.wecomLabel || "企业微信")} ${esc(STATE.aiLabel || "AI 服务")}${icon("download")}导出记录 `)}
会话记录 ${icon("user")}${esc(selected.name || "未选择")} ${esc(selected.count || "0")} 条消息 ${esc(selected.badge || "")} ${icon("clock")}${esc(selected.time || "")} 更新
${chatHtml}
客户记忆 ${tags}
${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。").replace(/\n/g," ")}
◔ 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮
最近更新 ${activities("复制全部记录")}`;
};
aiView = function(){
if (STATE.demo) return wireDemoAi(_aiView());
const ai = STATE.ai || {};
const toolsOn = ai.tools || [true,true,true];
const names = ["挂号登记","客户查询","回访记录"];
const toolTokens = ["▣","⌕","▤"];
const tab = STATE.aiTab || "基础设置";
const tabs = ["基础设置","知识与工具","安全策略"].map(name=>`
${name} `).join("");
const contextValue = Math.max(1,Number(ai.context || 8));
const lengthIndex = Math.max(0,Math.min(2,Number(ai.replyLengthIndex ?? 1)));
const lengthLabel = ["短","中等","长"][lengthIndex];
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 plan = ai.plan || {};
const knowledgeStatus = connectedKnowledge ? "知识库已同步" : "知识库未连接";
const toolStatus = toolCount === 3 ? "全部运行正常" : `${toolCount} 个已启用`;
const reviewRules = ai.reviewRules || [];
const reviewRulesHtml = reviewRules.length
? reviewRules.map(r=>`
${icon("shield")} ${esc(r.label)} ${r.enabled?"已启用":"已停用"}
${esc((r.keywords||[]).slice(0,6).join("、"))}${(r.keywords||[]).length>6?" 等":""}
`).join("")
: `
暂无关键词规则
`;
const blanketReview = (STATE.sendMode || "auto") === "review";
return `
${head("AI 设置","配置模型、客服人格、知识库与工具能力",`
${configLabel}▷ 测试 AI ${icon("send")}保存并发布 `)}
${tabs}
客服人格 AI
✧ 实时预览 您好!我是甄养堂${esc(ai.name || "贴心管家")}, 很高兴为您服务 😊
请问有什么可以帮您的吗?
✣ 模型编排 由后台决定,本机只执行 答题模型: ${esc(plan.answer || "后台未配置")}
裁判模型: ${esc(plan.judge || "未启用")}
裁判模式: ${esc(plan.mode || "—")}
编排版本: ${plan.version ? "v" + esc(plan.version) : "—"}
上下文轮数: ${esc(ai.context || 8)} 轮
网关:${esc(plan.gateway || "未下发——模型调用会失败,请去「系统设置 → 后台连接」")}
最后同步:${esc(plan.syncedAt || "尚未同步")}
模型接口、密钥、温度、最大 tokens 现在统一在后台的「AI 模型 → 模型清单 / 角色编排」 里配。 上下文轮数和 MCP 在「桌面端 → 客户端配置」 里配,随配置下发——在这台机器上改会被下一次同步覆盖。
▰ 企业知识库 ${esc(knowledgeCount)}个知识片段
● ${esc(syncLabel)} ✓ ${connectedKnowledge?"已连接":"未连接"} ${icon("refresh")}立即同步 ${icon("folder")}管理知识
◇ MCP 工具 单次最多调用 ${esc(ai.mcpRounds || 5)} 轮 · 由后台下发 工具清单和调用轮数在后台的「桌面端 → 客户端配置 → MCP 服务器」 里配,随配置下发。 在这台机器上改会被下一次同步覆盖,所以这里只展示不修改。
${icon("shield")} 选择性审核规则 由后台决定,本机只执行 ${reviewRulesHtml}
命中任意一条,这一条回复才会停下来等人工确认;其余照常自动发送。模型裁判判定为高风险时同样会转人工,那一条不在这份清单里、关不掉。
在后台的「桌面端 → 客户端配置 → 审核规则」 里配置,随配置下发。
发送模式 ${icon("shield")}${blanketReview?"全部审核后发送":"自动发送(命中规则单独审核)"}
在「自动化设置」页顶部切换;这个总开关和上面的选择性规则是 OR 的关系,任一个成立都会拦下。
配置状态 ${statusCells([["cloud",STATE.aiOk===false?"AI 服务异常":"云端模型正常","最后检查:刚刚"],["folder",knowledgeStatus,`${esc(knowledgeCount)} 个知识片段`],["folder",`${toolCount} 个工具可用`,toolStatus],["shield","安全策略已生效","严格模式"]])}`;
};
automationView = function(){
if (STATE.demo) return _automationView();
const a = STATE.automation || {};
const autoOn = (STATE.sendMode || "auto") === "auto";
return `
${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`
${esc(a.wecom || "企业微信未识别")} 规则已生效${icon("save")}保存设置 `)}
${icon("send")} 自动发送 ${icon("users")} 审核后发送
监听与发送策略 ${icon("activity")} ● 规则运行中 ${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`
`).join("")}
企业微信窗口 ${icon("clipboard")} 主窗口: ● ${esc(a.wecom || "未识别")}
${icon("user")} 当前账号: ${esc(a.account || "—")}
${icon("globe")} DPI: ${esc(STATE.dpi || "—")}
${icon("monitor")} 最小化恢复: ● 已开启
${icon("refresh")}重新检测
消息处理 ✓ 同一客户连续消息自动合并
✓ 保留最近 轮上下文 ${esc(a.context || 8)} 轮
✓ 一次只处理 1 个会话 1 个
异常与兜底 ${icon("clock")} AI 超时:${esc(a.timeout || 120)} 秒后重试 2 次
${icon("alert")} 发送失败:自动重试 已开启
${icon("message")} 固定回复:${esc(a.reply || "在的,请稍等")} 编辑
高风险转人工 ● 已开启 ${icon("shield")} 诊断 ${icon("package")} 用药调整 ${icon("coins")} 投诉退款
${icon("settings")}规则管理
启动前检查 ${statusCells([["check","企业微信已登录", STATE.wecomConnected ? "检查完成" : "等待连接"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准", STATE.wecomConnected ? "检查完成" : "待校准"],["shield","规则配置有效","检查完成"]])}`;
};
logsView = function(){
if (STATE.demo) return _logsView();
const events = STATE.logs || [];
const list = events.length ? events.map(x=>`
✓
${esc(x.time)} ● ${icon(x.icon || "activity")}
${esc(x.title)} ${esc(x.detail)}
${esc(x.state || "")} `).join("") : `
当前没有运行记录
`;
const ex = STATE.exceptions || {};
const detail = STATE.logDetail || {title:"等待事件", text:"开始监听后,这里会显示最近一条运行事件。", tags:[]};
const chain = (STATE.chain && STATE.chain.length ? STATE.chain : [{icon:"activity",title:"扫描未读",time:""},{icon:"message",title:"读取消息",time:""},{icon:"brain",title:"请求 AI",time:""},{icon:"send",title:"回填发送",time:""}]);
return `
${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`
${icon("refresh")}实时刷新 ${icon("download")}导出日志 ${icon("tray")} 清空日志 ${esc(STATE.statusLabel || "监控中")}`)}${liveRibbonLogs()}
实时事件流 ${["全部","连接","识别","AI","发送","警告"].map(n=>`${n} `).join("")}${icon("search")}
${list}
系统健康 ${[["企业微信连接", STATE.wecomConnected ? "正常" : "待连接"],["AI 服务", STATE.aiOk ? "正常" : "异常"],["窗口识别", STATE.wecomConnected ? "正常" : "待监听"]].map(x=>`
${icon("check")}
${x[0]} ${x[1]} `).join("")}
${icon("clock")}运行时长 ${esc(STATE.runtime || "00:00:00")}
当前回复链路 ${chain.map((x,i)=>`
${icon(x.icon || "activity")}
${esc(x.title)} ${esc(x.time || "")} `).join("")}
事件详情 ${icon("user")}
${esc(detail.title)} ${esc(detail.text)}
${(detail.tags||[]).map(t=>`${esc(t)} `).join("")}
异常追踪 ${icon("alert")}
发送失败${esc(ex.sendFail || 0)}
${icon("clock")}
模型超时${esc(ex.timeout || 0)}
${icon("monitor")}
窗口丢失${esc(ex.windowLost || 0)}
查看异常 ›
最近活动 ${activities("查看完整记录")}`;
};
systemView = function(){
if (STATE.demo) return _systemView();
const s = STATE.system || {};
const on = k => s[k] ? "on" : "";
return `
${head("系统设置","管理软件启动、数据存储、通知与系统维护",`
当前版本 v${esc(s.version || "2.0")} ${icon("info")} ${icon("refresh")}检查更新 ${icon("save")}保存设置 `)}
基础设置 ${icon("activity")}
启动后自动监听
${icon("log")}
关闭窗口时保持后台运行
${icon("globe")}界面语言 简体中文
${icon("monitor")}缩放比例 ${["自动","100%","125%","150%","200%"].map(n=>`${n} `).join("")}
数据与存储 ${esc(s.storage || "—")}已使用
● 会话归档 ${esc(s.archiveSize || "—")}
● 运行日志 ${esc(s.logSize || "—")}
● 缓存数据 ${esc(s.cacheSize || "—")}
数据保留 天
${icon("folder")}打开目录 ${icon("cloud")}立即备份 ${icon("wifi")} 清理缓存
通知提醒 ${icon("hourglass")}
模型超时提醒
${icon("bell")}
发送失败提醒 消息发送失败,已加入重试队列
${esc(s.noticeTime || "—")}●
后台连接 后台地址
${icon("cloud")}配置同步: ${esc(s.syncState || "未检测")}
${icon("wifi")}模型网关: ${esc(s.gatewayState || "未同步")}
${esc(s.gatewayUrl || "网关地址由后台下发,连接成功后自动填入")}
${icon("wifi")} 保存并连接 模型、密钥和角色编排都在后台配,本机只需要填这一个地址
维护与诊断 ${icon("refresh")}
检查更新 ${esc(s.updateLabel || "已是最新版本")}
${icon("clipboard")}
导出诊断 导出系统诊断报告
${icon("refresh")}
恢复默认设置 重置所有设置项
${icon("shield")}
查看许可信息 查看软件许可详情
系统状态 ${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","最近备份", esc(s.lastBackup || "尚未备份")],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务", STATE.listening ? "运行中" : "已停止"]])}`;
};
views = {auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView};
function syncOrb(){
const orb = document.getElementById("orb");
const label = document.getElementById("orb-label");
if (orb) orb.classList.toggle("paused", !STATE.listening);
if (label) label.textContent = STATE.statusLabel || (STATE.listening ? "监听中" : "已停止");
// 侧栏这颗球就是开始/暂停按钮,提示语要跟着状态走
const listener = document.getElementById("listener");
if (listener) listener.title = STATE.listening ? "点击暂停监听" : "点击开始监听";
}
function coreFingerprint(state){
const sys = state.system || {};
const ai = state.ai || {};
const auto = state.automation || {};
const metrics = state.metrics || {};
const view = state.view || "auto";
const common = {
view: state.view,
demo: state.demo,
listening: state.listening,
wecomConnected: state.wecomConnected,
aiOk: state.aiOk,
wecomLabel: state.wecomLabel,
aiLabel: state.aiLabel,
};
const people = (state.people || []).map(p => [
p.key, p.name, p.status, p.bubbles, p.avatar, p.tone, p.dot
]);
const task = state.task ? {
key: state.task.key,
name: state.task.name,
sessionId: state.task.sessionId,
sessionDisplay: state.task.sessionDisplay,
action: state.task.action,
stepIndex: state.task.stepIndex,
answer: state.task.answer,
messageCount: state.task.messageCount,
confidence: state.task.confidence,
messages: (state.task.messages || []).map(message => [message.time, message.text]),
steps: (state.task.steps || []).map(step => [step[0], step[1]]),
} : null;
const metricModes = [Number(metrics.failed) > 0, Number(metrics.warnings) > 0];
let page;
if (view === "auto") {
page = {
selectedKey: state.selectedKey,
queueFilter: state.queueFilter,
queueSort: state.queueSort,
people,
task,
journey: (state.journey || []).map(j => [j.title, j.status, j.mode, j.done, j.active]),
windowStats: state.windowStats,
metricModes,
};
} else if (view === "queue") {
page = {
selectedKey: state.selectedKey,
queueFilter: state.queueFilter,
queueSort: state.queueSort,
people,
task,
metricModes,
};
} else if (view === "archive") {
page = {
archiveFilter: state.archiveFilter,
archiveQuery: state.archiveQuery,
archiveDate: state.archiveDate,
selectedArchive: state.selectedArchive,
archives: (state.archives || []).map(a => [
a.key, a.badge, a.count, a.name, a.time, a.preview, a.avatar, a.tone
]),
chat: (state.chat || []).map(message => [message.role, message.time, message.text]),
memory: state.memory,
settle: state.settle,
};
} else if (view === "ai") {
page = {aiTab: state.aiTab, ai};
} else if (view === "automation") {
page = {sendMode: state.sendMode, dpi: state.dpi, auto, tray: sys.tray};
} else if (view === "logs") {
page = {
statusLabel: state.statusLabel,
eventFilter: state.eventFilter,
eventQuery: state.eventQuery,
exceptions: state.exceptions,
logDetail: state.logDetail,
chain: (state.chain || []).map(item => [item.icon, item.title]),
metricModes,
};
} else {
const {
storage: _storage,
archiveSize: _archiveSize,
logSize: _logSize,
cacheSize: _cacheSize,
noticeTime: _noticeTime,
...systemStructure
} = sys;
page = {system: systemStructure};
}
return JSON.stringify({common, page});
}
function logsFingerprint(state){
return JSON.stringify((state.logs || []).map(x => [x.time, x.title, x.detail, x.state]));
}
function logRowsHtml(events){
events = (events || []).slice(-5);
if (!events.length){
return `
当前没有运行记录
`;
}
return events.map(x=>`
✓
${esc(x.time)} ● ${icon(x.icon || "activity")}
${esc(x.title)} ${esc(x.detail)}
${esc(x.state || "")} `).join("");
}
function wireDemoArchive(html){
let rowIndex = 0;
return html
.replace('data-action="toast">' + icon("download") + '导出记录', 'data-action="export-archive">' + icon("download") + '导出记录')
.replace('
', '
')
.replace('
全部会话⌄ ', '
全部会话⌄ ')
.replace('
今日 ', '
今日 ')
.replace('
AI 已回复 ', '
AI 已回复 ')
.replace('
已转人工 ', '
已转人工 ')
.replace('
2026-08-18 □ ', '
2026-08-18 □ ')
.replace(/
/g, (_match, classes) => `
`)
.replace('
' + icon("refresh") + ' ', '
' + icon("refresh") + ' ')
.replace('data-action="logs">复制全部记录', 'data-action="copy-archive">复制全部记录');
}
function wireDemoAi(html){
const activeTab = STATE.aiTab || "基础设置";
let toolIndex = 0;
return html
.replace('
配置已同步', '
配置已同步')
.replace('
▷ 测试 AI ', '
▷ 测试 AI ')
.replace('
' + icon("send") + '保存并发布 ', '
' + icon("send") + '保存并发布 ')
.replace('
基础设置 知识与工具 安全策略
', `
${["基础设置","知识与工具","安全策略"].map(name=>`${name} `).join("")}
`)
.replace('
', '
')
.replace('