@xmanrui/dsh-im 4.15.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.en.md +2 -1
  2. package/README.md +9 -5
  3. package/lib/client.js +1729 -614
  4. package/lib/index.js +303 -293
  5. package/package.json +6 -2
  6. package/plugin-src/client/access-policy-settings.js +6 -0
  7. package/plugin-src/client/channels/feishu/index.js +15 -3
  8. package/plugin-src/client/channels/wecom-app/api.js +124 -0
  9. package/plugin-src/client/channels/wecom-app/index.js +612 -0
  10. package/plugin-src/client/channels/wecom-app/styles.js +25 -0
  11. package/plugin-src/client/delivery-settings.js +7 -0
  12. package/plugin-src/client/i18n.js +63 -0
  13. package/plugin-src/client/index.js +18 -0
  14. package/plugin-src/client/model-setting.js +135 -69
  15. package/plugin-src/client/styles.js +22 -7
  16. package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
  17. package/plugin-src/host/channels/discord/index.mjs +8 -10
  18. package/plugin-src/host/channels/feishu/index.mjs +8 -15
  19. package/plugin-src/host/channels/office/index.mjs +9 -5
  20. package/plugin-src/host/channels/qq/index.mjs +8 -15
  21. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  22. package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
  23. package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
  24. package/plugin-src/host/channels/shared/startup.mjs +47 -0
  25. package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
  26. package/plugin-src/host/channels/slack/index.mjs +8 -10
  27. package/plugin-src/host/channels/telegram/index.mjs +8 -10
  28. package/plugin-src/host/channels/wecom/index.mjs +8 -15
  29. package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
  30. package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
  31. package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
  32. package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
  33. package/plugin-src/host/channels/weixin/index.mjs +8 -15
  34. package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
  35. package/plugin-src/host/delivery-adapter.mjs +4 -0
  36. package/plugin-src/host/delivery-suggestions.mjs +4 -0
  37. package/plugin-src/host/index.mjs +7 -2
  38. package/scripts/verify-model-setting.mjs +54 -0
  39. package/src/channels/feishu/bridge.mjs +295 -9
  40. package/src/channels/feishu/feishu-cards.mjs +38 -0
  41. package/src/channels/feishu/feishu-channel.mjs +88 -20
  42. package/src/channels/shared/bot-workspace-store.mjs +11 -3
  43. package/src/channels/shared/command-catalog.mjs +102 -0
  44. package/src/channels/shared/i18n-en/feishu.mjs +12 -0
  45. package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
  46. package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
  47. package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
  48. package/src/channels/shared/i18n-en.mjs +2 -0
  49. package/src/channels/shared/model-setting.mjs +43 -8
  50. package/src/channels/shared/text-harness-bridge.mjs +2 -26
  51. package/src/channels/telegram/telegram-api.mjs +18 -6
  52. package/src/channels/telegram/telegram-runtime.mjs +34 -32
  53. package/src/channels/wecom/send-error.mjs +31 -0
  54. package/src/channels/wecom/wecom-bridge.mjs +59 -17
  55. package/src/channels/wecom-app/callback-server.mjs +471 -0
  56. package/src/channels/wecom-app/config-store.mjs +240 -0
  57. package/src/channels/wecom-app/harness-client.mjs +11 -0
  58. package/src/channels/wecom-app/state-store.mjs +107 -0
  59. package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
  60. package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
  61. package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
  62. package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
@@ -24,7 +24,8 @@ import {
24
24
  validateContextEnhancementConfig,
25
25
  } from './context-enhancement.mjs';
26
26
  import {
27
- modelCatalogHas,
27
+ confirmsModelSelection,
28
+ modelCatalogEntry,
28
29
  normalizeModelCatalog,
29
30
  sameModelSelection,
30
31
  validateModelSelection,
@@ -1372,7 +1373,7 @@ export function createBotWorkspaceScope(
1372
1373
  model,
1373
1374
  options.signal ? { signal: options.signal } : {},
1374
1375
  );
1375
- if (!sameModelSelection(selected?.selected, model)) {
1376
+ if (!confirmsModelSelection(selected?.selected, model)) {
1376
1377
  const error = new Error('Harness did not confirm the selected model');
1377
1378
  error.code = 'model-selection-mismatch';
1378
1379
  throw error;
@@ -1625,9 +1626,16 @@ export function createWorkspaceAwareController(controller, {
1625
1626
  const catalog = model && modelCatalog
1626
1627
  ? await resolveModelCatalog(modelCatalog)
1627
1628
  : null;
1628
- if (model && (!modelCatalog || !modelCatalogHas(catalog, model))) {
1629
+ const entry = modelCatalogEntry(catalog, model);
1630
+ if (model && (!modelCatalog || !entry)) {
1629
1631
  throw unavailableModel();
1630
1632
  }
1633
+ if (model?.reasoningEffort !== undefined
1634
+ && !entry.reasoning?.efforts.some((effort) => effort.id === model.reasoningEffort)) {
1635
+ const error = new Error('当前模型不支持所选思考强度,请重新选择。');
1636
+ error.code = 'model-reasoning-unavailable';
1637
+ throw error;
1638
+ }
1631
1639
  await workspaces.setModel(botId, model, { incarnation });
1632
1640
  return decorateResult(
1633
1641
  workspaces,
@@ -0,0 +1,102 @@
1
+ import { t } from './i18n.mjs';
2
+
3
+ const TEXT_CHANNELS = Object.freeze(['telegram', 'slack', 'discord', 'whatsapp']);
4
+
5
+ function defineCatalogCommand(name, description, help, { aliases = [], ...options } = {}) {
6
+ return Object.freeze({
7
+ name,
8
+ description,
9
+ channels: TEXT_CHANNELS,
10
+ menuVisible: true,
11
+ ...options,
12
+ aliases: Object.freeze(aliases.map((alias) => Object.freeze({
13
+ menuVisible: true,
14
+ ...(typeof alias === 'string' ? { name: alias } : alias),
15
+ }))),
16
+ help: Object.freeze(help),
17
+ });
18
+ }
19
+
20
+ // Presentation metadata for TextHarnessBridge commands, in help order.
21
+ // Keep each command's aliases, short description and full usage together here.
22
+ // Handlers still own parsing and authorization; privateOnly is descriptive.
23
+ // enabled: false removes a definition from both consumers; menuVisible: false
24
+ // hides a command (or alias) only from the native menu, leaving help available.
25
+ export const SHARED_COMMAND_CATALOG = Object.freeze([
26
+ defineCatalogCommand('new', '开启一个全新会话', [
27
+ '/new 开启一个全新会话',
28
+ ]),
29
+ defineCatalogCommand('compact', '压缩当前会话的较早上下文', [
30
+ '/compact 压缩当前会话的较早上下文',
31
+ ]),
32
+ defineCatalogCommand('history', '查看最近历史消息(仅私聊)', [
33
+ '/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
34
+ ], { privateOnly: true }),
35
+ defineCatalogCommand('workspace', '切换工作区', [
36
+ '/workspace 工作区序号或绝对路径 切换工作区',
37
+ ], { aliases: ['ws'] }),
38
+ defineCatalogCommand('workspacelist', '列出工作区绝对路径', [
39
+ '/workspacelist 列出工作区绝对路径',
40
+ '/ws、/wsl、/workspaces 工作区命令别名',
41
+ ], { aliases: ['workspaces', 'wsl'] }),
42
+ defineCatalogCommand('sessionlist', '列出会话 ID 和标题', [
43
+ '/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
44
+ '/sessionlist --limit N 仅列出当前工作区前 N 个会话',
45
+ ], { aliases: ['sessions'] }),
46
+ defineCatalogCommand('session', '将当前聊天绑定到指定会话', [
47
+ '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
48
+ ]),
49
+ defineCatalogCommand('models', '按序号列出所有可用模型', [
50
+ '/models 按序号列出所有可用模型',
51
+ ]),
52
+ defineCatalogCommand('reasoninglist', '按序号列出当前模型可用推理等级', [
53
+ '/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
54
+ ], { aliases: ['reasonings'] }),
55
+ defineCatalogCommand('reasoning', '查看或切换当前推理等级', [
56
+ '/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
57
+ ]),
58
+ defineCatalogCommand('model', '查看或切换当前会话模型', [
59
+ '/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型',
60
+ '示例:先发 /models,再发 /model 2 [推理等级ID]',
61
+ ]),
62
+ defineCatalogCommand('presetlist', '列出可用 Agent Preset', [
63
+ '/presetlist 或 /presets 按序号列出可用 Agent Preset',
64
+ ], { aliases: ['presets'] }),
65
+ defineCatalogCommand('preset', '查看或设置新会话 Agent Preset', [
66
+ '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
67
+ '纯数字 ID:/preset id:<ID>',
68
+ '/preset --default 跟随 Host 默认',
69
+ ]),
70
+ defineCatalogCommand('stop', '停止当前任务', [
71
+ '/stop 停止当前任务',
72
+ ]),
73
+ defineCatalogCommand('steer', '纠偏当前任务', [
74
+ '/steer 补充指令 纠偏当前任务',
75
+ ]),
76
+ defineCatalogCommand('batch', '开始批量输入(仅私聊)', [
77
+ '/batch 开始批量输入(仅私聊,最多 10 条文字)',
78
+ ], { privateOnly: true }),
79
+ defineCatalogCommand('send', '提交当前批次', [
80
+ '/send 提交当前批次',
81
+ ], { privateOnly: true }),
82
+ defineCatalogCommand('cancel', '取消当前批次', [
83
+ '/cancel 取消当前批次',
84
+ ], { privateOnly: true }),
85
+ defineCatalogCommand('status', '检查连接状态', [
86
+ '/status 检查连接状态',
87
+ ]),
88
+ defineCatalogCommand('version', '查看插件版本', [
89
+ '/version 查看插件版本',
90
+ ]),
91
+ defineCatalogCommand('help', '显示帮助', [
92
+ '/help 显示本帮助',
93
+ ]),
94
+ ]);
95
+
96
+ export function commandsForChannel(channel, catalog = SHARED_COMMAND_CATALOG) {
97
+ return catalog.filter((item) => item.enabled !== false && item.channels.includes(channel));
98
+ }
99
+
100
+ export function commandHelpLines(channel, catalog = SHARED_COMMAND_CATALOG) {
101
+ return commandsForChannel(channel, catalog).flatMap((item) => item.help.map((line) => t(line)));
102
+ }
@@ -375,4 +375,16 @@ export default {
375
375
  '任务失败': 'Failed',
376
376
  // feishu/bridge.mjs — step push context injection
377
377
  '📎 已注入会话上下文': '📎 Context injected',
378
+ // feishu/feishu-cards.mjs — question card answered state + custom entry (issue #162)
379
+ '✏️ 其他答案…': '✏️ Other answer…',
380
+ '✅ 已回答{progress}': '✅ Answered{progress}',
381
+ '✅ 已选择:{label}': '✅ Selected: {label}',
382
+ '回答已提交,对话将继续。': 'Answer submitted. The conversation will continue.',
383
+ // feishu/bridge.mjs — answered interaction notices + custom answer guidance (issue #162)
384
+ '这个问题已经回答过了。': 'This question has already been answered.',
385
+ '想自定义答案?直接发送文字消息即可,将作为本题答案提交。':
386
+ 'Want a custom answer? Just send a text message and it will be submitted as your answer.',
387
+ // feishu/bridge.mjs — thinking status heartbeat
388
+ '⏳ 正在思考中…(已运行 {elapsed})': '⏳ Thinking… (elapsed {elapsed})',
389
+ '⏳ 正在思考中…': '⏳ Thinking…',
378
390
  };
@@ -118,6 +118,9 @@ export default {
118
118
  '直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。':
119
119
  'Send text, an image, a file, or a voice message already transcribed to text to continue the current session.',
120
120
  '{label}机器人已连接 DeepSeek Harness。': 'The {label} bot is connected to DeepSeek Harness.',
121
+ '查看最近历史消息(仅私聊)': 'Show recent history (private chats only)',
122
+ '按序号列出当前模型可用推理等级': 'List reasoning efforts for the current model by index',
123
+ '查看或切换当前推理等级': 'Show or switch the current reasoning effort',
121
124
  '/new 开启一个全新会话': '/new Start a brand-new session',
122
125
  '/compact 压缩当前会话的较早上下文': '/compact Compact the earlier context of the current session',
123
126
  '/workspace 工作区序号或绝对路径 切换工作区':
@@ -1,5 +1,14 @@
1
1
  // English translations (shared-c area). Keys are exact Chinese literals passed to t().
2
2
  export default {
3
+ '微信': 'Weixin',
4
+ '飞书': 'Feishu',
5
+ '钉钉': 'DingTalk',
6
+ '企业微信': 'Enterprise WeChat',
7
+ '{channel}正在初始化,请稍后重新读取。': '{channel} is initializing. Please reload shortly.',
8
+ '{channel}配置格式错误。请检查其数据目录中的 config.json 和 workspaces.json,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration is invalid. Check config.json and workspaces.json in its data directory, then restart DSH. See the startup log for details.',
9
+ '{channel}配置格式错误。请检查其数据目录中的 config.json,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration is invalid. Check config.json in its data directory, then restart DSH. See the startup log for details.',
10
+ '无法读取或写入{channel}配置。请检查其数据目录的访问权限,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration could not be read or written. Check access to its data directory, then restart DSH. See the startup log for details.',
11
+ '{channel}初始化失败。请查看 DSH 启动日志中 failed to activate {id} 后的错误,修复后重启 DSH。': '{channel} initialization failed. Check the error after "failed to activate {id}" in the DSH startup log, fix it, then restart DSH.',
3
12
  // history-command.mjs / command help
4
13
  '/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)':
5
14
  '/history [count] Preview recent messages (default 3, maximum 5)',
@@ -0,0 +1,33 @@
1
+ // English translations (wecom-app area). Keys are exact Chinese literals passed to t().
2
+ export default {
3
+ '企业微信应用': 'WeCom app',
4
+ '企业微信应用凭据缺失,请重新绑定。': 'WeCom app credentials are missing; please bind the app again.',
5
+ '企业微信应用凭据不完整,请重新绑定。': 'WeCom app credentials are incomplete; please bind the app again.',
6
+ '企业微信应用连接未就绪,插件会自动重试。': 'The WeCom app connection is not ready; the plugin will retry automatically.',
7
+ '企业微信应用连接未就绪,插件会自动重试': 'The WeCom app connection is not ready; the plugin will retry automatically',
8
+ '企业微信应用已绑定,但尚未就绪。': 'The WeCom app is bound but not ready yet.',
9
+ '企业微信应用已更新,但尚未就绪。': 'The WeCom app settings were saved, but the connection is not ready yet.',
10
+ '企业微信应用仍未就绪,请稍后重试。': 'The WeCom app is still not ready; please try again later.',
11
+ '企业微信应用当前离线': 'The WeCom app is currently offline',
12
+ '企业微信应用回调通道就绪': 'The WeCom app callback channel is ready',
13
+ '企业微信应用({corpId})': 'WeCom app ({corpId})',
14
+ '请输入正确的企业 ID(例如 ww1234567890abcdef)。': 'Enter a valid corp ID (for example ww1234567890abcdef).',
15
+ '请输入正确的应用 AgentId(纯数字)。': 'Enter a valid AgentId (digits only).',
16
+ '应用 Secret 格式不正确。': 'The app Secret format is invalid.',
17
+ '回调 Token 格式不正确。': 'The callback Token format is invalid.',
18
+ 'EncodingAESKey 必须是 43 位字母或数字。': 'EncodingAESKey must be 43 alphanumeric characters.',
19
+ '代理地址必须是 http(s) 地址': 'The API proxy address must be an http(s) URL',
20
+ '代理地址必须是 http(s) 地址。': 'The API proxy address must be an http(s) URL.',
21
+ '回调基址必须是 http(s) 地址': 'The callback base URL must be an http(s) URL',
22
+ '回调基址必须是 http(s) 地址。': 'The callback base URL must be an http(s) URL.',
23
+ '回调密钥已重置,但机器人重启失败,请手动重连。': 'The callback secret was reset, but the bot failed to restart; please reconnect manually.',
24
+ '企业微信应用交互问题发送失败。': 'Failed to send the WeCom app interaction question.',
25
+ '企业微信应用目前支持文字和图片消息。': 'The WeCom app currently supports text and image messages.',
26
+ '企业微信应用已连接 DeepSeek Harness,可在微信中直接使用。': 'The WeCom app is connected to DeepSeek Harness and works directly inside WeChat.',
27
+ '企业微信应用已连接 DeepSeek Harness。直接发送文字或图片即可开始,发送 /help 查看可用命令。': 'The WeCom app is connected to DeepSeek Harness. Send text or images to start, or send /help for available commands.',
28
+ '企业微信应用与 DeepSeek Harness 连接正常。': 'The WeCom app and DeepSeek Harness are connected.',
29
+ '结果文件「{name}」已生成,但企业微信应用当前没有文件消息发送权限,请检查应用的文件发送能力。': 'The result file "{name}" was generated, but the WeCom app currently lacks file-message permission; check the app file capability.',
30
+ '结果文件「{name}」超过企业微信可发送的文件大小,未发送。': 'The result file "{name}" exceeds the WeCom file size limit and was not sent.',
31
+ '结果文件「{name}」已生成,但企业微信拒绝了该文件消息。': 'The result file "{name}" was generated, but WeCom rejected the file message.',
32
+ '结果文件「{name}」发送失败,请稍后重试。': 'The result file "{name}" failed to send; please try again later.',
33
+ };
@@ -9,6 +9,7 @@ import sharedC from './i18n-en/shared-c.mjs';
9
9
  import feishu from './i18n-en/feishu.mjs';
10
10
  import dingtalk from './i18n-en/dingtalk.mjs';
11
11
  import wecom from './i18n-en/wecom.mjs';
12
+ import wecomApp from './i18n-en/wecom-app.mjs';
12
13
  import qq from './i18n-en/qq.mjs';
13
14
  import weixin from './i18n-en/weixin.mjs';
14
15
  import slack from './i18n-en/slack.mjs';
@@ -25,6 +26,7 @@ export const EN = Object.freeze(Object.assign(
25
26
  feishu,
26
27
  dingtalk,
27
28
  wecom,
29
+ wecomApp,
28
30
  qq,
29
31
  weixin,
30
32
  slack,
@@ -1,5 +1,6 @@
1
1
  const MAX_PROVIDER_ID_LENGTH = 256;
2
2
  const MAX_MODEL_ID_LENGTH = 1_024;
3
+ const MAX_EFFORT_ID_LENGTH = 256;
3
4
  const MAX_LABEL_LENGTH = 256;
4
5
  const UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
5
6
  const UNSAFE_ID_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
@@ -27,7 +28,10 @@ export function normalizeModelSelection(value) {
27
28
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
28
29
  const provider = cleanId(value.provider, MAX_PROVIDER_ID_LENGTH);
29
30
  const model = cleanId(value.model, MAX_MODEL_ID_LENGTH);
30
- return provider && model ? { provider, model } : null;
31
+ const reasoningEffort = value.reasoningEffort === undefined
32
+ ? undefined : cleanId(value.reasoningEffort, MAX_EFFORT_ID_LENGTH);
33
+ if (!provider || !model || reasoningEffort === '') return null;
34
+ return { provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) };
31
35
  }
32
36
 
33
37
  export function validateModelSelection(value) {
@@ -42,7 +46,31 @@ export function validateModelSelection(value) {
42
46
  }
43
47
 
44
48
  export function sameModelSelection(left, right) {
45
- return left?.provider === right?.provider && left?.model === right?.model;
49
+ return left?.provider === right?.provider && left?.model === right?.model
50
+ && left?.reasoningEffort === right?.reasoningEffort;
51
+ }
52
+
53
+ // Harness may resolve an omitted effort to the model's default.
54
+ export function confirmsModelSelection(actual, requested) {
55
+ return actual?.provider === requested?.provider && actual?.model === requested?.model
56
+ && (requested?.reasoningEffort === undefined
57
+ || actual?.reasoningEffort === requested.reasoningEffort);
58
+ }
59
+
60
+ function normalizeReasoning(value) {
61
+ if (!value || typeof value !== 'object' || !Array.isArray(value.efforts)) return undefined;
62
+ const efforts = [];
63
+ const seen = new Set();
64
+ for (const entry of value.efforts) {
65
+ const id = cleanId(entry?.id, MAX_EFFORT_ID_LENGTH);
66
+ if (!id || seen.has(id)) continue;
67
+ seen.add(id);
68
+ const description = cleanLabel(entry.description, '');
69
+ efforts.push({ id, name: cleanLabel(entry.name, id), ...(description ? { description } : {}) });
70
+ }
71
+ if (!efforts.length) return undefined;
72
+ const defaultEffort = cleanId(value.defaultEffort, MAX_EFFORT_ID_LENGTH);
73
+ return { efforts, ...(seen.has(defaultEffort) ? { defaultEffort } : {}) };
46
74
  }
47
75
 
48
76
  export function modelSelectionId(value) {
@@ -68,9 +96,13 @@ export function normalizeModelCatalog(value) {
68
96
  const key = `${provider}\u0000${model}`;
69
97
  if (seen.has(key)) continue;
70
98
  seen.add(key);
99
+ const description = cleanLabel(entry.description, '');
100
+ const reasoning = normalizeReasoning(entry.reasoning);
71
101
  models.push({
72
102
  id: model,
73
103
  name: cleanLabel(entry.name, model),
104
+ ...(description ? { description } : {}),
105
+ ...(reasoning ? { reasoning } : {}),
74
106
  });
75
107
  }
76
108
  if (models.length > 0) {
@@ -91,13 +123,16 @@ export function normalizeModelCatalog(value) {
91
123
  return { groups, failures };
92
124
  }
93
125
 
94
- export function modelCatalogHas(catalog, selection) {
126
+ export function modelCatalogEntry(catalog, selection) {
95
127
  const normalized = normalizeModelSelection(selection);
96
- if (!normalized) return false;
97
- return normalizeModelCatalog(catalog).groups.some(
98
- (group) => group.id === normalized.provider
99
- && group.models.some((model) => model.id === normalized.model),
100
- );
128
+ if (!normalized) return undefined;
129
+ return normalizeModelCatalog(catalog).groups
130
+ .filter((group) => group.id === normalized.provider)
131
+ .flatMap((group) => group.models).find((model) => model.id === normalized.model);
132
+ }
133
+
134
+ export function modelCatalogHas(catalog, selection) {
135
+ return modelCatalogEntry(catalog, selection) !== undefined;
101
136
  }
102
137
 
103
138
  export async function listModelCatalog(harness, options = {}) {
@@ -1,5 +1,6 @@
1
1
  import { createDeferredDeliveryCoordinator, deferredOutcomeText } from './deferred-delivery-coordinator.mjs';
2
2
  import { t } from './i18n.mjs';
3
+ import { commandHelpLines } from './command-catalog.mjs';
3
4
  import {
4
5
  COMMAND_PERMISSION_DENIED_MESSAGE,
5
6
  evaluateInboundAccess,
@@ -605,32 +606,7 @@ export class TextHarnessBridge {
605
606
  t('{label}机器人已连接 DeepSeek Harness。', { label: this.#descriptor.label }),
606
607
  '',
607
608
  t('直接发送文字、图片或文件即可继续当前会话。'),
608
- t('/new 开启一个全新会话'),
609
- t('/compact 压缩当前会话的较早上下文'),
610
- t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
611
- t('/workspace 工作区序号或绝对路径 切换工作区'),
612
- t('/workspacelist 列出工作区绝对路径'),
613
- t('/ws、/wsl、/workspaces 工作区命令别名'),
614
- t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
615
- t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
616
- t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
617
- t('/models 按序号列出所有可用模型'),
618
- t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
619
- t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
620
- t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
621
- t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
622
- t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
623
- t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
624
- t('纯数字 ID:/preset id:<ID>'),
625
- t('/preset --default 跟随 Host 默认'),
626
- t('/stop 停止当前任务'),
627
- t('/steer 补充指令 纠偏当前任务'),
628
- t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
629
- t('/send 提交当前批次'),
630
- t('/cancel 取消当前批次'),
631
- t('/status 检查连接状态'),
632
- t('/version 查看插件版本'),
633
- t('/help 显示本帮助'),
609
+ ...commandHelpLines(this.#descriptor.key),
634
610
  ].join('\n'));
635
611
  return;
636
612
  }
@@ -96,8 +96,16 @@ function validBotCommand(value) {
96
96
  return Boolean(value)
97
97
  && typeof value === 'object' && !Array.isArray(value)
98
98
  && typeof value.command === 'string' && TELEGRAM_COMMAND_NAME.test(value.command)
99
- && typeof value.description === 'string' && value.description.length >= 1
100
- && value.description.length <= 256;
99
+ && typeof value.description === 'string' && value.description.trim().length >= 1
100
+ && [...value.description].length <= 256;
101
+ }
102
+
103
+ export function validateTelegramCommands(commands, { allowEmpty = false } = {}) {
104
+ if (!Array.isArray(commands) || (!allowEmpty && commands.length === 0)
105
+ || commands.length > 100 || commands.some((command) => !validBotCommand(command))
106
+ || new Set(commands.map((item) => item.command)).size !== commands.length) {
107
+ throw new TypeError('Telegram bot commands are invalid');
108
+ }
101
109
  }
102
110
 
103
111
  export const COMMANDS_MENU_BUTTON = Object.freeze({ type: 'commands' });
@@ -334,10 +342,7 @@ export class TelegramApi {
334
342
  }
335
343
 
336
344
  async setMyCommands({ commands, scope, languageCode, signal } = {}) {
337
- if (!Array.isArray(commands) || commands.length === 0
338
- || commands.some((command) => !validBotCommand(command))) {
339
- throw new TypeError('Telegram bot commands are invalid');
340
- }
345
+ validateTelegramCommands(commands);
341
346
  return this.#call('setMyCommands', {
342
347
  commands,
343
348
  ...(scope ? { scope } : {}),
@@ -345,6 +350,13 @@ export class TelegramApi {
345
350
  }, { signal });
346
351
  }
347
352
 
353
+ async deleteMyCommands({ scope, languageCode, signal } = {}) {
354
+ return this.#call('deleteMyCommands', {
355
+ ...(scope ? { scope } : {}),
356
+ ...(cleanString(languageCode) ? { language_code: cleanString(languageCode) } : {}),
357
+ }, { signal });
358
+ }
359
+
348
360
  async setChatMenuButton({ menuButton = COMMANDS_MENU_BUTTON, signal } = {}) {
349
361
  if (!menuButton || typeof menuButton !== 'object' || Array.isArray(menuButton)) {
350
362
  throw new TypeError('Telegram menu button is invalid');
@@ -5,7 +5,8 @@ import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
5
5
  import { t } from '../shared/i18n.mjs';
6
6
  import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
7
7
  import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
8
- import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
8
+ import { SHARED_COMMAND_CATALOG, commandsForChannel } from '../shared/command-catalog.mjs';
9
+ import { COMMANDS_MENU_BUTTON, TelegramApi, validateTelegramCommands } from './telegram-api.mjs';
9
10
  import { createTelegramHttpTransport } from './telegram-http.mjs';
10
11
  import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
11
12
  import {
@@ -17,39 +18,30 @@ import {
17
18
  TELEGRAM_ACCESS_MODES,
18
19
  } from './config-store.mjs';
19
20
 
20
- export const TELEGRAM_COMMAND_MENU = Object.freeze([
21
- { command: 'new', description: '开启一个全新会话' },
22
- { command: 'compact', description: '压缩当前会话的较早上下文' },
23
- { command: 'workspace', description: '切换工作区' },
24
- { command: 'ws', description: '切换工作区' },
25
- { command: 'workspacelist', description: '列出工作区绝对路径' },
26
- { command: 'workspaces', description: '列出工作区绝对路径' },
27
- { command: 'wsl', description: '列出工作区绝对路径' },
28
- { command: 'sessionlist', description: '列出会话 ID 和标题' },
29
- { command: 'sessions', description: '列出会话 ID 和标题' },
30
- { command: 'session', description: '将当前聊天绑定到指定会话' },
31
- { command: 'models', description: '按序号列出所有可用模型' },
32
- { command: 'model', description: '查看或切换当前会话模型' },
33
- { command: 'presetlist', description: '列出可用 Agent Preset' },
34
- { command: 'presets', description: '列出可用 Agent Preset' },
35
- { command: 'preset', description: '查看或设置新会话 Agent Preset' },
36
- { command: 'stop', description: '停止当前任务' },
37
- { command: 'steer', description: '纠偏当前任务' },
38
- { command: 'batch', description: '开始批量输入(仅私聊)' },
39
- { command: 'send', description: '提交当前批次' },
40
- { command: 'cancel', description: '取消当前批次' },
41
- { command: 'status', description: '检查连接状态' },
42
- { command: 'version', description: '查看插件版本' },
43
- { command: 'help', description: '显示帮助' },
44
- ]);
21
+ function commandMenuEntries(catalog, translate) {
22
+ return commandsForChannel('telegram', catalog)
23
+ .filter((item) => item.menuVisible !== false)
24
+ .flatMap((item) => [item, ...(item.aliases ?? [])]
25
+ .filter((entry) => entry.menuVisible !== false && entry.enabled !== false
26
+ && (!entry.channels || entry.channels.includes('telegram')))
27
+ .map((entry) => ({
28
+ command: entry.name,
29
+ description: translate(entry.description ?? item.description),
30
+ })));
31
+ }
45
32
 
46
- export function telegramCommandMenu() {
47
- return TELEGRAM_COMMAND_MENU.map((item) => ({
48
- ...item,
49
- description: t(item.description),
50
- }));
33
+ export function telegramCommandMenu(catalog = SHARED_COMMAND_CATALOG) {
34
+ const commands = commandMenuEntries(catalog, t);
35
+ validateTelegramCommands(commands, { allowEmpty: true });
36
+ return commands;
51
37
  }
52
38
 
39
+ // Retain the source-language export for existing consumers, derived from the
40
+ // catalog. Runtime synchronization always generates a fresh, localized list.
41
+ export const TELEGRAM_COMMAND_MENU = Object.freeze(
42
+ commandMenuEntries(SHARED_COMMAND_CATALOG, (text) => text).map((item) => Object.freeze(item)),
43
+ );
44
+
53
45
  function escaped(value) {
54
46
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
55
47
  }
@@ -772,6 +764,7 @@ export class TelegramRuntime {
772
764
  #replyTimeoutMs;
773
765
  #createApi;
774
766
  #createHttpTransport;
767
+ #commandCatalog;
775
768
  #status = createTelegramRuntimeStatus();
776
769
  #httpTransport = null;
777
770
  #api = null;
@@ -791,6 +784,7 @@ export class TelegramRuntime {
791
784
  replyTimeoutMs = 600_000,
792
785
  createApi = (options) => new TelegramApi(options),
793
786
  createHttpTransport = createTelegramHttpTransport,
787
+ commandCatalog = SHARED_COMMAND_CATALOG,
794
788
  }) {
795
789
  if (!config || !token || !harness || !state) {
796
790
  throw new TypeError('TelegramRuntime requires config, token, Harness, and state');
@@ -805,6 +799,7 @@ export class TelegramRuntime {
805
799
  this.#replyTimeoutMs = replyTimeoutMs;
806
800
  this.#createApi = createApi;
807
801
  this.#createHttpTransport = createHttpTransport;
802
+ this.#commandCatalog = commandCatalog;
808
803
  }
809
804
 
810
805
  get status() {
@@ -883,7 +878,14 @@ export class TelegramRuntime {
883
878
  throw error;
884
879
  }
885
880
  try {
886
- await api.setMyCommands({ commands: telegramCommandMenu(), signal: controller.signal });
881
+ const commands = telegramCommandMenu(this.#commandCatalog);
882
+ // Both operations use the existing default scope and language. Sending
883
+ // the full list replaces old entries; an empty catalog clears that list.
884
+ if (commands.length > 0) {
885
+ await api.setMyCommands({ commands, signal: controller.signal });
886
+ } else {
887
+ await api.deleteMyCommands({ signal: controller.signal });
888
+ }
887
889
  await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
888
890
  } catch (error) {
889
891
  this.#logger.warn?.(
@@ -0,0 +1,31 @@
1
+ import { channelDeliveryFailure } from '../shared/message-failure.mjs';
2
+
3
+ // Normalize only errors from SDK send calls, never Harness or menu-building errors.
4
+ export function wecomSendError(error, operation) {
5
+ if (error?.wecomOperation || error?.name === 'AbortError'
6
+ || error instanceof TypeError || error instanceof ReferenceError || error instanceof SyntaxError) return error;
7
+ const value = error?.errcode ?? error?.body?.errcode ?? error?.providerCode;
8
+ const providerCode = typeof value === 'number'
9
+ || (typeof value === 'string' && /^-?\d+$/u.test(value)) ? Number(value) : NaN;
10
+ const rejected = Number.isSafeInteger(providerCode) && providerCode !== 0;
11
+ const disconnected = error?.message === 'WebSocket not connected, unable to send data';
12
+ const wrapped = channelDeliveryFailure(error, { uncertain: !rejected && !disconnected });
13
+ // Keep the same provider mappings as wecomArtifactError.
14
+ if (providerCode === 48002) wrapped.code = 'channel-permission';
15
+ if (providerCode === 45009) wrapped.code = 'channel-rate-limit';
16
+ if (rejected) wrapped.providerCode = providerCode;
17
+ wrapped.wecomOperation = operation;
18
+ wrapped.message = `Enterprise WeChat ${operation} failed (${wrapped.code}, provider=${wrapped.providerCode ?? '-'})`;
19
+ return wrapped;
20
+ }
21
+
22
+ // The SDK rejection frame can contain private payloads. Log only diagnostic fields.
23
+ export function wecomSendDiagnostic(error) {
24
+ if (!error?.wecomOperation) return undefined;
25
+ return {
26
+ operation: error.wecomOperation,
27
+ code: error.code,
28
+ providerCode: error.providerCode,
29
+ status: error.status,
30
+ };
31
+ }