@xmanrui/dsh-im 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +18 -6
- package/README.md +18 -6
- package/lib/index.js +206 -176
- package/package.json +1 -1
- package/plugin-src/host/index.mjs +2 -0
- package/src/channels/dingtalk/config-store.mjs +5 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +105 -82
- package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +3 -1
- package/src/channels/discord/config-store.mjs +2 -1
- package/src/channels/discord/discord-api.mjs +25 -1
- package/src/channels/discord/discord-runtime.mjs +237 -11
- package/src/channels/feishu/bridge.mjs +169 -180
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +57 -21
- package/src/channels/feishu/feishu-runtime.mjs +10 -9
- package/src/channels/feishu/message-utils.mjs +4 -3
- package/src/channels/office/office-job-executor.mjs +15 -14
- package/src/channels/office/office-runtime.mjs +6 -5
- package/src/channels/qq/config-store.mjs +3 -1
- package/src/channels/qq/markdown-reply.mjs +4 -2
- package/src/channels/qq/qq-bridge.mjs +149 -130
- package/src/channels/qq/qq-controller.mjs +17 -16
- package/src/channels/qq/qq-runtime.mjs +3 -2
- package/src/channels/shared/agent-preset.mjs +3 -1
- package/src/channels/shared/compact-command.mjs +15 -14
- package/src/channels/shared/connection-test.mjs +6 -4
- package/src/channels/shared/control-command.mjs +13 -11
- package/src/channels/shared/editable-message-stream.mjs +4 -2
- package/src/channels/shared/harness-approval.mjs +23 -21
- package/src/channels/shared/harness-client.mjs +3 -2
- package/src/channels/shared/harness-question.mjs +8 -6
- package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
- package/src/channels/shared/i18n-en/discord.mjs +8 -0
- package/src/channels/shared/i18n-en/feishu.mjs +217 -0
- package/src/channels/shared/i18n-en/office.mjs +23 -0
- package/src/channels/shared/i18n-en/qq.mjs +54 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
- package/src/channels/shared/i18n-en/slack.mjs +27 -0
- package/src/channels/shared/i18n-en/telegram.mjs +21 -0
- package/src/channels/shared/i18n-en/wecom.mjs +38 -0
- package/src/channels/shared/i18n-en/weixin.mjs +80 -0
- package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
- package/src/channels/shared/i18n-en.mjs +35 -0
- package/src/channels/shared/i18n.mjs +35 -0
- package/src/channels/shared/image-prompt.mjs +17 -15
- package/src/channels/shared/inbound-file.mjs +5 -3
- package/src/channels/shared/model-command.mjs +35 -31
- package/src/channels/shared/preset-command.mjs +41 -38
- package/src/channels/shared/semantic/artifact-delivery.mjs +170 -0
- package/src/channels/shared/semantic/artifact.mjs +2 -2
- package/src/channels/shared/semantic/delivery.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +154 -136
- package/src/channels/shared/token-bot-controller.mjs +32 -11
- package/src/channels/shared/workspace-command.mjs +69 -58
- package/src/channels/slack/config-store.mjs +3 -1
- package/src/channels/slack/slack-api.mjs +6 -5
- package/src/channels/slack/slack-controller.mjs +10 -9
- package/src/channels/slack/slack-runtime.mjs +16 -4
- package/src/channels/telegram/config-store.mjs +2 -1
- package/src/channels/telegram/telegram-api.mjs +111 -19
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +383 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +154 -153
- package/src/channels/wecom/wecom-controller.mjs +20 -19
- package/src/channels/wecom/wecom-runtime.mjs +2 -1
- package/src/channels/weixin/config-store.mjs +2 -1
- package/src/channels/weixin/weixin-api.mjs +140 -107
- package/src/channels/weixin/weixin-bridge.mjs +86 -123
- package/src/channels/weixin/weixin-controller.mjs +22 -21
- package/src/channels/weixin/weixin-runtime.mjs +3 -2
- package/src/channels/whatsapp/config-store.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
- package/src/channels/whatsapp/whatsapp-runtime.mjs +47 -13
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
|
@@ -31,45 +31,43 @@ import {
|
|
|
31
31
|
inboundFileUserMessage,
|
|
32
32
|
} from '../shared/inbound-file.mjs';
|
|
33
33
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
34
|
+
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
|
35
|
+
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
34
36
|
import {
|
|
35
|
-
materializeOutboundArtifact,
|
|
36
|
-
releaseOutboundArtifact,
|
|
37
|
-
trackOutboundArtifactProviderPromise,
|
|
38
|
-
} from '../shared/semantic/artifact.mjs';
|
|
39
|
-
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
37
|
createDeliveryReceipt,
|
|
42
|
-
mergeDeliveryReceipts,
|
|
43
38
|
} from '../shared/semantic/delivery.mjs';
|
|
39
|
+
import { t } from '../shared/i18n.mjs';
|
|
44
40
|
|
|
45
41
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
// Built lazily: t() must run after setImHostLanguage, not at import time.
|
|
44
|
+
function helpText() {
|
|
45
|
+
return [
|
|
46
|
+
t('企业微信机器人已连接 DeepSeek Harness。'),
|
|
47
|
+
'',
|
|
48
|
+
t('直接发送文字、图片或文件即可继续当前会话。'),
|
|
49
|
+
t('/new 开启一个全新会话'),
|
|
50
|
+
t('/compact 压缩当前会话的较早上下文'),
|
|
51
|
+
t('/workspace 工作区绝对路径 切换工作区'),
|
|
52
|
+
t('/workspacelist 列出工作区绝对路径'),
|
|
53
|
+
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
54
|
+
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
55
|
+
t('/models 按序号列出所有可用模型'),
|
|
56
|
+
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
|
|
57
|
+
t('示例:先发 /models,再发 /model 2'),
|
|
58
|
+
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
59
|
+
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
60
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
61
|
+
t('/preset --default 跟随 Host 默认'),
|
|
62
|
+
t('/stop 停止当前任务'),
|
|
63
|
+
t('/steer 补充指令 纠偏当前任务'),
|
|
64
|
+
t('/status 检查连接状态'),
|
|
65
|
+
t('/help 显示本帮助'),
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
69
68
|
const MAX_REPLY_BYTES = 18_000;
|
|
70
69
|
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
71
70
|
const MAX_PREFETCHED_IMAGES = 4;
|
|
72
|
-
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
73
71
|
|
|
74
72
|
function nonEmptyString(value) {
|
|
75
73
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -142,7 +140,7 @@ function imageSource(client, image) {
|
|
|
142
140
|
throw new ImagePromptError(
|
|
143
141
|
'image-too-large',
|
|
144
142
|
`Enterprise WeChat image exceeds ${maxBytes} bytes`,
|
|
145
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
143
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
146
144
|
);
|
|
147
145
|
}
|
|
148
146
|
return { data, name: result?.filename };
|
|
@@ -224,7 +222,7 @@ function prefetchInboundImages(message, signal) {
|
|
|
224
222
|
throw new ImagePromptError(
|
|
225
223
|
'image-too-large',
|
|
226
224
|
`Enterprise WeChat image exceeds ${maxBytes} bytes`,
|
|
227
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
225
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
228
226
|
);
|
|
229
227
|
}
|
|
230
228
|
return result;
|
|
@@ -243,7 +241,7 @@ function imageQueueFullMessage(message) {
|
|
|
243
241
|
throw new ImagePromptError(
|
|
244
242
|
'image-queue-full',
|
|
245
243
|
`Enterprise WeChat already has ${MAX_PREFETCHED_IMAGES} prefetched images`,
|
|
246
|
-
'当前待处理图片较多,请稍后重新发送。',
|
|
244
|
+
t('当前待处理图片较多,请稍后重新发送。'),
|
|
247
245
|
);
|
|
248
246
|
},
|
|
249
247
|
})),
|
|
@@ -277,31 +275,32 @@ function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
|
|
|
277
275
|
|
|
278
276
|
function progressText(update) {
|
|
279
277
|
if (update?.type === 'text') return update.text;
|
|
280
|
-
if (update?.type === 'tool') return
|
|
278
|
+
if (update?.type === 'tool') return t('正在使用{name}…', { name: update.name });
|
|
281
279
|
return update?.text;
|
|
282
280
|
}
|
|
283
281
|
|
|
284
282
|
function artifactFailureText(fileName, error) {
|
|
285
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim()
|
|
283
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim()
|
|
284
|
+
|| t('结果文件');
|
|
286
285
|
switch (error?.code) {
|
|
287
286
|
case 'artifact-delivery-uncertain':
|
|
288
|
-
return
|
|
287
|
+
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
289
288
|
case 'artifact-permission-required':
|
|
290
|
-
return
|
|
289
|
+
return t('结果文件「{name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。', { name });
|
|
291
290
|
case 'artifact-too-large':
|
|
292
|
-
return
|
|
291
|
+
return t('结果文件「{name}」超过当前企业微信机器人可发送的文件大小,未发送。', { name });
|
|
293
292
|
case 'artifact-empty':
|
|
294
|
-
return
|
|
293
|
+
return t('结果文件「{name}」为空,企业微信不允许发送空文件。', { name });
|
|
295
294
|
case 'artifact-changed':
|
|
296
295
|
case 'artifact-invalid':
|
|
297
296
|
case 'artifact-unavailable':
|
|
298
|
-
return
|
|
297
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
299
298
|
case 'artifact-rate-limited':
|
|
300
|
-
return
|
|
299
|
+
return t('结果文件「{name}」暂时被企业微信限流,未能发送,请稍后重试。', { name });
|
|
301
300
|
case 'artifact-provider-rejected':
|
|
302
|
-
return
|
|
301
|
+
return t('结果文件「{name}」已生成,但企业微信拒绝了该文件或文件消息。', { name });
|
|
303
302
|
default:
|
|
304
|
-
return
|
|
303
|
+
return t('结果文件「{name}」已生成,但暂时未能通过企业微信发送,请稍后重试。', { name });
|
|
305
304
|
}
|
|
306
305
|
}
|
|
307
306
|
|
|
@@ -353,9 +352,74 @@ function wecomArtifactError(error, { dispatched = false } = {}) {
|
|
|
353
352
|
return wrapped;
|
|
354
353
|
}
|
|
355
354
|
|
|
355
|
+
async function sendWecomMedia(
|
|
356
|
+
client,
|
|
357
|
+
chatId,
|
|
358
|
+
file,
|
|
359
|
+
mediaType,
|
|
360
|
+
{ signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
|
|
361
|
+
) {
|
|
362
|
+
signal?.throwIfAborted();
|
|
363
|
+
if (typeof client?.uploadMedia !== 'function'
|
|
364
|
+
|| typeof client?.sendMediaMessage !== 'function') {
|
|
365
|
+
const unavailable = new Error(`Enterprise WeChat ${mediaType} delivery is unavailable`);
|
|
366
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
367
|
+
throw unavailable;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
371
|
+
const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
372
|
+
let uploaded;
|
|
373
|
+
try {
|
|
374
|
+
const pending = client.uploadMedia(file.bytes, {
|
|
375
|
+
type: mediaType,
|
|
376
|
+
filename: file.fileName,
|
|
377
|
+
});
|
|
378
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
379
|
+
uploaded = await waitWithSignal(pending, waitSignal);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
382
|
+
throw wecomArtifactError(error);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
signal?.throwIfAborted();
|
|
386
|
+
const mediaId = nonEmptyString(uploaded?.media_id);
|
|
387
|
+
if (!mediaId) {
|
|
388
|
+
const rejected = new Error(`Enterprise WeChat ${mediaType} upload returned no media id`);
|
|
389
|
+
rejected.code = 'artifact-provider-rejected';
|
|
390
|
+
throw rejected;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let sent;
|
|
394
|
+
try {
|
|
395
|
+
const pending = client.sendMediaMessage(chatId, mediaType, mediaId);
|
|
396
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
397
|
+
sent = await waitWithSignal(pending, waitSignal);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
400
|
+
throw wecomArtifactError(error, { dispatched: true });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
signal?.throwIfAborted();
|
|
404
|
+
const providerCode = Number(sent?.body?.errcode ?? sent?.errcode);
|
|
405
|
+
if (Number.isFinite(providerCode) && providerCode !== 0) {
|
|
406
|
+
throw wecomArtifactError({ providerCode });
|
|
407
|
+
}
|
|
408
|
+
return sent;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Send one materialized artifact through Enterprise WeChat's native image message. */
|
|
412
|
+
export function sendWecomImage(client, chatId, file, options) {
|
|
413
|
+
return sendWecomMedia(client, chatId, file, 'image', options);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function sendWecomFile(client, chatId, file, options) {
|
|
417
|
+
return sendWecomMedia(client, chatId, file, 'file', options);
|
|
418
|
+
}
|
|
419
|
+
|
|
356
420
|
function answerTextForDelivery(answer, artifacts) {
|
|
357
421
|
if (typeof answer === 'string' && answer.trim()) return answer;
|
|
358
|
-
return artifacts.length > 0 ? '结果文件已生成。' : '任务已完成,但没有生成可显示的文本。';
|
|
422
|
+
return artifacts.length > 0 ? t('结果文件已生成。') : t('任务已完成,但没有生成可显示的文本。');
|
|
359
423
|
}
|
|
360
424
|
|
|
361
425
|
function providerMessageId(result) {
|
|
@@ -475,7 +539,7 @@ export class WecomHarnessBridge {
|
|
|
475
539
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
476
540
|
this.#status.lastError = error?.message ?? String(error);
|
|
477
541
|
this.#logger.error?.('[dsh-im:wecom] failed to process a command');
|
|
478
|
-
return this.#sendImmediate(frame, chatId, '消息处理失败,请稍后重试。')
|
|
542
|
+
return this.#sendImmediate(frame, chatId, t('消息处理失败,请稍后重试。'))
|
|
479
543
|
.catch(() => undefined);
|
|
480
544
|
}).finally(() => {
|
|
481
545
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -650,98 +714,35 @@ export class WecomHarnessBridge {
|
|
|
650
714
|
if (artifacts.length === 0) {
|
|
651
715
|
return { receipt: baseReceipt, failureNoticeVisible: false };
|
|
652
716
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const file = await materializeOutboundArtifact(artifact, {
|
|
665
|
-
signal: this.#signal,
|
|
666
|
-
});
|
|
667
|
-
this.#signal?.throwIfAborted();
|
|
668
|
-
const timeout = AbortSignal.timeout(this.#fileUploadTimeoutMs);
|
|
669
|
-
const waitSignal = this.#signal ? AbortSignal.any([this.#signal, timeout]) : timeout;
|
|
670
|
-
let uploaded;
|
|
671
|
-
try {
|
|
672
|
-
const pending = this.#client.uploadMedia(file.bytes, {
|
|
673
|
-
type: 'file',
|
|
674
|
-
filename: file.fileName,
|
|
675
|
-
});
|
|
676
|
-
trackOutboundArtifactProviderPromise(file, pending);
|
|
677
|
-
uploaded = await waitWithSignal(pending, waitSignal);
|
|
678
|
-
} catch (error) {
|
|
679
|
-
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
680
|
-
throw wecomArtifactError(error);
|
|
681
|
-
}
|
|
682
|
-
this.#signal?.throwIfAborted();
|
|
683
|
-
const mediaId = nonEmptyString(uploaded?.media_id);
|
|
684
|
-
if (!mediaId) {
|
|
685
|
-
const rejected = new Error('Enterprise WeChat upload returned no media id');
|
|
686
|
-
rejected.code = 'artifact-provider-rejected';
|
|
687
|
-
throw rejected;
|
|
688
|
-
}
|
|
689
|
-
let sent;
|
|
690
|
-
try {
|
|
691
|
-
const pending = this.#client.sendMediaMessage(chatId, 'file', mediaId);
|
|
692
|
-
trackOutboundArtifactProviderPromise(file, pending);
|
|
693
|
-
sent = await waitWithSignal(pending, waitSignal);
|
|
694
|
-
} catch (error) {
|
|
695
|
-
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
696
|
-
throw wecomArtifactError(error, { dispatched: true });
|
|
697
|
-
}
|
|
698
|
-
this.#signal?.throwIfAborted();
|
|
699
|
-
const providerCode = Number(sent?.body?.errcode ?? sent?.errcode);
|
|
700
|
-
if (Number.isFinite(providerCode) && providerCode !== 0) {
|
|
701
|
-
throw wecomArtifactError({ providerCode });
|
|
702
|
-
}
|
|
703
|
-
const messageId = providerMessageId(sent);
|
|
704
|
-
receipts.push(createDeliveryReceipt({
|
|
705
|
-
deliveryId: file.deliveryKey,
|
|
706
|
-
presentation: 'wecom-file',
|
|
707
|
-
providerMessageIds: messageId ? [messageId] : [],
|
|
708
|
-
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
709
|
-
}));
|
|
710
|
-
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
|
|
711
|
-
} catch (error) {
|
|
712
|
-
if (this.#signal?.aborted) throw error;
|
|
713
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
714
|
-
this.#logger.warn?.(
|
|
715
|
-
`[dsh-im:wecom] result file delivery failed (${error?.code ?? 'unknown'})`,
|
|
716
|
-
);
|
|
717
|
-
let providerMessageIds = [];
|
|
718
|
-
try {
|
|
719
|
-
providerMessageIds = await this.#sendActive(
|
|
720
|
-
chatId,
|
|
721
|
-
artifactFailureText(artifact?.fileName, error),
|
|
722
|
-
);
|
|
723
|
-
failureNoticeVisible = true;
|
|
724
|
-
} catch (noticeError) {
|
|
725
|
-
if (this.#signal?.aborted) throw noticeError;
|
|
726
|
-
this.#logger.warn?.('[dsh-im:wecom] unable to send the safe result-file failure notice');
|
|
727
|
-
}
|
|
728
|
-
receipts.push(createArtifactFailureReceipt({
|
|
729
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
730
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
731
|
-
error,
|
|
732
|
-
providerMessageIds,
|
|
733
|
-
}));
|
|
734
|
-
} finally {
|
|
735
|
-
releaseOutboundArtifact(artifact);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return {
|
|
739
|
-
receipt: mergeDeliveryReceipts({
|
|
740
|
-
deliveryId: replyTo,
|
|
741
|
-
presentation: baseReceipt ? 'wecom-text-and-files' : 'wecom-files',
|
|
742
|
-
receipts,
|
|
717
|
+
const delivery = await deliverOutboundArtifacts({
|
|
718
|
+
artifacts,
|
|
719
|
+
baseReceipt,
|
|
720
|
+
deliveryId: replyTo,
|
|
721
|
+
aggregatePresentation: baseReceipt ? 'wecom-text-and-files' : 'wecom-files',
|
|
722
|
+
alwaysMerge: true,
|
|
723
|
+
channelKey: 'wecom',
|
|
724
|
+
signal: this.#signal,
|
|
725
|
+
sendImage: (file) => sendWecomImage(this.#client, chatId, file, {
|
|
726
|
+
signal: this.#signal,
|
|
727
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
743
728
|
}),
|
|
744
|
-
|
|
729
|
+
sendFile: (file) => sendWecomFile(this.#client, chatId, file, {
|
|
730
|
+
signal: this.#signal,
|
|
731
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
732
|
+
}),
|
|
733
|
+
sendFailureNotice: (artifact, error) => this.#sendActive(
|
|
734
|
+
chatId,
|
|
735
|
+
artifactFailureText(artifact?.fileName, error),
|
|
736
|
+
),
|
|
737
|
+
logger: this.#logger,
|
|
738
|
+
});
|
|
739
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
740
|
+
+ delivery.artifactsSent;
|
|
741
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
742
|
+
+ delivery.artifactSendErrors;
|
|
743
|
+
return {
|
|
744
|
+
receipt: delivery.receipt,
|
|
745
|
+
failureNoticeVisible: delivery.failureNoticeVisible,
|
|
745
746
|
};
|
|
746
747
|
}
|
|
747
748
|
|
|
@@ -766,25 +767,25 @@ export class WecomHarnessBridge {
|
|
|
766
767
|
let streamStarted = false;
|
|
767
768
|
try {
|
|
768
769
|
if (!text && !hasImages && !hasFiles) {
|
|
769
|
-
await this.#sendImmediate(frame, chatId, '目前支持文字、图片、文件和语音转写消息。');
|
|
770
|
+
await this.#sendImmediate(frame, chatId, t('目前支持文字、图片、文件和语音转写消息。'));
|
|
770
771
|
await this.#state.markSeen(messageId);
|
|
771
772
|
return;
|
|
772
773
|
}
|
|
773
774
|
const command = text.toLowerCase();
|
|
774
775
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
775
|
-
await this.#sendImmediate(frame, chatId,
|
|
776
|
+
await this.#sendImmediate(frame, chatId, helpText());
|
|
776
777
|
await this.#state.markSeen(messageId);
|
|
777
778
|
return;
|
|
778
779
|
}
|
|
779
780
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
780
781
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
781
|
-
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
|
782
|
+
await this.#sendImmediate(frame, chatId, t('企业微信机器人与 DeepSeek Harness 连接正常。'));
|
|
782
783
|
await this.#state.markSeen(messageId);
|
|
783
784
|
return;
|
|
784
785
|
}
|
|
785
786
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
786
787
|
await this.#state.clearSession(key);
|
|
787
|
-
await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
|
|
788
|
+
await this.#sendImmediate(frame, chatId, t('已开启新会话。请发送你的问题。'));
|
|
788
789
|
await this.#state.markSeen(messageId);
|
|
789
790
|
return;
|
|
790
791
|
}
|
|
@@ -815,7 +816,7 @@ export class WecomHarnessBridge {
|
|
|
815
816
|
|
|
816
817
|
streamId = this.#generateReqId('stream');
|
|
817
818
|
try {
|
|
818
|
-
await this.#client.replyStream(frame, streamId, '正在思考中…', false);
|
|
819
|
+
await this.#client.replyStream(frame, streamId, t('正在思考中…'), false);
|
|
819
820
|
streamStarted = true;
|
|
820
821
|
} catch (error) {
|
|
821
822
|
this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
|
|
@@ -914,7 +915,7 @@ export class WecomHarnessBridge {
|
|
|
914
915
|
} catch (error) {
|
|
915
916
|
if (error?.code === 'turn-stopped') {
|
|
916
917
|
if (streamStarted && streamId) {
|
|
917
|
-
await this.#client.replyStream(frame, streamId, '已停止。', true)
|
|
918
|
+
await this.#client.replyStream(frame, streamId, t('已停止。'), true)
|
|
918
919
|
.catch(() => undefined);
|
|
919
920
|
}
|
|
920
921
|
await this.#state.markSeen(messageId);
|
|
@@ -925,7 +926,7 @@ export class WecomHarnessBridge {
|
|
|
925
926
|
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
926
927
|
const errorText = inboundFileUserMessage(error)
|
|
927
928
|
?? imagePromptUserMessage(error)
|
|
928
|
-
?? '消息处理失败,请稍后重试。';
|
|
929
|
+
?? t('消息处理失败,请稍后重试。');
|
|
929
930
|
try {
|
|
930
931
|
if (streamStarted && streamId) {
|
|
931
932
|
await this.#client.replyStream(frame, streamId, errorText, true);
|
|
@@ -961,7 +962,7 @@ export class WecomHarnessBridge {
|
|
|
961
962
|
|
|
962
963
|
const text = nonEmptyString(interactionReplyText(frame));
|
|
963
964
|
if (!text) {
|
|
964
|
-
await this.#sendImmediate(frame, chatId, '请用文字回答当前问题。')
|
|
965
|
+
await this.#sendImmediate(frame, chatId, t('请用文字回答当前问题。'))
|
|
965
966
|
.catch(() => undefined);
|
|
966
967
|
return;
|
|
967
968
|
}
|
|
@@ -969,7 +970,7 @@ export class WecomHarnessBridge {
|
|
|
969
970
|
const pending = this.#pendingInteractions.get(key);
|
|
970
971
|
if (!pending || pending !== expected || pending.submitting) {
|
|
971
972
|
if (claimed && (!pending || pending !== expected)) {
|
|
972
|
-
await this.#sendImmediate(frame, chatId,
|
|
973
|
+
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
|
|
973
974
|
.catch(() => undefined);
|
|
974
975
|
return;
|
|
975
976
|
}
|
|
@@ -990,7 +991,7 @@ export class WecomHarnessBridge {
|
|
|
990
991
|
try {
|
|
991
992
|
await this.#presentInteraction(pending);
|
|
992
993
|
} catch {
|
|
993
|
-
this.#status.lastError = '企业微信交互问题发送失败。';
|
|
994
|
+
this.#status.lastError = t('企业微信交互问题发送失败。');
|
|
994
995
|
this.#logger.error?.('[dsh-im:wecom] failed to retry an interaction question');
|
|
995
996
|
pending.interaction.reconnect?.();
|
|
996
997
|
return;
|
|
@@ -998,7 +999,7 @@ export class WecomHarnessBridge {
|
|
|
998
999
|
const presentedPending = this.#pendingInteractions.get(key);
|
|
999
1000
|
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
|
1000
1001
|
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
1001
|
-
await this.#sendImmediate(frame, chatId,
|
|
1002
|
+
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
|
|
1002
1003
|
.catch(() => undefined);
|
|
1003
1004
|
return;
|
|
1004
1005
|
}
|
|
@@ -1021,7 +1022,7 @@ export class WecomHarnessBridge {
|
|
|
1021
1022
|
try {
|
|
1022
1023
|
await this.#presentInteraction(pending);
|
|
1023
1024
|
} catch {
|
|
1024
|
-
this.#status.lastError = '企业微信交互问题发送失败。';
|
|
1025
|
+
this.#status.lastError = t('企业微信交互问题发送失败。');
|
|
1025
1026
|
this.#logger.error?.('[dsh-im:wecom] failed to send the next interaction question');
|
|
1026
1027
|
pending.interaction.reconnect?.();
|
|
1027
1028
|
}
|
|
@@ -1045,7 +1046,7 @@ export class WecomHarnessBridge {
|
|
|
1045
1046
|
if (this.#pendingInteractions.get(key) === pending) {
|
|
1046
1047
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
1047
1048
|
}
|
|
1048
|
-
await this.#sendImmediate(frame, chatId,
|
|
1049
|
+
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
|
|
1049
1050
|
.catch(() => undefined);
|
|
1050
1051
|
return;
|
|
1051
1052
|
}
|
|
@@ -1053,9 +1054,9 @@ export class WecomHarnessBridge {
|
|
|
1053
1054
|
pending.submitting = false;
|
|
1054
1055
|
pending.answers.pop();
|
|
1055
1056
|
pending.index -= 1;
|
|
1056
|
-
this.#status.lastError = '回答提交失败。';
|
|
1057
|
+
this.#status.lastError = t('回答提交失败。');
|
|
1057
1058
|
this.#logger.error?.('[dsh-im:wecom] failed to answer a Harness interaction');
|
|
1058
|
-
await this.#sendImmediate(frame, chatId, '回答提交失败,请重新发送当前问题的答案。')
|
|
1059
|
+
await this.#sendImmediate(frame, chatId, t('回答提交失败,请重新发送当前问题的答案。'))
|
|
1059
1060
|
.catch(() => undefined);
|
|
1060
1061
|
}
|
|
1061
1062
|
}
|
|
@@ -1100,7 +1101,7 @@ export class WecomHarnessBridge {
|
|
|
1100
1101
|
});
|
|
1101
1102
|
await this.#sendActive(
|
|
1102
1103
|
chatId,
|
|
1103
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
1104
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
1104
1105
|
).catch(() => undefined);
|
|
1105
1106
|
return;
|
|
1106
1107
|
}
|
|
@@ -1188,7 +1189,7 @@ export class WecomHarnessBridge {
|
|
|
1188
1189
|
await this.#state.markSeen(messageId);
|
|
1189
1190
|
this.#status.messagesReceived += 1;
|
|
1190
1191
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
1191
|
-
await this.#sendImmediate(frame, chatId,
|
|
1192
|
+
await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。')).catch(() => undefined);
|
|
1192
1193
|
}
|
|
1193
1194
|
|
|
1194
1195
|
#takePendingInteraction(key, interactionId) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
connectionTestMessage,
|
|
6
6
|
connectionTestTargetUnavailable,
|
|
7
7
|
} from '../shared/connection-test.mjs';
|
|
8
|
+
import { t } from '../shared/i18n.mjs';
|
|
8
9
|
|
|
9
10
|
const ACTIVE_ATTEMPT_STATES = new Set(['pending', 'connecting']);
|
|
10
11
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled', 'expired']);
|
|
@@ -82,14 +83,14 @@ export class WecomController {
|
|
|
82
83
|
if (this.#closed || existing?.ready || existing?.wecomConnectionState === 'connecting') return;
|
|
83
84
|
const secret = await this.#resolveSecret(config.secretRef);
|
|
84
85
|
if (!secret) {
|
|
85
|
-
this.#errors.set(config.botId, safeError('missing-secret', '企业微信机器人凭据缺失,请移除后重新扫码。'));
|
|
86
|
+
this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
try {
|
|
89
90
|
await this.#startRuntime(config, secret);
|
|
90
91
|
this.#errors.delete(config.botId);
|
|
91
92
|
} catch (error) {
|
|
92
|
-
this.#errors.set(config.botId, safeError('connection-failed', '企业微信连接未就绪,插件会自动重试。'));
|
|
93
|
+
this.#errors.set(config.botId, safeError('connection-failed', t('企业微信连接未就绪,插件会自动重试。')));
|
|
93
94
|
this.#logger.warn?.(`[dsh-im:wecom] bot ${config.botId} failed to initialize`);
|
|
94
95
|
} finally {
|
|
95
96
|
this.#touch();
|
|
@@ -134,8 +135,8 @@ export class WecomController {
|
|
|
134
135
|
} catch (error) {
|
|
135
136
|
record.state = record.controller.signal.aborted ? 'cancelled' : 'failed';
|
|
136
137
|
record.error = record.controller.signal.aborted
|
|
137
|
-
? safeError('cancelled', '扫码绑定已取消。')
|
|
138
|
-
: safeError('qr-start-failed', '无法生成企业微信二维码,请稍后重试。');
|
|
138
|
+
? safeError('cancelled', t('扫码绑定已取消。'))
|
|
139
|
+
: safeError('qr-start-failed', t('无法生成企业微信二维码,请稍后重试。'));
|
|
139
140
|
this.#finishAttempt(record);
|
|
140
141
|
throw error;
|
|
141
142
|
}
|
|
@@ -150,7 +151,7 @@ export class WecomController {
|
|
|
150
151
|
}
|
|
151
152
|
if (Date.now() >= record.expiresAt) {
|
|
152
153
|
record.state = 'expired';
|
|
153
|
-
record.error = safeError('expired', '企业微信二维码已过期,请重新生成。');
|
|
154
|
+
record.error = safeError('expired', t('企业微信二维码已过期,请重新生成。'));
|
|
154
155
|
record.controller.abort();
|
|
155
156
|
this.#finishAttempt(record);
|
|
156
157
|
return publicAttempt(record);
|
|
@@ -200,7 +201,7 @@ export class WecomController {
|
|
|
200
201
|
} catch {
|
|
201
202
|
this.#errors.set(
|
|
202
203
|
identity.botId,
|
|
203
|
-
safeError('connection-failed', '企业微信机器人已绑定,消息连接暂未就绪。'),
|
|
204
|
+
safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')),
|
|
204
205
|
);
|
|
205
206
|
this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} credential connection failed`);
|
|
206
207
|
}
|
|
@@ -216,7 +217,7 @@ export class WecomController {
|
|
|
216
217
|
record.controller.abort();
|
|
217
218
|
await Promise.allSettled([record.polling, record.transition].filter(Boolean));
|
|
218
219
|
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
|
219
|
-
record.error ??= safeError('cancelled', '扫码绑定已取消。');
|
|
220
|
+
record.error ??= safeError('cancelled', t('扫码绑定已取消。'));
|
|
220
221
|
this.#finishAttempt(record);
|
|
221
222
|
}
|
|
222
223
|
return publicAttempt(record);
|
|
@@ -232,7 +233,7 @@ export class WecomController {
|
|
|
232
233
|
await this.#startRuntime(config, secret);
|
|
233
234
|
this.#errors.delete(botId);
|
|
234
235
|
} catch (error) {
|
|
235
|
-
this.#errors.set(botId, safeError('connection-failed', '企业微信连接仍未就绪,请稍后重试。'));
|
|
236
|
+
this.#errors.set(botId, safeError('connection-failed', t('企业微信连接仍未就绪,请稍后重试。')));
|
|
236
237
|
throw error;
|
|
237
238
|
} finally {
|
|
238
239
|
this.#touch();
|
|
@@ -247,10 +248,10 @@ export class WecomController {
|
|
|
247
248
|
return this.#withBotTransition(botId, async () => {
|
|
248
249
|
const runtime = this.#runtimes.get(botId);
|
|
249
250
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
250
|
-
throw connectionTestTargetUnavailable('企业微信机器人');
|
|
251
|
+
throw connectionTestTargetUnavailable(t('企业微信机器人'));
|
|
251
252
|
}
|
|
252
253
|
return runtime.sendConnectionTest(connectionTestMessage(
|
|
253
|
-
|
|
254
|
+
t('企业微信机器人({botId})', { botId: maskWecomBotId(config.remoteBotId) }),
|
|
254
255
|
));
|
|
255
256
|
});
|
|
256
257
|
}
|
|
@@ -295,11 +296,11 @@ export class WecomController {
|
|
|
295
296
|
state,
|
|
296
297
|
connected,
|
|
297
298
|
configured: true,
|
|
298
|
-
bot: { name: '企业微信机器人', appIdMasked: maskWecomBotId(config.remoteBotId) },
|
|
299
|
+
bot: { name: t('企业微信机器人'), appIdMasked: maskWecomBotId(config.remoteBotId) },
|
|
299
300
|
health: {
|
|
300
301
|
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
|
301
|
-
summary: connected ? '企业微信 WebSocket 长连接运行正常'
|
|
302
|
-
: state === 'error' ? '企业微信连接未就绪,插件会自动重试' : '企业微信连接当前离线',
|
|
302
|
+
summary: connected ? t('企业微信 WebSocket 长连接运行正常')
|
|
303
|
+
: state === 'error' ? t('企业微信连接未就绪,插件会自动重试') : t('企业微信连接当前离线'),
|
|
303
304
|
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
|
304
305
|
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
|
305
306
|
},
|
|
@@ -341,13 +342,13 @@ export class WecomController {
|
|
|
341
342
|
if (result.status === 'waiting') return;
|
|
342
343
|
if (result.status === 'expired') {
|
|
343
344
|
record.state = 'expired';
|
|
344
|
-
record.error = safeError('expired', '企业微信二维码已过期,请重新生成。');
|
|
345
|
+
record.error = safeError('expired', t('企业微信二维码已过期,请重新生成。'));
|
|
345
346
|
this.#finishAttempt(record);
|
|
346
347
|
return;
|
|
347
348
|
}
|
|
348
349
|
if (result.status !== 'success') {
|
|
349
350
|
record.state = 'failed';
|
|
350
|
-
record.error = safeError('qr-connect-failed', '企业微信扫码没有完成,请重新生成二维码。');
|
|
351
|
+
record.error = safeError('qr-connect-failed', t('企业微信扫码没有完成,请重新生成二维码。'));
|
|
351
352
|
this.#finishAttempt(record);
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
@@ -362,7 +363,7 @@ export class WecomController {
|
|
|
362
363
|
} catch (error) {
|
|
363
364
|
if (record.controller.signal.aborted) return;
|
|
364
365
|
record.state = 'failed';
|
|
365
|
-
record.error = safeError('qr-connect-failed', '企业微信扫码服务暂时不可用,请重新生成二维码。');
|
|
366
|
+
record.error = safeError('qr-connect-failed', t('企业微信扫码服务暂时不可用,请重新生成二维码。'));
|
|
366
367
|
this.#logger.warn?.('[dsh-im:wecom] QR polling failed');
|
|
367
368
|
this.#finishAttempt(record);
|
|
368
369
|
}
|
|
@@ -379,10 +380,10 @@ export class WecomController {
|
|
|
379
380
|
} catch (error) {
|
|
380
381
|
if (record.controller.signal.aborted) {
|
|
381
382
|
record.state = 'cancelled';
|
|
382
|
-
record.error = safeError('cancelled', '扫码绑定已取消。');
|
|
383
|
+
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
|
383
384
|
} else {
|
|
384
385
|
record.state = 'failed';
|
|
385
|
-
record.error = safeError('activation-failed', '企业微信已授权,但无法安全保存接入配置。');
|
|
386
|
+
record.error = safeError('activation-failed', t('企业微信已授权,但无法安全保存接入配置。'));
|
|
386
387
|
this.#logger.error?.('[dsh-im:wecom] provisioning failed');
|
|
387
388
|
}
|
|
388
389
|
} finally {
|
|
@@ -429,7 +430,7 @@ export class WecomController {
|
|
|
429
430
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
430
431
|
throw error;
|
|
431
432
|
}
|
|
432
|
-
this.#errors.set(identity.botId, safeError('connection-failed', '企业微信机器人已绑定,消息连接暂未就绪。'));
|
|
433
|
+
this.#errors.set(identity.botId, safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')));
|
|
433
434
|
this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} activation connection failed`);
|
|
434
435
|
}
|
|
435
436
|
this.#touch();
|
|
@@ -2,6 +2,7 @@ import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/
|
|
|
2
2
|
|
|
3
3
|
import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
|
|
4
4
|
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
5
6
|
|
|
6
7
|
function timeoutError() {
|
|
7
8
|
const error = new Error('Enterprise WeChat WebSocket authentication timed out');
|
|
@@ -202,7 +203,7 @@ export class WecomRuntime {
|
|
|
202
203
|
return sendRememberedConnectionTest({
|
|
203
204
|
state: this.#state,
|
|
204
205
|
text,
|
|
205
|
-
channelLabel: '企业微信机器人',
|
|
206
|
+
channelLabel: t('企业微信机器人'),
|
|
206
207
|
send: async ({ chatId }, content) => {
|
|
207
208
|
if (!this.#status.ready || !this.#client) {
|
|
208
209
|
throw new Error('Enterprise WeChat runtime is not connected');
|