@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.
Files changed (47) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/lib/client.js +321 -109
  4. package/lib/index.js +214 -207
  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 +1 -1
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +49 -16
  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/feishu/bridge.mjs +99 -43
  31. package/src/channels/feishu/multi-bot-controller.mjs +2 -0
  32. package/src/channels/qq/qq-bridge.mjs +83 -28
  33. package/src/channels/qq/qq-controller.mjs +2 -0
  34. package/src/channels/shared/harness-client.mjs +40 -4
  35. package/src/channels/shared/i18n-en/shared-a.mjs +77 -0
  36. package/src/channels/shared/message-failure.mjs +244 -0
  37. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  38. package/src/channels/shared/text-harness-bridge.mjs +65 -60
  39. package/src/channels/shared/token-bot-controller.mjs +2 -0
  40. package/src/channels/slack/slack-controller.mjs +2 -0
  41. package/src/channels/wecom/wecom-bridge.mjs +49 -15
  42. package/src/channels/wecom/wecom-controller.mjs +2 -0
  43. package/src/channels/weixin/weixin-api.mjs +47 -0
  44. package/src/channels/weixin/weixin-bridge.mjs +261 -43
  45. package/src/channels/weixin/weixin-controller.mjs +2 -15
  46. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  47. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -1,6 +1,20 @@
1
1
  import * as React from 'react';
2
2
 
3
- import { h } from './i18n.js';
3
+ import { h, isEnglish } from './i18n.js';
4
+
5
+ function messageErrorTime(value) {
6
+ try {
7
+ return new Intl.DateTimeFormat(isEnglish() ? 'en-US' : 'zh-CN', {
8
+ year: 'numeric',
9
+ month: '2-digit',
10
+ day: '2-digit',
11
+ hour: '2-digit',
12
+ minute: '2-digit',
13
+ }).format(new Date(value));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
4
18
 
5
19
  export function ChannelListHeading({ className = '', id, title, connectionLabel }) {
6
20
  const helpId = React.useId();
@@ -46,3 +60,24 @@ export function BotStatusMeta({
46
60
  h('span', null, '最近检查'),
47
61
  h('span', null, formatCheckedTime(lastCheckedAt))));
48
62
  }
63
+
64
+ export function LastMessageErrorSummary({ className = '', error }) {
65
+ if (!error) return null;
66
+ const occurredAt = messageErrorTime(error.at);
67
+ return h('div', {
68
+ className: `${className} dim-cardSummary`.trim(),
69
+ role: 'status',
70
+ },
71
+ h('strong', null, '最近一条消息处理失败'),
72
+ ':',
73
+ h('span', null, error.message),
74
+ '(',
75
+ h('span', null, '错误码'),
76
+ ` ${error.code} · `,
77
+ h('span', null, '参考号'),
78
+ ` ${error.referenceId}`,
79
+ occurredAt ? h(React.Fragment, null,
80
+ ' · ',
81
+ h('time', { dateTime: new Date(error.at).toISOString() }, occurredAt)) : null,
82
+ ')');
83
+ }
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const DINGTALK_RPC_CHANNEL = '/dingtalk';
4
5
 
@@ -178,6 +179,7 @@ function normalizeBot(value) {
178
179
  messagesReceived: nonNegativeInteger(stats.messagesReceived),
179
180
  messagesReplied: nonNegativeInteger(stats.messagesReplied),
180
181
  },
182
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
181
183
  error: normalizeError(value.error, 'DINGTALK_ACCOUNT_ERROR', '钉钉连接尚未就绪') ?? null,
182
184
  };
183
185
  }
@@ -9,7 +9,11 @@ import {
9
9
  EMPTY_AGENT_PRESET_CATALOG,
10
10
  } from '../../agent-preset.js';
11
11
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
12
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
12
+ import {
13
+ BotStatusMeta,
14
+ ChannelListHeading,
15
+ LastMessageErrorSummary,
16
+ } from '../../channel-card-meta.js';
13
17
  import {
14
18
  DINGTALK_ENDPOINTS,
15
19
  DINGTALK_RPC_CHANNEL,
@@ -262,6 +266,10 @@ export function AccountCard({
262
266
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) },
263
267
  '移除接入')),
264
268
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
269
+ account.lastMessageError ? h(LastMessageErrorSummary, {
270
+ className: 'ddt-summary',
271
+ error: account.lastMessageError,
272
+ }) : null,
265
273
  feedback ? h('div', {
266
274
  className: 'ddt-summary dim-cardFeedback',
267
275
  role: 'status',
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
10
+ import { normalizeLastMessageError } from "../../last-message-error.js";
10
11
 
11
12
  export const FEISHU_RPC_CHANNEL = "/feishu";
12
13
 
@@ -206,6 +207,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
206
207
  groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
207
208
  bot: normalizeBot(value.bot),
208
209
  health: normalizeHealth(value.health, connected),
210
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
209
211
  error: normalizeError(value.error),
210
212
  };
211
213
  }
@@ -226,6 +228,7 @@ export function normalizeBotsSnapshot(value) {
226
228
  configured: true,
227
229
  bot: value.bot,
228
230
  health: value.health,
231
+ lastMessageError: value.lastMessageError,
229
232
  error: value.error,
230
233
  }];
231
234
  }
@@ -23,7 +23,11 @@ import {
23
23
  EMPTY_AGENT_PRESET_CATALOG,
24
24
  } from "../../agent-preset.js";
25
25
  import { useWorkspaceSnapshotFence } from "../../workspace-snapshot-fence.js";
26
- import { BotStatusMeta, ChannelListHeading } from "../../channel-card-meta.js";
26
+ import {
27
+ BotStatusMeta,
28
+ ChannelListHeading,
29
+ LastMessageErrorSummary,
30
+ } from "../../channel-card-meta.js";
27
31
  import { installFeishuStyles } from "./styles.js";
28
32
 
29
33
  export const name = "feishu-settings";
@@ -661,6 +665,10 @@ export function BotCard({
661
665
  }, "移除接入")),
662
666
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
663
667
  summary) : null,
668
+ connection.lastMessageError ? h(LastMessageErrorSummary, {
669
+ className: "bxf-healthSummary",
670
+ error: connection.lastMessageError,
671
+ }) : null,
664
672
  testNotice ? h("div", {
665
673
  className: "bxf-healthSummary dim-cardFeedback",
666
674
  role: "status",
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const QQ_RPC_CHANNEL = '/qq';
4
5
 
@@ -94,6 +95,7 @@ function normalizeBot(value) {
94
95
  summary: text(value.health?.summary, connected ? 'QQ WebSocket 长连接运行正常' : 'QQ 连接尚未就绪'),
95
96
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
96
97
  },
98
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
97
99
  error: isRecord(value.error) ? {
98
100
  code: text(value.error.code, 'QQ_ACCOUNT_ERROR', 80),
99
101
  message: text(value.error.message, 'QQ 连接尚未就绪'),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
15
19
  import {
16
20
  QQ_ENDPOINTS,
@@ -201,6 +205,10 @@ export function AccountCard({
201
205
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
202
206
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
203
207
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
208
+ account.lastMessageError ? h(LastMessageErrorSummary, {
209
+ className: 'ddt-summary',
210
+ error: account.lastMessageError,
211
+ }) : null,
204
212
  feedback ? h('div', {
205
213
  className: 'ddt-summary dim-cardFeedback',
206
214
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
4
5
 
@@ -68,6 +69,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
68
69
  ),
69
70
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
70
71
  },
72
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
71
73
  error: isRecord(value.error) ? {
72
74
  code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
73
75
  message: text(value.error.message, `${channel}连接尚未就绪`),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
 
15
19
  const Button = React.forwardRef(function Button(
16
20
  { children, kind = 'secondary', className = '', ...props },
@@ -121,6 +125,10 @@ export function createTokenChannelSettings(definition) {
121
125
  disabled: Boolean(busy),
122
126
  }, '移除接入')),
123
127
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
128
+ account.lastMessageError ? h(LastMessageErrorSummary, {
129
+ className: 'ddt-summary',
130
+ error: account.lastMessageError,
131
+ }) : null,
124
132
  testNotice ? h('div', {
125
133
  className: 'ddt-summary dim-cardFeedback',
126
134
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const WECOM_RPC_CHANNEL = '/wecom';
4
5
 
@@ -103,6 +104,7 @@ function normalizeBot(value) {
103
104
  summary: text(value.health?.summary, connected ? '企业微信 WebSocket 长连接运行正常' : '企业微信连接尚未就绪'),
104
105
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
105
106
  },
107
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
106
108
  error: isRecord(value.error) ? {
107
109
  code: text(value.error.code, 'WECOM_ACCOUNT_ERROR', 80),
108
110
  message: text(value.error.message, '企业微信连接尚未就绪'),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
15
19
  import {
16
20
  WECOM_ENDPOINTS,
@@ -200,6 +204,10 @@ export function AccountCard({
200
204
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
201
205
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
202
206
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
207
+ account.lastMessageError ? h(LastMessageErrorSummary, {
208
+ className: 'ddt-summary',
209
+ error: account.lastMessageError,
210
+ }) : null,
203
211
  feedback ? h('div', {
204
212
  className: 'ddt-summary dim-cardFeedback',
205
213
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const WEIXIN_RPC_CHANNEL = '/weixin';
4
5
  export const WEIXIN_ENDPOINTS = Object.freeze({
@@ -48,15 +49,6 @@ function normalizeTestMessage(value) {
48
49
  return { sent: false, code };
49
50
  }
50
51
 
51
- function normalizeMessageError(value) {
52
- if (!isRecord(value)) return null;
53
- const code = string(value.code).slice(0, 64);
54
- const reason = string(value.reason).slice(0, 128);
55
- const message = string(value.message).slice(0, 500);
56
- const at = timestamp(value.at);
57
- return code && reason && message && at !== null ? { code, reason, message, at } : null;
58
- }
59
-
60
52
  export function unwrapRpcResult(result) {
61
53
  if (!isRecord(result) || typeof result.ok !== 'boolean') {
62
54
  throw new Error('微信服务返回了无法识别的响应');
@@ -141,7 +133,7 @@ function normalizeBot(value) {
141
133
  messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
142
134
  messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
143
135
  },
144
- lastMessageError: normalizeMessageError(value.lastMessageError),
136
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
145
137
  error: isRecord(value.error)
146
138
  ? {
147
139
  code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
@@ -22,7 +22,11 @@ import {
22
22
  EMPTY_AGENT_PRESET_CATALOG,
23
23
  } from '../../agent-preset.js';
24
24
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
25
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
25
+ import {
26
+ BotStatusMeta,
27
+ ChannelListHeading,
28
+ LastMessageErrorSummary,
29
+ } from '../../channel-card-meta.js';
26
30
  import { installWeixinStyles } from './styles.js';
27
31
 
28
32
  export const name = 'weixin-settings';
@@ -247,10 +251,10 @@ export function AccountCard({
247
251
  busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
248
252
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
249
253
  summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
250
- account.lastMessageError ? h('div', {
251
- className: 'dxw-summary dim-cardSummary',
252
- role: 'status',
253
- }, `最近一条消息处理失败:${account.lastMessageError.message}`) : null,
254
+ account.lastMessageError ? h(LastMessageErrorSummary, {
255
+ className: 'dxw-summary',
256
+ error: account.lastMessageError,
257
+ }) : null,
254
258
  feedback ? h('div', {
255
259
  className: 'dxw-summary dim-cardFeedback',
256
260
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
4
5
 
@@ -106,6 +107,7 @@ function normalizeBot(value) {
106
107
  ? 'WhatsApp Web 关联设备运行正常' : 'WhatsApp 连接尚未就绪'),
107
108
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
108
109
  },
110
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
109
111
  error: isRecord(value.error) ? {
110
112
  code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
111
113
  message: text(value.error.message, 'WhatsApp 连接尚未就绪'),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
15
19
  import {
16
20
  WHATSAPP_ENDPOINTS,
@@ -345,6 +349,10 @@ export function WhatsappAccountCard({
345
349
  className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
346
350
  }, '移除接入')),
347
351
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
352
+ account.lastMessageError ? h(LastMessageErrorSummary, {
353
+ className: 'ddt-summary',
354
+ error: account.lastMessageError,
355
+ }) : null,
348
356
  testNotice ? h('div', {
349
357
  className: 'ddt-summary dim-cardFeedback',
350
358
  role: 'status',
@@ -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',
@@ -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 = [
@@ -326,6 +332,7 @@ export function createDingtalkBridgeStatus({ pendingSenders = [] } = {}) {
326
332
  lastReplyAt: null,
327
333
  lastRejectedAt: null,
328
334
  lastError: null,
335
+ lastMessageError: null,
329
336
  pendingSenders: structuredClone(pendingSenders),
330
337
  stats: {
331
338
  messagesReceived: 0,
@@ -484,9 +491,13 @@ export class DingtalkHarnessBridge {
484
491
  commandRunner,
485
492
  ).catch((error) => {
486
493
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
487
- this.#status.lastError = t('钉钉命令处理失败。');
488
- this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
489
- 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);
490
501
  }).finally(() => {
491
502
  this.#acceptedMessageIds.delete(messageId);
492
503
  this.#commandTasks.delete(task);
@@ -659,9 +670,13 @@ export class DingtalkHarnessBridge {
659
670
  this.#status.lastError = null;
660
671
  }).catch(async (error) => {
661
672
  if (this.#signal?.aborted) return;
662
- this.#status.lastError = t('钉钉命令处理失败。');
663
- this.#logger.error?.('[dsh-dingtalk] failed to process a batch input message', safeErrorDiagnostic(error));
664
- 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);
665
680
  }).finally(() => {
666
681
  this.#acceptedMessageIds.delete(messageId);
667
682
  this.#commandTasks.delete(task);
@@ -819,7 +834,7 @@ export class DingtalkHarnessBridge {
819
834
  });
820
835
  }
821
836
  } catch (error) {
822
- textDeliveryError = error;
837
+ textDeliveryError = channelDeliveryFailure(error);
823
838
  }
824
839
  const delivery = await this.#deliverArtifacts(
825
840
  fileTarget(message, sender, this.#clientId),
@@ -829,9 +844,15 @@ export class DingtalkHarnessBridge {
829
844
  textReceipt,
830
845
  );
831
846
  if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
847
+ if (textDeliveryError && delivery.artifactSendErrors === 0) {
848
+ setLastMessageFailure(this.#status, textDeliveryError);
849
+ }
832
850
  increment(this.#status, 'messagesReplied');
833
851
  this.#status.lastReplyAt = new Date().toISOString();
834
852
  this.#status.lastError = null;
853
+ if (!textDeliveryError && delivery.artifactSendErrors === 0) {
854
+ clearLastMessageFailure(this.#status);
855
+ }
835
856
  return delivery.receipt;
836
857
  } catch (error) {
837
858
  let batchFailureMessage = null;
@@ -848,15 +869,19 @@ export class DingtalkHarnessBridge {
848
869
  return;
849
870
  }
850
871
  if (this.#signal?.aborted) return;
851
- 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
+ });
852
879
  this.#logger.error?.(
853
- '[dsh-dingtalk] failed to process an inbound message',
880
+ `[dsh-dingtalk] failed to process an inbound message [${failure.referenceId}]`,
854
881
  safeErrorDiagnostic(error),
855
882
  );
856
883
  try {
857
- const errorText = inboundFileUserMessage(error)
858
- ?? dingtalkImageErrorUserMessage(error)
859
- ?? t(CARD_ERROR_TEXT);
884
+ const errorText = messageFailureText(failure);
860
885
  const visibleError = batchFailureMessage
861
886
  ? `${errorText}\n\n${batchFailureMessage}`
862
887
  : errorText;
@@ -1201,9 +1226,13 @@ export class DingtalkHarnessBridge {
1201
1226
  sendFile: typeof this.#api.sendFile === 'function'
1202
1227
  ? (file) => sendArtifact('sendFile', file)
1203
1228
  : undefined,
1204
- 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(
1205
1234
  sessionWebhook,
1206
- artifactFailureText(artifact?.fileName, error),
1235
+ messageFailureText(failure),
1207
1236
  ),
1208
1237
  logger: this.#logger,
1209
1238
  });
@@ -1211,7 +1240,11 @@ export class DingtalkHarnessBridge {
1211
1240
  + delivery.artifactsSent;
1212
1241
  this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
1213
1242
  + delivery.artifactSendErrors;
1214
- return { receipt: delivery.receipt, userVisible: delivery.userVisible };
1243
+ return {
1244
+ receipt: delivery.receipt,
1245
+ userVisible: delivery.userVisible,
1246
+ artifactSendErrors: delivery.artifactSendErrors,
1247
+ };
1215
1248
  }
1216
1249
  }
1217
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,