grix-connector 3.10.2 → 3.12.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/dist/adapter/claude/claude-bridge-server.js +1 -1
- package/dist/adapter/claude/claude-tools.js +1 -1
- package/dist/adapter/claude/claude-worker-client.js +1 -1
- package/dist/adapter/claude/mcp-http-launcher.js +2 -2
- package/dist/adapter/claude/result-timeout.js +1 -1
- package/dist/adapter/claude/skill-scanner.js +2 -2
- package/dist/adapter/opencode/opencode-adapter.js +2 -2
- package/dist/adapter/pi/pi-adapter.js +4 -4
- package/dist/bridge/bridge.js +9 -9
- package/dist/bridge/event-queue.js +1 -1
- package/dist/core/access/allowlist-store.js +1 -1
- package/dist/core/file-ops/list-files.js +1 -1
- package/dist/core/provider-quota/index.js +1 -1
- package/dist/core/provider-quota/presentation.js +1 -0
- package/dist/core/provider-quota/providers.js +2 -2
- package/dist/core/provider-quota/service.js +1 -0
- package/dist/default-skills/grix-admin/SKILL.md +278 -25
- package/dist/default-skills/grix-admin/references/api-contract.md +293 -0
- package/dist/default-skills/index.js +1 -1
- package/dist/log.js +2 -2
- package/dist/manager.js +2 -2
- package/dist/mcp/stream-http/config.js +1 -1
- package/dist/mcp/stream-http/connection-binding.js +1 -1
- package/dist/mcp/stream-http/security.js +1 -1
- package/dist/mcp/stream-http/tool-executor.js +1 -1
- package/dist/mcp/stream-http/tool-registry.js +1 -1
- package/dist/mcp/stream-http/tool-schemas.js +1 -1
- package/openclaw-plugin/skills/grix-admin/SKILL.md +110 -24
- package/openclaw-plugin/skills/grix-admin/references/api-contract.md +85 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
const c=25e3,d=
|
|
1
|
+
const c=25e3,d=6e4,l=18e5;class m{config;callbacks;running=new Map;queued=[];timers=new Map;composingTimers=new Map;adapterDoneEventIds=new Set;pauseReasons=new Set;heldEventIds=new Map;get ready(){return this.pauseReasons.size===0}constructor(e,t){this.config=e,this.callbacks=t}pause(e){this.pauseReasons.add(e)}resume(e){this.pauseReasons.delete(e)&&this.ready&&this.drainNext()}submit(e){return this.running.has(e.event_id)||this.queued.some(t=>t.event_id===e.event_id)?"accepted":this.ready&&this.running.size<this.config.maxConcurrent?(this.startRunning(e),"accepted"):this.config.maxQueued<=0||this.queued.length>=this.config.maxQueued?(this.callbacks.onRejected(e,"queue full"),"rejected"):(this.enqueue(e),"accepted")}cancel(e){const t=this.queued.findIndex(i=>i.event_id===e);if(t>=0){if(!this.config.cancelableQueued)return!1;const[i]=this.queued.splice(t,1);return this.clearTimer(e),this.clearHold(e),this.callbacks.onStateChange(e,i.session_id,"canceled",{reason:"canceled by user"}),this.broadcastQueuePositions(),this.drainNext(),this.checkStopComposing(i.session_id),!0}return this.running.has(e)&&this.config.cancelableRunning?(this.callbacks.onCancelRunning(e),!0):!1}removeQueued(e){const t=this.queued.findIndex(u=>u.event_id===e);if(t<0)return!1;const[i]=this.queued.splice(t,1);this.clearTimer(e);const n=this.clearHold(e);return this.broadcastQueuePositions(),n&&this.drainNext(),this.checkStopComposing(i.session_id),!0}complete(e){const i=this.running.get(e)?.session_id;this.running.delete(e),this.adapterDoneEventIds.delete(e),this.clearTimer(e),queueMicrotask(()=>this.drainNext()),i&&this.checkStopComposing(i)}clear(e,t="queue cleared"){const i=[],n=[];let u=!1;for(const s of this.queued)s.session_id===e?(this.clearTimer(s.event_id),this.clearHold(s.event_id)&&(u=!0),this.callbacks.onStateChange(s.event_id,e,"canceled",{reason:t}),i.push(s.event_id)):n.push(s);return this.queued=n,i.length>0&&this.broadcastQueuePositions(),u&&this.drainNext(),this.checkStopComposing(e),i}reorder(e,t){const i=[],n=[];if(this.queued.forEach((o,r)=>{o.session_id===e&&(i.push(r),n.push(o))}),n.length===0)return[];const u=new Map(n.map(o=>[o.event_id,o])),s=[];for(const o of t){const r=u.get(o);r&&(u.delete(o),s.push(r))}for(const o of n)u.has(o.event_id)&&s.push(o);return s.some((o,r)=>o!==n[r])&&(i.forEach((o,r)=>{this.queued[o]=s[r]}),this.drainNext()),s.map(o=>o.event_id)}hold(e,t,i){if(!this.queued.find(r=>r.event_id===e))return"not_found";const s=Number.isFinite(i)&&i>0?Math.min(18e5,Math.max(6e4,Math.floor(i))):0,a=this.heldEventIds.get(e);a?a.timer&&clearTimeout(a.timer):this.clearTimer(e);const o=s>0?setTimeout(()=>{this.expireHold(e)},s):null;return o?.unref(),this.heldEventIds.set(e,{reason:t||"manual",expireAt:s>0?Date.now()+s:0,timer:o}),this.broadcastQueuePositions(),"ok"}release(e){return this.queued.findIndex(i=>i.event_id===e)<0?"not_found":(this.clearHold(e)&&(this.armQueueTimeout(e),this.broadcastQueuePositions(),queueMicrotask(()=>this.drainNext())),"ok")}editQueued(e,t){if(typeof t!="string"||t.trim().length===0)return"empty_content";const i=this.queued.find(n=>n.event_id===e);return i?(i.content=t,this.heldEventIds.has(e)?this.release(e):this.broadcastQueuePositions(),"ok"):"not_found"}expireHold(e){this.heldEventIds.has(e)&&this.release(e)==="not_found"&&this.clearHold(e)}clearHold(e){const t=this.heldEventIds.get(e);return t?(t.timer&&clearTimeout(t.timer),this.heldEventIds.delete(e),!0):!1}armQueueTimeout(e){if(this.config.queueTimeoutMs<=0)return;this.clearTimer(e);const t=setTimeout(()=>{this.timeoutEvent(e)},this.config.queueTimeoutMs);t.unref(),this.timers.set(e,t)}drainQueuedForSession(e){const t=[],i=[];for(const n of this.queued)n.session_id===e?(this.clearTimer(n.event_id),this.clearHold(n.event_id),t.push(n)):i.push(n);return this.queued=i,t}snapshot(e){const t=[...this.running.values()].filter(s=>s.session_id===e),i=t.map(s=>s.event_id),n=t.map(s=>({event_id:s.event_id,content_preview:this.buildQueueItemTitle(s.content),title:this.buildQueueItemTitle(s.content),summary:this.buildQueueItemTitle(s.content)})),u=this.queued.flatMap((s,a)=>{if(s.session_id!==e)return[];const o=this.heldEventIds.get(s.event_id);return[{event_id:s.event_id,position:a+1,content_preview:this.buildQueueItemTitle(s.content),content:s.content,title:this.buildQueueItemTitle(s.content),summary:this.buildQueueItemTitle(s.content),held:!!o,held_reason:o?.reason??""}]});return{running:i,running_items:n,queued:u}}hasCapacity(){return this.running.size<this.config.maxConcurrent}get runningCount(){return this.running.size}get queuedCount(){return this.queued.length}destroy(){for(const e of this.timers.values())clearTimeout(e);this.timers.clear();for(const e of this.heldEventIds.values())e.timer&&clearTimeout(e.timer);this.heldEventIds.clear();for(const e of this.composingTimers.values())clearInterval(e);this.composingTimers.clear(),this.queued=[],this.running.clear(),this.pauseReasons.clear()}enqueue(e){this.queued.push(e);const t=this.queued.length;this.callbacks.onStateChange(e.event_id,e.session_id,"queued",{queue_position:t,queue_total:t,actions:this.config.cancelableQueued?[{type:"cancel"}]:[],content_preview:this.buildQueueItemTitle(e.content),content:e.content,held:!1,held_reason:""}),this.armQueueTimeout(e.event_id),this.ensureComposing(e.session_id)}timeoutEvent(e){const t=this.queued.findIndex(n=>n.event_id===e);if(t<0||this.heldEventIds.has(e))return;const[i]=this.queued.splice(t,1);this.timers.delete(e),this.callbacks.onStateChange(e,i.session_id,"failed",{reason:"queue timeout"}),this.broadcastQueuePositions(),this.drainNext(),this.checkStopComposing(i.session_id)}startRunning(e){this.running.set(e.event_id,e),this.callbacks.onStateChange(e.event_id,e.session_id,"running",{actions:this.config.cancelableRunning?[{type:"stop"}]:[],content_preview:this.buildQueueItemTitle(e.content),content:e.content}),this.ensureComposing(e.session_id),this.callbacks.onDeliver(e)}drainNext(){for(;this.ready&&this.running.size<this.config.maxConcurrent&&this.queued.length>0&&!this.heldEventIds.has(this.queued[0].event_id);){const e=this.queued.shift();this.clearTimer(e.event_id),this.startRunning(e)}this.queued.length>0&&this.broadcastQueuePositions()}broadcastQueuePositions(){const e=this.queued.length;for(let t=0;t<e;t++){const i=this.queued[t],n=this.heldEventIds.get(i.event_id);this.callbacks.onStateChange(i.event_id,i.session_id,"queued",{queue_position:t+1,queue_total:e,actions:this.config.cancelableQueued?[{type:"cancel"}]:[],content_preview:this.buildQueueItemTitle(i.content),content:i.content,held:!!n,held_reason:n?.reason??""})}}clearTimer(e){const t=this.timers.get(e);t&&(clearTimeout(t),this.timers.delete(e))}buildQueueItemTitle(e){const t=String(e??"").replace(/\s+/g," ").trim();return t?t.length>64?`${t.slice(0,64)}...`:t:"Message"}ensureComposing(e){if(!this.callbacks.onComposing||this.composingTimers.has(e))return;this.callbacks.onComposing(e,!0,this.getFirstRunningEventId(e));const t=setInterval(()=>{this.sessionHasEvents(e)?this.callbacks.onComposing(e,!0,this.getFirstRunningEventId(e)):this.stopComposing(e)},25e3);t.unref(),this.composingTimers.set(e,t)}checkStopComposing(e){this.sessionHasEvents(e)||this.stopComposing(e)}markAdapterDone(e){const t=this.running.get(e);t&&(this.adapterDoneEventIds.add(e),this.sessionHasEvents(t.session_id)||this.stopComposing(t.session_id))}stopComposing(e){const t=this.composingTimers.get(e);t&&(clearInterval(t),this.composingTimers.delete(e)),this.callbacks.onComposing?.(e,!1)}sessionHasEvents(e){for(const t of this.running.values())if(t.session_id===e&&!this.adapterDoneEventIds.has(t.event_id))return!0;return this.queued.some(t=>t.session_id===e)}getFirstRunningEventId(e){for(const t of this.running.values())if(t.session_id===e&&!this.adapterDoneEventIds.has(t.event_id))return t.event_id}}export{m as EventQueue};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readJSONFile as o,writeJSONFileAtomic as
|
|
1
|
+
import{readJSONFile as o,writeJSONFileAtomic as i}from"../util/json-file.js";function n(t){const r=o(t);return r&&typeof r=="object"&&"owners"in r&&Array.isArray(r.owners)?r.owners.filter(e=>typeof e=="string"&&e.trim().length>0):[]}async function s(t,r){await i(t,{owners:r})}export{n as readAllowlist,s as writeAllowlist};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const
|
|
1
|
+
import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const i of c){if(!p&&i.name.startsWith("."))continue;const t=l(a,i.name),e={id:t,name:i.name,is_directory:i.isDirectory()};try{if(i.isDirectory()){const o=await m(t);e.modified_at=o.mtime.toISOString()}else{const o=await m(t);e.size=o.size,e.modified_at=o.mtime.toISOString(),e.mime_type=n(i.name)}}catch{}s.push(e)}return s.sort((i,t)=>i.is_directory!==t.is_directory?i.is_directory?-1:1:i.name.localeCompare(t.name)),s}export{f as listFiles,n as resolveMimeType};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{detectProvider as i,queryProviderQuota as
|
|
1
|
+
import{detectProvider as o,detectProviderFromModel as i,normalizeProviderId as t,queryProviderQuota as d,readKimiProviderSettings as v,resolveKimiProviderSettings as a}from"./providers.js";import{ProviderQuotaService as s,sharedProviderQuotaService as P,resolveQuotaBaseUrl as u}from"./service.js";import{providerQuotaToRateLimits as l,providerQuotaToCodexRateLimits as p}from"./presentation.js";export{s as ProviderQuotaService,o as detectProvider,i as detectProviderFromModel,t as normalizeProviderId,p as providerQuotaToCodexRateLimits,l as providerQuotaToRateLimits,d as queryProviderQuota,v as readKimiProviderSettings,a as resolveKimiProviderSettings,u as resolveQuotaBaseUrl,P as sharedProviderQuotaService};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function a(e){if(!e)return 0;const i=Date.parse(e);return Number.isFinite(i)?Math.floor(i/1e3):0}function d(e,i=Date.now()){const n=e.tiers.find(r=>r.name==="five_hour"),t=e.tiers.find(r=>r.name==="weekly_limit");return n||t?{...n?{fiveHour:{usedPercentage:n.usedPercent,resetsAt:a(n.resetsAt)}}:{},...t?{sevenDay:{usedPercentage:t.usedPercent,resetsAt:a(t.resetsAt)}}:{},sampledAt:i}:e.balance?{credit:{remaining:e.balance.remaining,total:e.balance.total,used:e.balance.used,unit:e.balance.unit,resetsAt:a(e.balance.resetsAt??null)},planName:e.planName,sampledAt:i}:null}function l(e,i=Date.now()){const n=e.tiers.find(s=>s.name==="five_hour"),t=e.tiers.find(s=>s.name==="weekly_limit");if(n||t){const s={credits:{hasCredits:!0,unlimited:!1,balance:null},sampledAt:i};return n&&(s.primary={usedPercent:n.usedPercent,windowMinutes:300,resetsAt:n.resetsAt}),t&&(s.secondary={usedPercent:t.usedPercent,windowMinutes:10080,resetsAt:t.resetsAt}),{rateLimits:s,primaryPercent:n?.usedPercent??0,secondaryPercent:t?.usedPercent??0,primaryWindowMin:n?300:0,secondaryWindowMin:t?10080:0}}if(!e.balance)return null;const r=e.balance,c=r.total&&r.total>0?Math.min(100,(r.used??r.total-r.remaining)/r.total*100):0;return{rateLimits:{primary:{usedPercent:c,windowMinutes:0,resetsAt:null},secondary:{usedPercent:0,windowMinutes:0,resetsAt:null},credits:{hasCredits:!0,unlimited:!1,balance:r.remaining},sampledAt:i},primaryPercent:c,secondaryPercent:0,primaryWindowMin:0,secondaryWindowMin:0}}export{l as providerQuotaToCodexRateLimits,d as providerQuotaToRateLimits};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{readFileSync as y,writeFileSync as
|
|
2
|
-
[`,s+1),u=(c>=0?n.slice(s,c):n.slice(s)).match(/^\s*base_url\s*=\s*"([^"]+)"/m);u&&(t=u[1].trim())}}catch{}let a;try{const r=_(e,"credentials","kimi-code.json"),n=JSON.parse(y(r,"utf8")),s=typeof n.access_token=="string"?n.access_token.trim():"",c=typeof n.expires_at=="number"?n.expires_at:null;s&&(!c||Date.now()<c*1e3-i)&&(a=s)}catch{}return{baseUrl:t,apiKey:a}}const q="17e5f671-d194-4dfb-9706-5516cb48c098";function Y(){return(process.env.KIMI_CODE_OAUTH_HOST||process.env.KIMI_OAUTH_HOST||"https://auth.kimi.com").replace(/\/+$/,"")}const Z=30*1e3;async function re(o){const e=$(o);if(e.apiKey)return e;const i=await K(o);return{baseUrl:e.baseUrl,apiKey:i}}async function K(o){const e=o||I(),i=_(e,"credentials","kimi-code.json");let t;try{t=JSON.parse(y(i,"utf8"))}catch{return}const a=typeof t.refresh_token=="string"?t.refresh_token.trim():"";if(!a)return;const r=_(e,"credentials","kimi-code.lock.lock");if(V(r))try{const n=$(o);if(n.apiKey)return n.apiKey;let s="";try{s=y(_(e,"device_id"),"utf8").trim()}catch{}const c=await fetch(`${Y()}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",...s?{"X-Msh-Device-Id":s}:{}},body:new URLSearchParams({client_id:q,grant_type:"refresh_token",refresh_token:a}),signal:AbortSignal.timeout(8e3)}),l=await c.json().catch(()=>({})),u=typeof l.access_token=="string"?l.access_token:"";if(c.status!==200||!u)return $(o).apiKey;const p=Number(l.expires_in),h={access_token:u,refresh_token:typeof l.refresh_token=="string"&&l.refresh_token?l.refresh_token:a,expires_at:Math.floor(Date.now()/1e3)+(Number.isFinite(p)&&p>0?p:900),scope:typeof l.scope=="string"&&l.scope?l.scope:t.scope??"kimi-code",token_type:typeof l.token_type=="string"&&l.token_type?l.token_type:"Bearer",expires_in:Number.isFinite(p)&&p>0?p:900};return D(i,JSON.stringify(h),{mode:384}),u}catch{return}finally{try{x(r)}catch{}}}function V(o){try{return N(o),!0}catch{}try{const e=G(o);return Date.now()-e.mtimeMs<Z?!1:(x(o),N(o),!0)}catch{return!1}}async function E(o){const e="zhipu",i="Zhipu GLM";try{const t=await fetch("https://api.z.ai/api/monitor/usage/quota/limit",{method:"GET",headers:{Authorization:o,"Content-Type":"application/json","Accept-Language":"en-US,en"},signal:AbortSignal.timeout(f)});if(t.status===401||t.status===403)return d(e,i,`Authentication failed (HTTP ${t.status})`);if(!t.ok){const c=await t.text().catch(()=>"");return d(e,i,`API error (HTTP ${t.status}): ${c.slice(0,200)}`)}const a=await t.json();if(a.success===!1)return d(e,i,`API error: ${a.msg??"Unknown error"}`);const r=a.data;if(!r)return d(e,i,"Missing data field");const n=typeof r.level=="string"?r.level:null,s=M(r);return{provider:e,providerLabel:i,planName:n,tiers:s,balance:null,success:!0,error:null}}catch(t){return d(e,i,`Network error: ${t instanceof Error?t.message:String(t)}`)}}function M(o){const e=Array.isArray(o.limits)?o.limits:[],i=[];for(const a of e){if(String(a.type??"").toUpperCase()!=="TOKENS_LIMIT")continue;const n=m(a.percentage)??0,s=typeof a.nextResetTime=="number"?a.nextResetTime:Number.MAX_SAFE_INTEGER,c=s===Number.MAX_SAFE_INTEGER?null:v(s);i.push({percentage:n,resetMs:s,resetIso:c})}i.sort((a,r)=>a.resetMs-r.resetMs);const t=[];if(i.length>0){const a=i[0];t.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(a.percentage*100)/100,resetsAt:a.resetIso})}return t}async function L(o){const e="kimi",i="Kimi";try{const t=await fetch("https://api.kimi.com/coding/v1/usages",{method:"GET",headers:{Authorization:`Bearer ${o}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(t.status===401||t.status===403)return d(e,i,`Authentication failed (HTTP ${t.status})`);if(!t.ok){const c=await t.text().catch(()=>"");return d(e,i,`API error (HTTP ${t.status}): ${c.slice(0,200)}`)}const a=await t.json(),r=[],n=Array.isArray(a.limits)?a.limits:[];for(const c of n){const l=c.detail;if(!l)continue;const u=m(l.limit)??1,p=m(l.remaining)??0,h=S(l.resetTime),b=Math.max(0,u-p);r.push({name:"five_hour",label:"5h limit",usedPercent:u>0?Math.round(b/u*1e4)/100:0,resetsAt:h})}const s=a.usage;if(s){const c=m(s.limit)??1,l=m(s.remaining)??0,u=S(s.resetTime),p=Math.max(0,c-l);r.push({name:"weekly_limit",label:"Weekly limit",usedPercent:c>0?Math.round(p/c*1e4)/100:0,resetsAt:u})}return{provider:e,providerLabel:i,planName:null,tiers:r,balance:null,success:!0,error:null}}catch(t){return d(e,i,`Network error: ${t instanceof Error?t.message:String(t)}`)}}async function w(o,e){const i=e?"minimax_cn":"minimax_en",t="MiniMax",a=e?"api.minimaxi.com":"api.minimax.io";try{const r=await fetch(`https://${a}/v1/api/openplatform/coding_plan/remains`,{method:"GET",headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json"},signal:AbortSignal.timeout(f)});if(r.status===401||r.status===403)return d(i,t,`Authentication failed (HTTP ${r.status})`);if(!r.ok){const p=await r.text().catch(()=>"");return d(i,t,`API error (HTTP ${r.status}): ${p.slice(0,200)}`)}const n=await r.json(),s=n.base_resp;if(s&&typeof s.status_code=="number"&&s.status_code!==0)return d(i,t,`API error (code ${s.status_code}): ${s.status_msg??"Unknown"}`);const c=[],u=(Array.isArray(n.model_remains)?n.model_remains:[])[0];if(u){const p=m(u.current_interval_total_count)??0,h=m(u.current_interval_usage_count)??0,b=typeof u.end_time=="number"?u.end_time:null;p>0&&c.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(h/p*1e4)/100,resetsAt:b!==null?v(b):null});const g=m(u.current_weekly_total_count)??0,C=m(u.current_weekly_usage_count)??0,P=typeof u.weekly_end_time=="number"?u.weekly_end_time:null;g>0&&c.push({name:"weekly_limit",label:"Weekly limit",usedPercent:Math.round(C/g*1e4)/100,resetsAt:P!==null?v(P):null})}return{provider:i,providerLabel:t,planName:null,tiers:c,balance:null,success:!0,error:null}}catch(r){return d(i,t,`Network error: ${r instanceof Error?r.message:String(r)}`)}}async function j(o){const e="deepseek",i="DeepSeek";try{const t=await fetch("https://api.deepseek.com/user/balance",{method:"GET",headers:{Authorization:`Bearer ${o}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(t.status===401||t.status===403)return d(e,i,`Authentication failed (HTTP ${t.status})`);if(!t.ok){const u=await t.text().catch(()=>"");return d(e,i,`API error (HTTP ${t.status}): ${u.slice(0,200)}`)}const a=await t.json(),r=a.is_available===!0,s=(Array.isArray(a.balance_infos)?a.balance_infos:[])[0];if(!s)return d(e,i,"No balance info returned");const c=String(s.currency??"CNY"),l=m(s.total_balance);return{provider:e,providerLabel:i,planName:null,tiers:[],balance:{remaining:l??0,total:null,used:null,unit:c},success:!0,error:r?null:"Insufficient balance"}}catch(t){return d(e,i,`Network error: ${t instanceof Error?t.message:String(t)}`)}}async function O(o){const e="stepfun",i="StepFun";try{const t=await fetch("https://api.stepfun.com/v1/accounts",{method:"GET",headers:{Authorization:`Bearer ${o}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(t.status===401||t.status===403)return d(e,i,`Authentication failed (HTTP ${t.status})`);if(!t.ok){const n=await t.text().catch(()=>"");return d(e,i,`API error (HTTP ${t.status}): ${n.slice(0,200)}`)}const a=await t.json(),r=m(a.balance)??0;return{provider:e,providerLabel:i,planName:null,tiers:[],balance:{remaining:r,total:null,used:null,unit:"CNY"},success:!0,error:null}}catch(t){return d(e,i,`Network error: ${t instanceof Error?t.message:String(t)}`)}}async function k(o,e){const i=e?"siliconflow_cn":"siliconflow_en",t=e?"SiliconFlow":"SiliconFlow (EN)",a=e?"api.siliconflow.cn":"api.siliconflow.com",r=e?"CNY":"USD";try{const n=await fetch(`https://${a}/v1/user/info`,{method:"GET",headers:{Authorization:`Bearer ${o}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(n.status===401||n.status===403)return d(i,t,`Authentication failed (HTTP ${n.status})`);if(!n.ok){const u=await n.text().catch(()=>"");return d(i,t,`API error (HTTP ${n.status}): ${u.slice(0,200)}`)}const c=(await n.json()).data;if(!c)return d(i,t,"Missing data field");const l=m(c.totalBalance)??0;return{provider:i,providerLabel:t,planName:null,tiers:[],balance:{remaining:l,total:null,used:null,unit:r},success:!0,error:null}}catch(n){return d(i,t,`Network error: ${n instanceof Error?n.message:String(n)}`)}}async function H(o){const e="openrouter",i="OpenRouter";try{const t=await fetch("https://openrouter.ai/api/v1/credits",{method:"GET",headers:{Authorization:`Bearer ${o}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(t.status===401||t.status===403)return d(e,i,`Authentication failed (HTTP ${t.status})`);if(!t.ok){const l=await t.text().catch(()=>"");return d(e,i,`API error (HTTP ${t.status}): ${l.slice(0,200)}`)}const a=await t.json(),r=a.data??a,n=m(r.total_credits)??0,s=m(r.total_usage)??0,c=n-s;return{provider:e,providerLabel:i,planName:null,tiers:[],balance:{remaining:c,total:n,used:s,unit:"USD"},success:!0,error:null}}catch(t){return d(e,i,`Network error: ${t instanceof Error?t.message:String(t)}`)}}async function R(o){const e="novita",i="Novita AI";try{const t=await fetch("https://api.novita.ai/v3/user/balance",{method:"GET",headers:{Authorization:`Bearer ${o}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(t.status===401||t.status===403)return d(e,i,`Authentication failed (HTTP ${t.status})`);if(!t.ok){const n=await t.text().catch(()=>"");return d(e,i,`API error (HTTP ${t.status}): ${n.slice(0,200)}`)}const a=await t.json(),r=(m(a.availableBalance)??0)/1e4;return{provider:e,providerLabel:i,planName:null,tiers:[],balance:{remaining:r,total:null,used:null,unit:"USD"},success:!0,error:null}}catch(t){return d(e,i,`Network error: ${t instanceof Error?t.message:String(t)}`)}}const A=new Map,X=300*1e3;async function J(o,e){const i=`${e.slice(0,8)}@${o}`,t=A.get(i);if(t&&Date.now()-t.timestamp<=X){const n=await B(t.providerId,o,e);if(n)return n;A.delete(i)}else t&&A.delete(i);const a=["zhipu","deepseek","kimi","openrouter","stepfun","minimax_cn","siliconflow_cn","novita"],r=await Promise.allSettled(a.map(n=>B(n,o,e)));for(let n=0;n<r.length;n++){const s=r[n];if(s.status==="fulfilled"&&s.value?.success)return A.set(i,{providerId:a[n],timestamp:Date.now()}),s.value}return null}const T=new Map,W=300*1e3;async function Q(o){const e=o.slice(0,8),i=T.get(e);if(i&&Date.now()-i.timestamp<=W){const r=[i.providerId],n=await Promise.allSettled(r.map(s=>U(s,o)));for(const s of n)if(s.status==="fulfilled"&&s.value?.success)return s.value;T.delete(e)}else i&&T.delete(e);const t=["zhipu","deepseek","kimi","openrouter","stepfun","minimax_cn","siliconflow_cn","novita"],a=await Promise.allSettled(t.map(r=>U(r,o)));for(let r=0;r<a.length;r++){const n=a[r];if(n.status==="fulfilled"&&n.value?.success)return T.set(e,{providerId:t[r],timestamp:Date.now()}),n.value}return null}async function U(o,e){try{switch(o){case"zhipu":return E(e);case"kimi":return L(e);case"minimax_cn":return w(e,!0);case"minimax_en":return w(e,!1);case"deepseek":return j(e);case"stepfun":return O(e);case"siliconflow_cn":return k(e,!0);case"siliconflow_en":return k(e,!1);case"openrouter":return H(e);case"novita":return R(e)}}catch{return null}}async function B(o,e,i){const t=e.replace(/\/+$/,""),a={Authorization:"Bearer ${apiKey}",Accept:"application/json"};try{switch(o){case"zhipu":{const r=await fetch("${origin}/api/monitor/usage/quota/limit",{method:"GET",headers:{...a,Authorization:i,"Content-Type":"application/json","Accept-Language":"en-US,en"},signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json();if(n.success===!1)return null;const s=n.data;if(!s)return null;const c=typeof s.level=="string"?s.level:null,l=M(s);return{provider:"zhipu",providerLabel:"Zhipu GLM",planName:c,tiers:l,balance:null,success:!0,error:null}}case"deepseek":{const r=await fetch("${origin}/user/balance",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json(),s=n.is_available===!0,l=(Array.isArray(n.balance_infos)?n.balance_infos:[])[0];if(!l)return null;const u=String(l.currency??"CNY"),p=m(l.total_balance);return{provider:"deepseek",providerLabel:"DeepSeek",planName:null,tiers:[],balance:{remaining:p??0,total:null,used:null,unit:u},success:!0,error:s?null:"Insufficient balance"}}case"kimi":{const r=await fetch("${origin}/coding/v1/usages",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json(),s=[],c=Array.isArray(n.limits)?n.limits:[];for(const l of c){const u=l.detail;if(!u)continue;const p=m(u.limit)??1,h=m(u.remaining)??0,b=S(u.resetTime),g=Math.max(0,p-h);s.push({name:"five_hour",label:"5h limit",usedPercent:p>0?Math.round(g/p*1e4)/100:0,resetsAt:b})}return{provider:"kimi",providerLabel:"Kimi",planName:null,tiers:s,balance:null,success:!0,error:null}}case"openrouter":{const r=await fetch("${origin}/api/v1/credits",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json(),s=n.data??n,c=m(s.total_credits)??0,l=m(s.total_usage)??0,u=c-l;return{provider:"openrouter",providerLabel:"OpenRouter",planName:null,tiers:[],balance:{remaining:u,total:c,used:l,unit:"USD"},success:!0,error:null}}case"stepfun":{const r=await fetch("${origin}/v1/accounts",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json(),s=m(n.balance)??0;return{provider:"stepfun",providerLabel:"StepFun",planName:null,tiers:[],balance:{remaining:s,total:null,used:null,unit:"CNY"},success:!0,error:null}}case"minimax_cn":{const r=await fetch("${origin}/v1/api/openplatform/coding_plan/remains",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json(),s=n.base_resp;if(s&&typeof s.status_code=="number"&&s.status_code!==0)return null;const l=(Array.isArray(n.model_remains)?n.model_remains:[])[0];if(!l)return null;const u=[],p=m(l.current_interval_total_count)??0,h=m(l.current_interval_usage_count)??0;return p>0&&u.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(h/p*1e4)/100,resetsAt:null}),{provider:"minimax_cn",providerLabel:"MiniMax",planName:null,tiers:u,balance:null,success:!0,error:null}}case"siliconflow_cn":{const r=await fetch("${origin}/v1/user/info",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const s=(await r.json()).data;if(!s)return null;const c=m(s.totalBalance)??0;return{provider:"siliconflow_cn",providerLabel:"SiliconFlow",planName:null,tiers:[],balance:{remaining:c,total:null,used:null,unit:"CNY"},success:!0,error:null}}case"novita":{const r=await fetch("${origin}/v3/user/balance",{method:"GET",headers:a,signal:AbortSignal.timeout(f)});if(!r.ok)return null;const n=await r.json(),s=(m(n.availableBalance)??0)/1e4;return{provider:"novita",providerLabel:"Novita AI",planName:null,tiers:[],balance:{remaining:s,total:null,used:null,unit:"USD"},success:!0,error:null}}default:return null}}catch{return null}}async function ie(o,e){if(!e.trim())return{provider:"unknown",providerLabel:"Unknown",planName:null,tiers:[],balance:null,success:!1,error:"API key is empty"};const i=F(o);if(i)switch(i.id){case"zhipu":return E(e);case"kimi":return L(e);case"minimax_cn":return w(e,!0);case"minimax_en":return w(e,!1);case"deepseek":return j(e);case"stepfun":return O(e);case"siliconflow_cn":return k(e,!0);case"siliconflow_en":return k(e,!1);case"openrouter":return H(e);case"novita":return R(e)}const t=await J(o,e);if(t)return t;const a=await Q(e);return a||{provider:"unknown",providerLabel:"Unknown",planName:null,tiers:[],balance:null,success:!1,error:`Could not identify provider for base URL: ${o}`}}export{F as detectProvider,ie as queryProviderQuota,$ as readKimiProviderSettings,re as resolveKimiProviderSettings};
|
|
1
|
+
import{createHash as L}from"node:crypto";import{readFileSync as y,writeFileSync as j,mkdirSync as P,rmdirSync as x,statSync as z}from"node:fs";import{homedir as O}from"node:os";import{join as _}from"node:path";const R=new Set(["zhipu","kimi","minimax_cn","minimax_en","deepseek","stepfun","siliconflow_cn","siliconflow_en","openrouter","novita"]),H={zai:"zhipu","z.ai":"zhipu",glm:"zhipu",bigmodel:"zhipu",moonshot:"kimi",minimax:"minimax_cn",siliconflow:"siliconflow_cn"};function U(s){const t=s?.trim().toLowerCase();if(!t)return null;const r=H[t]??t;return R.has(r)?r:null}function B(s){const t=s.toLowerCase();return t.includes("open.bigmodel.cn")||t.includes("bigmodel.cn")||t.includes("api.z.ai")?{id:"zhipu",label:"Zhipu GLM"}:t.includes("api.kimi.com")?{id:"kimi",label:"Kimi"}:t.includes("api.minimaxi.com")?{id:"minimax_cn",label:"MiniMax"}:t.includes("api.minimax.io")?{id:"minimax_en",label:"MiniMax"}:t.includes("api.deepseek.com")?{id:"deepseek",label:"DeepSeek"}:t.includes("api.stepfun.ai")||t.includes("api.stepfun.com")?{id:"stepfun",label:"StepFun"}:t.includes("api.siliconflow.cn")?{id:"siliconflow_cn",label:"SiliconFlow"}:t.includes("api.siliconflow.com")?{id:"siliconflow_en",label:"SiliconFlow"}:t.includes("openrouter.ai")?{id:"openrouter",label:"OpenRouter"}:t.includes("api.novita.ai")?{id:"novita",label:"Novita AI"}:null}function ie(s){const t=s?.trim().toLowerCase()??"";return t?/(^|[\/:_-])(glm|zhipu|zai)([\/:_.-]|$)/.test(t)?{id:"zhipu",label:"Zhipu GLM"}:/(^|[\/:_-])(kimi|moonshot)([\/:_.-]|$)/.test(t)?{id:"kimi",label:"Kimi"}:/(^|[\/:_-])deepseek([\/:_.-]|$)/.test(t)?{id:"deepseek",label:"DeepSeek"}:null:null}const f=1e4;function d(s,t,r){return{provider:s,providerLabel:t,planName:null,tiers:[],balance:null,success:!1,error:r}}function m(s){if(typeof s=="number")return s;if(typeof s=="string"){const t=Number(s);return Number.isFinite(t)?t:null}return null}function w(s){if(!Number.isFinite(s)||s<=0)return null;try{return new Date(s).toISOString()}catch{return null}}function v(s){if(typeof s=="string")return s;if(typeof s=="number"){const t=s<1e12?s*1e3:s;return w(t)}return null}function N(){return(process.env.KIMI_CODE_HOME||"").trim()||_(O(),".kimi-code")}function T(s){const t=s||N(),r=300*1e3;let e;try{const a=_(t,"config.toml"),n=y(a,"utf8"),i=n.indexOf('[providers."managed:kimi-code"]');if(i>=0){const c=n.indexOf(`
|
|
2
|
+
[`,i+1),u=(c>=0?n.slice(i,c):n.slice(i)).match(/^\s*base_url\s*=\s*"([^"]+)"/m);u&&(e=u[1].trim())}}catch{}let o;try{const a=_(t,"credentials","kimi-code.json"),n=JSON.parse(y(a,"utf8")),i=typeof n.access_token=="string"?n.access_token.trim():"",c=typeof n.expires_at=="number"?n.expires_at:null;i&&(!c||Date.now()<c*1e3-r)&&(o=i)}catch{}return{baseUrl:e,apiKey:o}}const D="17e5f671-d194-4dfb-9706-5516cb48c098";function F(){return(process.env.KIMI_CODE_OAUTH_HOST||process.env.KIMI_OAUTH_HOST||"https://auth.kimi.com").replace(/\/+$/,"")}const G=30*1e3;async function se(s){const t=T(s);if(t.apiKey)return t;const r=await C(s);return{baseUrl:t.baseUrl,apiKey:r}}async function C(s){const t=s||N(),r=_(t,"credentials","kimi-code.json");let e;try{e=JSON.parse(y(r,"utf8"))}catch{return}const o=typeof e.refresh_token=="string"?e.refresh_token.trim():"";if(!o)return;const a=_(t,"credentials","kimi-code.lock.lock");if(q(a))try{const n=T(s);if(n.apiKey)return n.apiKey;let i="";try{i=y(_(t,"device_id"),"utf8").trim()}catch{}const c=await fetch(`${F()}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",...i?{"X-Msh-Device-Id":i}:{}},body:new URLSearchParams({client_id:D,grant_type:"refresh_token",refresh_token:o}),signal:AbortSignal.timeout(8e3)}),l=await c.json().catch(()=>({})),u=typeof l.access_token=="string"?l.access_token:"";if(c.status!==200||!u)return T(s).apiKey;const p=Number(l.expires_in),h={access_token:u,refresh_token:typeof l.refresh_token=="string"&&l.refresh_token?l.refresh_token:o,expires_at:Math.floor(Date.now()/1e3)+(Number.isFinite(p)&&p>0?p:900),scope:typeof l.scope=="string"&&l.scope?l.scope:e.scope??"kimi-code",token_type:typeof l.token_type=="string"&&l.token_type?l.token_type:"Bearer",expires_in:Number.isFinite(p)&&p>0?p:900};return j(r,JSON.stringify(h),{mode:384}),u}catch{return}finally{try{x(a)}catch{}}}function q(s){try{return P(s),!0}catch{}try{const t=z(s);return Date.now()-t.mtimeMs<G?!1:(x(s),P(s),!0)}catch{return!1}}async function K(s){const t="zhipu",r="Zhipu GLM";try{const e=await fetch("https://api.z.ai/api/monitor/usage/quota/limit",{method:"GET",headers:{Authorization:s,"Content-Type":"application/json","Accept-Language":"en-US,en"},signal:AbortSignal.timeout(f)});if(e.status===401||e.status===403)return d(t,r,`Authentication failed (HTTP ${e.status})`);if(!e.ok){const c=await e.text().catch(()=>"");return d(t,r,`API error (HTTP ${e.status}): ${c.slice(0,200)}`)}const o=await e.json();if(o.success===!1)return d(t,r,`API error: ${o.msg??"Unknown error"}`);const a=o.data;if(!a)return d(t,r,"Missing data field");const n=typeof a.level=="string"?a.level:null,i=M(a);return{provider:t,providerLabel:r,planName:n,tiers:i,balance:null,success:!0,error:null}}catch(e){return d(t,r,`Network error: ${e instanceof Error?e.message:String(e)}`)}}function M(s){const t=Array.isArray(s.limits)?s.limits:[],r=[];for(const o of t){if(String(o.type??"").toUpperCase()!=="TOKENS_LIMIT")continue;const n=m(o.percentage)??0,i=m(o.nextResetTime)??Number.MAX_SAFE_INTEGER,c=i===Number.MAX_SAFE_INTEGER?null:w(i);r.push({percentage:n,resetMs:i,resetIso:c})}r.sort((o,a)=>o.resetMs-a.resetMs);const e=[];if(r.length>0){const o=r[0];e.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(o.percentage*100)/100,resetsAt:o.resetIso})}if(r.length>1){const o=r[r.length-1];o.resetMs!==r[0].resetMs&&e.push({name:"weekly_limit",label:"Weekly limit",usedPercent:Math.round(o.percentage*100)/100,resetsAt:o.resetIso})}return e}async function Y(s){const t="kimi",r="Kimi";try{const e=await fetch("https://api.kimi.com/coding/v1/usages",{method:"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(e.status===401||e.status===403)return d(t,r,`Authentication failed (HTTP ${e.status})`);if(!e.ok){const c=await e.text().catch(()=>"");return d(t,r,`API error (HTTP ${e.status}): ${c.slice(0,200)}`)}const o=await e.json();if(!Array.isArray(o.limits)&&!o.usage)return d(t,r,"Unexpected quota response schema");const a=[],n=Array.isArray(o.limits)?o.limits:[];for(const c of n){const l=c.detail;if(!l)continue;const u=m(l.limit)??1,p=m(l.remaining)??0,h=v(l.resetTime),b=Math.max(0,u-p);a.push({name:"five_hour",label:"5h limit",usedPercent:u>0?Math.round(b/u*1e4)/100:0,resetsAt:h})}const i=o.usage;if(i){const c=m(i.limit)??1,l=m(i.remaining)??0,u=v(i.resetTime),p=Math.max(0,c-l);a.push({name:"weekly_limit",label:"Weekly limit",usedPercent:c>0?Math.round(p/c*1e4)/100:0,resetsAt:u})}return{provider:t,providerLabel:r,planName:null,tiers:a,balance:null,success:!0,error:null}}catch(e){return d(t,r,`Network error: ${e instanceof Error?e.message:String(e)}`)}}async function I(s,t){const r=t?"minimax_cn":"minimax_en",e="MiniMax",o=t?"api.minimaxi.com":"api.minimax.io";try{const a=await fetch(`https://${o}/v1/api/openplatform/coding_plan/remains`,{method:"GET",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},signal:AbortSignal.timeout(f)});if(a.status===401||a.status===403)return d(r,e,`Authentication failed (HTTP ${a.status})`);if(!a.ok){const p=await a.text().catch(()=>"");return d(r,e,`API error (HTTP ${a.status}): ${p.slice(0,200)}`)}const n=await a.json(),i=n.base_resp;if(i&&typeof i.status_code=="number"&&i.status_code!==0)return d(r,e,`API error (code ${i.status_code}): ${i.status_msg??"Unknown"}`);const c=[],u=(Array.isArray(n.model_remains)?n.model_remains:[])[0];if(u){const p=m(u.current_interval_total_count)??0,h=m(u.current_interval_usage_count)??0,b=typeof u.end_time=="number"?u.end_time:null;p>0&&c.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(h/p*1e4)/100,resetsAt:b!==null?w(b):null});const g=m(u.current_weekly_total_count)??0,A=m(u.current_weekly_usage_count)??0,$=typeof u.weekly_end_time=="number"?u.weekly_end_time:null;g>0&&c.push({name:"weekly_limit",label:"Weekly limit",usedPercent:Math.round(A/g*1e4)/100,resetsAt:$!==null?w($):null})}return{provider:r,providerLabel:e,planName:null,tiers:c,balance:null,success:!0,error:null}}catch(a){return d(r,e,`Network error: ${a instanceof Error?a.message:String(a)}`)}}async function Z(s){const t="deepseek",r="DeepSeek";try{const e=await fetch("https://api.deepseek.com/user/balance",{method:"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(e.status===401||e.status===403)return d(t,r,`Authentication failed (HTTP ${e.status})`);if(!e.ok){const u=await e.text().catch(()=>"");return d(t,r,`API error (HTTP ${e.status}): ${u.slice(0,200)}`)}const o=await e.json(),a=o.is_available===!0,i=(Array.isArray(o.balance_infos)?o.balance_infos:[])[0];if(!i)return d(t,r,"No balance info returned");const c=String(i.currency??"CNY"),l=m(i.total_balance);return{provider:t,providerLabel:r,planName:null,tiers:[],balance:{remaining:l??0,total:null,used:null,unit:c},success:!0,error:a?null:"Insufficient balance"}}catch(e){return d(t,r,`Network error: ${e instanceof Error?e.message:String(e)}`)}}async function V(s){const t="stepfun",r="StepFun";try{const e=await fetch("https://api.stepfun.com/v1/accounts",{method:"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(e.status===401||e.status===403)return d(t,r,`Authentication failed (HTTP ${e.status})`);if(!e.ok){const n=await e.text().catch(()=>"");return d(t,r,`API error (HTTP ${e.status}): ${n.slice(0,200)}`)}const o=await e.json(),a=m(o.balance)??0;return{provider:t,providerLabel:r,planName:null,tiers:[],balance:{remaining:a,total:null,used:null,unit:"CNY"},success:!0,error:null}}catch(e){return d(t,r,`Network error: ${e instanceof Error?e.message:String(e)}`)}}async function E(s,t){const r=t?"siliconflow_cn":"siliconflow_en",e=t?"SiliconFlow":"SiliconFlow (EN)",o=t?"api.siliconflow.cn":"api.siliconflow.com",a=t?"CNY":"USD";try{const n=await fetch(`https://${o}/v1/user/info`,{method:"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(n.status===401||n.status===403)return d(r,e,`Authentication failed (HTTP ${n.status})`);if(!n.ok){const u=await n.text().catch(()=>"");return d(r,e,`API error (HTTP ${n.status}): ${u.slice(0,200)}`)}const c=(await n.json()).data;if(!c)return d(r,e,"Missing data field");const l=m(c.totalBalance)??0;return{provider:r,providerLabel:e,planName:null,tiers:[],balance:{remaining:l,total:null,used:null,unit:a},success:!0,error:null}}catch(n){return d(r,e,`Network error: ${n instanceof Error?n.message:String(n)}`)}}async function W(s){const t="openrouter",r="OpenRouter";try{const e=await fetch("https://openrouter.ai/api/v1/credits",{method:"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(e.status===401||e.status===403)return d(t,r,`Authentication failed (HTTP ${e.status})`);if(!e.ok){const l=await e.text().catch(()=>"");return d(t,r,`API error (HTTP ${e.status}): ${l.slice(0,200)}`)}const o=await e.json(),a=o.data??o,n=m(a.total_credits)??0,i=m(a.total_usage)??0,c=n-i;return{provider:t,providerLabel:r,planName:null,tiers:[],balance:{remaining:c,total:n,used:i,unit:"USD"},success:!0,error:null}}catch(e){return d(t,r,`Network error: ${e instanceof Error?e.message:String(e)}`)}}async function X(s){const t="novita",r="Novita AI";try{const e=await fetch("https://api.novita.ai/v3/user/balance",{method:"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json"},signal:AbortSignal.timeout(f)});if(e.status===401||e.status===403)return d(t,r,`Authentication failed (HTTP ${e.status})`);if(!e.ok){const n=await e.text().catch(()=>"");return d(t,r,`API error (HTTP ${e.status}): ${n.slice(0,200)}`)}const o=await e.json(),a=(m(o.availableBalance)??0)/1e4;return{provider:t,providerLabel:r,planName:null,tiers:[],balance:{remaining:a,total:null,used:null,unit:"USD"},success:!0,error:null}}catch(e){return d(t,r,`Network error: ${e instanceof Error?e.message:String(e)}`)}}const k=new Map,J=300*1e3;async function Q(s,t){const r=L("sha256").update(t).digest("hex").slice(0,16),e=`${s.trim().replace(/\/+$/,"").toLowerCase()}|${r}`,o=k.get(e);if(o&&Date.now()-o.timestamp<=J){const i=await S(o.providerId,s,t);if(i)return i;k.delete(e)}else o&&k.delete(e);const a=["zhipu","deepseek","kimi","openrouter","stepfun","minimax_cn","siliconflow_cn","novita"],n=await Promise.allSettled(a.map(i=>S(i,s,t)));for(let i=0;i<n.length;i++){const c=n[i];if(c.status==="fulfilled"&&c.value?.success)return k.set(e,{providerId:a[i],timestamp:Date.now()}),c.value}return null}async function S(s,t,r){const e=t.replace(/\/+$/,""),o={Authorization:`Bearer ${r}`,Accept:"application/json"};try{switch(s){case"zhipu":{const a=await fetch(`${e}/api/monitor/usage/quota/limit`,{method:"GET",headers:{...o,Authorization:r,"Content-Type":"application/json","Accept-Language":"en-US,en"},signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json();if(n.success===!1)return null;const i=n.data;if(!i)return null;const c=typeof i.level=="string"?i.level:null,l=M(i);return{provider:"zhipu",providerLabel:"Zhipu GLM",planName:c,tiers:l,balance:null,success:!0,error:null}}case"deepseek":{const a=await fetch(`${e}/user/balance`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json(),i=n.is_available===!0,l=(Array.isArray(n.balance_infos)?n.balance_infos:[])[0];if(!l)return null;const u=String(l.currency??"CNY"),p=m(l.total_balance);return{provider:"deepseek",providerLabel:"DeepSeek",planName:null,tiers:[],balance:{remaining:p??0,total:null,used:null,unit:u},success:!0,error:i?null:"Insufficient balance"}}case"kimi":{const a=await fetch(`${e}/coding/v1/usages`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json();if(!Array.isArray(n.limits)&&!n.usage)return null;const i=[],c=Array.isArray(n.limits)?n.limits:[];for(const u of c){const p=u.detail;if(!p)continue;const h=m(p.limit)??1,b=m(p.remaining)??0,g=v(p.resetTime),A=Math.max(0,h-b);i.push({name:"five_hour",label:"5h limit",usedPercent:h>0?Math.round(A/h*1e4)/100:0,resetsAt:g})}const l=n.usage;if(l){const u=m(l.limit)??1,p=m(l.remaining)??0,h=v(l.resetTime),b=Math.max(0,u-p);i.push({name:"weekly_limit",label:"Weekly limit",usedPercent:u>0?Math.round(b/u*1e4)/100:0,resetsAt:h})}return{provider:"kimi",providerLabel:"Kimi",planName:null,tiers:i,balance:null,success:!0,error:null}}case"openrouter":{const a=await fetch(`${e}/api/v1/credits`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json(),i=n.data??n;if(i.total_credits===void 0&&i.total_usage===void 0)return null;const c=m(i.total_credits)??0,l=m(i.total_usage)??0,u=c-l;return{provider:"openrouter",providerLabel:"OpenRouter",planName:null,tiers:[],balance:{remaining:u,total:c,used:l,unit:"USD"},success:!0,error:null}}case"stepfun":{const a=await fetch(`${e}/v1/accounts`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json();if(n.balance===void 0)return null;const i=m(n.balance)??0;return{provider:"stepfun",providerLabel:"StepFun",planName:null,tiers:[],balance:{remaining:i,total:null,used:null,unit:"CNY"},success:!0,error:null}}case"minimax_cn":case"minimax_en":{const a=await fetch(`${e}/v1/api/openplatform/coding_plan/remains`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json(),i=n.base_resp;if(i&&typeof i.status_code=="number"&&i.status_code!==0)return null;const l=(Array.isArray(n.model_remains)?n.model_remains:[])[0];if(!l)return null;const u=[],p=m(l.current_interval_total_count)??0,h=m(l.current_interval_usage_count)??0;return p>0&&u.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(h/p*1e4)/100,resetsAt:null}),{provider:s,providerLabel:"MiniMax",planName:null,tiers:u,balance:null,success:!0,error:null}}case"siliconflow_cn":case"siliconflow_en":{const a=await fetch(`${e}/v1/user/info`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const i=(await a.json()).data;if(!i)return null;const c=m(i.totalBalance)??0;return{provider:s,providerLabel:"SiliconFlow",planName:null,tiers:[],balance:{remaining:c,total:null,used:null,unit:"CNY"},success:!0,error:null}}case"novita":{const a=await fetch(`${e}/v3/user/balance`,{method:"GET",headers:o,signal:AbortSignal.timeout(f)});if(!a.ok)return null;const n=await a.json();if(n.availableBalance===void 0)return null;const i=(m(n.availableBalance)??0)/1e4;return{provider:"novita",providerLabel:"Novita AI",planName:null,tiers:[],balance:{remaining:i,total:null,used:null,unit:"USD"},success:!0,error:null}}default:return null}}catch{return null}}async function ae(s,t,r){if(!t.trim())return{provider:"unknown",providerLabel:"Unknown",planName:null,tiers:[],balance:null,success:!1,error:"API key is empty"};const e=U(r),o=B(s);if(o)switch(o.id){case"zhipu":return K(t);case"kimi":return Y(t);case"minimax_cn":return I(t,!0);case"minimax_en":return I(t,!1);case"deepseek":return Z(t);case"stepfun":return V(t);case"siliconflow_cn":return E(t,!0);case"siliconflow_en":return E(t,!1);case"openrouter":return W(t);case"novita":return X(t)}if(e){const n=await S(e,s,t);return n||d(e,e,`Quota API unavailable through base URL: ${s}`)}const a=await Q(s,t);return a||{provider:"unknown",providerLabel:"Unknown",planName:null,tiers:[],balance:null,success:!1,error:`Could not identify provider for base URL: ${s}`}}export{B as detectProvider,ie as detectProviderFromModel,U as normalizeProviderId,ae as queryProviderQuota,T as readKimiProviderSettings,se as resolveKimiProviderSettings};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as f}from"node:crypto";import{detectProvider as c,normalizeProviderId as l,queryProviderQuota as w}from"./providers.js";class y{ttlMs;now;cache=new Map;inFlight=new Map;constructor(e={}){this.ttlMs=e.ttlMs??6e4,this.now=e.now??Date.now}cacheKey(e){const t=l(e.providerId)??c(e.baseUrl)?.id??"unknown",i=p(e).toLowerCase(),a=e.accountKey?.trim()||"default",r=f("sha256").update(e.apiKey).digest("hex").slice(0,16);return`${t}|${i}|${a}|${r}`}async query(e,t={}){const i=this.cacheKey(e),a=this.now(),r=this.cache.get(i);if(!t.fresh&&r&&a-r.sampledAt<=this.ttlMs)return{...r,cached:!0};const s=this.inFlight.get(i);if(s)return{...await s,cached:!1};const o=(async()=>{const u=l(e.providerId)??c(e.baseUrl)?.id??void 0,h=await w(p(e),e.apiKey,u),d={quota:h,sampledAt:this.now(),cacheKey:i};return h.success&&this.cache.set(i,d),d})();this.inFlight.set(i,o);try{return{...await o,cached:!1}}finally{this.inFlight.delete(i)}}invalidate(e){if(!e){this.cache.clear();return}this.cache.delete(this.cacheKey(e))}}function p(n){const e=n.quotaBaseUrl?.trim();if(e)return e.replace(/\/+$/,"");const t=n.baseUrl.trim().replace(/\/+$/,"");if(c(t))return t;try{return new URL(t).origin}catch{return t}}const g=new y;export{y as ProviderQuotaService,p as resolveQuotaBaseUrl,g as sharedProviderQuotaService};
|
|
@@ -1,35 +1,288 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grix-admin
|
|
3
|
-
description:
|
|
4
|
-
trigger: 当用户要在 Grix 平台创建 Agent、管理分类、给 Agent 分配分类、或轮换 Agent 的 API key 时
|
|
3
|
+
description: Responsible for OpenClaw and grix-connector local configuration, binding, and runtime convergence; can create new remote API agents through the current agent's WS channel, and supports querying, creating, modifying agent categories and assigning categories to agents, reusable across agent creation and management flows.
|
|
5
4
|
---
|
|
6
5
|
|
|
7
|
-
# Grix Admin
|
|
6
|
+
# Grix Agent Admin
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
platform. This skill covers only platform-side management — it does not touch
|
|
11
|
-
any local connector configuration or binding.
|
|
8
|
+
`grix-admin` is responsible for three things:
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
1. Landing existing remote agent parameters into local OpenClaw or grix-connector, and handling runtime convergence after binding.
|
|
11
|
+
2. When the current main agent is already online and has the corresponding scope, creating new remote API agents through `grix_admin`'s direct actions, then continuing with local landing.
|
|
12
|
+
3. During agent creation or subsequent agent management, reusing `grix_admin`'s direct actions to query categories, create categories, modify categories, and assign categories to agents.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
## Entry Method
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- `
|
|
21
|
-
- `
|
|
22
|
-
|
|
23
|
-
- `
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
1. In most cases, enter this skill from `grix_admin`'s `task` entry; the first line of `task` must clearly state `bind-local`, `create-and-bind`, `category-manage`, `connector-bind-local`, or `create-and-connector-bind`.
|
|
17
|
+
2. Only when executing "remote API agent creation / category query / category creation / category modification / category assignment" remote steps within this skill should you directly call `grix_admin` once, without passing `task` again.
|
|
18
|
+
3. In new flows, always explicitly pass `action` when directly calling `grix_admin`:
|
|
19
|
+
- `create_agent`
|
|
20
|
+
- `list_categories`
|
|
21
|
+
- `create_category`
|
|
22
|
+
- `update_category`
|
|
23
|
+
- `assign_category`
|
|
24
|
+
4. The legacy direct call format for `create_agent` (passing only `agentName` and other fields without `action`) is still compatible, but should not be used in new flows.
|
|
25
|
+
5. Use `bind-local` / `create-and-bind` for **OpenClaw** local configuration; use `connector-bind-local` / `create-and-connector-bind` for **grix-connector** local configuration. Do not mix the two targets in a single invocation.
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Direct Action List
|
|
28
28
|
|
|
29
|
-
1.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
1. `action=create_agent`
|
|
30
|
+
- Required: `agentName`
|
|
31
|
+
- Optional: `introduction`, `isMain`, `categoryId`, `categoryName`, `parentCategoryId`, `categorySortOrder`
|
|
32
|
+
- `categoryId` and `categoryName` cannot be provided simultaneously
|
|
33
|
+
- When `categoryName` is given, it first checks for duplicates under `parentCategoryId`; if not found, creates and assigns
|
|
34
|
+
2. `action=list_categories`
|
|
35
|
+
- Required: (none)
|
|
36
|
+
3. `action=create_category`
|
|
37
|
+
- Required: `name`, `parentId`
|
|
38
|
+
- Optional: `sortOrder`
|
|
39
|
+
4. `action=update_category`
|
|
40
|
+
- Required: `categoryId`, `name`, `parentId`
|
|
41
|
+
- Optional: `sortOrder`
|
|
42
|
+
5. `action=assign_category`
|
|
43
|
+
- Required: `agentId`, `categoryId`
|
|
44
|
+
- `categoryId=0` means clear the category
|
|
45
|
+
|
|
46
|
+
## Mode A: bind-local (Initial Handoff from grix-register)
|
|
47
|
+
|
|
48
|
+
Input fields (written in `grix_admin.task`, all required):
|
|
49
|
+
|
|
50
|
+
1. First line must be `bind-local`
|
|
51
|
+
2. `agent_name`
|
|
52
|
+
3. `agent_id`
|
|
53
|
+
4. `api_endpoint`
|
|
54
|
+
5. `api_key`
|
|
55
|
+
|
|
56
|
+
Execution rules:
|
|
57
|
+
|
|
58
|
+
1. Do not perform remote creation; execute local binding directly. Do not call any script that directly modifies `openclaw.json`.
|
|
59
|
+
2. Prepare local directories first:
|
|
60
|
+
- `workspace=~/.openclaw/workspace-<agent_name>`
|
|
61
|
+
- `agentDir=~/.openclaw/agents/<agent_name>/agent`
|
|
62
|
+
- Persona files go only in the `workspace` root: `IDENTITY.md`, `SOUL.md`, `AGENTS.md`, and optionally `USER.md` / `MEMORY.md`
|
|
63
|
+
- Do not put persona files in `agentDir`; `agentDir` is the per-agent runtime state directory managed by OpenClaw
|
|
64
|
+
- If `workspace` is missing required persona files, add minimal files to avoid an empty workspace for the new agent
|
|
65
|
+
3. Read existing configuration; if paths do not exist, treat as empty object / empty array:
|
|
66
|
+
- `channels.grix.accounts`
|
|
67
|
+
- `agents.list`
|
|
68
|
+
- `tools.profile`
|
|
69
|
+
- `tools.alsoAllow`
|
|
70
|
+
- `tools.sessions.visibility`
|
|
71
|
+
- To confirm existing Grix bindings, additionally use `openclaw agents bindings --agent <agent_name> --json` to view the current binding list
|
|
72
|
+
4. Calculate target values for this operation:
|
|
73
|
+
- `channels.grix.accounts.<agent_name>`: write `name`, `enabled=true`, `apiKey`, `wsUrl`, `agentId`
|
|
74
|
+
- `agents.list`: ensure entry exists with `id=<agent_name>`, `name=<agent_name>`, `workspace`, `agentDir`, `model`
|
|
75
|
+
- Grix binding: ensure the target agent is ultimately bound to `grix:<agent_name>`
|
|
76
|
+
- `tools.profile`: set to `"coding"`
|
|
77
|
+
- `tools.alsoAllow`: must include at least `message`, `grix_query`, `grix_group`, `grix_register`, `grix_message_send`, `grix_message_unsend`
|
|
78
|
+
- If the current binding target is the main agent, also ensure that agent's own `tools.alsoAllow` retains `grix_admin`, `grix_egg`, `grix_update`, `openclaw_memory_setup`; this set goes only at the agent level, not in global `tools.alsoAllow`
|
|
79
|
+
- `tools.sessions.visibility`: set to `"agent"`
|
|
80
|
+
- If `channels.grix.enabled=false`, change it back to `true`
|
|
81
|
+
5. Rules for determining `model`:
|
|
82
|
+
- First reuse the existing `model` from that local agent's entry
|
|
83
|
+
- If the existing entry has none, use `agents.defaults.model.primary`
|
|
84
|
+
- If still unavailable, clearly state that model is missing, stop execution, do not guess
|
|
85
|
+
6. Write using official CLI item by item; do not overwrite the entire config:
|
|
86
|
+
- `openclaw config set channels.grix.accounts.<agent_name> '<ACCOUNT_JSON>' --strict-json`
|
|
87
|
+
- `openclaw config set agents.list '<NEXT_AGENTS_LIST_JSON>' --strict-json`
|
|
88
|
+
- `openclaw agents bind --agent <agent_name> --bind grix:<agent_name>`
|
|
89
|
+
- `openclaw config set tools.profile '"coding"' --strict-json`
|
|
90
|
+
- `openclaw config set tools.alsoAllow '["message","grix_query","grix_group","grix_register","grix_message_send","grix_message_unsend"]' --strict-json`
|
|
91
|
+
- If the current binding target is the main agent, also merge `grix_admin`, `grix_egg`, `grix_update`, `openclaw_memory_setup` into that agent's own `agents.list` record's `tools.alsoAllow`; do not put this set in global `tools.alsoAllow`
|
|
92
|
+
- `openclaw config set tools.sessions.visibility '"agent"' --strict-json`
|
|
93
|
+
- Only when the current config explicitly has `channels.grix.enabled` turned off, execute `openclaw config set channels.grix.enabled true --strict-json`
|
|
94
|
+
7. After writing, must perform static validation:
|
|
95
|
+
- `openclaw config validate`
|
|
96
|
+
- `openclaw config get --json channels.grix.accounts.<agent_name>`
|
|
97
|
+
- `openclaw config get --json agents.list`
|
|
98
|
+
- `openclaw agents bindings --agent <agent_name> --json`
|
|
99
|
+
8. If this invocation already has real verification conditions, must immediately perform a real routing verification; prefer reusing the current install/acceptance context, do not invent a new probe. The following situations are all considered as binding runtime not yet switched successfully:
|
|
100
|
+
- Reply falls to the main agent
|
|
101
|
+
- Reply behaves as the default assistant
|
|
102
|
+
- Reply still shows old persona, old config, or obvious ID mismatch
|
|
103
|
+
9. Only when this `bind-local` invocation itself handles real verification, and step 7 static validation passed but step 8 real routing verification still fails, is one `openclaw gateway restart` allowed as targeted remediation; after restart, must redo the same round of real routing verification.
|
|
104
|
+
10. Success definition:
|
|
105
|
+
- Can perform real verification: only "static validation passed + real routing verification passed" counts as `bind-local` complete
|
|
106
|
+
- This invocation cannot yet perform real verification: can only clearly state "config has been written, runtime not yet tested, needs subsequent upper-level flow to continue verification"; do not claim it has fully taken effect
|
|
107
|
+
|
|
108
|
+
## Mode B: create-and-bind (Subsequent Management When Main Channel and Scope Are Available)
|
|
109
|
+
|
|
110
|
+
Fields written in `grix_admin.task`:
|
|
111
|
+
|
|
112
|
+
1. First line must be `create-and-bind`
|
|
113
|
+
2. `agentName` (required)
|
|
114
|
+
3. `introduction` (optional)
|
|
115
|
+
4. `isMain` (optional, default `false`)
|
|
116
|
+
5. `categoryId` (optional): assign the new agent directly to an existing category
|
|
117
|
+
6. `categoryName` (optional): create if not exists, then assign
|
|
118
|
+
7. `parentCategoryId` (optional): only used in the `categoryName` approach, default `0`
|
|
119
|
+
8. `categorySortOrder` (optional): only used when creating a category
|
|
120
|
+
|
|
121
|
+
Execution rules:
|
|
122
|
+
|
|
123
|
+
1. Confirm the current session is bound to a valid Grix account; cross-account execution is prohibited.
|
|
124
|
+
2. If both `categoryId` and `categoryName` are provided, report an error and stop immediately to avoid ambiguity.
|
|
125
|
+
3. Call `grix_admin` only once with `action=create_agent`, delegating remote creation and optional category handling to it; pass:
|
|
126
|
+
- `action=create_agent`
|
|
127
|
+
- `agentName`
|
|
128
|
+
- Optional `introduction`
|
|
129
|
+
- Optional `isMain`
|
|
130
|
+
- Optional `categoryId`
|
|
131
|
+
- Optional `categoryName`
|
|
132
|
+
- Optional `parentCategoryId`
|
|
133
|
+
- Optional `categorySortOrder`
|
|
134
|
+
4. After remote creation succeeds, read `createdAgent.id`, `createdAgent.agent_name`, `createdAgent.api_endpoint`, `createdAgent.api_key` from the return result.
|
|
135
|
+
5. If the request included category information, check whether the return already includes the category assignment result; if not, proceed with supplementary steps according to `categoryId` / `categoryName` rules and explain the reason.
|
|
136
|
+
6. In the `categoryName` flow, if multiple categories with the exact same name appear under the same parent, stop and ask the owner to clean up categories or use an explicit `categoryId` instead.
|
|
137
|
+
7. After remote creation and optional category steps succeed, immediately transition to the `bind-local` local binding steps; if this invocation already has real verification context, follow the same "static validation -> real routing verification -> one restart if needed -> same-round retest" convergence rules; otherwise explicitly hand the "continue real verification" responsibility back to the upper-level flow.
|
|
138
|
+
8. `isMain=true` should only be used when actually creating a new main API agent; generally subsequent new agents should not enable this by default.
|
|
139
|
+
9. Throughout the `create-and-bind` flow, do not treat "config written successfully" as completion; only when this invocation itself handles real routing verification and verification fails should one `openclaw gateway restart` be treated as targeted remediation.
|
|
140
|
+
|
|
141
|
+
## Mode C: category-manage (Category Management)
|
|
142
|
+
|
|
143
|
+
Fields written in `grix_admin.task`:
|
|
144
|
+
|
|
145
|
+
1. First line must be `category-manage`
|
|
146
|
+
2. `operation` (required): only `list`, `create`, `update`, `assign` are allowed
|
|
147
|
+
3. `name` (`create` / `update` required)
|
|
148
|
+
4. `parentId` (`create` / `update` required)
|
|
149
|
+
5. `sortOrder` (`create` / `update` optional)
|
|
150
|
+
6. `categoryId` (`update` / `assign` required; for `assign`, `0` means clear)
|
|
151
|
+
7. `agentId` (`assign` required)
|
|
152
|
+
|
|
153
|
+
Execution rules:
|
|
154
|
+
|
|
155
|
+
1. Strictly bound to the current session account; cross-account execution is prohibited.
|
|
156
|
+
2. All remote steps must only be completed through `grix_admin`'s direct actions; hand-written HTTP and temporary scripts are prohibited.
|
|
157
|
+
3. `operation=list`
|
|
158
|
+
- Call `action=list_categories`
|
|
159
|
+
4. `operation=create`
|
|
160
|
+
- Call `action=create_category`
|
|
161
|
+
5. `operation=update`
|
|
162
|
+
- Call `action=update_category`
|
|
163
|
+
6. `operation=assign`
|
|
164
|
+
- Call `action=assign_category`
|
|
165
|
+
- `categoryId=0` explicitly means clear the agent's current category
|
|
166
|
+
7. If the current management task also includes "creating a new agent", prefer using `create-and-bind`; do not split remote creation into other custom flows.
|
|
167
|
+
|
|
168
|
+
## Mode D: connector-bind-local (grix-connector Local Binding)
|
|
169
|
+
|
|
170
|
+
Input fields (written in `grix_admin.task`, all required):
|
|
171
|
+
|
|
172
|
+
1. First line must be `connector-bind-local`
|
|
173
|
+
2. `agent_name`
|
|
174
|
+
3. `agent_id`
|
|
175
|
+
4. `api_endpoint`
|
|
176
|
+
5. `api_key`
|
|
177
|
+
6. `client_type` (optional, default `pi`)
|
|
178
|
+
|
|
179
|
+
Execution rules:
|
|
180
|
+
|
|
181
|
+
1. Do not perform remote creation; execute local grix-connector binding directly. Do not invoke any OpenClaw CLI commands in this mode.
|
|
182
|
+
2. Target file: `~/.grix/config/agents.json`. If the file or directory does not exist, initialize it as `{ "agents": [] }`.
|
|
183
|
+
3. Read the existing `agents` array. If an entry with the same `name` as `agent_name` already exists, update it in place; otherwise append a new entry.
|
|
184
|
+
4. Write the following fields into the entry:
|
|
185
|
+
- `name`: `agent_name`
|
|
186
|
+
- `ws_url`: `api_endpoint`
|
|
187
|
+
- `agent_id`: `agent_id`
|
|
188
|
+
- `api_key`: `api_key`
|
|
189
|
+
- `client_type`: `client_type` (default `pi`)
|
|
190
|
+
5. Preserve valid JSON and set file permissions to `0o600`. Before writing, create a timestamped backup at `~/.grix/config/agents.json.bak.<YYYYMMDDHHMMSS>` and also set the backup file permissions to `0o600`.
|
|
191
|
+
6. If `grix-connector` daemon is running (`grix-connector status` returns `daemon_state=running`), trigger reload via the daemon Admin API:
|
|
192
|
+
- Default endpoint: `POST http://127.0.0.1:19580/api/reload`
|
|
193
|
+
- The actual admin port may be overridden by `GRIX_ADMIN_PORT` or `--admin-port`; if the default fails, read `~/.grix/data/admin-port` for the current port.
|
|
194
|
+
- This endpoint is synchronous: it waits for `manager.reload()` to finish and returns `{ ok: true, result }` or an error. Unlike the CLI `grix-connector reload`, it does not suffer from "signal sent but config not yet applied" race conditions, and it surfaces JSON parse errors or `RELOAD_UNSAFE` failures to the caller.
|
|
195
|
+
- If the reload request returns an error, report the error and stop; do not proceed to verification.
|
|
196
|
+
7. After reload succeeds, verify that the daemon has loaded the new entry via `GET /api/agents`:
|
|
197
|
+
- Find an entry where `name === agent_name`.
|
|
198
|
+
- Confirm the entry exists and reports `alive === true`.
|
|
199
|
+
- **Important**: `alive=true` only means the daemon has started the Agent instance; it does **not** prove the Agent has successfully connected to the Grix platform or that the API key is valid. Authentication failures do not flip `alive` to `false`.
|
|
200
|
+
- Therefore, perform a secondary convergence check: inspect the latest daemon log in `~/.grix/log/` (files are named `grix-connector-<YYYY-MM-DD>.log`; list the directory and open the most recent one) for WebSocket connection success / authentication failure messages for this Agent, or ask the owner to send a test message to the Agent.
|
|
201
|
+
- If the secondary check fails, state "config loaded and instance started, but platform connection not yet verified"; do not claim full convergence.
|
|
202
|
+
8. If this invocation cannot perform real verification, clearly state "config has been written, reload completed, runtime not yet tested, needs subsequent flow to continue verification"; do not claim it has fully taken effect.
|
|
203
|
+
9. Success definition:
|
|
204
|
+
- Can perform real verification: "config written + reload succeeded + Agent entry exists with `alive=true` + platform connection verified (log or test message)" counts as `connector-bind-local` complete.
|
|
205
|
+
- Cannot perform real verification: only "config written + reload succeeded" can be claimed.
|
|
206
|
+
|
|
207
|
+
## Mode E: create-and-connector-bind (Create Remote Agent Then Bind to grix-connector)
|
|
208
|
+
|
|
209
|
+
Fields written in `grix_admin.task`:
|
|
210
|
+
|
|
211
|
+
1. First line must be `create-and-connector-bind`
|
|
212
|
+
2. `agentName` (required)
|
|
213
|
+
3. `introduction` (optional)
|
|
214
|
+
4. `isMain` (optional, default `false`)
|
|
215
|
+
5. `clientType` (optional, default `pi`)
|
|
216
|
+
6. `categoryId` (optional): assign the new agent directly to an existing category
|
|
217
|
+
7. `categoryName` (optional): create if not exists, then assign
|
|
218
|
+
8. `parentCategoryId` (optional): only used in the `categoryName` approach, default `0`
|
|
219
|
+
9. `categorySortOrder` (optional): only used when creating a category
|
|
220
|
+
|
|
221
|
+
Execution rules:
|
|
222
|
+
|
|
223
|
+
1. Confirm the current session is bound to a valid Grix account; cross-account execution is prohibited.
|
|
224
|
+
2. If both `categoryId` and `categoryName` are provided, report an error and stop immediately to avoid ambiguity.
|
|
225
|
+
3. Call `grix_admin` only once with `action=create_agent`, delegating remote creation and optional category handling to it; pass:
|
|
226
|
+
- `action=create_agent`
|
|
227
|
+
- `agentName`
|
|
228
|
+
- Optional `introduction`
|
|
229
|
+
- Optional `isMain`
|
|
230
|
+
- Optional `categoryId`
|
|
231
|
+
- Optional `categoryName`
|
|
232
|
+
- Optional `parentCategoryId`
|
|
233
|
+
- Optional `categorySortOrder`
|
|
234
|
+
4. After remote creation succeeds, read `createdAgent.id`, `createdAgent.agent_name`, `createdAgent.api_endpoint`, `createdAgent.api_key` from the return result.
|
|
235
|
+
5. If the request included category information, check whether the return already includes the category assignment result; if not, proceed with supplementary steps according to `categoryId` / `categoryName` rules and explain the reason.
|
|
236
|
+
6. In the `categoryName` flow, if multiple categories with the exact same name appear under the same parent, stop and ask the owner to clean up categories or use an explicit `categoryId` instead.
|
|
237
|
+
7. After remote creation and optional category steps succeed, immediately transition to the `connector-bind-local` local binding steps using the returned parameters and `clientType`.
|
|
238
|
+
8. Trigger reload via the synchronous Admin API `POST /api/reload` (see Mode D for port resolution and failure handling); then verify the new Agent entry exists and `alive=true` via `GET /api/agents`, and perform the secondary platform-connection verification (log inspection or test message).
|
|
239
|
+
9. `isMain=true` should only be used when actually creating a new main API agent; generally subsequent new agents should not enable this by default.
|
|
240
|
+
10. Throughout the `create-and-connector-bind` flow, do not treat "config written successfully" as completion; only when this invocation handles real verification and verification passes is it complete.
|
|
241
|
+
|
|
242
|
+
## Remote Creation Fallback Conditions
|
|
243
|
+
|
|
244
|
+
If the current task has neither an existing `agent_name`, `agent_id`, `api_endpoint`, `api_key`, nor an available online main channel or `agent.api.create` permission, stop this skill first and clearly prompt the user to create the remote agent through the backend admin path. After obtaining these parameters:
|
|
245
|
+
|
|
246
|
+
- For OpenClaw target, proceed with `bind-local`.
|
|
247
|
+
- For grix-connector target, proceed with `connector-bind-local`.
|
|
248
|
+
|
|
249
|
+
## Guardrails (Applicable to All Modes)
|
|
250
|
+
|
|
251
|
+
1. Never ask user for website account/password.
|
|
252
|
+
2. `bind-local` mode must not call back to `grix-register` to avoid circular routing.
|
|
253
|
+
3. All remote creation / category-related actions must only go through `grix_admin` direct actions via the current account's WS channel; do not hand-write HTTP or fall back to legacy scripts.
|
|
254
|
+
4. The complete `api_key` is only passed back once; do not repeatedly echo it in plaintext.
|
|
255
|
+
5. Before local `openclaw config set` / `validate` succeeds, do not claim config is complete; when this invocation can perform real verification, do not claim `bind-local` / `create-and-bind` is complete until real routing verification passes.
|
|
256
|
+
6. During an install private chat, do not manually modify `openclaw.json` and then execute `openclaw gateway restart`.
|
|
257
|
+
7. Do not reference or call `grix_agent_bind.py`; for OpenClaw modes this skill only uses official OpenClaw CLI commands.
|
|
258
|
+
8. For `connector-bind-local` / `create-and-connector-bind`, directly reading and writing `~/.grix/config/agents.json` is the intended mechanism; this is grix-connector's own configuration domain, not a third-party product.
|
|
259
|
+
9. When writing `~/.grix/config/agents.json`, always create a timestamped backup first, preserve valid JSON, and set both the config file and the backup file permissions to `0o600`.
|
|
260
|
+
10. After writing grix-connector config, trigger reload through the synchronous Admin API `POST /api/reload` (not the CLI `grix-connector reload`) so that reload errors are visible to the caller. Verify the Agent entry exists and reports `alive=true`, and perform a secondary platform-connection check before claiming full convergence.
|
|
261
|
+
|
|
262
|
+
## Error Handling Rules
|
|
263
|
+
|
|
264
|
+
1. `bind-local` / `connector-bind-local` missing fields: clearly state which field is missing and stop.
|
|
265
|
+
2. `create-and-bind` / `create-and-connector-bind` missing `agentName`: clearly state which field is missing and stop.
|
|
266
|
+
3. `create-and-bind` / `create-and-connector-bind` with both `categoryId` and `categoryName`: clearly state the conflict and stop.
|
|
267
|
+
4. `category-manage` missing `operation` or operation-specific fields: clearly state which field is missing and stop.
|
|
268
|
+
5. Remote returns `code=4003` or message explicitly mentions `agent.api.create`: tell the owner to grant `agent.api.create` on the Agent permissions page.
|
|
269
|
+
6. Remote returns `code=4003` or message explicitly mentions `agent.category.list` / `agent.category.create` / `agent.category.update` / `agent.category.assign`: tell the owner to grant the corresponding scope on the Agent permissions page.
|
|
270
|
+
7. Missing remote agent parameters and current account cannot create: clearly require backend admin creation first.
|
|
271
|
+
8. Local config failure (OpenClaw): return the failed command and result, then stop; emphasize which `get` / `set` / `validate` step failed.
|
|
272
|
+
9. Local config failure (grix-connector): report the exact JSON parse error, write error, or `~/.grix/config/agents.json` state, then stop.
|
|
273
|
+
10. Synchronous Admin API reload failed (e.g., returns non-2xx, `RELOAD_UNSAFE`, JSON parse error in agents.json): report the error body and daemon status; do not proceed to verification or claim the Agent is online.
|
|
274
|
+
11. Admin API verification failed (Agent entry missing or not `alive=true`): report the observed entry or missing entry, then stop.
|
|
275
|
+
12. Secondary platform-connection verification failed (log shows authentication/connection errors, or test message not delivered): state "config loaded and instance started, but platform connection not verified"; converge as partially complete, do not claim full convergence.
|
|
276
|
+
13. This invocation handles real routing verification, and after one `openclaw gateway restart` retest still fails: clearly state "static config has been written, but runtime has not yet switched successfully"; converge as failed or partially complete, do not write it as success.
|
|
277
|
+
|
|
278
|
+
## Response Style
|
|
279
|
+
|
|
280
|
+
1. Clearly state whether the current execution is `bind-local`, `create-and-bind`, `category-manage`, `connector-bind-local`, or `create-and-connector-bind`.
|
|
281
|
+
2. Report in phases: remote creation / category handling (if any) / local config writing / reload / validation results.
|
|
282
|
+
3. Clearly state whether local config has taken effect; if only static config succeeded but this invocation cannot perform real verification, can only write "config has been written, runtime not yet tested, needs subsequent flow to continue verification"; if only the category step succeeded, also state that clearly.
|
|
283
|
+
4. For grix-connector flows, explicitly report whether the synchronous Admin API reload succeeded, whether the Admin API shows the Agent entry exists with `alive=true`, and the result of any secondary platform-connection verification.
|
|
284
|
+
5. If remote creation succeeded but category or local binding subsequently failed, clearly state it is "partially complete"; do not generically write it as success.
|
|
285
|
+
|
|
286
|
+
## References
|
|
287
|
+
|
|
288
|
+
1. [references/api-contract.md](references/api-contract.md)
|