grix-connector 3.7.1 → 3.8.1
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/core/admin/admin-server.js +1 -1
- package/dist/core/installer/manual-guide.js +1 -1
- package/dist/core/installer/registry.js +1 -1
- package/dist/core/proxy/hermes-profile-relay.js +1 -0
- package/dist/core/proxy/hermes-profile-relay.py +497 -0
- package/dist/core/proxy/relay-credential-router.js +1 -0
- package/dist/grix.js +6 -6
- package/dist/mcp/stream-http/security.js +1 -1
- package/openclaw-plugin/index.js +217 -74
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as u}from"node:http";import{log as c}from"../log/logger.js";class f{server=null;token;handler=null;upgradeHandler=null;probeHandler=null;installHandler=null;proxyHandler=null;constructor(e){this.token=e}setAgentHandler(e){this.handler=e}setUpgradeHandler(e){this.upgradeHandler=e}setProbeHandler(e){this.probeHandler=e}setInstallHandler(e){this.installHandler=e}setProxyHandler(e){this.proxyHandler=e}async start(e){return new Promise((n,t)=>{this.server=u((r,s)=>this.handleRequest(r,s)),this.server.listen(e,"127.0.0.1",()=>{c.info("admin",`Listening on 127.0.0.1:${e}`),n()}),this.server.on("error",t)})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>e())})}handleRequest(e,n){const t=e.method??"",r=e.url??"";if(r==="/api/agents"&&t==="GET")this.handleList(n);else if(r==="/api/agents"&&t==="POST")this.readBody(e).then(s=>this.handleAdd(n,s)).catch(s=>this.error(n,s));else if(t==="DELETE"&&r.startsWith("/api/agents/")){const s=decodeURIComponent(r.slice(12));this.handleRemove(n,s)}else if(t==="POST"&&r.match(/^\/api\/agents\/[^/]+\/restart$/)){const s=decodeURIComponent(r.slice(12,r.lastIndexOf("/restart")));this.handleRestart(n,s)}else if(r==="/api/reload"&&t==="POST")this.handleReload(n);else if(r==="/api/upgrade"&&t==="GET")this.handleCheckUpgrade(n);else if(r==="/api/upgrade"&&t==="POST")this.handleTriggerUpgrade(n);else if(t==="GET"&&r.startsWith("/api/probe"))this.handleProbe(e,n,r);else if(r==="/api/install"&&t==="GET")this.handleInstallList(n);else if(r==="/api/install"&&t==="POST")this.readBody(e).then(s=>this.handleInstall(n,s)).catch(s=>this.error(n,s));else if(t==="GET"&&r.startsWith("/api/install/")){const s=decodeURIComponent(r.slice(13));this.handleInstallProgress(n,s)}else if(r==="/api/proxy"&&t==="GET")this.handleProxyStatus(n);else if(t==="PUT"&&r.match(/^\/api\/proxy\/agents\/[^/]+\/enabled$/)){const s=decodeURIComponent(r.slice(18,-8));this.readBody(e).then(o=>this.handleProxySetAgentRelay(n,s,o)).catch(o=>this.error(n,o))}else if(t==="PUT"&&r.match(/^\/api\/proxy\/agents\/[^/]+\/relay-credential$/)){const s=decodeURIComponent(r.slice(18,-17));this.readBody(e).then(o=>this.handleProxySetAgentRelayCredential(n,s,o)).catch(o=>this.error(n,o))}else if(t==="PUT"&&r==="/api/proxy/enabled")this.readBody(e).then(s=>this.handleProxySetEnabled(n,s)).catch(s=>this.error(n,s));else if(t==="PUT"&&r==="/api/proxy/default-route")this.readBody(e).then(s=>this.handleProxySetDefaultRoute(n,s)).catch(s=>this.error(n,s));else if(t==="PUT"&&r==="/api/proxy/intercept-hosts")this.readBody(e).then(s=>this.handleProxySetInterceptHosts(n,s)).catch(s=>this.error(n,s));else if(t==="PUT"&&r.match(/^\/api\/proxy\/routes\/[^/]+$/)){const s=decodeURIComponent(r.slice(18));this.readBody(e).then(o=>this.handleProxySetRoute(n,s,o)).catch(o=>this.error(n,o))}else if(t==="DELETE"&&r.startsWith("/api/proxy/routes/")){const s=decodeURIComponent(r.slice(18));this.handleProxyDeleteRoute(n,s)}else this.json(n,404,{error:"not_found"})}ensureProxy(e){return this.proxyHandler?!0:(this.json(e,501,{error:"proxy not configured"}),!1)}handleProxyStatus(e){if(this.ensureProxy(e))try{this.json(e,200,this.proxyHandler.status())}catch(n){this.error(e,n)}}async handleProxySetRoute(e,n,t){if(!this.ensureProxy(e))return;const r=t;if(!r||typeof r.targetBaseUrl!="string"||!r.targetBaseUrl.trim()){this.json(e,400,{error:"targetBaseUrl is required"});return}try{new URL(r.targetBaseUrl)}catch{this.json(e,400,{error:"targetBaseUrl must be a valid URL"});return}if(r.headers!==void 0&&!this.isStringRecord(r.headers)){this.json(e,400,{error:"headers must be a string map"});return}if(r.model!==void 0&&typeof r.model!="string"){this.json(e,400,{error:"model must be a string"});return}if(r.modelMap!==void 0&&!this.isStringRecord(r.modelMap)){this.json(e,400,{error:"modelMap must be a string map"});return}if(r.passthrough!==void 0&&typeof r.passthrough!="boolean"){this.json(e,400,{error:"passthrough must be a boolean"});return}if(r.relayOnly!==void 0&&typeof r.relayOnly!="boolean"){this.json(e,400,{error:"relayOnly must be a boolean"});return}if(r.codexResponsesToChat!==void 0&&typeof r.codexResponsesToChat!="boolean"){this.json(e,400,{error:"codexResponsesToChat must be a boolean"});return}if(r.passthrough===!0&&r.codexResponsesToChat===!0){this.json(e,400,{error:"passthrough and codexResponsesToChat are mutually exclusive"});return}try{const s=await this.proxyHandler.setRoute(n,t);this.json(e,200,s??{ok:!0})}catch(s){this.error(e,s)}}handleProxyDeleteRoute(e,n){this.ensureProxy(e)&&this.proxyHandler.deleteRoute(n).then(()=>{e.writeHead(204),e.end()}).catch(t=>this.error(e,t))}async handleProxySetAgentRelay(e,n,t){if(!this.ensureProxy(e))return;const r=n.trim();if(!r){this.json(e,400,{error:"agent name is required"});return}const s=t;if(typeof s?.enabled!="boolean"){this.json(e,400,{error:"enabled must be a boolean"});return}try{const o=await this.proxyHandler.setAgentRelay(r,s.enabled);this.json(e,200,o??{ok:!0})}catch(o){this.error(e,o)}}async handleProxySetAgentRelayCredential(e,n,t){if(!this.ensureProxy(e))return;const r=n.trim();if(!r){this.json(e,400,{error:"agent name is required",code:"INVALID_ARGUMENT"});return}const s=t;if(typeof s?.agent_id!="string"||!s.agent_id.trim()){this.json(e,400,{error:"agent_id is required",code:"MISSING_AGENT_ID"});return}if(typeof s?.virtual_key!="string"||!s.virtual_key.trim()){this.json(e,400,{error:"virtual_key is required",code:"MISSING_VIRTUAL_KEY"});return}if(s.anthropic_base_url!==void 0&&typeof s.anthropic_base_url!="string"){this.json(e,400,{error:"anthropic_base_url must be a string",code:"INVALID_ARGUMENT"});return}if(s.openai_base_url!==void 0&&typeof s.openai_base_url!="string"){this.json(e,400,{error:"openai_base_url must be a string",code:"INVALID_ARGUMENT"});return}if(s.model!==void 0&&typeof s.model!="string"){this.json(e,400,{error:"model must be a string",code:"INVALID_ARGUMENT"});return}try{const o=await this.proxyHandler.setAgentRelayCredential(r,{agentId:s.agent_id,virtualKey:s.virtual_key,anthropicBaseUrl:typeof s.anthropic_base_url=="string"?s.anthropic_base_url:void 0,openaiBaseUrl:typeof s.openai_base_url=="string"?s.openai_base_url:void 0,model:typeof s.model=="string"?s.model:void 0});this.json(e,200,o??{ok:!0})}catch(o){this.error(e,o)}}async handleProxySetEnabled(e,n){if(!this.ensureProxy(e))return;const t=n;if(typeof t?.enabled!="boolean"){this.json(e,400,{error:"enabled must be a boolean"});return}if(t.enabled){this.json(e,400,{error:'relay is per-agent; use PUT /api/proxy/agents/<name>/enabled {"enabled":true}'});return}try{const r=await this.proxyHandler.disableAll();this.json(e,200,r??{ok:!0})}catch(r){this.error(e,r)}}async handleProxySetDefaultRoute(e,n){if(!this.ensureProxy(e))return;const r=n?.routeKey??null;if(r!==null&&typeof r!="string"){this.json(e,400,{error:"routeKey must be a string or null"});return}try{const s=await this.proxyHandler.setDefaultRoute(r);this.json(e,200,s??{ok:!0})}catch(s){this.error(e,s)}}async handleProxySetInterceptHosts(e,n){if(!this.ensureProxy(e))return;const t=n;if(!Array.isArray(t?.hosts)||!t.hosts.every(r=>typeof r=="string")){this.json(e,400,{error:"hosts must be a string array"});return}try{const r=await this.proxyHandler.setInterceptHosts(t.hosts);this.json(e,200,r??{ok:!0})}catch(r){this.error(e,r)}}isStringRecord(e){return typeof e!="object"||e===null||Array.isArray(e)?!1:Object.values(e).every(n=>typeof n=="string")}handleList(e){try{const n=this.handler?.list()??[];this.json(e,200,n)}catch(n){this.error(e,n)}}async handleAdd(e,n){try{const t=await this.handler.add(n);this.json(e,201,t??{ok:!0})}catch(t){this.error(e,t)}}handleRemove(e,n){this.handler.remove(n).then(()=>{e.writeHead(204),e.end()}).catch(t=>this.error(e,t))}handleRestart(e,n){this.handler.restart(n).then(()=>{this.json(e,200,{ok:!0})}).catch(t=>this.error(e,t))}handleReload(e){this.handler.reload().then(n=>{this.json(e,200,{ok:!0,result:n})}).catch(n=>this.error(e,n))}handleCheckUpgrade(e){if(!this.upgradeHandler){this.json(e,501,{error:"upgrade not configured"});return}this.upgradeHandler.check().then(n=>{this.json(e,200,n)}).catch(n=>this.error(e,n))}handleTriggerUpgrade(e){if(!this.upgradeHandler){this.json(e,501,{error:"upgrade not configured"});return}this.upgradeHandler.trigger(),this.json(e,200,{ok:!0,message:"upgrade check triggered"})}error(e,n){const t=n;t.code==="NOT_FOUND"?this.json(e,404,{error:t.message??"not found"}):t.code==="RELOAD_UNSAFE"||t.code==="NO_RELAY_ROUTE"||t.code==="AGENT_ID_MISMATCH"?this.json(e,409,{error:t.message,code:t.code}):t.code==="UNKNOWN_AGENT"||t.code==="UNSUPPORTED_OS"||t.code==="UNSUPPORTED_CLIENT_TYPE"||t.code==="MISSING_AGENT_ID"||t.code==="MISSING_VIRTUAL_KEY"||t.code==="MISSING_TARGET_BASE_URL"||t.code==="MISSING_MODEL"||t.code==="INVALID_TARGET_BASE_URL"||t.code==="INVALID_ARGUMENT"?this.json(e,400,{error:t.message,code:t.code}):t.code==="ALREADY_INSTALLED"||t.code==="INSTALL_IN_PROGRESS"?this.json(e,409,{error:t.message,code:t.code}):t.code==="INSTALL_FAILED"||t.code==="INSTALL_TIMEOUT"||t.code==="PREFLIGHT_FAILED"||t.code==="VERIFICATION_FAILED"||t.code==="PREREQ_MISSING"||t.code==="PREREQ_INSTALL_FAILED"||t.code==="FALLBACK_EXHAUSTED"||t.code==="ENVIRONMENT_UNSUPPORTED"?this.json(e,500,{error:t.message,code:t.code}):(c.error("admin",`Handler error: ${t.message??n}`),this.json(e,500,{error:t.message??"internal error"}))}json(e,n,t){const r=JSON.stringify(t);e.writeHead(n,{"Content-Type":"application/json"}),e.end(r)}readBody(e){return new Promise((n,t)=>{let r="";e.setEncoding("utf8"),e.on("data",s=>{r+=s}),e.on("end",()=>{try{n(JSON.parse(r))}catch{t(new Error("invalid JSON body"))}}),e.on("error",t)})}handleProbe(e,n,t){if(!this.probeHandler){this.json(n,501,{error:"probe not configured"});return}const r=t.indexOf("?"),s=r>=0?t.slice(0,r):t,o=r>=0?new URLSearchParams(t.slice(r+1)):new URLSearchParams,a={};o.get("conversation")==="true"&&(a.conversation=!0),o.get("fresh")==="true"&&(a.fresh=!0);const l=Number(o.get("timeoutMs"));Number.isFinite(l)&&l>0&&(a.timeoutMs=l);const d=s.match(/^\/api\/probe\/(.+)$/);if(d){const i=decodeURIComponent(d[1]);this.probeHandler.probeOne(i,a).then(h=>{this.json(n,200,h)}).catch(h=>this.error(n,h));return}this.probeHandler.probeAll(a).then(i=>{this.json(n,200,i)}).catch(i=>this.error(n,i))}handleInstallList(e){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}try{const n=this.installHandler.listInstallable();this.json(e,200,n)}catch(n){this.error(e,n)}}handleInstallProgress(e,n){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}const t=this.installHandler.getInstallProgress(n);if(!t){this.json(e,200,{agentType:n,status:"unknown",inProgress:!1,progress:null});return}let r,s,o;switch(t.phase){case"completed":r="done",s="\u5B89\u88C5\u5B8C\u6210";break;case"failed":r="error",o=t.outputTail||"\u5B89\u88C5\u5931\u8D25";break;case"preflight":r="pending",s="\u68C0\u67E5\u524D\u7F6E\u4F9D\u8D56...";break;case"installing_prereq":r="downloading",s=t.currentPrereq?`\u6B63\u5728\u5B89\u88C5\u524D\u7F6E\u4F9D\u8D56: ${t.currentPrereq}`:"\u6B63\u5728\u5B89\u88C5\u524D\u7F6E\u4F9D\u8D56...";break;case"installing":r="installing",s=`\u6B63\u5728\u5B89\u88C5 ${n}...`;break;case"verifying":r="installing",s="\u9A8C\u8BC1\u5B89\u88C5...";break;default:r="unknown"}this.json(e,200,{agentType:n,status:r,inProgress:!0,progress:t.elapsedMs?Math.min(.9,t.elapsedMs/3e4):.1,message:s,error:o})}async handleInstall(e,n){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}try{const t=n;if(!t||typeof t.agentType!="string"||!t.agentType){this.json(e,400,{error:"agentType is required"});return}const r=await this.installHandler.installAgent(t);if(r.ok)this.json(e,200,r);else{const s=r.error?.code;s==="UNKNOWN_AGENT"||s==="UNSUPPORTED_OS"?this.json(e,400,r):s==="INSTALL_IN_PROGRESS"?this.json(e,409,r):this.json(e,500,r)}}catch(t){this.error(e,t)}}}export{f as AdminServer};
|
|
1
|
+
import{createServer as u}from"node:http";import{log as c}from"../log/logger.js";class g{server=null;token;handler=null;upgradeHandler=null;probeHandler=null;installHandler=null;proxyHandler=null;relayCredentialHandler=null;constructor(e){this.token=e}setAgentHandler(e){this.handler=e}setUpgradeHandler(e){this.upgradeHandler=e}setProbeHandler(e){this.probeHandler=e}setInstallHandler(e){this.installHandler=e}setProxyHandler(e){this.proxyHandler=e}setRelayCredentialHandler(e){this.relayCredentialHandler=e}async start(e){return new Promise((n,t)=>{this.server=u((r,o)=>this.handleRequest(r,o)),this.server.listen(e,"127.0.0.1",()=>{c.info("admin",`Listening on 127.0.0.1:${e}`),n()}),this.server.on("error",t)})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>e())})}handleRequest(e,n){const t=e.method??"",r=e.url??"";if(r==="/api/agents"&&t==="GET")this.handleList(n);else if(r==="/api/agents"&&t==="POST")this.readBody(e).then(o=>this.handleAdd(n,o)).catch(o=>this.error(n,o));else if(t==="DELETE"&&r.startsWith("/api/agents/")){const o=decodeURIComponent(r.slice(12));this.handleRemove(n,o)}else if(t==="POST"&&r.match(/^\/api\/agents\/[^/]+\/restart$/)){const o=decodeURIComponent(r.slice(12,r.lastIndexOf("/restart")));this.handleRestart(n,o)}else if(r==="/api/reload"&&t==="POST")this.handleReload(n);else if(r==="/api/upgrade"&&t==="GET")this.handleCheckUpgrade(n);else if(r==="/api/upgrade"&&t==="POST")this.handleTriggerUpgrade(n);else if(t==="GET"&&r.startsWith("/api/probe"))this.handleProbe(e,n,r);else if(r==="/api/install"&&t==="GET")this.handleInstallList(n);else if(r==="/api/install"&&t==="POST")this.readBody(e).then(o=>this.handleInstall(n,o)).catch(o=>this.error(n,o));else if(t==="GET"&&r.startsWith("/api/install/")){const o=decodeURIComponent(r.slice(13));this.handleInstallProgress(n,o)}else if(r==="/api/proxy"&&t==="GET")this.handleProxyStatus(n);else if(t==="PUT"&&r.match(/^\/api\/proxy\/agents\/[^/]+\/enabled$/)){const o=decodeURIComponent(r.slice(18,-8));this.readBody(e).then(s=>this.handleProxySetAgentRelay(n,o,s)).catch(s=>this.error(n,s))}else if(t==="PUT"&&r.match(/^\/api\/proxy\/agents\/[^/]+\/relay-credential$/)){const o=decodeURIComponent(r.slice(18,-17));this.readBody(e).then(s=>this.handleProxySetAgentRelayCredential(n,o,s)).catch(s=>this.error(n,s))}else if(r==="/api/proxy/hermes-profiles"&&t==="GET")this.handleHermesProfileList(n);else if(t==="PUT"&&r.match(/^\/api\/proxy\/relay-credential\/[^/]+$/)){const o=decodeURIComponent(r.slice(28));this.readBody(e).then(s=>this.handleRelayCredentialEnable(n,o,s)).catch(s=>this.error(n,s))}else if(t==="DELETE"&&r.match(/^\/api\/proxy\/relay-credential\/[^/]+$/)){const o=decodeURIComponent(r.slice(28));this.handleRelayCredentialDisable(n,o)}else if(t==="PUT"&&r==="/api/proxy/enabled")this.readBody(e).then(o=>this.handleProxySetEnabled(n,o)).catch(o=>this.error(n,o));else if(t==="PUT"&&r==="/api/proxy/default-route")this.readBody(e).then(o=>this.handleProxySetDefaultRoute(n,o)).catch(o=>this.error(n,o));else if(t==="PUT"&&r==="/api/proxy/intercept-hosts")this.readBody(e).then(o=>this.handleProxySetInterceptHosts(n,o)).catch(o=>this.error(n,o));else if(t==="PUT"&&r.match(/^\/api\/proxy\/routes\/[^/]+$/)){const o=decodeURIComponent(r.slice(18));this.readBody(e).then(s=>this.handleProxySetRoute(n,o,s)).catch(s=>this.error(n,s))}else if(t==="DELETE"&&r.startsWith("/api/proxy/routes/")){const o=decodeURIComponent(r.slice(18));this.handleProxyDeleteRoute(n,o)}else this.json(n,404,{error:"not_found"})}ensureRelayCredential(e){return this.relayCredentialHandler?!0:(this.json(e,501,{error:"relay credential handler not configured"}),!1)}handleHermesProfileList(e){this.ensureRelayCredential(e)&&this.relayCredentialHandler.listHermesProfiles().then(n=>this.json(e,200,{profiles:n})).catch(n=>this.error(e,n))}async handleRelayCredentialEnable(e,n,t){if(!this.ensureRelayCredential(e))return;const r=t,o=s=>typeof s=="string"&&s.trim()?s.trim():void 0;try{const s=await this.relayCredentialHandler.enable(n.trim(),{virtualKey:o(r?.virtual_key)??"",anthropicBaseUrl:o(r?.anthropic_base_url),openaiBaseUrl:o(r?.openai_base_url),model:o(r?.model)});this.json(e,200,s??{ok:!0})}catch(s){this.error(e,s)}}handleRelayCredentialDisable(e,n){this.ensureRelayCredential(e)&&this.relayCredentialHandler.disable(n.trim()).then(t=>this.json(e,200,t??{ok:!0})).catch(t=>this.error(e,t))}ensureProxy(e){return this.proxyHandler?!0:(this.json(e,501,{error:"proxy not configured"}),!1)}handleProxyStatus(e){if(this.ensureProxy(e))try{this.json(e,200,this.proxyHandler.status())}catch(n){this.error(e,n)}}async handleProxySetRoute(e,n,t){if(!this.ensureProxy(e))return;const r=t;if(!r||typeof r.targetBaseUrl!="string"||!r.targetBaseUrl.trim()){this.json(e,400,{error:"targetBaseUrl is required"});return}try{new URL(r.targetBaseUrl)}catch{this.json(e,400,{error:"targetBaseUrl must be a valid URL"});return}if(r.headers!==void 0&&!this.isStringRecord(r.headers)){this.json(e,400,{error:"headers must be a string map"});return}if(r.model!==void 0&&typeof r.model!="string"){this.json(e,400,{error:"model must be a string"});return}if(r.modelMap!==void 0&&!this.isStringRecord(r.modelMap)){this.json(e,400,{error:"modelMap must be a string map"});return}if(r.passthrough!==void 0&&typeof r.passthrough!="boolean"){this.json(e,400,{error:"passthrough must be a boolean"});return}if(r.relayOnly!==void 0&&typeof r.relayOnly!="boolean"){this.json(e,400,{error:"relayOnly must be a boolean"});return}if(r.codexResponsesToChat!==void 0&&typeof r.codexResponsesToChat!="boolean"){this.json(e,400,{error:"codexResponsesToChat must be a boolean"});return}if(r.passthrough===!0&&r.codexResponsesToChat===!0){this.json(e,400,{error:"passthrough and codexResponsesToChat are mutually exclusive"});return}try{const o=await this.proxyHandler.setRoute(n,t);this.json(e,200,o??{ok:!0})}catch(o){this.error(e,o)}}handleProxyDeleteRoute(e,n){this.ensureProxy(e)&&this.proxyHandler.deleteRoute(n).then(()=>{e.writeHead(204),e.end()}).catch(t=>this.error(e,t))}async handleProxySetAgentRelay(e,n,t){if(!this.ensureProxy(e))return;const r=n.trim();if(!r){this.json(e,400,{error:"agent name is required"});return}const o=t;if(typeof o?.enabled!="boolean"){this.json(e,400,{error:"enabled must be a boolean"});return}try{const s=await this.proxyHandler.setAgentRelay(r,o.enabled);this.json(e,200,s??{ok:!0})}catch(s){this.error(e,s)}}async handleProxySetAgentRelayCredential(e,n,t){if(!this.ensureProxy(e))return;const r=n.trim();if(!r){this.json(e,400,{error:"agent name is required",code:"INVALID_ARGUMENT"});return}const o=t;if(typeof o?.agent_id!="string"||!o.agent_id.trim()){this.json(e,400,{error:"agent_id is required",code:"MISSING_AGENT_ID"});return}if(typeof o?.virtual_key!="string"||!o.virtual_key.trim()){this.json(e,400,{error:"virtual_key is required",code:"MISSING_VIRTUAL_KEY"});return}if(o.anthropic_base_url!==void 0&&typeof o.anthropic_base_url!="string"){this.json(e,400,{error:"anthropic_base_url must be a string",code:"INVALID_ARGUMENT"});return}if(o.openai_base_url!==void 0&&typeof o.openai_base_url!="string"){this.json(e,400,{error:"openai_base_url must be a string",code:"INVALID_ARGUMENT"});return}if(o.model!==void 0&&typeof o.model!="string"){this.json(e,400,{error:"model must be a string",code:"INVALID_ARGUMENT"});return}try{const s=await this.proxyHandler.setAgentRelayCredential(r,{agentId:o.agent_id,virtualKey:o.virtual_key,anthropicBaseUrl:typeof o.anthropic_base_url=="string"?o.anthropic_base_url:void 0,openaiBaseUrl:typeof o.openai_base_url=="string"?o.openai_base_url:void 0,model:typeof o.model=="string"?o.model:void 0});this.json(e,200,s??{ok:!0})}catch(s){this.error(e,s)}}async handleProxySetEnabled(e,n){if(!this.ensureProxy(e))return;const t=n;if(typeof t?.enabled!="boolean"){this.json(e,400,{error:"enabled must be a boolean"});return}if(t.enabled){this.json(e,400,{error:'relay is per-agent; use PUT /api/proxy/agents/<name>/enabled {"enabled":true}'});return}try{const r=await this.proxyHandler.disableAll();this.json(e,200,r??{ok:!0})}catch(r){this.error(e,r)}}async handleProxySetDefaultRoute(e,n){if(!this.ensureProxy(e))return;const r=n?.routeKey??null;if(r!==null&&typeof r!="string"){this.json(e,400,{error:"routeKey must be a string or null"});return}try{const o=await this.proxyHandler.setDefaultRoute(r);this.json(e,200,o??{ok:!0})}catch(o){this.error(e,o)}}async handleProxySetInterceptHosts(e,n){if(!this.ensureProxy(e))return;const t=n;if(!Array.isArray(t?.hosts)||!t.hosts.every(r=>typeof r=="string")){this.json(e,400,{error:"hosts must be a string array"});return}try{const r=await this.proxyHandler.setInterceptHosts(t.hosts);this.json(e,200,r??{ok:!0})}catch(r){this.error(e,r)}}isStringRecord(e){return typeof e!="object"||e===null||Array.isArray(e)?!1:Object.values(e).every(n=>typeof n=="string")}handleList(e){try{const n=this.handler?.list()??[];this.json(e,200,n)}catch(n){this.error(e,n)}}async handleAdd(e,n){try{const t=await this.handler.add(n);this.json(e,201,t??{ok:!0})}catch(t){this.error(e,t)}}handleRemove(e,n){this.handler.remove(n).then(()=>{e.writeHead(204),e.end()}).catch(t=>this.error(e,t))}handleRestart(e,n){this.handler.restart(n).then(()=>{this.json(e,200,{ok:!0})}).catch(t=>this.error(e,t))}handleReload(e){this.handler.reload().then(n=>{this.json(e,200,{ok:!0,result:n})}).catch(n=>this.error(e,n))}handleCheckUpgrade(e){if(!this.upgradeHandler){this.json(e,501,{error:"upgrade not configured"});return}this.upgradeHandler.check().then(n=>{this.json(e,200,n)}).catch(n=>this.error(e,n))}handleTriggerUpgrade(e){if(!this.upgradeHandler){this.json(e,501,{error:"upgrade not configured"});return}this.upgradeHandler.trigger(),this.json(e,200,{ok:!0,message:"upgrade check triggered"})}error(e,n){const t=n;t.code==="NOT_FOUND"?this.json(e,404,{error:t.message??"not found"}):t.code==="PROFILE_NOT_FOUND"?this.json(e,404,{error:t.message,code:t.code}):t.code==="PROXY_UNAVAILABLE"?this.json(e,503,{error:t.message,code:t.code}):t.code==="AMBIGUOUS_AGENT_ID"?this.json(e,409,{error:t.message,code:t.code}):t.code==="HERMES_NOT_FOUND"||t.code==="HERMES_IMPORT_FAILED"||t.code==="SCRIPT_MISSING"||t.code==="SCRIPT_FAILED"||t.code==="CONFIG_WRITE_FAILED"||t.code==="CONFIG_WRITE_VERIFY_FAILED"||t.code==="CONFIG_UNREADABLE"?this.json(e,500,{error:t.message,code:t.code}):t.code==="RELOAD_UNSAFE"||t.code==="NO_RELAY_ROUTE"||t.code==="AGENT_ID_MISMATCH"?this.json(e,409,{error:t.message,code:t.code}):t.code==="UNKNOWN_AGENT"||t.code==="UNSUPPORTED_OS"||t.code==="UNSUPPORTED_CLIENT_TYPE"||t.code==="MISSING_AGENT_ID"||t.code==="MISSING_VIRTUAL_KEY"||t.code==="MISSING_TARGET_BASE_URL"||t.code==="MISSING_MODEL"||t.code==="MISSING_BASE_URL"||t.code==="MISSING_API_KEY"||t.code==="INVALID_BASE_URL"||t.code==="BAD_REQUEST"||t.code==="INVALID_TARGET_BASE_URL"||t.code==="INVALID_ARGUMENT"?this.json(e,400,{error:t.message,code:t.code}):t.code==="ALREADY_INSTALLED"||t.code==="INSTALL_IN_PROGRESS"?this.json(e,409,{error:t.message,code:t.code}):t.code==="INSTALL_FAILED"||t.code==="INSTALL_TIMEOUT"||t.code==="PREFLIGHT_FAILED"||t.code==="VERIFICATION_FAILED"||t.code==="PREREQ_MISSING"||t.code==="PREREQ_INSTALL_FAILED"||t.code==="FALLBACK_EXHAUSTED"||t.code==="ENVIRONMENT_UNSUPPORTED"?this.json(e,500,{error:t.message,code:t.code}):(c.error("admin",`Handler error: ${t.message??n}`),this.json(e,500,{error:t.message??"internal error"}))}json(e,n,t){const r=JSON.stringify(t);e.writeHead(n,{"Content-Type":"application/json"}),e.end(r)}readBody(e){return new Promise((n,t)=>{let r="";e.setEncoding("utf8"),e.on("data",o=>{r+=o}),e.on("end",()=>{try{n(JSON.parse(r))}catch{t(new Error("invalid JSON body"))}}),e.on("error",t)})}handleProbe(e,n,t){if(!this.probeHandler){this.json(n,501,{error:"probe not configured"});return}const r=t.indexOf("?"),o=r>=0?t.slice(0,r):t,s=r>=0?new URLSearchParams(t.slice(r+1)):new URLSearchParams,a={};s.get("conversation")==="true"&&(a.conversation=!0),s.get("fresh")==="true"&&(a.fresh=!0);const l=Number(s.get("timeoutMs"));Number.isFinite(l)&&l>0&&(a.timeoutMs=l);const h=o.match(/^\/api\/probe\/(.+)$/);if(h){const i=decodeURIComponent(h[1]);this.probeHandler.probeOne(i,a).then(d=>{this.json(n,200,d)}).catch(d=>this.error(n,d));return}this.probeHandler.probeAll(a).then(i=>{this.json(n,200,i)}).catch(i=>this.error(n,i))}handleInstallList(e){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}try{const n=this.installHandler.listInstallable();this.json(e,200,n)}catch(n){this.error(e,n)}}handleInstallProgress(e,n){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}const t=this.installHandler.getInstallProgress(n);if(!t){this.json(e,200,{agentType:n,status:"unknown",inProgress:!1,progress:null});return}let r,o,s;switch(t.phase){case"completed":r="done",o="\u5B89\u88C5\u5B8C\u6210";break;case"failed":r="error",s=t.outputTail||"\u5B89\u88C5\u5931\u8D25";break;case"preflight":r="pending",o="\u68C0\u67E5\u524D\u7F6E\u4F9D\u8D56...";break;case"installing_prereq":r="downloading",o=t.currentPrereq?`\u6B63\u5728\u5B89\u88C5\u524D\u7F6E\u4F9D\u8D56: ${t.currentPrereq}`:"\u6B63\u5728\u5B89\u88C5\u524D\u7F6E\u4F9D\u8D56...";break;case"installing":r="installing",o=`\u6B63\u5728\u5B89\u88C5 ${n}...`;break;case"verifying":r="installing",o="\u9A8C\u8BC1\u5B89\u88C5...";break;default:r="unknown"}this.json(e,200,{agentType:n,status:r,inProgress:!0,progress:t.elapsedMs?Math.min(.9,t.elapsedMs/3e4):.1,message:o,error:s})}async handleInstall(e,n){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}try{const t=n;if(!t||typeof t.agentType!="string"||!t.agentType){this.json(e,400,{error:"agentType is required"});return}const r=await this.installHandler.installAgent(t);if(r.ok)this.json(e,200,r);else{const o=r.error?.code;o==="UNKNOWN_AGENT"||o==="UNSUPPORTED_OS"?this.json(e,400,r):o==="INSTALL_IN_PROGRESS"?this.json(e,409,r):this.json(e,500,r)}}catch(t){this.error(e,t)}}}export{g as AdminServer};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getInstallCommand as f,getCliBinary as g}from"./registry.js";import{getPrereqManualInstructions as $}from"./prereq-installer.js";import{getMirrorInstallCommands as
|
|
1
|
+
import{getInstallCommand as f,getCliBinary as g}from"./registry.js";import{getPrereqManualInstructions as $}from"./prereq-installer.js";import{getMirrorInstallCommands as r}from"./npm-registry.js";function v(u){const{agentType:e,os:p,env:h,missingPrereqs:c}=u,s=[],a=g(e)??e,o=f(e,p),m=b(p);if(s.push(`=== ${e} \u4EBA\u5DE5\u5B89\u88C5\u6307\u5357 (${m}) ===`),s.push(""),s.push("\u5F53\u524D\u73AF\u5883:"),s.push(` \u7CFB\u7EDF: ${h.osVersion} (${h.arch})`),s.push(` Node.js: ${h.nodeVersion??"\u672A\u5B89\u88C5"}`),s.push(` npm: ${h.npmVersion??"\u672A\u5B89\u88C5"}`),s.push(""),u.error&&(s.push("\u81EA\u52A8\u5B89\u88C5\u5931\u8D25\u539F\u56E0:"),s.push(` ${u.error}`),s.push("")),c.length>0){s.push("--- \u6B65\u9AA4 1: \u5B89\u88C5\u524D\u7F6E\u4F9D\u8D56 ---"),s.push("");for(const n of c){const i=$(n.id,p);s.push(`\u25B8 ${n.label}${n.minVersion?` (\u9700\u8981 >= ${n.minVersion})`:""}`),i&&s.push(` \u5B89\u88C5\u65B9\u6CD5: ${i}`),s.push(` \u9A8C\u8BC1: ${n.id==="node"?"node --version":n.id==="npm"?"npm --version":`${n.id} --version`}`),s.push("")}}const l=c.length>0?"2":"1";if(s.push(`--- \u6B65\u9AA4 ${l}: \u5B89\u88C5 ${e} ---`),s.push(""),o){if(s.push(" \u63A8\u8350\u65B9\u5F0F:"),s.push(` ${o.command}`),s.push(""),o.mode==="npm"&&o.npmPackage){const n=r(o.npmPackage);s.push(" \u955C\u50CF\u5B89\u88C5\uFF08\u56FD\u5185\u7F51\u7EDC\u6162\u6216\u88AB\u5899\u65F6\u4F7F\u7528\uFF09:");for(const i of n)s.push(` # ${i.label}`),s.push(` ${i.command}`);s.push("")}if(o.fallback){if(s.push(" \u5907\u9009\u65B9\u5F0F\uFF08\u63A8\u8350\u5B89\u88C5\u5931\u8D25\u65F6\u4F7F\u7528\uFF09:"),s.push(` ${o.fallback.command}`),o.fallback.mode==="npm"&&o.fallback.npmPackage){const n=r(o.fallback.npmPackage);for(const i of n)s.push(` # ${i.label}`),s.push(` ${i.command}`)}s.push("")}}const d=c.length>0?"3":"2";s.push(`--- \u6B65\u9AA4 ${d}: \u9A8C\u8BC1\u5B89\u88C5 ---`),s.push(""),s.push(` ${a} --version`),s.push(""),s.push(" \u5982\u679C\u663E\u793A\u7248\u672C\u53F7\uFF0C\u8BF4\u660E\u5B89\u88C5\u6210\u529F\u3002"),s.push(""),s.push("--- \u5E38\u89C1\u95EE\u9898 ---"),s.push(""),s.push(" Q: \u5B89\u88C5\u540E\u547D\u4EE4\u627E\u4E0D\u5230\uFF1F"),s.push(" A: \u53EF\u80FD\u9700\u8981\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\uFF0C\u6216\u8FD0\u884C:"),p==="macos"||p==="linux"?(s.push(" source ~/.zshrc # zsh"),s.push(" source ~/.bashrc # bash")):s.push(" \u91CD\u65B0\u6253\u5F00 PowerShell \u6216 CMD \u7A97\u53E3"),s.push(""),o?.mode==="npm"&&(s.push(" Q: npm install -g \u62A5\u6743\u9650\u9519\u8BEF\uFF1F"),p==="macos"||p==="linux"?(s.push(" A: \u4E0D\u8981\u7528 sudo npm install -g\u3002\u5EFA\u8BAE\u7528\u4EE5\u4E0B\u65B9\u5F0F\u4E4B\u4E00:"),s.push(" 1. brew install node (macOS\uFF0C\u63A8\u8350)"),s.push(" 2. \u4F7F\u7528 fnm/nvm \u7BA1\u7406 Node.js (Linux)"),s.push(" 3. \u914D\u7F6E npm \u5168\u5C40\u76EE\u5F55: npm config set prefix ~/.npm-global")):s.push(" A: \u4EE5\u7BA1\u7406\u5458\u8EAB\u4EFD\u6253\u5F00 PowerShell \u540E\u91CD\u8BD5"),s.push("")),s.push("--- \u5B98\u65B9\u6587\u6863 ---"),s.push("");const t={claude:"https://docs.anthropic.com/en/docs/claude-code/overview",codex:"https://github.com/openai/codex",gemini:"https://github.com/google-gemini/gemini-cli",qwen:"https://github.com/QwenLM/qwen-code",cursor:"https://cursor.com/docs/cli/installation",copilot:"https://github.com/github/copilot-cli",kiro:"https://kiro.dev/docs/cli/",kimi:"https://moonshotai.github.io/kimi-cli/",openclaw:"https://github.com/openclaw/openclaw",reasonix:"https://github.com/esengine/DeepSeek-Reasonix"};return t[e]&&s.push(` ${e}: ${t[e]}`),s.join(`
|
|
2
2
|
`)}function b(u){switch(u){case"macos":return"macOS";case"linux":return"Linux";case"windows":return"Windows"}}export{v as generateManualGuide};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function e
|
|
1
|
+
function i(e,o){return{command:`npm install -g ${e}`,mode:"npm",npmPackage:e,timeoutMs:o?.timeoutMs??12e4,prerequisites:["node","npm"],minNodeVersion:o?.minNodeVersion}}function n(e,o){return{command:e,mode:"shell",timeoutMs:o?.timeoutMs??12e4,prerequisites:o?.prerequisites}}function a(e,o,s){return{command:e,mode:"exec",execArgs:o,timeoutMs:s?.timeoutMs??6e4,prerequisites:s?.prerequisites,skipVerification:s?.skipVerification}}const l={claude:{cliBinary:"claude",macos:i("@anthropic-ai/claude-code",{minNodeVersion:"18.0"}),linux:i("@anthropic-ai/claude-code",{minNodeVersion:"18.0"}),windows:i("@anthropic-ai/claude-code",{minNodeVersion:"18.0"})},codex:{cliBinary:"codex",macos:i("@openai/codex"),linux:i("@openai/codex"),windows:i("@openai/codex")},gemini:{cliBinary:"gemini",macos:{...i("@google/gemini-cli",{minNodeVersion:"18.0"}),fallback:n("npx @google/gemini-cli --yes",{timeoutMs:12e4})},linux:i("@google/gemini-cli",{minNodeVersion:"18.0"}),windows:i("@google/gemini-cli",{minNodeVersion:"18.0"})},qwen:{cliBinary:"qwen",macos:{...n("curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash",{prerequisites:["curl"]}),fallback:i("@qwen-code/qwen-code@latest",{minNodeVersion:"22.0"})},linux:{...n("curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash",{prerequisites:["curl"]}),fallback:i("@qwen-code/qwen-code@latest",{minNodeVersion:"22.0"})},windows:{...n(`powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"`),fallback:i("@qwen-code/qwen-code@latest",{minNodeVersion:"22.0"})}},cursor:{cliBinary:"agent",macos:n("curl https://cursor.com/install -fsS | bash",{prerequisites:["curl"]}),linux:n("curl https://cursor.com/install -fsS | bash",{prerequisites:["curl"]}),windows:n(`powershell -ExecutionPolicy ByPass -c "irm 'https://cursor.com/install?win32=true' | iex"`)},copilot:{cliBinary:"copilot",macos:i("@github/copilot",{minNodeVersion:"22.0"}),linux:i("@github/copilot",{minNodeVersion:"22.0"}),windows:i("@github/copilot",{minNodeVersion:"22.0"})},kiro:{cliBinary:"kiro-cli",macos:n("curl -fsSL https://cli.kiro.dev/install | bash",{prerequisites:["curl"]}),linux:n("curl -fsSL https://cli.kiro.dev/install | bash",{prerequisites:["curl"]}),windows:null},kimi:{cliBinary:"kimi",macos:n("curl -LsSf https://code.kimi.com/install.sh | bash",{prerequisites:["curl"],timeoutMs:3e5}),linux:n("curl -LsSf https://code.kimi.com/install.sh | bash",{prerequisites:["curl"],timeoutMs:3e5}),windows:n('powershell -ExecutionPolicy ByPass -c "irm https://code.kimi.com/install.ps1 | iex"',{timeoutMs:3e5})},openclaw:{cliBinary:"openclaw",macos:i("openclaw@latest",{minNodeVersion:"22.0"}),linux:i("openclaw@latest",{minNodeVersion:"22.0"}),windows:i("openclaw@latest",{minNodeVersion:"22.0"})},reasonix:{cliBinary:"reasonix",macos:i("reasonix"),linux:i("reasonix"),windows:i("reasonix")},pi:{cliBinary:"pi",macos:i("@earendil-works/pi-coding-agent",{minNodeVersion:"22.19"}),linux:i("@earendil-works/pi-coding-agent",{minNodeVersion:"22.19"}),windows:i("@earendil-works/pi-coding-agent",{minNodeVersion:"22.19"})},agy:{cliBinary:"agy",macos:n("curl -fsSL https://antigravity.google/cli/install.sh | bash",{prerequisites:["curl"]}),linux:n("curl -fsSL https://antigravity.google/cli/install.sh | bash",{prerequisites:["curl"]}),windows:n('powershell -ExecutionPolicy ByPass -c "irm https://antigravity.google/cli/install.ps1 | iex"')},hermes:{cliBinary:"hermes",macos:null,linux:null,windows:null},codewhale:{cliBinary:"codewhale",macos:i("codewhale"),linux:i("codewhale"),windows:i("codewhale")},opencode:{cliBinary:"opencode",macos:{...n("curl -fsSL https://opencode.ai/install | bash",{prerequisites:["curl"]}),fallback:i("opencode-ai")},linux:{...n("curl -fsSL https://opencode.ai/install | bash",{prerequisites:["curl"]}),fallback:i("opencode-ai")},windows:i("opencode-ai")}};function u(){switch(process.platform){case"darwin":return"macos";case"linux":return"linux";case"win32":return"windows";default:return"linux"}}function r(e){return e.mode==="exec"&&e.execArgs&&e.execArgs.length>0?[e.command,...e.execArgs].join(" "):e.command}function m(e,o){const s=l[e];return s?s[o]:null}function d(e){return l[e]?.cliBinary??null}function c(e,o){const s=l[e];if(!s)return null;const t=s[o];return{agentType:e,cliBinary:s.cliBinary,supported:t!==null,installCommand:t?r(t):null,prerequisites:t?.prerequisites}}function p(e){return Object.keys(l).sort().map(o=>c(o,e)).filter(o=>o!==null)}function w(e){return e in l}export{u as detectPlatformOS,r as formatInstallCommand,c as getAgentInstallInfo,p as getAllAgentInstallInfo,d as getCliBinary,m as getInstallCommand,w as isKnownAgent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execFile as h}from"node:child_process";import{existsSync as a}from"node:fs";import{readFile as g}from"node:fs/promises";import{homedir as f}from"node:os";import{delimiter as w,dirname as S,join as l}from"node:path";import{fileURLToPath as I}from"node:url";const R=3e4,v="hermes-profile-relay.py";class i extends Error{code;constructor(o,n){super(o),this.code=n,this.name="HermesProfileRelayError"}}function E(){return l(S(I(import.meta.url)),v)}function P(){const e=(process.env.PATH??"").split(w).filter(Boolean),o=process.platform==="win32"?["hermes.exe","hermes.cmd","hermes"]:["hermes"];for(const n of e)for(const r of o){const t=l(n,r);if(a(t))return t}}async function A(){const e=process.env.GRIX_HERMES_PYTHON?.trim();if(e){if(!a(e))throw new i(`GRIX_HERMES_PYTHON points to a missing interpreter: ${e}`,"HERMES_NOT_FOUND");return e}const o=P();if(o)try{const r=(await g(o,"utf8")).slice(0,512),t=/^#!\s*(\S+)/.exec(r)?.[1];if(t&&a(t))return t}catch{}const n=l(f(),".hermes","hermes-agent",".venv","bin","python3");if(a(n))return n;throw new i("cannot locate hermes' python interpreter (set GRIX_HERMES_PYTHON to override)","HERMES_NOT_FOUND")}async function d(e){const o=await A(),n=E();if(!a(n))throw new i(`relay script is missing: ${n}`,"SCRIPT_MISSING");const r=await new Promise((m,_)=>{h(o,[n],{timeout:R,cwd:f(),maxBuffer:8388608},(u,s,y)=>{if(s&&s.trim())return m(s);if(u)return _(new i(`hermes relay script failed: ${String(y||u.message).slice(0,400)}`,"SCRIPT_FAILED"));m(s)}).stdin?.end(JSON.stringify(e))});let t;try{t=JSON.parse(r.trim())}catch{throw new i(`hermes relay script returned non-JSON output: ${r.slice(0,200)}`,"SCRIPT_FAILED")}if(!t.ok)throw new i(String(t.error_msg??"hermes relay script reported failure"),String(t.error_code??"SCRIPT_FAILED"));return t}function c(e){return typeof e=="boolean"?e:void 0}function p(e){return{profile:String(e.profile??""),home:String(e.home??""),gatewayRunning:e.gateway_running===!0,restartRequired:e.restart_required===!0,providerKept:c(e.provider_kept),restoredModel:c(e.restored_model),userSwitchedAway:c(e.user_switched_away),modelCleared:c(e.model_cleared),warning:typeof e.warning=="string"?e.warning:void 0,snapshot:typeof e.snapshot=="string"?e.snapshot:void 0}}async function G(){const e=await d({command:"discover"}),o=Array.isArray(e.profiles)?e.profiles:[],n=Array.isArray(e.unreadable)?e.unreadable:[];return{profiles:o.map(r=>N(r)),unreadable:n.map(r=>{const t=r;return{name:String(t.name??""),home:String(t.home??""),error:String(t.error??"")}})}}function N(e){return{name:String(e.name??""),home:String(e.home??""),agentId:String(e.agent_id??""),gatewayRunning:e.gateway_running===!0,relayActive:e.relay_active===!0,modelProvider:typeof e.model_provider=="string"?e.model_provider:void 0,modelDefault:typeof e.model_default=="string"?e.model_default:void 0}}async function L(){const e=await d({command:"discover"});return(Array.isArray(e.profiles)?e.profiles:[]).map(n=>{const r=n;return{name:String(r.name??""),home:String(r.home??""),agentId:String(r.agent_id??""),gatewayRunning:r.gateway_running===!0,relayActive:r.relay_active===!0,modelProvider:typeof r.model_provider=="string"?r.model_provider:void 0,modelDefault:typeof r.model_default=="string"?r.model_default:void 0}})}async function U(e){const o=e.agentId?.trim(),n=e.baseUrl?.trim(),r=e.virtualKey?.trim(),t=e.model?.trim();if(!o)throw new i("agent_id is required","MISSING_AGENT_ID");if(!r)throw new i("virtual_key is required","MISSING_VIRTUAL_KEY");if(!n)throw new i("openai_base_url is required","MISSING_BASE_URL");try{new URL(n)}catch{throw new i("openai_base_url must be a valid URL","INVALID_BASE_URL")}if(!t)throw new i("model is required for hermes","MISSING_MODEL");return p(await d({command:"apply",agent_id:o,base_url:n,api_key:r,model:t}))}async function q(e){const o=e?.trim();if(!o)throw new i("agent_id is required","MISSING_AGENT_ID");return p(await d({command:"clear",agent_id:o}))}export{i as HermesProfileRelayError,U as applyHermesProfileRelay,q as clearHermesProfileRelay,L as discoverHermesProfiles,G as discoverHermesProfilesRaw,A as resolveHermesPython};
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Grix 中转(虚拟 Key)在**独立 Hermes** profile 上的落地执行端。
|
|
3
|
+
|
|
4
|
+
与连接器托管的 agent 不同:独立 Hermes 是用户自己跑的 gateway 进程,连接器既不 spawn 它、
|
|
5
|
+
也不在 agents.json 里认识它,因此没有"spawn 时注入 env"这一步——只能直接改它 profile 里的
|
|
6
|
+
config.yaml。目标 profile **不靠传路径、靠 agent_id 认**:每个接了 Grix 的 profile 都在
|
|
7
|
+
``channels.grix.wsUrl`` 里带着自己的 ``agent_id``,用它跟服务端下发的 agent_id 对上号。
|
|
8
|
+
|
|
9
|
+
**为什么必须用 Hermes 自己的解释器跑**(而不是在连接器里用 Node 解析 YAML):
|
|
10
|
+
配置的读写全交给宿主的 ``hermes_cli.config``,它负责原子写(temp+fsync+replace)、保留原
|
|
11
|
+
文件权限与 owner、对照 raw 配置剥离默认值、以及把文件尾那段注释区重新写回去。自己解析回写
|
|
12
|
+
YAML 会把注释区丢掉,也会跟宿主的归一化规则漂移。写入语义与用户在聊天里敲 ``/model`` 完全
|
|
13
|
+
一致,走的是同一个 ``save_config``。
|
|
14
|
+
|
|
15
|
+
协议:stdin 收一条 JSON 请求,stdout 回一条 JSON 响应。api_key 绝不进输出。
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import contextlib
|
|
20
|
+
import copy
|
|
21
|
+
import io
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
28
|
+
from urllib.parse import parse_qs, urlparse
|
|
29
|
+
|
|
30
|
+
# 这个脚本躺在连接器的源码树里,而 Python 会把"脚本所在目录"和 cwd 塞进 sys.path 的最前面。
|
|
31
|
+
# 宿主 Hermes 内部大量使用顶层 import(utils、gateway 等),一旦连接器目录下有同名文件或目录,
|
|
32
|
+
# 它就会解析错位并炸在 import 上。清掉这两个来源——本脚本不从相邻目录 import 任何东西。
|
|
33
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
34
|
+
sys.path[:] = [
|
|
35
|
+
entry for entry in sys.path
|
|
36
|
+
if entry and os.path.abspath(entry) != _HERE
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from hermes_cli.config import (
|
|
41
|
+
clear_model_endpoint_credentials,
|
|
42
|
+
read_raw_config,
|
|
43
|
+
save_config,
|
|
44
|
+
)
|
|
45
|
+
from hermes_cli.profiles import list_profiles
|
|
46
|
+
except Exception as _import_exc: # noqa: BLE001 - 必须报清楚,不能被下游当成"没有 profile"
|
|
47
|
+
print(json.dumps({
|
|
48
|
+
"ok": False,
|
|
49
|
+
"error_code": "HERMES_IMPORT_FAILED",
|
|
50
|
+
"error_msg": f"cannot import hermes modules ({type(_import_exc).__name__}: {_import_exc}); "
|
|
51
|
+
"this script must run with hermes' own interpreter",
|
|
52
|
+
}))
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
# 连接器写进 Hermes 的固定 provider id。
|
|
56
|
+
GRIX_PROVIDER_ID = "grix"
|
|
57
|
+
# 记录开中转前的原模型设置,供关闭时精确还原。放 profile 目录,不进 config.yaml。
|
|
58
|
+
SIDECAR_NAME = ".grix-relay-state.json"
|
|
59
|
+
SNAPSHOT_PREFIX = "config.yaml.grix-relay-snapshot."
|
|
60
|
+
# 快照是含凭证的整份配置副本,不能无限堆。每次开中转建新的,只留最近这几个。
|
|
61
|
+
SNAPSHOT_KEEP = 3
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RelayError(Exception):
|
|
65
|
+
def __init__(self, code: str, msg: str) -> None:
|
|
66
|
+
super().__init__(msg)
|
|
67
|
+
self.code = code
|
|
68
|
+
self.msg = msg
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _redact(text: str, req: Dict[str, Any]) -> str:
|
|
72
|
+
"""把本次请求里的 api_key 从要吐出去的文本里抹掉。
|
|
73
|
+
|
|
74
|
+
宿主抛的异常今天恰好不回显输入,但那不是能长期依赖的保证——换个带输入回显的异常,Key 就
|
|
75
|
+
跟着错误信息出去了。真做脱敏,而不是在注释里许诺。
|
|
76
|
+
"""
|
|
77
|
+
secret = str(req.get("api_key") or "").strip()
|
|
78
|
+
if secret and secret in text:
|
|
79
|
+
text = text.replace(secret, "***redacted***")
|
|
80
|
+
return text
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _agent_id_of(cfg: Dict[str, Any]) -> str:
|
|
84
|
+
"""从 ``channels.grix.wsUrl`` 的 query 里取 agent_id —— profile 与平台 agent 的唯一锚点。"""
|
|
85
|
+
channels = cfg.get("channels")
|
|
86
|
+
if not isinstance(channels, dict):
|
|
87
|
+
return ""
|
|
88
|
+
grix = channels.get(GRIX_PROVIDER_ID)
|
|
89
|
+
if not isinstance(grix, dict):
|
|
90
|
+
return ""
|
|
91
|
+
ws_url = grix.get("wsUrl")
|
|
92
|
+
if not isinstance(ws_url, str) or not ws_url.strip():
|
|
93
|
+
return ""
|
|
94
|
+
try:
|
|
95
|
+
values = parse_qs(urlparse(ws_url).query).get("agent_id") or []
|
|
96
|
+
except Exception:
|
|
97
|
+
return ""
|
|
98
|
+
return str(values[0]).strip() if values else ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _config_path(home: str) -> str:
|
|
102
|
+
return os.path.join(home, "config.yaml")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _read_profile_config(home: str) -> Dict[str, Any]:
|
|
106
|
+
"""读某个 profile 的原始配置(不合并默认值)。切 HERMES_HOME 即切目标 profile。
|
|
107
|
+
|
|
108
|
+
宿主的 ``read_raw_config`` **自己吞掉解析错误并回一个空 dict**,永远不抛。不识别这一点的话,
|
|
109
|
+
一个语法坏掉的 config.yaml 会表现成"这个 profile 没接 Grix",最终报 PROFILE_NOT_FOUND——
|
|
110
|
+
把人引向完全错误的方向。文件有内容却解析成空 ⇒ 它是坏的,如实抛出去。
|
|
111
|
+
"""
|
|
112
|
+
os.environ["HERMES_HOME"] = home
|
|
113
|
+
cfg = read_raw_config() or {}
|
|
114
|
+
if not cfg:
|
|
115
|
+
path = _config_path(home)
|
|
116
|
+
try:
|
|
117
|
+
broken = os.path.exists(path) and os.path.getsize(path) > 0
|
|
118
|
+
except OSError:
|
|
119
|
+
broken = False
|
|
120
|
+
if broken:
|
|
121
|
+
raise RelayError(
|
|
122
|
+
"CONFIG_UNREADABLE",
|
|
123
|
+
f"config.yaml exists but hermes parsed it as empty (likely malformed): {path}",
|
|
124
|
+
)
|
|
125
|
+
return cfg
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _save_profile_config(home: str, cfg: Dict[str, Any]) -> None:
|
|
129
|
+
os.environ["HERMES_HOME"] = home
|
|
130
|
+
save_config(cfg)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _verify_write(home: str, expect_relay: bool) -> None:
|
|
134
|
+
"""回读校验:写入必须真的落盘了。
|
|
135
|
+
|
|
136
|
+
宿主的 ``save_config`` 在托管安装下(``is_managed()``)只往 stderr 打一行就 return,**不抛
|
|
137
|
+
异常**——不校验的话,接口会在配置一个字节都没变的情况下报成功。跨进程覆盖(gateway 同时
|
|
138
|
+
在写同一份 config,双方都是 read-modify-write)同样会让我们的写入无声消失。回读是把这两种
|
|
139
|
+
假成功变成可见失败的唯一办法。
|
|
140
|
+
"""
|
|
141
|
+
fresh = _read_profile_config(home)
|
|
142
|
+
providers = fresh.get("providers")
|
|
143
|
+
has_provider = isinstance(providers, dict) and GRIX_PROVIDER_ID in providers
|
|
144
|
+
on_grix = _relay_active(fresh)
|
|
145
|
+
|
|
146
|
+
if expect_relay and not (has_provider and on_grix):
|
|
147
|
+
raise RelayError(
|
|
148
|
+
"CONFIG_WRITE_VERIFY_FAILED",
|
|
149
|
+
"config.yaml did not take the relay settings after save "
|
|
150
|
+
f"(grix provider present={has_provider}, model points at grix={on_grix}); "
|
|
151
|
+
"the write was silently dropped — hermes may be a managed install, "
|
|
152
|
+
"or its gateway overwrote the file concurrently",
|
|
153
|
+
)
|
|
154
|
+
# 关中转只看"模型还指不指着 grix"。grix provider 本身是**故意留下**的(虚拟 Key 是按
|
|
155
|
+
# agent 绑定的长期凭证,删了下次开就得换新 Key、统计会断),它的存在不代表中转还开着。
|
|
156
|
+
if not expect_relay and on_grix:
|
|
157
|
+
raise RelayError(
|
|
158
|
+
"CONFIG_WRITE_VERIFY_FAILED",
|
|
159
|
+
"config.yaml still points the model at the grix provider after save; "
|
|
160
|
+
"the write was silently dropped — hermes may be a managed install, "
|
|
161
|
+
"or its gateway overwrote the file concurrently",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _iter_profiles() -> List[Tuple[str, str, bool]]:
|
|
166
|
+
"""(name, home, gateway_running),含 default profile。用宿主的枚举,不自己拼路径。"""
|
|
167
|
+
out: List[Tuple[str, str, bool]] = []
|
|
168
|
+
for info in list_profiles():
|
|
169
|
+
out.append((str(info.name), str(info.path), bool(getattr(info, "gateway_running", False))))
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _model_section(cfg: Dict[str, Any]) -> Dict[str, Any]:
|
|
174
|
+
raw = cfg.get("model")
|
|
175
|
+
if isinstance(raw, dict):
|
|
176
|
+
return raw
|
|
177
|
+
section: Dict[str, Any] = {}
|
|
178
|
+
# 历史上 model 可能是个裸字符串(模型名),按宿主的语义归一成 {default: <name>}。
|
|
179
|
+
if isinstance(raw, str) and raw.strip():
|
|
180
|
+
section["default"] = raw.strip()
|
|
181
|
+
cfg["model"] = section
|
|
182
|
+
return section
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _relay_active(cfg: Dict[str, Any]) -> bool:
|
|
186
|
+
"""这个 profile 当前是不是正开着中转。
|
|
187
|
+
|
|
188
|
+
判据是**模型指没指着 grix**,不是"sidecar 在不在"。两者在"用户中途自己 /model 切走后又
|
|
189
|
+
重开中转"这个场景里会分岔:sidecar 还在,但盘上的 model 段已经是用户的新选择了。
|
|
190
|
+
"""
|
|
191
|
+
raw = cfg.get("model")
|
|
192
|
+
if not isinstance(raw, dict):
|
|
193
|
+
return False
|
|
194
|
+
return str(raw.get("provider") or "").strip() == GRIX_PROVIDER_ID
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _sidecar_path(home: str) -> str:
|
|
198
|
+
return os.path.join(home, SIDECAR_NAME)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _read_sidecar(home: str) -> Optional[Dict[str, Any]]:
|
|
202
|
+
path = _sidecar_path(home)
|
|
203
|
+
if not os.path.exists(path):
|
|
204
|
+
return None
|
|
205
|
+
try:
|
|
206
|
+
with open(path, encoding="utf-8") as handle:
|
|
207
|
+
data = json.load(handle)
|
|
208
|
+
return data if isinstance(data, dict) else None
|
|
209
|
+
except Exception:
|
|
210
|
+
# 记录读坏了就当没有:宁可不还原 model 段,也不能拿瞎猜的值去覆盖用户配置。
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _write_sidecar(home: str, data: Dict[str, Any]) -> None:
|
|
215
|
+
# 用 os.open 一步带权限创建:先 open(w) 再 chmod 的话,中间有一个窗口文件是 umask 默认权限,
|
|
216
|
+
# 而这里面存着用户自己的 api_key(我们从 model 段抄下来的原值)。
|
|
217
|
+
path = _sidecar_path(home)
|
|
218
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
219
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
220
|
+
json.dump(data, handle, indent=2, ensure_ascii=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _prune_snapshots(home: str, keep: int = SNAPSHOT_KEEP) -> None:
|
|
224
|
+
"""只留最近 keep 个快照——每个都是含凭证的整份配置副本,不能无限堆。"""
|
|
225
|
+
try:
|
|
226
|
+
rows = sorted(
|
|
227
|
+
(name for name in os.listdir(home)
|
|
228
|
+
if name.startswith(SNAPSHOT_PREFIX) and name.endswith(".bak")),
|
|
229
|
+
reverse=True,
|
|
230
|
+
)
|
|
231
|
+
for name in rows[keep:]:
|
|
232
|
+
with contextlib.suppress(OSError):
|
|
233
|
+
os.remove(os.path.join(home, name))
|
|
234
|
+
except OSError:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _snapshot(home: str) -> Optional[str]:
|
|
239
|
+
"""开中转前存一份整份配置快照,纯逃生舱:不参与自动还原,只给人工兜底。"""
|
|
240
|
+
src = _config_path(home)
|
|
241
|
+
if not os.path.exists(src):
|
|
242
|
+
return None
|
|
243
|
+
# 时间戳带上微秒:只精确到秒的话,同一秒内的两次开启会互相覆盖。
|
|
244
|
+
stamp = time.strftime("%Y%m%d-%H%M%S") + "-%06d" % (int(time.time() * 1_000_000) % 1_000_000)
|
|
245
|
+
dst = os.path.join(home, SNAPSHOT_PREFIX + stamp + ".bak")
|
|
246
|
+
try:
|
|
247
|
+
shutil.copy2(src, dst)
|
|
248
|
+
except Exception:
|
|
249
|
+
return None
|
|
250
|
+
_prune_snapshots(home)
|
|
251
|
+
return os.path.basename(dst)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _find_profile(agent_id: str) -> Tuple[str, str, bool, Dict[str, Any]]:
|
|
255
|
+
"""按 agent_id 定位 profile。匹配不到、或多个 profile 抢同一个 agent_id,都要报错不猜。"""
|
|
256
|
+
agent_id = (agent_id or "").strip()
|
|
257
|
+
if not agent_id:
|
|
258
|
+
raise RelayError("MISSING_AGENT_ID", "agent_id is required")
|
|
259
|
+
|
|
260
|
+
matches: List[Tuple[str, str, bool, Dict[str, Any]]] = []
|
|
261
|
+
unreadable: List[str] = []
|
|
262
|
+
for name, home, running in _iter_profiles():
|
|
263
|
+
try:
|
|
264
|
+
cfg = _read_profile_config(home)
|
|
265
|
+
except Exception as exc: # noqa: BLE001
|
|
266
|
+
# 读不了的 profile 可能正是目标。绝不能静默跳过后报"没找到"——那会让一个坏掉的
|
|
267
|
+
# config.yaml 表现成"这个 agent 不是 hermes",把人引向完全错误的方向。
|
|
268
|
+
unreadable.append(f"{name} ({type(exc).__name__}: {exc})")
|
|
269
|
+
continue
|
|
270
|
+
if _agent_id_of(cfg) == agent_id:
|
|
271
|
+
matches.append((name, home, running, cfg))
|
|
272
|
+
|
|
273
|
+
if not matches:
|
|
274
|
+
detail = f"no hermes profile is bound to agent_id {agent_id}"
|
|
275
|
+
if unreadable:
|
|
276
|
+
detail += f"; {len(unreadable)} profile(s) could not be read: {'; '.join(unreadable)}"
|
|
277
|
+
raise RelayError("PROFILE_NOT_FOUND", detail)
|
|
278
|
+
if len(matches) > 1:
|
|
279
|
+
names = ", ".join(sorted(m[0] for m in matches))
|
|
280
|
+
raise RelayError(
|
|
281
|
+
"AMBIGUOUS_AGENT_ID",
|
|
282
|
+
f"agent_id {agent_id} is claimed by multiple profiles: {names}",
|
|
283
|
+
)
|
|
284
|
+
return matches[0]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_discover(_req: Dict[str, Any]) -> Dict[str, Any]:
|
|
288
|
+
profiles = []
|
|
289
|
+
unreadable: List[Dict[str, str]] = []
|
|
290
|
+
for name, home, running in _iter_profiles():
|
|
291
|
+
try:
|
|
292
|
+
cfg = _read_profile_config(home)
|
|
293
|
+
except Exception as exc: # noqa: BLE001 - 报出来,不装作这个 profile 不存在
|
|
294
|
+
unreadable.append({"name": name, "home": home, "error": f"{type(exc).__name__}: {exc}"})
|
|
295
|
+
continue
|
|
296
|
+
model = _model_section(dict(cfg))
|
|
297
|
+
agent_id = _agent_id_of(cfg)
|
|
298
|
+
if not agent_id:
|
|
299
|
+
# 没接 Grix 的 profile(比如纯本地用的)不是我们的目标,直接跳过。
|
|
300
|
+
continue
|
|
301
|
+
profiles.append({
|
|
302
|
+
"name": name,
|
|
303
|
+
"home": home,
|
|
304
|
+
"agent_id": agent_id,
|
|
305
|
+
"gateway_running": running,
|
|
306
|
+
"relay_active": str(model.get("provider") or "").strip() == GRIX_PROVIDER_ID,
|
|
307
|
+
"model_provider": model.get("provider"),
|
|
308
|
+
"model_default": model.get("default"),
|
|
309
|
+
})
|
|
310
|
+
return {"ok": True, "profiles": profiles, "unreadable": unreadable}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def cmd_apply(req: Dict[str, Any]) -> Dict[str, Any]:
|
|
314
|
+
base_url = str(req.get("base_url") or "").strip()
|
|
315
|
+
api_key = str(req.get("api_key") or "").strip()
|
|
316
|
+
model = str(req.get("model") or "").strip()
|
|
317
|
+
if not base_url:
|
|
318
|
+
raise RelayError("MISSING_BASE_URL", "base_url is required to enable Grix relay")
|
|
319
|
+
if not api_key:
|
|
320
|
+
raise RelayError("MISSING_API_KEY", "api_key is required to enable Grix relay")
|
|
321
|
+
if not model:
|
|
322
|
+
# hermes 的 provider 配置里模型名必填,缺了就是"写进去了、聊天必失败"的静默半配置。
|
|
323
|
+
raise RelayError("MISSING_MODEL", "model is required (hermes provider config needs a model name)")
|
|
324
|
+
|
|
325
|
+
name, home, running, cfg = _find_profile(req.get("agent_id"))
|
|
326
|
+
model_cfg = _model_section(cfg)
|
|
327
|
+
|
|
328
|
+
# 只在**当前没开着中转**时记原值——这时盘上的 model 段按定义就是用户自己的选择。
|
|
329
|
+
#
|
|
330
|
+
# 判据必须是"是否正在中转中"(_relay_active),不能是"sidecar 在不在":
|
|
331
|
+
# - 中转中重复下发 → 不记,否则会把"中转中"的 grix 当原值存下来,关了再也回不去;
|
|
332
|
+
# - 用户中途 /model 切走后再重开 → sidecar 还在,但盘上已是用户的新选择,**必须刷新**,
|
|
333
|
+
# 否则关中转时会拿陈旧的旧值把用户的选择冲掉。
|
|
334
|
+
already_on = _relay_active(cfg)
|
|
335
|
+
sidecar = _read_sidecar(home) if already_on else None
|
|
336
|
+
if not already_on:
|
|
337
|
+
sidecar = {
|
|
338
|
+
# 整段快照,不手抄"我们会改哪些字段"的清单:宿主的 clear_model_endpoint_credentials
|
|
339
|
+
# 哪天多清一个字段,手抄清单就会静默丢掉那个字段的原值。
|
|
340
|
+
"previous_model": copy.deepcopy(model_cfg),
|
|
341
|
+
"snapshot": _snapshot(home),
|
|
342
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
343
|
+
}
|
|
344
|
+
_write_sidecar(home, sidecar)
|
|
345
|
+
|
|
346
|
+
providers = cfg.get("providers")
|
|
347
|
+
if not isinstance(providers, dict):
|
|
348
|
+
providers = {}
|
|
349
|
+
cfg["providers"] = providers
|
|
350
|
+
providers[GRIX_PROVIDER_ID] = {
|
|
351
|
+
"name": "Grix",
|
|
352
|
+
"api": base_url,
|
|
353
|
+
"api_key": api_key,
|
|
354
|
+
"default_model": model,
|
|
355
|
+
"transport": "openai_chat",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# model.base_url / api_key 这些内联 endpoint 字段只对 provider=custom 有意义,留着会污染
|
|
359
|
+
# 命名 provider 的解析(宿主自己切换非 custom provider 时也做同样的清理)。
|
|
360
|
+
clear_model_endpoint_credentials(model_cfg, clear_base_url=True)
|
|
361
|
+
model_cfg["provider"] = GRIX_PROVIDER_ID
|
|
362
|
+
model_cfg["default"] = model
|
|
363
|
+
|
|
364
|
+
_save_profile_config(home, cfg)
|
|
365
|
+
_verify_write(home, expect_relay=True)
|
|
366
|
+
return {
|
|
367
|
+
"ok": True,
|
|
368
|
+
"profile": name,
|
|
369
|
+
"home": home,
|
|
370
|
+
"model": model,
|
|
371
|
+
"gateway_running": running,
|
|
372
|
+
"restart_required": True,
|
|
373
|
+
"snapshot": (sidecar or {}).get("snapshot"),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def cmd_clear(req: Dict[str, Any]) -> Dict[str, Any]:
|
|
378
|
+
name, home, running, cfg = _find_profile(req.get("agent_id"))
|
|
379
|
+
model_cfg = _model_section(cfg)
|
|
380
|
+
sidecar = _read_sidecar(home)
|
|
381
|
+
snapshot = (sidecar or {}).get("snapshot")
|
|
382
|
+
|
|
383
|
+
# **grix provider 连同虚拟 Key 原样留着,不删。**
|
|
384
|
+
#
|
|
385
|
+
# 虚拟 Key 是按 agent 绑定的长期凭证:服务端只在签发那一刻给一次明文,之后再要只能
|
|
386
|
+
# resend——而 resend 会签一把新的并吊销旧的。关中转就把本地这份删掉的话,下次开中转
|
|
387
|
+
# 就只能换新 Key,于是每开关一轮换一把,按 Key 统计的用量被切成互不相连的碎片。
|
|
388
|
+
# 关中转要的是"不再走中转",把模型指回用户原来的供应商就够了;留下的 grix provider
|
|
389
|
+
# 没有任何东西指向它,不影响推理,下次开中转直接指回来即可。
|
|
390
|
+
providers = cfg.get("providers")
|
|
391
|
+
provider_kept = isinstance(providers, dict) and GRIX_PROVIDER_ID in providers
|
|
392
|
+
|
|
393
|
+
# 中转开着的时候用户可能自己 /model 切走了——那是他的新选择,无脑把旧值写回去会把它冲掉。
|
|
394
|
+
# 只有确认模型还指着 grix 时才动 model 段。
|
|
395
|
+
still_on_grix = _relay_active(cfg)
|
|
396
|
+
previous = (sidecar or {}).get("previous_model")
|
|
397
|
+
restored = False
|
|
398
|
+
model_cleared = False
|
|
399
|
+
|
|
400
|
+
if still_on_grix:
|
|
401
|
+
if isinstance(previous, dict):
|
|
402
|
+
cfg["model"] = copy.deepcopy(previous)
|
|
403
|
+
restored = True
|
|
404
|
+
else:
|
|
405
|
+
# sidecar 丢了或读坏了。**绝不能把 model.provider=grix 留在盘上**——它会指向一个
|
|
406
|
+
# 刚被我们摘掉的 provider,配置直接是坏的,比还原和不还原都糟。清掉指向让 hermes
|
|
407
|
+
# 回落到它自己的默认,并在响应里如实说明我们没能还原成用户原来的模型。
|
|
408
|
+
model_cfg.pop("provider", None)
|
|
409
|
+
model_cfg.pop("default", None)
|
|
410
|
+
model_cleared = True
|
|
411
|
+
|
|
412
|
+
if restored or model_cleared:
|
|
413
|
+
_save_profile_config(home, cfg)
|
|
414
|
+
_verify_write(home, expect_relay=False)
|
|
415
|
+
|
|
416
|
+
sidecar_path = _sidecar_path(home)
|
|
417
|
+
if os.path.exists(sidecar_path):
|
|
418
|
+
with contextlib.suppress(Exception):
|
|
419
|
+
os.remove(sidecar_path)
|
|
420
|
+
|
|
421
|
+
result = {
|
|
422
|
+
"ok": True,
|
|
423
|
+
"profile": name,
|
|
424
|
+
"home": home,
|
|
425
|
+
# 中转 provider(连同虚拟 Key)留在配置里,供下次开中转直接复用——见上面为什么不删。
|
|
426
|
+
"provider_kept": provider_kept,
|
|
427
|
+
"restored_model": restored,
|
|
428
|
+
# 用户中途自己换过模型:model 段是他自己的选择,我们没动。
|
|
429
|
+
"user_switched_away": bool(not still_on_grix),
|
|
430
|
+
"gateway_running": running,
|
|
431
|
+
"restart_required": bool(restored or model_cleared),
|
|
432
|
+
}
|
|
433
|
+
if snapshot:
|
|
434
|
+
# 逃生舱得告诉人它在哪,否则等于没有。
|
|
435
|
+
result["snapshot"] = snapshot
|
|
436
|
+
if model_cleared:
|
|
437
|
+
result["model_cleared"] = True
|
|
438
|
+
result["warning"] = (
|
|
439
|
+
"relay state file was missing or unreadable: the grix provider was removed and the "
|
|
440
|
+
"model selection was cleared (hermes will fall back to its default). The previous "
|
|
441
|
+
"model settings could not be restored"
|
|
442
|
+
+ (f"; a pre-relay snapshot is kept at {snapshot}" if snapshot else "")
|
|
443
|
+
)
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
COMMANDS = {"discover": cmd_discover, "apply": cmd_apply, "clear": cmd_clear}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def main() -> int:
|
|
451
|
+
try:
|
|
452
|
+
raw = sys.stdin.read()
|
|
453
|
+
req = json.loads(raw) if raw.strip() else {}
|
|
454
|
+
except Exception as exc:
|
|
455
|
+
print(json.dumps({"ok": False, "error_code": "BAD_REQUEST", "error_msg": str(exc)}))
|
|
456
|
+
return 1
|
|
457
|
+
|
|
458
|
+
command = str(req.get("command") or "").strip()
|
|
459
|
+
handler = COMMANDS.get(command)
|
|
460
|
+
if handler is None:
|
|
461
|
+
print(json.dumps({
|
|
462
|
+
"ok": False,
|
|
463
|
+
"error_code": "BAD_REQUEST",
|
|
464
|
+
"error_msg": f"unknown command: {command or '(empty)'}",
|
|
465
|
+
}))
|
|
466
|
+
return 1
|
|
467
|
+
|
|
468
|
+
# 宿主的配置层会往 stdout 打提示(managed 配置等),那会污染我们的 JSON 协议。
|
|
469
|
+
# 业务执行期间把 stdout 折到 stderr,只有最终响应写回真正的 stdout。
|
|
470
|
+
real_stdout = sys.stdout
|
|
471
|
+
try:
|
|
472
|
+
with contextlib.redirect_stdout(io.StringIO()) as captured:
|
|
473
|
+
result = handler(req)
|
|
474
|
+
noise = captured.getvalue().strip()
|
|
475
|
+
if noise:
|
|
476
|
+
print(noise, file=sys.stderr)
|
|
477
|
+
except RelayError as exc:
|
|
478
|
+
print(json.dumps({
|
|
479
|
+
"ok": False,
|
|
480
|
+
"error_code": exc.code,
|
|
481
|
+
"error_msg": _redact(exc.msg, req),
|
|
482
|
+
}), file=real_stdout)
|
|
483
|
+
return 1
|
|
484
|
+
except Exception as exc: # noqa: BLE001 - 兜底
|
|
485
|
+
print(json.dumps({
|
|
486
|
+
"ok": False,
|
|
487
|
+
"error_code": "CONFIG_WRITE_FAILED",
|
|
488
|
+
"error_msg": _redact(f"{type(exc).__name__}: {exc}", req),
|
|
489
|
+
}), file=real_stdout)
|
|
490
|
+
return 1
|
|
491
|
+
|
|
492
|
+
print(json.dumps(result, ensure_ascii=False), file=real_stdout)
|
|
493
|
+
return 0
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
if __name__ == "__main__":
|
|
497
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class o extends Error{code;constructor(r,e){super(r),this.code=e,this.name="RelayRouteError"}}function l(t,r,e){return t?.code!=="PROFILE_NOT_FOUND"?t:new o(`agent_id ${r} matches neither of the two places it could live on this machine: it is not among the ${e} agent(s) in this connector's agents.json, and no hermes profile is bound to it`,"NOT_FOUND")}async function c(t,r,e){const n=r?.trim();if(!n)throw new o("agent_id is required","MISSING_AGENT_ID");if(!e.virtualKey?.trim())throw new o("virtual_key is required","MISSING_VIRTUAL_KEY");const i=t.listAgents(),a=i.find(s=>(s.agentId??"").trim()===n);if(a)return{target:"managed-agent",result:await t.enableManaged(a.name,n,e)};try{return{target:"hermes-profile",result:await t.enableHermesProfile(n,e)}}catch(s){throw l(s,n,i.length)}}async function d(t,r){const e=r?.trim();if(!e)throw new o("agent_id is required","MISSING_AGENT_ID");const n=t.listAgents(),i=n.find(a=>(a.agentId??"").trim()===e);if(i)return{target:"managed-agent",result:await t.disableManaged(i.name)};try{return{target:"hermes-profile",result:await t.disableHermesProfile(e)}}catch(a){throw l(a,e,n.length)}}export{o as RelayRouteError,d as disableRelayCredential,c as enableRelayCredential};
|