@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
|
@@ -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 {
|
|
@@ -32,14 +33,10 @@ import {
|
|
|
32
33
|
harnessQuestionText,
|
|
33
34
|
validHarnessQuestion,
|
|
34
35
|
} from './harness-question.mjs';
|
|
36
|
+
import { deliverOutboundArtifacts } from './semantic/artifact-delivery.mjs';
|
|
35
37
|
import {
|
|
36
|
-
materializeOutboundArtifact,
|
|
37
|
-
releaseOutboundArtifact,
|
|
38
|
-
} from './semantic/artifact.mjs';
|
|
39
|
-
import {
|
|
40
|
-
createArtifactFailureReceipt,
|
|
41
38
|
createDeliveryReceipt,
|
|
42
|
-
|
|
39
|
+
createTextDeliveryBlock,
|
|
43
40
|
providerMessageIdsFor,
|
|
44
41
|
} from './semantic/delivery.mjs';
|
|
45
42
|
|
|
@@ -59,38 +56,38 @@ function canClaimInteractionReply(message, pending, senderId) {
|
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
function artifactFailureText(fileName, error, descriptor) {
|
|
62
|
-
const name = String(fileName ?? '结果文件')
|
|
59
|
+
const name = String(fileName ?? t('结果文件'))
|
|
63
60
|
.replace(/[\r\n]+/g, ' ')
|
|
64
61
|
.trim()
|
|
65
|
-
.slice(0, 255) || '结果文件';
|
|
62
|
+
.slice(0, 255) || t('结果文件');
|
|
66
63
|
switch (error?.code) {
|
|
67
64
|
case 'artifact-delivery-uncertain':
|
|
68
|
-
return
|
|
65
|
+
return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
69
66
|
case 'artifact-permission-required':
|
|
70
67
|
if (descriptor?.key === 'slack') {
|
|
71
|
-
return
|
|
68
|
+
return t('结果文件「{name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。', { name });
|
|
72
69
|
}
|
|
73
70
|
if (descriptor?.key === 'discord') {
|
|
74
|
-
return
|
|
71
|
+
return t('结果文件「{name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。', { name });
|
|
75
72
|
}
|
|
76
73
|
if (descriptor?.key === 'telegram') {
|
|
77
|
-
return
|
|
74
|
+
return t('结果文件「{name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。', { name });
|
|
78
75
|
}
|
|
79
|
-
return
|
|
76
|
+
return t('结果文件「{name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。', { name });
|
|
80
77
|
case 'artifact-too-large':
|
|
81
|
-
return
|
|
78
|
+
return t('结果文件「{name}」超过当前渠道大小上限,未发送。', { name });
|
|
82
79
|
case 'artifact-empty':
|
|
83
|
-
return
|
|
80
|
+
return t('结果文件「{name}」为空,未发送。', { name });
|
|
84
81
|
case 'artifact-invalid':
|
|
85
82
|
case 'artifact-changed':
|
|
86
83
|
case 'artifact-unavailable':
|
|
87
|
-
return
|
|
84
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
88
85
|
case 'artifact-rate-limited':
|
|
89
|
-
return
|
|
86
|
+
return t('结果文件「{name}」暂时被当前渠道限流,未能发送,请稍后重试。', { name });
|
|
90
87
|
case 'artifact-provider-rejected':
|
|
91
|
-
return
|
|
88
|
+
return t('结果文件「{name}」已生成,但当前渠道拒绝了该文件或文件消息。', { name });
|
|
92
89
|
default:
|
|
93
|
-
return
|
|
90
|
+
return t('结果文件「{name}」已生成,但当前渠道暂时未能发送,请稍后重试。', { name });
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
93
|
|
|
@@ -325,7 +322,7 @@ export class TextHarnessBridge {
|
|
|
325
322
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
326
323
|
this.#status.lastError = error?.message ?? String(error);
|
|
327
324
|
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a command:`, error);
|
|
328
|
-
await this.#bot.sendText(target, '消息处理失败,请稍后重试。').catch(() => undefined);
|
|
325
|
+
await this.#bot.sendText(target, t('消息处理失败,请稍后重试。')).catch(() => undefined);
|
|
329
326
|
}
|
|
330
327
|
}
|
|
331
328
|
|
|
@@ -333,79 +330,35 @@ export class TextHarnessBridge {
|
|
|
333
330
|
return sendRememberedConnectionTest({
|
|
334
331
|
state: this.#state,
|
|
335
332
|
text,
|
|
336
|
-
channelLabel:
|
|
333
|
+
channelLabel: t('{label}机器人', { label: this.#descriptor.label }),
|
|
337
334
|
send: (target, message) => this.#bot.sendText(target, message),
|
|
338
335
|
});
|
|
339
336
|
}
|
|
340
337
|
|
|
341
338
|
async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt) {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
} catch (error) {
|
|
366
|
-
if (this.#signal?.aborted) throw error;
|
|
367
|
-
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
|
|
368
|
-
this.#logger.warn?.(
|
|
369
|
-
`[dsh-im:${this.#descriptor.key}] result file delivery failed (${error?.code ?? 'unknown'})`,
|
|
370
|
-
);
|
|
371
|
-
let providerMessageIds = [];
|
|
372
|
-
let noticeSent = false;
|
|
373
|
-
try {
|
|
374
|
-
const notice = await this.#bot.sendText(
|
|
375
|
-
target,
|
|
376
|
-
artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
377
|
-
);
|
|
378
|
-
providerMessageIds = providerMessageIdsFor(notice);
|
|
379
|
-
noticeSent = true;
|
|
380
|
-
} catch {
|
|
381
|
-
this.#logger.warn?.(
|
|
382
|
-
`[dsh-im:${this.#descriptor.key}] unable to send the safe result-file failure notice`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const failureReceipt = createArtifactFailureReceipt({
|
|
386
|
-
artifactId: artifact?.artifactId ?? 'unknown',
|
|
387
|
-
deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
|
|
388
|
-
error,
|
|
389
|
-
providerMessageIds,
|
|
390
|
-
});
|
|
391
|
-
receipts.push(failureReceipt);
|
|
392
|
-
if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
|
|
393
|
-
} finally {
|
|
394
|
-
releaseOutboundArtifact(artifact);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
const receipt = receipts.length === 0
|
|
398
|
-
? null
|
|
399
|
-
: receipts.length === 1
|
|
400
|
-
? receipts[0]
|
|
401
|
-
: mergeDeliveryReceipts({
|
|
402
|
-
deliveryId: replyTo,
|
|
403
|
-
presentation: baseReceipt
|
|
404
|
-
? `${this.#descriptor.key}-text-and-files`
|
|
405
|
-
: `${this.#descriptor.key}-files`,
|
|
406
|
-
receipts,
|
|
407
|
-
});
|
|
408
|
-
return { receipt, userVisible };
|
|
339
|
+
const delivery = await deliverOutboundArtifacts({
|
|
340
|
+
artifacts,
|
|
341
|
+
baseReceipt,
|
|
342
|
+
deliveryId: replyTo,
|
|
343
|
+
channelKey: this.#descriptor.key,
|
|
344
|
+
signal: this.#signal,
|
|
345
|
+
sendImage: typeof this.#bot.sendImage === 'function'
|
|
346
|
+
? (file) => this.#bot.sendImage(target, file)
|
|
347
|
+
: undefined,
|
|
348
|
+
sendFile: typeof this.#bot.sendFile === 'function'
|
|
349
|
+
? (file) => this.#bot.sendFile(target, file)
|
|
350
|
+
: undefined,
|
|
351
|
+
sendFailureNotice: (artifact, error) => this.#bot.sendText(
|
|
352
|
+
target,
|
|
353
|
+
artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
354
|
+
),
|
|
355
|
+
logger: this.#logger,
|
|
356
|
+
});
|
|
357
|
+
this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
|
|
358
|
+
+ delivery.artifactsSent;
|
|
359
|
+
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
360
|
+
+ delivery.artifactSendErrors;
|
|
361
|
+
return { receipt: delivery.receipt, userVisible: delivery.userVisible };
|
|
409
362
|
}
|
|
410
363
|
|
|
411
364
|
async #process(message, messageId, senderId, conversationKey, {
|
|
@@ -421,6 +374,7 @@ export class TextHarnessBridge {
|
|
|
421
374
|
const target = message.replyTarget;
|
|
422
375
|
const text = cleanText(message.content);
|
|
423
376
|
let stream = null;
|
|
377
|
+
let semanticStream = false;
|
|
424
378
|
try {
|
|
425
379
|
this.#signal?.throwIfAborted();
|
|
426
380
|
if (message.kind === 'group' && message.addressed !== true) {
|
|
@@ -431,38 +385,38 @@ export class TextHarnessBridge {
|
|
|
431
385
|
const hasImages = hasInboundImages(message);
|
|
432
386
|
const hasFiles = hasInboundFiles(message);
|
|
433
387
|
if (!text && !hasImages && !hasFiles) {
|
|
434
|
-
await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
|
|
388
|
+
await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
|
|
435
389
|
return;
|
|
436
390
|
}
|
|
437
391
|
const command = text.toLowerCase();
|
|
438
392
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
439
393
|
await this.#bot.sendText(target, [
|
|
440
|
-
|
|
394
|
+
t('{label}机器人已连接 DeepSeek Harness。', { label: this.#descriptor.label }),
|
|
441
395
|
'',
|
|
442
|
-
'直接发送文字、图片或文件即可继续当前会话。',
|
|
443
|
-
'/new 开启一个全新会话',
|
|
444
|
-
'/compact 压缩当前会话的较早上下文',
|
|
445
|
-
'/workspace 工作区绝对路径 切换工作区',
|
|
446
|
-
'/workspacelist 列出工作区绝对路径',
|
|
447
|
-
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
448
|
-
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
449
|
-
'/models 按序号列出所有可用模型',
|
|
450
|
-
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
451
|
-
'示例:先发 /models,再发 /model 2',
|
|
452
|
-
'/presetlist 按序号列出可用 Agent Preset',
|
|
453
|
-
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
|
454
|
-
'纯数字 ID:/preset id:<ID>',
|
|
455
|
-
'/preset --default 跟随 Host 默认',
|
|
456
|
-
'/stop 停止当前任务',
|
|
457
|
-
'/steer 补充指令 纠偏当前任务',
|
|
458
|
-
'/status 检查连接状态',
|
|
459
|
-
'/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 显示本帮助'),
|
|
460
414
|
].join('\n'));
|
|
461
415
|
return;
|
|
462
416
|
}
|
|
463
417
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
464
418
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
465
|
-
await this.#bot.sendText(target,
|
|
419
|
+
await this.#bot.sendText(target, t('{label}机器人与 DeepSeek Harness 连接正常。', { label: this.#descriptor.label }));
|
|
466
420
|
return;
|
|
467
421
|
}
|
|
468
422
|
const workspaceCommand = !hasImages && !hasFiles
|
|
@@ -476,7 +430,7 @@ export class TextHarnessBridge {
|
|
|
476
430
|
}
|
|
477
431
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
478
432
|
await this.#state.clearSession(conversationKey);
|
|
479
|
-
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
433
|
+
await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
|
|
480
434
|
return;
|
|
481
435
|
}
|
|
482
436
|
const compactCommand = !hasImages && !hasFiles
|
|
@@ -497,7 +451,17 @@ export class TextHarnessBridge {
|
|
|
497
451
|
this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
|
|
498
452
|
});
|
|
499
453
|
let streamFinished = false;
|
|
500
|
-
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') {
|
|
501
465
|
try {
|
|
502
466
|
stream = await this.#bot.openStream(target);
|
|
503
467
|
} catch (error) {
|
|
@@ -524,35 +488,48 @@ export class TextHarnessBridge {
|
|
|
524
488
|
control: { owner: this, key: conversationKey },
|
|
525
489
|
onUpdate: stream ? async (update) => {
|
|
526
490
|
const progress = update.type === 'text' ? update.text
|
|
527
|
-
: update.type === 'tool' ?
|
|
528
|
-
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
|
+
}
|
|
529
498
|
} : undefined,
|
|
530
499
|
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
531
500
|
key: conversationKey,
|
|
532
501
|
actor: senderId,
|
|
533
502
|
target,
|
|
534
|
-
requiresMention: message.kind === 'group',
|
|
503
|
+
requiresMention: message.kind === 'group' && message.requiresMention !== false,
|
|
535
504
|
}),
|
|
536
505
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
537
506
|
files: message.files,
|
|
538
507
|
},
|
|
539
508
|
});
|
|
540
|
-
const
|
|
541
|
-
|
|
509
|
+
const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
|
|
510
|
+
const visibleAnswer = fileOnlyCompletion
|
|
511
|
+
? t(FILE_ONLY_COMPLETION_TEXT)
|
|
542
512
|
: answer;
|
|
513
|
+
const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
|
|
543
514
|
let textDeliveryError = null;
|
|
544
515
|
let textReceipt = null;
|
|
545
516
|
if (stream) {
|
|
546
517
|
try {
|
|
547
|
-
const result = await stream.finish(
|
|
518
|
+
const result = await stream.finish(semanticStream
|
|
519
|
+
? createTextDeliveryBlock(visibleAnswer, answerFormat)
|
|
520
|
+
: visibleAnswer);
|
|
548
521
|
streamFinished = true;
|
|
549
522
|
textReceipt = createDeliveryReceipt({
|
|
550
523
|
deliveryId: messageId,
|
|
551
|
-
presentation:
|
|
524
|
+
presentation: result?.presentation
|
|
525
|
+
?? stream.presentation
|
|
526
|
+
?? `${this.#descriptor.key}-stream`,
|
|
552
527
|
providerMessageIds: [
|
|
553
528
|
...providerMessageIdsFor(stream),
|
|
554
529
|
...providerMessageIdsFor(result),
|
|
555
530
|
],
|
|
531
|
+
deliveryOutcome: result?.deliveryOutcome,
|
|
532
|
+
reason: result?.reason,
|
|
556
533
|
});
|
|
557
534
|
} catch (error) {
|
|
558
535
|
stream.cancel?.();
|
|
@@ -564,40 +541,74 @@ export class TextHarnessBridge {
|
|
|
564
541
|
}
|
|
565
542
|
if (!streamFinished) {
|
|
566
543
|
try {
|
|
567
|
-
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);
|
|
568
550
|
textReceipt = createDeliveryReceipt({
|
|
569
551
|
deliveryId: messageId,
|
|
570
|
-
presentation: `${this.#descriptor.key}-text`,
|
|
552
|
+
presentation: result?.presentation ?? `${this.#descriptor.key}-text`,
|
|
571
553
|
providerMessageIds: providerMessageIdsFor(result),
|
|
554
|
+
deliveryOutcome: result?.deliveryOutcome,
|
|
555
|
+
reason: result?.reason,
|
|
572
556
|
});
|
|
573
557
|
} catch (error) {
|
|
574
558
|
textDeliveryError = error;
|
|
575
559
|
}
|
|
576
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
|
+
}
|
|
577
566
|
// A failed final text must not discard an already registered result file.
|
|
578
567
|
// Settle the independent attachment path before surfacing the text error.
|
|
579
568
|
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
|
580
|
-
if (textDeliveryError && !delivery.userVisible)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}
|
|
584
578
|
return delivery.receipt;
|
|
585
579
|
} catch (error) {
|
|
586
580
|
if (error?.code === 'turn-stopped') {
|
|
587
581
|
if (stream) {
|
|
588
582
|
try {
|
|
589
|
-
await stream.finish('已停止。');
|
|
583
|
+
await stream.finish(t('已停止。'));
|
|
590
584
|
} catch {
|
|
591
585
|
stream.cancel?.();
|
|
592
586
|
}
|
|
593
587
|
}
|
|
594
588
|
return;
|
|
595
589
|
}
|
|
596
|
-
|
|
597
|
-
|
|
590
|
+
if (this.#signal?.aborted) {
|
|
591
|
+
stream?.cancel?.();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
598
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
|
+
};
|
|
599
608
|
const imageErrorMessage = imagePromptUserMessage(error);
|
|
600
609
|
if (imageErrorMessage) {
|
|
610
|
+
if (await presentStreamFailure(imageErrorMessage)) return;
|
|
611
|
+
stream?.cancel?.();
|
|
601
612
|
try {
|
|
602
613
|
await this.#bot.sendText(target, imageErrorMessage);
|
|
603
614
|
} catch (sendError) {
|
|
@@ -610,6 +621,8 @@ export class TextHarnessBridge {
|
|
|
610
621
|
}
|
|
611
622
|
const fileErrorMessage = inboundFileUserMessage(error);
|
|
612
623
|
if (fileErrorMessage) {
|
|
624
|
+
if (await presentStreamFailure(fileErrorMessage)) return;
|
|
625
|
+
stream?.cancel?.();
|
|
613
626
|
try {
|
|
614
627
|
await this.#bot.sendText(target, fileErrorMessage);
|
|
615
628
|
} catch (sendError) {
|
|
@@ -621,14 +634,19 @@ export class TextHarnessBridge {
|
|
|
621
634
|
return;
|
|
622
635
|
}
|
|
623
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?.();
|
|
624
641
|
try {
|
|
625
|
-
await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
|
|
642
|
+
await this.#bot.sendText(target, t('消息处理失败,请稍后重试。'));
|
|
626
643
|
} catch (sendError) {
|
|
627
644
|
this.#logger.error?.(
|
|
628
645
|
`[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
|
|
629
646
|
sendError,
|
|
630
647
|
);
|
|
631
648
|
}
|
|
649
|
+
return error.deliveryReceipt;
|
|
632
650
|
} finally {
|
|
633
651
|
await Promise.allSettled([
|
|
634
652
|
this.#cancelPendingInteraction(conversationKey),
|
|
@@ -664,7 +682,7 @@ export class TextHarnessBridge {
|
|
|
664
682
|
const text = cleanText(message.content);
|
|
665
683
|
if (!text || hasInboundImages(message) || hasInboundFiles(message)) {
|
|
666
684
|
try {
|
|
667
|
-
await this.#bot.sendText(target, '请用文字回答当前问题。');
|
|
685
|
+
await this.#bot.sendText(target, t('请用文字回答当前问题。'));
|
|
668
686
|
} catch (error) {
|
|
669
687
|
this.#logger.error?.(
|
|
670
688
|
`[dsh-im:${this.#descriptor.key}] failed to reject a non-text interaction reply:`,
|
|
@@ -692,7 +710,7 @@ export class TextHarnessBridge {
|
|
|
692
710
|
try {
|
|
693
711
|
await this.#presentInteraction(pending);
|
|
694
712
|
} catch (error) {
|
|
695
|
-
this.#status.lastError =
|
|
713
|
+
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
|
696
714
|
this.#logger.error?.(
|
|
697
715
|
`[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
|
|
698
716
|
error,
|
|
@@ -730,7 +748,7 @@ export class TextHarnessBridge {
|
|
|
730
748
|
try {
|
|
731
749
|
await this.#presentInteraction(pending);
|
|
732
750
|
} catch (error) {
|
|
733
|
-
this.#status.lastError =
|
|
751
|
+
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
|
734
752
|
this.#logger.error?.(
|
|
735
753
|
`[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
|
|
736
754
|
error,
|
|
@@ -756,7 +774,7 @@ export class TextHarnessBridge {
|
|
|
756
774
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
757
775
|
if (this.#signal?.aborted) return;
|
|
758
776
|
try {
|
|
759
|
-
await this.#bot.sendText(target, INTERACTION_RESOLVED_TEXT);
|
|
777
|
+
await this.#bot.sendText(target, t(INTERACTION_RESOLVED_TEXT));
|
|
760
778
|
} catch (sendError) {
|
|
761
779
|
this.#logger.error?.(
|
|
762
780
|
`[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
|
|
@@ -769,13 +787,13 @@ export class TextHarnessBridge {
|
|
|
769
787
|
pending.submitting = false;
|
|
770
788
|
pending.answers.pop();
|
|
771
789
|
pending.index -= 1;
|
|
772
|
-
this.#status.lastError = '回答提交失败。';
|
|
790
|
+
this.#status.lastError = t('回答提交失败。');
|
|
773
791
|
this.#logger.error?.(
|
|
774
792
|
`[dsh-im:${this.#descriptor.key}] failed to answer a Harness interaction:`,
|
|
775
793
|
error,
|
|
776
794
|
);
|
|
777
795
|
try {
|
|
778
|
-
await this.#bot.sendText(target, '回答提交失败,请重新发送当前问题的答案。');
|
|
796
|
+
await this.#bot.sendText(target, t('回答提交失败,请重新发送当前问题的答案。'));
|
|
779
797
|
} catch (sendError) {
|
|
780
798
|
this.#logger.error?.(
|
|
781
799
|
`[dsh-im:${this.#descriptor.key}] failed to send an interaction retry notice:`,
|
|
@@ -822,7 +840,7 @@ export class TextHarnessBridge {
|
|
|
822
840
|
try {
|
|
823
841
|
await this.#bot.sendText(
|
|
824
842
|
target,
|
|
825
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
843
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
826
844
|
);
|
|
827
845
|
} catch (error) {
|
|
828
846
|
this.#logger.error?.(
|
|
@@ -923,7 +941,7 @@ export class TextHarnessBridge {
|
|
|
923
941
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
924
942
|
}
|
|
925
943
|
try {
|
|
926
|
-
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
|
|
944
|
+
await this.#bot.sendText(message.replyTarget, t(INTERACTION_RESOLVED_TEXT));
|
|
927
945
|
} catch (error) {
|
|
928
946
|
this.#logger.error?.(
|
|
929
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
|
},
|