@xmanrui/dsh-im 1.5.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 +16 -4
- package/README.md +16 -4
- package/lib/index.js +208 -178
- 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 +2 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
- 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 +131 -123
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +5 -4
- 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 +57 -52
- 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 +1 -1
- package/src/channels/shared/semantic/delivery.mjs +38 -0
- package/src/channels/shared/text-harness-bridge.mjs +130 -63
- 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 +72 -6
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +373 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +59 -55
- 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-bridge.mjs +50 -49
- 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 +5 -4
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
|
@@ -40,8 +40,11 @@ import {
|
|
|
40
40
|
providerMessageIdsFor,
|
|
41
41
|
} from '../shared/semantic/delivery.mjs';
|
|
42
42
|
import { sendMarkdownReply } from './markdown-reply.mjs';
|
|
43
|
+
import { t } from '../shared/i18n.mjs';
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
function interactionResolvedText() {
|
|
46
|
+
return t('这个问题已在其他客户端处理,无需再次回答。');
|
|
47
|
+
}
|
|
45
48
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
46
49
|
|
|
47
50
|
export const QQ_IMAGE_HOSTS = Object.freeze([
|
|
@@ -55,28 +58,30 @@ export const QQ_IMAGE_HOSTS = Object.freeze([
|
|
|
55
58
|
|
|
56
59
|
const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
function helpText() {
|
|
62
|
+
return [
|
|
63
|
+
t('QQ 机器人已连接 DeepSeek Harness。'),
|
|
64
|
+
'',
|
|
65
|
+
t('直接发送文字、图片或文件即可继续当前会话。'),
|
|
66
|
+
t('/new 开启一个全新会话'),
|
|
67
|
+
t('/compact 压缩当前会话的较早上下文'),
|
|
68
|
+
t('/workspace 工作区绝对路径 切换工作区'),
|
|
69
|
+
t('/workspacelist 列出工作区绝对路径'),
|
|
70
|
+
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
71
|
+
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
72
|
+
t('/models 按序号列出所有可用模型'),
|
|
73
|
+
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
|
|
74
|
+
t('示例:先发 /models,再发 /model 2'),
|
|
75
|
+
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
76
|
+
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
77
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
78
|
+
t('/preset --default 跟随 Host 默认'),
|
|
79
|
+
t('/stop 停止当前任务'),
|
|
80
|
+
t('/steer 补充指令 纠偏当前任务'),
|
|
81
|
+
t('/status 检查连接状态'),
|
|
82
|
+
t('/help 显示本帮助'),
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
80
85
|
|
|
81
86
|
function conversationKey(message) {
|
|
82
87
|
return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
|
|
@@ -167,35 +172,35 @@ function nonEmptyString(value) {
|
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
function artifactFailureText(fileName, error) {
|
|
170
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
175
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
171
176
|
if (error?.name === 'UploadDailyLimitExceededError') {
|
|
172
|
-
return
|
|
177
|
+
return t('结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。', { name });
|
|
173
178
|
}
|
|
174
179
|
switch (error?.code) {
|
|
175
180
|
case 'artifact-delivery-uncertain':
|
|
176
|
-
return
|
|
181
|
+
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
177
182
|
case 'artifact-permission-required':
|
|
178
|
-
return
|
|
183
|
+
return t('结果文件「{name}」已生成,但当前 QQ 机器人没有文件消息权限。', { name });
|
|
179
184
|
case 'artifact-too-large':
|
|
180
|
-
return
|
|
185
|
+
return t('结果文件「{name}」超过当前 QQ 机器人可发送的文件大小,未发送。', { name });
|
|
181
186
|
case 'artifact-empty':
|
|
182
|
-
return
|
|
187
|
+
return t('结果文件「{name}」为空,QQ 不允许发送空文件。', { name });
|
|
183
188
|
case 'artifact-changed':
|
|
184
189
|
case 'artifact-invalid':
|
|
185
190
|
case 'artifact-unavailable':
|
|
186
|
-
return
|
|
191
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
187
192
|
case 'artifact-rate-limited':
|
|
188
|
-
return
|
|
193
|
+
return t('结果文件「{name}」暂时被 QQ 限流,未能发送,请稍后重试。', { name });
|
|
189
194
|
case 'artifact-provider-rejected':
|
|
190
|
-
return
|
|
195
|
+
return t('结果文件「{name}」已生成,但 QQ 拒绝了该文件或文件消息。', { name });
|
|
191
196
|
default:
|
|
192
|
-
return
|
|
197
|
+
return t('结果文件「{name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。', { name });
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
function answerTextForDelivery(answer, artifacts) {
|
|
197
202
|
if (typeof answer === 'string' && answer.trim()) return answer;
|
|
198
|
-
return artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
203
|
+
return artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
function qqArtifactError(error, { dispatched = false } = {}) {
|
|
@@ -420,7 +425,7 @@ export class QqHarnessBridge {
|
|
|
420
425
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
421
426
|
this.#status.lastError = error?.message ?? String(error);
|
|
422
427
|
this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
|
|
423
|
-
return this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
|
428
|
+
return this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
|
424
429
|
.catch(() => undefined);
|
|
425
430
|
}).finally(() => {
|
|
426
431
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -609,25 +614,25 @@ export class QqHarnessBridge {
|
|
|
609
614
|
let stream = null;
|
|
610
615
|
try {
|
|
611
616
|
if (!text && !hasImages && !hasFiles) {
|
|
612
|
-
await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
|
|
617
|
+
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
613
618
|
await this.#state.markSeen(messageId);
|
|
614
619
|
return;
|
|
615
620
|
}
|
|
616
621
|
const command = text.toLowerCase();
|
|
617
622
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
618
|
-
await this.#bot.sendText(target,
|
|
623
|
+
await this.#bot.sendText(target, helpText());
|
|
619
624
|
await this.#state.markSeen(messageId);
|
|
620
625
|
return;
|
|
621
626
|
}
|
|
622
627
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
623
628
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
624
|
-
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
|
629
|
+
await this.#bot.sendText(target, t('QQ 机器人与 DeepSeek Harness 连接正常。'));
|
|
625
630
|
await this.#state.markSeen(messageId);
|
|
626
631
|
return;
|
|
627
632
|
}
|
|
628
633
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
629
634
|
await this.#state.clearSession(key);
|
|
630
|
-
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
635
|
+
await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
|
|
631
636
|
await this.#state.markSeen(messageId);
|
|
632
637
|
return;
|
|
633
638
|
}
|
|
@@ -771,7 +776,7 @@ export class QqHarnessBridge {
|
|
|
771
776
|
this.#logger.warn?.('[dsh-im:qq] unable to cancel a stopped QQ stream:', streamError);
|
|
772
777
|
}
|
|
773
778
|
try {
|
|
774
|
-
await this.#bot.sendText(target, '已停止。');
|
|
779
|
+
await this.#bot.sendText(target, t('已停止。'));
|
|
775
780
|
} catch (sendError) {
|
|
776
781
|
this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
|
|
777
782
|
}
|
|
@@ -791,7 +796,7 @@ export class QqHarnessBridge {
|
|
|
791
796
|
target,
|
|
792
797
|
inboundFileUserMessage(error)
|
|
793
798
|
?? imagePromptUserMessage(error)
|
|
794
|
-
?? '消息处理失败,请稍后重试。',
|
|
799
|
+
?? t('消息处理失败,请稍后重试。'),
|
|
795
800
|
);
|
|
796
801
|
await this.#state.markSeen(messageId);
|
|
797
802
|
} catch (sendError) {
|
|
@@ -818,14 +823,14 @@ export class QqHarnessBridge {
|
|
|
818
823
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
819
824
|
const text = nonEmptyString(safeText(message));
|
|
820
825
|
if (!text || hasQqImageAttachments(message) || hasQqFileAttachments(message)) {
|
|
821
|
-
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
|
826
|
+
await this.#bot.sendText(message.replyTarget, t('请用文字回答当前问题。'));
|
|
822
827
|
return;
|
|
823
828
|
}
|
|
824
829
|
|
|
825
830
|
const pending = this.#pendingInteractions.get(key);
|
|
826
831
|
if (!pending || pending !== expected || pending.submitting) {
|
|
827
832
|
if (claimed && (!pending || pending !== expected)) {
|
|
828
|
-
await this.#bot.sendText(message.replyTarget,
|
|
833
|
+
await this.#bot.sendText(message.replyTarget, interactionResolvedText());
|
|
829
834
|
return;
|
|
830
835
|
}
|
|
831
836
|
return this.#enqueueMessage(message, messageId, key, {
|
|
@@ -838,7 +843,7 @@ export class QqHarnessBridge {
|
|
|
838
843
|
try {
|
|
839
844
|
await this.#presentInteraction(pending);
|
|
840
845
|
} catch {
|
|
841
|
-
this.#status.lastError = 'QQ 交互问题发送失败。';
|
|
846
|
+
this.#status.lastError = t('QQ 交互问题发送失败。');
|
|
842
847
|
this.#logger.error?.('[dsh-im:qq] failed to retry an interaction question');
|
|
843
848
|
pending.interaction.reconnect?.();
|
|
844
849
|
return;
|
|
@@ -846,7 +851,7 @@ export class QqHarnessBridge {
|
|
|
846
851
|
const presentedPending = this.#pendingInteractions.get(key);
|
|
847
852
|
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
|
848
853
|
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
849
|
-
await this.#bot.sendText(message.replyTarget,
|
|
854
|
+
await this.#bot.sendText(message.replyTarget, interactionResolvedText())
|
|
850
855
|
.catch(() => undefined);
|
|
851
856
|
return;
|
|
852
857
|
}
|
|
@@ -869,7 +874,7 @@ export class QqHarnessBridge {
|
|
|
869
874
|
try {
|
|
870
875
|
await this.#presentInteraction(pending);
|
|
871
876
|
} catch {
|
|
872
|
-
this.#status.lastError = 'QQ 交互问题发送失败。';
|
|
877
|
+
this.#status.lastError = t('QQ 交互问题发送失败。');
|
|
873
878
|
this.#logger.error?.('[dsh-im:qq] failed to send the next interaction question');
|
|
874
879
|
pending.interaction.reconnect?.();
|
|
875
880
|
}
|
|
@@ -891,16 +896,16 @@ export class QqHarnessBridge {
|
|
|
891
896
|
if (this.#signal?.aborted) return;
|
|
892
897
|
if (error?.code === 'interaction-not-pending') {
|
|
893
898
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
894
|
-
await this.#bot.sendText(pending.target,
|
|
899
|
+
await this.#bot.sendText(pending.target, interactionResolvedText()).catch(() => undefined);
|
|
895
900
|
return;
|
|
896
901
|
}
|
|
897
902
|
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
898
903
|
pending.submitting = false;
|
|
899
904
|
pending.answers.pop();
|
|
900
905
|
pending.index -= 1;
|
|
901
|
-
this.#status.lastError = '回答提交失败。';
|
|
906
|
+
this.#status.lastError = t('回答提交失败。');
|
|
902
907
|
this.#logger.error?.('[dsh-im:qq] failed to answer a Harness interaction');
|
|
903
|
-
await this.#bot.sendText(pending.target, '回答提交失败,请重新发送当前问题的答案。')
|
|
908
|
+
await this.#bot.sendText(pending.target, t('回答提交失败,请重新发送当前问题的答案。'))
|
|
904
909
|
.catch(() => undefined);
|
|
905
910
|
}
|
|
906
911
|
}
|
|
@@ -945,7 +950,7 @@ export class QqHarnessBridge {
|
|
|
945
950
|
});
|
|
946
951
|
await this.#bot.sendText(
|
|
947
952
|
target,
|
|
948
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
953
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
949
954
|
).catch(() => undefined);
|
|
950
955
|
return;
|
|
951
956
|
}
|
|
@@ -1030,7 +1035,7 @@ export class QqHarnessBridge {
|
|
|
1030
1035
|
await this.#state.markSeen(messageId);
|
|
1031
1036
|
this.#status.messagesReceived += 1;
|
|
1032
1037
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
1033
|
-
await this.#bot.sendText(message.replyTarget,
|
|
1038
|
+
await this.#bot.sendText(message.replyTarget, interactionResolvedText()).catch(() => undefined);
|
|
1034
1039
|
}
|
|
1035
1040
|
|
|
1036
1041
|
#takePendingInteraction(key, interactionId) {
|
|
@@ -1072,7 +1077,7 @@ export class QqHarnessBridge {
|
|
|
1072
1077
|
if (!this.#state.hasSeen(messageId)) {
|
|
1073
1078
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
1074
1079
|
}
|
|
1075
|
-
await this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
|
1080
|
+
await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
|
1076
1081
|
.catch(() => undefined);
|
|
1077
1082
|
}
|
|
1078
1083
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
4
|
+
import { t } from '../shared/i18n.mjs';
|
|
4
5
|
import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
|
|
5
6
|
|
|
6
7
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
|
|
@@ -77,14 +78,14 @@ export class QqController {
|
|
|
77
78
|
if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
|
|
78
79
|
const appSecret = await this.#resolveSecret(config.secretRef);
|
|
79
80
|
if (!appSecret) {
|
|
80
|
-
this.#errors.set(config.botId, safeError('missing-secret', 'QQ 机器人凭据缺失,请移除后重新扫码。'));
|
|
81
|
+
this.#errors.set(config.botId, safeError('missing-secret', t('QQ 机器人凭据缺失,请移除后重新扫码。')));
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
try {
|
|
84
85
|
await this.#startRuntime(config, appSecret);
|
|
85
86
|
this.#errors.delete(config.botId);
|
|
86
87
|
} catch (error) {
|
|
87
|
-
this.#errors.set(config.botId, safeError('connection-failed', 'QQ 连接未就绪,插件会自动重试。'));
|
|
88
|
+
this.#errors.set(config.botId, safeError('connection-failed', t('QQ 连接未就绪,插件会自动重试。')));
|
|
88
89
|
this.#logger.warn?.(`[dsh-im:qq] bot ${config.botId} failed to initialize:`, error);
|
|
89
90
|
} finally {
|
|
90
91
|
this.#touch();
|
|
@@ -147,7 +148,7 @@ export class QqController {
|
|
|
147
148
|
onFailure: (error) => {
|
|
148
149
|
if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES.has(record.state)) return;
|
|
149
150
|
record.state = 'failed';
|
|
150
|
-
record.error = safeError('qr-connect-failed', 'QQ 扫码服务暂时不可用,请重新生成二维码。');
|
|
151
|
+
record.error = safeError('qr-connect-failed', t('QQ 扫码服务暂时不可用,请重新生成二维码。'));
|
|
151
152
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
152
153
|
this.#touch();
|
|
153
154
|
firstQrReject(error);
|
|
@@ -158,10 +159,10 @@ export class QqController {
|
|
|
158
159
|
} catch (error) {
|
|
159
160
|
if (record.controller.signal.aborted) {
|
|
160
161
|
record.state = 'cancelled';
|
|
161
|
-
record.error = safeError('cancelled', '扫码绑定已取消。');
|
|
162
|
+
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
|
162
163
|
} else if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
|
163
164
|
record.state = 'failed';
|
|
164
|
-
record.error = safeError('qr-start-failed', '无法生成 QQ 二维码,请稍后重试。');
|
|
165
|
+
record.error = safeError('qr-start-failed', t('无法生成 QQ 二维码,请稍后重试。'));
|
|
165
166
|
}
|
|
166
167
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
167
168
|
this.#touch();
|
|
@@ -207,7 +208,7 @@ export class QqController {
|
|
|
207
208
|
await this.#startRuntime(config, normalizedSecret);
|
|
208
209
|
this.#errors.delete(identity.botId);
|
|
209
210
|
} catch (error) {
|
|
210
|
-
this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
|
|
211
|
+
this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
|
|
211
212
|
this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} credential connection failed:`, error);
|
|
212
213
|
}
|
|
213
214
|
this.#touch();
|
|
@@ -223,7 +224,7 @@ export class QqController {
|
|
|
223
224
|
record.dispose?.();
|
|
224
225
|
await record.task?.catch(() => undefined);
|
|
225
226
|
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
|
226
|
-
record.error ??= safeError('cancelled', '扫码绑定已取消。');
|
|
227
|
+
record.error ??= safeError('cancelled', t('扫码绑定已取消。'));
|
|
227
228
|
}
|
|
228
229
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
229
230
|
this.#touch();
|
|
@@ -240,7 +241,7 @@ export class QqController {
|
|
|
240
241
|
await this.#startRuntime(config, secret);
|
|
241
242
|
this.#errors.delete(botId);
|
|
242
243
|
} catch (error) {
|
|
243
|
-
this.#errors.set(botId, safeError('connection-failed', 'QQ 连接仍未就绪,请稍后重试。'));
|
|
244
|
+
this.#errors.set(botId, safeError('connection-failed', t('QQ 连接仍未就绪,请稍后重试。')));
|
|
244
245
|
throw error;
|
|
245
246
|
} finally {
|
|
246
247
|
this.#touch();
|
|
@@ -255,12 +256,12 @@ export class QqController {
|
|
|
255
256
|
return this.#withBotTransition(botId, async () => {
|
|
256
257
|
const runtime = this.#runtimes.get(botId);
|
|
257
258
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
258
|
-
const error = new Error('QQ机器人尚未连接');
|
|
259
|
+
const error = new Error(t('QQ机器人尚未连接'));
|
|
259
260
|
error.code = 'test-target-unavailable';
|
|
260
261
|
throw error;
|
|
261
262
|
}
|
|
262
263
|
await runtime.sendConnectionTest(connectionTestMessage(
|
|
263
|
-
|
|
264
|
+
t('QQ 机器人({appId})', { appId: maskQqAppId(config.appId) }),
|
|
264
265
|
));
|
|
265
266
|
return { sent: true };
|
|
266
267
|
});
|
|
@@ -306,11 +307,11 @@ export class QqController {
|
|
|
306
307
|
state,
|
|
307
308
|
connected,
|
|
308
309
|
configured: true,
|
|
309
|
-
bot: { name: 'QQ机器人', appIdMasked: maskQqAppId(config.appId) },
|
|
310
|
+
bot: { name: t('QQ机器人'), appIdMasked: maskQqAppId(config.appId) },
|
|
310
311
|
health: {
|
|
311
312
|
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
|
312
|
-
summary: connected ? 'QQ WebSocket 长连接运行正常'
|
|
313
|
-
: state === 'error' ? 'QQ 连接未就绪,插件会自动重试' : 'QQ 连接当前离线',
|
|
313
|
+
summary: connected ? t('QQ WebSocket 长连接运行正常')
|
|
314
|
+
: state === 'error' ? t('QQ 连接未就绪,插件会自动重试') : t('QQ 连接当前离线'),
|
|
314
315
|
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
|
315
316
|
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
|
316
317
|
},
|
|
@@ -364,10 +365,10 @@ export class QqController {
|
|
|
364
365
|
} catch (error) {
|
|
365
366
|
if (record.controller.signal.aborted) {
|
|
366
367
|
record.state = 'cancelled';
|
|
367
|
-
record.error = safeError('cancelled', '扫码绑定已取消。');
|
|
368
|
+
record.error = safeError('cancelled', t('扫码绑定已取消。'));
|
|
368
369
|
} else {
|
|
369
370
|
record.state = 'failed';
|
|
370
|
-
record.error = safeError('activation-failed', 'QQ 已授权,但无法安全保存接入配置。');
|
|
371
|
+
record.error = safeError('activation-failed', t('QQ 已授权,但无法安全保存接入配置。'));
|
|
371
372
|
this.#logger.error?.('[dsh-im:qq] provisioning failed:', error);
|
|
372
373
|
}
|
|
373
374
|
} finally {
|
|
@@ -417,7 +418,7 @@ export class QqController {
|
|
|
417
418
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
418
419
|
throw error;
|
|
419
420
|
}
|
|
420
|
-
this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
|
|
421
|
+
this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
|
|
421
422
|
this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} activation connection failed:`, error);
|
|
422
423
|
}
|
|
423
424
|
this.#touch();
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
connectionTestTarget,
|
|
5
5
|
connectionTestTargetUnavailable,
|
|
6
6
|
} from '../shared/connection-test.mjs';
|
|
7
|
+
import { t } from '../shared/i18n.mjs';
|
|
7
8
|
import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
|
|
8
9
|
|
|
9
10
|
function timeoutError() {
|
|
@@ -73,7 +74,7 @@ export class QqRuntime {
|
|
|
73
74
|
|
|
74
75
|
async sendConnectionTest(text) {
|
|
75
76
|
if (!this.#status.ready || !this.#bot) {
|
|
76
|
-
throw connectionTestTargetUnavailable('QQ机器人');
|
|
77
|
+
throw connectionTestTargetUnavailable(t('QQ机器人'));
|
|
77
78
|
}
|
|
78
79
|
const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
|
|
79
80
|
? this.#config.ownerUserOpenid.trim()
|
|
@@ -88,7 +89,7 @@ export class QqRuntime {
|
|
|
88
89
|
: (ownerUserOpenid && ownerUserOpenid !== '*'
|
|
89
90
|
? { scope: 'c2c', targetId: ownerUserOpenid }
|
|
90
91
|
: null);
|
|
91
|
-
if (!target) throw connectionTestTargetUnavailable('QQ机器人');
|
|
92
|
+
if (!target) throw connectionTestTargetUnavailable(t('QQ机器人'));
|
|
92
93
|
await this.#bot.sendText(target, text);
|
|
93
94
|
return { sent: true };
|
|
94
95
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
1
3
|
/** Matches DeepSeek Harness agent-preset directory ids. */
|
|
2
4
|
export const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
3
5
|
|
|
@@ -17,7 +19,7 @@ export function validateAgentPresetId(value) {
|
|
|
17
19
|
if (value == null || value === '') return null;
|
|
18
20
|
const id = normalizeAgentPresetId(value);
|
|
19
21
|
if (!id) {
|
|
20
|
-
const error = new Error('Agent Preset 无效。');
|
|
22
|
+
const error = new Error(t('Agent Preset 无效。'));
|
|
21
23
|
error.code = 'agent-preset-invalid';
|
|
22
24
|
throw error;
|
|
23
25
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
1
2
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
2
3
|
|
|
3
4
|
const COMPACT_COMMAND = /^\/compact(?=$|\s)([\s\S]*)$/i;
|
|
@@ -41,27 +42,27 @@ function compactResultText(result) {
|
|
|
41
42
|
const text = result.text?.trim() ?? '';
|
|
42
43
|
const compacted = /^Compacted (\d+) history items \(~(\d+) tokens\)\.$/u.exec(text);
|
|
43
44
|
if (compacted) {
|
|
44
|
-
return
|
|
45
|
+
return t('已压缩 {count} 条历史记录(约 {tokens} 个 token)。', { count: compacted[1], tokens: compacted[2] });
|
|
45
46
|
}
|
|
46
|
-
if (COMPACT_RESULT_TEXT.has(text)) return COMPACT_RESULT_TEXT.get(text);
|
|
47
|
+
if (COMPACT_RESULT_TEXT.has(text)) return t(COMPACT_RESULT_TEXT.get(text));
|
|
47
48
|
if (text) return text;
|
|
48
|
-
return result.kind === 'success' ? '上下文压缩完成。' : '上下文压缩失败。';
|
|
49
|
+
return result.kind === 'success' ? t('上下文压缩完成。') : t('上下文压缩失败。');
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function compactErrorMessage(error) {
|
|
52
53
|
const code = error?.code ?? error?.failure?.code;
|
|
53
54
|
if (code === 'session-not-found') {
|
|
54
|
-
return '当前聊天绑定的会话已不存在,请发送新消息开启会话。';
|
|
55
|
+
return t('当前聊天绑定的会话已不存在,请发送新消息开启会话。');
|
|
55
56
|
}
|
|
56
|
-
if (code === 'agent-busy') return '当前会话正在生成回复,请稍后重试。';
|
|
57
|
-
if (code === 'cancelled' || error?.name === 'AbortError') return '上下文压缩已取消。';
|
|
57
|
+
if (code === 'agent-busy') return t('当前会话正在生成回复,请稍后重试。');
|
|
58
|
+
if (code === 'cancelled' || error?.name === 'AbortError') return t('上下文压缩已取消。');
|
|
58
59
|
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
59
|
-
return '工作区或机器人状态已发生变化,请重试。';
|
|
60
|
+
return t('工作区或机器人状态已发生变化,请重试。');
|
|
60
61
|
}
|
|
61
62
|
if (code === 'commands-unavailable') {
|
|
62
|
-
return '当前 Harness 暂不支持从机器人执行上下文压缩。';
|
|
63
|
+
return t('当前 Harness 暂不支持从机器人执行上下文压缩。');
|
|
63
64
|
}
|
|
64
|
-
return '上下文压缩失败,请稍后重试。';
|
|
65
|
+
return t('上下文压缩失败,请稍后重试。');
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -72,21 +73,21 @@ export async function runCompactCommand(text, harness, state, conversationKey, o
|
|
|
72
73
|
if (typeof text !== 'string') return null;
|
|
73
74
|
const match = COMPACT_COMMAND.exec(text.trim());
|
|
74
75
|
if (!match) return null;
|
|
75
|
-
if (match[1].trim()) return commandResult(COMPACT_USAGE);
|
|
76
|
+
if (match[1].trim()) return commandResult(t(COMPACT_USAGE));
|
|
76
77
|
if (typeof state?.sessionFor !== 'function') {
|
|
77
|
-
return commandResult('当前机器人没有可用的会话状态。');
|
|
78
|
+
return commandResult(t('当前机器人没有可用的会话状态。'));
|
|
78
79
|
}
|
|
79
80
|
const sessionId = state.sessionFor(conversationKey);
|
|
80
81
|
if (typeof sessionId !== 'string' || !sessionId) {
|
|
81
|
-
return commandResult('当前聊天还没有可压缩的会话,请先发送一条消息。');
|
|
82
|
+
return commandResult(t('当前聊天还没有可压缩的会话,请先发送一条消息。'));
|
|
82
83
|
}
|
|
83
84
|
if (typeof harness?.executeCommand !== 'function') {
|
|
84
|
-
return commandResult('当前机器人暂不支持上下文压缩。');
|
|
85
|
+
return commandResult(t('当前机器人暂不支持上下文压缩。'));
|
|
85
86
|
}
|
|
86
87
|
try {
|
|
87
88
|
const execution = await harness.executeCommand(sessionId, '/compact', options);
|
|
88
89
|
if (execution === undefined) {
|
|
89
|
-
return commandResult('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。');
|
|
90
|
+
return commandResult(t('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。'));
|
|
90
91
|
}
|
|
91
92
|
return commandResult(compactResultText(execution?.result));
|
|
92
93
|
} catch (error) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
1
3
|
const targets = new WeakMap();
|
|
2
4
|
|
|
3
5
|
export const CONNECTION_TEST_STATE_IDENTITY = Symbol('dsh-im.connection-test-state-identity');
|
|
@@ -25,13 +27,13 @@ export function connectionTestTarget(state) {
|
|
|
25
27
|
return target ? structuredClone(target) : null;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export function connectionTestMessage(botName, channelLabel = '机器人') {
|
|
30
|
+
export function connectionTestMessage(botName, channelLabel = t('机器人')) {
|
|
29
31
|
const name = cleanText(botName) ?? channelLabel;
|
|
30
|
-
return
|
|
32
|
+
return `${t('✅ DeepSeek Harness 连接测试成功')}\n${t('这条消息由插件页面中的“{name}”机器人卡片发出。', { name })}`;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
|
34
|
-
const error = new Error(
|
|
35
|
+
export function connectionTestTargetUnavailable(channelLabel = t('机器人')) {
|
|
36
|
+
const error = new Error(t('{channelLabel}尚未收到可用于测试的私聊消息。', { channelLabel }));
|
|
35
37
|
error.code = 'test-target-unavailable';
|
|
36
38
|
return error;
|
|
37
39
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
1
3
|
const CONTROL_COMMAND = /^\/(?:stop|steer)(?=$|\s)/iu;
|
|
2
4
|
const STOP_COMMAND = /^\/stop(?=$|\s)/iu;
|
|
3
5
|
const STOP_USAGE = '用法:/stop(不带参数)';
|
|
@@ -40,35 +42,35 @@ export async function runControlCommand(text, harness, state, key, {
|
|
|
40
42
|
const command = text.trim();
|
|
41
43
|
const stop = STOP_COMMAND.test(command);
|
|
42
44
|
|
|
43
|
-
if (hasImages) return commandResult(TEXT_ONLY);
|
|
45
|
+
if (hasImages) return commandResult(t(TEXT_ONLY));
|
|
44
46
|
|
|
45
47
|
if (stop) {
|
|
46
|
-
if (!/^\/stop$/iu.test(command)) return commandResult(STOP_USAGE);
|
|
48
|
+
if (!/^\/stop$/iu.test(command)) return commandResult(t(STOP_USAGE));
|
|
47
49
|
const session = boundSession(harness, state, key);
|
|
48
|
-
if (!session) return commandResult('当前聊天没有正在运行的任务。');
|
|
50
|
+
if (!session) return commandResult(t('当前聊天没有正在运行的任务。'));
|
|
49
51
|
if (typeof session.stopActiveTurn !== 'function') {
|
|
50
52
|
throw new TypeError('Harness session does not support stopping active turns');
|
|
51
53
|
}
|
|
52
54
|
const stopped = await session.stopActiveTurn(control, requestOptions(signal));
|
|
53
55
|
return stopped
|
|
54
|
-
? commandResult('已请求停止当前任务。', { stopped: true })
|
|
55
|
-
: commandResult('当前聊天没有正在运行的任务。');
|
|
56
|
+
? commandResult(t('已请求停止当前任务。'), { stopped: true })
|
|
57
|
+
: commandResult(t('当前聊天没有正在运行的任务。'));
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
const match = /^\/steer(?:\s+([\s\S]*))?$/iu.exec(command);
|
|
59
61
|
const instruction = match?.[1]?.trim() ?? '';
|
|
60
|
-
if (!instruction) return commandResult(STEER_USAGE);
|
|
62
|
+
if (!instruction) return commandResult(t(STEER_USAGE));
|
|
61
63
|
if (pendingInteraction) {
|
|
62
64
|
return commandResult([
|
|
63
|
-
'当前任务正在等待你的回答或审批。',
|
|
65
|
+
t('当前任务正在等待你的回答或审批。'),
|
|
64
66
|
'',
|
|
65
|
-
'请先处理当前请求,或者发送 /stop 停止任务。',
|
|
67
|
+
t('请先处理当前请求,或者发送 /stop 停止任务。'),
|
|
66
68
|
].join('\n'));
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
const session = boundSession(harness, state, key);
|
|
70
72
|
if (!session) {
|
|
71
|
-
return commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
|
73
|
+
return commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
|
|
72
74
|
}
|
|
73
75
|
if (typeof session.steerActiveTurn !== 'function') {
|
|
74
76
|
throw new TypeError('Harness session does not support steering active turns');
|
|
@@ -79,6 +81,6 @@ export async function runControlCommand(text, harness, state, key, {
|
|
|
79
81
|
requestOptions(signal),
|
|
80
82
|
);
|
|
81
83
|
return steered
|
|
82
|
-
? commandResult('已提交补充指令,Agent 会在下一步读取。')
|
|
83
|
-
: commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
|
84
|
+
? commandResult(t('已提交补充指令,Agent 会在下一步读取。'))
|
|
85
|
+
: commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
|
|
84
86
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
1
3
|
export function splitMessageText(value, limit) {
|
|
2
4
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
3
5
|
if (!text) return [];
|
|
@@ -15,7 +17,7 @@ export function splitMessageText(value, limit) {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function createEditableMessageStream({
|
|
18
|
-
initialText = '正在处理…',
|
|
20
|
+
initialText = t('正在处理…'),
|
|
19
21
|
limit,
|
|
20
22
|
updateIntervalMs = 800,
|
|
21
23
|
create,
|
|
@@ -77,7 +79,7 @@ export function createEditableMessageStream({
|
|
|
77
79
|
pending = null;
|
|
78
80
|
await inFlight?.catch(() => undefined);
|
|
79
81
|
const chunks = splitMessageText(text, limit);
|
|
80
|
-
const first = chunks[0] ?? '处理完成。';
|
|
82
|
+
const first = chunks[0] ?? t('处理完成。');
|
|
81
83
|
if (first !== lastSent) await edit(messageId, first);
|
|
82
84
|
lastSent = first;
|
|
83
85
|
for (const chunk of chunks.slice(1)) {
|