@xmanrui/dsh-im 2.4.0 → 2.5.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 +1 -1
- package/README.md +1 -1
- package/lib/client.js +321 -109
- package/lib/index.js +214 -207
- package/package.json +1 -1
- package/plugin-src/client/channel-card-meta.js +36 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +9 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +9 -1
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +9 -1
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -1
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +9 -1
- package/plugin-src/client/channels/weixin/api.js +2 -10
- package/plugin-src/client/channels/weixin/index.js +9 -5
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +9 -1
- package/plugin-src/client/i18n.js +4 -0
- package/plugin-src/client/index.js +16 -2
- package/plugin-src/client/last-message-error.js +17 -0
- package/plugin-src/client/styles.js +5 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +49 -16
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
- package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
- package/src/channels/feishu/bridge.mjs +99 -43
- package/src/channels/feishu/multi-bot-controller.mjs +2 -0
- package/src/channels/qq/qq-bridge.mjs +83 -28
- package/src/channels/qq/qq-controller.mjs +2 -0
- package/src/channels/shared/harness-client.mjs +40 -4
- package/src/channels/shared/i18n-en/shared-a.mjs +77 -0
- package/src/channels/shared/message-failure.mjs +244 -0
- package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
- package/src/channels/shared/text-harness-bridge.mjs +65 -60
- package/src/channels/shared/token-bot-controller.mjs +2 -0
- package/src/channels/slack/slack-controller.mjs +2 -0
- package/src/channels/wecom/wecom-bridge.mjs +49 -15
- package/src/channels/wecom/wecom-controller.mjs +2 -0
- package/src/channels/weixin/weixin-api.mjs +47 -0
- package/src/channels/weixin/weixin-bridge.mjs +261 -43
- package/src/channels/weixin/weixin-controller.mjs +2 -15
- package/src/channels/weixin/weixin-runtime.mjs +1 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
|
|
3
3
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
4
4
|
import { t } from '../shared/i18n.mjs';
|
|
5
|
+
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
5
6
|
import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
|
|
6
7
|
|
|
7
8
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
|
|
@@ -319,6 +320,7 @@ export class QqController {
|
|
|
319
320
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
320
321
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
321
322
|
},
|
|
323
|
+
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
322
324
|
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
323
325
|
};
|
|
324
326
|
});
|
|
@@ -532,6 +532,41 @@ export class HarnessInteractionError extends Error {
|
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
export class HarnessTurnError extends Error {
|
|
536
|
+
constructor(code, { reason, providerCode } = {}) {
|
|
537
|
+
super(`Harness turn failed (${code})`);
|
|
538
|
+
this.name = 'HarnessTurnError';
|
|
539
|
+
this.code = code;
|
|
540
|
+
this.promptAccepted = true;
|
|
541
|
+
if (reason && typeof reason === 'object') this.reason = reason;
|
|
542
|
+
if (typeof providerCode === 'string' && providerCode) this.providerCode = providerCode;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function harnessTurnError(reason) {
|
|
547
|
+
const kind = nonEmptyText(reason?.kind) ?? nonEmptyText(reason);
|
|
548
|
+
if (kind === 'error') {
|
|
549
|
+
const failure = reason?.error ?? reason?.failure;
|
|
550
|
+
return new HarnessTurnError('harness-turn-failed', {
|
|
551
|
+
reason,
|
|
552
|
+
providerCode: nonEmptyText(failure?.code) ?? undefined,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (kind === 'max-tokens') return new HarnessTurnError('model-max-tokens', { reason });
|
|
556
|
+
if (kind === 'blocked') return new HarnessTurnError('turn-blocked', { reason });
|
|
557
|
+
if (['interrupted', 'stopped', 'cancelled', 'canceled'].includes(kind)) {
|
|
558
|
+
return new HarnessTurnError('turn-interrupted', { reason });
|
|
559
|
+
}
|
|
560
|
+
if (kind === 'aborted') return new HarnessTurnError('turn-aborted', { reason });
|
|
561
|
+
if (kind === 'completed') return new HarnessTurnError('model-empty-response', { reason });
|
|
562
|
+
return new HarnessTurnError('harness-turn-failed', { reason });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function harnessTurnSucceeded(reason) {
|
|
566
|
+
if (reason === null || reason === undefined) return true;
|
|
567
|
+
return (nonEmptyText(reason?.kind) ?? nonEmptyText(reason)) === 'completed';
|
|
568
|
+
}
|
|
569
|
+
|
|
535
570
|
export class HarnessClient {
|
|
536
571
|
#baseUrl;
|
|
537
572
|
#workspace;
|
|
@@ -1282,6 +1317,9 @@ export class HarnessClient {
|
|
|
1282
1317
|
}
|
|
1283
1318
|
if (!tracker.finished) continue;
|
|
1284
1319
|
turnFinished = true;
|
|
1320
|
+
if (!ownership?.stopRequested && !harnessTurnSucceeded(tracker.reason)) {
|
|
1321
|
+
throw harnessTurnError(tracker.reason);
|
|
1322
|
+
}
|
|
1285
1323
|
// An accepted /stop revokes attachment delivery even when Harness
|
|
1286
1324
|
// preserved a useful partial text answer for the existing UX.
|
|
1287
1325
|
const artifactCount = ownership?.stopRequested
|
|
@@ -1292,11 +1330,9 @@ export class HarnessClient {
|
|
|
1292
1330
|
}
|
|
1293
1331
|
if (artifactCount > 0) return '';
|
|
1294
1332
|
if (ownership?.stopRequested) throw turnStoppedError();
|
|
1295
|
-
throw
|
|
1296
|
-
`Harness turn ended without a text reply${tracker.reason ? ` (${JSON.stringify(tracker.reason)})` : ''}`,
|
|
1297
|
-
);
|
|
1333
|
+
throw harnessTurnError(tracker.reason);
|
|
1298
1334
|
}
|
|
1299
|
-
throw new
|
|
1335
|
+
throw new HarnessTurnError('harness-reply-timeout');
|
|
1300
1336
|
} catch (error) {
|
|
1301
1337
|
// Once cancellation was accepted, transport/poll failures and timeouts
|
|
1302
1338
|
// describe the convergence of that stop, not an unrelated ask failure.
|
|
@@ -27,6 +27,83 @@ export default {
|
|
|
27
27
|
'结果文件「{name}」已生成,但当前渠道暂时未能发送,请稍后重试。':
|
|
28
28
|
'The result file "{name}" was generated, but this channel could not send it right now. Please try again later.',
|
|
29
29
|
'消息处理失败,请稍后重试。': 'Failed to process the message. Please try again later.',
|
|
30
|
+
'卡片已结束,请查看后续消息。': 'This card has ended. Please check the next message.',
|
|
31
|
+
'工具调用「{name}」未成功,请检查工具配置或稍后重试。': 'Tool call "{name}" did not succeed. Check the tool configuration or try again later.',
|
|
32
|
+
'无法连接处理服务,消息尚未提交。请确认 DeepSeek Harness 正在运行后重试。':
|
|
33
|
+
'Could not connect to the processing service, so the message was not submitted. Make sure DeepSeek Harness is running, then try again.',
|
|
34
|
+
'处理服务响应超时,消息尚未开始处理。请稍后重试。':
|
|
35
|
+
'The processing service timed out before the message started. Please try again later.',
|
|
36
|
+
'暂时无法确认任务状态,任务可能已经开始。请先等待或发送 /stop,不要立即重复提交。':
|
|
37
|
+
'The task status could not be confirmed and the task may have started. Wait or send /stop before submitting it again.',
|
|
38
|
+
'消息提交结果未能确认,任务可能已经开始。请先等待或发送 /status 查看状态,不要立即重复提交。':
|
|
39
|
+
'The message submission could not be confirmed and the task may have started. Wait or send /status before submitting it again.',
|
|
40
|
+
'暂时无法读取任务进度,任务可能仍在运行。请先等待或发送 /stop,不要立即重复提交。':
|
|
41
|
+
'Task progress is temporarily unavailable and the task may still be running. Wait or send /stop before submitting it again.',
|
|
42
|
+
'处理服务拒绝了机器人连接。请管理员检查 Harness 地址、代理或访问配置后重试。':
|
|
43
|
+
'The processing service rejected the bot connection. Ask an administrator to check the Harness address, proxy, or access settings.',
|
|
44
|
+
'机器人与 DeepSeek Harness 的接口不兼容。请管理员检查 Harness 地址并更新相关版本。':
|
|
45
|
+
'The bot and DeepSeek Harness use incompatible APIs. Ask an administrator to check the Harness address and update the related components.',
|
|
46
|
+
'DeepSeek Harness 暂时无法完成请求,请稍后重试。':
|
|
47
|
+
'DeepSeek Harness could not complete the request. Please try again later.',
|
|
48
|
+
'等待模型回复超时,任务可能仍在运行。请先等待或发送 /stop,不要立即重复提交。':
|
|
49
|
+
'Waiting for the model reply timed out and the task may still be running. Wait or send /stop before submitting it again.',
|
|
50
|
+
'模型凭据缺失或已失效。请管理员检查模型配置后重试。':
|
|
51
|
+
'The model credentials are missing or invalid. Ask an administrator to check the model settings.',
|
|
52
|
+
'模型额度或余额不足,本次任务未完成。请管理员补充额度或切换模型后重试。':
|
|
53
|
+
'The model quota or balance is insufficient. Ask an administrator to add quota or switch models, then try again.',
|
|
54
|
+
'模型服务正在限流,本次任务未完成。请稍后重试。':
|
|
55
|
+
'The model service is rate-limiting requests. Please try again later.',
|
|
56
|
+
'当前会话内容超过模型上下文上限。请发送 /compact 或 /new 后重试。':
|
|
57
|
+
'This conversation exceeds the model context limit. Send /compact or /new, then try again.',
|
|
58
|
+
'当前模型不存在或暂不可用。请发送 /models 查看并使用 /model 切换模型。':
|
|
59
|
+
'The current model does not exist or is unavailable. Send /models and use /model to switch models.',
|
|
60
|
+
'当前模型不存在或不支持所选配置。请发送 /models,并使用 /model 重新选择。':
|
|
61
|
+
'The current model does not exist or does not support the selected settings. Send /models and use /model to choose again.',
|
|
62
|
+
'当前模型不支持这类内容或所选配置。请调整内容、模型或推理等级后重试。':
|
|
63
|
+
'The current model does not support this content or the selected settings. Adjust the content, model, or reasoning effort and try again.',
|
|
64
|
+
'当前模型不支持所选配置。请切换模型或推理等级后重试。':
|
|
65
|
+
'The current model does not support the selected settings. Switch the model or reasoning effort, then try again.',
|
|
66
|
+
'模型服务响应超时,本次任务未完成。请稍后重试。':
|
|
67
|
+
'The model service timed out and the task did not finish. Please try again later.',
|
|
68
|
+
'暂时无法连接模型服务,本次任务未完成。请稍后重试。':
|
|
69
|
+
'The model service is temporarily unreachable and the task did not finish. Please try again later.',
|
|
70
|
+
'模型服务暂时异常,本次任务未完成。请稍后重试。':
|
|
71
|
+
'The model service is temporarily unavailable and the task did not finish. Please try again later.',
|
|
72
|
+
'模型回复中断或格式异常,本次任务未完成。请重试。':
|
|
73
|
+
'The model reply was interrupted or malformed, so the task did not finish. Please try again.',
|
|
74
|
+
'模型没有返回可显示的内容。请重试;若持续发生,请切换模型。':
|
|
75
|
+
'The model returned no displayable content. Try again, or switch models if this keeps happening.',
|
|
76
|
+
'模型拒绝处理当前内容。请调整问题内容后重试。':
|
|
77
|
+
'The model rejected the current content. Revise the request and try again.',
|
|
78
|
+
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。':
|
|
79
|
+
'The model reached its output limit without producing a displayable result. Reduce the task scope and try again.',
|
|
80
|
+
'任务正在等待无法在当前渠道完成的操作。请在 DeepSeek Harness 中处理后再试。':
|
|
81
|
+
'The task is waiting for an action that cannot be completed in this channel. Handle it in DeepSeek Harness, then try again.',
|
|
82
|
+
'任务被意外中断,本次未完成。请重试。':
|
|
83
|
+
'The task was unexpectedly interrupted and did not finish. Please try again.',
|
|
84
|
+
'当前会话已不存在。请发送 /new 创建新会话后重试。':
|
|
85
|
+
'The current Session no longer exists. Send /new to create one, then try again.',
|
|
86
|
+
'当前会话仍在处理上一项任务。请等待完成,或发送 /stop 后重试。':
|
|
87
|
+
'The current Session is still processing the previous task. Wait for it to finish, or send /stop before trying again.',
|
|
88
|
+
'工作区或会话状态刚刚发生变化。请重新发送这条消息。':
|
|
89
|
+
'The Workspace or Session state just changed. Please send this message again.',
|
|
90
|
+
'当前工作区不存在或暂不可用。请重新选择工作区后重试。':
|
|
91
|
+
'The current Workspace does not exist or is unavailable. Select another Workspace and try again.',
|
|
92
|
+
'当前 Agent Preset 不存在或暂不可用。请发送 /presetlist 后重新选择。':
|
|
93
|
+
'The current Agent Preset does not exist or is unavailable. Send /presetlist and select another one.',
|
|
94
|
+
'回复已经生成,但机器人没有发送权限。请联系管理员检查渠道权限或重新绑定机器人。':
|
|
95
|
+
'The reply was generated, but the bot cannot send it. Ask an administrator to check channel permissions or reconnect the bot.',
|
|
96
|
+
'回复已经生成,但当前渠道正在限流,暂时无法发送。请稍后重试。':
|
|
97
|
+
'The reply was generated, but this channel is rate-limiting messages. Please try again later.',
|
|
98
|
+
'回复发送结果未能确认。请先检查聊天内是否已经收到,不要立即重复提交。':
|
|
99
|
+
'Reply delivery could not be confirmed. Check whether it already arrived before submitting the task again.',
|
|
100
|
+
'回复已经生成,但当前渠道暂时无法发送。请稍后重试。':
|
|
101
|
+
'The reply was generated, but this channel could not send it. Please try again later.',
|
|
102
|
+
'当前消息包含无法处理的图片或文件。请调整后重新发送。':
|
|
103
|
+
'This message contains an image or file that cannot be processed. Adjust it and send it again.',
|
|
104
|
+
'任务未完成,暂时无法确定原因。请重试;若持续发生,请将参考号提供给管理员。':
|
|
105
|
+
'The task did not finish and the cause could not be determined. Try again; if it persists, give the reference ID to an administrator.',
|
|
106
|
+
'错误码:{code};参考号:{referenceId}': 'Error code: {code}; reference: {referenceId}',
|
|
30
107
|
'{label}机器人': '{label} bot',
|
|
31
108
|
'目前支持文字和图片消息。': 'Only text and image messages are supported at the moment.',
|
|
32
109
|
'目前支持文字、图片和文件消息。':
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { t } from './i18n.mjs';
|
|
4
|
+
|
|
5
|
+
const PROVIDER_FAILURES = Object.freeze({
|
|
6
|
+
AUTH: 'MODEL_AUTH',
|
|
7
|
+
MISSING_CREDENTIAL: 'MODEL_AUTH',
|
|
8
|
+
INVALID_CREDENTIAL: 'MODEL_AUTH',
|
|
9
|
+
QUOTA: 'MODEL_QUOTA',
|
|
10
|
+
RATE_LIMIT: 'MODEL_RATE_LIMIT',
|
|
11
|
+
CONTEXT_WINDOW_EXCEEDED: 'MODEL_CONTEXT_LIMIT',
|
|
12
|
+
UNKNOWN_MODEL: 'MODEL_UNAVAILABLE',
|
|
13
|
+
NO_ADAPTER: 'MODEL_UNAVAILABLE',
|
|
14
|
+
UNSUPPORTED_OPTION: 'MODEL_CONFIG',
|
|
15
|
+
UNSUPPORTED_REASONING_EFFORT: 'MODEL_CONFIG',
|
|
16
|
+
TIMEOUT: 'MODEL_TIMEOUT',
|
|
17
|
+
TRANSPORT: 'MODEL_TRANSPORT',
|
|
18
|
+
SERVER: 'MODEL_SERVICE',
|
|
19
|
+
STREAM_CLOSED: 'MODEL_STREAM',
|
|
20
|
+
MALFORMED_RESPONSE: 'MODEL_STREAM',
|
|
21
|
+
EMPTY_RESPONSE: 'MODEL_EMPTY_REPLY',
|
|
22
|
+
CONTENT_FILTER: 'MODEL_CONTENT_REJECTED',
|
|
23
|
+
UNSUPPORTED_CONTENT: 'MODEL_CONFIG',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const FAILURE_MESSAGES = Object.freeze({
|
|
27
|
+
HARNESS_CONNECT:
|
|
28
|
+
'无法连接处理服务,消息尚未提交。请确认 DeepSeek Harness 正在运行后重试。',
|
|
29
|
+
HARNESS_TIMEOUT:
|
|
30
|
+
'处理服务响应超时,消息尚未开始处理。请稍后重试。',
|
|
31
|
+
HARNESS_RESULT_UNCERTAIN:
|
|
32
|
+
'暂时无法确认任务状态,任务可能已经开始。请先等待或发送 /stop,不要立即重复提交。',
|
|
33
|
+
HARNESS_ACCESS:
|
|
34
|
+
'处理服务拒绝了机器人连接。请管理员检查 Harness 地址、代理或访问配置后重试。',
|
|
35
|
+
HARNESS_PROTOCOL:
|
|
36
|
+
'机器人与 DeepSeek Harness 的接口不兼容。请管理员检查 Harness 地址并更新相关版本。',
|
|
37
|
+
HARNESS_SERVICE:
|
|
38
|
+
'DeepSeek Harness 暂时无法完成请求,请稍后重试。',
|
|
39
|
+
MODEL_REPLY_TIMEOUT:
|
|
40
|
+
'等待模型回复超时,任务可能仍在运行。请先等待或发送 /stop,不要立即重复提交。',
|
|
41
|
+
MODEL_AUTH:
|
|
42
|
+
'模型凭据缺失或已失效。请管理员检查模型配置后重试。',
|
|
43
|
+
MODEL_QUOTA:
|
|
44
|
+
'模型额度或余额不足,本次任务未完成。请管理员补充额度或切换模型后重试。',
|
|
45
|
+
MODEL_RATE_LIMIT:
|
|
46
|
+
'模型服务正在限流,本次任务未完成。请稍后重试。',
|
|
47
|
+
MODEL_CONTEXT_LIMIT:
|
|
48
|
+
'当前会话内容超过模型上下文上限。请发送 /compact 或 /new 后重试。',
|
|
49
|
+
MODEL_UNAVAILABLE:
|
|
50
|
+
'当前模型不存在或不支持所选配置。请发送 /models,并使用 /model 重新选择。',
|
|
51
|
+
MODEL_CONFIG:
|
|
52
|
+
'当前模型不支持这类内容或所选配置。请调整内容、模型或推理等级后重试。',
|
|
53
|
+
MODEL_TIMEOUT:
|
|
54
|
+
'模型服务响应超时,本次任务未完成。请稍后重试。',
|
|
55
|
+
MODEL_TRANSPORT:
|
|
56
|
+
'暂时无法连接模型服务,本次任务未完成。请稍后重试。',
|
|
57
|
+
MODEL_SERVICE:
|
|
58
|
+
'模型服务暂时异常,本次任务未完成。请稍后重试。',
|
|
59
|
+
MODEL_STREAM:
|
|
60
|
+
'模型回复中断或格式异常,本次任务未完成。请重试。',
|
|
61
|
+
MODEL_EMPTY_REPLY:
|
|
62
|
+
'模型没有返回可显示的内容。请重试;若持续发生,请切换模型。',
|
|
63
|
+
MODEL_CONTENT_REJECTED:
|
|
64
|
+
'模型拒绝处理当前内容。请调整问题内容后重试。',
|
|
65
|
+
MODEL_OUTPUT_LIMIT:
|
|
66
|
+
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。',
|
|
67
|
+
TURN_BLOCKED:
|
|
68
|
+
'任务正在等待无法在当前渠道完成的操作。请在 DeepSeek Harness 中处理后再试。',
|
|
69
|
+
TURN_INTERRUPTED:
|
|
70
|
+
'任务被意外中断,本次未完成。请重试。',
|
|
71
|
+
SESSION_NOT_FOUND:
|
|
72
|
+
'当前会话已不存在。请发送 /new 创建新会话后重试。',
|
|
73
|
+
SESSION_BUSY:
|
|
74
|
+
'当前会话仍在处理上一项任务。请等待完成,或发送 /stop 后重试。',
|
|
75
|
+
SESSION_STALE:
|
|
76
|
+
'工作区或会话状态刚刚发生变化。请重新发送这条消息。',
|
|
77
|
+
WORKSPACE_UNAVAILABLE:
|
|
78
|
+
'当前工作区不存在或暂不可用。请重新选择工作区后重试。',
|
|
79
|
+
PRESET_UNAVAILABLE:
|
|
80
|
+
'当前 Agent Preset 不存在或暂不可用。请发送 /presetlist 后重新选择。',
|
|
81
|
+
CHANNEL_PERMISSION:
|
|
82
|
+
'回复已经生成,但机器人没有发送权限。请联系管理员检查渠道权限或重新绑定机器人。',
|
|
83
|
+
CHANNEL_RATE_LIMIT:
|
|
84
|
+
'回复已经生成,但当前渠道正在限流,暂时无法发送。请稍后重试。',
|
|
85
|
+
CHANNEL_DELIVERY_UNCERTAIN:
|
|
86
|
+
'回复发送结果未能确认。请先检查聊天内是否已经收到,不要立即重复提交。',
|
|
87
|
+
CHANNEL_DELIVERY:
|
|
88
|
+
'回复已经生成,但当前渠道暂时无法发送。请稍后重试。',
|
|
89
|
+
INPUT_INVALID:
|
|
90
|
+
'当前消息包含无法处理的图片或文件。请调整后重新发送。',
|
|
91
|
+
INTERNAL_UNKNOWN:
|
|
92
|
+
'任务未完成,暂时无法确定原因。请重试;若持续发生,请将参考号提供给管理员。',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function providerFailureCode(error) {
|
|
96
|
+
if (error?.code !== 'harness-turn-failed') return null;
|
|
97
|
+
const value = error?.providerCode ?? error?.details?.providerCode;
|
|
98
|
+
if (typeof value !== 'string') return null;
|
|
99
|
+
return PROVIDER_FAILURES[value.trim().toUpperCase()] ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function failureCode(error) {
|
|
103
|
+
const code = typeof error?.code === 'string' ? error.code : '';
|
|
104
|
+
const providerCode = providerFailureCode(error);
|
|
105
|
+
if (providerCode) return providerCode;
|
|
106
|
+
|
|
107
|
+
if (code === 'harness-connect-failed') {
|
|
108
|
+
return ['session.prompt', 'session.history'].includes(error?.method)
|
|
109
|
+
? 'HARNESS_RESULT_UNCERTAIN'
|
|
110
|
+
: 'HARNESS_CONNECT';
|
|
111
|
+
}
|
|
112
|
+
if (code === 'harness-timeout') {
|
|
113
|
+
return ['session.prompt', 'session.history'].includes(error?.method)
|
|
114
|
+
? 'HARNESS_RESULT_UNCERTAIN'
|
|
115
|
+
: 'HARNESS_TIMEOUT';
|
|
116
|
+
}
|
|
117
|
+
if (code === 'harness-reply-timeout') return 'MODEL_REPLY_TIMEOUT';
|
|
118
|
+
if ([
|
|
119
|
+
'harness-auth-required', 'harness-proxy-auth-required',
|
|
120
|
+
'harness-loopback-forbidden', 'harness-host-untrusted',
|
|
121
|
+
'harness-request-forbidden',
|
|
122
|
+
].includes(code)) return 'HARNESS_ACCESS';
|
|
123
|
+
if (['harness-api-not-found', 'harness-response-invalid'].includes(code)) {
|
|
124
|
+
return 'HARNESS_PROTOCOL';
|
|
125
|
+
}
|
|
126
|
+
if (code === 'harness-turn-failed') return 'INTERNAL_UNKNOWN';
|
|
127
|
+
if (['harness-http-failed', 'harness-rpc-rejected'].includes(code)) return 'HARNESS_SERVICE';
|
|
128
|
+
if (code === 'model-empty-response') return 'MODEL_EMPTY_REPLY';
|
|
129
|
+
if (code === 'model-max-tokens') return 'MODEL_OUTPUT_LIMIT';
|
|
130
|
+
if (code === 'turn-blocked') return 'TURN_BLOCKED';
|
|
131
|
+
if (['turn-interrupted', 'turn-aborted'].includes(code)) return 'TURN_INTERRUPTED';
|
|
132
|
+
if (code === 'session-not-found') return 'SESSION_NOT_FOUND';
|
|
133
|
+
if (code === 'agent-busy') return 'SESSION_BUSY';
|
|
134
|
+
if (code === 'workspace-session-stale') return 'SESSION_STALE';
|
|
135
|
+
if (code.startsWith('workspace-')) return 'WORKSPACE_UNAVAILABLE';
|
|
136
|
+
if (code.startsWith('agent-preset-')) return 'PRESET_UNAVAILABLE';
|
|
137
|
+
if (code.startsWith('image-') || code.startsWith('inbound-file-')
|
|
138
|
+
|| code === 'attachment-error') return 'INPUT_INVALID';
|
|
139
|
+
|
|
140
|
+
const status = Number(error?.status ?? error?.httpStatus);
|
|
141
|
+
if (status === 401 || status === 403
|
|
142
|
+
|| [
|
|
143
|
+
'channel-permission',
|
|
144
|
+
'permission-required',
|
|
145
|
+
'artifact-permission-required',
|
|
146
|
+
'forbidden',
|
|
147
|
+
'stale-token',
|
|
148
|
+
].includes(code)) {
|
|
149
|
+
return 'CHANNEL_PERMISSION';
|
|
150
|
+
}
|
|
151
|
+
if (status === 429
|
|
152
|
+
|| ['channel-rate-limit', 'rate-limited', 'artifact-rate-limited'].includes(code)) {
|
|
153
|
+
return 'CHANNEL_RATE_LIMIT';
|
|
154
|
+
}
|
|
155
|
+
if (['channel-delivery-uncertain', 'delivery-uncertain', 'artifact-delivery-uncertain']
|
|
156
|
+
.includes(code)) {
|
|
157
|
+
return 'CHANNEL_DELIVERY_UNCERTAIN';
|
|
158
|
+
}
|
|
159
|
+
if (code === 'channel-delivery-failed' || code.startsWith('artifact-')
|
|
160
|
+
|| ['network-error', 'timeout'].includes(code)) {
|
|
161
|
+
return 'CHANNEL_DELIVERY';
|
|
162
|
+
}
|
|
163
|
+
return 'INTERNAL_UNKNOWN';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function safeReferenceId(value) {
|
|
167
|
+
return typeof value === 'string' && /^[A-Z0-9-]{6,40}$/u.test(value)
|
|
168
|
+
? value
|
|
169
|
+
: `MF-${randomUUID().slice(0, 8).toUpperCase()}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function safeFailureReason(value) {
|
|
173
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/u.test(value)) return null;
|
|
174
|
+
return value.toUpperCase().replaceAll('-', '_');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function classifyMessageFailure(error, {
|
|
178
|
+
userMessage,
|
|
179
|
+
reason,
|
|
180
|
+
referenceId,
|
|
181
|
+
at = Date.now(),
|
|
182
|
+
} = {}) {
|
|
183
|
+
const safeReason = safeFailureReason(reason);
|
|
184
|
+
const classifiedCode = failureCode(error);
|
|
185
|
+
const code = classifiedCode === 'INTERNAL_UNKNOWN'
|
|
186
|
+
&& safeReason
|
|
187
|
+
&& typeof userMessage === 'string'
|
|
188
|
+
&& userMessage.trim()
|
|
189
|
+
? 'INPUT_INVALID'
|
|
190
|
+
: classifiedCode;
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
code,
|
|
193
|
+
reason: safeReason ?? code,
|
|
194
|
+
message: typeof userMessage === 'string' && userMessage.trim()
|
|
195
|
+
? userMessage.trim()
|
|
196
|
+
: t(FAILURE_MESSAGES[code]),
|
|
197
|
+
referenceId: safeReferenceId(referenceId),
|
|
198
|
+
at: Number.isFinite(at) ? at : Date.now(),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function messageFailureText(failure) {
|
|
203
|
+
return `${failure.message}\n\n${t('错误码:{code};参考号:{referenceId}', failure)}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function setLastMessageFailure(status, error, options) {
|
|
207
|
+
const failure = classifyMessageFailure(error, options);
|
|
208
|
+
status.lastMessageError = failure;
|
|
209
|
+
return failure;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function clearLastMessageFailure(status) {
|
|
213
|
+
status.lastMessageError = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function channelDeliveryFailure(error, { uncertain = true } = {}) {
|
|
217
|
+
const wrapped = new Error('Channel message delivery failed', { cause: error });
|
|
218
|
+
const status = Number(error?.status ?? error?.httpStatus);
|
|
219
|
+
wrapped.code = status === 401 || status === 403
|
|
220
|
+
? 'channel-permission'
|
|
221
|
+
: status === 429
|
|
222
|
+
? 'channel-rate-limit'
|
|
223
|
+
: uncertain
|
|
224
|
+
? 'channel-delivery-uncertain'
|
|
225
|
+
: 'channel-delivery-failed';
|
|
226
|
+
if (Number.isInteger(status)) wrapped.status = status;
|
|
227
|
+
return wrapped;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function publicMessageFailure(value) {
|
|
231
|
+
if (!value || typeof value !== 'object'
|
|
232
|
+
|| typeof value.code !== 'string' || !value.code
|
|
233
|
+
|| typeof value.reason !== 'string' || !value.reason
|
|
234
|
+
|| typeof value.message !== 'string' || !value.message
|
|
235
|
+
|| typeof value.referenceId !== 'string' || !value.referenceId
|
|
236
|
+
|| !Number.isFinite(value.at)) return null;
|
|
237
|
+
return {
|
|
238
|
+
code: value.code.slice(0, 64),
|
|
239
|
+
reason: value.reason.slice(0, 64),
|
|
240
|
+
message: value.message.slice(0, 500),
|
|
241
|
+
referenceId: value.referenceId.slice(0, 40),
|
|
242
|
+
at: value.at,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -73,6 +73,7 @@ export async function deliverOutboundArtifacts({
|
|
|
73
73
|
sendFile,
|
|
74
74
|
sendImage,
|
|
75
75
|
sendFailureNotice,
|
|
76
|
+
onFailure,
|
|
76
77
|
logger,
|
|
77
78
|
}) {
|
|
78
79
|
const receipts = baseReceipt ? [baseReceipt] : [];
|
|
@@ -107,14 +108,20 @@ export async function deliverOutboundArtifacts({
|
|
|
107
108
|
} catch (error) {
|
|
108
109
|
if (isAbort(error, signal)) throw error;
|
|
109
110
|
artifactSendErrors += 1;
|
|
111
|
+
const failure = typeof onFailure === 'function'
|
|
112
|
+
? await onFailure(artifact, error)
|
|
113
|
+
: null;
|
|
114
|
+
const reference = typeof failure?.referenceId === 'string'
|
|
115
|
+
? ` [${failure.referenceId}]`
|
|
116
|
+
: '';
|
|
110
117
|
logger?.warn?.(
|
|
111
|
-
`[dsh-im:${channelKey}] result artifact delivery failed (${error?.code ?? 'unknown'})`,
|
|
118
|
+
`[dsh-im:${channelKey}] result artifact delivery failed${reference} (${error?.code ?? 'unknown'})`,
|
|
112
119
|
);
|
|
113
120
|
let messageIds = [];
|
|
114
121
|
if (typeof sendFailureNotice === 'function') {
|
|
115
122
|
try {
|
|
116
123
|
signal?.throwIfAborted();
|
|
117
|
-
const notice = await sendFailureNotice(artifact, error);
|
|
124
|
+
const notice = await sendFailureNotice(artifact, error, failure);
|
|
118
125
|
signal?.throwIfAborted();
|
|
119
126
|
messageIds = providerIds(notice);
|
|
120
127
|
failureNoticeVisible = true;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from './batch-input.mjs';
|
|
28
28
|
import {
|
|
29
29
|
hasInboundImages,
|
|
30
|
+
imagePromptDiagnostic,
|
|
30
31
|
imagePromptUserMessage,
|
|
31
32
|
promptContentForMessage,
|
|
32
33
|
} from './image-prompt.mjs';
|
|
@@ -45,6 +46,12 @@ import {
|
|
|
45
46
|
createTextDeliveryBlock,
|
|
46
47
|
providerMessageIdsFor,
|
|
47
48
|
} from './semantic/delivery.mjs';
|
|
49
|
+
import {
|
|
50
|
+
channelDeliveryFailure,
|
|
51
|
+
clearLastMessageFailure,
|
|
52
|
+
messageFailureText,
|
|
53
|
+
setLastMessageFailure,
|
|
54
|
+
} from './message-failure.mjs';
|
|
48
55
|
|
|
49
56
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
50
57
|
const FILE_ONLY_COMPLETION_TEXT = '任务已完成。';
|
|
@@ -106,6 +113,7 @@ export function createTextBridgeStatus() {
|
|
|
106
113
|
lastReplyAt: null,
|
|
107
114
|
lastRejectedAt: null,
|
|
108
115
|
lastError: null,
|
|
116
|
+
lastMessageError: null,
|
|
109
117
|
};
|
|
110
118
|
}
|
|
111
119
|
|
|
@@ -320,8 +328,9 @@ export class TextHarnessBridge {
|
|
|
320
328
|
})().catch(async (error) => {
|
|
321
329
|
if (this.#signal?.aborted) return;
|
|
322
330
|
this.#status.lastError = error?.message ?? String(error);
|
|
331
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
323
332
|
this.#logger.error?.(
|
|
324
|
-
`[dsh-im:${this.#descriptor.key}] failed to process a batch input message:`,
|
|
333
|
+
`[dsh-im:${this.#descriptor.key}] failed to process a batch input message [${failure.referenceId}]:`,
|
|
325
334
|
error,
|
|
326
335
|
);
|
|
327
336
|
}).finally(() => {
|
|
@@ -399,8 +408,12 @@ export class TextHarnessBridge {
|
|
|
399
408
|
} catch (error) {
|
|
400
409
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
401
410
|
this.#status.lastError = error?.message ?? String(error);
|
|
402
|
-
|
|
403
|
-
|
|
411
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
412
|
+
this.#logger.error?.(
|
|
413
|
+
`[dsh-im:${this.#descriptor.key}] failed to process a command [${failure.referenceId}]:`,
|
|
414
|
+
error,
|
|
415
|
+
);
|
|
416
|
+
await this.#bot.sendText(target, messageFailureText(failure)).catch(() => undefined);
|
|
404
417
|
}
|
|
405
418
|
}
|
|
406
419
|
|
|
@@ -426,9 +439,13 @@ export class TextHarnessBridge {
|
|
|
426
439
|
sendFile: typeof this.#bot.sendFile === 'function'
|
|
427
440
|
? (file) => this.#bot.sendFile(target, file)
|
|
428
441
|
: undefined,
|
|
429
|
-
|
|
442
|
+
onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
|
|
443
|
+
userMessage: artifactFailureText(artifact?.fileName, error, this.#descriptor),
|
|
444
|
+
reason: error?.code,
|
|
445
|
+
}),
|
|
446
|
+
sendFailureNotice: (_artifact, _error, failure) => this.#bot.sendText(
|
|
430
447
|
target,
|
|
431
|
-
|
|
448
|
+
messageFailureText(failure),
|
|
432
449
|
),
|
|
433
450
|
logger: this.#logger,
|
|
434
451
|
});
|
|
@@ -436,7 +453,11 @@ export class TextHarnessBridge {
|
|
|
436
453
|
+ delivery.artifactsSent;
|
|
437
454
|
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
438
455
|
+ delivery.artifactSendErrors;
|
|
439
|
-
return {
|
|
456
|
+
return {
|
|
457
|
+
receipt: delivery.receipt,
|
|
458
|
+
userVisible: delivery.userVisible,
|
|
459
|
+
artifactSendErrors: delivery.artifactSendErrors,
|
|
460
|
+
};
|
|
440
461
|
}
|
|
441
462
|
|
|
442
463
|
async #process(message, messageId, senderId, conversationKey, {
|
|
@@ -643,25 +664,37 @@ export class TextHarnessBridge {
|
|
|
643
664
|
reason: result?.reason,
|
|
644
665
|
});
|
|
645
666
|
} catch (error) {
|
|
646
|
-
textDeliveryError = error;
|
|
667
|
+
textDeliveryError = channelDeliveryFailure(error);
|
|
647
668
|
}
|
|
648
669
|
}
|
|
670
|
+
const finalDeliveryUnknown = textReceipt?.deliveryOutcome === 'unknown';
|
|
649
671
|
if (textReceipt?.deliveryOutcome === 'failed') {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
672
|
+
textDeliveryError = channelDeliveryFailure(
|
|
673
|
+
new Error(`Final text delivery failed (${textReceipt.reason ?? 'unknown'})`),
|
|
674
|
+
{ uncertain: false },
|
|
675
|
+
);
|
|
676
|
+
} else if (finalDeliveryUnknown) {
|
|
677
|
+
textDeliveryError = channelDeliveryFailure(
|
|
678
|
+
new Error(`Final text delivery outcome is unknown (${textReceipt.reason ?? 'unknown'})`),
|
|
679
|
+
);
|
|
653
680
|
}
|
|
654
681
|
// A failed final text must not discard an already registered result file.
|
|
655
682
|
// Settle the independent attachment path before surfacing the text error.
|
|
656
683
|
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
|
657
|
-
if (textDeliveryError && !delivery.userVisible) {
|
|
684
|
+
if (textDeliveryError && (!delivery.userVisible || finalDeliveryUnknown)) {
|
|
658
685
|
textDeliveryError.deliveryReceipt = delivery.receipt;
|
|
659
686
|
throw textDeliveryError;
|
|
660
687
|
}
|
|
688
|
+
if (textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
689
|
+
setLastMessageFailure(this.#status, textDeliveryError);
|
|
690
|
+
}
|
|
661
691
|
if (delivery.userVisible) {
|
|
662
692
|
this.#status.messagesReplied += 1;
|
|
663
693
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
664
694
|
this.#status.lastError = null;
|
|
695
|
+
if (!textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
696
|
+
clearLastMessageFailure(this.#status);
|
|
697
|
+
}
|
|
665
698
|
}
|
|
666
699
|
return delivery.receipt;
|
|
667
700
|
} catch (error) {
|
|
@@ -688,10 +721,15 @@ export class TextHarnessBridge {
|
|
|
688
721
|
}
|
|
689
722
|
this.#status.lastError = error?.message ?? String(error);
|
|
690
723
|
const presentStreamFailure = async (text) => {
|
|
691
|
-
|
|
724
|
+
const method = typeof stream?.fail === 'function'
|
|
725
|
+
? 'fail'
|
|
726
|
+
: (typeof stream?.finish === 'function' ? 'finish' : null);
|
|
727
|
+
if (!method) return false;
|
|
692
728
|
try {
|
|
693
|
-
const result = await stream
|
|
694
|
-
return
|
|
729
|
+
const result = await stream[method](text);
|
|
730
|
+
return method === 'fail'
|
|
731
|
+
? Boolean(result) && result.deliveryOutcome !== 'failed'
|
|
732
|
+
: result !== false && result?.deliveryOutcome !== 'failed';
|
|
695
733
|
} catch (streamError) {
|
|
696
734
|
this.#logger.warn?.(
|
|
697
735
|
`[dsh-im:${this.#descriptor.key}] unable to finalize the failed stream:`,
|
|
@@ -700,58 +738,25 @@ export class TextHarnessBridge {
|
|
|
700
738
|
return false;
|
|
701
739
|
}
|
|
702
740
|
};
|
|
703
|
-
if (failedBatch?.retained) {
|
|
704
|
-
this.#logger.error?.(
|
|
705
|
-
`[dsh-im:${this.#descriptor.key}] failed to submit a batch input:`,
|
|
706
|
-
error,
|
|
707
|
-
);
|
|
708
|
-
if (await presentStreamFailure(failedBatch.message)) return;
|
|
709
|
-
stream?.cancel?.();
|
|
710
|
-
try {
|
|
711
|
-
await this.#bot.sendText(target, failedBatch.message);
|
|
712
|
-
} catch (sendError) {
|
|
713
|
-
this.#logger.error?.(
|
|
714
|
-
`[dsh-im:${this.#descriptor.key}] failed to send the batch retry notice:`,
|
|
715
|
-
sendError,
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
741
|
const imageErrorMessage = imagePromptUserMessage(error);
|
|
721
|
-
if (imageErrorMessage) {
|
|
722
|
-
if (await presentStreamFailure(imageErrorMessage)) return;
|
|
723
|
-
stream?.cancel?.();
|
|
724
|
-
try {
|
|
725
|
-
await this.#bot.sendText(target, imageErrorMessage);
|
|
726
|
-
} catch (sendError) {
|
|
727
|
-
this.#logger.error?.(
|
|
728
|
-
`[dsh-im:${this.#descriptor.key}] failed to send the image error reply:`,
|
|
729
|
-
sendError,
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
742
|
const fileErrorMessage = inboundFileUserMessage(error);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
|
|
749
|
-
if (await presentStreamFailure('消息处理失败,请稍后重试。')) {
|
|
743
|
+
const failure = setLastMessageFailure(this.#status, error, {
|
|
744
|
+
userMessage: fileErrorMessage ?? imageErrorMessage,
|
|
745
|
+
reason: imagePromptDiagnostic(error)?.reason,
|
|
746
|
+
});
|
|
747
|
+
const failureText = failedBatch?.retained
|
|
748
|
+
? `${messageFailureText(failure)}\n\n${failedBatch.message}`
|
|
749
|
+
: messageFailureText(failure);
|
|
750
|
+
this.#logger.error?.(
|
|
751
|
+
`[dsh-im:${this.#descriptor.key}] failed to process a message [${failure.referenceId}]:`,
|
|
752
|
+
error,
|
|
753
|
+
);
|
|
754
|
+
if (await presentStreamFailure(failureText)) {
|
|
750
755
|
return error.deliveryReceipt;
|
|
751
756
|
}
|
|
752
757
|
stream?.cancel?.();
|
|
753
758
|
try {
|
|
754
|
-
await this.#bot.sendText(target,
|
|
759
|
+
await this.#bot.sendText(target, failureText);
|
|
755
760
|
} catch (sendError) {
|
|
756
761
|
this.#logger.error?.(
|
|
757
762
|
`[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
|