@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/README.md +9 -0
- package/channel-api.ts +3 -0
- package/index.ts +11 -12
- package/package.json +1 -1
- package/runtime-api.ts +3 -0
- package/src/__sdk-stub__.ts +26 -8
- package/src/channel.ts +391 -336
- package/src/inbound.test.ts +405 -583
- package/src/inbound.ts +175 -407
- package/src/mqtt-client.ts +66 -50
- package/src/outbound.test.ts +36 -26
- package/src/outbound.ts +37 -75
- package/src/runtime-api.ts +28 -0
- package/src/runtime.ts +8 -12
- package/tsconfig.json +1 -1
- package/vitest.config.ts +13 -7
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
|
-
|
|
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
|
-
|
|
26
|
+
buildSocketChatMediaPayload,
|
|
27
27
|
looksLikeSocketChatTargetId,
|
|
28
28
|
normalizeSocketChatTarget,
|
|
29
|
+
parseSocketChatTarget,
|
|
29
30
|
sendSocketChatMessage,
|
|
30
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
128
|
-
|
|
130
|
+
// -----------------------------------------------------------------------
|
|
131
|
+
// Account config management
|
|
132
|
+
// -----------------------------------------------------------------------
|
|
133
|
+
config: {
|
|
134
|
+
listAccountIds: (cfg) => listSocketChatAccountIds(cfg as CoreConfig),
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
resolveAccount: (cfg, accountId) =>
|
|
137
|
+
resolveSocketChatAccount(cfg as CoreConfig, accountId ?? DEFAULT_ACCOUNT_ID),
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
defaultAccountId: (cfg) => resolveDefaultSocketChatAccountId(cfg as CoreConfig),
|
|
134
140
|
|
|
135
|
-
|
|
141
|
+
isConfigured: (account) => isSocketChatAccountConfigured(account),
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
return account.config.defaultTo?.trim() || undefined;
|
|
158
|
-
},
|
|
172
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
173
|
+
setSocketChatAccountEnabled({ cfg: cfg as CoreConfig, accountId, enabled }),
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
});
|