grix-connector 3.9.0 → 3.9.2
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/acp/acp-adapter.js +16 -14
- package/dist/bridge/adapter-pool.js +1 -1
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/event-queue.js +1 -1
- package/dist/core/aibot/client.js +2 -2
- package/dist/core/aibot/connection-handle.js +1 -1
- package/dist/core/provider-quota/providers.js +2 -2
- package/dist/default-skills/grix-egg/SKILL.md +48 -40
- package/dist/default-skills/index.js +1 -1
- package/dist/default-skills/tailnet-file-share/SKILL.md +62 -48
- package/dist/manager.js +1 -1
- package/dist/mcp/stream-http/security.js +1 -1
- package/dist/protocol/event-mapper.js +5 -5
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{EventEmitter as v}from"node:events";import{randomUUID as b}from"node:crypto";import p from"node:os";import m from"ws";import{log as a}from"../log/index.js";import{getMachineName as w}from"../util/index.js";import{detectTailnetIPv4 as E,ensureServerAndGetPort as q,getFileServerHttpsPort as A}from"../files/file-serve.js";import{AUTH_CODE_AGENT_DELETED as k,KICKED_REASON_AGENT_DELETED as S,AgentDeletedError as $}from"./errors.js";function
|
|
2
|
-
`}),this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)||a.warn("aibot",`send_msg NOT sent (ws not open) event=${e.event_id??""} session=${e.session_id??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}editMsg(e){this.sendPacket("edit_msg",e)}sendEventResult(e){if(!this.ws||this.ws.readyState!==m.OPEN){this.sendPacket("event_result",e);return}this.sendEventResultReliable(e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendSessionActivitySet(e){this.sendPacket("session_activity_set",e)}sendCodexEvent(e){this.sendPacket("codex_event",e)}sendUpdateBindingCard(e){this.sendPacket("update_binding_card",e)}sendSkillsUpdate(e){this.sendPacket("agent_skills_update",e)}sendPing(){this.sendPacket("ping",{})}sendEventState(e){this.sendPacket("event_state",e)}sendEventCancelResult(e){this.sendPacket("event_cancel_result",e)}sendQueueClearResult(e){this.sendPacket("queue_clear_result",e)}sendQueueSnapshot(e){this.sendPacket("queue_snapshot",e)}agentInvoke(e,t,s=15e3){return new Promise((i,n)=>{const h=b(),c=Math.max(1e3,Math.min(s,6e4)),o=setTimeout(()=>{this.pendingInvokes.delete(h),n(new Error(`agent_invoke timeout: ${e}`))},c);this.pendingInvokes.set(h,{resolve:i,reject:n,timer:o}),this.sendPacket("agent_invoke",{invoke_id:h,action:e,params:t,timeout_ms:c})})}sendMcpFrame(e,t){this.sendPacket("mcp_frame",{session_id:e,frame:t})}request(e,t,s){return new Promise((i,n)=>{const h=++this.seq,c=setTimeout(()=>{this.pendingRequests.delete(h),n(new Error(`request timeout: ${e} (expected ${s.expected.join("/")})`))},s.timeoutMs);this.pendingRequests.set(h,{expected:s.expected,resolve:i,reject:n,timer:c}),this.sendPacket(e,t,h)||(this.pendingRequests.delete(h),clearTimeout(c),n(new Error(`send failed: ${e}`)))})}async sendStreamChunkRequest(e,t=2e4){return this.request("client_stream_chunk",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendText(e,t=2e4){return this.request("send_msg",{msg_type:1,...e},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendMedia(e,t=2e4){return this.request("send_msg",{...e,msg_type:2},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async editMessage(e,t=2e4){return this.request("edit_msg",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async deleteMessage(e,t,s=2e4){return this.request("delete_msg",{session_id:e,msg_id:t},{expected:["send_ack","send_nack","error"],timeoutMs:s})}async sendEventResultRequest(e,t=5e3){return this.request("event_result",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}disconnect(){a.info("aibot",`disconnect() agent=${this.config.clientType}:${this.config.agentId} wasConnected=${this.connected} reconnecting=${this.reconnecting} reconnectAttempts=${this.reconnectAttempts}`),this.connected=!1,this.everConnected=!1,this.reconnecting=!1,this.reconnectAttempts=0,this.stopHeartbeat(),this.rejectAllPendingInvokes("disconnect"),this.rejectAllPendingRequests("disconnect"),this.outboundBuffer.length=0,this.ws&&(this.ws.close(1e3,"client disconnect"),this.ws=null)}static MAX_CONSECUTIVE_AUTH_FAILURES=5;async attemptReconnect(){if(this.reconnecting||this.agentDeleted)return;this.reconnecting=!0,a.info("aibot",`attemptReconnect start agent=${this.config.clientType}:${this.config.agentId} fromAttempts=${this.reconnectAttempts}`),this.emit("disconnected");let e=0;for(;this.reconnecting;){const t=Math.min(1e3*2**this.reconnectAttempts,3e4),s=Math.floor(t*.2*Math.random());if(this.reconnectAttempts++,await new Promise(i=>setTimeout(i,t+s)),!this.reconnecting)return;try{await this.connect();const i=this.reconnectAttempts;this.reconnectAttempts=0,this.reconnecting=!1,a.info("aibot",`reconnect succeeded agent=${this.config.clientType}:${this.config.agentId} attempt=${i}`);return}catch(i){if(this.ws){try{this.ws.close()}catch{}this.ws=null}const n=i instanceof Error?i.message:String(i);if(a.warn("aibot",`reconnect failed agent=${this.config.clientType}:${this.config.agentId} attempt=${this.reconnectAttempts} err=${n}`),i instanceof $){this.agentDeleted=!0,this.reconnecting=!1,a.error("aibot",`reconnect aborted: agent deleted on platform agent=${this.config.clientType}:${this.config.agentId}`),this.emit("agentDeleted",{source:"auth_ack",code:k});return}if(/Auth failed/i.test(n)){if(e++,e>=f.MAX_CONSECUTIVE_AUTH_FAILURES){this.reconnecting=!1,a.error("aibot",`reconnect giving up after ${e} consecutive auth failures agent=${this.config.clientType}:${this.config.agentId}`);return}}else e=0}}}sendPacket(e,t,s){if(this.ws&&this.ws.readyState===m.OPEN){const i=this.ws.bufferedAmount>f.BACKPRESSURE_THRESHOLD;if(!i||!f.DROPPABLE_COMMANDS.has(e)){if(i&&f.DROPPABLE_COMMANDS.has(e))return!1;const n=s??++this.seq;if(e==="client_stream_chunk"&&t&&typeof t=="object"){const c=t.event_id;if(c&&(this.seqEventMap.set(n,c),this.seqEventMap.size>200)){const o=this.seqEventMap.keys().next().value;o!==void 0&&this.seqEventMap.delete(o)}}const h={cmd:e,seq:n,payload:t};this.packetLog?.logOutboundPacket(e,n,t,"sent");try{const c=this.ws.readyState,o=this.ws.bufferedAmount;return this.ws.send(JSON.stringify(h),d=>{if(e==="event_result"){const r=t;d?a.warn("aibot",`event_result ws send callback failed event=${r.event_id??""} status=${r.status??""} seq=${n} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`event_result ws send callback ok event=${r.event_id??""} status=${r.status??""} seq=${n} readyState=${c} bufferedAmount=${o}`)}else if(e==="client_stream_chunk"){const r=t;d?a.warn("aibot",`stream_chunk ws send failed event=${r.event_id??""} session=${r.session_id??""} seq=${n} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`stream_chunk ws send ok event=${r.event_id??""} session=${r.session_id??""} seq=${n} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} client_msg_id=${r.client_msg_id??""} quoted_message_id=${r.quoted_message_id??""} readyState=${c} bufferedAmount=${o}`)}else if(e==="event_ack"){const r=t;d?a.warn("aibot",`event_ack ws send failed event=${r.event_id??""} seq=${n} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`event_ack ws send ok event=${r.event_id??""} seq=${n} readyState=${c} bufferedAmount=${o}`)}else if(e==="send_msg"){const r=t;d?a.warn("aibot",`send_msg ws send failed event=${r.event_id??""} session=${r.session_id??""} seq=${n} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`send_msg ws send ok event=${r.event_id??""} session=${r.session_id??""} seq=${n} readyState=${c} bufferedAmount=${o}`)}else if(d){const r=t;a.warn("aibot",`${e} ws send failed seq=${n} session=${r.session_id??""} event=${r.event_id??""} client_msg_id=${r.client_msg_id??""} readyState=${c} bufferedAmount=${o} err=${d.message}`)}}),!0}catch(c){return this.emitClientError(new Error(`sendPacket failed: ${c}`)),!1}}}if(f.DROPPABLE_COMMANDS.has(e))return this.packetLog?.logOutboundPacket(e,s??0,t,"dropped"),!1;if(s!==void 0)return this.packetLog?.logOutboundPacket(e,s,t,"dropped"),!1;if(this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&(this.outboundBuffer=this.outboundBuffer.filter(i=>f.BUFFER_OVERFLOW_RETAIN_COMMANDS.has(i.cmd)),this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&this.outboundBuffer.shift()),this.outboundBuffer.push({cmd:e,payload:t}),this.packetLog?.logOutboundPacket(e,s??0,t,"buffered"),e==="client_stream_chunk"){const i=t;a.info("aibot",`stream_chunk buffered (ws not open) event=${i.event_id??""} session=${i.session_id??""} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}return!1}async sendEventResultReliable(e){const t=this.ackPolicy?.max_retries??3,s=this.ackPolicy?.push_ack_timeout_ms??5e3,i=750;for(let n=1;n<=t;n++){const h=this.ws?.readyState??-1,c=this.ws?.bufferedAmount??0;a.info("aibot",`event_result send attempt event=${e.event_id} status=${e.status} attempt=${n}/${t} readyState=${h} bufferedAmount=${c}`);try{const o=await this.sendEventResultRequest(e,s);if(o.cmd==="send_ack"){const r=o.payload;a.info("aibot",`event_result ack event=${e.event_id} status=${e.status} attempt=${n}/${t} ack_event=${r.event_id??""} ack_status=${r.status??""}`);return}const d=o.payload;if(a.warn("aibot",`event_result rejected event=${e.event_id} status=${e.status} attempt=${n}/${t} cmd=${o.cmd} code=${d.code??""} msg=${d.msg??""}${d.ref_cmd?` ref_cmd=${d.ref_cmd}`:""}${d.ref_id?` ref_id=${d.ref_id}`:""}`),d.code===4003){a.warn("aibot",`event_result stopping retries: 4003 ownership denied event=${e.event_id}`);return}return}catch(o){const d=o instanceof Error?o.message:String(o);if(a.warn("aibot",`event_result attempt failed event=${e.event_id} status=${e.status} attempt=${n}/${t} err=${d}`),n===t){this.emitClientError(new Error(`event_result ack failed after ${t} attempts: event=${e.event_id} status=${e.status}`));return}await new Promise(r=>setTimeout(r,i*n))}}}purgeBufferedStreamChunks(e){const t=this.outboundBuffer.length;this.outboundBuffer=this.outboundBuffer.filter(s=>s.cmd!=="client_stream_chunk"?!0:s.payload?.event_id!==e),this.outboundBuffer.length<t&&a.info("aibot",`purged ${t-this.outboundBuffer.length} buffered stream chunks for event=${e}`)}emitClientError(e){if(this.listenerCount("error")===0){a.warn("aibot",`Client error (no listeners): ${e.message}`);return}this.emit("error",e)}flushOutboundBuffer(){if(this.outboundBuffer.length===0||!this.ws||this.ws.readyState!==m.OPEN)return;const e=this.outboundBuffer;this.outboundBuffer=[];for(const{cmd:t,payload:s}of e){const i=++this.seq;if(t==="client_stream_chunk"&&s&&typeof s=="object"){const h=s.event_id;h&&this.seqEventMap.set(i,h)}const n={cmd:t,seq:i,payload:s};try{this.ws.send(JSON.stringify(n))}catch{break}}if(this.seqEventMap.size>200){const t=[...this.seqEventMap.entries()].sort((s,i)=>s[0]-i[0]);this.seqEventMap.clear();for(const[s,i]of t.slice(-100))this.seqEventMap.set(s,i)}}handleInvokeResult(e){const t=this.pendingInvokes.get(e.invoke_id);t&&(this.pendingInvokes.delete(e.invoke_id),clearTimeout(t.timer),e.code===0?t.resolve(e.data??null):t.reject(new Error(`agent_invoke error code=${e.code}: ${e.msg??""}`)))}rejectAllPendingInvokes(e){for(const[,t]of this.pendingInvokes)clearTimeout(t.timer),t.reject(new Error(`agent_invoke canceled: ${e}`));this.pendingInvokes.clear()}rejectAllPendingRequests(e){for(const[,t]of this.pendingRequests)clearTimeout(t.timer),t.reject(new Error(`request canceled: ${e}`));this.pendingRequests.clear()}cleanupSocket(){if(this.ws)try{this.ws.close()}catch{}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatFailures=0,this.heartbeatTimer=setInterval(()=>{this.connected&&this.request("ping",{ts:Date.now()},{expected:["pong"],timeoutMs:5e3}).then(()=>{this.heartbeatFailures=0}).catch(()=>{this.connected&&(this.heartbeatFailures++,!(this.heartbeatFailures<f.HEARTBEAT_MAX_FAILURES)&&(this.cleanupSocket(),this.attemptReconnect()))})},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{f as AibotClient};
|
|
1
|
+
import{EventEmitter as v}from"node:events";import{randomUUID as b}from"node:crypto";import p from"node:os";import m from"ws";import{log as a}from"../log/index.js";import{getMachineName as w}from"../util/index.js";import{detectTailnetIPv4 as E,ensureServerAndGetPort as q,getFileServerHttpsPort as A}from"../files/file-serve.js";import{AUTH_CODE_AGENT_DELETED as k,KICKED_REASON_AGENT_DELETED as S,AgentDeletedError as $}from"./errors.js";function R(g){return g.replace(/(?<=[\[:,\[]\s*)(\d{16,})(?=\s*[,}\]\n])/g,'"$1"')}function y(g){const e=[...g??["stream_chunk","local_action_v1","agent_invoke"]];return e.includes("agent_invoke")||e.push("agent_invoke"),e.includes("event_result_ack")||e.push("event_result_ack"),e}const P="aibot-agent-api-v1",T=1;class f extends v{static DROPPABLE_COMMANDS=new Set(["update_binding_card"]);static BUFFER_OVERFLOW_RETAIN_COMMANDS=new Set(["event_result","codex_event","client_stream_chunk"]);static MAX_OUTBOUND_BUFFER_SIZE=1e3;static BACKPRESSURE_THRESHOLD=64*1024;ws=null;seq=0;heartbeatTimer=null;heartbeatSec=30;heartbeatFailures=0;static HEARTBEAT_MAX_FAILURES=2;connected=!1;reconnecting=!1;reconnectAttempts=0;everConnected=!1;agentDeleted=!1;config;packetLog;pendingInvokes=new Map;seqEventMap=new Map;pendingRequests=new Map;outboundBuffer=[];ackPolicy=null;constructor(e,t){super(),this.packetLog=t?.packetLog??null,this.config={url:e.url,agentId:e.agentId,apiKey:e.apiKey,clientType:e.clientType,clientVersion:e.clientVersion??"",adapterHint:e.adapterHint??"",capabilities:y(e.capabilities),localActions:e.localActions??["exec_approve","exec_reject"],skills:e.skills}}get isConnected(){return this.connected}async connect(){let e,t,s;const i=(async()=>{try{if(e=await E(),e!==void 0)try{t=await q(e);const n=A();n>0&&(s=n)}catch(n){a.warn("aibot",`file server pre-start failed: ${n}`)}}catch(n){a.warn("aibot",`tailnet detect failed: ${n}`)}})();return new Promise((n,h)=>{const c=new m(this.config.url);this.ws=c;const o=setTimeout(()=>{h(new Error("Auth timeout: no auth_ack received within 15s")),this.cleanupSocket()},15e3),d=++this.seq,r=setTimeout(()=>{this.pendingRequests.delete(d),h(new Error("Auth request timeout")),this.cleanupSocket()},15e3);this.pendingRequests.set(d,{expected:["auth_ack"],resolve:u=>{clearTimeout(o);const l=u.payload;l.code===0?(this.connected=!0,this.everConnected=!0,this.reconnectAttempts=0,l.heartbeat_sec&&(this.heartbeatSec=l.heartbeat_sec),l.ack_policy&&(this.ackPolicy=l.ack_policy,a.info("aibot",`ack_policy received: push_ack_timeout_ms=${l.ack_policy.push_ack_timeout_ms??"default"} max_retries=${l.ack_policy.max_retries??"default"} timeout_action=${l.ack_policy.timeout_action??"default"}`)),this.startHeartbeat(),this.flushOutboundBuffer(),this.emit("auth",l),n(l)):l.code===k?(this.agentDeleted=!0,h(new $(`Agent deleted: code=${l.code} msg=${l.msg}`))):h(new Error(`Auth failed: code=${l.code} msg=${l.msg}`))},reject:u=>{clearTimeout(o),h(u)},timer:r}),c.on("open",async()=>{await i;const u={agent_id:this.config.agentId,api_key:this.config.apiKey,client_type:this.config.clientType,protocol_version:P,contract_version:T,capabilities:this.config.capabilities??[],local_actions:this.config.localActions,skills:this.config.skills};this.config.sharedOwnerId&&(u.shared_owner_id=this.config.sharedOwnerId),this.config.clientVersion&&(u.client="grix-connector",u.client_version=this.config.clientVersion,u.host_type=this.config.clientType,u.host_version=this.config.clientVersion),this.config.adapterHint&&(u.adapter_hint=this.config.adapterHint),u.host_meta={hostname:w(),platform:p.platform(),arch:p.arch(),os_release:p.release(),...e!==void 0&&{tailnet_ip:e},...t!==void 0&&t>0&&{file_server_port:t},...s!==void 0&&s>0&&{file_server_https_port:s}},this.config.concurrency&&(u.concurrency=this.config.concurrency),this.sendPacket("auth",u,d)}),c.on("message",u=>{if(this.ws!==c)return;let l;try{l=JSON.parse(R(u.toString()))}catch{return}try{this.handlePacket(l)}catch(_){this.emitClientError(new Error(`handlePacket error: ${_}`))}}),c.on("close",(u,l)=>{if(this.ws!==c)return;this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("websocket closed"),this.emit("close",u,l.toString());const _=u!==1e3&&this.everConnected&&!this.agentDeleted;a.info("aibot",`ws closed agent=${this.config.clientType}:${this.config.agentId} code=${u} reason=${l.toString()||"<none>"} everConnected=${this.everConnected} reconnecting=${this.reconnecting} agentDeleted=${this.agentDeleted} willReconnect=${_}`),_&&this.attemptReconnect()}),c.on("error",u=>{this.ws===c&&(this.emitClientError(u instanceof Error?u:new Error(String(u))),this.connected||h(u))})})}handlePacket(e){if(this.packetLog?.logInboundPacket(e.cmd,e.seq,e.payload),e.seq>0&&this.pendingRequests.has(e.seq)){const t=this.pendingRequests.get(e.seq);this.pendingRequests.delete(e.seq),clearTimeout(t.timer),t.expected.includes(e.cmd)?t.resolve(e):t.reject(new Error(`unexpected response: got ${e.cmd}, expected ${t.expected.join("/")}`));return}switch(e.cmd){case"auth_ack":break;case"ping":{this.sendPacket("pong",e.payload??{});break}case"event_msg":{this.emit("event",e.payload);break}case"local_action":{this.emit("localAction",e.payload);break}case"event_stop":{this.emit("stop",e.payload);break}case"event_revoke":{this.emit("revoke",e.payload);break}case"event_edit":{this.emit("edit",e.payload);break}case"event_cancel":{this.emit("eventCancel",e.payload);break}case"queue_clear":{this.emit("queueClear",e.payload);break}case"queue_reorder":{this.emit("queueReorder",e.payload);break}case"queue_snapshot_query":{this.emit("queueSnapshotQuery",e.payload);break}case"control_share_set":{this.emit("shareSet",e.payload);break}case"agent_profile_push":{this.emit("profilePush",e.payload);break}case"kicked":{const t=e.payload;if(this.emit("kicked",t),this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("kicked"),this.outboundBuffer.length=0,t?.reason===S){if(this.agentDeleted=!0,this.reconnecting=!1,this.ws){try{this.ws.close(4001,"kicked")}catch{}this.ws=null}a.error("aibot",`kicked: agent deleted on platform agent=${this.config.clientType}:${this.config.agentId}, reconnect disabled`),this.emit("agentDeleted",{source:"kicked",reason:t.reason});break}if(this.reconnectAttempts=Math.max(this.reconnectAttempts,3),this.ws){try{this.ws.close(4001,"kicked")}catch{}this.ws=null}break}case"error":{const t=e.payload,s=[t.ref_cmd?`ref_cmd=${t.ref_cmd}`:"",t.ref_id?`ref_id=${t.ref_id}`:""].filter(Boolean).join(" ");this.emitClientError(new Error(`Server error: code=${t.code} msg=${t.msg}${s?` ${s}`:""}`));break}case"agent_invoke_result":{this.handleInvokeResult(e.payload);break}case"mcp_frame":{const t=e.payload;this.emit("mcpFrame",t.session_id??"",t.frame??null);break}case"send_ack":break;case"send_nack":{const t=e.payload;if(t.code===4003&&e.seq>0){const s=this.seqEventMap.get(e.seq);s&&(this.seqEventMap.delete(e.seq),this.purgeBufferedStreamChunks(s),a.warn("aibot",`stream chunk rejected (4003), purging buffered chunks for event=${s}`),this.emit("streamRejected",s,t.code))}break}case"local_action_ack":break;default:break}}sendEventAck(e){this.sendPacket("event_ack",e)||a.warn("aibot",`event_ack NOT sent (ws not open) event=${e.event_id} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}sendStreamChunk(e){!e.delta_content&&!e.is_finish&&(a.warn("aibot",`stream_chunk delta_content empty, patched to newline event=${e.event_id??""} session=${e.session_id} chunk_seq=${e.chunk_seq} is_finish=${e.is_finish}`),e={...e,delta_content:`
|
|
2
|
+
`}),this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)||a.warn("aibot",`send_msg NOT sent (ws not open) event=${e.event_id??""} session=${e.session_id??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}editMsg(e){this.sendPacket("edit_msg",e)}sendEventResult(e){if(!this.ws||this.ws.readyState!==m.OPEN){this.sendPacket("event_result",e);return}this.sendEventResultReliable(e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendSessionActivitySet(e){this.sendPacket("session_activity_set",e)}sendCodexEvent(e){this.sendPacket("codex_event",e)}sendUpdateBindingCard(e){this.sendPacket("update_binding_card",e)}sendSkillsUpdate(e){this.sendPacket("agent_skills_update",e)}sendPing(){this.sendPacket("ping",{})}sendEventState(e){this.sendPacket("event_state",e)}sendEventCancelResult(e){this.sendPacket("event_cancel_result",e)}sendQueueClearResult(e){this.sendPacket("queue_clear_result",e)}sendQueueReorderResult(e){this.sendPacket("queue_reorder_result",e)}sendQueueSnapshot(e){this.sendPacket("queue_snapshot",e)}agentInvoke(e,t,s=15e3){return new Promise((i,n)=>{const h=b(),c=Math.max(1e3,Math.min(s,6e4)),o=setTimeout(()=>{this.pendingInvokes.delete(h),n(new Error(`agent_invoke timeout: ${e}`))},c);this.pendingInvokes.set(h,{resolve:i,reject:n,timer:o}),this.sendPacket("agent_invoke",{invoke_id:h,action:e,params:t,timeout_ms:c})})}sendMcpFrame(e,t){this.sendPacket("mcp_frame",{session_id:e,frame:t})}request(e,t,s){return new Promise((i,n)=>{const h=++this.seq,c=setTimeout(()=>{this.pendingRequests.delete(h),n(new Error(`request timeout: ${e} (expected ${s.expected.join("/")})`))},s.timeoutMs);this.pendingRequests.set(h,{expected:s.expected,resolve:i,reject:n,timer:c}),this.sendPacket(e,t,h)||(this.pendingRequests.delete(h),clearTimeout(c),n(new Error(`send failed: ${e}`)))})}async sendStreamChunkRequest(e,t=2e4){return this.request("client_stream_chunk",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendText(e,t=2e4){return this.request("send_msg",{msg_type:1,...e},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendMedia(e,t=2e4){return this.request("send_msg",{...e,msg_type:2},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async editMessage(e,t=2e4){return this.request("edit_msg",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async deleteMessage(e,t,s=2e4){return this.request("delete_msg",{session_id:e,msg_id:t},{expected:["send_ack","send_nack","error"],timeoutMs:s})}async sendEventResultRequest(e,t=5e3){return this.request("event_result",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}disconnect(){a.info("aibot",`disconnect() agent=${this.config.clientType}:${this.config.agentId} wasConnected=${this.connected} reconnecting=${this.reconnecting} reconnectAttempts=${this.reconnectAttempts}`),this.connected=!1,this.everConnected=!1,this.reconnecting=!1,this.reconnectAttempts=0,this.stopHeartbeat(),this.rejectAllPendingInvokes("disconnect"),this.rejectAllPendingRequests("disconnect"),this.outboundBuffer.length=0,this.ws&&(this.ws.close(1e3,"client disconnect"),this.ws=null)}static MAX_CONSECUTIVE_AUTH_FAILURES=5;async attemptReconnect(){if(this.reconnecting||this.agentDeleted)return;this.reconnecting=!0,a.info("aibot",`attemptReconnect start agent=${this.config.clientType}:${this.config.agentId} fromAttempts=${this.reconnectAttempts}`),this.emit("disconnected");let e=0;for(;this.reconnecting;){const t=Math.min(1e3*2**this.reconnectAttempts,3e4),s=Math.floor(t*.2*Math.random());if(this.reconnectAttempts++,await new Promise(i=>setTimeout(i,t+s)),!this.reconnecting)return;try{await this.connect();const i=this.reconnectAttempts;this.reconnectAttempts=0,this.reconnecting=!1,a.info("aibot",`reconnect succeeded agent=${this.config.clientType}:${this.config.agentId} attempt=${i}`);return}catch(i){if(this.ws){try{this.ws.close()}catch{}this.ws=null}const n=i instanceof Error?i.message:String(i);if(a.warn("aibot",`reconnect failed agent=${this.config.clientType}:${this.config.agentId} attempt=${this.reconnectAttempts} err=${n}`),i instanceof $){this.agentDeleted=!0,this.reconnecting=!1,a.error("aibot",`reconnect aborted: agent deleted on platform agent=${this.config.clientType}:${this.config.agentId}`),this.emit("agentDeleted",{source:"auth_ack",code:k});return}if(/Auth failed/i.test(n)){if(e++,e>=f.MAX_CONSECUTIVE_AUTH_FAILURES){this.reconnecting=!1,a.error("aibot",`reconnect giving up after ${e} consecutive auth failures agent=${this.config.clientType}:${this.config.agentId}`);return}}else e=0}}}sendPacket(e,t,s){if(this.ws&&this.ws.readyState===m.OPEN){const i=this.ws.bufferedAmount>f.BACKPRESSURE_THRESHOLD;if(!i||!f.DROPPABLE_COMMANDS.has(e)){if(i&&f.DROPPABLE_COMMANDS.has(e))return!1;const n=s??++this.seq;if(e==="client_stream_chunk"&&t&&typeof t=="object"){const c=t.event_id;if(c&&(this.seqEventMap.set(n,c),this.seqEventMap.size>200)){const o=this.seqEventMap.keys().next().value;o!==void 0&&this.seqEventMap.delete(o)}}const h={cmd:e,seq:n,payload:t};this.packetLog?.logOutboundPacket(e,n,t,"sent");try{const c=this.ws.readyState,o=this.ws.bufferedAmount;return this.ws.send(JSON.stringify(h),d=>{if(e==="event_result"){const r=t;d?a.warn("aibot",`event_result ws send callback failed event=${r.event_id??""} status=${r.status??""} seq=${n} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`event_result ws send callback ok event=${r.event_id??""} status=${r.status??""} seq=${n} readyState=${c} bufferedAmount=${o}`)}else if(e==="client_stream_chunk"){const r=t;d?a.warn("aibot",`stream_chunk ws send failed event=${r.event_id??""} session=${r.session_id??""} seq=${n} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`stream_chunk ws send ok event=${r.event_id??""} session=${r.session_id??""} seq=${n} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} client_msg_id=${r.client_msg_id??""} quoted_message_id=${r.quoted_message_id??""} readyState=${c} bufferedAmount=${o}`)}else if(e==="event_ack"){const r=t;d?a.warn("aibot",`event_ack ws send failed event=${r.event_id??""} seq=${n} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`event_ack ws send ok event=${r.event_id??""} seq=${n} readyState=${c} bufferedAmount=${o}`)}else if(e==="send_msg"){const r=t;d?a.warn("aibot",`send_msg ws send failed event=${r.event_id??""} session=${r.session_id??""} seq=${n} readyState=${c} bufferedAmount=${o} err=${d.message}`):a.info("aibot",`send_msg ws send ok event=${r.event_id??""} session=${r.session_id??""} seq=${n} readyState=${c} bufferedAmount=${o}`)}else if(d){const r=t;a.warn("aibot",`${e} ws send failed seq=${n} session=${r.session_id??""} event=${r.event_id??""} client_msg_id=${r.client_msg_id??""} readyState=${c} bufferedAmount=${o} err=${d.message}`)}}),!0}catch(c){return this.emitClientError(new Error(`sendPacket failed: ${c}`)),!1}}}if(f.DROPPABLE_COMMANDS.has(e))return this.packetLog?.logOutboundPacket(e,s??0,t,"dropped"),!1;if(s!==void 0)return this.packetLog?.logOutboundPacket(e,s,t,"dropped"),!1;if(this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&(this.outboundBuffer=this.outboundBuffer.filter(i=>f.BUFFER_OVERFLOW_RETAIN_COMMANDS.has(i.cmd)),this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&this.outboundBuffer.shift()),this.outboundBuffer.push({cmd:e,payload:t}),this.packetLog?.logOutboundPacket(e,s??0,t,"buffered"),e==="client_stream_chunk"){const i=t;a.info("aibot",`stream_chunk buffered (ws not open) event=${i.event_id??""} session=${i.session_id??""} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}return!1}async sendEventResultReliable(e){const t=this.ackPolicy?.max_retries??3,s=this.ackPolicy?.push_ack_timeout_ms??5e3,i=750;for(let n=1;n<=t;n++){const h=this.ws?.readyState??-1,c=this.ws?.bufferedAmount??0;a.info("aibot",`event_result send attempt event=${e.event_id} status=${e.status} attempt=${n}/${t} readyState=${h} bufferedAmount=${c}`);try{const o=await this.sendEventResultRequest(e,s);if(o.cmd==="send_ack"){const r=o.payload;a.info("aibot",`event_result ack event=${e.event_id} status=${e.status} attempt=${n}/${t} ack_event=${r.event_id??""} ack_status=${r.status??""}`);return}const d=o.payload;if(a.warn("aibot",`event_result rejected event=${e.event_id} status=${e.status} attempt=${n}/${t} cmd=${o.cmd} code=${d.code??""} msg=${d.msg??""}${d.ref_cmd?` ref_cmd=${d.ref_cmd}`:""}${d.ref_id?` ref_id=${d.ref_id}`:""}`),d.code===4003){a.warn("aibot",`event_result stopping retries: 4003 ownership denied event=${e.event_id}`);return}return}catch(o){const d=o instanceof Error?o.message:String(o);if(a.warn("aibot",`event_result attempt failed event=${e.event_id} status=${e.status} attempt=${n}/${t} err=${d}`),n===t){this.emitClientError(new Error(`event_result ack failed after ${t} attempts: event=${e.event_id} status=${e.status}`));return}await new Promise(r=>setTimeout(r,i*n))}}}purgeBufferedStreamChunks(e){const t=this.outboundBuffer.length;this.outboundBuffer=this.outboundBuffer.filter(s=>s.cmd!=="client_stream_chunk"?!0:s.payload?.event_id!==e),this.outboundBuffer.length<t&&a.info("aibot",`purged ${t-this.outboundBuffer.length} buffered stream chunks for event=${e}`)}emitClientError(e){if(this.listenerCount("error")===0){a.warn("aibot",`Client error (no listeners): ${e.message}`);return}this.emit("error",e)}flushOutboundBuffer(){if(this.outboundBuffer.length===0||!this.ws||this.ws.readyState!==m.OPEN)return;const e=this.outboundBuffer;this.outboundBuffer=[];for(const{cmd:t,payload:s}of e){const i=++this.seq;if(t==="client_stream_chunk"&&s&&typeof s=="object"){const h=s.event_id;h&&this.seqEventMap.set(i,h)}const n={cmd:t,seq:i,payload:s};try{this.ws.send(JSON.stringify(n))}catch{break}}if(this.seqEventMap.size>200){const t=[...this.seqEventMap.entries()].sort((s,i)=>s[0]-i[0]);this.seqEventMap.clear();for(const[s,i]of t.slice(-100))this.seqEventMap.set(s,i)}}handleInvokeResult(e){const t=this.pendingInvokes.get(e.invoke_id);t&&(this.pendingInvokes.delete(e.invoke_id),clearTimeout(t.timer),e.code===0?t.resolve(e.data??null):t.reject(new Error(`agent_invoke error code=${e.code}: ${e.msg??""}`)))}rejectAllPendingInvokes(e){for(const[,t]of this.pendingInvokes)clearTimeout(t.timer),t.reject(new Error(`agent_invoke canceled: ${e}`));this.pendingInvokes.clear()}rejectAllPendingRequests(e){for(const[,t]of this.pendingRequests)clearTimeout(t.timer),t.reject(new Error(`request canceled: ${e}`));this.pendingRequests.clear()}cleanupSocket(){if(this.ws)try{this.ws.close()}catch{}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatFailures=0,this.heartbeatTimer=setInterval(()=>{this.connected&&this.request("ping",{ts:Date.now()},{expected:["pong"],timeoutMs:5e3}).then(()=>{this.heartbeatFailures=0}).catch(()=>{this.connected&&(this.heartbeatFailures++,!(this.heartbeatFailures<f.HEARTBEAT_MAX_FAILURES)&&(this.cleanupSocket(),this.attemptReconnect()))})},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{f as AibotClient};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
class r{client;_status="ready";connectedAt;listeners=[];authAck=null;constructor(e){this.client=e,this.connectedAt=Date.now(),this.addInternalListener("close",()=>{this._status!=="closing"&&(this._status="reconnecting")}),this.addInternalListener("auth",t=>{this._status="ready",t&&(this.authAck=t)}),this.addInternalListener("disconnected",()=>{this._status!=="closing"&&this._status!=="closed"&&(this._status="reconnecting")})}get status(){return this._status}getStatusSnapshot(){return{status:this._status,connectedAt:this.connectedAt,reconnectAttempts:0}}sendEventAck(e){this.client.sendEventAck(e)}sendStreamChunk(e){this.client.sendStreamChunk(e)}sendMsg(e){this.client.sendMsg(e)}editMsg(e){this.client.editMsg(e)}sendEventResult(e){this.client.sendEventResult(e)}sendLocalActionResult(e){this.client.sendLocalActionResult(e)}sendEventStopAck(e){this.client.sendEventStopAck(e)}sendEventStopResult(e){this.client.sendEventStopResult(e)}sendSessionActivitySet(e){this.client.sendSessionActivitySet(e)}sendCodexEvent(e){this.client.sendCodexEvent(e)}sendUpdateBindingCard(e){this.client.sendUpdateBindingCard(e)}sendSkillsUpdate(e){this.client.sendSkillsUpdate(e)}sendPing(){this.client.sendPing()}sendEventState(e){this.client.sendEventState(e)}sendEventCancelResult(e){this.client.sendEventCancelResult(e)}sendQueueClearResult(e){this.client.sendQueueClearResult(e)}sendQueueSnapshot(e){this.client.sendQueueSnapshot(e)}agentInvoke(e,t,s){return this.client.agentInvoke(e,t,s)}sendStreamChunkRequest(e,t){return this.client.sendStreamChunkRequest(e,t)}sendText(e,t){return this.client.sendText(e,t)}sendMedia(e,t){return this.client.sendMedia(e,t)}editMessage(e,t){return this.client.editMessage(e,t)}deleteMessage(e,t,s){return this.client.deleteMessage(e,t,s)}sendEventResultRequest(e,t){return this.client.sendEventResultRequest(e,t)}sendCodexEventReliable(e,t){return this.client.request("codex_event",e,{expected:["send_ack","send_nack","error"],timeoutMs:t??2e4})}onEvent(e){return this.subscribe("event",e)}onLocalAction(e){return this.subscribe("localAction",e)}onStop(e){return this.subscribe("stop",e)}onRevoke(e){return this.subscribe("revoke",e)}onEdit(e){return this.subscribe("edit",e)}onEventCancel(e){return this.subscribe("eventCancel",e)}onQueueClear(e){return this.subscribe("queueClear",e)}onQueueSnapshotQuery(e){return this.subscribe("queueSnapshotQuery",e)}onKicked(e){return this.subscribe("kicked",e)}onAgentDeleted(e){return this.subscribe("agentDeleted",e)}onShareSet(e){return this.subscribe("shareSet",e)}onProfilePush(e){return this.subscribe("profilePush",e)}onError(e){return this.subscribe("error",e)}onClose(e){return this.subscribe("close",e)}onDisconnected(e){return this.subscribe("disconnected",e)}onStreamRejected(e){return this.subscribe("streamRejected",e)}onReconnected(e){return this.subscribe("auth",e)}disconnect(){this._status="closing",this.removeAllListeners(),this.client.disconnect(),this._status="closed"}sendMcpFrame(e,t){this.client.sendMcpFrame(e,t)}onMcpFrame(e){return this.subscribe("mcpFrame",e)}subscribe(e,t){return this.client.on(e,t),this.listeners.push({event:e,handler:t}),()=>{this.client.removeListener(e,t);const s=this.listeners.findIndex(n=>n.event===e&&n.handler===t);s>=0&&this.listeners.splice(s,1)}}addInternalListener(e,t){this.client.on(e,t),this.listeners.push({event:e,handler:t})}removeAllListeners(){for(const{event:e,handler:t}of this.listeners)this.client.removeListener(e,t);this.listeners.length=0}}export{r as AibotConnectionHandleImpl};
|
|
1
|
+
class r{client;_status="ready";connectedAt;listeners=[];authAck=null;constructor(e){this.client=e,this.connectedAt=Date.now(),this.addInternalListener("close",()=>{this._status!=="closing"&&(this._status="reconnecting")}),this.addInternalListener("auth",t=>{this._status="ready",t&&(this.authAck=t)}),this.addInternalListener("disconnected",()=>{this._status!=="closing"&&this._status!=="closed"&&(this._status="reconnecting")})}get status(){return this._status}getStatusSnapshot(){return{status:this._status,connectedAt:this.connectedAt,reconnectAttempts:0}}sendEventAck(e){this.client.sendEventAck(e)}sendStreamChunk(e){this.client.sendStreamChunk(e)}sendMsg(e){this.client.sendMsg(e)}editMsg(e){this.client.editMsg(e)}sendEventResult(e){this.client.sendEventResult(e)}sendLocalActionResult(e){this.client.sendLocalActionResult(e)}sendEventStopAck(e){this.client.sendEventStopAck(e)}sendEventStopResult(e){this.client.sendEventStopResult(e)}sendSessionActivitySet(e){this.client.sendSessionActivitySet(e)}sendCodexEvent(e){this.client.sendCodexEvent(e)}sendUpdateBindingCard(e){this.client.sendUpdateBindingCard(e)}sendSkillsUpdate(e){this.client.sendSkillsUpdate(e)}sendPing(){this.client.sendPing()}sendEventState(e){this.client.sendEventState(e)}sendEventCancelResult(e){this.client.sendEventCancelResult(e)}sendQueueClearResult(e){this.client.sendQueueClearResult(e)}sendQueueReorderResult(e){this.client.sendQueueReorderResult(e)}sendQueueSnapshot(e){this.client.sendQueueSnapshot(e)}agentInvoke(e,t,s){return this.client.agentInvoke(e,t,s)}sendStreamChunkRequest(e,t){return this.client.sendStreamChunkRequest(e,t)}sendText(e,t){return this.client.sendText(e,t)}sendMedia(e,t){return this.client.sendMedia(e,t)}editMessage(e,t){return this.client.editMessage(e,t)}deleteMessage(e,t,s){return this.client.deleteMessage(e,t,s)}sendEventResultRequest(e,t){return this.client.sendEventResultRequest(e,t)}sendCodexEventReliable(e,t){return this.client.request("codex_event",e,{expected:["send_ack","send_nack","error"],timeoutMs:t??2e4})}onEvent(e){return this.subscribe("event",e)}onLocalAction(e){return this.subscribe("localAction",e)}onStop(e){return this.subscribe("stop",e)}onRevoke(e){return this.subscribe("revoke",e)}onEdit(e){return this.subscribe("edit",e)}onEventCancel(e){return this.subscribe("eventCancel",e)}onQueueClear(e){return this.subscribe("queueClear",e)}onQueueReorder(e){return this.subscribe("queueReorder",e)}onQueueSnapshotQuery(e){return this.subscribe("queueSnapshotQuery",e)}onKicked(e){return this.subscribe("kicked",e)}onAgentDeleted(e){return this.subscribe("agentDeleted",e)}onShareSet(e){return this.subscribe("shareSet",e)}onProfilePush(e){return this.subscribe("profilePush",e)}onError(e){return this.subscribe("error",e)}onClose(e){return this.subscribe("close",e)}onDisconnected(e){return this.subscribe("disconnected",e)}onStreamRejected(e){return this.subscribe("streamRejected",e)}onReconnected(e){return this.subscribe("auth",e)}disconnect(){this._status="closing",this.removeAllListeners(),this.client.disconnect(),this._status="closed"}sendMcpFrame(e,t){this.client.sendMcpFrame(e,t)}onMcpFrame(e){return this.subscribe("mcpFrame",e)}subscribe(e,t){return this.client.on(e,t),this.listeners.push({event:e,handler:t}),()=>{this.client.removeListener(e,t);const s=this.listeners.findIndex(n=>n.event===e&&n.handler===t);s>=0&&this.listeners.splice(s,1)}}addInternalListener(e,t){this.client.on(e,t),this.listeners.push({event:e,handler:t})}removeAllListeners(){for(const{event:e,handler:t}of this.listeners)this.client.removeListener(e,t);this.listeners.length=0}}export{r as AibotConnectionHandleImpl};
|
|
@@ -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=b(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 F="17e5f671-d194-4dfb-9706-5516cb48c098";function q(){return(process.env.KIMI_CODE_OAUTH_HOST||process.env.KIMI_OAUTH_HOST||"https://auth.kimi.com").replace(/\/+$/,"")}const Y=30*1e3;async function ne(o){const e=$(o);if(e.apiKey)return e;const i=await Z(o);return{baseUrl:e.baseUrl,apiKey:i}}async function Z(o){const e=o||b(I(),".kimi"),i=b(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=b(e,"credentials","kimi-code.lock.lock");if(V(r))try{const n=$(o);if(n.apiKey)return n.apiKey;let s="";try{s=y(b(e,"device_id"),"utf8").trim()}catch{}const c=await fetch(`${q()}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",...s?{"X-Msh-Device-Id":s}:{}},body:new URLSearchParams({client_id:F,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 m=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(m)&&m>0?m: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(m)&&m>0?m:900};return z(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=C(o);return Date.now()-e.mtimeMs<Y?!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=p(a.percentage)??0,s=typeof a.nextResetTime=="number"?a.nextResetTime:Number.MAX_SAFE_INTEGER,c=s===Number.MAX_SAFE_INTEGER?null:w(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=p(l.limit)??1,m=p(l.remaining)??0,h=S(l.resetTime),_=Math.max(0,u-m);r.push({name:"five_hour",label:"5h limit",usedPercent:u>0?Math.round(_/u*1e4)/100:0,resetsAt:h})}const s=a.usage;if(s){const c=p(s.limit)??1,l=p(s.remaining)??0,u=S(s.resetTime),m=Math.max(0,c-l);r.push({name:"weekly_limit",label:"Weekly limit",usedPercent:c>0?Math.round(m/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 v(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 m=await r.text().catch(()=>"");return d(i,t,`API error (HTTP ${r.status}): ${m.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 m=p(u.current_interval_total_count)??0,h=p(u.current_interval_usage_count)??0,_=typeof u.end_time=="number"?u.end_time:null;m>0&&c.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(h/m*1e4)/100,resetsAt:_!==null?w(_):null});const g=p(u.current_weekly_total_count)??0,H=p(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(H/g*1e4)/100,resetsAt:P!==null?w(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=p(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=p(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=p(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 R(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=p(r.total_credits)??0,s=p(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 U(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=(p(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 G(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=>G(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,K=300*1e3;async function W(o){const e=o.slice(0,8),i=T.get(e);if(i&&Date.now()-i.timestamp<=K){const r=[i.providerId],n=await Promise.allSettled(r.map(s=>B(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=>B(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 B(o,e){try{switch(o){case"zhipu":return E(e);case"kimi":return L(e);case"minimax_cn":return v(e,!0);case"minimax_en":return v(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 R(e);case"novita":return U(e)}}catch{return null}}async function G(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"),m=p(l.total_balance);return{provider:"deepseek",providerLabel:"DeepSeek",planName:null,tiers:[],balance:{remaining:m??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 m=p(u.limit)??1,h=p(u.remaining)??0,_=S(u.resetTime),g=Math.max(0,m-h);s.push({name:"five_hour",label:"5h limit",usedPercent:m>0?Math.round(g/m*1e4)/100:0,resetsAt:_})}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=p(s.total_credits)??0,l=p(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=p(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=[],m=p(l.current_interval_total_count)??0,h=p(l.current_interval_usage_count)??0;return m>0&&u.push({name:"five_hour",label:"5h limit",usedPercent:Math.round(h/m*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=p(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=(p(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 re(o,e){if(!e.trim())return{provider:"unknown",providerLabel:"Unknown",planName:null,tiers:[],balance:null,success:!1,error:"API key is empty"};const i=D(o);if(i)switch(i.id){case"zhipu":return E(e);case"kimi":return L(e);case"minimax_cn":return v(e,!0);case"minimax_en":return v(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 R(e);case"novita":return U(e)}const t=await J(o,e);if(t)return t;const a=await W(e);return a||{provider:"unknown",providerLabel:"Unknown",planName:null,tiers:[],balance:null,success:!1,error:`Could not identify provider for base URL: ${o}`}}export{D as detectProvider,re as queryProviderQuota,$ as readKimiProviderSettings,ne as resolveKimiProviderSettings};
|
|
1
|
+
import{readFileSync as y,writeFileSync as D,mkdirSync as N,rmdirSync as x,statSync as G}from"node:fs";import{homedir as z}from"node:os";import{join as _}from"node:path";function F(o){const e=o.toLowerCase();return e.includes("open.bigmodel.cn")||e.includes("bigmodel.cn")||e.includes("api.z.ai")?{id:"zhipu",label:"Zhipu GLM"}:e.includes("api.kimi.com")?{id:"kimi",label:"Kimi"}:e.includes("api.minimaxi.com")?{id:"minimax_cn",label:"MiniMax"}:e.includes("api.minimax.io")?{id:"minimax_en",label:"MiniMax"}:e.includes("api.deepseek.com")?{id:"deepseek",label:"DeepSeek"}:e.includes("api.stepfun.ai")||e.includes("api.stepfun.com")?{id:"stepfun",label:"StepFun"}:e.includes("api.siliconflow.cn")?{id:"siliconflow_cn",label:"SiliconFlow"}:e.includes("api.siliconflow.com")?{id:"siliconflow_en",label:"SiliconFlow"}:e.includes("openrouter.ai")?{id:"openrouter",label:"OpenRouter"}:e.includes("api.novita.ai")?{id:"novita",label:"Novita AI"}:null}const f=1e4;function d(o,e,i){return{provider:o,providerLabel:e,planName:null,tiers:[],balance:null,success:!1,error:i}}function m(o){if(typeof o=="number")return o;if(typeof o=="string"){const e=Number(o);return Number.isFinite(e)?e:null}return null}function v(o){if(!Number.isFinite(o)||o<=0)return null;try{return new Date(o).toISOString()}catch{return null}}function S(o){if(typeof o=="string")return o;if(typeof o=="number"){const e=o<1e12?o*1e3:o;return v(e)}return null}function I(){return(process.env.KIMI_CODE_HOME||"").trim()||_(z(),".kimi-code")}function $(o){const e=o||I(),i=300*1e3;let t;try{const r=_(e,"config.toml"),n=y(r,"utf8"),s=n.indexOf('[providers."managed:kimi-code"]');if(s>=0){const c=n.indexOf(`
|
|
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};
|
|
@@ -4,43 +4,48 @@ description: Install a Grix egg skill package into your own skill directory. Use
|
|
|
4
4
|
trigger: 当收到含「技能包: <URL>」的安装指令、要求把虾蛋技能包装进自己的技能目录时
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Grix Egg —
|
|
7
|
+
# Grix Egg — Skill Package Self-Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
For an agent like you, incubating an egg means exactly one thing: **installing a
|
|
10
|
+
skill package into your own skill directory** so you can use it from then on.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
When you receive an install instruction containing a `技能包: <URL>` line, use
|
|
13
|
+
this skill to install it, self-check, and send back an install-status card. The
|
|
14
|
+
whole job is done by you on this machine — no other agent installs it for you,
|
|
15
|
+
and the main agent cannot touch your skill directory.
|
|
12
16
|
|
|
13
|
-
##
|
|
17
|
+
## What the instruction gives you
|
|
14
18
|
|
|
15
|
-
- `技能包: <URL>`
|
|
16
|
-
- `install_id: <ID>`
|
|
17
|
-
- `grix agent id: <ID>`
|
|
19
|
+
- `技能包: <URL>` — download URL of the skill package to install (always present).
|
|
20
|
+
- `install_id: <ID>` — the ID of this install; echo it verbatim in the status card (always present).
|
|
21
|
+
- `grix agent id: <ID>` — your own grix agent id; use it as `target_agent_id` in the status card (may be present).
|
|
18
22
|
|
|
19
|
-
##
|
|
23
|
+
## What you must do
|
|
20
24
|
|
|
21
|
-
1.
|
|
22
|
-
2.
|
|
23
|
-
3.
|
|
24
|
-
4.
|
|
25
|
-
5.
|
|
26
|
-
6.
|
|
27
|
-
7.
|
|
25
|
+
1. Extract the download URL after `技能包:`.
|
|
26
|
+
2. Determine your own skill directory: it is **the parent of the directory this grix-egg skill lives in**. E.g. if this skill is at `~/.codex/skills/grix-egg/`, the skill directory is `~/.codex/skills/`; at `~/.claude/skills/grix-egg/` it is `~/.claude/skills/`, and so on.
|
|
27
|
+
3. Download and unpack the skill package.
|
|
28
|
+
4. Copy every unpacked skill (any directory containing a `SKILL.md`) into your skill directory, whole directory at a time.
|
|
29
|
+
5. Confirm the new skill's `SKILL.md` now exists in your skill directory.
|
|
30
|
+
6. **Send back the install-status card** (see below) so the platform can close out this install as success/failure.
|
|
31
|
+
7. Then tell the user, in one natural-language sentence, which skill(s) were installed.
|
|
28
32
|
|
|
29
|
-
##
|
|
33
|
+
## Steps 2–5: install (run in shell)
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
Replace the two angle-bracket placeholders with real values, then run.
|
|
36
|
+
`SKILLS_DIR` is the parent of the directory this skill lives in.
|
|
32
37
|
|
|
33
38
|
```bash
|
|
34
39
|
set -e
|
|
35
|
-
URL="
|
|
36
|
-
SKILLS_DIR="
|
|
40
|
+
URL="<skill package URL>"
|
|
41
|
+
SKILLS_DIR="<absolute path of your skill directory>"
|
|
37
42
|
|
|
38
43
|
TMP="$(mktemp -d)"
|
|
39
44
|
curl -fsSL "$URL" -o "$TMP/skill.zip"
|
|
40
45
|
mkdir -p "$TMP/unpacked"
|
|
41
46
|
unzip -o -q "$TMP/skill.zip" -d "$TMP/unpacked"
|
|
42
47
|
|
|
43
|
-
#
|
|
48
|
+
# Copy every skill directory containing a SKILL.md into the skill directory
|
|
44
49
|
found=0
|
|
45
50
|
while IFS= read -r -d '' f; do
|
|
46
51
|
d="$(dirname "$f")"
|
|
@@ -48,7 +53,7 @@ while IFS= read -r -d '' f; do
|
|
|
48
53
|
found=1
|
|
49
54
|
done < <(find "$TMP/unpacked" -name SKILL.md -print0)
|
|
50
55
|
|
|
51
|
-
#
|
|
56
|
+
# Fallback: SKILL.md sits loose at the unpack root — create a skill directory named after its `name` and move everything in
|
|
52
57
|
if [ "$found" -eq 0 ] && [ -f "$TMP/unpacked/SKILL.md" ]; then
|
|
53
58
|
name="$(grep -m1 '^name:' "$TMP/unpacked/SKILL.md" | sed 's/^name:[[:space:]]*//')"
|
|
54
59
|
[ -z "$name" ] && name="egg-skill"
|
|
@@ -56,35 +61,38 @@ if [ "$found" -eq 0 ] && [ -f "$TMP/unpacked/SKILL.md" ]; then
|
|
|
56
61
|
cp -R "$TMP/unpacked/." "$SKILLS_DIR/$name/"
|
|
57
62
|
fi
|
|
58
63
|
|
|
59
|
-
#
|
|
64
|
+
# Self-check: list the skill directory and confirm the new skill is in place
|
|
60
65
|
ls -1 "$SKILLS_DIR"
|
|
61
66
|
rm -rf "$TMP"
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
##
|
|
69
|
+
## Step 6: send back the install-status card
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
After installing and self-checking, **send a separate, standalone message**
|
|
72
|
+
whose content is exactly the single card-link line below (the message contains
|
|
73
|
+
only this line — no other text):
|
|
67
74
|
|
|
68
|
-
-
|
|
75
|
+
- Success:
|
|
69
76
|
```
|
|
70
|
-
grix://card/egg_install_status?status=success&install_id=<install_id>&target_agent_id=<grix agent id>&summary
|
|
77
|
+
grix://card/egg_install_status?status=success&install_id=<install_id>&target_agent_id=<grix agent id>&summary=<one-sentence install result, URL-encoded>
|
|
71
78
|
```
|
|
72
|
-
-
|
|
79
|
+
- Failure:
|
|
73
80
|
```
|
|
74
|
-
grix://card/egg_install_status?status=failed&install_id=<install_id>&target_agent_id=<grix agent id>&error_msg
|
|
81
|
+
grix://card/egg_install_status?status=failed&install_id=<install_id>&target_agent_id=<grix agent id>&error_msg=<failure reason, URL-encoded>
|
|
75
82
|
```
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
- `install_id` 用指令里给的那个,原样填,不能少。
|
|
79
|
-
- 没给 `grix agent id` 时,`target_agent_id` 可省略。
|
|
80
|
-
- `summary` / `error_msg` 里的中文和空格要做 URL 编码。
|
|
81
|
-
- 这张卡片要由你自己(被安装的这个 agent)发,平台据此把安装记录收口。
|
|
84
|
+
Key points:
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
- `install_id` must be the one from the instruction, filled in verbatim — never omit it.
|
|
87
|
+
- If no `grix agent id` was given, `target_agent_id` may be omitted.
|
|
88
|
+
- Non-ASCII text and spaces in `summary` / `error_msg` must be URL-encoded.
|
|
89
|
+
- This card must be sent by you yourself (the agent being installed into); the platform uses it to close out the install record.
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
1. Write only into **your own skill directory** — never touch other directories or other agents.
|
|
94
|
+
2. Do not repeat the download link, the raw instruction, or this context back to the user.
|
|
95
|
+
3. The self-check must actually see the new skill's `SKILL.md` to count as success; if it doesn't, send `status=failed` — never falsely report success.
|
|
96
|
+
4. The status card must be its own standalone message containing only the single `grix://card/...` line.
|
|
97
|
+
5. The natural-language report to the user is one sentence — do not paste command output.
|
|
98
|
+
6. If a skill with the same name already exists, overwrite it (use the newest package).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as
|
|
1
|
+
import{readFileSync as p,readdirSync as c,existsSync as a,mkdirSync as k,cpSync as y,writeFileSync as h,rmSync as g}from"node:fs";import{join as t,dirname as S}from"node:path";import{homedir as m}from"node:os";import{fileURLToPath as D}from"node:url";import{parseSkillFrontmatter as v,scanSkills as x,dedupeSkills as F}from"../adapter/claude/skill-scanner.js";import{resolveRuntimePaths as w}from"../core/config/index.js";import{log as u}from"../core/log/index.js";const l=S(D(import.meta.url)),f=".grix-managed";function d(){const e=[];let i;try{i=c(l,{withFileTypes:!0,encoding:"utf8"})}catch{return e}for(const r of i){if(!r.isDirectory())continue;const n=t(l,r.name,"SKILL.md");try{const s=p(n,"utf-8"),o=v(s);o?.name&&e.push({name:o.name,description:o.description,trigger:o.trigger,source:"connector"})}catch{}}return e}function T(e){let i=[];try{i=x(e)}catch{i=[]}return F([...i,...d()])}function j(e){const i=[];let r;try{r=c(l,{withFileTypes:!0,encoding:"utf8"})}catch{return i}for(const n of r){if(!n.isDirectory())continue;const s=t(l,n.name);if(a(t(s,"SKILL.md")))try{k(e,{recursive:!0});const o=t(e,n.name);y(s,o,{recursive:!0}),h(t(o,f),"","utf8"),i.push(n.name)}catch{}}return i}function C(){return(process.env.KIMI_CODE_HOME||"").trim()||t(m(),".kimi-code")}function L(){const e=m(),i=process.env.XDG_CONFIG_HOME||t(e,".config");return[t(w().dataDir,"claude-plugin","skills"),t(e,".codex","skills"),t(e,".gemini","skills"),t(e,".qwen","skills"),t(e,".pi","agent","skills"),t(e,".codewhale","skills"),t(i,"opencode","skills"),t(e,".cursor","skills"),t(e,".openhuman","skills"),t(e,".kiro","skills"),t(e,".reasonix","skills"),t(C(),"skills")]}function O(){const e=[];for(const i of L()){let r;try{r=c(i,{withFileTypes:!0,encoding:"utf8"})}catch{continue}for(const n of r){if(!n.isDirectory())continue;const s=t(i,n.name);if(a(t(s,f)))try{g(s,{recursive:!0,force:!0}),e.push(s)}catch{}}}return e.length>0&&u.info("default-skills",`Cleaned ${e.length} projected skill dir(s)`),e}function b(){const e=d();if(e.length===0){u.warn("default-skills","No connector system skills found \u2014 dist/default-skills/ may be missing SKILL.md files");return}u.info("default-skills",`Connector system skills available: [${e.map(i=>i.name).join(", ")}]`)}export{T as buildReportedSkills,O as cleanupProjectedSkills,b as logDefaultSkillsCheck,C as resolveKimiCodeHome,d as scanDefaultSkills,j as syncDefaultSkillsToDir};
|
|
@@ -1,74 +1,88 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tailnet-file-share
|
|
3
|
-
description:
|
|
3
|
+
description: Turn a local file into a tailnet link — images render as inline previews, everything else gets a download link. Trigger when the user asks to view, send, share, download, or export any file on this machine.
|
|
4
4
|
trigger: 当用户要求查看、发送、分享、下载、导出本机上的任何文件时
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Tailnet File Share
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. 将工具返回的 `markdown` 字段原样写入你的回复,不要修改
|
|
11
|
-
- 图片文件(jpg/png/gif/webp/svg 等):工具返回 ``,在聊天中直接渲染为图片预览
|
|
12
|
-
- 其他文件:工具返回 `[文件名](url)`,用户点击即可下载
|
|
13
|
-
3. 不要输出原始文件路径,不要尝试粘贴或转述文件内容
|
|
14
|
-
4. 链接只绑本机 tailnet 内网地址、外网访问不到,用户随时点开回看都可以,正常情况下不需要关心也不要提任何有效期问题,直接把链接发给用户即可
|
|
9
|
+
When the user wants to view or obtain a file on this machine (images, logs, reports, build artifacts, exported data, etc.), you must:
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
1. Call the `grix_file_link` tool with the file's absolute path.
|
|
12
|
+
2. Copy the tool's `markdown` field into your reply verbatim — do not modify it.
|
|
13
|
+
- Image files (jpg/png/gif/webp/svg, etc.): the tool returns ``, which renders as an inline image preview in chat.
|
|
14
|
+
- Other files: the tool returns `[filename](url)`, which the user clicks to download.
|
|
15
|
+
3. Do not print the raw file path, and do not paste or paraphrase the file contents.
|
|
16
|
+
4. The link is bound to this machine's tailnet-internal address and is unreachable from the public internet. The user can reopen it any time, so under normal circumstances there is no expiry to worry about and none to mention — just send the link.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
在目标会话里以**原生附件消息**出现(图片/视频在聊天里直接内联展示,无需 tailnet、
|
|
20
|
-
无需装证书)。支持图片/视频/文档/压缩包,单文件最大 50 MB。按场景选择:
|
|
18
|
+
## When to use `grix_file_upload` (native attachment) instead
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
Besides `grix_file_link` there is a `grix_file_upload` tool: it uploads a local
|
|
21
|
+
file to the Grix platform, where it appears in the target session as a **native
|
|
22
|
+
attachment message** (images/videos display inline in chat — no tailnet, no
|
|
23
|
+
certificate install needed). It supports images, videos, documents, and
|
|
24
|
+
archives, up to 50 MB per file. Choose by scenario:
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
- The file should be a proper attachment in the chat (especially sending images or videos for the user to look at) → use `grix_file_upload`.
|
|
27
|
+
- The file is large (>50 MB), a clickable download link is all that's needed, or it's an arbitrary local-path artifact → use `grix_file_link`.
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
工具结果里还会返回 `ca_install_url` —— 这是根 CA 证书的安装链接。
|
|
29
|
+
## HTTPS and the one-time trust-certificate install
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
tailnet IP 变了也不用重装。
|
|
31
|
+
Download links are HTTPS (`https://…`), signed on the fly by a self-signed CA
|
|
32
|
+
built into this machine with a 10-year validity. The tool result also returns
|
|
33
|
+
`ca_install_url` — the install link for the root CA certificate.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
- **The first time you send a file link to a given user** (or whenever they report "the link won't open / certificate not trusted / not secure"), send them `ca_install_url` as the "install trust certificate" link along with the steps below.
|
|
36
|
+
- Each device only needs this **once**; afterwards, for 10 years, every download link from this machine is warning-free, and no reinstall is needed even if the tailnet IP changes.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
到「设置」顶部出现"已下载描述文件",点进去安装 →
|
|
39
|
-
再到「设置 → 通用 → 关于本机 → 证书信任设置」,把该证书开关打开(开启完全信任)。这一步必须做,否则系统仍不信任。
|
|
40
|
-
(iOS 必须用 Safari 打开,其他浏览器不会触发描述文件安装。服务端已按设备自动返回 .mobileconfig,无需手动加参数。)
|
|
41
|
-
- **Mac**:点链接下载 `.crt` → 双击用「钥匙串访问」打开 → 找到该证书 → 双击 →「信任」展开 →「使用此证书时」选"始终信任"。
|
|
42
|
-
- **Android**:点 `ca_install_url` 下载 `.crt` →「设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书」选择刚下载的文件安装
|
|
43
|
-
(新版安卓不支持浏览器内一键安装,必须走系统设置这一步)。
|
|
44
|
-
- **Windows**:双击 `.crt` →「安装证书」→ 选存储位置 →「将所有证书放入下列存储」→ 选"受信任的根证书颁发机构"。
|
|
38
|
+
Install guidance to send the user (pick the one matching their device):
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
- **iPhone / iPad**: open `ca_install_url` in Safari — it prompts "This website is trying to download a configuration profile" → Allow → a "Profile Downloaded" entry appears at the top of Settings; tap it to install → then go to Settings → General → About → Certificate Trust Settings and turn the switch on for that certificate (enable full trust). This step is mandatory, otherwise the system still won't trust it. (iOS must use Safari; other browsers won't trigger profile installation. The server already returns a `.mobileconfig` based on the device, so no manual parameters are needed.)
|
|
41
|
+
- **Mac**: click the link to download the `.crt` → double-click to open it in Keychain Access → find the certificate → double-click → expand "Trust" → set "When using this certificate" to "Always Trust".
|
|
42
|
+
- **Android**: tap `ca_install_url` to download the `.crt` → Settings → Security → Encryption & credentials → Install a certificate → CA certificate, and pick the downloaded file (recent Android versions do not support one-tap install from the browser; the Settings route is required).
|
|
43
|
+
- **Windows**: double-click the `.crt` → "Install Certificate" → choose the store location → "Place all certificates in the following store" → select "Trusted Root Certification Authorities".
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
If the `grix_file_link` call fails (e.g. Tailscale is not connected), tell the
|
|
46
|
+
user the local file path and let them retrieve it themselves.
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
## Scope (important): this CA only covers `grix_file_link`'s own service
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
The built-in CA above and the certificates it issues **only make the download
|
|
51
|
+
service started by `grix_file_link` (this machine's tailnet address) trusted by
|
|
52
|
+
the device**. It does not cover — and cannot fix — other services the user
|
|
53
|
+
starts themselves.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
2. 或者干脆别让用户开自己的 HTTPS,把要分享的文件改走 grix_file_link,由本机内置服务发出来,自动被信任、有效期也合规。
|
|
58
|
-
- 注意:Grix 内置 CA 的**私钥只留在跑连接器的这台机器上、不对外发**(`ca_install_url` 只给公钥证书,不给私钥)。因此只有**与连接器同一台机器**上的服务能借这张 CA 来签证书;其它机器上的服务必须自带 CA,并在每台设备各装一次。
|
|
55
|
+
When the user has **started their own HTTPS service on the tailnet** (a
|
|
56
|
+
self-hosted site / API / media server, etc.) and the browser reports a
|
|
57
|
+
certificate error, be clear that this is a separate matter:
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
- A device only trusts "certificates issued by a CA it has personally installed". A self-hosted service uses its own CA (e.g. mkcert), which the device never installed, so it reports `not trusted / impersonation / NET::ERR_CERT_AUTHORITY_INVALID` — this has nothing to do with whether the Grix CA is installed.
|
|
60
|
+
- There are only two ways to make a self-hosted service trusted:
|
|
61
|
+
1. Have that service sign its certificate with a CA the device already trusts, and keep the leaf certificate's validity **≤ 398 days** (otherwise, even with the CA trusted, it reports `NET::ERR_CERT_VALIDITY_TOO_LONG`);
|
|
62
|
+
2. Or simply don't have the user run their own HTTPS — share the files via `grix_file_link` instead, served by the built-in service, automatically trusted and with compliant validity.
|
|
63
|
+
- Note: the Grix built-in CA's **private key stays only on the machine running the connector and is never handed out** (`ca_install_url` serves the public certificate only, not the private key). So only services **on the same machine as the connector** can borrow this CA for signing; services on other machines must bring their own CA and install it once per device.
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
In short: if it can be sent via `grix_file_link`, send it that way — it's the
|
|
66
|
+
least trouble. Certificate trust for self-hosted services is outside this
|
|
67
|
+
skill's responsibility and must be solved on that service's side.
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
## Two trust paths: inside the Grix app vs. the system browser (diagnose this first)
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
The same link opened **in the Grix app** and **in a system browser (Safari /
|
|
72
|
+
Chrome)** goes through two entirely independent certificate-trust mechanisms,
|
|
73
|
+
with different errors and different fixes. When a user reports "won't open /
|
|
74
|
+
certificate error", first find out which path they were on.
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
- **Inside the Grix app** (image previews, in-app downloads, etc. use the app's own network stack): the app has a built-in trust rule — if the link is a tailnet address (`100.64.0.0/10` range) and the certificate's issuer name contains `Grix Tailnet Local CA`, it is allowed through, and **the user does not need to install any certificate on the device**. So `grix_file_link` links generally work out of the box in the app with zero install. If it still won't open in the app, it's most likely not a tailnet IP, or the issuer name doesn't match — not a "certificate not installed" problem.
|
|
77
|
+
- **System browser (Safari / Chrome)**: this goes through the phone's / computer's system trust store, which has nothing to do with the app's rule. The root CA must be installed as described in *HTTPS and the one-time trust-certificate install* above (on iOS, install the `.mobileconfig` and enable full trust; on Android, go through Settings; and so on). `NET::ERR_CERT_AUTHORITY_INVALID`, "impersonation", and `NET::ERR_CERT_VALIDITY_TOO_LONG` in the user's screenshots are almost always this path — guide them to install the CA.
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
- 需要在 **系统浏览器** 里打开:app 那条规则不起作用,仍需在每台设备装上该服务自己的 CA(见上一节《适用范围》)。
|
|
79
|
+
The same distinction applies to **self-hosted services**, decided by where the user wants to view it:
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
- Only needs to display **inside the Grix app**: the self-hosted service does not have to distribute a private key to the user, and no certificate install is needed — it only needs to ① be served on a tailnet IP, and ② have its CA's issuer name carry `Grix Tailnet Local CA`, and the app will trust it.
|
|
82
|
+
- Needs to open in a **system browser**: the app's rule does not apply; that service's own CA must still be installed on each device (see *Scope* above).
|
|
83
|
+
|
|
84
|
+
Note: the in-app rule is **a string match on the issuer name**, not a binding to
|
|
85
|
+
a specific CA's public-key fingerprint. It is a convenience trade-off within a
|
|
86
|
+
private tailnet only and **is not a hard security boundary** — anyone on the
|
|
87
|
+
same tailnet can have the app trust them simply by naming their CA with the same
|
|
88
|
+
prefix. Do not treat it as a trust root that stands up to an attacker.
|