@xmanrui/dsh-im 3.1.0 → 3.2.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 (64) hide show
  1. package/README.en.md +15 -0
  2. package/README.md +15 -0
  3. package/lib/client.js +1653 -333
  4. package/lib/index.js +235 -223
  5. package/package.json +5 -1
  6. package/plugin-src/client/channels/dingtalk/api.js +3 -0
  7. package/plugin-src/client/channels/dingtalk/index.js +18 -5
  8. package/plugin-src/client/channels/feishu/api.js +3 -0
  9. package/plugin-src/client/channels/feishu/index.js +18 -5
  10. package/plugin-src/client/channels/qq/api.js +3 -0
  11. package/plugin-src/client/channels/qq/index.js +13 -0
  12. package/plugin-src/client/channels/shared/token-api.js +3 -0
  13. package/plugin-src/client/channels/shared/token-channel.js +13 -1
  14. package/plugin-src/client/channels/wecom/api.js +3 -0
  15. package/plugin-src/client/channels/wecom/index.js +13 -0
  16. package/plugin-src/client/channels/weixin/api.js +3 -0
  17. package/plugin-src/client/channels/weixin/index.js +19 -5
  18. package/plugin-src/client/channels/whatsapp/api.js +3 -0
  19. package/plugin-src/client/channels/whatsapp/index.js +13 -0
  20. package/plugin-src/client/context-enhancement.js +275 -0
  21. package/plugin-src/client/i18n.js +54 -3
  22. package/plugin-src/client/styles.js +78 -0
  23. package/plugin-src/client/update-panel.js +108 -7
  24. package/plugin-src/host/channels/dingtalk/production.mjs +1 -0
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  26. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  27. package/plugin-src/host/channels/feishu/rpc.mjs +13 -0
  28. package/plugin-src/host/channels/qq/production.mjs +1 -0
  29. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  30. package/plugin-src/host/channels/shared/context-enhancement-rpc.mjs +17 -0
  31. package/plugin-src/host/channels/shared/production.mjs +1 -0
  32. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  33. package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
  34. package/plugin-src/host/channels/slack/production.mjs +1 -0
  35. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  36. package/plugin-src/host/channels/wecom/production.mjs +1 -0
  37. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  38. package/plugin-src/host/channels/weixin/production.mjs +1 -0
  39. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  40. package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
  41. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  42. package/plugin-src/host/update-runtime.mjs +3 -2
  43. package/plugin-src/host/update-service.mjs +2 -0
  44. package/scripts/verify-package.mjs +15 -2
  45. package/src/channels/dingtalk/dingtalk-api.mjs +39 -16
  46. package/src/channels/dingtalk/dingtalk-bridge.mjs +52 -23
  47. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -1
  48. package/src/channels/discord/discord-runtime.mjs +18 -4
  49. package/src/channels/feishu/bridge.mjs +20 -4
  50. package/src/channels/feishu/feishu-channel.mjs +100 -44
  51. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  52. package/src/channels/qq/qq-bridge.mjs +20 -4
  53. package/src/channels/qq/qq-runtime.mjs +4 -0
  54. package/src/channels/shared/bot-workspace-store.mjs +103 -6
  55. package/src/channels/shared/context-enhancement.mjs +135 -0
  56. package/src/channels/shared/i18n-en/feishu.mjs +2 -0
  57. package/src/channels/shared/text-harness-bridge.mjs +19 -4
  58. package/src/channels/slack/slack-runtime.mjs +4 -0
  59. package/src/channels/telegram/telegram-runtime.mjs +24 -2
  60. package/src/channels/wecom/wecom-bridge.mjs +18 -3
  61. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  62. package/src/channels/weixin/weixin-bridge.mjs +19 -4
  63. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  64. package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
@@ -1,4 +1,5 @@
1
1
  import QRCode from 'qrcode';
2
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
5
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -18,6 +19,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
18
19
  deleteBot: 'bot.delete',
19
20
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
21
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
21
23
  approveSender: 'bot.sender.approve',
22
24
  revokeSender: 'bot.sender.revoke',
23
25
  });
@@ -94,6 +96,10 @@ function payloadFailure(endpoint, payload) {
94
96
  return validAgentPresetPayload(payload)
95
97
  ? null : '请选择 Agent Preset。';
96
98
  }
99
+ if (endpoint === DINGTALK_ENDPOINTS.setContextEnhancement) {
100
+ return validContextEnhancementPayload(payload)
101
+ ? null : '请提交有效的上下文增强设置。';
102
+ }
97
103
  if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
98
104
  return exactKeys(payload, ['botId', 'requestId', 'confirm'])
99
105
  && validId(payload.botId)
@@ -251,6 +257,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
251
257
  await controller.updateWorkspace(payload.botId, payload.workspace),
252
258
  cachedEncode,
253
259
  );
260
+ } else if (endpoint === DINGTALK_ENDPOINTS.setContextEnhancement) {
261
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
262
+ value = await controller.updateContextEnhancement(
263
+ payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
264
+ );
254
265
  } else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
255
266
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
256
267
  value = await publicStatus(
@@ -98,6 +98,8 @@ export async function createProductionController(ctx, config = {}, internals = {
98
98
  workspaces,
99
99
  method: 'removeBot',
100
100
  botIdFromRemoved: (removed) => removed.id,
101
+ saveMethod: 'saveBot',
102
+ botIdFromSave: (bot) => bot?.id,
101
103
  })
102
104
  : configStore;
103
105
  // State is lazy per bot. A corrupt legacy file can therefore fail only the
@@ -167,6 +169,7 @@ export async function createProductionController(ctx, config = {}, internals = {
167
169
  ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
168
170
  harness: workspaceScope.harness,
169
171
  state: workspaceScope.state,
172
+ contextEnhancement: { botId: id, getSettings: () => workspaces.contextEnhancementFor(id) },
170
173
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
171
174
  ...(wsAgent ? { wsAgent } : {}),
172
175
  logger: {
@@ -8,6 +8,8 @@ import { publicMessageFailure } from '../../../../src/channels/shared/message-fa
8
8
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
9
9
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
10
10
  import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
11
+ import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
12
+ import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
11
13
  import {
12
14
  isFeishuGroupResponseMode,
13
15
  normalizeFeishuGroupResponseMode,
@@ -276,6 +278,7 @@ function publicBotEntry(entry) {
276
278
  connected,
277
279
  configured: source.configured === true,
278
280
  agentPreset: normalizeAgentPresetId(source.agentPreset),
281
+ contextEnhancement: normalizeContextEnhancementConfig(source.contextEnhancement),
279
282
  groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
280
283
  groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
281
284
  bot: publicBot(source.bot),
@@ -415,6 +418,10 @@ function validPayload(endpoint, payload) {
415
418
  return validAgentPresetPayload(payload)
416
419
  ? null : '请选择 Agent Preset。';
417
420
  }
421
+ if (endpoint === FEISHU_ENDPOINTS.setContextEnhancement) {
422
+ return validContextEnhancementPayload(payload)
423
+ ? null : '请提交有效的上下文增强设置。';
424
+ }
418
425
  if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
419
426
  return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
420
427
  && safeOpaqueId(payload.botId)
@@ -666,6 +673,12 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
666
673
  await controller.updateWorkspace(payload.botId, payload.workspace),
667
674
  { encodeQr: cachedEncodeQr },
668
675
  );
676
+ } else if (endpoint === FEISHU_ENDPOINTS.setContextEnhancement) {
677
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
678
+ value = await controller.updateContextEnhancement(
679
+ payload.botId, payload.config,
680
+ (status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
681
+ );
669
682
  } else if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
670
683
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
671
684
  value = await toPublicFeishuStatus(
@@ -100,6 +100,7 @@ export async function createProductionController(ctx, config = {}, internals = {
100
100
  appSecret,
101
101
  harness: workspaceScope.harness,
102
102
  state: workspaceScope.state,
103
+ contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
103
104
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
104
105
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
105
106
  logger: {
@@ -3,6 +3,7 @@ import {
3
3
  connectionTestTargetUnavailable,
4
4
  publicConnectionTestResult,
5
5
  } from '../../../../src/channels/shared/connection-test.mjs';
6
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
6
7
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
7
8
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
8
9
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -18,6 +19,7 @@ export const QQ_ENDPOINTS = Object.freeze({
18
19
  deleteBot: 'bot.delete',
19
20
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
21
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
21
23
  });
22
24
  export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
23
25
 
@@ -76,6 +78,10 @@ function payloadFailure(endpoint, payload) {
76
78
  return validAgentPresetPayload(payload)
77
79
  ? null : '请选择 Agent Preset。';
78
80
  }
81
+ if (endpoint === QQ_ENDPOINTS.setContextEnhancement) {
82
+ return validContextEnhancementPayload(payload)
83
+ ? null : '请提交有效的上下文增强设置。';
84
+ }
79
85
  return 'Unknown QQ endpoint.';
80
86
  }
81
87
 
@@ -171,6 +177,11 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
171
177
  await controller.updateWorkspace(payload.botId, payload.workspace),
172
178
  cachedEncode,
173
179
  );
180
+ } else if (endpoint === QQ_ENDPOINTS.setContextEnhancement) {
181
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
182
+ value = await controller.updateContextEnhancement(
183
+ payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
184
+ );
174
185
  } else if (endpoint === QQ_ENDPOINTS.setAgentPreset) {
175
186
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
176
187
  value = await publicStatus(
@@ -0,0 +1,17 @@
1
+ import { validateContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
2
+
3
+ export const SET_CONTEXT_ENHANCEMENT_ENDPOINT = 'bot.context-enhancement.set';
4
+
5
+ export function validContextEnhancementPayload(payload) {
6
+ try {
7
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
8
+ || Reflect.ownKeys(payload).length !== 2
9
+ || !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'config')
10
+ || typeof payload.botId !== 'string'
11
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
12
+ validateContextEnhancementConfig(payload.config);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
@@ -102,6 +102,7 @@ export async function createTokenProductionController(ctx, config, internals, de
102
102
  token,
103
103
  harness: workspaceScope.harness,
104
104
  state: workspaceScope.state,
105
+ contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
105
106
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
106
107
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
107
108
  logger: {
@@ -1,3 +1,4 @@
1
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
1
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
3
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
3
4
  import {
@@ -17,6 +18,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
17
18
  deleteBot: 'bot.delete',
18
19
  setWorkspace: SET_WORKSPACE_ENDPOINT,
19
20
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
21
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
20
22
  });
21
23
 
22
24
  const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@@ -71,6 +73,10 @@ function payloadFailure(endpoint, payload) {
71
73
  return validAgentPresetPayload(payload)
72
74
  ? null : '请选择 Agent Preset。';
73
75
  }
76
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
77
+ return validContextEnhancementPayload(payload)
78
+ ? null : '请提交有效的上下文增强设置。';
79
+ }
74
80
  return 'Unknown bot endpoint.';
75
81
  }
76
82
 
@@ -152,6 +158,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
152
158
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
153
159
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
154
160
  value = await controller.updateWorkspace(payload.botId, payload.workspace);
161
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
162
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
163
+ value = await controller.updateContextEnhancement(payload.botId, payload.config);
155
164
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
156
165
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
157
166
  value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
@@ -22,6 +22,7 @@ export function publicWorkspaceError(error) {
22
22
  'workspace-bot-not-found',
23
23
  'agent-preset-invalid',
24
24
  'agent-preset-unavailable',
25
+ 'context-enhancement-invalid',
25
26
  ].includes(error?.code)) return null;
26
27
  return { code: error.code, message: error.message };
27
28
  }
@@ -89,6 +89,7 @@ export async function createProductionController(ctx, config = {}, internals = {
89
89
  appToken,
90
90
  harness: workspaceScope.harness,
91
91
  state: workspaceScope.state,
92
+ contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
92
93
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
93
94
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
94
95
  logger: {
@@ -1,3 +1,4 @@
1
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
1
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
3
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
3
4
  import {
@@ -18,6 +19,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
18
19
  deleteBot: 'bot.delete',
19
20
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
21
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
21
23
  });
22
24
  export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
23
25
 
@@ -75,6 +77,10 @@ function payloadFailure(endpoint, payload) {
75
77
  return validAgentPresetPayload(payload)
76
78
  ? null : '请选择 Agent Preset。';
77
79
  }
80
+ if (endpoint === SLACK_ENDPOINTS.setContextEnhancement) {
81
+ return validContextEnhancementPayload(payload)
82
+ ? null : '请提交有效的上下文增强设置。';
83
+ }
78
84
  return 'Unknown Slack endpoint.';
79
85
  }
80
86
 
@@ -154,6 +160,10 @@ export function createSlackRpcHandler(controller) {
154
160
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
155
161
  value = await controller.updateWorkspace(payload.botId, payload.workspace);
156
162
  }
163
+ else if (endpoint === SLACK_ENDPOINTS.setContextEnhancement) {
164
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
165
+ value = await controller.updateContextEnhancement(payload.botId, payload.config);
166
+ }
157
167
  else if (endpoint === SLACK_ENDPOINTS.setAgentPreset) {
158
168
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
159
169
  value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
@@ -103,6 +103,7 @@ export async function createProductionController(ctx, config = {}, internals = {
103
103
  secret,
104
104
  harness: workspaceScope.harness,
105
105
  state: workspaceScope.state,
106
+ contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
106
107
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
107
108
  connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
108
109
  maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
@@ -1,4 +1,5 @@
1
1
  import QRCode from 'qrcode';
2
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
5
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -18,6 +19,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
18
19
  deleteBot: 'bot.delete',
19
20
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
21
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
22
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
21
23
  });
22
24
  export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
23
25
 
@@ -76,6 +78,10 @@ function payloadFailure(endpoint, payload) {
76
78
  return validAgentPresetPayload(payload)
77
79
  ? null : '请选择 Agent Preset。';
78
80
  }
81
+ if (endpoint === WECOM_ENDPOINTS.setContextEnhancement) {
82
+ return validContextEnhancementPayload(payload)
83
+ ? null : '请提交有效的上下文增强设置。';
84
+ }
79
85
  return 'Unknown Enterprise WeChat endpoint.';
80
86
  }
81
87
 
@@ -172,6 +178,11 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
172
178
  await controller.updateWorkspace(payload.botId, payload.workspace),
173
179
  cachedEncode,
174
180
  );
181
+ } else if (endpoint === WECOM_ENDPOINTS.setContextEnhancement) {
182
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
183
+ value = await controller.updateContextEnhancement(
184
+ payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
185
+ );
175
186
  } else if (endpoint === WECOM_ENDPOINTS.setAgentPreset) {
176
187
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
177
188
  value = await publicStatus(
@@ -107,6 +107,7 @@ export async function createProductionController(ctx, config = {}, internals = {
107
107
  token,
108
108
  harness: workspaceScope.harness,
109
109
  state: workspaceScope.state,
110
+ contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
110
111
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
111
112
  maxMessageChars: config.maxMessageChars ?? DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
112
113
  logger: {
@@ -1,4 +1,5 @@
1
1
  import QRCode from 'qrcode';
2
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import {
4
5
  publicWorkspaceError,
@@ -25,6 +26,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
25
26
  deleteBot: 'bot.delete',
26
27
  setWorkspace: SET_WORKSPACE_ENDPOINT,
27
28
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
29
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
28
30
  });
29
31
  export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
30
32
 
@@ -83,6 +85,10 @@ function payloadFailure(endpoint, payload) {
83
85
  return validAgentPresetPayload(payload)
84
86
  ? null : '请选择 Agent Preset。';
85
87
  }
88
+ if (endpoint === WEIXIN_ENDPOINTS.setContextEnhancement) {
89
+ return validContextEnhancementPayload(payload)
90
+ ? null : '请提交有效的上下文增强设置。';
91
+ }
86
92
  return 'Unknown Weixin endpoint.';
87
93
  }
88
94
 
@@ -207,6 +213,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
207
213
  await controller.updateWorkspace(payload.botId, payload.workspace),
208
214
  cachedEncode,
209
215
  );
216
+ } else if (endpoint === WEIXIN_ENDPOINTS.setContextEnhancement) {
217
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
218
+ value = await controller.updateContextEnhancement(
219
+ payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
220
+ );
210
221
  } else if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
211
222
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
212
223
  value = await publicStatus(
@@ -106,6 +106,7 @@ export async function createProductionController(ctx, config = {}, internals = {
106
106
  authDir,
107
107
  harness: workspaceScope.harness,
108
108
  state: workspaceScope.state,
109
+ contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
109
110
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
110
111
  connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
111
112
  createSession,
@@ -2,6 +2,7 @@ import QRCode from 'qrcode';
2
2
 
3
3
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
4
4
  import { normalizeWhatsappAccessPolicy } from '../../../../src/channels/whatsapp/config-store.mjs';
5
+ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
5
6
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
6
7
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
7
8
  import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@@ -17,6 +18,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
17
18
  setAccessPolicy: 'bot.access-policy.set',
18
19
  setWorkspace: SET_WORKSPACE_ENDPOINT,
19
20
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
21
+ setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
20
22
  });
21
23
  export const WHATSAPP_RPC_ENDPOINTS = Object.freeze(Object.values(WHATSAPP_ENDPOINTS));
22
24
 
@@ -71,6 +73,10 @@ function payloadFailure(endpoint, payload) {
71
73
  return validAgentPresetPayload(payload)
72
74
  ? null : '请选择 Agent Preset。';
73
75
  }
76
+ if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
77
+ return validContextEnhancementPayload(payload)
78
+ ? null : '请提交有效的上下文增强设置。';
79
+ }
74
80
  return 'Unknown WhatsApp endpoint.';
75
81
  }
76
82
 
@@ -173,6 +179,11 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
173
179
  await controller.updateWorkspace(payload.botId, payload.workspace),
174
180
  cachedEncode,
175
181
  );
182
+ } else if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
183
+ if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
184
+ value = await controller.updateContextEnhancement(
185
+ payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
186
+ );
176
187
  } else if (endpoint === WHATSAPP_ENDPOINTS.setAgentPreset) {
177
188
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
178
189
  value = await publicStatus(
@@ -280,6 +280,8 @@ export function createUpdateRuntime(options = {}) {
280
280
 
281
281
  const profile = await packageAt(runtime.profileDir);
282
282
  const installed = await packageAt(join(runtime.profileDir, 'node_modules', PACKAGE_NAME));
283
+ result.sourceInstall = !registrySpec(profile.manifest.dependencies?.[PACKAGE_NAME])
284
+ || !inside(join(runtime.profileDir, 'node_modules'), installed.directory);
283
285
  result.installedVersion = typeof installed.manifest.version === 'string' ? installed.manifest.version : null;
284
286
  result.packageValid = await validPackage(installed);
285
287
  const loaded = await loadedPackage;
@@ -298,8 +300,7 @@ export function createUpdateRuntime(options = {}) {
298
300
  if (boundProfile !== undefined && boundProfile !== identity) result.blockedReason = 'installation-changed';
299
301
  else if (!sameLoadedPackage && boundProfile === undefined) result.blockedReason = 'installation-changed';
300
302
  else if (!result.packageValid) result.blockedReason = 'invalid-installation';
301
- else if (!registrySpec(profile.manifest.dependencies?.[PACKAGE_NAME])
302
- || !inside(join(runtime.profileDir, 'node_modules'), installed.directory)) result.blockedReason = 'source-install';
303
+ else if (result.sourceInstall) result.blockedReason = 'source-install';
303
304
  else if (runtime.blockedReason) result.blockedReason = runtime.blockedReason;
304
305
  else if (!sameLoadedPackage) result.blockedReason = 'pending-restart';
305
306
  else if (preflight) await checkRegistry(runtime);
@@ -201,6 +201,8 @@ export function createUpdateService({
201
201
  latestVersion: checked?.release.version ?? null,
202
202
  profileName: environment.profileName ?? null,
203
203
  environmentKind: environment.environmentKind ?? 'cli',
204
+ // Preserve source protection even when restart/recovery takes status priority.
205
+ sourceInstall: environment.sourceInstall === true || environment.blockedReason === 'source-install',
204
206
  canInstall,
205
207
  blockedReason,
206
208
  checkedAt: checked?.checkedAt ?? null,
@@ -29,6 +29,7 @@ const required = [
29
29
  'plugin-src/client/channels/slack/index.js',
30
30
  'plugin-src/client/i18n.js',
31
31
  'plugin-src/client/update-panel.js',
32
+ 'plugin-src/client/context-enhancement.js',
32
33
  'plugin-src/host/update-service.mjs',
33
34
  'plugin-src/host/update-runtime.mjs',
34
35
  'plugin-src/host/update-rpc.mjs',
@@ -51,6 +52,7 @@ const required = [
51
52
  'src/channels/discord/discord-runtime.mjs',
52
53
  'src/channels/whatsapp/whatsapp-runtime.mjs',
53
54
  'src/channels/whatsapp/whatsapp-web-session.mjs',
55
+ 'src/channels/shared/context-enhancement.mjs',
54
56
  ];
55
57
  await Promise.all(required.map((path) => access(resolve(root, path))));
56
58
 
@@ -135,8 +137,19 @@ if ((client.match(/\.slots\.inject\(\s*["']settings\.section["']/gu) ?? []).leng
135
137
  if (client.includes('settings.plugins.tab') || clientSources.includes('settings.plugins.tab')) {
136
138
  throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
137
139
  }
138
- if (/role:\s*["']switch|type:\s*["']checkbox/.test(client)) {
139
- throw new Error('client bundle contains a channel enable switch');
140
+ // Connections still have no channel-enable toggle. Only the shared context
141
+ // editor owns checkable inputs: two scope switches and one mapped field input.
142
+ const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
143
+ const otherClientSources = clientSources.replace(contextEditorSource, '');
144
+ if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
145
+ || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 2
146
+ || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
147
+ throw new Error('checkable inputs must be limited to the context-enhancement editor');
148
+ }
149
+ for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
150
+ if (!host.includes(marker) || !client.includes(marker)) {
151
+ throw new Error(`context-enhancement marker missing from Host or Client bundle: ${marker}`);
152
+ }
140
153
  }
141
154
  if (!client.includes('container-type: inline-size')
142
155
  || !client.includes('@container (max-width: 680px)')) {
@@ -316,16 +316,30 @@ function normalizeCardTarget(target) {
316
316
  }
317
317
  if (target?.type === 'group') {
318
318
  const openConversationId = nonEmptyString(target.openConversationId);
319
- if (openConversationId) return { type: 'group', openConversationId };
319
+ if (openConversationId) {
320
+ return { type: 'group', openConversationId, atUserIds: target.atUserIds };
321
+ }
320
322
  }
321
323
  throw new TypeError('DingTalk AI Card target is invalid');
322
324
  }
323
325
 
324
- function cardData(text, flowStatus) {
326
+ function cardMarkdown(text, target) {
327
+ const escape = (value) => String(value).replace(/[&<>"']/g, (char) => ({
328
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
329
+ })[char]);
330
+ const mentions = target?.type === 'group'
331
+ ? Object.entries(target.atUserIds ?? {}).map(([userId, name]) => (
332
+ `<a atId="${escape(userId)}">${escape(name)}</a>`
333
+ )).join(' ')
334
+ : '';
335
+ return normalizeDingtalkCardMarkdown(mentions ? `${mentions}\n\n${text}` : text);
336
+ }
337
+
338
+ function cardData(text, flowStatus, target) {
325
339
  return {
326
340
  cardParamMap: {
327
341
  flowStatus,
328
- msgContent: normalizeDingtalkCardMarkdown(text),
342
+ msgContent: cardMarkdown(text, target),
329
343
  staticMsgContent: '',
330
344
  sys_full_json_obj: JSON.stringify({ order: ['msgContent'] }),
331
345
  config: JSON.stringify({ autoLayout: true }),
@@ -339,7 +353,7 @@ function cardDeliverBody(cardInstanceId, target, robotCode) {
339
353
  return {
340
354
  ...base,
341
355
  openSpaceId: `dtv1.card//IM_GROUP.${target.openConversationId}`,
342
- imGroupOpenDeliverModel: { robotCode },
356
+ imGroupOpenDeliverModel: { robotCode, atUserIds: target.atUserIds },
343
357
  };
344
358
  }
345
359
  return {
@@ -575,7 +589,7 @@ export function createDingtalkApi({
575
589
  }
576
590
  }
577
591
 
578
- async function failCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
592
+ async function failCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
579
593
  const instanceId = nonEmptyString(cardInstanceId);
580
594
  const content = nonEmptyString(text);
581
595
  if (!instanceId) throw new TypeError('cardInstanceId is required');
@@ -589,7 +603,7 @@ export function createDingtalkApi({
589
603
  outTrackId: instanceId,
590
604
  guid: randomUUID(),
591
605
  key: 'msgContent',
592
- content: normalizeDingtalkCardMarkdown(content),
606
+ content: cardMarkdown(content, target),
593
607
  isFull: true,
594
608
  isFinalize: false,
595
609
  isError: true,
@@ -602,7 +616,7 @@ export function createDingtalkApi({
602
616
  method: 'PUT',
603
617
  body: {
604
618
  outTrackId: instanceId,
605
- cardData: cardData(content, '5'),
619
+ cardData: cardData(content, '5', target),
606
620
  cardUpdateOptions: { updateCardDataByKey: true },
607
621
  },
608
622
  headers,
@@ -883,6 +897,9 @@ export function createDingtalkApi({
883
897
  cardParamMap: { config: JSON.stringify({ autoLayout: true }) },
884
898
  },
885
899
  callbackType: 'STREAM',
900
+ cardAtUserIds: normalizedTarget.atUserIds
901
+ ? Object.keys(normalizedTarget.atUserIds)
902
+ : undefined,
886
903
  imGroupOpenSpaceModel: { supportForward: true },
887
904
  imRobotOpenSpaceModel: { supportForward: true },
888
905
  },
@@ -899,7 +916,7 @@ export function createDingtalkApi({
899
916
  delivered = true;
900
917
  await cardRequest('v1.0/card/instances', {
901
918
  method: 'PUT',
902
- body: { outTrackId: cardInstanceId, cardData: cardData(content, '2') },
919
+ body: { outTrackId: cardInstanceId, cardData: cardData(content, '2', normalizedTarget) },
903
920
  headers,
904
921
  signal,
905
922
  action: 'AI Card 启动',
@@ -910,7 +927,7 @@ export function createDingtalkApi({
910
927
  outTrackId: cardInstanceId,
911
928
  guid: randomUUID(),
912
929
  key: 'msgContent',
913
- content: normalizeDingtalkCardMarkdown(content).replace(/\n+$/, ''),
930
+ content: cardMarkdown(content, normalizedTarget).replace(/\n+$/, ''),
914
931
  isFull: true,
915
932
  isFinalize: false,
916
933
  isError: false,
@@ -927,6 +944,7 @@ export function createDingtalkApi({
927
944
  clientSecret: appSecret,
928
945
  cardInstanceId,
929
946
  text: t('卡片已结束,请查看后续消息。'),
947
+ target: normalizedTarget,
930
948
  signal: cleanupSignal,
931
949
  }).catch(() => undefined);
932
950
  }
@@ -935,7 +953,7 @@ export function createDingtalkApi({
935
953
  return { cardInstanceId };
936
954
  },
937
955
 
938
- async updateAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
956
+ async updateAiCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
939
957
  const instanceId = nonEmptyString(cardInstanceId);
940
958
  const content = nonEmptyString(text);
941
959
  if (!instanceId) throw new TypeError('cardInstanceId is required');
@@ -947,7 +965,7 @@ export function createDingtalkApi({
947
965
  outTrackId: instanceId,
948
966
  guid: randomUUID(),
949
967
  key: 'msgContent',
950
- content: normalizeDingtalkCardMarkdown(content).replace(/\n+$/, ''),
968
+ content: cardMarkdown(content, target).replace(/\n+$/, ''),
951
969
  isFull: true,
952
970
  isFinalize: false,
953
971
  isError: false,
@@ -959,14 +977,14 @@ export function createDingtalkApi({
959
977
  return true;
960
978
  },
961
979
 
962
- async finishAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
980
+ async finishAiCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
963
981
  const instanceId = nonEmptyString(cardInstanceId);
964
982
  const content = nonEmptyString(text);
965
983
  if (!instanceId) throw new TypeError('cardInstanceId is required');
966
984
  if (!content) throw new TypeError('text is required');
967
985
  const token = await accessToken({ clientId, clientSecret, signal });
968
986
  const headers = { 'x-acs-dingtalk-access-token': token };
969
- const normalizedContent = normalizeDingtalkCardMarkdown(content);
987
+ const normalizedContent = cardMarkdown(content, target);
970
988
  await cardRequest('v1.0/card/streaming', {
971
989
  method: 'PUT',
972
990
  body: {
@@ -987,7 +1005,7 @@ export function createDingtalkApi({
987
1005
  method: 'PUT',
988
1006
  body: {
989
1007
  outTrackId: instanceId,
990
- cardData: cardData(content, '3'),
1008
+ cardData: cardData(content, '3', target),
991
1009
  cardUpdateOptions: { updateCardDataByKey: true },
992
1010
  },
993
1011
  headers,
@@ -1008,13 +1026,18 @@ export function createDingtalkApi({
1008
1026
 
1009
1027
  failAiCard: failCard,
1010
1028
 
1011
- async sendText({ clientId, clientSecret, sessionWebhook, text, signal }) {
1029
+ async sendText({ clientId, clientSecret, sessionWebhook, text, at, signal }) {
1012
1030
  const content = nonEmptyString(text);
1013
1031
  if (!content) throw new TypeError('text is required');
1014
1032
  const webhook = normalizeDingtalkSessionWebhook(sessionWebhook);
1015
1033
  const token = await accessToken({ clientId, clientSecret, signal });
1034
+ const replyAt = at && typeof at === 'object'
1035
+ && ((Array.isArray(at.atUserIds) && at.atUserIds.length > 0)
1036
+ || (Array.isArray(at.atMobiles) && at.atMobiles.length > 0)
1037
+ || at.isAtAll === true)
1038
+ ? { at } : {};
1016
1039
  const response = await requestJson(fetchImpl, webhook, {
1017
- body: { msgtype: 'text', text: { content } },
1040
+ body: { msgtype: 'text', text: { content }, ...replyAt },
1018
1041
  headers: { 'x-acs-dingtalk-access-token': token },
1019
1042
  signal,
1020
1043
  action: '消息回复',