@xmanrui/dsh-im 0.17.1 → 0.18.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 (46) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +931 -538
  4. package/lib/index.js +189 -144
  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 +5 -0
  10. package/plugin-src/client/channels/feishu/index.js +39 -2
  11. package/plugin-src/client/channels/qq/api.js +5 -0
  12. package/plugin-src/client/channels/qq/index.js +37 -6
  13. package/plugin-src/client/channels/shared/token-api.js +5 -0
  14. package/plugin-src/client/channels/shared/token-channel.js +34 -6
  15. package/plugin-src/client/channels/wecom/api.js +5 -0
  16. package/plugin-src/client/channels/wecom/index.js +37 -6
  17. package/plugin-src/client/channels/weixin/api.js +5 -0
  18. package/plugin-src/client/channels/weixin/index.js +50 -4
  19. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  20. package/plugin-src/client/channels/whatsapp/index.js +30 -4
  21. package/plugin-src/client/i18n.js +9 -0
  22. package/plugin-src/client/styles.js +8 -0
  23. package/plugin-src/host/channels/dingtalk/production.mjs +10 -4
  24. package/plugin-src/host/channels/dingtalk/rpc.mjs +12 -0
  25. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +1 -1
  26. package/plugin-src/host/channels/feishu/production.mjs +6 -3
  27. package/plugin-src/host/channels/feishu/rpc.mjs +17 -0
  28. package/plugin-src/host/channels/qq/production.mjs +10 -4
  29. package/plugin-src/host/channels/qq/rpc.mjs +12 -0
  30. package/plugin-src/host/channels/shared/agent-preset-rpc.mjs +25 -0
  31. package/plugin-src/host/channels/shared/production.mjs +10 -4
  32. package/plugin-src/host/channels/shared/rpc.mjs +12 -0
  33. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  34. package/plugin-src/host/channels/slack/production.mjs +10 -4
  35. package/plugin-src/host/channels/slack/rpc.mjs +13 -0
  36. package/plugin-src/host/channels/wecom/production.mjs +10 -4
  37. package/plugin-src/host/channels/wecom/rpc.mjs +12 -0
  38. package/plugin-src/host/channels/weixin/production.mjs +10 -4
  39. package/plugin-src/host/channels/weixin/rpc.mjs +15 -0
  40. package/plugin-src/host/channels/whatsapp/production.mjs +10 -4
  41. package/plugin-src/host/channels/whatsapp/rpc.mjs +12 -0
  42. package/src/channels/discord/discord-api.mjs +1 -1
  43. package/src/channels/shared/agent-preset.mjs +74 -0
  44. package/src/channels/shared/bot-workspace-store.mjs +141 -14
  45. package/src/channels/shared/harness-client.mjs +6 -4
  46. package/src/channels/weixin/weixin-api.mjs +1 -1
@@ -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) {
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
2
4
 
3
5
  export const WHATSAPP_ENDPOINTS = Object.freeze({
@@ -8,6 +10,7 @@ export const WHATSAPP_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 PROVISION_STATES = new Set(['starting', 'pending', '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, 'WhatsApp机器人', 100),
87
91
  idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
@@ -109,6 +113,7 @@ export function normalizeSnapshot(value) {
109
113
  bots,
110
114
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
111
115
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
116
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
112
117
  };
113
118
  }
114
119
 
@@ -4,6 +4,11 @@ import { WhatsappLogoGlyph } from '../../channel-logos.js';
4
4
  import { 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 {
@@ -173,6 +178,7 @@ export function WhatsappAccountCard({
173
178
  removing,
174
179
  onReconnect,
175
180
  onWorkspaceSave,
181
+ onAgentPresetSave,
176
182
  onRequestRemove,
177
183
  onConfirmRemove,
178
184
  onCancelRemove,
@@ -205,6 +211,11 @@ export function WhatsappAccountCard({
205
211
  disabled: Boolean(busy),
206
212
  onSave: onWorkspaceSave,
207
213
  }),
214
+ h(AgentPresetEditor, {
215
+ agentPreset: account.agentPreset,
216
+ disabled: Boolean(busy),
217
+ onSave: onAgentPresetSave,
218
+ }),
208
219
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
209
220
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
210
221
  testNotice ? h('div', {
@@ -229,6 +240,7 @@ export function WhatsappAccountCard({
229
240
  export function WhatsappSettingsTab({ rpcCall }) {
230
241
  const [model, setModel] = React.useState({
231
242
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
243
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
232
244
  });
233
245
  const [provision, setProvision] = React.useState(null);
234
246
  const [busy, setBusy] = React.useState(false);
@@ -264,7 +276,10 @@ export function WhatsappSettingsTab({ rpcCall }) {
264
276
  const snapshot = normalizeSnapshot(await invoke(WHATSAPP_ENDPOINTS.status, {}, signal));
265
277
  if (!mounted.current || signal?.aborted
266
278
  || !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
267
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
279
+ setModel({
280
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
281
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
282
+ });
268
283
  if (restore && snapshot.provisioning) setProvision({
269
284
  ...snapshot.provisioning,
270
285
  durationMs: Math.max(1, snapshot.provisioning.expiresAt - Date.now()),
@@ -392,7 +407,10 @@ export function WhatsappSettingsTab({ rpcCall }) {
392
407
  const value = await invoke(endpoint, payload);
393
408
  const snapshot = normalizeSnapshot(value);
394
409
  if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
395
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
410
+ setModel({
411
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
412
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
413
+ });
396
414
  if (operation === 'reconnect') {
397
415
  setTestNoticeByBot((current) => ({
398
416
  ...current,
@@ -441,6 +459,12 @@ export function WhatsappSettingsTab({ rpcCall }) {
441
459
  WHATSAPP_ENDPOINTS.setWorkspace,
442
460
  { botId: account.botId, workspace },
443
461
  ),
462
+ onAgentPresetSave: (agentPreset) => botAction(
463
+ account,
464
+ 'preset',
465
+ WHATSAPP_ENDPOINTS.setAgentPreset,
466
+ { botId: account.botId, agentPreset },
467
+ ),
444
468
  onRequestRemove: () => setRemoveTarget(account.botId),
445
469
  onCancelRemove: () => setRemoveTarget(null),
446
470
  onConfirmRemove: async () => {
@@ -453,7 +477,9 @@ export function WhatsappSettingsTab({ rpcCall }) {
453
477
  })))))
454
478
  : null;
455
479
 
456
- return h('section', {
480
+ return h(AgentPresetCatalogContext.Provider, {
481
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
482
+ }, h('section', {
457
483
  className: 'ddt-page dwa-page dim-channelPage',
458
484
  'aria-label': 'WhatsApp 设置',
459
485
  },
@@ -490,5 +516,5 @@ export function WhatsappSettingsTab({ rpcCall }) {
490
516
  : model.bots.length === 0
491
517
  ? h(EmptyView, { busy, onStart: () => void startProvisioning(false) })
492
518
  : null,
493
- botList));
519
+ botList)));
494
520
  }
@@ -117,6 +117,15 @@ const EN = Object.freeze({
117
117
  '工作区路径不存在。': 'The workspace path does not exist.',
118
118
  '工作区路径必须指向一个目录。': 'The workspace path must point to a directory.',
119
119
  '找不到要修改的机器人。': 'The bot could not be found.',
120
+ 'Agent Preset': 'Agent Preset',
121
+ '跟随 Host 默认': 'Follow the Host default',
122
+ '(已不可用)': ' (unavailable)',
123
+ '只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。': 'This affects only new sessions. If the current chat already has a session, send /new, then send a regular message to apply it.',
124
+ '当前 Agent Preset 已不可用,请选择其他 Preset 或跟随 Host 默认。': 'The current Agent Preset is unavailable. Choose another preset or follow the Host default.',
125
+ 'Agent Preset 修改失败,请重试。': 'Could not update the Agent Preset. Try again.',
126
+ '请选择 Agent Preset。': 'Choose an Agent Preset.',
127
+ 'Agent Preset 无效。': 'The Agent Preset is invalid.',
128
+ 'Agent Preset 不存在或不可用。': 'The Agent Preset does not exist or is unavailable.',
120
129
  '尚未检查': 'Not checked yet',
121
130
  '刚刚': 'Just now',
122
131
  '检查连接': 'Check connection',
@@ -142,6 +142,14 @@ const CSS = String.raw`
142
142
  .dim-panel .dim-workspaceEdit { grid-column: 2; grid-row: 1; padding: 0; border: 0; color: #1677ff; background: transparent; font: inherit; font-weight: 560; white-space: nowrap; cursor: pointer; }
143
143
  .dim-panel .dim-workspaceEdit:disabled { cursor: not-allowed; opacity: .55; }
144
144
  .dim-panel .dim-workspacePath { min-width: 0; max-width: 100%; grid-column: 1 / -1; grid-row: 2; display: block; overflow-x: auto; overflow-y: hidden; color: var(--dsw-alias-label-primary, #1f2329); font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; }
145
+ .dim-panel .dim-preset { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 4px; margin-top: 6px; padding: 6px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
146
+ .dim-panel .dim-presetHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
147
+ .dim-panel .dim-presetHeader > span { grid-column: 1; grid-row: 1; white-space: nowrap; }
148
+ .dim-panel .dim-presetStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
149
+ .dim-panel .dim-presetSelect { min-width: 0; max-width: 100%; grid-column: 1 / -1; grid-row: 2; height: 30px; padding: 0 8px; 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-1, #fff); font: inherit; font-size: 12px; }
150
+ .dim-panel .dim-presetSelect:disabled { cursor: not-allowed; opacity: .55; }
151
+ .dim-panel .dim-presetHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.4; }
152
+ .dim-panel .dim-presetError { grid-column: 1 / -1; grid-row: 4; margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.4; }
145
153
  .dim-directoryPickerBackdrop { --dim-blue: var(--dsw-alias-state-business-primary, #3370ff); --dim-blue-soft: color-mix(in srgb, var(--dim-blue) 9%, transparent); position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 24px; background: rgb(15 17 21 / 42%); backdrop-filter: blur(3px); }
146
154
  .dim-directoryPickerBackdrop, .dim-directoryPickerBackdrop *, .dim-directoryPickerBackdrop *::before, .dim-directoryPickerBackdrop *::after { box-sizing: border-box; }
147
155
  .dim-directoryPicker { width: min(720px, 100%); height: min(620px, calc(100vh - 48px)); min-height: 420px; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 18px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 24px 72px rgb(15 17 21 / 24%); }
@@ -14,6 +14,7 @@ import {
14
14
  createWorkspaceAwareController,
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
+ import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
17
18
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
19
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
19
20
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
@@ -63,7 +64,9 @@ export async function createProductionController(ctx, config = {}, internals = {
63
64
  if (canListConfiguredBots) {
64
65
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
65
66
  }
66
- await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
67
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
68
+ defaultAgentPreset: config.agentPreset,
69
+ })));
67
70
  const observedConfigStore = typeof configStore.remove === 'function'
68
71
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
69
72
  : configStore;
@@ -89,7 +92,6 @@ export async function createProductionController(ctx, config = {}, internals = {
89
92
  const harness = new Harness({
90
93
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
91
94
  workspace: defaultWorkspace,
92
- ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
93
95
  autostart: false,
94
96
  dshBin: config.dshBin ?? 'dsh',
95
97
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -103,7 +105,7 @@ export async function createProductionController(ctx, config = {}, internals = {
103
105
  logger,
104
106
  createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
105
107
  const state = await stateFor(botId);
106
- await workspaces.ensure(botId);
108
+ await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
107
109
  const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
108
110
  return new Runtime({
109
111
  config: botConfig,
@@ -135,7 +137,11 @@ export async function createProductionController(ctx, config = {}, internals = {
135
137
  }
136
138
  },
137
139
  });
138
- const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
140
+ const controller = createWorkspaceAwareController(coreController, {
141
+ workspaces,
142
+ stateFor,
143
+ agentPresetCatalog: () => listAgentPresetCatalog(ctx),
144
+ });
139
145
  const supervisor = createSupervisor({
140
146
  controller,
141
147
  harness,
@@ -1,6 +1,7 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
3
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
+ import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
4
5
  import {
5
6
  connectionTestTargetUnavailable,
6
7
  publicConnectionTestResult,
@@ -16,6 +17,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
16
17
  reconnectBot: 'bot.reconnect',
17
18
  deleteBot: 'bot.delete',
18
19
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
19
21
  approveSender: 'bot.sender.approve',
20
22
  revokeSender: 'bot.sender.revoke',
21
23
  });
@@ -88,6 +90,10 @@ function payloadFailure(endpoint, payload) {
88
90
  return validWorkspacePayload(payload)
89
91
  ? null : '请输入工作区绝对路径。';
90
92
  }
93
+ if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
94
+ return validAgentPresetPayload(payload)
95
+ ? null : '请选择 Agent Preset。';
96
+ }
91
97
  if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
92
98
  return exactKeys(payload, ['botId', 'requestId', 'confirm'])
93
99
  && validId(payload.botId)
@@ -245,6 +251,12 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
245
251
  await controller.updateWorkspace(payload.botId, payload.workspace),
246
252
  cachedEncode,
247
253
  );
254
+ } else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
255
+ if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
256
+ value = await publicStatus(
257
+ await controller.updateAgentPreset(payload.botId, payload.agentPreset),
258
+ cachedEncode,
259
+ );
248
260
  } else if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
249
261
  value = await publicStatus(
250
262
  await controller.approveSender(payload.botId, payload.requestId),
@@ -129,7 +129,7 @@ export class ConnectionSupervisor {
129
129
  await this.#controller.initialize();
130
130
  if (this.#closed) return;
131
131
 
132
- const status = this.#controller.status();
132
+ const status = await this.#controller.status();
133
133
  this.#resolveReady?.(status);
134
134
  this.#resolveReady = null;
135
135
  const current = totals(status);