@xmanrui/dsh-im 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +1297 -600
  4. package/lib/index.js +190 -145
  5. package/package.json +1 -1
  6. package/plugin-src/client/agent-preset.js +110 -0
  7. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +48 -2
  9. package/plugin-src/client/channels/feishu/api.js +28 -5
  10. package/plugin-src/client/channels/feishu/index.js +330 -41
  11. package/plugin-src/client/channels/feishu/styles.js +46 -0
  12. package/plugin-src/client/channels/qq/api.js +5 -0
  13. package/plugin-src/client/channels/qq/index.js +37 -6
  14. package/plugin-src/client/channels/shared/token-api.js +5 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +34 -6
  16. package/plugin-src/client/channels/wecom/api.js +5 -0
  17. package/plugin-src/client/channels/wecom/index.js +37 -6
  18. package/plugin-src/client/channels/weixin/api.js +5 -0
  19. package/plugin-src/client/channels/weixin/index.js +50 -4
  20. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +30 -4
  22. package/plugin-src/client/i18n.js +59 -0
  23. package/plugin-src/client/styles.js +8 -0
  24. package/plugin-src/host/channels/dingtalk/production.mjs +10 -4
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +12 -0
  26. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +1 -1
  27. package/plugin-src/host/channels/feishu/production.mjs +8 -3
  28. package/plugin-src/host/channels/feishu/rpc.mjs +78 -7
  29. package/plugin-src/host/channels/qq/production.mjs +10 -4
  30. package/plugin-src/host/channels/qq/rpc.mjs +12 -0
  31. package/plugin-src/host/channels/shared/agent-preset-rpc.mjs +25 -0
  32. package/plugin-src/host/channels/shared/production.mjs +10 -4
  33. package/plugin-src/host/channels/shared/rpc.mjs +12 -0
  34. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  35. package/plugin-src/host/channels/slack/production.mjs +10 -4
  36. package/plugin-src/host/channels/slack/rpc.mjs +13 -0
  37. package/plugin-src/host/channels/wecom/production.mjs +10 -4
  38. package/plugin-src/host/channels/wecom/rpc.mjs +12 -0
  39. package/plugin-src/host/channels/weixin/production.mjs +10 -4
  40. package/plugin-src/host/channels/weixin/rpc.mjs +15 -0
  41. package/plugin-src/host/channels/whatsapp/production.mjs +10 -4
  42. package/plugin-src/host/channels/whatsapp/rpc.mjs +12 -0
  43. package/src/channels/discord/discord-api.mjs +1 -1
  44. package/src/channels/feishu/bridge.mjs +29 -4
  45. package/src/channels/feishu/feishu-runtime.mjs +14 -0
  46. package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
  47. package/src/channels/feishu/group-response-mode.mjs +15 -0
  48. package/src/channels/feishu/multi-bot-controller.mjs +258 -9
  49. package/src/channels/feishu/plugin-config-store.mjs +3 -0
  50. package/src/channels/feishu/repair-manager.mjs +17 -3
  51. package/src/channels/shared/agent-preset.mjs +74 -0
  52. package/src/channels/shared/bot-workspace-store.mjs +141 -14
  53. package/src/channels/shared/harness-client.mjs +6 -4
  54. package/src/channels/telegram/telegram-api.mjs +31 -0
  55. package/src/channels/telegram/telegram-runtime.mjs +25 -1
  56. package/src/channels/weixin/weixin-api.mjs +1 -1
@@ -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
  }
@@ -68,6 +68,24 @@ const EN = Object.freeze({
68
68
  'WhatsApp账号': 'WhatsApp account',
69
69
  '微信设置': 'WeChat settings',
70
70
  '飞书机器人设置': 'Feishu bot settings',
71
+ '群聊响应方式': 'Group response mode',
72
+ '仅在 @机器人时响应(推荐)': 'Only respond when @mentioned (recommended)',
73
+ '响应所有群消息': 'Respond to all group messages',
74
+ '响应所有群消息(需飞书敏感权限)': 'Respond to all group messages (requires a sensitive Feishu scope)',
75
+ '重新授权': 'Reauthorize',
76
+ '去授权': 'Authorize',
77
+ '重新授权群消息权限': 'Reauthorize group-message permission',
78
+ '授权群消息权限': 'Authorize group-message permission',
79
+ '正在准备授权…': 'Preparing authorization…',
80
+ '正在准备…': 'Preparing…',
81
+ '私聊始终响应;群聊仅处理明确 @当前机器人的消息。群消息权限已开通,再次切换无需授权。': 'Direct messages always work; group chats require an explicit @mention of this bot. The group-message permission is already granted, so switching again needs no authorization.',
82
+ '私聊始终响应;群聊仅处理明确 @当前机器人的消息。选择全部消息后会打开飞书官方授权流程。': 'Direct messages always work; group chats require an explicit @mention of this bot. Selecting all messages opens the official Feishu authorization flow.',
83
+ '已开通“获取群组中所有消息”权限(im:message.group_msg);机器人会处理群聊中的所有可见消息。': 'The “Read all messages in associated group chat” scope (im:message.group_msg) is granted; the bot processes every visible group message.',
84
+ '尚未确认“获取群组中所有消息”权限,请完成飞书授权。': 'The “Read all messages in associated group chat” scope has not been confirmed. Complete Feishu authorization.',
85
+ '私聊始终响应;群聊仅处理明确 @当前机器人的消息。': 'Direct messages always work; group chats require an explicit @mention of this bot.',
86
+ '需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。': 'Grant this bot the “Read all messages in associated group chat” Feishu scope (im:message.group_msg); once granted, it will process every visible group message.',
87
+ '群聊响应方式修改失败,请重试。': 'Could not update the group response mode. Try again.',
88
+ '群消息权限授权失败,请重试。': 'Could not authorize group-message permission. Try again.',
71
89
  '钉钉设置': 'DingTalk settings',
72
90
  '企业微信设置': 'WeCom settings',
73
91
  '扫码接入机器人': 'Scan QR code',
@@ -117,6 +135,15 @@ const EN = Object.freeze({
117
135
  '工作区路径不存在。': 'The workspace path does not exist.',
118
136
  '工作区路径必须指向一个目录。': 'The workspace path must point to a directory.',
119
137
  '找不到要修改的机器人。': 'The bot could not be found.',
138
+ 'Agent Preset': 'Agent Preset',
139
+ '跟随 Host 默认': 'Follow the Host default',
140
+ '(已不可用)': ' (unavailable)',
141
+ '只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。': 'This affects only new sessions. If the current chat already has a session, send /new, then send a regular message to apply it.',
142
+ '当前 Agent Preset 已不可用,请选择其他 Preset 或跟随 Host 默认。': 'The current Agent Preset is unavailable. Choose another preset or follow the Host default.',
143
+ 'Agent Preset 修改失败,请重试。': 'Could not update the Agent Preset. Try again.',
144
+ '请选择 Agent Preset。': 'Choose an Agent Preset.',
145
+ 'Agent Preset 无效。': 'The Agent Preset is invalid.',
146
+ 'Agent Preset 不存在或不可用。': 'The Agent Preset does not exist or is unavailable.',
120
147
  '尚未检查': 'Not checked yet',
121
148
  '刚刚': 'Just now',
122
149
  '检查连接': 'Check connection',
@@ -181,6 +208,7 @@ const EN = Object.freeze({
181
208
  '生成 QQ 二维码': 'Generate QQ QR code',
182
209
  '正在生成二维码…': 'Generating QR code…',
183
210
  '正在准备授权二维码': 'Preparing authorization QR code',
211
+ '正在准备权限授权二维码': 'Preparing permission authorization QR code',
184
212
  '正在准备微信二维码': 'Preparing WeChat QR code',
185
213
  '正在添加新机器人': 'Adding a new bot',
186
214
  '正在申请钉钉授权二维码…': 'Requesting DingTalk authorization QR code…',
@@ -198,6 +226,16 @@ const EN = Object.freeze({
198
226
  '在飞书中打开': 'Open in Feishu',
199
227
  '取消添加': 'Cancel',
200
228
  '使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
229
+ '使用飞书确认群消息权限': 'Confirm group-message permission with Feishu',
230
+ '扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the “Read all messages in associated group chat” scope. It does not create a new app. After confirmation, “Respond to all group messages” is enabled automatically; other bots are unaffected.',
231
+ '核对现有应用,并确认“获取群组中所有消息”权限': 'Review the existing app and confirm the “Read all messages in associated group chat” permission',
232
+ '保持本页打开,等待权限生效并自动切换响应方式': 'Keep this page open while the permission takes effect and the response mode switches automatically',
233
+ '取消授权': 'Cancel authorization',
234
+ '已确认,正在启用全部消息模式': 'Confirmed. Enabling all-message mode',
235
+ '权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Saving the setting and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
236
+ '权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
237
+ '正在为现有飞书应用申请群消息权限二维码,请稍候。': 'Requesting a group-message permission QR code for the existing Feishu app…',
238
+ '群消息权限没有开通完成': 'Group-message permission was not granted',
201
239
  '扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the card-button callback. It does not create a new app. This bot reconnects briefly after confirmation; other bots are not affected.',
202
240
  '核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
203
241
  '保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
@@ -210,14 +248,25 @@ const EN = Object.freeze({
210
248
  '修复卡片按钮': 'Repair card buttons',
211
249
  '等待扫码…': 'Waiting for scan…',
212
250
  '飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
251
+ '飞书服务返回了不匹配的群消息权限二维码': 'Feishu returned a group-message permission QR code for a different bot',
213
252
  '飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
253
+ '飞书服务返回的应用更新信息缺少 botId': 'Feishu app-update status is missing the bot ID',
214
254
  '飞书服务返回了不匹配的注册进度': 'Feishu returned registration progress for a different operation',
215
255
  '此机器人': 'this bot',
256
+ '用于为${botName}开通群消息权限的一次性授权二维码': 'One-time QR code for granting group-message permission to ${botName}',
257
+ '正在为「${botName}」开通群消息权限': 'Granting group-message permission to “${botName}”',
216
258
  '${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
259
+ '${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。': 'Group-message permission QR code generated for ${botName ?? "bot"}. Confirm it with Feishu.',
260
+ '${targetBotName}已开通群消息权限,并启用“响应所有群消息”。': '${targetBotName} now has group-message permission and “Respond to all group messages” is enabled.',
261
+ '${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。': '${targetBot.bot.name} now has group-message permission and “Respond to all group messages” is enabled.',
217
262
  '${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
218
263
  '已取消卡片按钮修复。': 'Card-button repair was cancelled.',
264
+ '已取消群消息权限授权。': 'Group-message permission authorization was cancelled.',
219
265
  '卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
266
+ '群消息权限已更新,但暂时无法确认机器人连接状态': 'The group-message permission was updated, but the bot connection could not be confirmed yet',
220
267
  '飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
268
+ '飞书群消息权限开通失败': 'Could not grant the Feishu group-message permission',
269
+ '请先完成当前飞书授权操作,再开通群消息权限。': 'Finish the current Feishu authorization before granting group-message permission.',
221
270
  '已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
222
271
  '正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
223
272
  '正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
@@ -516,16 +565,26 @@ function translateDynamic(text) {
516
565
  if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
517
566
  match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
518
567
  if (match) return `Remove ${match[1]} from DeepSeek Harness`;
568
+ match = /^(.+)的飞书授权流程$/.exec(text);
569
+ if (match) return `Feishu authorization flow for ${match[1]}`;
519
570
  match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
520
571
  if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
572
+ match = /^用于为(.+)开通群消息权限的一次性授权二维码$/.exec(text);
573
+ if (match) return `One-time QR code for granting group-message permission to ${match[1]}`;
521
574
  match = /^正在修复「(.+)」$/.exec(text);
522
575
  if (match) return `Repairing “${match[1]}”`;
576
+ match = /^正在为「(.+)」开通群消息权限$/.exec(text);
577
+ if (match) return `Granting group-message permission to “${match[1]}”`;
523
578
  match = /^修复(.+)的卡片按钮$/.exec(text);
524
579
  if (match) return `Repair card buttons for ${match[1]}`;
525
580
  match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
526
581
  if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
582
+ match = /^(.+)的群消息权限二维码已生成,请使用飞书确认。$/.exec(text);
583
+ if (match) return `Group-message permission QR code generated for ${match[1]}. Confirm it with Feishu.`;
527
584
  match = /^(.+)的卡片按钮已修复。$/.exec(text);
528
585
  if (match) return `Card buttons repaired for ${match[1]}.`;
586
+ match = /^(.+)已开通群消息权限,并启用“响应所有群消息”。$/.exec(text);
587
+ if (match) return `${match[1]} now has group-message permission and “Respond to all group messages” is enabled.`;
529
588
  match = /^(检查连接|重试连接)(.+)$/.exec(text);
530
589
  if (match) return `${localizeText(match[1])} ${match[2]}`;
531
590
  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%); }
@@ -14,6 +14,7 @@ import {
14
14
  createWorkspaceAwareController,
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
+ import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
17
18
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
19
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
19
20
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
@@ -63,7 +64,9 @@ export async function createProductionController(ctx, config = {}, internals = {
63
64
  if (canListConfiguredBots) {
64
65
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
65
66
  }
66
- await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
67
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
68
+ defaultAgentPreset: config.agentPreset,
69
+ })));
67
70
  const observedConfigStore = typeof configStore.remove === 'function'
68
71
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
69
72
  : configStore;
@@ -89,7 +92,6 @@ export async function createProductionController(ctx, config = {}, internals = {
89
92
  const harness = new Harness({
90
93
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
91
94
  workspace: defaultWorkspace,
92
- ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
93
95
  autostart: false,
94
96
  dshBin: config.dshBin ?? 'dsh',
95
97
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -103,7 +105,7 @@ export async function createProductionController(ctx, config = {}, internals = {
103
105
  logger,
104
106
  createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
105
107
  const state = await stateFor(botId);
106
- await workspaces.ensure(botId);
108
+ await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
107
109
  const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
108
110
  return new Runtime({
109
111
  config: botConfig,
@@ -135,7 +137,11 @@ export async function createProductionController(ctx, config = {}, internals = {
135
137
  }
136
138
  },
137
139
  });
138
- const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
140
+ const controller = createWorkspaceAwareController(coreController, {
141
+ workspaces,
142
+ stateFor,
143
+ agentPresetCatalog: () => listAgentPresetCatalog(ctx),
144
+ });
139
145
  const supervisor = createSupervisor({
140
146
  controller,
141
147
  harness,
@@ -1,6 +1,7 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
3
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
+ import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
4
5
  import {
5
6
  connectionTestTargetUnavailable,
6
7
  publicConnectionTestResult,
@@ -16,6 +17,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
16
17
  reconnectBot: 'bot.reconnect',
17
18
  deleteBot: 'bot.delete',
18
19
  setWorkspace: SET_WORKSPACE_ENDPOINT,
20
+ setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
19
21
  approveSender: 'bot.sender.approve',
20
22
  revokeSender: 'bot.sender.revoke',
21
23
  });
@@ -88,6 +90,10 @@ function payloadFailure(endpoint, payload) {
88
90
  return validWorkspacePayload(payload)
89
91
  ? null : '请输入工作区绝对路径。';
90
92
  }
93
+ if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
94
+ return validAgentPresetPayload(payload)
95
+ ? null : '请选择 Agent Preset。';
96
+ }
91
97
  if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
92
98
  return exactKeys(payload, ['botId', 'requestId', 'confirm'])
93
99
  && validId(payload.botId)
@@ -245,6 +251,12 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
245
251
  await controller.updateWorkspace(payload.botId, payload.workspace),
246
252
  cachedEncode,
247
253
  );
254
+ } else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
255
+ if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
256
+ value = await publicStatus(
257
+ await controller.updateAgentPreset(payload.botId, payload.agentPreset),
258
+ cachedEncode,
259
+ );
248
260
  } else if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
249
261
  value = await publicStatus(
250
262
  await controller.approveSender(payload.botId, payload.requestId),
@@ -129,7 +129,7 @@ export class ConnectionSupervisor {
129
129
  await this.#controller.initialize();
130
130
  if (this.#closed) return;
131
131
 
132
- const status = this.#controller.status();
132
+ const status = await this.#controller.status();
133
133
  this.#resolveReady?.(status);
134
134
  this.#resolveReady = null;
135
135
  const current = totals(status);
@@ -20,6 +20,7 @@ import {
20
20
  createWorkspaceAwareController,
21
21
  observeBotWorkspaceRemovals,
22
22
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
23
+ import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
23
24
 
24
25
  function harnessOrigin(webServer, configured) {
25
26
  if (configured !== undefined) return new URL(configured);
@@ -77,7 +78,9 @@ export async function createProductionController(ctx, config = {}, internals = {
77
78
  if (canListConfiguredBots) {
78
79
  await workspaces.reconcile(configuredBots.map((bot) => bot.id));
79
80
  }
80
- await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.id)));
81
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.id, {
82
+ defaultAgentPreset: config.agentPreset,
83
+ })));
81
84
  const observedConfigStore = typeof configStore.removeBot === 'function'
82
85
  ? observeBotWorkspaceRemovals(configStore, {
83
86
  workspaces,
@@ -115,7 +118,6 @@ export async function createProductionController(ctx, config = {}, internals = {
115
118
  const harness = new Harness({
116
119
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
117
120
  workspace: defaultWorkspace,
118
- ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
119
121
  // This plugin is already hosted by a running DSH process. Starting a
120
122
  // second DSH would create a competing server and lifecycle.
121
123
  autostart: false,
@@ -133,7 +135,7 @@ export async function createProductionController(ctx, config = {}, internals = {
133
135
  createRuntime: async ({ botId, config: botConfig, appSecret, repair }) => {
134
136
  const state = await stateFor(botConfig);
135
137
  const id = botId ?? botConfig.id ?? botConfig.appId;
136
- await workspaces.ensure(id);
138
+ await workspaces.ensure(id, { defaultAgentPreset: config.agentPreset });
137
139
  const workspaceScope = createBotWorkspaceScope(harness, { botId: id, workspaces, state });
138
140
  return new Runtime({
139
141
  lark,
@@ -142,6 +144,8 @@ export async function createProductionController(ctx, config = {}, internals = {
142
144
  appId: botConfig.appId,
143
145
  appSecret,
144
146
  domain: botConfig.domain,
147
+ botOpenId: botConfig.botOpenId,
148
+ groupResponseMode: botConfig.groupResponseMode,
145
149
  ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
146
150
  harness: workspaceScope.harness,
147
151
  state: workspaceScope.state,
@@ -166,6 +170,7 @@ export async function createProductionController(ctx, config = {}, internals = {
166
170
  const controller = createWorkspaceAwareController(coreController, {
167
171
  workspaces,
168
172
  stateFor: stateForBotId,
173
+ agentPresetCatalog: () => listAgentPresetCatalog(ctx),
169
174
  });
170
175
 
171
176
  const supervisor = createSupervisor({
@@ -1,7 +1,16 @@
1
1
  import QRCode from 'qrcode';
2
+ import {
3
+ normalizeAgentPresetCatalog,
4
+ normalizeAgentPresetId,
5
+ } from '../../../../src/channels/shared/agent-preset.mjs';
2
6
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
3
7
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
4
8
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
9
+ import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
10
+ import {
11
+ isFeishuGroupResponseMode,
12
+ normalizeFeishuGroupResponseMode,
13
+ } from '../../../../src/channels/feishu/group-response-mode.mjs';
5
14
  import {
6
15
  FEISHU_ENDPOINTS,
7
16
  FEISHU_RPC_CHANNEL,
@@ -21,8 +30,17 @@ const REGISTRATION_STATES = new Set([
21
30
  'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
22
31
  'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
23
32
  ]);
24
- const REGISTRATION_OPERATIONS = new Set(['provision', 'callback_repair']);
33
+ const REGISTRATION_OPERATIONS = new Set([
34
+ 'provision',
35
+ 'callback_repair',
36
+ 'group_message_permission',
37
+ ]);
25
38
  const CALLBACK_REPAIR_OPERATION = 'callback_repair';
39
+ const GROUP_MESSAGE_PERMISSION_OPERATION = 'group_message_permission';
40
+ const TARGETED_APP_UPDATE_OPERATIONS = new Set([
41
+ CALLBACK_REPAIR_OPERATION,
42
+ GROUP_MESSAGE_PERMISSION_OPERATION,
43
+ ]);
26
44
  const OFFICIAL_REGISTRATION_HOSTS = new Set([
27
45
  'accounts.feishu.cn',
28
46
  'accounts.larksuite.com',
@@ -54,6 +72,9 @@ const PUBLIC_ERROR_MESSAGES = Object.freeze({
54
72
  credential_update_failed: 'Unable to store the repaired Feishu credentials.',
55
73
  credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
56
74
  repair_connection_failed: 'The callback update was accepted, but the selected bot could not reconnect.',
75
+ group_message_permission_required: 'Authorize access to all group messages before enabling this mode.',
76
+ group_message_permission_save_failed: 'Feishu granted the permission, but all-message mode could not be saved.',
77
+ group_message_permission_connection_failed: 'Feishu granted the permission, but the selected bot could not reconnect.',
57
78
  card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
58
79
  card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
59
80
  card_action_probe_timeout: 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.',
@@ -133,7 +154,7 @@ function safeRegistrationUrl(value, operation, expectedHost) {
133
154
  || url.port
134
155
  || url.username
135
156
  || url.password) return undefined;
136
- if (operation === CALLBACK_REPAIR_OPERATION) {
157
+ if (TARGETED_APP_UPDATE_OPERATIONS.has(operation)) {
137
158
  const clientIds = url.searchParams.getAll('clientID');
138
159
  const transportKinds = url.searchParams.getAll('tp');
139
160
  const addons = url.searchParams.getAll('addons');
@@ -235,7 +256,7 @@ async function publicProvisioning(registration, encodeQr, expectedHost) {
235
256
  // remote update is committed. Keep only the opaque attempt identity so a
236
257
  // browser reload can resume polling the non-cancellable verification phase.
237
258
  if (registration.state === 'saving'
238
- && registration.operation === CALLBACK_REPAIR_OPERATION
259
+ && TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
239
260
  && registration.botId) {
240
261
  return { ...projection, submitted: true };
241
262
  }
@@ -253,6 +274,9 @@ function publicBotEntry(entry) {
253
274
  state: connectionState(source, registration, connected),
254
275
  connected,
255
276
  configured: source.configured === true,
277
+ agentPreset: normalizeAgentPresetId(source.agentPreset),
278
+ groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
279
+ groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
256
280
  bot: publicBot(source.bot),
257
281
  health: publicHealth(source, connected),
258
282
  };
@@ -268,12 +292,12 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
268
292
  const registration = publicRegistration(source.registration);
269
293
  const facts = connectionFacts(source.connection);
270
294
  const connected = source.connected === true || facts.connected;
271
- const repairTarget = registration.operation === CALLBACK_REPAIR_OPERATION
295
+ const appUpdateTarget = TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
272
296
  && registration.botId
273
297
  && Array.isArray(source.bots)
274
298
  ? source.bots.find((entry) => entry?.botId === registration.botId)
275
299
  : undefined;
276
- const expectedRegistrationHost = repairTarget?.bot?.domain === 'lark'
300
+ const expectedRegistrationHost = appUpdateTarget?.bot?.domain === 'lark'
277
301
  ? 'open.larksuite.com'
278
302
  : 'open.feishu.cn';
279
303
  const provisioning = await publicProvisioning(
@@ -294,6 +318,7 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
294
318
  bot: publicBot(source.bot),
295
319
  health: publicHealth(source, connected),
296
320
  bots,
321
+ agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
297
322
  totals: {
298
323
  configured: bots.length || (source.configured === true ? 1 : 0),
299
324
  connected: bots.length ? bots.filter((bot) => bot.connected).length : (connected ? 1 : 0),
@@ -338,6 +363,11 @@ function validPayload(endpoint, payload) {
338
363
  ? null
339
364
  : 'Callback repair requires a single valid botId.';
340
365
  }
366
+ if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
367
+ return hasOnlyKeys(payload, new Set(['botId'])) && safeOpaqueId(payload.botId)
368
+ ? null
369
+ : 'Group message permission update requires a single valid botId.';
370
+ }
341
371
  if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
342
372
  return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
343
373
  && validCredential(payload.appId, 256)
@@ -378,6 +408,17 @@ function validPayload(endpoint, payload) {
378
408
  return validWorkspacePayload(payload)
379
409
  ? null : '请输入工作区绝对路径。';
380
410
  }
411
+ if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
412
+ return validAgentPresetPayload(payload)
413
+ ? null : '请选择 Agent Preset。';
414
+ }
415
+ if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
416
+ return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
417
+ && safeOpaqueId(payload.botId)
418
+ && isFeishuGroupResponseMode(payload.groupResponseMode)
419
+ ? null
420
+ : '请选择群聊响应方式。';
421
+ }
381
422
  return 'Unknown Feishu endpoint.';
382
423
  }
383
424
 
@@ -497,6 +538,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
497
538
  throw new Error('Callback repair did not produce a safe QR code.');
498
539
  }
499
540
  attemptQr.set(attemptId, value.verificationUrl);
541
+ } else if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
542
+ if (typeof controller.startGroupMessagePermission !== 'function') {
543
+ throw new Error('Feishu group message permission update is unavailable.');
544
+ }
545
+ const started = await controller.startGroupMessagePermission(payload.botId);
546
+ const attemptId = String(publicRegistration(started?.registration).attempt);
547
+ const ready = await waitForQr(controller, started, attemptId, signal);
548
+ value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
549
+ if (!value
550
+ || value.operation !== GROUP_MESSAGE_PERMISSION_OPERATION
551
+ || value.botId !== payload.botId) {
552
+ throw new Error('Group message permission update did not produce a safe QR code.');
553
+ }
554
+ attemptQr.set(attemptId, value.verificationUrl);
500
555
  } else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
501
556
  const current = await statusForRegistration(controller, payload.attemptId);
502
557
  if (!current || !sameAttempt(current, payload.attemptId)) {
@@ -531,14 +586,16 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
531
586
  after = await controller.cancelRegistration(payload.attemptId);
532
587
  }
533
588
  const afterRegistration = publicRegistration(after?.registration);
534
- if (registration.operation === CALLBACK_REPAIR_OPERATION
589
+ if (TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
535
590
  && registration.state === 'saving'
536
591
  && ['saving', 'succeeded'].includes(afterRegistration.state)) {
537
592
  value = {
538
593
  status: pollStatus(after),
539
594
  operation: afterRegistration.operation,
540
595
  ...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
541
- message: 'Callback repair was already submitted and is still being verified.',
596
+ message: registration.operation === GROUP_MESSAGE_PERMISSION_OPERATION
597
+ ? 'The permission update was already submitted and is still being applied.'
598
+ : 'Callback repair was already submitted and is still being verified.',
542
599
  };
543
600
  }
544
601
  if (value?.status !== 'connecting') {
@@ -606,6 +663,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
606
663
  await controller.updateWorkspace(payload.botId, payload.workspace),
607
664
  { encodeQr: cachedEncodeQr },
608
665
  );
666
+ } else if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
667
+ if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
668
+ value = await toPublicFeishuStatus(
669
+ await controller.updateAgentPreset(payload.botId, payload.agentPreset),
670
+ { encodeQr: cachedEncodeQr },
671
+ );
672
+ } else if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
673
+ if (typeof controller.updateGroupResponseMode !== 'function') {
674
+ throw new Error('Group response mode update is unavailable');
675
+ }
676
+ value = await toPublicFeishuStatus(
677
+ await controller.updateGroupResponseMode(payload.botId, payload.groupResponseMode),
678
+ { encodeQr: cachedEncodeQr },
679
+ );
609
680
  } else {
610
681
  if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
611
682
  value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
@@ -14,6 +14,7 @@ import {
14
14
  createWorkspaceAwareController,
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
+ import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
17
18
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
19
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
19
20
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
@@ -57,7 +58,9 @@ export async function createProductionController(ctx, config = {}, internals = {
57
58
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
58
59
  const configuredBots = configStore.list();
59
60
  await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
60
- await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
61
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
62
+ defaultAgentPreset: config.agentPreset,
63
+ })));
61
64
  const observedConfigStore = typeof configStore.remove === 'function'
62
65
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
63
66
  : configStore;
@@ -80,7 +83,6 @@ export async function createProductionController(ctx, config = {}, internals = {
80
83
  const harness = new Harness({
81
84
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
82
85
  workspace: defaultWorkspace,
83
- ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
84
86
  autostart: false,
85
87
  dshBin: config.dshBin ?? 'dsh',
86
88
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -94,7 +96,7 @@ export async function createProductionController(ctx, config = {}, internals = {
94
96
  logger,
95
97
  createRuntime: async ({ botId, config: botConfig, appSecret }) => {
96
98
  const state = await stateFor(botId);
97
- await workspaces.ensure(botId);
99
+ await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
98
100
  const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
99
101
  return new Runtime({
100
102
  config: botConfig,
@@ -125,7 +127,11 @@ export async function createProductionController(ctx, config = {}, internals = {
125
127
  }
126
128
  },
127
129
  });
128
- const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
130
+ const controller = createWorkspaceAwareController(coreController, {
131
+ workspaces,
132
+ stateFor,
133
+ agentPresetCatalog: () => listAgentPresetCatalog(ctx),
134
+ });
129
135
  const supervisor = createSupervisor({
130
136
  controller,
131
137
  harness,