@wecode-ai/weibo-openclaw-plugin 1.0.0 → 1.0.1

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.
Files changed (124) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +62 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/accounts.d.ts +10 -0
  6. package/dist/src/accounts.d.ts.map +1 -0
  7. package/dist/src/accounts.js +113 -0
  8. package/dist/src/accounts.js.map +1 -0
  9. package/dist/src/bot.d.ts +26 -0
  10. package/dist/src/bot.d.ts.map +1 -0
  11. package/dist/src/bot.js +385 -0
  12. package/dist/src/bot.js.map +1 -0
  13. package/dist/src/channel.d.ts +4 -0
  14. package/dist/src/channel.d.ts.map +1 -0
  15. package/dist/src/channel.js +317 -0
  16. package/dist/src/channel.js.map +1 -0
  17. package/dist/src/client.d.ts +51 -0
  18. package/dist/src/client.d.ts.map +1 -0
  19. package/dist/src/client.js +336 -0
  20. package/dist/src/client.js.map +1 -0
  21. package/dist/src/config-schema.d.ts +86 -0
  22. package/dist/src/config-schema.d.ts.map +1 -0
  23. package/{src/config-schema.ts → dist/src/config-schema.js} +19 -24
  24. package/dist/src/config-schema.js.map +1 -0
  25. package/dist/src/fingerprint.d.ts +4 -0
  26. package/dist/src/fingerprint.d.ts.map +1 -0
  27. package/dist/src/fingerprint.js +19 -0
  28. package/dist/src/fingerprint.js.map +1 -0
  29. package/dist/src/monitor.d.ts +13 -0
  30. package/dist/src/monitor.d.ts.map +1 -0
  31. package/dist/src/monitor.js +169 -0
  32. package/dist/src/monitor.js.map +1 -0
  33. package/dist/src/outbound-stream.d.ts +32 -0
  34. package/dist/src/outbound-stream.d.ts.map +1 -0
  35. package/dist/src/outbound-stream.js +184 -0
  36. package/dist/src/outbound-stream.js.map +1 -0
  37. package/dist/src/outbound.d.ts +3 -0
  38. package/dist/src/outbound.d.ts.map +1 -0
  39. package/dist/src/outbound.js +38 -0
  40. package/dist/src/outbound.js.map +1 -0
  41. package/dist/src/plugin-sdk-compat.d.ts +21 -0
  42. package/dist/src/plugin-sdk-compat.d.ts.map +1 -0
  43. package/dist/src/plugin-sdk-compat.js +47 -0
  44. package/dist/src/plugin-sdk-compat.js.map +1 -0
  45. package/dist/src/policy.d.ts +5 -0
  46. package/dist/src/policy.d.ts.map +1 -0
  47. package/dist/src/policy.js +6 -0
  48. package/dist/src/policy.js.map +1 -0
  49. package/dist/src/runtime.d.ts +4 -0
  50. package/dist/src/runtime.d.ts.map +1 -0
  51. package/dist/src/runtime.js +11 -0
  52. package/dist/src/runtime.js.map +1 -0
  53. package/dist/src/search-schema.d.ts +6 -0
  54. package/dist/src/search-schema.d.ts.map +1 -0
  55. package/dist/src/search-schema.js +5 -0
  56. package/dist/src/search-schema.js.map +1 -0
  57. package/dist/src/send.d.ts +14 -0
  58. package/dist/src/send.d.ts.map +1 -0
  59. package/dist/src/send.js +58 -0
  60. package/dist/src/send.js.map +1 -0
  61. package/dist/src/sim-page.d.ts +51 -0
  62. package/dist/src/sim-page.d.ts.map +1 -0
  63. package/dist/src/sim-page.js +72 -0
  64. package/dist/src/sim-page.js.map +1 -0
  65. package/dist/src/sim-store.d.ts +54 -0
  66. package/dist/src/sim-store.d.ts.map +1 -0
  67. package/dist/src/sim-store.js +117 -0
  68. package/dist/src/sim-store.js.map +1 -0
  69. package/dist/src/targets.d.ts +4 -0
  70. package/dist/src/targets.d.ts.map +1 -0
  71. package/dist/src/targets.js +15 -0
  72. package/dist/src/targets.js.map +1 -0
  73. package/dist/src/token.d.ts +27 -0
  74. package/dist/src/token.d.ts.map +1 -0
  75. package/dist/src/token.js +144 -0
  76. package/dist/src/token.js.map +1 -0
  77. package/dist/src/tools-config.d.ts +21 -0
  78. package/dist/src/tools-config.d.ts.map +1 -0
  79. package/dist/src/tools-config.js +42 -0
  80. package/dist/src/tools-config.js.map +1 -0
  81. package/dist/src/types.d.ts +76 -0
  82. package/dist/src/types.d.ts.map +1 -0
  83. package/dist/src/types.js +2 -0
  84. package/dist/src/types.js.map +1 -0
  85. package/dist/src/weibo-hot-search.d.ts +44 -0
  86. package/dist/src/weibo-hot-search.d.ts.map +1 -0
  87. package/dist/src/weibo-hot-search.js +215 -0
  88. package/dist/src/weibo-hot-search.js.map +1 -0
  89. package/dist/src/weibo-search.d.ts +71 -0
  90. package/dist/src/weibo-search.d.ts.map +1 -0
  91. package/dist/src/weibo-search.js +182 -0
  92. package/dist/src/weibo-search.js.map +1 -0
  93. package/dist/src/weibo-status.d.ts +61 -0
  94. package/dist/src/weibo-status.d.ts.map +1 -0
  95. package/dist/src/weibo-status.js +194 -0
  96. package/dist/src/weibo-status.js.map +1 -0
  97. package/openclaw.plugin.json +12 -0
  98. package/package.json +4 -5
  99. package/skills/weibo-hot-search/SKILL.md +161 -0
  100. package/skills/weibo-search/SKILL.md +116 -0
  101. package/skills/weibo-status/SKILL.md +97 -0
  102. package/index.ts +0 -67
  103. package/src/accounts.ts +0 -134
  104. package/src/bot.ts +0 -486
  105. package/src/channel.ts +0 -391
  106. package/src/client.ts +0 -435
  107. package/src/fingerprint.ts +0 -25
  108. package/src/monitor.ts +0 -206
  109. package/src/outbound-stream.ts +0 -241
  110. package/src/outbound.ts +0 -49
  111. package/src/plugin-sdk-compat.ts +0 -82
  112. package/src/policy.ts +0 -10
  113. package/src/runtime.ts +0 -14
  114. package/src/search-schema.ts +0 -7
  115. package/src/send.ts +0 -80
  116. package/src/sim-page.ts +0 -140
  117. package/src/sim-store.ts +0 -186
  118. package/src/targets.ts +0 -14
  119. package/src/token.ts +0 -207
  120. package/src/tools-config.ts +0 -55
  121. package/src/types.ts +0 -95
  122. package/src/weibo-hot-search.ts +0 -345
  123. package/src/weibo-search.ts +0 -333
  124. package/src/weibo-status.ts +0 -341
package/src/client.ts DELETED
@@ -1,435 +0,0 @@
1
- import { createRequire } from "module";
2
- import WebSocket from "ws";
3
- import type { ResolvedWeiboAccount, WeiboRuntimeStatusPatch } from "./types.js";
4
-
5
- // Read version from package.json
6
- const require = createRequire(import.meta.url);
7
- const { version: PLUGIN_VERSION } = require("../package.json") as { version: string };
8
- import {
9
- getValidToken,
10
- clearTokenCache,
11
- formatWeiboTokenFetchErrorMessage,
12
- isRetryableWeiboTokenFetchError,
13
- } from "./token.js";
14
- import { getWeiboConnectionFingerprint } from "./fingerprint.js";
15
-
16
- export type WebSocketMessageHandler = (data: unknown) => void;
17
- export type WebSocketErrorHandler = (error: Error) => void;
18
- export type WebSocketCloseHandler = (code: number, reason: string) => void;
19
- export type WebSocketOpenHandler = () => void;
20
- export type WebSocketStatusHandler = (patch: WeiboRuntimeStatusPatch) => void;
21
-
22
- // Ping interval: 30 seconds
23
- const PING_INTERVAL_MS = 30_000;
24
- // Initial reconnect delay: 1 second
25
- const INITIAL_RECONNECT_DELAY_MS = 1_000;
26
- // Maximum reconnect delay: 60 seconds
27
- const MAX_RECONNECT_DELAY_MS = 60_000;
28
- // Maximum reconnect attempts (0 = infinite)
29
- const MAX_RECONNECT_ATTEMPTS = 0;
30
-
31
- export type WeiboClientOptions = {
32
- onMessage?: WebSocketMessageHandler;
33
- onError?: WebSocketErrorHandler;
34
- onClose?: WebSocketCloseHandler;
35
- onOpen?: WebSocketOpenHandler;
36
- onStatus?: WebSocketStatusHandler;
37
- autoReconnect?: boolean;
38
- maxReconnectAttempts?: number;
39
- };
40
-
41
- function isRetryableConnectError(err: unknown): boolean {
42
- const retryableTokenError = isRetryableWeiboTokenFetchError(err);
43
- if (retryableTokenError !== null) {
44
- return retryableTokenError;
45
- }
46
-
47
- if (err instanceof Error) {
48
- const message = err.message.toLowerCase();
49
- if (message.includes("not configured")) {
50
- return false;
51
- }
52
- }
53
-
54
- return true;
55
- }
56
-
57
- export class WeiboWebSocketClient {
58
- private ws: WebSocket | null = null;
59
- private messageHandler: WebSocketMessageHandler | null = null;
60
- private errorHandler: WebSocketErrorHandler | null = null;
61
- private closeHandler: WebSocketCloseHandler | null = null;
62
- private openHandler: WebSocketOpenHandler | null = null;
63
- private statusHandler: WebSocketStatusHandler | null = null;
64
-
65
- // Heartbeat
66
- private pingInterval: NodeJS.Timeout | null = null;
67
- private lastPongTime: number = 0;
68
- private readonly PING_TIMEOUT_MS = 10_000;
69
-
70
- // Reconnection
71
- private reconnectAttempts = 0;
72
- private reconnectTimeout: NodeJS.Timeout | null = null;
73
- private isConnecting = false;
74
- private shouldReconnect = true;
75
- private autoReconnect: boolean;
76
- private maxReconnectAttempts: number;
77
-
78
- constructor(
79
- private account: ResolvedWeiboAccount,
80
- private options: WeiboClientOptions = {}
81
- ) {
82
- this.autoReconnect = options.autoReconnect ?? true;
83
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? MAX_RECONNECT_ATTEMPTS;
84
- this.messageHandler = options.onMessage ?? null;
85
- this.errorHandler = options.onError ?? null;
86
- this.closeHandler = options.onClose ?? null;
87
- this.openHandler = options.onOpen ?? null;
88
- this.statusHandler = options.onStatus ?? null;
89
- }
90
-
91
- private emitStatus(patch: WeiboRuntimeStatusPatch): void {
92
- this.statusHandler?.(patch);
93
- }
94
-
95
- async connect(): Promise<void> {
96
- if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) {
97
- return;
98
- }
99
-
100
- this.isConnecting = true;
101
- this.shouldReconnect = true;
102
- this.emitStatus({
103
- running: true,
104
- connected: false,
105
- connectionState: "connecting",
106
- nextRetryAt: null,
107
- lastError: null,
108
- });
109
-
110
- const { wsEndpoint, appId, tokenEndpoint } = this.account;
111
-
112
- if (!wsEndpoint) {
113
- this.isConnecting = false;
114
- throw new Error(
115
- `WebSocket endpoint not configured for account "${this.account.accountId}"`
116
- );
117
- }
118
-
119
- if (!appId) {
120
- this.isConnecting = false;
121
- throw new Error(
122
- `App ID not configured for account "${this.account.accountId}"`
123
- );
124
- }
125
-
126
- try {
127
- // Fetch token from API
128
- const token = await getValidToken(this.account, tokenEndpoint);
129
-
130
- const url = new URL(wsEndpoint);
131
- url.searchParams.set("app_id", appId);
132
- url.searchParams.set("token", token);
133
- url.searchParams.set("version", PLUGIN_VERSION);
134
-
135
- this.ws = new WebSocket(url.toString());
136
-
137
- this.ws.on("open", () => {
138
- this.isConnecting = false;
139
- this.reconnectAttempts = 0;
140
- this.lastPongTime = Date.now();
141
- this.startHeartbeat();
142
- this.emitStatus({
143
- running: true,
144
- connected: true,
145
- connectionState: "connected",
146
- reconnectAttempts: 0,
147
- nextRetryAt: null,
148
- lastConnectedAt: Date.now(),
149
- lastError: null,
150
- });
151
- this.openHandler?.();
152
- });
153
-
154
- this.ws.on("message", (data) => {
155
- try {
156
- const text = data.toString();
157
-
158
- // Handle pong response
159
- if (text === "pong" || text === "{\"type\":\"pong\"}") {
160
- this.lastPongTime = Date.now();
161
- return;
162
- }
163
-
164
- const parsed = JSON.parse(text);
165
- this.messageHandler?.(parsed);
166
- } catch {
167
- // Ignore invalid JSON
168
- }
169
- });
170
-
171
- this.ws.on("error", (err) => {
172
- this.emitStatus({
173
- running: true,
174
- connected: false,
175
- connectionState: "error",
176
- lastError: err.message,
177
- });
178
- this.errorHandler?.(err);
179
- });
180
-
181
- this.ws.on("close", (code, reason) => {
182
- this.isConnecting = false;
183
- this.stopHeartbeat();
184
- const reasonStr = reason.toString() || "unknown";
185
- if (code === 4002 || /invalid token/i.test(reasonStr)) {
186
- clearTokenCache(this.account.accountId);
187
- }
188
- this.emitStatus({
189
- running: this.shouldReconnect,
190
- connected: false,
191
- connectionState:
192
- this.shouldReconnect && this.autoReconnect ? "error" : "stopped",
193
- lastDisconnect: {
194
- code,
195
- reason: reasonStr,
196
- at: Date.now(),
197
- },
198
- });
199
- this.closeHandler?.(code, reasonStr);
200
-
201
- // Auto reconnect if enabled
202
- if (this.shouldReconnect && this.autoReconnect) {
203
- this.scheduleReconnect(reasonStr);
204
- }
205
- });
206
-
207
- this.ws.on("pong", () => {
208
- this.lastPongTime = Date.now();
209
- });
210
- } catch (err) {
211
- this.isConnecting = false;
212
- const message =
213
- formatWeiboTokenFetchErrorMessage(err)
214
- ?? (err instanceof Error ? err.message : String(err));
215
- this.emitStatus({
216
- running: true,
217
- connected: false,
218
- connectionState: "error",
219
- lastError: message,
220
- });
221
- // Schedule reconnect on connection failure
222
- if (this.shouldReconnect && this.autoReconnect && isRetryableConnectError(err)) {
223
- this.scheduleReconnect(message);
224
- }
225
- throw err;
226
- }
227
- }
228
-
229
- private startHeartbeat(): void {
230
- this.stopHeartbeat();
231
-
232
- this.pingInterval = setInterval(() => {
233
- if (this.ws?.readyState === WebSocket.OPEN) {
234
- // Check if we received pong recently
235
- const timeSinceLastPong = Date.now() - this.lastPongTime;
236
- if (timeSinceLastPong > PING_INTERVAL_MS + this.PING_TIMEOUT_MS) {
237
- // Connection may be dead, close and reconnect
238
- console.warn(
239
- `weibo[${this.account.accountId}]: pong timeout, closing connection`
240
- );
241
- this.ws.terminate();
242
- return;
243
- }
244
-
245
- // Send ping
246
- try {
247
- this.ws.send(JSON.stringify({ type: "ping" }));
248
- } catch (err) {
249
- console.error(
250
- `weibo[${this.account.accountId}]: failed to send ping:`,
251
- err
252
- );
253
- }
254
- }
255
- }, PING_INTERVAL_MS);
256
- }
257
-
258
- private stopHeartbeat(): void {
259
- if (this.pingInterval) {
260
- clearInterval(this.pingInterval);
261
- this.pingInterval = null;
262
- }
263
- }
264
-
265
- private scheduleReconnect(lastError?: string): void {
266
- if (this.reconnectTimeout) {
267
- return;
268
- }
269
-
270
- // Check max reconnect attempts
271
- if (
272
- this.maxReconnectAttempts > 0 &&
273
- this.reconnectAttempts >= this.maxReconnectAttempts
274
- ) {
275
- console.error(
276
- `weibo[${this.account.accountId}]: max reconnect attempts reached`
277
- );
278
- return;
279
- }
280
-
281
- // Calculate delay with exponential backoff
282
- const delay = Math.min(
283
- INITIAL_RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts),
284
- MAX_RECONNECT_DELAY_MS
285
- );
286
-
287
- this.reconnectAttempts++;
288
- this.emitStatus({
289
- running: true,
290
- connected: false,
291
- connectionState: "backoff",
292
- reconnectAttempts: this.reconnectAttempts,
293
- nextRetryAt: Date.now() + delay,
294
- lastError: lastError ?? null,
295
- });
296
-
297
- console.log(
298
- `weibo[${this.account.accountId}]: reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
299
- );
300
-
301
- this.reconnectTimeout = setTimeout(() => {
302
- this.reconnectTimeout = null;
303
- this.connect().catch((err) => {
304
- console.error(
305
- `weibo[${this.account.accountId}]: reconnect failed:`,
306
- err
307
- );
308
- });
309
- }, delay);
310
- }
311
-
312
- onMessage(handler: WebSocketMessageHandler): void {
313
- this.messageHandler = handler;
314
- }
315
-
316
- onError(handler: WebSocketErrorHandler): void {
317
- this.errorHandler = handler;
318
- }
319
-
320
- onClose(handler: WebSocketCloseHandler): void {
321
- this.closeHandler = handler;
322
- }
323
-
324
- onOpen(handler: WebSocketOpenHandler): void {
325
- this.openHandler = handler;
326
- }
327
-
328
- onStatus(handler: WebSocketStatusHandler): void {
329
- this.statusHandler = handler;
330
- }
331
-
332
- send(data: unknown): boolean {
333
- if (this.ws?.readyState === WebSocket.OPEN) {
334
- try {
335
- this.ws.send(JSON.stringify(data));
336
- return true;
337
- } catch (err) {
338
- console.error(
339
- `weibo[${this.account.accountId}]: failed to send message:`,
340
- err
341
- );
342
- return false;
343
- }
344
- }
345
- return false;
346
- }
347
-
348
- isConnected(): boolean {
349
- return this.ws?.readyState === WebSocket.OPEN;
350
- }
351
-
352
- close(): void {
353
- this.shouldReconnect = false;
354
- this.stopHeartbeat();
355
-
356
- if (this.reconnectTimeout) {
357
- clearTimeout(this.reconnectTimeout);
358
- this.reconnectTimeout = null;
359
- }
360
-
361
- this.ws?.close();
362
- this.ws = null;
363
- this.emitStatus({
364
- running: false,
365
- connected: false,
366
- connectionState: "stopped",
367
- nextRetryAt: null,
368
- lastStopAt: Date.now(),
369
- });
370
- }
371
- }
372
-
373
- type ClientCacheEntry = {
374
- client: WeiboWebSocketClient;
375
- fingerprint: string;
376
- };
377
-
378
- function applyOptions(client: WeiboWebSocketClient, options?: WeiboClientOptions): void {
379
- if (!options) {
380
- return;
381
- }
382
- if (options.onMessage) {
383
- client.onMessage(options.onMessage);
384
- }
385
- if (options.onError) {
386
- client.onError(options.onError);
387
- }
388
- if (options.onClose) {
389
- client.onClose(options.onClose);
390
- }
391
- if (options.onOpen) {
392
- client.onOpen(options.onOpen);
393
- }
394
- if (options.onStatus) {
395
- client.onStatus(options.onStatus);
396
- }
397
- }
398
-
399
- // Client cache for reusing connections
400
- const clientCache = new Map<string, ClientCacheEntry>();
401
-
402
- export function createWeiboClient(
403
- account: ResolvedWeiboAccount,
404
- options?: WeiboClientOptions
405
- ): WeiboWebSocketClient {
406
- const fingerprint = getWeiboConnectionFingerprint(account);
407
- const cached = clientCache.get(account.accountId);
408
- if (cached) {
409
- if (cached.fingerprint === fingerprint) {
410
- applyOptions(cached.client, options);
411
- return cached.client;
412
- }
413
- cached.client.close();
414
- clearTokenCache(account.accountId);
415
- clientCache.delete(account.accountId);
416
- }
417
- const client = new WeiboWebSocketClient(account, options);
418
- clientCache.set(account.accountId, { client, fingerprint });
419
- return client;
420
- }
421
-
422
- export function clearClientCache(accountId?: string): void {
423
- if (accountId) {
424
- const cached = clientCache.get(accountId);
425
- if (cached) {
426
- cached.client.close();
427
- clientCache.delete(accountId);
428
- }
429
- } else {
430
- for (const cached of clientCache.values()) {
431
- cached.client.close();
432
- }
433
- clientCache.clear();
434
- }
435
- }
@@ -1,25 +0,0 @@
1
- import type { ResolvedWeiboAccount } from "./types.js";
2
-
3
- function normalizePart(value: unknown): string {
4
- return String(value ?? "").trim();
5
- }
6
-
7
- export function getWeiboConnectionFingerprint(account: ResolvedWeiboAccount): string {
8
- return JSON.stringify({
9
- appId: normalizePart(account.appId),
10
- appSecret: normalizePart(account.appSecret),
11
- wsEndpoint: normalizePart(account.wsEndpoint),
12
- tokenEndpoint: normalizePart(account.tokenEndpoint),
13
- });
14
- }
15
-
16
- export function getWeiboTokenFingerprint(
17
- account: ResolvedWeiboAccount,
18
- tokenEndpoint?: string,
19
- ): string {
20
- return JSON.stringify({
21
- appId: normalizePart(account.appId),
22
- appSecret: normalizePart(account.appSecret),
23
- tokenEndpoint: normalizePart(tokenEndpoint ?? account.tokenEndpoint),
24
- });
25
- }
package/src/monitor.ts DELETED
@@ -1,206 +0,0 @@
1
- import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
- import type { ResolvedWeiboAccount, WeiboRuntimeStatusPatch } from "./types.js";
3
- import { resolveWeiboAccount, listEnabledWeiboAccounts } from "./accounts.js";
4
- import { clearClientCache, createWeiboClient, WeiboWebSocketClient } from "./client.js";
5
- import { clearTokenCache, formatWeiboTokenFetchErrorMessage } from "./token.js";
6
- import { handleWeiboMessage, type WeiboMessageEvent } from "./bot.js";
7
- import { waitUntilAbortCompat } from "./plugin-sdk-compat.js";
8
-
9
- export type MonitorWeiboOpts = {
10
- config?: ClawdbotConfig;
11
- runtime?: RuntimeEnv;
12
- abortSignal?: AbortSignal;
13
- accountId?: string;
14
- statusSink?: (patch: WeiboRuntimeStatusPatch) => void;
15
- };
16
-
17
- // Track connections per account
18
- const wsClients = new Map<string, WeiboWebSocketClient>();
19
-
20
- async function monitorSingleAccount(params: {
21
- cfg: ClawdbotConfig;
22
- account: ResolvedWeiboAccount;
23
- runtime?: RuntimeEnv;
24
- abortSignal?: AbortSignal;
25
- statusSink?: (patch: WeiboRuntimeStatusPatch) => void;
26
- }): Promise<void> {
27
- const { cfg, account, runtime, abortSignal, statusSink } = params;
28
- const { accountId } = account;
29
- const log = runtime?.log ?? console.log;
30
- const error = runtime?.error ?? console.error;
31
- const emitStatus = (patch: WeiboRuntimeStatusPatch) => statusSink?.(patch);
32
-
33
- log(`weibo[${accountId}]: connecting WebSocket...`);
34
- emitStatus({
35
- running: true,
36
- connected: false,
37
- connectionState: "connecting",
38
- lastStartAt: Date.now(),
39
- lastStopAt: null,
40
- lastError: null,
41
- });
42
-
43
- const client = createWeiboClient(account);
44
- wsClients.set(accountId, client);
45
- client.onStatus(emitStatus);
46
-
47
- client.onMessage((data) => {
48
- try {
49
- const msg = data as { type?: string; payload?: unknown };
50
- if (msg.type === "message" && msg.payload) {
51
- emitStatus({ lastInboundAt: Date.now() });
52
- const event = msg as WeiboMessageEvent;
53
- handleWeiboMessage({ cfg, event, accountId, runtime }).catch((err) => {
54
- error(`weibo[${accountId}]: error handling message: ${String(err)}`);
55
- });
56
- }
57
- } catch (err) {
58
- error(`weibo[${accountId}]: error processing message: ${String(err)}`);
59
- }
60
- });
61
-
62
- client.onError((err) => {
63
- emitStatus({
64
- running: true,
65
- connected: false,
66
- connectionState: "error",
67
- lastError: err.message,
68
- });
69
- error(`weibo[${accountId}]: WebSocket error: ${err.message}`);
70
- });
71
-
72
- client.onOpen(() => {
73
- emitStatus({
74
- running: true,
75
- connected: true,
76
- connectionState: "connected",
77
- reconnectAttempts: 0,
78
- nextRetryAt: null,
79
- lastConnectedAt: Date.now(),
80
- lastError: null,
81
- });
82
- });
83
-
84
- client.onClose((code, reason) => {
85
- emitStatus({
86
- running: !abortSignal?.aborted,
87
- connected: false,
88
- connectionState: abortSignal?.aborted ? "stopped" : "error",
89
- lastDisconnect: {
90
- code,
91
- reason,
92
- at: Date.now(),
93
- },
94
- });
95
- log(`weibo[${accountId}]: WebSocket closed (code: ${code}, reason: ${reason})`);
96
- wsClients.delete(accountId);
97
- });
98
-
99
- // Handle abort signal
100
- const handleAbort = () => {
101
- log(`weibo[${accountId}]: abort signal received, closing connection`);
102
- client.close();
103
- wsClients.delete(accountId);
104
- emitStatus({
105
- running: false,
106
- connected: false,
107
- connectionState: "stopped",
108
- nextRetryAt: null,
109
- lastStopAt: Date.now(),
110
- });
111
- };
112
-
113
- if (abortSignal?.aborted) {
114
- handleAbort();
115
- return;
116
- }
117
-
118
- abortSignal?.addEventListener("abort", handleAbort, { once: true });
119
-
120
- try {
121
- await client.connect();
122
- log(`weibo[${accountId}]: WebSocket connected`);
123
- } catch (err) {
124
- const message = formatWeiboTokenFetchErrorMessage(err) ?? String(err);
125
- emitStatus({
126
- running: true,
127
- connected: false,
128
- connectionState: "error",
129
- lastError: message,
130
- });
131
- error(`weibo[${accountId}]: failed to connect: ${message}`);
132
- }
133
-
134
- // Keep the channel task alive until the gateway aborts it. Returning early here
135
- // causes the gateway supervisor to interpret startup as an exit and trigger
136
- // provider auto-restart loops.
137
- await waitUntilAbortCompat(abortSignal);
138
- }
139
-
140
- export async function monitorWeiboProvider(opts: MonitorWeiboOpts = {}): Promise<void> {
141
- const cfg = opts.config;
142
- if (!cfg) {
143
- throw new Error("Config is required for Weibo monitor");
144
- }
145
-
146
- const log = opts.runtime?.log ?? console.log;
147
-
148
- // If accountId is specified, only monitor that account
149
- if (opts.accountId) {
150
- const account = resolveWeiboAccount({ cfg, accountId: opts.accountId });
151
- if (!account.enabled || !account.configured) {
152
- throw new Error(`Weibo account "${opts.accountId}" not configured or disabled`);
153
- }
154
- return monitorSingleAccount({
155
- cfg,
156
- account,
157
- runtime: opts.runtime,
158
- abortSignal: opts.abortSignal,
159
- statusSink: opts.statusSink,
160
- });
161
- }
162
-
163
- // Otherwise, start all enabled accounts
164
- const accounts = listEnabledWeiboAccounts(cfg);
165
- if (accounts.length === 0) {
166
- throw new Error("No enabled Weibo accounts configured");
167
- }
168
-
169
- log(`weibo: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
170
-
171
- // Start all accounts in parallel
172
- await Promise.all(
173
- accounts.map((account) =>
174
- monitorSingleAccount({
175
- cfg,
176
- account,
177
- runtime: opts.runtime,
178
- abortSignal: opts.abortSignal,
179
- statusSink: opts.statusSink,
180
- }),
181
- ),
182
- );
183
- }
184
-
185
- export function stopWeiboMonitor(accountId?: string): void {
186
- if (accountId) {
187
- const client = wsClients.get(accountId);
188
- if (client) {
189
- client.close();
190
- wsClients.delete(accountId);
191
- }
192
- clearClientCache(accountId);
193
- clearTokenCache(accountId);
194
- } else {
195
- for (const client of wsClients.values()) {
196
- client.close();
197
- }
198
- wsClients.clear();
199
- clearClientCache();
200
- clearTokenCache();
201
- }
202
- }
203
-
204
- export async function reconnectWeiboMonitor(accountId?: string): Promise<void> {
205
- stopWeiboMonitor(accountId);
206
- }