'use strict'; const $ = s => document.querySelector(s); const $$ = s => Array.from(document.querySelectorAll(s)); const token = $('meta[name="workbench-token"]').content; let overview = null, settings = {}, currentRun = null, currentPlan = null, fileData = null, currentCard = null, poll = null; const signals = {repeat:'反复提问',risk:'风险 / 事故线索',rework:'重复踩坑线索',future:'后续复用线索'}; const fields = {source_id:'消息 ID', conversation:'会话名称', conversation_id:'会话 ID', kind:'会话类型', sender:'发送人名称', sender_id:'发送人 ID', timestamp:'时间', content:'内容', message_type:'消息类型', quote:'引用'}; const titles = {connect:'连接 Mac 微信',overview:'看见对话中的知识需求',import:'导入你的聊天记录',analyze:'发现值得沉淀的知识',settings:'模型接口与隐私设置'}; function el(tag, text, className) { const n = document.createElement(tag); if(text !== undefined) n.textContent = text; if(className) n.className=className; return n; } function notify(text, error=false, level='success') { const n=$('#notice'); n.textContent=text; n.hidden=false; n.classList.toggle('error',error); n.classList.toggle('warning',!error&&level==='warning'); } async function api(path, body) { const opts=body===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Workbench-Token':token},body:JSON.stringify(body)}; const response=await fetch(path,opts); const data=await response.json(); if(!response.ok)throw Error(data.error||'请求失败'); return data; } async function busy(button, work) {button.disabled=true;try{await work();}catch(e){notify(e.message,true);}finally{button.disabled=button.id.startsWith('mac-')&&button.id!=='mac-refresh'&&(macRunning||!macAvailable);if(button.id==='mac-create-copy')button.disabled=button.disabled||!macRecoveryPlan||!$('#mac-copy-consent').checked;if(button.id==='mac-launch-copy')button.disabled=button.disabled||!macCopyReady;}} function page(name) {if(!titles[name])name='overview';$$('.page').forEach(n=>n.hidden=n.id!=='page-'+name);$$('nav button').forEach(n=>n.classList.toggle('active',n.dataset.page===name));$('#page-title').textContent=titles[name];location.hash=name;} function displayTime(time) { if(!time)return '—'; const offset=settings.utc_offset||'+08:00'; const sign=offset[0]==='-'?-1:1; const minutes=sign*(Number(offset.slice(1,3))*60+Number(offset.slice(4,6))); const dt=new Date(new Date(time).getTime()+minutes*60000); return dt.toISOString().slice(0,19).replace('T',' '); } function selected(select) {return Array.from(select.selectedOptions).map(n=>n.value);} function filters() { const start=$('#start').value,end=$('#end').value; if(start&&end&&start>end)throw Error('开始时间不能晚于结束时间'); return {start,end,kind:$('#filter-kind').value,conversations:selected($('#include-conversations')),exclude:selected($('#exclude-conversations')),search:$('#filter-search').value.trim(),utc_offset:settings.utc_offset||'+08:00'}; } function invalidatePlan() {currentPlan=null;$('#preview-panel').hidden=true;} async function refresh() { overview=await api('/api/overview'); const stats=$('#stats');stats.replaceChildren(); const metrics=[['已导入消息',overview.count,'可重复导入,自动去重'],['会话',overview.conversations.length,'群聊与私聊'],['群聊',overview.conversations.filter(c=>c.kind==='group').length,'可跨群汇总相似问题'],['分析记录',overview.runs.length,'保留审核与修正']]; for(const [name,num,note] of metrics){const node=el('div',undefined,'stat');node.append(el('small',name),el('strong',num.toLocaleString()),el('span',note));stats.append(node);} $('#conversations').replaceChildren();$('#empty').hidden=overview.count>0; for(const c of overview.conversations){const tr=el('tr');for(const text of [c.conversation,c.kind==='group'?'群聊':'私聊',c.count,c.media,displayTime(c.first),displayTime(c.last)])tr.append(el('td',text));$('#conversations').append(tr);} for(const selector of ['#include-conversations','#exclude-conversations']){const select=$(selector),old=new Set(selected(select));select.replaceChildren();for(const c of overview.conversations){const opt=el('option',`${c.conversation} · ${c.kind==='group'?'群':'私聊'} · ${c.count} 条`);opt.value=c.conversation_id;opt.selected=old.has(opt.value);select.append(opt);}} const history=$('#history');history.replaceChildren(); for(const r of overview.runs){const b=el('button');b.append(el('span',`${displayTime(r.created)} · ${r.mode==='api'?'模型分析':'本地规则'}`),el('small',`${r.coverage.messages} 条记录 · ${r.coverage.conversations} 个会话 · ${{complete:'已完成',running:'进行中',failed:'失败'}[r.status]}`));b.addEventListener('click',()=>loadRun(r.id).catch(e=>notify(e.message,true)));history.append(b);} if(!overview.runs.length)history.append(el('p','还没有分析记录。','muted')); } async function loadSettings() { settings=await api('/api/settings');$('#api-endpoint').value=settings.api.endpoint||'';$('#api-model').value=settings.api.model||'';$('#custom-words').value=(settings.custom||[]).join('\n');$('#utc-offset').value=settings.utc_offset||'+08:00';$('#key-state').textContent=settings.has_key?'当前运行进程中已配置密钥;留空保存会保留它。':'当前没有内存密钥;本地免密接口可留空。';$('#timezone-label').textContent='筛选与显示时区:UTC'+settings.utc_offset+'。不填时间代表全部已导入记录。'; } async function loadFile(file) { fileData=null;$('#import-button').disabled=true;$('#sqlite-options').hidden=true; if(!file)return; if(file.size>45*1024*1024)throw Error('单个文件不能超过 45 MB,请拆分后导入'); const bytes=new Uint8Array(await file.arrayBuffer());let binary='';for(let i=0;iloadRun(id).catch(e=>notify(e.message,true)),2500); } function renderCards() { const container=$('#cards');container.replaceChildren();if(!currentRun)return; const search=$('#card-search').value.toLowerCase(),signal=$('#card-signal').value; const cards=currentRun.cards.filter(c=>(!search||(c.title+' '+c.summary).toLowerCase().includes(search))&&(!signal||c.signals.includes(signal))); for(const c of cards){ const n=el('article',undefined,'card'),top=el('div',undefined,'card-top'),label=el('label'),check=el('input');check.type='checkbox';check.value=c.id;check.className='card-select';check.setAttribute('aria-label','选择主题:'+c.title);label.append(check,el('h3',c.title));top.append(label,el('span','优先级 '+c.priority,'score'));n.append(top); const tags=el('div',undefined,'tags');for(const s of c.signals)tags.append(el('span',signals[s],'tag '+s));tags.append(el('span',c.strategy,'tag'));n.append(tags,el('p',c.summary),el('div',`${c.event_count} 个独立讨论候选 · ${c.conversations.length} 个会话 · ${c.participants.length} 位参与人`,'card-meta')); if(c.responders.length)n.append(el('div','经验提供者候选:'+c.responders.join('、'),'card-meta')); const bottom=el('div',undefined,'card-bottom'),b=el('button','查看证据与审核 ↗');b.addEventListener('click',()=>openCard(c.id).catch(e=>notify(e.message,true)));bottom.append(el('small',(c.reviewed?'已人工审核':'待人工审核')+' · '+c.status),b);n.append(bottom);container.append(n); } if(!cards.length)container.append(el('div',currentRun.status==='complete'?'没有匹配的候选主题。当前规则可能未识别出问题,也可以扩大时间范围。':'任务尚未生成完整结果。','empty')); } async function openCard(id) { currentCard=currentRun.cards.find(c=>c.id===id);if(!currentCard)return; const card=currentCard;$('#detail-title').textContent=card.title;const content=$('#detail-content');content.replaceChildren(); const grid=el('div',undefined,'review-grid'); for(const [field,name] of [['title','主题'],['summary','摘要'],['answer','经验候选(尚需验证)'],['boundaries','适用边界与证据缺口'],['notes','人工备注']]){const label=el('label',name),input=el(field==='title'?'input':'textarea');input.id='edit-'+field;input.value=card[field]||'';if(input.tagName==='TEXTAREA')input.rows=3;label.append(input);grid.append(label);} const strategyLabel=el('label','处理策略'),strategy=el('select');strategy.id='edit-strategy';for(const v of ['正式资产','轻量记录','专家带教','暂不投入']){const op=el('option',v);op.value=v;op.selected=v===card.strategy;strategy.append(op);}strategyLabel.append(strategy);grid.append(strategyLabel); const label=el('label',undefined,'check'),reviewed=el('input');reviewed.type='checkbox';reviewed.id='edit-reviewed';reviewed.checked=card.reviewed;label.append(reviewed,document.createTextNode('已人工审核(不代表经过实践验证)'));grid.append(label); const save=el('button','保存修正','primary');save.addEventListener('click',()=>busy(save,async()=>{const body={run_id:currentRun.id,ids:[id],action:'edit',strategy:strategy.value,reviewed:reviewed.checked};for(const field of ['title','summary','answer','boundaries','notes'])body[field]=$('#edit-'+field).value;await api('/api/card',body);await loadRun(currentRun.id);$('#detail-dialog').close();notify('已保存人工修正。');}));grid.append(save);content.append(grid); content.append(el('h3','原始讨论与证据','detail-evidence-title'),el('p','下方展示候选事件的全部已收集上下文。★ 为该卡引用的消息。事件窗口可能遗漏延迟回复或混入其他话题。经验提供者仅按他人的较长非问句回复识别,不代表专家认定。','muted')); const loading=el('p','正在读取本地原文…','muted');content.append(loading);$('#detail-dialog').showModal(); const events=await api('/api/context?run_id='+currentRun.id+'&card_id='+id);loading.remove(); const splitButton=el('button','将勾选的讨论拆成新主题');splitButton.disabled=events.length<2;const splitChecks=[]; for(const event of events){const box=el('div',undefined,'event'),l=el('label'),cb=el('input');cb.type='checkbox';cb.value=event.id;splitChecks.push(cb);l.append(cb,document.createTextNode(`${event.id} · ${event.conversation} · ${displayTime(event.messages[0].timestamp)}`));box.append(l); for(const m of event.messages){const msg=el('div',undefined,'message');msg.append(el('div',`${card.evidence_ids.includes(m.id)?'★ ':''}#${m.id} · ${m.sender} · ${displayTime(m.timestamp)} · ${m.message_type}`,'msg-meta'),el('p',m.content||'[非文字消息,无文字内容]'));if(m.quote)msg.append(el('blockquote',m.quote));box.append(msg);}content.append(box); } splitButton.addEventListener('click',()=>busy(splitButton,async()=>{const ids=splitChecks.filter(c=>c.checked).map(c=>c.value);await api('/api/card',{run_id:currentRun.id,ids:[id],action:'split',event_ids:ids});await loadRun(currentRun.id);$('#detail-dialog').close();notify('已拆分主题,请重新审核。');}));content.append(splitButton); } for(const [field,label] of Object.entries(fields)){const l=el('label',label),input=el('input');input.dataset.mapping=field;input.placeholder='自动匹配';l.append(input);$('#mapping-fields').append(l);} $$('[data-page]').forEach(n=>n.addEventListener('click',()=>page(n.dataset.page))); $$('[data-go]').forEach(n=>n.addEventListener('click',()=>page(n.dataset.go))); window.addEventListener('hashchange',()=>page(location.hash.slice(1))); $$('[data-action="refresh"]').forEach(n=>n.addEventListener('click',()=>busy(n,refresh))); $$('[data-action="demo"]').forEach(n=>n.addEventListener('click',()=>busy(n,async()=>{const r=await api('/api/demo',{});await refresh();notify(`已导入虚构样例 ${r.added} 条,重复 ${r.duplicates} 条。可直接进入“发现知识需求”。`);}))); $('#import-file').addEventListener('change',e=>loadFile(e.target.files[0]).catch(error=>{fileData=null;$('#import-button').disabled=true;notify(error.message,true);})); $('#import-button').addEventListener('click',e=>busy(e.target,async()=>{if(!fileData)throw Error('请先选择文件');const mapping={};$$('[data-mapping]').forEach(n=>{if(n.value.trim())mapping[n.dataset.mapping]=n.value.trim();});const options={conversation:$('#default-conversation').value.trim(),kind:$('#default-kind').value,utc_offset:settings.utc_offset||'+08:00',mapping,table:$('#sqlite-table').value};const result=await api('/api/import',{...fileData,options});$('#import-result').hidden=false;$('#import-result').textContent=JSON.stringify(result,null,2);invalidatePlan();await refresh();notify(`导入完成:新增 ${result.added},重复 ${result.duplicates},拒绝 ${result.rejected}。${result.rejected?'请查看下方具体行号和原因。':''}`);})); $('#save-settings').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/settings',{api:{endpoint:$('#api-endpoint').value.trim(),model:$('#api-model').value.trim()},key:$('#api-key').value,clear_key:$('#clear-key').checked,utc_offset:$('#utc-offset').value.trim(),custom:$('#custom-words').value.split('\n').map(x=>x.trim()).filter(Boolean)});$('#api-key').value='';$('#clear-key').checked=false;invalidatePlan();await loadSettings();await refresh();notify('配置已保存,密钥未写入磁盘。');})); $('#analyze-local').addEventListener('click',e=>busy(e.target,async()=>{const r=await api('/api/analyze',{filters:filters(),cross:$('#cross').checked});await loadRun(r.id);await refresh();notify('本地分析完成。请查看原文,核对主题与适用条件。');})); $('#preview-api').addEventListener('click',e=>busy(e.target,async()=>{invalidatePlan();const p=await api('/api/preview',{filters:filters(),cross:$('#cross').checked});currentPlan=p.plan_id;$('#preview-summary').textContent=`目标:${p.endpoint} · ${p.batches} 批 · 约 ${p.characters.toLocaleString()} 字符 · ${p.coverage.messages} 条原始记录 / ${p.coverage.events} 个讨论候选`;$('#preview-text').value=JSON.stringify(p.requests,null,2);$('#preview-panel').hidden=false;$('#preview-panel').scrollIntoView({behavior:'smooth',block:'start'});})); $('#send-api').addEventListener('click',e=>busy(e.target,async()=>{if(!currentPlan)throw Error('请重新生成预览');const id=currentPlan;invalidatePlan();const r=await api('/api/send',{plan_id:id});await loadRun(r.id);await refresh();notify('已开始发送预览中的脱敏请求。请保持本地服务运行。');})); $('#close-preview').addEventListener('click',invalidatePlan); ['start','end','filter-kind','include-conversations','exclude-conversations','filter-search','cross'].forEach(id=>$('#'+id).addEventListener('change',invalidatePlan)); $('#card-search').addEventListener('input',renderCards);$('#card-signal').addEventListener('change',renderCards); $('#close-detail').addEventListener('click',()=>$('#detail-dialog').close()); $('#merge-cards').addEventListener('click',()=>{const ids=$$('.card-select:checked').map(n=>n.value);if(ids.length<2)return notify('请至少勾选两张主题卡片。',true);$('#merge-title').value=currentRun.cards.find(c=>c.id===ids[0]).title;$('#merge-dialog').showModal();}); $('#cancel-merge').addEventListener('click',()=>$('#merge-dialog').close()); $('#confirm-merge').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/card',{run_id:currentRun.id,ids:$$('.card-select:checked').map(n=>n.value),action:'merge',title:$('#merge-title').value.trim()});await loadRun(currentRun.id);$('#merge-dialog').close();notify('已合并主题,原摘要保留在备注中,请重新审核。');})); let macTimer = null, lastMacJob = null, macRunning = false, macAvailable = false, macRecoveryPlan = null, macCopyReady = false, macNextAction = null; async function macStatus(){ clearTimeout(macTimer); const state=await api('/api/mac/status'); $('#mac-platform').textContent=state.available?`本地服务运行于 Mac · ${state.architecture}。扫描和读取只操作这台 Mac。`:'当前服务运行于非 Mac 环境。请把工具复制到安装微信的 Mac 上启动,再在那台 Mac 的浏览器打开此页面。'; const running=state.job?.status==='running'; macRunning=running; macAvailable=state.available;macCopyReady=state.debug_copy_ready; for(const id of ['mac-scan','mac-folder','mac-capture','mac-capture-admin','mac-dependencies','mac-import','mac-permissions','mac-preview-copy'])$('#'+id).disabled=!state.available||running; $('#mac-launch-copy').disabled=!state.available||running||!state.debug_copy_ready; $('#mac-diagnostic-next').disabled=!state.available||running; $('#mac-create-copy').disabled=!state.available||running||!macRecoveryPlan||!$('#mac-copy-consent').checked; $('#mac-key-file').disabled=running; const select=$('#mac-account'),previous=select.value;select.replaceChildren();$('#mac-accounts').replaceChildren(); for(const account of state.accounts){ const option=el('option',account.name);option.value=account.id;option.selected=account.id===previous;select.append(option); const node=el('div',undefined,'mac-account-info');node.append(el('strong',account.name),el('p',`${account.databases} 个核心数据库 · ${(account.bytes/1024**2).toFixed(1)} MiB · 已可读或密钥匹配 ${account.readable}/${account.databases}`),el('p',account.path,'muted'));$('#mac-accounts').append(node); } if(!state.accounts.length){const option=el('option','请先扫描或选择数据文件夹');option.value='';select.append(option);} $('#mac-key-state').textContent=state.keys_ready?'候选密钥已在运行内存。账号卡上的匹配数量决定实际可读范围。重启知微后需重新读取。':'尚未读取或导入密钥;未加密的可读数据库可直接进入第 3 步。'; const job=state.job;$('#mac-job-panel').hidden=!job; const diagnostic=state.diagnostic;$('#mac-diagnostic-panel').hidden=!diagnostic; const guidance=state.next_step;macNextAction=guidance?.action||null; if(guidance){$('#mac-next-title').textContent=guidance.title;$('#mac-next-description').textContent=guidance.description;$('#mac-diagnostic-next').textContent=guidance.button;} if(diagnostic){$('#mac-diagnostic-summary').textContent=diagnostic.summary;const yes=value=>value===true?'是':value===false?'否':'未知';$('#mac-diagnostic-details').textContent=[`诊断类型:${diagnostic.code}`,`强化运行时:${yes(diagnostic.hardened_runtime)}`,`允许调试:${yes(diagnostic.get_task_allow)}`,`管理员进程:${yes(diagnostic.administrator)}`,`进程读取返回码:${diagnostic.task_for_pid_code??'尚未尝试'}`].join('\n');} if(job){$('#mac-job-stage').textContent=job.stage;$('#mac-job-error').hidden=job.status!=='failed';$('#mac-job-error').textContent=job.error||'';$('#mac-job-result').hidden=!job.result;$('#mac-job-result').textContent=job.result?.message||''; const info=$('#mac-job-details');info.replaceChildren();const labels={parsed:'已解析消息',added:'新增消息',duplicates:'重复消息',backup:'本地备份位置',copy:'临时副本位置'};for(const [field,label] of Object.entries(labels)){if(job.result?.[field]!==undefined){info.append(el('dt',label),el('dd',String(job.result[field])));}}info.hidden=!info.childElementCount; if(!running&&lastMacJob!==job.id){lastMacJob=job.id;if(job.status==='complete'){if(job.action==='import')await refresh();notify(job.result?.message||'接入操作已完成',false,job.result?.level||'success');}else{notify(guidance?.title||job.error||'接入未完成',true);}if(location.hash==='#connect'&&diagnostic)$('#mac-diagnostic-panel').scrollIntoView({behavior:'smooth',block:'start'});} if(running)macTimer=setTimeout(()=>macStatus().catch(e=>notify(e.message,true)),1800); } return state; } async function macStart(action,extra={}){await api('/api/mac/start',{action,...extra});const state=await macStatus();const target=state.diagnostic&&state.job?.status!=='running'?'#mac-diagnostic-panel':'#mac-job-panel';$(target).scrollIntoView({behavior:'smooth',block:'start'});} async function previewMacCopy(){ if(!$('#mac-account').value){$('#mac-account').scrollIntoView({behavior:'smooth',block:'center'});throw Error('请先扫描并选择账号,再预览完整备份与临时副本方案');} const plan=await api('/api/mac/recovery-plan',{account:$('#mac-account').value}); macRecoveryPlan=plan.plan_id;$('#mac-copy-consent').checked=false;$('#mac-create-copy').disabled=true;$('#mac-copy-plan').hidden=false; $('#mac-copy-paths').textContent=`原版:${plan.source}\n临时副本:${plan.copy}\n完整备份:${plan.backup}\n预计至少需要 ${(plan.estimated_bytes/1024**3).toFixed(2)} GiB 可用空间`; $('#mac-copy-effects').replaceChildren(...plan.effects.map(effect=>el('li',effect))); $('#mac-copy-plan').scrollIntoView({behavior:'smooth',block:'start'}); } $('#mac-diagnostic-next').addEventListener('click',e=>busy(e.target,async()=>{ if(macNextAction==='preview_copy')return previewMacCopy(); if(macNextAction==='capture_admin')return macStart('capture',{administrator:true}); if(macNextAction==='key_file'){$('#mac-key-options').open=true;$('#mac-key-options').scrollIntoView({behavior:'smooth',block:'center'});$('#mac-key-file').focus();return;} if(['capture','launch_copy','permissions'].includes(macNextAction))return macStart(macNextAction); throw Error('请先重新检查权限,再选择下一步'); })); $('#mac-show-recovery').addEventListener('click',()=>$('#mac-recovery-section').scrollIntoView({behavior:'smooth',block:'start'})); $('#mac-scan').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/mac/scan',{});await macStatus();})); $('#mac-folder').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/mac/scan',{choose_folder:true});await macStatus();})); $('#mac-refresh').addEventListener('click',e=>busy(e.target,macStatus)); $('#mac-capture').addEventListener('click',e=>busy(e.target,()=>macStart('capture'))); $('#mac-capture-admin').addEventListener('click',e=>busy(e.target,()=>macStart('capture',{administrator:true}))); $('#mac-permissions').addEventListener('click',e=>busy(e.target,()=>macStart('permissions'))); $('#mac-preview-copy').addEventListener('click',e=>busy(e.target,previewMacCopy)); $('#mac-copy-consent').addEventListener('change',()=>{$('#mac-create-copy').disabled=!macAvailable||macRunning||!macRecoveryPlan||!$('#mac-copy-consent').checked;}); $('#mac-account').addEventListener('change',()=>{macRecoveryPlan=null;$('#mac-copy-plan').hidden=true;$('#mac-copy-consent').checked=false;$('#mac-create-copy').disabled=true;}); $('#mac-create-copy').addEventListener('click',e=>busy(e.target,async()=>{if(!macRecoveryPlan||!$('#mac-copy-consent').checked)throw Error('请先预览并明确同意操作影响');const plan=macRecoveryPlan;macRecoveryPlan=null;$('#mac-copy-plan').hidden=true;await macStart('debug_copy',{account:$('#mac-account').value,plan_id:plan,consent:true});})); $('#mac-launch-copy').addEventListener('click',e=>busy(e.target,()=>macStart('launch_copy'))); $('#mac-dependencies').addEventListener('click',e=>busy(e.target,()=>macStart('dependencies'))); $('#mac-import').addEventListener('click',e=>busy(e.target,()=>macStart('import',{account:$('#mac-account').value,include_biz:$('#mac-include-biz').checked}))); $('#mac-key-file').addEventListener('change',async e=>{try{const file=e.target.files[0];if(!file)return;if(file.size>1024**2)throw Error('密钥文件最多 1 MB');await api('/api/mac/keys',{keys:JSON.parse(await file.text())});await macStatus();notify('密钥文件已在本机内存处理,不会发送给模型。');}catch(error){notify(error.message,true);}finally{e.target.value='';}}); async function init(){await loadSettings();await refresh();page(location.hash.slice(1)||'overview');await macStatus();} init().catch(e=>notify(e.message,true));