@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const DELIVERY_RECEIPT_SCHEMA_VERSION = 1;
|
|
2
2
|
|
|
3
3
|
const ARTIFACT_OUTCOMES = new Set(['sent', 'rejected', 'failed', 'unknown']);
|
|
4
|
+
const DELIVERY_OUTCOMES = new Set(['sent', 'failed', 'unknown']);
|
|
5
|
+
const TEXT_FORMATS = new Set(['plain', 'markdown']);
|
|
4
6
|
const REJECTED_ARTIFACT_ERRORS = new Set([
|
|
5
7
|
'artifact-changed',
|
|
6
8
|
'artifact-context-required',
|
|
@@ -52,6 +54,24 @@ function requiredString(value, name) {
|
|
|
52
54
|
return value;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
export function createTextDeliveryBlock(value, format = 'plain') {
|
|
58
|
+
const block = typeof value === 'string'
|
|
59
|
+
? { kind: 'text', text: value, format }
|
|
60
|
+
: value;
|
|
61
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)
|
|
62
|
+
|| block.kind !== 'text' || typeof block.text !== 'string' || !block.text.trim()) {
|
|
63
|
+
throw new TypeError('text delivery block must contain non-empty text');
|
|
64
|
+
}
|
|
65
|
+
if (!TEXT_FORMATS.has(block.format)) {
|
|
66
|
+
throw new TypeError('text delivery format must be plain or markdown');
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
kind: 'text',
|
|
70
|
+
text: block.text,
|
|
71
|
+
format: block.format,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
function providerIds(values) {
|
|
56
76
|
if (!Array.isArray(values)) throw new TypeError('providerMessageIds must be an array');
|
|
57
77
|
const ids = [];
|
|
@@ -98,12 +118,22 @@ export function createDeliveryReceipt({
|
|
|
98
118
|
presentation,
|
|
99
119
|
providerMessageIds = [],
|
|
100
120
|
artifacts = [],
|
|
121
|
+
deliveryOutcome,
|
|
122
|
+
reason,
|
|
101
123
|
}) {
|
|
124
|
+
if (deliveryOutcome !== undefined && !DELIVERY_OUTCOMES.has(deliveryOutcome)) {
|
|
125
|
+
throw new TypeError('deliveryOutcome must be sent, failed, or unknown');
|
|
126
|
+
}
|
|
127
|
+
const normalizedReason = reason === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: requiredString(reason, 'delivery reason');
|
|
102
130
|
return Object.freeze({
|
|
103
131
|
schemaVersion: DELIVERY_RECEIPT_SCHEMA_VERSION,
|
|
104
132
|
deliveryId: requiredString(deliveryId, 'deliveryId'),
|
|
105
133
|
presentation: requiredString(presentation, 'presentation'),
|
|
106
134
|
providerMessageIds: providerIds(providerMessageIds),
|
|
135
|
+
...(deliveryOutcome === undefined ? {} : { deliveryOutcome }),
|
|
136
|
+
...(normalizedReason === undefined ? {} : { reason: normalizedReason }),
|
|
107
137
|
artifacts: artifactResults(artifacts),
|
|
108
138
|
});
|
|
109
139
|
}
|
|
@@ -137,11 +167,17 @@ export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
|
|
|
137
167
|
}
|
|
138
168
|
const messageIds = [];
|
|
139
169
|
const artifacts = new Map();
|
|
170
|
+
let deliveryOutcome;
|
|
171
|
+
let reason;
|
|
140
172
|
for (const receipt of receipts) {
|
|
141
173
|
if (!receipt || receipt.schemaVersion !== DELIVERY_RECEIPT_SCHEMA_VERSION) {
|
|
142
174
|
throw new TypeError('receipt must use DeliveryReceipt schema version 1');
|
|
143
175
|
}
|
|
144
176
|
messageIds.push(...(receipt.providerMessageIds ?? []));
|
|
177
|
+
if (deliveryOutcome === undefined && receipt.deliveryOutcome !== undefined) {
|
|
178
|
+
deliveryOutcome = receipt.deliveryOutcome;
|
|
179
|
+
reason = receipt.reason;
|
|
180
|
+
}
|
|
145
181
|
for (const artifact of receipt.artifacts ?? []) {
|
|
146
182
|
artifacts.set(artifact.artifactId, artifact);
|
|
147
183
|
}
|
|
@@ -150,6 +186,8 @@ export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
|
|
|
150
186
|
deliveryId,
|
|
151
187
|
presentation,
|
|
152
188
|
providerMessageIds: messageIds,
|
|
189
|
+
deliveryOutcome,
|
|
190
|
+
reason,
|
|
153
191
|
artifacts: [...artifacts.values()],
|
|
154
192
|
});
|
|
155
193
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
1
2
|
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
2
3
|
import { runCompactCommand } from './compact-command.mjs';
|
|
3
4
|
import {
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
import { deliverOutboundArtifacts } from './semantic/artifact-delivery.mjs';
|
|
36
37
|
import {
|
|
37
38
|
createDeliveryReceipt,
|
|
39
|
+
createTextDeliveryBlock,
|
|
38
40
|
providerMessageIdsFor,
|
|
39
41
|
} from './semantic/delivery.mjs';
|
|
40
42
|
|
|
@@ -54,38 +56,38 @@ function canClaimInteractionReply(message, pending, senderId) {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
function artifactFailureText(fileName, error, descriptor) {
|
|
57
|
-
const name = String(fileName ?? '结果文件')
|
|
59
|
+
const name = String(fileName ?? t('结果文件'))
|
|
58
60
|
.replace(/[\r\n]+/g, ' ')
|
|
59
61
|
.trim()
|
|
60
|
-
.slice(0, 255) || '结果文件';
|
|
62
|
+
.slice(0, 255) || t('结果文件');
|
|
61
63
|
switch (error?.code) {
|
|
62
64
|
case 'artifact-delivery-uncertain':
|
|
63
|
-
return
|
|
65
|
+
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
64
66
|
case 'artifact-permission-required':
|
|
65
67
|
if (descriptor?.key === 'slack') {
|
|
66
|
-
return
|
|
68
|
+
return t('结果文件「{name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。', { name });
|
|
67
69
|
}
|
|
68
70
|
if (descriptor?.key === 'discord') {
|
|
69
|
-
return
|
|
71
|
+
return t('结果文件「{name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。', { name });
|
|
70
72
|
}
|
|
71
73
|
if (descriptor?.key === 'telegram') {
|
|
72
|
-
return
|
|
74
|
+
return t('结果文件「{name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。', { name });
|
|
73
75
|
}
|
|
74
|
-
return
|
|
76
|
+
return t('结果文件「{name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。', { name });
|
|
75
77
|
case 'artifact-too-large':
|
|
76
|
-
return
|
|
78
|
+
return t('结果文件「{name}」超过当前渠道大小上限,未发送。', { name });
|
|
77
79
|
case 'artifact-empty':
|
|
78
|
-
return
|
|
80
|
+
return t('结果文件「{name}」为空,未发送。', { name });
|
|
79
81
|
case 'artifact-invalid':
|
|
80
82
|
case 'artifact-changed':
|
|
81
83
|
case 'artifact-unavailable':
|
|
82
|
-
return
|
|
84
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
83
85
|
case 'artifact-rate-limited':
|
|
84
|
-
return
|
|
86
|
+
return t('结果文件「{name}」暂时被当前渠道限流,未能发送,请稍后重试。', { name });
|
|
85
87
|
case 'artifact-provider-rejected':
|
|
86
|
-
return
|
|
88
|
+
return t('结果文件「{name}」已生成,但当前渠道拒绝了该文件或文件消息。', { name });
|
|
87
89
|
default:
|
|
88
|
-
return
|
|
90
|
+
return t('结果文件「{name}」已生成,但当前渠道暂时未能发送,请稍后重试。', { name });
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -320,7 +322,7 @@ export class TextHarnessBridge {
|
|
|
320
322
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
321
323
|
this.#status.lastError = error?.message ?? String(error);
|
|
322
324
|
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a command:`, error);
|
|
323
|
-
await this.#bot.sendText(target, '消息处理失败,请稍后重试。').catch(() => undefined);
|
|
325
|
+
await this.#bot.sendText(target, t('消息处理失败,请稍后重试。')).catch(() => undefined);
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
|
|
@@ -328,7 +330,7 @@ export class TextHarnessBridge {
|
|
|
328
330
|
return sendRememberedConnectionTest({
|
|
329
331
|
state: this.#state,
|
|
330
332
|
text,
|
|
331
|
-
channelLabel:
|
|
333
|
+
channelLabel: t('{label}机器人', { label: this.#descriptor.label }),
|
|
332
334
|
send: (target, message) => this.#bot.sendText(target, message),
|
|
333
335
|
});
|
|
334
336
|
}
|
|
@@ -372,6 +374,7 @@ export class TextHarnessBridge {
|
|
|
372
374
|
const target = message.replyTarget;
|
|
373
375
|
const text = cleanText(message.content);
|
|
374
376
|
let stream = null;
|
|
377
|
+
let semanticStream = false;
|
|
375
378
|
try {
|
|
376
379
|
this.#signal?.throwIfAborted();
|
|
377
380
|
if (message.kind === 'group' && message.addressed !== true) {
|
|
@@ -382,38 +385,38 @@ export class TextHarnessBridge {
|
|
|
382
385
|
const hasImages = hasInboundImages(message);
|
|
383
386
|
const hasFiles = hasInboundFiles(message);
|
|
384
387
|
if (!text && !hasImages && !hasFiles) {
|
|
385
|
-
await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
|
|
388
|
+
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
386
389
|
return;
|
|
387
390
|
}
|
|
388
391
|
const command = text.toLowerCase();
|
|
389
392
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
390
393
|
await this.#bot.sendText(target, [
|
|
391
|
-
|
|
394
|
+
t('{label}机器人已连接 DeepSeek Harness。', { label: this.#descriptor.label }),
|
|
392
395
|
'',
|
|
393
|
-
'直接发送文字、图片或文件即可继续当前会话。',
|
|
394
|
-
'/new 开启一个全新会话',
|
|
395
|
-
'/compact 压缩当前会话的较早上下文',
|
|
396
|
-
'/workspace 工作区绝对路径 切换工作区',
|
|
397
|
-
'/workspacelist 列出工作区绝对路径',
|
|
398
|
-
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
399
|
-
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
400
|
-
'/models 按序号列出所有可用模型',
|
|
401
|
-
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
402
|
-
'示例:先发 /models,再发 /model 2',
|
|
403
|
-
'/presetlist 按序号列出可用 Agent Preset',
|
|
404
|
-
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
|
405
|
-
'纯数字 ID:/preset id:<ID>',
|
|
406
|
-
'/preset --default 跟随 Host 默认',
|
|
407
|
-
'/stop 停止当前任务',
|
|
408
|
-
'/steer 补充指令 纠偏当前任务',
|
|
409
|
-
'/status 检查连接状态',
|
|
410
|
-
'/help 显示本帮助',
|
|
396
|
+
t('直接发送文字、图片或文件即可继续当前会话。'),
|
|
397
|
+
t('/new 开启一个全新会话'),
|
|
398
|
+
t('/compact 压缩当前会话的较早上下文'),
|
|
399
|
+
t('/workspace 工作区绝对路径 切换工作区'),
|
|
400
|
+
t('/workspacelist 列出工作区绝对路径'),
|
|
401
|
+
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
402
|
+
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
403
|
+
t('/models 按序号列出所有可用模型'),
|
|
404
|
+
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
|
|
405
|
+
t('示例:先发 /models,再发 /model 2'),
|
|
406
|
+
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
407
|
+
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
408
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
409
|
+
t('/preset --default 跟随 Host 默认'),
|
|
410
|
+
t('/stop 停止当前任务'),
|
|
411
|
+
t('/steer 补充指令 纠偏当前任务'),
|
|
412
|
+
t('/status 检查连接状态'),
|
|
413
|
+
t('/help 显示本帮助'),
|
|
411
414
|
].join('\n'));
|
|
412
415
|
return;
|
|
413
416
|
}
|
|
414
417
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
415
418
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
416
|
-
await this.#bot.sendText(target,
|
|
419
|
+
await this.#bot.sendText(target, t('{label}机器人与 DeepSeek Harness 连接正常。', { label: this.#descriptor.label }));
|
|
417
420
|
return;
|
|
418
421
|
}
|
|
419
422
|
const workspaceCommand = !hasImages && !hasFiles
|
|
@@ -427,7 +430,7 @@ export class TextHarnessBridge {
|
|
|
427
430
|
}
|
|
428
431
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
429
432
|
await this.#state.clearSession(conversationKey);
|
|
430
|
-
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
433
|
+
await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
|
|
431
434
|
return;
|
|
432
435
|
}
|
|
433
436
|
const compactCommand = !hasImages && !hasFiles
|
|
@@ -448,7 +451,17 @@ export class TextHarnessBridge {
|
|
|
448
451
|
this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
|
|
449
452
|
});
|
|
450
453
|
let streamFinished = false;
|
|
451
|
-
if (typeof this.#bot.
|
|
454
|
+
if (typeof this.#bot.openDeliveryStream === 'function') {
|
|
455
|
+
try {
|
|
456
|
+
stream = await this.#bot.openDeliveryStream(target);
|
|
457
|
+
semanticStream = true;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
this.#logger.warn?.(
|
|
460
|
+
`[dsh-im:${this.#descriptor.key}] unable to start a semantic reply stream; using final delivery:`,
|
|
461
|
+
error,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
} else if (typeof this.#bot.openStream === 'function') {
|
|
452
465
|
try {
|
|
453
466
|
stream = await this.#bot.openStream(target);
|
|
454
467
|
} catch (error) {
|
|
@@ -475,35 +488,48 @@ export class TextHarnessBridge {
|
|
|
475
488
|
control: { owner: this, key: conversationKey },
|
|
476
489
|
onUpdate: stream ? async (update) => {
|
|
477
490
|
const progress = update.type === 'text' ? update.text
|
|
478
|
-
: update.type === 'tool' ?
|
|
479
|
-
if (progress)
|
|
491
|
+
: update.type === 'tool' ? t('正在使用{name}…', { name: update.name }) : update.text;
|
|
492
|
+
if (progress) {
|
|
493
|
+
const format = update.type === 'text' ? 'markdown' : 'plain';
|
|
494
|
+
await stream.update(semanticStream
|
|
495
|
+
? createTextDeliveryBlock(progress, format)
|
|
496
|
+
: progress);
|
|
497
|
+
}
|
|
480
498
|
} : undefined,
|
|
481
499
|
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
482
500
|
key: conversationKey,
|
|
483
501
|
actor: senderId,
|
|
484
502
|
target,
|
|
485
|
-
requiresMention: message.kind === 'group',
|
|
503
|
+
requiresMention: message.kind === 'group' && message.requiresMention !== false,
|
|
486
504
|
}),
|
|
487
505
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
488
506
|
files: message.files,
|
|
489
507
|
},
|
|
490
508
|
});
|
|
491
|
-
const
|
|
492
|
-
|
|
509
|
+
const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
|
|
510
|
+
const visibleAnswer = fileOnlyCompletion
|
|
511
|
+
? t(FILE_ONLY_COMPLETION_TEXT)
|
|
493
512
|
: answer;
|
|
513
|
+
const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
|
|
494
514
|
let textDeliveryError = null;
|
|
495
515
|
let textReceipt = null;
|
|
496
516
|
if (stream) {
|
|
497
517
|
try {
|
|
498
|
-
const result = await stream.finish(
|
|
518
|
+
const result = await stream.finish(semanticStream
|
|
519
|
+
? createTextDeliveryBlock(visibleAnswer, answerFormat)
|
|
520
|
+
: visibleAnswer);
|
|
499
521
|
streamFinished = true;
|
|
500
522
|
textReceipt = createDeliveryReceipt({
|
|
501
523
|
deliveryId: messageId,
|
|
502
|
-
presentation:
|
|
524
|
+
presentation: result?.presentation
|
|
525
|
+
?? stream.presentation
|
|
526
|
+
?? `${this.#descriptor.key}-stream`,
|
|
503
527
|
providerMessageIds: [
|
|
504
528
|
...providerMessageIdsFor(stream),
|
|
505
529
|
...providerMessageIdsFor(result),
|
|
506
530
|
],
|
|
531
|
+
deliveryOutcome: result?.deliveryOutcome,
|
|
532
|
+
reason: result?.reason,
|
|
507
533
|
});
|
|
508
534
|
} catch (error) {
|
|
509
535
|
stream.cancel?.();
|
|
@@ -515,40 +541,74 @@ export class TextHarnessBridge {
|
|
|
515
541
|
}
|
|
516
542
|
if (!streamFinished) {
|
|
517
543
|
try {
|
|
518
|
-
const result =
|
|
544
|
+
const result = typeof this.#bot.sendDelivery === 'function'
|
|
545
|
+
? await this.#bot.sendDelivery(
|
|
546
|
+
target,
|
|
547
|
+
createTextDeliveryBlock(visibleAnswer, answerFormat),
|
|
548
|
+
)
|
|
549
|
+
: await this.#bot.sendText(target, visibleAnswer);
|
|
519
550
|
textReceipt = createDeliveryReceipt({
|
|
520
551
|
deliveryId: messageId,
|
|
521
|
-
presentation: `${this.#descriptor.key}-text`,
|
|
552
|
+
presentation: result?.presentation ?? `${this.#descriptor.key}-text`,
|
|
522
553
|
providerMessageIds: providerMessageIdsFor(result),
|
|
554
|
+
deliveryOutcome: result?.deliveryOutcome,
|
|
555
|
+
reason: result?.reason,
|
|
523
556
|
});
|
|
524
557
|
} catch (error) {
|
|
525
558
|
textDeliveryError = error;
|
|
526
559
|
}
|
|
527
560
|
}
|
|
561
|
+
if (textReceipt?.deliveryOutcome === 'failed') {
|
|
562
|
+
const reason = textReceipt.reason ?? 'text-delivery-failed';
|
|
563
|
+
textDeliveryError = new Error(`Final text delivery failed (${reason})`);
|
|
564
|
+
textDeliveryError.code = reason;
|
|
565
|
+
}
|
|
528
566
|
// A failed final text must not discard an already registered result file.
|
|
529
567
|
// Settle the independent attachment path before surfacing the text error.
|
|
530
568
|
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
|
531
|
-
if (textDeliveryError && !delivery.userVisible)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
569
|
+
if (textDeliveryError && !delivery.userVisible) {
|
|
570
|
+
textDeliveryError.deliveryReceipt = delivery.receipt;
|
|
571
|
+
throw textDeliveryError;
|
|
572
|
+
}
|
|
573
|
+
if (delivery.userVisible) {
|
|
574
|
+
this.#status.messagesReplied += 1;
|
|
575
|
+
this.#status.lastReplyAt = new Date().toISOString();
|
|
576
|
+
this.#status.lastError = null;
|
|
577
|
+
}
|
|
535
578
|
return delivery.receipt;
|
|
536
579
|
} catch (error) {
|
|
537
580
|
if (error?.code === 'turn-stopped') {
|
|
538
581
|
if (stream) {
|
|
539
582
|
try {
|
|
540
|
-
await stream.finish('已停止。');
|
|
583
|
+
await stream.finish(t('已停止。'));
|
|
541
584
|
} catch {
|
|
542
585
|
stream.cancel?.();
|
|
543
586
|
}
|
|
544
587
|
}
|
|
545
588
|
return;
|
|
546
589
|
}
|
|
547
|
-
|
|
548
|
-
|
|
590
|
+
if (this.#signal?.aborted) {
|
|
591
|
+
stream?.cancel?.();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
549
594
|
this.#status.lastError = error?.message ?? String(error);
|
|
595
|
+
const presentStreamFailure = async (text) => {
|
|
596
|
+
if (typeof stream?.fail !== 'function') return false;
|
|
597
|
+
try {
|
|
598
|
+
const result = await stream.fail(text);
|
|
599
|
+
return Boolean(result) && result.deliveryOutcome !== 'failed';
|
|
600
|
+
} catch (streamError) {
|
|
601
|
+
this.#logger.warn?.(
|
|
602
|
+
`[dsh-im:${this.#descriptor.key}] unable to finalize the failed stream:`,
|
|
603
|
+
streamError,
|
|
604
|
+
);
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
550
608
|
const imageErrorMessage = imagePromptUserMessage(error);
|
|
551
609
|
if (imageErrorMessage) {
|
|
610
|
+
if (await presentStreamFailure(imageErrorMessage)) return;
|
|
611
|
+
stream?.cancel?.();
|
|
552
612
|
try {
|
|
553
613
|
await this.#bot.sendText(target, imageErrorMessage);
|
|
554
614
|
} catch (sendError) {
|
|
@@ -561,6 +621,8 @@ export class TextHarnessBridge {
|
|
|
561
621
|
}
|
|
562
622
|
const fileErrorMessage = inboundFileUserMessage(error);
|
|
563
623
|
if (fileErrorMessage) {
|
|
624
|
+
if (await presentStreamFailure(fileErrorMessage)) return;
|
|
625
|
+
stream?.cancel?.();
|
|
564
626
|
try {
|
|
565
627
|
await this.#bot.sendText(target, fileErrorMessage);
|
|
566
628
|
} catch (sendError) {
|
|
@@ -572,14 +634,19 @@ export class TextHarnessBridge {
|
|
|
572
634
|
return;
|
|
573
635
|
}
|
|
574
636
|
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
|
|
637
|
+
if (await presentStreamFailure('消息处理失败,请稍后重试。')) {
|
|
638
|
+
return error.deliveryReceipt;
|
|
639
|
+
}
|
|
640
|
+
stream?.cancel?.();
|
|
575
641
|
try {
|
|
576
|
-
await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
|
|
642
|
+
await this.#bot.sendText(target, t('消息处理失败,请稍后重试。'));
|
|
577
643
|
} catch (sendError) {
|
|
578
644
|
this.#logger.error?.(
|
|
579
645
|
`[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
|
|
580
646
|
sendError,
|
|
581
647
|
);
|
|
582
648
|
}
|
|
649
|
+
return error.deliveryReceipt;
|
|
583
650
|
} finally {
|
|
584
651
|
await Promise.allSettled([
|
|
585
652
|
this.#cancelPendingInteraction(conversationKey),
|
|
@@ -615,7 +682,7 @@ export class TextHarnessBridge {
|
|
|
615
682
|
const text = cleanText(message.content);
|
|
616
683
|
if (!text || hasInboundImages(message) || hasInboundFiles(message)) {
|
|
617
684
|
try {
|
|
618
|
-
await this.#bot.sendText(target, '请用文字回答当前问题。');
|
|
685
|
+
await this.#bot.sendText(target, t('请用文字回答当前问题。'));
|
|
619
686
|
} catch (error) {
|
|
620
687
|
this.#logger.error?.(
|
|
621
688
|
`[dsh-im:${this.#descriptor.key}] failed to reject a non-text interaction reply:`,
|
|
@@ -643,7 +710,7 @@ export class TextHarnessBridge {
|
|
|
643
710
|
try {
|
|
644
711
|
await this.#presentInteraction(pending);
|
|
645
712
|
} catch (error) {
|
|
646
|
-
this.#status.lastError =
|
|
713
|
+
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
|
647
714
|
this.#logger.error?.(
|
|
648
715
|
`[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
|
|
649
716
|
error,
|
|
@@ -681,7 +748,7 @@ export class TextHarnessBridge {
|
|
|
681
748
|
try {
|
|
682
749
|
await this.#presentInteraction(pending);
|
|
683
750
|
} catch (error) {
|
|
684
|
-
this.#status.lastError =
|
|
751
|
+
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
|
685
752
|
this.#logger.error?.(
|
|
686
753
|
`[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
|
|
687
754
|
error,
|
|
@@ -707,7 +774,7 @@ export class TextHarnessBridge {
|
|
|
707
774
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
708
775
|
if (this.#signal?.aborted) return;
|
|
709
776
|
try {
|
|
710
|
-
await this.#bot.sendText(target, INTERACTION_RESOLVED_TEXT);
|
|
777
|
+
await this.#bot.sendText(target, t(INTERACTION_RESOLVED_TEXT));
|
|
711
778
|
} catch (sendError) {
|
|
712
779
|
this.#logger.error?.(
|
|
713
780
|
`[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
|
|
@@ -720,13 +787,13 @@ export class TextHarnessBridge {
|
|
|
720
787
|
pending.submitting = false;
|
|
721
788
|
pending.answers.pop();
|
|
722
789
|
pending.index -= 1;
|
|
723
|
-
this.#status.lastError = '回答提交失败。';
|
|
790
|
+
this.#status.lastError = t('回答提交失败。');
|
|
724
791
|
this.#logger.error?.(
|
|
725
792
|
`[dsh-im:${this.#descriptor.key}] failed to answer a Harness interaction:`,
|
|
726
793
|
error,
|
|
727
794
|
);
|
|
728
795
|
try {
|
|
729
|
-
await this.#bot.sendText(target, '回答提交失败,请重新发送当前问题的答案。');
|
|
796
|
+
await this.#bot.sendText(target, t('回答提交失败,请重新发送当前问题的答案。'));
|
|
730
797
|
} catch (sendError) {
|
|
731
798
|
this.#logger.error?.(
|
|
732
799
|
`[dsh-im:${this.#descriptor.key}] failed to send an interaction retry notice:`,
|
|
@@ -773,7 +840,7 @@ export class TextHarnessBridge {
|
|
|
773
840
|
try {
|
|
774
841
|
await this.#bot.sendText(
|
|
775
842
|
target,
|
|
776
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
843
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
777
844
|
);
|
|
778
845
|
} catch (error) {
|
|
779
846
|
this.#logger.error?.(
|
|
@@ -874,7 +941,7 @@ export class TextHarnessBridge {
|
|
|
874
941
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
875
942
|
}
|
|
876
943
|
try {
|
|
877
|
-
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
|
|
944
|
+
await this.#bot.sendText(message.replyTarget, t(INTERACTION_RESOLVED_TEXT));
|
|
878
945
|
} catch (error) {
|
|
879
946
|
this.#logger.error?.(
|
|
880
947
|
`[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectionTestMessage } from './connection-test.mjs';
|
|
2
|
+
import { t } from './i18n.mjs';
|
|
2
3
|
|
|
3
4
|
function cleanString(value) {
|
|
4
5
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -70,7 +71,9 @@ export class TokenBotController {
|
|
|
70
71
|
if (!token) {
|
|
71
72
|
this.#errors.set(config.botId, safeError(
|
|
72
73
|
'missing-token',
|
|
73
|
-
|
|
74
|
+
t('{label}机器人凭据缺失,请移除后重新接入。', {
|
|
75
|
+
label: this.#descriptor.label,
|
|
76
|
+
}),
|
|
74
77
|
));
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
@@ -80,7 +83,9 @@ export class TokenBotController {
|
|
|
80
83
|
} catch (error) {
|
|
81
84
|
this.#errors.set(config.botId, safeError(
|
|
82
85
|
'connection-failed',
|
|
83
|
-
|
|
86
|
+
t('{label}连接未就绪,插件会自动重试。', {
|
|
87
|
+
label: this.#descriptor.label,
|
|
88
|
+
}),
|
|
84
89
|
));
|
|
85
90
|
this.#logger.warn?.(
|
|
86
91
|
`[dsh-im:${this.#descriptor.key}] bot ${config.botId} failed to initialize:`,
|
|
@@ -129,7 +134,9 @@ export class TokenBotController {
|
|
|
129
134
|
} catch (error) {
|
|
130
135
|
this.#errors.set(identity.botId, safeError(
|
|
131
136
|
'connection-failed',
|
|
132
|
-
|
|
137
|
+
t('{label}机器人已接入,消息连接暂未就绪。', {
|
|
138
|
+
label: this.#descriptor.label,
|
|
139
|
+
}),
|
|
133
140
|
));
|
|
134
141
|
this.#logger.warn?.(
|
|
135
142
|
`[dsh-im:${this.#descriptor.key}] bot ${identity.botId} credential connection failed:`,
|
|
@@ -153,7 +160,9 @@ export class TokenBotController {
|
|
|
153
160
|
} catch (error) {
|
|
154
161
|
this.#errors.set(botId, safeError(
|
|
155
162
|
'connection-failed',
|
|
156
|
-
|
|
163
|
+
t('{label}连接仍未就绪,请稍后重试。', {
|
|
164
|
+
label: this.#descriptor.label,
|
|
165
|
+
}),
|
|
157
166
|
));
|
|
158
167
|
throw error;
|
|
159
168
|
} finally {
|
|
@@ -181,7 +190,9 @@ export class TokenBotController {
|
|
|
181
190
|
} catch (error) {
|
|
182
191
|
this.#errors.set(botId, safeError(
|
|
183
192
|
'connection-failed',
|
|
184
|
-
|
|
193
|
+
t('{label}连接仍未就绪,请稍后重试。', {
|
|
194
|
+
label: this.#descriptor.label,
|
|
195
|
+
}),
|
|
185
196
|
));
|
|
186
197
|
throw error;
|
|
187
198
|
} finally {
|
|
@@ -197,14 +208,19 @@ export class TokenBotController {
|
|
|
197
208
|
return this.#withBotTransition(botId, async () => {
|
|
198
209
|
const runtime = this.#runtimes.get(botId);
|
|
199
210
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
200
|
-
const error = new Error(
|
|
211
|
+
const error = new Error(t('{label}机器人尚未连接', {
|
|
212
|
+
label: this.#descriptor.label,
|
|
213
|
+
}));
|
|
201
214
|
error.code = 'test-target-unavailable';
|
|
202
215
|
throw error;
|
|
203
216
|
}
|
|
204
|
-
const cardLabel =
|
|
217
|
+
const cardLabel = t('{name}({id})', {
|
|
218
|
+
name: config.name,
|
|
219
|
+
id: this.#maskPlatformId(config.platformId),
|
|
220
|
+
});
|
|
205
221
|
await runtime.sendConnectionTest(connectionTestMessage(
|
|
206
222
|
cardLabel,
|
|
207
|
-
|
|
223
|
+
t('{label}机器人', { label: this.#descriptor.label }),
|
|
208
224
|
));
|
|
209
225
|
return { sent: true };
|
|
210
226
|
});
|
|
@@ -260,9 +276,14 @@ export class TokenBotController {
|
|
|
260
276
|
},
|
|
261
277
|
health: {
|
|
262
278
|
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
|
263
|
-
summary: connected ?
|
|
264
|
-
:
|
|
265
|
-
|
|
279
|
+
summary: connected ? t('{label}{connectionLabel}运行正常', {
|
|
280
|
+
label: this.#descriptor.label,
|
|
281
|
+
connectionLabel: t(this.#descriptor.connectionLabel),
|
|
282
|
+
})
|
|
283
|
+
: state === 'error' ? t('{label}连接未就绪,插件会自动重试', {
|
|
284
|
+
label: this.#descriptor.label,
|
|
285
|
+
})
|
|
286
|
+
: t('{label}连接当前离线', { label: this.#descriptor.label }),
|
|
266
287
|
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
|
267
288
|
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
|
268
289
|
},
|