@xmanrui/dsh-im 4.10.0 → 4.12.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 (62) hide show
  1. package/PROACTIVE_DELIVERY.en.md +15 -1
  2. package/PROACTIVE_DELIVERY.md +15 -1
  3. package/README.en.md +7 -2
  4. package/README.md +8 -3
  5. package/lib/client.js +2341 -1315
  6. package/lib/index.js +250 -245
  7. package/package.json +4 -3
  8. package/plugin-src/client/channel-card-meta.js +9 -1
  9. package/plugin-src/client/channels/dingtalk/api.js +4 -0
  10. package/plugin-src/client/channels/dingtalk/index.js +41 -7
  11. package/plugin-src/client/channels/feishu/api.js +4 -0
  12. package/plugin-src/client/channels/feishu/group-settings.js +581 -0
  13. package/plugin-src/client/channels/feishu/index.js +64 -233
  14. package/plugin-src/client/channels/qq/api.js +4 -0
  15. package/plugin-src/client/channels/qq/index.js +41 -7
  16. package/plugin-src/client/channels/shared/collapsible-account.js +80 -0
  17. package/plugin-src/client/channels/shared/token-api.js +4 -0
  18. package/plugin-src/client/channels/shared/token-channel.js +62 -29
  19. package/plugin-src/client/channels/wecom/api.js +4 -0
  20. package/plugin-src/client/channels/wecom/index.js +41 -7
  21. package/plugin-src/client/channels/weixin/api.js +4 -0
  22. package/plugin-src/client/channels/weixin/index.js +41 -7
  23. package/plugin-src/client/channels/whatsapp/api.js +4 -0
  24. package/plugin-src/client/channels/whatsapp/index.js +40 -7
  25. package/plugin-src/client/delivery-settings.js +73 -5
  26. package/plugin-src/client/i18n.js +22 -0
  27. package/plugin-src/client/model-setting.js +104 -0
  28. package/plugin-src/client/styles.js +62 -0
  29. package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
  30. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  31. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  32. package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
  33. package/plugin-src/host/channels/qq/production.mjs +3 -0
  34. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  35. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
  36. package/plugin-src/host/channels/shared/production.mjs +3 -0
  37. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  38. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  39. package/plugin-src/host/channels/slack/production.mjs +3 -0
  40. package/plugin-src/host/channels/slack/rpc.mjs +9 -0
  41. package/plugin-src/host/channels/wecom/production.mjs +3 -0
  42. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  43. package/plugin-src/host/channels/weixin/production.mjs +3 -0
  44. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  45. package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
  46. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  47. package/plugin-src/host/delivery-adapter.mjs +141 -3
  48. package/plugin-src/host/delivery-rpc.mjs +12 -0
  49. package/plugin-src/host/delivery-service.mjs +110 -3
  50. package/plugin-src/host/delivery-suggestions.mjs +46 -0
  51. package/plugin-src/host/harness-session-coordinator.mjs +6 -5
  52. package/plugin-src/host/index.mjs +18 -1
  53. package/plugin-src/host/session-sync-coordinator.mjs +217 -0
  54. package/scripts/verify-package.mjs +12 -6
  55. package/src/channels/shared/bot-workspace-store.mjs +285 -38
  56. package/src/channels/shared/harness-client.mjs +124 -43
  57. package/src/channels/shared/i18n-en/wecom.mjs +24 -0
  58. package/src/channels/shared/model-setting.mjs +110 -0
  59. package/src/channels/wecom/wecom-bridge.mjs +275 -2
  60. package/src/channels/wecom/wecom-cards.mjs +101 -0
  61. package/src/channels/wecom/wecom-runtime.mjs +2 -0
  62. package/src/channels/whatsapp/whatsapp-runtime.mjs +78 -26
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
2
3
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
4
  import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
4
5
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
@@ -30,6 +31,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
30
31
  reconnectBot: 'bot.reconnect',
31
32
  deleteBot: 'bot.delete',
32
33
  setWorkspace: 'bot.workspace.set',
34
+ setModel: SET_MODEL_ENDPOINT,
33
35
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
34
36
  setContextEnhancement: 'bot.context-enhancement.set',
35
37
  setAccessPolicy: 'bot.access-policy.set',
@@ -60,6 +62,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
60
62
  connected,
61
63
  state: connected ? 'connected' : state,
62
64
  workspace: text(value.workspace, '', 4_096),
65
+ model: normalizeModelSelection(value.model),
63
66
  agentPreset: normalizeAgentPresetId(value.agentPreset),
64
67
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
65
68
  ...(Object.hasOwn(value, 'accessPolicy')
@@ -97,6 +100,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
97
100
  bots,
98
101
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
99
102
  agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
103
+ modelCatalog: normalizeModelCatalog(source.modelCatalog),
100
104
  };
101
105
  };
102
106
 
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-binding.js';
4
+ import { CollapsibleAccountSection } from './collapsible-account.js';
4
5
  import { h } from '../../i18n.js';
5
6
  import { installDingtalkStyles } from '../dingtalk/styles.js';
6
7
  import { WorkspaceEditor } from '../../workspace-editor.js';
@@ -10,6 +11,11 @@ import {
10
11
  AgentPresetEditor,
11
12
  EMPTY_AGENT_PRESET_CATALOG,
12
13
  } from '../../agent-preset.js';
14
+ import {
15
+ EMPTY_MODEL_CATALOG,
16
+ ModelCatalogContext,
17
+ ModelEditor,
18
+ } from '../../model-setting.js';
13
19
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
14
20
  import {
15
21
  BotSettingsButton,
@@ -75,7 +81,7 @@ export function createTokenChannelSettings(definition) {
75
81
  accountSettingsEndpoint = null,
76
82
  } = definition;
77
83
 
78
- function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAgentPresetSave, onContextEnhancementSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
84
+ function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onModelSave, onAgentPresetSave, onContextEnhancementSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
79
85
  const state = busy === 'reconnect' ? 'connecting' : account.state;
80
86
  const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
81
87
  const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
@@ -83,32 +89,45 @@ export function createTokenChannelSettings(definition) {
83
89
  const identity = account.bot.username ? `@${account.bot.username}` : account.bot.idMasked;
84
90
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
85
91
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
86
- h('div', { className: 'ddt-accountTop dim-botCardTop' },
87
- h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
88
- h('div', { className: `ddt-avatar dim-botAvatar ${avatarClass}`, 'aria-hidden': 'true' },
89
- h(LogoGlyph, { size: 29 })),
90
- h('div', { className: 'dim-botName' },
91
- h('h3', null, account.bot.name), h('p', null, identity))),
92
- h('div', { className: 'dim-botCardTools' },
93
- h(BotStatusMeta, {
94
- className: 'ddt-health',
95
- dotClassName: 'ddt-dot',
96
- tone,
97
- stateLabel,
98
- lastCheckedAt: account.health.lastCheckedAt,
99
- formatCheckedTime: checkedTime,
100
- }),
101
- h(BotSettingsButton, {
102
- channel: channel.toLowerCase(),
103
- botId: account.botId,
104
- botName: account.bot.name,
105
- connected: account.connected,
106
- accessPolicy: account.accessPolicy,
107
- }))),
108
- h(WorkspaceEditor, {
109
- workspace: account.workspace,
92
+ h(CollapsibleAccountSection, {
93
+ id: `tok-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
94
+ header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
95
+ h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
96
+ h('div', { className: `ddt-avatar dim-botAvatar ${avatarClass}`, 'aria-hidden': 'true' },
97
+ h(LogoGlyph, { size: 29 })),
98
+ h('div', { className: 'dim-botName' },
99
+ h('h3', null, account.bot.name), h('p', null, identity))),
100
+ h('div', {
101
+ className: 'dim-botCardTools',
102
+ // The header is the collapse toggle; keep inner controls clickable.
103
+ onClick: (event) => { event.stopPropagation(); },
104
+ onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') event.stopPropagation(); },
105
+ },
106
+ h(BotStatusMeta, {
107
+ className: 'ddt-health',
108
+ dotClassName: 'ddt-dot',
109
+ tone,
110
+ stateLabel,
111
+ lastCheckedAt: account.health.lastCheckedAt,
112
+ formatCheckedTime: checkedTime,
113
+ }),
114
+ h(BotSettingsButton, {
115
+ channel: channel.toLowerCase(),
116
+ botId: account.botId,
117
+ botName: account.bot.name,
118
+ connected: account.connected,
119
+ accessPolicy: account.accessPolicy,
120
+ }))),
121
+ },
122
+ h(WorkspaceEditor, {
123
+ workspace: account.workspace,
124
+ disabled: Boolean(busy),
125
+ onSave: onWorkspaceSave,
126
+ }),
127
+ h(ModelEditor, {
128
+ model: account.model,
110
129
  disabled: Boolean(busy),
111
- onSave: onWorkspaceSave,
130
+ onSave: onModelSave,
112
131
  }),
113
132
  h(AgentPresetEditor, {
114
133
  agentPreset: account.agentPreset,
@@ -147,7 +166,9 @@ export function createTokenChannelSettings(definition) {
147
166
  testNotice ? h('div', {
148
167
  className: 'ddt-summary dim-cardFeedback',
149
168
  role: 'status',
150
- }, testNotice) : null))),
169
+ }, testNotice) : null)),
170
+ ),
171
+ ),
151
172
  removing ? h('div', { className: 'ddt-confirm dim-confirm', role: 'alertdialog' },
152
173
  h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
153
174
  h('p', null, `这会停止消息连接,并删除本机保存的 ${credentialNoun}、机器人配置及会话映射。${platformLabel}中的机器人不会被自动删除。`),
@@ -161,6 +182,7 @@ export function createTokenChannelSettings(definition) {
161
182
  const [model, setModel] = React.useState({
162
183
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
163
184
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
185
+ modelCatalog: EMPTY_MODEL_CATALOG,
164
186
  });
165
187
  const [credentialOpen, setCredentialOpen] = React.useState(false);
166
188
  const [credentialError, setCredentialError] = React.useState(null);
@@ -198,6 +220,7 @@ export function createTokenChannelSettings(definition) {
198
220
  setModel({
199
221
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
200
222
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
223
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
201
224
  });
202
225
  } catch (error) {
203
226
  if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
@@ -244,6 +267,7 @@ export function createTokenChannelSettings(definition) {
244
267
  setModel({
245
268
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
246
269
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
270
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
247
271
  });
248
272
  }
249
273
  setCredentialOpen(false);
@@ -266,6 +290,7 @@ export function createTokenChannelSettings(definition) {
266
290
  setModel({
267
291
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
268
292
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
293
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
269
294
  });
270
295
  }
271
296
  if (mounted.current && operation === 'reconnect') {
@@ -318,6 +343,12 @@ export function createTokenChannelSettings(definition) {
318
343
  endpoints.setWorkspace,
319
344
  { botId: account.botId, workspace },
320
345
  ),
346
+ onModelSave: (selectedModel) => botAction(
347
+ account,
348
+ 'model',
349
+ endpoints.setModel,
350
+ { botId: account.botId, model: selectedModel },
351
+ ),
321
352
  onAgentPresetSave: (agentPreset) => botAction(
322
353
  account,
323
354
  'preset',
@@ -350,7 +381,9 @@ export function createTokenChannelSettings(definition) {
350
381
  })))))
351
382
  : null;
352
383
 
353
- return h(AgentPresetCatalogContext.Provider, {
384
+ return h(ModelCatalogContext.Provider, {
385
+ value: model.modelCatalog ?? EMPTY_MODEL_CATALOG,
386
+ }, h(AgentPresetCatalogContext.Provider, {
354
387
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
355
388
  }, h('section', {
356
389
  className: `ddt-page ${pageClass} dim-channelPage`,
@@ -418,7 +451,7 @@ export function createTokenChannelSettings(definition) {
418
451
  'aria-hidden': 'true',
419
452
  }, h(LogoGlyph, { size: 64 }))))
420
453
  : null,
421
- botList)));
454
+ botList))));
422
455
  }
423
456
 
424
457
  return { SettingsTab, AccountCard };
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
2
3
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
4
  import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
4
5
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
@@ -14,6 +15,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
14
15
  reconnectBot: 'bot.reconnect',
15
16
  deleteBot: 'bot.delete',
16
17
  setWorkspace: 'bot.workspace.set',
18
+ setModel: SET_MODEL_ENDPOINT,
17
19
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
18
20
  setContextEnhancement: 'bot.context-enhancement.set',
19
21
  setAccessPolicy: 'bot.access-policy.set',
@@ -99,6 +101,7 @@ function normalizeBot(value) {
99
101
  connected,
100
102
  state: connected ? 'connected' : state,
101
103
  workspace: text(value.workspace, '', 4_096),
104
+ model: normalizeModelSelection(value.model),
102
105
  agentPreset: normalizeAgentPresetId(value.agentPreset),
103
106
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
104
107
  ...(Object.hasOwn(value, 'accessPolicy')
@@ -131,6 +134,7 @@ export function normalizeSnapshot(value) {
131
134
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
132
135
  testMessage: normalizeTestMessage(source.testMessage),
133
136
  agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
137
+ modelCatalog: normalizeModelCatalog(source.modelCatalog),
134
138
  };
135
139
  }
136
140
 
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { WecomLogoGlyph } from '../../channel-logos.js';
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
5
+ import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
5
6
  import { h } from '../../i18n.js';
6
7
  import { WorkspaceEditor } from '../../workspace-editor.js';
7
8
  import { ContextEnhancementEditor } from '../../context-enhancement.js';
@@ -10,6 +11,11 @@ import {
10
11
  AgentPresetEditor,
11
12
  EMPTY_AGENT_PRESET_CATALOG,
12
13
  } from '../../agent-preset.js';
14
+ import {
15
+ EMPTY_MODEL_CATALOG,
16
+ ModelCatalogContext,
17
+ ModelEditor,
18
+ } from '../../model-setting.js';
13
19
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
14
20
  import {
15
21
  BotSettingsButton,
@@ -166,6 +172,7 @@ export function AccountCard({
166
172
  removing,
167
173
  onReconnect,
168
174
  onWorkspaceSave,
175
+ onModelSave,
169
176
  onAgentPresetSave,
170
177
  onContextEnhancementSave,
171
178
  onRequestRemove,
@@ -177,12 +184,19 @@ export function AccountCard({
177
184
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
178
185
  return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
179
186
  h('div', { className: 'ddt-cardBody dim-botCardBody' },
180
- h('div', { className: 'ddt-accountTop dim-botCardTop' },
187
+ h(CollapsibleAccountSection, {
188
+ id: `wec-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
189
+ header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
181
190
  h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
182
191
  h('div', { className: 'ddt-avatar dim-botAvatar dwecom-avatar', 'aria-hidden': 'true' }, h(WecomLogoGlyph, { size: 29 })),
183
192
  h('div', { className: 'dim-botName' },
184
193
  h('h3', null, account.bot.name), h('p', null, account.bot.appIdMasked))),
185
- h('div', { className: 'dim-botCardTools' },
194
+ h('div', {
195
+ className: 'dim-botCardTools',
196
+ // The header is the collapse toggle; keep inner controls clickable.
197
+ onClick: (event) => { event.stopPropagation(); },
198
+ onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') event.stopPropagation(); },
199
+ },
186
200
  h(BotStatusMeta, {
187
201
  className: 'ddt-health',
188
202
  dotClassName: 'ddt-dot',
@@ -197,12 +211,18 @@ export function AccountCard({
197
211
  botName: account.bot.name,
198
212
  connected: account.connected,
199
213
  accessPolicy: account.accessPolicy,
200
- }))),
201
- h(WorkspaceEditor, {
214
+ })))
215
+ },
216
+ h(WorkspaceEditor, {
202
217
  workspace: account.workspace,
203
218
  disabled: Boolean(busy),
204
219
  onSave: onWorkspaceSave,
205
220
  }),
221
+ h(ModelEditor, {
222
+ model: account.model,
223
+ disabled: Boolean(busy),
224
+ onSave: onModelSave,
225
+ }),
206
226
  h(AgentPresetEditor, {
207
227
  agentPreset: account.agentPreset,
208
228
  disabled: Boolean(busy),
@@ -227,7 +247,9 @@ export function AccountCard({
227
247
  className: 'ddt-summary dim-cardFeedback',
228
248
  role: 'status',
229
249
  'aria-live': 'polite',
230
- }, feedback) : null))),
250
+ }, feedback) : null)),
251
+ ),
252
+ ),
231
253
  removing ? h(RemoveConfirmation, {
232
254
  account, busy: busy === 'delete', onConfirm: onConfirmRemove, onCancel: onCancelRemove,
233
255
  }) : null);
@@ -237,6 +259,7 @@ export function WecomSettingsTab({ rpcCall }) {
237
259
  const [model, setModel] = React.useState({
238
260
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
239
261
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
262
+ modelCatalog: EMPTY_MODEL_CATALOG,
240
263
  });
241
264
  const [provision, setProvision] = React.useState(null);
242
265
  const [busy, setBusy] = React.useState(false);
@@ -298,6 +321,7 @@ export function WecomSettingsTab({ rpcCall }) {
298
321
  setModel({
299
322
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
300
323
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
324
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
301
325
  });
302
326
  if (restore && snapshot.provisioning) setProvision({
303
327
  ...snapshot.provisioning,
@@ -365,6 +389,7 @@ export function WecomSettingsTab({ rpcCall }) {
365
389
  setModel({
366
390
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
367
391
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
392
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
368
393
  });
369
394
  }
370
395
  setCredentialOpen(false);
@@ -428,6 +453,7 @@ export function WecomSettingsTab({ rpcCall }) {
428
453
  setModel({
429
454
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
430
455
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
456
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
431
457
  });
432
458
  }
433
459
  return snapshot;
@@ -509,6 +535,12 @@ export function WecomSettingsTab({ rpcCall }) {
509
535
  WECOM_ENDPOINTS.setWorkspace,
510
536
  { botId: account.botId, workspace },
511
537
  ),
538
+ onModelSave: (selectedModel) => botAction(
539
+ account,
540
+ 'model',
541
+ WECOM_ENDPOINTS.setModel,
542
+ { botId: account.botId, model: selectedModel },
543
+ ),
512
544
  onAgentPresetSave: (agentPreset) => botAction(
513
545
  account,
514
546
  'preset',
@@ -544,7 +576,9 @@ export function WecomSettingsTab({ rpcCall }) {
544
576
  })
545
577
  : null;
546
578
 
547
- return h(AgentPresetCatalogContext.Provider, {
579
+ return h(ModelCatalogContext.Provider, {
580
+ value: model.modelCatalog ?? EMPTY_MODEL_CATALOG,
581
+ }, h(AgentPresetCatalogContext.Provider, {
548
582
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
549
583
  }, h('section', { className: 'ddt-page dwecom-page dim-channelPage', 'aria-label': '企业微信设置' },
550
584
  h(Heading, {
@@ -565,5 +599,5 @@ export function WecomSettingsTab({ rpcCall }) {
565
599
  provisionView,
566
600
  model.bots.length === 0 && !provision && !credentialOpen
567
601
  ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
568
- botList)));
602
+ botList))));
569
603
  }
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
2
3
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
4
  import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
4
5
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
@@ -13,6 +14,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
13
14
  reconnectBot: 'bot.reconnect',
14
15
  deleteBot: 'bot.delete',
15
16
  setWorkspace: 'bot.workspace.set',
17
+ setModel: SET_MODEL_ENDPOINT,
16
18
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
17
19
  setContextEnhancement: 'bot.context-enhancement.set',
18
20
  setAccessPolicy: 'bot.access-policy.set',
@@ -124,6 +126,7 @@ function normalizeBot(value) {
124
126
  connected,
125
127
  configured: value.configured === true,
126
128
  workspace: string(value.workspace).slice(0, 4_096),
129
+ model: normalizeModelSelection(value.model),
127
130
  agentPreset: normalizeAgentPresetId(value.agentPreset),
128
131
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
129
132
  ...(Object.hasOwn(value, 'accessPolicy')
@@ -169,6 +172,7 @@ export function normalizeSnapshot(value) {
169
172
  provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : null,
170
173
  testMessage: normalizeTestMessage(value.testMessage),
171
174
  agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
175
+ modelCatalog: normalizeModelCatalog(value.modelCatalog),
172
176
  };
173
177
  }
174
178
 
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { WeixinLogoGlyph } from '../../channel-logos.js';
4
4
  import { QrActionIcon } from '../../credential-binding.js';
5
+ import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
5
6
  import { h } from '../../i18n.js';
6
7
  import {
7
8
  WEIXIN_ENDPOINTS,
@@ -21,6 +22,11 @@ import {
21
22
  AgentPresetEditor,
22
23
  EMPTY_AGENT_PRESET_CATALOG,
23
24
  } from '../../agent-preset.js';
25
+ import {
26
+ EMPTY_MODEL_CATALOG,
27
+ ModelCatalogContext,
28
+ ModelEditor,
29
+ } from '../../model-setting.js';
24
30
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
25
31
  import {
26
32
  BotSettingsButton,
@@ -208,6 +214,7 @@ export function AccountCard({
208
214
  removing,
209
215
  onReconnect,
210
216
  onWorkspaceSave,
217
+ onModelSave,
211
218
  onAgentPresetSave,
212
219
  onContextEnhancementSave,
213
220
  onRequestRemove,
@@ -219,11 +226,18 @@ export function AccountCard({
219
226
  const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
220
227
  return h('article', { className: 'dxw-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
221
228
  h('div', { className: 'dxw-cardBody dim-botCardBody' },
222
- h('div', { className: 'dxw-accountTop dim-botCardTop' },
229
+ h(CollapsibleAccountSection, {
230
+ id: `dxw-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
231
+ header: h('div', { className: 'dxw-accountTop dim-botCardTop' },
223
232
  h('div', { className: 'dxw-accountIdentity dim-botIdentity' },
224
233
  h('div', { className: 'dxw-avatar dim-botAvatar', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 27 })),
225
234
  h('div', { className: 'dim-botName' }, h('h3', null, account.bot.name), h('p', null, account.bot.accountIdMasked))),
226
- h('div', { className: 'dim-botCardTools' },
235
+ h('div', {
236
+ className: 'dim-botCardTools',
237
+ // The header is the collapse toggle; keep inner controls clickable.
238
+ onClick: (event) => { event.stopPropagation(); },
239
+ onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') event.stopPropagation(); },
240
+ },
227
241
  h(BotStatusMeta, {
228
242
  className: 'dxw-health',
229
243
  dotClassName: 'dxw-dot',
@@ -238,12 +252,18 @@ export function AccountCard({
238
252
  botName: account.bot.name,
239
253
  connected: account.connected,
240
254
  accessPolicy: account.accessPolicy,
241
- }))),
242
- h(WorkspaceEditor, {
255
+ })))
256
+ },
257
+ h(WorkspaceEditor, {
243
258
  workspace: account.workspace,
244
259
  disabled: Boolean(busy),
245
260
  onSave: onWorkspaceSave,
246
261
  }),
262
+ h(ModelEditor, {
263
+ model: account.model,
264
+ disabled: Boolean(busy),
265
+ onSave: onModelSave,
266
+ }),
247
267
  h(AgentPresetEditor, {
248
268
  agentPreset: account.agentPreset,
249
269
  disabled: Boolean(busy),
@@ -270,7 +290,9 @@ export function AccountCard({
270
290
  className: 'dxw-summary dim-cardFeedback',
271
291
  role: 'status',
272
292
  'aria-live': 'polite',
273
- }, feedback) : null))),
293
+ }, feedback) : null)),
294
+ ),
295
+ ),
274
296
  removing ? h('div', { className: 'dxw-confirm dim-confirm', role: 'alertdialog' },
275
297
  h('strong', null, '从此 Harness 移除这个微信账号?'),
276
298
  h('p', null, '这会停止消息连接,并删除本机保存的 bot_token、账号配置和会话映射。其他微信账号不受影响。'),
@@ -296,6 +318,7 @@ function AccountList(props) {
296
318
  removing: props.removeTarget === account.botId,
297
319
  onReconnect: () => props.onReconnect(account),
298
320
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
321
+ onModelSave: (model) => props.onModelSave(account, model),
299
322
  onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(account, agentPreset),
300
323
  onContextEnhancementSave: (config) => props.onContextEnhancementSave(account, config),
301
324
  onRequestRemove: () => props.onRequestRemove(account),
@@ -324,6 +347,7 @@ export function WeixinSettingsTab({ rpcCall }) {
324
347
  const [model, setModel] = React.useState({
325
348
  phase: 'loading', bots: [], totals: EMPTY_TOTALS, revision: 0, error: null,
326
349
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
350
+ modelCatalog: EMPTY_MODEL_CATALOG,
327
351
  });
328
352
  const [provision, setProvision] = React.useState(null);
329
353
  const [busy, setBusy] = React.useState(false);
@@ -367,6 +391,7 @@ export function WeixinSettingsTab({ rpcCall }) {
367
391
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
368
392
  revision: snapshot.revision, error: null,
369
393
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
394
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
370
395
  });
371
396
  if (snapshot.provisioning) {
372
397
  setProvision((current) => mergeWeixinProvisioningSnapshot(
@@ -558,6 +583,7 @@ export function WeixinSettingsTab({ rpcCall }) {
558
583
  setModel((current) => ({
559
584
  ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision,
560
585
  agentPresetCatalog: snapshot.agentPresetCatalog ?? current.agentPresetCatalog,
586
+ modelCatalog: snapshot.modelCatalog ?? current.modelCatalog,
561
587
  }));
562
588
  }
563
589
  const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
@@ -603,6 +629,7 @@ export function WeixinSettingsTab({ rpcCall }) {
603
629
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
604
630
  revision: snapshot.revision, error: null,
605
631
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
632
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
606
633
  });
607
634
  }
608
635
  } finally {
@@ -625,6 +652,7 @@ export function WeixinSettingsTab({ rpcCall }) {
625
652
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
626
653
  revision: snapshot.revision, error: null,
627
654
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
655
+ modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
628
656
  });
629
657
  }
630
658
  } finally {
@@ -646,6 +674,7 @@ export function WeixinSettingsTab({ rpcCall }) {
646
674
  setModel((current) => ({
647
675
  ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision,
648
676
  agentPresetCatalog: snapshot.agentPresetCatalog ?? current.agentPresetCatalog,
677
+ modelCatalog: snapshot.modelCatalog ?? current.modelCatalog,
649
678
  }));
650
679
  }
651
680
  setRemoveTarget(null);
@@ -686,7 +715,9 @@ export function WeixinSettingsTab({ rpcCall }) {
686
715
  });
687
716
  }
688
717
 
689
- return h(AgentPresetCatalogContext.Provider, {
718
+ return h(ModelCatalogContext.Provider, {
719
+ value: model.modelCatalog ?? EMPTY_MODEL_CATALOG,
720
+ }, h(AgentPresetCatalogContext.Provider, {
690
721
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
691
722
  }, h('section', { className: 'dxw-page dim-channelPage', 'aria-label': '微信设置' },
692
723
  h(Heading, {
@@ -721,6 +752,9 @@ export function WeixinSettingsTab({ rpcCall }) {
721
752
  removeTarget,
722
753
  onReconnect: (account) => void reconnect(account),
723
754
  onWorkspaceSave: saveWorkspace,
755
+ onModelSave: (account, selectedModel) => saveBotSetting(
756
+ account, 'model', WEIXIN_ENDPOINTS.setModel, { model: selectedModel },
757
+ ),
724
758
  onAgentPresetSave: (account, agentPreset) => saveBotSetting(
725
759
  account, 'preset', WEIXIN_ENDPOINTS.setAgentPreset, { agentPreset },
726
760
  ),
@@ -732,5 +766,5 @@ export function WeixinSettingsTab({ rpcCall }) {
732
766
  onCancelRemove: () => setRemoveTarget(null),
733
767
  })
734
768
  : null),
735
- ));
769
+ )));
736
770
  }
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
2
3
  import { normalizeLastMessageError } from '../../last-message-error.js';
3
4
  import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
4
5
  import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
@@ -14,6 +15,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
14
15
  deleteBot: 'bot.delete',
15
16
  setAccessPolicy: 'bot.access-policy.set',
16
17
  setWorkspace: 'bot.workspace.set',
18
+ setModel: SET_MODEL_ENDPOINT,
17
19
  setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
18
20
  setContextEnhancement: 'bot.context-enhancement.set',
19
21
  });
@@ -90,6 +92,7 @@ function normalizeBot(value) {
90
92
  connected,
91
93
  state: connected ? 'connected' : state,
92
94
  workspace: text(value.workspace, '', 4_096),
95
+ model: normalizeModelSelection(value.model),
93
96
  agentPreset: normalizeAgentPresetId(value.agentPreset),
94
97
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
95
98
  ...(Object.hasOwn(value, 'accessPolicy')
@@ -124,6 +127,7 @@ export function normalizeSnapshot(value) {
124
127
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
125
128
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
126
129
  agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
130
+ modelCatalog: normalizeModelCatalog(source.modelCatalog),
127
131
  };
128
132
  }
129
133