@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "2.4.0",
3
+ "version": "2.6.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;
@@ -8,8 +8,14 @@ export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
8
8
  export const DINGTALK_API_BASE_URL = 'https://api.dingtalk.com/';
9
9
  export const DINGTALK_REGISTRATION_SOURCE = 'DING_DWS_CLAW';
10
10
  export const DINGTALK_AI_CARD_TEMPLATE_ID = '02fcf2f4-5e02-4a85-b672-46d1f715543e.schema';
11
+ export const DINGTALK_THINKING_REACTION_NAME = '🤔思考中';
12
+ export const DINGTALK_DONE_REACTION_NAME = '✅已完成';
13
+ export const DINGTALK_ERROR_REACTION_NAME = '❌处理失败';
11
14
 
12
15
  const DEFAULT_TIMEOUT_MS = 15_000;
16
+ const REACTION_TIMEOUT_MS = 5_000;
17
+ const TEXT_REACTION_ID = '2659900';
18
+ const TEXT_REACTION_BACKGROUND_ID = 'im_bg_1';
13
19
  const REGISTRATION_STATUSES = new Set(['WAITING', 'SUCCESS', 'FAIL', 'EXPIRED']);
14
20
 
15
21
  export class DingtalkApiError extends Error {
@@ -406,18 +412,20 @@ export function createDingtalkApi({
406
412
  if (!source) throw new TypeError('registrationSource is required');
407
413
  const tokenCache = new Map();
408
414
  const tokenRequests = new Map();
415
+ const reactionTokenRequests = new Map();
409
416
  let cardSlotTail = Promise.resolve();
410
417
  let nextCardRequestAt = 0;
411
418
 
412
419
  const endpoint = (base, pathname) => new URL(pathname.replace(/^\//, ''), base);
413
420
 
414
- async function accessToken({ clientId, clientSecret, signal }) {
421
+ async function accessToken({ clientId, clientSecret, signal, requestKind = 'normal' }) {
415
422
  const appKey = nonEmptyString(clientId);
416
423
  const appSecret = nonEmptyString(clientSecret);
417
424
  if (!appKey || !appSecret) throw new TypeError('clientId and clientSecret are required');
418
425
  const cached = tokenCache.get(appKey);
419
426
  if (cached && cached.expiresAt > now()) return cached.token;
420
- if (tokenRequests.has(appKey)) return tokenRequests.get(appKey);
427
+ const requests = requestKind === 'reaction' ? reactionTokenRequests : tokenRequests;
428
+ if (requests.has(appKey)) return requests.get(appKey);
421
429
 
422
430
  const request = (async () => {
423
431
  const value = await requestJson(fetchImpl, endpoint(apiBase, 'v1.0/oauth2/accessToken'), {
@@ -431,9 +439,75 @@ export function createDingtalkApi({
431
439
  const refreshAfterMs = Math.max(1_000, (expiresInSeconds - 60) * 1_000);
432
440
  tokenCache.set(appKey, { token, expiresAt: now() + refreshAfterMs });
433
441
  return token;
434
- })().finally(() => tokenRequests.delete(appKey));
435
- tokenRequests.set(appKey, request);
436
- return request;
442
+ })();
443
+ const shared = request.finally(() => requests.delete(appKey));
444
+ requests.set(appKey, shared);
445
+ return shared;
446
+ }
447
+
448
+ async function changeReaction({
449
+ clientId,
450
+ clientSecret,
451
+ robotCode,
452
+ messageId,
453
+ conversationId,
454
+ reactionName = DINGTALK_THINKING_REACTION_NAME,
455
+ signal,
456
+ }, action) {
457
+ const appKey = nonEmptyString(clientId);
458
+ const appSecret = nonEmptyString(clientSecret);
459
+ const botCode = nonEmptyString(robotCode) ?? appKey;
460
+ const openMsgId = nonEmptyString(messageId);
461
+ const openConversationId = nonEmptyString(conversationId);
462
+ const emotionName = nonEmptyString(reactionName);
463
+ if (!appKey || !appSecret) throw new TypeError('clientId and clientSecret are required');
464
+ if (!botCode) throw new TypeError('robotCode is required');
465
+ if (!openMsgId || !openConversationId) {
466
+ throw new TypeError('messageId and conversationId are required');
467
+ }
468
+ if (!emotionName) throw new TypeError('reactionName is required');
469
+ // Reactions deduplicate with each other but never own the normal reply's
470
+ // in-flight token request. Both paths may still reuse a cached token.
471
+ const token = await accessToken({
472
+ clientId: appKey,
473
+ clientSecret: appSecret,
474
+ signal,
475
+ requestKind: 'reaction',
476
+ });
477
+ const response = await requestJson(
478
+ fetchImpl,
479
+ endpoint(apiBase, `v1.0/robot/emotion/${action}`),
480
+ {
481
+ body: {
482
+ robotCode: botCode,
483
+ openMsgId,
484
+ openConversationId,
485
+ emotionType: 2,
486
+ emotionName,
487
+ textEmotion: {
488
+ emotionId: TEXT_REACTION_ID,
489
+ emotionName,
490
+ text: emotionName,
491
+ backgroundId: TEXT_REACTION_BACKGROUND_ID,
492
+ },
493
+ },
494
+ headers: { 'x-acs-dingtalk-access-token': token },
495
+ signal,
496
+ timeoutMs: REACTION_TIMEOUT_MS,
497
+ action: action === 'reply' ? '消息状态添加' : '消息状态撤回',
498
+ },
499
+ );
500
+ const rejection = response?.success === false
501
+ ? safeProviderCode(response?.code ?? response?.errcode) ?? 'rejected'
502
+ : rejectedProviderResponse(response);
503
+ if (rejection) {
504
+ throw new DingtalkApiError(
505
+ 'reaction-rejected',
506
+ '钉钉服务拒绝了消息状态请求。',
507
+ { providerCode: rejection },
508
+ );
509
+ }
510
+ return true;
437
511
  }
438
512
 
439
513
  async function messageFileDownloadUrl({
@@ -690,6 +764,28 @@ export function createDingtalkApi({
690
764
 
691
765
  accessToken,
692
766
 
767
+ async addReaction(request) {
768
+ return changeReaction(request, 'reply');
769
+ },
770
+
771
+ async recallReaction(request) {
772
+ return changeReaction(request, 'recall');
773
+ },
774
+
775
+ async addThinkingReaction(request) {
776
+ return changeReaction({
777
+ ...request,
778
+ reactionName: DINGTALK_THINKING_REACTION_NAME,
779
+ }, 'reply');
780
+ },
781
+
782
+ async recallThinkingReaction(request) {
783
+ return changeReaction({
784
+ ...request,
785
+ reactionName: DINGTALK_THINKING_REACTION_NAME,
786
+ }, 'recall');
787
+ },
788
+
693
789
  async downloadImage({
694
790
  clientId,
695
791
  clientSecret,
@@ -830,7 +926,7 @@ export function createDingtalkApi({
830
926
  clientId: appKey,
831
927
  clientSecret: appSecret,
832
928
  cardInstanceId,
833
- text: t('消息处理失败,请稍后重试。'),
929
+ text: t('卡片已结束,请查看后续消息。'),
834
930
  signal: cleanupSignal,
835
931
  }).catch(() => undefined);
836
932
  }