mingdao-harness 0.1.69 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,6 +43,26 @@ header select{flex:none}
43
43
  /* 输入框上方工作状态条(审计:长任务静默硬伤——实时显示执行步数/耗时与后台任务数)。
44
44
  与输入框同宽对齐(复用 #composer 的居中容器),不再横贯整个聊天窗口 */
45
45
  #workStatus{max-width:860px;margin:0 auto 4px;display:flex;align-items:center;gap:10px;padding:6px 14px;background:var(--panel);border:1px solid var(--border);border-radius:12px;font-size:12.5px;color:var(--dim);flex:none}
46
+ /* 顶部常驻活动条(静默深度优化):与输入框同列、生成期间常驻——即使滚动离开消息区也始终可见 */
47
+ #liveBar{position:sticky;top:0;z-index:20;max-width:860px;margin:0 auto 8px;display:flex;align-items:center;gap:10px;padding:8px 16px;background:linear-gradient(135deg,rgba(61,220,151,.12),rgba(34,184,207,.12));border:1px solid rgba(61,220,151,.45);border-radius:12px;font-size:13px;color:var(--text);box-shadow:0 2px 14px rgba(0,0,0,.25)}
48
+ #liveBar .spinner{margin-right:2px}
49
+ #liveBar .lb-phase{color:var(--accent2);font-weight:600;white-space:nowrap}
50
+ #liveBar .lb-stat{color:var(--dim);white-space:nowrap}
51
+ /* 左侧轨迹固定栏 + 右侧子代理面板(布局重构:轨迹在左、子代理在右) */
52
+ #trajRail{position:fixed;left:0;top:52px;bottom:0;width:42px;background:var(--bg2);border-right:1px solid var(--border);z-index:35;display:flex;flex-direction:column;align-items:center;padding-top:10px;gap:10px}
53
+ #trajRail .rail-btn{width:34px;height:34px;border-radius:10px;border:1px solid var(--border);background:var(--bg3);cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center}
54
+ #trajRail .rail-btn:hover{border-color:var(--accent2);color:var(--accent2)}
55
+ #trajRail .rail-btn.on{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#06121a;border:none}
56
+ #subPanel{position:fixed;right:0;top:52px;bottom:0;width:320px;background:var(--bg2);border-left:1px solid var(--border);z-index:40;display:flex;flex-direction:column}
57
+ .sb-head{padding:12px 14px;font-size:14px;font-weight:600;border-bottom:1px solid var(--border);color:var(--accent2);display:flex;align-items:center;gap:8px}
58
+ .sb-head button{margin-left:auto}
59
+ #sbList{flex:1;overflow-y:auto;padding:8px}
60
+ .sb-item{padding:8px 10px;border:1px solid var(--border);border-radius:10px;margin-bottom:8px;font-size:12.5px;background:var(--bg3);cursor:pointer}
61
+ .sb-item:hover{border-color:var(--accent2)}
62
+ .sb-item .sb-q{color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
63
+ .sb-item .sb-meta{color:var(--faint);font-size:11.5px;margin-top:3px}
64
+ .sb-item .sb-body{display:none;color:var(--dim);font-size:11.5px;margin-top:6px;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow-y:auto;border-top:1px dashed var(--border);padding-top:6px}
65
+ .sb-item.open .sb-body{display:block}
46
66
  #workStatus .ws-busy{color:var(--accent2);display:flex;align-items:center;gap:8px;flex:1;min-width:0}
47
67
  #workStatus .ws-bg{cursor:pointer;color:var(--warn);white-space:nowrap}
48
68
  #workStatus .ws-bg:hover{color:var(--text)}
@@ -199,9 +219,16 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
199
219
  <div class="spacer"></div>
200
220
  <input id="sessionSearch" placeholder="搜索会话…" title="关键词检索历史会话">
201
221
  <select id="sessions" title="历史会话"><option value="">历史会话</option></select>
202
- <button id="tasksBtn" title="任务面板">任务</button><button id="sessMgrBtn" title="管理会话(重命名/删除)">✎</button>
222
+ <button id="tasksBtn" title="任务面板">任务</button><button id="subsBtn" title="子代理面板">🤖 子代理</button><button id="sessMgrBtn" title="管理会话(重命名/删除)">✎</button>
203
223
  <button id="newChat">新对话</button>
204
224
  </header>
225
+ <div id="trajRail">
226
+ <button class="rail-btn" id="trajRailBtn" title="轨迹(点击查看详情)">🧭</button>
227
+ </div>
228
+ <aside id="subPanel" style="display:none">
229
+ <div class="sb-head">🤖 子代理<span id="sbCount" style="font-size:11.5px;color:var(--faint);font-weight:400"></span><button id="sbClose">关闭</button></div>
230
+ <div id="sbList"></div>
231
+ </aside>
205
232
  <aside id="trajPanel" style="display:none">
206
233
  <div class="tj-head">🧭 任务轨迹<span id="tjSummary" style="font-size:11.5px;color:var(--faint);font-weight:400"></span><button id="tjClose">关闭</button></div>
207
234
  <div id="tjList"></div>
@@ -210,7 +237,7 @@ footer{flex:none;border-top:1px solid var(--border);background:var(--bg2);paddin
210
237
  <div class="tp-head">任务面板 <span id="tpCount"></span><button id="tpClose" style="float:right">✕</button></div>
211
238
  <div id="tpList"></div>
212
239
  </aside>
213
- <main><div id="chat"></div></main>
240
+ <main><div id="liveBar" style="display:none"></div><div id="chat"></div></main>
214
241
  <footer>
215
242
  <div id="workStatus" style="display:none"></div>
216
243
  <input type="file" id="fileInput" accept="image/png,image/jpeg,image/gif,image/webp,.txt,.md,.json,.js,.py,.log,.csv,.html,.css" multiple style="display:none">
@@ -485,7 +512,9 @@ $('#dirPickOk').onclick = () => { const cb = pickerCb; pickerCb = null; $('#dirM
485
512
  $('#dirPickNone').onclick = () => { const cb = pickerCb; pickerCb = null; $('#dirModal').style.display = 'none'; if (cb) cb(null); };
486
513
  $('#dirPickCancel').onclick = () => { pickerCb = null; $('#dirModal').style.display = 'none'; };
487
514
  const chatEl = $('#chat'), input = $('#input'), sendBtn = $('#sendBtn');
488
- let activeAiMsg=null, bgRunning=0, curSteps=0, curWorkT0=0; // 本轮进度/后台任务数(工作状态条与轨迹共用)
515
+ let activeAiMsg=null, bgRunning=0, curSteps=0, curWorkT0=0, curTools=0, curTasks=0; // 本轮进度(活动条/状态条/轨迹共用)
516
+ let curPhase='模型推理中'; // 阶段语义(服务端 progress 事件下发)
517
+ const sessionSubs=[]; // 本会话全部子代理(task 工具)条目:{seq, question, result, msg}
489
518
  // 回到底部悬浮按钮:滚动容器为 main;上滚超过 300px 时出现,点击平滑回底
490
519
  (function(){
491
520
  const scroller = document.querySelector('main');
@@ -775,14 +804,14 @@ async function send(){
775
804
  await handleEvents(resp.body, ev=>{
776
805
  if(ev.taskId){ taskId=ev.taskId; curTaskId=taskId; } // 质检 L1:主停止按钮只断本任务
777
806
  if(ev.type==='ask') disarm(); else arm(); // 有活动→重置;权限等待→暂停计时
778
- if(ev.type==='progress'){ const p=ev.seconds||0; showThink(); think.textContent='💭 正在工作 '+Math.floor(p/60)+' 分 '+Math.round(p%60)+' 秒 · 第 '+msg._steps+' 回合 · 已执行 '+ev.steps+' 步…'; renderWorkStatus(); }
779
- if(ev.type==='text'){ onActivity(); raw+=ev.delta; update(); }
807
+ if(ev.type==='progress'){ const p=ev.seconds||0; if(ev.phase) curPhase=String(ev.phase); curTools=Number(ev.steps)||0; curTasks=Number(ev.tasks)||0; showThink(); think.textContent='💭 正在工作 '+Math.floor(p/60)+' 分 '+Math.round(p%60)+' 秒 · 第 '+msg._steps+' 回合 · '+curPhase+' · 已执行 '+ev.steps+' 步…'; renderWorkStatus(); }
808
+ if(ev.type==='text'){ curPhase='模型输出中'; onActivity(); raw+=ev.delta; update(); }
780
809
  else if(ev.type==='reasoning'){ if(!reason){ reason=addReasoning(msg); reason.dataset.full=''; } if(!reason.parentElement.open) reason.parentElement.open=true; reason.parentElement.querySelector('summary').classList.add('live'); reason.dataset.full+=ev.delta; const full=reason.dataset.full; reason.textContent=(full.length>9000?full.slice(-9000)+'\n…(思考内容较长,仅显示末尾;已 '+full.length+' 字符)':full); reason.scrollTop=reason.scrollHeight; scroll(); }
781
- else if(ev.type==='turnStart'){ stepsCount+=1; curSteps+=1; msg._steps=curSteps; msg._traj.push({kind:'turn', t:Date.now()}); showThink(); think.textContent='💭 第 '+msg._steps+' 回合:模型推理中…'; renderWorkStatus(); scroll(); }
782
- else if(ev.type==='turnEnd'){ showThink(); think.textContent='⏸ 第 '+msg._steps+' 回合完成(累计 '+(ev.toolSteps||0)+' 工具步)· 进入下一回合…'; renderWorkStatus(); }
783
- else if(ev.type==='toolStart'){ stepsCount+=1; curSteps+=1; msg._steps=curSteps; msg._traj.push({kind:'tool', seq:ev.seq, name:ev.name, args:ev.args, t:Date.now(), done:false}); onActivity(); update(); renderToolStartEvent(ev); renderWorkStatus(); }
810
+ else if(ev.type==='turnStart'){ stepsCount+=1; curSteps+=1; msg._steps=curSteps; curPhase='模型推理中'; msg._traj.push({kind:'turn', t:Date.now()}); showThink(); think.textContent='💭 第 '+msg._steps+' 回合:模型推理中…'; renderWorkStatus(); scroll(); }
811
+ else if(ev.type==='turnEnd'){ curPhase='回合完成,进入下一回合'; showThink(); think.textContent='⏸ 第 '+msg._steps+' 回合完成(累计 '+(ev.toolSteps||0)+' 工具步)· 进入下一回合…'; renderWorkStatus(); }
812
+ else if(ev.type==='toolStart'){ stepsCount+=1; curSteps+=1; curPhase=ev.name==='task'?'子代理执行中':'执行工具中'; msg._steps=curSteps; msg._traj.push({kind:'tool', seq:ev.seq, name:ev.name, args:ev.args, t:Date.now(), done:false}); onActivity(); update(); renderToolStartEvent(ev); renderWorkStatus(); }
784
813
  else if(ev.type==='code'){ onActivity(); const pre=document.createElement('pre'); pre.innerHTML='<code>'+highlight(ev.code,ev.lang)+'</code>'; insertBeforeActiveMsg(pre); scroll(); }
785
- else if(ev.type==='tool'){ onActivity(); update(); const pending=runningTools.get(ev.seq); if(pending){ pending.remove(); runningTools.delete(ev.seq); } const tj=msg._traj.find(x=>x.kind==='tool'&&x.seq===ev.seq); if(tj){ tj.done=true; tj.result=ev.result; tj.durationMs=ev.durationMs; tj.card=pending||null; } renderToolEvent(ev); }
814
+ else if(ev.type==='tool'){ onActivity(); update(); const pending=runningTools.get(ev.seq); if(pending){ pending.remove(); runningTools.delete(ev.seq); } const tj=msg._traj.find(x=>x.kind==='tool'&&x.seq===ev.seq); if(tj){ tj.done=true; tj.result=ev.result; tj.durationMs=ev.durationMs; tj.card=pending||null; } if(ev.name==='task'){ sessionSubs.push({seq:ev.seq, question:String(ev.args?.question||ev.args?.prompt||''), result:ev.result, durationMs:ev.durationMs, msg}); renderSubPanel(); } renderToolEvent(ev); }
786
815
  else if(ev.type==='toolDenied'){ onActivity(); const tj=msg._traj.find(x=>x.kind==='tool'&&x.seq===ev.seq); if(tj){ tj.done=true; tj.denied=ev.reason||'未授权'; } const d=document.createElement('div'); d.className='errline'; d.textContent='✖ '+(ev.reason==='未授权'||!ev.reason?'未授权':ev.reason)+':'+ev.name; msg.appendChild(d); scroll(); }
787
816
  else if(ev.type==='banner'){ renderBanner(ev); }
788
817
  else if(ev.type==='ask'){ update(); askModal(ev).then(a=>fetch('/api/permission',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:ev.id,answer:a,taskId})}).then(()=>arm()).catch(()=>{})); }
@@ -791,7 +820,7 @@ async function send(){
791
820
  else if(ev.type==='done'){ console.log('[MingDao] done 事件:session=' + ev.session); onActivity(); if(ev.budget){ const b=ev.budget; if(hintTextEl) hintTextEl.textContent='预算 '+Math.round(b.used/1000)+'K/'+Math.round(b.total/1000)+'K('+Math.round(b.used/b.total*100)+'%)· 本轮完成 · 提示栏右侧为今日费用与命中率'; } refreshStatusBar(); if(ev.stats&&ev.stats.deliverables&&ev.stats.deliverables.length){ const card=document.createElement('div'); card.className='deliver'; card.innerHTML='<div class="t">📦 交付物('+ev.stats.deliverables.length+' 个文件)</div>'+ev.stats.deliverables.map(f=>'<div class="i">'+esc(f)+(f.toLowerCase().endsWith('.html')?' <span style="color:var(--accent2)">— 浏览器打开即可运行</span>':'')+'</div>').join(''); msg.appendChild(card); } if(ev.note){ const d=document.createElement('div'); d.className='errline'; d.style.color='var(--warn)'; d.textContent=ev.note; msg.appendChild(d); } currentSession=ev.session; update(); refreshSessions(); updateTasksPanel(); }
792
821
  });
793
822
  }catch(e){ onActivity(); const d=document.createElement('div'); d.className='errline'; d.textContent=(e&&e.name==='AbortError')?(killedByWatchdog?'响应超时已中断(120 秒无任何响应),请重试':'已中断'):(e&&e.message)||'网络错误'; msg.appendChild(d); scroll(); }
794
- finally{ clearInterval(hintTimer); disarm(); generating=false; setBtn(); curTaskId=null; console.log('[MingDao] 回合收尾:generating=false,按钮恢复发送'); hintEl.classList.remove('working'); if(hintTextEl) hintTextEl.textContent=defaultHint; pending=false; content.innerHTML=renderMarkdown(raw); attachTrajMeta(msg); activeAiMsg=null; curWorkT0=0; renderWorkStatus(); scroll(); updateTasksPanel(); }
823
+ finally{ clearInterval(hintTimer); disarm(); generating=false; setBtn(); curTaskId=null; curPhase='模型推理中'; console.log('[MingDao] 回合收尾:generating=false,按钮恢复发送'); hintEl.classList.remove('working'); if(hintTextEl) hintTextEl.textContent=defaultHint; pending=false; content.innerHTML=renderMarkdown(raw); attachTrajMeta(msg); activeAiMsg=null; curWorkT0=0; renderWorkStatus(); scroll(); updateTasksPanel(); }
795
824
  }
796
825
 
797
826
  let curTaskId=null; // 本轮 SSE 任务 id(质检 L1:主停止按钮只断本任务,不再误伤其他 tab)
@@ -799,17 +828,27 @@ function abort(){ fetch('/api/abort',{method:'POST',headers:{'Content-Type':'app
799
828
 
800
829
  // —— 输入框上方工作状态条(审计:长任务静默硬伤 → 实时进度可见) ——
801
830
  function renderWorkStatus(){
831
+ renderLiveBar();
802
832
  const el=$('#workStatus'); if(!el) return;
803
833
  let html='';
804
834
  if(generating){
805
835
  const secs=curWorkT0?Math.round((Date.now()-curWorkT0)/1000):0;
806
- html='<span class="ws-busy"><span class="spinner"></span>任务执行中:第 '+curSteps+' 步 · '+Math.floor(secs/60)+' 分 '+Math.round(secs%60)+' 秒 完成后总结与交付物显示在本轮消息底部(停止请用输入框右侧「■ 停止」按钮)</span>';
836
+ html='<span class="ws-busy"><span class="spinner"></span>⏳ '+curSteps+' 步 · '+Math.floor(secs/60)+' 分 '+Math.round(secs%60)+' 秒 · '+curTools+' 工具步</span>';
807
837
  } else if(bgRunning>0){
808
838
  html='<span class="ws-bg">🛠 '+bgRunning+' 个后台任务运行中 — 点击查看</span>';
809
839
  }
810
840
  el.style.display=html?'flex':'none';
811
841
  el.innerHTML=html;
812
- const bg=el.querySelector('.ws-bg'); if(bg) bg.onclick=()=>{ const tp=$('#tasksPanel'); tp.style.display = tp.style.display==='none'?'flex':'none'; if(tp.style.display==='flex'){ $('#trajPanel').style.display='none'; updateTasksPanel(); } };
842
+ const bg=el.querySelector('.ws-bg'); if(bg) bg.onclick=()=>{ const tp=$('#tasksPanel'); tp.style.display = tp.style.display==='none'?'flex':'none'; if(tp.style.display==='flex'){ $('#trajPanel').style.display='none'; $('#subPanel').style.display='none'; updateTasksPanel(); } };
843
+ }
844
+ // 顶部常驻活动条(静默完美解决层):生成期间始终可见,滚动不影响
845
+ function renderLiveBar(){
846
+ const el=$('#liveBar'); if(!el) return;
847
+ if(!generating){ el.style.display='none'; el.innerHTML=''; return; }
848
+ const secs=curWorkT0?Math.round((Date.now()-curWorkT0)/1000):0;
849
+ el.style.display='flex';
850
+ el.innerHTML='<span class="spinner"></span><span class="lb-phase">正在执行:'+esc(curPhase)+'</span>'+
851
+ '<span class="lb-stat">第 '+curSteps+' 步 · 已 '+Math.floor(secs/60)+' 分 '+Math.round(secs%60)+' 秒 · '+curTools+' 工具步'+(curTasks>0?' · '+curTasks+' 个子代理':'')+'</span>';
813
852
  }
814
853
  // 轨迹元行:任务完成后附在本轮消息顶部(步数/子代理数,点击打开轨迹面板)
815
854
  function attachTrajMeta(msg){
@@ -855,6 +894,8 @@ function openTraj(msg){
855
894
  }
856
895
  $('#trajPanel').style.display='flex';
857
896
  $('#tasksPanel').style.display='none';
897
+ $('#subPanel').style.display='none';
898
+ $('#trajRailBtn').classList.add('on');
858
899
  }
859
900
  function resultText(r){
860
901
  if(r==null||r===undefined) return '';
@@ -864,7 +905,43 @@ function resultText(r){
864
905
  return JSON.stringify(r);
865
906
  }
866
907
  function truncText(t,n){ t=String(t||''); return t.length>n?t.slice(0,n)+'\n…(截断,共 '+t.length+' 字)':t; }
867
- $('#tjClose').onclick=()=>{ $('#trajPanel').style.display='none'; };
908
+ $('#tjClose').onclick=()=>{ $('#trajPanel').style.display='none'; $('#trajRailBtn').classList.remove('on'); };
909
+ $('#trajRailBtn').onclick=()=>{
910
+ const p=$('#trajPanel');
911
+ const willShow=p.style.display==='none';
912
+ if(willShow){ const m=lastMsgWithTraj(); if(!m){ uiAlert('本轮还没有可展示的轨迹(先发起一个任务)'); return; } openTraj(m); }
913
+ else p.style.display='none';
914
+ $('#trajRailBtn').classList.toggle('on', willShow);
915
+ };
916
+ function lastMsgWithTraj(){
917
+ const msgs=[...document.querySelectorAll('#chat .msg-ai')].reverse();
918
+ for(const m of msgs){ if(m._traj && m._traj.length) return m; }
919
+ return msgs.length ? msgs[0] : null;
920
+ }
921
+ function renderSubPanel(){
922
+ const count=sessionSubs.length;
923
+ const btn=$('#subsBtn'); if(btn) btn.textContent='🤖 子代理'+(count>0?' '+count:'');
924
+ $('#sbCount').textContent=count>0?('('+count+' 个)'):'';
925
+ if($('#subPanel').style.display!=='flex') return;
926
+ const list=$('#sbList'); list.innerHTML='';
927
+ for(const sub of [...sessionSubs].reverse()){
928
+ const div=document.createElement('div'); div.className='sb-item';
929
+ div.innerHTML='<div class="sb-q">🤖 '+esc(sub.question.slice(0,60))+'</div><div class="sb-meta">'+(sub.durationMs!=null?sub.durationMs+'ms · ':'')+((sub.result&&sub.result.ok===false)?'失败':'完成')+'</div>';
930
+ const body=document.createElement('div'); body.className='sb-body';
931
+ body.textContent='任务:'+sub.question+'\n\n结果:'+truncText(resultText(sub.result), 1500);
932
+ div.appendChild(body);
933
+ div.onclick=(e)=>{ if(e.target.closest('.sb-body')) return; div.classList.toggle('open'); };
934
+ list.appendChild(div);
935
+ }
936
+ if(!count) list.innerHTML='<div class="empty" style="color:var(--faint);font-size:12px;padding:10px">本会话还没有子代理(task 工具会派生子代理)</div>';
937
+ }
938
+ $('#subsBtn').onclick=()=>{
939
+ const p=$('#subPanel');
940
+ const willShow=p.style.display==='none';
941
+ p.style.display=willShow?'flex':'none';
942
+ if(willShow){ $('#tasksPanel').style.display='none'; $('#trajPanel').style.display='none'; $('#trajRailBtn').classList.remove('on'); renderSubPanel(); }
943
+ };
944
+ $('#sbClose').onclick=()=>{ $('#subPanel').style.display='none'; };
868
945
  // —— 任务面板(多会话并行) ——
869
946
  function setBtn(){
870
947
  sendBtn.textContent = generating ? '■ 停止' : '发送';
@@ -912,7 +989,7 @@ async function refreshStatusBar(){
912
989
  $('#statusBar').textContent = s.turns+' 轮 · '+s.steps+' 步 | LLM '+fmtDur(s.llmMs)+' · 工具调用 '+fmtDur(s.toolMs)+' | 首 token 平均 '+ft+' · '+tps+' | 缓存命中 '+rate+' | 输入 '+fmtTok(s.prompt)+' tok · 输出 '+fmtTok(s.completion)+' tok';
913
990
  }
914
991
  refreshStatusBar(); setInterval(refreshStatusBar, 15000);
915
- $('#tasksBtn').onclick=()=>{ const p=$('#tasksPanel'); p.style.display = p.style.display==='none'?'flex':'none'; if(p.style.display==='flex') updateTasksPanel(); };
992
+ $('#tasksBtn').onclick=()=>{ const p=$('#tasksPanel'); p.style.display = p.style.display==='none'?'flex':'none'; if(p.style.display==='flex'){ $('#subPanel').style.display='none'; $('#trajPanel').style.display='none'; $('#trajRailBtn').classList.remove('on'); updateTasksPanel(); } };
916
993
  $('#tpClose').onclick=()=>{ $('#tasksPanel').style.display='none'; };
917
994
 
918
995
  async function refreshSessions(q){ const u=q?'/api/sessions?q='+encodeURIComponent(q):'/api/sessions'; const r=await fetch(u).catch(()=>null); if(!r)return; const j=await r.json(); const sel=$('#sessions'); sel.innerHTML='<option value="">历史会话</option>'; for(const s of j.sessions){ const o=document.createElement('option'); o.value=s.file; o.textContent=s.label; sel.appendChild(o); } if(currentSession&&!q) sel.value=currentSession; }
package/src/web/server.js CHANGED
@@ -248,7 +248,15 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
248
248
  // 同时充当客户端无活动看门狗的活动源)
249
249
  const progressTimer = setInterval(() => {
250
250
  try {
251
- send({ type: 'progress', seconds: Math.round((Date.now() - entry.startedAt) / 1000), steps: io?.stats?.().toolCount || 0 });
251
+ // 质检(静默深度优化):progress 附带阶段语义与子代理数,客户端顶部常驻活动条据此实时播报
252
+ const phase = entry.pendingAsk ? '等待权限确认' : io?._turnActive ? '模型推理中' : '执行工具中';
253
+ send({
254
+ type: 'progress',
255
+ seconds: Math.round((Date.now() - entry.startedAt) / 1000),
256
+ steps: io?.stats?.().toolCount || 0,
257
+ tasks: io?.stats?.().taskCount || 0,
258
+ phase,
259
+ });
252
260
  } catch {}
253
261
  }, 5000);
254
262
  const userMessage = String(body.message ?? '').trim();
@@ -435,7 +443,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
435
443
  durationMs: r.durationMs,
436
444
  truncated: r.truncated,
437
445
  aborted: r.aborted,
438
- note: r.note || (r.text ? '' : '(模型本轮没有输出正文)'),
446
+ note: r.note || (r.text ? '' : `(本轮共执行 ${io.stats().toolCount} 步工具操作${io.stats().deliverables.length ? `、交付 ${io.stats().deliverables.length} 个文件` : ''},但模型没有输出总结文字——可追问「总结一下刚才的工作」)`),
439
447
  stats: io.stats(),
440
448
  session: path.basename(session.file),
441
449
  });
package/src/web/web-io.js CHANGED
@@ -9,11 +9,12 @@ export function createWebIO({ send, askHandler, setAbortHandler }) {
9
9
  showReasoning: true,
10
10
  _pending: null,
11
11
  _toolCount: 0,
12
+ _taskCount: 0, // 子代理(task 工具)次数:进度心跳与轨迹面板用
12
13
  _deliverables: [],
13
14
  _activeTools: [], // {seq, key}:并行批次下 toolStart↔tool 按 name+args 配对(审计 P2-7)
14
15
  // 交付物与步数统计:写/编辑成功的文件路径(去重)
15
16
  stats() {
16
- return { toolCount: io._toolCount, deliverables: [...new Set(io._deliverables)] };
17
+ return { toolCount: io._toolCount, taskCount: io._taskCount, deliverables: [...new Set(io._deliverables)] };
17
18
  },
18
19
  setShowReasoning(v) {
19
20
  io.showReasoning = !!v;
@@ -57,6 +58,7 @@ export function createWebIO({ send, askHandler, setAbortHandler }) {
57
58
  },
58
59
  renderTool(name, args, result, durationMs) {
59
60
  io._toolCount += 1;
61
+ if (name === 'task') io._taskCount += 1;
60
62
  if ((name === 'write' || name === 'edit') && args?.path && result && result.ok !== false) {
61
63
  io._deliverables.push(String(args.path));
62
64
  }