352 lines
43 KiB
Python
352 lines
43 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Build console-app.js from the original mock script plus live-data hooks."""
|
||
from pathlib import Path
|
||
|
||
root = Path(__file__).resolve().parent
|
||
script = (root / "_orig_script.js").read_text(encoding="utf-8")
|
||
for old, new in [
|
||
("const people=", "var people="),
|
||
("let listening=true", "var listening=true"),
|
||
("const head=", "var head="),
|
||
("const connected=", "var connected="),
|
||
("const ribbon=", "var ribbon="),
|
||
("const queueList=", "var queueList="),
|
||
("const activities=", "var activities="),
|
||
("const journey=", "var journey="),
|
||
("const statusCells=", "var statusCells="),
|
||
("const views=", "var views="),
|
||
("const glyphIcons=", "var glyphIcons="),
|
||
]:
|
||
script = script.replace(old, new, 1)
|
||
|
||
script = script.replace(
|
||
"function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById(\"stage\").style.setProperty(\"--scale\",String(s))}addEventListener(\"resize\",fit);fit();render((location.hash||\"#auto\").slice(1));",
|
||
"function fit(){const s=Math.min(innerWidth/1630,innerHeight/965);document.getElementById(\"stage\").style.setProperty(\"--scale\",String(s))}",
|
||
1,
|
||
)
|
||
|
||
header = r'''
|
||
const icon=n=>`<svg class="ico"><use href="#i-${n}"/></svg>`;
|
||
window.STATE = window.STATE || { view: "auto", demo: true, ready: false, listening: true };
|
||
function esc(value){
|
||
return String(value ?? "").replace(/[&<>"']/g, ch => ({
|
||
"&":"&","<":"<",">":">",'"':""","'":"'"
|
||
}[ch]));
|
||
}
|
||
function bridge(){ return window.qtBridge || null; }
|
||
function callBridge(name, ...args){
|
||
const api = bridge();
|
||
if (!api || typeof api[name] !== "function") return false;
|
||
api[name](...args);
|
||
return true;
|
||
}
|
||
'''
|
||
|
||
# Original script starts with icon=...; drop that duplicate.
|
||
if script.lstrip().startswith("const icon="):
|
||
script = script.lstrip()
|
||
script = script[script.index(";") + 1 :]
|
||
|
||
footer = r'''
|
||
const _queueList = queueList;
|
||
const _activities = activities;
|
||
const _connected = connected;
|
||
const _journey = journey;
|
||
|
||
connected = function(extra="", primary=""){
|
||
const wecom = STATE.wecomLabel || (STATE.wecomConnected ? "企业微信已连接" : "企业微信未连接");
|
||
const ai = STATE.aiLabel || (STATE.aiOk === false ? "AI 服务异常" : "AI 服务正常");
|
||
const listenLabel = STATE.listening ? `${icon("pause")}暂停监听` : `${icon("play")}恢复监听`;
|
||
return `<span class="btn"><i class="led"></i>${esc(wecom)}</span><span class="btn"><i class="led"></i>${esc(ai)}</span>${extra}<button class="btn ${primary}" data-action="toggle-listen">${listenLabel}</button>`;
|
||
};
|
||
|
||
queueList = function(archive=false){
|
||
if (STATE.demo) return _queueList(archive);
|
||
const rows = archive ? (STATE.archives || []) : (STATE.people || []);
|
||
if (!rows.length){
|
||
const title = archive ? "暂无会话档案" : "当前没有待处理任务";
|
||
const hint = archive ? "新的客户会话会自动出现在这里" : "所有客户消息均已完成";
|
||
return `<div class="empty-board"><div><div class="metric-icon green" style="margin:0 auto 12px">${icon("check")}</div><strong style="color:var(--navy);display:block;margin-bottom:6px">${title}</strong><span>${hint}</span></div></div>`;
|
||
}
|
||
return `<div class="queue-list">${rows.map((p,i)=>{
|
||
const active = p.active || p.key === STATE.selectedKey || (!STATE.selectedKey && i===0);
|
||
const preview = archive ? (p.preview || "会话已归档") : `<i class="${p.dot || (p.status==="待重试"?"orange": i?"gray":"")}"></i>${esc(p.status || "")}`;
|
||
const meta = archive
|
||
? `<span>${esc(p.time || "")}</span><span class="badge ${p.badge==="已转人工"?"orange":""}">${esc(p.badge || "AI 已回复")}</span>`
|
||
: `<span>${esc(p.wait || "")}</span><span class="bubble-count">${icon("message")} ${p.bubbles || 0}</span>`;
|
||
return `<div class="queue-row ${active?"active":""}" data-action="${archive?"select-archive":"select-task"}" data-key="${esc(p.key || "")}"><div class="avatar ${esc(p.tone || "")}">${esc(p.avatar || "客")}</div><div class="q-copy"><strong>${esc(p.name || "")}</strong><span>${preview}</span></div><div class="q-meta">${meta}</div></div>`;
|
||
}).join("")}</div>`;
|
||
};
|
||
|
||
activities = function(more="查看完整日志"){
|
||
const items = STATE.demo ? null : (STATE.activities || []);
|
||
if (STATE.demo || !items) return _activities(more);
|
||
const cards = items.length ? items.map(x=>{
|
||
const tone = x.tone || "";
|
||
const color = tone==="orange"?"var(--orange)":tone==="blue"?"var(--blue)":"var(--green)";
|
||
return `<div class="activity-card"><div class="activity-icon ${esc(tone)}">${icon(x.icon || "activity")}</div><time>${esc(x.time || "")}</time><strong>${esc(x.title || "")}</strong><p>${esc(x.detail || "")}</p><span class="activity-state" style="color:${color}">● ${esc(x.state || "")}</span></div>`;
|
||
}).join("") : `<div class="empty-board" style="height:82px"><span>暂无执行记录</span></div>`;
|
||
const action = more === "复制全部记录" ? "copy-archive" : (more === "查看完整记录" ? "full-log" : "logs");
|
||
return `<div class="activity-strip">${cards}<button class="btn blue activity-more" data-action="${action}">${esc(more)} ›</button></div>`;
|
||
};
|
||
|
||
journey = function(){
|
||
if (STATE.demo || !(STATE.journey && STATE.journey.length)) return _journey();
|
||
return `<div class="journey">${STATE.journey.map((x,i)=>{
|
||
const mode = x.mode || (x.active ? "active" : x.wait ? "wait" : "");
|
||
const token = x.done ? "✓" : String(i+1);
|
||
return `<div class="j-step ${mode}"><div class="j-dot">${token}</div><div class="j-copy"><strong>${esc(x.title)}</strong><span>${esc(x.status)}</span></div><div class="j-time">${esc(x.time)}</div></div>`;
|
||
}).join("")}</div>`;
|
||
};
|
||
|
||
const _autoView = autoView;
|
||
const _queueView = queueView;
|
||
const _archiveView = archiveView;
|
||
const _aiView = aiView;
|
||
const _automationView = automationView;
|
||
const _logsView = logsView;
|
||
const _systemView = systemView;
|
||
|
||
function liveRibbonAuto(){
|
||
const m = STATE.metrics || {};
|
||
return ribbon([
|
||
["监听状态", m.listen || (STATE.listening ? "运行中" : "已停止"), "activity", STATE.listening ? "green" : ""],
|
||
["排队", m.queue || "0", "users"],
|
||
["今日回复", m.today || "0", "message"],
|
||
["失败", m.failed || "0", "alert", Number(m.failed) > 0 ? "red" : ""],
|
||
]);
|
||
}
|
||
function liveRibbonQueue(){
|
||
const m = STATE.metrics || {};
|
||
return ribbon([
|
||
["处理中", m.processing || "0", "check", "", "blue"],
|
||
["等待中", m.waiting || "0", "clock"],
|
||
["待重试", m.retry || "0", "refresh", "orange"],
|
||
["平均等待", m.avgWait || "0s", "clock", "green"],
|
||
]);
|
||
}
|
||
function liveRibbonLogs(){
|
||
const m = STATE.metrics || {};
|
||
return ribbon([
|
||
["企业微信", m.wecomHealth || "待连接", "message", STATE.wecomConnected ? "green" : ""],
|
||
["AI 成功率", m.aiRate || "—", "brain", "", "blue"],
|
||
["平均响应", m.avgResp || "—", "clock", "", "blue"],
|
||
["警告", m.warnings || "0", "alert", Number(m.warnings) > 0 ? "red" : ""],
|
||
]);
|
||
}
|
||
|
||
function currentTaskCard(){
|
||
const task = STATE.task;
|
||
if (!task){
|
||
return `<section class="panel panel-pad task-card"><div class="panel-title"><h2>当前处理任务</h2></div><div class="empty-board" style="height:280px"><div><strong style="color:var(--navy);display:block;margin-bottom:6px">暂无处理中的任务</strong><span>新的客户消息入队后会显示在这里</span></div></div></section>`;
|
||
}
|
||
const messages = (task.messages || []).map(x=>`<div class="message-line"><time>${esc(x.time || "")}</time><span>${esc(x.text || "")}</span></div>`).join("") || `<div class="message-line"><span>等待读取客户消息</span></div>`;
|
||
const answer = task.answer ? `<div class="answer-block">${esc(task.answer).replaceAll("\\n","<br>")}</div>` : `<div class="answer-block" style="color:var(--muted)">尚未生成回复</div>`;
|
||
return `<section class="panel panel-pad task-card"><div class="panel-title"><h2>当前处理任务</h2><div class="task-head-meta"><span class="badge">${icon("user")}${esc(task.name || "")}</span><span class="badge">${esc(task.messageCount || "已读取消息")}</span><span class="badge">${esc(task.action || "")}</span></div></div><div class="subhead">复制的聊天内容</div><div class="copy-block">${messages}</div><div class="subhead">AI 回复预览</div>${answer}<div class="answer-meta"><span>${icon("shield")}置信度 <b>${esc(task.confidence || "—")}</b></span><span>${icon("clock")}模型耗时 ${esc(task.latency || "—")}</span></div><div class="task-actions"><button class="btn small danger" data-action="cancel-task" data-key="${esc(task.key || "")}">${icon("stop")}终止本任务</button><button class="btn small orange" data-action="handoff-task" data-key="${esc(task.key || "")}">${icon("user")}转人工</button></div></section>`;
|
||
}
|
||
|
||
function windowStatusCard(){
|
||
const stats = STATE.windowStats || [["folder","窗口", STATE.wecomConnected ? "已识别" : "未识别"],["message","当前会话", (STATE.task && STATE.task.name) || "—"],["monitor","DPI", STATE.dpi || "—"],["shield","人工保护", STATE.idleLabel || "—"],["refresh","最小化恢复", "已开启"]];
|
||
return `<section class="panel panel-pad window-card"><div class="panel-title"><h2>企业微信窗口状态</h2></div><div class="window-status">${stats.map(x=>`<div class="window-stat">${icon(x[0])}<strong>${esc(x[1])}</strong><span>${esc(x[2])}</span></div>`).join("")}</div></section>`;
|
||
}
|
||
|
||
function queueDetailPanel(){
|
||
const task = STATE.task;
|
||
if (!task){
|
||
return `<section class="panel panel-pad"><div class="panel-title"><h2>任务详情</h2></div><div class="empty-board"><div><strong style="color:var(--navy);display:block;margin-bottom:6px">暂无选中任务</strong><span>选择左侧任务后查看来源消息与处理进度</span></div></div></section>`;
|
||
}
|
||
const steps = task.steps || [["入队","等待中"],["读取消息","等待中"],["请求 AI","等待中"],["生成回复","等待中"],["回填发送","等待中"],["完成","等待中"]];
|
||
const active = Number(task.stepIndex ?? -1);
|
||
const process = steps.map((x,i)=>`<div class="process-step ${active<0?"":i<active?"done":i===active?"active":""}"><div class="process-dot">${i<active?"✓":i+1}</div><strong>${esc(x[0])}</strong><span>${esc(x[1])}</span></div>`).join("");
|
||
const message = (task.messages && task.messages[0]) || {time:"", text:"队列为空时,选择任务后会在这里显示来源消息。"};
|
||
return `<section class="panel panel-pad"><div class="panel-title"><h2>任务详情</h2></div><div class="detail-stats"><div class="detail-stat"><span>客户</span><b>${esc(task.name || "暂无选中任务")}</b></div><div class="detail-stat"><span>会话 ID</span><b>${esc(task.sessionId || "--")} <button class="btn small" data-action="copy-session" data-key="${esc(task.sessionId || "")}" style="height:24px;padding:0 8px">${icon("copy")}</button></b></div><div class="detail-stat"><span>已等待</span><b>${esc(task.wait || "--")}</b></div><div class="detail-stat"><span>重试</span><b>${esc(task.retries || "0 次")}</b></div></div><div class="subhead">来源消息</div><div class="copy-block"><div class="message-line"><time>${esc(message.time || "")}</time><span>${esc(message.text || "")}</span></div></div><div class="process-title">${icon("sparkles")} ${esc(task.action || "等待新任务")}</div><div class="process-line">${process}</div><div class="task-actions" style="margin-top:17px;justify-content:center"><button class="btn danger" data-action="cancel-task" data-key="${esc(task.key || "")}">× 取消任务</button><button class="btn blue" data-action="retry-task" data-key="${esc(task.key || "")}">${icon("refresh")}立即重试</button><button class="btn orange" data-action="handoff-task" data-key="${esc(task.key || "")}">${icon("users")}转人工</button></div></section>`;
|
||
}
|
||
|
||
function schedulePanel(){
|
||
const s = (STATE.task && STATE.task.schedule) || {priority:"—", eta:"—", protect:"—", retry:"—"};
|
||
return `<section class="panel panel-pad"><div class="panel-title"><h2>调度信息</h2></div><div class="stack"><div class="side-stat"><div class="metric-icon">${icon("flag")}</div><div><span>优先级</span><b>${esc(s.priority)}</b></div></div><div class="side-stat"><div class="metric-icon">${icon("clock")}</div><div><span>预计完成</span><b>${esc(s.eta)}</b></div></div><div class="side-stat"><div class="metric-icon green">${icon("shield")}</div><div><span>人工保护</span><b style="color:${s.protect==="已开启"?"var(--green)":"inherit"}">${esc(s.protect)}</b></div></div><div class="side-stat"><div class="metric-icon">${icon("refresh")}</div><div><span>下次重试</span><b>${esc(s.retry)}</b></div></div></div></section>`;
|
||
}
|
||
|
||
autoView = function(){
|
||
if (STATE.demo) return _autoView();
|
||
const count = (STATE.people || []).length;
|
||
return `<div class="screen">${head("桌面自动化控制台","监听企业微信未读消息,并由 AI 自动生成与发送回复",connected(`<button class="btn blue" data-action="start-listen">${icon("send")}自动发送</button>`,"danger"))}${liveRibbonAuto()}<div class="auto-grid"><section class="panel panel-pad queue-card"><div class="panel-title"><h2>任务队列</h2><div class="toolbar"><span class="badge">${count}</span>${icon("filter")}${icon("sort")}</div></div>${queueList()}<div class="queue-footer"><span>共 ${count} 个会话</span><button class="round-btn" data-action="refresh">${icon("refresh")}</button></div></section>${currentTaskCard()}<section class="panel panel-pad journey-card"><div class="panel-title"><h2>自动化执行状态</h2><small>${esc(STATE.journeyTotal || "")}</small></div>${journey()}</section>${windowStatusCard()}</div><section class="panel activity-panel"><div class="panel-title"><h2>实时事件</h2></div>${activities()}</section></div>`;
|
||
};
|
||
|
||
queueView = function(){
|
||
if (STATE.demo) return _queueView();
|
||
const count = (STATE.people || []).length;
|
||
const filter = STATE.queueFilter || "全部";
|
||
const pills = ["全部","处理中","等待中","待重试"].map(name=>`<button class="filter-pill ${filter===name?"active":""}" data-action="queue-filter" data-key="${name}">${name}</button>`).join("");
|
||
return `<div class="screen">${head("任务队列","查看排队、处理中与待重试的自动回复任务",connected(`<button class="btn blue" data-action="start-listen">${icon("send")}自动发送</button>`,"danger"))}${liveRibbonQueue()}<div class="queue-view-grid"><section class="panel panel-pad"><div class="panel-title"><h2>全部任务</h2></div><div class="queue-filters">${pills}</div>${queueList()}<div class="queue-footer"><span>共 ${count} 个任务</span><button class="round-btn" data-action="refresh">${icon("refresh")}</button></div></section>${queueDetailPanel()}${schedulePanel()}</div><section class="panel activity-panel"><div class="panel-title"><h2>任务执行记录</h2></div>${activities()}</section></div>`;
|
||
};
|
||
|
||
archiveView = function(){
|
||
if (STATE.demo) return _archiveView();
|
||
const rows = STATE.archives || [];
|
||
const chat = STATE.chat || [];
|
||
const selected = rows.find(x=>x.key===STATE.selectedArchive) || rows[0] || {};
|
||
const chatHtml = chat.length ? chat.map(x=>`<div class="chat-line ${x.role==="assistant"?"ai":""}"><span class="mini-avatar">${icon(x.role==="assistant"?"bot":"user")}</span><div class="chat-bubble"><time>${esc(x.time || "")}</time>${esc(x.text || "")}</div></div>`).join("") : `<div class="empty-board" style="height:220px"><span>选择会话后显示聊天记录</span></div>`;
|
||
const tags = (STATE.memory && STATE.memory.tags || []).map((t,i)=>`<span class="tag ${i===1?"violet":i===2?"blue":""}">${esc(t)}</span>`).join("") || `<span class="tag">暂无标签</span>`;
|
||
const settle = (STATE.settle || []).map(x=>`<div class="settle-row"><div class="settle-icon">${x.iconHtml || icon("clipboard")}</div><div><strong>${esc(x.title)}</strong><small>${esc(x.hint)}</small></div><b>${esc(x.value)}</b></div>`).join("");
|
||
const filter = STATE.archiveFilter || "全部会话";
|
||
return `<div class="screen">${head("会话归档","保存客户上下文、自动回复与回访线索",`<span class="btn"><i class="led"></i>${esc(STATE.wecomLabel || "企业微信")}</span><span class="btn"><i class="led"></i>${esc(STATE.aiLabel || "AI 服务")}</span><button class="btn blue" data-action="export-archive">${icon("download")}导出记录</button>`)}<section class="panel filter-row"><label class="search">${icon("search")}<input id="archive-search" placeholder="搜索客户或会话 ID" value="${esc(STATE.archiveQuery || "")}"></label>${["全部会话","今日","AI 已回复","已转人工"].map(name=>`<button class="filter-pill ${filter.startsWith(name)?"active":""}" data-action="archive-filter" data-key="${name}">${name}${name==="全部会话"?"⌄":""}</button>`).join("")}<button class="filter-pill" data-action="archive-date">${esc(STATE.archiveDate || "日期")} □</button></section><div class="archive-grid"><section class="panel panel-pad"><div class="panel-title"><h2>最近会话</h2></div>${queueList(true)}<div class="queue-footer"><span>共 ${rows.length} 个会话</span><button class="round-btn" data-action="refresh">${icon("refresh")}</button></div></section><section class="panel panel-pad"><div class="panel-title"><h2>会话记录</h2></div><div class="toolbar" style="margin-bottom:12px"><span class="badge">${icon("user")}${esc(selected.name || "未选择")}</span><span class="badge">${esc(selected.count || "0")} 条消息</span><span class="badge">${esc(selected.badge || "")}</span><span class="badge">${icon("clock")}${esc(selected.time || "")}</span></div><div class="chat">${chatHtml}</div></section><div class="stack"><section class="panel panel-pad memory-card"><div class="panel-title"><h2>客户记忆</h2></div><div class="toolbar">${tags}</div><div class="memory-copy">${esc((STATE.memory && STATE.memory.text) || "选择会话后显示长期上下文。")}</div><span class="badge" style="margin-top:10px">${icon("clock")} 上下文 ${esc((STATE.memory && STATE.memory.rounds) || 0)} 轮</span></section><section class="panel panel-pad settle-card"><div class="panel-title"><h2>业务沉淀</h2></div><div class="stack" style="gap:8px">${settle || "<div class='empty-board' style='height:120px'><span>暂无业务沉淀</span></div>"}</div></section></div></div><section class="panel panel-pad archive-bottom"><div class="panel-title"><h2>最近更新</h2></div>${activities("复制全部记录")}</section></div>`;
|
||
};
|
||
|
||
aiView = function(){
|
||
if (STATE.demo) return _aiView();
|
||
const ai = STATE.ai || {};
|
||
const toolsOn = ai.tools || [true, true, true];
|
||
const names = ["挂号登记","客户查询","回访记录"];
|
||
const toolIcons = ["clipboard","search","clipboard"];
|
||
return `<div class="screen">${head("AI 设置","配置模型、客服人格、知识库与工具能力",`<span class="btn"><i class="led"></i>配置已同步</span><button class="btn" data-action="test-ai">${icon("play")} 测试 AI</button><button class="btn primary" data-action="save-ai">${icon("send")}保存并发布</button>`)}<div class="tabs"><button class="tab active">基础设置</button><button class="tab">知识与工具</button><button class="tab">安全策略</button></div><div class="ai-top"><section class="panel panel-pad"><div class="panel-title"><h2>客服人格</h2></div><div class="ai-profile"><div class="ai-ball">AI</div><div class="stack"><div class="field"><label>客服名称:</label><input class="input" id="ai-name" value="${esc(ai.name || "贴心管家")}" maxlength="40"></div><div class="toolbar"><b style="font-size:13px">服务风格:</b><span class="badge green">温和</span><span class="badge">专业</span><span class="badge" style="color:#7657e7;background:rgba(125,91,236,.08)">简洁</span></div><div class="field"><label>人格提示词:</label><textarea class="textarea" id="ai-prompt" readonly>${esc(ai.prompt || "")}</textarea></div></div><div class="preview-box"><b>${icon("sparkles")} 实时预览</b><p>您好!我是甄养堂${esc(ai.name || "贴心管家")},<br>很高兴为您服务 😊</p><p>请问有什么可以帮您的吗?</p></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("brain")} 模型引擎</h2></div><div class="field"><label>当前模型</label><button class="select model" data-action="pick-model" style="text-align:left">${icon("sparkles")} ${esc(ai.model || "云端模型")}</button></div><div class="form-grid" style="margin-top:17px"><div class="field"><label>备用模型</label><div class="select">${esc(ai.backup || "由云端策略管理")}</div></div><div class="field"><label>上下文</label><div class="select">${esc(ai.context || 8)} 轮</div></div><div class="field"><label>最大回复</label><div class="select">${esc(ai.maxTokens || 500)} tokens</div></div></div><div class="form-grid two" style="margin-top:18px"><div class="field"><label>温度 ${esc(ai.temperature || "0.35")}</label><input class="range" id="ai-temp" type="range" min="0" max="200" value="${Math.round(Number(ai.temperature || 0.35)*100)}"></div><div class="field"><label>回复长度 中等</label><input class="range" type="range" value="55"></div></div></section></div><div class="ai-bottom"><section class="panel panel-pad"><div class="panel-title"><h2>${icon("tray")} 企业知识库</h2></div><div class="knowledge"><div class="donut"><strong>${esc(ai.knowledge || "0")}<small>个知识片段</small></strong></div><div class="stack"><span class="badge green">● 已同步</span><span class="badge green">✓ 已连接</span><button class="btn small primary" data-action="sync-knowledge">${icon("refresh")}立即同步</button><button class="btn small" data-action="open-data">${icon("folder")}管理知识</button></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("package")} MCP 工具</h2><small>单次最多调用 ${esc(ai.mcpRounds || 5)} 个工具 ⓘ</small></div><div class="tool-grid">${names.map((n,i)=>`<div class="tool-card"><div class="tool-icon">${icon(toolIcons[i])}</div><b>${n}</b><div class="row"><small>${toolsOn[i]?"已启用":"未启用"}</small><div class="switch ${toolsOn[i]?"on":""}" data-action="toggle-tool" data-key="${i}"></div></div></div>`).join("")}</div></section><section class="panel panel-pad"><div class="panel-title"><h2>${icon("shield")} 安全边界</h2></div><div class="stack"><div class="setting-line"><span>${icon("shield")} 不做医疗诊断</span></div><div class="setting-line"><span>${icon("pill")} 不修改用药方案</span></div><div class="setting-line"><span>${icon("users")} 高风险转人工</span></div></div><div class="row" style="margin-top:10px;justify-content:flex-end"><span>当前模式:</span><span class="badge green">${icon("shield")}严格模式</span></div></section></div><section class="panel panel-pad config-strip"><div class="panel-title"><h2>配置状态</h2></div>${statusCells([["cloud","云端模型正常","最后检查:刚刚"],["folder","知识库已同步",`${ai.knowledge || 0} 个知识片段`],["folder","3 个工具可用","全部运行正常"],["shield","安全策略已生效","严格模式"]])}</section></div>`;
|
||
};
|
||
|
||
automationView = function(){
|
||
if (STATE.demo) return _automationView();
|
||
const a = STATE.automation || {};
|
||
const autoOn = (STATE.sendMode || "auto") === "auto";
|
||
return `<div class="screen">${head("自动化设置","配置企业微信监听、消息合并与自动发送规则",`<span class="btn"><i class="led"></i>${esc(a.wecom || "企业微信未识别")}</span><span class="btn"><i class="led"></i>规则已生效</span><button class="btn primary" data-action="save-automation">${icon("save")}保存设置</button>`)}<div class="tabs"><button class="tab ${autoOn?"active":""}" data-action="send-mode" data-key="auto">${icon("send")} 自动发送</button><button class="tab ${autoOn?"":"active"}" data-action="send-mode" data-key="review">${icon("users")} 审核后发送</button></div><div class="automation-top"><section class="panel panel-pad"><div class="panel-title"><h2>监听与发送策略</h2></div><span class="badge green">${icon("activity")} ● 规则运行中</span><div class="auto-flow">${[["activity","扫描未读"],["log","提取消息"],["folder","合并消息"],["brain","请求 AI"],["send","回填发送"]].map(x=>`<div class="flow-step"><div class="flow-circle">${icon(x[0])}</div><strong>${x[1]}</strong></div>`).join("")}</div><div class="form-grid"><div class="flow-pill">${icon("clock")}扫描间隔 <b style="color:#2d65f0">${esc(a.poll || "2")} 秒</b></div><div class="flow-pill">${icon("hourglass")} 连续消息等待 <b style="color:#2d65f0">${esc(a.batch || "20")} 秒</b></div><div class="flow-pill">${icon("clock")}发送前等待 <b style="color:#2d65f0">${esc(a.delay || "1")} 秒</b></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>企业微信窗口</h2></div><div class="wechat-graphic"><div class="fake-window"><div class="fake-check">✓</div></div><div class="stack"><div class="row"><span>${icon("clipboard")} 主窗口:</span><b style="color:var(--green)">● ${esc(a.wecom || "未识别")}</b></div><div class="row"><span>${icon("user")} 当前账号:</span><b>${esc(a.account || "—")}</b></div><div class="row"><span>${icon("globe")} DPI:</span><b>${esc(STATE.dpi || "—")}</b></div><div class="row"><span>${icon("monitor")} 最小化恢复:</span><b style="color:var(--green)">● 已开启</b></div></div></div><div class="task-actions"><button class="btn small blue" data-action="redetect">${icon("refresh")}重新检测</button></div></section></div><div class="automation-mid"><section class="panel panel-pad"><div class="panel-title"><h2>人机共存</h2></div><div class="setting-line"><span>启用人工操作保护</span><div class="switch ${a.mouseProtect?"on":""}" data-action="toggle-idle"></div></div><div style="text-align:center;margin-top:8px">鼠标静止 <b style="color:#2562ed">${esc(a.idle || "5")} 秒</b> 后恢复</div><div class="mouse-orb"><span style="color:#315fec">${icon("bot")}<br><small>AI 运行中</small></span><div class="mouse-pointer">${icon("cursor")}</div><span style="color:var(--orange)">${icon("pause")}<br><small>检测到人工操作<br>AI 暂停</small></span></div></section><section class="panel panel-pad"><div class="panel-title"><h2>消息处理</h2></div><div class="stack"><div class="setting-line"><span>✓ 同一客户连续消息自动合并</span></div><div class="setting-line"><span>✓ 保留最近 ${esc(a.context || 8)} 轮上下文</span><span class="badge">${esc(a.context || 8)} 轮</span></div><div class="setting-line"><span>✓ 一次只处理 1 个会话</span><span class="badge">1 个</span></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>异常与兜底</h2></div><div class="stack"><div class="setting-line"><span style="color:var(--orange)">${icon("clock")}</span><span style="flex:1">AI 超时:重试 2 次</span><span class="badge">2 次</span></div><div class="setting-line"><span style="color:var(--orange)">${icon("alert")}</span><span style="flex:1">发送失败:自动重试</span><span class="badge green">已开启</span></div><div class="setting-line"><span style="color:#3c69f4">${icon("message")}</span><span style="flex:1">固定回复:${esc(a.reply || "在的,请稍等")}</span><span class="badge">编辑</span></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>高风险转人工</h2><span class="badge green">● 已开启</span></div><div class="risk-tags"><span class="risk-tag">${icon("shield")} 诊断</span><span class="risk-tag">${icon("package")} 用药调整</span><span class="risk-tag">${icon("coins")} 投诉退款</span></div><button class="btn small blue" style="margin:11px auto 0;display:flex" data-action="security-rules">${icon("settings")}规则管理</button></section></div><section class="panel panel-pad check-strip"><div class="panel-title"><h2>启动前检查</h2></div>${statusCells([["check","企业微信已登录", STATE.wecomConnected ? "检查完成" : "等待连接"],["cloud","AI 服务正常","检查完成"],["activity","窗口坐标已校准", STATE.wecomConnected ? "检查完成" : "待校准"],["shield","规则配置有效","检查完成"]])}</section></div>`;
|
||
};
|
||
|
||
logsView = function(){
|
||
if (STATE.demo) return _logsView();
|
||
const events = STATE.logs || [];
|
||
const list = events.length ? events.map(x=>`<div class="event-row"><div class="event-node">✓</div><time>${esc(x.time)} ●</time><div class="event-kind" style="color:${x.tone==="orange"?"var(--orange)":x.tone==="green"?"var(--green)":"var(--blue)"}">${icon(x.icon || "activity")}</div><div class="event-copy"><strong>${esc(x.title)}</strong><span>${esc(x.detail)}</span></div><span class="badge ${esc(x.tone || "")}">${esc(x.state || "")}</span></div>`).join("") : `<div class="soft" style="height:170px;display:grid;place-items:center;color:var(--muted)">当前没有运行记录</div>`;
|
||
const ex = STATE.exceptions || {};
|
||
const detail = STATE.logDetail || {title:"等待事件", text:"开始监听后,这里会显示最近一条运行事件。", tags:[]};
|
||
return `<div class="screen">${head("运行日志","实时追踪连接、消息识别、AI 调用与自动发送事件",`<button class="btn" data-action="refresh">${icon("refresh")}实时刷新</button><button class="btn" data-action="export-logs">${icon("download")}导出日志</button><button class="btn danger" data-action="clear-logs">${icon("tray")} 清空日志</button><span class="btn" style="color:var(--green)"><span class="orb" style="width:31px;height:31px"></span>${esc(STATE.statusLabel || "监控中")}</span>`)}${liveRibbonLogs()}<div class="logs-grid"><section class="panel panel-pad event-flow"><div class="panel-title"><h2>实时事件流</h2></div><div class="toolbar" style="margin-bottom:7px">${["全部","连接","识别","AI","发送","警告"].map(n=>`<button class="filter-pill ${(STATE.eventFilter||"全部")===n?"active":""}" style="height:34px;padding:0 17px" data-action="event-filter" data-key="${n}">${n}</button>`).join("")}<label class="search" style="height:34px;width:180px;margin-left:auto">${icon("search")}<input id="event-search" placeholder="搜索事件内容" value="${esc(STATE.eventQuery || "")}"></label></div><div class="event-list" id="log-list">${list}</div></section><section class="panel panel-pad"><div class="panel-title"><h2>系统健康</h2></div><div class="health-rings">${[["企业微信连接", STATE.wecomConnected ? "正常" : "待连接"],["AI 服务","正常"],["窗口识别", STATE.wecomConnected ? "正常" : "待监听"]].map(x=>`<div><div class="health-ring">${icon("check")}</div><strong>${x[0]}</strong><span>${x[1]}</span></div>`).join("")}</div><div class="btn small" style="display:flex;margin:10px auto 0;width:350px">${icon("clock")}运行时长 <b style="color:#2b62f2">${esc(STATE.runtime || "00:00:00")}</b></div></section><section class="panel panel-pad"><div class="panel-title"><h2>当前回复链路</h2></div><div class="chain">${[["activity","扫描未读",""],["message","读取消息",""],["brain","请求 AI",""],["send","回填发送",""]].map(x=>`<div class="chain-step"><div class="flow-circle">${icon(x[0])}</div><strong>${x[1]}</strong><span>${x[2]}</span></div>`).join("")}</div></section></div><div class="log-mid"><section class="panel panel-pad" style="background:linear-gradient(130deg,rgba(255,253,250,.82),rgba(255,245,239,.72))"><div class="panel-title"><h2>事件详情</h2></div><div class="row" style="justify-content:flex-start;gap:16px"><div class="metric-icon orange">${icon("user")}</div><div><b>${esc(detail.title)}</b><p style="margin:4px 0 7px;font-size:12px">${esc(detail.text)}</p><div class="toolbar">${(detail.tags||[]).map(t=>`<span class="badge">${esc(t)}</span>`).join("")}</div></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>异常追踪</h2></div><div class="exception-grid"><div class="exception"><div class="metric-icon red">${icon("alert")}</div><div>发送失败<b>${esc(ex.sendFail || 0)}</b></div></div><div class="exception"><div class="metric-icon" style="color:#785cf1">${icon("clock")}</div><div>模型超时<b>${esc(ex.timeout || 0)}</b></div></div><div class="exception"><div class="metric-icon orange">${icon("monitor")}</div><div>窗口丢失<b>${esc(ex.windowLost || 0)}</b></div></div><button class="btn blue" data-action="event-filter" data-key="警告">查看异常 ›</button></div></section></div><section class="panel activity-panel recent-activity"><div class="panel-title"><h2>最近活动</h2></div>${activities("查看完整记录")}</section></div>`;
|
||
};
|
||
|
||
systemView = function(){
|
||
if (STATE.demo) return _systemView();
|
||
const s = STATE.system || {};
|
||
const on = k => s[k] ? "on" : "";
|
||
return `<div class="screen">${head("系统设置","管理软件启动、数据存储、通知与系统维护",`<span class="btn">当前版本 v${esc(s.version || "2.0")} ${icon("info")}</span><button class="btn" data-action="check-update">${icon("refresh")}检查更新</button><button class="btn primary" data-action="save-system">${icon("save")}保存设置</button>`)}<div class="system-top"><section class="panel panel-pad"><div class="panel-title"><h2>基础设置</h2></div><div class="stack" style="gap:8px"><div class="system-line">${icon("power")}<span>开机自动启动</span><div class="switch ${on("autoLaunch")}" data-action="sys-toggle" data-key="auto_launch"></div></div><div class="system-line">${icon("activity")}<span>启动后自动监听</span><div class="switch ${on("autoMonitor")}" data-action="sys-toggle" data-key="auto_monitor"></div></div><div class="system-line">${icon("tray")}<span>最小化到系统托盘</span><div class="switch ${on("tray")}" data-action="sys-toggle" data-key="minimize_on_close"></div></div><div class="system-line">${icon("log")}<span>关闭窗口时保持后台运行</span><div class="switch ${on("background")}" data-action="sys-toggle" data-key="keep_background"></div></div><div class="system-line">${icon("globe")}<span>界面语言: 简体中文</span>${icon("chevron")}</div><div class="system-line">${icon("monitor")}<span>缩放比例: 自动</span>${icon("chevron")}</div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>数据与存储</h2></div><div class="storage-grid"><div class="donut" style="width:145px;height:145px"><strong>${esc(s.storage || "—")}<small>已使用</small></strong></div><div class="stack"><div class="row"><span>● 会话归档</span><span>${esc(s.archiveSize || "—")}</span></div><div class="row"><span>● 运行日志</span><span>${esc(s.logSize || "—")}</span></div><div class="row"><span>● 缓存数据</span><span>${esc(s.cacheSize || "—")}</span></div></div></div><div class="row" style="justify-content:center;margin-top:7px"><span>数据保留</span><span class="btn small">${esc(s.retention || 90)} 天</span></div><div class="storage-actions"><button class="btn small blue" data-action="open-data">${icon("folder")}打开目录</button><button class="btn small blue" data-action="backup">${icon("cloud")}立即备份</button><button class="btn small blue" data-action="clear-cache">${icon("wifi")} 清理缓存</button></div></section><section class="panel panel-pad"><div class="panel-title"><h2>通知提醒</h2></div><div class="stack" style="gap:8px"><div class="system-line">${icon("send")}<span>发送失败提醒</span><div class="switch ${on("notifyFail")}" data-action="sys-toggle" data-key="notify_send_failed"></div></div><div class="system-line">${icon("hourglass")}<span>模型超时提醒</span><div class="switch ${on("notifyTimeout")}" data-action="sys-toggle" data-key="notify_timeout"></div></div><div class="system-line">${icon("user")}<span>需要人工跟进</span><div class="switch ${on("notifyManual")}" data-action="sys-toggle" data-key="notify_manual"></div></div><div class="system-line">${icon("wifi")}<span>企业微信连接中断</span><div class="switch ${on("notifyDisconnect")}" data-action="sys-toggle" data-key="notify_disconnect"></div></div><div class="notice-preview"><div class="metric-icon">${icon("bell")}</div><div><b>发送失败提醒</b><small style="display:block">消息发送失败,已加入重试队列</small></div><time>—<br><span style="color:#3772ff">●</span></time></div></div></section></div><div class="system-mid"><section class="panel panel-pad"><div class="panel-title"><h2>安全与隐私</h2></div><div class="stack" style="gap:6px"><div class="system-line">${icon("shield")}<span>敏感信息脱敏:</span><b>已开启 ●</b></div><div class="system-line">${icon("lock")}<span>本地数据加密:</span><b>已开启 ●</b></div><div class="system-line">${icon("lock")}<span>日志隐藏聊天正文:</span><b>已开启 ●</b></div><div class="system-line">${icon("log")}<span>退出时清理剪贴板:</span><b>已开启 ●</b></div></div></section><section class="panel panel-pad"><div class="panel-title"><h2>网络与服务</h2></div><div class="stack" style="gap:7px"><div class="system-line">${icon("cloud")}<span>云端 AI:</span><b>已连接 ●</b></div><div class="system-line">${icon("folder")}<span>企业知识库:</span><b>已连接 ●</b></div><div class="system-line">${icon("wifi")}<span>网络延迟:</span><b>— ●</b></div></div><button class="btn small blue" style="margin:10px auto 0;display:flex" data-action="test-ai">${icon("wifi")} 连接测试</button></section><section class="panel panel-pad"><div class="panel-title"><h2>维护与诊断</h2></div><div class="maintenance"><div class="maint-card" data-action="check-update"><div class="metric-icon">${icon("refresh")}</div><div><strong>检查更新</strong><small>已是最新版本</small></div></div><div class="maint-card" data-action="export-logs"><div class="metric-icon">${icon("clipboard")}</div><div><strong>导出诊断</strong><small>导出系统诊断报告</small></div></div><div class="maint-card" data-action="reset-settings"><div class="metric-icon">${icon("refresh")}</div><div><strong>恢复默认设置</strong><small>重置所有设置项</small></div></div><div class="maint-card" data-action="license"><div class="metric-icon">${icon("shield")}</div><div><strong>查看许可信息</strong><small>查看软件许可详情</small></div></div></div></section></div><section class="panel panel-pad system-status"><div class="panel-title"><h2>系统状态</h2></div>${statusCells([["shield","配置已保存","所有设置已生效"],["cloud","数据目录正常","本地数据可写"],["shield","安全策略正常","所有安全策略已启用"],["monitor","后台服务", STATE.listening ? "运行中" : "已停止"]])}</section></div>`;
|
||
};
|
||
|
||
views = {auto:autoView,queue:queueView,archive:archiveView,ai:aiView,automation:automationView,logs:logsView,system:systemView};
|
||
|
||
const _render = render;
|
||
render = function(name){
|
||
if(!views[name]) name = STATE.view || "auto";
|
||
STATE.view = name;
|
||
listening = !!STATE.listening;
|
||
_render(name);
|
||
const orb = document.getElementById("orb");
|
||
const label = document.getElementById("orb-label");
|
||
if (orb) orb.classList.toggle("paused", !STATE.listening);
|
||
if (label) label.textContent = STATE.statusLabel || (STATE.listening ? "监听中" : "已停止");
|
||
};
|
||
|
||
window.applyState = function(payload){
|
||
const data = typeof payload === "string" ? JSON.parse(payload) : (payload || {});
|
||
const prev = STATE.view;
|
||
Object.assign(STATE, data);
|
||
STATE.ready = true;
|
||
if (typeof data.listening === "boolean") listening = data.listening;
|
||
const active = document.activeElement;
|
||
const typing = active && /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName);
|
||
if (typing && (STATE.view || prev) === prev && ["ai","automation","system","archive","logs"].includes(prev)) {
|
||
return;
|
||
}
|
||
render(STATE.view || prev || "auto");
|
||
};
|
||
|
||
window.showConsoleView = function(name){ render(name); };
|
||
|
||
document.addEventListener("click", e => {
|
||
const nav = e.target.closest("[data-view]");
|
||
if (nav){
|
||
const name = nav.dataset.view;
|
||
render(name);
|
||
callBridge("navigate", name);
|
||
e.stopImmediatePropagation();
|
||
return;
|
||
}
|
||
const action = e.target.closest("[data-action]");
|
||
if (!action) return;
|
||
const a = action.dataset.action;
|
||
const key = action.dataset.key || "";
|
||
if (a === "toggle-listen" || a === "start-listen"){
|
||
if (!callBridge("toggleListen")) {
|
||
listening = !listening;
|
||
STATE.listening = listening;
|
||
render(STATE.view);
|
||
}
|
||
e.stopImmediatePropagation();
|
||
return;
|
||
}
|
||
const map = {
|
||
logs: () => callBridge("navigate", "logs") || render("logs"),
|
||
refresh: () => callBridge("refresh") || toast("已刷新"),
|
||
"clear-logs": () => callBridge("clearLogs") || toast("日志已清空"),
|
||
"cancel-task": () => callBridge("cancelTask", key),
|
||
"retry-task": () => callBridge("retryTask", key),
|
||
"handoff-task": () => callBridge("handoffTask", key),
|
||
"select-task": () => callBridge("selectTask", key),
|
||
"select-archive": () => callBridge("selectArchive", key),
|
||
"queue-filter": () => callBridge("setQueueFilter", key),
|
||
"archive-filter": () => callBridge("setArchiveFilter", key),
|
||
"event-filter": () => callBridge("setEventFilter", key),
|
||
"copy-session": () => callBridge("copyText", key),
|
||
"copy-archive": () => callBridge("copyArchive"),
|
||
"export-archive": () => callBridge("exportArchive"),
|
||
"export-logs": () => callBridge("exportLogs"),
|
||
"full-log": () => callBridge("showFullLog"),
|
||
"save-ai": () => callBridge("saveAi", JSON.stringify({name: (document.getElementById("ai-name")||{}).value || "", temperature: Number((document.getElementById("ai-temp")||{}).value || 35)/100})),
|
||
"test-ai": () => callBridge("testAi"),
|
||
"pick-model": () => callBridge("pickModel"),
|
||
"sync-knowledge": () => callBridge("syncKnowledge"),
|
||
"open-data": () => callBridge("openDataDir"),
|
||
"save-automation": () => callBridge("saveAutomation"),
|
||
"send-mode": () => callBridge("setSendMode", key),
|
||
"toggle-idle": () => callBridge("toggleIdle"),
|
||
"redetect": () => callBridge("redetectWecom"),
|
||
"security-rules": () => callBridge("showSecurityRules"),
|
||
"save-system": () => callBridge("saveSystem"),
|
||
"sys-toggle": () => callBridge("toggleSetting", key),
|
||
"check-update": () => callBridge("checkUpdate"),
|
||
"backup": () => callBridge("backupData"),
|
||
"clear-cache": () => callBridge("clearCache"),
|
||
"reset-settings": () => callBridge("resetSettings"),
|
||
license: () => callBridge("showLicense"),
|
||
"toggle-tool": () => callBridge("toggleTool", key),
|
||
};
|
||
if (map[a]){
|
||
map[a]();
|
||
e.stopImmediatePropagation();
|
||
}
|
||
}, true);
|
||
|
||
document.addEventListener("change", e => {
|
||
if (e.target && e.target.id === "archive-search") callBridge("setArchiveQuery", e.target.value || "");
|
||
if (e.target && e.target.id === "event-search") callBridge("setEventQuery", e.target.value || "");
|
||
});
|
||
|
||
const _fit = fit;
|
||
fit = function(){
|
||
const embedded = document.documentElement.classList.contains("embedded");
|
||
const w = embedded ? 1630 : 1630;
|
||
const h = embedded ? 920 : 965;
|
||
const s = Math.min(innerWidth / w, innerHeight / h);
|
||
document.getElementById("stage").style.setProperty("--scale", String(s));
|
||
};
|
||
|
||
function connectQt(){
|
||
if (typeof qt === "undefined" || !qt.webChannelTransport || typeof QWebChannel === "undefined") return;
|
||
new QWebChannel(qt.webChannelTransport, channel => {
|
||
window.qtBridge = channel.objects.bridge;
|
||
if (window.qtBridge.stateChanged) window.qtBridge.stateChanged.connect(window.applyState);
|
||
if (window.qtBridge.requestState) window.qtBridge.requestState();
|
||
});
|
||
}
|
||
connectQt();
|
||
addEventListener("resize", fit);
|
||
fit();
|
||
render((STATE.view || (location.hash || "#auto").slice(1) || "auto"));
|
||
'''
|
||
|
||
(root / "console-app.js").write_text(header + script + footer, encoding="utf-8")
|
||
print("wrote", root / "console-app.js", (root / "console-app.js").stat().st_size)
|