@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/bot.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
3
|
+
import type {
|
|
4
|
+
WeiboInboundAttachmentPart,
|
|
5
|
+
WeiboMessageContext,
|
|
6
|
+
WeiboResponseContentPart,
|
|
7
|
+
WeiboResponseMessageInputItem,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { resolveWeiboAccount } from "./accounts.js";
|
|
10
|
+
import { createWeiboOutboundStream } from "./outbound-stream.js";
|
|
11
|
+
import { getWeiboRuntime } from "./runtime.js";
|
|
12
|
+
import { buildAgentMediaPayloadCompat } from "./plugin-sdk-compat.js";
|
|
13
|
+
|
|
14
|
+
// Simple in-memory dedup
|
|
15
|
+
const processedMessages = new Set<string>();
|
|
16
|
+
const MAX_INBOUND_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
17
|
+
const MAX_INBOUND_FILE_BYTES = 5 * 1024 * 1024;
|
|
18
|
+
const SUPPORTED_IMAGE_MIME_TYPES = new Set([
|
|
19
|
+
"image/jpeg",
|
|
20
|
+
"image/png",
|
|
21
|
+
"image/gif",
|
|
22
|
+
"image/webp",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function isDuplicate(messageId: string): boolean {
|
|
26
|
+
if (processedMessages.has(messageId)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
processedMessages.add(messageId);
|
|
30
|
+
// Cleanup old entries periodically
|
|
31
|
+
if (processedMessages.size > 1000) {
|
|
32
|
+
const toDelete = Array.from(processedMessages).slice(0, 500);
|
|
33
|
+
toDelete.forEach((id) => processedMessages.delete(id));
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveInboundMessageId(event: WeiboMessageEvent): string {
|
|
39
|
+
const explicitMessageId = event.payload.messageId.trim();
|
|
40
|
+
if (explicitMessageId) {
|
|
41
|
+
return explicitMessageId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const digest = createHash("sha1")
|
|
45
|
+
.update(JSON.stringify({
|
|
46
|
+
fromUserId: event.payload.fromUserId,
|
|
47
|
+
text: event.payload.text ?? "",
|
|
48
|
+
timestamp: event.payload.timestamp ?? null,
|
|
49
|
+
input: event.payload.input ?? [],
|
|
50
|
+
}))
|
|
51
|
+
.digest("hex")
|
|
52
|
+
.slice(0, 16);
|
|
53
|
+
|
|
54
|
+
return `weibo_inbound_${digest}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type WeiboMessageEvent = {
|
|
58
|
+
type: "message";
|
|
59
|
+
payload: {
|
|
60
|
+
messageId: string;
|
|
61
|
+
fromUserId: string;
|
|
62
|
+
text?: string;
|
|
63
|
+
timestamp?: number;
|
|
64
|
+
input?: WeiboResponseMessageInputItem[];
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type NormalizedWeiboInboundInput = {
|
|
69
|
+
text: string;
|
|
70
|
+
images: WeiboInboundAttachmentPart[];
|
|
71
|
+
files: WeiboInboundAttachmentPart[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function isSupportedWeiboContentPart(part: unknown): part is WeiboResponseContentPart {
|
|
75
|
+
if (!part || typeof part !== "object") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const type = (part as { type?: unknown }).type;
|
|
80
|
+
return type === "input_text" || type === "input_image" || type === "input_file";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeWeiboInboundInput(event: WeiboMessageEvent): NormalizedWeiboInboundInput {
|
|
84
|
+
const textParts: string[] = [];
|
|
85
|
+
const images: WeiboInboundAttachmentPart[] = [];
|
|
86
|
+
const files: WeiboInboundAttachmentPart[] = [];
|
|
87
|
+
|
|
88
|
+
for (const item of event.payload.input ?? []) {
|
|
89
|
+
if (item.type !== "message" || item.role !== "user" || !Array.isArray(item.content)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const part of item.content) {
|
|
94
|
+
if (!isSupportedWeiboContentPart(part)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (part.type === "input_text") {
|
|
99
|
+
if (typeof part.text === "string" && part.text) {
|
|
100
|
+
textParts.push(part.text);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const target = part.type === "input_image" ? images : files;
|
|
106
|
+
target.push({
|
|
107
|
+
mimeType: part.source.media_type,
|
|
108
|
+
filename: part.filename,
|
|
109
|
+
base64: part.source.data,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalizedText = textParts.length > 0
|
|
115
|
+
? textParts.join("\n")
|
|
116
|
+
: (event.payload.text ?? "");
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
text: normalizedText,
|
|
120
|
+
images,
|
|
121
|
+
files,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function persistWeiboInboundAttachments(params: {
|
|
126
|
+
normalized: NormalizedWeiboInboundInput;
|
|
127
|
+
runtimeCore: ReturnType<typeof getWeiboRuntime>;
|
|
128
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
129
|
+
}): Promise<ReturnType<typeof buildAgentMediaPayloadCompat>> {
|
|
130
|
+
const { normalized, runtimeCore, error } = params;
|
|
131
|
+
const mediaList: Array<{ path: string; contentType?: string | null }> = [];
|
|
132
|
+
|
|
133
|
+
for (const image of normalized.images) {
|
|
134
|
+
if (!SUPPORTED_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
|
135
|
+
error(`weibo: unsupported image mime type: ${image.mimeType}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const buffer = Buffer.from(image.base64, "base64");
|
|
141
|
+
if (buffer.length === 0) {
|
|
142
|
+
error(`weibo: empty image payload: ${image.filename ?? "unknown"}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const saved = await runtimeCore.channel.media.saveMediaBuffer(
|
|
147
|
+
buffer,
|
|
148
|
+
image.mimeType,
|
|
149
|
+
"inbound",
|
|
150
|
+
MAX_INBOUND_IMAGE_BYTES,
|
|
151
|
+
image.filename,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
mediaList.push({
|
|
155
|
+
path: saved.path,
|
|
156
|
+
contentType: saved.contentType,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
error(`weibo: failed to persist image input: ${String(err)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const file of normalized.files) {
|
|
164
|
+
try {
|
|
165
|
+
const buffer = Buffer.from(file.base64, "base64");
|
|
166
|
+
if (buffer.length === 0) {
|
|
167
|
+
error(`weibo: empty file payload: ${file.filename ?? "unknown"}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const saved = await runtimeCore.channel.media.saveMediaBuffer(
|
|
172
|
+
buffer,
|
|
173
|
+
file.mimeType,
|
|
174
|
+
"inbound",
|
|
175
|
+
MAX_INBOUND_FILE_BYTES,
|
|
176
|
+
file.filename,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
mediaList.push({
|
|
180
|
+
path: saved.path,
|
|
181
|
+
contentType: saved.contentType,
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
error(`weibo: failed to persist file input: ${String(err)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return buildAgentMediaPayloadCompat(mediaList);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type HandleWeiboMessageParams = {
|
|
192
|
+
cfg: ClawdbotConfig;
|
|
193
|
+
event: WeiboMessageEvent;
|
|
194
|
+
accountId: string;
|
|
195
|
+
runtime?: RuntimeEnv;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export async function handleWeiboMessage(params: HandleWeiboMessageParams): Promise<WeiboMessageContext | null> {
|
|
199
|
+
const { cfg, event, accountId, runtime } = params;
|
|
200
|
+
const log = runtime?.log ?? console.log;
|
|
201
|
+
const error = runtime?.error ?? console.error;
|
|
202
|
+
|
|
203
|
+
const account = resolveWeiboAccount({ cfg, accountId });
|
|
204
|
+
if (!account.enabled || !account.configured) {
|
|
205
|
+
error(`weibo[${accountId}]: account not enabled or configured`);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { fromUserId, timestamp } = event.payload;
|
|
210
|
+
const messageId = resolveInboundMessageId(event);
|
|
211
|
+
|
|
212
|
+
// Deduplication
|
|
213
|
+
if (isDuplicate(messageId)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const inboundAcceptedAt = Date.now();
|
|
217
|
+
const streamDebugEnabled = process.env.WEIBO_STREAM_DEBUG === "1";
|
|
218
|
+
const streamDebug = (tag: string, data?: Record<string, unknown>): void => {
|
|
219
|
+
if (!streamDebugEnabled) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const payload = data ? ` ${JSON.stringify(data)}` : "";
|
|
223
|
+
log(`weibo[${accountId}][stream-debug] ${tag}${payload}`);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Get runtime core
|
|
227
|
+
const core = getWeiboRuntime();
|
|
228
|
+
|
|
229
|
+
// Build message content
|
|
230
|
+
const normalized = normalizeWeiboInboundInput(event);
|
|
231
|
+
const content = normalized.text;
|
|
232
|
+
const hasText = content.trim().length > 0;
|
|
233
|
+
const hasAttachments = normalized.images.length > 0 || normalized.files.length > 0;
|
|
234
|
+
if (!hasText && !hasAttachments) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const mediaPayload = await persistWeiboInboundAttachments({
|
|
238
|
+
normalized,
|
|
239
|
+
runtimeCore: core,
|
|
240
|
+
error,
|
|
241
|
+
});
|
|
242
|
+
const hasPersistedMedia = Array.isArray(mediaPayload.MediaPaths) && mediaPayload.MediaPaths.length > 0;
|
|
243
|
+
if (!hasText && !hasPersistedMedia) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Resolve routing - find which agent should handle this message
|
|
248
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
249
|
+
cfg,
|
|
250
|
+
channel: "weibo",
|
|
251
|
+
accountId,
|
|
252
|
+
peer: {
|
|
253
|
+
kind: "direct",
|
|
254
|
+
id: fromUserId,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (!route.agentId) {
|
|
259
|
+
log(`weibo[${accountId}]: no agent route found for ${fromUserId}`);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log(`weibo[${accountId}]: received message from ${fromUserId}, routing to ${route.agentId} (session=${route.sessionKey})`);
|
|
264
|
+
|
|
265
|
+
// Enqueue system event for logging/monitoring
|
|
266
|
+
const preview = content.replace(/\s+/g, " ").slice(0, 160);
|
|
267
|
+
core.system.enqueueSystemEvent(`Weibo[${accountId}] DM from ${fromUserId}: ${preview}`, {
|
|
268
|
+
sessionKey: route.sessionKey,
|
|
269
|
+
contextKey: `weibo:message:${fromUserId}:${messageId}`,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Build the inbound envelope (message body for agent)
|
|
273
|
+
const body = core.channel.reply.formatInboundEnvelope({
|
|
274
|
+
channel: "Weibo",
|
|
275
|
+
from: fromUserId,
|
|
276
|
+
body: content,
|
|
277
|
+
timestamp: timestamp ?? Date.now(),
|
|
278
|
+
sender: { name: fromUserId, id: fromUserId },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Resolve text chunking settings
|
|
282
|
+
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "weibo", accountId, {
|
|
283
|
+
fallbackLimit: account.config.textChunkLimit ?? 4000,
|
|
284
|
+
});
|
|
285
|
+
const chunkMode = account.config.chunkMode
|
|
286
|
+
?? core.channel.text.resolveChunkMode(cfg, "weibo", accountId);
|
|
287
|
+
// Weibo real-time streaming is driven by onPartialReply; disable block streaming to avoid duplicate lanes.
|
|
288
|
+
const disableBlockStreaming = true;
|
|
289
|
+
streamDebug("dispatch_init", {
|
|
290
|
+
inboundMessageId: messageId,
|
|
291
|
+
fromUserId,
|
|
292
|
+
chunkMode,
|
|
293
|
+
textChunkLimit,
|
|
294
|
+
configuredBlockStreaming: account.config.blockStreaming,
|
|
295
|
+
disableBlockStreaming,
|
|
296
|
+
});
|
|
297
|
+
let currentOutboundMessageId: string | null = null;
|
|
298
|
+
let currentOutboundChunkId = 0;
|
|
299
|
+
let hasLoggedFirstChunkLatency = false;
|
|
300
|
+
|
|
301
|
+
const ensureOutboundMessageId = async (): Promise<string> => {
|
|
302
|
+
if (currentOutboundMessageId) {
|
|
303
|
+
return currentOutboundMessageId;
|
|
304
|
+
}
|
|
305
|
+
const { generateWeiboMessageId } = await import("./send.js");
|
|
306
|
+
currentOutboundMessageId = generateWeiboMessageId();
|
|
307
|
+
currentOutboundChunkId = 0;
|
|
308
|
+
return currentOutboundMessageId;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const sendOutboundChunk = async (params: {
|
|
312
|
+
text: string;
|
|
313
|
+
done: boolean;
|
|
314
|
+
source: "partial" | "deliver" | "settled";
|
|
315
|
+
}): Promise<void> => {
|
|
316
|
+
const { sendMessageWeibo } = await import("./send.js");
|
|
317
|
+
const outboundMessageId = await ensureOutboundMessageId();
|
|
318
|
+
streamDebug("send_chunk", {
|
|
319
|
+
source: params.source,
|
|
320
|
+
messageId: outboundMessageId,
|
|
321
|
+
chunkId: currentOutboundChunkId,
|
|
322
|
+
done: params.done,
|
|
323
|
+
textLen: params.text.length,
|
|
324
|
+
preview: params.text.slice(0, 80),
|
|
325
|
+
});
|
|
326
|
+
await sendMessageWeibo({
|
|
327
|
+
cfg,
|
|
328
|
+
to: fromUserId,
|
|
329
|
+
text: params.text,
|
|
330
|
+
messageId: outboundMessageId,
|
|
331
|
+
chunkId: currentOutboundChunkId,
|
|
332
|
+
done: params.done,
|
|
333
|
+
});
|
|
334
|
+
if (!hasLoggedFirstChunkLatency && params.text.length > 0) {
|
|
335
|
+
const elapsedMs = Math.max(0, Date.now() - inboundAcceptedAt);
|
|
336
|
+
log(`weibo[${accountId}]: first chunk first-char latency=${elapsedMs}ms`);
|
|
337
|
+
hasLoggedFirstChunkLatency = true;
|
|
338
|
+
}
|
|
339
|
+
currentOutboundChunkId += 1;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const outboundStream = createWeiboOutboundStream({
|
|
343
|
+
chunkMode,
|
|
344
|
+
textChunkLimit,
|
|
345
|
+
emit: sendOutboundChunk,
|
|
346
|
+
chunkTextWithMode: (text, limit, mode) =>
|
|
347
|
+
core.channel.text.chunkTextWithMode(text, limit, mode),
|
|
348
|
+
streamDebug,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Build final inbound context
|
|
352
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
353
|
+
Body: body,
|
|
354
|
+
BodyForAgent: content,
|
|
355
|
+
BodyForCommands: content,
|
|
356
|
+
RawBody: content,
|
|
357
|
+
CommandBody: content,
|
|
358
|
+
From: `weibo:${fromUserId}`,
|
|
359
|
+
To: fromUserId,
|
|
360
|
+
SessionKey: route.sessionKey,
|
|
361
|
+
AccountId: route.accountId,
|
|
362
|
+
ChatType: "direct",
|
|
363
|
+
ConversationLabel: fromUserId,
|
|
364
|
+
SenderName: fromUserId,
|
|
365
|
+
SenderId: fromUserId,
|
|
366
|
+
Provider: "weibo" as const,
|
|
367
|
+
Surface: "weibo" as const,
|
|
368
|
+
MessageSid: messageId,
|
|
369
|
+
Timestamp: timestamp ?? Date.now(),
|
|
370
|
+
WasMentioned: true,
|
|
371
|
+
CommandAuthorized: true,
|
|
372
|
+
OriginatingChannel: "weibo" as const,
|
|
373
|
+
OriginatingTo: fromUserId,
|
|
374
|
+
...mediaPayload,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Create a dispatcher that sends replies back to Weibo
|
|
378
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
379
|
+
deliver: async (reply, info?: { kind?: string }) => {
|
|
380
|
+
const isFinalDeliver = info?.kind !== "block";
|
|
381
|
+
const before = outboundStream.snapshot();
|
|
382
|
+
streamDebug("deliver_enter", {
|
|
383
|
+
kind: info?.kind ?? "unknown",
|
|
384
|
+
isFinalDeliver,
|
|
385
|
+
textLen: (reply.text ?? "").length,
|
|
386
|
+
...before,
|
|
387
|
+
});
|
|
388
|
+
await outboundStream.pushDeliverText({
|
|
389
|
+
text: reply.text ?? "",
|
|
390
|
+
isFinal: isFinalDeliver,
|
|
391
|
+
});
|
|
392
|
+
streamDebug("deliver_exit", {
|
|
393
|
+
kind: info?.kind ?? "unknown",
|
|
394
|
+
isFinalDeliver,
|
|
395
|
+
...outboundStream.snapshot(),
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
onError: (err, info) => {
|
|
399
|
+
error(`weibo[${accountId}] ${info.kind} reply failed: ${String(err)}`);
|
|
400
|
+
},
|
|
401
|
+
onIdle: () => {
|
|
402
|
+
log(`weibo[${accountId}]: reply dispatcher idle`);
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Dispatch to agent
|
|
407
|
+
log(`weibo[${accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const onSettled = async () => {
|
|
411
|
+
streamDebug("dispatcher_settled_before", {
|
|
412
|
+
currentOutboundMessageId,
|
|
413
|
+
currentOutboundChunkId,
|
|
414
|
+
...outboundStream.snapshot(),
|
|
415
|
+
});
|
|
416
|
+
await outboundStream.settle();
|
|
417
|
+
streamDebug("dispatcher_settled_after", {
|
|
418
|
+
currentOutboundMessageId,
|
|
419
|
+
currentOutboundChunkId,
|
|
420
|
+
...outboundStream.snapshot(),
|
|
421
|
+
});
|
|
422
|
+
currentOutboundMessageId = null;
|
|
423
|
+
currentOutboundChunkId = 0;
|
|
424
|
+
markDispatchIdle();
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const runDispatch = () => core.channel.reply.dispatchReplyFromConfig({
|
|
428
|
+
ctx: ctxPayload,
|
|
429
|
+
cfg,
|
|
430
|
+
dispatcher,
|
|
431
|
+
replyOptions: {
|
|
432
|
+
...replyOptions,
|
|
433
|
+
disableBlockStreaming,
|
|
434
|
+
onPartialReply: async (payload) => {
|
|
435
|
+
streamDebug("on_partial_reply", {
|
|
436
|
+
textLen: (payload.text ?? "").length,
|
|
437
|
+
preview: (payload.text ?? "").slice(0, 80),
|
|
438
|
+
});
|
|
439
|
+
await outboundStream.pushPartialSnapshot(payload.text ?? "");
|
|
440
|
+
},
|
|
441
|
+
onAssistantMessageStart: () => {
|
|
442
|
+
streamDebug("on_assistant_message_start");
|
|
443
|
+
},
|
|
444
|
+
onReasoningEnd: () => {
|
|
445
|
+
streamDebug("on_reasoning_end");
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const withReplyDispatcher = (core.channel.reply as {
|
|
451
|
+
withReplyDispatcher?: (params: {
|
|
452
|
+
dispatcher: unknown;
|
|
453
|
+
run: () => Promise<{ queuedFinal: boolean; counts: { final: number } }>;
|
|
454
|
+
onSettled?: () => Promise<void> | void;
|
|
455
|
+
}) => Promise<{ queuedFinal: boolean; counts: { final: number } }>;
|
|
456
|
+
}).withReplyDispatcher;
|
|
457
|
+
|
|
458
|
+
const result = typeof withReplyDispatcher === "function"
|
|
459
|
+
? await withReplyDispatcher({
|
|
460
|
+
dispatcher,
|
|
461
|
+
onSettled,
|
|
462
|
+
run: runDispatch,
|
|
463
|
+
})
|
|
464
|
+
: await (async () => {
|
|
465
|
+
try {
|
|
466
|
+
return await runDispatch();
|
|
467
|
+
} finally {
|
|
468
|
+
await onSettled();
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
|
|
472
|
+
log(`weibo[${accountId}]: dispatch complete (queuedFinal=${result.queuedFinal}, replies=${result.counts.final})`);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
error(`weibo[${accountId}]: failed to dispatch message: ${String(err)}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Build and return message context
|
|
478
|
+
const messageContext: WeiboMessageContext = {
|
|
479
|
+
messageId,
|
|
480
|
+
senderId: fromUserId,
|
|
481
|
+
text: content,
|
|
482
|
+
createTime: timestamp,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
return messageContext;
|
|
486
|
+
}
|