@xmanrui/dsh-im 4.15.0 → 4.16.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.
Files changed (62) hide show
  1. package/README.en.md +2 -1
  2. package/README.md +9 -5
  3. package/lib/client.js +1729 -614
  4. package/lib/index.js +303 -293
  5. package/package.json +6 -2
  6. package/plugin-src/client/access-policy-settings.js +6 -0
  7. package/plugin-src/client/channels/feishu/index.js +15 -3
  8. package/plugin-src/client/channels/wecom-app/api.js +124 -0
  9. package/plugin-src/client/channels/wecom-app/index.js +612 -0
  10. package/plugin-src/client/channels/wecom-app/styles.js +25 -0
  11. package/plugin-src/client/delivery-settings.js +7 -0
  12. package/plugin-src/client/i18n.js +63 -0
  13. package/plugin-src/client/index.js +18 -0
  14. package/plugin-src/client/model-setting.js +135 -69
  15. package/plugin-src/client/styles.js +22 -7
  16. package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
  17. package/plugin-src/host/channels/discord/index.mjs +8 -10
  18. package/plugin-src/host/channels/feishu/index.mjs +8 -15
  19. package/plugin-src/host/channels/office/index.mjs +9 -5
  20. package/plugin-src/host/channels/qq/index.mjs +8 -15
  21. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  22. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
  23. package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
  24. package/plugin-src/host/channels/shared/startup.mjs +47 -0
  25. package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
  26. package/plugin-src/host/channels/slack/index.mjs +8 -10
  27. package/plugin-src/host/channels/telegram/index.mjs +8 -10
  28. package/plugin-src/host/channels/wecom/index.mjs +8 -15
  29. package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
  30. package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
  31. package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
  32. package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
  33. package/plugin-src/host/channels/weixin/index.mjs +8 -15
  34. package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
  35. package/plugin-src/host/delivery-adapter.mjs +4 -0
  36. package/plugin-src/host/delivery-suggestions.mjs +4 -0
  37. package/plugin-src/host/index.mjs +7 -2
  38. package/scripts/verify-model-setting.mjs +54 -0
  39. package/src/channels/feishu/bridge.mjs +295 -9
  40. package/src/channels/feishu/feishu-cards.mjs +38 -0
  41. package/src/channels/feishu/feishu-channel.mjs +88 -20
  42. package/src/channels/shared/bot-workspace-store.mjs +11 -3
  43. package/src/channels/shared/command-catalog.mjs +102 -0
  44. package/src/channels/shared/i18n-en/feishu.mjs +12 -0
  45. package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
  46. package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
  47. package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
  48. package/src/channels/shared/i18n-en.mjs +2 -0
  49. package/src/channels/shared/model-setting.mjs +43 -8
  50. package/src/channels/shared/text-harness-bridge.mjs +2 -26
  51. package/src/channels/telegram/telegram-api.mjs +18 -6
  52. package/src/channels/telegram/telegram-runtime.mjs +34 -32
  53. package/src/channels/wecom/send-error.mjs +31 -0
  54. package/src/channels/wecom/wecom-bridge.mjs +59 -17
  55. package/src/channels/wecom-app/callback-server.mjs +471 -0
  56. package/src/channels/wecom-app/config-store.mjs +240 -0
  57. package/src/channels/wecom-app/harness-client.mjs +11 -0
  58. package/src/channels/wecom-app/state-store.mjs +107 -0
  59. package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
  60. package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
  61. package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
  62. package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
@@ -0,0 +1,1059 @@
1
+ import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
2
+ import {
3
+ harnessAnswerForQuestion,
4
+ harnessQuestionText,
5
+ validHarnessQuestion,
6
+ } from '../shared/harness-question.mjs';
7
+ import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
8
+ import {
9
+ BatchInputManager,
10
+ batchInputBusyMessage,
11
+ isBatchInputCommand,
12
+ } from '../shared/batch-input.mjs';
13
+ import { runCompactCommand } from '../shared/compact-command.mjs';
14
+ import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
15
+ import {
16
+ isControlCommand,
17
+ runControlCommand,
18
+ } from '../shared/control-command.mjs';
19
+ import {
20
+ isModelCommand,
21
+ runModelCommand,
22
+ } from '../shared/model-command.mjs';
23
+ import {
24
+ isPresetCommand,
25
+ runPresetCommand,
26
+ } from '../shared/preset-command.mjs';
27
+ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
28
+ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
29
+ import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
30
+ import {
31
+ DEFAULT_IMAGE_PROMPT,
32
+ hasInboundImages,
33
+ imagePromptDiagnostic,
34
+ imagePromptUserMessage,
35
+ } from '../shared/image-prompt.mjs';
36
+ import {
37
+ hasInboundFiles,
38
+ inboundFileUserMessage,
39
+ } from '../shared/inbound-file.mjs';
40
+ import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
41
+ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
42
+ import {
43
+ createDeliveryReceipt,
44
+ providerMessageIdsFor,
45
+ } from '../shared/semantic/delivery.mjs';
46
+ import {
47
+ hasReplyReference,
48
+ promptContentForInboundMessage,
49
+ } from '../shared/semantic/reply-reference.mjs';
50
+ import {
51
+ channelDeliveryFailure,
52
+ clearLastMessageFailure,
53
+ messageFailureText,
54
+ setLastMessageFailure,
55
+ } from '../shared/message-failure.mjs';
56
+ import {
57
+ COMMAND_PERMISSION_DENIED_MESSAGE,
58
+ evaluateInboundAccess,
59
+ } from '../shared/inbound-access.mjs';
60
+ import { t } from '../shared/i18n.mjs';
61
+ import { WECOM_APP_TEXT_MAX_BYTES, splitUtf8ByBytes } from './wecom-app-api.mjs';
62
+
63
+ const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
64
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
65
+ const UNSUPPORTED_INBOUND_TEXT = () => t('企业微信应用目前支持文字和图片消息。');
66
+
67
+ const HELP_TEXT = () => [
68
+ t('企业微信应用已连接 DeepSeek Harness,可在微信中直接使用。'),
69
+ '',
70
+ t('直接发送文字或图片即可继续当前会话。'),
71
+ t('/new 开启一个全新会话'),
72
+ t('/compact 压缩当前会话的较早上下文'),
73
+ t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
74
+ t('/workspace 工作区序号或绝对路径 切换工作区'),
75
+ t('/workspacelist 列出工作区绝对路径'),
76
+ t('/ws、/wsl、/workspaces 工作区命令别名'),
77
+ t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
78
+ t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
79
+ t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
80
+ t('/models 按序号列出所有可用模型'),
81
+ t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
82
+ t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
83
+ t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
84
+ t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
85
+ t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
86
+ t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
87
+ t('纯数字 ID:/preset id:<ID>'),
88
+ t('/preset --default 跟随 Host 默认'),
89
+ t('/stop 停止当前任务'),
90
+ t('/steer 补充指令 纠偏当前任务'),
91
+ t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
92
+ t('/send 提交当前批次'),
93
+ t('/cancel 取消当前批次'),
94
+ t('/status 检查连接状态'),
95
+ t('/version 查看插件版本'),
96
+ t('/help 显示本帮助'),
97
+ ].join('\n');
98
+
99
+ const WELCOME_TEXT = () => t('企业微信应用已连接 DeepSeek Harness。直接发送文字或图片即可开始,发送 /help 查看可用命令。');
100
+
101
+ function conversationKey(userId) {
102
+ return `p2p:${userId}`;
103
+ }
104
+
105
+ function nonEmptyString(value) {
106
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
107
+ }
108
+
109
+ function messageText(message) {
110
+ if (message?.msgtype !== 'text') return '';
111
+ return typeof message.text?.content === 'string' ? message.text.content : '';
112
+ }
113
+
114
+ function hasImageItems(message) {
115
+ return message?.msgtype === 'image' && Boolean(message.image?.url || message.mediaId);
116
+ }
117
+
118
+ function supportedInboundType(message) {
119
+ return message?.msgtype === 'text' || message?.msgtype === 'image';
120
+ }
121
+
122
+ function answerTextForDelivery(answer, artifacts) {
123
+ return typeof answer === 'string' && answer.trim()
124
+ ? answer
125
+ : (Array.isArray(artifacts) && artifacts.length > 0 ? t('结果文件已生成。') : answer);
126
+ }
127
+
128
+ // Inbound image source: WeCom either hands out a directly fetchable PicUrl or
129
+ // a media_id downloadable through the authenticated media API. Loads are cached
130
+ // so an eager prefetch survives the queue wait without a second download.
131
+ function imageSource(api, image) {
132
+ const url = nonEmptyString(image?.url);
133
+ const mediaId = nonEmptyString(image?.mediaId) ?? nonEmptyString(image?.media_id);
134
+ if (!url && !mediaId) return null;
135
+ let cached = null;
136
+ return {
137
+ async load({ signal, maxBytes } = {}) {
138
+ signal?.throwIfAborted();
139
+ if (cached) return cached;
140
+ let data;
141
+ let name;
142
+ if (url) {
143
+ const response = await fetch(url, { signal });
144
+ if (!response.ok) throw new Error(`企业微信图片下载失败(HTTP ${response.status})`);
145
+ const buffer = Buffer.from(await response.arrayBuffer());
146
+ if (Number.isFinite(maxBytes) && buffer.length > maxBytes) {
147
+ const error = new Error(`企业微信图片超过 ${maxBytes} 字节`);
148
+ error.code = 'image-too-large';
149
+ throw error;
150
+ }
151
+ data = buffer;
152
+ const declared = nonEmptyString(response.headers?.get?.('content-disposition'));
153
+ name = declared?.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/iu)?.[1] ?? 'image';
154
+ } else {
155
+ const result = await api.downloadMedia({ mediaId, signal });
156
+ data = result.data;
157
+ name = result.filename ?? 'image';
158
+ }
159
+ if (Number.isFinite(maxBytes) && data.length > maxBytes) {
160
+ const error = new Error(`企业微信图片超过 ${maxBytes} 字节`);
161
+ error.code = 'image-too-large';
162
+ throw error;
163
+ }
164
+ cached = { data, name };
165
+ return cached;
166
+ },
167
+ };
168
+ }
169
+
170
+ function wecomAppInboundMessage(message, api) {
171
+ return {
172
+ content: messageText(message),
173
+ images: hasImageItems(message)
174
+ ? [imageSource(api, { url: message.image?.url, mediaId: message.mediaId })].filter(Boolean)
175
+ : [],
176
+ files: [],
177
+ };
178
+ }
179
+
180
+ function canClaimInteractionReply(message, pending) {
181
+ return pending.questions[pending.index]
182
+ && nonEmptyString(message?.from?.userid) === pending.actor
183
+ && !hasImageItems(message)
184
+ && nonEmptyString(messageText(message));
185
+ }
186
+
187
+ export class WecomAppBridge {
188
+ #api;
189
+ #harness;
190
+ #state;
191
+ #status;
192
+ #contextEnhancement;
193
+ #accessPolicy;
194
+ #logger;
195
+ #replyTimeoutMs;
196
+ #signal;
197
+ #streamRegistry;
198
+ #maxChunkBytes;
199
+ #queues = new Map();
200
+ #acceptedMessageIds = new Map();
201
+ #commandTasks = new Set();
202
+ #approvalTasks = new Set();
203
+ #pendingInteractions = new Map();
204
+ #interactionKeys = new Map();
205
+ #approvals;
206
+ #batchInputs;
207
+ #deferred;
208
+ #closed = false;
209
+
210
+ constructor({
211
+ api,
212
+ harness,
213
+ state,
214
+ status,
215
+ contextEnhancement,
216
+ accessPolicy,
217
+ logger = console,
218
+ replyTimeoutMs = 600_000,
219
+ signal,
220
+ streamRegistry,
221
+ maxChunkBytes = WECOM_APP_TEXT_MAX_BYTES,
222
+ }) {
223
+ if (!api || typeof api.sendText !== 'function') throw new TypeError('WecomAppBridge requires a WecomAppApi');
224
+ if (!harness || !state) throw new TypeError('WecomAppBridge requires Harness and state');
225
+ this.#api = api;
226
+ this.#harness = harness;
227
+ this.#state = state;
228
+ this.#status = status;
229
+ this.#contextEnhancement = contextEnhancement;
230
+ this.#accessPolicy = accessPolicy;
231
+ this.#logger = logger;
232
+ this.#replyTimeoutMs = replyTimeoutMs;
233
+ this.#signal = signal;
234
+ this.#streamRegistry = streamRegistry;
235
+ this.#maxChunkBytes = maxChunkBytes;
236
+ this.#deferred = createDeferredDeliveryCoordinator({
237
+ harness, state, signal, logger,
238
+ deliver: (entry, outcome) => this.#deliverDeferredOutcome(entry, outcome),
239
+ });
240
+ this.#approvals = new HarnessApprovalQueue({ label: 'wecom-app', logger });
241
+ this.#batchInputs = new BatchInputManager();
242
+ }
243
+
244
+ get status() {
245
+ return structuredClone(this.#status);
246
+ }
247
+
248
+ async acceptEvent(message) {
249
+ if (this.#closed || this.#signal?.aborted) return;
250
+ const event = nonEmptyString(message?.event) ?? nonEmptyString(message?.eventtype);
251
+ if (event !== 'subscribe' && event !== 'enter_chat') return;
252
+ const sender = nonEmptyString(message?.from?.userid) ?? nonEmptyString(message?.FromUserName);
253
+ if (!sender) return;
254
+ try {
255
+ await this.#send(sender, WELCOME_TEXT());
256
+ } catch (error) {
257
+ this.#logger.warn?.('[dsh-im:wecom-app] failed to send the welcome message:', error);
258
+ }
259
+ }
260
+
261
+ accept(message, { sink } = {}) {
262
+ if (this.#closed || this.#signal?.aborted) return Promise.resolve();
263
+ const messageId = nonEmptyString(message?.msgid);
264
+ const sender = nonEmptyString(message?.from?.userid);
265
+ if (!messageId || !sender || !supportedInboundType(message)
266
+ || this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
267
+ return Promise.resolve();
268
+ }
269
+ const commandText = nonEmptyString(messageText(message)) ?? '';
270
+ const hasImages = hasImageItems(message);
271
+ const access = evaluateInboundAccess(this.#accessPolicy, {
272
+ conversationType: 'direct',
273
+ senderIds: sender,
274
+ text: commandText,
275
+ hasImages,
276
+ hasFiles: false,
277
+ });
278
+ if (!access.allowed) {
279
+ this.#acceptedMessageIds.set(messageId, null);
280
+ return this.#finishAccessDecision(messageId, sender, access);
281
+ }
282
+ this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
283
+ this.#contextEnhancement,
284
+ 'direct',
285
+ ));
286
+ rememberConnectionTestTarget(this.#state, { toUserId: sender });
287
+ const key = conversationKey(sender);
288
+ const pending = this.#pendingInteractions.get(key);
289
+ const batchCommand = isBatchInputCommand(commandText);
290
+ const batchStatus = this.#batchInputs.status(key);
291
+ if (batchCommand || batchStatus.phase === 'collecting') {
292
+ const exactBatchStart = /^\/batch$/iu.test(commandText);
293
+ const result = exactBatchStart
294
+ && batchStatus.phase === 'idle'
295
+ && (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
296
+ ? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
297
+ : this.#batchInputs.handle(key, commandText, { plainText: !hasImages });
298
+ if (result.handled) {
299
+ if (result.kind === 'submit') {
300
+ return this.#enqueueMessage({
301
+ ...message,
302
+ msgtype: 'text',
303
+ text: { content: result.prompt },
304
+ }, messageId, key, { batchSubmission: result, sink: null });
305
+ }
306
+ return this.#finishBatchResult(messageId, key, sender, result);
307
+ }
308
+ }
309
+ const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
310
+ : isControlCommand(commandText)
311
+ ? runControlCommand
312
+ : (isModelCommand(commandText)
313
+ ? runModelCommand
314
+ : (isPresetCommand(commandText) ? runPresetCommand : null));
315
+ if (commandRunner) {
316
+ let task;
317
+ task = this.#processFastCommand(message, messageId, key, sender, commandText, commandRunner)
318
+ .catch((error) => {
319
+ if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
320
+ this.#status.lastError = error?.message ?? String(error);
321
+ const failure = setLastMessageFailure(this.#status, error);
322
+ this.#logger.error?.(
323
+ `[dsh-im:wecom-app] failed to process a command [${failure.referenceId}]:`,
324
+ error,
325
+ );
326
+ return this.#send(sender, messageFailureText(failure)).catch(() => undefined);
327
+ })
328
+ .finally(() => {
329
+ this.#acceptedMessageIds.delete(messageId);
330
+ this.#commandTasks.delete(task);
331
+ });
332
+ this.#commandTasks.add(task);
333
+ return task;
334
+ }
335
+ const approval = this.#approvals.claimReply({
336
+ key,
337
+ actor: sender,
338
+ messageId,
339
+ text: hasImages ? '' : commandText,
340
+ addressed: true,
341
+ hasPendingQuestion: Boolean(pending),
342
+ questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
343
+ ? pending.queue
344
+ : null,
345
+ isQuestionPending: () => this.#pendingInteractions.has(key),
346
+ send: (text) => this.#send(sender, text),
347
+ });
348
+ if (approval) {
349
+ let task;
350
+ task = approval.process(async () => {
351
+ if (this.#state.hasSeen(messageId)) return false;
352
+ await this.#state.markSeen(messageId);
353
+ this.#status.messagesReceived += 1;
354
+ this.#status.lastMessageAt = new Date().toISOString();
355
+ return true;
356
+ }).finally(() => {
357
+ this.#acceptedMessageIds.delete(messageId);
358
+ this.#approvalTasks.delete(task);
359
+ });
360
+ this.#approvalTasks.add(task);
361
+ return task;
362
+ }
363
+ if (pending?.submitting || pending?.claimedReplyMessageId) {
364
+ return this.#enqueueMessage(message, messageId, key, { sink: null });
365
+ }
366
+ if (pending) {
367
+ if (canClaimInteractionReply(message, pending)) {
368
+ pending.claimedReplyMessageId = messageId;
369
+ }
370
+ const previous = pending.queue ?? Promise.resolve();
371
+ const current = previous
372
+ .catch(() => undefined)
373
+ .then(() => this.#processInteractionReply(message, messageId, key, pending))
374
+ .catch((error) => this.#handleInteractionFailure(message, messageId, error))
375
+ .finally(() => {
376
+ this.#acceptedMessageIds.delete(messageId);
377
+ if (pending.claimedReplyMessageId === messageId) pending.claimedReplyMessageId = null;
378
+ if (pending.queue === current) pending.queue = null;
379
+ });
380
+ pending.queue = current;
381
+ return current;
382
+ }
383
+ return this.#enqueueMessage(message, messageId, key, { sink });
384
+ }
385
+
386
+ #enqueueMessage(message, messageId, key, {
387
+ releaseMessageId = true,
388
+ alreadyRecorded = false,
389
+ batchSubmission = null,
390
+ sink = null,
391
+ } = {}) {
392
+ const preparedMessage = wecomAppInboundMessage(message, this.#api);
393
+ const previous = this.#queues.get(key) ?? Promise.resolve();
394
+ const current = previous
395
+ .catch(() => undefined)
396
+ .then(() => this.#process(message, key, {
397
+ alreadyRecorded,
398
+ preparedMessage,
399
+ batchSubmission,
400
+ sink,
401
+ }))
402
+ .finally(() => {
403
+ if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
404
+ if (this.#queues.get(key) === current) this.#queues.delete(key);
405
+ });
406
+ this.#queues.set(key, current);
407
+ return current;
408
+ }
409
+
410
+ #finishBatchResult(messageId, key, sender, result) {
411
+ let task;
412
+ task = Promise.resolve().then(async () => {
413
+ if (this.#state.hasSeen(messageId)) return;
414
+ await this.#state.markSeen(messageId);
415
+ this.#status.messagesReceived += 1;
416
+ this.#status.lastMessageAt = new Date().toISOString();
417
+ if (result.message) await this.#send(sender, result.message);
418
+ this.#status.lastError = null;
419
+ }).catch(async (error) => {
420
+ if (this.#signal?.aborted) return;
421
+ this.#status.lastError = error?.message ?? String(error);
422
+ const failure = setLastMessageFailure(this.#status, error);
423
+ this.#logger.error?.(
424
+ `[dsh-im:wecom-app] failed to process a batch input message [${failure.referenceId}]:`,
425
+ error,
426
+ );
427
+ await this.#send(sender, messageFailureText(failure)).catch(() => undefined);
428
+ }).finally(() => {
429
+ this.#acceptedMessageIds.delete(messageId);
430
+ this.#commandTasks.delete(task);
431
+ });
432
+ this.#commandTasks.add(task);
433
+ return task;
434
+ }
435
+
436
+ #finishAccessDecision(messageId, sender, access) {
437
+ let task;
438
+ task = Promise.resolve().then(async () => {
439
+ if (this.#state.hasSeen(messageId)) return;
440
+ await this.#state.markSeen(messageId);
441
+ if (access.reason === 'command-not-allowed') {
442
+ this.#status.messagesReceived += 1;
443
+ this.#status.lastMessageAt = new Date().toISOString();
444
+ await this.#send(sender, t(COMMAND_PERMISSION_DENIED_MESSAGE));
445
+ this.#status.messagesReplied += 1;
446
+ this.#status.lastReplyAt = new Date().toISOString();
447
+ } else {
448
+ this.#status.messagesRejected = (this.#status.messagesRejected ?? 0) + 1;
449
+ this.#status.lastRejectedAt = new Date().toISOString();
450
+ }
451
+ this.#status.lastError = null;
452
+ }).catch((error) => {
453
+ if (this.#signal?.aborted) return;
454
+ this.#status.lastError = error?.message ?? String(error);
455
+ this.#logger.error?.('[dsh-im:wecom-app] failed to apply inbound access policy:', error);
456
+ }).finally(() => {
457
+ this.#acceptedMessageIds.delete(messageId);
458
+ this.#commandTasks.delete(task);
459
+ });
460
+ this.#commandTasks.add(task);
461
+ return task;
462
+ }
463
+
464
+ async #deliverDeferredOutcome(entry, outcome) {
465
+ await this.#send(entry.target.toUserId, deferredOutcomeText(outcome));
466
+ return true;
467
+ }
468
+
469
+ async waitForIdle() {
470
+ await Promise.allSettled([
471
+ ...this.#queues.values(),
472
+ ...[...this.#pendingInteractions.values()].flatMap((pending) => (
473
+ pending.queue ? [pending.queue] : []
474
+ )),
475
+ ...this.#approvalTasks,
476
+ ...this.#commandTasks,
477
+ ]);
478
+ await this.#deferred.whenIdle();
479
+ }
480
+
481
+ async close() {
482
+ this.#closed = true;
483
+ this.#deferred.close();
484
+ }
485
+
486
+ async #processFastCommand(message, messageId, key, sender, text, runner) {
487
+ this.#signal?.throwIfAborted();
488
+ if (this.#state.hasSeen(messageId)) return;
489
+ await this.#state.markSeen(messageId);
490
+ this.#status.messagesReceived += 1;
491
+ this.#status.lastMessageAt = new Date().toISOString();
492
+ const result = await runner(text, this.#harness, this.#state, key, {
493
+ signal: this.#signal,
494
+ isDirect: true,
495
+ hasImages: hasImageItems(message),
496
+ hasFiles: false,
497
+ pendingInteraction: this.#pendingInteractions.has(key)
498
+ || this.#approvals.hasPending(key),
499
+ control: { owner: this, key },
500
+ deferredDelivery: this.#deferred,
501
+ });
502
+ if (result?.stopped) {
503
+ await Promise.allSettled([
504
+ this.#cancelPendingInteraction(key),
505
+ this.#approvals.closeRoute(key),
506
+ ]);
507
+ }
508
+ for (const reply of result?.messages ?? [result?.message]) {
509
+ if (!reply) continue;
510
+ await this.#send(sender, reply);
511
+ }
512
+ this.#status.lastError = null;
513
+ }
514
+
515
+ async #process(message, key, {
516
+ alreadyRecorded = false,
517
+ preparedMessage,
518
+ batchSubmission = null,
519
+ sink = null,
520
+ } = {}) {
521
+ this.#signal?.throwIfAborted();
522
+ const messageId = nonEmptyString(message?.msgid);
523
+ const sender = nonEmptyString(message?.from?.userid);
524
+ if (!messageId || !sender) return;
525
+ if (!alreadyRecorded) {
526
+ if (this.#state.hasSeen(messageId)) return;
527
+ this.#status.messagesReceived += 1;
528
+ this.#status.lastMessageAt = new Date().toISOString();
529
+ }
530
+ let batchSettled = batchSubmission === null;
531
+ let promptRecorded = false;
532
+ let streamActive = Boolean(sink && !sink.finished());
533
+ let streamedText = '';
534
+ try {
535
+ const promptMessage = preparedMessage ?? wecomAppInboundMessage(message, this.#api);
536
+ const text = promptMessage.content;
537
+ const hasImages = hasInboundImages(promptMessage);
538
+ const hasFiles = hasInboundFiles(promptMessage);
539
+ const hasReply = hasReplyReference(promptMessage);
540
+ if (!text && !hasImages && !hasFiles && !hasReply) {
541
+ await this.#send(sender, UNSUPPORTED_INBOUND_TEXT());
542
+ await this.#state.markSeen(messageId);
543
+ return;
544
+ }
545
+ const command = text.trim().toLowerCase();
546
+ if (!hasImages && !hasFiles && command === '/help') {
547
+ await this.#send(sender, HELP_TEXT());
548
+ await this.#state.markSeen(messageId);
549
+ return;
550
+ }
551
+ if (!hasImages && !hasFiles && command === '/status') {
552
+ await this.#harness.ensureRunning({ signal: this.#signal });
553
+ await this.#send(sender, t('企业微信应用与 DeepSeek Harness 连接正常。'));
554
+ await this.#state.markSeen(messageId);
555
+ return;
556
+ }
557
+ if (!hasImages && !hasFiles && command === '/new') {
558
+ await this.#state.clearSession(key);
559
+ await this.#send(sender, t('已开启新会话。请发送你的问题。'));
560
+ await this.#state.markSeen(messageId);
561
+ return;
562
+ }
563
+ const workspaceCommand = hasImages || hasFiles
564
+ ? null
565
+ : await runWorkspaceCommand(text, this.#harness, key);
566
+ if (workspaceCommand) {
567
+ for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
568
+ await this.#send(sender, reply);
569
+ }
570
+ await this.#state.markSeen(messageId);
571
+ return;
572
+ }
573
+ const compactCommand = hasImages || hasFiles
574
+ ? null
575
+ : await runCompactCommand(text, this.#harness, this.#state, key, { signal: this.#signal });
576
+ if (compactCommand) {
577
+ await this.#send(sender, compactCommand.message);
578
+ await this.#state.markSeen(messageId);
579
+ return;
580
+ }
581
+
582
+ let answer;
583
+ let artifacts = [];
584
+ try {
585
+ let content = hasImages || hasReply
586
+ ? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
587
+ : undefined;
588
+ const snapshot = this.#acceptedMessageIds.get(messageId);
589
+ let contextEnhanced = false;
590
+ if (snapshot) {
591
+ const originalContent = content ?? text;
592
+ content = enhanceContextContent(originalContent, snapshot, () => ({
593
+ channel: 'wecom-app',
594
+ senderId: sender,
595
+ chatId: sender,
596
+ }));
597
+ contextEnhanced = content !== originalContent;
598
+ }
599
+ await this.#state.markSeen(messageId);
600
+ promptRecorded = true;
601
+ ({ answer, artifacts = [] } = await askInWorkspaceSession({
602
+ deferredDelivery: () => ({ coordinator: this.#deferred, target: { toUserId: sender } }),
603
+ harness: this.#harness,
604
+ state: this.#state,
605
+ key,
606
+ text,
607
+ content,
608
+ contextEnhanced,
609
+ createOptions: { signal: this.#signal },
610
+ existsOptions: { signal: this.#signal },
611
+ askOptions: {
612
+ timeoutMs: this.#replyTimeoutMs,
613
+ signal: this.#signal,
614
+ control: { owner: this, key },
615
+ onUpdate: streamActive
616
+ ? async (update) => {
617
+ if (update?.type !== 'text') return;
618
+ streamedText = update.text;
619
+ this.#streamRegistry.appendStream(sink.streamId, streamedText, { replace: true });
620
+ }
621
+ : undefined,
622
+ onInteraction: (interaction) => this.#handleInteraction(interaction, {
623
+ key,
624
+ actor: sender,
625
+ }),
626
+ onInteractionResolved: async (resolution) => {
627
+ await this.#handleInteractionResolved(resolution);
628
+ },
629
+ files: promptMessage.files,
630
+ },
631
+ }));
632
+ if (batchSubmission) {
633
+ this.#batchInputs.complete(key, batchSubmission.token);
634
+ batchSettled = true;
635
+ }
636
+ } catch (error) {
637
+ if (streamActive && !sink.finished()) {
638
+ const diagnostic = imagePromptDiagnostic(error);
639
+ if (diagnostic?.userMessage) {
640
+ this.#streamRegistry.appendStream(sink.streamId, `\n${diagnostic.userMessage}`);
641
+ }
642
+ this.#streamRegistry.finishStream(sink.streamId);
643
+ }
644
+ throw error;
645
+ }
646
+ const displayAnswer = answerTextForDelivery(answer, artifacts);
647
+ if (streamActive && !sink.finished()) {
648
+ if (displayAnswer && displayAnswer !== streamedText) {
649
+ this.#streamRegistry.appendStream(sink.streamId, displayAnswer, { replace: true });
650
+ }
651
+ this.#streamRegistry.finishStream(sink.streamId);
652
+ }
653
+ const answerText = displayAnswer ?? t('结果文件已生成。');
654
+ let textDeliveryError = null;
655
+ let textReceipt = null;
656
+ try {
657
+ if (streamActive && sink.refreshes() > 0) {
658
+ textReceipt = createDeliveryReceipt({
659
+ deliveryId: messageId,
660
+ presentation: 'wecom-app-stream',
661
+ providerMessageIds: [sink.streamId],
662
+ });
663
+ } else {
664
+ // The client never consumed the stream (or streaming is disabled):
665
+ // fall back to explicit text messages so the answer still arrives.
666
+ textReceipt = createDeliveryReceipt({
667
+ deliveryId: messageId,
668
+ presentation: 'wecom-app-text',
669
+ providerMessageIds: await this.#send(sender, answerText),
670
+ });
671
+ }
672
+ } catch (error) {
673
+ textDeliveryError = channelDeliveryFailure(error);
674
+ }
675
+ const delivery = await this.#deliverArtifacts(sender, messageId, artifacts, textReceipt);
676
+ if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
677
+ if (textDeliveryError && delivery.artifactSendErrors === 0) {
678
+ setLastMessageFailure(this.#status, textDeliveryError);
679
+ }
680
+ if (!promptRecorded) await this.#state.markSeen(messageId);
681
+ this.#status.messagesReplied += 1;
682
+ this.#status.lastReplyAt = new Date().toISOString();
683
+ this.#status.lastError = null;
684
+ if (!textDeliveryError && delivery.artifactSendErrors === 0) {
685
+ clearLastMessageFailure(this.#status);
686
+ }
687
+ return delivery.receipt;
688
+ } catch (error) {
689
+ let batchFailureMessage = null;
690
+ if (!batchSettled && batchSubmission) {
691
+ if (error?.code === 'turn-stopped') {
692
+ this.#batchInputs.complete(key, batchSubmission.token);
693
+ } else {
694
+ batchFailureMessage = this.#batchInputs.fail(key, batchSubmission.token).message ?? null;
695
+ }
696
+ batchSettled = true;
697
+ }
698
+ if (error?.code === 'turn-stopped') {
699
+ if (!promptRecorded) await this.#state.markSeen(messageId);
700
+ return;
701
+ }
702
+ if (this.#signal?.aborted) return;
703
+ this.#status.lastError = error?.message ?? String(error);
704
+ const userMessage = inboundFileUserMessage(error) ?? imagePromptUserMessage(error);
705
+ const imageDiagnostic = imagePromptDiagnostic(error);
706
+ const failure = setLastMessageFailure(this.#status, error, {
707
+ userMessage: userMessage ?? undefined,
708
+ reason: imageDiagnostic?.reason ?? undefined,
709
+ });
710
+ this.#logger.error?.(
711
+ `[dsh-im:wecom-app] failed to process an inbound message [${failure.referenceId}]:`,
712
+ error,
713
+ );
714
+ try {
715
+ await this.#send(
716
+ sender,
717
+ batchFailureMessage
718
+ ? `${messageFailureText(failure)}\n\n${batchFailureMessage}`
719
+ : messageFailureText(failure),
720
+ );
721
+ if (!promptRecorded) await this.#state.markSeen(messageId);
722
+ } catch (sendError) {
723
+ this.#logger.error?.('[dsh-im:wecom-app] failed to send the safe error reply:', sendError);
724
+ }
725
+ }
726
+ }
727
+
728
+ async #send(sender, text) {
729
+ const target = nonEmptyString(sender);
730
+ const value = typeof text === 'string' ? text : '';
731
+ if (!target || !value) return [];
732
+ const providerMessageIds = [];
733
+ const chunks = splitUtf8ByBytes(value, this.#maxChunkBytes);
734
+ for (const chunk of chunks) {
735
+ if (!chunk) continue;
736
+ const result = await this.#api.sendText({ userId: target, content: chunk, signal: this.#signal });
737
+ providerMessageIds.push(...providerMessageIdsFor(result));
738
+ }
739
+ return providerMessageIds;
740
+ }
741
+
742
+ async #processInteractionReply(message, messageId, key, expected) {
743
+ this.#signal?.throwIfAborted();
744
+ const current = this.#pendingInteractions.get(key);
745
+ const claimed = expected.claimedReplyMessageId === messageId;
746
+ if (!current || current !== expected || current.submitting) {
747
+ if (claimed && (!current || current !== expected)) {
748
+ return this.#discardResolvedInteractionReply(message, messageId);
749
+ }
750
+ return this.#enqueueMessage(message, messageId, key, { releaseMessageId: false, sink: null });
751
+ }
752
+ if (this.#state.hasSeen(messageId)) return;
753
+ await this.#state.markSeen(messageId);
754
+ this.#status.messagesReceived += 1;
755
+ this.#status.lastMessageAt = new Date().toISOString();
756
+
757
+ const text = nonEmptyString(messageText(message));
758
+ if (!text || hasImageItems(message)) {
759
+ await this.#send(expected.actor, t('请用文字回答当前问题。'));
760
+ return;
761
+ }
762
+
763
+ const pending = this.#pendingInteractions.get(key);
764
+ if (!pending || pending !== expected || pending.submitting) {
765
+ if (claimed && (!pending || pending !== expected)) {
766
+ await this.#send(expected.actor, INTERACTION_RESOLVED_TEXT());
767
+ return;
768
+ }
769
+ return this.#enqueueMessage(message, messageId, key, {
770
+ releaseMessageId: false,
771
+ alreadyRecorded: true,
772
+ sink: null,
773
+ });
774
+ }
775
+ if (pending.needsPresentation) {
776
+ try {
777
+ await this.#presentInteraction(pending);
778
+ } catch {
779
+ this.#status.lastError = t('企业微信应用交互问题发送失败。');
780
+ this.#logger.error?.('[dsh-im:wecom-app] failed to retry an interaction question');
781
+ pending.interaction.reconnect?.();
782
+ return;
783
+ }
784
+ const presentedPending = this.#pendingInteractions.get(key);
785
+ if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
786
+ if (claimed && (!presentedPending || presentedPending !== expected)) {
787
+ await this.#send(expected.actor, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
788
+ return;
789
+ }
790
+ return this.#enqueueMessage(message, messageId, key, {
791
+ releaseMessageId: false,
792
+ alreadyRecorded: true,
793
+ sink: null,
794
+ });
795
+ }
796
+ }
797
+
798
+ const question = pending.questions[pending.index];
799
+ if (!question) return;
800
+ pending.answers.push(harnessAnswerForQuestion(question, text));
801
+ pending.index += 1;
802
+ if (pending.index < pending.questions.length) {
803
+ if (pending.claimedReplyMessageId === messageId) {
804
+ pending.claimedReplyMessageId = null;
805
+ }
806
+ pending.needsPresentation = true;
807
+ try {
808
+ await this.#presentInteraction(pending);
809
+ } catch {
810
+ this.#status.lastError = t('企业微信应用交互问题发送失败。');
811
+ this.#logger.error?.('[dsh-im:wecom-app] failed to send the next interaction question');
812
+ pending.interaction.reconnect?.();
813
+ }
814
+ return;
815
+ }
816
+
817
+ pending.submitting = true;
818
+ try {
819
+ await pending.interaction.respond({
820
+ ok: true,
821
+ value: {
822
+ sessionId: pending.sessionId,
823
+ answer: { answers: pending.answers },
824
+ },
825
+ });
826
+ this.#clearPendingInteraction(key, pending.interactionId);
827
+ this.#status.lastError = null;
828
+ } catch (error) {
829
+ if (this.#signal?.aborted) return;
830
+ if (error?.code === 'interaction-not-pending') {
831
+ this.#clearPendingInteraction(key, pending.interactionId);
832
+ await this.#send(pending.actor, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
833
+ return;
834
+ }
835
+ if (this.#pendingInteractions.get(key) !== pending) return;
836
+ pending.submitting = false;
837
+ pending.answers.pop();
838
+ pending.index -= 1;
839
+ this.#status.lastError = t('回答提交失败。');
840
+ this.#logger.error?.('[dsh-im:wecom-app] failed to answer a Harness interaction');
841
+ await this.#send(pending.actor, t('回答提交失败,请重新发送当前问题的答案。')).catch(() => undefined);
842
+ }
843
+ }
844
+
845
+ async #handleInteraction(interaction, { key, actor }) {
846
+ if (interaction?.kind === 'approval') {
847
+ return this.#approvals.handleRequested(interaction, {
848
+ key,
849
+ actor,
850
+ send: (text) => this.#send(actor, text),
851
+ });
852
+ }
853
+ if (interaction?.kind !== 'question') return;
854
+ const questions = interaction?.payload?.questions;
855
+ const interactionId = typeof interaction?.interactionId === 'string'
856
+ ? interaction.interactionId
857
+ : interaction?.rpcId;
858
+ if (typeof interaction?.rpcId !== 'string'
859
+ || typeof interactionId !== 'string'
860
+ || typeof interaction.sessionId !== 'string'
861
+ || !Array.isArray(questions)
862
+ || questions.length === 0
863
+ || questions.some((question) => !validHarnessQuestion(question))) {
864
+ this.#logger.warn?.('[dsh-im:wecom-app] ignored an invalid Harness question interaction');
865
+ return;
866
+ }
867
+
868
+ if (interaction.recovered === true) {
869
+ await interaction.respond({
870
+ ok: false,
871
+ error: {
872
+ code: 'cancelled',
873
+ message: 'Wecom-app safely cancelled an interaction left by an earlier client.',
874
+ details: {},
875
+ },
876
+ });
877
+ await this.#send(actor, t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。')).catch(() => undefined);
878
+ return;
879
+ }
880
+
881
+ const existing = this.#pendingInteractions.get(key);
882
+ if (existing?.interactionId === interactionId) {
883
+ existing.interaction = interaction;
884
+ if (existing.needsPresentation) await this.#presentInteraction(existing);
885
+ return;
886
+ }
887
+ if (this.#interactionKeys.has(interactionId)) return;
888
+ if (existing) {
889
+ await interaction.respond({
890
+ ok: false,
891
+ error: {
892
+ code: 'cancelled',
893
+ message: 'Wecom-app is already handling another user interaction.',
894
+ details: {},
895
+ },
896
+ });
897
+ return;
898
+ }
899
+
900
+ const pending = {
901
+ kind: 'question',
902
+ interactionId,
903
+ sessionId: interaction.sessionId,
904
+ interaction,
905
+ actor,
906
+ questions,
907
+ answers: [],
908
+ index: 0,
909
+ queue: null,
910
+ claimedReplyMessageId: null,
911
+ presentationPromise: null,
912
+ submitting: false,
913
+ needsPresentation: true,
914
+ };
915
+ this.#pendingInteractions.set(key, pending);
916
+ this.#interactionKeys.set(interactionId, key);
917
+ await this.#presentInteraction(pending);
918
+ }
919
+
920
+ async #handleInteractionResolved(resolution) {
921
+ if (resolution?.kind === 'approval') {
922
+ await this.#approvals.handleResolved(resolution);
923
+ return;
924
+ }
925
+ const interactionId = resolution?.interactionId;
926
+ if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
927
+ const key = this.#interactionKeys.get(interactionId);
928
+ if (!key) return;
929
+ this.#clearPendingInteraction(key, interactionId);
930
+ }
931
+
932
+ #presentInteraction(pending) {
933
+ if (!pending.needsPresentation) return Promise.resolve();
934
+ if (pending.presentationPromise) return pending.presentationPromise;
935
+ const question = pending.questions[pending.index];
936
+ if (!question) return Promise.resolve();
937
+ const presentation = this.#send(
938
+ pending.actor,
939
+ harnessQuestionText(question, pending.index, pending.questions.length),
940
+ ).then(() => {
941
+ pending.needsPresentation = false;
942
+ }).finally(() => {
943
+ if (pending.presentationPromise === presentation) pending.presentationPromise = null;
944
+ });
945
+ pending.presentationPromise = presentation;
946
+ return presentation;
947
+ }
948
+
949
+ async #discardResolvedInteractionReply(message, messageId) {
950
+ if (this.#state.hasSeen(messageId)) return;
951
+ await this.#state.markSeen(messageId);
952
+ this.#status.messagesReceived += 1;
953
+ this.#status.lastMessageAt = new Date().toISOString();
954
+ await this.#send(
955
+ nonEmptyString(message?.from?.userid),
956
+ INTERACTION_RESOLVED_TEXT(),
957
+ ).catch(() => undefined);
958
+ }
959
+
960
+ #takePendingInteraction(key, interactionId) {
961
+ const pending = this.#pendingInteractions.get(key);
962
+ if (!pending
963
+ || (interactionId !== undefined && pending.interactionId !== interactionId)) return null;
964
+ this.#pendingInteractions.delete(key);
965
+ this.#interactionKeys.delete(pending.interactionId);
966
+ return pending;
967
+ }
968
+
969
+ #clearPendingInteraction(key, interactionId) {
970
+ return this.#takePendingInteraction(key, interactionId) !== null;
971
+ }
972
+
973
+ async #cancelPendingInteraction(key) {
974
+ const pending = this.#takePendingInteraction(key);
975
+ if (!pending || pending.kind !== 'question') return;
976
+ try {
977
+ await pending.interaction.respond({
978
+ ok: false,
979
+ error: {
980
+ code: 'cancelled',
981
+ message: 'The Enterprise WeChat app interaction ended before the user answered.',
982
+ details: {},
983
+ },
984
+ }, { signal: AbortSignal.timeout(5_000) });
985
+ } catch (error) {
986
+ if (error?.code !== 'interaction-not-pending') {
987
+ this.#logger.warn?.('[dsh-im:wecom-app] failed to cancel a pending Harness interaction');
988
+ }
989
+ }
990
+ }
991
+
992
+ async #handleInteractionFailure(message, messageId, error) {
993
+ if (this.#signal?.aborted) return;
994
+ this.#status.lastError = error?.message ?? String(error);
995
+ const failure = setLastMessageFailure(this.#status, error);
996
+ this.#logger.error?.(
997
+ `[dsh-im:wecom-app] failed to process an interaction reply [${failure.referenceId}]:`,
998
+ error,
999
+ );
1000
+ if (!this.#state.hasSeen(messageId)) {
1001
+ await this.#state.markSeen(messageId).catch(() => undefined);
1002
+ }
1003
+ await this.#send(
1004
+ nonEmptyString(message?.from?.userid),
1005
+ messageFailureText(failure),
1006
+ ).catch(() => undefined);
1007
+ }
1008
+
1009
+ async #deliverArtifacts(sender, replyTo, artifacts, baseReceipt) {
1010
+ const sendArtifact = (kind) => (file) => this.#api.sendArtifactFile({
1011
+ userId: sender,
1012
+ kind,
1013
+ bytes: file.bytes,
1014
+ filename: file.fileName,
1015
+ signal: this.#signal,
1016
+ });
1017
+ const delivery = await deliverOutboundArtifacts({
1018
+ artifacts,
1019
+ baseReceipt,
1020
+ deliveryId: replyTo,
1021
+ aggregatePresentation: baseReceipt ? 'wecom-app-text-and-files' : 'wecom-app-files',
1022
+ channelKey: 'wecom-app',
1023
+ signal: this.#signal,
1024
+ sendImage: (file) => sendArtifact('image')(file),
1025
+ sendFile: (file) => sendArtifact('file')(file),
1026
+ onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
1027
+ userMessage: artifactFailureText(artifact?.fileName, error),
1028
+ reason: error?.code,
1029
+ }),
1030
+ sendFailureNotice: (_artifact, _error, failure) => this.#send(sender, messageFailureText(failure)),
1031
+ logger: this.#logger,
1032
+ });
1033
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + delivery.artifactsSent;
1034
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + delivery.artifactSendErrors;
1035
+ return {
1036
+ receipt: delivery.receipt,
1037
+ userVisible: delivery.userVisible,
1038
+ artifactSendErrors: delivery.artifactSendErrors,
1039
+ };
1040
+ }
1041
+ }
1042
+
1043
+ function artifactFailureText(fileName, error) {
1044
+ const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
1045
+ switch (error?.code) {
1046
+ case 'artifact-delivery-uncertain':
1047
+ return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
1048
+ case 'artifact-permission-required':
1049
+ return t('结果文件「{name}」已生成,但企业微信应用当前没有文件消息发送权限,请检查应用的文件发送能力。', { name });
1050
+ case 'artifact-too-large':
1051
+ return t('结果文件「{name}」超过企业微信可发送的文件大小,未发送。', { name });
1052
+ case 'artifact-rate-limited':
1053
+ return t('结果文件「{name}」暂时被企业微信限流,未能发送,请稍后重试。', { name });
1054
+ case 'artifact-provider-rejected':
1055
+ return t('结果文件「{name}」已生成,但企业微信拒绝了该文件消息。', { name });
1056
+ default:
1057
+ return t('结果文件「{name}」发送失败,请稍后重试。', { name });
1058
+ }
1059
+ }