@xmanrui/dsh-im 0.17.0 → 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 (49) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +964 -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 +15 -0
  18. package/plugin-src/client/channels/weixin/index.js +54 -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 +29 -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/shared/image-prompt.mjs +32 -1
  47. package/src/channels/weixin/weixin-api.mjs +1 -1
  48. package/src/channels/weixin/weixin-bridge.mjs +23 -3
  49. package/src/channels/weixin/weixin-controller.mjs +15 -0
@@ -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']);
@@ -45,6 +48,15 @@ function normalizeTestMessage(value) {
45
48
  return { sent: false, code };
46
49
  }
47
50
 
51
+ function normalizeMessageError(value) {
52
+ if (!isRecord(value)) return null;
53
+ const code = string(value.code).slice(0, 64);
54
+ const reason = string(value.reason).slice(0, 128);
55
+ const message = string(value.message).slice(0, 500);
56
+ const at = timestamp(value.at);
57
+ return code && reason && message && at !== null ? { code, reason, message, at } : null;
58
+ }
59
+
48
60
  export function unwrapRpcResult(result) {
49
61
  if (!isRecord(result) || typeof result.ok !== 'boolean') {
50
62
  throw new Error('微信服务返回了无法识别的响应');
@@ -115,6 +127,7 @@ function normalizeBot(value) {
115
127
  connected,
116
128
  configured: value.configured === true,
117
129
  workspace: string(value.workspace).slice(0, 4_096),
130
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
118
131
  bot: {
119
132
  name: string(value.bot.name, '微信机器人'),
120
133
  accountIdMasked: string(value.bot.accountIdMasked, '已安全保存'),
@@ -128,6 +141,7 @@ function normalizeBot(value) {
128
141
  messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
129
142
  messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
130
143
  },
144
+ lastMessageError: normalizeMessageError(value.lastMessageError),
131
145
  error: isRecord(value.error)
132
146
  ? {
133
147
  code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
@@ -153,6 +167,7 @@ export function normalizeSnapshot(value) {
153
167
  },
154
168
  provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : null,
155
169
  testMessage: normalizeTestMessage(value.testMessage),
170
+ agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
156
171
  };
157
172
  }
158
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,8 +234,17 @@ 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,
244
+ account.lastMessageError ? h('div', {
245
+ className: 'dxw-summary dim-cardSummary',
246
+ role: 'status',
247
+ }, `最近一条消息处理失败:${account.lastMessageError.message}`) : null,
233
248
  feedback ? h('div', {
234
249
  className: 'dxw-summary dim-cardSummary',
235
250
  role: 'status',
@@ -260,6 +275,7 @@ function AccountList(props) {
260
275
  removing: props.removeTarget === account.botId,
261
276
  onReconnect: () => props.onReconnect(account),
262
277
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
278
+ onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(account, agentPreset),
263
279
  onRequestRemove: () => props.onRequestRemove(account),
264
280
  onConfirmRemove: () => props.onConfirmRemove(account),
265
281
  onCancelRemove: props.onCancelRemove,
@@ -285,6 +301,7 @@ export function mergeWeixinProvisioningSnapshot(
285
301
  export function WeixinSettingsTab({ rpcCall }) {
286
302
  const [model, setModel] = React.useState({
287
303
  phase: 'loading', bots: [], totals: EMPTY_TOTALS, revision: 0, error: null,
304
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
288
305
  });
289
306
  const [provision, setProvision] = React.useState(null);
290
307
  const [busy, setBusy] = React.useState(false);
@@ -327,6 +344,7 @@ export function WeixinSettingsTab({ rpcCall }) {
327
344
  setModel({
328
345
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
329
346
  revision: snapshot.revision, error: null,
347
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
330
348
  });
331
349
  if (snapshot.provisioning) {
332
350
  setProvision((current) => mergeWeixinProvisioningSnapshot(
@@ -515,7 +533,10 @@ export function WeixinSettingsTab({ rpcCall }) {
515
533
  { botId: account.botId, sendTest: true },
516
534
  ));
517
535
  if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
518
- 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
+ }));
519
540
  }
520
541
  const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
521
542
  let feedback;
@@ -559,6 +580,29 @@ export function WeixinSettingsTab({ rpcCall }) {
559
580
  setModel({
560
581
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals,
561
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,
562
606
  });
563
607
  }
564
608
  } finally {
@@ -577,7 +621,10 @@ export function WeixinSettingsTab({ rpcCall }) {
577
621
  confirm: true,
578
622
  }));
579
623
  if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
580
- 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
+ }));
581
628
  }
582
629
  setRemoveTarget(null);
583
630
  announce('微信账号及本机凭据已移除。');
@@ -617,7 +664,9 @@ export function WeixinSettingsTab({ rpcCall }) {
617
664
  });
618
665
  }
619
666
 
620
- 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': '微信设置' },
621
670
  h(Heading, {
622
671
  totals: model.totals,
623
672
  adding: Boolean(provision),
@@ -650,12 +699,13 @@ export function WeixinSettingsTab({ rpcCall }) {
650
699
  removeTarget,
651
700
  onReconnect: (account) => void reconnect(account),
652
701
  onWorkspaceSave: saveWorkspace,
702
+ onAgentPresetSave: saveAgentPreset,
653
703
  onRequestRemove: (account) => setRemoveTarget(account.botId),
654
704
  onConfirmRemove: (account) => void remove(account),
655
705
  onCancelRemove: () => setRemoveTarget(null),
656
706
  })
657
707
  : null),
658
- );
708
+ ));
659
709
  }
660
710
 
661
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',
@@ -449,6 +458,20 @@ const EN = Object.freeze({
449
458
  '微信绑定没有完成': 'WeChat setup did not complete',
450
459
  '微信连接正常': 'WeChat connection is healthy',
451
460
  '微信连接未就绪': 'WeChat connection is not ready',
461
+ '当前模型不支持图片,请用 /models 查看可用模型,再用 /model <序号> 切换后重发。': 'The current model does not support images. Use /models to list models, then /model <number> to switch and resend.',
462
+ '图片超过宿主允许的大小,请压缩后重试。': 'The image exceeds the Host size limit. Compress it and try again.',
463
+ '图片分辨率过高,请压缩后重试。': 'The image resolution is too high. Compress it and try again.',
464
+ '图片内容无效或格式不受支持,请重新发送。': 'The image is invalid or unsupported. Send it again.',
465
+ '未能读取图片内容,请重新发送。': 'The image could not be read. Send it again.',
466
+ '图片格式与实际内容不一致,请重新发送。': 'The declared image format does not match its content. Send it again.',
467
+ '一次发送的图片数量超过宿主限制,请减少后重试。': 'The message exceeds the Host image-count limit. Remove some images and try again.',
468
+ '图片总大小超过宿主限制,请减少图片或压缩后重试。': 'The images exceed the Host total-size limit. Remove or compress some images and try again.',
469
+ '图片下载地址发生了重定向,暂时无法读取。': 'The image download redirected and cannot be read.',
470
+ '图片超过 5 MB,请压缩后重试。': 'The image exceeds 5 MB. Compress it and try again.',
471
+ '一次发送的图片总大小过大,请减少图片数量或压缩后重试。': 'The images are too large in total. Remove or compress some images and try again.',
472
+ '图片下载失败,请重新发送后再试。': 'The image download failed. Send it again.',
473
+ '暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。': 'This image format is unsupported. Send a JPEG, PNG, WebP, or GIF image.',
474
+ '消息处理失败,请稍后重试。': 'Message processing failed. Try again later.',
452
475
  '微信服务没有返回有效的账号列表': 'WeChat did not return a valid account list',
453
476
  '尚未绑定微信': 'No WeChat account connected yet',
454
477
  '用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码': 'One-time QR code for connecting a WeChat bot to DeepSeek Harness',
@@ -522,6 +545,12 @@ function translateDynamic(text) {
522
545
  }
523
546
  match = /^二维码剩余 (.+)$/.exec(text);
524
547
  if (match) return `QR code expires in ${match[1]}`;
548
+ match = /^最近一条消息处理失败:(.+)$/.exec(text);
549
+ if (match) return `Latest message failed: ${localizeText(match[1])}`;
550
+ match = /^图片下载失败(HTTP (.+)),请重新发送后再试。$/.exec(text);
551
+ if (match) return `The image download failed (HTTP ${match[1]}). Send it again.`;
552
+ match = /^一次最多只能处理 (\d+) 张图片。$/.exec(text);
553
+ if (match) return `A message can contain at most ${match[1]} images.`;
525
554
  match = /^状态刷新失败:(.+)$/.exec(text);
526
555
  if (match) return `Status refresh failed: ${match[1]}`;
527
556
  match = /^状态自动刷新失败:(.+)$/.exec(text);
@@ -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%); }