@wendongfly/myhi 1.3.98 → 1.3.100

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/dist/admin.html CHANGED
@@ -143,6 +143,32 @@
143
143
  <div class="msg" id="gw-msg"></div>
144
144
  </div>
145
145
 
146
+ <!-- 命名密钥库 -->
147
+ <div class="card">
148
+ <h3>密钥库(Secret Vault)</h3>
149
+ <div style="font-size:0.75rem;color:#6e7681;margin-bottom:0.6rem;line-height:1.5">
150
+ 集中存 GitLab PAT / 部署 token / 网关 key / 证书等敏感值,spawn 会话时注入成环境变量,AI/命令用
151
+ <code style="background:#161b22;padding:0.1em 0.3em;border-radius:3px">$名字</code>
152
+ 引用——密钥值不进聊天、不落 transcript。类型选「文件」(证书等)时注入的是路径
153
+ <code style="background:#161b22;padding:0.1em 0.3em;border-radius:3px">$MYHI_SECRET_名字</code>。
154
+ 作用域「项目」仅注给该 cwd 的会话(最小权限)。<b>密钥值绝不写进记忆/CLAUDE.md。</b>
155
+ </div>
156
+ <div id="sec-list" style="margin-bottom:0.6rem"></div>
157
+ <div class="inp-row" style="gap:0.4rem;flex-wrap:wrap">
158
+ <input type="text" class="inp" id="sec-name" placeholder="名字(如 GITLAB_PAT,作环境变量名)" style="flex:1;min-width:150px">
159
+ <select class="inp" id="sec-type" style="max-width:90px"><option value="env">变量</option><option value="file">文件</option></select>
160
+ <select class="inp" id="sec-scope" style="max-width:100px" onchange="toggleSecScope()"><option value="global">全局</option><option value="project">项目</option></select>
161
+ </div>
162
+ <div class="inp-row" id="sec-cwd-row" style="margin-top:0.4rem;display:none">
163
+ <input type="text" class="inp" id="sec-cwd" placeholder="项目 cwd(如 D:\project\myhi)">
164
+ </div>
165
+ <div class="inp-row" style="margin-top:0.4rem">
166
+ <input type="password" class="inp" id="sec-value" placeholder="密钥值 / 证书内容(编辑时留空保持不变)">
167
+ <button class="btn-sm" onclick="addSecret()">保存</button>
168
+ </div>
169
+ <div class="msg" id="sec-msg"></div>
170
+ </div>
171
+
146
172
  <div class="uptime" id="uptime"></div>
147
173
  </div>
148
174
 
@@ -175,6 +201,74 @@ async function showAdmin() {
175
201
  await refresh();
176
202
  setInterval(refresh, 10000);
177
203
  loadGateway();
204
+ loadSecrets();
205
+ }
206
+
207
+ function toggleSecScope() {
208
+ const isProj = document.getElementById('sec-scope').value === 'project';
209
+ document.getElementById('sec-cwd-row').style.display = isProj ? '' : 'none';
210
+ }
211
+
212
+ function escHtml(s) { return String(s).replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
213
+
214
+ async function loadSecrets() {
215
+ try {
216
+ const r = await fetch('/api/secrets', { headers: H() });
217
+ if (!r.ok) return;
218
+ const d = await r.json();
219
+ const box = document.getElementById('sec-list');
220
+ let html = '';
221
+ const row = (s, scope, cwd) => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid #21262d;font-size:0.8rem">
222
+ <span style="color:#e6edf3;font-family:monospace">${escHtml(s.name)}</span>
223
+ <span style="color:#6e7681">${s.type === 'file' ? '📄文件' : '🔑变量'}</span>
224
+ <span style="color:#6e7681;font-family:monospace;flex:1">${escHtml(s.preview)}</span>
225
+ <button class="btn-sm" style="padding:0.1rem 0.5rem" onclick="delSecret('${escHtml(s.name)}','${scope}','${escHtml(cwd || '')}')">删</button>
226
+ </div>`;
227
+ if (d.global && d.global.length) {
228
+ html += '<div style="color:#8b949e;font-size:0.72rem;margin:0.2rem 0">全局</div>';
229
+ html += d.global.map(s => row(s, 'global', '')).join('');
230
+ }
231
+ for (const p of (d.projects || [])) {
232
+ if (!p.secrets.length) continue;
233
+ html += `<div style="color:#8b949e;font-size:0.72rem;margin:0.4rem 0 0.2rem;font-family:monospace">项目 ${escHtml(p.cwd)}</div>`;
234
+ html += p.secrets.map(s => row(s, 'project', p.cwd)).join('');
235
+ }
236
+ box.innerHTML = html || '<div style="color:#6e7681;font-size:0.78rem">(暂无密钥)</div>';
237
+ } catch {}
238
+ }
239
+
240
+ async function addSecret() {
241
+ const name = document.getElementById('sec-name').value.trim();
242
+ const value = document.getElementById('sec-value').value;
243
+ const type = document.getElementById('sec-type').value;
244
+ const scope = document.getElementById('sec-scope').value;
245
+ const cwd = document.getElementById('sec-cwd').value.trim();
246
+ const msg = document.getElementById('sec-msg');
247
+ try {
248
+ const r = await fetch('/api/secrets', { method: 'POST', headers: H(), body: JSON.stringify({ name, value, type, scope, cwd }) });
249
+ const d = await r.json();
250
+ if (d.ok) {
251
+ msg.className = 'msg msg-ok';
252
+ msg.textContent = '已保存,新建(该作用域的)会话自动生效';
253
+ document.getElementById('sec-name').value = '';
254
+ document.getElementById('sec-value').value = '';
255
+ loadSecrets();
256
+ } else {
257
+ msg.className = 'msg msg-err';
258
+ msg.textContent = d.error || '失败';
259
+ }
260
+ } catch {
261
+ msg.className = 'msg msg-err';
262
+ msg.textContent = '连接失败';
263
+ }
264
+ }
265
+
266
+ async function delSecret(name, scope, cwd) {
267
+ if (!confirm(`删除密钥 ${name}?`)) return;
268
+ try {
269
+ await fetch('/api/secrets', { method: 'DELETE', headers: H(), body: JSON.stringify({ name, scope, cwd }) });
270
+ loadSecrets();
271
+ } catch {}
178
272
  }
179
273
 
180
274
  async function loadGateway() {
package/dist/attach.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as e}from"module";if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var t={};const s=e(import.meta.url)("module");const r=e(import.meta.url)("fs");const o=e(import.meta.url)("path");const n=e(import.meta.url)("os");const i=e(import.meta.url)("readline");const c=(0,s.createRequire)(import.meta.url);const{io:p}=c("socket.io-client");const a=process.env.MYHI_SERVER||"http://localhost:12300";let m=null;const d=process.argv.slice(2);for(let e=0;e<d.length;e++){if((d[e]==="--password"||d[e]==="-pw")&&d[e+1]){m=d[e+1];d.splice(e,2);e--}}let u;if(m){u={password:m}}else{try{u={token:(0,r.readFileSync)((0,o.join)((0,n.homedir)(),".myhi","token"),"utf8").trim()}}catch{console.error("[myhi] 未找到 token,请使用 --password <密码> 或先启动服务器");process.exit(1)}}function cleanup(e){try{process.stdin.setRawMode(false)}catch{}process.stdin.pause();e.disconnect()}function attach(e,t){e.emit("join",t);e.on("joined",(s=>{const r=s.mode==="agent";process.stderr.write(`\r\n[myhi] 已附加到 "${s.title}" (${t}) 模式=${r?"agent":"pty"}\r\n`);process.stderr.write("[myhi] 按 Ctrl+] 分离\r\n\r\n");e.emit("take-control",{sessionId:t});if(r){let t="";e.on("agent:message",(e=>{if(!e)return;switch(e.type){case"system":if(e.subtype==="init")process.stderr.write("[会话已连接]\r\n");else if(e.subtype==="interrupted")process.stderr.write("\r\n[已中断]\r\n");break;case"assistant":if(e.message?.content){for(const t of e.message.content){if(t.type==="text")process.stdout.write(t.text+"\r\n");else if(t.type==="tool_use"){process.stdout.write(`\r\n[工具] ${t.name}\r\n`);if(t.input){const e=typeof t.input==="string"?t.input:JSON.stringify(t.input,null,2);process.stdout.write(""+e.slice(0,500)+"\r\n")}}}}break;case"content_block_delta":if(e.delta?.text){process.stdout.write(e.delta.text);t+=e.delta.text}break;case"content_block_stop":if(t){process.stdout.write("\r\n");t=""}break;case"tool_use":process.stdout.write(`\r\n[工具] ${e.tool_name||e.name||"?"}\r\n`);break;case"tool_result":if(e.content){const t=typeof e.content==="string"?e.content:Array.isArray(e.content)?e.content.map((e=>e.text||"")).join(""):"";if(t)process.stdout.write(""+t.slice(0,1e3)+"\r\n")}break;case"result":process.stdout.write(`\r\n[完成] ${e.duration_ms?(e.duration_ms/1e3).toFixed(1)+"s":""} ${e.total_cost_usd?"$"+e.total_cost_usd.toFixed(4):""}\r\n\r\n`);break}}));e.on("agent:history",(e=>{for(const t of e){if(t.type==="user")process.stdout.write(`> ${t.content}\r\n`);else if(t.type==="result")process.stdout.write("[完成]\r\n")}process.stdout.write("\r\n")}));e.on("agent:busy",(e=>{if(e)process.stderr.write("[思考中...]\r\n")}));e.on("agent:error",(e=>{process.stderr.write(`[错误] ${e.message}\r\n`)}));const s=(0,i.createInterface)({input:process.stdin,output:process.stdout,prompt:"> "});let r=false;e.on("agent:busy",(e=>{r=e;if(!e)s.prompt()}));s.prompt();s.on("line",(t=>{const o=t.trim();if(!o){if(!r)s.prompt();return}if(o==="/quit"||o==="/exit"||o==="/q"){process.stderr.write("[myhi] 已分离\r\n");cleanup(e);process.exit(0)}if(r){process.stderr.write("[正在处理中,请等待]\r\n");return}e.emit("agent:query",{prompt:o})}));s.on("close",(()=>{cleanup(e);process.exit(0)}))}else{try{process.stdin.setRawMode(true)}catch{}process.stdin.resume();process.stdin.setEncoding("binary");process.stdin.on("data",(t=>{if(t===""){process.stderr.write("\r\n[myhi] 已分离\r\n");cleanup(e);process.exit(0)}e.emit("input",t)}));e.on("output",(e=>{process.stdout.write(e,"binary")}));process.stdout.on("resize",(()=>{e.emit("resize",{cols:process.stdout.columns||80,rows:process.stdout.rows||24})}))}}));e.on("control-denied",(({reason:e})=>{process.stderr.write(`\r\n[myhi] 获取控制权失败: ${e}\r\n`)}));e.on("control-changed",(({holder:t,holderName:s})=>{if(t&&t!==e.id){process.stderr.write(`\r\n[myhi] ${s||"其他用户"} 已获取控制权,当前为只读\r\n`)}else if(!t){process.stderr.write("\r\n[myhi] 控制权已释放\r\n")}}));e.on("kicked",(({reason:t})=>{process.stderr.write(`\r\n[myhi] ${t||"你已被管理员踢出"}\r\n`);cleanup(e);process.exit(1)}));e.on("session-exit",(({code:t})=>{process.stderr.write(`\r\n[myhi] 会话已退出 (code ${t})\r\n`);cleanup(e);process.exit(0)}));e.on("error",(({message:t})=>{process.stderr.write(`\r\n[myhi] 错误: ${t}\r\n`);cleanup(e);process.exit(1)}))}async function pickSession(e){return new Promise((t=>{e.emit("list");e.once("sessions",(s=>{const r=s.filter((e=>e.alive));if(!r.length){process.stderr.write("[myhi] 没有活跃的会话。\n");cleanup(e);process.exit(0)}process.stdout.write("\n活跃会话:\n");r.forEach(((e,t)=>{const s=e.viewers>0?` (${e.viewers} 人在线)`:"";const r=e.mode==="agent"?" [Agent]":" [PTY]";process.stdout.write(` [${t+1}] ${e.title}${r}${s} — ${e.id}\n`)}));process.stdout.write("\n");const o=(0,i.createInterface)({input:process.stdin,output:process.stdout});o.question("选择会话编号: ",(s=>{o.close();const n=parseInt(s,10)-1;if(n<0||n>=r.length){process.stderr.write("[myhi] 无效的选择。\n");cleanup(e);process.exit(1)}t(r[n].id)}))}))}))}async function createAndAttach(e,t){return new Promise((s=>{e.emit("create",t,(t=>{if(!t?.ok){process.stderr.write(`[myhi] 创建失败: ${t?.error||"未知错误"}\n`);cleanup(e);process.exit(1)}process.stdout.write(`[myhi] 已创建会话 "${t.session.title}" — ${t.session.id}\n`);s(t.session.id)}))}))}async function promptNew(e){const t=(0,i.createInterface)({input:process.stdin,output:process.stdout});const ask=e=>new Promise((s=>t.question(e,s)));const s=(await ask("会话名称 [shell]: ")).trim()||"shell";const r=(await ask("启动命令(可选): ")).trim()||undefined;t.close();return createAndAttach(e,{title:s,initCmd:r})}const l=p(a,{transports:["websocket","polling"],auth:u});l.on("connect_error",(e=>{process.stderr.write(`[myhi] 连接失败: ${e.message}\n`);process.exit(1)}));l.on("connect",(async()=>{const e=d[0];if(e==="--new"){const e=d[1];const t=d[2]||undefined;const s=e?await createAndAttach(l,{title:e,initCmd:t}):await promptNew(l);attach(l,s)}else{const t=e||await pickSession(l);attach(l,t)}}));for(const e of["SIGINT","SIGTERM"]){process.on(e,(()=>{cleanup(l);process.exit(0)}))}process.on("exit",(()=>{try{process.stdin.setRawMode(false)}catch{}}));
2
+ import{createRequire as e}from"module";if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var t={};const s=e(import.meta.url)("module");const r=e(import.meta.url)("fs");const o=e(import.meta.url)("path");const n=e(import.meta.url)("os");const i=e(import.meta.url)("readline");const c=(0,s.createRequire)(import.meta.url);const{io:p}=c("socket.io-client");const a=process.env.MYHI_SERVER||"http://localhost:12300";let m=null;const u=process.argv.slice(2);for(let e=0;e<u.length;e++){if((u[e]==="--password"||u[e]==="-pw")&&u[e+1]){m=u[e+1];u.splice(e,2);e--}}let d;if(m){d={password:m}}else{try{d={token:(0,r.readFileSync)((0,o.join)((0,n.homedir)(),".myhi","token"),"utf8").trim()}}catch{console.error("[myhi] 未找到 token,请使用 --password <密码> 或先启动服务器");process.exit(1)}}function cleanup(e){try{process.stdin.setRawMode(false)}catch{}process.stdin.pause();e.disconnect()}function attach(e,t){e.emit("join",t);e.on("joined",(s=>{const r=s.mode==="agent";process.stderr.write(`\r\n[myhi] 已附加到 "${s.title}" (${t}) 模式=${r?"agent":"pty"}\r\n`);process.stderr.write("[myhi] 按 Ctrl+] 分离\r\n\r\n");e.emit("take-control",{sessionId:t});if(r){let t="";e.on("agent:message",(e=>{if(!e)return;switch(e.type){case"system":if(e.subtype==="init")process.stderr.write("[会话已连接]\r\n");else if(e.subtype==="interrupted")process.stderr.write("\r\n[已中断]\r\n");break;case"assistant":if(e.message?.content){for(const t of e.message.content){if(t.type==="text")process.stdout.write(t.text+"\r\n");else if(t.type==="tool_use"){const e=t.name==="Task"?`子代理 · ${t.input&&t.input.subagent_type||"general-purpose"}`:t.name;process.stdout.write(`\r\n[工具] ${e}\r\n`);if(t.input){const e=typeof t.input==="string"?t.input:JSON.stringify(t.input,null,2);process.stdout.write(""+e.slice(0,500)+"\r\n")}}}}break;case"content_block_delta":if(e.delta?.text){process.stdout.write(e.delta.text);t+=e.delta.text}break;case"content_block_stop":if(t){process.stdout.write("\r\n");t=""}break;case"tool_use":process.stdout.write(`\r\n[工具] ${e.tool_name||e.name||"?"}\r\n`);break;case"tool_result":if(e.content){const t=typeof e.content==="string"?e.content:Array.isArray(e.content)?e.content.map((e=>e.text||"")).join(""):"";if(t)process.stdout.write(""+t.slice(0,1e3)+"\r\n")}break;case"result":process.stdout.write(`\r\n[完成] ${e.duration_ms?(e.duration_ms/1e3).toFixed(1)+"s":""} ${e.total_cost_usd?"$"+e.total_cost_usd.toFixed(4):""}\r\n\r\n`);break}}));e.on("agent:history",(e=>{for(const t of e){if(t.type==="user")process.stdout.write(`> ${t.content}\r\n`);else if(t.type==="result")process.stdout.write("[完成]\r\n")}process.stdout.write("\r\n")}));e.on("agent:busy",(e=>{if(e)process.stderr.write("[思考中...]\r\n")}));e.on("agent:error",(e=>{process.stderr.write(`[错误] ${e.message}\r\n`)}));const s=(0,i.createInterface)({input:process.stdin,output:process.stdout,prompt:"> "});let r=false;e.on("agent:busy",(e=>{r=e;if(!e)s.prompt()}));s.prompt();s.on("line",(t=>{const o=t.trim();if(!o){if(!r)s.prompt();return}if(o==="/quit"||o==="/exit"||o==="/q"){process.stderr.write("[myhi] 已分离\r\n");cleanup(e);process.exit(0)}if(r){process.stderr.write("[正在处理中,请等待]\r\n");return}e.emit("agent:query",{prompt:o})}));s.on("close",(()=>{cleanup(e);process.exit(0)}))}else{try{process.stdin.setRawMode(true)}catch{}process.stdin.resume();process.stdin.setEncoding("binary");process.stdin.on("data",(t=>{if(t===""){process.stderr.write("\r\n[myhi] 已分离\r\n");cleanup(e);process.exit(0)}e.emit("input",t)}));e.on("output",(e=>{process.stdout.write(e,"binary")}));process.stdout.on("resize",(()=>{e.emit("resize",{cols:process.stdout.columns||80,rows:process.stdout.rows||24})}))}}));e.on("control-denied",(({reason:e})=>{process.stderr.write(`\r\n[myhi] 获取控制权失败: ${e}\r\n`)}));e.on("control-changed",(({holder:t,holderName:s})=>{if(t&&t!==e.id){process.stderr.write(`\r\n[myhi] ${s||"其他用户"} 已获取控制权,当前为只读\r\n`)}else if(!t){process.stderr.write("\r\n[myhi] 控制权已释放\r\n")}}));e.on("kicked",(({reason:t})=>{process.stderr.write(`\r\n[myhi] ${t||"你已被管理员踢出"}\r\n`);cleanup(e);process.exit(1)}));e.on("session-exit",(({code:t})=>{process.stderr.write(`\r\n[myhi] 会话已退出 (code ${t})\r\n`);cleanup(e);process.exit(0)}));e.on("error",(({message:t})=>{process.stderr.write(`\r\n[myhi] 错误: ${t}\r\n`);cleanup(e);process.exit(1)}))}async function pickSession(e){return new Promise((t=>{e.emit("list");e.once("sessions",(s=>{const r=s.filter((e=>e.alive));if(!r.length){process.stderr.write("[myhi] 没有活跃的会话。\n");cleanup(e);process.exit(0)}process.stdout.write("\n活跃会话:\n");r.forEach(((e,t)=>{const s=e.viewers>0?` (${e.viewers} 人在线)`:"";const r=e.mode==="agent"?" [Agent]":" [PTY]";process.stdout.write(` [${t+1}] ${e.title}${r}${s} — ${e.id}\n`)}));process.stdout.write("\n");const o=(0,i.createInterface)({input:process.stdin,output:process.stdout});o.question("选择会话编号: ",(s=>{o.close();const n=parseInt(s,10)-1;if(n<0||n>=r.length){process.stderr.write("[myhi] 无效的选择。\n");cleanup(e);process.exit(1)}t(r[n].id)}))}))}))}async function createAndAttach(e,t){return new Promise((s=>{e.emit("create",t,(t=>{if(!t?.ok){process.stderr.write(`[myhi] 创建失败: ${t?.error||"未知错误"}\n`);cleanup(e);process.exit(1)}process.stdout.write(`[myhi] 已创建会话 "${t.session.title}" — ${t.session.id}\n`);s(t.session.id)}))}))}async function promptNew(e){const t=(0,i.createInterface)({input:process.stdin,output:process.stdout});const ask=e=>new Promise((s=>t.question(e,s)));const s=(await ask("会话名称 [shell]: ")).trim()||"shell";const r=(await ask("启动命令(可选): ")).trim()||undefined;t.close();return createAndAttach(e,{title:s,initCmd:r})}const l=p(a,{transports:["websocket","polling"],auth:d});l.on("connect_error",(e=>{process.stderr.write(`[myhi] 连接失败: ${e.message}\n`);process.exit(1)}));l.on("connect",(async()=>{const e=u[0];if(e==="--new"){const e=u[1];const t=u[2]||undefined;const s=e?await createAndAttach(l,{title:e,initCmd:t}):await promptNew(l);attach(l,s)}else{const t=e||await pickSession(l);attach(l,t)}}));for(const e of["SIGINT","SIGTERM"]){process.on(e,(()=>{cleanup(l);process.exit(0)}))}process.on("exit",(()=>{try{process.stdin.setRawMode(false)}catch{}}));
package/dist/chat.html CHANGED
@@ -932,7 +932,7 @@
932
932
 
933
933
  // 工具调用(Claude Code 使用 box-drawing 字符或特定标签)
934
934
  if (/^[╭┌│╰┘╮┐└┤├]/.test(lines[0]?.trim()) ||
935
- /^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Agent)\s*[:(]/i.test(lines[0]?.trim())) {
935
+ /^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Task|Agent)\s*[:(]/i.test(lines[0]?.trim())) {
936
936
  return { type: 'tool', name: extractToolName(clean) };
937
937
  }
938
938
 
@@ -950,11 +950,22 @@
950
950
  }
951
951
 
952
952
  function extractToolName(clean) {
953
- const m = clean.match(/^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Agent)/i);
953
+ const m = clean.match(/^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Task|Agent)/i);
954
954
  return m ? m[1] : '工具';
955
955
  }
956
956
 
957
- const TOOL_ICONS = { Read:'📄', Edit:'✏️', Write:'📝', Bash:'💻', Grep:'🔍', Glob:'📂', WebFetch:'🌐', WebSearch:'🔎', LSP:'🔧', Agent:'🤖' };
957
+ const TOOL_ICONS = { Read:'📄', Edit:'✏️', Write:'📝', Bash:'💻', Grep:'🔍', Glob:'📂', WebFetch:'🌐', WebSearch:'🔎', LSP:'🔧', Task:'🤖', Agent:'🤖' };
958
+
959
+ // 子代理(Task 工具)渲染:把 subagent_type / description / prompt 拆开展示,
960
+ // 而不是甩一坨 input JSON。返回 { label, body } 交给 addToolMessage。
961
+ function subagentToolInfo(input) {
962
+ const type = (input && input.subagent_type) || 'general-purpose';
963
+ const desc = (input && input.description) || '';
964
+ const prompt = (input && input.prompt) || '';
965
+ const label = `子代理 · ${type}`;
966
+ const body = (desc ? `📋 ${desc}\n\n` : '') + (prompt || JSON.stringify(input || {}, null, 2));
967
+ return { label, body };
968
+ }
958
969
 
959
970
  // ── 消息渲染 ──────────────────────────────────
960
971
  function addInputMessage(text) {
@@ -1210,7 +1221,7 @@
1210
1221
  // 造成「文本堆在上面、工具回显落在后面」的错序——重进后按 history 顺序回放才正常。
1211
1222
  endStream();
1212
1223
  removeThinking();
1213
- const icon = TOOL_ICONS[toolName] || '🔧';
1224
+ const icon = TOOL_ICONS[toolName] || (typeof toolName === 'string' && toolName.startsWith('子代理') ? '🤖' : '🔧');
1214
1225
  const collapsed = settings.collapseTools;
1215
1226
 
1216
1227
  // 创建单个工具项
@@ -1652,7 +1663,12 @@
1652
1663
  if (block.name === 'AskUserQuestion' && block.input) {
1653
1664
  openAskSheet(block.input);
1654
1665
  }
1655
- addToolMessage(JSON.stringify(block.input || {}, null, 2), block.name || '工具');
1666
+ if (block.name === 'Task' && block.input) {
1667
+ const info = subagentToolInfo(block.input);
1668
+ addToolMessage(info.body, info.label);
1669
+ } else {
1670
+ addToolMessage(JSON.stringify(block.input || {}, null, 2), block.name || '工具');
1671
+ }
1656
1672
  } else if (block.type === 'tool_result') {
1657
1673
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
1658
1674
  addRawOutput(content);
@@ -1714,7 +1730,12 @@
1714
1730
  if (msg.name === 'AskUserQuestion' && msg.input) {
1715
1731
  openAskSheet(msg.input);
1716
1732
  }
1717
- addToolMessage(JSON.stringify(msg.input || {}, null, 2), msg.name || '工具');
1733
+ if (msg.name === 'Task' && msg.input) {
1734
+ const info = subagentToolInfo(msg.input);
1735
+ addToolMessage(info.body, info.label);
1736
+ } else {
1737
+ addToolMessage(JSON.stringify(msg.input || {}, null, 2), msg.name || '工具');
1738
+ }
1718
1739
  setWorkState('working');
1719
1740
  break;
1720
1741
  case 'tool_result':