@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
|
@@ -32,19 +32,19 @@ import {
|
|
|
32
32
|
prefetchInboundFiles,
|
|
33
33
|
} from '../shared/inbound-file.mjs';
|
|
34
34
|
import {
|
|
35
|
-
materializeOutboundArtifact,
|
|
36
|
-
releaseOutboundArtifact,
|
|
37
35
|
trackOutboundArtifactProviderPromise,
|
|
38
36
|
} from '../shared/semantic/artifact.mjs';
|
|
37
|
+
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
39
38
|
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
39
|
createDeliveryReceipt,
|
|
42
|
-
mergeDeliveryReceipts,
|
|
43
40
|
providerMessageIdsFor,
|
|
44
41
|
} from '../shared/semantic/delivery.mjs';
|
|
45
42
|
import { sendMarkdownReply } from './markdown-reply.mjs';
|
|
43
|
+
import { t } from '../shared/i18n.mjs';
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
function interactionResolvedText() {
|
|
46
|
+
return t('这个问题已在其他客户端处理,无需再次回答。');
|
|
47
|
+
}
|
|
48
48
|
const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
|
|
49
49
|
|
|
50
50
|
export const QQ_IMAGE_HOSTS = Object.freeze([
|
|
@@ -58,28 +58,30 @@ export const QQ_IMAGE_HOSTS = Object.freeze([
|
|
|
58
58
|
|
|
59
59
|
const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
|
83
85
|
|
|
84
86
|
function conversationKey(message) {
|
|
85
87
|
return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
|
|
@@ -170,35 +172,35 @@ function nonEmptyString(value) {
|
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
function artifactFailureText(fileName, error) {
|
|
173
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
175
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
174
176
|
if (error?.name === 'UploadDailyLimitExceededError') {
|
|
175
|
-
return
|
|
177
|
+
return t('结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。', { name });
|
|
176
178
|
}
|
|
177
179
|
switch (error?.code) {
|
|
178
180
|
case 'artifact-delivery-uncertain':
|
|
179
|
-
return
|
|
181
|
+
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
180
182
|
case 'artifact-permission-required':
|
|
181
|
-
return
|
|
183
|
+
return t('结果文件「{name}」已生成,但当前 QQ 机器人没有文件消息权限。', { name });
|
|
182
184
|
case 'artifact-too-large':
|
|
183
|
-
return
|
|
185
|
+
return t('结果文件「{name}」超过当前 QQ 机器人可发送的文件大小,未发送。', { name });
|
|
184
186
|
case 'artifact-empty':
|
|
185
|
-
return
|
|
187
|
+
return t('结果文件「{name}」为空,QQ 不允许发送空文件。', { name });
|
|
186
188
|
case 'artifact-changed':
|
|
187
189
|
case 'artifact-invalid':
|
|
188
190
|
case 'artifact-unavailable':
|
|
189
|
-
return
|
|
191
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
190
192
|
case 'artifact-rate-limited':
|
|
191
|
-
return
|
|
193
|
+
return t('结果文件「{name}」暂时被 QQ 限流,未能发送,请稍后重试。', { name });
|
|
192
194
|
case 'artifact-provider-rejected':
|
|
193
|
-
return
|
|
195
|
+
return t('结果文件「{name}」已生成,但 QQ 拒绝了该文件或文件消息。', { name });
|
|
194
196
|
default:
|
|
195
|
-
return
|
|
197
|
+
return t('结果文件「{name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。', { name });
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
function answerTextForDelivery(answer, artifacts) {
|
|
200
202
|
if (typeof answer === 'string' && answer.trim()) return answer;
|
|
201
|
-
return artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
203
|
+
return artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
function qqArtifactError(error, { dispatched = false } = {}) {
|
|
@@ -210,7 +212,7 @@ function qqArtifactError(error, { dispatched = false } = {}) {
|
|
|
210
212
|
if (status === 401 || status === 403) wrapped.code = 'artifact-permission-required';
|
|
211
213
|
else if (status === 413) wrapped.code = 'artifact-too-large';
|
|
212
214
|
else if (status === 429) wrapped.code = 'artifact-rate-limited';
|
|
213
|
-
else if (
|
|
215
|
+
else if ([400, 404, 405, 406, 410, 415, 422].includes(status)) {
|
|
214
216
|
wrapped.code = 'artifact-provider-rejected';
|
|
215
217
|
} else {
|
|
216
218
|
wrapped.code = dispatched ? 'artifact-delivery-uncertain' : 'artifact-provider-failed';
|
|
@@ -245,6 +247,68 @@ function waitWithSignal(promise, signal) {
|
|
|
245
247
|
});
|
|
246
248
|
}
|
|
247
249
|
|
|
250
|
+
/** Send one materialized artifact through QQ's native image message. */
|
|
251
|
+
export async function sendQqImage(
|
|
252
|
+
bot,
|
|
253
|
+
target,
|
|
254
|
+
file,
|
|
255
|
+
{ signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
|
|
256
|
+
) {
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
if (typeof bot?.sendImage !== 'function') {
|
|
259
|
+
const unavailable = new Error('QQ image delivery is unavailable');
|
|
260
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
261
|
+
throw unavailable;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
266
|
+
const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
267
|
+
const pending = bot.sendImage(
|
|
268
|
+
target,
|
|
269
|
+
{ buffer: file.bytes },
|
|
270
|
+
{ onProgress: () => signal?.throwIfAborted() },
|
|
271
|
+
);
|
|
272
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
273
|
+
return await waitWithSignal(pending, waitSignal);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
276
|
+
throw qqArtifactError(error, { dispatched: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function sendQqFile(
|
|
281
|
+
bot,
|
|
282
|
+
target,
|
|
283
|
+
file,
|
|
284
|
+
{ signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
|
|
285
|
+
) {
|
|
286
|
+
signal?.throwIfAborted();
|
|
287
|
+
if (typeof bot?.sendFile !== 'function') {
|
|
288
|
+
const unavailable = new Error('QQ file delivery is unavailable');
|
|
289
|
+
unavailable.code = 'artifact-provider-unavailable';
|
|
290
|
+
throw unavailable;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
295
|
+
const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
296
|
+
const pending = bot.sendFile(
|
|
297
|
+
target,
|
|
298
|
+
{ buffer: file.bytes },
|
|
299
|
+
{
|
|
300
|
+
fileName: file.fileName,
|
|
301
|
+
onProgress: () => signal?.throwIfAborted(),
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
305
|
+
return await waitWithSignal(pending, waitSignal);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
308
|
+
throw qqArtifactError(error, { dispatched: true });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
248
312
|
function canClaimInteractionReply(message, pending) {
|
|
249
313
|
return pending.questions[pending.index]
|
|
250
314
|
&& nonEmptyString(message?.senderId) === pending.actor
|
|
@@ -361,7 +425,7 @@ export class QqHarnessBridge {
|
|
|
361
425
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
362
426
|
this.#status.lastError = error?.message ?? String(error);
|
|
363
427
|
this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
|
|
364
|
-
return this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
|
428
|
+
return this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
|
365
429
|
.catch(() => undefined);
|
|
366
430
|
}).finally(() => {
|
|
367
431
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -491,80 +555,35 @@ export class QqHarnessBridge {
|
|
|
491
555
|
if (artifacts.length === 0) {
|
|
492
556
|
return { receipt: baseReceipt, failureNoticeVisible: false };
|
|
493
557
|
}
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
signal: this.#signal,
|
|
506
|
-
});
|
|
507
|
-
this.#signal?.throwIfAborted();
|
|
508
|
-
let result;
|
|
509
|
-
try {
|
|
510
|
-
const timeout = AbortSignal.timeout(this.#fileUploadTimeoutMs);
|
|
511
|
-
const waitSignal = this.#signal ? AbortSignal.any([this.#signal, timeout]) : timeout;
|
|
512
|
-
const pending = this.#bot.sendFile(
|
|
513
|
-
target,
|
|
514
|
-
{ buffer: file.bytes },
|
|
515
|
-
{
|
|
516
|
-
fileName: file.fileName,
|
|
517
|
-
onProgress: () => this.#signal?.throwIfAborted(),
|
|
518
|
-
},
|
|
519
|
-
);
|
|
520
|
-
trackOutboundArtifactProviderPromise(file, pending);
|
|
521
|
-
result = await waitWithSignal(pending, waitSignal);
|
|
522
|
-
} catch (error) {
|
|
523
|
-
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
524
|
-
throw qqArtifactError(error, { dispatched: true });
|
|
525
|
-
}
|
|
526
|
-
this.#signal?.throwIfAborted();
|
|
527
|
-
const messageId = nonEmptyString(result?.message?.id);
|
|
528
|
-
receipts.push(createDeliveryReceipt({
|
|
529
|
-
deliveryId: file.deliveryKey,
|
|
530
|
-
presentation: 'qq-file',
|
|
531
|
-
providerMessageIds: messageId ? [messageId] : [],
|
|
532
|
-
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
|
|
533
|
-
}));
|
|
534
|
-
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
|
|
535
|
-
} catch (rawError) {
|
|
536
|
-
if (this.#signal?.aborted) throw rawError;
|
|
537
|
-
const error = qqArtifactError(rawError);
|
|
538
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
539
|
-
this.#logger.warn?.(
|
|
540
|
-
`[dsh-im:qq] result file delivery failed (${error?.code ?? error?.name ?? 'unknown'})`,
|
|
541
|
-
);
|
|
542
|
-
let providerMessageIds = [];
|
|
543
|
-
try {
|
|
544
|
-
const notice = await this.#bot.sendText(target, artifactFailureText(artifact?.fileName, error));
|
|
545
|
-
failureNoticeVisible = true;
|
|
546
|
-
providerMessageIds = providerMessageIdsFor(notice);
|
|
547
|
-
} catch (noticeError) {
|
|
548
|
-
if (this.#signal?.aborted) throw noticeError;
|
|
549
|
-
this.#logger.warn?.('[dsh-im:qq] unable to send the safe result-file failure notice');
|
|
550
|
-
}
|
|
551
|
-
receipts.push(createArtifactFailureReceipt({
|
|
552
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
553
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
554
|
-
error,
|
|
555
|
-
providerMessageIds,
|
|
556
|
-
}));
|
|
557
|
-
} finally {
|
|
558
|
-
releaseOutboundArtifact(artifact);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
receipt: mergeDeliveryReceipts({
|
|
563
|
-
deliveryId: replyTo,
|
|
564
|
-
presentation: baseReceipt ? 'qq-text-and-files' : 'qq-files',
|
|
565
|
-
receipts,
|
|
558
|
+
const delivery = await deliverOutboundArtifacts({
|
|
559
|
+
artifacts,
|
|
560
|
+
baseReceipt,
|
|
561
|
+
deliveryId: replyTo,
|
|
562
|
+
aggregatePresentation: baseReceipt ? 'qq-text-and-files' : 'qq-files',
|
|
563
|
+
alwaysMerge: true,
|
|
564
|
+
channelKey: 'qq',
|
|
565
|
+
signal: this.#signal,
|
|
566
|
+
sendImage: (file) => sendQqImage(this.#bot, target, file, {
|
|
567
|
+
signal: this.#signal,
|
|
568
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
566
569
|
}),
|
|
567
|
-
|
|
570
|
+
sendFile: (file) => sendQqFile(this.#bot, target, file, {
|
|
571
|
+
signal: this.#signal,
|
|
572
|
+
timeoutMs: this.#fileUploadTimeoutMs,
|
|
573
|
+
}),
|
|
574
|
+
sendFailureNotice: (artifact, error) => this.#bot.sendText(
|
|
575
|
+
target,
|
|
576
|
+
artifactFailureText(artifact?.fileName, error),
|
|
577
|
+
),
|
|
578
|
+
logger: this.#logger,
|
|
579
|
+
});
|
|
580
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
581
|
+
+ delivery.artifactsSent;
|
|
582
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
583
|
+
+ delivery.artifactSendErrors;
|
|
584
|
+
return {
|
|
585
|
+
receipt: delivery.receipt,
|
|
586
|
+
failureNoticeVisible: delivery.failureNoticeVisible,
|
|
568
587
|
};
|
|
569
588
|
}
|
|
570
589
|
|
|
@@ -595,25 +614,25 @@ export class QqHarnessBridge {
|
|
|
595
614
|
let stream = null;
|
|
596
615
|
try {
|
|
597
616
|
if (!text && !hasImages && !hasFiles) {
|
|
598
|
-
await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
|
|
617
|
+
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
599
618
|
await this.#state.markSeen(messageId);
|
|
600
619
|
return;
|
|
601
620
|
}
|
|
602
621
|
const command = text.toLowerCase();
|
|
603
622
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
604
|
-
await this.#bot.sendText(target,
|
|
623
|
+
await this.#bot.sendText(target, helpText());
|
|
605
624
|
await this.#state.markSeen(messageId);
|
|
606
625
|
return;
|
|
607
626
|
}
|
|
608
627
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
609
628
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
610
|
-
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
|
629
|
+
await this.#bot.sendText(target, t('QQ 机器人与 DeepSeek Harness 连接正常。'));
|
|
611
630
|
await this.#state.markSeen(messageId);
|
|
612
631
|
return;
|
|
613
632
|
}
|
|
614
633
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
615
634
|
await this.#state.clearSession(key);
|
|
616
|
-
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
635
|
+
await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
|
|
617
636
|
await this.#state.markSeen(messageId);
|
|
618
637
|
return;
|
|
619
638
|
}
|
|
@@ -757,7 +776,7 @@ export class QqHarnessBridge {
|
|
|
757
776
|
this.#logger.warn?.('[dsh-im:qq] unable to cancel a stopped QQ stream:', streamError);
|
|
758
777
|
}
|
|
759
778
|
try {
|
|
760
|
-
await this.#bot.sendText(target, '已停止。');
|
|
779
|
+
await this.#bot.sendText(target, t('已停止。'));
|
|
761
780
|
} catch (sendError) {
|
|
762
781
|
this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
|
|
763
782
|
}
|
|
@@ -777,7 +796,7 @@ export class QqHarnessBridge {
|
|
|
777
796
|
target,
|
|
778
797
|
inboundFileUserMessage(error)
|
|
779
798
|
?? imagePromptUserMessage(error)
|
|
780
|
-
?? '消息处理失败,请稍后重试。',
|
|
799
|
+
?? t('消息处理失败,请稍后重试。'),
|
|
781
800
|
);
|
|
782
801
|
await this.#state.markSeen(messageId);
|
|
783
802
|
} catch (sendError) {
|
|
@@ -804,14 +823,14 @@ export class QqHarnessBridge {
|
|
|
804
823
|
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
805
824
|
const text = nonEmptyString(safeText(message));
|
|
806
825
|
if (!text || hasQqImageAttachments(message) || hasQqFileAttachments(message)) {
|
|
807
|
-
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
|
826
|
+
await this.#bot.sendText(message.replyTarget, t('请用文字回答当前问题。'));
|
|
808
827
|
return;
|
|
809
828
|
}
|
|
810
829
|
|
|
811
830
|
const pending = this.#pendingInteractions.get(key);
|
|
812
831
|
if (!pending || pending !== expected || pending.submitting) {
|
|
813
832
|
if (claimed && (!pending || pending !== expected)) {
|
|
814
|
-
await this.#bot.sendText(message.replyTarget,
|
|
833
|
+
await this.#bot.sendText(message.replyTarget, interactionResolvedText());
|
|
815
834
|
return;
|
|
816
835
|
}
|
|
817
836
|
return this.#enqueueMessage(message, messageId, key, {
|
|
@@ -824,7 +843,7 @@ export class QqHarnessBridge {
|
|
|
824
843
|
try {
|
|
825
844
|
await this.#presentInteraction(pending);
|
|
826
845
|
} catch {
|
|
827
|
-
this.#status.lastError = 'QQ 交互问题发送失败。';
|
|
846
|
+
this.#status.lastError = t('QQ 交互问题发送失败。');
|
|
828
847
|
this.#logger.error?.('[dsh-im:qq] failed to retry an interaction question');
|
|
829
848
|
pending.interaction.reconnect?.();
|
|
830
849
|
return;
|
|
@@ -832,7 +851,7 @@ export class QqHarnessBridge {
|
|
|
832
851
|
const presentedPending = this.#pendingInteractions.get(key);
|
|
833
852
|
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
|
834
853
|
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
835
|
-
await this.#bot.sendText(message.replyTarget,
|
|
854
|
+
await this.#bot.sendText(message.replyTarget, interactionResolvedText())
|
|
836
855
|
.catch(() => undefined);
|
|
837
856
|
return;
|
|
838
857
|
}
|
|
@@ -855,7 +874,7 @@ export class QqHarnessBridge {
|
|
|
855
874
|
try {
|
|
856
875
|
await this.#presentInteraction(pending);
|
|
857
876
|
} catch {
|
|
858
|
-
this.#status.lastError = 'QQ 交互问题发送失败。';
|
|
877
|
+
this.#status.lastError = t('QQ 交互问题发送失败。');
|
|
859
878
|
this.#logger.error?.('[dsh-im:qq] failed to send the next interaction question');
|
|
860
879
|
pending.interaction.reconnect?.();
|
|
861
880
|
}
|
|
@@ -877,16 +896,16 @@ export class QqHarnessBridge {
|
|
|
877
896
|
if (this.#signal?.aborted) return;
|
|
878
897
|
if (error?.code === 'interaction-not-pending') {
|
|
879
898
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
880
|
-
await this.#bot.sendText(pending.target,
|
|
899
|
+
await this.#bot.sendText(pending.target, interactionResolvedText()).catch(() => undefined);
|
|
881
900
|
return;
|
|
882
901
|
}
|
|
883
902
|
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
884
903
|
pending.submitting = false;
|
|
885
904
|
pending.answers.pop();
|
|
886
905
|
pending.index -= 1;
|
|
887
|
-
this.#status.lastError = '回答提交失败。';
|
|
906
|
+
this.#status.lastError = t('回答提交失败。');
|
|
888
907
|
this.#logger.error?.('[dsh-im:qq] failed to answer a Harness interaction');
|
|
889
|
-
await this.#bot.sendText(pending.target, '回答提交失败,请重新发送当前问题的答案。')
|
|
908
|
+
await this.#bot.sendText(pending.target, t('回答提交失败,请重新发送当前问题的答案。'))
|
|
890
909
|
.catch(() => undefined);
|
|
891
910
|
}
|
|
892
911
|
}
|
|
@@ -931,7 +950,7 @@ export class QqHarnessBridge {
|
|
|
931
950
|
});
|
|
932
951
|
await this.#bot.sendText(
|
|
933
952
|
target,
|
|
934
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
953
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
935
954
|
).catch(() => undefined);
|
|
936
955
|
return;
|
|
937
956
|
}
|
|
@@ -1016,7 +1035,7 @@ export class QqHarnessBridge {
|
|
|
1016
1035
|
await this.#state.markSeen(messageId);
|
|
1017
1036
|
this.#status.messagesReceived += 1;
|
|
1018
1037
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
1019
|
-
await this.#bot.sendText(message.replyTarget,
|
|
1038
|
+
await this.#bot.sendText(message.replyTarget, interactionResolvedText()).catch(() => undefined);
|
|
1020
1039
|
}
|
|
1021
1040
|
|
|
1022
1041
|
#takePendingInteraction(key, interactionId) {
|
|
@@ -1058,7 +1077,7 @@ export class QqHarnessBridge {
|
|
|
1058
1077
|
if (!this.#state.hasSeen(messageId)) {
|
|
1059
1078
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
1060
1079
|
}
|
|
1061
|
-
await this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
|
1080
|
+
await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
|
|
1062
1081
|
.catch(() => undefined);
|
|
1063
1082
|
}
|
|
1064
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
|
}
|