@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+
3
+ import { h } from './i18n.js';
4
+
5
+ export const SET_AGENT_PRESET_ENDPOINT = 'bot.preset.set';
6
+
7
+ const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
8
+
9
+ export const EMPTY_AGENT_PRESET_CATALOG = Object.freeze({
10
+ defaultId: '',
11
+ items: Object.freeze([]),
12
+ });
13
+
14
+ export const AgentPresetCatalogContext = React.createContext(EMPTY_AGENT_PRESET_CATALOG);
15
+
16
+ export function normalizeAgentPresetId(value) {
17
+ if (typeof value !== 'string') return '';
18
+ const id = value.trim();
19
+ return PRESET_ID.test(id) ? id : '';
20
+ }
21
+
22
+ export function normalizeAgentPresetCatalog(value) {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
24
+ return { defaultId: '', items: [] };
25
+ }
26
+ const items = [];
27
+ const seen = new Set();
28
+ for (const entry of Array.isArray(value.items) ? value.items : []) {
29
+ const id = typeof entry === 'string'
30
+ ? normalizeAgentPresetId(entry)
31
+ : normalizeAgentPresetId(entry?.id);
32
+ if (!id || seen.has(id)) continue;
33
+ seen.add(id);
34
+ const label = typeof entry?.label === 'string' && entry.label.trim()
35
+ ? entry.label.trim().slice(0, 128)
36
+ : typeof entry?.name === 'string' && entry.name.trim()
37
+ ? entry.name.trim().slice(0, 128)
38
+ : id;
39
+ items.push({ id, label });
40
+ }
41
+ return {
42
+ defaultId: normalizeAgentPresetId(value.defaultId),
43
+ items,
44
+ };
45
+ }
46
+
47
+ export function AgentPresetEditor({ agentPreset = '', disabled = false, onSave }) {
48
+ const catalog = React.useContext(AgentPresetCatalogContext) ?? EMPTY_AGENT_PRESET_CATALOG;
49
+ const current = normalizeAgentPresetId(agentPreset);
50
+ const [saving, setSaving] = React.useState(false);
51
+ const [error, setError] = React.useState(null);
52
+
53
+ const items = [];
54
+ const seen = new Set();
55
+ for (const item of Array.isArray(catalog.items) ? catalog.items : []) {
56
+ if (!item?.id || seen.has(item.id)) continue;
57
+ seen.add(item.id);
58
+ items.push(item);
59
+ }
60
+ const currentUnavailable = Boolean(current && !seen.has(current));
61
+ if (currentUnavailable) items.push({ id: current, label: current, unavailable: true });
62
+
63
+ const inheritLabel = '跟随 Host 默认';
64
+
65
+ const change = async (event) => {
66
+ const next = event.target.value;
67
+ if (next === current || saving || disabled) return;
68
+ setSaving(true);
69
+ setError(null);
70
+ try {
71
+ await onSave?.(next || null);
72
+ } catch (cause) {
73
+ setError(cause?.message ?? 'Agent Preset 修改失败,请重试。');
74
+ } finally {
75
+ setSaving(false);
76
+ }
77
+ };
78
+
79
+ return h('div', { className: 'dim-preset' },
80
+ h('div', { className: 'dim-presetHeader' },
81
+ h('span', null, 'Agent Preset'),
82
+ saving ? h('span', { className: 'dim-presetStatus' }, '保存中…') : null),
83
+ React.createElement('select', {
84
+ className: 'dim-presetSelect',
85
+ value: current,
86
+ disabled: disabled || saving,
87
+ 'aria-label': 'Agent Preset',
88
+ onChange: (event) => { void change(event); },
89
+ },
90
+ h('option', { value: '' }, inheritLabel),
91
+ ...items.map((item) => h(
92
+ 'option',
93
+ { key: item.id, value: item.id },
94
+ item.unavailable
95
+ ? [item.id, '(已不可用)']
96
+ : item.label && item.label !== item.id ? `${item.label}(${item.id})` : item.id,
97
+ )),
98
+ ),
99
+ h(
100
+ 'small',
101
+ { className: 'dim-presetHelp' },
102
+ '只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。',
103
+ ),
104
+ error || currentUnavailable ? h(
105
+ 'p',
106
+ { className: 'dim-presetError', role: error ? 'alert' : 'status' },
107
+ error ?? '当前 Agent Preset 已不可用,请选择其他 Preset 或跟随 Host 默认。',
108
+ ) : null,
109
+ );
110
+ }
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  export const DINGTALK_RPC_CHANNEL = '/dingtalk';
2
4
 
3
5
  export const DINGTALK_ENDPOINTS = Object.freeze({
@@ -9,6 +11,7 @@ export const DINGTALK_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 ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@@ -157,6 +160,7 @@ function normalizeBot(value) {
157
160
  connected,
158
161
  configured: value.configured !== false,
159
162
  workspace: optionalString(value.workspace, 4_096) ?? '',
163
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
160
164
  bot: {
161
165
  name: optionalString(bot.name, 100) ?? '钉钉机器人',
162
166
  clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
@@ -200,6 +204,7 @@ export function normalizeSnapshot(value) {
200
204
  },
201
205
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
202
206
  testMessage: normalizeTestMessage(source.testMessage),
207
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
203
208
  };
204
209
  }
205
210
 
@@ -3,6 +3,11 @@ import * as React from 'react';
3
3
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
4
4
  import { h } from '../../i18n.js';
5
5
  import { WorkspaceEditor } from '../../workspace-editor.js';
6
+ import {
7
+ AgentPresetCatalogContext,
8
+ AgentPresetEditor,
9
+ EMPTY_AGENT_PRESET_CATALOG,
10
+ } from '../../agent-preset.js';
6
11
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
7
12
  import {
8
13
  DINGTALK_ENDPOINTS,
@@ -213,6 +218,7 @@ export function AccountCard({
213
218
  removing,
214
219
  onReconnect,
215
220
  onWorkspaceSave,
221
+ onAgentPresetSave,
216
222
  onRequestRemove,
217
223
  onConfirmRemove,
218
224
  onCancelRemove,
@@ -241,6 +247,11 @@ export function AccountCard({
241
247
  disabled: Boolean(busy),
242
248
  onSave: onWorkspaceSave,
243
249
  }),
250
+ h(AgentPresetEditor, {
251
+ agentPreset: account.agentPreset,
252
+ disabled: Boolean(busy),
253
+ onSave: onAgentPresetSave,
254
+ }),
244
255
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
245
256
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
246
257
  feedback ? h('div', {
@@ -272,6 +283,7 @@ function AccountList(props) {
272
283
  removing: props.removeTarget === account.botId,
273
284
  onReconnect: () => props.onReconnect(account),
274
285
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
286
+ onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(account, agentPreset),
275
287
  onRequestRemove: () => props.onRequestRemove(account),
276
288
  onConfirmRemove: () => props.onConfirmRemove(account),
277
289
  onCancelRemove: props.onCancelRemove,
@@ -283,6 +295,7 @@ const EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 });
283
295
  export function DingtalkSettingsTab({ rpcCall }) {
284
296
  const [model, setModel] = React.useState({
285
297
  phase: 'loading', bots: [], totals: EMPTY_TOTALS, revision: 0, error: null,
298
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
286
299
  });
287
300
  const [provision, setProvision] = React.useState(null);
288
301
  const [busy, setBusy] = React.useState(false);
@@ -389,6 +402,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
389
402
  totals: snapshot.totals,
390
403
  revision: snapshot.revision,
391
404
  error: null,
405
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
392
406
  });
393
407
  discardStaleFeedback(snapshot);
394
408
  if (restoreProvisioning && snapshot.provisioning) {
@@ -504,6 +518,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
504
518
  totals: snapshot.totals,
505
519
  revision: snapshot.revision,
506
520
  error: null,
521
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
507
522
  });
508
523
  discardStaleFeedback(snapshot);
509
524
  }
@@ -638,6 +653,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
638
653
  totals: snapshot.totals,
639
654
  revision: snapshot.revision,
640
655
  error: null,
656
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
641
657
  });
642
658
  discardStaleFeedback(snapshot);
643
659
  }
@@ -700,6 +716,33 @@ export function DingtalkSettingsTab({ rpcCall }) {
700
716
  totals: snapshot.totals,
701
717
  revision: snapshot.revision,
702
718
  error: null,
719
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
720
+ });
721
+ discardStaleFeedback(snapshot);
722
+ }
723
+ } finally {
724
+ const shouldRefresh = workspaceFence.endMutation();
725
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
726
+ if (mountedRef.current) setBotBusy(account.botId, null);
727
+ }
728
+ }, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
729
+
730
+ const saveAgentPreset = React.useCallback(async (account, agentPreset) => {
731
+ const snapshotVersion = workspaceFence.beginMutation();
732
+ setBotBusy(account.botId, 'preset');
733
+ try {
734
+ const snapshot = normalizeSnapshot(await invoke(
735
+ DINGTALK_ENDPOINTS.setAgentPreset,
736
+ { botId: account.botId, agentPreset },
737
+ ));
738
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
739
+ setModel({
740
+ phase: 'ready',
741
+ bots: snapshot.bots,
742
+ totals: snapshot.totals,
743
+ revision: snapshot.revision,
744
+ error: null,
745
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
703
746
  });
704
747
  discardStaleFeedback(snapshot);
705
748
  }
@@ -762,7 +805,9 @@ export function DingtalkSettingsTab({ rpcCall }) {
762
805
  })
763
806
  : null;
764
807
 
765
- return h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
808
+ return h(AgentPresetCatalogContext.Provider, {
809
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
810
+ }, h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
766
811
  h(Heading, {
767
812
  totals: model.totals,
768
813
  adding: Boolean(provision),
@@ -798,11 +843,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
798
843
  removeTarget,
799
844
  onReconnect: (account) => void reconnect(account),
800
845
  onWorkspaceSave: saveWorkspace,
846
+ onAgentPresetSave: saveAgentPreset,
801
847
  onRequestRemove: (account) => setRemoveTarget(account.botId),
802
848
  onConfirmRemove: (account) => void remove(account),
803
849
  onCancelRemove: () => setRemoveTarget(null),
804
850
  })
805
- : null));
851
+ : null)));
806
852
  }
807
853
 
808
854
  export function apply(ctx) {
@@ -6,6 +6,8 @@
6
6
  * must never be returned by any endpoint on this channel.
7
7
  */
8
8
 
9
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
10
+
9
11
  export const FEISHU_RPC_CHANNEL = "/feishu";
10
12
 
11
13
  export const FEISHU_ENDPOINTS = Object.freeze({
@@ -19,6 +21,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
19
21
  disconnectBot: "bot.disconnect",
20
22
  deleteBot: "bot.delete",
21
23
  setWorkspace: "bot.workspace.set",
24
+ setAgentPreset: "bot.preset.set",
22
25
  // Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
23
26
  testConnection: "connection.test",
24
27
  disconnect: "connection.disconnect",
@@ -182,6 +185,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
182
185
  connected,
183
186
  configured: value.configured !== false,
184
187
  workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
188
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
185
189
  bot: normalizeBot(value.bot),
186
190
  health: normalizeHealth(value.health, connected),
187
191
  error: normalizeError(value.error),
@@ -236,6 +240,7 @@ export function normalizeBotsSnapshot(value) {
236
240
  ? normalizeProvisioning(value.provisioning)
237
241
  : undefined,
238
242
  error: normalizeError(value.error),
243
+ agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
239
244
  };
240
245
  }
241
246
 
@@ -16,6 +16,11 @@ import {
16
16
  } from "./api.js";
17
17
  import { 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 { installFeishuStyles } from "./styles.js";
21
26
 
@@ -418,6 +423,7 @@ export function BotCard({
418
423
  onReconnect,
419
424
  onRepairCallback,
420
425
  onWorkspaceSave,
426
+ onAgentPresetSave,
421
427
  onRequestRemove,
422
428
  onConfirmRemove,
423
429
  onCancelRemove,
@@ -468,6 +474,11 @@ export function BotCard({
468
474
  disabled: Boolean(busy),
469
475
  onSave: onWorkspaceSave,
470
476
  }),
477
+ h(AgentPresetEditor, {
478
+ agentPreset: connection.agentPreset,
479
+ disabled: Boolean(busy),
480
+ onSave: onAgentPresetSave,
481
+ }),
471
482
  h("div", { className: "bxf-connectedFooter dim-cardFooter" },
472
483
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
473
484
  summary) : null,
@@ -524,6 +535,7 @@ function BotList(props) {
524
535
  onReconnect: () => props.onReconnect(bot),
525
536
  onRepairCallback: () => props.onRepairCallback(bot),
526
537
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
538
+ onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
527
539
  onRequestRemove: () => props.onRequestRemove(bot),
528
540
  onConfirmRemove: () => props.onConfirmRemove(bot),
529
541
  onCancelRemove: props.onCancelRemove,
@@ -575,6 +587,7 @@ export function mergeFeishuSnapshotState(
575
587
  provisioning,
576
588
  pageError: null,
577
589
  statusError: null,
590
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? current.agentPresetCatalog,
578
591
  };
579
592
  }
580
593
 
@@ -587,6 +600,7 @@ export function FeishuSettingsTab({ rpcCall }) {
587
600
  provisioning: null,
588
601
  pageError: null,
589
602
  statusError: null,
603
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
590
604
  });
591
605
  const [pageBusy, setPageBusy] = React.useState(false);
592
606
  const [provisionBusy, setProvisionBusy] = React.useState(false);
@@ -1067,6 +1081,26 @@ export function FeishuSettingsTab({ rpcCall }) {
1067
1081
  }
1068
1082
  }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
1069
1083
 
1084
+ const saveAgentPreset = React.useCallback(async (connection, agentPreset) => {
1085
+ const { botId } = connection;
1086
+ const snapshotVersion = workspaceFence.beginMutation();
1087
+ setBotBusy(botId, "preset");
1088
+ setBotError(botId, null);
1089
+ try {
1090
+ const snapshot = normalizeBotsSnapshot(await invoke(
1091
+ FEISHU_ENDPOINTS.setAgentPreset,
1092
+ { botId, agentPreset },
1093
+ ));
1094
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
1095
+ mergeSnapshot(snapshot);
1096
+ }
1097
+ } finally {
1098
+ const shouldRefresh = workspaceFence.endMutation();
1099
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
1100
+ if (mountedRef.current) setBotBusy(botId, null);
1101
+ }
1102
+ }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
1103
+
1070
1104
  const requestRemove = React.useCallback((connection) => {
1071
1105
  setRemoveTargetId(connection.botId);
1072
1106
  }, []);
@@ -1173,7 +1207,9 @@ export function FeishuSettingsTab({ rpcCall }) {
1173
1207
  else removeButtonRefs.current.delete(botId);
1174
1208
  }, []);
1175
1209
 
1176
- return h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
1210
+ return h(AgentPresetCatalogContext.Provider, {
1211
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
1212
+ }, h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
1177
1213
  h(Heading, {
1178
1214
  totals: model.totals,
1179
1215
  onAdd: () => void startProvisioning(),
@@ -1217,6 +1253,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1217
1253
  onReconnect: (bot) => void reconnectOneBot(bot),
1218
1254
  onRepairCallback: repairCallback,
1219
1255
  onWorkspaceSave: saveWorkspace,
1256
+ onAgentPresetSave: saveAgentPreset,
1220
1257
  onRequestRemove: requestRemove,
1221
1258
  onConfirmRemove: (bot) => void confirmRemove(bot),
1222
1259
  onCancelRemove: cancelRemove,
@@ -1225,7 +1262,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1225
1262
  })
1226
1263
  : null,
1227
1264
  ),
1228
- );
1265
+ ));
1229
1266
  }
1230
1267
 
1231
1268
  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 QQ_RPC_CHANNEL = '/qq';
2
4
 
3
5
  export const QQ_ENDPOINTS = Object.freeze({
@@ -9,6 +11,7 @@ export const QQ_ENDPOINTS = Object.freeze({
9
11
  reconnectBot: 'bot.reconnect',
10
12
  deleteBot: 'bot.delete',
11
13
  setWorkspace: 'bot.workspace.set',
14
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
12
15
  });
13
16
 
14
17
  const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
@@ -82,6 +85,7 @@ function normalizeBot(value) {
82
85
  connected,
83
86
  state: connected ? 'connected' : state,
84
87
  workspace: text(value.workspace, '', 4_096),
88
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
85
89
  bot: {
86
90
  name: text(value.bot?.name, 'QQ机器人', 100),
87
91
  appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
@@ -117,6 +121,7 @@ export function normalizeSnapshot(value) {
117
121
  bots,
118
122
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
119
123
  provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
124
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
120
125
  ...(testMessage ? { testMessage } : {}),
121
126
  };
122
127
  }
@@ -4,6 +4,11 @@ import { QqLogoGlyph } from '../../channel-logos.js';
4
4
  import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
5
5
  import { h } from '../../i18n.js';
6
6
  import { WorkspaceEditor } from '../../workspace-editor.js';
7
+ import {
8
+ AgentPresetCatalogContext,
9
+ AgentPresetEditor,
10
+ EMPTY_AGENT_PRESET_CATALOG,
11
+ } from '../../agent-preset.js';
7
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
8
13
  import { installDingtalkStyles } from '../dingtalk/styles.js';
9
14
  import {
@@ -156,6 +161,7 @@ export function AccountCard({
156
161
  removing,
157
162
  onReconnect,
158
163
  onWorkspaceSave,
164
+ onAgentPresetSave,
159
165
  onRequestRemove,
160
166
  onConfirmRemove,
161
167
  onCancelRemove,
@@ -180,6 +186,11 @@ export function AccountCard({
180
186
  disabled: Boolean(busy),
181
187
  onSave: onWorkspaceSave,
182
188
  }),
189
+ h(AgentPresetEditor, {
190
+ agentPreset: account.agentPreset,
191
+ disabled: Boolean(busy),
192
+ onSave: onAgentPresetSave,
193
+ }),
183
194
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
184
195
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
185
196
  feedback ? h('div', {
@@ -196,7 +207,10 @@ export function AccountCard({
196
207
  }
197
208
 
198
209
  export function QqSettingsTab({ rpcCall }) {
199
- const [model, setModel] = React.useState({ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null });
210
+ const [model, setModel] = React.useState({
211
+ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
212
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
213
+ });
200
214
  const [provision, setProvision] = React.useState(null);
201
215
  const [busy, setBusy] = React.useState(false);
202
216
  const [busyByBot, setBusyByBot] = React.useState({});
@@ -233,7 +247,10 @@ export function QqSettingsTab({ rpcCall }) {
233
247
  const snapshot = normalizeSnapshot(await invoke(QQ_ENDPOINTS.status, {}, signal));
234
248
  if (!mounted.current || signal?.aborted
235
249
  || !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
236
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
250
+ setModel({
251
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
252
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
253
+ });
237
254
  if (restore && snapshot.provisioning) setProvision({
238
255
  ...snapshot.provisioning,
239
256
  durationMs: Math.max(1, snapshot.provisioning.expiresAt - Date.now()),
@@ -297,7 +314,10 @@ export function QqSettingsTab({ rpcCall }) {
297
314
  ));
298
315
  if (!mounted.current) return;
299
316
  if (workspaceFence.canCommitMutation(snapshotVersion)) {
300
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
317
+ setModel({
318
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
319
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
320
+ });
301
321
  }
302
322
  setCredentialOpen(false);
303
323
  } catch (error) {
@@ -357,7 +377,10 @@ export function QqSettingsTab({ rpcCall }) {
357
377
  try {
358
378
  const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
359
379
  if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
360
- setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
380
+ setModel({
381
+ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
382
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
383
+ });
361
384
  }
362
385
  return snapshot;
363
386
  } finally {
@@ -424,6 +447,12 @@ export function QqSettingsTab({ rpcCall }) {
424
447
  QQ_ENDPOINTS.setWorkspace,
425
448
  { botId: account.botId, workspace },
426
449
  ),
450
+ onAgentPresetSave: (agentPreset) => botAction(
451
+ account,
452
+ 'preset',
453
+ QQ_ENDPOINTS.setAgentPreset,
454
+ { botId: account.botId, agentPreset },
455
+ ),
427
456
  onRequestRemove: () => setRemoveTarget(account.botId),
428
457
  onCancelRemove: () => setRemoveTarget(null),
429
458
  onConfirmRemove: async () => {
@@ -447,7 +476,9 @@ export function QqSettingsTab({ rpcCall }) {
447
476
  })
448
477
  : null;
449
478
 
450
- return h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
479
+ return h(AgentPresetCatalogContext.Provider, {
480
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
481
+ }, h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
451
482
  h(Heading, {
452
483
  totals: model.totals,
453
484
  adding: Boolean(provision),
@@ -465,7 +496,7 @@ export function QqSettingsTab({ rpcCall }) {
465
496
  provisionView,
466
497
  model.bots.length === 0 && !provision && !credentialOpen
467
498
  ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
468
- botList));
499
+ botList)));
469
500
  }
470
501
 
471
502
  export function apply(ctx) {
@@ -1,3 +1,5 @@
1
+ import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+
1
3
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
2
4
 
3
5
  function isRecord(value) {
@@ -25,6 +27,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
25
27
  reconnectBot: 'bot.reconnect',
26
28
  deleteBot: 'bot.delete',
27
29
  setWorkspace: 'bot.workspace.set',
30
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
28
31
  });
29
32
 
30
33
  export function createTokenChannelApi(channel, connectionSummary, {
@@ -52,6 +55,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
52
55
  connected,
53
56
  state: connected ? 'connected' : state,
54
57
  workspace: text(value.workspace, '', 4_096),
58
+ agentPreset: normalizeAgentPresetId(value.agentPreset),
55
59
  bot: {
56
60
  name: text(value.bot?.name, `${channel}机器人`, 100),
57
61
  username: text(value.bot?.username, '', 100),
@@ -82,6 +86,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
82
86
  revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
83
87
  bots,
84
88
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
89
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
85
90
  };
86
91
  };
87
92