@xmanrui/dsh-im 2.4.0 → 2.6.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.
Files changed (60) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/lib/client.js +322 -109
  4. package/lib/index.js +218 -211
  5. package/package.json +1 -1
  6. package/plugin-src/client/channel-card-meta.js +36 -1
  7. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +9 -1
  9. package/plugin-src/client/channels/feishu/api.js +3 -0
  10. package/plugin-src/client/channels/feishu/index.js +9 -1
  11. package/plugin-src/client/channels/qq/api.js +2 -0
  12. package/plugin-src/client/channels/qq/index.js +9 -1
  13. package/plugin-src/client/channels/shared/token-api.js +2 -0
  14. package/plugin-src/client/channels/shared/token-channel.js +9 -1
  15. package/plugin-src/client/channels/wecom/api.js +2 -0
  16. package/plugin-src/client/channels/wecom/index.js +9 -1
  17. package/plugin-src/client/channels/weixin/api.js +2 -10
  18. package/plugin-src/client/channels/weixin/index.js +9 -5
  19. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  20. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  21. package/plugin-src/client/i18n.js +4 -0
  22. package/plugin-src/client/index.js +16 -2
  23. package/plugin-src/client/last-message-error.js +17 -0
  24. package/plugin-src/client/styles.js +5 -1
  25. package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
  26. package/src/channels/dingtalk/dingtalk-api.mjs +102 -6
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +247 -38
  28. package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
  29. package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
  30. package/src/channels/discord/discord-api.mjs +35 -0
  31. package/src/channels/discord/discord-bridge.mjs +1 -0
  32. package/src/channels/discord/discord-runtime.mjs +26 -0
  33. package/src/channels/feishu/bridge.mjs +115 -69
  34. package/src/channels/feishu/multi-bot-controller.mjs +2 -0
  35. package/src/channels/qq/qq-bridge.mjs +83 -28
  36. package/src/channels/qq/qq-controller.mjs +2 -0
  37. package/src/channels/shared/harness-client.mjs +40 -4
  38. package/src/channels/shared/i18n-en/shared-a.mjs +77 -0
  39. package/src/channels/shared/message-failure.mjs +244 -0
  40. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  41. package/src/channels/shared/status-reaction.mjs +107 -0
  42. package/src/channels/shared/text-harness-bridge.mjs +123 -66
  43. package/src/channels/shared/token-bot-controller.mjs +2 -0
  44. package/src/channels/slack/manifest.mjs +1 -0
  45. package/src/channels/slack/slack-api.mjs +28 -0
  46. package/src/channels/slack/slack-bridge.mjs +5 -0
  47. package/src/channels/slack/slack-controller.mjs +2 -0
  48. package/src/channels/slack/slack-runtime.mjs +24 -0
  49. package/src/channels/telegram/telegram-api.mjs +9 -0
  50. package/src/channels/telegram/telegram-bridge.mjs +1 -0
  51. package/src/channels/telegram/telegram-runtime.mjs +20 -0
  52. package/src/channels/wecom/wecom-bridge.mjs +49 -15
  53. package/src/channels/wecom/wecom-controller.mjs +2 -0
  54. package/src/channels/weixin/weixin-api.mjs +47 -0
  55. package/src/channels/weixin/weixin-bridge.mjs +261 -43
  56. package/src/channels/weixin/weixin-controller.mjs +2 -15
  57. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  58. package/src/channels/whatsapp/whatsapp-bridge.mjs +1 -0
  59. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
  60. package/src/channels/whatsapp/whatsapp-runtime.mjs +36 -0
@@ -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;
@@ -0,0 +1,107 @@
1
+ const DEFAULT_TIMEOUT_MS = 2_000;
2
+
3
+ const NOOP_REACTION = Object.freeze({
4
+ success() {},
5
+ error() {},
6
+ clear() {},
7
+ settled: () => Promise.resolve(),
8
+ });
9
+
10
+ function increment(status, key) {
11
+ if (!status || typeof status !== 'object') return;
12
+ status[key] = (status[key] ?? 0) + 1;
13
+ }
14
+
15
+ async function runWithTimeout(operation, timeoutMs) {
16
+ const signal = AbortSignal.timeout(timeoutMs);
17
+ let onAbort;
18
+ const aborted = new Promise((_, reject) => {
19
+ onAbort = () => reject(signal.reason ?? new DOMException('Timed out', 'TimeoutError'));
20
+ signal.addEventListener('abort', onAbort, { once: true });
21
+ });
22
+ try {
23
+ return await Promise.race([operation(signal), aborted]);
24
+ } finally {
25
+ signal.removeEventListener('abort', onAbort);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Starts a best-effort status reaction lifecycle which is deliberately not
31
+ * part of the caller's message queue. Calls are serialized only for this one
32
+ * source message; every provider operation is bounded and absorbs failures.
33
+ */
34
+ export function beginStatusReaction({
35
+ adapter,
36
+ target,
37
+ reactions,
38
+ status,
39
+ logger = console,
40
+ label = 'channel',
41
+ timeoutMs = DEFAULT_TIMEOUT_MS,
42
+ } = {}) {
43
+ if (!target
44
+ || typeof adapter?.addReaction !== 'function'
45
+ || typeof adapter?.removeReaction !== 'function'
46
+ || typeof reactions?.processing !== 'string'
47
+ || !reactions.processing
48
+ || !Number.isSafeInteger(timeoutMs)
49
+ || timeoutMs <= 0) return NOOP_REACTION;
50
+
51
+ let currentReaction = null;
52
+ let terminal = false;
53
+
54
+ const safely = async (kind, operation) => {
55
+ try {
56
+ const value = await runWithTimeout(operation, timeoutMs);
57
+ increment(status, kind === 'add' ? 'reactionsAdded' : 'reactionsRemoved');
58
+ return { ok: true, value };
59
+ } catch (cause) {
60
+ increment(status, 'reactionErrors');
61
+ logger.warn?.(
62
+ `[dsh-im:${label}] status reaction ${kind} failed:`,
63
+ cause?.message ?? cause?.name ?? String(cause),
64
+ );
65
+ return { ok: false, value: null };
66
+ }
67
+ };
68
+
69
+ const transition = async (emoji) => {
70
+ if (currentReaction !== null) {
71
+ const previous = currentReaction;
72
+ currentReaction = null;
73
+ await safely('remove', (signal) => adapter.removeReaction(
74
+ target,
75
+ previous,
76
+ { signal },
77
+ ));
78
+ }
79
+ if (typeof emoji !== 'string' || !emoji) return;
80
+ const added = await safely('add', (signal) => adapter.addReaction(
81
+ target,
82
+ emoji,
83
+ { signal },
84
+ ));
85
+ if (added.ok && added.value !== undefined && added.value !== null) {
86
+ currentReaction = added.value;
87
+ }
88
+ };
89
+
90
+ // Calling an async function starts the provider request synchronously up to
91
+ // its first await, while the returned tail remains completely detached from
92
+ // normal message processing.
93
+ let tail = transition(reactions.processing);
94
+ const finish = (emoji) => {
95
+ if (terminal) return;
96
+ terminal = true;
97
+ tail = tail.then(() => transition(emoji), () => transition(emoji));
98
+ void tail.catch(() => undefined);
99
+ };
100
+
101
+ return Object.freeze({
102
+ success: () => finish(reactions.success),
103
+ error: () => finish(reactions.error),
104
+ clear: () => finish(null),
105
+ settled: () => tail,
106
+ });
107
+ }