@tencent-weixin/openclaw-weixin 2.4.3 → 2.4.4
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/dist/src/api/api.js +44 -3
- package/dist/src/api/api.js.map +1 -1
- package/dist/src/api/types.js +2 -0
- package/dist/src/api/types.js.map +1 -1
- package/dist/src/channel.js +7 -1
- package/dist/src/channel.js.map +1 -1
- package/dist/src/config/config-schema.js +1 -0
- package/dist/src/config/config-schema.js.map +1 -1
- package/dist/src/config/reply-progress.js +5 -0
- package/dist/src/config/reply-progress.js.map +1 -0
- package/dist/src/media/voice-outbound.js +98 -1
- package/dist/src/media/voice-outbound.js.map +1 -1
- package/dist/src/messaging/batch-session.js +271 -0
- package/dist/src/messaging/batch-session.js.map +1 -0
- package/dist/src/messaging/error-notice.js +1 -0
- package/dist/src/messaging/error-notice.js.map +1 -1
- package/dist/src/messaging/outbound-hooks.js +2 -0
- package/dist/src/messaging/outbound-hooks.js.map +1 -1
- package/dist/src/messaging/process-message.js +32 -7
- package/dist/src/messaging/process-message.js.map +1 -1
- package/dist/src/messaging/reply-progress-sender.js +93 -0
- package/dist/src/messaging/reply-progress-sender.js.map +1 -0
- package/dist/src/messaging/run-context.js +9 -0
- package/dist/src/messaging/run-context.js.map +1 -0
- package/dist/src/messaging/run-report-session.js +123 -0
- package/dist/src/messaging/run-report-session.js.map +1 -0
- package/dist/src/messaging/send.js +40 -2
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/monitor/monitor.js +3 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/dist/src/streaming/batch-session.js +271 -0
- package/dist/src/streaming/batch-session.js.map +1 -0
- package/dist/src/streaming/stream-piece.js +54 -0
- package/dist/src/streaming/stream-piece.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/api.ts +52 -3
- package/src/api/types.ts +16 -0
- package/src/channel.ts +7 -1
- package/src/config/config-schema.ts +1 -0
- package/src/config/reply-progress.ts +10 -0
- package/src/messaging/error-notice.ts +2 -0
- package/src/messaging/outbound-hooks.ts +4 -0
- package/src/messaging/process-message.ts +32 -7
- package/src/messaging/reply-progress-sender.ts +122 -0
- package/src/messaging/send-media.ts +1 -1
- package/src/messaging/send.ts +60 -7
- package/src/monitor/monitor.ts +3 -0
package/src/api/api.ts
CHANGED
|
@@ -295,11 +295,42 @@ export async function apiGetFetch(params: {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Combine an internal timeout controller with an optional external abort signal.
|
|
300
|
+
* This lets gateway channel-stop aborts cancel in-flight long-poll requests
|
|
301
|
+
* immediately while preserving the existing timeout-driven AbortError path.
|
|
302
|
+
*/
|
|
303
|
+
function combineAbortSignals(params: {
|
|
304
|
+
internal?: AbortController;
|
|
305
|
+
external?: AbortSignal;
|
|
306
|
+
}): { signal?: AbortSignal; cleanup: () => void } {
|
|
307
|
+
const { internal, external } = params;
|
|
308
|
+
if (!external) {
|
|
309
|
+
return { signal: internal?.signal, cleanup: () => {} };
|
|
310
|
+
}
|
|
311
|
+
if (!internal) {
|
|
312
|
+
return { signal: external, cleanup: () => {} };
|
|
313
|
+
}
|
|
314
|
+
if (external.aborted) {
|
|
315
|
+
internal.abort();
|
|
316
|
+
return { signal: internal.signal, cleanup: () => {} };
|
|
317
|
+
}
|
|
318
|
+
const onExternalAbort = () => internal.abort();
|
|
319
|
+
external.addEventListener("abort", onExternalAbort, { once: true });
|
|
320
|
+
return {
|
|
321
|
+
signal: internal.signal,
|
|
322
|
+
cleanup: () => external.removeEventListener("abort", onExternalAbort),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
298
326
|
/**
|
|
299
327
|
* Common fetch wrapper: POST JSON to a Weixin API endpoint.
|
|
300
328
|
* When `timeoutMs` is provided, the request is aborted after that many milliseconds.
|
|
301
329
|
* When omitted, no client-side timeout is applied (relies on OS/TCP stack).
|
|
302
330
|
* Returns the raw response text on success; throws on HTTP error or timeout.
|
|
331
|
+
*
|
|
332
|
+
* When `abortSignal` is provided, an external abort (e.g. gateway channel stop)
|
|
333
|
+
* cancels the in-flight request immediately instead of waiting for `timeoutMs`.
|
|
303
334
|
*/
|
|
304
335
|
export async function apiPostFetch(params: {
|
|
305
336
|
baseUrl: string;
|
|
@@ -308,6 +339,7 @@ export async function apiPostFetch(params: {
|
|
|
308
339
|
token?: string;
|
|
309
340
|
timeoutMs?: number;
|
|
310
341
|
label: string;
|
|
342
|
+
abortSignal?: AbortSignal;
|
|
311
343
|
}): Promise<string> {
|
|
312
344
|
const base = ensureTrailingSlash(params.baseUrl);
|
|
313
345
|
const url = new URL(params.endpoint, base);
|
|
@@ -320,12 +352,16 @@ export async function apiPostFetch(params: {
|
|
|
320
352
|
controller != null && params.timeoutMs !== undefined
|
|
321
353
|
? setTimeout(() => controller.abort(), params.timeoutMs)
|
|
322
354
|
: undefined;
|
|
355
|
+
const { signal, cleanup } = combineAbortSignals({
|
|
356
|
+
internal: controller,
|
|
357
|
+
external: params.abortSignal,
|
|
358
|
+
});
|
|
323
359
|
try {
|
|
324
360
|
const res = await fetch(url.toString(), {
|
|
325
361
|
method: "POST",
|
|
326
362
|
headers: hdrs,
|
|
327
363
|
body: params.body,
|
|
328
|
-
...(
|
|
364
|
+
...(signal ? { signal } : {}),
|
|
329
365
|
});
|
|
330
366
|
if (t !== undefined) clearTimeout(t);
|
|
331
367
|
const rawText = await res.text();
|
|
@@ -337,6 +373,8 @@ export async function apiPostFetch(params: {
|
|
|
337
373
|
} catch (err) {
|
|
338
374
|
if (t !== undefined) clearTimeout(t);
|
|
339
375
|
throw err;
|
|
376
|
+
} finally {
|
|
377
|
+
cleanup();
|
|
340
378
|
}
|
|
341
379
|
}
|
|
342
380
|
|
|
@@ -351,6 +389,11 @@ export async function getUpdates(
|
|
|
351
389
|
baseUrl: string;
|
|
352
390
|
token?: string;
|
|
353
391
|
timeoutMs?: number;
|
|
392
|
+
/**
|
|
393
|
+
* Optional external abort signal from the gateway. When stopping the channel,
|
|
394
|
+
* this aborts the in-flight long-poll immediately so hot reload can restart.
|
|
395
|
+
*/
|
|
396
|
+
abortSignal?: AbortSignal;
|
|
354
397
|
},
|
|
355
398
|
): Promise<GetUpdatesResp> {
|
|
356
399
|
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
@@ -365,13 +408,19 @@ export async function getUpdates(
|
|
|
365
408
|
token: params.token,
|
|
366
409
|
timeoutMs: timeout,
|
|
367
410
|
label: "getUpdates",
|
|
411
|
+
abortSignal: params.abortSignal,
|
|
368
412
|
});
|
|
369
413
|
const resp: GetUpdatesResp = JSON.parse(rawText);
|
|
370
414
|
return resp;
|
|
371
415
|
} catch (err) {
|
|
372
|
-
// Long-poll timeout
|
|
416
|
+
// Long-poll timeout or external abort are both normal control-flow exits.
|
|
417
|
+
// The monitor loop checks abortSignal after return and exits when needed.
|
|
373
418
|
if (err instanceof Error && err.name === "AbortError") {
|
|
374
|
-
|
|
419
|
+
if (params.abortSignal?.aborted) {
|
|
420
|
+
logger.debug(`getUpdates: aborted by external signal`);
|
|
421
|
+
} else {
|
|
422
|
+
logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
|
|
423
|
+
}
|
|
375
424
|
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
376
425
|
}
|
|
377
426
|
throw err;
|
package/src/api/types.ts
CHANGED
|
@@ -74,6 +74,8 @@ export const MessageItemType = {
|
|
|
74
74
|
VOICE: 3,
|
|
75
75
|
FILE: 4,
|
|
76
76
|
VIDEO: 5,
|
|
77
|
+
TOOL_CALL_START: 11,
|
|
78
|
+
TOOL_CALL_RESULT: 12,
|
|
77
79
|
} as const;
|
|
78
80
|
|
|
79
81
|
export const MessageState = {
|
|
@@ -147,6 +149,17 @@ export interface RefMessage {
|
|
|
147
149
|
title?: string; // 摘要
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
export interface ToolCallStartItem {
|
|
153
|
+
tool_name?: string;
|
|
154
|
+
tool_call_id?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ToolCallResultItem {
|
|
158
|
+
tool_name?: string;
|
|
159
|
+
tool_call_id?: string;
|
|
160
|
+
status?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
150
163
|
export interface MessageItem {
|
|
151
164
|
type?: number;
|
|
152
165
|
create_time_ms?: number;
|
|
@@ -159,6 +172,8 @@ export interface MessageItem {
|
|
|
159
172
|
voice_item?: VoiceItem;
|
|
160
173
|
file_item?: FileItem;
|
|
161
174
|
video_item?: VideoItem;
|
|
175
|
+
tool_call_start_item?: ToolCallStartItem;
|
|
176
|
+
tool_call_result_item?: ToolCallResultItem;
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
/** Unified message (proto: WeixinMessage). Replaces the old split Message + MessageContent + FullMessage. */
|
|
@@ -177,6 +192,7 @@ export interface WeixinMessage {
|
|
|
177
192
|
message_state?: number;
|
|
178
193
|
item_list?: MessageItem[];
|
|
179
194
|
context_token?: string;
|
|
195
|
+
run_id?: string;
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
/** GetUpdates request: bytes fields are base64 strings in JSON. */
|
package/src/channel.ts
CHANGED
|
@@ -167,7 +167,13 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
167
167
|
schema: {
|
|
168
168
|
type: "object",
|
|
169
169
|
additionalProperties: false,
|
|
170
|
-
properties: {
|
|
170
|
+
properties: {
|
|
171
|
+
replyProgressMessages: {
|
|
172
|
+
type: "boolean",
|
|
173
|
+
default: true,
|
|
174
|
+
description: "Send structured tool-call progress messages.",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
171
177
|
},
|
|
172
178
|
},
|
|
173
179
|
capabilities: {
|
|
@@ -17,6 +17,7 @@ const weixinAccountSchema = z.object({
|
|
|
17
17
|
/** Top-level weixin config schema (token is stored in credentials file, not config). */
|
|
18
18
|
export const WeixinConfigSchema = weixinAccountSchema.extend({
|
|
19
19
|
accounts: z.record(z.string(), weixinAccountSchema).optional(),
|
|
20
|
+
replyProgressMessages: z.boolean().default(true),
|
|
20
21
|
/** ISO 8601; bumped on each successful login to refresh gateway config from disk. */
|
|
21
22
|
channelConfigUpdatedAt: z.string().optional(),
|
|
22
23
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
|
|
3
|
+
type WeixinChannelConfig = {
|
|
4
|
+
replyProgressMessages?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function resolveReplyProgressMessagesEnabled(cfg: OpenClawConfig): boolean {
|
|
8
|
+
const section = cfg.channels?.["openclaw-weixin"] as WeixinChannelConfig | undefined;
|
|
9
|
+
return section?.replyProgressMessages !== false;
|
|
10
|
+
}
|
|
@@ -12,6 +12,7 @@ export async function sendWeixinErrorNotice(params: {
|
|
|
12
12
|
message: string;
|
|
13
13
|
baseUrl: string;
|
|
14
14
|
token?: string;
|
|
15
|
+
runId?: string;
|
|
15
16
|
errLog: (m: string) => void;
|
|
16
17
|
}): Promise<void> {
|
|
17
18
|
if (!params.contextToken) {
|
|
@@ -22,6 +23,7 @@ export async function sendWeixinErrorNotice(params: {
|
|
|
22
23
|
baseUrl: params.baseUrl,
|
|
23
24
|
token: params.token,
|
|
24
25
|
contextToken: params.contextToken,
|
|
26
|
+
...(params.runId ? { runId: params.runId } : {}),
|
|
25
27
|
}});
|
|
26
28
|
logger.debug(`sendWeixinErrorNotice: sent to=${params.to}`);
|
|
27
29
|
} catch (err) {
|
|
@@ -20,6 +20,7 @@ export async function applyWeixinMessageSendingHook(params: {
|
|
|
20
20
|
text: string;
|
|
21
21
|
accountId?: string;
|
|
22
22
|
mediaUrl?: string;
|
|
23
|
+
runId?: string;
|
|
23
24
|
}): Promise<{ cancelled: boolean; text: string }> {
|
|
24
25
|
const hookRunner = getGlobalHookRunner();
|
|
25
26
|
if (!hookRunner?.hasHooks("message_sending")) {
|
|
@@ -33,6 +34,7 @@ export async function applyWeixinMessageSendingHook(params: {
|
|
|
33
34
|
metadata: {
|
|
34
35
|
channel: CHANNEL_ID,
|
|
35
36
|
accountId: params.accountId,
|
|
37
|
+
runId: params.runId,
|
|
36
38
|
...(params.mediaUrl ? { mediaUrls: [params.mediaUrl] } : {}),
|
|
37
39
|
},
|
|
38
40
|
},
|
|
@@ -60,6 +62,7 @@ export function emitWeixinMessageSent(params: {
|
|
|
60
62
|
success: boolean;
|
|
61
63
|
error?: string;
|
|
62
64
|
accountId?: string;
|
|
65
|
+
runId?: string;
|
|
63
66
|
}): void {
|
|
64
67
|
const hookRunner = getGlobalHookRunner();
|
|
65
68
|
if (!hookRunner?.hasHooks("message_sent")) return;
|
|
@@ -71,6 +74,7 @@ export function emitWeixinMessageSent(params: {
|
|
|
71
74
|
channelId: CHANNEL_ID,
|
|
72
75
|
accountId: params.accountId,
|
|
73
76
|
conversationId: params.to,
|
|
77
|
+
runId: params.runId,
|
|
74
78
|
});
|
|
75
79
|
fireAndForgetHook(
|
|
76
80
|
Promise.resolve(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime";
|
|
4
5
|
import {
|
|
@@ -14,6 +15,7 @@ import { MessageItemType, TypingStatus } from "../api/types.js";
|
|
|
14
15
|
import { loadWeixinAccount } from "../auth/accounts.js";
|
|
15
16
|
import { readFrameworkAllowFromList } from "../auth/pairing.js";
|
|
16
17
|
import { downloadRemoteImageToTemp } from "../cdn/upload.js";
|
|
18
|
+
import { resolveReplyProgressMessagesEnabled } from "../config/reply-progress.js";
|
|
17
19
|
import { downloadMediaFromItem } from "../media/media-download.js";
|
|
18
20
|
import { logger } from "../util/logger.js";
|
|
19
21
|
import { redactBody, redactToken } from "../util/redact.js";
|
|
@@ -31,6 +33,7 @@ import type { WeixinInboundMediaOpts } from "./inbound.js";
|
|
|
31
33
|
import { sendWeixinMediaFile } from "./send-media.js";
|
|
32
34
|
import { StreamingMarkdownFilter } from "./markdown-filter.js";
|
|
33
35
|
import { sendMessageWeixin } from "./send.js";
|
|
36
|
+
import { WeixinReplyProgressSender } from "./reply-progress-sender.js";
|
|
34
37
|
import { handleSlashCommand } from "./slash-commands.js";
|
|
35
38
|
|
|
36
39
|
const MEDIA_OUTBOUND_TEMP_DIR = path.join(resolvePreferredOpenClawTmpDir(), "weixin/media/outbound-temp");
|
|
@@ -270,6 +273,19 @@ export async function processOneMessage(
|
|
|
270
273
|
if (contextToken) {
|
|
271
274
|
setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
|
|
272
275
|
}
|
|
276
|
+
const runId = randomUUID();
|
|
277
|
+
const replyProgressSender = resolveReplyProgressMessagesEnabled(deps.config)
|
|
278
|
+
? new WeixinReplyProgressSender({
|
|
279
|
+
runId,
|
|
280
|
+
to: ctx.To,
|
|
281
|
+
accountId: deps.accountId,
|
|
282
|
+
opts: {
|
|
283
|
+
baseUrl: deps.baseUrl,
|
|
284
|
+
token: deps.token,
|
|
285
|
+
contextToken,
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
: undefined;
|
|
273
289
|
const humanDelay = deps.channelRuntime.reply.resolveHumanDelayConfig(deps.config, route.agentId);
|
|
274
290
|
|
|
275
291
|
const hasTypingTicket = Boolean(deps.typingTicket);
|
|
@@ -336,6 +352,7 @@ export async function processOneMessage(
|
|
|
336
352
|
text,
|
|
337
353
|
accountId: deps.accountId,
|
|
338
354
|
mediaUrl,
|
|
355
|
+
runId,
|
|
339
356
|
});
|
|
340
357
|
if (sendingResult.cancelled) {
|
|
341
358
|
logger.info(`outbound: cancelled by message_sending hook to=${ctx.To}`);
|
|
@@ -368,8 +385,9 @@ export async function processOneMessage(
|
|
|
368
385
|
baseUrl: deps.baseUrl,
|
|
369
386
|
token: deps.token,
|
|
370
387
|
contextToken,
|
|
388
|
+
runId,
|
|
371
389
|
}});
|
|
372
|
-
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
|
|
390
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
|
|
373
391
|
logger.info(`outbound: text sent to=${ctx.To}`);
|
|
374
392
|
return;
|
|
375
393
|
}
|
|
@@ -377,10 +395,10 @@ export async function processOneMessage(
|
|
|
377
395
|
filePath,
|
|
378
396
|
to: ctx.To,
|
|
379
397
|
text,
|
|
380
|
-
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
|
|
398
|
+
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId },
|
|
381
399
|
cdnBaseUrl: deps.cdnBaseUrl,
|
|
382
400
|
});
|
|
383
|
-
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
|
|
401
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
|
|
384
402
|
logger.info(`outbound: media sent OK to=${ctx.To}`);
|
|
385
403
|
} else {
|
|
386
404
|
logger.debug(`outbound: sending text message to=${ctx.To}`);
|
|
@@ -388,12 +406,13 @@ export async function processOneMessage(
|
|
|
388
406
|
baseUrl: deps.baseUrl,
|
|
389
407
|
token: deps.token,
|
|
390
408
|
contextToken,
|
|
409
|
+
runId,
|
|
391
410
|
}});
|
|
392
|
-
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
|
|
411
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
|
|
393
412
|
logger.info(`outbound: text sent OK to=${ctx.To}`);
|
|
394
413
|
}
|
|
395
414
|
} catch (err) {
|
|
396
|
-
emitWeixinMessageSent({ to: ctx.To, content: text, success: false, error: String(err), accountId: deps.accountId });
|
|
415
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: false, error: String(err), accountId: deps.accountId, runId });
|
|
397
416
|
logger.error(
|
|
398
417
|
`outbound: FAILED to=${ctx.To} mediaUrl=${mediaUrl ?? "none"} err=${String(err)} stack=${(err as Error).stack ?? ""}`,
|
|
399
418
|
);
|
|
@@ -421,6 +440,7 @@ export async function processOneMessage(
|
|
|
421
440
|
message: notice,
|
|
422
441
|
baseUrl: deps.baseUrl,
|
|
423
442
|
token: deps.token,
|
|
443
|
+
runId,
|
|
424
444
|
errLog: deps.errLog,
|
|
425
445
|
});
|
|
426
446
|
},
|
|
@@ -435,7 +455,11 @@ export async function processOneMessage(
|
|
|
435
455
|
ctx: finalized,
|
|
436
456
|
cfg: deps.config,
|
|
437
457
|
dispatcher,
|
|
438
|
-
replyOptions: {
|
|
458
|
+
replyOptions: {
|
|
459
|
+
...replyOptions,
|
|
460
|
+
...(replyProgressSender?.replyOptions ?? {}),
|
|
461
|
+
disableBlockStreaming: true,
|
|
462
|
+
},
|
|
439
463
|
}),
|
|
440
464
|
});
|
|
441
465
|
logger.debug(`dispatchReplyFromConfig: done agentId=${route.agentId ?? "(none)"}`);
|
|
@@ -446,6 +470,7 @@ export async function processOneMessage(
|
|
|
446
470
|
throw err;
|
|
447
471
|
} finally {
|
|
448
472
|
markDispatchIdle();
|
|
473
|
+
await replyProgressSender?.finalize();
|
|
449
474
|
|
|
450
475
|
logger.info(
|
|
451
476
|
`debug-check: accountId=${deps.accountId} debug=${String(debug)} hasContextToken=${Boolean(contextToken)}`,
|
|
@@ -489,7 +514,7 @@ export async function processOneMessage(
|
|
|
489
514
|
await sendMessageWeixin({
|
|
490
515
|
to: ctx.To,
|
|
491
516
|
text: timingText,
|
|
492
|
-
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
|
|
517
|
+
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId },
|
|
493
518
|
});
|
|
494
519
|
logger.info(`debug-timing: sent OK`);
|
|
495
520
|
} catch (debugErr) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { WeixinApiOptions } from "../api/api.js";
|
|
2
|
+
import type { MessageItem } from "../api/types.js";
|
|
3
|
+
import { MessageItemType } from "../api/types.js";
|
|
4
|
+
import { logger } from "../util/logger.js";
|
|
5
|
+
|
|
6
|
+
import { sendMessageItemWeixin } from "./send.js";
|
|
7
|
+
|
|
8
|
+
export type WeixinReplyProgressSenderDeps = {
|
|
9
|
+
runId: string;
|
|
10
|
+
to: string;
|
|
11
|
+
accountId: string;
|
|
12
|
+
opts: WeixinApiOptions & {
|
|
13
|
+
contextToken?: string;
|
|
14
|
+
runId?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ToolItemEventPayload = {
|
|
19
|
+
itemId?: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
phase?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function normalizeToolStatus(status?: string): string {
|
|
28
|
+
if (status === "completed") return "completed";
|
|
29
|
+
if (status === "failed") return "failed";
|
|
30
|
+
if (status === "blocked") return "blocked";
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class WeixinReplyProgressSender {
|
|
35
|
+
readonly runId: string;
|
|
36
|
+
|
|
37
|
+
private readonly to: string;
|
|
38
|
+
private readonly accountId: string;
|
|
39
|
+
private readonly opts: WeixinReplyProgressSenderDeps["opts"];
|
|
40
|
+
private finalized = false;
|
|
41
|
+
private sendChain: Promise<void> = Promise.resolve();
|
|
42
|
+
|
|
43
|
+
constructor(deps: WeixinReplyProgressSenderDeps) {
|
|
44
|
+
this.runId = deps.runId;
|
|
45
|
+
this.to = deps.to;
|
|
46
|
+
this.accountId = deps.accountId;
|
|
47
|
+
this.opts = { ...deps.opts, runId: deps.runId };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get replyOptions() {
|
|
51
|
+
return {
|
|
52
|
+
runId: this.runId,
|
|
53
|
+
onItemEvent: (payload: ToolItemEventPayload) => this.handleToolItemEvent(payload),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private enqueueMessage(item: MessageItem, label: string): void {
|
|
58
|
+
if (this.finalized) return;
|
|
59
|
+
this.sendChain = this.sendChain
|
|
60
|
+
.then(async () => {
|
|
61
|
+
await sendMessageItemWeixin({
|
|
62
|
+
to: this.to,
|
|
63
|
+
item,
|
|
64
|
+
opts: this.opts,
|
|
65
|
+
label,
|
|
66
|
+
});
|
|
67
|
+
})
|
|
68
|
+
.catch((err) => {
|
|
69
|
+
logger.warn(`${label}: failed to=${this.to} accountId=${this.accountId} runId=${this.runId} err=${String(err)}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private handleToolItemEvent(payload: ToolItemEventPayload): void {
|
|
74
|
+
if (this.finalized) return;
|
|
75
|
+
if (payload.kind !== "tool") return;
|
|
76
|
+
if (payload.phase !== "start" && payload.phase !== "end") return;
|
|
77
|
+
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const toolName = payload.name?.trim() || payload.title?.trim() || "tool";
|
|
80
|
+
const toolCallId = payload.itemId?.trim() || undefined;
|
|
81
|
+
|
|
82
|
+
if (payload.phase === "start") {
|
|
83
|
+
this.enqueueMessage(
|
|
84
|
+
{
|
|
85
|
+
type: MessageItemType.TOOL_CALL_START,
|
|
86
|
+
create_time_ms: now,
|
|
87
|
+
is_completed: false,
|
|
88
|
+
tool_call_start_item: {
|
|
89
|
+
tool_name: toolName,
|
|
90
|
+
tool_call_id: toolCallId,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
"sendToolCallStartMessage",
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.enqueueMessage(
|
|
99
|
+
{
|
|
100
|
+
type: MessageItemType.TOOL_CALL_RESULT,
|
|
101
|
+
create_time_ms: now,
|
|
102
|
+
is_completed: true,
|
|
103
|
+
tool_call_result_item: {
|
|
104
|
+
tool_name: toolName,
|
|
105
|
+
tool_call_id: toolCallId,
|
|
106
|
+
status: normalizeToolStatus(payload.status),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
"sendToolCallResultMessage",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async finalize(): Promise<void> {
|
|
114
|
+
if (this.finalized) return;
|
|
115
|
+
this.finalized = true;
|
|
116
|
+
try {
|
|
117
|
+
await this.sendChain;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logger.warn(`WeixinReplyProgressSender.finalize: send drain failed runId=${this.runId} err=${String(err)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -18,7 +18,7 @@ export async function sendWeixinMediaFile(params: {
|
|
|
18
18
|
filePath: string;
|
|
19
19
|
to: string;
|
|
20
20
|
text: string;
|
|
21
|
-
opts: WeixinApiOptions & { contextToken?: string };
|
|
21
|
+
opts: WeixinApiOptions & { contextToken?: string; runId?: string };
|
|
22
22
|
cdnBaseUrl: string;
|
|
23
23
|
}): Promise<{ messageId: string }> {
|
|
24
24
|
const { filePath, to, text, opts, cdnBaseUrl } = params;
|
package/src/messaging/send.ts
CHANGED
|
@@ -10,6 +10,11 @@ import type { UploadedFileInfo } from "../cdn/upload.js";
|
|
|
10
10
|
|
|
11
11
|
export { StreamingMarkdownFilter } from "./markdown-filter.js";
|
|
12
12
|
|
|
13
|
+
type WeixinMessageSendOptions = WeixinApiOptions & {
|
|
14
|
+
contextToken?: string;
|
|
15
|
+
runId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
13
18
|
function generateClientId(): string {
|
|
14
19
|
return generateId("openclaw-weixin");
|
|
15
20
|
}
|
|
@@ -19,9 +24,10 @@ function buildTextMessageReq(params: {
|
|
|
19
24
|
to: string;
|
|
20
25
|
text: string;
|
|
21
26
|
contextToken?: string;
|
|
27
|
+
runId?: string;
|
|
22
28
|
clientId: string;
|
|
23
29
|
}): SendMessageReq {
|
|
24
|
-
const { to, text, contextToken, clientId } = params;
|
|
30
|
+
const { to, text, contextToken, runId, clientId } = params;
|
|
25
31
|
const item_list: MessageItem[] = text
|
|
26
32
|
? [{ type: MessageItemType.TEXT, text_item: { text } }]
|
|
27
33
|
: [];
|
|
@@ -34,6 +40,7 @@ function buildTextMessageReq(params: {
|
|
|
34
40
|
message_state: MessageState.FINISH,
|
|
35
41
|
item_list: item_list.length ? item_list : undefined,
|
|
36
42
|
context_token: contextToken ?? undefined,
|
|
43
|
+
run_id: runId ?? undefined,
|
|
37
44
|
},
|
|
38
45
|
};
|
|
39
46
|
}
|
|
@@ -42,14 +49,16 @@ function buildTextMessageReq(params: {
|
|
|
42
49
|
function buildSendMessageReq(params: {
|
|
43
50
|
to: string;
|
|
44
51
|
contextToken?: string;
|
|
52
|
+
runId?: string;
|
|
45
53
|
payload: ReplyPayload;
|
|
46
54
|
clientId: string;
|
|
47
55
|
}): SendMessageReq {
|
|
48
|
-
const { to, contextToken, payload, clientId } = params;
|
|
56
|
+
const { to, contextToken, runId, payload, clientId } = params;
|
|
49
57
|
return buildTextMessageReq({
|
|
50
58
|
to,
|
|
51
59
|
text: payload.text ?? "",
|
|
52
60
|
contextToken,
|
|
61
|
+
runId,
|
|
53
62
|
clientId,
|
|
54
63
|
});
|
|
55
64
|
}
|
|
@@ -60,7 +69,7 @@ function buildSendMessageReq(params: {
|
|
|
60
69
|
export async function sendMessageWeixin(params: {
|
|
61
70
|
to: string;
|
|
62
71
|
text: string;
|
|
63
|
-
opts:
|
|
72
|
+
opts: WeixinMessageSendOptions;
|
|
64
73
|
}): Promise<{ messageId: string }> {
|
|
65
74
|
const { to, text, opts } = params;
|
|
66
75
|
if (!opts.contextToken) {
|
|
@@ -70,6 +79,7 @@ export async function sendMessageWeixin(params: {
|
|
|
70
79
|
const req = buildSendMessageReq({
|
|
71
80
|
to,
|
|
72
81
|
contextToken: opts.contextToken,
|
|
82
|
+
runId: opts.runId,
|
|
73
83
|
payload: { text },
|
|
74
84
|
clientId,
|
|
75
85
|
});
|
|
@@ -87,6 +97,47 @@ export async function sendMessageWeixin(params: {
|
|
|
87
97
|
return { messageId: clientId };
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
/** Send a single structured MessageItem downstream. */
|
|
101
|
+
export async function sendMessageItemWeixin(params: {
|
|
102
|
+
to: string;
|
|
103
|
+
item: MessageItem;
|
|
104
|
+
opts: WeixinMessageSendOptions;
|
|
105
|
+
clientId?: string;
|
|
106
|
+
label?: string;
|
|
107
|
+
}): Promise<{ messageId: string }> {
|
|
108
|
+
const { to, item, opts } = params;
|
|
109
|
+
if (!opts.contextToken) {
|
|
110
|
+
logger.warn(`sendMessageItemWeixin: contextToken missing for to=${to}, sending without context`);
|
|
111
|
+
}
|
|
112
|
+
const clientId = params.clientId ?? generateClientId();
|
|
113
|
+
const req: SendMessageReq = {
|
|
114
|
+
msg: {
|
|
115
|
+
from_user_id: "",
|
|
116
|
+
to_user_id: to,
|
|
117
|
+
client_id: clientId,
|
|
118
|
+
message_type: MessageType.BOT,
|
|
119
|
+
message_state: MessageState.FINISH,
|
|
120
|
+
item_list: [item],
|
|
121
|
+
context_token: opts.contextToken ?? undefined,
|
|
122
|
+
run_id: opts.runId,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
await sendMessageApi({
|
|
127
|
+
baseUrl: opts.baseUrl,
|
|
128
|
+
token: opts.token,
|
|
129
|
+
timeoutMs: opts.timeoutMs,
|
|
130
|
+
body: req,
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
logger.error(
|
|
134
|
+
`${params.label ?? "sendMessageItemWeixin"}: failed to=${to} clientId=${clientId} err=${String(err)}`,
|
|
135
|
+
);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
return { messageId: clientId };
|
|
139
|
+
}
|
|
140
|
+
|
|
90
141
|
/**
|
|
91
142
|
* Send one or more MessageItems (optionally preceded by a text caption) downstream.
|
|
92
143
|
* Each item is sent as its own request so that item_list always has exactly one entry.
|
|
@@ -95,10 +146,11 @@ async function sendMediaItems(params: {
|
|
|
95
146
|
to: string;
|
|
96
147
|
text: string;
|
|
97
148
|
mediaItem: MessageItem;
|
|
98
|
-
opts:
|
|
149
|
+
opts: WeixinMessageSendOptions;
|
|
99
150
|
label: string;
|
|
100
151
|
}): Promise<{ messageId: string }> {
|
|
101
152
|
const { to, text, mediaItem, opts, label } = params;
|
|
153
|
+
const runId = opts.runId;
|
|
102
154
|
|
|
103
155
|
const items: MessageItem[] = [];
|
|
104
156
|
if (text) {
|
|
@@ -118,6 +170,7 @@ async function sendMediaItems(params: {
|
|
|
118
170
|
message_state: MessageState.FINISH,
|
|
119
171
|
item_list: [item],
|
|
120
172
|
context_token: opts.contextToken ?? undefined,
|
|
173
|
+
run_id: runId,
|
|
121
174
|
},
|
|
122
175
|
};
|
|
123
176
|
try {
|
|
@@ -152,7 +205,7 @@ export async function sendImageMessageWeixin(params: {
|
|
|
152
205
|
to: string;
|
|
153
206
|
text: string;
|
|
154
207
|
uploaded: UploadedFileInfo;
|
|
155
|
-
opts:
|
|
208
|
+
opts: WeixinMessageSendOptions;
|
|
156
209
|
}): Promise<{ messageId: string }> {
|
|
157
210
|
const { to, text, uploaded, opts } = params;
|
|
158
211
|
if (!opts.contextToken) {
|
|
@@ -186,7 +239,7 @@ export async function sendVideoMessageWeixin(params: {
|
|
|
186
239
|
to: string;
|
|
187
240
|
text: string;
|
|
188
241
|
uploaded: UploadedFileInfo;
|
|
189
|
-
opts:
|
|
242
|
+
opts: WeixinMessageSendOptions;
|
|
190
243
|
}): Promise<{ messageId: string }> {
|
|
191
244
|
const { to, text, uploaded, opts } = params;
|
|
192
245
|
if (!opts.contextToken) {
|
|
@@ -218,7 +271,7 @@ export async function sendFileMessageWeixin(params: {
|
|
|
218
271
|
text: string;
|
|
219
272
|
fileName: string;
|
|
220
273
|
uploaded: UploadedFileInfo;
|
|
221
|
-
opts:
|
|
274
|
+
opts: WeixinMessageSendOptions;
|
|
222
275
|
}): Promise<{ messageId: string }> {
|
|
223
276
|
const { to, text, fileName, uploaded, opts } = params;
|
|
224
277
|
if (!opts.contextToken) {
|
package/src/monitor/monitor.ts
CHANGED
|
@@ -96,6 +96,9 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
|
|
|
96
96
|
token,
|
|
97
97
|
get_updates_buf: getUpdatesBuf,
|
|
98
98
|
timeoutMs: nextTimeoutMs,
|
|
99
|
+
// Stop/hot-reload should cancel the in-flight long-poll immediately
|
|
100
|
+
// instead of waiting for the server-side long-poll timeout.
|
|
101
|
+
abortSignal,
|
|
99
102
|
});
|
|
100
103
|
aLog.debug(
|
|
101
104
|
`getUpdates response: ret=${resp.ret}, msgs=${resp.msgs?.length ?? 0}, get_updates_buf_length=${resp.get_updates_buf?.length ?? 0}`,
|