@openclaw-channel/socket-chat 1.0.6 → 1.0.8

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.
@@ -1,20 +1,29 @@
1
1
  import type { MqttClient, IPublishPacket } from "mqtt";
2
2
  import mqtt from "mqtt";
3
- import type { ChannelGatewayContext } from "openclaw/plugin-sdk";
4
3
  import { fetchMqttConfigCached, invalidateMqttConfigCache } from "./api.js";
5
- import { handleInboundMessage } from "./inbound.js";
6
- import type { ResolvedSocketChatAccount } from "./config.js";
7
- import type { SocketChatInboundMessage, SocketChatMqttConfig, SocketChatStatusPatch } from "./types.js";
8
- import { buildTextPayload, sendSocketChatMessage } from "./outbound.js";
4
+ import { handleSocketChatInbound } from "./inbound.js";
5
+ import type { CoreConfig, ResolvedSocketChatAccount } from "./config.js";
6
+ import type {
7
+ SocketChatInboundMessage,
8
+ SocketChatMqttConfig,
9
+ SocketChatOutboundPayload,
10
+ SocketChatStatusPatch,
11
+ } from "./types.js";
12
+ import { parseSocketChatTarget, sendSocketChatMessage } from "./outbound.js";
9
13
 
10
14
  const MAX_RECONNECT_ATTEMPTS_DEFAULT = 10;
11
15
  const RECONNECT_BASE_DELAY_MS_DEFAULT = 2000;
12
16
  const RECONNECT_MAX_DELAY_MS = 60_000;
13
17
 
14
- type LogSink = NonNullable<ChannelGatewayContext["log"]>;
18
+ type LogSink = {
19
+ info: (m: string) => void;
20
+ warn: (m: string) => void;
21
+ error: (m: string) => void;
22
+ debug?: (m: string) => void;
23
+ };
15
24
 
16
25
  // ---------------------------------------------------------------------------
17
- // 活跃连接注册表(供 outbound 查找当前 MQTT client
26
+ // Active connection registry (used by outbound to find the current MQTT client)
18
27
  // ---------------------------------------------------------------------------
19
28
  const activeClients = new Map<string, MqttClient>();
20
29
  const activeMqttConfigs = new Map<string, SocketChatMqttConfig>();
@@ -43,7 +52,7 @@ export function clearActiveMqttSession(accountId: string): void {
43
52
  }
44
53
 
45
54
  // ---------------------------------------------------------------------------
46
- // 工具函数
55
+ // Helpers
47
56
  // ---------------------------------------------------------------------------
48
57
 
49
58
  function buildMqttUrl(mqttConfig: SocketChatMqttConfig): string {
@@ -77,9 +86,10 @@ function parseInboundMessage(raw: Buffer | string): SocketChatInboundMessage | n
77
86
  messageId: m.messageId,
78
87
  type: typeof m.type === "string" ? m.type : undefined,
79
88
  url: typeof m.url === "string" ? m.url : undefined,
80
- mediaInfo: m.mediaInfo && typeof m.mediaInfo === "object" && !Array.isArray(m.mediaInfo)
81
- ? (m.mediaInfo as Record<string, unknown>)
82
- : undefined,
89
+ mediaInfo:
90
+ m.mediaInfo && typeof m.mediaInfo === "object" && !Array.isArray(m.mediaInfo)
91
+ ? (m.mediaInfo as Record<string, unknown>)
92
+ : undefined,
83
93
  };
84
94
  } catch {
85
95
  return null;
@@ -94,47 +104,48 @@ function backoffDelay(attempt: number, baseMs: number): number {
94
104
  function waitMs(ms: number, signal: AbortSignal): Promise<void> {
95
105
  return new Promise((resolve) => {
96
106
  const timer = setTimeout(resolve, ms);
97
- signal.addEventListener("abort", () => {
98
- clearTimeout(timer);
99
- resolve();
100
- }, { once: true });
107
+ signal.addEventListener(
108
+ "abort",
109
+ () => {
110
+ clearTimeout(timer);
111
+ resolve();
112
+ },
113
+ { once: true },
114
+ );
101
115
  });
102
116
  }
103
117
 
104
118
  // ---------------------------------------------------------------------------
105
- // 核心 Monitor 循环
119
+ // Core monitor loop
106
120
  // ---------------------------------------------------------------------------
107
121
 
108
122
  /**
109
- * 启动 MQTT 监听循环,支持:
110
- * - 自动拉取远端 MQTT 配置(带缓存)
111
- * - 自动重连 + 指数退避
112
- * - 注册活跃 client outbound 发消息
113
- * - AbortSignal 优雅停止
123
+ * Start the MQTT monitoring loop with:
124
+ * - Remote MQTT config fetch (with TTL cache)
125
+ * - Auto-reconnect with exponential backoff
126
+ * - Active client registry for outbound sends
127
+ * - AbortSignal-based graceful shutdown
114
128
  */
115
129
  export async function monitorSocketChatProviderWithRegistry(params: {
116
130
  account: ResolvedSocketChatAccount;
117
131
  accountId: string;
118
- ctx: ChannelGatewayContext<ResolvedSocketChatAccount>;
132
+ config: CoreConfig;
133
+ abortSignal: AbortSignal;
119
134
  log: LogSink;
135
+ statusSink: (patch: SocketChatStatusPatch) => void;
120
136
  }): Promise<void> {
121
- const { account, accountId, ctx, log } = params;
122
- const { abortSignal } = ctx;
137
+ const { account, accountId, config, abortSignal, log, statusSink } = params;
123
138
  const maxReconnects = account.config.maxReconnectAttempts ?? MAX_RECONNECT_ATTEMPTS_DEFAULT;
124
139
  const reconnectBaseMs = account.config.reconnectBaseDelayMs ?? RECONNECT_BASE_DELAY_MS_DEFAULT;
125
140
  const mqttConfigTtlMs = (account.config.mqttConfigTtlSec ?? 300) * 1000;
126
141
 
127
142
  let reconnectAttempts = 0;
128
143
 
129
- const setStatus = (patch: SocketChatStatusPatch): void => {
130
- ctx.setStatus({ accountId, ...patch } as Parameters<typeof ctx.setStatus>[0]);
131
- };
132
-
133
- setStatus({ running: true, lastStartAt: Date.now() });
144
+ statusSink({ running: true, lastStartAt: Date.now() });
134
145
 
135
146
  try {
136
147
  while (!abortSignal.aborted) {
137
- // 1. 拉取 MQTT 配置
148
+ // 1. Fetch MQTT config
138
149
  let mqttConfig: SocketChatMqttConfig;
139
150
  try {
140
151
  mqttConfig = await fetchMqttConfigCached({
@@ -147,18 +158,18 @@ export async function monitorSocketChatProviderWithRegistry(params: {
147
158
  } catch (err) {
148
159
  const message = err instanceof Error ? err.message : String(err);
149
160
  log.error(`[${accountId}] failed to fetch MQTT config: ${message}`);
150
- setStatus({ lastError: message });
161
+ statusSink({ lastError: message });
151
162
  reconnectAttempts++;
152
163
  if (reconnectAttempts > maxReconnects) {
153
164
  log.error(`[${accountId}] max reconnect attempts (${maxReconnects}) reached`);
154
165
  break;
155
166
  }
156
- setStatus({ reconnectAttempts });
167
+ statusSink({ reconnectAttempts });
157
168
  await waitMs(backoffDelay(reconnectAttempts, reconnectBaseMs), abortSignal);
158
169
  continue;
159
170
  }
160
171
 
161
- // 2. 建立 MQTT 连接
172
+ // 2. Establish MQTT connection
162
173
  const mqttUrl = buildMqttUrl(mqttConfig);
163
174
  log.info(`[${accountId}] connecting to ${mqttUrl} (clientId=${mqttConfig.clientId})`);
164
175
 
@@ -167,14 +178,14 @@ export async function monitorSocketChatProviderWithRegistry(params: {
167
178
  username: mqttConfig.username,
168
179
  password: mqttConfig.password,
169
180
  clean: true,
170
- reconnectPeriod: 0, // 禁用 mqtt.js 内置重连,由外层循环管理
181
+ reconnectPeriod: 0, // disable mqtt.js auto-reconnect; managed by outer loop
171
182
  connectTimeout: 15_000,
172
183
  keepalive: 60,
173
184
  });
174
185
 
175
186
  setActiveMqttClient(accountId, client);
176
187
 
177
- // 等待连接关闭(正常关闭或错误都会 resolve)
188
+ // Wait until connection closes (normally or on error)
178
189
  await new Promise<void>((resolve) => {
179
190
  const onAbort = (): void => {
180
191
  log.info(`[${accountId}] abort signal, disconnecting`);
@@ -185,7 +196,7 @@ export async function monitorSocketChatProviderWithRegistry(params: {
185
196
 
186
197
  client.on("connect", () => {
187
198
  reconnectAttempts = 0;
188
- setStatus({
199
+ statusSink({
189
200
  connected: true,
190
201
  reconnectAttempts: 0,
191
202
  lastConnectedAt: Date.now(),
@@ -205,74 +216,79 @@ export async function monitorSocketChatProviderWithRegistry(params: {
205
216
 
206
217
  client.on("message", (topic: string, rawPayload: Buffer, _packet: IPublishPacket) => {
207
218
  if (topic !== mqttConfig.reciveTopic) return;
208
- setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
219
+ statusSink({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
209
220
 
210
221
  const msg = parseInboundMessage(rawPayload);
211
222
  if (!msg) {
212
223
  log.warn(`[${accountId}] unparseable message on ${topic}`);
213
224
  return;
214
225
  }
215
- // 跳过机器人自己的回声消息
226
+ // Skip the bot's own echo messages
216
227
  if (msg.robotId === mqttConfig.robotId && msg.senderId === mqttConfig.robotId) {
217
228
  return;
218
229
  }
219
230
 
220
231
  log.info(
221
232
  `[${accountId}] inbound msg ${msg.messageId} from ${msg.senderId}` +
222
- (msg.isGroup ? ` in group ${msg.groupId}` : ""),
233
+ (msg.isGroup ? ` in group ${msg.groupId}` : ""),
223
234
  );
224
235
 
225
- void handleInboundMessage({
236
+ void handleSocketChatInbound({
226
237
  msg,
227
238
  accountId,
228
- ctx,
239
+ config,
229
240
  log,
241
+ statusSink,
230
242
  sendReply: async (to: string, text: string) => {
231
- const payload = buildTextPayload(to, text);
243
+ const base = parseSocketChatTarget(to);
244
+ const payload: SocketChatOutboundPayload = {
245
+ ...base,
246
+ messages: [{ type: 1, content: text }],
247
+ };
232
248
  await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
233
249
  },
234
250
  }).catch((e: unknown) => {
235
251
  log.error(
236
- `[${accountId}] handleInboundMessage error: ${e instanceof Error ? e.message : String(e)}`,
252
+ `[${accountId}] handleSocketChatInbound error: ${e instanceof Error ? e.message : String(e)}`,
237
253
  );
238
254
  });
239
255
  });
240
256
 
241
257
  client.on("error", (err: Error) => {
242
258
  log.warn(`[${accountId}] MQTT error: ${err.message}`);
243
- setStatus({ lastError: err.message });
259
+ statusSink({ lastError: err.message });
244
260
  });
245
261
 
246
262
  client.on("close", () => {
247
- setStatus({ connected: false, lastDisconnect: new Date().toISOString() });
263
+ statusSink({ connected: false, lastDisconnect: new Date().toISOString() });
248
264
  abortSignal.removeEventListener("abort", onAbort);
249
265
  resolve();
250
266
  });
251
267
  });
252
268
 
253
- // 清理注册表
269
+ // Remove from registry
254
270
  activeClients.delete(accountId);
255
271
 
256
272
  if (abortSignal.aborted) break;
257
273
 
258
- // 3. 重连退避
274
+ // 3. Reconnect with backoff
259
275
  reconnectAttempts++;
260
276
  if (reconnectAttempts > maxReconnects) {
261
277
  log.error(`[${accountId}] max reconnect attempts (${maxReconnects}) reached, giving up`);
262
278
  break;
263
279
  }
264
- // 清除 MQTT config 缓存,下次重连时重新拉取(token 可能已过期)
280
+ // Invalidate config cache so next reconnect re-fetches (token may have expired)
265
281
  invalidateMqttConfigCache({ apiBaseUrl: account.apiBaseUrl!, apiKey: account.apiKey! });
266
282
  const delay = backoffDelay(reconnectAttempts, reconnectBaseMs);
267
283
  log.info(
268
284
  `[${accountId}] reconnect ${reconnectAttempts}/${maxReconnects} in ${Math.round(delay)}ms`,
269
285
  );
270
- setStatus({ reconnectAttempts });
286
+ statusSink({ reconnectAttempts });
271
287
  await waitMs(delay, abortSignal);
272
288
  }
273
289
  } finally {
274
290
  clearActiveMqttSession(accountId);
275
- setStatus({ running: false, connected: false, lastStopAt: Date.now() });
291
+ statusSink({ running: false, connected: false, lastStopAt: Date.now() });
276
292
  log.info(`[${accountId}] monitor stopped`);
277
293
  }
278
294
  }
@@ -1,12 +1,21 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import {
3
- buildMediaPayload,
4
- buildTextPayload,
3
+ buildSocketChatMediaPayload,
4
+ buildSocketChatTextPayload,
5
5
  looksLikeSocketChatTargetId,
6
6
  normalizeSocketChatTarget,
7
7
  parseSocketChatTarget,
8
8
  } from "./outbound.js";
9
9
 
10
+ // Mock mqtt-client to prevent deep transitive imports from pulling in the full openclaw runtime
11
+ vi.mock("./mqtt-client.js", () => ({
12
+ getActiveMqttClient: vi.fn(() => null),
13
+ getActiveMqttConfig: vi.fn(() => null),
14
+ monitorSocketChatProviderWithRegistry: vi.fn(),
15
+ getActiveMqttSession: vi.fn(() => null),
16
+ clearActiveMqttSession: vi.fn(),
17
+ }));
18
+
10
19
  // ---------------------------------------------------------------------------
11
20
  // parseSocketChatTarget
12
21
  // ---------------------------------------------------------------------------
@@ -104,12 +113,12 @@ describe("looksLikeSocketChatTargetId", () => {
104
113
  });
105
114
 
106
115
  // ---------------------------------------------------------------------------
107
- // buildTextPayload
116
+ // buildSocketChatTextPayload
108
117
  // ---------------------------------------------------------------------------
109
118
 
110
- describe("buildTextPayload", () => {
119
+ describe("buildSocketChatTextPayload", () => {
111
120
  it("builds a DM text payload", () => {
112
- const payload = buildTextPayload("wxid_abc", "hello");
121
+ const payload = buildSocketChatTextPayload("wxid_abc", "hello");
113
122
  expect(payload.isGroup).toBe(false);
114
123
  expect(payload.contactId).toBe("wxid_abc");
115
124
  expect(payload.messages).toEqual([{ type: 1, content: "hello" }]);
@@ -117,35 +126,35 @@ describe("buildTextPayload", () => {
117
126
  });
118
127
 
119
128
  it("builds a group text payload", () => {
120
- const payload = buildTextPayload("group:17581395450@chatroom", "hi group");
129
+ const payload = buildSocketChatTextPayload("group:17581395450@chatroom", "hi group");
121
130
  expect(payload.isGroup).toBe(true);
122
131
  expect(payload.groupId).toBe("17581395450@chatroom");
123
132
  expect(payload.messages).toEqual([{ type: 1, content: "hi group" }]);
124
133
  });
125
134
 
126
135
  it("extracts mentions from group target string", () => {
127
- const payload = buildTextPayload("group:17581395450@chatroom|wxid_a,wxid_b", "hi");
136
+ const payload = buildSocketChatTextPayload("group:17581395450@chatroom|wxid_a,wxid_b", "hi");
128
137
  expect(payload.mentionIds).toEqual(["wxid_a", "wxid_b"]);
129
138
  });
130
139
 
131
140
  it("allows explicit override of mentionIds", () => {
132
- const payload = buildTextPayload("group:17581395450@chatroom", "hi", { mentionIds: ["wxid_override"] });
141
+ const payload = buildSocketChatTextPayload("group:17581395450@chatroom", "hi", { mentionIds: ["wxid_override"] });
133
142
  expect(payload.mentionIds).toEqual(["wxid_override"]);
134
143
  });
135
144
 
136
145
  it("sets mentionIds to undefined when empty", () => {
137
- const payload = buildTextPayload("group:17581395450@chatroom", "hi", { mentionIds: [] });
146
+ const payload = buildSocketChatTextPayload("group:17581395450@chatroom", "hi", { mentionIds: [] });
138
147
  expect(payload.mentionIds).toBeUndefined();
139
148
  });
140
149
  });
141
150
 
142
151
  // ---------------------------------------------------------------------------
143
- // buildMediaPayload
152
+ // buildSocketChatMediaPayload
144
153
  // ---------------------------------------------------------------------------
145
154
 
146
- describe("buildMediaPayload", () => {
155
+ describe("buildSocketChatMediaPayload", () => {
147
156
  it("builds a media-only payload without caption", () => {
148
- const payload = buildMediaPayload("wxid_abc", "https://img.example.com/photo.jpg");
157
+ const payload = buildSocketChatMediaPayload("wxid_abc", "https://img.example.com/photo.jpg");
149
158
  expect(payload.isGroup).toBe(false);
150
159
  expect(payload.messages).toEqual([
151
160
  { type: 2, url: "https://img.example.com/photo.jpg" },
@@ -153,7 +162,7 @@ describe("buildMediaPayload", () => {
153
162
  });
154
163
 
155
164
  it("includes caption text before image when provided", () => {
156
- const payload = buildMediaPayload("wxid_abc", "https://img.example.com/photo.jpg", "Look at this");
165
+ const payload = buildSocketChatMediaPayload("wxid_abc", "https://img.example.com/photo.jpg", "Look at this");
157
166
  expect(payload.messages).toEqual([
158
167
  { type: 1, content: "Look at this" },
159
168
  { type: 2, url: "https://img.example.com/photo.jpg" },
@@ -161,14 +170,14 @@ describe("buildMediaPayload", () => {
161
170
  });
162
171
 
163
172
  it("skips empty caption", () => {
164
- const payload = buildMediaPayload("wxid_abc", "https://img.example.com/photo.jpg", " ");
173
+ const payload = buildSocketChatMediaPayload("wxid_abc", "https://img.example.com/photo.jpg", " ");
165
174
  expect(payload.messages).toEqual([
166
175
  { type: 2, url: "https://img.example.com/photo.jpg" },
167
176
  ]);
168
177
  });
169
178
 
170
179
  it("builds group media payload", () => {
171
- const payload = buildMediaPayload("group:17581395450@chatroom", "https://img.example.com/photo.jpg");
180
+ const payload = buildSocketChatMediaPayload("group:17581395450@chatroom", "https://img.example.com/photo.jpg");
172
181
  expect(payload.isGroup).toBe(true);
173
182
  expect(payload.groupId).toBe("17581395450@chatroom");
174
183
  });
package/src/outbound.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { MqttClient } from "mqtt";
2
- import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
3
2
  import { DEFAULT_ACCOUNT_ID } from "./config.js";
4
3
  import { getActiveMqttClient, getActiveMqttConfig } from "./mqtt-client.js";
5
4
  import type { SocketChatMqttConfig, SocketChatOutboundPayload } from "./types.js";
@@ -53,27 +52,22 @@ export function looksLikeSocketChatTargetId(s: string): boolean {
53
52
  }
54
53
 
55
54
  /**
56
- * 构建纯文本发送 payload
55
+ * 构建文字消息 payload(纯函数,不发送)
57
56
  */
58
- export function buildTextPayload(
57
+ export function buildSocketChatTextPayload(
59
58
  to: string,
60
59
  text: string,
61
- opts: { mentionIds?: string[] } = {},
60
+ opts?: { mentionIds?: string[] },
62
61
  ): SocketChatOutboundPayload {
63
62
  const base = parseSocketChatTarget(to);
64
- const mentionIds =
65
- opts.mentionIds ?? (base.isGroup ? base.mentionIds : undefined);
66
- return {
67
- ...base,
68
- mentionIds: mentionIds?.length ? mentionIds : undefined,
69
- messages: [{ type: 1, content: text }],
70
- };
63
+ const mentionIds = opts?.mentionIds?.length ? opts.mentionIds : undefined;
64
+ return { ...base, messages: [{ type: 1, content: text }], ...(mentionIds ? { mentionIds } : {}) };
71
65
  }
72
66
 
73
67
  /**
74
68
  * 构建图片发送 payload(可附带文字 caption)
75
69
  */
76
- export function buildMediaPayload(
70
+ export function buildSocketChatMediaPayload(
77
71
  to: string,
78
72
  imageUrl: string,
79
73
  caption?: string,
@@ -110,66 +104,34 @@ export async function sendSocketChatMessage(params: {
110
104
  }
111
105
 
112
106
  // ---------------------------------------------------------------------------
113
- // ChannelOutboundAdapter 实现
107
+ // Shared outbound helper used by channel.ts and inbound.ts
114
108
  // ---------------------------------------------------------------------------
115
109
 
116
110
  /**
117
- * socket-chat 出站适配器。
118
- *
119
- * 通过注册表查找当前账号的活跃 MQTT client 发送消息:
120
- * - sendText:发送纯文字
121
- * - sendMedia:优先发图片(type:2),无 mediaUrl 时退化为纯文字
122
- * - resolveTarget:规范化目标地址(strip socket-chat: 前缀)
111
+ * 通过活跃的 MQTT client 发送文字消息到指定目标
123
112
  */
124
- export const socketChatOutbound: ChannelOutboundAdapter = {
125
- deliveryMode: "direct",
126
- textChunkLimit: 4096,
127
-
128
- resolveTarget: ({ to }) => {
129
- const normalized = to ? normalizeSocketChatTarget(to) : undefined;
130
- if (!normalized) {
131
- return { ok: false, error: new Error(`Invalid socket-chat target: "${to}"`) };
132
- }
133
- return { ok: true, to: normalized };
134
- },
135
-
136
- sendText: async ({ to, text, accountId }) => {
137
- const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
138
- const client = getActiveMqttClient(resolvedAccountId);
139
- const mqttConfig = getActiveMqttConfig(resolvedAccountId);
140
-
141
- if (!client || !mqttConfig) {
142
- throw new Error(
143
- `[socket-chat] No active MQTT connection for account "${resolvedAccountId}". ` +
113
+ export async function sendSocketChatText(params: {
114
+ to: string;
115
+ text: string;
116
+ accountId?: string;
117
+ }): Promise<{ channel: string; messageId: string }> {
118
+ const { to, text } = params;
119
+ const resolvedAccountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
120
+ const client = getActiveMqttClient(resolvedAccountId);
121
+ const mqttConfig = getActiveMqttConfig(resolvedAccountId);
122
+
123
+ if (!client || !mqttConfig) {
124
+ throw new Error(
125
+ `[socket-chat] No active MQTT connection for account "${resolvedAccountId}". ` +
144
126
  "Is the gateway running?",
145
- );
146
- }
147
-
148
- const payload = buildTextPayload(to, text);
149
- const result = await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
150
- return { channel: "socket-chat", messageId: result.messageId };
151
- },
152
-
153
- sendMedia: async ({ to, text, mediaUrl, accountId }) => {
154
- const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
155
- const client = getActiveMqttClient(resolvedAccountId);
156
- const mqttConfig = getActiveMqttConfig(resolvedAccountId);
157
-
158
- if (!client || !mqttConfig) {
159
- throw new Error(
160
- `[socket-chat] No active MQTT connection for account "${resolvedAccountId}".`,
161
- );
162
- }
163
-
164
- // 有图片 URL 时发图片(可附带 caption),否则退化为纯文字
165
- if (mediaUrl) {
166
- const payload = buildMediaPayload(to, mediaUrl, text);
167
- const result = await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
168
- return { channel: "socket-chat", messageId: result.messageId };
169
- }
127
+ );
128
+ }
170
129
 
171
- const payload = buildTextPayload(to, text);
172
- const result = await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
173
- return { channel: "socket-chat", messageId: result.messageId };
174
- },
175
- };
130
+ const base = parseSocketChatTarget(to);
131
+ const payload: SocketChatOutboundPayload = {
132
+ ...base,
133
+ messages: [{ type: 1, content: text }],
134
+ };
135
+ const result = await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
136
+ return { channel: "socket-chat", messageId: result.messageId };
137
+ }
@@ -0,0 +1,28 @@
1
+ // Internal runtime barrel for socket-chat extension.
2
+ // All SDK subpath imports are centralised here.
3
+ // Production files import from "./runtime-api.js" — never directly from "openclaw/plugin-sdk/*".
4
+
5
+ export type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
6
+ export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";
7
+ export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
8
+ export { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
9
+ export type {
10
+ ChannelAccountSnapshot,
11
+ ChannelStatusIssue,
12
+ } from "openclaw/plugin-sdk/status-helpers";
13
+
14
+ export { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
15
+ export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
16
+ export { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
17
+ export { dispatchInboundReplyWithBase } from "openclaw/plugin-sdk/inbound-reply-dispatch";
18
+ export {
19
+ deliverFormattedTextWithAttachments,
20
+ buildMediaPayload,
21
+ } from "openclaw/plugin-sdk/reply-payload";
22
+ export { resolveChannelMediaMaxBytes, detectMime } from "openclaw/plugin-sdk/media-runtime";
23
+ export { resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
24
+ export {
25
+ buildBaseChannelStatusSummary,
26
+ buildBaseAccountStatusSnapshot,
27
+ collectStatusIssuesFromLastError,
28
+ } from "openclaw/plugin-sdk/status-helpers";
package/src/runtime.ts CHANGED
@@ -1,14 +1,10 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
+ import type { PluginRuntime } from "./runtime-api.js";
2
3
 
3
- let runtime: PluginRuntime | null = null;
4
+ const {
5
+ setRuntime: setSocketChatRuntime,
6
+ getRuntime: getSocketChatRuntime,
7
+ clearRuntime: clearSocketChatRuntime,
8
+ } = createPluginRuntimeStore<PluginRuntime>("socket-chat runtime not initialized");
4
9
 
5
- export function setSocketChatRuntime(next: PluginRuntime): void {
6
- runtime = next;
7
- }
8
-
9
- export function getSocketChatRuntime(): PluginRuntime {
10
- if (!runtime) {
11
- throw new Error("socket-chat runtime not initialized");
12
- }
13
- return runtime;
14
- }
10
+ export { setSocketChatRuntime, getSocketChatRuntime, clearSocketChatRuntime };
package/tsconfig.json CHANGED
@@ -12,6 +12,6 @@
12
12
  "sourceMap": true,
13
13
  "lib": ["ES2022", "DOM"]
14
14
  },
15
- "include": ["index.ts", "src/**/*.ts"],
15
+ "include": ["index.ts", "channel-api.ts", "runtime-api.ts", "src/**/*.ts"],
16
16
  "exclude": ["node_modules", "dist"]
17
17
  }
package/vitest.config.ts CHANGED
@@ -4,17 +4,23 @@ import { defineConfig } from "vitest/config";
4
4
 
5
5
  const dir = path.dirname(fileURLToPath(import.meta.url));
6
6
  const openclawSrc = path.join(dir, "..", "openclaw", "src");
7
+ const sdkSrc = path.join(openclawSrc, "plugin-sdk");
7
8
 
8
9
  export default defineConfig({
9
10
  resolve: {
10
11
  alias: [
11
- // Point directly to the allowlist-match module to avoid pulling in the
12
- // entire plugin-sdk dependency tree (which includes json5 and other
13
- // modules that fail to load outside the openclaw workspace).
14
- {
15
- find: "openclaw/plugin-sdk",
16
- replacement: path.join(dir, "src", "__sdk-stub__.ts"),
17
- },
12
+ // Per-subpath aliases must come BEFORE the broad "openclaw/plugin-sdk" fallback.
13
+ { find: "openclaw/plugin-sdk/runtime-store", replacement: path.join(sdkSrc, "runtime-store.ts") },
14
+ { find: "openclaw/plugin-sdk/channel-pairing", replacement: path.join(sdkSrc, "channel-pairing.ts") },
15
+ { find: "openclaw/plugin-sdk/channel-lifecycle", replacement: path.join(sdkSrc, "channel-lifecycle.ts") },
16
+ { find: "openclaw/plugin-sdk/inbound-reply-dispatch", replacement: path.join(sdkSrc, "inbound-reply-dispatch.ts") },
17
+ { find: "openclaw/plugin-sdk/reply-payload", replacement: path.join(sdkSrc, "reply-payload.ts") },
18
+ { find: "openclaw/plugin-sdk/media-runtime", replacement: path.join(sdkSrc, "media-runtime.ts") },
19
+ { find: "openclaw/plugin-sdk/allow-from", replacement: path.join(sdkSrc, "allow-from.ts") },
20
+ { find: "openclaw/plugin-sdk/status-helpers", replacement: path.join(sdkSrc, "status-helpers.ts") },
21
+ { find: "openclaw/plugin-sdk/channel-core", replacement: path.join(sdkSrc, "channel-core.ts") },
22
+ // Broad fallback for any remaining "openclaw/plugin-sdk" (main barrel) imports.
23
+ { find: "openclaw/plugin-sdk", replacement: path.join(dir, "src", "__sdk-stub__.ts") },
18
24
  ],
19
25
  },
20
26
  test: {