@openclaw-channel/socket-chat 1.0.5 → 1.0.7

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/src/channel.ts CHANGED
@@ -1,13 +1,13 @@
1
- import type {
2
- ChannelAccountSnapshot,
3
- ChannelPlugin,
4
- ChannelStatusIssue,
5
- } from "openclaw/plugin-sdk";
6
1
  import {
7
- buildBaseChannelStatusSummary,
8
2
  buildBaseAccountStatusSnapshot,
3
+ buildBaseChannelStatusSummary,
9
4
  collectStatusIssuesFromLastError,
10
- } from "openclaw/plugin-sdk";
5
+ createAccountStatusSink,
6
+ createChatChannelPlugin,
7
+ type ChannelAccountSnapshot,
8
+ type ChannelPlugin,
9
+ type ChannelStatusIssue,
10
+ } from "./runtime-api.js";
11
11
  import {
12
12
  DEFAULT_ACCOUNT_ID,
13
13
  applySocketChatAccountConfig,
@@ -23,370 +23,425 @@ import {
23
23
  } from "./config.js";
24
24
  import { probeSocketChatAccount } from "./probe.js";
25
25
  import {
26
- buildTextPayload,
26
+ buildSocketChatMediaPayload,
27
27
  looksLikeSocketChatTargetId,
28
28
  normalizeSocketChatTarget,
29
+ parseSocketChatTarget,
29
30
  sendSocketChatMessage,
30
- socketChatOutbound,
31
+ sendSocketChatText,
31
32
  } from "./outbound.js";
32
33
  import {
34
+ clearActiveMqttSession,
33
35
  getActiveMqttClient,
34
36
  getActiveMqttConfig,
35
37
  monitorSocketChatProviderWithRegistry,
36
38
  } from "./mqtt-client.js";
39
+ import { getSocketChatRuntime } from "./runtime.js";
40
+ import type { SocketChatOutboundPayload } from "./types.js";
37
41
 
38
42
  // ---------------------------------------------------------------------------
39
- // ChannelPlugin 实现
43
+ // ChannelPlugin implementation
40
44
  // ---------------------------------------------------------------------------
41
45
 
42
- export const socketChatPlugin: ChannelPlugin<ResolvedSocketChatAccount> = {
43
- // -------------------------------------------------------------------------
44
- // 身份标识
45
- // -------------------------------------------------------------------------
46
- id: "socket-chat",
47
-
48
- meta: {
49
- id: "socket-chat",
50
- label: "Socket Chat",
51
- selectionLabel: "Socket Chat (MQTT plugin)",
52
- docsPath: "/channels/socket-chat",
53
- docsLabel: "socket-chat",
54
- blurb: "MQTT-based IM bridge; configure an API key to connect.",
55
- order: 90,
56
- quickstartAllowFrom: true,
57
- },
58
-
59
- // -------------------------------------------------------------------------
60
- // 能力声明
61
- // -------------------------------------------------------------------------
62
- capabilities: {
63
- chatTypes: ["direct", "group"],
64
- media: true, // 支持图片(type: 2)
65
- reactions: false,
66
- threads: false,
67
- polls: false,
68
- nativeCommands: false,
69
- blockStreaming: true, // MQTT 不适合流式输出,等 AI 回复完整后再发
70
- },
71
-
72
- // -------------------------------------------------------------------------
73
- // 配置 Schema
74
- // -------------------------------------------------------------------------
75
- configSchema: {
76
- schema: {
77
- type: "object",
78
- additionalProperties: false,
79
- properties: {
80
- apiKey: { type: "string" },
81
- apiBaseUrl: { type: "string" },
82
- name: { type: "string" },
83
- enabled: { type: "boolean" },
84
- dmPolicy: { type: "string", enum: ["pairing", "open", "allowlist"] },
85
- allowFrom: { type: "array", items: { type: "string" } },
86
- defaultTo: { type: "string" },
87
- requireMention: { type: "boolean" },
88
- mqttConfigTtlSec: { type: "number" },
89
- maxReconnectAttempts: { type: "number" },
90
- reconnectBaseDelayMs: { type: "number" },
91
- accounts: {
46
+ export const socketChatPlugin: ChannelPlugin<ResolvedSocketChatAccount> =
47
+ createChatChannelPlugin<ResolvedSocketChatAccount>({
48
+ base: {
49
+ // -----------------------------------------------------------------------
50
+ // Identity
51
+ // -----------------------------------------------------------------------
52
+ id: "socket-chat",
53
+
54
+ meta: {
55
+ id: "socket-chat",
56
+ label: "Socket Chat",
57
+ selectionLabel: "Socket Chat (MQTT plugin)",
58
+ docsPath: "/channels/socket-chat",
59
+ docsLabel: "socket-chat",
60
+ blurb: "MQTT-based IM bridge; configure an API key to connect.",
61
+ order: 90,
62
+ quickstartAllowFrom: true,
63
+ },
64
+
65
+ // -----------------------------------------------------------------------
66
+ // Capabilities
67
+ // -----------------------------------------------------------------------
68
+ capabilities: {
69
+ chatTypes: ["direct", "group"],
70
+ media: true,
71
+ reactions: false,
72
+ threads: false,
73
+ polls: false,
74
+ nativeCommands: false,
75
+ blockStreaming: true,
76
+ },
77
+
78
+ // -----------------------------------------------------------------------
79
+ // Config schema
80
+ // -----------------------------------------------------------------------
81
+ configSchema: {
82
+ schema: {
92
83
  type: "object",
93
- additionalProperties: {
94
- type: "object",
95
- properties: {
96
- apiKey: { type: "string" },
97
- apiBaseUrl: { type: "string" },
98
- name: { type: "string" },
99
- enabled: { type: "boolean" },
100
- dmPolicy: { type: "string", enum: ["pairing", "open", "allowlist"] },
101
- allowFrom: { type: "array", items: { type: "string" } },
102
- defaultTo: { type: "string" },
103
- requireMention: { type: "boolean" },
104
- mqttConfigTtlSec: { type: "number" },
105
- maxReconnectAttempts: { type: "number" },
106
- reconnectBaseDelayMs: { type: "number" },
84
+ additionalProperties: false,
85
+ properties: {
86
+ apiKey: { type: "string" },
87
+ apiBaseUrl: { type: "string" },
88
+ name: { type: "string" },
89
+ enabled: { type: "boolean" },
90
+ dmPolicy: { type: "string", enum: ["pairing", "open", "allowlist"] },
91
+ allowFrom: { type: "array", items: { type: "string" } },
92
+ defaultTo: { type: "string" },
93
+ requireMention: { type: "boolean" },
94
+ mqttConfigTtlSec: { type: "number" },
95
+ maxReconnectAttempts: { type: "number" },
96
+ reconnectBaseDelayMs: { type: "number" },
97
+ accounts: {
98
+ type: "object",
99
+ additionalProperties: {
100
+ type: "object",
101
+ properties: {
102
+ apiKey: { type: "string" },
103
+ apiBaseUrl: { type: "string" },
104
+ name: { type: "string" },
105
+ enabled: { type: "boolean" },
106
+ dmPolicy: { type: "string", enum: ["pairing", "open", "allowlist"] },
107
+ allowFrom: { type: "array", items: { type: "string" } },
108
+ defaultTo: { type: "string" },
109
+ requireMention: { type: "boolean" },
110
+ mqttConfigTtlSec: { type: "number" },
111
+ maxReconnectAttempts: { type: "number" },
112
+ reconnectBaseDelayMs: { type: "number" },
113
+ },
114
+ },
107
115
  },
108
116
  },
109
117
  },
118
+ uiHints: {
119
+ apiKey: { label: "API Key", sensitive: true, help: "API Key for fetching MQTT connection config" },
120
+ apiBaseUrl: { label: "API Base URL", help: "Backend service URL; defaults to https://api-bot.aibotk.com" },
121
+ dmPolicy: { label: "DM Policy", help: "pairing=require approval, open=anyone, allowlist=whitelist" },
122
+ allowFrom: { label: "Allow From", help: "Sender IDs allowed to trigger AI" },
123
+ requireMention: { label: "Require @mention in groups", help: "Whether group messages must @mention the bot" },
124
+ mqttConfigTtlSec: { label: "MQTT config cache TTL (seconds)", advanced: true },
125
+ maxReconnectAttempts: { label: "Max reconnect attempts", advanced: true },
126
+ reconnectBaseDelayMs: { label: "Reconnect base delay (ms)", advanced: true },
127
+ },
110
128
  },
111
- },
112
- uiHints: {
113
- apiKey: { label: "API Key", sensitive: true, help: "用于获取 MQTT 连接配置的 API Key" },
114
- apiBaseUrl: { label: "API Base URL", help: "后端服务地址,留空使用默认值 https://api-bot.aibotk.com" },
115
- dmPolicy: { label: "私信策略", help: "pairing=需配对, open=任意人, allowlist=白名单" },
116
- allowFrom: { label: "允许来源", help: "允许触发 AI 的发送者 ID 列表" },
117
- requireMention: { label: "群消息需@提及", help: "群组消息是否必须@提及机器人才触发" },
118
- mqttConfigTtlSec: { label: "MQTT 配置缓存时间(秒)", advanced: true },
119
- maxReconnectAttempts: { label: "最大重连次数", advanced: true },
120
- reconnectBaseDelayMs: { label: "重连基础延迟(毫秒)", advanced: true },
121
- },
122
- },
123
129
 
124
- // -------------------------------------------------------------------------
125
- // 账号配置管理
126
- // -------------------------------------------------------------------------
127
- config: {
128
- listAccountIds: (cfg) => listSocketChatAccountIds(cfg as CoreConfig),
130
+ // -----------------------------------------------------------------------
131
+ // Account config management
132
+ // -----------------------------------------------------------------------
133
+ config: {
134
+ listAccountIds: (cfg) => listSocketChatAccountIds(cfg as CoreConfig),
129
135
 
130
- resolveAccount: (cfg, accountId) =>
131
- resolveSocketChatAccount(cfg as CoreConfig, accountId ?? DEFAULT_ACCOUNT_ID),
136
+ resolveAccount: (cfg, accountId) =>
137
+ resolveSocketChatAccount(cfg as CoreConfig, accountId ?? DEFAULT_ACCOUNT_ID),
132
138
 
133
- defaultAccountId: (cfg) => resolveDefaultSocketChatAccountId(cfg as CoreConfig),
139
+ defaultAccountId: (cfg) => resolveDefaultSocketChatAccountId(cfg as CoreConfig),
134
140
 
135
- isConfigured: (account) => isSocketChatAccountConfigured(account),
141
+ isConfigured: (account) => isSocketChatAccountConfigured(account),
136
142
 
137
- describeAccount: (account): ChannelAccountSnapshot => ({
138
- accountId: account.accountId,
139
- name: account.name,
140
- enabled: account.enabled,
141
- configured: isSocketChatAccountConfigured(account),
142
- }),
143
+ describeAccount: (account): ChannelAccountSnapshot => ({
144
+ accountId: account.accountId,
145
+ name: account.name,
146
+ enabled: account.enabled,
147
+ configured: isSocketChatAccountConfigured(account),
148
+ }),
143
149
 
144
- resolveAllowFrom: ({ cfg, accountId }) => {
145
- const account = resolveSocketChatAccount(cfg as CoreConfig, accountId ?? DEFAULT_ACCOUNT_ID);
146
- return account.config.allowFrom ?? [];
147
- },
150
+ resolveAllowFrom: ({ cfg, accountId }) => {
151
+ const account = resolveSocketChatAccount(
152
+ cfg as CoreConfig,
153
+ accountId ?? DEFAULT_ACCOUNT_ID,
154
+ );
155
+ return account.config.allowFrom ?? [];
156
+ },
148
157
 
149
- formatAllowFrom: ({ allowFrom }) =>
150
- allowFrom
151
- .map((e) => String(e).trim())
152
- .filter(Boolean)
153
- .map((e) => e.replace(/^socket-chat:/i, "").toLowerCase()),
158
+ formatAllowFrom: ({ allowFrom }) =>
159
+ allowFrom
160
+ .map((e) => String(e).trim())
161
+ .filter(Boolean)
162
+ .map((e) => e.replace(/^socket-chat:/i, "").toLowerCase()),
163
+
164
+ resolveDefaultTo: ({ cfg, accountId }) => {
165
+ const account = resolveSocketChatAccount(
166
+ cfg as CoreConfig,
167
+ accountId ?? DEFAULT_ACCOUNT_ID,
168
+ );
169
+ return account.config.defaultTo?.trim() || undefined;
170
+ },
154
171
 
155
- resolveDefaultTo: ({ cfg, accountId }) => {
156
- const account = resolveSocketChatAccount(cfg as CoreConfig, accountId ?? DEFAULT_ACCOUNT_ID);
157
- return account.config.defaultTo?.trim() || undefined;
158
- },
172
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
173
+ setSocketChatAccountEnabled({ cfg: cfg as CoreConfig, accountId, enabled }),
159
174
 
160
- setAccountEnabled: ({ cfg, accountId, enabled }) =>
161
- setSocketChatAccountEnabled({ cfg: cfg as CoreConfig, accountId, enabled }),
162
-
163
- deleteAccount: ({ cfg, accountId }) =>
164
- deleteSocketChatAccount({ cfg: cfg as CoreConfig, accountId }),
165
- },
166
-
167
- // -------------------------------------------------------------------------
168
- // 配对(pairing)
169
- // -------------------------------------------------------------------------
170
- pairing: {
171
- idLabel: "socketChatUserId",
172
- normalizeAllowEntry: (entry) => entry.replace(/^(socket-chat|sc):/i, ""),
173
- notifyApproval: async ({ cfg, id }) => {
174
- // 找到有活跃 MQTT 连接的账号发送配对批准通知
175
- // 优先用 default 账号,fallback 到任意有活跃连接的账号
176
- const accountIds = listSocketChatAccountIds(cfg as CoreConfig);
177
- let targetAccountId = DEFAULT_ACCOUNT_ID;
178
- for (const aid of accountIds) {
179
- if (getActiveMqttClient(aid)) {
180
- targetAccountId = aid;
181
- break;
182
- }
183
- }
184
- const client = getActiveMqttClient(targetAccountId);
185
- const mqttConfig = getActiveMqttConfig(targetAccountId);
186
- if (!client || !mqttConfig) return;
187
- const payload = buildTextPayload(id, "You have been approved to chat with this assistant.");
188
- await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
189
- },
190
- },
191
-
192
- // -------------------------------------------------------------------------
193
- // 安全策略
194
- // -------------------------------------------------------------------------
195
- security: {
196
- resolveDmPolicy: ({ cfg, accountId, account }) => {
197
- const resolvedId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
198
- const usesAccountPath = Boolean(
199
- (cfg as CoreConfig).channels?.["socket-chat"]?.accounts?.[resolvedId],
200
- );
201
- const basePath = usesAccountPath
202
- ? `channels.socket-chat.accounts.${resolvedId}.`
203
- : "channels.socket-chat.";
204
- return {
205
- policy: account.config.dmPolicy ?? "pairing",
206
- allowFrom: account.config.allowFrom ?? [],
207
- policyPath: `${basePath}dmPolicy`,
208
- allowFromPath: basePath,
209
- approveHint: `Run: openclaw channels pair socket-chat <userId>`,
210
- normalizeEntry: (raw) => raw.replace(/^(socket-chat|sc):/i, ""),
211
- };
212
- },
175
+ deleteAccount: ({ cfg, accountId }) =>
176
+ deleteSocketChatAccount({ cfg: cfg as CoreConfig, accountId }),
177
+ },
213
178
 
214
- collectWarnings: ({ account }) => {
215
- const warnings: string[] = [];
216
- if (!account.config.allowFrom?.length && account.config.dmPolicy === "open") {
217
- warnings.push(
218
- "- socket-chat: dmPolicy=\"open\" allows any sender to trigger AI. " +
219
- "Consider setting dmPolicy=\"pairing\" or configuring allowFrom.",
220
- );
221
- }
222
- return warnings;
223
- },
224
- },
225
-
226
- // -------------------------------------------------------------------------
227
- // 群组策略
228
- // -------------------------------------------------------------------------
229
- groups: {
230
- resolveRequireMention: ({ cfg, accountId }) => {
231
- const account = resolveSocketChatAccount(cfg as CoreConfig, accountId ?? DEFAULT_ACCOUNT_ID);
232
- // 默认群消息需要 @提及才触发
233
- return account.config.requireMention !== false;
234
- },
235
- resolveToolPolicy: () => undefined,
236
- },
237
-
238
- // -------------------------------------------------------------------------
239
- // 消息目标规范化
240
- // -------------------------------------------------------------------------
241
- messaging: {
242
- normalizeTarget: normalizeSocketChatTarget,
243
- targetResolver: {
244
- looksLikeId: looksLikeSocketChatTargetId,
245
- hint: "<contactId|group:groupId|group:groupId@userId1,userId2>",
246
- },
247
- },
248
-
249
- // -------------------------------------------------------------------------
250
- // 出站发送
251
- // -------------------------------------------------------------------------
252
- outbound: socketChatOutbound,
253
-
254
- // -------------------------------------------------------------------------
255
- // 状态管理
256
- // -------------------------------------------------------------------------
257
- status: {
258
- defaultRuntime: {
259
- accountId: DEFAULT_ACCOUNT_ID,
260
- running: false,
261
- connected: false,
262
- reconnectAttempts: 0,
263
- lastConnectedAt: null,
264
- lastDisconnect: null,
265
- lastEventAt: null,
266
- lastStartAt: null,
267
- lastStopAt: null,
268
- lastError: null,
269
- lastInboundAt: null,
270
- lastOutboundAt: null,
271
- },
179
+ // -----------------------------------------------------------------------
180
+ // Groups
181
+ // -----------------------------------------------------------------------
182
+ groups: {
183
+ resolveRequireMention: ({ cfg, accountId }) => {
184
+ const account = resolveSocketChatAccount(
185
+ cfg as CoreConfig,
186
+ accountId ?? DEFAULT_ACCOUNT_ID,
187
+ );
188
+ return account.config.requireMention !== false;
189
+ },
190
+ resolveToolPolicy: () => undefined,
191
+ },
272
192
 
273
- collectStatusIssues: (snapshots): ChannelStatusIssue[] => {
274
- const issues: ChannelStatusIssue[] = [];
275
- for (const snap of snapshots) {
276
- issues.push(...collectStatusIssuesFromLastError("socket-chat", [snap]));
277
- if (!snap.configured) {
278
- issues.push({
279
- channel: "socket-chat",
280
- accountId: snap.accountId,
281
- kind: "config",
282
- message: "socket-chat account is not configured (missing apiKey).",
283
- fix: "Run: openclaw channels add socket-chat",
193
+ // -----------------------------------------------------------------------
194
+ // Messaging target
195
+ // -----------------------------------------------------------------------
196
+ messaging: {
197
+ normalizeTarget: normalizeSocketChatTarget,
198
+ targetResolver: {
199
+ looksLikeId: looksLikeSocketChatTargetId,
200
+ hint: "<contactId|group:groupId|group:groupId|userId1,userId2>",
201
+ },
202
+ },
203
+
204
+ // -----------------------------------------------------------------------
205
+ // Status
206
+ // -----------------------------------------------------------------------
207
+ status: {
208
+ defaultRuntime: {
209
+ accountId: DEFAULT_ACCOUNT_ID,
210
+ running: false,
211
+ connected: false,
212
+ reconnectAttempts: 0,
213
+ lastConnectedAt: null,
214
+ lastDisconnect: null,
215
+ lastEventAt: null,
216
+ lastStartAt: null,
217
+ lastStopAt: null,
218
+ lastError: null,
219
+ lastInboundAt: null,
220
+ lastOutboundAt: null,
221
+ },
222
+
223
+ collectStatusIssues: (snapshots): ChannelStatusIssue[] => {
224
+ const issues: ChannelStatusIssue[] = [];
225
+ for (const snap of snapshots) {
226
+ issues.push(...collectStatusIssuesFromLastError("socket-chat", [snap]));
227
+ if (!snap.configured) {
228
+ issues.push({
229
+ channel: "socket-chat",
230
+ accountId: snap.accountId,
231
+ kind: "config",
232
+ message: "socket-chat account is not configured (missing apiKey).",
233
+ fix: "Run: openclaw channels add socket-chat",
234
+ });
235
+ }
236
+ }
237
+ return issues;
238
+ },
239
+
240
+ buildChannelSummary: ({ snapshot }) => buildBaseChannelStatusSummary(snapshot),
241
+
242
+ probeAccount: async ({ account, timeoutMs }) =>
243
+ probeSocketChatAccount({ account, timeoutMs }),
244
+
245
+ buildAccountSnapshot: ({ account, runtime, probe }) => {
246
+ const configured = isSocketChatAccountConfigured(account);
247
+ const accountWithConfigured = { ...account, configured };
248
+ const base = buildBaseAccountStatusSnapshot({ account: accountWithConfigured, runtime, probe });
249
+ return {
250
+ ...base,
251
+ probe,
252
+ ...(probe && typeof probe === "object" && "host" in probe
253
+ ? {
254
+ baseUrl: `${(probe as { host?: string }).host}:${(probe as { port?: string }).port}`,
255
+ }
256
+ : {}),
257
+ lastInboundAt: runtime?.lastInboundAt ?? null,
258
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
259
+ };
260
+ },
261
+ },
262
+
263
+ // -----------------------------------------------------------------------
264
+ // Gateway: start MQTT monitor
265
+ // -----------------------------------------------------------------------
266
+ gateway: {
267
+ startAccount: async (ctx) => {
268
+ const account = ctx.account;
269
+
270
+ if (!isSocketChatAccountConfigured(account)) {
271
+ ctx.log?.error?.(
272
+ `[${account.accountId}] socket-chat not configured (missing apiKey)`,
273
+ );
274
+ return;
275
+ }
276
+
277
+ ctx.log?.info?.(`[${account.accountId}] starting socket-chat MQTT provider`);
278
+
279
+ const statusSink = createAccountStatusSink({
280
+ accountId: ctx.accountId,
281
+ setStatus: ctx.setStatus,
284
282
  });
285
- }
286
- }
287
- return issues;
288
- },
289
283
 
290
- buildChannelSummary: ({ snapshot }) =>
291
- buildBaseChannelStatusSummary(snapshot),
292
-
293
- probeAccount: async ({ account, timeoutMs }) =>
294
- probeSocketChatAccount({ account, timeoutMs }),
295
-
296
- buildAccountSnapshot: ({ account, runtime, probe }) => {
297
- const configured = isSocketChatAccountConfigured(account);
298
- const accountWithConfigured = { ...account, configured };
299
- const base = buildBaseAccountStatusSnapshot({ account: accountWithConfigured, runtime, probe });
300
- return {
301
- ...base,
302
- // 补充 probe 信息到 snapshot
303
- probe,
304
- ...(probe && typeof probe === "object" && "host" in probe
305
- ? { baseUrl: `${(probe as { host?: string }).host}:${(probe as { port?: string }).port}` }
306
- : {}),
307
- lastInboundAt: runtime?.lastInboundAt ?? null,
308
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
309
- };
310
- },
311
- },
312
-
313
- // -------------------------------------------------------------------------
314
- // Gateway:启动 MQTT 监听
315
- // -------------------------------------------------------------------------
316
- gateway: {
317
- startAccount: async (ctx) => {
318
- const account = ctx.account;
319
-
320
- if (!isSocketChatAccountConfigured(account)) {
321
- ctx.log?.error?.(
322
- `[${account.accountId}] socket-chat not configured (missing apiKey)`,
323
- );
324
- return;
325
- }
326
-
327
- ctx.log?.info?.(`[${account.accountId}] starting socket-chat MQTT provider`);
328
-
329
- return monitorSocketChatProviderWithRegistry({
330
- account,
331
- accountId: account.accountId,
332
- ctx,
333
- log: ctx.log ?? {
334
- info: () => {},
335
- warn: () => {},
336
- error: () => {},
284
+ return monitorSocketChatProviderWithRegistry({
285
+ account,
286
+ accountId: account.accountId,
287
+ config: ctx.cfg as CoreConfig,
288
+ abortSignal: ctx.abortSignal,
289
+ log: ctx.log ?? { info: () => {}, warn: () => {}, error: () => {} },
290
+ statusSink,
291
+ });
292
+ },
293
+
294
+ logoutAccount: async ({ accountId }) => {
295
+ clearActiveMqttSession(accountId);
296
+ return { cleared: true, loggedOut: true };
297
+ },
298
+ },
299
+
300
+ // -----------------------------------------------------------------------
301
+ // Hot reload
302
+ // -----------------------------------------------------------------------
303
+ reload: {
304
+ configPrefixes: ["channels.socket-chat"],
305
+ },
306
+
307
+ // -----------------------------------------------------------------------
308
+ // CLI setup wizard
309
+ // -----------------------------------------------------------------------
310
+ setup: {
311
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
312
+
313
+ applyAccountConfig: ({ cfg, accountId, input }) => {
314
+ const apiKey = (input.token ?? "").trim();
315
+ const apiBaseUrl = (input.httpUrl ?? "").trim() || undefined;
316
+ return applySocketChatAccountConfig({
317
+ cfg: cfg as CoreConfig,
318
+ accountId,
319
+ apiKey,
320
+ apiBaseUrl,
321
+ name: input.name,
322
+ });
337
323
  },
338
- });
324
+
325
+ validateInput: ({ input }) => {
326
+ if (!input.token?.trim()) {
327
+ return "socket-chat requires --token <apiKey>";
328
+ }
329
+ return null;
330
+ },
331
+ },
332
+
333
+ // -----------------------------------------------------------------------
334
+ // Agent prompt hints
335
+ // -----------------------------------------------------------------------
336
+ agentPrompt: {
337
+ messageToolHints: () => [
338
+ "- socket-chat: to send to a group, use target format `group:<groupId>` (groupId may contain @, e.g. `group:17581395450@chatroom`). " +
339
+ "To @mention users in a group: `group:<groupId>|<userId1>,<userId2>`.",
340
+ "- socket-chat: to send an image, provide a public HTTP URL as the media parameter.",
341
+ "- socket-chat: direct messages use the sender's contactId as the target.",
342
+ ],
343
+ },
339
344
  },
340
345
 
341
- logoutAccount: async ({ accountId }) => {
342
- const client = getActiveMqttClient(accountId);
343
- client?.end(true);
344
- return { cleared: true, loggedOut: true };
346
+ // -------------------------------------------------------------------------
347
+ // Pairing
348
+ // -------------------------------------------------------------------------
349
+ pairing: {
350
+ idLabel: "socketChatUserId",
351
+ normalizeAllowEntry: (entry) => entry.replace(/^(socket-chat|sc):/i, ""),
352
+ notifyApproval: async ({ cfg, id }) => {
353
+ // Find an account with an active MQTT connection to deliver the approval notification.
354
+ // Prefer the default account; fall back to any account with an active connection.
355
+ const accountIds = listSocketChatAccountIds(cfg as CoreConfig);
356
+ let targetAccountId = DEFAULT_ACCOUNT_ID;
357
+ for (const aid of accountIds) {
358
+ if (getActiveMqttClient(aid)) {
359
+ targetAccountId = aid;
360
+ break;
361
+ }
362
+ }
363
+ const client = getActiveMqttClient(targetAccountId);
364
+ const mqttConfig = getActiveMqttConfig(targetAccountId);
365
+ if (!client || !mqttConfig) return;
366
+ const base = parseSocketChatTarget(id);
367
+ const payload: SocketChatOutboundPayload = {
368
+ ...base,
369
+ messages: [{ type: 1, content: "You have been approved to chat with this assistant." }],
370
+ };
371
+ await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
372
+ },
345
373
  },
346
- },
347
-
348
- // -------------------------------------------------------------------------
349
- // 配置变更热重载
350
- // -------------------------------------------------------------------------
351
- reload: {
352
- configPrefixes: ["channels.socket-chat"],
353
- },
354
-
355
- // -------------------------------------------------------------------------
356
- // CLI setup 向导
357
- // -------------------------------------------------------------------------
358
- setup: {
359
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
360
-
361
- applyAccountConfig: ({ cfg, accountId, input }) => {
362
- const apiKey = (input.token ?? "").trim();
363
- const apiBaseUrl = (input.httpUrl ?? "").trim() || undefined;
364
- return applySocketChatAccountConfig({
365
- cfg: cfg as CoreConfig,
366
- accountId,
367
- apiKey,
368
- apiBaseUrl,
369
- name: input.name,
370
- });
374
+
375
+ // -------------------------------------------------------------------------
376
+ // Security
377
+ // -------------------------------------------------------------------------
378
+ security: {
379
+ resolveDmPolicy: ({ cfg, accountId, account }) => {
380
+ const resolvedId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
381
+ const usesAccountPath = Boolean(
382
+ (cfg as CoreConfig).channels?.["socket-chat"]?.accounts?.[resolvedId],
383
+ );
384
+ const basePath = usesAccountPath
385
+ ? `channels.socket-chat.accounts.${resolvedId}.`
386
+ : "channels.socket-chat.";
387
+ return {
388
+ policy: account.config.dmPolicy ?? "pairing",
389
+ allowFrom: account.config.allowFrom ?? [],
390
+ policyPath: `${basePath}dmPolicy`,
391
+ allowFromPath: basePath,
392
+ approveHint: `Run: openclaw channels pair socket-chat <userId>`,
393
+ normalizeEntry: (raw) => raw.replace(/^(socket-chat|sc):/i, ""),
394
+ };
395
+ },
396
+
397
+ collectWarnings: ({ account }) => {
398
+ const warnings: string[] = [];
399
+ if (!account.config.allowFrom?.length && account.config.dmPolicy === "open") {
400
+ warnings.push(
401
+ '- socket-chat: dmPolicy="open" allows any sender to trigger AI. ' +
402
+ 'Consider setting dmPolicy="pairing" or configuring allowFrom.',
403
+ );
404
+ }
405
+ return warnings;
406
+ },
371
407
  },
372
408
 
373
- validateInput: ({ input }) => {
374
- if (!input.token?.trim()) {
375
- return "socket-chat requires --token <apiKey>";
376
- }
377
- return null;
409
+ // -------------------------------------------------------------------------
410
+ // Outbound
411
+ // -------------------------------------------------------------------------
412
+ outbound: {
413
+ base: {
414
+ deliveryMode: "block" as const,
415
+ chunker: (text: string, limit: number) =>
416
+ getSocketChatRuntime().channel.text.chunkMarkdownText(text, limit),
417
+ chunkerMode: "markdown" as const,
418
+ textChunkLimit: 4096,
419
+ },
420
+ attachedResults: {
421
+ channel: "socket-chat",
422
+
423
+ sendText: async ({ to, text, accountId: aid }) =>
424
+ sendSocketChatText({ to, text, accountId: aid }),
425
+
426
+ sendMedia: async ({ to, text, mediaUrl, accountId: aid }) => {
427
+ const resolvedAccountId = aid ?? DEFAULT_ACCOUNT_ID;
428
+ const client = getActiveMqttClient(resolvedAccountId);
429
+ const mqttConfig = getActiveMqttConfig(resolvedAccountId);
430
+
431
+ if (!client || !mqttConfig) {
432
+ throw new Error(
433
+ `[socket-chat] No active MQTT connection for account "${resolvedAccountId}".`,
434
+ );
435
+ }
436
+
437
+ if (mediaUrl) {
438
+ const payload = buildSocketChatMediaPayload(to, mediaUrl, text);
439
+ const result = await sendSocketChatMessage({ mqttClient: client, mqttConfig, payload });
440
+ return { channel: "socket-chat", messageId: result.messageId };
441
+ }
442
+
443
+ return sendSocketChatText({ to, text, accountId: aid });
444
+ },
445
+ },
378
446
  },
379
- },
380
-
381
- // -------------------------------------------------------------------------
382
- // Agent prompt 提示词(告诉 AI 如何使用该 channel)
383
- // -------------------------------------------------------------------------
384
- agentPrompt: {
385
- messageToolHints: () => [
386
- "- socket-chat: to send to a group, use target format `group:<groupId>`. " +
387
- "To @mention users in a group: `group:<groupId>@<userId1>,<userId2>`.",
388
- "- socket-chat: to send an image, provide a public HTTP URL as the media parameter.",
389
- "- socket-chat: direct messages use the sender's contactId as the target.",
390
- ],
391
- },
392
- };
447
+ });