@xmanrui/dsh-im 4.20.2 → 4.21.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.
- package/README.en.md +21 -9
- package/README.md +21 -9
- package/lib/client.js +116 -21
- package/lib/index.js +288 -287
- package/package.json +17 -1
- package/plugin-src/client/channel-logos.js +18 -5
- package/plugin-src/client/channels/imessage/styles.js +1 -1
- package/plugin-src/client/channels/slack/styles.js +1 -1
- package/plugin-src/client/channels/weixin/connection-error.js +4 -1
- package/plugin-src/client/i18n.js +8 -0
- package/plugin-src/client/model-setting.js +4 -2
- package/plugin-src/client/session-channel-logos.js +1 -2
- package/plugin-src/client/styles.js +3 -2
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/qq/rpc.mjs +2 -1
- package/plugin-src/host/index.mjs +7 -0
- package/plugin-src/host/injected-context.mjs +104 -0
- package/plugin-src/host/modern-harness-api.mjs +91 -3
- package/scripts/verify-model-setting.mjs +4 -1
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +105 -27
- package/src/channels/discord/discord-runtime.mjs +4 -1
- package/src/channels/feishu/bridge.mjs +44 -3
- package/src/channels/feishu/feishu-channel.mjs +1 -1
- package/src/channels/qq/qq-bridge.mjs +36 -3
- package/src/channels/qq/qq-controller.mjs +11 -5
- package/src/channels/qq/state-error.mjs +17 -0
- package/src/channels/qq/state-store.mjs +35 -9
- package/src/channels/shared/batch-input.mjs +22 -2
- package/src/channels/shared/bot-workspace-store.mjs +46 -26
- package/src/channels/shared/config-read-error.mjs +24 -0
- package/src/channels/shared/context-enhancement.mjs +40 -3
- package/src/channels/shared/control-command.mjs +8 -1
- package/src/channels/shared/harness-client.mjs +20 -12
- package/src/channels/shared/harness-question.mjs +10 -2
- package/src/channels/shared/i18n-en/dingtalk.mjs +1 -0
- package/src/channels/shared/i18n-en/qq.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +11 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/i18n-en/weixin.mjs +2 -0
- package/src/channels/shared/im-source-guidance.mjs +65 -0
- package/src/channels/shared/injected-context.mjs +362 -0
- package/src/channels/shared/semantic/artifact.mjs +4 -4
- package/src/channels/shared/semantic/reply-reference.mjs +2 -1
- package/src/channels/shared/text-harness-bridge.mjs +221 -5
- package/src/channels/shared/token-config-store.mjs +23 -8
- package/src/channels/shared/workspace-session.mjs +16 -1
- package/src/channels/slack/slack-runtime.mjs +3 -1
- package/src/channels/telegram/telegram-api.mjs +62 -2
- package/src/channels/telegram/telegram-bridge.mjs +66 -1
- package/src/channels/telegram/telegram-rich-message.mjs +6 -4
- package/src/channels/telegram/telegram-runtime.mjs +128 -6
- package/src/channels/wecom/wecom-bridge.mjs +15 -1
- package/src/channels/wecom-app/config-store.mjs +3 -1
- package/src/channels/wecom-app/wecom-app-bridge.mjs +12 -1
- package/src/channels/weixin/config-store.mjs +24 -15
- package/src/channels/weixin/connection-error.en.mjs +21 -0
- package/src/channels/weixin/connection-error.mjs +21 -6
- package/src/channels/weixin/diagnostic-details.mjs +24 -1
- package/src/channels/weixin/weixin-api.mjs +52 -13
- package/src/channels/weixin/weixin-bridge.mjs +14 -1
|
@@ -42,10 +42,6 @@ export const TELEGRAM_COMMAND_MENU = Object.freeze(
|
|
|
42
42
|
commandMenuEntries(SHARED_COMMAND_CATALOG, (text) => text).map((item) => Object.freeze(item)),
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
function escaped(value) {
|
|
46
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
45
|
function mentionedUsername(message, username) {
|
|
50
46
|
if (!username) return false;
|
|
51
47
|
return [
|
|
@@ -60,9 +56,27 @@ function mentionedUsername(message, username) {
|
|
|
60
56
|
}));
|
|
61
57
|
}
|
|
62
58
|
|
|
59
|
+
function isUsernameBoundary(character) {
|
|
60
|
+
return character === undefined || !(/[a-z0-9_]/i).test(character);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
63
|
function withoutBotMention(text, username) {
|
|
64
64
|
if (!username || typeof text !== 'string') return text;
|
|
65
|
-
|
|
65
|
+
const target = `@${username}`.toLowerCase();
|
|
66
|
+
let result = '';
|
|
67
|
+
let offset = 0;
|
|
68
|
+
while (offset < text.length) {
|
|
69
|
+
// Case-fold only the candidate: Unicode casing can change the full text's length.
|
|
70
|
+
if (text[offset] === '@'
|
|
71
|
+
&& text.slice(offset, offset + target.length).toLowerCase() === target
|
|
72
|
+
&& isUsernameBoundary(text[offset + target.length])) {
|
|
73
|
+
offset += target.length;
|
|
74
|
+
} else {
|
|
75
|
+
result += text[offset];
|
|
76
|
+
offset += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result.trim();
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
const IMAGE_MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
|
@@ -210,6 +224,56 @@ function telegramReplyReference(message, { quote, loadReplyContent } = {}) {
|
|
|
210
224
|
};
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Normalize an inline-keyboard press into the same shape the bridge already
|
|
229
|
+
* consumes for messages, so a button reuses the existing conversation key,
|
|
230
|
+
* access policy, and pending-interaction routing instead of a parallel path.
|
|
231
|
+
* A press always counts as addressed: the user acted on a message the bot sent.
|
|
232
|
+
*/
|
|
233
|
+
export function normalizeTelegramCallback(update, { botId } = {}) {
|
|
234
|
+
const callback = update?.callback_query;
|
|
235
|
+
const message = callback?.message;
|
|
236
|
+
const chatId = message?.chat?.id;
|
|
237
|
+
const senderId = callback?.from?.id;
|
|
238
|
+
const providerMessageId = message?.message_id;
|
|
239
|
+
if (!Number.isSafeInteger(update?.update_id)
|
|
240
|
+
|| typeof callback?.id !== 'string' || !callback.id
|
|
241
|
+
|| chatId === undefined || senderId === undefined
|
|
242
|
+
|| !Number.isSafeInteger(providerMessageId)) return null;
|
|
243
|
+
if (!['private', 'group', 'supergroup'].includes(message.chat?.type)) return null;
|
|
244
|
+
const direct = message.chat.type === 'private';
|
|
245
|
+
const messageThreadId = Number.isSafeInteger(message.message_thread_id)
|
|
246
|
+
? message.message_thread_id : undefined;
|
|
247
|
+
const conversationId = messageThreadId === undefined
|
|
248
|
+
? String(chatId) : `${chatId}:${messageThreadId}`;
|
|
249
|
+
return {
|
|
250
|
+
messageId: String(update.update_id),
|
|
251
|
+
callbackQueryId: callback.id,
|
|
252
|
+
providerMessageId,
|
|
253
|
+
senderId: String(senderId),
|
|
254
|
+
senderIsBot: callback.from?.is_bot === true,
|
|
255
|
+
kind: direct ? 'direct' : 'group',
|
|
256
|
+
conversationId,
|
|
257
|
+
data: typeof callback.data === 'string' ? callback.data : '',
|
|
258
|
+
addressed: true,
|
|
259
|
+
replyTarget: {
|
|
260
|
+
chatId,
|
|
261
|
+
chatType: message.chat.type,
|
|
262
|
+
messageThreadId,
|
|
263
|
+
},
|
|
264
|
+
reactionTarget: { chatId, messageId: providerMessageId },
|
|
265
|
+
contextSource: () => ({
|
|
266
|
+
senderName: [callback.from?.first_name, callback.from?.last_name]
|
|
267
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
268
|
+
.map((value) => value.trim()).join(' ') || callback.from?.username,
|
|
269
|
+
conversationTitle: direct ? undefined : message.chat?.title,
|
|
270
|
+
chatId: String(chatId),
|
|
271
|
+
threadId: messageThreadId === undefined ? undefined : String(messageThreadId),
|
|
272
|
+
}),
|
|
273
|
+
...(botId === undefined ? {} : { botId }),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
213
277
|
export function normalizeTelegramUpdate(update, {
|
|
214
278
|
botId,
|
|
215
279
|
username,
|
|
@@ -463,6 +527,46 @@ export class TelegramBotClient {
|
|
|
463
527
|
return { providerMessageIds };
|
|
464
528
|
}
|
|
465
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Deliver a card whose inline keyboard carries the interaction payloads.
|
|
532
|
+
* Kept separate from sendText so a markup failure can be caught and degraded
|
|
533
|
+
* to the plain-text flow by the caller without losing the text itself.
|
|
534
|
+
*/
|
|
535
|
+
async sendInteractionCard(target, { text, markup, replyToMessageId } = {}) {
|
|
536
|
+
const result = await this.#api.sendMessage({
|
|
537
|
+
chatId: target.chatId,
|
|
538
|
+
text,
|
|
539
|
+
replyToMessageId: replyToMessageId ?? target.replyToMessageId,
|
|
540
|
+
messageThreadId: target.messageThreadId,
|
|
541
|
+
replyMarkup: markup,
|
|
542
|
+
signal: this.#signal,
|
|
543
|
+
});
|
|
544
|
+
const providerMessageIds = [];
|
|
545
|
+
if (Number.isSafeInteger(result?.message_id)) {
|
|
546
|
+
providerMessageIds.push(String(result.message_id));
|
|
547
|
+
}
|
|
548
|
+
return { providerMessageIds };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Replace a delivered card's keyboard in place; an empty keyboard removes it. */
|
|
552
|
+
async updateInteractionCard(target, providerMessageId, { markup } = {}) {
|
|
553
|
+
await this.#api.editMessageReplyMarkup({
|
|
554
|
+
chatId: target.chatId,
|
|
555
|
+
messageId: Number(providerMessageId),
|
|
556
|
+
replyMarkup: markup,
|
|
557
|
+
signal: this.#signal,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Stop the client-side spinner on a pressed button. */
|
|
562
|
+
async answerInteractionCallback(callbackQueryId, notice) {
|
|
563
|
+
await this.#api.answerCallbackQuery({
|
|
564
|
+
callbackQueryId,
|
|
565
|
+
text: notice,
|
|
566
|
+
signal: this.#signal,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
466
570
|
async addReaction(target, emoji, { signal } = {}) {
|
|
467
571
|
const reactionKey = String(emoji ?? '').trim();
|
|
468
572
|
await this.#api.setMessageReaction({
|
|
@@ -1086,7 +1190,8 @@ export class TelegramRuntime {
|
|
|
1086
1190
|
// All updates have arrived together; cursor persistence must not move the
|
|
1087
1191
|
// settings boundary for the later messages in this received batch.
|
|
1088
1192
|
const received = updates.map((update) => {
|
|
1089
|
-
const chatType = update?.message?.chat?.type
|
|
1193
|
+
const chatType = update?.message?.chat?.type
|
|
1194
|
+
?? update?.callback_query?.message?.chat?.type;
|
|
1090
1195
|
return {
|
|
1091
1196
|
update,
|
|
1092
1197
|
contextSnapshot: captureContextEnhancement(this.#contextEnhancement,
|
|
@@ -1096,6 +1201,23 @@ export class TelegramRuntime {
|
|
|
1096
1201
|
});
|
|
1097
1202
|
for (const { update, contextSnapshot } of received) {
|
|
1098
1203
|
if (signal.aborted) return;
|
|
1204
|
+
const callback = normalizeTelegramCallback(update, {
|
|
1205
|
+
botId: this.#config.platformId,
|
|
1206
|
+
});
|
|
1207
|
+
if (callback) {
|
|
1208
|
+
if (typeof this.#bridge.acceptCallback === 'function') {
|
|
1209
|
+
void this.#bridge.acceptCallback(callback, { contextSnapshot }).catch((error) => {
|
|
1210
|
+
if (signal.aborted) return;
|
|
1211
|
+
this.#logger.error?.(
|
|
1212
|
+
`[dsh-im:telegram] bot ${this.#config.botId} callback handling failed:`,
|
|
1213
|
+
error,
|
|
1214
|
+
);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
cursor = update.update_id + 1;
|
|
1218
|
+
await this.#state.setCursor(cursor);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1099
1221
|
const message = normalizeTelegramUpdate(update, {
|
|
1100
1222
|
botId: this.#config.platformId,
|
|
1101
1223
|
username: this.#config.username,
|
|
@@ -33,7 +33,11 @@ import {
|
|
|
33
33
|
} from '../shared/preset-command.mjs';
|
|
34
34
|
import { runWorkspaceCommand, workspacePathSnapshot } from '../shared/workspace-command.mjs';
|
|
35
35
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
captureContextEnhancement,
|
|
38
|
+
captureContextEnhancementSource,
|
|
39
|
+
enhanceContextContent,
|
|
40
|
+
} from '../shared/context-enhancement.mjs';
|
|
37
41
|
import {
|
|
38
42
|
hasInboundImages,
|
|
39
43
|
ImagePromptError,
|
|
@@ -928,6 +932,9 @@ export class WecomHarnessBridge {
|
|
|
928
932
|
...body,
|
|
929
933
|
msgtype: 'text',
|
|
930
934
|
text: { content: result.prompt },
|
|
935
|
+
// The submission is exactly the collected text, so a quote on the
|
|
936
|
+
// command itself must not become part of it.
|
|
937
|
+
quote: undefined,
|
|
931
938
|
},
|
|
932
939
|
}, messageId, key, { batchSubmission: result });
|
|
933
940
|
}
|
|
@@ -1163,6 +1170,11 @@ export class WecomHarnessBridge {
|
|
|
1163
1170
|
|| this.#approvals.hasPending(key),
|
|
1164
1171
|
control: { owner: this, key },
|
|
1165
1172
|
deferredDelivery: this.#deferred,
|
|
1173
|
+
enhancement: captureContextEnhancementSource(
|
|
1174
|
+
this.#contextEnhancement,
|
|
1175
|
+
bodyOf(frame).chattype === 'single' ? 'direct' : 'group',
|
|
1176
|
+
() => ({ channel: 'wecom', senderId: bodyOf(frame).from?.userid, chatId }),
|
|
1177
|
+
),
|
|
1166
1178
|
});
|
|
1167
1179
|
if (result?.stopped) {
|
|
1168
1180
|
await Promise.allSettled([
|
|
@@ -1366,6 +1378,8 @@ export class WecomHarnessBridge {
|
|
|
1366
1378
|
key,
|
|
1367
1379
|
text,
|
|
1368
1380
|
content,
|
|
1381
|
+
titleText: batchSubmission?.title,
|
|
1382
|
+
sourceGuidance: snapshot?.config?.guidance,
|
|
1369
1383
|
contextEnhanced,
|
|
1370
1384
|
createOptions: { signal: this.#signal },
|
|
1371
1385
|
existsOptions: { signal: this.#signal },
|
|
@@ -17,7 +17,9 @@ function safeIntegrationId(value) {
|
|
|
17
17
|
|
|
18
18
|
function safeRef(value, prefix) {
|
|
19
19
|
const ref = cleanString(value);
|
|
20
|
-
|
|
20
|
+
if (!ref?.startsWith(prefix)) return null;
|
|
21
|
+
const suffix = ref.slice(prefix.length);
|
|
22
|
+
return /^[A-F0-9]{24}$/.test(suffix) ? ref : null;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// A bot identity is stable per (corpId, agentId) pair so re-binding the same
|
|
@@ -26,7 +26,11 @@ import {
|
|
|
26
26
|
} from '../shared/preset-command.mjs';
|
|
27
27
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
28
28
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
captureContextEnhancement,
|
|
31
|
+
captureContextEnhancementSource,
|
|
32
|
+
enhanceContextContent,
|
|
33
|
+
} from '../shared/context-enhancement.mjs';
|
|
30
34
|
import {
|
|
31
35
|
DEFAULT_IMAGE_PROMPT,
|
|
32
36
|
hasInboundImages,
|
|
@@ -498,6 +502,11 @@ export class WecomAppBridge {
|
|
|
498
502
|
|| this.#approvals.hasPending(key),
|
|
499
503
|
control: { owner: this, key },
|
|
500
504
|
deferredDelivery: this.#deferred,
|
|
505
|
+
enhancement: captureContextEnhancementSource(
|
|
506
|
+
this.#contextEnhancement,
|
|
507
|
+
'direct',
|
|
508
|
+
() => ({ channel: 'wecom-app', senderId: sender, chatId: sender }),
|
|
509
|
+
),
|
|
501
510
|
});
|
|
502
511
|
if (result?.stopped) {
|
|
503
512
|
await Promise.allSettled([
|
|
@@ -605,6 +614,8 @@ export class WecomAppBridge {
|
|
|
605
614
|
key,
|
|
606
615
|
text,
|
|
607
616
|
content,
|
|
617
|
+
titleText: batchSubmission?.title,
|
|
618
|
+
sourceGuidance: snapshot?.config?.guidance,
|
|
608
619
|
contextEnhanced,
|
|
609
620
|
createOptions: { signal: this.#signal },
|
|
610
621
|
existsOptions: { signal: this.#signal },
|
|
@@ -4,6 +4,7 @@ import { dirname } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { normalizeWeixinApiBaseUrl } from './weixin-api.mjs';
|
|
6
6
|
import { t } from '../shared/i18n.mjs';
|
|
7
|
+
import { configValidationError, withConfigResource } from '../shared/config-read-error.mjs';
|
|
7
8
|
|
|
8
9
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, accounts: Object.freeze([]) });
|
|
9
10
|
|
|
@@ -37,20 +38,24 @@ export function maskWeixinAccountId(accountId) {
|
|
|
37
38
|
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function normalizeAccount(value) {
|
|
41
|
-
if (!value || typeof value !== 'object') return
|
|
41
|
+
function normalizeAccount(value, invalid = () => null) {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return invalid('', 'expected-object');
|
|
42
43
|
const accountId = cleanString(value.accountId);
|
|
43
44
|
const ownerUserId = cleanString(value.ownerUserId);
|
|
44
45
|
const botId = safeBotId(value.botId);
|
|
45
46
|
const tokenRef = safeTokenRef(value.tokenRef);
|
|
46
|
-
if (!accountId
|
|
47
|
+
if (!accountId) return invalid('.accountId', 'invalid-string');
|
|
48
|
+
if (!ownerUserId) return invalid('.ownerUserId', 'invalid-string');
|
|
49
|
+
if (!botId) return invalid('.botId', 'invalid-identifier');
|
|
50
|
+
if (!tokenRef) return invalid('.tokenRef', 'invalid-identifier');
|
|
47
51
|
const derived = deriveWeixinBotIdentity(accountId);
|
|
48
|
-
if (derived.botId !== botId
|
|
52
|
+
if (derived.botId !== botId) return invalid('.botId', 'identity-mismatch');
|
|
53
|
+
if (derived.tokenRef !== tokenRef) return invalid('.tokenRef', 'identity-mismatch');
|
|
49
54
|
let baseUrl;
|
|
50
55
|
try {
|
|
51
56
|
baseUrl = normalizeWeixinApiBaseUrl(value.baseUrl);
|
|
52
|
-
} catch {
|
|
53
|
-
return
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return invalid('.baseUrl', error?.code === 'untrusted-base-url' ? 'untrusted-api-url' : 'invalid-api-url');
|
|
54
59
|
}
|
|
55
60
|
return Object.freeze({
|
|
56
61
|
botId,
|
|
@@ -64,16 +69,21 @@ function normalizeAccount(value) {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
function normalizeDocument(value) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
const invalid = (field, issue) => {
|
|
73
|
+
throw configValidationError('dsh-weixin config contains invalid account data', field, issue);
|
|
74
|
+
};
|
|
75
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return invalid('$', 'expected-object');
|
|
76
|
+
if (value.version !== 1) return invalid('version', 'unsupported-version');
|
|
77
|
+
if (!Array.isArray(value.accounts)) return invalid('accounts', 'expected-array');
|
|
78
|
+
const accounts = value.accounts.map((account, index) => normalizeAccount(account,
|
|
79
|
+
(field, issue) => invalid(`accounts[${index}]${field}`, issue)));
|
|
70
80
|
const ids = new Set();
|
|
71
81
|
const accountIds = new Set();
|
|
72
82
|
const refs = new Set();
|
|
73
|
-
for (const account of accounts) {
|
|
74
|
-
if (ids.has(account.botId)
|
|
75
|
-
|
|
76
|
-
}
|
|
83
|
+
for (const [index, account] of accounts.entries()) {
|
|
84
|
+
if (ids.has(account.botId)) return invalid(`accounts[${index}].botId`, 'duplicate-identity');
|
|
85
|
+
if (accountIds.has(account.accountId)) return invalid(`accounts[${index}].accountId`, 'duplicate-identity');
|
|
86
|
+
if (refs.has(account.tokenRef)) return invalid(`accounts[${index}].tokenRef`, 'duplicate-identity');
|
|
77
87
|
ids.add(account.botId);
|
|
78
88
|
accountIds.add(account.accountId);
|
|
79
89
|
refs.add(account.tokenRef);
|
|
@@ -93,10 +103,9 @@ export class WeixinConfigStore {
|
|
|
93
103
|
async load() {
|
|
94
104
|
try {
|
|
95
105
|
const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
96
|
-
if (!normalized) throw new Error('dsh-weixin config contains invalid account data');
|
|
97
106
|
this.#value = normalized;
|
|
98
107
|
} catch (error) {
|
|
99
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
108
|
+
if (error?.code !== 'ENOENT') throw withConfigResource(error, 'account-config');
|
|
100
109
|
this.#value = EMPTY_DOCUMENT;
|
|
101
110
|
}
|
|
102
111
|
return this;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
// Shared diagnostic copy for the WeChat Host and settings client.
|
|
2
2
|
export default {
|
|
3
|
+
"微信配置格式错误:{file}。请查看诊断详情,修复后重启 DSH。": "The WeChat configuration is invalid: {file}. See the diagnostic details, fix the file, and restart DSH.",
|
|
4
|
+
"配置文件": "Configuration file",
|
|
5
|
+
"配置字段": "Configuration field",
|
|
6
|
+
"校验原因": "Validation issue",
|
|
7
|
+
"应为 JSON 对象。": "Expected a JSON object.",
|
|
8
|
+
"应为 JSON 数组。": "Expected a JSON array.",
|
|
9
|
+
"配置版本缺失或不受当前插件支持。": "The configuration version is missing or unsupported by this plugin.",
|
|
10
|
+
"字段缺失或不是非空字符串。": "The field is missing or is not a non-empty string.",
|
|
11
|
+
"标识符格式不符合要求。": "The identifier has an invalid format.",
|
|
12
|
+
"标识符与 accountId 派生结果不一致。": "The identifier does not match the value derived from accountId.",
|
|
13
|
+
"账号标识重复。": "The account identifier is duplicated.",
|
|
14
|
+
"微信服务地址不是有效 URL。": "The WeChat service address is not a valid URL.",
|
|
15
|
+
"微信服务地址必须使用受信任的 HTTPS 域名和端口。": "The WeChat service address must use a trusted HTTPS domain and port.",
|
|
16
|
+
"工作区路径必须是当前操作系统的绝对路径。": "The workspace path must be absolute on the current operating system.",
|
|
17
|
+
"Agent Preset 标识无效。": "The Agent Preset identifier is invalid.",
|
|
18
|
+
"模型设置须包含有效的 provider、model 和可选 reasoningEffort。": "Model settings require valid provider and model fields, with an optional valid reasoningEffort.",
|
|
19
|
+
"投递目标的标识、结构或路由不符合当前配置版本要求。": "The delivery target identifier, structure or route is invalid for this configuration version.",
|
|
20
|
+
"当前配置版本不允许此字段。": "This field is not allowed in this configuration version.",
|
|
21
|
+
"JSON 语法无效。": "The JSON syntax is invalid.",
|
|
22
|
+
"请检查微信渠道数据目录中的 {file},修复后重启 DSH;“重新读取”不会重新加载配置。": "Check {file} in the WeChat channel data directory, fix it, and restart DSH; refreshing the status does not reload configuration.",
|
|
23
|
+
"字段位置中的序号从 0 开始,按文件中的条目顺序计数,不包含真实账号标识。": "Field positions use zero-based entry order in the file without including actual account identifiers.",
|
|
3
24
|
"插件版本": "Plugin version",
|
|
4
25
|
"DSH 微信管理接口返回了无法识别的响应,请重新读取状态。": "The DSH WeChat management endpoint returned an invalid response. Refresh the status.",
|
|
5
26
|
"无法读取现有登录凭据。请检查 DSH 凭据存储。": "Could not read the saved login credential. Check the DSH credential store.",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import packageInfo from '../../../package.json' with { type: 'json' };
|
|
3
3
|
import { t } from '../shared/i18n.mjs';
|
|
4
|
-
import { normalizeWeixinDiagnosticDetails } from './diagnostic-details.mjs';
|
|
4
|
+
import { CONFIG_ISSUE_LABELS, normalizeWeixinDiagnosticDetails } from './diagnostic-details.mjs';
|
|
5
|
+
import { configReadErrorDetails } from '../shared/config-read-error.mjs';
|
|
5
6
|
|
|
6
7
|
const MESSAGES = Object.freeze({
|
|
7
8
|
'credential-read-failed': '无法读取现有登录凭据。请检查 DSH 凭据存储。',
|
|
@@ -123,6 +124,13 @@ function hintFor(code, details) {
|
|
|
123
124
|
if (details.reason === 'ENOTFOUND' || details.reason === 'EAI_AGAIN') return t('微信服务域名解析失败,请检查运行 DSH 的机器的网络和 DNS 设置后重试。');
|
|
124
125
|
if (['EACCES', 'EPERM', 'EROFS'].includes(details.reason)) return t('请检查 DSH 数据目录和对应文件的读写权限。');
|
|
125
126
|
if (details.reason === 'ENOSPC') return t('磁盘空间不足,请释放空间后重试。');
|
|
127
|
+
if (code.startsWith('weixin-startup-') && details.file) {
|
|
128
|
+
const explanation = details.reason === 'invalid-json' ? t('JSON 语法无效。')
|
|
129
|
+
: details.issue ? t(CONFIG_ISSUE_LABELS[details.issue]) : '';
|
|
130
|
+
return [explanation, t('请检查微信渠道数据目录中的 {file},修复后重启 DSH;“重新读取”不会重新加载配置。', { file: details.file }),
|
|
131
|
+
details.field ? t('字段位置中的序号从 0 开始,按文件中的条目顺序计数,不包含真实账号标识。') : '',
|
|
132
|
+
].filter(Boolean).join(' ');
|
|
133
|
+
}
|
|
126
134
|
if (details.reason === 'invalid-json' && details.resource) return t('本机文件格式无效,请检查对应配置或状态文件;不要清空登录凭据。');
|
|
127
135
|
if (/CERT|TLS|SSL/.test(details.reason ?? '')) return t('请检查运行 DSH 的机器的系统时间、证书和网络设置。');
|
|
128
136
|
if (code === 'stale-token' || code === 'missing-token') return t('请移除失效接入并重新扫码绑定。');
|
|
@@ -143,17 +151,21 @@ export function createWeixinDiagnostics({ logger = console, now = Date.now } = {
|
|
|
143
151
|
const selected = chain.find(error => knownWeixinErrorCode(error.code));
|
|
144
152
|
const code = selected?.code ?? (knownWeixinErrorCode(context.code) ? context.code : 'weixin-operation-failed');
|
|
145
153
|
const staged = chain.map(error => ownedStages.get(error)).find(Boolean) ?? {};
|
|
154
|
+
const configDetails = chain.map(configReadErrorDetails).find(Boolean) ?? {};
|
|
146
155
|
const defaults = CODE_STAGES[code] ?? [];
|
|
147
156
|
const reason = chain.map(error => normalizeWeixinDiagnosticDetails({ reason: error.code }).reason).find(Boolean)
|
|
148
157
|
?? (chain.some(error => error instanceof SyntaxError) ? 'invalid-json' : undefined);
|
|
149
158
|
const numeric = field => chain.map(error => normalizeWeixinDiagnosticDetails({ [field]: field === 'httpStatus' ? error.status : error[field] })[field]).find(value => value !== undefined);
|
|
150
159
|
const details = normalizeWeixinDiagnosticDetails({
|
|
151
|
-
...context,
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
...context, ...configDetails,
|
|
161
|
+
file: { 'account-config': 'config.json', 'workspace-config': 'workspaces.json' }[configDetails.resource],
|
|
162
|
+
stage: staged.stage ?? defaults[0] ?? (code.startsWith('harness-') ? 'harness.check' : context.stage),
|
|
163
|
+
resource: staged.resource ?? defaults[1] ?? configDetails.resource ?? context.resource,
|
|
164
|
+
reason: reason ?? configDetails.reason ?? context.reason, httpStatus: numeric('httpStatus'), providerCode: numeric('providerCode'), pluginVersion: packageInfo.version,
|
|
154
165
|
});
|
|
155
166
|
const botId = /^wx_[a-f0-9]{24}$/.test(context.botId ?? '') ? context.botId : undefined;
|
|
156
|
-
const key = JSON.stringify([botId, details.operation, details.stage, code, details.reason, details.httpStatus, details.providerCode
|
|
167
|
+
const key = JSON.stringify([botId, details.operation, details.stage, code, details.reason, details.httpStatus, details.providerCode,
|
|
168
|
+
details.resource, details.file, details.field, details.issue]);
|
|
157
169
|
const previous = recent.get(key);
|
|
158
170
|
const time = now();
|
|
159
171
|
if (context.automatic && previous && time - previous.time < 60_000) {
|
|
@@ -168,7 +180,10 @@ export function createWeixinDiagnostics({ logger = console, now = Date.now } = {
|
|
|
168
180
|
details.referenceId = `WX-CONN-${randomUUID().replaceAll('-', '').slice(0, 8).toUpperCase()}`;
|
|
169
181
|
details.occurredAt = new Date(time).toISOString();
|
|
170
182
|
details.hint = hintFor(code, details);
|
|
171
|
-
const
|
|
183
|
+
const message = code === 'weixin-startup-config-invalid' && details.file
|
|
184
|
+
? t('微信配置格式错误:{file}。请查看诊断详情,修复后重启 DSH。', { file: details.file })
|
|
185
|
+
: t(MESSAGES[code], { status: details.httpStatus ?? '?' });
|
|
186
|
+
const publicError = { code, message, details };
|
|
172
187
|
const wrapped = new Error(publicError.message, { cause });
|
|
173
188
|
wrapped.code = code;
|
|
174
189
|
wrapped.publicError = publicError;
|
|
@@ -15,9 +15,29 @@ const REASONS = new Set([
|
|
|
15
15
|
'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
|
|
16
16
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_TLS_CERT_ALTNAME_INVALID', 'ERR_SSL_WRONG_VERSION_NUMBER',
|
|
17
17
|
'ENOENT', 'EACCES', 'EPERM', 'ENOSPC', 'EROFS', 'ENOTDIR', 'EISDIR', 'EBUSY', 'EIO', 'EXDEV', 'EMFILE', 'ENFILE', 'EEXIST',
|
|
18
|
-
'invalid-json', 'read-only',
|
|
18
|
+
'invalid-json', 'invalid-config', 'read-only',
|
|
19
19
|
]);
|
|
20
20
|
const RESOURCES = new Set(['credential-store', 'account-config', 'account-state', 'workspace-config', 'workspace-directory']);
|
|
21
|
+
const CONFIG_FILES = new Set(['config.json', 'workspaces.json']);
|
|
22
|
+
export const CONFIG_ISSUE_LABELS = Object.freeze({
|
|
23
|
+
'expected-object': '应为 JSON 对象。',
|
|
24
|
+
'expected-array': '应为 JSON 数组。',
|
|
25
|
+
'unsupported-version': '配置版本缺失或不受当前插件支持。',
|
|
26
|
+
'invalid-string': '字段缺失或不是非空字符串。',
|
|
27
|
+
'invalid-identifier': '标识符格式不符合要求。',
|
|
28
|
+
'identity-mismatch': '标识符与 accountId 派生结果不一致。',
|
|
29
|
+
'duplicate-identity': '账号标识重复。',
|
|
30
|
+
'invalid-api-url': '微信服务地址不是有效 URL。',
|
|
31
|
+
'untrusted-api-url': '微信服务地址必须使用受信任的 HTTPS 域名和端口。',
|
|
32
|
+
'invalid-workspace-path': '工作区路径必须是当前操作系统的绝对路径。',
|
|
33
|
+
'invalid-agent-preset': 'Agent Preset 标识无效。',
|
|
34
|
+
'invalid-model-selection': '模型设置须包含有效的 provider、model 和可选 reasoningEffort。',
|
|
35
|
+
'invalid-delivery-target': '投递目标的标识、结构或路由不符合当前配置版本要求。',
|
|
36
|
+
'unexpected-field': '当前配置版本不允许此字段。',
|
|
37
|
+
});
|
|
38
|
+
// Only schema-owned field names and numeric entry positions may leave the Host.
|
|
39
|
+
// Map keys are replaced with zero-based positions to avoid exposing identities.
|
|
40
|
+
const CONFIG_FIELD = /^(?:\$|version|accounts(?:\[\d{1,10}\](?:\.(?:accountId|ownerUserId|botId|tokenRef|baseUrl))?)?|(?:workspaces|agentPresets|models)(?:\[\d{1,10}\]\.(?:key|value))?|deliveryTargets(?:\[\d{1,10}\]\.(?:key|targets(?:\[\d{1,10}\])?))?)$/;
|
|
21
41
|
|
|
22
42
|
export function normalizeWeixinDiagnosticDetails(value) {
|
|
23
43
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
@@ -25,6 +45,9 @@ export function normalizeWeixinDiagnosticDetails(value) {
|
|
|
25
45
|
for (const [field, allowed] of [['operation', OPERATIONS], ['stage', STAGES], ['reason', REASONS], ['resource', RESOURCES]]) {
|
|
26
46
|
if (allowed.has(value[field])) result[field] = value[field];
|
|
27
47
|
}
|
|
48
|
+
if (CONFIG_FILES.has(value.file)) result.file = value.file;
|
|
49
|
+
if (typeof value.field === 'string' && CONFIG_FIELD.test(value.field)) result.field = value.field;
|
|
50
|
+
if (typeof value.issue === 'string' && Object.hasOwn(CONFIG_ISSUE_LABELS, value.issue)) result.issue = value.issue;
|
|
28
51
|
if (/^WX-CONN-[A-F0-9]{8}$/.test(value.referenceId ?? '')) result.referenceId = value.referenceId;
|
|
29
52
|
if (typeof value.occurredAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value.occurredAt)
|
|
30
53
|
&& Number.isFinite(Date.parse(value.occurredAt))) result.occurredAt = value.occurredAt;
|
|
@@ -21,6 +21,8 @@ const ILINK_CLIENT_VERSION = (2 << 16) | (4 << 8) | 6;
|
|
|
21
21
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
22
22
|
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
23
23
|
const WEIXIN_CDN_UPLOAD_RETRIES = 3;
|
|
24
|
+
const WEIXIN_CDN_UPLOAD_IDLE_TIMEOUT_MS = 60_000;
|
|
25
|
+
const WEIXIN_CDN_UPLOAD_CHUNK_BYTES = 64 * 1024;
|
|
24
26
|
const WEIXIN_MESSAGE_ID_TIMESTAMP_SHIFT = 22n;
|
|
25
27
|
const WEIXIN_MESSAGE_ID_MIN_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
|
26
28
|
const WEIXIN_MESSAGE_ID_MAX_FUTURE_MS = 24 * 60 * 60 * 1_000;
|
|
@@ -79,6 +81,9 @@ function weixinArtifactError(cause, { fallback = 'artifact-provider-rejected' }
|
|
|
79
81
|
|| /(?:rate.?limit|too.?many)/i.test(providerText)) {
|
|
80
82
|
code = 'artifact-rate-limited';
|
|
81
83
|
message = 'Weixin rate-limited file delivery.';
|
|
84
|
+
} else if (cause?.code === 'upload-timeout') {
|
|
85
|
+
code = 'artifact-upload-timeout';
|
|
86
|
+
message = 'Weixin file upload stalled; the file message was not sent.';
|
|
82
87
|
} else if (fallback === 'artifact-provider-rejected') {
|
|
83
88
|
message = 'Weixin rejected the file message.';
|
|
84
89
|
}
|
|
@@ -339,25 +344,54 @@ function weixinCdnUploadUrl(response, fileKey) {
|
|
|
339
344
|
return trustedWeixinCdnUploadUrl(url);
|
|
340
345
|
}
|
|
341
346
|
|
|
342
|
-
function encryptWeixinUpload(bytes, key) {
|
|
347
|
+
async function* encryptWeixinUpload(bytes, key, { signal, onProgress }) {
|
|
343
348
|
const cipher = createCipheriv('aes-128-ecb', key, null);
|
|
344
|
-
|
|
349
|
+
// Let fetch backpressure drive encryption, without keeping whole-file
|
|
350
|
+
// ciphertext copies alongside a potentially large artifact buffer.
|
|
351
|
+
for (let offset = 0; offset < bytes.byteLength; offset += WEIXIN_CDN_UPLOAD_CHUNK_BYTES) {
|
|
352
|
+
signal.throwIfAborted();
|
|
353
|
+
const chunk = cipher.update(bytes.subarray(offset, offset + WEIXIN_CDN_UPLOAD_CHUNK_BYTES));
|
|
354
|
+
onProgress();
|
|
355
|
+
if (chunk.byteLength) yield chunk;
|
|
356
|
+
}
|
|
357
|
+
signal.throwIfAborted();
|
|
358
|
+
onProgress();
|
|
359
|
+
yield cipher.final();
|
|
345
360
|
}
|
|
346
361
|
|
|
347
|
-
async function uploadWeixinCdn(fetchImpl, url,
|
|
362
|
+
async function uploadWeixinCdn(fetchImpl, url, bytes, key, { signal } = {}) {
|
|
348
363
|
let lastError;
|
|
349
364
|
for (let attempt = 1; attempt <= WEIXIN_CDN_UPLOAD_RETRIES; attempt += 1) {
|
|
350
365
|
signal?.throwIfAborted();
|
|
366
|
+
const idleController = new AbortController();
|
|
367
|
+
const uploadSignal = signal
|
|
368
|
+
? AbortSignal.any([signal, idleController.signal])
|
|
369
|
+
: idleController.signal;
|
|
370
|
+
let timer;
|
|
371
|
+
let active = true;
|
|
372
|
+
const onProgress = () => {
|
|
373
|
+
if (!active) return;
|
|
374
|
+
clearTimeout(timer);
|
|
375
|
+
timer = setTimeout(() => idleController.abort(new WeixinApiError(
|
|
376
|
+
'upload-timeout', '微信文件上传长时间没有进展,已超时。',
|
|
377
|
+
)), WEIXIN_CDN_UPLOAD_IDLE_TIMEOUT_MS);
|
|
378
|
+
};
|
|
379
|
+
const body = encryptWeixinUpload(bytes, key, { signal: uploadSignal, onProgress });
|
|
380
|
+
let response;
|
|
381
|
+
onProgress();
|
|
351
382
|
try {
|
|
352
|
-
|
|
383
|
+
response = await fetchImpl(url, {
|
|
353
384
|
method: 'POST',
|
|
354
|
-
headers: {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
385
|
+
headers: {
|
|
386
|
+
'content-type': 'application/octet-stream',
|
|
387
|
+
'content-length': String(aesEcbPaddedSize(bytes.byteLength)),
|
|
388
|
+
},
|
|
389
|
+
body,
|
|
390
|
+
duplex: 'half',
|
|
391
|
+
signal: uploadSignal,
|
|
359
392
|
redirect: 'error',
|
|
360
393
|
});
|
|
394
|
+
uploadSignal.throwIfAborted();
|
|
361
395
|
if (response.status >= 400 && response.status < 500) {
|
|
362
396
|
throw new WeixinApiError(
|
|
363
397
|
'upload-rejected',
|
|
@@ -373,16 +407,21 @@ async function uploadWeixinCdn(fetchImpl, url, ciphertext, { signal } = {}) {
|
|
|
373
407
|
);
|
|
374
408
|
}
|
|
375
409
|
const downloadParam = nonEmptyString(response.headers.get('x-encrypted-param'));
|
|
376
|
-
await response.body?.cancel?.().catch(() => undefined);
|
|
377
410
|
if (!downloadParam) {
|
|
378
411
|
throw new WeixinApiError('invalid-upload-response', '微信文件上传响应缺少下载参数。');
|
|
379
412
|
}
|
|
380
413
|
return downloadParam;
|
|
381
414
|
} catch (error) {
|
|
382
415
|
if (signal?.aborted) throw abortError(signal);
|
|
416
|
+
if (idleController.signal.aborted) error = idleController.signal.reason;
|
|
383
417
|
if (error instanceof WeixinApiError
|
|
384
418
|
&& (error.code === 'upload-rejected' || error.status < 500)) throw error;
|
|
385
419
|
lastError = error;
|
|
420
|
+
} finally {
|
|
421
|
+
active = false;
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
await body.return();
|
|
424
|
+
await response?.body?.cancel?.().catch(() => undefined);
|
|
386
425
|
}
|
|
387
426
|
}
|
|
388
427
|
if (lastError instanceof WeixinApiError) throw lastError;
|
|
@@ -524,10 +563,10 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
524
563
|
));
|
|
525
564
|
}
|
|
526
565
|
const uploadUrl = weixinCdnUploadUrl(upload, fileKey);
|
|
527
|
-
const
|
|
566
|
+
const ciphertextSize = aesEcbPaddedSize(file.bytes.byteLength);
|
|
528
567
|
let downloadParam;
|
|
529
568
|
try {
|
|
530
|
-
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl,
|
|
569
|
+
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, file.bytes, aesKey, { signal });
|
|
531
570
|
} catch (error) {
|
|
532
571
|
if (signal?.aborted) throw abortError(signal);
|
|
533
572
|
const status = Number(error?.status);
|
|
@@ -564,7 +603,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
564
603
|
client_id: clientId,
|
|
565
604
|
message_type: 2,
|
|
566
605
|
message_state: 2,
|
|
567
|
-
item_list: [createItem({ file, media, ciphertextSize
|
|
606
|
+
item_list: [createItem({ file, media, ciphertextSize })],
|
|
568
607
|
...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
|
|
569
608
|
...(nonEmptyString(runId) ? { run_id: runId.trim() } : {}),
|
|
570
609
|
},
|
|
@@ -36,7 +36,11 @@ import {
|
|
|
36
36
|
} from '../shared/preset-command.mjs';
|
|
37
37
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
38
38
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
captureContextEnhancement,
|
|
41
|
+
captureContextEnhancementSource,
|
|
42
|
+
enhanceContextContent,
|
|
43
|
+
} from '../shared/context-enhancement.mjs';
|
|
40
44
|
import {
|
|
41
45
|
hasInboundImages,
|
|
42
46
|
imagePromptDiagnostic,
|
|
@@ -262,6 +266,8 @@ function artifactFailureText(fileName, error) {
|
|
|
262
266
|
return t('结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。', { name });
|
|
263
267
|
case 'artifact-too-large':
|
|
264
268
|
return t('结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。', { name });
|
|
269
|
+
case 'artifact-upload-timeout':
|
|
270
|
+
return t('结果文件「{name}」上传微信时长时间没有进展,已超时,文件尚未发送。请检查网络后重试,或压缩、拆分文件后发送。', { name });
|
|
265
271
|
case 'artifact-rate-limited':
|
|
266
272
|
return t('结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。', { name });
|
|
267
273
|
case 'artifact-provider-rejected':
|
|
@@ -707,6 +713,11 @@ export class WeixinHarnessBridge {
|
|
|
707
713
|
|| this.#approvals.hasPending(key),
|
|
708
714
|
control: { owner: this, key },
|
|
709
715
|
deferredDelivery: this.#deferred,
|
|
716
|
+
enhancement: captureContextEnhancementSource(
|
|
717
|
+
this.#contextEnhancement,
|
|
718
|
+
'direct',
|
|
719
|
+
() => ({ channel: 'weixin', senderId: sender, chatId: sender }),
|
|
720
|
+
),
|
|
710
721
|
});
|
|
711
722
|
if (result?.stopped) {
|
|
712
723
|
await Promise.allSettled([
|
|
@@ -825,6 +836,8 @@ export class WeixinHarnessBridge {
|
|
|
825
836
|
key,
|
|
826
837
|
text,
|
|
827
838
|
content,
|
|
839
|
+
titleText: batchSubmission?.title,
|
|
840
|
+
sourceGuidance: snapshot?.config?.guidance,
|
|
828
841
|
contextEnhanced,
|
|
829
842
|
createOptions: { signal: this.#signal },
|
|
830
843
|
existsOptions: { signal: this.#signal },
|