@43world/43chat-openclaw-plugin 0.1.6

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 ADDED
@@ -0,0 +1,415 @@
1
+ import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type { Chat43Config, Resolved43ChatAccount } from "./types.js";
3
+ import {
4
+ list43ChatAccountIds,
5
+ resolve43ChatAccount,
6
+ resolveDefault43ChatAccountId,
7
+ } from "./accounts.js";
8
+ import { chat43Outbound } from "./outbound.js";
9
+ import { probe43ChatAccount } from "./client.js";
10
+ import { looksLike43ChatId, normalize43ChatTarget } from "./targets.js";
11
+ import { sendMessage43Chat } from "./send.js";
12
+ import packageJson from "../package.json" with { type: "json" };
13
+
14
+ const DEFAULT_ACCOUNT_ID = "default";
15
+ const PAIRING_APPROVED_MESSAGE = "✓ You have been approved to chat with this agent.";
16
+
17
+ const meta = {
18
+ id: "43chat",
19
+ label: "43Chat",
20
+ selectionLabel: "43Chat",
21
+ docsPath: "/channels/43chat",
22
+ docsLabel: "43chat",
23
+ blurb: "43Chat OpenAPI + SSE channel.",
24
+ order: 85,
25
+ version: packageJson.version,
26
+ };
27
+
28
+ export const chat43Plugin: ChannelPlugin<Resolved43ChatAccount> = {
29
+ id: "43chat",
30
+ meta,
31
+
32
+ pairing: {
33
+ idLabel: "chat43Id",
34
+ normalizeAllowEntry: (entry: string) => entry.replace(/^43chat:/i, ""),
35
+ notifyApproval: async ({ cfg, id }) => {
36
+ await sendMessage43Chat({
37
+ cfg,
38
+ to: id,
39
+ text: PAIRING_APPROVED_MESSAGE,
40
+ });
41
+ },
42
+ },
43
+
44
+ capabilities: {
45
+ chatTypes: ["direct", "group"],
46
+ polls: false,
47
+ threads: false,
48
+ media: false,
49
+ reactions: false,
50
+ edit: false,
51
+ reply: false,
52
+ },
53
+
54
+ agentPrompt: {
55
+ messageToolHints: () => [
56
+ "- 🚨 核心规则:只发送最终回复内容到 43Chat",
57
+ "- 回复 43Chat 消息时流程:1. 内部静默查看历史记录 2. 分析和思考 3. 只发送一次最终回复",
58
+ "- ❌ 严禁发送:思考过程、内部信号、系统错误、调试/日志、指令输出、非人类内容、无意义标记",
59
+ "- ✅ 只发送:最终人类可读回复,一次性发送",
60
+ '- 发送前自查:"这是人类会说的话吗?" 直接回复,无多余说明或标记。',
61
+ "- 43Chat 目标说明:省略 `target` 表示回复当前会话;显式目标格式为 `user:<id>` 或 `group:<id>`。",
62
+ ],
63
+ },
64
+
65
+ reload: { configPrefixes: ["channels.43chat"] },
66
+
67
+ configSchema: {
68
+ schema: {
69
+ type: "object",
70
+ additionalProperties: false,
71
+ properties: {
72
+ enabled: { type: "boolean", default: true, title: "启用账号" },
73
+ baseUrl: { type: "string", format: "uri", default: "https://43chat.cn", title: "43Chat 地址" },
74
+ apiKey: { type: "string", title: "API Key" },
75
+ version: { type: "string", default: packageJson.version, title: "当前插件版本" },
76
+ dmPolicy: {
77
+ type: "string",
78
+ enum: ["open", "pairing"],
79
+ default: "open",
80
+ title: "私信策略",
81
+ },
82
+ allowFrom: {
83
+ type: "array",
84
+ items: { type: "string" },
85
+ title: "允许列表",
86
+ },
87
+ requestTimeoutMs: {
88
+ type: "integer",
89
+ minimum: 1000,
90
+ title: "请求超时(ms)",
91
+ },
92
+ sseReconnectDelayMs: {
93
+ type: "integer",
94
+ minimum: 100,
95
+ title: "SSE重连起始延迟(ms)",
96
+ },
97
+ sseMaxReconnectDelayMs: {
98
+ type: "integer",
99
+ minimum: 1000,
100
+ title: "SSE最大重连延迟(ms)",
101
+ },
102
+ textChunkLimit: {
103
+ type: "integer",
104
+ minimum: 1,
105
+ title: "文本分片限制",
106
+ },
107
+ chunkMode: {
108
+ type: "string",
109
+ enum: ["length", "newline", "raw"],
110
+ default: "newline",
111
+ title: "分片模式",
112
+ },
113
+ blockStreaming: {
114
+ type: "boolean",
115
+ default: false,
116
+ title: "启用流式块发送",
117
+ },
118
+ accounts: {
119
+ type: "object",
120
+ additionalProperties: {
121
+ type: "object",
122
+ properties: {
123
+ enabled: { type: "boolean", default: true, title: "启用账号" },
124
+ name: { type: "string", title: "账号名称" },
125
+ baseUrl: { type: "string", default: "https://43chat.cn", format: "uri", title: "43Chat 地址" },
126
+ apiKey: { type: "string", title: "API Key" },
127
+ requestTimeoutMs: { type: "integer", minimum: 1000, title: "请求超时(ms)" },
128
+ sseReconnectDelayMs: { type: "integer", minimum: 100, title: "SSE重连起始延迟(ms)" },
129
+ sseMaxReconnectDelayMs: { type: "integer", minimum: 1000, title: "SSE最大重连延迟(ms)" },
130
+ textChunkLimit: { type: "integer", minimum: 1, title: "文本分片限制" },
131
+ chunkMode: {
132
+ type: "string",
133
+ enum: ["length", "newline", "raw"],
134
+ default: "newline",
135
+ title: "分片模式",
136
+ },
137
+ blockStreaming: {
138
+ type: "boolean",
139
+ default: false,
140
+ title: "启用流式块发送",
141
+ },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ },
147
+ },
148
+
149
+ config: {
150
+ listAccountIds: (cfg: ClawdbotConfig) => list43ChatAccountIds(cfg),
151
+ resolveAccount: (cfg: ClawdbotConfig, accountId?: string | null) =>
152
+ resolve43ChatAccount({ cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID }),
153
+ defaultAccountId: (cfg: ClawdbotConfig) => resolveDefault43ChatAccountId(cfg),
154
+ setAccountEnabled: ({
155
+ cfg,
156
+ accountId,
157
+ enabled,
158
+ }: {
159
+ cfg: ClawdbotConfig;
160
+ accountId: string;
161
+ enabled: boolean;
162
+ }) => {
163
+ if (accountId === DEFAULT_ACCOUNT_ID) {
164
+ return {
165
+ ...cfg,
166
+ channels: {
167
+ ...cfg.channels,
168
+ ["43chat"]: {
169
+ ...(cfg.channels?.["43chat"] as Record<string, unknown> | undefined),
170
+ enabled,
171
+ },
172
+ },
173
+ };
174
+ }
175
+
176
+ const chatCfg = cfg.channels?.["43chat"] as Chat43Config | undefined;
177
+ return {
178
+ ...cfg,
179
+ channels: {
180
+ ...cfg.channels,
181
+ ["43chat"]: {
182
+ ...chatCfg,
183
+ accounts: {
184
+ ...chatCfg?.accounts,
185
+ [accountId]: {
186
+ ...chatCfg?.accounts?.[accountId],
187
+ enabled,
188
+ },
189
+ },
190
+ },
191
+ },
192
+ };
193
+ },
194
+ deleteAccount: ({
195
+ cfg,
196
+ accountId,
197
+ }: {
198
+ cfg: ClawdbotConfig;
199
+ accountId: string;
200
+ }) => {
201
+ if (accountId === DEFAULT_ACCOUNT_ID) {
202
+ const next = { ...cfg } as ClawdbotConfig;
203
+ const nextChannels = { ...cfg.channels };
204
+ delete (nextChannels as Record<string, unknown>)["43chat"];
205
+ if (Object.keys(nextChannels).length > 0) {
206
+ next.channels = nextChannels;
207
+ } else {
208
+ delete next.channels;
209
+ }
210
+ return next;
211
+ }
212
+
213
+ const chatCfg = cfg.channels?.["43chat"] as Chat43Config | undefined;
214
+ const accounts = { ...chatCfg?.accounts };
215
+ delete accounts[accountId];
216
+ return {
217
+ ...cfg,
218
+ channels: {
219
+ ...cfg.channels,
220
+ ["43chat"]: {
221
+ ...chatCfg,
222
+ accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
223
+ },
224
+ },
225
+ };
226
+ },
227
+ isConfigured: (account: Resolved43ChatAccount) => account.configured,
228
+ describeAccount: (account: Resolved43ChatAccount) => ({
229
+ accountId: account.accountId,
230
+ enabled: account.enabled,
231
+ configured: account.configured,
232
+ name: account.name,
233
+ baseUrl: account.baseUrl,
234
+ }),
235
+ resolveAllowFrom: ({
236
+ cfg,
237
+ accountId,
238
+ }: {
239
+ cfg: ClawdbotConfig;
240
+ accountId?: string | null;
241
+ }) => {
242
+ const account = resolve43ChatAccount({ cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID });
243
+ return (account.config.allowFrom ?? [])
244
+ .map((entry) => String(entry).trim())
245
+ .filter(Boolean);
246
+ },
247
+ formatAllowFrom: ({ allowFrom }: { allowFrom: (string | number)[] }) =>
248
+ allowFrom
249
+ .map((entry) => String(entry).trim())
250
+ .filter(Boolean),
251
+ },
252
+
253
+ security: {
254
+ collectWarnings: () => [],
255
+ },
256
+
257
+ setup: {
258
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
259
+ applyAccountConfig: ({
260
+ cfg,
261
+ accountId,
262
+ }: {
263
+ cfg: ClawdbotConfig;
264
+ accountId?: string;
265
+ }) => {
266
+ const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
267
+ if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
268
+ return {
269
+ ...cfg,
270
+ channels: {
271
+ ...cfg.channels,
272
+ ["43chat"]: {
273
+ ...(cfg.channels?.["43chat"] as Record<string, unknown> | undefined),
274
+ enabled: true,
275
+ },
276
+ },
277
+ };
278
+ }
279
+
280
+ const chatCfg = cfg.channels?.["43chat"] as Chat43Config | undefined;
281
+ return {
282
+ ...cfg,
283
+ channels: {
284
+ ...cfg.channels,
285
+ ["43chat"]: {
286
+ ...chatCfg,
287
+ accounts: {
288
+ ...chatCfg?.accounts,
289
+ [resolvedAccountId]: {
290
+ ...chatCfg?.accounts?.[resolvedAccountId],
291
+ enabled: true,
292
+ },
293
+ },
294
+ },
295
+ },
296
+ };
297
+ },
298
+ },
299
+
300
+ messaging: {
301
+ normalizeTarget: (raw: string) => normalize43ChatTarget(raw) ?? undefined,
302
+ targetResolver: {
303
+ looksLikeId: looksLike43ChatId,
304
+ hint: "<user:<id>|group:<id>>",
305
+ },
306
+ },
307
+
308
+ directory: {
309
+ self: async () => null,
310
+ listPeers: async () => [],
311
+ listGroups: async () => [],
312
+ listPeersLive: async () => [],
313
+ listGroupsLive: async () => [],
314
+ },
315
+
316
+ outbound: chat43Outbound,
317
+
318
+ status: {
319
+ defaultRuntime: {
320
+ accountId: DEFAULT_ACCOUNT_ID,
321
+ running: false,
322
+ connected: false,
323
+ mode: "idle",
324
+ reconnectAttempts: 0,
325
+ lastConnectedAt: null,
326
+ lastDisconnect: null,
327
+ lastStartAt: null,
328
+ lastStopAt: null,
329
+ lastInboundAt: null,
330
+ lastOutboundAt: null,
331
+ lastError: null,
332
+ port: null,
333
+ } as never,
334
+ buildChannelSummary: ({ snapshot }: { snapshot: Record<string, unknown> }) => ({
335
+ configured: (snapshot.configured as boolean) ?? false,
336
+ running: (snapshot.running as boolean) ?? false,
337
+ connected: (snapshot.connected as boolean) ?? false,
338
+ connectionState:
339
+ (snapshot.connectionState as string | null)
340
+ ?? (snapshot.mode as string | null)
341
+ ?? null,
342
+ reconnectAttempts: (snapshot.reconnectAttempts as number | null) ?? 0,
343
+ nextRetryAt: (snapshot.nextRetryAt as number | null) ?? null,
344
+ lastConnectedAt: (snapshot.lastConnectedAt as number | null) ?? null,
345
+ lastDisconnect: (snapshot.lastDisconnect as Record<string, unknown> | null) ?? null,
346
+ lastStartAt: (snapshot.lastStartAt as number | null) ?? null,
347
+ lastStopAt: (snapshot.lastStopAt as number | null) ?? null,
348
+ lastInboundAt: (snapshot.lastInboundAt as number | null) ?? null,
349
+ lastOutboundAt: (snapshot.lastOutboundAt as number | null) ?? null,
350
+ lastError: (snapshot.lastError as string | null) ?? null,
351
+ baseUrl: (snapshot.baseUrl as string | null) ?? null,
352
+ probe: snapshot.probe,
353
+ lastProbeAt: (snapshot.lastProbeAt as number | null) ?? null,
354
+ }),
355
+ probeAccount: async ({ account }) => probe43ChatAccount({ account }),
356
+ buildAccountSnapshot: ({ account, runtime, probe }) => {
357
+ const runtimeRecord = runtime as Record<string, unknown> | undefined;
358
+ return {
359
+ accountId: account.accountId,
360
+ enabled: account.enabled,
361
+ configured: account.configured,
362
+ name: account.name,
363
+ baseUrl: account.baseUrl,
364
+ running: (runtime?.running as boolean) ?? false,
365
+ connected: (runtime?.connected as boolean) ?? (probe as { ok?: boolean } | undefined)?.ok ?? false,
366
+ mode:
367
+ (runtimeRecord?.connectionState as string | null)
368
+ ?? (runtime?.mode as string | null)
369
+ ?? null,
370
+ reconnectAttempts: (runtime?.reconnectAttempts as number | null) ?? 0,
371
+ lastConnectedAt: (runtime?.lastConnectedAt as number | null) ?? null,
372
+ lastDisconnect: (runtime?.lastDisconnect as Record<string, unknown> | null) ?? null,
373
+ lastStartAt: (runtime?.lastStartAt as number | null) ?? null,
374
+ lastStopAt: (runtime?.lastStopAt as number | null) ?? null,
375
+ lastInboundAt: (runtime?.lastInboundAt as number | null) ?? null,
376
+ lastOutboundAt: (runtime?.lastOutboundAt as number | null) ?? null,
377
+ lastError: (runtime?.lastError as string | null) ?? null,
378
+ nextRetryAt: (runtimeRecord?.nextRetryAt as number | null) ?? null,
379
+ probe,
380
+ } as never;
381
+ },
382
+ },
383
+
384
+ gateway: {
385
+ startAccount: async (ctx) => {
386
+ const { monitor43ChatProvider } = await import("./monitor.js");
387
+ ctx.setStatus({
388
+ accountId: ctx.accountId,
389
+ baseUrl: ctx.account.baseUrl ?? null,
390
+ running: true,
391
+ connected: false,
392
+ mode: "connecting",
393
+ lastStartAt: Date.now(),
394
+ lastStopAt: null,
395
+ lastError: null,
396
+ } as never);
397
+ ctx.log?.info(`starting 43chat[${ctx.accountId}] SSE`);
398
+ return monitor43ChatProvider({
399
+ config: ctx.cfg,
400
+ runtime: ctx.runtime,
401
+ abortSignal: ctx.abortSignal,
402
+ accountId: ctx.accountId,
403
+ statusSink: (patch) =>
404
+ ctx.setStatus({
405
+ accountId: ctx.accountId,
406
+ baseUrl: ctx.account.baseUrl ?? null,
407
+ ...patch,
408
+ mode:
409
+ (patch.connectionState as string | undefined)
410
+ ?? (ctx.getStatus().mode as string | undefined),
411
+ } as never),
412
+ });
413
+ },
414
+ },
415
+ };