@xmanrui/dsh-im 4.18.1 → 4.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.en.md +1 -0
  2. package/README.md +1 -0
  3. package/lib/client.js +917 -521
  4. package/lib/index.js +269 -267
  5. package/package.json +1 -1
  6. package/plugin-src/client/bot-alias.js +169 -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 +41 -6
  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/session-sync-coordinator.mjs +20 -1
  38. package/src/channels/feishu/bridge.mjs +330 -10
  39. package/src/channels/feishu/feishu-cards.mjs +1 -1
  40. package/src/channels/feishu/feishu-runtime.mjs +6 -0
  41. package/src/channels/feishu/state-store.mjs +17 -0
  42. package/src/channels/shared/bot-alias.mjs +29 -0
  43. package/src/channels/shared/bot-workspace-store.mjs +71 -3
  44. package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
  45. package/src/channels/shared/message-failure.mjs +2 -1
  46. package/src/channels/shared/model-command.mjs +3 -2
  47. package/src/channels/shared/session-sync-registry.mjs +22 -0
@@ -4,6 +4,16 @@ export const IM_LOCALE_NAMESPACE = 'dsh-im';
4
4
 
5
5
  const EN = Object.freeze({
6
6
  '$locale': 'en',
7
+ '修改别名': 'Edit alias',
8
+ '关闭修改别名': 'Close alias editor',
9
+ '原名称': 'Original name',
10
+ '别名': 'Alias',
11
+ '恢复原名称': 'Restore original name',
12
+ '例如:客服助手': 'e.g. Customer support',
13
+ '仅更改显示名称,留空则显示原名称。': 'Only changes the display name. Leave blank to use the original name.',
14
+ '别名保存失败,请重试。': 'Could not save the alias. Try again.',
15
+ '别名不能包含换行或控制字符,且最多 80 个字符。': 'Use at most 80 characters without line breaks or control characters.',
16
+ '请输入有效的别名(最多 80 个字符)。': 'Enter a valid alias (up to 80 characters).',
7
17
  ' macOS Messages 连接': ' macOS Messages connection',
8
18
  '接入 iMessage': 'Connect iMessage',
9
19
  '先在 macOS 系统设置中授予 Messages 权限。': 'Grant Messages permissions in macOS System Settings first.',
@@ -1,6 +1,38 @@
1
1
  export const IM_STYLE_ID = 'xmanrui-dsh-im-settings';
2
2
 
3
3
  const CSS = String.raw`
4
+ .dim-aliasName { display: flex; align-items: center; gap: 4px; min-width: 0; }
5
+ .dim-aliasName h3 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6
+ .dim-aliasName h3:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 3px; }
7
+ .dim-botNameTooltip { position: fixed; z-index: 1000; box-sizing: border-box; width: max-content; max-width: min(320px, calc(100vw - 16px)); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font: 500 12px/18px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; white-space: normal; overflow-wrap: anywhere; pointer-events: none; animation: dim-botNameTooltip-in .15s ease; }
8
+ @keyframes dim-botNameTooltip-in { from { opacity: 0; } to { opacity: 1; } }
9
+ @media (prefers-reduced-motion: reduce) { .dim-botNameTooltip { animation: none; } }
10
+ .dim-aliasEntry { display: inline-flex; flex: none; }
11
+ .dim-aliasEdit { display: grid; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; border-radius: 5px; color: var(--dsw-alias-label-tertiary, #8f959e); background: transparent; cursor: pointer; }
12
+ .dim-aliasEdit svg { opacity: .55; transition: opacity .15s ease; }
13
+ .dim-aliasName:hover .dim-aliasEdit:not(:disabled) svg, .dim-aliasEdit:focus-visible svg { opacity: 1; }
14
+ .dim-aliasEdit:hover:not(:disabled), .dim-aliasEdit:focus-visible { color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
15
+ .dim-aliasDialog { box-sizing: border-box; width: min(380px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; padding: 22px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 12px 36px rgb(0 0 0 / 18%); font: 13px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
16
+ .dim-aliasDialog * { box-sizing: border-box; }
17
+ .dim-aliasDialog::backdrop { background: rgb(15 17 21 / 30%); }
18
+ .dim-aliasHeader { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
19
+ .dim-aliasHeader h3 { margin: 0; font-size: 16px; }
20
+ .dim-aliasDialog button { font: inherit; cursor: pointer; }
21
+ .dim-aliasDialog .dim-aliasClose { width: 28px; height: 28px; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--dsw-alias-label-secondary, #646a73); font-size: 20px; }
22
+ .dim-aliasOriginal { display: flex; flex-wrap: wrap; gap: 6px 14px; padding: 10px 12px; margin-bottom: 18px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #f5f6f7); overflow-wrap: anywhere; }
23
+ .dim-aliasOriginal > span:first-child { flex: none; color: var(--dsw-alias-label-secondary, #646a73); }
24
+ .dim-aliasDialog label { display: block; margin-bottom: 7px; }
25
+ .dim-aliasDialog input { width: 100%; min-height: 38px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 6px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; }
26
+ .dim-aliasHelp { margin: 8px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; }
27
+ .dim-aliasError { color: var(--dsw-alias-state-danger-primary, #c53030); overflow-wrap: anywhere; }
28
+ .dim-aliasFooter { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; margin-top: 24px; }
29
+ .dim-aliasRestore { padding: 4px 0; border: 0; color: var(--dsw-alias-state-business-primary, #3370ff); background: transparent; }
30
+ .dim-aliasActions { display: flex; gap: 8px; margin-left: auto; }
31
+ .dim-aliasActions button { padding: 7px 14px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 6px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); }
32
+ .dim-aliasActions .dim-aliasSave { color: #fff; border-color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-state-business-primary, #3370ff); }
33
+ .dim-aliasEdit:disabled, .dim-aliasDialog button:disabled, .dim-aliasDialog input:disabled { opacity: .55; cursor: not-allowed; }
34
+ .dim-aliasEdit:focus-visible, .dim-aliasDialog button:focus-visible, .dim-aliasDialog input:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
35
+ @media (pointer: coarse) { .dim-aliasEdit, .dim-aliasDialog button { min-width: 44px; min-height: 44px; } .dim-aliasDialog input { font-size: 16px; } }
4
36
  .dim-page {
5
37
  --dim-blue: var(--dsw-alias-state-business-primary, #3370ff);
6
38
  --dim-blue-soft: color-mix(in srgb, var(--dim-blue) 9%, transparent);
@@ -375,9 +407,9 @@ const CSS = String.raw`
375
407
  /* Header tooltips may extend beyond the card; the collapsible body clips its own content. */
376
408
  .dim-panel .dim-botCard { position: relative; min-width: 0; width: 100%; max-width: 100%; overflow: visible; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
377
409
  .dim-panel .dim-botCard::before { display: none; }
378
- .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px; }
410
+ .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px 8px; }
379
411
  .dim-collapsibleAccount { min-width: 0; display: flex; flex-direction: column; }
380
- .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; -webkit-user-select: none; }
412
+ .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; -webkit-user-select: none; }
381
413
  .dim-collapsibleHead:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 8px; }
382
414
  .dim-collapsibleHeaderContent { min-width: 0; flex: 1 1 auto; display: flex; align-items: center; }
383
415
  .dim-collapsibleChevron { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 9px; height: 9px; border-right: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); border-bottom: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); transform: rotate(-45deg); transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform-origin: 50% 50%; }
@@ -387,14 +419,17 @@ const CSS = String.raw`
387
419
  .dim-collapsibleAccount.is-open > .dim-collapsibleBody { grid-template-rows: 1fr; }
388
420
  .dim-collapsibleBodyInner { min-height: 0; overflow: hidden; }
389
421
  .dim-collapsibleAccount:not(.is-open) .dim-collapsibleBodyInner { visibility: hidden; }
390
- .dim-panel .dim-botCardTop { min-width: 0; max-width: 100%; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
391
- .dim-panel .dim-botIdentity { min-width: 0; flex: 1 1 0; display: flex; align-items: center; gap: 10px; }
422
+ /* Reclaim horizontal spacing for names while keeping status on the same row,
423
+ including when a channel's mobile stylesheet requests a column layout. */
424
+ .dim-panel .dim-botCardTop { min-width: 0; width: 100%; max-width: 100%; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: flex-start; justify-content: space-between; gap: 6px; }
425
+ .dim-panel .dim-botIdentity { min-width: 0; flex: 1 1 0; display: flex; align-items: center; gap: 6px; }
392
426
  .dim-panel .dim-botAvatar { flex: none; width: 38px; height: 38px; display: grid; place-items: center; overflow: hidden; border-radius: 11px; box-shadow: none; }
393
427
  .dim-panel .dim-botAvatar svg { width: 27px; height: 27px; }
394
- .dim-panel .dim-botName { min-width: 0; }
428
+ .dim-panel .dim-botName { min-width: 0; flex: 1; }
429
+ .dim-panel .dim-aliasName { gap: 2px; }
395
430
  .dim-panel .dim-botName h3 { overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; font-weight: 650; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
396
431
  .dim-panel .dim-botName p { overflow: hidden; margin: 4px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
397
- .dim-panel .dim-botCardTools { flex: none; display: flex; align-items: flex-start; gap: 8px; }
432
+ .dim-panel .dim-botCardTools { flex: none; display: flex; align-items: flex-start; gap: 4px; }
398
433
  .dim-panel .dim-botHealthGroup { min-width: 0; max-width: 100%; flex: none; display: grid; justify-items: end; gap: 5px; }
399
434
  .dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
400
435
  .dim-panel .dim-lastChecked { display: inline-flex; align-items: baseline; gap: 4px; color: var(--dsw-alias-label-tertiary, #8f959e); font: inherit; font-size: 11px; font-weight: 400; line-height: normal; white-space: nowrap; }
@@ -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(
@@ -3,6 +3,7 @@ import {
3
3
  consumeDshImInputOrigin,
4
4
  textFromHarnessContent,
5
5
  } from '../../src/channels/shared/harness-client.mjs';
6
+ import { deliverSessionSyncMirror } from '../../src/channels/shared/session-sync-registry.mjs';
6
7
 
7
8
  const DSH_USER_PREFIX = '[来自 DSH]\n';
8
9
  const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
@@ -111,6 +112,10 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
111
112
  if (event.surfaceOp !== 'append') return;
112
113
  if (state.origin === 'unknown') state.origin = origin;
113
114
  if (state.origin !== 'dsh' || origin !== 'dsh') return;
115
+ // The user echo stays as plain text even when the mirror owns the
116
+ // turn: the card also quotes the question, but keeping the echo here
117
+ // guarantees recipients are established so a failed mirror can still
118
+ // fall back to the final-answer text delivery.
114
119
  const text = textFromHarnessContent(event.data?.content);
115
120
  if (!text) return;
116
121
 
@@ -151,9 +156,23 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
151
156
  turns.delete(sessionId);
152
157
  if (state.origin !== 'dsh' || !completedTurn(event.data?.reason)
153
158
  || !state.recipients?.size || !state.assistant.text) return;
159
+ // A pending card is not proof of delivery. Only suppress this target's
160
+ // text after its renderer confirms the complete final answer is visible.
161
+ // Targets without a renderer retain the original text delivery path.
162
+ const recipients = [...state.recipients.values()];
163
+ const rendered = await Promise.all(recipients.map(async (target) => {
164
+ try {
165
+ return await deliverSessionSyncMirror(target, sessionId, state.turn, state.assistant.text);
166
+ } catch (error) {
167
+ logFailure('card delivery', target, error);
168
+ return false;
169
+ }
170
+ }));
171
+ const plain = recipients.filter((_target, index) => !rendered[index]);
172
+ if (plain.length === 0) return;
154
173
  await deliver(
155
174
  sessionId,
156
- state.recipients.values(),
175
+ plain,
157
176
  `${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
158
177
  'assistant delivery',
159
178
  );