@xmanrui/dsh-im 2.3.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 +9 -4
- package/README.md +9 -4
- package/lib/client.js +411 -156
- package/lib/index.js +218 -210
- 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 +46 -28
- package/plugin-src/client/channels/feishu/styles.js +26 -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 +31 -25
- 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 +50 -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 +148 -70
- package/src/channels/feishu/feishu-cards.mjs +10 -6
- package/src/channels/feishu/feishu-runtime.mjs +0 -1
- package/src/channels/feishu/message-utils.mjs +1 -1
- package/src/channels/feishu/multi-bot-controller.mjs +3 -17
- package/src/channels/feishu/repair-manager.mjs +7 -2
- package/src/channels/qq/qq-bridge.mjs +84 -28
- package/src/channels/qq/qq-controller.mjs +2 -0
- package/src/channels/shared/control-command.mjs +11 -1
- package/src/channels/shared/harness-client.mjs +40 -4
- package/src/channels/shared/i18n-en/feishu.mjs +27 -25
- package/src/channels/shared/i18n-en/shared-a.mjs +78 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +1 -0
- package/src/channels/shared/i18n-en/telegram.mjs +1 -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 +66 -60
- package/src/channels/shared/token-bot-controller.mjs +2 -0
- package/src/channels/slack/slack-controller.mjs +2 -0
- package/src/channels/telegram/telegram-runtime.mjs +1 -0
- package/src/channels/wecom/wecom-bridge.mjs +50 -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 +262 -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
|
@@ -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
|
'目前支持文字、图片和文件消息。':
|
|
@@ -69,6 +146,7 @@ export default {
|
|
|
69
146
|
'/stop 停止当前任务': '/stop Stop the current task',
|
|
70
147
|
'/steer 补充指令 纠偏当前任务': '/steer <additional instruction> Steer the current task',
|
|
71
148
|
'/status 检查连接状态': '/status Check the connection status',
|
|
149
|
+
'/version 查看插件版本': '/version Show the plugin version',
|
|
72
150
|
'/help 显示本帮助': '/help Show this help',
|
|
73
151
|
'{label}机器人与 DeepSeek Harness 连接正常。':
|
|
74
152
|
'The {label} bot is connected to DeepSeek Harness and working normally.',
|
|
@@ -242,6 +242,7 @@ export default {
|
|
|
242
242
|
|
|
243
243
|
// control-command.mjs
|
|
244
244
|
'用法:/stop(不带参数)': 'Usage: /stop (no arguments)',
|
|
245
|
+
'用法:/version(不带参数)': 'Usage: /version (no arguments)',
|
|
245
246
|
'用法:/steer <补充指令>': 'Usage: /steer <additional instruction>',
|
|
246
247
|
'控制命令仅支持纯文字,请移除图片后重试。':
|
|
247
248
|
'Control commands support text only; please remove images and try again.',
|
|
@@ -13,6 +13,7 @@ export default {
|
|
|
13
13
|
'停止当前任务': 'Stop the current task',
|
|
14
14
|
'纠偏当前任务': 'Steer the current task',
|
|
15
15
|
'检查连接状态': 'Check connection status',
|
|
16
|
+
'查看插件版本': 'Show plugin version',
|
|
16
17
|
'显示帮助': 'Show help',
|
|
17
18
|
'该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。':
|
|
18
19
|
'This Telegram bot already has a Webhook configured. Remove the Webhook from the original service and try again.',
|
|
@@ -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, {
|
|
@@ -494,6 +515,7 @@ export class TextHarnessBridge {
|
|
|
494
515
|
t('/send 提交当前批次'),
|
|
495
516
|
t('/cancel 取消当前批次'),
|
|
496
517
|
t('/status 检查连接状态'),
|
|
518
|
+
t('/version 查看插件版本'),
|
|
497
519
|
t('/help 显示本帮助'),
|
|
498
520
|
].join('\n'));
|
|
499
521
|
return;
|
|
@@ -642,25 +664,37 @@ export class TextHarnessBridge {
|
|
|
642
664
|
reason: result?.reason,
|
|
643
665
|
});
|
|
644
666
|
} catch (error) {
|
|
645
|
-
textDeliveryError = error;
|
|
667
|
+
textDeliveryError = channelDeliveryFailure(error);
|
|
646
668
|
}
|
|
647
669
|
}
|
|
670
|
+
const finalDeliveryUnknown = textReceipt?.deliveryOutcome === 'unknown';
|
|
648
671
|
if (textReceipt?.deliveryOutcome === 'failed') {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
+
);
|
|
652
680
|
}
|
|
653
681
|
// A failed final text must not discard an already registered result file.
|
|
654
682
|
// Settle the independent attachment path before surfacing the text error.
|
|
655
683
|
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
|
656
|
-
if (textDeliveryError && !delivery.userVisible) {
|
|
684
|
+
if (textDeliveryError && (!delivery.userVisible || finalDeliveryUnknown)) {
|
|
657
685
|
textDeliveryError.deliveryReceipt = delivery.receipt;
|
|
658
686
|
throw textDeliveryError;
|
|
659
687
|
}
|
|
688
|
+
if (textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
689
|
+
setLastMessageFailure(this.#status, textDeliveryError);
|
|
690
|
+
}
|
|
660
691
|
if (delivery.userVisible) {
|
|
661
692
|
this.#status.messagesReplied += 1;
|
|
662
693
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
663
694
|
this.#status.lastError = null;
|
|
695
|
+
if (!textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
696
|
+
clearLastMessageFailure(this.#status);
|
|
697
|
+
}
|
|
664
698
|
}
|
|
665
699
|
return delivery.receipt;
|
|
666
700
|
} catch (error) {
|
|
@@ -687,10 +721,15 @@ export class TextHarnessBridge {
|
|
|
687
721
|
}
|
|
688
722
|
this.#status.lastError = error?.message ?? String(error);
|
|
689
723
|
const presentStreamFailure = async (text) => {
|
|
690
|
-
|
|
724
|
+
const method = typeof stream?.fail === 'function'
|
|
725
|
+
? 'fail'
|
|
726
|
+
: (typeof stream?.finish === 'function' ? 'finish' : null);
|
|
727
|
+
if (!method) return false;
|
|
691
728
|
try {
|
|
692
|
-
const result = await stream
|
|
693
|
-
return
|
|
729
|
+
const result = await stream[method](text);
|
|
730
|
+
return method === 'fail'
|
|
731
|
+
? Boolean(result) && result.deliveryOutcome !== 'failed'
|
|
732
|
+
: result !== false && result?.deliveryOutcome !== 'failed';
|
|
694
733
|
} catch (streamError) {
|
|
695
734
|
this.#logger.warn?.(
|
|
696
735
|
`[dsh-im:${this.#descriptor.key}] unable to finalize the failed stream:`,
|
|
@@ -699,58 +738,25 @@ export class TextHarnessBridge {
|
|
|
699
738
|
return false;
|
|
700
739
|
}
|
|
701
740
|
};
|
|
702
|
-
if (failedBatch?.retained) {
|
|
703
|
-
this.#logger.error?.(
|
|
704
|
-
`[dsh-im:${this.#descriptor.key}] failed to submit a batch input:`,
|
|
705
|
-
error,
|
|
706
|
-
);
|
|
707
|
-
if (await presentStreamFailure(failedBatch.message)) return;
|
|
708
|
-
stream?.cancel?.();
|
|
709
|
-
try {
|
|
710
|
-
await this.#bot.sendText(target, failedBatch.message);
|
|
711
|
-
} catch (sendError) {
|
|
712
|
-
this.#logger.error?.(
|
|
713
|
-
`[dsh-im:${this.#descriptor.key}] failed to send the batch retry notice:`,
|
|
714
|
-
sendError,
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
741
|
const imageErrorMessage = imagePromptUserMessage(error);
|
|
720
|
-
if (imageErrorMessage) {
|
|
721
|
-
if (await presentStreamFailure(imageErrorMessage)) return;
|
|
722
|
-
stream?.cancel?.();
|
|
723
|
-
try {
|
|
724
|
-
await this.#bot.sendText(target, imageErrorMessage);
|
|
725
|
-
} catch (sendError) {
|
|
726
|
-
this.#logger.error?.(
|
|
727
|
-
`[dsh-im:${this.#descriptor.key}] failed to send the image error reply:`,
|
|
728
|
-
sendError,
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
742
|
const fileErrorMessage = inboundFileUserMessage(error);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
|
|
748
|
-
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)) {
|
|
749
755
|
return error.deliveryReceipt;
|
|
750
756
|
}
|
|
751
757
|
stream?.cancel?.();
|
|
752
758
|
try {
|
|
753
|
-
await this.#bot.sendText(target,
|
|
759
|
+
await this.#bot.sendText(target, failureText);
|
|
754
760
|
} catch (sendError) {
|
|
755
761
|
this.#logger.error?.(
|
|
756
762
|
`[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connectionTestMessage } from './connection-test.mjs';
|
|
2
2
|
import { t } from './i18n.mjs';
|
|
3
|
+
import { publicMessageFailure } from './message-failure.mjs';
|
|
3
4
|
|
|
4
5
|
function cleanString(value) {
|
|
5
6
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -291,6 +292,7 @@ export class TokenBotController {
|
|
|
291
292
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
292
293
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
293
294
|
},
|
|
295
|
+
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
294
296
|
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
295
297
|
};
|
|
296
298
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
2
|
+
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
2
3
|
import { t } from '../shared/i18n.mjs';
|
|
3
4
|
import { deriveSlackBotIdentity, maskSlackBotId } from './config-store.mjs';
|
|
4
5
|
import { inspectSlackCredentials } from './slack-api.mjs';
|
|
@@ -254,6 +255,7 @@ export class SlackController {
|
|
|
254
255
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
255
256
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
256
257
|
},
|
|
258
|
+
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
257
259
|
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
258
260
|
};
|
|
259
261
|
});
|
|
@@ -32,6 +32,7 @@ export const TELEGRAM_COMMAND_MENU = Object.freeze([
|
|
|
32
32
|
{ command: 'send', description: '提交当前批次' },
|
|
33
33
|
{ command: 'cancel', description: '取消当前批次' },
|
|
34
34
|
{ command: 'status', description: '检查连接状态' },
|
|
35
|
+
{ command: 'version', description: '查看插件版本' },
|
|
35
36
|
{ command: 'help', description: '显示帮助' },
|
|
36
37
|
]);
|
|
37
38
|
|