@xmanrui/dsh-im 0.18.0 → 1.0.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 (37) hide show
  1. package/README.en.md +12 -1
  2. package/README.md +12 -1
  3. package/lib/client.js +341 -37
  4. package/lib/index.js +162 -156
  5. package/package.json +1 -1
  6. package/plugin-src/client/channels/feishu/api.js +23 -5
  7. package/plugin-src/client/channels/feishu/index.js +291 -39
  8. package/plugin-src/client/channels/feishu/styles.js +46 -0
  9. package/plugin-src/client/i18n.js +50 -0
  10. package/plugin-src/host/channels/dingtalk/production.mjs +5 -2
  11. package/plugin-src/host/channels/feishu/production.mjs +7 -2
  12. package/plugin-src/host/channels/feishu/rpc.mjs +61 -7
  13. package/plugin-src/host/channels/qq/production.mjs +5 -2
  14. package/plugin-src/host/channels/shared/production.mjs +5 -2
  15. package/plugin-src/host/channels/slack/production.mjs +5 -2
  16. package/plugin-src/host/channels/wecom/production.mjs +5 -2
  17. package/plugin-src/host/channels/weixin/production.mjs +5 -2
  18. package/plugin-src/host/channels/whatsapp/production.mjs +5 -2
  19. package/src/channels/dingtalk/dingtalk-bridge.mjs +11 -1
  20. package/src/channels/discord/discord-api.mjs +1 -1
  21. package/src/channels/feishu/bridge.mjs +40 -5
  22. package/src/channels/feishu/feishu-cards.mjs +4 -0
  23. package/src/channels/feishu/feishu-runtime.mjs +14 -0
  24. package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
  25. package/src/channels/feishu/group-response-mode.mjs +15 -0
  26. package/src/channels/feishu/multi-bot-controller.mjs +258 -9
  27. package/src/channels/feishu/plugin-config-store.mjs +3 -0
  28. package/src/channels/feishu/repair-manager.mjs +17 -3
  29. package/src/channels/qq/qq-bridge.mjs +11 -1
  30. package/src/channels/shared/bot-workspace-store.mjs +75 -4
  31. package/src/channels/shared/preset-command.mjs +305 -0
  32. package/src/channels/shared/text-harness-bridge.mjs +11 -1
  33. package/src/channels/telegram/telegram-api.mjs +31 -0
  34. package/src/channels/telegram/telegram-runtime.mjs +27 -1
  35. package/src/channels/wecom/wecom-bridge.mjs +11 -1
  36. package/src/channels/weixin/weixin-api.mjs +1 -1
  37. package/src/channels/weixin/weixin-bridge.mjs +11 -1
@@ -407,6 +407,50 @@ const CSS = String.raw`
407
407
  .bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
408
408
  .bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
409
409
 
410
+ .bxf-responseMode { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 5px; margin-top: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
411
+ .bxf-responseModeHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
412
+ .bxf-responseModeHeader > span:first-child { grid-column: 1; grid-row: 1; white-space: nowrap; }
413
+ .bxf-responseModeStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
414
+ .bxf-responseModeSelect { min-width: 0; width: 100%; grid-column: 1 / -1; grid-row: 2; height: 32px; padding: 0 30px 0 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background-color: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease, background-color .16s ease; }
415
+ .bxf-responseModeSelect:hover:not(:disabled) { border-color: color-mix(in srgb, var(--bxf-accent) 45%, var(--dsw-alias-border-l2, #dfe1e5)); }
416
+ .bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
417
+ .bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
418
+ .bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
419
+ .bxf-responseModePermissionAction { grid-column: 1 / -1; grid-row: 4; display: flex; justify-content: flex-start; margin-top: 2px; }
420
+ .bxf-responseModePermissionButton { min-height: 28px; padding: 3px 9px; color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 30%, var(--dsw-alias-border-l2, #dfe1e5)); background: var(--dsw-alias-bg-layer-1, #fff); }
421
+ .bxf-responseModePermissionButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
422
+ .bxf-responseModeError { grid-column: 1 / -1; grid-row: 5; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
423
+
424
+ .bxf-botProvision {
425
+ position: relative;
426
+ margin-top: 14px;
427
+ scroll-margin-block: 20px;
428
+ animation: bxf-revealProvision .2s var(--ds-ease-out, ease-out) both;
429
+ }
430
+ .bxf-botProvision:focus { outline: none; }
431
+ .bxf-botProvision:focus-visible {
432
+ outline: 2px solid color-mix(in srgb, var(--bxf-accent) 75%, transparent);
433
+ outline-offset: 3px;
434
+ border-radius: 12px;
435
+ }
436
+ .bxf-botProvision > .bxf-provisionCard {
437
+ border-radius: 11px;
438
+ box-shadow: none;
439
+ background: color-mix(in srgb, var(--bxf-accent) 2.5%, var(--dsw-alias-bg-layer-3, #fff));
440
+ }
441
+ .bxf-botProvision .bxf-cardBody { padding: 18px; }
442
+ .bxf-botProvision .bxf-qrLayout {
443
+ grid-template-columns: 184px minmax(0, 1fr);
444
+ align-items: start;
445
+ gap: 24px;
446
+ }
447
+ .bxf-botProvision .bxf-qrFrame { width: 176px; height: 176px; padding: 10px; border-radius: 11px; }
448
+ .bxf-botProvision .bxf-countdown { width: 176px; }
449
+ .bxf-botProvision .bxf-qrCopy h3 { font-size: 18px; line-height: 26px; }
450
+ .bxf-botProvision .bxf-steps { gap: 8px; margin-top: 14px; }
451
+ .bxf-botProvision .bxf-actions { margin-top: 16px; }
452
+ .bxf-botProvision .bxf-inlineError { min-height: 160px; padding: 22px; }
453
+
410
454
  .bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
411
455
  .bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
412
456
  .bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
@@ -459,6 +503,7 @@ const CSS = String.raw`
459
503
  @keyframes bxf-rotate { to { transform: rotate(360deg); } }
460
504
  @keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
461
505
  @keyframes bxf-shimmer { to { background-position: -220% 0; } }
506
+ @keyframes bxf-revealProvision { from { opacity: 0; transform: translateY(-5px); } }
462
507
 
463
508
  @container (max-width: 620px) {
464
509
  .bxf-headingTools { gap: 6px; }
@@ -470,6 +515,7 @@ const CSS = String.raw`
470
515
  .bxf-intro { grid-template-columns: minmax(0, 1fr); }
471
516
  .bxf-markStage { display: none; }
472
517
  .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
518
+ .bxf-botProvision .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); }
473
519
  .bxf-qrCopy { width: 100%; }
474
520
  .bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
475
521
  .bxf-connectedTop { align-items: flex-start; flex-direction: column; }
@@ -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',
@@ -190,6 +208,7 @@ const EN = Object.freeze({
190
208
  '生成 QQ 二维码': 'Generate QQ QR code',
191
209
  '正在生成二维码…': 'Generating QR code…',
192
210
  '正在准备授权二维码': 'Preparing authorization QR code',
211
+ '正在准备权限授权二维码': 'Preparing permission authorization QR code',
193
212
  '正在准备微信二维码': 'Preparing WeChat QR code',
194
213
  '正在添加新机器人': 'Adding a new bot',
195
214
  '正在申请钉钉授权二维码…': 'Requesting DingTalk authorization QR code…',
@@ -207,6 +226,16 @@ const EN = Object.freeze({
207
226
  '在飞书中打开': 'Open in Feishu',
208
227
  '取消添加': 'Cancel',
209
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',
210
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.',
211
240
  '核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
212
241
  '保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
@@ -219,14 +248,25 @@ const EN = Object.freeze({
219
248
  '修复卡片按钮': 'Repair card buttons',
220
249
  '等待扫码…': 'Waiting for scan…',
221
250
  '飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
251
+ '飞书服务返回了不匹配的群消息权限二维码': 'Feishu returned a group-message permission QR code for a different bot',
222
252
  '飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
253
+ '飞书服务返回的应用更新信息缺少 botId': 'Feishu app-update status is missing the bot ID',
223
254
  '飞书服务返回了不匹配的注册进度': 'Feishu returned registration progress for a different operation',
224
255
  '此机器人': 'this bot',
256
+ '用于为${botName}开通群消息权限的一次性授权二维码': 'One-time QR code for granting group-message permission to ${botName}',
257
+ '正在为「${botName}」开通群消息权限': 'Granting group-message permission to “${botName}”',
225
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.',
226
262
  '${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
227
263
  '已取消卡片按钮修复。': 'Card-button repair was cancelled.',
264
+ '已取消群消息权限授权。': 'Group-message permission authorization was cancelled.',
228
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',
229
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.',
230
270
  '已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
231
271
  '正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
232
272
  '正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
@@ -525,16 +565,26 @@ function translateDynamic(text) {
525
565
  if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
526
566
  match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
527
567
  if (match) return `Remove ${match[1]} from DeepSeek Harness`;
568
+ match = /^(.+)的飞书授权流程$/.exec(text);
569
+ if (match) return `Feishu authorization flow for ${match[1]}`;
528
570
  match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
529
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]}`;
530
574
  match = /^正在修复「(.+)」$/.exec(text);
531
575
  if (match) return `Repairing “${match[1]}”`;
576
+ match = /^正在为「(.+)」开通群消息权限$/.exec(text);
577
+ if (match) return `Granting group-message permission to “${match[1]}”`;
532
578
  match = /^修复(.+)的卡片按钮$/.exec(text);
533
579
  if (match) return `Repair card buttons for ${match[1]}`;
534
580
  match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
535
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.`;
536
584
  match = /^(.+)的卡片按钮已修复。$/.exec(text);
537
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.`;
538
588
  match = /^(检查连接|重试连接)(.+)$/.exec(text);
539
589
  if (match) return `${localizeText(match[1])} ${match[2]}`;
540
590
  match = /^移除(.+)$/.exec(text);
@@ -53,6 +53,7 @@ export async function createProductionController(ctx, config = {}, internals = {
53
53
  const logger = typeof ctx.logger === 'function'
54
54
  ? ctx.logger('dsh-dingtalk')
55
55
  : (ctx.logger ?? console);
56
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
56
57
  const paths = pluginPaths(config);
57
58
  const configStore = await new ConfigStore(paths.config).load();
58
59
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -106,7 +107,9 @@ export async function createProductionController(ctx, config = {}, internals = {
106
107
  createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
107
108
  const state = await stateFor(botId);
108
109
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
109
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
110
+ const workspaceScope = createBotWorkspaceScope(harness, {
111
+ botId, workspaces, state, agentPresetCatalog,
112
+ });
110
113
  return new Runtime({
111
114
  config: botConfig,
112
115
  clientSecret,
@@ -140,7 +143,7 @@ export async function createProductionController(ctx, config = {}, internals = {
140
143
  const controller = createWorkspaceAwareController(coreController, {
141
144
  workspaces,
142
145
  stateFor,
143
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
146
+ agentPresetCatalog,
144
147
  });
145
148
  const supervisor = createSupervisor({
146
149
  controller,
@@ -66,6 +66,7 @@ export async function createProductionController(ctx, config = {}, internals = {
66
66
  const logger = typeof ctx.logger === 'function'
67
67
  ? ctx.logger('dsh-feishu')
68
68
  : (ctx.logger ?? console);
69
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
69
70
  const paths = pluginPaths(config);
70
71
  const configStore = await new ConfigStore(paths.config).load();
71
72
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -136,7 +137,9 @@ export async function createProductionController(ctx, config = {}, internals = {
136
137
  const state = await stateFor(botConfig);
137
138
  const id = botId ?? botConfig.id ?? botConfig.appId;
138
139
  await workspaces.ensure(id, { defaultAgentPreset: config.agentPreset });
139
- const workspaceScope = createBotWorkspaceScope(harness, { botId: id, workspaces, state });
140
+ const workspaceScope = createBotWorkspaceScope(harness, {
141
+ botId: id, workspaces, state, agentPresetCatalog,
142
+ });
140
143
  return new Runtime({
141
144
  lark,
142
145
  botId: id,
@@ -144,6 +147,8 @@ export async function createProductionController(ctx, config = {}, internals = {
144
147
  appId: botConfig.appId,
145
148
  appSecret,
146
149
  domain: botConfig.domain,
150
+ botOpenId: botConfig.botOpenId,
151
+ groupResponseMode: botConfig.groupResponseMode,
147
152
  ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
148
153
  harness: workspaceScope.harness,
149
154
  state: workspaceScope.state,
@@ -168,7 +173,7 @@ export async function createProductionController(ctx, config = {}, internals = {
168
173
  const controller = createWorkspaceAwareController(coreController, {
169
174
  workspaces,
170
175
  stateFor: stateForBotId,
171
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
176
+ agentPresetCatalog,
172
177
  });
173
178
 
174
179
  const supervisor = createSupervisor({
@@ -7,6 +7,10 @@ import { publicConnectionTestResult } from '../../../../src/channels/shared/conn
7
7
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
8
8
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
9
9
  import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
10
+ import {
11
+ isFeishuGroupResponseMode,
12
+ normalizeFeishuGroupResponseMode,
13
+ } from '../../../../src/channels/feishu/group-response-mode.mjs';
10
14
  import {
11
15
  FEISHU_ENDPOINTS,
12
16
  FEISHU_RPC_CHANNEL,
@@ -26,8 +30,17 @@ const REGISTRATION_STATES = new Set([
26
30
  'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
27
31
  'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
28
32
  ]);
29
- const REGISTRATION_OPERATIONS = new Set(['provision', 'callback_repair']);
33
+ const REGISTRATION_OPERATIONS = new Set([
34
+ 'provision',
35
+ 'callback_repair',
36
+ 'group_message_permission',
37
+ ]);
30
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
+ ]);
31
44
  const OFFICIAL_REGISTRATION_HOSTS = new Set([
32
45
  'accounts.feishu.cn',
33
46
  'accounts.larksuite.com',
@@ -59,6 +72,9 @@ const PUBLIC_ERROR_MESSAGES = Object.freeze({
59
72
  credential_update_failed: 'Unable to store the repaired Feishu credentials.',
60
73
  credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
61
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.',
62
78
  card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
63
79
  card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
64
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.',
@@ -138,7 +154,7 @@ function safeRegistrationUrl(value, operation, expectedHost) {
138
154
  || url.port
139
155
  || url.username
140
156
  || url.password) return undefined;
141
- if (operation === CALLBACK_REPAIR_OPERATION) {
157
+ if (TARGETED_APP_UPDATE_OPERATIONS.has(operation)) {
142
158
  const clientIds = url.searchParams.getAll('clientID');
143
159
  const transportKinds = url.searchParams.getAll('tp');
144
160
  const addons = url.searchParams.getAll('addons');
@@ -240,7 +256,7 @@ async function publicProvisioning(registration, encodeQr, expectedHost) {
240
256
  // remote update is committed. Keep only the opaque attempt identity so a
241
257
  // browser reload can resume polling the non-cancellable verification phase.
242
258
  if (registration.state === 'saving'
243
- && registration.operation === CALLBACK_REPAIR_OPERATION
259
+ && TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
244
260
  && registration.botId) {
245
261
  return { ...projection, submitted: true };
246
262
  }
@@ -259,6 +275,8 @@ function publicBotEntry(entry) {
259
275
  connected,
260
276
  configured: source.configured === true,
261
277
  agentPreset: normalizeAgentPresetId(source.agentPreset),
278
+ groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
279
+ groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
262
280
  bot: publicBot(source.bot),
263
281
  health: publicHealth(source, connected),
264
282
  };
@@ -274,12 +292,12 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
274
292
  const registration = publicRegistration(source.registration);
275
293
  const facts = connectionFacts(source.connection);
276
294
  const connected = source.connected === true || facts.connected;
277
- const repairTarget = registration.operation === CALLBACK_REPAIR_OPERATION
295
+ const appUpdateTarget = TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
278
296
  && registration.botId
279
297
  && Array.isArray(source.bots)
280
298
  ? source.bots.find((entry) => entry?.botId === registration.botId)
281
299
  : undefined;
282
- const expectedRegistrationHost = repairTarget?.bot?.domain === 'lark'
300
+ const expectedRegistrationHost = appUpdateTarget?.bot?.domain === 'lark'
283
301
  ? 'open.larksuite.com'
284
302
  : 'open.feishu.cn';
285
303
  const provisioning = await publicProvisioning(
@@ -345,6 +363,11 @@ function validPayload(endpoint, payload) {
345
363
  ? null
346
364
  : 'Callback repair requires a single valid botId.';
347
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
+ }
348
371
  if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
349
372
  return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
350
373
  && validCredential(payload.appId, 256)
@@ -389,6 +412,13 @@ function validPayload(endpoint, payload) {
389
412
  return validAgentPresetPayload(payload)
390
413
  ? null : '请选择 Agent Preset。';
391
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
+ }
392
422
  return 'Unknown Feishu endpoint.';
393
423
  }
394
424
 
@@ -508,6 +538,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
508
538
  throw new Error('Callback repair did not produce a safe QR code.');
509
539
  }
510
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);
511
555
  } else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
512
556
  const current = await statusForRegistration(controller, payload.attemptId);
513
557
  if (!current || !sameAttempt(current, payload.attemptId)) {
@@ -542,14 +586,16 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
542
586
  after = await controller.cancelRegistration(payload.attemptId);
543
587
  }
544
588
  const afterRegistration = publicRegistration(after?.registration);
545
- if (registration.operation === CALLBACK_REPAIR_OPERATION
589
+ if (TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
546
590
  && registration.state === 'saving'
547
591
  && ['saving', 'succeeded'].includes(afterRegistration.state)) {
548
592
  value = {
549
593
  status: pollStatus(after),
550
594
  operation: afterRegistration.operation,
551
595
  ...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
552
- 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.',
553
599
  };
554
600
  }
555
601
  if (value?.status !== 'connecting') {
@@ -623,6 +669,14 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
623
669
  await controller.updateAgentPreset(payload.botId, payload.agentPreset),
624
670
  { encodeQr: cachedEncodeQr },
625
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
+ );
626
680
  } else {
627
681
  if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
628
682
  value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
@@ -50,6 +50,7 @@ export async function createProductionController(ctx, config = {}, internals = {
50
50
  const QrAuth = internals.QrAuth ?? QqQrAuth;
51
51
  const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor;
52
52
  const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:qq') : (ctx.logger ?? console);
53
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
53
54
  const paths = pluginPaths(config);
54
55
  const configStore = await new ConfigStore(paths.config).load();
55
56
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -97,7 +98,9 @@ export async function createProductionController(ctx, config = {}, internals = {
97
98
  createRuntime: async ({ botId, config: botConfig, appSecret }) => {
98
99
  const state = await stateFor(botId);
99
100
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
100
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
101
+ const workspaceScope = createBotWorkspaceScope(harness, {
102
+ botId, workspaces, state, agentPresetCatalog,
103
+ });
101
104
  return new Runtime({
102
105
  config: botConfig,
103
106
  appSecret,
@@ -130,7 +133,7 @@ export async function createProductionController(ctx, config = {}, internals = {
130
133
  const controller = createWorkspaceAwareController(coreController, {
131
134
  workspaces,
132
135
  stateFor,
133
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
136
+ agentPresetCatalog,
134
137
  });
135
138
  const supervisor = createSupervisor({
136
139
  controller,
@@ -52,6 +52,7 @@ export async function createTokenProductionController(ctx, config, internals, de
52
52
  const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
53
53
  const logger = typeof ctx.logger === 'function'
54
54
  ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
55
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
55
56
  const paths = pluginPaths(config, channel);
56
57
  const configStore = await new ResolvedConfigStore(paths.config).load();
57
58
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -98,7 +99,9 @@ export async function createTokenProductionController(ctx, config, internals, de
98
99
  createRuntime: async ({ botId, config: botConfig, token }) => {
99
100
  const state = await stateFor(botId);
100
101
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
101
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
102
+ const workspaceScope = createBotWorkspaceScope(harness, {
103
+ botId, workspaces, state, agentPresetCatalog,
104
+ });
102
105
  return new ResolvedRuntime({
103
106
  ...channelRuntimeOptions,
104
107
  config: botConfig,
@@ -132,7 +135,7 @@ export async function createTokenProductionController(ctx, config, internals, de
132
135
  const controller = createWorkspaceAwareController(coreController, {
133
136
  workspaces,
134
137
  stateFor,
135
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
138
+ agentPresetCatalog,
136
139
  });
137
140
  const supervisor = createSupervisor({
138
141
  channel,
@@ -30,6 +30,7 @@ export async function createProductionController(ctx, config = {}, internals = {
30
30
  const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
31
31
  const logger = typeof ctx.logger === 'function'
32
32
  ? ctx.logger('dsh-im:slack') : (ctx.logger ?? console);
33
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
33
34
  const paths = pluginPaths(config, 'slack');
34
35
  const configStore = await new ResolvedConfigStore(paths.config).load();
35
36
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -76,7 +77,9 @@ export async function createProductionController(ctx, config = {}, internals = {
76
77
  createRuntime: async ({ botId, config: botConfig, botToken, appToken }) => {
77
78
  const state = await stateFor(botId);
78
79
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
79
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
80
+ const workspaceScope = createBotWorkspaceScope(harness, {
81
+ botId, workspaces, state, agentPresetCatalog,
82
+ });
80
83
  return new ResolvedRuntime({
81
84
  config: botConfig,
82
85
  botToken,
@@ -110,7 +113,7 @@ export async function createProductionController(ctx, config = {}, internals = {
110
113
  const controller = createWorkspaceAwareController(coreController, {
111
114
  workspaces,
112
115
  stateFor,
113
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
116
+ agentPresetCatalog,
114
117
  });
115
118
  const supervisor = createSupervisor({
116
119
  channel: 'slack',
@@ -50,6 +50,7 @@ export async function createProductionController(ctx, config = {}, internals = {
50
50
  const QrAuth = internals.QrAuth ?? WecomQrAuth;
51
51
  const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor;
52
52
  const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:wecom') : (ctx.logger ?? console);
53
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
53
54
  const paths = pluginPaths(config);
54
55
  const configStore = await new ConfigStore(paths.config).load();
55
56
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -100,7 +101,9 @@ export async function createProductionController(ctx, config = {}, internals = {
100
101
  createRuntime: async ({ botId, config: botConfig, secret }) => {
101
102
  const state = await stateFor(botId);
102
103
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
103
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
104
+ const workspaceScope = createBotWorkspaceScope(harness, {
105
+ botId, workspaces, state, agentPresetCatalog,
106
+ });
104
107
  return new Runtime({
105
108
  config: botConfig,
106
109
  secret,
@@ -134,7 +137,7 @@ export async function createProductionController(ctx, config = {}, internals = {
134
137
  const controller = createWorkspaceAwareController(coreController, {
135
138
  workspaces,
136
139
  stateFor,
137
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
140
+ agentPresetCatalog,
138
141
  });
139
142
  const supervisor = createSupervisor({
140
143
  controller,
@@ -53,6 +53,7 @@ export async function createProductionController(ctx, config = {}, internals = {
53
53
  const logger = typeof ctx.logger === 'function'
54
54
  ? ctx.logger('dsh-weixin')
55
55
  : (ctx.logger ?? console);
56
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
56
57
  const paths = pluginPaths(config);
57
58
  const configStore = await new ConfigStore(paths.config).load();
58
59
  const defaultWorkspace = resolve(config.workspace ?? process.cwd());
@@ -100,7 +101,9 @@ export async function createProductionController(ctx, config = {}, internals = {
100
101
  createRuntime: async ({ botId, config: accountConfig, token }) => {
101
102
  const state = await stateFor(botId);
102
103
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
103
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
104
+ const workspaceScope = createBotWorkspaceScope(harness, {
105
+ botId, workspaces, state, agentPresetCatalog,
106
+ });
104
107
  return new Runtime({
105
108
  api,
106
109
  config: accountConfig,
@@ -134,7 +137,7 @@ export async function createProductionController(ctx, config = {}, internals = {
134
137
  const controller = createWorkspaceAwareController(coreController, {
135
138
  workspaces,
136
139
  stateFor,
137
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
140
+ agentPresetCatalog,
138
141
  });
139
142
  const supervisor = createSupervisor({
140
143
  controller,
@@ -50,6 +50,7 @@ export async function createProductionController(ctx, config = {}, internals = {
50
50
  if (!ctx?.webServer) throw new TypeError('dsh-im WhatsApp requires ctx.webServer');
51
51
  const logger = typeof ctx.logger === 'function'
52
52
  ? ctx.logger('dsh-im:whatsapp') : (ctx.logger ?? console);
53
+ const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
53
54
  const ConfigStore = internals.ConfigStore ?? WhatsappConfigStore;
54
55
  const StateStore = internals.StateStore ?? WhatsappStateStore;
55
56
  const Harness = internals.HarnessClient ?? WhatsappHarnessClient;
@@ -103,7 +104,9 @@ export async function createProductionController(ctx, config = {}, internals = {
103
104
  createRuntime: async ({ botId, config: botConfig, authDir }) => {
104
105
  const state = await stateFor(botId);
105
106
  await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
106
- const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
107
+ const workspaceScope = createBotWorkspaceScope(harness, {
108
+ botId, workspaces, state, agentPresetCatalog,
109
+ });
107
110
  return new Runtime({
108
111
  config: botConfig,
109
112
  authDir,
@@ -141,7 +144,7 @@ export async function createProductionController(ctx, config = {}, internals = {
141
144
  const controller = createWorkspaceAwareController(coreController, {
142
145
  workspaces,
143
146
  stateFor,
144
- agentPresetCatalog: () => listAgentPresetCatalog(ctx),
147
+ agentPresetCatalog,
145
148
  });
146
149
  const supervisor = createSupervisor({
147
150
  channel: 'whatsapp',
@@ -18,6 +18,10 @@ import {
18
18
  isModelCommand,
19
19
  runModelCommand,
20
20
  } from '../shared/model-command.mjs';
21
+ import {
22
+ isPresetCommand,
23
+ runPresetCommand,
24
+ } from '../shared/preset-command.mjs';
21
25
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
22
26
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
23
27
  import {
@@ -44,6 +48,10 @@ const HELP_TEXT = [
44
48
  '/models 按序号列出所有可用模型',
45
49
  '/model [序号或完整模型ID] 查看或切换当前会话模型',
46
50
  '示例:先发 /models,再发 /model 2',
51
+ '/presetlist 按序号列出可用 Agent Preset',
52
+ '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
53
+ '纯数字 ID:/preset id:<ID>',
54
+ '/preset --default 跟随 Host 默认',
47
55
  '/stop 停止当前任务',
48
56
  '/steer 补充指令 纠偏当前任务',
49
57
  '/status 检查连接状态',
@@ -336,7 +344,9 @@ export class DingtalkHarnessBridge {
336
344
  const commandText = nonEmptyString(promptMessage.content) ?? '';
337
345
  const commandRunner = isControlCommand(commandText)
338
346
  ? runControlCommand
339
- : (isModelCommand(commandText) ? runModelCommand : null);
347
+ : (isModelCommand(commandText)
348
+ ? runModelCommand
349
+ : (isPresetCommand(commandText) ? runPresetCommand : null));
340
350
  const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
341
351
  if (commandRunner && sessionWebhook && addressed) {
342
352
  let task;
@@ -108,7 +108,7 @@ export class DiscordApi {
108
108
  headers: {
109
109
  authorization: `Bot ${this.#token}`,
110
110
  'content-type': 'application/json',
111
- 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.18.0)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 1.0.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),