@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 +94 -0
- package/dist/attach.js +1 -1
- package/dist/chat.html +27 -6
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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':
|