@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/README.md +59 -0
- package/index.ts +67 -0
- package/package.json +51 -0
- package/src/accounts.ts +134 -0
- package/src/bot.ts +486 -0
- package/src/channel.ts +391 -0
- package/src/client.ts +435 -0
- package/src/config-schema.ts +58 -0
- package/src/fingerprint.ts +25 -0
- package/src/monitor.ts +206 -0
- package/src/outbound-stream.ts +241 -0
- package/src/outbound.ts +49 -0
- package/src/plugin-sdk-compat.ts +82 -0
- package/src/policy.ts +10 -0
- package/src/runtime.ts +14 -0
- package/src/search-schema.ts +7 -0
- package/src/send.ts +80 -0
- package/src/sim-page.ts +140 -0
- package/src/sim-store.ts +186 -0
- package/src/targets.ts +14 -0
- package/src/token.ts +207 -0
- package/src/tools-config.ts +55 -0
- package/src/types.ts +95 -0
- package/src/weibo-hot-search.ts +345 -0
- package/src/weibo-search.ts +333 -0
- package/src/weibo-status.ts +341 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { z };
|
|
3
|
+
|
|
4
|
+
const DmPolicySchema = z.enum(["open", "pairing"]).default("open");
|
|
5
|
+
|
|
6
|
+
// Chunk mode:
|
|
7
|
+
// - length: split by character limit
|
|
8
|
+
// - newline: split at paragraph boundaries (blank lines)
|
|
9
|
+
// - raw: forward upstream chunks as-is (no secondary chunking)
|
|
10
|
+
const ChunkModeSchema = z.enum(["length", "newline", "raw"]).default("raw");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Weibo tools configuration.
|
|
14
|
+
* Controls which tool categories are enabled.
|
|
15
|
+
*/
|
|
16
|
+
export const WeiboToolsConfigSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
search: z.boolean().optional(), // Search operations (default: true)
|
|
19
|
+
myWeibo: z.boolean().optional(), // My weibo operations (default: true)
|
|
20
|
+
hotSearch: z.boolean().optional(), // Hot search operations (default: true)
|
|
21
|
+
})
|
|
22
|
+
.strict()
|
|
23
|
+
.optional();
|
|
24
|
+
|
|
25
|
+
const WeiboSharedConfigShape = {
|
|
26
|
+
dmPolicy: DmPolicySchema.optional(),
|
|
27
|
+
allowFrom: z.array(z.string()).optional(),
|
|
28
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
29
|
+
chunkMode: ChunkModeSchema,
|
|
30
|
+
// Whether to allow OpenClaw block streaming for this channel/account.
|
|
31
|
+
// true: stream block replies progressively; false: final-only delivery.
|
|
32
|
+
blockStreaming: z.boolean().default(true),
|
|
33
|
+
tools: WeiboToolsConfigSchema,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const WeiboAccountConfigSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
enabled: z.boolean().optional(),
|
|
39
|
+
name: z.string().optional(),
|
|
40
|
+
appId: z.string().optional(),
|
|
41
|
+
appSecret: z.string().optional(),
|
|
42
|
+
wsEndpoint: z.string().url().default("ws://open-im.api.weibo.com/ws/stream"),
|
|
43
|
+
tokenEndpoint: z.string().url().default("http://open-im.api.weibo.com/open/auth/ws_token"),
|
|
44
|
+
...WeiboSharedConfigShape,
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
47
|
+
|
|
48
|
+
export const WeiboConfigSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
enabled: z.boolean().optional(),
|
|
51
|
+
appId: z.string().optional(),
|
|
52
|
+
appSecret: z.string().optional(),
|
|
53
|
+
wsEndpoint: z.string().url().default("ws://open-im.api.weibo.com/ws/stream"),
|
|
54
|
+
tokenEndpoint: z.string().url().default("http://open-im.api.weibo.com/open/auth/ws_token"),
|
|
55
|
+
...WeiboSharedConfigShape,
|
|
56
|
+
accounts: z.record(z.string(), WeiboAccountConfigSchema.optional()).optional(),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|