@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
@@ -15,6 +15,10 @@ import {
15
15
  isModelCommand,
16
16
  runModelCommand,
17
17
  } from '../shared/model-command.mjs';
18
+ import {
19
+ isPresetCommand,
20
+ runPresetCommand,
21
+ } from '../shared/preset-command.mjs';
18
22
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
19
23
  import {
20
24
  fetchImageBuffer,
@@ -49,6 +53,10 @@ const HELP_TEXT = [
49
53
  '/models 按序号列出所有可用模型',
50
54
  '/model [序号或完整模型ID] 查看或切换当前会话模型',
51
55
  '示例:先发 /models,再发 /model 2',
56
+ '/presetlist 按序号列出可用 Agent Preset',
57
+ '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
58
+ '纯数字 ID:/preset id:<ID>',
59
+ '/preset --default 跟随 Host 默认',
52
60
  '/stop 停止当前任务',
53
61
  '/steer 补充指令 纠偏当前任务',
54
62
  '/status 检查连接状态',
@@ -201,7 +209,9 @@ export class QqHarnessBridge {
201
209
  const commandText = safeText(message);
202
210
  const commandRunner = isControlCommand(commandText)
203
211
  ? runControlCommand
204
- : (isModelCommand(commandText) ? runModelCommand : null);
212
+ : (isModelCommand(commandText)
213
+ ? runModelCommand
214
+ : (isPresetCommand(commandText) ? runPresetCommand : null));
205
215
  const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
206
216
  const addressed = message.kind !== 'group'
207
217
  || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
@@ -535,6 +535,19 @@ function resolveAgentPresetCatalog(catalog) {
535
535
  : normalizeAgentPresetCatalog(value);
536
536
  }
537
537
 
538
+ function unavailableAgentPreset() {
539
+ const error = new Error('Agent Preset 不存在或不可用。');
540
+ error.code = 'agent-preset-unavailable';
541
+ return error;
542
+ }
543
+
544
+ function assertCurrentBotScope(isCurrentScope) {
545
+ if (isCurrentScope()) return;
546
+ const error = new Error('找不到要修改的机器人。');
547
+ error.code = 'workspace-bot-not-found';
548
+ throw error;
549
+ }
550
+
538
551
  function decorateResult(workspaces, result, catalog) {
539
552
  const decorate = (value) => {
540
553
  const decorated = workspaces.decorateStatus(value);
@@ -580,14 +593,74 @@ export function observeBotWorkspaceRemovals(
580
593
  });
581
594
  }
582
595
 
583
- export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
596
+ export function createBotWorkspaceScope(
597
+ harness,
598
+ { botId, workspaces, state, agentPresetCatalog } = {},
599
+ ) {
584
600
  if (!harness || !workspaces || !state) throw new TypeError('harness, workspaces, and state are required');
585
601
  const incarnation = workspaces.incarnationFor(botId);
586
602
  const isCurrentScope = () => workspaces.has(botId)
587
603
  && workspaces.incarnationFor(botId) === incarnation;
604
+ const presetSettings = async (catalog = agentPresetCatalog) => {
605
+ let normalizedCatalog;
606
+ try {
607
+ normalizedCatalog = await resolveAgentPresetCatalog(catalog)
608
+ ?? normalizeAgentPresetCatalog(null);
609
+ } catch (error) {
610
+ assertCurrentBotScope(isCurrentScope);
611
+ throw error;
612
+ }
613
+ assertCurrentBotScope(isCurrentScope);
614
+ return {
615
+ agentPreset: workspaces.agentPresetFor(botId),
616
+ agentPresetCatalog: normalizedCatalog,
617
+ };
618
+ };
588
619
  const sessionGenerations = new Map();
589
620
  const scopedHarness = new Proxy(harness, {
590
621
  get(target, property) {
622
+ if (property === 'agentPresetSettings') {
623
+ return async (options = {}) => {
624
+ options?.signal?.throwIfAborted();
625
+ assertCurrentBotScope(isCurrentScope);
626
+ const settings = await presetSettings();
627
+ options?.signal?.throwIfAborted();
628
+ return settings;
629
+ };
630
+ }
631
+ if (property === 'updateAgentPreset') {
632
+ return async (value, options = {}) => {
633
+ options?.signal?.throwIfAborted();
634
+ assertCurrentBotScope(isCurrentScope);
635
+ const agentPreset = value === '--default' ? null : validateAgentPresetId(value);
636
+ let catalog = null;
637
+ if (agentPreset) {
638
+ ({ agentPresetCatalog: catalog } = await presetSettings());
639
+ options?.signal?.throwIfAborted();
640
+ if (!catalog.items.some((item) => item.id === agentPreset)) {
641
+ throw unavailableAgentPreset();
642
+ }
643
+ }
644
+ await workspaces.setAgentPreset(botId, agentPreset, { incarnation });
645
+ assertCurrentBotScope(isCurrentScope);
646
+ if (catalog) {
647
+ return {
648
+ agentPreset: workspaces.agentPresetFor(botId),
649
+ agentPresetCatalog: catalog,
650
+ };
651
+ }
652
+ try {
653
+ return await presetSettings();
654
+ } catch (error) {
655
+ if (error?.code === 'workspace-bot-not-found') throw error;
656
+ assertCurrentBotScope(isCurrentScope);
657
+ return {
658
+ agentPreset: workspaces.agentPresetFor(botId),
659
+ agentPresetCatalog: normalizeAgentPresetCatalog(null),
660
+ };
661
+ }
662
+ };
663
+ }
591
664
  if (property === 'currentWorkspace') {
592
665
  return () => {
593
666
  if (!isCurrentScope()) {
@@ -912,9 +985,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
912
985
  : null;
913
986
  if (normalizedAgentPreset && agentPresetCatalog
914
987
  && !catalog?.items.some((item) => item.id === normalizedAgentPreset)) {
915
- const error = new Error('Agent Preset 不存在或不可用。');
916
- error.code = 'agent-preset-unavailable';
917
- throw error;
988
+ throw unavailableAgentPreset();
918
989
  }
919
990
  await workspaces.setAgentPreset(botId, normalizedAgentPreset, { incarnation });
920
991
  return decorateResult(
@@ -0,0 +1,305 @@
1
+ import {
2
+ normalizeAgentPresetCatalog,
3
+ normalizeAgentPresetId,
4
+ } from './agent-preset.mjs';
5
+ import { withSessionBindingLock } from './session-binding-lock.mjs';
6
+ import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
7
+ import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
8
+
9
+ const PRESET_COMMAND = /^\/preset(?=$|\s)/iu;
10
+ const PRESET_LIST_COMMAND = /^\/presetlist(?=$|\s)/iu;
11
+ const PRESET_LIST_USAGE = '用法:/presetlist(不带参数)';
12
+ const PRESET_USAGE = [
13
+ '用法:',
14
+ '/preset 查看当前设置',
15
+ '/preset <序号> 按最近一次 /presetlist 的序号选择',
16
+ '/preset <ID> 按 Agent Preset ID 选择',
17
+ '/preset id:<纯数字 ID> 选择纯数字 ID',
18
+ '/preset --default 跟随 Host 默认',
19
+ ].join('\n');
20
+ const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
21
+ const LIST_SNAPSHOTS = new WeakMap();
22
+ export const PRESET_LIST_SNAPSHOT_TTL_MS = 15 * 60_000;
23
+ export const PRESET_LIST_SNAPSHOT_MAX_ENTRIES = 256;
24
+
25
+ function commandResult(message) {
26
+ return {
27
+ handled: true,
28
+ message,
29
+ messages: splitWorkspaceCommandMessage(message),
30
+ };
31
+ }
32
+
33
+ function safeDisplayText(value) {
34
+ if (typeof value !== 'string') return '';
35
+ return value.replace(UNSAFE_DISPLAY_TEXT_GLOBAL, ' ').replace(/\s+/gu, ' ').trim();
36
+ }
37
+
38
+ function rpcOptions(signal) {
39
+ return signal ? { signal } : {};
40
+ }
41
+
42
+ function normalizeSettings(value) {
43
+ if (!value || typeof value !== 'object' || Array.isArray(value)
44
+ || !value.agentPresetCatalog || typeof value.agentPresetCatalog !== 'object'
45
+ || !Array.isArray(value.agentPresetCatalog.items)) {
46
+ throw new TypeError('Harness returned invalid Agent Preset settings');
47
+ }
48
+ const agentPreset = value.agentPreset === null
49
+ ? null
50
+ : normalizeAgentPresetId(value.agentPreset);
51
+ if (value.agentPreset !== null && agentPreset === null) {
52
+ throw new TypeError('Harness returned an invalid current Agent Preset');
53
+ }
54
+ return {
55
+ agentPreset,
56
+ agentPresetCatalog: normalizeAgentPresetCatalog(value.agentPresetCatalog),
57
+ };
58
+ }
59
+
60
+ function presetItemText(item) {
61
+ const label = safeDisplayText(item.label) || item.id;
62
+ return `${label}(${item.id})`;
63
+ }
64
+
65
+ function itemFor(catalog, id) {
66
+ return catalog.items.find((item) => item.id === id) ?? null;
67
+ }
68
+
69
+ function defaultDescription(catalog) {
70
+ if (!catalog.defaultId) return '未设置或当前不可用';
71
+ const item = itemFor(catalog, catalog.defaultId);
72
+ return item
73
+ ? presetItemText(item)
74
+ : `${catalog.defaultId}(当前不可用)`;
75
+ }
76
+
77
+ function currentDescription(settings) {
78
+ const { agentPreset, agentPresetCatalog: catalog } = settings;
79
+ if (agentPreset === null) {
80
+ const item = itemFor(catalog, catalog.defaultId);
81
+ return item
82
+ ? `跟随 Host 默认:${presetItemText(item)}`
83
+ : '跟随 Host 默认(Host 默认当前不可用)';
84
+ }
85
+ const item = itemFor(catalog, agentPreset);
86
+ return item
87
+ ? presetItemText(item)
88
+ : `${agentPreset}(已不可用)`;
89
+ }
90
+
91
+ function formatCurrent(settings) {
92
+ return [
93
+ '当前机器人用于新会话的 Agent Preset:',
94
+ currentDescription(settings),
95
+ '',
96
+ '已有会话不会受此设置影响。',
97
+ '查看可用项:/presetlist',
98
+ '恢复跟随 Host 默认:/preset --default',
99
+ ].join('\n');
100
+ }
101
+
102
+ function formatList(settings) {
103
+ const { agentPreset, agentPresetCatalog: catalog } = settings;
104
+ const lines = [
105
+ '当前机器人用于新会话的 Agent Preset:',
106
+ currentDescription(settings),
107
+ '',
108
+ `Host 默认:${defaultDescription(catalog)}`,
109
+ '',
110
+ `可用 Agent Preset(${catalog.items.length}):`,
111
+ ];
112
+ if (catalog.items.length === 0) {
113
+ lines.push('当前没有可用 Agent Preset。');
114
+ } else {
115
+ catalog.items.forEach((item, index) => {
116
+ const markers = [];
117
+ if (item.id === catalog.defaultId) markers.push('Host 默认');
118
+ if (item.id === agentPreset) markers.push('当前选择');
119
+ if (agentPreset === null && item.id === catalog.defaultId) markers.push('当前生效');
120
+ const annotation = markers.length > 0 ? `(${markers.join(',')})` : '';
121
+ lines.push(`${index + 1}. ${presetItemText(item)}${annotation}`);
122
+ });
123
+ }
124
+ lines.push(
125
+ '',
126
+ '选择:/preset <序号或 ID>',
127
+ '纯数字 ID:/preset id:<ID>',
128
+ '恢复跟随 Host 默认:/preset --default',
129
+ );
130
+ return lines.join('\n');
131
+ }
132
+
133
+ function formatUpdated(settings) {
134
+ return [
135
+ '当前机器人用于新会话的 Agent Preset 已设置为:',
136
+ currentDescription(settings),
137
+ '',
138
+ '已有会话不变。若当前聊天已有会话,请先发送 /new,再发送普通消息,才会使用新设置创建会话。',
139
+ ].join('\n');
140
+ }
141
+
142
+ function stateSnapshots(state, { create = false } = {}) {
143
+ if ((typeof state !== 'object' || state === null) && typeof state !== 'function') return null;
144
+ let snapshots = LIST_SNAPSHOTS.get(state);
145
+ if (!snapshots && create) {
146
+ snapshots = new Map();
147
+ LIST_SNAPSHOTS.set(state, snapshots);
148
+ }
149
+ return snapshots ?? null;
150
+ }
151
+
152
+ function pruneExpiredSnapshots(snapshots, now) {
153
+ for (const [snapshotKey, snapshot] of snapshots) {
154
+ if (snapshot.expiresAt <= now) snapshots.delete(snapshotKey);
155
+ }
156
+ }
157
+
158
+ function saveSnapshot(state, key, items) {
159
+ const snapshots = stateSnapshots(state, { create: true });
160
+ if (!snapshots) return;
161
+ const now = Date.now();
162
+ pruneExpiredSnapshots(snapshots, now);
163
+ snapshots.delete(key);
164
+ snapshots.set(key, {
165
+ expiresAt: now + PRESET_LIST_SNAPSHOT_TTL_MS,
166
+ ids: items.map((item) => item.id),
167
+ });
168
+ while (snapshots.size > PRESET_LIST_SNAPSHOT_MAX_ENTRIES) {
169
+ const oldest = snapshots.keys().next();
170
+ if (oldest.done) break;
171
+ snapshots.delete(oldest.value);
172
+ }
173
+ }
174
+
175
+ function loadSnapshot(state, key) {
176
+ const snapshots = stateSnapshots(state);
177
+ const snapshot = snapshots?.get(key);
178
+ if (!snapshots || !snapshot) return null;
179
+ if (snapshot.expiresAt <= Date.now()) {
180
+ snapshots.delete(key);
181
+ return null;
182
+ }
183
+ snapshots.delete(key);
184
+ snapshots.set(key, snapshot);
185
+ return snapshot.ids;
186
+ }
187
+
188
+ function presetFromSnapshot(state, key, requested) {
189
+ if (!/^\d+$/u.test(requested)) return { numeric: false, id: null };
190
+ const index = Number(requested);
191
+ if (!Number.isSafeInteger(index) || index < 1) {
192
+ return { numeric: true, error: 'Agent Preset 序号无效,请先执行 /presetlist。' };
193
+ }
194
+ const snapshot = loadSnapshot(state, key);
195
+ if (!snapshot) {
196
+ return { numeric: true, error: '请先执行 /presetlist,再按列表序号选择 Agent Preset。' };
197
+ }
198
+ const id = snapshot[index - 1];
199
+ return id
200
+ ? { numeric: true, id }
201
+ : { numeric: true, error: 'Agent Preset 序号不存在,请重新执行 /presetlist。' };
202
+ }
203
+
204
+ function errorCode(error) {
205
+ return error?.code ?? error?.failure?.code;
206
+ }
207
+
208
+ function presetErrorMessage(error, action) {
209
+ const code = errorCode(error);
210
+ if (code === 'agent-preset-invalid') {
211
+ return `Agent Preset ID 格式无效。\n${PRESET_USAGE}`;
212
+ }
213
+ if (code === 'agent-preset-unavailable') {
214
+ return 'Agent Preset 不存在或当前不可用,请重新执行 /presetlist。';
215
+ }
216
+ if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
217
+ return '工作区或机器人状态已发生变化,请重试。';
218
+ }
219
+ if (code === 'cancelled' || error?.name === 'AbortError') {
220
+ if (action === 'list') return '获取 Agent Preset 列表已取消。';
221
+ if (action === 'current') return '获取 Agent Preset 设置已取消。';
222
+ return 'Agent Preset 修改已取消。';
223
+ }
224
+ if (action === 'list') return '暂时无法获取 Agent Preset 列表,请稍后重试。';
225
+ if (action === 'current') return '暂时无法获取 Agent Preset 设置,请稍后重试。';
226
+ return 'Agent Preset 修改失败,请稍后重试。';
227
+ }
228
+
229
+ async function settings(harness, options) {
230
+ if (typeof harness?.agentPresetSettings !== 'function') {
231
+ throw new TypeError('Harness does not support Agent Preset settings');
232
+ }
233
+ return normalizeSettings(await harness.agentPresetSettings(options));
234
+ }
235
+
236
+ async function update(harness, value, options) {
237
+ if (typeof harness?.updateAgentPreset !== 'function') {
238
+ throw new TypeError('Harness does not support updating Agent Preset settings');
239
+ }
240
+ return normalizeSettings(await harness.updateAgentPreset(value, options));
241
+ }
242
+
243
+ export function isPresetCommand(text) {
244
+ if (typeof text !== 'string') return false;
245
+ const command = text.trim();
246
+ return PRESET_LIST_COMMAND.test(command) || PRESET_COMMAND.test(command);
247
+ }
248
+
249
+ export async function runPresetCommand(text, harness, state, key, options = {}) {
250
+ if (!isPresetCommand(text)) return null;
251
+ const command = text.trim();
252
+ if (options.hasImages) {
253
+ return commandResult('Agent Preset 命令仅支持纯文字,请移除图片后重试。');
254
+ }
255
+ const requestOptions = rpcOptions(options.signal);
256
+
257
+ if (PRESET_LIST_COMMAND.test(command)) {
258
+ if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(PRESET_LIST_USAGE);
259
+ try {
260
+ const current = await settings(harness, requestOptions);
261
+ saveSnapshot(state, key, current.agentPresetCatalog.items);
262
+ return commandResult(formatList(current));
263
+ } catch (error) {
264
+ return commandResult(presetErrorMessage(error, 'list'));
265
+ }
266
+ }
267
+
268
+ const match = /^\/preset(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(command);
269
+ if (!match) return commandResult(PRESET_USAGE);
270
+ const requested = match[1];
271
+ if (!requested) {
272
+ try {
273
+ return commandResult(formatCurrent(await settings(harness, requestOptions)));
274
+ } catch (error) {
275
+ return commandResult(presetErrorMessage(error, 'current'));
276
+ }
277
+ }
278
+
279
+ let selected;
280
+ if (requested.toLowerCase() === '--default') {
281
+ selected = null;
282
+ } else {
283
+ const explicitNumericId = /^id:(\d+)$/iu.exec(requested);
284
+ if (explicitNumericId) {
285
+ selected = explicitNumericId[1];
286
+ } else {
287
+ const fromSnapshot = presetFromSnapshot(state, key, requested);
288
+ if (fromSnapshot.numeric) {
289
+ if (fromSnapshot.error) return commandResult(fromSnapshot.error);
290
+ selected = fromSnapshot.id;
291
+ } else {
292
+ selected = normalizeAgentPresetId(requested);
293
+ if (!selected) return commandResult(`Agent Preset ID 格式无效。\n${PRESET_USAGE}`);
294
+ }
295
+ }
296
+ }
297
+
298
+ try {
299
+ return await withSessionBindingLock(state, key, async () => (
300
+ commandResult(formatUpdated(await update(harness, selected, requestOptions)))
301
+ ));
302
+ } catch (error) {
303
+ return commandResult(presetErrorMessage(error, 'update'));
304
+ }
305
+ }
@@ -12,6 +12,10 @@ import {
12
12
  isModelCommand,
13
13
  runModelCommand,
14
14
  } from './model-command.mjs';
15
+ import {
16
+ isPresetCommand,
17
+ runPresetCommand,
18
+ } from './preset-command.mjs';
15
19
  import { askInWorkspaceSession } from './workspace-session.mjs';
16
20
  import { HarnessApprovalQueue } from './harness-approval.mjs';
17
21
  import {
@@ -122,7 +126,9 @@ export class TextHarnessBridge {
122
126
  const text = cleanText(normalized.content);
123
127
  const commandRunner = isControlCommand(text)
124
128
  ? runControlCommand
125
- : (isModelCommand(text) ? runModelCommand : null);
129
+ : (isModelCommand(text)
130
+ ? runModelCommand
131
+ : (isPresetCommand(text) ? runPresetCommand : null));
126
132
  if (commandRunner && (normalized.kind !== 'group' || normalized.addressed === true)) {
127
133
  let task;
128
134
  task = this.#processFastCommand(
@@ -319,6 +325,10 @@ export class TextHarnessBridge {
319
325
  '/models 按序号列出所有可用模型',
320
326
  '/model [序号或完整模型ID] 查看或切换当前会话模型',
321
327
  '示例:先发 /models,再发 /model 2',
328
+ '/presetlist 按序号列出可用 Agent Preset',
329
+ '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
330
+ '纯数字 ID:/preset id:<ID>',
331
+ '/preset --default 跟随 Host 默认',
322
332
  '/stop 停止当前任务',
323
333
  '/steer 补充指令 纠偏当前任务',
324
334
  '/status 检查连接状态',
@@ -16,6 +16,18 @@ export function validTelegramToken(value) {
16
16
  return typeof value === 'string' && /^\d{5,20}:[A-Za-z0-9_-]{20,}$/.test(value.trim());
17
17
  }
18
18
 
19
+ const TELEGRAM_COMMAND_NAME = /^[a-z0-9_]{1,32}$/;
20
+
21
+ function validBotCommand(value) {
22
+ return Boolean(value)
23
+ && typeof value === 'object' && !Array.isArray(value)
24
+ && typeof value.command === 'string' && TELEGRAM_COMMAND_NAME.test(value.command)
25
+ && typeof value.description === 'string' && value.description.length >= 1
26
+ && value.description.length <= 256;
27
+ }
28
+
29
+ export const COMMANDS_MENU_BUTTON = Object.freeze({ type: 'commands' });
30
+
19
31
  export class TelegramApi {
20
32
  #token;
21
33
  #fetch;
@@ -113,6 +125,25 @@ export class TelegramApi {
113
125
  }, { signal });
114
126
  }
115
127
 
128
+ async setMyCommands({ commands, scope, languageCode, signal } = {}) {
129
+ if (!Array.isArray(commands) || commands.length === 0
130
+ || commands.some((command) => !validBotCommand(command))) {
131
+ throw new TypeError('Telegram bot commands are invalid');
132
+ }
133
+ return this.#call('setMyCommands', {
134
+ commands,
135
+ ...(scope ? { scope } : {}),
136
+ ...(cleanString(languageCode) ? { language_code: cleanString(languageCode) } : {}),
137
+ }, { signal });
138
+ }
139
+
140
+ async setChatMenuButton({ menuButton = COMMANDS_MENU_BUTTON, signal } = {}) {
141
+ if (!menuButton || typeof menuButton !== 'object' || Array.isArray(menuButton)) {
142
+ throw new TypeError('Telegram menu button is invalid');
143
+ }
144
+ return this.#call('setChatMenuButton', { menu_button: menuButton }, { signal });
145
+ }
146
+
116
147
  async #call(method, payload, { signal, timeoutMs = 15_000 } = {}) {
117
148
  const url = new URL(this.#baseUrl);
118
149
  url.pathname = `${url.pathname.replace(/\/$/, '')}/bot${this.#token}/${method}`;
@@ -1,11 +1,28 @@
1
1
  import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
2
- import { TelegramApi } from './telegram-api.mjs';
2
+ import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
3
3
  import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
4
4
  import {
5
5
  TELEGRAM_ACCESS_MODES,
6
6
  normalizeTelegramAccessPolicy,
7
7
  } from './config-store.mjs';
8
8
 
9
+ export const TELEGRAM_COMMAND_MENU = Object.freeze([
10
+ { command: 'new', description: '开启一个全新会话' },
11
+ { command: 'compact', description: '压缩当前会话的较早上下文' },
12
+ { command: 'workspace', description: '切换工作区' },
13
+ { command: 'workspacelist', description: '列出工作区绝对路径' },
14
+ { command: 'sessionlist', description: '列出会话 ID 和标题' },
15
+ { command: 'session', description: '将当前聊天绑定到指定会话' },
16
+ { command: 'models', description: '按序号列出所有可用模型' },
17
+ { command: 'model', description: '查看或切换当前会话模型' },
18
+ { command: 'presetlist', description: '列出可用 Agent Preset' },
19
+ { command: 'preset', description: '查看或设置新会话 Agent Preset' },
20
+ { command: 'stop', description: '停止当前任务' },
21
+ { command: 'steer', description: '纠偏当前任务' },
22
+ { command: 'status', description: '检查连接状态' },
23
+ { command: 'help', description: '显示帮助' },
24
+ ]);
25
+
9
26
  function escaped(value) {
10
27
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
28
  }
@@ -290,6 +307,15 @@ export class TelegramRuntime {
290
307
  error.code = 'webhook-configured';
291
308
  throw error;
292
309
  }
310
+ try {
311
+ await api.setMyCommands({ commands: TELEGRAM_COMMAND_MENU, signal: controller.signal });
312
+ await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
313
+ } catch (error) {
314
+ this.#logger.warn?.(
315
+ `[dsh-im:telegram] bot ${this.#config.botId} command menu setup failed:`,
316
+ error,
317
+ );
318
+ }
293
319
  const client = new TelegramBotClient({ api, signal: controller.signal });
294
320
  this.#bridge = new TelegramHarnessBridge({
295
321
  bot: client,
@@ -14,6 +14,10 @@ import {
14
14
  isModelCommand,
15
15
  runModelCommand,
16
16
  } from '../shared/model-command.mjs';
17
+ import {
18
+ isPresetCommand,
19
+ runPresetCommand,
20
+ } from '../shared/preset-command.mjs';
17
21
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
18
22
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
19
23
  import {
@@ -37,6 +41,10 @@ const HELP_TEXT = [
37
41
  '/models 按序号列出所有可用模型',
38
42
  '/model [序号或完整模型ID] 查看或切换当前会话模型',
39
43
  '示例:先发 /models,再发 /model 2',
44
+ '/presetlist 按序号列出可用 Agent Preset',
45
+ '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
46
+ '纯数字 ID:/preset id:<ID>',
47
+ '/preset --default 跟随 Host 默认',
40
48
  '/stop 停止当前任务',
41
49
  '/steer 补充指令 纠偏当前任务',
42
50
  '/status 检查连接状态',
@@ -292,7 +300,9 @@ export class WecomHarnessBridge {
292
300
  const commandText = nonEmptyString(commandMessage.content) ?? '';
293
301
  const commandRunner = isControlCommand(commandText)
294
302
  ? runControlCommand
295
- : (isModelCommand(commandText) ? runModelCommand : null);
303
+ : (isModelCommand(commandText)
304
+ ? runModelCommand
305
+ : (isPresetCommand(commandText) ? runPresetCommand : null));
296
306
  if (commandRunner) {
297
307
  let task;
298
308
  task = this.#processFastCommand(
@@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
184
184
  function baseInfo() {
185
185
  return {
186
186
  channel_version: WEIXIN_PROTOCOL_VERSION,
187
- bot_agent: 'DeepSeekHarness/0.18.0',
187
+ bot_agent: 'DeepSeekHarness/1.0.0',
188
188
  };
189
189
  }
190
190
 
@@ -19,6 +19,10 @@ import {
19
19
  isModelCommand,
20
20
  runModelCommand,
21
21
  } from '../shared/model-command.mjs';
22
+ import {
23
+ isPresetCommand,
24
+ runPresetCommand,
25
+ } from '../shared/preset-command.mjs';
22
26
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
23
27
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
24
28
  import {
@@ -45,6 +49,10 @@ const HELP_TEXT = [
45
49
  '/models 按序号列出所有可用模型',
46
50
  '/model [序号或完整模型ID] 查看或切换当前会话模型',
47
51
  '示例:先发 /models,再发 /model 2',
52
+ '/presetlist 按序号列出可用 Agent Preset',
53
+ '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
54
+ '纯数字 ID:/preset id:<ID>',
55
+ '/preset --default 跟随 Host 默认',
48
56
  '/stop 停止当前任务',
49
57
  '/steer 补充指令 纠偏当前任务',
50
58
  '/status 检查连接状态',
@@ -166,7 +174,9 @@ export class WeixinHarnessBridge {
166
174
  const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
167
175
  const commandRunner = isControlCommand(commandText)
168
176
  ? runControlCommand
169
- : (isModelCommand(commandText) ? runModelCommand : null);
177
+ : (isModelCommand(commandText)
178
+ ? runModelCommand
179
+ : (isPresetCommand(commandText) ? runPresetCommand : null));
170
180
  if (commandRunner && sender === this.#ownerUserId) {
171
181
  let task;
172
182
  task = this.#processFastCommand(