@wendongfly/myhi 1.3.97 → 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{}}));