@nexa26/nexa 0.1.1 → 0.1.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/README.md CHANGED
@@ -2,20 +2,15 @@
2
2
 
3
3
  这是 `Nexa` 的 OpenClaw 原生 channel/plugin 文档。
4
4
 
5
- ## 推荐配置
5
+ ## 推荐接入方式
6
+
7
+ 推荐优先使用 Nexa App 生成的 一键登录命令:
8
+
9
+ ```bash
10
+ openclaw gateway call nexa.login --json --params 'code'
11
+ ```
12
+
13
+ ## 手动配置(fallback)
6
14
 
7
15
  OpenClaw 配置放在 `channels["nexa"]` 下。当前配置请在NEXA APP中获取
8
16
 
9
- ```json
10
- {
11
- "channels": {
12
- "nexa": {
13
- "controlPlane": {
14
- "enabled": true,
15
- "serverUrl": "https://your-api.example.com",
16
- "credentials": "channelKey=ach_xxx;channelSecret=acs_xxx"
17
- }
18
- }
19
- }
20
- }
21
- ```
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{createRuntimeStore as r}from"./runtime-store.js";import{createNexaOpenClawPlugin as t}from"./plugin.js";export default function e(e){const o=r(),i=t({store:o});e.registerChannel({plugin:i}),i.register(e)}
1
+ import{createRuntimeStore as t}from"./runtime-store.js";import{createNexaOpenClawPlugin as r}from"./plugin.js";import{createFileBackedPluginStateStore as e}from"./plugin-state.js";export default function o(o){const i=t(),s=r({store:i,stateStore:e()});o.registerChannel({plugin:s}),s.register(o)}
@@ -0,0 +1 @@
1
+ import{chmod as n,mkdir as e,readFile as t,rename as r,rm as a,writeFile as l}from"node:fs/promises";import{homedir as o}from"node:os";import c from"node:path";export const DEFAULT_STATE_DIR=c.join(o(),".openclaw","extensions","nexa");export const DEFAULT_STATE_PATH=c.join(DEFAULT_STATE_DIR,"state.json");function i(n){return!!n&&"object"==typeof n&&!Array.isArray(n)}function s(n){if(!i(n)||!i(n.controlPlane))return null;const e=n.controlPlane;return e.serverUrl&&e.channelKey&&e.channelSecret?{version:Number(n.version||1),controlPlane:{enabled:!1!==e.enabled,serverUrl:String(e.serverUrl),channelKey:String(e.channelKey),channelSecret:String(e.channelSecret)},defaultOpenClawAgentId:n.defaultOpenClawAgentId?String(n.defaultOpenClawAgentId):"main",agentChannelId:n.agentChannelId?String(n.agentChannelId):null,environment:n.environment?String(n.environment):null,issuedAt:n.issuedAt?String(n.issuedAt):null,updatedAt:n.updatedAt?String(n.updatedAt):null}:null}export function createFileBackedPluginStateStore({statePath:o=DEFAULT_STATE_PATH}={}){const i=c.dirname(o);return{path:o,async load(){try{const n=await t(o,"utf8");return s(JSON.parse(n))}catch(n){if("ENOENT"===n?.code)return null;throw n}},async save(t){const a=s({version:1,...t,updatedAt:(new Date).toISOString()});if(!a)throw new Error("invalid_nexa_plugin_state");await e(i,{recursive:!0});const c=`${o}.${process.pid}.${Date.now()}.tmp`;await l(c,`${JSON.stringify(a,null,2)}\n`,"utf8"),await n(c,384),await r(c,o);try{await n(o,384)}catch{}return a},async clear(){try{await a(o)}catch(n){if("ENOENT"!==n?.code)throw n}}}}export function mergeConfigWithPluginState(n={},e){if(!e?.controlPlane)return n;const t=n?.channels||{},r=t.nexa||{},a=r.controlPlane||{};return a.serverUrl&&(a.channelKey||a.credentials)&&(a.channelSecret||a.credentials)?n:{...n,channels:{...t,nexa:{...r,controlPlane:{...a,enabled:!0,serverUrl:e.controlPlane.serverUrl,channelKey:e.controlPlane.channelKey,channelSecret:e.controlPlane.channelSecret,credentials:`channelKey=${e.controlPlane.channelKey};channelSecret=${e.controlPlane.channelSecret}`}}}}}
package/dist/plugin.js CHANGED
@@ -1 +1 @@
1
- import{URL as e}from"node:url";import{DEFAULT_ACCOUNT_ID as t,DEFAULT_CHANNEL_TYPE as n,NEXA_OPENCLAW_CHANNEL_ID as o,listAccountIds as a,resolveControlPlane as r,resolveAccount as s}from"./config.js";import{normalizeWebhookEvent as c,resolveOutboundTarget as i}from"./normalize.js";import{createInboundAdapter as l}from"./inbound-adapter.js";import{createDefaultInboundHandler as d}from"./default-inbound-handler.js";import{createRuntimeBridge as u}from"./runtime-bridge.js";import{authenticateControlPlane as g,fetchControlPlaneAgents as p,fetchControlPlaneBindings as h,fetchControlPlaneConfig as m,isUnauthorizedControlPlaneError as f,sendHeartbeat as I,sendReply as w}from"./server-client.js";import{createServerDispatch as y}from"./server-dispatch.js";import{createServerSocket as k}from"./server-socket.js";import{computeNextHeartbeatDelay as b,computeNextPollDelay as x}from"./transport-timing.js";async function M(e,t,n,o){const{sendTextToStream:a}=await import("./stream-client.js");return a(e,t,n,o)}async function v(e,t,n){const{verifyStreamWebhook:o}=await import("./stream-client.js");return o(e,t,n)}async function S(e,t,n){const{ensureStreamUser:o}=await import("./stream-client.js");return o(e,t,n)}function T(e,t,n){return e.statusCode=t,e.setHeader("content-type","application/json"),e.end(JSON.stringify(n)),!0}function $(e,t){return t?.transport?.preferredTransport||e?.transport?.preferredTransport||"long-poll"}function C(t,n,o){const a=o?.transport?.websocketUrl||n?.transport?.websocketUrl;if(!a)return null;if(a.startsWith("ws://")||a.startsWith("wss://"))return a;const r=new e(t.serverUrl),s="https:"===r.protocol?"wss:":"ws:";return new e(a,`${s}//${r.host}`).toString()}export function createNexaOpenClawPlugin({store:P,inboundHandler:U,verifyWebhook:A=v,sendText:B=M,ensureUser:R=S,createServerSocketClient:j=k}={}){const N=new Map,O=new Map,V=new Map,W=new Map,D=new Map,G=new Map,L=new Map;let H=null;function J(e){const n=e||t,o=G.get(n);if(o)return o;const a={timer:null,attempt:0,connecting:!1,stopped:!1};return G.set(n,a),a}function z(e){const n=G.get(e||t);n?.timer&&(clearTimeout(n.timer),n.timer=null)}function _(e){const n=e||t,o=O.get(n);o&&(o.stopped=!0,o.timer&&clearTimeout(o.timer),O.delete(n))}function q(e,n,o,a){const r=e||t;if(O.has(r))return;const s={stopped:!1,timer:null,emptyStreak:0,failureStreak:0};O.set(r,s);const c=e=>{s.stopped||(s.timer=setTimeout(i,Math.max(0,e)))},i=async()=>{if(s.stopped)return;if(D.get(r))return void c(o.pollIntervalMs);D.set(r,!0);let e=o.pollIntervalMs,t=0;try{t=await n.pollOnce(),s.failureStreak=0,s.emptyStreak=t>0?0:s.emptyStreak+1}catch(e){if(s.failureStreak+=1,f(e)){const e=L.get(r);try{await(e?.refreshRemoteState?.())}catch(e){a?.error?.(`[nexa] poll re-auth failed: ${e?.message||e}`)}}a?.error?.(`[nexa] inbox poll failed: ${e?.message||e}`)}finally{e=x({baseMs:o.pollIntervalMs,idleMinMs:o.pollIdleMinMs,idleMaxMs:o.pollIdleMaxMs,emptyStreak:s.emptyStreak,failureStreak:s.failureStreak,deliveredCount:t,jitterRatio:o.pollJitterRatio}),D.set(r,!1)}s.stopped||O.get(r)!==s||c(e)};c(0)}async function E(e){const{accountId:t,websocketUrl:n,token:o,logger:a,serverDispatch:r,controlPlane:s}=e;if(!n||V.has(t))return!1;const c=j({websocketUrl:n,token:o,logger:a,dispatch:r,onDisconnect:()=>{V.delete(t),!G.get(t)?.stopped&&L.has(t)&&(q(t,r,s,a),K(t))}});await c.connect(),V.set(t,c),_(t);const i=J(t);return i.attempt=0,i.connecting=!1,z(t),!0}function K(e){const n=e||t,o=L.get(n);if(!o?.shouldUseWebSocket)return;const a=J(n);if(a.stopped||a.timer||a.connecting||V.has(n))return;a.attempt+=1;const r=Math.min(o.controlPlane.websocketReconnectMaxMs,o.controlPlane.websocketReconnectBaseMs*2**Math.max(a.attempt-1,0));a.timer=setTimeout(async()=>{if(a.timer=null,!a.stopped&&!V.has(n)){a.connecting=!0;try{await(o.refreshRemoteState?.()),await E(o)}catch(e){o.logger?.error?.(`[nexa] websocket reconnect failed: ${e?.message||e}`)}finally{a.connecting=!1,a.stopped||V.has(n)||K(n)}}},r)}return{id:o,meta:{id:o,label:"Nexa",selectionLabel:"Nexa OpenClaw",detailLabel:"Nexa OpenClaw Channel",docsPath:"/channels/nexa",blurb:"Nexa native channel plugin for OpenClaw groups.",aliases:["nexa","getstream"]},capabilities:{chatTypes:["direct","group"]},config:{listAccountIds:a,resolveAccount:s},outbound:{deliveryMode:"direct",sendText:async e=>{const o=s(e?.config||{},e?.accountId||t),a=i(e,o.defaultChannelType||n);return B(o,a,e?.text||"",{userId:e?.userId,custom:e?.custom})}},gateway:{startAccount:async({accountId:e,cfg:n,log:o,abortSignal:a})=>{try{const i=s(n,e||t),l=r(n),d=H,k=d?.logger||o||console;if(k?.info?.(`[nexa] startAccount accountId=${i.accountId} controlPlaneEnabled=${l.enabled} serverUrl=${l.serverUrl||"missing"} channelKeyPresent=${!!l.channelKey} channelSecretPresent=${!!l.channelSecret}`),l.enabled){let e=null,n=null,a=null,r=[],s=[],c=null;const x=(e=[])=>{P?.replaceBindings?.(e.map(e=>({accountId:i.accountId,channelType:e.externalChannelType||i.defaultChannelType,channelId:e.externalChannelId,route:{...e.routeConfig||{},nexaAgentId:e.agentId,openclawAgentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||null,agentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||e.routeConfig?.agentId||e.agentId,replyUserId:e.routeConfig?.replyUserId||e.providerBotUserId},metadata:{requireMention:e.requireMention,providerBotUserId:e.providerBotUserId}})))},M=async()=>c||(c=(async()=>{k?.info?.(`[nexa] authenticating control plane accountId=${i.accountId} serverUrl=${l.serverUrl}`),e=await g({...l,pluginVersion:"0.1.0"}),n=e.token,[a,r,s]=await Promise.all([m(l,n),p(l,n),h(l,n)]),P?.setRemoteState?.({token:n,controlPlane:l,auth:e,config:a,agents:r,bindings:s}),x(s||[]);const t=L.get(i.accountId);return t&&(t.token=n,t.websocketUrl=C(l,e,a)),k?.info?.(`[nexa] control plane ready accountId=${i.accountId} preferredTransport=${$(e,a)} websocketUrlPresent=${!!C(l,e,a)} bindings=${s?.length||0} agents=${r?.length||0}`),{auth:e,token:n,remoteConfig:a,remoteAgents:r,remoteBindings:s}})().finally(()=>{c=null}),c);await M();const v=async(e,t,o,a={})=>{const r={channelType:t.channelType,channelId:t.channelId,text:o,userId:a.userId,userName:a.userName,userImage:a.userImage,custom:a.custom};try{const e=await w(l,n,r);return{ok:!0,messageId:e.messageId,createdAt:e.createdAt}}catch(e){if(!f(e))throw e;await M();const t=await w(l,n,r);return{ok:!0,messageId:t.messageId,createdAt:t.createdAt}}},S=async()=>({ok:!0}),T=u({api:d,logger:d?.logger||k||console,sendText:v,ensureUser:S}),U=y({controlPlane:l,getToken:()=>n,store:P,runtimeBridge:T,logger:d?.logger||k||console});!function({accountId:e,controlPlane:n,logger:o,refreshRemoteState:a,getToken:r,getRemoteConfig:s}){const c=e||t;if(N.has(c))return;const i={stopped:!1,timer:null,failureStreak:0};N.set(c,i);const l=e=>{i.stopped||(i.timer=setTimeout(d,Math.max(0,e)))},d=async()=>{if(i.stopped)return;if(W.get(c))return void l(n.heartbeatIntervalMs);W.set(c,!0);try{const e=s(),t=await I(n,r(),{pluginVersion:"0.1.0",configVersion:e?.configVersion,bindingVersion:e?.bindingVersion});i.failureStreak=0,t?.configVersion&&t.configVersion!==e?.configVersion&&await(a?.())}catch(e){if(i.failureStreak+=1,f(e))try{await(a?.())}catch(e){o?.error?.(`[nexa] heartbeat re-auth failed: ${e?.message||e}`)}o?.error?.(`[nexa] heartbeat failed: ${e?.message||e}`)}finally{W.set(c,!1)}const e=b({baseMs:n.heartbeatIntervalMs,failureStreak:i.failureStreak,maxMs:n.heartbeatFailureMaxMs,jitterRatio:n.heartbeatJitterRatio});i.stopped||N.get(c)!==i||l(e)};l(n.heartbeatIntervalMs)}({accountId:i.accountId,controlPlane:l,logger:d?.logger||o||console,refreshRemoteState:M,getToken:()=>n,getRemoteConfig:()=>a});const A=$(e,a),B=C(l,e,a);if(J(i.accountId).stopped=!1,L.set(i.accountId,{accountId:i.accountId,controlPlane:l,token:n,websocketUrl:B,serverDispatch:U,logger:d?.logger||k||console,shouldUseWebSocket:"websocket"===A&&!!B,refreshRemoteState:M}),k?.info?.(`[nexa] transport selected accountId=${i.accountId} preferred=${A} websocketUrlPresent=${!!B}`),"websocket"===A&&B)try{await E({accountId:i.accountId,controlPlane:l,token:n,websocketUrl:B,serverDispatch:U,logger:d?.logger||k||console})}catch(e){o?.error?.(`[nexa] websocket transport unavailable, falling back to poll: ${e?.message||e}`),q(i.accountId,U,l,o),K(i.accountId)}else q(i.accountId,U,l,o)}return o?.info?.(`[nexa] account ready: ${i.accountId}`),await(c=a,c?c.aborted?Promise.resolve():new Promise(e=>{c.addEventListener("abort",e,{once:!0})}):new Promise(()=>{})),{ok:!0,accountId:i.accountId,stopped:!0}}catch(e){throw o?.error?.(`[nexa] startAccount failed: ${e?.stack||e?.message||e}`),e}var c},stopAccount:async({accountId:e,log:n})=>{const o=e||t;J(o).stopped=!0,z(o),L.delete(o),N.get(o)&&function(e){const n=e||t,o=N.get(n);o&&(o.stopped=!0,o.timer&&clearTimeout(o.timer),N.delete(n))}(o),W.delete(o),_(o),D.delete(o);const a=V.get(o);return a&&(a.close?.(),V.delete(o)),G.delete(o),n?.info?.(`[nexa] account stopped: ${o}`),{ok:!0,accountId:o}}},register(r){H=r,r.registerHttpRoute({path:"/nexa/webhook",auth:"plugin",match:"exact",handler:async(n,o)=>{if("POST"!==n.method)return T(o,405,{ok:!1,error:"method_not_allowed"});const a=function(n){const o=n.headers.host?`http://${n.headers.host}`:"http://localhost";return new e(n.url||"/",o).searchParams.get("accountId")||t}(n),i=s(r.config||{},a),g=await async function(e){return"string"==typeof e.rawBody?e.rawBody:await new Promise((t,n)=>{const o=[];e.on("data",e=>o.push(Buffer.isBuffer(e)?e:Buffer.from(e))),e.on("end",()=>t(Buffer.concat(o).toString("utf8"))),e.on("error",n)})}(n),p=i.webhookSignatureHeader||"x-signature",h=n.headers?.[p],m=Array.isArray(h)?h[0]:h;if(!m||!await A(i,g,m))return T(o,401,{ok:!1,error:"invalid_signature"});const f=JSON.parse(g),I=c(f),w=I.channelId?P?.getBinding?.({accountId:a,channelType:I.channelType,channelId:I.channelId}):null;P?.setLastWebhook?.({receivedAt:(new Date).toISOString(),accountId:a,account:i,normalized:I,binding:w});const y=u({api:r,logger:r.logger||console,sendText:B,ensureUser:R}),k=U||d({logger:r.logger||console,runtimeBridge:y}),b=l({inboundHandler:k,store:P,logger:r.logger||console}),x=await b.dispatch({accountId:a,account:i,binding:w,normalized:I,raw:f});return r.logger?.info?.(`[nexa] webhook accepted type=${I.eventType} channel=${I.channelType}:${I.channelId||"unknown"}`),T(o,200,{ok:!0,accountId:a,accepted:!0,eventType:I.eventType,channelId:I.channelId,delivered:x.delivered,dispatchReason:x.reason})}}),r.registerGatewayMethod("nexa.bindChannel",({params:e,respond:o})=>{const a=P?.bindChannel?.({accountId:e?.accountId||t,channelType:e?.channelType||n,channelId:e?.channelId,route:e?.route||{},metadata:e?.metadata||null});o(!0,{ok:!0,binding:a})}),r.registerGatewayMethod("nexa.unbindChannel",({params:e,respond:o})=>{const a=P?.unbindChannel?.({accountId:e?.accountId||t,channelType:e?.channelType||n,channelId:e?.channelId});o(!0,{ok:!0,removed:a})}),r.registerGatewayMethod("nexa.bindings",({respond:e})=>{e(!0,{ok:!0,bindings:P?.getBindings?.()||[]})}),r.registerGatewayMethod("nexa.status",({respond:e})=>{e(!0,{ok:!0,channelId:o,accounts:a(r.config||{}),bindings:P?.getBindings?.()||[],controlPlane:P?.getRemoteState?.()||null})}),r.registerGatewayMethod("nexa.lastWebhook",({respond:e})=>{e(!0,{ok:!0,webhook:P?.getLastWebhook?.()||null})}),r.registerGatewayMethod("nexa.lastDispatch",({respond:e})=>{e(!0,{ok:!0,dispatch:P?.getLastDispatch?.()||null})}),r.registerChannel({plugin:this})}}}export const createStreamChatPlugin=createNexaOpenClawPlugin;
1
+ import{URL as e}from"node:url";import{Buffer as t}from"node:buffer";import{createDecipheriv as n,scryptSync as r}from"node:crypto";import{createRequire as o}from"node:module";import{DEFAULT_ACCOUNT_ID as a,DEFAULT_CHANNEL_TYPE as s,NEXA_OPENCLAW_CHANNEL_ID as c,listAccountIds as l,resolveControlPlane as i,resolveAccount as d}from"./config.js";import{normalizeWebhookEvent as u,resolveOutboundTarget as g}from"./normalize.js";import{createInboundAdapter as p}from"./inbound-adapter.js";import{createDefaultInboundHandler as h}from"./default-inbound-handler.js";import{createRuntimeBridge as f}from"./runtime-bridge.js";import{authenticateControlPlane as m,fetchControlPlaneAgents as w,fetchControlPlaneBindings as I,fetchControlPlaneConfig as y,exchangeBootstrap as b,isUnauthorizedControlPlaneError as k,sendHeartbeat as v,sendReply as x}from"./server-client.js";import{createServerDispatch as S}from"./server-dispatch.js";import{createServerSocket as C}from"./server-socket.js";import{computeNextHeartbeatDelay as M,computeNextPollDelay as T}from"./transport-timing.js";import{mergeConfigWithPluginState as A}from"./plugin-state.js";const P=o(import.meta.url),{version:_}=P("../package.json");async function U(e,t,n,r){const{sendTextToStream:o}=await import("./stream-client.js");return o(e,t,n,r)}async function $(e,t,n){const{verifyStreamWebhook:r}=await import("./stream-client.js");return r(e,t,n)}async function O(e,t,n){const{ensureStreamUser:r}=await import("./stream-client.js");return r(e,t,n)}function j(e,t,n){return e.statusCode=t,e.setHeader("content-type","application/json"),e.end(JSON.stringify(n)),!0}function R(e,t){return t?.transport?.preferredTransport||e?.transport?.preferredTransport||"long-poll"}function E(t,n,r){const o=r?.transport?.websocketUrl||n?.transport?.websocketUrl;if(!o)return null;if(o.startsWith("ws://")||o.startsWith("wss://"))return o;const a=new e(t.serverUrl),s="https:"===a.protocol?"wss:":"ws:";return new e(o,`${s}//${a.host}`).toString()}export function createNexaOpenClawPlugin({store:o,inboundHandler:P,verifyWebhook:B=$,sendText:N=U,ensureUser:W=O,createServerSocketClient:D=C,stateStore:G={path:null,load:async()=>null,save:async()=>{throw new Error("nexa_state_store_not_configured")},clear:async()=>{}}}={}){const V=new Map,L=new Map,J=new Map,q=new Map,H=new Map,K=new Map,z=new Map,F=new Map;let Q=null;function X(e){const t=e||a,n=K.get(t);if(n)return n;const r={timer:null,attempt:0,connecting:!1,stopped:!1};return K.set(t,r),r}function Y(e){const t=K.get(e||a);t?.timer&&(clearTimeout(t.timer),t.timer=null)}async function Z(e,t){const n=e||a,r=F.get(n);if(r){r.controller.abort(),await r.promise.catch(()=>{}),F.delete(n);try{await oe.gateway.stopAccount({accountId:n,log:t})}catch{}}}function ee(e){const t=e||a,n=L.get(t);n&&(n.stopped=!0,n.timer&&clearTimeout(n.timer),L.delete(t))}function te(e,t,n,r){const o=e||a;if(L.has(o))return;const s={stopped:!1,timer:null,emptyStreak:0,failureStreak:0};L.set(o,s);const c=e=>{s.stopped||(s.timer=setTimeout(l,Math.max(0,e)))},l=async()=>{if(s.stopped)return;if(H.get(o))return void c(n.pollIntervalMs);H.set(o,!0);let e=n.pollIntervalMs,a=0;try{a=await t.pollOnce(),s.failureStreak=0,s.emptyStreak=a>0?0:s.emptyStreak+1}catch(e){if(s.failureStreak+=1,k(e)){const e=z.get(o);try{await(e?.refreshRemoteState?.())}catch(e){r?.error?.(`[nexa] poll re-auth failed: ${e?.message||e}`)}}r?.error?.(`[nexa] inbox poll failed: ${e?.message||e}`)}finally{e=T({baseMs:n.pollIntervalMs,idleMinMs:n.pollIdleMinMs,idleMaxMs:n.pollIdleMaxMs,emptyStreak:s.emptyStreak,failureStreak:s.failureStreak,deliveredCount:a,jitterRatio:n.pollJitterRatio}),H.set(o,!1)}s.stopped||L.get(o)!==s||c(e)};c(0)}async function ne(e){const{accountId:t,websocketUrl:n,token:r,logger:o,serverDispatch:a,controlPlane:s}=e;if(!n||J.has(t))return!1;const c=D({websocketUrl:n,token:r,logger:o,dispatch:a,onDisconnect:()=>{J.delete(t),!K.get(t)?.stopped&&z.has(t)&&(te(t,a,s,o),re(t))}});await c.connect(),J.set(t,c),ee(t);const l=X(t);return l.attempt=0,l.connecting=!1,Y(t),!0}function re(e){const t=e||a,n=z.get(t);if(!n?.shouldUseWebSocket)return;const r=X(t);if(r.stopped||r.timer||r.connecting||J.has(t))return;r.attempt+=1;const o=Math.min(n.controlPlane.websocketReconnectMaxMs,n.controlPlane.websocketReconnectBaseMs*2**Math.max(r.attempt-1,0));r.timer=setTimeout(async()=>{if(r.timer=null,!r.stopped&&!J.has(t)){r.connecting=!0;try{await(n.refreshRemoteState?.()),await ne(n)}catch(e){n.logger?.error?.(`[nexa] websocket reconnect failed: ${e?.message||e}`)}finally{r.connecting=!1,r.stopped||J.has(t)||re(t)}}},o)}const oe={id:c,meta:{id:c,label:"Nexa",selectionLabel:"Nexa OpenClaw",detailLabel:"Nexa OpenClaw Channel",docsPath:"/channels/nexa",blurb:"Nexa native channel plugin for OpenClaw groups.",aliases:["nexa","getstream"]},capabilities:{chatTypes:["direct","group"]},config:{listAccountIds:l,resolveAccount:d},outbound:{deliveryMode:"direct",sendText:async e=>{const t=d(e?.config||{},e?.accountId||a),n=g(e,t.defaultChannelType||s);return N(t,n,e?.text||"",{userId:e?.userId,custom:e?.custom})}},gateway:{startAccount:async({accountId:e,cfg:t,log:n,abortSignal:r})=>{try{const c=await G.load(),l=A(t,c),u=d(l,e||a),g=i(l),p=Q,h=p?.logger||n||console;if(h?.info?.(`[nexa] startAccount accountId=${u.accountId} controlPlaneEnabled=${g.enabled} serverUrl=${g.serverUrl||"missing"} channelKeyPresent=${!!g.channelKey} channelSecretPresent=${!!g.channelSecret}`),g.enabled){let e=null,t=null,r=null,s=[],c=[],l=null;const i=(e=[])=>{o?.replaceBindings?.(e.map(e=>({accountId:u.accountId,channelType:e.externalChannelType||u.defaultChannelType,channelId:e.externalChannelId,route:{...e.routeConfig||{},nexaAgentId:e.agentId,openclawAgentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||null,agentId:e.routeConfig?.openclawAgentId||e.openclawAgentId||e.routeConfig?.agentId||e.agentId,replyUserId:e.routeConfig?.replyUserId||e.providerBotUserId},metadata:{requireMention:e.requireMention,providerBotUserId:e.providerBotUserId}})))},d=async()=>l||(l=(async()=>{h?.info?.(`[nexa] authenticating control plane accountId=${u.accountId} serverUrl=${g.serverUrl}`),e=await m({...g,pluginVersion:_}),t=e.token,[r,s,c]=await Promise.all([y(g,t),w(g,t),I(g,t)]),o?.setRemoteState?.({token:t,controlPlane:g,auth:e,config:r,agents:s,bindings:c}),i(c||[]);const n=z.get(u.accountId);return n&&(n.token=t,n.websocketUrl=E(g,e,r)),h?.info?.(`[nexa] control plane ready accountId=${u.accountId} preferredTransport=${R(e,r)} websocketUrlPresent=${!!E(g,e,r)} bindings=${c?.length||0} agents=${s?.length||0}`),{auth:e,token:t,remoteConfig:r,remoteAgents:s,remoteBindings:c}})().finally(()=>{l=null}),l);await d();const b=async(e,n,r,o={})=>{const a={channelType:n.channelType,channelId:n.channelId,text:r,userId:o.userId,userName:o.userName,userImage:o.userImage,custom:o.custom};try{const e=await x(g,t,a);return{ok:!0,messageId:e.messageId,createdAt:e.createdAt}}catch(e){if(!k(e))throw e;await d();const n=await x(g,t,a);return{ok:!0,messageId:n.messageId,createdAt:n.createdAt}}},C=async()=>({ok:!0}),T=f({api:p,logger:p?.logger||h||console,sendText:b,ensureUser:C}),A=S({controlPlane:g,getToken:()=>t,store:o,runtimeBridge:T,logger:p?.logger||h||console});!function({accountId:e,controlPlane:t,logger:n,refreshRemoteState:r,getToken:o,getRemoteConfig:s}){const c=e||a;if(V.has(c))return;const l={stopped:!1,timer:null,failureStreak:0};V.set(c,l);const i=e=>{l.stopped||(l.timer=setTimeout(d,Math.max(0,e)))},d=async()=>{if(l.stopped)return;if(q.get(c))return void i(t.heartbeatIntervalMs);q.set(c,!0);try{const e=s(),n=await v(t,o(),{pluginVersion:_,configVersion:e?.configVersion,bindingVersion:e?.bindingVersion});l.failureStreak=0,n?.configVersion&&n.configVersion!==e?.configVersion&&await(r?.())}catch(e){if(l.failureStreak+=1,k(e))try{await(r?.())}catch(e){n?.error?.(`[nexa] heartbeat re-auth failed: ${e?.message||e}`)}n?.error?.(`[nexa] heartbeat failed: ${e?.message||e}`)}finally{q.set(c,!1)}const e=M({baseMs:t.heartbeatIntervalMs,failureStreak:l.failureStreak,maxMs:t.heartbeatFailureMaxMs,jitterRatio:t.heartbeatJitterRatio});l.stopped||V.get(c)!==l||i(e)};i(t.heartbeatIntervalMs)}({accountId:u.accountId,controlPlane:g,logger:p?.logger||n||console,refreshRemoteState:d,getToken:()=>t,getRemoteConfig:()=>r});const P=R(e,r),U=E(g,e,r);if(X(u.accountId).stopped=!1,z.set(u.accountId,{accountId:u.accountId,controlPlane:g,token:t,websocketUrl:U,serverDispatch:A,logger:p?.logger||h||console,shouldUseWebSocket:"websocket"===P&&!!U,refreshRemoteState:d}),h?.info?.(`[nexa] transport selected accountId=${u.accountId} preferred=${P} websocketUrlPresent=${!!U}`),"websocket"===P&&U)try{await ne({accountId:u.accountId,controlPlane:g,token:t,websocketUrl:U,serverDispatch:A,logger:p?.logger||h||console})}catch(e){n?.error?.(`[nexa] websocket transport unavailable, falling back to poll: ${e?.message||e}`),te(u.accountId,A,g,n),re(u.accountId)}else te(u.accountId,A,g,n)}return n?.info?.(`[nexa] account ready: ${u.accountId}`),await(s=r,s?s.aborted?Promise.resolve():new Promise(e=>{s.addEventListener("abort",e,{once:!0})}):new Promise(()=>{})),{ok:!0,accountId:u.accountId,stopped:!0}}catch(e){throw n?.error?.(`[nexa] startAccount failed: ${e?.stack||e?.message||e}`),e}var s},stopAccount:async({accountId:e,log:t})=>{const n=e||a;X(n).stopped=!0,Y(n),z.delete(n),V.get(n)&&function(e){const t=e||a,n=V.get(t);n&&(n.stopped=!0,n.timer&&clearTimeout(n.timer),V.delete(t))}(n),q.delete(n),ee(n),H.delete(n);const r=J.get(n);return r&&(r.close?.(),J.delete(n)),K.delete(n),t?.info?.(`[nexa] account stopped: ${n}`),{ok:!0,accountId:n}}},register(i){Q=i,i.registerHttpRoute({path:"/nexa/webhook",auth:"plugin",match:"exact",handler:async(n,r)=>{if("POST"!==n.method)return j(r,405,{ok:!1,error:"method_not_allowed"});const s=function(t){const n=t.headers.host?`http://${t.headers.host}`:"http://localhost";return new e(t.url||"/",n).searchParams.get("accountId")||a}(n),c=d(i.config||{},s),l=await async function(e){return"string"==typeof e.rawBody?e.rawBody:await new Promise((n,r)=>{const o=[];e.on("data",e=>o.push(t.isBuffer(e)?e:t.from(e))),e.on("end",()=>n(t.concat(o).toString("utf8"))),e.on("error",r)})}(n),g=c.webhookSignatureHeader||"x-signature",m=n.headers?.[g],w=Array.isArray(m)?m[0]:m;if(!w||!await B(c,l,w))return j(r,401,{ok:!1,error:"invalid_signature"});const I=JSON.parse(l),y=u(I),b=y.channelId?o?.getBinding?.({accountId:s,channelType:y.channelType,channelId:y.channelId}):null;o?.setLastWebhook?.({receivedAt:(new Date).toISOString(),accountId:s,account:c,normalized:y,binding:b});const k=f({api:i,logger:i.logger||console,sendText:N,ensureUser:W}),v=P||h({logger:i.logger||console,runtimeBridge:k}),x=p({inboundHandler:v,store:o,logger:i.logger||console}),S=await x.dispatch({accountId:s,account:c,binding:b,normalized:y,raw:I});return i.logger?.info?.(`[nexa] webhook accepted type=${y.eventType} channel=${y.channelType}:${y.channelId||"unknown"}`),j(r,200,{ok:!0,accountId:s,accepted:!0,eventType:y.eventType,channelId:y.channelId,delivered:S.delivered,dispatchReason:S.reason})}}),i.registerGatewayMethod("nexa.bindChannel",({params:e,respond:t})=>{const n=o?.bindChannel?.({accountId:e?.accountId||a,channelType:e?.channelType||s,channelId:e?.channelId,route:e?.route||{},metadata:e?.metadata||null});t(!0,{ok:!0,binding:n})}),i.registerGatewayMethod("nexa.unbindChannel",({params:e,respond:t})=>{const n=o?.unbindChannel?.({accountId:e?.accountId||a,channelType:e?.channelType||s,channelId:e?.channelId});t(!0,{ok:!0,removed:n})}),i.registerGatewayMethod("nexa.bindings",({respond:e})=>{e(!0,{ok:!0,bindings:o?.getBindings?.()||[]})}),i.registerGatewayMethod("nexa.login",async({params:e,respond:s})=>{try{const c=e?.code,l=e?.bootstrap,d=c?function(e){if("string"!=typeof e||!e.trim())throw new Error("code_required");if(!e.startsWith("ocb_"))throw new Error("invalid_code_prefix");const o=t.from(e.slice(4),"base64url");if(o.length<=28)throw new Error("invalid_code_payload");const a=o.subarray(0,12),s=o.subarray(12,28),c=o.subarray(28),l=n("aes-256-gcm",r(process.env.OPENCLAW_BOOTSTRAP_CODE_SECRET||"nexa-opaque-code-v1","openclaw-nexa-bootstrap-code",32),a);l.setAuthTag(s);const i=t.concat([l.update(c),l.final()]).toString("utf8"),d=JSON.parse(i);if(!d||"object"!=typeof d)throw new Error("invalid_code_payload");return d}(c):function(e){if("string"!=typeof e||!e.trim())throw new Error("bootstrap_required");const n=e.split(".");if(n.length<2)throw new Error("invalid_bootstrap");const r=JSON.parse(function(e){const n=String(e||"").replace(/-/g,"+").replace(/_/g,"/"),r=n.padEnd(4*Math.ceil(n.length/4),"=");return t.from(r,"base64").toString("utf8")}(n[1]));if(!r||"object"!=typeof r)throw new Error("invalid_bootstrap_payload");return r}(l);if(!d.bootstrapServer)throw new Error("bootstrap_server_missing");if(!d.exp||1e3*Number(d.exp)<=Date.now())throw new Error(c?"code_expired":"bootstrap_expired");if(c&&(!d.exchangeCode||!d.jti))throw new Error("invalid_code_exchange_payload");if(!c&&"openclaw-plugin"!==d.aud)throw new Error("invalid_bootstrap_audience");const u=await b(d.bootstrapServer,c?{exchangeCode:d.exchangeCode,jti:d.jti}:{bootstrap:l}),g=await G.save({controlPlane:{enabled:!0,serverUrl:u.serverUrl,channelKey:u.channelKey,channelSecret:u.channelSecret},defaultOpenClawAgentId:u.defaultOpenClawAgentId||"main",agentChannelId:u.agentChannelId||d.agentChannelId||null,environment:u.environment||d.env||null,issuedAt:(new Date).toISOString()});o?.setRemoteState?.(null),o?.replaceBindings?.([]);const p=A(i.config||{},g);await Z(a,i.logger||console),function(e,t,n){const r=e||a,o=new AbortController,s=oe.gateway.startAccount({accountId:r,cfg:t,log:n,abortSignal:o.signal}).catch(e=>{n?.error?.(`[nexa] managed account start failed: ${e?.message||e}`)}).finally(()=>{const e=F.get(r);e?.controller===o&&F.delete(r)});F.set(r,{controller:o,promise:s})}(a,p,i.logger||console),s(!0,{ok:!0,environment:g.environment,defaultOpenClawAgentId:g.defaultOpenClawAgentId,agentChannelId:g.agentChannelId,serverUrl:g.controlPlane.serverUrl,statePath:G.path||null})}catch(e){s(!1,{ok:!1,error:e?.message||String(e)})}}),i.registerGatewayMethod("nexa.logout",async({respond:e})=>{try{await Z(a,i.logger||console),await G.clear(),o?.setRemoteState?.(null),o?.replaceBindings?.([]),e(!0,{ok:!0})}catch(t){e(!1,{ok:!1,error:t?.message||String(t)})}}),i.registerGatewayMethod("nexa.whoami",async({respond:e})=>{try{const t=await G.load(),n=o?.getRemoteState?.()||null;e(!0,{ok:!0,loggedIn:!!t,environment:t?.environment||null,defaultOpenClawAgentId:t?.defaultOpenClawAgentId||n?.defaultOpenClawAgentId||null,agentChannelId:t?.agentChannelId||null,serverUrl:t?.controlPlane?.serverUrl||null,statePath:G.path||null})}catch(t){e(!1,{ok:!1,error:t?.message||String(t)})}}),i.registerGatewayMethod("nexa.status",({respond:e})=>{e(!0,{ok:!0,channelId:c,accounts:l(i.config||{}),bindings:o?.getBindings?.()||[],controlPlane:o?.getRemoteState?.()||null})}),i.registerGatewayMethod("nexa.lastWebhook",({respond:e})=>{e(!0,{ok:!0,webhook:o?.getLastWebhook?.()||null})}),i.registerGatewayMethod("nexa.lastDispatch",({respond:e})=>{e(!0,{ok:!0,dispatch:o?.getLastDispatch?.()||null})}),i.registerChannel({plugin:this})}};return oe}export const createStreamChatPlugin=createNexaOpenClawPlugin;
@@ -1 +1 @@
1
- function n(n){const e=String(n||"").replace(/\/+$/,"");return e.endsWith("/api/v1/openclaw")?e:`${e}/api/v1/openclaw`}async function e(n,e,t,r){const o=await fetch(e,{method:n,headers:{"content-type":"application/json",...r?{authorization:`Bearer ${r}`}:{}},body:t?JSON.stringify(t):void 0});if(!o.ok){const n=await o.text(),e=new Error(`Request failed ${o.status}: ${n||o.statusText}`);throw e.status=o.status,e}return o.json()}export function isUnauthorizedControlPlaneError(n){return 401===Number(n?.status||n?.statusCode)}export async function authenticateControlPlane(t){return e("POST",`${n(t.serverUrl)}/plugin/auth`,{channelKey:t.channelKey,channelSecret:t.channelSecret,pluginVersion:t.pluginVersion})}export async function fetchControlPlaneConfig(t,r){return e("GET",`${n(t.serverUrl)}/plugin/config`,void 0,r)}export async function fetchControlPlaneAgents(t,r){return e("GET",`${n(t.serverUrl)}/plugin/agents`,void 0,r)}export async function fetchControlPlaneBindings(t,r){return e("GET",`${n(t.serverUrl)}/plugin/bindings`,void 0,r)}export async function sendHeartbeat(t,r,o){return e("POST",`${n(t.serverUrl)}/plugin/heartbeat`,o,r)}export async function pollInbox(t,r){return e("POST",`${n(t.serverUrl)}/plugin/inbox/poll`,{},r)}export async function ackInboxEvent(t,r,o){return e("POST",`${n(t.serverUrl)}/plugin/inbox/${o}/ack`,{},r)}export async function sendReply(t,r,o){return e("POST",`${n(t.serverUrl)}/plugin/reply`,o,r)}
1
+ function n(n){const e=String(n||"").replace(/\/+$/,"");return e.endsWith("/api/v1/openclaw")?e:`${e}/api/v1/openclaw`}async function e(n,e,t,r){const o=await fetch(e,{method:n,headers:{"content-type":"application/json",...r?{authorization:`Bearer ${r}`}:{}},body:t?JSON.stringify(t):void 0});if(!o.ok){const n=await o.text(),e=new Error(`Request failed ${o.status}: ${n||o.statusText}`);throw e.status=o.status,e}return o.json()}export function isUnauthorizedControlPlaneError(n){return 401===Number(n?.status||n?.statusCode)}export async function authenticateControlPlane(t){return e("POST",`${n(t.serverUrl)}/plugin/auth`,{channelKey:t.channelKey,channelSecret:t.channelSecret,pluginVersion:t.pluginVersion})}export async function exchangeBootstrap(t,r){return e("POST",`${n(t)}/bootstrap/exchange`,r)}export async function fetchControlPlaneConfig(t,r){return e("GET",`${n(t.serverUrl)}/plugin/config`,void 0,r)}export async function fetchControlPlaneAgents(t,r){return e("GET",`${n(t.serverUrl)}/plugin/agents`,void 0,r)}export async function fetchControlPlaneBindings(t,r){return e("GET",`${n(t.serverUrl)}/plugin/bindings`,void 0,r)}export async function sendHeartbeat(t,r,o){return e("POST",`${n(t.serverUrl)}/plugin/heartbeat`,o,r)}export async function pollInbox(t,r){return e("POST",`${n(t.serverUrl)}/plugin/inbox/poll`,{},r)}export async function ackInboxEvent(t,r,o){return e("POST",`${n(t.serverUrl)}/plugin/inbox/${o}/ack`,{},r)}export async function sendReply(t,r,o){return e("POST",`${n(t.serverUrl)}/plugin/reply`,o,r)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexa26/nexa",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Nexa OpenClaw native channel plugin",