UNPKG

grix-connector

Version:

Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.

2 lines (1 loc) 5.77 kB
import{createServer as g}from"node:http";import{log as c}from"../log/logger.js";class m{server=null;token;handler=null;upgradeHandler=null;probeHandler=null;installHandler=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}async start(e){return new Promise((r,t)=>{this.server=g((n,s)=>this.handleRequest(n,s)),this.server.listen(e,"127.0.0.1",()=>{c.info("admin",`Listening on 127.0.0.1:${e}`),r()}),this.server.on("error",t)})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>e())})}handleRequest(e,r){const t=e.method??"",n=e.url??"";if(n==="/api/agents"&&t==="GET")this.handleList(r);else if(n==="/api/agents"&&t==="POST")this.readBody(e).then(s=>this.handleAdd(r,s)).catch(s=>this.error(r,s));else if(t==="DELETE"&&n.startsWith("/api/agents/")){const s=decodeURIComponent(n.slice(12));this.handleRemove(r,s)}else if(t==="POST"&&n.match(/^\/api\/agents\/[^/]+\/restart$/)){const s=decodeURIComponent(n.slice(12,n.lastIndexOf("/restart")));this.handleRestart(r,s)}else if(n==="/api/upgrade"&&t==="GET")this.handleCheckUpgrade(r);else if(n==="/api/upgrade"&&t==="POST")this.handleTriggerUpgrade(r);else if(t==="GET"&&n.startsWith("/api/probe"))this.handleProbe(e,r,n);else if(n==="/api/install"&&t==="GET")this.handleInstallList(r);else if(n==="/api/install"&&t==="POST")this.readBody(e).then(s=>this.handleInstall(r,s)).catch(s=>this.error(r,s));else if(t==="GET"&&n.startsWith("/api/install/")){const s=decodeURIComponent(n.slice(13));this.handleInstallProgress(r,s)}else this.json(r,404,{error:"not_found"})}handleList(e){try{const r=this.handler?.list()??[];this.json(e,200,r)}catch(r){this.error(e,r)}}async handleAdd(e,r){try{const t=await this.handler.add(r);this.json(e,201,t??{ok:!0})}catch(t){this.error(e,t)}}handleRemove(e,r){this.handler.remove(r).then(()=>{e.writeHead(204),e.end()}).catch(t=>this.error(e,t))}handleRestart(e,r){this.handler.restart(r).then(()=>{this.json(e,200,{ok:!0})}).catch(t=>this.error(e,t))}handleCheckUpgrade(e){if(!this.upgradeHandler){this.json(e,501,{error:"upgrade not configured"});return}this.upgradeHandler.check().then(r=>{this.json(e,200,r)}).catch(r=>this.error(e,r))}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,r){const t=r;t.code==="NOT_FOUND"?this.json(e,404,{error:t.message??"not found"}):t.code==="UNKNOWN_AGENT"||t.code==="UNSUPPORTED_OS"?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??r}`),this.json(e,500,{error:t.message??"internal error"}))}json(e,r,t){const n=JSON.stringify(t);e.writeHead(r,{"Content-Type":"application/json"}),e.end(n)}readBody(e){return new Promise((r,t)=>{let n="";e.setEncoding("utf8"),e.on("data",s=>{n+=s}),e.on("end",()=>{try{r(JSON.parse(n))}catch{t(new Error("invalid JSON body"))}}),e.on("error",t)})}handleProbe(e,r,t){if(!this.probeHandler){this.json(r,501,{error:"probe not configured"});return}const n=t.indexOf("?"),s=n>=0?t.slice(0,n):t,a=n>=0?new URLSearchParams(t.slice(n+1)):new URLSearchParams,i={};a.get("conversation")==="true"&&(i.conversation=!0),a.get("fresh")==="true"&&(i.fresh=!0);const l=Number(a.get("timeoutMs"));Number.isFinite(l)&&l>0&&(i.timeoutMs=l);const d=s.match(/^\/api\/probe\/(.+)$/);if(d){const o=decodeURIComponent(d[1]);this.probeHandler.probeOne(o,i).then(h=>{this.json(r,200,h)}).catch(h=>this.error(r,h));return}this.probeHandler.probeAll(i).then(o=>{this.json(r,200,o)}).catch(o=>this.error(r,o))}handleInstallList(e){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}try{const r=this.installHandler.listInstallable();this.json(e,200,r)}catch(r){this.error(e,r)}}handleInstallProgress(e,r){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}const t=this.installHandler.getInstallProgress(r);if(!t){this.json(e,200,{agentType:r,status:"unknown",inProgress:!1,progress:null});return}let n,s,a;switch(t.phase){case"completed":n="done",s="\u5B89\u88C5\u5B8C\u6210";break;case"failed":n="error",a=t.outputTail||"\u5B89\u88C5\u5931\u8D25";break;case"preflight":n="pending",s="\u68C0\u67E5\u524D\u7F6E\u4F9D\u8D56...";break;case"installing_prereq":n="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":n="installing",s=`\u6B63\u5728\u5B89\u88C5 ${r}...`;break;case"verifying":n="installing",s="\u9A8C\u8BC1\u5B89\u88C5...";break;default:n="unknown"}this.json(e,200,{agentType:r,status:n,inProgress:!0,progress:t.elapsedMs?Math.min(.9,t.elapsedMs/3e4):.1,message:s,error:a})}async handleInstall(e,r){if(!this.installHandler){this.json(e,501,{error:"install not configured"});return}try{const t=r;if(!t||typeof t.agentType!="string"||!t.agentType){this.json(e,400,{error:"agentType is required"});return}const n=await this.installHandler.installAgent(t);if(n.ok)this.json(e,200,n);else{const s=n.error?.code;s==="UNKNOWN_AGENT"||s==="UNSUPPORTED_OS"?this.json(e,400,n):s==="INSTALL_IN_PROGRESS"?this.json(e,409,n):this.json(e,500,n)}}catch(t){this.error(e,t)}}}export{m as AdminServer};