@wecode-ai/weibo-openclaw-plugin 1.0.0

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,391 @@
1
+ import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type { ResolvedWeiboAccount, WeiboConfig } from "./types.js";
3
+ import {
4
+ resolveWeiboAccount,
5
+ listWeiboAccountIds,
6
+ resolveDefaultWeiboAccountId,
7
+ } from "./accounts.js";
8
+ import { weiboOutbound } from "./outbound.js";
9
+ import { normalizeWeiboTarget, looksLikeWeiboId } from "./targets.js";
10
+ import { sendMessageWeibo } from "./send.js";
11
+
12
+ const DEFAULT_ACCOUNT_ID = "default";
13
+ const PAIRING_APPROVED_MESSAGE = "✓ You have been approved to chat with this agent.";
14
+
15
+ const meta = {
16
+ id: "weibo",
17
+ label: "Weibo",
18
+ selectionLabel: "Weibo (微博)",
19
+ docsPath: "/channels/weibo",
20
+ docsLabel: "weibo",
21
+ blurb: "Weibo direct message channel.",
22
+ order: 80,
23
+ };
24
+
25
+ export const weiboPlugin: ChannelPlugin<ResolvedWeiboAccount> = {
26
+ id: "weibo",
27
+ meta,
28
+
29
+ pairing: {
30
+ idLabel: "weiboUserId",
31
+ normalizeAllowEntry: (entry: string) => entry.replace(/^user:/i, ""),
32
+ notifyApproval: async ({ cfg, id }) => {
33
+ await sendMessageWeibo({
34
+ cfg,
35
+ to: id,
36
+ text: PAIRING_APPROVED_MESSAGE,
37
+ });
38
+ },
39
+ },
40
+
41
+ capabilities: {
42
+ chatTypes: ["direct"],
43
+ polls: false,
44
+ threads: false,
45
+ media: false,
46
+ reactions: false,
47
+ edit: false,
48
+ reply: false,
49
+ },
50
+
51
+ agentPrompt: {
52
+ messageToolHints: () => [
53
+ "- Weibo targeting: omit `target` to reply to the current conversation. Explicit targets: `user:userId`.",
54
+ ],
55
+ },
56
+
57
+ reload: { configPrefixes: ["channels.weibo"] },
58
+
59
+ configSchema: {
60
+ schema: {
61
+ type: "object",
62
+ additionalProperties: false,
63
+ properties: {
64
+ enabled: { type: "boolean", default: true, title: "启用账号", description: "是否启用此微博账号" },
65
+ appId: { type: "string", title: "App ID", description: "App ID" },
66
+ appSecret: { type: "string", title: "App Secret", description: "App Secret" },
67
+ wsEndpoint: { type: "string", format: "uri", default: "ws://open-im.api.weibo.com/ws/stream", title: "WebSocket 地址", description: "微博 WebSocket 服务地址" },
68
+ tokenEndpoint: { type: "string", format: "uri", default: "http://open-im.api.weibo.com/open/auth/ws_token", title: "Token 服务地址", description: "获取 WebSocket Token 的服务地址" },
69
+ dmPolicy: { type: "string", enum: ["open", "pairing"], default: "open", title: "私信策略", description: "open=允许所有人私信, pairing=仅允许配对用户" },
70
+ allowFrom: { type: "array", items: { type: "string" }, title: "允许列表", description: "允许发送私信的用户 ID 列表(白名单)" },
71
+ textChunkLimit: { type: "integer", minimum: 1, title: "文本分片限制", description: "单条消息最大字符数,超出后自动分片" },
72
+ chunkMode: { type: "string", enum: ["length", "newline", "raw"], default: "raw", title: "分片模式", description: "newline=按段落分片, length=按字符数分片, raw=转发上游分片" },
73
+ accounts: {
74
+ type: "object",
75
+ additionalProperties: {
76
+ type: "object",
77
+ properties: {
78
+ enabled: { type: "boolean", default: true, title: "启用账号", description: "是否启用此微博账号" },
79
+ name: { type: "string", title: "账号名称", description: "账号显示名称(可选)" },
80
+ appId: { type: "string", title: "App ID", description: "App ID" },
81
+ appSecret: { type: "string", title: "App Secret", description: "App Secret" },
82
+ wsEndpoint: { type: "string", default: "ws://open-im.api.weibo.com/ws/stream", title: "WebSocket 地址", description: "微博 WebSocket 服务地址" },
83
+ tokenEndpoint: { type: "string", default: "http://open-im.api.weibo.com/open/auth/ws_token", title: "Token 服务地址", description: "获取 WebSocket Token 的服务地址" },
84
+ textChunkLimit: { type: "integer", minimum: 1, title: "文本分片限制", description: "单条消息最大字符数,超出后自动分片" },
85
+ chunkMode: { type: "string", enum: ["length", "newline", "raw"], default: "raw", title: "分片模式", description: "newline=按段落分片, length=按字符数分片, raw=转发上游分片" },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+
93
+ config: {
94
+ listAccountIds: (cfg: ClawdbotConfig) => listWeiboAccountIds(cfg),
95
+ resolveAccount: (cfg: ClawdbotConfig, accountId?: string | null) =>
96
+ resolveWeiboAccount({ cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID }),
97
+ defaultAccountId: (cfg: ClawdbotConfig) => resolveDefaultWeiboAccountId(cfg),
98
+ setAccountEnabled: ({
99
+ cfg,
100
+ accountId,
101
+ enabled,
102
+ }: {
103
+ cfg: ClawdbotConfig;
104
+ accountId: string;
105
+ enabled: boolean;
106
+ }) => {
107
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
108
+
109
+ if (isDefault) {
110
+ return {
111
+ ...cfg,
112
+ channels: {
113
+ ...cfg.channels,
114
+ weibo: {
115
+ ...cfg.channels?.weibo,
116
+ enabled,
117
+ },
118
+ },
119
+ };
120
+ }
121
+
122
+ const weiboCfg = cfg.channels?.weibo as WeiboConfig | undefined;
123
+ return {
124
+ ...cfg,
125
+ channels: {
126
+ ...cfg.channels,
127
+ weibo: {
128
+ ...weiboCfg,
129
+ accounts: {
130
+ ...weiboCfg?.accounts,
131
+ [accountId]: {
132
+ ...weiboCfg?.accounts?.[accountId],
133
+ enabled,
134
+ },
135
+ },
136
+ },
137
+ },
138
+ };
139
+ },
140
+ deleteAccount: ({
141
+ cfg,
142
+ accountId,
143
+ }: {
144
+ cfg: ClawdbotConfig;
145
+ accountId: string;
146
+ }) => {
147
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
148
+
149
+ if (isDefault) {
150
+ const next = { ...cfg } as ClawdbotConfig;
151
+ const nextChannels = { ...cfg.channels };
152
+ delete (nextChannels as Record<string, unknown>).weibo;
153
+ if (Object.keys(nextChannels).length > 0) {
154
+ next.channels = nextChannels;
155
+ } else {
156
+ delete next.channels;
157
+ }
158
+ return next;
159
+ }
160
+
161
+ const weiboCfg = cfg.channels?.weibo as WeiboConfig | undefined;
162
+ const accounts = { ...weiboCfg?.accounts };
163
+ delete accounts[accountId];
164
+
165
+ return {
166
+ ...cfg,
167
+ channels: {
168
+ ...cfg.channels,
169
+ weibo: {
170
+ ...weiboCfg,
171
+ accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
172
+ },
173
+ },
174
+ };
175
+ },
176
+ isConfigured: (account: ResolvedWeiboAccount) => account.configured,
177
+ describeAccount: (account: ResolvedWeiboAccount) => ({
178
+ accountId: account.accountId,
179
+ enabled: account.enabled,
180
+ configured: account.configured,
181
+ name: account.name,
182
+ appId: account.appId,
183
+ }),
184
+ resolveAllowFrom: ({
185
+ cfg,
186
+ accountId,
187
+ }: {
188
+ cfg: ClawdbotConfig;
189
+ accountId?: string | null;
190
+ }) => {
191
+ const account = resolveWeiboAccount({ cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID });
192
+ return (account.config?.allowFrom ?? [])
193
+ .map((entry) => String(entry).trim())
194
+ .filter(Boolean);
195
+ },
196
+ formatAllowFrom: ({
197
+ allowFrom,
198
+ }: {
199
+ cfg: ClawdbotConfig;
200
+ accountId?: string | null;
201
+ allowFrom: (string | number)[];
202
+ }) =>
203
+ allowFrom
204
+ .map((entry) => String(entry).trim())
205
+ .filter(Boolean),
206
+ },
207
+
208
+ security: {
209
+ collectWarnings: () => [],
210
+ },
211
+
212
+ setup: {
213
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
214
+ applyAccountConfig: ({
215
+ cfg,
216
+ accountId,
217
+ }: {
218
+ cfg: ClawdbotConfig;
219
+ accountId?: string;
220
+ }) => {
221
+ const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
222
+
223
+ if (isDefault) {
224
+ return {
225
+ ...cfg,
226
+ channels: {
227
+ ...cfg.channels,
228
+ weibo: {
229
+ ...cfg.channels?.weibo,
230
+ enabled: true,
231
+ },
232
+ },
233
+ };
234
+ }
235
+
236
+ const weiboCfg = cfg.channels?.weibo as WeiboConfig | undefined;
237
+ return {
238
+ ...cfg,
239
+ channels: {
240
+ ...cfg.channels,
241
+ weibo: {
242
+ ...weiboCfg,
243
+ accounts: {
244
+ ...weiboCfg?.accounts,
245
+ [accountId]: {
246
+ ...weiboCfg?.accounts?.[accountId],
247
+ enabled: true,
248
+ },
249
+ },
250
+ },
251
+ },
252
+ };
253
+ },
254
+ },
255
+
256
+ messaging: {
257
+ normalizeTarget: (raw: string) => {
258
+ const result = normalizeWeiboTarget(raw);
259
+ return result || undefined;
260
+ },
261
+ targetResolver: {
262
+ looksLikeId: looksLikeWeiboId,
263
+ hint: "<userId>",
264
+ },
265
+ },
266
+
267
+ directory: {
268
+ self: async () => null,
269
+ listPeers: async () => [],
270
+ listGroups: async () => [],
271
+ listPeersLive: async () => [],
272
+ listGroupsLive: async () => [],
273
+ },
274
+
275
+ outbound: weiboOutbound,
276
+
277
+ status: {
278
+ defaultRuntime: {
279
+ accountId: DEFAULT_ACCOUNT_ID,
280
+ running: false,
281
+ connected: false,
282
+ mode: "idle",
283
+ reconnectAttempts: 0,
284
+ lastConnectedAt: null,
285
+ lastDisconnect: null,
286
+ lastStartAt: null,
287
+ lastStopAt: null,
288
+ lastInboundAt: null,
289
+ lastOutboundAt: null,
290
+ lastError: null,
291
+ port: null,
292
+ } as never,
293
+ buildChannelSummary: ({ snapshot }: { snapshot: Record<string, unknown> }) => ({
294
+ configured: (snapshot.configured as boolean) ?? false,
295
+ running: (snapshot.running as boolean) ?? false,
296
+ connected: (snapshot.connected as boolean) ?? false,
297
+ connectionState:
298
+ (snapshot.connectionState as string | null)
299
+ ?? (snapshot.mode as string | null)
300
+ ?? null,
301
+ reconnectAttempts: (snapshot.reconnectAttempts as number | null) ?? 0,
302
+ nextRetryAt: (snapshot.nextRetryAt as number | null) ?? null,
303
+ lastConnectedAt: (snapshot.lastConnectedAt as number | null) ?? null,
304
+ lastDisconnect: (snapshot.lastDisconnect as Record<string, unknown> | null) ?? null,
305
+ lastStartAt: (snapshot.lastStartAt as number | null) ?? null,
306
+ lastStopAt: (snapshot.lastStopAt as number | null) ?? null,
307
+ lastInboundAt: (snapshot.lastInboundAt as number | null) ?? null,
308
+ lastOutboundAt: (snapshot.lastOutboundAt as number | null) ?? null,
309
+ lastError: (snapshot.lastError as string | null) ?? null,
310
+ port: (snapshot.port as number | null) ?? null,
311
+ }),
312
+ probeAccount: async () => ({ ok: true }),
313
+ buildAccountSnapshot: ({ account, runtime }) => {
314
+ const runtimeRecord = runtime as Record<string, unknown> | undefined;
315
+ const disconnect = runtime?.lastDisconnect as
316
+ | { at?: unknown; code?: unknown; reason?: unknown }
317
+ | string
318
+ | null
319
+ | undefined;
320
+
321
+ return {
322
+ accountId: account.accountId,
323
+ enabled: account.enabled,
324
+ configured: account.configured,
325
+ name: account.name,
326
+ appId: account.appId,
327
+ running: (runtime?.running as boolean) ?? false,
328
+ connected: (runtime?.connected as boolean) ?? false,
329
+ mode:
330
+ (runtimeRecord?.connectionState as string | null)
331
+ ?? (runtime?.mode as string | null)
332
+ ?? null,
333
+ reconnectAttempts: (runtime?.reconnectAttempts as number | null) ?? 0,
334
+ lastConnectedAt: (runtime?.lastConnectedAt as number | null) ?? null,
335
+ lastDisconnect:
336
+ disconnect && typeof disconnect === "object"
337
+ ? {
338
+ at: Number(disconnect.at ?? Date.now()),
339
+ status:
340
+ typeof disconnect.code === "number" ? disconnect.code : undefined,
341
+ error:
342
+ typeof disconnect.reason === "string" ? disconnect.reason : undefined,
343
+ }
344
+ : disconnect ?? null,
345
+ lastStartAt: (runtime?.lastStartAt as number | null) ?? null,
346
+ lastStopAt: (runtime?.lastStopAt as number | null) ?? null,
347
+ lastInboundAt: (runtime?.lastInboundAt as number | null) ?? null,
348
+ lastOutboundAt: (runtime?.lastOutboundAt as number | null) ?? null,
349
+ lastError: (runtime?.lastError as string | null) ?? null,
350
+ port: (runtime?.port as number | null) ?? null,
351
+ connectionState:
352
+ (runtimeRecord?.connectionState as string | null)
353
+ ?? (runtime?.mode as string | null)
354
+ ?? null,
355
+ nextRetryAt: (runtimeRecord?.nextRetryAt as number | null) ?? null,
356
+ } as never;
357
+ },
358
+ },
359
+
360
+ gateway: {
361
+ startAccount: async (ctx) => {
362
+ const { monitorWeiboProvider } = await import("./monitor.js");
363
+ ctx.setStatus({
364
+ accountId: ctx.accountId,
365
+ port: null,
366
+ running: true,
367
+ connected: false,
368
+ mode: "connecting",
369
+ lastStartAt: Date.now(),
370
+ lastStopAt: null,
371
+ lastError: null,
372
+ } as never);
373
+ ctx.log?.info(`starting weibo[${ctx.accountId}] WebSocket`);
374
+ return monitorWeiboProvider({
375
+ config: ctx.cfg,
376
+ runtime: ctx.runtime,
377
+ abortSignal: ctx.abortSignal,
378
+ accountId: ctx.accountId,
379
+ statusSink: (patch) =>
380
+ ctx.setStatus({
381
+ accountId: ctx.accountId,
382
+ port: null,
383
+ ...patch,
384
+ mode:
385
+ (patch.connectionState as string | undefined)
386
+ ?? (ctx.getStatus().mode as string | undefined),
387
+ } as never),
388
+ });
389
+ },
390
+ },
391
+ };