@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
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { normalizeWeixinApiBaseUrl } from './weixin-api.mjs';
|
|
6
|
+
import { t } from '../shared/i18n.mjs';
|
|
6
7
|
|
|
7
8
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, accounts: Object.freeze([]) });
|
|
8
9
|
|
|
@@ -32,7 +33,7 @@ export function deriveWeixinBotIdentity(accountId) {
|
|
|
32
33
|
|
|
33
34
|
export function maskWeixinAccountId(accountId) {
|
|
34
35
|
const value = cleanString(accountId) ?? '';
|
|
35
|
-
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : '微信机器人';
|
|
36
|
+
if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : t('微信机器人');
|
|
36
37
|
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -457,6 +457,123 @@ function validateLoginResponse(value) {
|
|
|
457
457
|
export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
458
458
|
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
459
459
|
|
|
460
|
+
async function sendArtifact({
|
|
461
|
+
baseUrl,
|
|
462
|
+
token,
|
|
463
|
+
toUserId,
|
|
464
|
+
file,
|
|
465
|
+
contextToken,
|
|
466
|
+
runId,
|
|
467
|
+
signal,
|
|
468
|
+
}, { mediaType, createItem }) {
|
|
469
|
+
const recipient = nonEmptyString(toUserId);
|
|
470
|
+
if (!recipient || !file || typeof file !== 'object'
|
|
471
|
+
|| typeof file.fileName !== 'string' || !file.fileName
|
|
472
|
+
|| !Buffer.isBuffer(file.bytes)) {
|
|
473
|
+
throw new TypeError('toUserId and a file are required');
|
|
474
|
+
}
|
|
475
|
+
signal?.throwIfAborted();
|
|
476
|
+
const fileKey = randomBytes(16).toString('hex');
|
|
477
|
+
const aesKey = randomBytes(16);
|
|
478
|
+
const rawMd5 = createHash('md5').update(file.bytes).digest('hex');
|
|
479
|
+
let upload;
|
|
480
|
+
try {
|
|
481
|
+
upload = await requestJson(fetchImpl, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
baseUrl,
|
|
484
|
+
endpoint: 'ilink/bot/getuploadurl',
|
|
485
|
+
token,
|
|
486
|
+
signal,
|
|
487
|
+
body: {
|
|
488
|
+
filekey: fileKey,
|
|
489
|
+
media_type: mediaType,
|
|
490
|
+
to_user_id: recipient,
|
|
491
|
+
rawsize: file.bytes.byteLength,
|
|
492
|
+
rawfilemd5: rawMd5,
|
|
493
|
+
filesize: aesEcbPaddedSize(file.bytes.byteLength),
|
|
494
|
+
no_need_thumb: true,
|
|
495
|
+
aeskey: aesKey.toString('hex'),
|
|
496
|
+
base_info: baseInfo(),
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (signal?.aborted) throw abortError(signal);
|
|
501
|
+
const status = Number(error?.status);
|
|
502
|
+
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
503
|
+
? 'artifact-provider-rejected'
|
|
504
|
+
: 'artifact-provider-failed';
|
|
505
|
+
throw weixinArtifactError(error, { fallback });
|
|
506
|
+
}
|
|
507
|
+
const uploadRejection = rejectedProviderResponse(upload);
|
|
508
|
+
if (uploadRejection) {
|
|
509
|
+
throw weixinArtifactError(new WeixinApiError(
|
|
510
|
+
'upload-url-rejected',
|
|
511
|
+
'微信服务拒绝了文件上传请求。',
|
|
512
|
+
{ providerCode: uploadRejection },
|
|
513
|
+
));
|
|
514
|
+
}
|
|
515
|
+
const uploadUrl = weixinCdnUploadUrl(upload, fileKey);
|
|
516
|
+
const ciphertext = encryptWeixinUpload(file.bytes, aesKey);
|
|
517
|
+
let downloadParam;
|
|
518
|
+
try {
|
|
519
|
+
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, ciphertext, { signal });
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (signal?.aborted) throw abortError(signal);
|
|
522
|
+
const status = Number(error?.status);
|
|
523
|
+
const fallback = error?.code === 'upload-rejected' || (status >= 400 && status < 500)
|
|
524
|
+
? 'artifact-provider-rejected'
|
|
525
|
+
: 'artifact-provider-failed';
|
|
526
|
+
throw weixinArtifactError(error, { fallback });
|
|
527
|
+
}
|
|
528
|
+
signal?.throwIfAborted();
|
|
529
|
+
const deliverySeed = nonEmptyString(file.deliveryKey) ?? nonEmptyString(file.artifactId)
|
|
530
|
+
?? randomUUID();
|
|
531
|
+
const clientIdSeed = mediaType === 3 ? deliverySeed : `${deliverySeed}\u0000${mediaType}`;
|
|
532
|
+
const clientId = `dsh-weixin-${createHash('sha256')
|
|
533
|
+
.update(clientIdSeed)
|
|
534
|
+
.digest('hex')
|
|
535
|
+
.slice(0, 32)}`;
|
|
536
|
+
const media = {
|
|
537
|
+
encrypt_query_param: downloadParam,
|
|
538
|
+
aes_key: Buffer.from(aesKey.toString('hex')).toString('base64'),
|
|
539
|
+
encrypt_type: 1,
|
|
540
|
+
};
|
|
541
|
+
let response;
|
|
542
|
+
try {
|
|
543
|
+
response = await requestJson(fetchImpl, {
|
|
544
|
+
method: 'POST',
|
|
545
|
+
baseUrl,
|
|
546
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
547
|
+
token,
|
|
548
|
+
signal,
|
|
549
|
+
body: {
|
|
550
|
+
msg: {
|
|
551
|
+
from_user_id: '',
|
|
552
|
+
to_user_id: recipient,
|
|
553
|
+
client_id: clientId,
|
|
554
|
+
message_type: 2,
|
|
555
|
+
message_state: 2,
|
|
556
|
+
item_list: [createItem({ file, media, ciphertextSize: ciphertext.byteLength })],
|
|
557
|
+
...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
|
|
558
|
+
...(nonEmptyString(runId) ? { run_id: runId.trim() } : {}),
|
|
559
|
+
},
|
|
560
|
+
base_info: baseInfo(),
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
} catch (error) {
|
|
564
|
+
throw classifyWeixinFinalDeliveryError(error, signal);
|
|
565
|
+
}
|
|
566
|
+
const sendRejection = rejectedProviderResponse(response);
|
|
567
|
+
if (sendRejection) {
|
|
568
|
+
throw weixinArtifactError(new WeixinApiError(
|
|
569
|
+
'send-rejected',
|
|
570
|
+
'微信服务拒绝了文件消息。',
|
|
571
|
+
{ providerCode: sendRejection },
|
|
572
|
+
));
|
|
573
|
+
}
|
|
574
|
+
return { messageId: clientId };
|
|
575
|
+
}
|
|
576
|
+
|
|
460
577
|
return Object.freeze({
|
|
461
578
|
inboundImages(message) {
|
|
462
579
|
return extractWeixinImages(message, { fetchImpl });
|
|
@@ -549,115 +666,31 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
549
666
|
return true;
|
|
550
667
|
},
|
|
551
668
|
|
|
552
|
-
async sendFile(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const aesKey = randomBytes(16);
|
|
562
|
-
const rawMd5 = createHash('md5').update(file.bytes).digest('hex');
|
|
563
|
-
let upload;
|
|
564
|
-
try {
|
|
565
|
-
upload = await requestJson(fetchImpl, {
|
|
566
|
-
method: 'POST',
|
|
567
|
-
baseUrl,
|
|
568
|
-
endpoint: 'ilink/bot/getuploadurl',
|
|
569
|
-
token,
|
|
570
|
-
signal,
|
|
571
|
-
body: {
|
|
572
|
-
filekey: fileKey,
|
|
573
|
-
media_type: 3,
|
|
574
|
-
to_user_id: recipient,
|
|
575
|
-
rawsize: file.bytes.byteLength,
|
|
576
|
-
rawfilemd5: rawMd5,
|
|
577
|
-
filesize: aesEcbPaddedSize(file.bytes.byteLength),
|
|
578
|
-
no_need_thumb: true,
|
|
579
|
-
aeskey: aesKey.toString('hex'),
|
|
580
|
-
base_info: baseInfo(),
|
|
669
|
+
async sendFile(request) {
|
|
670
|
+
return sendArtifact(request, {
|
|
671
|
+
mediaType: 3,
|
|
672
|
+
createItem: ({ file, media }) => ({
|
|
673
|
+
type: 4,
|
|
674
|
+
file_item: {
|
|
675
|
+
media,
|
|
676
|
+
file_name: file.fileName,
|
|
677
|
+
len: String(file.bytes.byteLength),
|
|
581
678
|
},
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
'upload-url-rejected',
|
|
595
|
-
'微信服务拒绝了文件上传请求。',
|
|
596
|
-
{ providerCode: uploadRejection },
|
|
597
|
-
));
|
|
598
|
-
}
|
|
599
|
-
const uploadUrl = weixinCdnUploadUrl(upload, fileKey);
|
|
600
|
-
const ciphertext = encryptWeixinUpload(file.bytes, aesKey);
|
|
601
|
-
let downloadParam;
|
|
602
|
-
try {
|
|
603
|
-
downloadParam = await uploadWeixinCdn(fetchImpl, uploadUrl, ciphertext, { signal });
|
|
604
|
-
} catch (error) {
|
|
605
|
-
if (signal?.aborted) throw abortError(signal);
|
|
606
|
-
const status = Number(error?.status);
|
|
607
|
-
const fallback = error?.code === 'upload-rejected' || (status >= 400 && status < 500)
|
|
608
|
-
? 'artifact-provider-rejected'
|
|
609
|
-
: 'artifact-provider-failed';
|
|
610
|
-
throw weixinArtifactError(error, { fallback });
|
|
611
|
-
}
|
|
612
|
-
signal?.throwIfAborted();
|
|
613
|
-
const deliverySeed = nonEmptyString(file.deliveryKey) ?? nonEmptyString(file.artifactId)
|
|
614
|
-
?? randomUUID();
|
|
615
|
-
const clientId = `dsh-weixin-${createHash('sha256').update(deliverySeed).digest('hex').slice(0, 32)}`;
|
|
616
|
-
let response;
|
|
617
|
-
try {
|
|
618
|
-
response = await requestJson(fetchImpl, {
|
|
619
|
-
method: 'POST',
|
|
620
|
-
baseUrl,
|
|
621
|
-
endpoint: 'ilink/bot/sendmessage',
|
|
622
|
-
token,
|
|
623
|
-
signal,
|
|
624
|
-
body: {
|
|
625
|
-
msg: {
|
|
626
|
-
from_user_id: '',
|
|
627
|
-
to_user_id: recipient,
|
|
628
|
-
client_id: clientId,
|
|
629
|
-
message_type: 2,
|
|
630
|
-
message_state: 2,
|
|
631
|
-
item_list: [{
|
|
632
|
-
type: 4,
|
|
633
|
-
file_item: {
|
|
634
|
-
media: {
|
|
635
|
-
encrypt_query_param: downloadParam,
|
|
636
|
-
aes_key: Buffer.from(aesKey.toString('hex')).toString('base64'),
|
|
637
|
-
encrypt_type: 1,
|
|
638
|
-
},
|
|
639
|
-
file_name: file.fileName,
|
|
640
|
-
len: String(file.bytes.byteLength),
|
|
641
|
-
},
|
|
642
|
-
}],
|
|
643
|
-
...(nonEmptyString(contextToken) ? { context_token: contextToken.trim() } : {}),
|
|
644
|
-
...(nonEmptyString(runId) ? { run_id: runId.trim() } : {}),
|
|
645
|
-
},
|
|
646
|
-
base_info: baseInfo(),
|
|
679
|
+
}),
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
async sendImage(request) {
|
|
684
|
+
return sendArtifact(request, {
|
|
685
|
+
mediaType: 1,
|
|
686
|
+
createItem: ({ media, ciphertextSize }) => ({
|
|
687
|
+
type: 2,
|
|
688
|
+
image_item: {
|
|
689
|
+
media,
|
|
690
|
+
mid_size: ciphertextSize,
|
|
647
691
|
},
|
|
648
|
-
})
|
|
649
|
-
}
|
|
650
|
-
throw classifyWeixinFinalDeliveryError(error, signal);
|
|
651
|
-
}
|
|
652
|
-
const sendRejection = rejectedProviderResponse(response);
|
|
653
|
-
if (sendRejection) {
|
|
654
|
-
throw weixinArtifactError(new WeixinApiError(
|
|
655
|
-
'send-rejected',
|
|
656
|
-
'微信服务拒绝了文件消息。',
|
|
657
|
-
{ providerCode: sendRejection },
|
|
658
|
-
));
|
|
659
|
-
}
|
|
660
|
-
return { messageId: clientId };
|
|
692
|
+
}),
|
|
693
|
+
});
|
|
661
694
|
},
|
|
662
695
|
|
|
663
696
|
async notifyStart({ baseUrl, token, signal }) {
|
|
@@ -38,41 +38,37 @@ import {
|
|
|
38
38
|
prefetchInboundFiles,
|
|
39
39
|
} from '../shared/inbound-file.mjs';
|
|
40
40
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
41
|
+
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
41
42
|
import {
|
|
42
|
-
materializeOutboundArtifact,
|
|
43
|
-
releaseOutboundArtifact,
|
|
44
|
-
} from '../shared/semantic/artifact.mjs';
|
|
45
|
-
import {
|
|
46
|
-
createArtifactFailureReceipt,
|
|
47
43
|
createDeliveryReceipt,
|
|
48
|
-
mergeDeliveryReceipts,
|
|
49
44
|
providerMessageIdsFor,
|
|
50
45
|
} from '../shared/semantic/delivery.mjs';
|
|
46
|
+
import { t } from '../shared/i18n.mjs';
|
|
51
47
|
|
|
52
|
-
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
53
|
-
const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
|
48
|
+
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
|
49
|
+
const GENERIC_PROCESSING_ERROR = () => t('消息处理失败,请稍后重试。');
|
|
54
50
|
|
|
55
|
-
const HELP_TEXT = [
|
|
56
|
-
'微信已连接 DeepSeek Harness。',
|
|
51
|
+
const HELP_TEXT = () => [
|
|
52
|
+
t('微信已连接 DeepSeek Harness。'),
|
|
57
53
|
'',
|
|
58
|
-
'直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。',
|
|
59
|
-
'/new 开启一个全新会话',
|
|
60
|
-
'/compact 压缩当前会话的较早上下文',
|
|
61
|
-
'/workspace 工作区绝对路径 切换工作区',
|
|
62
|
-
'/workspacelist 列出工作区绝对路径',
|
|
63
|
-
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
64
|
-
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
65
|
-
'/models 按序号列出所有可用模型',
|
|
66
|
-
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
67
|
-
'示例:先发 /models,再发 /model 2',
|
|
68
|
-
'/presetlist 按序号列出可用 Agent Preset',
|
|
69
|
-
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
|
70
|
-
'纯数字 ID:/preset id:<ID>',
|
|
71
|
-
'/preset --default 跟随 Host 默认',
|
|
72
|
-
'/stop 停止当前任务',
|
|
73
|
-
'/steer 补充指令 纠偏当前任务',
|
|
74
|
-
'/status 检查连接状态',
|
|
75
|
-
'/help 显示本帮助',
|
|
54
|
+
t('直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。'),
|
|
55
|
+
t('/new 开启一个全新会话'),
|
|
56
|
+
t('/compact 压缩当前会话的较早上下文'),
|
|
57
|
+
t('/workspace 工作区绝对路径 切换工作区'),
|
|
58
|
+
t('/workspacelist 列出工作区绝对路径'),
|
|
59
|
+
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
60
|
+
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
61
|
+
t('/models 按序号列出所有可用模型'),
|
|
62
|
+
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
|
|
63
|
+
t('示例:先发 /models,再发 /model 2'),
|
|
64
|
+
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
65
|
+
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
66
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
67
|
+
t('/preset --default 跟随 Host 默认'),
|
|
68
|
+
t('/stop 停止当前任务'),
|
|
69
|
+
t('/steer 补充指令 纠偏当前任务'),
|
|
70
|
+
t('/status 检查连接状态'),
|
|
71
|
+
t('/help 显示本帮助'),
|
|
76
72
|
].join('\n');
|
|
77
73
|
|
|
78
74
|
function conversationKey(userId) {
|
|
@@ -113,7 +109,7 @@ function canClaimInteractionReply(message, pending) {
|
|
|
113
109
|
&& nonEmptyString(extractWeixinText(message));
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
|
112
|
+
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR()) {
|
|
117
113
|
const diagnostic = imagePromptDiagnostic(error);
|
|
118
114
|
return {
|
|
119
115
|
code: diagnostic?.code ?? 'message-processing-failed',
|
|
@@ -124,24 +120,24 @@ function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
|
|
124
120
|
}
|
|
125
121
|
|
|
126
122
|
function artifactFailureText(fileName, error) {
|
|
127
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
123
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
128
124
|
switch (error?.code) {
|
|
129
125
|
case 'artifact-delivery-uncertain':
|
|
130
|
-
return
|
|
126
|
+
return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
131
127
|
case 'artifact-permission-required':
|
|
132
|
-
return
|
|
128
|
+
return t('结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。', { name });
|
|
133
129
|
case 'artifact-too-large':
|
|
134
|
-
return
|
|
130
|
+
return t('结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。', { name });
|
|
135
131
|
case 'artifact-rate-limited':
|
|
136
|
-
return
|
|
132
|
+
return t('结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。', { name });
|
|
137
133
|
case 'artifact-provider-rejected':
|
|
138
|
-
return
|
|
134
|
+
return t('结果文件「{name}」已生成,但微信拒绝了该文件消息。', { name });
|
|
139
135
|
case 'artifact-invalid':
|
|
140
136
|
case 'artifact-changed':
|
|
141
137
|
case 'artifact-unavailable':
|
|
142
|
-
return
|
|
138
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
143
139
|
default:
|
|
144
|
-
return
|
|
140
|
+
return t('结果文件「{name}」已生成,但暂时未能通过微信发送,请稍后重试。', { name });
|
|
145
141
|
}
|
|
146
142
|
}
|
|
147
143
|
|
|
@@ -249,7 +245,7 @@ export class WeixinHarnessBridge {
|
|
|
249
245
|
this.#status.lastError = error?.message ?? String(error);
|
|
250
246
|
this.#status.lastMessageError = safeMessageError(error);
|
|
251
247
|
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
|
252
|
-
return this.#send(sender, GENERIC_PROCESSING_ERROR, contextToken, runId)
|
|
248
|
+
return this.#send(sender, GENERIC_PROCESSING_ERROR(), contextToken, runId)
|
|
253
249
|
.catch(() => undefined);
|
|
254
250
|
}).finally(() => {
|
|
255
251
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -405,26 +401,26 @@ export class WeixinHarnessBridge {
|
|
|
405
401
|
const hasImages = hasInboundImages(promptMessage);
|
|
406
402
|
const hasFiles = hasInboundFiles(promptMessage);
|
|
407
403
|
if (!text && !hasImages && !hasFiles) {
|
|
408
|
-
await this.#send(sender, '目前支持文字、图片、文件,以及微信已转成文字的语音消息。', contextToken, runId);
|
|
404
|
+
await this.#send(sender, t('目前支持文字、图片、文件,以及微信已转成文字的语音消息。'), contextToken, runId);
|
|
409
405
|
await this.#state.markSeen(messageId);
|
|
410
406
|
return;
|
|
411
407
|
}
|
|
412
408
|
|
|
413
409
|
const command = text.trim().toLowerCase();
|
|
414
410
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
415
|
-
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
411
|
+
await this.#send(sender, HELP_TEXT(), contextToken, runId);
|
|
416
412
|
await this.#state.markSeen(messageId);
|
|
417
413
|
return;
|
|
418
414
|
}
|
|
419
415
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
420
416
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
421
|
-
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
|
417
|
+
await this.#send(sender, t('微信与 DeepSeek Harness 连接正常。'), contextToken, runId);
|
|
422
418
|
await this.#state.markSeen(messageId);
|
|
423
419
|
return;
|
|
424
420
|
}
|
|
425
421
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
426
422
|
await this.#state.clearSession(key);
|
|
427
|
-
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
423
|
+
await this.#send(sender, t('已开启新会话。请发送你的问题。'), contextToken, runId);
|
|
428
424
|
await this.#state.markSeen(messageId);
|
|
429
425
|
return;
|
|
430
426
|
}
|
|
@@ -488,7 +484,7 @@ export class WeixinHarnessBridge {
|
|
|
488
484
|
}
|
|
489
485
|
const answerText = typeof answer === 'string' && answer.trim()
|
|
490
486
|
? answer
|
|
491
|
-
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
487
|
+
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
492
488
|
let textDeliveryError = null;
|
|
493
489
|
let textReceipt = null;
|
|
494
490
|
try {
|
|
@@ -524,7 +520,7 @@ export class WeixinHarnessBridge {
|
|
|
524
520
|
this.#status.lastError = error?.message ?? String(error);
|
|
525
521
|
const userMessage = inboundFileUserMessage(error)
|
|
526
522
|
?? imagePromptUserMessage(error)
|
|
527
|
-
?? GENERIC_PROCESSING_ERROR;
|
|
523
|
+
?? GENERIC_PROCESSING_ERROR();
|
|
528
524
|
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
|
529
525
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
530
526
|
try {
|
|
@@ -562,7 +558,7 @@ export class WeixinHarnessBridge {
|
|
|
562
558
|
if (!text || hasWeixinImageItems(message)) {
|
|
563
559
|
await this.#send(
|
|
564
560
|
expected.actor,
|
|
565
|
-
'请用文字回答当前问题。',
|
|
561
|
+
t('请用文字回答当前问题。'),
|
|
566
562
|
contextToken,
|
|
567
563
|
runId,
|
|
568
564
|
);
|
|
@@ -574,7 +570,7 @@ export class WeixinHarnessBridge {
|
|
|
574
570
|
if (claimed && (!pending || pending !== expected)) {
|
|
575
571
|
await this.#send(
|
|
576
572
|
expected.actor,
|
|
577
|
-
INTERACTION_RESOLVED_TEXT,
|
|
573
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
578
574
|
contextToken,
|
|
579
575
|
runId,
|
|
580
576
|
);
|
|
@@ -591,7 +587,7 @@ export class WeixinHarnessBridge {
|
|
|
591
587
|
try {
|
|
592
588
|
await this.#presentInteraction(pending);
|
|
593
589
|
} catch {
|
|
594
|
-
this.#status.lastError = '微信交互问题发送失败。';
|
|
590
|
+
this.#status.lastError = t('微信交互问题发送失败。');
|
|
595
591
|
this.#logger.error?.('[dsh-weixin] failed to retry an interaction question');
|
|
596
592
|
pending.interaction.reconnect?.();
|
|
597
593
|
return;
|
|
@@ -601,7 +597,7 @@ export class WeixinHarnessBridge {
|
|
|
601
597
|
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
602
598
|
await this.#send(
|
|
603
599
|
expected.actor,
|
|
604
|
-
INTERACTION_RESOLVED_TEXT,
|
|
600
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
605
601
|
contextToken,
|
|
606
602
|
runId,
|
|
607
603
|
).catch(() => undefined);
|
|
@@ -626,7 +622,7 @@ export class WeixinHarnessBridge {
|
|
|
626
622
|
try {
|
|
627
623
|
await this.#presentInteraction(pending);
|
|
628
624
|
} catch {
|
|
629
|
-
this.#status.lastError = '微信交互问题发送失败。';
|
|
625
|
+
this.#status.lastError = t('微信交互问题发送失败。');
|
|
630
626
|
this.#logger.error?.('[dsh-weixin] failed to send the next interaction question');
|
|
631
627
|
pending.interaction.reconnect?.();
|
|
632
628
|
}
|
|
@@ -651,7 +647,7 @@ export class WeixinHarnessBridge {
|
|
|
651
647
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
652
648
|
await this.#send(
|
|
653
649
|
pending.actor,
|
|
654
|
-
INTERACTION_RESOLVED_TEXT,
|
|
650
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
655
651
|
pending.contextToken,
|
|
656
652
|
pending.runId,
|
|
657
653
|
).catch(() => undefined);
|
|
@@ -661,11 +657,11 @@ export class WeixinHarnessBridge {
|
|
|
661
657
|
pending.submitting = false;
|
|
662
658
|
pending.answers.pop();
|
|
663
659
|
pending.index -= 1;
|
|
664
|
-
this.#status.lastError = '回答提交失败。';
|
|
660
|
+
this.#status.lastError = t('回答提交失败。');
|
|
665
661
|
this.#logger.error?.('[dsh-weixin] failed to answer a Harness interaction');
|
|
666
662
|
await this.#send(
|
|
667
663
|
pending.actor,
|
|
668
|
-
'回答提交失败,请重新发送当前问题的答案。',
|
|
664
|
+
t('回答提交失败,请重新发送当前问题的答案。'),
|
|
669
665
|
pending.contextToken,
|
|
670
666
|
pending.runId,
|
|
671
667
|
).catch(() => undefined);
|
|
@@ -711,7 +707,7 @@ export class WeixinHarnessBridge {
|
|
|
711
707
|
});
|
|
712
708
|
await this.#send(
|
|
713
709
|
actor,
|
|
714
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
710
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
715
711
|
contextToken,
|
|
716
712
|
runId,
|
|
717
713
|
).catch(() => undefined);
|
|
@@ -797,7 +793,7 @@ export class WeixinHarnessBridge {
|
|
|
797
793
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
798
794
|
await this.#send(
|
|
799
795
|
nonEmptyString(message?.from_user_id),
|
|
800
|
-
INTERACTION_RESOLVED_TEXT,
|
|
796
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
801
797
|
nonEmptyString(message?.context_token) ?? undefined,
|
|
802
798
|
nonEmptyString(message?.run_id) ?? undefined,
|
|
803
799
|
).catch(() => undefined);
|
|
@@ -845,7 +841,7 @@ export class WeixinHarnessBridge {
|
|
|
845
841
|
}
|
|
846
842
|
await this.#send(
|
|
847
843
|
nonEmptyString(message?.from_user_id),
|
|
848
|
-
GENERIC_PROCESSING_ERROR,
|
|
844
|
+
GENERIC_PROCESSING_ERROR(),
|
|
849
845
|
nonEmptyString(message?.context_token) ?? undefined,
|
|
850
846
|
nonEmptyString(message?.run_id) ?? undefined,
|
|
851
847
|
).catch(() => undefined);
|
|
@@ -869,73 +865,40 @@ export class WeixinHarnessBridge {
|
|
|
869
865
|
}
|
|
870
866
|
|
|
871
867
|
async #deliverArtifacts(toUserId, replyTo, artifacts, contextToken, runId, baseReceipt) {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
);
|
|
908
|
-
let noticeSent = false;
|
|
909
|
-
const providerMessageIds = await this.#send(
|
|
910
|
-
toUserId,
|
|
911
|
-
artifactFailureText(artifact?.fileName, error),
|
|
912
|
-
contextToken,
|
|
913
|
-
runId,
|
|
914
|
-
).then((ids) => {
|
|
915
|
-
noticeSent = true;
|
|
916
|
-
return ids;
|
|
917
|
-
}).catch(() => []);
|
|
918
|
-
const failureReceipt = createArtifactFailureReceipt({
|
|
919
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
920
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
921
|
-
error,
|
|
922
|
-
providerMessageIds,
|
|
923
|
-
});
|
|
924
|
-
receipts.push(failureReceipt);
|
|
925
|
-
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
926
|
-
} finally {
|
|
927
|
-
releaseOutboundArtifact(artifact);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
const receipt = receipts.length === 0
|
|
931
|
-
? null
|
|
932
|
-
: receipts.length === 1
|
|
933
|
-
? receipts[0]
|
|
934
|
-
: mergeDeliveryReceipts({
|
|
935
|
-
deliveryId: replyTo,
|
|
936
|
-
presentation: baseReceipt ? 'weixin-text-and-files' : 'weixin-files',
|
|
937
|
-
receipts,
|
|
938
|
-
});
|
|
939
|
-
return { receipt, userVisible };
|
|
868
|
+
const sendArtifact = (method, file) => this.#api[method]({
|
|
869
|
+
baseUrl: this.#baseUrl,
|
|
870
|
+
token: this.#token,
|
|
871
|
+
toUserId,
|
|
872
|
+
file,
|
|
873
|
+
contextToken,
|
|
874
|
+
runId,
|
|
875
|
+
signal: this.#signal,
|
|
876
|
+
});
|
|
877
|
+
const delivery = await deliverOutboundArtifacts({
|
|
878
|
+
artifacts,
|
|
879
|
+
baseReceipt,
|
|
880
|
+
deliveryId: replyTo,
|
|
881
|
+
aggregatePresentation: baseReceipt ? 'weixin-text-and-files' : 'weixin-files',
|
|
882
|
+
channelKey: 'weixin',
|
|
883
|
+
signal: this.#signal,
|
|
884
|
+
sendImage: typeof this.#api.sendImage === 'function'
|
|
885
|
+
? (file) => sendArtifact('sendImage', file)
|
|
886
|
+
: undefined,
|
|
887
|
+
sendFile: typeof this.#api.sendFile === 'function'
|
|
888
|
+
? (file) => sendArtifact('sendFile', file)
|
|
889
|
+
: undefined,
|
|
890
|
+
sendFailureNotice: (artifact, error) => this.#send(
|
|
891
|
+
toUserId,
|
|
892
|
+
artifactFailureText(artifact?.fileName, error),
|
|
893
|
+
contextToken,
|
|
894
|
+
runId,
|
|
895
|
+
),
|
|
896
|
+
logger: this.#logger,
|
|
897
|
+
});
|
|
898
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
899
|
+
+ delivery.artifactsSent;
|
|
900
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
901
|
+
+ delivery.artifactSendErrors;
|
|
902
|
+
return { receipt: delivery.receipt, userVisible: delivery.userVisible };
|
|
940
903
|
}
|
|
941
904
|
}
|