@xmanrui/dsh-im 4.18.0 → 4.19.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 (51) hide show
  1. package/README.en.md +8 -3
  2. package/README.md +8 -3
  3. package/lib/client.js +809 -515
  4. package/lib/index.js +269 -267
  5. package/package.json +5 -1
  6. package/plugin-src/client/bot-alias.js +92 -0
  7. package/plugin-src/client/channels/dingtalk/api.js +3 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +7 -1
  9. package/plugin-src/client/channels/feishu/api.js +3 -0
  10. package/plugin-src/client/channels/feishu/index.js +7 -1
  11. package/plugin-src/client/channels/qq/api.js +3 -0
  12. package/plugin-src/client/channels/qq/index.js +9 -1
  13. package/plugin-src/client/channels/shared/token-api.js +3 -0
  14. package/plugin-src/client/channels/shared/token-channel.js +9 -2
  15. package/plugin-src/client/channels/wecom/api.js +3 -0
  16. package/plugin-src/client/channels/wecom/index.js +9 -1
  17. package/plugin-src/client/channels/wecom-app/api.js +3 -0
  18. package/plugin-src/client/channels/wecom-app/index.js +9 -1
  19. package/plugin-src/client/channels/weixin/api.js +3 -0
  20. package/plugin-src/client/channels/weixin/index.js +7 -1
  21. package/plugin-src/client/channels/whatsapp/api.js +3 -0
  22. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  23. package/plugin-src/client/i18n.js +10 -0
  24. package/plugin-src/client/styles.js +26 -0
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  26. package/plugin-src/host/channels/feishu/production.mjs +22 -0
  27. package/plugin-src/host/channels/feishu/rpc.mjs +14 -0
  28. package/plugin-src/host/channels/imessage/rpc.mjs +1 -0
  29. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  30. package/plugin-src/host/channels/shared/bot-alias-rpc.mjs +15 -0
  31. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  32. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  33. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  34. package/plugin-src/host/channels/wecom-app/rpc.mjs +10 -0
  35. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  36. package/plugin-src/host/channels/whatsapp/rpc.mjs +13 -0
  37. package/plugin-src/host/rpc-authority.mjs +3 -2
  38. package/plugin-src/host/session-sync-coordinator.mjs +19 -1
  39. package/scripts/verify-lan-management.mjs +176 -0
  40. package/src/channels/feishu/bridge.mjs +370 -7
  41. package/src/channels/feishu/feishu-cards.mjs +1 -1
  42. package/src/channels/feishu/feishu-runtime.mjs +6 -0
  43. package/src/channels/feishu/state-store.mjs +17 -0
  44. package/src/channels/shared/bot-alias.mjs +29 -0
  45. package/src/channels/shared/bot-workspace-store.mjs +71 -3
  46. package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
  47. package/src/channels/shared/message-failure.mjs +2 -1
  48. package/src/channels/shared/model-command.mjs +3 -2
  49. package/src/channels/shared/session-sync-registry.mjs +34 -0
  50. package/src/channels/shared/text-harness-bridge.mjs +38 -0
  51. package/src/channels/telegram/telegram-runtime.mjs +77 -25
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import QRCode from 'qrcode';
3
4
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
@@ -25,6 +26,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
25
26
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
26
27
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
27
28
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
29
+ setAlias: SET_ALIAS_ENDPOINT,
28
30
  approveSender: 'bot.sender.approve',
29
31
  revokeSender: 'bot.sender.revoke',
30
32
  });
@@ -112,6 +114,10 @@ function payloadFailure(endpoint, payload) {
112
114
  return validAccessPolicyPayload(payload)
113
115
  ? null : '请提交有效的访问设置。';
114
116
  }
117
+ if (endpoint === DINGTALK_ENDPOINTS.setAlias) {
118
+ return validAliasPayload(payload)
119
+ ? null : '请输入有效的别名(最多 80 个字符)。';
120
+ }
115
121
  if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
116
122
  return exactKeys(payload, ['botId', 'requestId', 'confirm'])
117
123
  && validId(payload.botId)
@@ -301,6 +307,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
301
307
  value = await controller.updateContextEnhancement(
302
308
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
303
309
  );
310
+ } else if (endpoint === DINGTALK_ENDPOINTS.setAlias) {
311
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
312
+ value = await controller.updateAlias(
313
+ payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
314
+ );
304
315
  } else if (endpoint === DINGTALK_ENDPOINTS.setAccessPolicy) {
305
316
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
306
317
  value = await controller.updateAccessPolicy(
@@ -162,6 +162,27 @@ export async function createProductionController(ctx, config = {}, internals = {
162
162
  if (!botConfig) throw new Error('Unknown Feishu bot');
163
163
  return stateFor(botConfig);
164
164
  };
165
+ // Session-sync mirror targets: which synced DMs (openId) belong to a
166
+ // Session. Resolved lazily per turn start so rebinding a delivery target
167
+ // takes effect without a restart.
168
+ const sessionSyncTargetsFor = async (sessionId) => {
169
+ if (typeof workspaces.listSessionSyncTargets !== 'function') return [];
170
+ const matches = [];
171
+ for (const entry of workspaces.listSessionSyncTargets()) {
172
+ let state;
173
+ try {
174
+ state = await stateForBotId(entry.botId);
175
+ } catch {
176
+ continue;
177
+ }
178
+ const bound = state?.snapshot?.().sessions?.[entry.conversationKey];
179
+ if (bound !== sessionId) continue;
180
+ const target = workspaces.deliveryTargetFor(entry.botId, entry.targetId);
181
+ const openId = target?.route?.openId;
182
+ if (openId) matches.push({ openId, botId: entry.botId, targetId: entry.targetId });
183
+ }
184
+ return matches;
185
+ };
165
186
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
166
187
  const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
167
188
  const inboundTtlService = inboundTtl?.service ?? inboundTtl;
@@ -220,6 +241,7 @@ export async function createProductionController(ctx, config = {}, internals = {
220
241
  groupTopicReply: botConfig.groupTopicReply,
221
242
  stepPush: botConfig.stepPush,
222
243
  stepPushMode: botConfig.stepPushMode,
244
+ sessionSyncTargetsFor: sessionSyncTargetsFor,
223
245
  ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
224
246
  harness: workspaceScope.harness,
225
247
  state: workspaceScope.state,
@@ -1,3 +1,5 @@
1
+ import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
3
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
4
  import QRCode from 'qrcode';
3
5
  import {
@@ -34,6 +36,7 @@ import {
34
36
  export const FEISHU_ENDPOINTS = Object.freeze({
35
37
  ...FEISHU_CLIENT_ENDPOINTS,
36
38
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
39
+ setAlias: SET_ALIAS_ENDPOINT,
37
40
  });
38
41
  export { FEISHU_RPC_CHANNEL };
39
42
  export const FEISHU_MULTI_ENDPOINTS = Object.freeze({
@@ -214,6 +217,7 @@ function connectionFacts(connection) {
214
217
  function publicBot(bot) {
215
218
  const source = bot && typeof bot === 'object' ? bot : {};
216
219
  const result = {
220
+ ...normalizeBotAlias(source),
217
221
  name: typeof source.name === 'string' && source.name.length > 0 ? source.name : '飞书机器人',
218
222
  };
219
223
  if (typeof source.avatarUrl === 'string') result.avatarUrl = source.avatarUrl;
@@ -451,6 +455,10 @@ function validPayload(endpoint, payload) {
451
455
  return validAccessPolicyPayload(payload)
452
456
  ? null : '请提交有效的访问设置。';
453
457
  }
458
+ if (endpoint === FEISHU_ENDPOINTS.setAlias) {
459
+ return validAliasPayload(payload)
460
+ ? null : '请输入有效的别名(最多 80 个字符)。';
461
+ }
454
462
  if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
455
463
  return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
456
464
  && safeOpaqueId(payload.botId)
@@ -735,6 +743,12 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
735
743
  payload.botId, payload.config,
736
744
  (status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
737
745
  );
746
+ } else if (endpoint === FEISHU_ENDPOINTS.setAlias) {
747
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
748
+ value = await controller.updateAlias(
749
+ payload.botId, payload.alias,
750
+ (status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
751
+ );
738
752
  } else if (endpoint === FEISHU_ENDPOINTS.setAccessPolicy) {
739
753
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
740
754
  value = await controller.updateAccessPolicy(
@@ -15,6 +15,7 @@ export const IMESSAGE_ENDPOINTS = Object.freeze({
15
15
  setAgentPreset: 'bot.agent-preset.set',
16
16
  setContextEnhancement: 'bot.context-enhancement.set',
17
17
  setAccessPolicy: 'bot.access-policy.set',
18
+ setAlias: 'bot.alias.set',
18
19
  });
19
20
  export const IMESSAGE_RPC_ENDPOINTS = Object.freeze(Object.values(IMESSAGE_ENDPOINTS));
20
21
 
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import QRCode from 'qrcode';
3
4
  import {
@@ -25,6 +26,7 @@ export const QQ_ENDPOINTS = Object.freeze({
25
26
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
26
27
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
27
28
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
29
+ setAlias: SET_ALIAS_ENDPOINT,
28
30
  });
29
31
  export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
30
32
 
@@ -94,6 +96,10 @@ function payloadFailure(endpoint, payload) {
94
96
  return validAccessPolicyPayload(payload)
95
97
  ? null : '请提交有效的访问设置。';
96
98
  }
99
+ if (endpoint === QQ_ENDPOINTS.setAlias) {
100
+ return validAliasPayload(payload)
101
+ ? null : '请输入有效的别名(最多 80 个字符)。';
102
+ }
97
103
  return 'Unknown QQ endpoint.';
98
104
  }
99
105
 
@@ -200,6 +206,11 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
200
206
  value = await controller.updateContextEnhancement(
201
207
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
202
208
  );
209
+ } else if (endpoint === QQ_ENDPOINTS.setAlias) {
210
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
211
+ value = await controller.updateAlias(
212
+ payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
213
+ );
203
214
  } else if (endpoint === QQ_ENDPOINTS.setAccessPolicy) {
204
215
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
205
216
  value = await controller.updateAccessPolicy(
@@ -0,0 +1,15 @@
1
+ import { validateBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
+ export { SET_ALIAS_ENDPOINT } from '../../../../src/channels/shared/bot-alias.mjs';
3
+
4
+ export function validAliasPayload(payload) {
5
+ try {
6
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
7
+ || Object.keys(payload).length !== 2
8
+ || typeof payload.botId !== 'string'
9
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
10
+ validateBotAlias(payload.alias);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from './bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
@@ -24,6 +25,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
24
25
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
25
26
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
26
27
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
28
+ setAlias: SET_ALIAS_ENDPOINT,
27
29
  });
28
30
 
29
31
  const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@@ -90,6 +92,10 @@ function payloadFailure(endpoint, payload) {
90
92
  return validAccessPolicyPayload(payload)
91
93
  ? null : '请提交有效的访问设置。';
92
94
  }
95
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setAlias) {
96
+ return validAliasPayload(payload)
97
+ ? null : '请输入有效的别名(最多 80 个字符)。';
98
+ }
93
99
  return 'Unknown bot endpoint.';
94
100
  }
95
101
 
@@ -177,6 +183,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
177
183
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
178
184
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
179
185
  value = await controller.updateContextEnhancement(payload.botId, payload.config);
186
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAlias) {
187
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
188
+ value = await controller.updateAlias(payload.botId, payload.alias);
180
189
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
181
190
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
182
191
  value = await controller.updateAccessPolicy(payload.botId, payload.policy);
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
@@ -25,6 +26,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
25
26
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
26
27
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
27
28
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
29
+ setAlias: SET_ALIAS_ENDPOINT,
28
30
  });
29
31
  export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
30
32
 
@@ -93,6 +95,10 @@ function payloadFailure(endpoint, payload) {
93
95
  return validAccessPolicyPayload(payload)
94
96
  ? null : '请提交有效的访问设置。';
95
97
  }
98
+ if (endpoint === SLACK_ENDPOINTS.setAlias) {
99
+ return validAliasPayload(payload)
100
+ ? null : '请输入有效的别名(最多 80 个字符)。';
101
+ }
96
102
  return 'Unknown Slack endpoint.';
97
103
  }
98
104
 
@@ -180,6 +186,10 @@ export function createSlackRpcHandler(controller) {
180
186
  if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
181
187
  value = await controller.updateContextEnhancement(payload.botId, payload.config);
182
188
  }
189
+ else if (endpoint === SLACK_ENDPOINTS.setAlias) {
190
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
191
+ value = await controller.updateAlias(payload.botId, payload.alias);
192
+ }
183
193
  else if (endpoint === SLACK_ENDPOINTS.setAccessPolicy) {
184
194
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
185
195
  value = await controller.updateAccessPolicy(payload.botId, payload.policy);
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import QRCode from 'qrcode';
3
4
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
@@ -25,6 +26,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
25
26
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
26
27
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
27
28
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
29
+ setAlias: SET_ALIAS_ENDPOINT,
28
30
  });
29
31
  export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
30
32
 
@@ -94,6 +96,10 @@ function payloadFailure(endpoint, payload) {
94
96
  return validAccessPolicyPayload(payload)
95
97
  ? null : '请提交有效的访问设置。';
96
98
  }
99
+ if (endpoint === WECOM_ENDPOINTS.setAlias) {
100
+ return validAliasPayload(payload)
101
+ ? null : '请输入有效的别名(最多 80 个字符)。';
102
+ }
97
103
  return 'Unknown Enterprise WeChat endpoint.';
98
104
  }
99
105
 
@@ -201,6 +207,11 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
201
207
  value = await controller.updateContextEnhancement(
202
208
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
203
209
  );
210
+ } else if (endpoint === WECOM_ENDPOINTS.setAlias) {
211
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
212
+ value = await controller.updateAlias(
213
+ payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
214
+ );
204
215
  } else if (endpoint === WECOM_ENDPOINTS.setAccessPolicy) {
205
216
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
206
217
  value = await controller.updateAccessPolicy(
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
@@ -23,6 +24,7 @@ export const WECOM_APP_ENDPOINTS = Object.freeze({
23
24
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
24
25
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
25
26
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
27
+ setAlias: SET_ALIAS_ENDPOINT,
26
28
  });
27
29
  export const WECOM_APP_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_APP_ENDPOINTS));
28
30
 
@@ -113,6 +115,9 @@ function payloadFailure(endpoint, payload) {
113
115
  if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
114
116
  return validAccessPolicyPayload(payload) ? null : '请提交有效的访问设置。';
115
117
  }
118
+ if (endpoint === WECOM_APP_ENDPOINTS.setAlias) {
119
+ return validAliasPayload(payload) ? null : '请输入有效的别名(最多 80 个字符)。';
120
+ }
116
121
  return 'Unknown Enterprise WeChat app endpoint.';
117
122
  }
118
123
 
@@ -189,6 +194,11 @@ export function createWecomAppRpcHandler(controller) {
189
194
  value = await controller.updateContextEnhancement(
190
195
  payload.botId, payload.config, (status) => publicStatus(status),
191
196
  );
197
+ } else if (endpoint === WECOM_APP_ENDPOINTS.setAlias) {
198
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
199
+ value = await controller.updateAlias(
200
+ payload.botId, payload.alias, (status) => publicStatus(status),
201
+ );
192
202
  } else if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
193
203
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
194
204
  value = await controller.updateAccessPolicy(
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import QRCode from 'qrcode';
3
4
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
@@ -32,6 +33,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
32
33
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
33
34
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
34
35
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
36
+ setAlias: SET_ALIAS_ENDPOINT,
35
37
  });
36
38
  export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
37
39
 
@@ -101,6 +103,10 @@ function payloadFailure(endpoint, payload) {
101
103
  return validAccessPolicyPayload(payload)
102
104
  ? null : '请提交有效的访问设置。';
103
105
  }
106
+ if (endpoint === WEIXIN_ENDPOINTS.setAlias) {
107
+ return validAliasPayload(payload)
108
+ ? null : '请输入有效的别名(最多 80 个字符)。';
109
+ }
104
110
  return 'Unknown Weixin endpoint.';
105
111
  }
106
112
 
@@ -236,6 +242,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
236
242
  value = await controller.updateContextEnhancement(
237
243
  payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
238
244
  );
245
+ } else if (endpoint === WEIXIN_ENDPOINTS.setAlias) {
246
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
247
+ value = await controller.updateAlias(
248
+ payload.botId, payload.alias, (status) => publicStatus(status, cachedEncode),
249
+ );
239
250
  } else if (endpoint === WEIXIN_ENDPOINTS.setAccessPolicy) {
240
251
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
241
252
  value = await controller.updateAccessPolicy(
@@ -1,3 +1,4 @@
1
+ import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import QRCode from 'qrcode';
3
4
 
@@ -18,6 +19,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
18
19
  reconnectBot: 'bot.reconnect',
19
20
  deleteBot: 'bot.delete',
20
21
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
22
+ setAlias: SET_ALIAS_ENDPOINT,
21
23
  setWorkspace: SET_WORKSPACE_ENDPOINT,
22
24
  setModel: SET_MODEL_ENDPOINT,
23
25
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
@@ -61,6 +63,10 @@ function payloadFailure(endpoint, payload) {
61
63
  return validAccessPolicyPayload(payload)
62
64
  ? null : '请提交有效的访问设置。';
63
65
  }
66
+ if (endpoint === WHATSAPP_ENDPOINTS.setAlias) {
67
+ return validAliasPayload(payload)
68
+ ? null : '请输入有效的别名(最多 80 个字符)。';
69
+ }
64
70
  if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
65
71
  return validWorkspacePayload(payload)
66
72
  ? null : '请输入工作区绝对路径。';
@@ -195,6 +201,13 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
195
201
  await controller.updateAgentPreset(payload.botId, payload.agentPreset),
196
202
  cachedEncode,
197
203
  );
204
+ } else if (endpoint === WHATSAPP_ENDPOINTS.setAlias) {
205
+ if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
206
+ value = await controller.updateAlias(
207
+ payload.botId,
208
+ payload.alias,
209
+ (status) => publicStatus(status, cachedEncode),
210
+ );
198
211
  } else if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
199
212
  if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
200
213
  value = await controller.updateAccessPolicy(
@@ -2,10 +2,11 @@ const RPC_AUTHORITIES = new Set(['loopback', 'trusted-host']);
2
2
 
3
3
  /**
4
4
  * Resolve the browser authority accepted by an IM management RPC channel.
5
- * The default keeps credential and bot-management operations on loopback.
5
+ * By default, use the browser authentication and Host/Origin trust checks
6
+ * already enforced by Harness before it dispatches a management Fetch route.
6
7
  */
7
8
  export function resolveRpcAuthority(value) {
8
- if (value === undefined) return 'loopback';
9
+ if (value === undefined) return 'trusted-host';
9
10
  if (RPC_AUTHORITIES.has(value)) return value;
10
11
  throw new TypeError('dsh-im rpcAuthority must be "loopback" or "trusted-host"');
11
12
  }
@@ -3,6 +3,10 @@ import {
3
3
  consumeDshImInputOrigin,
4
4
  textFromHarnessContent,
5
5
  } from '../../src/channels/shared/harness-client.mjs';
6
+ import {
7
+ isSessionSyncMirrored,
8
+ releaseSessionSyncMirror,
9
+ } from '../../src/channels/shared/session-sync-registry.mjs';
6
10
 
7
11
  const DSH_USER_PREFIX = '[来自 DSH]\n';
8
12
  const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
@@ -111,6 +115,10 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
111
115
  if (event.surfaceOp !== 'append') return;
112
116
  if (state.origin === 'unknown') state.origin = origin;
113
117
  if (state.origin !== 'dsh' || origin !== 'dsh') return;
118
+ // The user echo stays as plain text even when the mirror owns the
119
+ // turn: the card also quotes the question, but keeping the echo here
120
+ // guarantees recipients are established so a failed mirror can still
121
+ // fall back to the final-answer text delivery.
114
122
  const text = textFromHarnessContent(event.data?.content);
115
123
  if (!text) return;
116
124
 
@@ -151,9 +159,19 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
151
159
  turns.delete(sessionId);
152
160
  if (state.origin !== 'dsh' || !completedTurn(event.data?.reason)
153
161
  || !state.recipients?.size || !state.assistant.text) return;
162
+ // Per-target suppression: targets whose process-card mirror delivered
163
+ // the answer are skipped; every other synced target still gets the text.
164
+ const mirrored = [];
165
+ const plain = [];
166
+ for (const target of state.recipients.values()) {
167
+ if (isSessionSyncMirrored(sessionId, target.targetId, state.turn)) mirrored.push(target);
168
+ else plain.push(target);
169
+ }
170
+ for (const target of mirrored) releaseSessionSyncMirror(sessionId, target.targetId);
171
+ if (plain.length === 0) return;
154
172
  await deliver(
155
173
  sessionId,
156
- state.recipients.values(),
174
+ plain,
157
175
  `${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
158
176
  'assistant delivery',
159
177
  );
@@ -0,0 +1,176 @@
1
+ /** Run the built plugin through an original DSH CLI with an isolated, empty home. */
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { access, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
5
+ import { request as httpRequest } from 'node:http';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, join, resolve } from 'node:path';
8
+
9
+ const harnessRoot = process.argv[2];
10
+ if (!harnessRoot || process.argv.includes('--help')) {
11
+ console.log('Usage: node scripts/verify-lan-management.mjs /path/to/built/deepseek-harness');
12
+ process.exit(harnessRoot ? 0 : 1);
13
+ }
14
+ const pluginRoot = resolve(import.meta.dirname, '..');
15
+ const cli = resolve(harnessRoot, 'apps/cli/lib/bin.js');
16
+ await access(cli);
17
+ await access(join(pluginRoot, 'lib/index.js'));
18
+ // DSH 0.1.5 CLI intentionally permits only loopback listening. Exercise its
19
+ // real HTTP carrier with a trusted LAN authority, while keeping TCP local.
20
+ const lanIp = '192.168.1.100';
21
+
22
+ const directory = await mkdtemp(join(tmpdir(), 'dsh-im-lan-test-'));
23
+ const home = join(directory, 'home');
24
+ const profile = join(home, 'profiles/web');
25
+ const results = [];
26
+ let child;
27
+
28
+ function request(url, { method = 'GET', headers = {}, body } = {}) {
29
+ return new Promise((resolveRequest, reject) => {
30
+ const req = httpRequest({
31
+ hostname: '127.0.0.1', port: url.port, path: `${url.pathname}${url.search}`,
32
+ method, headers: { host: url.host, ...headers },
33
+ }, res => {
34
+ let text = '';
35
+ res.setEncoding('utf8');
36
+ res.on('data', chunk => { text += chunk; });
37
+ res.on('error', reject);
38
+ res.on('end', () => resolveRequest({ status: res.statusCode, headers: res.headers, body: text }));
39
+ });
40
+ req.on('error', reject);
41
+ req.setTimeout(10_000, () => req.destroy(new Error('HTTP request timed out')));
42
+ req.end(body);
43
+ });
44
+ }
45
+
46
+ function start() {
47
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => (
48
+ !/^DSH_/i.test(key) && !/(?:KEY|SECRET|TOKEN|PASSWORD|PROXY)/i.test(key)
49
+ )));
50
+ child = spawn(process.execPath, [cli, 'web', '--no-open', '--host', '127.0.0.1',
51
+ '--port', '0', '--trusted-host', lanIp], {
52
+ cwd: directory,
53
+ env: { ...env, DSH_HOME: home, DSH_AGENTS_HOME: join(directory, '.agents'),
54
+ DSH_TELEMETRY_DISABLED: '1', SSH_CONNECTION: '', SSH_TTY: '' },
55
+ stdio: ['ignore', 'pipe', 'pipe'],
56
+ });
57
+ return new Promise((resolveStart, reject) => {
58
+ let output = '';
59
+ const timer = setTimeout(() => finish(new Error('DSH startup timed out')), 45_000);
60
+ let settled = false;
61
+ function finish(error, url) {
62
+ if (settled) return;
63
+ settled = true;
64
+ clearTimeout(timer);
65
+ if (error) {
66
+ const safeOutput = output.replace(/([?&]token=)[^\s)]+/g, '$1<redacted>');
67
+ reject(new Error(`${error.message}\n${safeOutput}`));
68
+ } else resolveStart(new URL(url));
69
+ }
70
+ function append(chunk) {
71
+ output = `${output}${chunk}`.slice(-100_000);
72
+ const match = /dsh web: (http:\/\/[^\s]+)/.exec(output);
73
+ if (match) finish(null, match[1]);
74
+ }
75
+ child.stdout.on('data', append);
76
+ child.stderr.on('data', append);
77
+ child.once('error', error => finish(error));
78
+ child.once('exit', code => finish(new Error(`DSH exited before readiness: ${code}`)));
79
+ });
80
+ }
81
+
82
+ async function stop() {
83
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
84
+ const stopped = new Promise(resolveStop => child.once('exit', resolveStop));
85
+ child.kill('SIGTERM');
86
+ const timer = setTimeout(() => child.kill('SIGKILL'), 10_000);
87
+ try { await stopped; } finally { clearTimeout(timer); }
88
+ }
89
+
90
+ async function login(launchUrl, hostname) {
91
+ const url = new URL(launchUrl);
92
+ url.hostname = hostname;
93
+ const response = await request(url);
94
+ assert.equal(response.status, 303, 'Harness must exchange its launch token for a browser cookie');
95
+ const cookie = response.headers['set-cookie']?.map(value => value.split(';', 1)[0]).join('; ');
96
+ assert.ok(cookie, 'Harness did not issue a browser cookie');
97
+ return { origin: url.origin, cookie };
98
+ }
99
+
100
+ function rpc(browser, channel = 'feishu', method = 'connection.status', headers = {}, payload = {}) {
101
+ return request(new URL(`/api/dsh-im/${channel}`, browser.origin), {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'application/json', origin: browser.origin,
104
+ ...(browser.cookie ? { cookie: browser.cookie } : {}), ...headers },
105
+ body: JSON.stringify({ type: 'client-request', rpcId: 'lan-test',
106
+ method: `dsh-im/${channel}`, payload: { method, payload } }),
107
+ });
108
+ }
109
+
110
+ function expectStatus(name, response, expected, businessOk = false) {
111
+ assert.equal(response.status, expected, `${name}: ${response.body.slice(0, 300)}`);
112
+ if (businessOk) {
113
+ const envelope = JSON.parse(response.body);
114
+ assert.equal(envelope.type, 'server-response', name);
115
+ assert.equal(envelope.rpcId, 'lan-test', name);
116
+ assert.equal(envelope.result.ok, true, `${name}: ${response.body.slice(0, 300)}`);
117
+ }
118
+ results.push({ check: name, status: response.status, result: 'PASS' });
119
+ }
120
+
121
+ try {
122
+ await mkdir(profile, { recursive: true });
123
+ const packages = {
124
+ '@deepseek-ai/dsh-base': resolve(harnessRoot, 'packages/bundle/base'),
125
+ '@deepseek-ai/dsh-web-app': resolve(harnessRoot, 'packages/bundle/web-app'),
126
+ '@xmanrui/dsh-im': pluginRoot,
127
+ };
128
+ for (const [name, path] of Object.entries(packages)) {
129
+ const target = join(profile, 'node_modules', name);
130
+ await mkdir(dirname(target), { recursive: true });
131
+ await symlink(path, target, 'dir');
132
+ }
133
+ await writeFile(join(profile, 'cordis.yml'), '[]\n');
134
+ await writeFile(join(profile, 'package.json'), JSON.stringify({
135
+ name: 'dsh-im-lan-test', private: true,
136
+ dependencies: Object.fromEntries(Object.entries(packages).map(([name, path]) => [name, `link:${path}`])),
137
+ dsh: { profile: { bundles: Object.keys(packages) } },
138
+ }));
139
+
140
+ const launchUrl = await start();
141
+ const lanUrl = new URL(launchUrl);
142
+ lanUrl.hostname = lanIp;
143
+ const anonymous = { origin: lanUrl.origin };
144
+ expectStatus('LAN without login', await rpc(anonymous), 401);
145
+ const lan = await login(launchUrl, lanIp);
146
+ expectStatus('LAN authenticated web page', await request(new URL('/', lan.origin), {
147
+ headers: { cookie: lan.cookie },
148
+ }), 200);
149
+ for (const channel of ['feishu', 'weixin', 'dingtalk', 'wecom', 'wecom-app', 'qq',
150
+ 'slack', 'telegram', 'discord', 'whatsapp', 'imessage', 'office']) {
151
+ expectStatus(`LAN default: ${channel}`, await rpc(lan, channel), 200, true);
152
+ }
153
+ const delivery = await rpc(lan, 'dsh-im-delivery', 'target.list', {}, { botId: 'bot_missing' });
154
+ expectStatus('LAN delivery reaches business handler', delivery, 200);
155
+ assert.equal(JSON.parse(delivery.body).result.error.code, 'unknown-bot');
156
+ expectStatus('LAN forged cookie', await rpc({ ...lan, cookie: 'invalid=invalid' }), 401);
157
+ expectStatus('Untrusted Host', await rpc(lan, 'feishu', 'connection.status', { host: 'untrusted.invalid' }), 403);
158
+ expectStatus('Cross-origin request', await rpc(lan, 'feishu', 'connection.status', { origin: 'https://untrusted.invalid' }), 403);
159
+ expectStatus('LAN update remains local-only', await rpc(lan, 'dsh-im', 'update.status'), 403);
160
+ expectStatus('LAN TTL remains local-only', await rpc(lan, 'dsh-im-settings', 'settings.inbound-ttl.get'), 403);
161
+ const local = await login(launchUrl, '127.0.0.1');
162
+ expectStatus('Loopback default: feishu', await rpc(local), 200, true);
163
+
164
+ await stop();
165
+ await writeFile(join(profile, 'cordis.patch.yml'), '- id: xmanrui-dsh-im\n config:\n rpcAuthority: loopback\n');
166
+ const restrictedUrl = await start();
167
+ expectStatus('Explicit loopback rejects LAN', await rpc(await login(restrictedUrl, lanIp)), 403);
168
+ expectStatus('Explicit loopback accepts localhost', await rpc(await login(restrictedUrl, '127.0.0.1')), 200, true);
169
+ console.table(results);
170
+ console.log(`Passed ${results.length} real HTTP checks using LAN Host/Origin ${lanIp}.`);
171
+ console.log('TCP connections stayed on loopback. This checks the original CLI, authentication and built plugin, not a second-device browser.');
172
+ console.log('The temporary profile contains no bot credentials and is removed after the server stops.');
173
+ } finally {
174
+ await stop();
175
+ await rm(directory, { recursive: true, force: true });
176
+ }