@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.
Files changed (57) hide show
  1. package/README.en.md +9 -4
  2. package/README.md +9 -4
  3. package/lib/client.js +411 -156
  4. package/lib/index.js +218 -210
  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 +46 -28
  11. package/plugin-src/client/channels/feishu/styles.js +26 -1
  12. package/plugin-src/client/channels/qq/api.js +2 -0
  13. package/plugin-src/client/channels/qq/index.js +9 -1
  14. package/plugin-src/client/channels/shared/token-api.js +2 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +9 -1
  16. package/plugin-src/client/channels/wecom/api.js +2 -0
  17. package/plugin-src/client/channels/wecom/index.js +9 -1
  18. package/plugin-src/client/channels/weixin/api.js +2 -10
  19. package/plugin-src/client/channels/weixin/index.js +9 -5
  20. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  22. package/plugin-src/client/i18n.js +31 -25
  23. package/plugin-src/client/index.js +16 -2
  24. package/plugin-src/client/last-message-error.js +17 -0
  25. package/plugin-src/client/styles.js +5 -1
  26. package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
  27. package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
  28. package/src/channels/dingtalk/dingtalk-bridge.mjs +50 -16
  29. package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
  30. package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
  31. package/src/channels/feishu/bridge.mjs +148 -70
  32. package/src/channels/feishu/feishu-cards.mjs +10 -6
  33. package/src/channels/feishu/feishu-runtime.mjs +0 -1
  34. package/src/channels/feishu/message-utils.mjs +1 -1
  35. package/src/channels/feishu/multi-bot-controller.mjs +3 -17
  36. package/src/channels/feishu/repair-manager.mjs +7 -2
  37. package/src/channels/qq/qq-bridge.mjs +84 -28
  38. package/src/channels/qq/qq-controller.mjs +2 -0
  39. package/src/channels/shared/control-command.mjs +11 -1
  40. package/src/channels/shared/harness-client.mjs +40 -4
  41. package/src/channels/shared/i18n-en/feishu.mjs +27 -25
  42. package/src/channels/shared/i18n-en/shared-a.mjs +78 -0
  43. package/src/channels/shared/i18n-en/shared-b.mjs +1 -0
  44. package/src/channels/shared/i18n-en/telegram.mjs +1 -0
  45. package/src/channels/shared/message-failure.mjs +244 -0
  46. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  47. package/src/channels/shared/text-harness-bridge.mjs +66 -60
  48. package/src/channels/shared/token-bot-controller.mjs +2 -0
  49. package/src/channels/slack/slack-controller.mjs +2 -0
  50. package/src/channels/telegram/telegram-runtime.mjs +1 -0
  51. package/src/channels/wecom/wecom-bridge.mjs +50 -15
  52. package/src/channels/wecom/wecom-controller.mjs +2 -0
  53. package/src/channels/weixin/weixin-api.mjs +47 -0
  54. package/src/channels/weixin/weixin-bridge.mjs +262 -43
  55. package/src/channels/weixin/weixin-controller.mjs +2 -15
  56. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  57. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
@@ -8,6 +8,7 @@ const EN = Object.freeze({
8
8
  'IM机器人设置': 'IM bot settings',
9
9
  'IM 渠道': 'IM channels',
10
10
  '让 DeepSeek Harness 触手可及': 'DeepSeek Harness, always within reach',
11
+ '当前版本': 'Current version',
11
12
  'AI Office': 'AI Office',
12
13
  '(实验功能)': '(Experimental)',
13
14
  'AI Office 设置': 'AI Office settings',
@@ -113,6 +114,9 @@ const EN = Object.freeze({
113
114
  '消息通道': 'Message channel',
114
115
  '查看消息通道说明': 'View message channel details',
115
116
  '最近检查': 'Last checked',
117
+ '最近一条消息处理失败': 'Latest message failed',
118
+ '错误码': 'Code',
119
+ '参考号': 'Reference',
116
120
  '当前工作区': 'Current workspace',
117
121
  '选择目录': 'Choose folder',
118
122
  '选择机器人工作区目录': 'Select bot workspace folder',
@@ -231,7 +235,7 @@ const EN = Object.freeze({
231
235
  '保持本页打开,等待新机器人的长连接就绪': 'Keep this page open until the bot connection is ready',
232
236
  '在飞书中打开': 'Open in Feishu',
233
237
  '取消添加': 'Cancel',
234
- '使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
238
+ '使用飞书扫码补全权限': 'Scan with Feishu to complete permissions',
235
239
  '使用飞书确认群消息权限': 'Confirm group-message permission with Feishu',
236
240
  '扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the “Read all messages in associated group chat” scope. It does not create a new app. After confirmation, “Respond to all group messages” is enabled automatically; other bots are unaffected.',
237
241
  '核对现有应用,并确认“获取群组中所有消息”权限': 'Review the existing app and confirm the “Read all messages in associated group chat” permission',
@@ -242,18 +246,20 @@ const EN = Object.freeze({
242
246
  '权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
243
247
  '正在为现有飞书应用申请群消息权限二维码,请稍候。': 'Requesting a group-message permission QR code for the existing Feishu app…',
244
248
  '群消息权限没有开通完成': 'Group-message permission was not granted',
245
- '扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the card-button callback. It does not create a new app. This bot reconnects briefly after confirmation; other bots are not affected.',
246
- '核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
247
- '保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
248
- '取消修复': 'Cancel repair',
249
- '已确认,正在完成卡片按钮修复': 'Confirmed. Finishing card-button repair',
250
- '正在准备修复二维码': 'Preparing the repair QR code',
251
- '配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The update was submitted. Verifying the card callback and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
249
+ '扫码会更新现有飞书应用,最多增量补充卡片按钮回调、读取用户消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;不会创建新应用。确认页只显示当前缺少项,完成后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with up to three missing items: the card-button callback, im:message:readonly for reading images or files in user messages (shown by Feishu as “Read direct and group messages”), and im:resource for uploading images or files sent by the bot. It does not create a new app. The confirmation page shows only missing items; this bot reconnects briefly afterward, while other bots are unaffected.',
250
+ '核对现有应用名称,并确认只新增当前缺少的上述配置': 'Review the existing app name and confirm that only the missing items described above are added',
251
+ '保持本页打开,等待权限与回调补全完成': 'Keep this page open until permissions and the callback are complete',
252
+ '取消补全': 'Cancel setup',
253
+ '已确认,正在完成权限与回调配置': 'Confirmed. Completing permissions and callback setup',
254
+ '正在准备权限补全二维码': 'Preparing the permission-completion QR code',
255
+ '配置已提交,正在保存权限、验证卡片回调并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The update was submitted. Saving permissions, verifying the card callback, and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
252
256
  '正在为现有飞书应用申请一次性更新二维码,请稍候。': 'Requesting a one-time update QR code for the existing Feishu app…',
253
- '卡片按钮没有修复完成': 'Card-button repair did not finish',
254
- '修复卡片按钮': 'Repair card buttons',
257
+ '权限与回调没有补全完成': 'Permissions and callback setup did not finish',
258
+ '补全权限': 'Complete permissions',
259
+ '补全范围': 'Completion scope',
260
+ '最多增量添加卡片回调 card.action.trigger、读取消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;确认页只显示当前缺少项,不会创建新应用。': 'Adds up to the missing card callback card.action.trigger, im:message:readonly for reading images or files in messages (shown by Feishu as “Read direct and group messages”), and im:resource for uploading images or files sent by the bot. The confirmation page shows only missing items; no new app is created.',
255
261
  '等待扫码…': 'Waiting for scan…',
256
- '飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
262
+ '飞书服务返回了不匹配的权限补全二维码': 'Feishu returned a permission-completion QR code for a different bot',
257
263
  '飞书服务返回了不匹配的群消息权限二维码': 'Feishu returned a group-message permission QR code for a different bot',
258
264
  '飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
259
265
  '飞书服务返回的应用更新信息缺少 botId': 'Feishu app-update status is missing the bot ID',
@@ -261,16 +267,16 @@ const EN = Object.freeze({
261
267
  '此机器人': 'this bot',
262
268
  '用于为${botName}开通群消息权限的一次性授权二维码': 'One-time QR code for granting group-message permission to ${botName}',
263
269
  '正在为「${botName}」开通群消息权限': 'Granting group-message permission to “${botName}”',
264
- '${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
270
+ '${botName ?? "机器人"}的权限补全二维码已生成,请使用飞书扫码。': 'Permission-completion QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
265
271
  '${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。': 'Group-message permission QR code generated for ${botName ?? "bot"}. Confirm it with Feishu.',
266
272
  '${targetBotName}已开通群消息权限,并启用“响应所有群消息”。': '${targetBotName} now has group-message permission and “Respond to all group messages” is enabled.',
267
273
  '${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。': '${targetBot.bot.name} now has group-message permission and “Respond to all group messages” is enabled.',
268
274
  '${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
269
- '已取消卡片按钮修复。': 'Card-button repair was cancelled.',
275
+ '已取消补全权限与回调。': 'Completing permissions and the callback was cancelled.',
270
276
  '已取消群消息权限授权。': 'Group-message permission authorization was cancelled.',
271
- '卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
277
+ '权限与回调已更新,但暂时无法确认机器人连接状态': 'Permissions and the callback were updated, but the bot connection could not be confirmed yet',
272
278
  '群消息权限已更新,但暂时无法确认机器人连接状态': 'The group-message permission was updated, but the bot connection could not be confirmed yet',
273
- '飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
279
+ '飞书权限与回调补全失败': 'Could not complete Feishu permissions and callback setup',
274
280
  '飞书群消息权限开通失败': 'Could not grant the Feishu group-message permission',
275
281
  '请先完成当前飞书授权操作,再开通群消息权限。': 'Finish the current Feishu authorization before granting group-message permission.',
276
282
  '已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
@@ -598,22 +604,22 @@ function translateDynamic(text) {
598
604
  if (match) return `Remove ${match[1]} from DeepSeek Harness`;
599
605
  match = /^(.+)的飞书授权流程$/.exec(text);
600
606
  if (match) return `Feishu authorization flow for ${match[1]}`;
601
- match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
602
- if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
607
+ match = /^用于为(.+)补全权限与回调的一次性授权二维码$/.exec(text);
608
+ if (match) return `One-time QR code for completing permissions and the callback for ${match[1]}`;
603
609
  match = /^用于为(.+)开通群消息权限的一次性授权二维码$/.exec(text);
604
610
  if (match) return `One-time QR code for granting group-message permission to ${match[1]}`;
605
- match = /^正在修复「(.+)」$/.exec(text);
606
- if (match) return `Repairing “${match[1]}”`;
611
+ match = /^正在为「(.+)」补全权限与回调$/.exec(text);
612
+ if (match) return `Completing permissions and the callback for “${match[1]}”`;
607
613
  match = /^正在为「(.+)」开通群消息权限$/.exec(text);
608
614
  if (match) return `Granting group-message permission to “${match[1]}”`;
609
- match = /^修复(.+)的卡片按钮$/.exec(text);
610
- if (match) return `Repair card buttons for ${match[1]}`;
611
- match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
612
- if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
615
+ match = /^为(.+)补全权限与回调$/.exec(text);
616
+ if (match) return `Complete permissions and the callback for ${match[1]}`;
617
+ match = /^(.+)的权限补全二维码已生成,请使用飞书扫码。$/.exec(text);
618
+ if (match) return `Permission-completion QR code generated for ${match[1]}. Scan it with Feishu.`;
613
619
  match = /^(.+)的群消息权限二维码已生成,请使用飞书确认。$/.exec(text);
614
620
  if (match) return `Group-message permission QR code generated for ${match[1]}. Confirm it with Feishu.`;
615
- match = /^(.+)的卡片按钮已修复。$/.exec(text);
616
- if (match) return `Card buttons repaired for ${match[1]}.`;
621
+ match = /^(.+)的权限与回调已补全。$/.exec(text);
622
+ if (match) return `Permissions and the callback completed for ${match[1]}.`;
617
623
  match = /^(.+)已开通群消息权限,并启用“响应所有群消息”。$/.exec(text);
618
624
  if (match) return `${match[1]} now has group-message permission and “Respond to all group messages” is enabled.`;
619
625
  match = /^(检查连接|重试连接)(.+)$/.exec(text);
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import manifest from '../../package.json' with { type: 'json' };
2
3
 
3
4
  import {
4
5
  DingtalkLogoGlyph,
@@ -51,6 +52,7 @@ import { WorkspaceDirectoryPickerContext } from './workspace-editor.js';
51
52
 
52
53
  export const name = 'im-settings';
53
54
  export const inject = ['slots', 'connection', 'locale', 'workspaces'];
55
+ export const IM_PLUGIN_VERSION = manifest.version;
54
56
 
55
57
  const CHANNELS = Object.freeze([
56
58
  { id: 'weixin', label: '微信' },
@@ -159,6 +161,7 @@ export function IMSettingsTab({
159
161
  }) {
160
162
  const [selected, setSelected] = React.useState('weixin');
161
163
  const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
164
+ const versionTooltipId = React.useId();
162
165
  const githubTooltipId = React.useId();
163
166
  const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
164
167
  const reportLoopbackRecovery = React.useCallback((recovery) => {
@@ -195,9 +198,20 @@ export function IMSettingsTab({
195
198
  return h(WorkspaceDirectoryPickerContext.Provider, { value: workspaceDirectoryPicker },
196
199
  h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
197
200
  h('header', { className: 'dim-title' },
198
- h('div', { className: 'dim-brand' },
201
+ h('div', {
202
+ className: 'dim-brand',
203
+ tabIndex: 0,
204
+ 'aria-describedby': versionTooltipId,
205
+ },
199
206
  h('strong', { className: 'dim-brandName' }, 'DSH-IM'),
200
- h('p', null, '让 DeepSeek Harness 触手可及')),
207
+ h('p', null, '让 DeepSeek Harness 触手可及'),
208
+ h('span', {
209
+ id: versionTooltipId,
210
+ className: 'dim-versionTooltip',
211
+ role: 'tooltip',
212
+ },
213
+ h('span', null, '当前版本'),
214
+ h('strong', null, `v${IM_PLUGIN_VERSION}`))),
201
215
  h('span', { className: 'dim-githubAction' },
202
216
  h('a', {
203
217
  className: 'dim-githubLink',
@@ -0,0 +1,17 @@
1
+ function text(value, maxLength) {
2
+ if (typeof value !== 'string') return null;
3
+ const trimmed = value.trim();
4
+ return trimmed ? trimmed.slice(0, maxLength) : null;
5
+ }
6
+
7
+ export function normalizeLastMessageError(value) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
9
+ const code = text(value.code, 64);
10
+ const reason = text(value.reason, 64);
11
+ const message = text(value.message, 500);
12
+ const referenceId = text(value.referenceId, 40);
13
+ const at = Number.isFinite(value.at) ? value.at : null;
14
+ return code && reason && message && referenceId && at !== null
15
+ ? { code, reason, message, referenceId, at }
16
+ : null;
17
+ }
@@ -12,9 +12,13 @@ const CSS = String.raw`
12
12
  }
13
13
  .dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
14
14
  .dim-title { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; }
15
- .dim-brand { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
15
+ .dim-brand { position: relative; min-width: 0; width: max-content; max-width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; margin: -2px -6px; padding: 2px 6px; border-radius: 8px; cursor: help; }
16
+ .dim-brand:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
16
17
  .dim-brandName { color: var(--dsw-alias-label-primary, #1f2329); font-size: 20px; line-height: 24px; font-weight: 800; letter-spacing: .04em; }
17
18
  .dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 500; white-space: nowrap; }
19
+ .dim-versionTooltip { position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 20; width: max-content; max-width: min(220px, 80vw); display: inline-flex; align-items: center; gap: 6px; padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
20
+ .dim-versionTooltip strong { color: var(--dsw-alias-label-primary, #1f2329); font: 600 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; }
21
+ .dim-brand:hover .dim-versionTooltip, .dim-brand:focus .dim-versionTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
18
22
  .dim-githubAction { position: relative; display: inline-flex; flex: none; }
19
23
  .dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
20
24
  .dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
@@ -4,6 +4,7 @@ import {
4
4
  normalizeAgentPresetId,
5
5
  } from '../../../../src/channels/shared/agent-preset.mjs';
6
6
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
7
+ import { publicMessageFailure } from '../../../../src/channels/shared/message-failure.mjs';
7
8
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
8
9
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
9
10
  import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -281,6 +282,8 @@ function publicBotEntry(entry) {
281
282
  health: publicHealth(source, connected),
282
283
  };
283
284
  if (typeof source.workspace === 'string' && source.workspace) result.workspace = source.workspace;
285
+ const lastMessageError = publicMessageFailure(source.lastMessageError);
286
+ if (lastMessageError) result.lastMessageError = lastMessageError;
284
287
  const error = publicError(source.error);
285
288
  if (error) result.error = error;
286
289
  return result;
@@ -830,7 +830,7 @@ export function createDingtalkApi({
830
830
  clientId: appKey,
831
831
  clientSecret: appSecret,
832
832
  cardInstanceId,
833
- text: t('消息处理失败,请稍后重试。'),
833
+ text: t('卡片已结束,请查看后续消息。'),
834
834
  signal: cleanupSignal,
835
835
  }).catch(() => undefined);
836
836
  }
@@ -32,6 +32,7 @@ import {
32
32
  } from '../shared/batch-input.mjs';
33
33
  import {
34
34
  hasInboundImages,
35
+ imagePromptDiagnostic,
35
36
  imagePromptUserMessage,
36
37
  promptContentForMessage,
37
38
  } from '../shared/image-prompt.mjs';
@@ -46,10 +47,15 @@ import {
46
47
  createDeliveryReceipt,
47
48
  providerMessageIdsFor,
48
49
  } from '../shared/semantic/delivery.mjs';
50
+ import {
51
+ channelDeliveryFailure,
52
+ clearLastMessageFailure,
53
+ messageFailureText,
54
+ setLastMessageFailure,
55
+ } from '../shared/message-failure.mjs';
49
56
  import { t } from '../shared/i18n.mjs';
50
57
 
51
58
  const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
52
- const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
53
59
  const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
54
60
 
55
61
  const HELP_TEXT_LINES = [
@@ -77,6 +83,7 @@ const HELP_TEXT_LINES = [
77
83
  '/send 提交当前批次',
78
84
  '/cancel 取消当前批次',
79
85
  '/status 检查连接状态',
86
+ '/version 查看插件版本',
80
87
  '/help 显示本帮助',
81
88
  ];
82
89
 
@@ -325,6 +332,7 @@ export function createDingtalkBridgeStatus({ pendingSenders = [] } = {}) {
325
332
  lastReplyAt: null,
326
333
  lastRejectedAt: null,
327
334
  lastError: null,
335
+ lastMessageError: null,
328
336
  pendingSenders: structuredClone(pendingSenders),
329
337
  stats: {
330
338
  messagesReceived: 0,
@@ -483,9 +491,13 @@ export class DingtalkHarnessBridge {
483
491
  commandRunner,
484
492
  ).catch((error) => {
485
493
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
486
- this.#status.lastError = t('钉钉命令处理失败。');
487
- this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
488
- return this.#send(sessionWebhook, t(CARD_ERROR_TEXT)).catch(() => undefined);
494
+ this.#status.lastError = error?.message ?? String(error);
495
+ const failure = setLastMessageFailure(this.#status, error);
496
+ this.#logger.error?.(
497
+ `[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
498
+ safeErrorDiagnostic(error),
499
+ );
500
+ return this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
489
501
  }).finally(() => {
490
502
  this.#acceptedMessageIds.delete(messageId);
491
503
  this.#commandTasks.delete(task);
@@ -658,9 +670,13 @@ export class DingtalkHarnessBridge {
658
670
  this.#status.lastError = null;
659
671
  }).catch(async (error) => {
660
672
  if (this.#signal?.aborted) return;
661
- this.#status.lastError = t('钉钉命令处理失败。');
662
- this.#logger.error?.('[dsh-dingtalk] failed to process a batch input message', safeErrorDiagnostic(error));
663
- await this.#send(sessionWebhook, t(CARD_ERROR_TEXT)).catch(() => undefined);
673
+ this.#status.lastError = error?.message ?? String(error);
674
+ const failure = setLastMessageFailure(this.#status, error);
675
+ this.#logger.error?.(
676
+ `[dsh-dingtalk] failed to process a batch input message [${failure.referenceId}]`,
677
+ safeErrorDiagnostic(error),
678
+ );
679
+ await this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
664
680
  }).finally(() => {
665
681
  this.#acceptedMessageIds.delete(messageId);
666
682
  this.#commandTasks.delete(task);
@@ -818,7 +834,7 @@ export class DingtalkHarnessBridge {
818
834
  });
819
835
  }
820
836
  } catch (error) {
821
- textDeliveryError = error;
837
+ textDeliveryError = channelDeliveryFailure(error);
822
838
  }
823
839
  const delivery = await this.#deliverArtifacts(
824
840
  fileTarget(message, sender, this.#clientId),
@@ -828,9 +844,15 @@ export class DingtalkHarnessBridge {
828
844
  textReceipt,
829
845
  );
830
846
  if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
847
+ if (textDeliveryError && delivery.artifactSendErrors === 0) {
848
+ setLastMessageFailure(this.#status, textDeliveryError);
849
+ }
831
850
  increment(this.#status, 'messagesReplied');
832
851
  this.#status.lastReplyAt = new Date().toISOString();
833
852
  this.#status.lastError = null;
853
+ if (!textDeliveryError && delivery.artifactSendErrors === 0) {
854
+ clearLastMessageFailure(this.#status);
855
+ }
834
856
  return delivery.receipt;
835
857
  } catch (error) {
836
858
  let batchFailureMessage = null;
@@ -847,15 +869,19 @@ export class DingtalkHarnessBridge {
847
869
  return;
848
870
  }
849
871
  if (this.#signal?.aborted) return;
850
- this.#status.lastError = t('钉钉消息处理失败。');
872
+ this.#status.lastError = error?.message ?? String(error);
873
+ const userMessage = inboundFileUserMessage(error)
874
+ ?? dingtalkImageErrorUserMessage(error);
875
+ const failure = setLastMessageFailure(this.#status, error, {
876
+ userMessage,
877
+ reason: imagePromptDiagnostic(error)?.reason,
878
+ });
851
879
  this.#logger.error?.(
852
- '[dsh-dingtalk] failed to process an inbound message',
880
+ `[dsh-dingtalk] failed to process an inbound message [${failure.referenceId}]`,
853
881
  safeErrorDiagnostic(error),
854
882
  );
855
883
  try {
856
- const errorText = inboundFileUserMessage(error)
857
- ?? dingtalkImageErrorUserMessage(error)
858
- ?? t(CARD_ERROR_TEXT);
884
+ const errorText = messageFailureText(failure);
859
885
  const visibleError = batchFailureMessage
860
886
  ? `${errorText}\n\n${batchFailureMessage}`
861
887
  : errorText;
@@ -1200,9 +1226,13 @@ export class DingtalkHarnessBridge {
1200
1226
  sendFile: typeof this.#api.sendFile === 'function'
1201
1227
  ? (file) => sendArtifact('sendFile', file)
1202
1228
  : undefined,
1203
- sendFailureNotice: (artifact, error) => this.#send(
1229
+ onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
1230
+ userMessage: artifactFailureText(artifact?.fileName, error),
1231
+ reason: error?.code,
1232
+ }),
1233
+ sendFailureNotice: (_artifact, _error, failure) => this.#send(
1204
1234
  sessionWebhook,
1205
- artifactFailureText(artifact?.fileName, error),
1235
+ messageFailureText(failure),
1206
1236
  ),
1207
1237
  logger: this.#logger,
1208
1238
  });
@@ -1210,7 +1240,11 @@ export class DingtalkHarnessBridge {
1210
1240
  + delivery.artifactsSent;
1211
1241
  this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
1212
1242
  + delivery.artifactSendErrors;
1213
- return { receipt: delivery.receipt, userVisible: delivery.userVisible };
1243
+ return {
1244
+ receipt: delivery.receipt,
1245
+ userVisible: delivery.userVisible,
1246
+ artifactSendErrors: delivery.artifactSendErrors,
1247
+ };
1214
1248
  }
1215
1249
  }
1216
1250
 
@@ -1,7 +1,7 @@
1
1
  import { t } from '../shared/i18n.mjs';
2
2
 
3
3
  const DEFAULT_UPDATE_INTERVAL_MS = 500;
4
- const FAILURE_TEXT = '消息处理失败,请稍后重试。';
4
+ const CLOSED_TEXT = '卡片已结束,请查看后续消息。';
5
5
 
6
6
  function requiredText(value, name) {
7
7
  if (typeof value !== 'string') throw new TypeError(`${name} must be a string`);
@@ -102,7 +102,7 @@ export function createDingTalkCardStream({
102
102
  if (!cleanupPromise) {
103
103
  cleanupPromise = api.failAiCard({
104
104
  ...cardRequest,
105
- text: t(FAILURE_TEXT),
105
+ text: t(CLOSED_TEXT),
106
106
  signal: AbortSignal.timeout(5_000),
107
107
  }).then(
108
108
  () => true,
@@ -11,6 +11,7 @@ import {
11
11
  connectionTestTargetUnavailable,
12
12
  } from '../shared/connection-test.mjs';
13
13
  import { t } from '../shared/i18n.mjs';
14
+ import { publicMessageFailure } from '../shared/message-failure.mjs';
14
15
 
15
16
  const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
16
17
  const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
@@ -516,6 +517,7 @@ export class DingtalkController {
516
517
  messagesReceived: Number(currentStatus.messagesReceived) || 0,
517
518
  messagesReplied: Number(currentStatus.messagesReplied) || 0,
518
519
  },
520
+ lastMessageError: publicMessageFailure(currentStatus.lastMessageError),
519
521
  senders: {
520
522
  pending,
521
523
  approved: config.approvedSenders.map(publicApprovedSender),