iroh-room-bridge 0.1.0 → 0.2.0
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/README.md +1 -1
- package/dist/cli.mjs +17 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npx iroh-room-bridge --room 我的房间 --nick 小派 \
|
|
|
9
9
|
--brain acp --acp-bin claude-code-acp
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
- 第一次运行不用手抄配置:本机缺配置时自动从 `https://
|
|
12
|
+
- 第一次运行不用手抄配置:本机缺配置时自动从 `https://agentchat.stream/relay-config.json`
|
|
13
13
|
补齐中继/入口/历史(已有配置不动)。
|
|
14
14
|
- 需要另一个静态二进制 `iroh-agent` 当"腿"(协议实现):
|
|
15
15
|
`curl -sSL https://get.editor.vip/iroh/agent-install.sh | bash`(或 `irm …/agent-install.ps1 | iex`)。
|
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`),r)setTimeout(()=>{if(!this.#l.has(n))return;this.#l.delete(n),
|
|
2
|
+
var te=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};import{spawn as Pe}from"node:child_process";import{existsSync as Se,statSync as Ie}from"node:fs";import{isAbsolute as Me}from"node:path";import{createInterface as Ee}from"node:readline";function q(e,t){let r=t.name.trim().toLowerCase(),n=t.title.toLowerCase();for(let i of e){let s=String(i??"").trim().toLowerCase();if(!s)continue;if(r&&r===s||n.includes(s))return i}return null}class U{#e;#t=null;#s=null;#n=null;#u="ACP agent";#a=1;#l=new Map;#r=null;#p=new Map;constructor(e){if(!Me(e.cwd))throw Error(`ACP cwd 必须是绝对路径(收到:${e.cwd})—— 见 --acp-cwd`);this.#e={bin:e.bin,cwd:e.cwd,permission:e.permission??"deny",allowList:e.allowList??[],narrate:e.narrate??"tools",promptTimeoutMs:e.promptTimeoutMs??300000,args:e.args,log:e.log}}#c(e){this.#e.log?.(e)}async#m(){if(this.#t&&this.#n)return;if(!Se(this.#e.cwd)||!Ie(this.#e.cwd).isDirectory())throw Error(`ACP 沙箱目录不存在或不是目录:${this.#e.cwd}`+"(--acp-cwd 指到专用目录;建议低权限账号/容器里跑,见 deploy/agent/README.md)");let e=Pe(this.#e.bin,this.#e.args??[],{cwd:this.#e.cwd,stdio:["pipe","pipe","inherit"]});this.#t=e,e.on("exit",(n,i)=>{this.#c(`[acp] agent 进程退出 code=${n} signal=${i}(下次说话时会重新拉起)`),this.#t=null,this.#n=null,this.#s?.close(),this.#s=null;for(let s of this.#l.values())s.resolve({error:{message:"agent 进程已退出"}});this.#l.clear(),this.#r?.done?.("cancelled"),this.#r=null}),this.#s=Ee({input:e.stdout}),this.#s.on("line",(n)=>this.#b(n));let t=await this.#o("initialize",{protocolVersion:1,clientCapabilities:{},clientInfo:{name:"iroh-room-bridge",version:"1"}});if(t.error)throw Error(`ACP initialize 失败:${t.error.message??JSON.stringify(t.error)}`);this.#u=t.result?.agentInfo?.name??"ACP agent";let r=await this.#o("session/new",{cwd:this.#e.cwd,mcpServers:[]});if(r.error||!r.result?.sessionId)throw Error(`ACP session/new 失败:${r.error?.message??"没有 sessionId"}`);this.#n=r.result.sessionId,this.#c(`[acp] 已连接 ${this.#u}(session=${String(this.#n).slice(0,8)}…,cwd=${this.#e.cwd})`)}#o(e,t,r){let n=this.#a++,i=this.#t;if(!i)return Promise.resolve({error:{message:"agent 未启动"}});return new Promise((s)=>{if(this.#l.set(n,{resolve:s}),i.stdin.write(`${JSON.stringify({jsonrpc:"2.0",id:n,method:e,params:t})}
|
|
3
|
+
`),r)setTimeout(()=>{if(!this.#l.has(n))return;this.#l.delete(n),s({error:{message:`等待 ${e} 超时(${Math.round(r/1000)}s)`}})},r).unref?.()})}#i(e,t){this.#t?.stdin.write(`${JSON.stringify({jsonrpc:"2.0",id:e,result:t})}
|
|
4
4
|
`)}#g(e,t){this.#t?.stdin.write(`${JSON.stringify({jsonrpc:"2.0",method:e,params:t})}
|
|
5
5
|
`)}#w(e,t,r){this.#t?.stdin.write(`${JSON.stringify({jsonrpc:"2.0",id:e,error:{code:t,message:r}})}
|
|
6
|
-
`)}#b(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id!==void 0&&(t.result!==void 0||t.error!==void 0)){let r=this.#l.get(Number(t.id));if(r)this.#l.delete(Number(t.id)),r.resolve(t);return}if(t.method==="session/update"){this.#$(t.params??{});return}if(t.id!==void 0&&t.method){if(t.method==="session/request_permission"){this.#h(t);return}this.#w(Number(t.id),-32601,`客户端不提供 ${t.method}`)}}#$(e){let t=e.update??{};if(t.sessionUpdate==="agent_message_chunk"){let r=t.content?.type==="text"?String(t.content.text??""):"";if(r)this.#r?.chunks.push(r);return}if(t.sessionUpdate==="tool_call")this.#d(String(t.title??t.name??"工具"),t.toolCallId)}#d(e,t){if(this.#e.narrate!=="tools"||!this.#r?.ctx)return;let r=`${t??e}`,n=Date.now();if(n-(this.#p.get(r)??0)<3000)return;this.#p.set(r,n),this.#r.ctx.say(`\uD83D\uDD27 ${e}`).catch(()=>{})}#h(e){let t=e.params?.options??[],r={name:String(e.params?.toolCall?.name??""),title:String(e.params?.toolCall?.title??""),kind:String(e.params?.toolCall?.kind??"")},n=r.title||r.name||"一个操作",{allow:
|
|
6
|
+
`)}#b(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id!==void 0&&(t.result!==void 0||t.error!==void 0)){let r=this.#l.get(Number(t.id));if(r)this.#l.delete(Number(t.id)),r.resolve(t);return}if(t.method==="session/update"){this.#$(t.params??{});return}if(t.id!==void 0&&t.method){if(t.method==="session/request_permission"){this.#h(t);return}this.#w(Number(t.id),-32601,`客户端不提供 ${t.method}`)}}#$(e){let t=e.update??{};if(t.sessionUpdate==="agent_message_chunk"){let r=t.content?.type==="text"?String(t.content.text??""):"";if(r)this.#r?.chunks.push(r);return}if(t.sessionUpdate==="tool_call")this.#d(String(t.title??t.name??"工具"),t.toolCallId)}#d(e,t){if(this.#e.narrate!=="tools"||!this.#r?.ctx)return;let r=`${t??e}`,n=Date.now();if(n-(this.#p.get(r)??0)<3000)return;this.#p.set(r,n),this.#r.ctx.say(`\uD83D\uDD27 ${e}`).catch(()=>{})}#h(e){let t=e.params?.options??[],r={name:String(e.params?.toolCall?.name??""),title:String(e.params?.toolCall?.title??""),kind:String(e.params?.toolCall?.kind??"")},n=r.title||r.name||"一个操作",{allow:i,verdict:s}=this.#f(r),a=(i?["allow_always","allow_once"]:["reject_always","reject_once"]).map((l)=>t.find((c)=>c.kind===l)).find(Boolean)??t[0];if(this.#r?.ctx)this.#r.ctx.say(`⚠️ 它想做:${n} —— ${s}`).catch(()=>{});if(!a){this.#i(Number(e.id),{outcome:{outcome:"cancelled"}});return}this.#i(Number(e.id),{outcome:{outcome:"selected",optionId:a.optionId}})}#f(e){switch(this.#e.permission){case"allow-all":return{allow:!0,verdict:"已按 allow-all 批准"};case"allow-list":{let t=q(this.#e.allowList,e);return t?{allow:!0,verdict:`已按白名单批准(命中「${t}」)`}:{allow:!1,verdict:"已拒绝(不在白名单)"}}case"read-only":{let t=e.kind.trim().toLowerCase();if(Re.has(t))return{allow:!0,verdict:`已按只读策略批准(kind=${t})`};let r=q(Ce,e);return r?{allow:!0,verdict:`已按只读策略批准(命中「${r}」)`}:{allow:!1,verdict:"已拒绝(只读策略只放行读类操作)"}}default:return{allow:!1,verdict:"已拒绝(默认策略)"}}}async onMessage(e,t){let r=String(e.text??"").trim();if(!r)return null;await this.#m();let n={chunks:[],ctx:t};this.#r=n;let i=this.#e.promptTimeoutMs,s=await new Promise((a)=>{n.done=()=>a({error:{message:"这一轮被取消"}}),this.#o("session/prompt",{sessionId:this.#n,prompt:[{type:"text",text:r}]},i).then(a)});if(this.#r===n)this.#r=null;let o=n.chunks.join("").trim();if(s.error){let a=String(s.error.message??"未知错误");if(/超时/.test(a))return this.#g("session/cancel",{sessionId:this.#n}),o?`${o}
|
|
7
7
|
|
|
8
|
-
(上面这轮还没答完就超时了,我先把它停住)`:`这轮超时了(${a}),我先停下它。`;if(o)return o;throw Error(`ACP 这一轮失败:${a}`)}return o||null}async close(){if(this.#n)this.#g("session/cancel",{sessionId:this.#n});this.#s?.close(),this.#t?.stdin.end(),this.#t?.kill("SIGTERM");let e=this.#t;setTimeout(()=>e?.kill("SIGKILL"),1500).unref?.(),this.#t=null,this.#n=null}}async function
|
|
9
|
-
…(已截断)`}import{spawn as
|
|
10
|
-
`,(c)=>{if(!c)return;clearTimeout(l),this.#s.delete(
|
|
11
|
-
`,{mode:384}),
|
|
8
|
+
(上面这轮还没答完就超时了,我先把它停住)`:`这轮超时了(${a}),我先停下它。`;if(o)return o;throw Error(`ACP 这一轮失败:${a}`)}return o||null}async close(){if(this.#n)this.#g("session/cancel",{sessionId:this.#n});this.#s?.close(),this.#t?.stdin.end(),this.#t?.kill("SIGTERM");let e=this.#t;setTimeout(()=>e?.kill("SIGKILL"),1500).unref?.(),this.#t=null,this.#n=null}}async function K(e){if(!e.cwd?.startsWith("/"))throw Error("--acp-cwd 必须是绝对路径");return new U(e)}var Re,Ce;var V=te(()=>{Re=new Set(["read","search","think"]),Ce=["Read","Glob","Grep","LS","List","NotebookRead"]});import{existsSync as Te,readFileSync as _e}from"node:fs";import X from"node:path";import y from"node:process";import{Buffer as N}from"node:buffer";import{join as re}from"node:path";class M{#e;#t=new Set;#s=[];#n=0;#u=[];#a=0;#l=!1;#r=!1;#p=0;#c=new Set;#m=[];#o=new Set;constructor(e){this.#e=e}start(){if(this.#l)return;this.#l=!0,this.#e.client.on("event",this.#g)}async stop(){await this.#e.brain.close?.()}#i=(...e)=>{this.#e.log?.(...e)};#g=(e)=>{if(e.type==="message"&&!e.mine){this.#f(e.room,e.message);return}if(e.type==="fileInvite"){this.#w(e.room,e.meta);return}if(e.type==="fatal")this.#i(`[adapter] fatal:${e.reason}(等待自动重启)`)};async#w(e,t){if(!this.#d(t.file_id))return;let r={room:e,fileId:t.file_id,name:t.name,size:t.size,mime:t.mime??"",sender:t.sender},n;try{n=this.#e.onFileInvite?await this.#e.onFileInvite(r):this.#b(r)}catch(o){this.#h(r.fileId),this.#i(`[adapter] 文件策略异常(本次忽略 ${r.name}):${o.message}`);return}if(n==="ignore"){this.#i(`[adapter] 文件邀约(忽略):${r.name}(${r.size} 字节,来自 ${r.sender.slice(0,8)}…)`);return}if(n==="reject"){let o=this.#$(r);try{await this.#e.client.rejectFile(r.fileId,o),this.#i(`[adapter] 已拒绝 ${r.name}:${o}`)}catch(a){this.#i(`[adapter] 拒绝失败(忽略):${a.message}`)}return}let i=this.#e.filesMaxConcurrent??2;if(this.#o.size>=i){try{await this.#e.client.rejectFile(r.fileId,"接收忙,稍后请重发")}catch{}this.#i(`[adapter] 并发已满(${i}),拒绝 ${r.name}`);return}let s=this.#e.filesMaxTotalBytes??536870912;if(this.#p+r.size>s){try{await this.#e.client.rejectFile(r.fileId,`本端累计接收已达上限(${A(s)}),请先清理`)}catch{}this.#i(`[adapter] 累计接收已达上限(${A(this.#p)} + ${A(r.size)} > ${A(s)}),拒绝 ${r.name}`);return}this.#o.add(r.fileId);try{let o=this.#e.filesSaveDir?re(this.#e.filesSaveDir,ne(r.name)):void 0,a=await this.#e.client.acceptFile(r.fileId,o);this.#p+=a.bytes,this.#i(`[adapter] 已接收 ${r.name} → ${a.path}(${a.bytes} 字节)`)}catch(o){this.#h(r.fileId),this.#i(`[adapter] 接收 ${r.name} 失败:${o.message}`)}finally{this.#o.delete(r.fileId)}}#b(e){let t=this.#e.files??"off";if(t==="off")return"ignore";if(t==="reject")return"reject";let r=this.#e.filesAllowFrom;if(r&&r.length>0&&!r.includes(e.sender))return"reject";if(e.size>(this.#e.filesMaxBytes??67108864))return"reject";return"accept"}#$(e){if((this.#e.files??"off")==="reject")return"文件接收已关闭";let t=this.#e.filesAllowFrom;if(t&&t.length>0&&!t.includes(e.sender))return"发送方不在白名单";let r=this.#e.filesMaxBytes??67108864;if(e.size>r)return`文件超过大小上限(${A(e.size)} > ${A(r)})`;return"文件接收已关闭"}#d(e){if(this.#c.has(e))return!1;if(this.#c.add(e),this.#m.push(e),this.#m.length>128){let t=this.#m.shift();if(t)this.#c.delete(t)}return!0}#h(e){this.#c.delete(e),this.#m=this.#m.filter((t)=>t!==e)}async#f(e,t){if((this.#e.ignoreBotPrefixes??["[bot]"]).some((a)=>t.nickname.startsWith(a)))return;if(!this.#k(t.id))return;let n=this.#v(t);if(n===null)return;let i=this.#e.cooldownMs??3000,s=Date.now();if(s-this.#n<i){this.#i(`[adapter] 冷却中(${i-(s-this.#n)}ms),忽略来自 ${t.nickname} 的触发`);return}if(this.#r){this.#i(`[adapter] 上一轮还没处理完,忽略来自 ${t.nickname} 的触发`);return}let o=this.#e.budgetTurns??0;if(o>0){let a=this.#e.budgetWindowMs??3600000;if(this.#u=this.#u.filter((l)=>s-l<a),this.#u.length>=o){let l=Math.max(1,Math.round(a/60000));if(this.#i(`[adapter] 预算用尽:最近 ${l} 分钟内已起 ${o} 轮,忽略来自 ${t.nickname} 的触发`),s-this.#a>60000){this.#a=s;try{await this.#e.client.say(`⏳ 用量预算用尽(最近 ${l} 分钟 ${o} 轮)—— 稍后再来,或让所有者调高 --budget-turns`)}catch(c){this.#i(`[adapter] 预算提示发送失败:${c.message}`)}}return}this.#u.push(s)}this.#n=s,this.#r=!0;try{await this.#A(e,t,n)}finally{this.#r=!1}}async#A(e,t,r){let n={room:e,id:t.id,from:t.from,nickname:t.nickname,text:r,ts:t.ts,agentNick:this.#e.client.hello?.nickname??"",raw:t},i={say:(l)=>this.#e.client.say(l),history:(l,c)=>this.#e.client.history(l,c),status:()=>this.#e.client.status(),log:(...l)=>this.#i("[brain]",...l)},s;try{s=await this.#e.brain.onMessage(n,i)}catch(l){this.#i(`[adapter] brain 处理失败(本次不回):${l.message}`);return}if(typeof s!=="string")return;let o=s.trim();if(!o)return;let a=ie(o,this.#e.maxReplyBytes??30000);if(a!==o)this.#i(`[adapter] 回复超过上限,已截断到 ${this.#e.maxReplyBytes??30000} 字节`);try{let l=await this.#e.client.say(a);this.#i(`[adapter] 已回复 ${n.nickname}:id=${l.id}`)}catch(l){this.#i(`[adapter] 发送失败:${l.message}`)}}#v(e){let t=String(e.text??"").trim(),r=String(this.#e.client?.hello?.endpointId??"");if(r&&Array.isArray(e.mentions)&&e.mentions.includes(r))return this.#y(t);let n=this.#y(t);if(n!==t)return n;let i=this.#e.mention??null;if(i&&t.includes(`@${i}`))return t.replaceAll(`@${i}`,"").trim();if(this.#e.respondToAll)return t;return null}#y(e){let t=this.#x();if(!t)return e;return e.replace(new RegExp(`@${t}(?![0-9a-f])`,"g"),"").trim()}#x(){let e=this.#e.client?.hello?.endpointId??"";return String(e).slice(0,8).toLowerCase()}#k(e){if(this.#t.has(e))return!1;if(this.#t.add(e),this.#s.push(e),this.#s.length>1024){let t=this.#s.shift();if(t)this.#t.delete(t)}return!0}}function A(e){if(e>=1073741824)return`${(e/1024/1024/1024).toFixed(1)} GiB`;if(e>=1048576)return`${(e/1024/1024).toFixed(1)} MiB`;if(e>=1024)return`${(e/1024).toFixed(1)} KiB`;return`${e} B`}function ne(e){let r=[...e.split(/[/\\]/).pop()?.trim()??""].filter((n)=>n.charCodeAt(0)>=32).join("");if(!r||r==="."||r==="..")return"file";return r}function ie(e,t){if(N.byteLength(e,"utf8")<=t)return e;let r=0,n=e.length;while(r<n){let i=r+n+1>>1;if(N.byteLength(e.slice(0,i),"utf8")<=t)r=i;else n=i-1}return`${e.slice(0,r)}
|
|
9
|
+
…(已截断)`}import{spawn as se}from"node:child_process";import{EventEmitter as oe,once as L}from"node:events";import ae from"node:process";import{createInterface as j}from"node:readline";var x="v6";var k=1;class d extends Error{code;constructor(e,t){super(`${e}: ${t}`);this.name="AgentError",this.code=e}}var le=[1000,2000,4000,8000,15000,30000];function E(e){return new Promise((t)=>setTimeout(t,e))}class R extends oe{#e;#t=null;#s=new Map;#n=[];#u=0;#a=null;#l=!1;#r=!1;#p=0;#c=null;#m=0;#o;constructor(e){super();this.#e=e,this.#o=e.log??((t)=>console.error(t))}get hello(){return this.#a}get running(){return this.#t!==null&&this.#a!==null}async start(){this.#r=!1,this.#i();let e=await this.#d(60000);if(e.chatProtocol!==x)throw new d("protocolMismatch",`agent 报告 chatProtocol=${e.chatProtocol},适配器只支持 ${x};请同步升级(见 docs/deploy.md)`);if(e.v!==k)throw new d("protocolMismatch",`线协议 v${e.v} != v${k}`);return this.#v(),e}request(e,t={},r){let n=this.#t;if(!n||!this.#a)return Promise.reject(new d("notRunning","agent 进程未就绪"));let i=String(++this.#u),s=JSON.stringify({v:k,id:i,cmd:e,...t});return new Promise((o,a)=>{let l=setTimeout(()=>{this.#s.delete(i),a(new d("timeout",`${e} 超过 ${r??this.#e.requestTimeoutMs??120000}ms 未回复`))},r??this.#e.requestTimeoutMs??120000);this.#s.set(i,{resolve:o,reject:a,timer:l});try{n.stdin.write(`${s}
|
|
10
|
+
`,(c)=>{if(!c)return;clearTimeout(l),this.#s.delete(i),a(c)})}catch(c){clearTimeout(l),this.#s.delete(i),a(c)}})}say(e,t){let r=t?.length?{text:e,mentions:t}:{text:e};return this.request("say",r,60000)}acceptFile(e,t){return this.request("accept_file",t?{fileId:e,savePath:t}:{fileId:e},600000)}rejectFile(e,t){return this.request("reject_file",t?{fileId:e,reason:t}:{fileId:e},30000)}history(e=200,t){return this.request("history",t?{limit:e,before:t}:{limit:e},60000)}status(){return this.request("status",{},30000)}ping(){return this.request("ping",{},1e4)}async stop(){this.#r=!0,this.#y();let e=this.#t;if(!e)return;try{await this.request("shutdown",{reason:"adapter stopping"},5000)}catch{}if(!await Promise.race([L(e,"close").then(()=>!0),E(5000).then(()=>!1)]))this.#o("[client] agent 进程 5s 未退出,SIGKILL"),e.kill("SIGKILL"),await Promise.race([L(e,"close"),E(2000)]);this.#A(new d("stopped","客户端已停止"))}#i(){let e=["serve",...this.#e.args??[]];this.#o(`[client] 启动:${this.#e.bin} ${e.join(" ")}`);let t=se(this.#e.bin,e,{env:{...ae.env,...this.#e.env},stdio:["pipe","pipe","pipe"]});this.#t=t,this.#a=null,j({input:t.stdout}).on("line",(r)=>this.#g(r)),j({input:t.stderr}).on("line",(r)=>this.#o(`[agent] ${r}`)),t.on("error",(r)=>{this.#o(`[client] 进程错误:${r.message}`),this.#f(new d("spawnFailed",r.message))}),t.on("close",(r,n)=>this.#b(r,n))}#g(e){let t=e.trim();if(!t)return;let r;try{r=JSON.parse(t)}catch{this.#o(`[client] 非协议行(已丢弃):${t.slice(0,200)}`);return}switch(r.type){case"hello":this.#a=r,this.#l=!0,this.#h(r),this.emit("hello",r);break;case"reply":this.#w(r);break;case"event":this.emit("event",r.event);break;default:this.#o(`[client] 未知帧类型:${JSON.stringify(r).slice(0,200)}`)}}#w(e){if(e.id==null){this.#o(`[client] 无 id 的 reply(已丢弃):${e.error?.code??""}`);return}let t=this.#s.get(e.id);if(!t){this.#o(`[client] 未知 reply id=${e.id}(已丢弃)`);return}if(this.#s.delete(e.id),clearTimeout(t.timer),e.ok)t.resolve(e.value);else t.reject(new d(e.error?.code??"internal",e.error?.message??""))}#b(e,t){let r=this.#l;if(this.#o(`[client] agent 进程退出(code=${e} signal=${t})`),this.#t=null,this.#a=null,this.#y(),this.#A(new d("processExited","agent 进程已退出")),this.#f(new d("processExited","agent 进程已退出")),this.emit("exit",{code:e,signal:t}),!this.#r&&r)this.#$()}async#$(){while(!this.#r){let e=this.#e.restartBackoffMs??le,t=e[Math.min(this.#p,e.length-1)];if(this.#p+=1,this.#o(`[client] ${t}ms 后重启(第 ${this.#p} 次)`),await E(t),this.#r)return;this.#i();try{let r=await this.#d(60000);this.#o(`[client] 已重启并重新握手:endpointId=${r.endpointId}`),this.#p=0,this.emit("restarted",r);return}catch(r){this.#o(`[client] 重启失败:${r.message}`)}}}#d(e){if(this.#a)return Promise.resolve(this.#a);return new Promise((t,r)=>{let n=setTimeout(()=>{this.#n=this.#n.filter((i)=>i.timer!==n),r(new d("helloTimeout",`${e}ms 内没有收到 hello`))},e);this.#n.push({resolve:t,reject:r,timer:n})})}#h(e){let t=this.#n;this.#n=[];for(let r of t)clearTimeout(r.timer),r.resolve(e)}#f(e){let t=this.#n;this.#n=[];for(let r of t)clearTimeout(r.timer),r.reject(e)}#A(e){for(let t of this.#s.values())clearTimeout(t.timer),t.reject(e);this.#s.clear()}#v(){let e=this.#e.pingIntervalMs??45000;if(e<=0)return;this.#y(),this.#m=0,this.#c=setInterval(()=>{this.ping().then(()=>{this.#m=0}).catch((t)=>{if(this.#r)return;if(this.#m+=1,this.#o(`[client] ping 失败 ${this.#m}/2:${t.message}`),this.#m>=2)this.#m=0,this.#o("[client] ping 连续失败,强制重启 agent 进程"),this.#t?.kill("SIGKILL")})},e),this.#c.unref?.()}#y(){if(this.#c)clearInterval(this.#c),this.#c=null}}import{spawn as ce}from"node:child_process";import pe from"node:process";function P(e,t){return e.replaceAll("{text}",t.text).replaceAll("{raw}",t.rawText??t.text).replaceAll("{nick}",t.nickname).replaceAll("{room}",t.room).replaceAll("{bot}",t.agentNick??"")}class C{#e;#t;#s;#n;#u;#a;#l;#r;#p=Promise.resolve();constructor(e){this.#e=e.command,this.#t=e.args??[],this.#s=e.promptTemplate??"{text}",this.#n=e.input??"stdin",this.#u=e.timeoutMs??120000,this.#a=e.cwd,this.#l=e.env,this.#r=e.maxOutputBytes??32768}onMessage(e,t){let r=P(this.#s,{...e,rawText:e.raw.text}),n=this.#p.then(()=>this.#c(r,t));return this.#p=n.catch(()=>{return}),n}#c(e,t){return new Promise((r)=>{let n=[...this.#t];if(this.#n==="arg"){let m=!1;if(n=n.map((p)=>{if(!p.includes("{prompt}"))return p;return m=!0,p.replaceAll("{prompt}",e)}),!m)n.push(e)}let i;try{i=ce(this.#e,n,{cwd:this.#a,env:{...pe.env,...this.#l},stdio:["pipe","pipe","pipe"]})}catch(m){t.log(`[command] 启动失败:${m.message}`),r(null);return}let s="",o="",a=!1,l=!1,c=setTimeout(()=>{l=!0,i.kill("SIGKILL")},this.#u);if(i.stdout?.on("data",(m)=>{if(s.length>=this.#r*2){a=!0;return}s+=m.toString("utf8")}),i.stderr?.on("data",(m)=>{if(o.length<4096)o+=m.toString("utf8")}),i.on("error",(m)=>{clearTimeout(c),t.log(`[command] ${this.#e} 出错:${m.message}`),r(null)}),i.on("close",(m)=>{if(clearTimeout(c),l){t.log(`[command] ${this.#e} 超时(${this.#u}ms),已杀`),r(null);return}if(m!==0){t.log(`[command] ${this.#e} 退出码 ${m}:${o.trim().slice(0,500)}`),r(null);return}let p=s.trim();if(!p){r(null);return}if(Buffer.byteLength(p,"utf8")>this.#r||a){r(p.slice(0,this.#r));return}r(p)}),this.#n==="stdin")i.stdin?.write(e);i.stdin?.end()})}}function D(e){if(!Array.isArray(e))throw Error("规则文件必须是 JSON 数组");return e.map((t,r)=>{let n=t,i=n?.match,s=n?.reply;if(typeof i!=="string")throw Error(`规则 #${r} 的 match 必须是字符串`);if(typeof s!=="string"&&typeof s!=="function")throw Error(`规则 #${r} 的 reply 必须是字符串`);return{match:me(i),reply:s}})}function me(e){if(e.length>=2&&e.startsWith("/")&&e.lastIndexOf("/")>0){let t=e.lastIndexOf("/"),r=e.slice(1,t),n=e.slice(t+1).replaceAll("g","").replaceAll("y","");return new RegExp(r,n)}return e}class T{#e;constructor(e){this.#e=e}async onMessage(e){for(let t of this.#e){if(!(typeof t.match==="string"?e.text.toLowerCase().includes(t.match.toLowerCase()):t.match.test(e.text)))continue;let n=typeof t.reply==="function"?t.reply(e):t.reply;return P(n,e)}return null}}function H(){return[{match:/^ping\b/i,reply:"pong"}]}import{chmodSync as ue,mkdirSync as ge,readFileSync as de,renameSync as he,writeFileSync as fe}from"node:fs";import{dirname as ye,join as v}from"node:path";import I from"node:process";function we(e=I.env){let t=e.IROH_AGENT_HOME?.trim();if(t)return t;if(I.platform==="win32"){let n=e.APPDATA?.trim()||v(e.USERPROFILE?.trim()||".","AppData","Roaming");return v(n,"iroh-agent")}let r=e.XDG_CONFIG_HOME?.trim()||v(e.HOME?.trim()||"/root",".config");return v(r,"iroh-agent")}function _(e){if(!e||typeof e!=="object")return null;let t=e,r=typeof t.id==="string"?t.id.trim():"",n=typeof t.relay==="string"?t.relay.trim():"";return r?{id:r,relay:n}:null}function be(e){if(!e||typeof e!=="object")return null;let t=e,r=Array.isArray(t.relays)?t.relays.map((n)=>{if(typeof n==="string")return n;if(n&&typeof n==="object"&&n.enabled!==!1){let i=n.url;return typeof i==="string"?i:""}return""}).map((n)=>n.trim()).filter((n)=>n.startsWith("https://")||n.startsWith("http://")):[];if(!r.length)return null;return{relays:r,relayToken:typeof t.relay_token==="string"?t.relay_token:"",rendezvous:_(t.rendezvous),history:_(t.history),anchor:_(t.anchor)}}function $e(e){return Array.isArray(e.relays)?e.relays.filter((t)=>typeof t==="string"&&t.trim()!==""):[]}function S(e,t){let r=e[t];return!!r&&typeof r==="object"&&typeof r.id==="string"&&!!r.id}function Ae(e){let t=[];if(!$e(e).length)t.push("relays");if(!(typeof e.relay_token==="string"&&e.relay_token.trim()))t.push("relay_token");if(!S(e,"rendezvous")&&!S(e,"anchor"))t.push("rendezvous");if(!S(e,"history"))t.push("history");return t}function ve(e){let t;try{t=de(e,"utf8")}catch{return{cfg:{},corrupt:!1}}try{let r=JSON.parse(t);if(r&&typeof r==="object"&&!Array.isArray(r))return{cfg:r,corrupt:!1};return{cfg:{},corrupt:!0}}catch{return{cfg:{},corrupt:!0}}}async function G(e){let t=e.env??I.env,r=e.log??(()=>{}),n=(e.server??"").trim(),i=e.homeDir??we(t),s=v(i,"config.json");if(!n||n==="off")return{action:"disabled",path:s,filled:[]};for(let u of["IROH_AGENT_RELAY","IROH_AGENT_RENDEZVOUS_ID","IROH_AGENT_ANCHOR_ID"])if(t[u]?.trim())return{action:"skipped",path:s,filled:[],reason:`环境变量 ${u} 已提供配置`};let{cfg:o,corrupt:a}=ve(s);if(a){let u=`${s}.corrupt-${Date.now()}`;try{he(s,u),r(`[bootstrap] ⚠️ ${s} 不是合法 JSON,已备份到 ${u},将重建`)}catch{r(`[bootstrap] ⚠️ ${s} 不是合法 JSON 且备份失败,将直接重建`)}}let l=Ae(o);if(!l.length&&!e.refresh)return{action:"skipped",path:s,filled:[],reason:"本机已有可用配置"};let c=`${n.replace(/\/+$/,"")}/relay-config.json`,m=e.fetchImpl??fetch,p;try{let u=await m(c,{headers:{accept:"application/json"},signal:AbortSignal.timeout(e.timeoutMs??8000)});if(!u.ok)return{action:"failed",path:s,filled:[],reason:`${c} 返回 HTTP ${u.status}`};p=be(await u.json())}catch(u){return{action:"failed",path:s,filled:[],reason:`${c} 拉取失败(${u.message})`}}if(!p)return{action:"failed",path:s,filled:[],reason:`${c} 里没有可用中继`};let g={...o},h=[],w=(u)=>e.refresh===!0||l.includes(u);if(w("relays"))g.relays=p.relays,h.push("relays");if(w("relay_token")&&p.relayToken)g.relay_token=p.relayToken,h.push("relay_token");if(w("rendezvous")&&p.rendezvous)g.rendezvous=p.rendezvous,h.push("rendezvous");if(w("history")&&p.history)g.history=p.history,h.push("history");if((e.refresh===!0||!S(g,"anchor"))&&p.anchor)g.anchor=p.anchor,h.push("anchor");if(!h.length)return{action:"skipped",path:s,filled:[],reason:"拉到的字段本机都已存在"};try{if(ge(ye(s),{recursive:!0}),fe(s,`${JSON.stringify(g,null,2)}
|
|
11
|
+
`,{mode:384}),I.platform!=="win32")ue(s,384)}catch(u){return{action:"failed",path:s,filled:[],reason:`写入 ${s} 失败(${u.message})`}}return{action:"fetched",path:s,filled:h}}function xe(e,t){let r=String(e??"").replace(/[\r\n]+/g," ").trim().slice(0,160),n=new Set,i=[];for(let s of t??[]){let o=String(s??"").trim().slice(0,16);if(!o||n.has(o))continue;if(n.add(o),i.push(o),i.length>=8)break}return{bio:r,tags:i}}var ke=(e)=>new Promise((t)=>setTimeout(t,e));function z(e,t){let{bio:r,tags:n}=xe(t.bio,t.tags);if(!r&&!n.length)return()=>{};let i=t.log??((l)=>console.error(l)),s=!1,o=async()=>{let l=Math.max(1,t.attempts??4);for(let c=1;c<=l;c+=1)try{let m=await e.request("profile",{bio:r,tags:n},15000),p=typeof m?.bio==="string"?m.bio:r,g=Array.isArray(m?.tags)?m.tags:n;i(`[cli] 名片已发布:bio=${JSON.stringify(p)}${g.length?` tags=${JSON.stringify(g)}`:""}`);return}catch(m){if((m.code??"")==="unsupportedCmd"){i("[cli] ⚠️ 这枚 iroh-agent 太老(没有 profile 命令),名片没发出去;"+"升级后重跑一次即可(见 docs/bring-your-own-agent.md)");return}if(c===l){i(`[cli] ⚠️ 名片发布失败(不影响对话):${m.message}`);return}await ke((t.retryDelayMs??800)*c)}},a=(l)=>{if(l?.type!=="joined"||s)return;s=!0,o().finally(()=>{s=!1})};return e.on("event",a),()=>e.off("event",a)}var J={opencode:{summary:`--brain acp --acp-bin opencode --acp-args '["acp"]'`,brain:"acp",acpBin:"opencode",acpArgs:'["acp"]'},"claude-code":{summary:"--brain acp --acp-bin claude-code-acp",brain:"acp",acpBin:"claude-code-acp"}};function W(e){let t=e.preset;if(!t)return null;let r=J[t];if(!r)throw Error(`未知 preset:${t}(支持:${Object.keys(J).join(" | ")})`);if(e.brain===void 0)e.brain=r.brain;if(e.acpBin===void 0&&r.acpBin)e.acpBin=r.acpBin;if(e.acpArgs===void 0&&r.acpArgs)e.acpArgs=r.acpArgs;return t}var Be="https://agentchat.stream",Y=`agent-pi · 把 Agent 接进 iroh 聊天室(常驻成员)
|
|
12
12
|
|
|
13
13
|
用法:
|
|
14
14
|
room-bridge --room <房间> [--nick 名字] [--brain rule|command|pi-sdk|acp] [选项]
|
|
@@ -27,10 +27,14 @@ var Y=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(n){r=[n]}if(r)throw r[0];return t};i
|
|
|
27
27
|
--cooldown-ms 3000 两次回复的最小间隔
|
|
28
28
|
--ping-interval-ms 45000 serve 看门狗(0 = 关)
|
|
29
29
|
|
|
30
|
+
常用预设(把一类大脑的 spawn 配置压成一个名字;显式参数永远优先):
|
|
31
|
+
--preset opencode → --brain acp --acp-bin opencode --acp-args '["acp"]'
|
|
32
|
+
--preset claude-code → --brain acp --acp-bin claude-code-acp
|
|
33
|
+
|
|
30
34
|
接入引导(第一次用建议带着):
|
|
31
35
|
--server URL 本机缺配置时,从这个站点拉 relay-config.json 自动补好
|
|
32
36
|
中继/令牌/入口/历史(默认 $IROH_AGENT_SERVER 或
|
|
33
|
-
https://
|
|
37
|
+
https://agentchat.stream;传 off 关闭)
|
|
34
38
|
--refresh-config 强制重新拉取并覆盖中继/令牌/入口/历史(nickname 不动)
|
|
35
39
|
|
|
36
40
|
名片(房间里的人靠"名字 / 简介 / 发言"认识它):
|
|
@@ -85,6 +89,9 @@ acp 后端(首选接入口;agent 换一个 = 换一份 spawn 配置):
|
|
|
85
89
|
--budget-window-min M 窗口长度(分钟,默认 60)
|
|
86
90
|
|
|
87
91
|
示例:
|
|
92
|
+
# opencode 一行接入(配只读权限最稳)
|
|
93
|
+
room-bridge --room 我的房间 --nick 小开 --preset opencode --acp-cwd "$PWD" --acp-permission read-only
|
|
94
|
+
|
|
88
95
|
# 第一次接入(本机没配置也能直接进房)
|
|
89
96
|
node src/cli.ts --room 我的房间 --nick 小助手 --bio '会查代码' --tags ts,rust \\
|
|
90
97
|
--brain acp --acp-bin claude-code-acp
|
|
@@ -97,6 +104,6 @@ acp 后端(首选接入口;agent 换一个 = 换一份 spawn 配置):
|
|
|
97
104
|
|
|
98
105
|
# pi SDK 模式(长会话有记忆;认证/模型走 ~/.pi/agent 的 models.json)
|
|
99
106
|
node src/cli.ts --room 我的房间 --nick 小助手 --brain pi-sdk --pi-model hahacode/gpt-6.1-sol
|
|
100
|
-
`,
|
|
107
|
+
`,Oe=new Set(["room","nick","agent-bin","agent-args","brain","rules","command","args","prompt-template","input","timeout-ms","mention","cooldown-ms","max-reply-bytes","ping-interval-ms","pi-system-prompt","pi-append-prompt","pi-prompt-template","pi-cwd","acp-bin","acp-args","acp-cwd","acp-permission","acp-allow","acp-narrate","acp-timeout-ms","budget-turns","budget-window-min","pi-tools","pi-model","tools","fetch-allow","server","preset","bio","tags","files","files-max-mb","files-allow","files-dir","files-max-concurrent","files-max-total-mb"]),Fe=new Set(["no-mention","respond-to-all","help","refresh-config"]);function Q(e){return e.replace(/-([a-z])/g,(t,r)=>r.toUpperCase())}function Ne(e){let t={};for(let r=0;r<e.length;r+=1){let n=e[r];if(!n.startsWith("--"))throw Error(`无法识别的参数:${n}`);let i=n.slice(2),s,o=i.indexOf("=");if(o>=0)s=i.slice(o+1),i=i.slice(0,o);if(Fe.has(i)){t[Q(i)]=!0;continue}if(!Oe.has(i))throw Error(`未知参数:--${i}`);if(s===void 0){if(s=e[r+1],s===void 0)throw Error(`--${i} 缺值`);r+=1}t[Q(i)]=s}return t}function f(e,t){if(e===void 0)return t;let r=Number(e);if(!Number.isFinite(r)||r<0)throw Error(`数值参数非法:${e}`);return r}function B(e,t){let r=JSON.parse(e);if(!Array.isArray(r))throw Error(`${t} 必须是 JSON 数组`);return r}function Le(){let e=y.env.IROH_AGENT_BIN?.trim();if(e)return e;try{let t=X.dirname(y.execPath);for(let r of["iroh-agent","iroh-agent.exe"]){let n=X.join(t,r);if(Te(n))return n}}catch{}return"iroh-agent"}async function Z(e){let t=new URL("./agent-io/",import.meta.url),r=[];for(let n of[".ts",".mjs"])try{return await import(new URL(`${e}${n}`,t).href)}catch(i){r.push(`${e}${n}: ${i.message}`)}throw Error(`加载 ${e} 模块失败(pi-sdk 大脑需要 @earendil-works/pi-coding-agent):`+`${r.join(";")}。单文件版刻意不带它 —— `+"改用 --brain acp / --brain command,或用仓库版(agent-pi 里先 npm ci)/ npm 版跑 pi-sdk")}function je(e,t){let r=e.brain??"rule";if(r==="rule"){let n=H();if(e.rules)n=D(JSON.parse(_e(e.rules,"utf8"))),t(`[cli] 已加载 ${n.length} 条规则:${e.rules}`);return new T(n)}if(r==="command"){if(!e.command)throw Error("--brain command 需要 --command(要执行的程序)");return new C({command:e.command,args:e.args?B(e.args,"--args"):[],promptTemplate:e.promptTemplate,input:e.input==="arg"?"arg":"stdin",timeoutMs:f(e.timeoutMs,120000)})}if(r==="pi-sdk")return new O(async()=>{let{createPiSdkBrain:n}=await Z("pi-sdk"),{buildCustomTools:i,CUSTOM_TOOL_NAMES:s}=await Z("tools"),o=De(e.piTools),a=(e.tools??"").split(",").map((c)=>c.trim()).filter(Boolean);for(let c of a)if(!s.includes(c))throw Error(`未知自定义工具:${c}(可选:${s.join(" | ")})`);let l=i(a,{fetchAllow:e.fetchAllow?e.fetchAllow.split(",").map((c)=>c.trim()).filter(Boolean):void 0,log:(...c)=>t(c.map(String).join(" "))});return t(`[cli] pi-sdk 大脑:tools=${Array.isArray(o)?o.join(","):o}${a.length?` +自定义[${a.join(",")}]`:""} model=${e.piModel??"默认选择"} 人设=${e.piSystemPrompt?"自定义":"默认"} cwd=${e.piCwd??"当前目录"}`),await n({agentNick:e.nick,systemPrompt:e.piSystemPrompt,appendSystemPrompt:e.piAppendPrompt?[e.piAppendPrompt]:void 0,promptTemplate:e.piPromptTemplate,cwd:e.piCwd,tools:o,customTools:l,model:e.piModel,log:(...c)=>t(c.map(String).join(" "))})});if(r==="acp")return new O(async()=>{await Promise.resolve().then(() => V());let i=e.acpCwd??y.cwd(),s=["allow-all","allow-list","read-only"].find((a)=>e.acpPermission===a)??"deny",o=e.acpAllow?e.acpAllow.split(",").map((a)=>a.trim()).filter(Boolean):[];if(s==="allow-list"&&!o.length)throw Error("--acp-permission=allow-list 需要 --acp-allow(如 --acp-allow Read,Grep)");if(s==="allow-all")t("[cli] ⚠️ --acp-permission=allow-all:agent 的工具调用**一律放行**。"+'这只适合"把功能对出来",长期跑请改用 allow-list / read-only(C1)');return t(`[cli] acp 后端:bin=${e.acpBin??"claude-code-acp"} cwd=${i} permission=${s}${s==="allow-list"?`(白名单:${o.join(",")})`:""} narrate=${e.acpNarrate==="off"?"off":"tools"}`),await K({bin:e.acpBin??"claude-code-acp",args:e.acpArgs?B(e.acpArgs,"--acp-args"):[],cwd:i,permission:s,allowList:o,narrate:e.acpNarrate==="off"?"off":"tools",promptTimeoutMs:f(e.acpTimeoutMs,300000),log:(...a)=>t(a.map(String).join(" "))})});throw Error(`未知 brain:${r}(支持 rule | command | pi-sdk | acp)`)}function De(e){if(!e||e==="none")return"none";if(e==="read-only"||e==="all")return e;let t=e.split(",").map((r)=>r.trim()).filter(Boolean);return t.length>0?t:"none"}class O{#e;#t=null;constructor(e){this.#e=e}async#s(){if(!this.#t)this.#t=this.#e().catch((e)=>{throw this.#t=null,e});return this.#t}async onMessage(e,t){return await(await this.#s()).onMessage(e,t)}async close(){if(!this.#t)return;try{await(await this.#t).close?.()}catch(e){console.error(`[agent-pi] brain 清理失败:${e.message}`)}}}function He(){return new Promise((e)=>{let t=!1,r=(n)=>{if(t){console.error(`[agent-pi] [cli] 忽略重复的 ${n}`);return}t=!0,e(n)};y.on("SIGINT",()=>r("SIGINT")),y.on("SIGTERM",()=>r("SIGTERM"))})}async function Ge(){let e=Ne(y.argv.slice(2));if(e.help){console.log(Y);return}let t=e.room??"";if(!t)throw Error(`缺少 --room(房间名就是访问凭据)
|
|
101
108
|
|
|
102
|
-
${
|
|
109
|
+
${Y}`);let r=(b)=>console.error(`[agent-pi] ${b}`),n=W(e);if(n)r(`[cli] preset=${n} → brain=${e.brain}${e.acpBin?` acp-bin=${e.acpBin}`:""}${e.acpArgs?` acp-args=${e.acpArgs}`:""}`);let i=e.nick,s=e.agentBin??Le(),o=["--room",t,...i?["--nick",i]:[]];if(e.agentArgs)o.push(...B(e.agentArgs,"--agent-args"));let a=e.server??y.env.IROH_AGENT_SERVER?.trim()??Be,l=await G({server:a,refresh:e.refreshConfig===!0,log:r});if(l.action==="fetched")r(`[cli] 已从 ${a} 引导配置(补上:${l.filled.join("、")})→ ${l.path}`);else if(l.action==="failed")r(`[cli] ⚠️ 配置引导失败:${l.reason}`),r("[cli] 继续启动;若进房后看不见别人,检查本机配置,或用 --server 指定站点");let c=new R({bin:s,args:o,pingIntervalMs:f(e.pingIntervalMs,45000),log:r}),m=z(c,{bio:e.bio,tags:e.tags?e.tags.split(",").map((b)=>b.trim()).filter(Boolean):[],log:r}),p=await c.start(),g=p.relay?`${p.relay.url}${p.relay.connected?"":"(未连接)"}`:"(无)";r(`[cli] hello:endpointId=${p.endpointId} chatProtocol=${p.chatProtocol} nick=${p.nickname} relay=${g}`);let h=je(e,r),w=e.noMention?null:e.mention??i??null,u=e.files==="accept"||e.files==="reject"?e.files:"off",F=new M({client:c,brain:h,room:t,mention:w,respondToAll:e.respondToAll===!0,cooldownMs:f(e.cooldownMs,3000),maxReplyBytes:f(e.maxReplyBytes,30000),files:u,filesMaxBytes:f(e.filesMaxMb,64)*1024*1024,filesAllowFrom:e.filesAllow?e.filesAllow.split(",").map((b)=>b.trim()).filter(Boolean):void 0,filesSaveDir:e.filesDir,filesMaxConcurrent:f(e.filesMaxConcurrent,2),filesMaxTotalBytes:f(e.filesMaxTotalMb,512)*1024*1024,log:(...b)=>r(b.map(String).join(" "))});if(F.start(),u==="accept"&&!e.filesAllow)r("[cli] ⚠️ --files accept 未配 --files-allow:房间里任何人都能给你发文件"+"(受单文件 / 并发 / 累计三道闸门约束,但请确认落盘目录所在分区够用、账号权限够低)");r(`[cli] 已就绪:mention=${w??"关"}(触发方式只有 @)`+`files=${u}${u==="accept"?`(上限 ${f(e.filesMaxMb,64)} MiB)`:""}`);let ee=await He();r(`[cli] 收到 ${ee},优雅退出中…`),m(),await F.stop(),await c.stop(),r("[cli] 已退出"),setTimeout(()=>y.exit(0),2000).unref()}Ge().catch((e)=>{console.error(`[agent-pi] 启动失败:${e.message}`),y.exit(1)});
|