@xmanrui/dsh-im 0.17.1 → 0.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 (56) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +1297 -600
  4. package/lib/index.js +190 -145
  5. package/package.json +1 -1
  6. package/plugin-src/client/agent-preset.js +110 -0
  7. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +48 -2
  9. package/plugin-src/client/channels/feishu/api.js +28 -5
  10. package/plugin-src/client/channels/feishu/index.js +330 -41
  11. package/plugin-src/client/channels/feishu/styles.js +46 -0
  12. package/plugin-src/client/channels/qq/api.js +5 -0
  13. package/plugin-src/client/channels/qq/index.js +37 -6
  14. package/plugin-src/client/channels/shared/token-api.js +5 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +34 -6
  16. package/plugin-src/client/channels/wecom/api.js +5 -0
  17. package/plugin-src/client/channels/wecom/index.js +37 -6
  18. package/plugin-src/client/channels/weixin/api.js +5 -0
  19. package/plugin-src/client/channels/weixin/index.js +50 -4
  20. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +30 -4
  22. package/plugin-src/client/i18n.js +59 -0
  23. package/plugin-src/client/styles.js +8 -0
  24. package/plugin-src/host/channels/dingtalk/production.mjs +10 -4
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +12 -0
  26. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +1 -1
  27. package/plugin-src/host/channels/feishu/production.mjs +8 -3
  28. package/plugin-src/host/channels/feishu/rpc.mjs +78 -7
  29. package/plugin-src/host/channels/qq/production.mjs +10 -4
  30. package/plugin-src/host/channels/qq/rpc.mjs +12 -0
  31. package/plugin-src/host/channels/shared/agent-preset-rpc.mjs +25 -0
  32. package/plugin-src/host/channels/shared/production.mjs +10 -4
  33. package/plugin-src/host/channels/shared/rpc.mjs +12 -0
  34. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  35. package/plugin-src/host/channels/slack/production.mjs +10 -4
  36. package/plugin-src/host/channels/slack/rpc.mjs +13 -0
  37. package/plugin-src/host/channels/wecom/production.mjs +10 -4
  38. package/plugin-src/host/channels/wecom/rpc.mjs +12 -0
  39. package/plugin-src/host/channels/weixin/production.mjs +10 -4
  40. package/plugin-src/host/channels/weixin/rpc.mjs +15 -0
  41. package/plugin-src/host/channels/whatsapp/production.mjs +10 -4
  42. package/plugin-src/host/channels/whatsapp/rpc.mjs +12 -0
  43. package/src/channels/discord/discord-api.mjs +1 -1
  44. package/src/channels/feishu/bridge.mjs +29 -4
  45. package/src/channels/feishu/feishu-runtime.mjs +14 -0
  46. package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
  47. package/src/channels/feishu/group-response-mode.mjs +15 -0
  48. package/src/channels/feishu/multi-bot-controller.mjs +258 -9
  49. package/src/channels/feishu/plugin-config-store.mjs +3 -0
  50. package/src/channels/feishu/repair-manager.mjs +17 -3
  51. package/src/channels/shared/agent-preset.mjs +74 -0
  52. package/src/channels/shared/bot-workspace-store.mjs +141 -14
  53. package/src/channels/shared/harness-client.mjs +6 -4
  54. package/src/channels/telegram/telegram-api.mjs +31 -0
  55. package/src/channels/telegram/telegram-runtime.mjs +25 -1
  56. package/src/channels/weixin/weixin-api.mjs +1 -1
@@ -407,6 +407,50 @@ const CSS = String.raw`
407
407
  .bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
408
408
  .bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
409
409
 
410
+ .bxf-responseMode { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 5px; margin-top: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
411
+ .bxf-responseModeHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
412
+ .bxf-responseModeHeader > span:first-child { grid-column: 1; grid-row: 1; white-space: nowrap; }
413
+ .bxf-responseModeStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
414
+ .bxf-responseModeSelect { min-width: 0; width: 100%; grid-column: 1 / -1; grid-row: 2; height: 32px; padding: 0 30px 0 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background-color: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease, background-color .16s ease; }
415
+ .bxf-responseModeSelect:hover:not(:disabled) { border-color: color-mix(in srgb, var(--bxf-accent) 45%, var(--dsw-alias-border-l2, #dfe1e5)); }
416
+ .bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
417
+ .bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
418
+ .bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
419
+ .bxf-responseModePermissionAction { grid-column: 1 / -1; grid-row: 4; display: flex; justify-content: flex-start; margin-top: 2px; }
420
+ .bxf-responseModePermissionButton { min-height: 28px; padding: 3px 9px; color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 30%, var(--dsw-alias-border-l2, #dfe1e5)); background: var(--dsw-alias-bg-layer-1, #fff); }
421
+ .bxf-responseModePermissionButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
422
+ .bxf-responseModeError { grid-column: 1 / -1; grid-row: 5; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
423
+
424
+ .bxf-botProvision {
425
+ position: relative;
426
+ margin-top: 14px;
427
+ scroll-margin-block: 20px;
428
+ animation: bxf-revealProvision .2s var(--ds-ease-out, ease-out) both;
429
+ }
430
+ .bxf-botProvision:focus { outline: none; }
431
+ .bxf-botProvision:focus-visible {
432
+ outline: 2px solid color-mix(in srgb, var(--bxf-accent) 75%, transparent);
433
+ outline-offset: 3px;
434
+ border-radius: 12px;
435
+ }
436
+ .bxf-botProvision > .bxf-provisionCard {
437
+ border-radius: 11px;
438
+ box-shadow: none;
439
+ background: color-mix(in srgb, var(--bxf-accent) 2.5%, var(--dsw-alias-bg-layer-3, #fff));
440
+ }
441
+ .bxf-botProvision .bxf-cardBody { padding: 18px; }
442
+ .bxf-botProvision .bxf-qrLayout {
443
+ grid-template-columns: 184px minmax(0, 1fr);
444
+ align-items: start;
445
+ gap: 24px;
446
+ }
447
+ .bxf-botProvision .bxf-qrFrame { width: 176px; height: 176px; padding: 10px; border-radius: 11px; }
448
+ .bxf-botProvision .bxf-countdown { width: 176px; }
449
+ .bxf-botProvision .bxf-qrCopy h3 { font-size: 18px; line-height: 26px; }
450
+ .bxf-botProvision .bxf-steps { gap: 8px; margin-top: 14px; }
451
+ .bxf-botProvision .bxf-actions { margin-top: 16px; }
452
+ .bxf-botProvision .bxf-inlineError { min-height: 160px; padding: 22px; }
453
+
410
454
  .bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
411
455
  .bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
412
456
  .bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
@@ -459,6 +503,7 @@ const CSS = String.raw`
459
503
  @keyframes bxf-rotate { to { transform: rotate(360deg); } }
460
504
  @keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
461
505
  @keyframes bxf-shimmer { to { background-position: -220% 0; } }
506
+ @keyframes bxf-revealProvision { from { opacity: 0; transform: translateY(-5px); } }
462
507
 
463
508
  @container (max-width: 620px) {
464
509
  .bxf-headingTools { gap: 6px; }
@@ -470,6 +515,7 @@ const CSS = String.raw`
470
515
  .bxf-intro { grid-template-columns: minmax(0, 1fr); }
471
516
  .bxf-markStage { display: none; }
472
517
  .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
518
+ .bxf-botProvision .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); }
473
519
  .bxf-qrCopy { width: 100%; }
474
520
  .bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
475
521
  .bxf-connectedTop { align-items: flex-start; flex-direction: column; }
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  export const QQ_RPC_CHANNEL = '/qq';
2
4
 
3
5
  export const QQ_ENDPOINTS = Object.freeze({
@@ -9,6 +11,7 @@ export const QQ_ENDPOINTS = Object.freeze({
9
11
  reconnectBot: 'bot.reconnect',
10
12
  deleteBot: 'bot.delete',
11
13
  setWorkspace: 'bot.workspace.set',
14
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
12
15
  });
13
16
 
14
17
  const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
@@ -82,6 +85,7 @@ function normalizeBot(value) {
82
85
  connected,
83
86
  state: connected ? 'connected' : state,
84
87
  workspace: text(value.workspace, '', 4_096),
88
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
85
89
  bot: {
86
90
  name: text(value.bot?.name, 'QQ机器人', 100),
87
91
  appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
@@ -117,6 +121,7 @@ export function normalizeSnapshot(value) {
117
121
  bots,
118
122
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
119
123
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
124
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
120
125
  ...(testMessage ? { testMessage } : {}),
121
126
  };
122
127
  }
@@ -4,6 +4,11 @@ import { QqLogoGlyph } from '../../channel-logos.js';
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
5
5
  import { h } from '../../i18n.js';
6
6
  import { WorkspaceEditor } from '../../workspace-editor.js';
7
+ import {
8
+ AgentPresetCatalogContext,
9
+ AgentPresetEditor,
10
+ EMPTY_AGENT_PRESET_CATALOG,
11
+ } from '../../agent-preset.js';
7
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
8
13
  import { installDingtalkStyles } from '../dingtalk/styles.js';
9
14
  import {
@@ -156,6 +161,7 @@ export function AccountCard({
156
161
  removing,
157
162
  onReconnect,
158
163
  onWorkspaceSave,
164
+ onAgentPresetSave,
159
165
  onRequestRemove,
160
166
  onConfirmRemove,
161
167
  onCancelRemove,
@@ -180,6 +186,11 @@ export function AccountCard({
180
186
  disabled: Boolean(busy),
181
187
  onSave: onWorkspaceSave,
182
188
  }),
189
+ h(AgentPresetEditor, {
190
+ agentPreset: account.agentPreset,
191
+ disabled: Boolean(busy),
192
+ onSave: onAgentPresetSave,
193
+ }),
183
194
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
184
195
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
185
196
  feedback ? h('div', {
@@ -196,7 +207,10 @@ export function AccountCard({
196
207
  }
197
208
 
198
209
  export function QqSettingsTab({ rpcCall }) {
199
- const [model, setModel] = React.useState({ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null });
210
+ const [model, setModel] = React.useState({
211
+ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
212
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
213
+ });
200
214
  const [provision, setProvision] = React.useState(null);
201
215
  const [busy, setBusy] = React.useState(false);
202
216
  const [busyByBot, setBusyByBot] = React.useState({});
@@ -233,7 +247,10 @@ export function QqSettingsTab({ rpcCall }) {
233
247
  const snapshot = normalizeSnapshot(await invoke(QQ_ENDPOINTS.status, {}, signal));
234
248
  if (!mounted.current || signal?.aborted
235
249
  || !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
236
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
250
+ setModel({
251
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
252
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
253
+ });
237
254
  if (restore && snapshot.provisioning) setProvision({
238
255
  ...snapshot.provisioning,
239
256
  durationMs: Math.max(1, snapshot.provisioning.expiresAt - Date.now()),
@@ -297,7 +314,10 @@ export function QqSettingsTab({ rpcCall }) {
297
314
  ));
298
315
  if (!mounted.current) return;
299
316
  if (workspaceFence.canCommitMutation(snapshotVersion)) {
300
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
317
+ setModel({
318
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
319
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
320
+ });
301
321
  }
302
322
  setCredentialOpen(false);
303
323
  } catch (error) {
@@ -357,7 +377,10 @@ export function QqSettingsTab({ rpcCall }) {
357
377
  try {
358
378
  const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
359
379
  if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
360
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
380
+ setModel({
381
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
382
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
383
+ });
361
384
  }
362
385
  return snapshot;
363
386
  } finally {
@@ -424,6 +447,12 @@ export function QqSettingsTab({ rpcCall }) {
424
447
  QQ_ENDPOINTS.setWorkspace,
425
448
  { botId: account.botId, workspace },
426
449
  ),
450
+ onAgentPresetSave: (agentPreset) => botAction(
451
+ account,
452
+ 'preset',
453
+ QQ_ENDPOINTS.setAgentPreset,
454
+ { botId: account.botId, agentPreset },
455
+ ),
427
456
  onRequestRemove: () => setRemoveTarget(account.botId),
428
457
  onCancelRemove: () => setRemoveTarget(null),
429
458
  onConfirmRemove: async () => {
@@ -447,7 +476,9 @@ export function QqSettingsTab({ rpcCall }) {
447
476
  })
448
477
  : null;
449
478
 
450
- return h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
479
+ return h(AgentPresetCatalogContext.Provider, {
480
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
481
+ }, h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
451
482
  h(Heading, {
452
483
  totals: model.totals,
453
484
  adding: Boolean(provision),
@@ -465,7 +496,7 @@ export function QqSettingsTab({ rpcCall }) {
465
496
  provisionView,
466
497
  model.bots.length === 0 && !provision && !credentialOpen
467
498
  ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
468
- botList));
499
+ botList)));
469
500
  }
470
501
 
471
502
  export function apply(ctx) {
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
2
4
 
3
5
  function isRecord(value) {
@@ -25,6 +27,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
25
27
  reconnectBot: 'bot.reconnect',
26
28
  deleteBot: 'bot.delete',
27
29
  setWorkspace: 'bot.workspace.set',
30
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
28
31
  });
29
32
 
30
33
  export function createTokenChannelApi(channel, connectionSummary, {
@@ -52,6 +55,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
52
55
  connected,
53
56
  state: connected ? 'connected' : state,
54
57
  workspace: text(value.workspace, '', 4_096),
58
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
55
59
  bot: {
56
60
  name: text(value.bot?.name, `${channel}机器人`, 100),
57
61
  username: text(value.bot?.username, '', 100),
@@ -82,6 +86,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
82
86
  revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
83
87
  bots,
84
88
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
89
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
85
90
  };
86
91
  };
87
92
 
@@ -4,6 +4,11 @@ import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-b
4
4
  import { h } from '../../i18n.js';
5
5
  import { installDingtalkStyles } from '../dingtalk/styles.js';
6
6
  import { WorkspaceEditor } from '../../workspace-editor.js';
7
+ import {
8
+ AgentPresetCatalogContext,
9
+ AgentPresetEditor,
10
+ EMPTY_AGENT_PRESET_CATALOG,
11
+ } from '../../agent-preset.js';
7
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
8
13
 
9
14
  const Button = React.forwardRef(function Button(
@@ -63,7 +68,7 @@ export function createTokenChannelSettings(definition) {
63
68
  accountSettingsEndpoint = null,
64
69
  } = definition;
65
70
 
66
- function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
71
+ function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAgentPresetSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
67
72
  const state = busy === 'reconnect' ? 'connecting' : account.state;
68
73
  const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
69
74
  const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
@@ -90,6 +95,11 @@ export function createTokenChannelSettings(definition) {
90
95
  disabled: Boolean(busy),
91
96
  onSave: onWorkspaceSave,
92
97
  }),
98
+ h(AgentPresetEditor, {
99
+ agentPreset: account.agentPreset,
100
+ disabled: Boolean(busy),
101
+ onSave: onAgentPresetSave,
102
+ }),
93
103
  AccountSettings ? h(AccountSettings, {
94
104
  account,
95
105
  busy: Boolean(busy),
@@ -122,6 +132,7 @@ export function createTokenChannelSettings(definition) {
122
132
  function SettingsTab({ rpcCall }) {
123
133
  const [model, setModel] = React.useState({
124
134
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
135
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
125
136
  });
126
137
  const [credentialOpen, setCredentialOpen] = React.useState(false);
127
138
  const [credentialError, setCredentialError] = React.useState(null);
@@ -156,7 +167,10 @@ export function createTokenChannelSettings(definition) {
156
167
  const snapshot = api.normalizeSnapshot(await invoke(endpoints.status, {}, signal));
157
168
  if (!mounted.current || signal?.aborted
158
169
  || !workspaceFence.canCommitStatus(workspaceVersion)) return;
159
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
170
+ setModel({
171
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
172
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
173
+ });
160
174
  } catch (error) {
161
175
  if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
162
176
  && workspaceFence.canCommitStatus(workspaceVersion)) {
@@ -199,7 +213,10 @@ export function createTokenChannelSettings(definition) {
199
213
  ));
200
214
  if (!mounted.current) return;
201
215
  if (workspaceFence.canCommitMutation(snapshotVersion)) {
202
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
216
+ setModel({
217
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
218
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
219
+ });
203
220
  }
204
221
  setCredentialOpen(false);
205
222
  } catch (error) {
@@ -218,7 +235,10 @@ export function createTokenChannelSettings(definition) {
218
235
  const value = await invoke(endpoint, payload);
219
236
  const snapshot = api.normalizeSnapshot(value);
220
237
  if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
221
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
238
+ setModel({
239
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
240
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
241
+ });
222
242
  }
223
243
  if (mounted.current && operation === 'reconnect') {
224
244
  setTestNoticeByBot((current) => ({
@@ -267,6 +287,12 @@ export function createTokenChannelSettings(definition) {
267
287
  endpoints.setWorkspace,
268
288
  { botId: account.botId, workspace },
269
289
  ),
290
+ onAgentPresetSave: (agentPreset) => botAction(
291
+ account,
292
+ 'preset',
293
+ endpoints.setAgentPreset,
294
+ { botId: account.botId, agentPreset },
295
+ ),
270
296
  onAccountSettingsSave: AccountSettings && accountSettingsEndpoint
271
297
  ? (payload) => botAction(
272
298
  account,
@@ -287,7 +313,9 @@ export function createTokenChannelSettings(definition) {
287
313
  })))))
288
314
  : null;
289
315
 
290
- return h('section', {
316
+ return h(AgentPresetCatalogContext.Provider, {
317
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
318
+ }, h('section', {
291
319
  className: `ddt-page ${pageClass} dim-channelPage`,
292
320
  'aria-label': `${channel} 设置`,
293
321
  },
@@ -353,7 +381,7 @@ export function createTokenChannelSettings(definition) {
353
381
  'aria-hidden': 'true',
354
382
  }, h(LogoGlyph, { size: 64 }))))
355
383
  : null,
356
- botList));
384
+ botList)));
357
385
  }
358
386
 
359
387
  return { SettingsTab, AccountCard };
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  export const WECOM_RPC_CHANNEL = '/wecom';
2
4
 
3
5
  export const WECOM_ENDPOINTS = Object.freeze({
@@ -9,6 +11,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
9
11
  reconnectBot: 'bot.reconnect',
10
12
  deleteBot: 'bot.delete',
11
13
  setWorkspace: 'bot.workspace.set',
14
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
12
15
  });
13
16
 
14
17
  const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
@@ -91,6 +94,7 @@ function normalizeBot(value) {
91
94
  connected,
92
95
  state: connected ? 'connected' : state,
93
96
  workspace: text(value.workspace, '', 4_096),
97
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
94
98
  bot: {
95
99
  name: text(value.bot?.name, '企业微信机器人', 100),
96
100
  appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
@@ -116,6 +120,7 @@ export function normalizeSnapshot(value) {
116
120
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
117
121
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
118
122
  testMessage: normalizeTestMessage(source.testMessage),
123
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
119
124
  };
120
125
  }
121
126
 
@@ -4,6 +4,11 @@ import { WecomLogoGlyph } from '../../channel-logos.js';
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
5
5
  import { h } from '../../i18n.js';
6
6
  import { WorkspaceEditor } from '../../workspace-editor.js';
7
+ import {
8
+ AgentPresetCatalogContext,
9
+ AgentPresetEditor,
10
+ EMPTY_AGENT_PRESET_CATALOG,
11
+ } from '../../agent-preset.js';
7
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
8
13
  import { installDingtalkStyles } from '../dingtalk/styles.js';
9
14
  import {
@@ -155,6 +160,7 @@ export function AccountCard({
155
160
  removing,
156
161
  onReconnect,
157
162
  onWorkspaceSave,
163
+ onAgentPresetSave,
158
164
  onRequestRemove,
159
165
  onConfirmRemove,
160
166
  onCancelRemove,
@@ -179,6 +185,11 @@ export function AccountCard({
179
185
  disabled: Boolean(busy),
180
186
  onSave: onWorkspaceSave,
181
187
  }),
188
+ h(AgentPresetEditor, {
189
+ agentPreset: account.agentPreset,
190
+ disabled: Boolean(busy),
191
+ onSave: onAgentPresetSave,
192
+ }),
182
193
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
183
194
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
184
195
  feedback ? h('div', {
@@ -195,7 +206,10 @@ export function AccountCard({
195
206
  }
196
207
 
197
208
  export function WecomSettingsTab({ rpcCall }) {
198
- const [model, setModel] = React.useState({ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null });
209
+ const [model, setModel] = React.useState({
210
+ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
211
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
212
+ });
199
213
  const [provision, setProvision] = React.useState(null);
200
214
  const [busy, setBusy] = React.useState(false);
201
215
  const [busyByBot, setBusyByBot] = React.useState({});
@@ -253,7 +267,10 @@ export function WecomSettingsTab({ rpcCall }) {
253
267
  const snapshot = normalizeSnapshot(await invoke(WECOM_ENDPOINTS.status, {}, signal));
254
268
  if (!mounted.current || signal?.aborted
255
269
  || !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
256
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
270
+ setModel({
271
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
272
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
273
+ });
257
274
  if (restore && snapshot.provisioning) setProvision({
258
275
  ...snapshot.provisioning,
259
276
  durationMs: Math.max(1, snapshot.provisioning.expiresAt - Date.now()),
@@ -317,7 +334,10 @@ export function WecomSettingsTab({ rpcCall }) {
317
334
  ));
318
335
  if (!mounted.current) return;
319
336
  if (workspaceFence.canCommitMutation(snapshotVersion)) {
320
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
337
+ setModel({
338
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
339
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
340
+ });
321
341
  }
322
342
  setCredentialOpen(false);
323
343
  } catch (error) {
@@ -377,7 +397,10 @@ export function WecomSettingsTab({ rpcCall }) {
377
397
  try {
378
398
  const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
379
399
  if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
380
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
400
+ setModel({
401
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
402
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
403
+ });
381
404
  }
382
405
  return snapshot;
383
406
  } finally {
@@ -455,6 +478,12 @@ export function WecomSettingsTab({ rpcCall }) {
455
478
  WECOM_ENDPOINTS.setWorkspace,
456
479
  { botId: account.botId, workspace },
457
480
  ),
481
+ onAgentPresetSave: (agentPreset) => botAction(
482
+ account,
483
+ 'preset',
484
+ WECOM_ENDPOINTS.setAgentPreset,
485
+ { botId: account.botId, agentPreset },
486
+ ),
458
487
  onRequestRemove: () => setRemoveTarget(account.botId),
459
488
  onCancelRemove: () => setRemoveTarget(null),
460
489
  onConfirmRemove: async () => {
@@ -478,7 +507,9 @@ export function WecomSettingsTab({ rpcCall }) {
478
507
  })
479
508
  : null;
480
509
 
481
- return h('section', { className: 'ddt-page dwecom-page dim-channelPage', 'aria-label': '企业微信设置' },
510
+ return h(AgentPresetCatalogContext.Provider, {
511
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
512
+ }, h('section', { className: 'ddt-page dwecom-page dim-channelPage', 'aria-label': '企业微信设置' },
482
513
  h(Heading, {
483
514
  totals: model.totals,
484
515
  adding: Boolean(provision),
@@ -497,7 +528,7 @@ export function WecomSettingsTab({ rpcCall }) {
497
528
  provisionView,
498
529
  model.bots.length === 0 && !provision && !credentialOpen
499
530
  ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
500
- botList));
531
+ botList)));
501
532
  }
502
533
 
503
534
  export function apply(ctx) {
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  export const WEIXIN_RPC_CHANNEL = '/weixin';
2
4
  export const WEIXIN_ENDPOINTS = Object.freeze({
3
5
  status: 'connection.status',
@@ -8,6 +10,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
8
10
  reconnectBot: 'bot.reconnect',
9
11
  deleteBot: 'bot.delete',
10
12
  setWorkspace: 'bot.workspace.set',
13
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
11
14
  });
12
15
 
13
16
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@@ -124,6 +127,7 @@ function normalizeBot(value) {
124
127
  connected,
125
128
  configured: value.configured === true,
126
129
  workspace: string(value.workspace).slice(0, 4_096),
130
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
127
131
  bot: {
128
132
  name: string(value.bot.name, '微信机器人'),
129
133
  accountIdMasked: string(value.bot.accountIdMasked, '已安全保存'),
@@ -163,6 +167,7 @@ export function normalizeSnapshot(value) {
163
167
  },
164
168
  provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : null,
165
169
  testMessage: normalizeTestMessage(value.testMessage),
170
+ agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
166
171
  };
167
172
  }
168
173
 
@@ -16,6 +16,11 @@ import {
16
16
  } from './api.js';
17
17
  import { createPollScheduler, useAnimationFrameScheduler } from '../../lifecycle.js';
18
18
  import { WorkspaceEditor } from '../../workspace-editor.js';
19
+ import {
20
+ AgentPresetCatalogContext,
21
+ AgentPresetEditor,
22
+ EMPTY_AGENT_PRESET_CATALOG,
23
+ } from '../../agent-preset.js';
19
24
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
20
25
  import { installWeixinStyles } from './styles.js';
21
26
 
@@ -202,6 +207,7 @@ export function AccountCard({
202
207
  removing,
203
208
  onReconnect,
204
209
  onWorkspaceSave,
210
+ onAgentPresetSave,
205
211
  onRequestRemove,
206
212
  onConfirmRemove,
207
213
  onCancelRemove,
@@ -228,6 +234,11 @@ export function AccountCard({
228
234
  disabled: Boolean(busy),
229
235
  onSave: onWorkspaceSave,
230
236
  }),
237
+ h(AgentPresetEditor, {
238
+ agentPreset: account.agentPreset,
239
+ disabled: Boolean(busy),
240
+ onSave: onAgentPresetSave,
241
+ }),
231
242
  h('div', { className: 'dxw-accountFooter dim-cardFooter' },
232
243
  summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
233
244
  account.lastMessageError ? h('div', {
@@ -264,6 +275,7 @@ function AccountList(props) {
264
275
  removing: props.removeTarget === account.botId,
265
276
  onReconnect: () => props.onReconnect(account),
266
277
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
278
+ onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(account, agentPreset),
267
279
  onRequestRemove: () => props.onRequestRemove(account),
268
280
  onConfirmRemove: () => props.onConfirmRemove(account),
269
281
  onCancelRemove: props.onCancelRemove,
@@ -289,6 +301,7 @@ export function mergeWeixinProvisioningSnapshot(
289
301
  export function WeixinSettingsTab({ rpcCall }) {
290
302
  const [model, setModel] = React.useState({
291
303
  phase: 'loading', bots: [], totals: EMPTY_TOTALS, revision: 0, error: null,
304
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
292
305
  });
293
306
  const [provision, setProvision] = React.useState(null);
294
307
  const [busy, setBusy] = React.useState(false);
@@ -331,6 +344,7 @@ export function WeixinSettingsTab({ rpcCall }) {
331
344
  setModel({
332
345
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
333
346
  revision: snapshot.revision, error: null,
347
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
334
348
  });
335
349
  if (snapshot.provisioning) {
336
350
  setProvision((current) => mergeWeixinProvisioningSnapshot(
@@ -519,7 +533,10 @@ export function WeixinSettingsTab({ rpcCall }) {
519
533
  { botId: account.botId, sendTest: true },
520
534
  ));
521
535
  if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
522
- setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
536
+ setModel((current) => ({
537
+ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision,
538
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? current.agentPresetCatalog,
539
+ }));
523
540
  }
524
541
  const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
525
542
  let feedback;
@@ -563,6 +580,29 @@ export function WeixinSettingsTab({ rpcCall }) {
563
580
  setModel({
564
581
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
565
582
  revision: snapshot.revision, error: null,
583
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
584
+ });
585
+ }
586
+ } finally {
587
+ const shouldRefresh = workspaceFence.endMutation();
588
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
589
+ if (mountedRef.current) setBotBusy(account.botId, null);
590
+ }
591
+ }, [invoke, loadStatus, setBotBusy, workspaceFence]);
592
+
593
+ const saveAgentPreset = React.useCallback(async (account, agentPreset) => {
594
+ const snapshotVersion = workspaceFence.beginMutation();
595
+ setBotBusy(account.botId, 'preset');
596
+ try {
597
+ const snapshot = normalizeSnapshot(await invoke(
598
+ WEIXIN_ENDPOINTS.setAgentPreset,
599
+ { botId: account.botId, agentPreset },
600
+ ));
601
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
602
+ setModel({
603
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
604
+ revision: snapshot.revision, error: null,
605
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
566
606
  });
567
607
  }
568
608
  } finally {
@@ -581,7 +621,10 @@ export function WeixinSettingsTab({ rpcCall }) {
581
621
  confirm: true,
582
622
  }));
583
623
  if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
584
- setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
624
+ setModel((current) => ({
625
+ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision,
626
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? current.agentPresetCatalog,
627
+ }));
585
628
  }
586
629
  setRemoveTarget(null);
587
630
  announce('微信账号及本机凭据已移除。');
@@ -621,7 +664,9 @@ export function WeixinSettingsTab({ rpcCall }) {
621
664
  });
622
665
  }
623
666
 
624
- return h('section', { className: 'dxw-page dim-channelPage', 'aria-label': '微信设置' },
667
+ return h(AgentPresetCatalogContext.Provider, {
668
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
669
+ }, h('section', { className: 'dxw-page dim-channelPage', 'aria-label': '微信设置' },
625
670
  h(Heading, {
626
671
  totals: model.totals,
627
672
  adding: Boolean(provision),
@@ -654,12 +699,13 @@ export function WeixinSettingsTab({ rpcCall }) {
654
699
  removeTarget,
655
700
  onReconnect: (account) => void reconnect(account),
656
701
  onWorkspaceSave: saveWorkspace,
702
+ onAgentPresetSave: saveAgentPreset,
657
703
  onRequestRemove: (account) => setRemoveTarget(account.botId),
658
704
  onConfirmRemove: (account) => void remove(account),
659
705
  onCancelRemove: () => setRemoveTarget(null),
660
706
  })
661
707
  : null),
662
- );
708
+ ));
663
709
  }
664
710
 
665
711
  export function apply(ctx) {