@wendongfly/myhi 1.3.98 → 1.3.101
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 +94 -0
- package/dist/attach.js +1 -1
- package/dist/chat.html +112 -8
- package/dist/index.js +1 -1
- package/dist/index.min.js +144 -144
- package/package.json +1 -1
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 => ({ '&':'&','<':'<','>':'>','"':'"' }[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
|
|
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("[90m[会话已连接][0m\r\n");else if(e.subtype==="interrupted")process.stderr.write("\r\n[33m[已中断][0m\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[36m[工具] ${e}[0m\r\n`);if(t.input){const e=typeof t.input==="string"?t.input:JSON.stringify(t.input,null,2);process.stdout.write("[90m"+e.slice(0,500)+"[0m\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[36m[工具] ${e.tool_name||e.name||"?"}[0m\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("[90m"+t.slice(0,1e3)+"[0m\r\n")}break;case"result":process.stdout.write(`\r\n[32m[完成][0m ${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(`[34m> ${t.content}[0m\r\n`);else if(t.type==="result")process.stdout.write("[32m[完成][0m\r\n")}process.stdout.write("\r\n")}));e.on("agent:busy",(e=>{if(e)process.stderr.write("[90m[思考中...][0m\r\n")}));e.on("agent:error",(e=>{process.stderr.write(`[31m[错误] ${e.message}[0m\r\n`)}));const s=(0,i.createInterface)({input:process.stdin,output:process.stdout,prompt:"[34m> [0m"});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("[33m[正在处理中,请等待][0m\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
|
@@ -382,7 +382,6 @@
|
|
|
382
382
|
<button class="sk sk-claude" onclick="openResumeSheet()">恢复</button>
|
|
383
383
|
<button class="sk sk-claude" onclick="doCompact()">压缩</button>
|
|
384
384
|
<button class="sk sk-claude" data-cmd="/clear">清除</button>
|
|
385
|
-
<button class="sk sk-claude" onclick="doRename()">命名</button>
|
|
386
385
|
<button class="sk sk-claude" onclick="openGitSheet()" style="color:var(--green)">提交</button>
|
|
387
386
|
<button class="sk" data-send="ctrl-c">Ctrl+C</button>
|
|
388
387
|
<button class="sk" data-send="esc">Esc</button>
|
|
@@ -393,7 +392,7 @@
|
|
|
393
392
|
<button class="sk sk-claude" onclick="doSlashCmd('/compact')">压缩</button>
|
|
394
393
|
<button class="sk sk-claude" onclick="openResumeSheet()">恢复</button>
|
|
395
394
|
<button class="sk sk-claude" onclick="doClear()">清除</button>
|
|
396
|
-
<button class="sk sk-claude" onclick="
|
|
395
|
+
<button class="sk sk-claude" onclick="openSecretsSheet()" style="color:var(--blue2)">🔑 密钥</button>
|
|
397
396
|
<button class="sk sk-claude" onclick="openGitSheet()" style="color:var(--green)">提交</button>
|
|
398
397
|
<button class="sk sk-claude" onclick="showMemory()">记忆</button>
|
|
399
398
|
<button class="sk sk-claude" onclick="openVaSheet()" style="color:var(--blue2)">语音</button>
|
|
@@ -534,6 +533,27 @@
|
|
|
534
533
|
</div>
|
|
535
534
|
</div>
|
|
536
535
|
|
|
536
|
+
<!-- 项目密钥(会话级,绑定本会话 cwd) -->
|
|
537
|
+
<div id="secrets-sheet" class="action-sheet">
|
|
538
|
+
<div class="action-sheet-backdrop" onclick="closeSecretsSheet()"></div>
|
|
539
|
+
<div class="action-sheet-box" style="max-height:82vh;display:flex;flex-direction:column">
|
|
540
|
+
<div class="action-sheet-title">项目密钥 · 本会话</div>
|
|
541
|
+
<div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.4rem;line-height:1.5">
|
|
542
|
+
只注入到<b>本项目 (cwd) 的会话</b>,命令里用 <code>$名字</code> 引用(证书类型用 <code>$MYHI_SECRET_名字</code>)。密钥值不进聊天。新增后需 <b>清除/重开会话</b>才注入进环境变量。
|
|
543
|
+
</div>
|
|
544
|
+
<div id="sec-sheet-list" style="flex:1;overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin"></div>
|
|
545
|
+
<div style="padding:0.3rem 0.2rem;display:flex;flex-direction:column;gap:0.4rem;border-top:1px solid var(--surface2)">
|
|
546
|
+
<div style="display:flex;gap:0.4rem">
|
|
547
|
+
<input id="sec-sheet-name" class="slash-inp" placeholder="名字 如 GITLAB_PAT" autocomplete="off" style="flex:1">
|
|
548
|
+
<select id="sec-sheet-type" class="slash-inp" style="max-width:88px"><option value="env">变量</option><option value="file">文件</option></select>
|
|
549
|
+
</div>
|
|
550
|
+
<input id="sec-sheet-value" class="slash-inp" type="password" placeholder="密钥值 / 证书内容" autocomplete="off">
|
|
551
|
+
</div>
|
|
552
|
+
<button class="action-sheet-cancel" style="background:var(--blue2);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="addProjectSecret()">保存密钥</button>
|
|
553
|
+
<button class="action-sheet-cancel" onclick="closeSecretsSheet()">关闭</button>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
537
557
|
<div id="memory-sheet" class="action-sheet">
|
|
538
558
|
<div class="action-sheet-backdrop" onclick="closeMemorySheet()"></div>
|
|
539
559
|
<div class="action-sheet-box" style="max-height:80vh;display:flex;flex-direction:column">
|
|
@@ -932,7 +952,7 @@
|
|
|
932
952
|
|
|
933
953
|
// 工具调用(Claude Code 使用 box-drawing 字符或特定标签)
|
|
934
954
|
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())) {
|
|
955
|
+
/^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Task|Agent)\s*[:(]/i.test(lines[0]?.trim())) {
|
|
936
956
|
return { type: 'tool', name: extractToolName(clean) };
|
|
937
957
|
}
|
|
938
958
|
|
|
@@ -950,11 +970,22 @@
|
|
|
950
970
|
}
|
|
951
971
|
|
|
952
972
|
function extractToolName(clean) {
|
|
953
|
-
const m = clean.match(/^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Agent)/i);
|
|
973
|
+
const m = clean.match(/^\s*(Read|Edit|Write|Bash|Grep|Glob|WebFetch|WebSearch|LSP|TodoRead|TodoWrite|Task|Agent)/i);
|
|
954
974
|
return m ? m[1] : '工具';
|
|
955
975
|
}
|
|
956
976
|
|
|
957
|
-
const TOOL_ICONS = { Read:'📄', Edit:'✏️', Write:'📝', Bash:'💻', Grep:'🔍', Glob:'📂', WebFetch:'🌐', WebSearch:'🔎', LSP:'🔧', Agent:'🤖' };
|
|
977
|
+
const TOOL_ICONS = { Read:'📄', Edit:'✏️', Write:'📝', Bash:'💻', Grep:'🔍', Glob:'📂', WebFetch:'🌐', WebSearch:'🔎', LSP:'🔧', Task:'🤖', Agent:'🤖' };
|
|
978
|
+
|
|
979
|
+
// 子代理(Task 工具)渲染:把 subagent_type / description / prompt 拆开展示,
|
|
980
|
+
// 而不是甩一坨 input JSON。返回 { label, body } 交给 addToolMessage。
|
|
981
|
+
function subagentToolInfo(input) {
|
|
982
|
+
const type = (input && input.subagent_type) || 'general-purpose';
|
|
983
|
+
const desc = (input && input.description) || '';
|
|
984
|
+
const prompt = (input && input.prompt) || '';
|
|
985
|
+
const label = `子代理 · ${type}`;
|
|
986
|
+
const body = (desc ? `📋 ${desc}\n\n` : '') + (prompt || JSON.stringify(input || {}, null, 2));
|
|
987
|
+
return { label, body };
|
|
988
|
+
}
|
|
958
989
|
|
|
959
990
|
// ── 消息渲染 ──────────────────────────────────
|
|
960
991
|
function addInputMessage(text) {
|
|
@@ -1210,7 +1241,7 @@
|
|
|
1210
1241
|
// 造成「文本堆在上面、工具回显落在后面」的错序——重进后按 history 顺序回放才正常。
|
|
1211
1242
|
endStream();
|
|
1212
1243
|
removeThinking();
|
|
1213
|
-
const icon = TOOL_ICONS[toolName] || '🔧';
|
|
1244
|
+
const icon = TOOL_ICONS[toolName] || (typeof toolName === 'string' && toolName.startsWith('子代理') ? '🤖' : '🔧');
|
|
1214
1245
|
const collapsed = settings.collapseTools;
|
|
1215
1246
|
|
|
1216
1247
|
// 创建单个工具项
|
|
@@ -1652,7 +1683,12 @@
|
|
|
1652
1683
|
if (block.name === 'AskUserQuestion' && block.input) {
|
|
1653
1684
|
openAskSheet(block.input);
|
|
1654
1685
|
}
|
|
1655
|
-
|
|
1686
|
+
if (block.name === 'Task' && block.input) {
|
|
1687
|
+
const info = subagentToolInfo(block.input);
|
|
1688
|
+
addToolMessage(info.body, info.label);
|
|
1689
|
+
} else {
|
|
1690
|
+
addToolMessage(JSON.stringify(block.input || {}, null, 2), block.name || '工具');
|
|
1691
|
+
}
|
|
1656
1692
|
} else if (block.type === 'tool_result') {
|
|
1657
1693
|
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
1658
1694
|
addRawOutput(content);
|
|
@@ -1714,7 +1750,12 @@
|
|
|
1714
1750
|
if (msg.name === 'AskUserQuestion' && msg.input) {
|
|
1715
1751
|
openAskSheet(msg.input);
|
|
1716
1752
|
}
|
|
1717
|
-
|
|
1753
|
+
if (msg.name === 'Task' && msg.input) {
|
|
1754
|
+
const info = subagentToolInfo(msg.input);
|
|
1755
|
+
addToolMessage(info.body, info.label);
|
|
1756
|
+
} else {
|
|
1757
|
+
addToolMessage(JSON.stringify(msg.input || {}, null, 2), msg.name || '工具');
|
|
1758
|
+
}
|
|
1718
1759
|
setWorkState('working');
|
|
1719
1760
|
break;
|
|
1720
1761
|
case 'tool_result':
|
|
@@ -2785,6 +2826,69 @@
|
|
|
2785
2826
|
}
|
|
2786
2827
|
};
|
|
2787
2828
|
|
|
2829
|
+
// ── 项目密钥(会话级,绑定本会话 cwd;普通登录即可,非 admin)─────────
|
|
2830
|
+
window.openSecretsSheet = function() {
|
|
2831
|
+
document.getElementById('secrets-sheet').classList.add('open');
|
|
2832
|
+
loadProjectSecrets();
|
|
2833
|
+
};
|
|
2834
|
+
window.closeSecretsSheet = function() {
|
|
2835
|
+
document.getElementById('secrets-sheet').classList.remove('open');
|
|
2836
|
+
};
|
|
2837
|
+
async function loadProjectSecrets() {
|
|
2838
|
+
const box = document.getElementById('sec-sheet-list');
|
|
2839
|
+
box.innerHTML = '<div style="text-align:center;color:var(--muted);padding:1rem 0">加载中...</div>';
|
|
2840
|
+
try {
|
|
2841
|
+
const r = await fetch(`/api/session/${SESSION_ID}/secrets`);
|
|
2842
|
+
if (!r.ok) { box.innerHTML = '<div style="color:var(--muted);padding:0.5rem">读取失败</div>'; return; }
|
|
2843
|
+
const d = await r.json();
|
|
2844
|
+
const row = (s, isGlobal) => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.3rem 0.2rem;border-bottom:1px solid var(--surface2)">
|
|
2845
|
+
<span style="font-family:monospace;color:var(--text)">${escHtml(s.name)}</span>
|
|
2846
|
+
<span style="color:var(--muted);font-size:0.7rem">${s.type === 'file' ? '📄' : '🔑'}</span>
|
|
2847
|
+
<span style="flex:1;font-family:monospace;color:var(--muted);font-size:0.72rem">${escHtml(s.preview)}</span>
|
|
2848
|
+
${isGlobal ? '<span style="color:var(--muted);font-size:0.68rem">全局·只读</span>'
|
|
2849
|
+
: `<button class="sk sk-claude" style="padding:0.05rem 0.5rem" onclick="delProjectSecret('${escHtml(s.name)}')">删</button>`}
|
|
2850
|
+
</div>`;
|
|
2851
|
+
let html = '';
|
|
2852
|
+
if (d.project && d.project.length) {
|
|
2853
|
+
html += '<div style="color:var(--muted);font-size:0.7rem;margin:0.2rem 0">本项目</div>' + d.project.map(s => row(s, false)).join('');
|
|
2854
|
+
} else {
|
|
2855
|
+
html += '<div style="color:var(--muted);font-size:0.75rem;padding:0.3rem">(本项目暂无密钥)</div>';
|
|
2856
|
+
}
|
|
2857
|
+
if (d.global && d.global.length) {
|
|
2858
|
+
html += '<div style="color:var(--muted);font-size:0.7rem;margin:0.5rem 0 0.2rem">继承自全局</div>' + d.global.map(s => row(s, true)).join('');
|
|
2859
|
+
}
|
|
2860
|
+
box.innerHTML = html;
|
|
2861
|
+
} catch { box.innerHTML = '<div style="color:var(--muted);padding:0.5rem">连接失败</div>'; }
|
|
2862
|
+
}
|
|
2863
|
+
window.addProjectSecret = async function() {
|
|
2864
|
+
const name = document.getElementById('sec-sheet-name').value.trim();
|
|
2865
|
+
const value = document.getElementById('sec-sheet-value').value;
|
|
2866
|
+
const type = document.getElementById('sec-sheet-type').value;
|
|
2867
|
+
if (!name || !value) { addStatusMessage('密钥名和值都要填'); return; }
|
|
2868
|
+
try {
|
|
2869
|
+
const r = await fetch(`/api/session/${SESSION_ID}/secrets`, {
|
|
2870
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2871
|
+
body: JSON.stringify({ name, value, type }),
|
|
2872
|
+
});
|
|
2873
|
+
const d = await r.json();
|
|
2874
|
+
if (d.ok) {
|
|
2875
|
+
document.getElementById('sec-sheet-name').value = '';
|
|
2876
|
+
document.getElementById('sec-sheet-value').value = '';
|
|
2877
|
+
addStatusMessage('项目密钥已保存,清除/重开会话后可用 $' + name + ' 引用');
|
|
2878
|
+
loadProjectSecrets();
|
|
2879
|
+
} else { addStatusMessage(d.error || '保存失败'); }
|
|
2880
|
+
} catch { addStatusMessage('连接失败'); }
|
|
2881
|
+
};
|
|
2882
|
+
window.delProjectSecret = async function(name) {
|
|
2883
|
+
try {
|
|
2884
|
+
await fetch(`/api/session/${SESSION_ID}/secrets`, {
|
|
2885
|
+
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
|
2886
|
+
body: JSON.stringify({ name }),
|
|
2887
|
+
});
|
|
2888
|
+
loadProjectSecrets();
|
|
2889
|
+
} catch {}
|
|
2890
|
+
};
|
|
2891
|
+
|
|
2788
2892
|
// ── 会话命名 ──────────────────────────────────
|
|
2789
2893
|
let _slashCmd = null;
|
|
2790
2894
|
const _slashTitles = { '/plan': '计划模式', '/compact': '压缩上下文', '/rename': '重命名会话' };
|