@nexa26/nexa 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ # Nexa OpenClaw Plugin
2
+
3
+ 这是 `Nexa` 的 OpenClaw 原生 channel/plugin 文档。
4
+
5
+ ## 推荐配置
6
+
7
+ OpenClaw 配置放在 `channels["nexa"]` 下。当前配置请在NEXA APP中获取
8
+
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/config.js ADDED
@@ -0,0 +1 @@
1
+ export const NEXA_OPENCLAW_CHANNEL_ID="nexa";export const DEFAULT_ACCOUNT_ID="default";export const DEFAULT_WEBHOOK_PATH="/nexa/webhook";export const DEFAULT_SIGNATURE_HEADER="x-signature";export const DEFAULT_CHANNEL_TYPE="messaging";export const DEFAULT_CHATMODE="oncall";export const DEFAULT_POLL_INTERVAL_MS=5e3;export const DEFAULT_HEARTBEAT_INTERVAL_MS=3e4;export const DEFAULT_WS_RECONNECT_BASE_MS=1e3;export const DEFAULT_WS_RECONNECT_MAX_MS=15e3;export const DEFAULT_POLL_IDLE_MIN_MS=1e4;export const DEFAULT_POLL_IDLE_MAX_MS=2e4;export const DEFAULT_POLL_JITTER_RATIO=.2;export const DEFAULT_HEARTBEAT_FAILURE_MAX_MS=12e4;export const DEFAULT_HEARTBEAT_JITTER_RATIO=.1;function e(e,n=[]){return Array.isArray(e)?e:"string"==typeof e&&e.trim()?e.split(",").map(e=>e.trim()).filter(Boolean):n}export function listAccountIds(e={}){const n=Object.keys(e?.channels?.nexa?.accounts??{});return n.length>0?n:resolveControlPlane(e).enabled?["default"]:[]}export function resolveAccount(n={},t="default"){const o=n?.channels?.nexa?.accounts?.default??{},r=n?.channels?.nexa?.accounts?.[t]??o;return r?{accountId:t,enabled:!1!==r.enabled,apiKey:r.apiKey||process.env.STREAMCHAT_API_KEY||process.env.GETSTREAM_API_KEY,apiSecret:r.apiSecret||process.env.STREAMCHAT_API_SECRET||process.env.GETSTREAM_API_SECRET,appId:r.appId,defaultChannelType:r.defaultChannelType||"messaging",webhookPath:r.webhookPath||"/nexa/webhook",webhookSignatureHeader:(r.webhookSignatureHeader||"x-signature").toLowerCase(),chatmode:r.chatmode||o.chatmode||"oncall",oncharPrefixes:e(r.oncharPrefixes||o.oncharPrefixes),requireMention:r.requireMention??o.requireMention??void 0,allowBots:!0===r.allowBots||!0===o.allowBots}:{accountId:t,enabled:!1}}export function ensureAccountConfig(e){if(!e?.enabled)throw new Error(`StreamChat account ${e?.accountId||"default"} is disabled or missing`);if(!e.apiKey||!e.apiSecret)throw new Error(`Nexa OpenClaw account ${e.accountId} is missing apiKey/apiSecret in channels["nexa"].accounts`);return e}export function getWebhookPath(e={},n="default"){return resolveAccount(e,n).webhookPath}export function resolveControlPlane(e={}){const n=e?.channels?.nexa?.controlPlane??{},t="string"==typeof(o=n.credentials)&&o.trim()?o.split(/[;\n,]+/).map(e=>e.trim()).filter(Boolean).reduce((e,n)=>{const t=n.indexOf("=");if(t<=0)return e;const o=n.slice(0,t).trim(),r=n.slice(t+1).trim();return o&&r?(e[o]=r,e):e},{}):{};var o;const r=n.channelKey||t.channelKey||process.env.NEXA_AGENT_CHANNEL_KEY,s=n.channelSecret||t.channelSecret||process.env.NEXA_AGENT_CHANNEL_SECRET;return{enabled:!0===n.enabled||!!n.serverUrl&&!!r&&!!s||!!process.env.NEXA_SERVER_URL&&!!process.env.NEXA_AGENT_CHANNEL_KEY&&!!process.env.NEXA_AGENT_CHANNEL_SECRET,serverUrl:n.serverUrl||process.env.NEXA_SERVER_URL,channelKey:r,channelSecret:s,pollIntervalMs:Number(n.pollIntervalMs||process.env.NEXA_POLL_INTERVAL_MS||5e3),pollIdleMinMs:Number(n.pollIdleMinMs||process.env.NEXA_POLL_IDLE_MIN_MS||1e4),pollIdleMaxMs:Number(n.pollIdleMaxMs||process.env.NEXA_POLL_IDLE_MAX_MS||2e4),pollJitterRatio:Number(n.pollJitterRatio||process.env.NEXA_POLL_JITTER_RATIO||.2),heartbeatIntervalMs:Number(n.heartbeatIntervalMs||process.env.NEXA_HEARTBEAT_INTERVAL_MS||3e4),heartbeatFailureMaxMs:Number(n.heartbeatFailureMaxMs||process.env.NEXA_HEARTBEAT_FAILURE_MAX_MS||12e4),heartbeatJitterRatio:Number(n.heartbeatJitterRatio||process.env.NEXA_HEARTBEAT_JITTER_RATIO||.1),websocketReconnectBaseMs:Number(n.websocketReconnectBaseMs||process.env.NEXA_WS_RECONNECT_BASE_MS||1e3),websocketReconnectMaxMs:Number(n.websocketReconnectMaxMs||process.env.NEXA_WS_RECONNECT_MAX_MS||15e3)}}
@@ -0,0 +1 @@
1
+ export function createDefaultInboundHandler({logger:e,runtimeBridge:n}={}){return async r=>{const t=r?.normalized||{},o=r?.binding||null;if("message.new"!==t.eventType)return{delivered:!1,reason:"ignored_event"};if(t.isBot&&!r?.account?.allowBots)return{delivered:!1,reason:"ignored_bot"};if(!o)return{delivered:!1,reason:"binding_required"};const i=r?.account?.chatmode||"oncall",a=function(e,n){return void 0!==n?.metadata?.requireMention?!1!==n.metadata.requireMention:void 0!==e?.account?.requireMention?!1!==e.account.requireMention:"onmessage"!==e?.account?.chatmode}(r,o),d=!!(t.mentionedUsers&&t.mentionedUsers.length>0);return"onchar"!==i||d||function(e){const n=e?.account?.oncharPrefixes||[],r=e?.normalized?.text?.trim()||"";return!(!r||0===n.length)&&n.some(e=>r.startsWith(e))}(r)?a&&!d&&"onchar"!==i?{delivered:!1,reason:"mention_required"}:(e?.info?.(`[nexa] inbound message accepted for bound channel ${o.channelType}:${o.channelId}`),n?n.run(r):{delivered:!1,reason:"runtime_not_connected",route:o.route||null}):{delivered:!1,reason:"prefix_required"}}}
@@ -0,0 +1 @@
1
+ export function createInboundAdapter({inboundHandler:e,logger:t,store:r}={}){return{async dispatch(n){if(!e){const e={delivered:!1,reason:"no_inbound_handler"};return r?.setLastDispatch?.({at:(new Date).toISOString(),event:n,result:e}),e}try{const t=await e(n)||{},a={delivered:!1!==t.delivered,...t};return r?.setLastDispatch?.({at:(new Date).toISOString(),event:n,result:a}),a}catch(e){const a={delivered:!1,reason:"dispatch_failed",error:e instanceof Error?e.message:"unknown_error"};return t?.error?.(`[nexa] inbound dispatch failed: ${a.error}`),r?.setLastDispatch?.({at:(new Date).toISOString(),event:n,result:a}),a}}}}
package/dist/index.js ADDED
@@ -0,0 +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)}
@@ -0,0 +1 @@
1
+ export function parseChannelRef(e,n="messaging"){if(!e)return null;if("string"==typeof e){const a=e.indexOf(":");return-1===a?{channelType:n,channelId:e}:{channelType:e.slice(0,a)||n,channelId:e.slice(a+1)}}return"object"==typeof e&&e.channelId?{channelType:e.channelType||n,channelId:e.channelId}:null}export function normalizeWebhookEvent(e){const n=e?.channel?.type||"messaging",a=e?.channel?.id||parseChannelRef(e?.message?.cid,n)?.channelId,s=e?.user||e?.message?.user||{};return{eventType:e?.type||"unknown",messageId:e?.message?.id,text:e?.message?.text||"",channelType:n,channelId:a,senderId:s.id,senderName:s.name||s.username,mentionedUsers:(e?.message?.mentioned_users||[]).map(e=>({id:e.id,name:e.name||e.username})),isBot:Boolean(s.is_bot||e?.message?.custom?.isAiGenerated||"openclaw"===e?.message?.custom?.source),raw:e}}export function resolveOutboundTarget(e,n="messaging"){const a=parseChannelRef(e?.target,n)||parseChannelRef(e?.channel,n)||parseChannelRef(e?.channelRef,n)||(e?.channelId?{channelType:e?.channelType||n,channelId:e.channelId}:null);if(!a?.channelId)throw new Error("Missing Stream channel target for outbound delivery");return a}
package/dist/plugin.js ADDED
@@ -0,0 +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;
@@ -0,0 +1 @@
1
+ import{spawn as e}from"node:child_process";import{existsSync as n,readdirSync as t}from"node:fs";import{dirname as r,join as s}from"node:path";function a(e){const n=e?.binding?.route||{};return n.sessionTarget?n.sessionTarget:`nexa:${e?.accountId||"default"}:${e?.normalized?.channelType||"messaging"}:${e?.normalized?.channelId||"unknown"}`}function o(e){if(!e)return"";if("string"==typeof e)return e.trim();if("string"==typeof e.text)return e.text.trim();if("string"==typeof e.replyText)return e.replyText.trim();if("string"==typeof e.message)return e.message.trim();if("string"==typeof e.content)return e.content.trim();if("string"==typeof e.stdout)return e.stdout.trim();if(Array.isArray(e.payloads)){const n=e.payloads.map(e=>e?.text||e?.content||e?.message||"").join("\n").trim();if(n)return n}if(Array.isArray(e.output)){const n=e.output.map(e=>"string"==typeof e?e:e?.text||e?.content||"").join("\n").trim();if(n)return n}if(e.output&&"string"==typeof e.output.text)return e.output.text.trim();if(Array.isArray(e.choices)&&e.choices[0]?.message?.content)return String(e.choices[0].message.content).trim();if(e.result&&"object"==typeof e.result){const n=o(e.result);if(n)return n}if(e.data&&"object"==typeof e.data){const n=o(e.data);if(n)return n}return""}function i(a,o){return new Promise((i,d)=>{const u=function(e=process.env){const a=e.NVM_DIR?s(e.NVM_DIR,"versions","node"):e.HOME?s(e.HOME,".nvm","versions","node"):null,o=[];if(a&&n(a)){const e=t(a).sort((e,n)=>n.localeCompare(e));for(const n of e)o.push(s(a,n,"bin","openclaw"))}const i=[e.OPENCLAW_CLI_PATH,...o,s(r(process.execPath),"openclaw"),"/opt/homebrew/bin/openclaw","/usr/local/bin/openclaw"];for(const e of i)if(e&&e.includes("/")&&n(e))return e;return"openclaw"}(process.env),c=u.includes("/")?r(u):null,g=c&&n(s(c,"node"))?{...process.env,PATH:process.env.PATH?`${c}:${process.env.PATH}`:c}:process.env,m=["agent","--agent",a.agentId,"--message",a.text,"--json","--to",a.sessionTarget];o?.info?.(`[nexa] invoking CLI fallback: ${u} ${m.join(" ")}`);const l=e(u,m,{stdio:["ignore","pipe","pipe"],env:g});let p="",f="";l.stdout.on("data",e=>{p+=e.toString()}),l.stderr.on("data",e=>{f+=e.toString()}),l.on("error",e=>{d(e)}),l.on("close",e=>{if(0!==e)return void d(new Error(f.trim()||`openclaw agent exited with code ${e}`));const n=p.trim();if(n)try{i(JSON.parse(n))}catch{i({text:n})}else i({text:""})})})}export function createRuntimeBridge({api:e,logger:n,sendText:t,ensureUser:r}={}){const s=new Set;return{async run(d){const u=d?.binding?.route||{},c=u.agentId||u.openclawAgentId;if(!c)return{delivered:!1,reason:"agent_id_required"};const g=d?.normalized||{},m=function(e){const n=e?.binding?.route||{},t=n.agentId||"agent";return{id:n.replyUserId||n.userId||n.botUserId||`openclaw_${String(t).replace(/[^a-zA-Z0-9_-]+/g,"_")}`,name:n.replyName||n.name||n.botName||t,image:n.replyImage||n.image||n.botImage}}(d),l={agentId:c,text:g.text||"",sessionTarget:a(d),metadata:{channel:"nexa",accountId:d?.accountId,channelType:g.channelType,channelId:g.channelId,senderId:g.senderId,senderName:g.senderName,messageId:g.messageId,route:u}};let p=null;try{p=await async function(e,n,t){const r=[["runtime.channels.handleInbound",e?.runtime?.channels?.handleInbound,[n]],["runtime.channels.dispatchInbound",e?.runtime?.channels?.dispatchInbound,[n]],["runtime.messages.handleInbound",e?.runtime?.messages?.handleInbound,[n]],["runtime.messages.dispatchInbound",e?.runtime?.messages?.dispatchInbound,[n]],["runtime.agents.send",e?.runtime?.agents?.send,[{agentId:n.agentId,message:n.text,text:n.text,sessionId:n.sessionTarget,sessionKey:n.sessionTarget,target:n.sessionTarget,metadata:n.metadata}]],["runtime.agent.send",e?.runtime?.agent?.send,[{agentId:n.agentId,message:n.text,text:n.text,sessionId:n.sessionTarget,sessionKey:n.sessionTarget,target:n.sessionTarget,metadata:n.metadata}]],["runtime.runEmbeddedPiAgent",e?.runtime?.runEmbeddedPiAgent,[{agentId:n.agentId,message:n.text,sessionKey:n.sessionTarget,text:n.text,metadata:n.metadata}]]];for(const[e,n,s]of r)if("function"==typeof n)return t?.info?.(`[nexa] invoking runtime strategy ${e}`),{strategy:e,result:await n(...s)};return null}(e,l,n)||{strategy:"cli.openclaw-agent",result:await i(l,n)}}catch(e){return{delivered:!1,reason:"runtime_failed",error:e instanceof Error?e.message:"unknown_runtime_error",route:u}}const f=o(p.result);if(!f)return{delivered:!1,reason:"empty_reply",route:u,strategy:p.strategy};const y=`${d?.account?.accountId||"default"}:${m.id}`;let I=null;if(!s.has(y))try{n?.info?.(`[nexa] ensuring stream user ${m.id}`),await(r?.(d.account,m,u)),s.add(y)}catch(e){I=e,n?.warn?.(`[nexa] ensure user failed for ${m.id}, attempting send anyway: ${e instanceof Error?e.message:"unknown_ensure_user_error"}`)}let h=null;try{n?.info?.(`[nexa] sending reply to stream ${g.channelType}:${g.channelId} as ${m.id}`),h=await t(d.account,{channelType:g.channelType,channelId:g.channelId},f,{userId:m.id,userName:m.name,userImage:m.image,custom:{source:"openclaw",isAiGenerated:!0,openclawAgentId:c}}),s.add(y),n?.info?.(`[nexa] sent reply to stream ${g.channelType}:${g.channelId}, messageId=${h?.messageId||"unknown"}`)}catch(e){return{delivered:!1,reason:"stream_send_failed",route:u,strategy:p.strategy,replyUserId:m.id,error:e instanceof Error?e.message:"unknown_stream_send_error",...I?{ensureUserError:I instanceof Error?I.message:"unknown_ensure_user_error"}:{}}}return{delivered:!0,reason:"runtime_reply_sent",route:u,strategy:p.strategy,replyUserId:m.id,messageId:h?.messageId,...I?{ensureUserError:I instanceof Error?I.message:"unknown_ensure_user_error"}:{}}}}}
@@ -0,0 +1 @@
1
+ export function createRuntimeStore(){let e=null,n=null,t=null;const a=new Map;function c(e,n,t){return`${e}:${n}:${t}`}return{setLastWebhook(n){e=n},getLastWebhook:()=>e,setLastDispatch(e){n=e},getLastDispatch:()=>n,setRemoteState(e){t=e},getRemoteState:()=>t,bindChannel(e){const n=c(e.accountId,e.channelType,e.channelId);return a.set(n,{...e,key:n}),a.get(n)},unbindChannel({accountId:e,channelType:n,channelId:t}){const l=c(e,n,t),o=a.get(l)||null;return a.delete(l),o},getBinding:({accountId:e,channelType:n,channelId:t})=>a.get(c(e,n,t))||null,getBindings:()=>Array.from(a.values()),replaceBindings(e=[]){a.clear();for(const n of e){const e=n.accountId||"default",t=n.channelType||"messaging",l=n.channelId||n.externalChannelId;if(!l)continue;const o=c(e,t,l);a.set(o,{...n,accountId:e,channelType:t,channelId:l,key:o})}return Array.from(a.values())}}}
@@ -0,0 +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)}
@@ -0,0 +1 @@
1
+ import{ackInboxEvent as e,pollInbox as n}from"./server-client.js";export function createServerDispatch({controlPlane:r,getToken:t,store:o,runtimeBridge:a,logger:i}={}){const c=()=>"function"==typeof t?t():void 0;async function s(n){const t=(o?.getRemoteState?.()||{}).config||{};let s;try{s=await a.run({accountId:"default",account:{accountId:"default",enabled:!0,apiKey:t.streamApiKey,apiSecret:t.streamApiSecret,appId:t.streamAppId,defaultChannelType:n?.binding?.externalChannelType||"messaging"},binding:{route:n?.binding?.route||{},metadata:{requireMention:n?.binding?.requireMention}},normalized:n?.normalized||{},raw:n?.rawPayload})}catch(e){s={delivered:!1,reason:"runtime_exception",error:e instanceof Error?e.message:"unknown"},i?.error?.(`[nexa] runtime exception for event ${n.eventId}: ${e instanceof Error?e.message:e}`)}o?.setLastDispatch?.({source:"server-inbox",event:n,result:s});try{const t=s?.error?`, error=${s.error}`:"";i?.info?.(`[nexa] acking inbox event ${n.eventId}, delivered=${!!s?.delivered}, reason=${s?.reason||"unknown"}${t}`),await e(r,c(),n.eventId),i?.info?.(`[nexa] acked inbox event ${n.eventId}`)}catch(e){i?.error?.(`[nexa] failed to ack inbox event ${n.eventId}: ${e instanceof Error?e.message:e}`)}if(!s?.delivered){const e=s?.error?`, error=${s.error}`:"";i?.warn?.(`[nexa] inbox event ${n.eventId} not delivered, reason=${s?.reason||"unknown"}${e}`)}return s}return{dispatchEvent:s,async pollOnce(){const e=await n(r,c()),t=e?.events||[];for(const e of t)await s(e);return t.length}}}
@@ -0,0 +1 @@
1
+ import{createRequire as e}from"node:module";const n=e(import.meta.url);function o(e,o){const{io:t}=function(){const e=["socket.io-client","../../../backend/node_modules/socket.io-client"];for(const o of e)try{return n(o)}catch{continue}throw new Error("Unable to load socket.io-client. Install it locally in plugins/openclaw-streamchat or ensure backend/node_modules/socket.io-client exists.")}();return t(e,o)}export function createServerSocket({websocketUrl:e,token:n,logger:t,dispatch:r,connectSocket:c=o,onDisconnect:i}={}){let s=null,a=!1;return{async connect(){if(!e)throw new Error("websocketUrl is required");if(!n)throw new Error("plugin token is required");const o=c(e,{transports:["websocket"],auth:{token:n},reconnection:!1,forceNew:!0});return s=o,await new Promise((e,n)=>{const r=()=>{"function"==typeof o.off&&(o.off("ready",c),o.off("auth_error",i),o.off("connect_error",s))},c=n=>{r(),a=!0,t?.info?.(`[nexa] websocket ready for ${n?.agentChannelId||"unknown-agent-channel"}`),e(n)},i=e=>{r(),n(new Error(e?.message||"Plugin websocket authentication failed"))},s=e=>{r(),n(e instanceof Error?e:new Error(String(e)))};o.on("ready",c),o.on("auth_error",i),o.on("connect_error",s)}),o.on("inbox_event",async e=>{try{await(r?.dispatchEvent?.(e))}catch(e){t?.error?.(`[nexa] websocket dispatch failed: ${e instanceof Error?e.message:e}`)}}),o.on("disconnect",e=>{a=!1,t?.warn?.(`[nexa] websocket disconnected: ${e}`),i?.(e)}),{ok:!0}},close(){a=!1,s?.disconnect?.(),s=null},isConnected:()=>a}}
@@ -0,0 +1 @@
1
+ import{StreamChat as e}from"stream-chat";import{ensureAccountConfig as t}from"./config.js";const r=new Map;export function getStreamClient(n){const a=t(n),o=`${a.accountId}:${a.apiKey}`;if(r.has(o))return r.get(o);const s=new e(a.apiKey,a.apiSecret,{timeout:1e4});return r.set(o,s),s}export async function sendTextToStream(e,t,r,n={}){const a=getStreamClient(e).channel(t.channelType,t.channelId),o=await a.sendMessage({text:r,user_id:n.userId,...n.custom?{custom:n.custom}:{}});return{ok:!0,messageId:o.message.id,createdAt:o.message.created_at}}export async function ensureStreamUser(e,t,r={}){if(!t?.id)throw new Error("reply user id is required");const n=getStreamClient(e);return await n.upsertUser({id:t.id,name:t.name,image:t.image,is_bot:!0,openclaw_agent_id:r.agentId||r.openclawAgentId,source:"openclaw"}),{ok:!0,userId:t.id}}export function verifyStreamWebhook(e,t,r){return getStreamClient(e).verifyWebhook(t,r)}
@@ -0,0 +1 @@
1
+ export function applyJitter(t,a=0,e=Math.random){if(!t||a<=0)return Math.max(0,Math.round(t||0));const r=Math.round(t*a),M=Math.round((2*e()-1)*r);return Math.max(0,Math.round(t+M))}export function computeNextPollDelay({baseMs:t,idleMinMs:a,idleMaxMs:e,emptyStreak:r,failureStreak:M,deliveredCount:n,jitterRatio:i},o=Math.random){return n>0?0:applyJitter(M>0?Math.min(e,Math.max(t,t*2**Math.max(M-1,0))):r<=0?t:Math.min(e,Math.max(a,t*2**Math.max(r-1,0))),i,o)}export function computeNextHeartbeatDelay({baseMs:t,failureStreak:a,maxMs:e,jitterRatio:r},M=Math.random){return applyJitter(a>0?Math.min(e,Math.max(t,t*2**Math.max(a-1,0))):t,r,M)}
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "nexa",
3
+ "channels": [
4
+ "nexa"
5
+ ],
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {}
10
+ }
11
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@nexa26/nexa",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Nexa OpenClaw native channel plugin",
7
+ "main": "dist/index.js",
8
+ "files": [
9
+ "dist",
10
+ "openclaw.plugin.json",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "test": "node --test src/*.test.js"
15
+ },
16
+ "dependencies": {
17
+ "socket.io-client": "^4.8.1",
18
+ "stream-chat": "^9.18.1"
19
+ },
20
+ "peerDependencies": {
21
+ "openclaw": "*"
22
+ },
23
+ "openclaw": {
24
+ "extensions": [
25
+ "dist/index.js"
26
+ ],
27
+ "channel": {
28
+ "id": "nexa",
29
+ "label": "Nexa",
30
+ "selectionLabel": "Nexa OpenClaw",
31
+ "docsPath": "/channels/nexa",
32
+ "docsLabel": "nexa",
33
+ "blurb": "Nexa native channel plugin for OpenClaw groups.",
34
+ "order": 80,
35
+ "aliases": [
36
+ "NEXA",
37
+ "getstream"
38
+ ]
39
+ },
40
+ "install": {
41
+ "npmSpec": "@nexa26/nexa",
42
+ "defaultChoice": "npm"
43
+ }
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }