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.
8 lines (7 loc) • 8.34 kB
JavaScript
import{randomUUID as y}from"node:crypto";import{writeFile as v}from"node:fs/promises";import{tmpdir as A}from"node:os";import{join as R}from"node:path";const f="https://grix.dhf.pub",T=15e3;class d extends Error{status;code;payload;constructor(r,e=0,t=-1,i=null){super(r),this.name="GrixAuthError",this.status=e,this.code=t,this.payload=i}}function p(n){process.stdout.write(`${JSON.stringify(n,null,2)}
`)}function k(){return["Usage:"," node scripts/grix_auth.js [--base-url <url>] <action> [options]","","Actions:"," fetch-captcha"," send-email-code --email <email> --scene <register|reset|change_password> [--captcha-id <id>] [--captcha-value <value>]"," register --email <email> --password <password> --email-code <code> [--device-id <id>] [--platform <platform>]"," login (--account <account>|--email <email>) --password <password> [--device-id <id>] [--platform <platform>]"," create-api-agent --access-token <token> --agent-name <name> [--avatar-url <url>] [--no-reuse-existing-agent] [--no-rotate-key-on-reuse]"].join(`
`)}function j(n){const r=(n??"").trim()||f;let e;try{e=new URL(r)}catch{throw new Error(`Invalid base URL: ${r}`)}if(!e.protocol||!e.host)throw new Error(`Invalid base URL: ${r}`);let t=e.pathname.replace(/\/+$/,"");return t?t.endsWith("/v1")||(t=`${t}/v1`):t="/v1",e.pathname=t,e.search="",e.hash="",e.toString().replace(/\/$/,"")}function J(n){const r=(n??"").trim()||f;let e;try{e=new URL(r)}catch{throw new Error(`Invalid base URL: ${r}`)}if(!e.protocol||!e.host)throw new Error(`Invalid base URL: ${r}`);let t=e.pathname.replace(/\/+$/,"");return t.endsWith("/v1")&&(t=t.slice(0,-3)),t=t.replace(/\/+$/,""),t||(t="/"),e.pathname=t.endsWith("/")?t:`${t}/`,e.search="",e.hash="",e.toString()}async function E(n,r,e){const t=new AbortController,i=setTimeout(()=>t.abort(),e);try{return await fetch(n,{...r,signal:t.signal})}finally{clearTimeout(i)}}async function h(n){const r=j(n.baseUrl),e=n.path.startsWith("/")?n.path:`/${n.path}`,t=`${r}${e}`,i={...n.headers??{}};let s;n.body!==void 0&&(s=JSON.stringify(n.body),i["Content-Type"]="application/json");let a;try{a=await E(t,{method:n.method,headers:i,body:s},T)}catch(g){const b=g instanceof Error?g.message:String(g);throw new d(`network error: ${b}`)}const o=a.status,c=await a.text();let m;try{m=JSON.parse(c)}catch(g){const b=c.slice(0,256);throw new d(`invalid json response: ${b}`,o,-1,g instanceof Error?g.message:String(g))}const _=m??{},w=Number(_.code??-1),x=String(_.msg??"").trim()||"unknown error";if(o>=400||w!==0)throw new d(x,o,w,m);return{api_base_url:r,status:o,data:_.data,payload:m}}async function N(n){const r=(n??"").trim();if(!r.startsWith("data:image/"))return"";const e=";base64,",t=r.indexOf(e);if(t<0)return"";const i=r.slice(t+e.length);let s;try{s=Buffer.from(i,"base64")}catch{return""}const a=R(A(),`grix-captcha-${y()}.png`);try{await v(a,s)}catch{return""}return a}function S(n,r,e){const t=r.data??{},i=t.user??{};return{ok:!0,action:n,api_base_url:r.api_base_url,access_token:String(t.access_token??""),refresh_token:String(t.refresh_token??""),expires_in:Number(t.expires_in??0),user_id:String(i.id??""),portal_url:J(e),data:t}}function O(n,r){const e=r.data??{},t=String(e.id??"").trim(),i=String(e.api_endpoint??"").trim(),s=String(e.api_key??"").trim(),a=String(e.agent_name??"").trim(),o={agent_name:a,agent_id:t,api_endpoint:i,api_key:s},c=["bind-local",`agent_name=${a}`,`agent_id=${t}`,`api_endpoint=${i}`,`api_key=${s}`,"do_not_create_remote_agent=true"].join(`
`);return{ok:!0,action:n,api_base_url:r.api_base_url,agent_id:t,agent_name:a,provider_type:Number(e.provider_type??0),api_endpoint:i,api_key:s,api_key_hint:String(e.api_key_hint??""),session_id:String(e.session_id??""),handoff:{target_tool:"grix_admin",task:c,bind_local:o},data:e}}function $(n){return`${(n??"").trim()||"web"}_${y()}`}async function P(n){const r=await h({method:"POST",path:"/auth/login",baseUrl:n.baseUrl,body:{account:n.account,password:n.password,device_id:n.deviceId,platform:n.platform}});return S("login",r,n.baseUrl)}async function I(n,r){const i=((await h({method:"GET",path:"/agents/list",baseUrl:n,headers:{Authorization:`Bearer ${r.trim()}`}})).data??{}).list;return Array.isArray(i)?i:[]}function L(n,r){const e=(r??"").trim();if(!e)return null;for(const t of n)if(String(t.agent_name??"").trim()===e&&Number(t.provider_type??0)===3&&Number(t.status??0)!==3)return t;return null}async function B(n,r,e){const t=await h({method:"POST",path:`/agents/${e.trim()}/api/key/rotate`,baseUrl:n,body:{},headers:{Authorization:`Bearer ${r.trim()}`}});return O("rotate-api-agent-key",t)}async function W(n){const r={agent_name:n.agentName.trim(),provider_type:3,is_main:!0},e=(n.avatarUrl??"").trim();e&&(r.avatar_url=e);const t=await h({method:"POST",path:"/agents/create",baseUrl:n.baseUrl,body:r,headers:{Authorization:`Bearer ${n.accessToken.trim()}`}});return O("create-api-agent",t)}async function z(n){if(n.preferExisting){const e=await I(n.baseUrl,n.accessToken),t=L(e,n.agentName);if(t){if(!n.rotateOnReuse)throw new d("existing provider_type=3 agent found but rotate-on-reuse is disabled; cannot obtain api_key safely",0,-1,{existing_agent:t});const i=await B(n.baseUrl,n.accessToken,String(t.id??"").trim());return i.source="reused_existing_agent_with_rotated_key",i.existing_agent=t,i}}const r=await W({baseUrl:n.baseUrl,accessToken:n.accessToken,agentName:n.agentName,avatarUrl:n.avatarUrl});return r.source="created_new_agent",r}function u(n,r){const e=`--${r}=`;for(let t=0;t<n.length;t+=1){const i=n[t]??"";if(i===`--${r}`){const s=n[t+1];return n.splice(t,2),s}if(i.startsWith(e))return n.splice(t,1),i.slice(e.length)}}function U(n,r){const e=`--${r}`,t=n.indexOf(e);return t>=0?(n.splice(t,1),!0):!1}function l(n,r){const e=u(n,r);if(e===void 0||String(e).trim()==="")throw new d(`missing required option: --${r}`);return String(e)}async function q(n,r,e){if(n==="fetch-captcha"){const t=await h({method:"GET",path:"/auth/captcha",baseUrl:r}),i=t.data??{},s=String(i.b64s??""),a=await N(s),o={ok:!0,action:"fetch-captcha",api_base_url:t.api_base_url,captcha_id:String(i.captcha_id??""),b64s:s};a&&(o.captcha_image_path=a),p(o);return}if(n==="send-email-code"){const t=l(e,"email").trim(),i=l(e,"scene").trim();if(!["register","reset","change_password"].includes(i))throw new d(`invalid scene: ${i}`);const s={email:t,scene:i},a=String(u(e,"captcha-id")??"").trim(),o=String(u(e,"captcha-value")??"").trim();if((i==="reset"||i==="change_password")&&(!a||!o))throw new d("captcha-id and captcha-value are required for reset/change_password");a&&(s.captcha_id=a),o&&(s.captcha_value=o);const c=await h({method:"POST",path:"/auth/send-code",baseUrl:r,body:s});p({ok:!0,action:"send-email-code",api_base_url:c.api_base_url,data:c.data});return}if(n==="register"){const t=l(e,"email").trim(),i=l(e,"password").trim(),s=l(e,"email-code").trim(),a=String(u(e,"platform")??"").trim()||"web",o=String(u(e,"device-id")??"").trim()||$(a),c=await h({method:"POST",path:"/auth/register",baseUrl:r,body:{email:t,password:i,email_code:s,device_id:o,platform:a}});p(S("register",c,r));return}if(n==="login"){const t=String(u(e,"email")??u(e,"account")??"").trim();if(!t)throw new d("either --email or --account is required");const i=l(e,"password").trim(),s=String(u(e,"platform")??"").trim()||"web",a=String(u(e,"device-id")??"").trim()||$(s),o=await P({baseUrl:r,account:t,password:i,deviceId:a,platform:s});p(o);return}if(n==="create-api-agent"){const t=l(e,"access-token").trim(),i=l(e,"agent-name").trim(),s=String(u(e,"avatar-url")??"").trim(),a=U(e,"no-reuse-existing-agent"),o=U(e,"no-rotate-key-on-reuse"),c=await z({baseUrl:r,accessToken:t,agentName:i,avatarUrl:s,preferExisting:!a,rotateOnReuse:!o});p(c);return}throw new d(`unsupported action: ${n}`)}async function C(){const n=process.argv.slice(2);if(n.includes("--help")||n.includes("-h"))return process.stdout.write(`${k()}
`),0;const r=String(u(n,"base-url")??f).trim()||f,e=String(n.shift()??"").trim();if(!e)return process.stderr.write(`${k()}
`),1;try{return await q(e,r,n),0}catch(t){if(t instanceof d)return p({ok:!1,action:e,status:t.status,code:t.code,error:t.message,payload:t.payload}),1;const i=t instanceof Error?t.message:String(t);return p({ok:!1,action:e,status:0,code:-1,error:i}),1}}C().then(n=>{process.exitCode=n});