@xmanrui/dsh-im 4.0.0 → 4.1.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 (74) hide show
  1. package/PROACTIVE_DELIVERY.en.md +321 -0
  2. package/PROACTIVE_DELIVERY.md +321 -0
  3. package/README.en.md +11 -5
  4. package/README.md +11 -5
  5. package/lib/client.js +1200 -110
  6. package/lib/index.js +229 -229
  7. package/package.json +4 -1
  8. package/plugin-src/client/channel-card-meta.js +44 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +16 -8
  10. package/plugin-src/client/channels/feishu/index.js +17 -9
  11. package/plugin-src/client/channels/qq/index.js +16 -8
  12. package/plugin-src/client/channels/shared/token-channel.js +16 -8
  13. package/plugin-src/client/channels/wecom/index.js +16 -8
  14. package/plugin-src/client/channels/weixin/index.js +16 -8
  15. package/plugin-src/client/channels/whatsapp/index.js +16 -8
  16. package/plugin-src/client/delivery-settings.js +749 -0
  17. package/plugin-src/client/i18n.js +82 -0
  18. package/plugin-src/client/index.js +59 -22
  19. package/plugin-src/client/styles.js +72 -0
  20. package/plugin-src/host/channels/dingtalk/index.mjs +3 -0
  21. package/plugin-src/host/channels/dingtalk/production.mjs +4 -0
  22. package/plugin-src/host/channels/discord/index.mjs +6 -1
  23. package/plugin-src/host/channels/feishu/index.mjs +3 -0
  24. package/plugin-src/host/channels/feishu/production.mjs +4 -0
  25. package/plugin-src/host/channels/qq/index.mjs +6 -1
  26. package/plugin-src/host/channels/qq/production.mjs +2 -0
  27. package/plugin-src/host/channels/shared/production.mjs +7 -0
  28. package/plugin-src/host/channels/slack/index.mjs +6 -1
  29. package/plugin-src/host/channels/slack/production.mjs +4 -0
  30. package/plugin-src/host/channels/telegram/index.mjs +6 -1
  31. package/plugin-src/host/channels/wecom/index.mjs +6 -1
  32. package/plugin-src/host/channels/wecom/production.mjs +4 -0
  33. package/plugin-src/host/channels/weixin/index.mjs +3 -0
  34. package/plugin-src/host/channels/weixin/production.mjs +4 -0
  35. package/plugin-src/host/channels/whatsapp/index.mjs +6 -1
  36. package/plugin-src/host/channels/whatsapp/production.mjs +4 -0
  37. package/plugin-src/host/delivery-adapter.mjs +179 -0
  38. package/plugin-src/host/delivery-http.mjs +132 -0
  39. package/plugin-src/host/delivery-rpc.mjs +158 -0
  40. package/plugin-src/host/delivery-service.mjs +224 -0
  41. package/plugin-src/host/delivery-suggestions.mjs +135 -0
  42. package/plugin-src/host/index.mjs +32 -5
  43. package/scripts/verify-package.mjs +3 -0
  44. package/src/channels/dingtalk/dingtalk-api.mjs +33 -0
  45. package/src/channels/dingtalk/dingtalk-bridge.mjs +2 -2
  46. package/src/channels/dingtalk/dingtalk-controller.mjs +14 -0
  47. package/src/channels/dingtalk/dingtalk-runtime.mjs +39 -0
  48. package/src/channels/discord/discord-runtime.mjs +16 -0
  49. package/src/channels/feishu/bridge.mjs +4 -4
  50. package/src/channels/feishu/feishu-cards.mjs +4 -4
  51. package/src/channels/feishu/feishu-runtime.mjs +35 -0
  52. package/src/channels/feishu/multi-bot-controller.mjs +15 -0
  53. package/src/channels/qq/qq-bridge.mjs +2 -2
  54. package/src/channels/qq/qq-controller.mjs +14 -0
  55. package/src/channels/qq/qq-runtime.mjs +23 -0
  56. package/src/channels/shared/bot-workspace-store.mjs +192 -6
  57. package/src/channels/shared/i18n-en/feishu.mjs +6 -2
  58. package/src/channels/shared/i18n-en/shared-a.mjs +4 -0
  59. package/src/channels/shared/preset-command.mjs +2 -2
  60. package/src/channels/shared/text-harness-bridge.mjs +7 -2
  61. package/src/channels/shared/token-bot-controller.mjs +16 -0
  62. package/src/channels/shared/workspace-command.mjs +1 -1
  63. package/src/channels/slack/slack-controller.mjs +14 -0
  64. package/src/channels/slack/slack-runtime.mjs +24 -0
  65. package/src/channels/telegram/telegram-runtime.mjs +26 -0
  66. package/src/channels/wecom/state-store.mjs +4 -0
  67. package/src/channels/wecom/wecom-bridge.mjs +2 -2
  68. package/src/channels/wecom/wecom-controller.mjs +14 -0
  69. package/src/channels/wecom/wecom-runtime.mjs +21 -0
  70. package/src/channels/weixin/weixin-bridge.mjs +2 -2
  71. package/src/channels/weixin/weixin-controller.mjs +14 -0
  72. package/src/channels/weixin/weixin-runtime.mjs +24 -0
  73. package/src/channels/whatsapp/whatsapp-controller.mjs +14 -0
  74. package/src/channels/whatsapp/whatsapp-runtime.mjs +19 -0
@@ -563,6 +563,41 @@ export class FeishuRuntime {
563
563
  });
564
564
  }
565
565
 
566
+ async sendProactiveText(target, text, { signal } = {}) {
567
+ if (!this.#status.ready || !this.#client) {
568
+ const error = new Error('飞书机器人尚未连接');
569
+ error.code = 'bot-not-connected';
570
+ throw error;
571
+ }
572
+ const receiveIdType = target?.kind === 'user' ? 'open_id'
573
+ : target?.kind === 'group' ? 'chat_id' : null;
574
+ const receiveId = target?.kind === 'user'
575
+ ? nonEmptyString(target?.route?.openId)
576
+ : target?.kind === 'group'
577
+ ? nonEmptyString(target?.route?.chatId)
578
+ : null;
579
+ if (!receiveIdType || !receiveId) {
580
+ const error = new TypeError('Invalid Feishu proactive delivery target');
581
+ error.code = 'invalid-target';
582
+ throw error;
583
+ }
584
+ signal?.throwIfAborted();
585
+ const response = await this.#client.im.v1.message.create({
586
+ params: { receive_id_type: receiveIdType },
587
+ data: {
588
+ receive_id: receiveId,
589
+ msg_type: 'text',
590
+ content: JSON.stringify({ text }),
591
+ },
592
+ });
593
+ if (response?.code && response.code !== 0) {
594
+ const error = new Error(`Feishu proactive delivery failed: ${response.msg || response.code}`);
595
+ error.code = 'target-rejected';
596
+ throw error;
597
+ }
598
+ return { sent: true };
599
+ }
600
+
566
601
  stop(options = {}) {
567
602
  if (this.#stopping) return this.#stopping;
568
603
 
@@ -537,6 +537,21 @@ export class MultiBotDshFeishuController {
537
537
  });
538
538
  }
539
539
 
540
+ async sendProactiveText(botId, target, text, options = {}) {
541
+ this.#assertOpen();
542
+ return this.#withBotTransition(botId, async () => {
543
+ this.#requireBot(botId);
544
+ const runtime = this.#runtimes.get(botId);
545
+ if (!isConnected(connectionStatus(runtime))
546
+ || typeof runtime.sendProactiveText !== 'function') {
547
+ const error = new Error('飞书机器人尚未连接');
548
+ error.code = 'bot-not-connected';
549
+ throw error;
550
+ }
551
+ return runtime.sendProactiveText(target, text, options);
552
+ });
553
+ }
554
+
540
555
  async disconnectBot(botId) {
541
556
  this.#assertOpen();
542
557
  // An operational pause only: credentials/config remain durable, so the
@@ -83,14 +83,14 @@ function helpText() {
83
83
  t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
84
84
  t('/workspace 工作区绝对路径 切换工作区'),
85
85
  t('/workspacelist 列出工作区绝对路径'),
86
- t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
86
+ t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
87
87
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
88
88
  t('/models 按序号列出所有可用模型'),
89
89
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
90
90
  t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
91
91
  t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
92
92
  t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
93
- t('/presetlist 按序号列出可用 Agent Preset'),
93
+ t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
94
94
  t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
95
95
  t('纯数字 ID:/preset id:<ID>'),
96
96
  t('/preset --default 跟随 Host 默认'),
@@ -268,6 +268,20 @@ export class QqController {
268
268
  });
269
269
  }
270
270
 
271
+ async sendProactiveText(botId, target, text, options = {}) {
272
+ const config = this.#configStore.get(botId);
273
+ if (!config) throw new Error('Unknown QQ bot');
274
+ return this.#withBotTransition(botId, async () => {
275
+ const runtime = this.#runtimes.get(botId);
276
+ if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
277
+ const error = new Error(t('QQ机器人尚未连接'));
278
+ error.code = 'bot-not-connected';
279
+ throw error;
280
+ }
281
+ return runtime.sendProactiveText(target, text, options);
282
+ });
283
+ }
284
+
271
285
  async deleteBot(botId) {
272
286
  const config = this.#configStore.get(botId);
273
287
  if (!config) throw new Error('Unknown QQ bot');
@@ -97,6 +97,29 @@ export class QqRuntime {
97
97
  return { sent: true };
98
98
  }
99
99
 
100
+ async sendProactiveText(target, text, { signal } = {}) {
101
+ const nativeId = target?.kind === 'user'
102
+ ? (typeof target?.route?.userOpenId === 'string' ? target.route.userOpenId.trim() : '')
103
+ : target?.kind === 'group'
104
+ ? (typeof target?.route?.groupOpenId === 'string' ? target.route.groupOpenId.trim() : '')
105
+ : '';
106
+ if (!nativeId) {
107
+ const error = new TypeError('Invalid QQ proactive delivery target');
108
+ error.code = 'invalid-target';
109
+ throw error;
110
+ }
111
+ if (!this.#status.ready || !this.#bot) {
112
+ const error = new Error('QQ bot is not connected');
113
+ error.code = 'bot-not-connected';
114
+ throw error;
115
+ }
116
+ signal?.throwIfAborted();
117
+ return this.#bot.sendText({
118
+ scope: target.kind === 'user' ? 'c2c' : 'group',
119
+ targetId: nativeId,
120
+ }, text);
121
+ }
122
+
100
123
  async start() {
101
124
  if (this.#status.ready && this.#bot) return this.status;
102
125
  if (this.#starting) return this.#starting;
@@ -49,8 +49,90 @@ function botIdOf(value) {
49
49
  return value;
50
50
  }
51
51
 
52
+ function deliveryTargetError(code, message, options) {
53
+ const error = new Error(message, options);
54
+ error.code = code;
55
+ return error;
56
+ }
57
+
58
+ function targetIdOf(value) {
59
+ if (typeof value !== 'string'
60
+ || !/^[A-Za-z0-9._:@-]{1,128}$/.test(value)) {
61
+ throw deliveryTargetError('invalid-target', 'Invalid target id');
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function jsonRecord(value) {
67
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
68
+ throw deliveryTargetError('invalid-target', 'Invalid target route');
69
+ }
70
+ try {
71
+ const serialized = JSON.stringify(value);
72
+ if (serialized === undefined) throw new TypeError('route is not JSON');
73
+ const route = JSON.parse(serialized);
74
+ if (!route || typeof route !== 'object' || Array.isArray(route)) {
75
+ throw new TypeError('route is not an object');
76
+ }
77
+ return route;
78
+ } catch (cause) {
79
+ throw deliveryTargetError('invalid-target', 'Invalid target route', { cause });
80
+ }
81
+ }
82
+
83
+ function normalizeDeliveryTarget(value, { targetId } = {}) {
84
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
85
+ throw deliveryTargetError('invalid-target', 'Invalid delivery target');
86
+ }
87
+ const allowed = targetId === undefined
88
+ ? new Set(['targetId', 'name', 'kind', 'route'])
89
+ : new Set(['name', 'kind', 'route']);
90
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
91
+ throw deliveryTargetError('invalid-target', 'Invalid delivery target');
92
+ }
93
+ const id = targetIdOf(targetId ?? value.targetId);
94
+ if (typeof value.kind !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(value.kind)) {
95
+ throw deliveryTargetError('invalid-target', 'Invalid target kind');
96
+ }
97
+ let name;
98
+ if (value.name !== undefined) {
99
+ if (typeof value.name !== 'string' || !value.name.trim() || value.name.trim().length > 80) {
100
+ throw deliveryTargetError('invalid-target', 'Invalid target name');
101
+ }
102
+ name = value.name.trim();
103
+ }
104
+ return {
105
+ targetId: id,
106
+ ...(name === undefined ? {} : { name }),
107
+ kind: value.kind,
108
+ route: jsonRecord(value.route),
109
+ };
110
+ }
111
+
112
+ function normalizeDeliveryTargets(value) {
113
+ const deliveryTargets = Object.create(null);
114
+ if (value === undefined) return deliveryTargets;
115
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
116
+ try {
117
+ for (const [botId, targets] of Object.entries(value)) {
118
+ botIdOf(botId);
119
+ if (!targets || typeof targets !== 'object' || Array.isArray(targets)) return null;
120
+ const normalizedTargets = Object.create(null);
121
+ for (const [targetId, target] of Object.entries(targets)) {
122
+ const normalized = normalizeDeliveryTarget(target, { targetId });
123
+ const { targetId: _targetId, ...stored } = normalized;
124
+ normalizedTargets[targetId] = stored;
125
+ }
126
+ deliveryTargets[botId] = normalizedTargets;
127
+ }
128
+ } catch {
129
+ return null;
130
+ }
131
+ return deliveryTargets;
132
+ }
133
+
52
134
  function normalizeDocument(value) {
53
- if (!value || value.version !== 1 || !value.workspaces
135
+ if (!value || ![1, 2].includes(value.version) || !value.workspaces
54
136
  || typeof value.workspaces !== 'object' || Array.isArray(value.workspaces)) return null;
55
137
  const workspaces = {};
56
138
  for (const [botId, workspace] of Object.entries(value.workspaces)) {
@@ -83,7 +165,16 @@ function normalizeDocument(value) {
83
165
  }
84
166
  }
85
167
  }
86
- return { version: 1, workspaces, agentPresets, contextEnhancement };
168
+ if (value.version === 1 && value.deliveryTargets !== undefined) return null;
169
+ const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets);
170
+ if (!deliveryTargets) return null;
171
+ return {
172
+ version: value.version,
173
+ workspaces,
174
+ agentPresets,
175
+ contextEnhancement,
176
+ deliveryTargets,
177
+ };
87
178
  }
88
179
 
89
180
  export async function validateWorkspacePath(value) {
@@ -112,9 +203,11 @@ export async function validateWorkspacePath(value) {
112
203
  export class BotWorkspaceStore {
113
204
  #path;
114
205
  #defaultWorkspace;
206
+ #version = 1;
115
207
  #workspaces = {};
116
208
  #agentPresets = {};
117
209
  #contextEnhancement = {};
210
+ #deliveryTargets = Object.create(null);
118
211
  #generations = new Map();
119
212
  #nextGeneration = 1;
120
213
  #incarnations = new Map();
@@ -135,14 +228,18 @@ export class BotWorkspaceStore {
135
228
  try {
136
229
  const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
137
230
  if (!normalized) throw new Error('dsh-im workspace config is invalid');
231
+ this.#version = normalized.version;
138
232
  this.#workspaces = normalized.workspaces;
139
233
  this.#agentPresets = normalized.agentPresets;
140
234
  this.#contextEnhancement = normalized.contextEnhancement;
235
+ this.#deliveryTargets = normalized.deliveryTargets;
141
236
  } catch (error) {
142
237
  if (error?.code !== 'ENOENT') throw error;
238
+ this.#version = 1;
143
239
  this.#workspaces = {};
144
240
  this.#agentPresets = {};
145
241
  this.#contextEnhancement = {};
242
+ this.#deliveryTargets = Object.create(null);
146
243
  }
147
244
  this.#generations.clear();
148
245
  this.#nextGeneration = 1;
@@ -181,6 +278,83 @@ export class BotWorkspaceStore {
181
278
  : DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
182
279
  }
183
280
 
281
+ listDeliveryTargets(botId) {
282
+ const id = botIdOf(botId);
283
+ if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
284
+ return Object.entries(this.#deliveryTargets[id] ?? {})
285
+ .map(([targetId, target]) => normalizeDeliveryTarget(target, { targetId }))
286
+ .sort((left, right) => left.targetId.localeCompare(right.targetId));
287
+ }
288
+
289
+ deliveryTargetFor(botId, targetId) {
290
+ const id = botIdOf(botId);
291
+ const targetKey = targetIdOf(targetId);
292
+ if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
293
+ const target = this.#deliveryTargets[id]?.[targetKey];
294
+ return target ? normalizeDeliveryTarget(target, { targetId: targetKey }) : null;
295
+ }
296
+
297
+ async createDeliveryTarget(botId, value) {
298
+ const id = botIdOf(botId);
299
+ const target = normalizeDeliveryTarget(value);
300
+ return this.#enqueue(id, async () => {
301
+ if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
302
+ if (Object.hasOwn(this.#deliveryTargets[id] ?? {}, target.targetId)) {
303
+ throw deliveryTargetError('target-conflict', 'Target already exists');
304
+ }
305
+ const { targetId, ...stored } = target;
306
+ const next = {
307
+ ...this.#deliveryTargets,
308
+ [id]: { ...(this.#deliveryTargets[id] ?? {}), [targetId]: stored },
309
+ };
310
+ await this.#persist(this.#contextEnhancement, next, 2);
311
+ this.#deliveryTargets = next;
312
+ this.#version = 2;
313
+ return normalizeDeliveryTarget(stored, { targetId });
314
+ });
315
+ }
316
+
317
+ async updateDeliveryTarget(botId, targetId, value) {
318
+ const id = botIdOf(botId);
319
+ const targetKey = targetIdOf(targetId);
320
+ const replacement = normalizeDeliveryTarget(value, { targetId: targetKey });
321
+ return this.#enqueue(id, async () => {
322
+ if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
323
+ if (!Object.hasOwn(this.#deliveryTargets[id] ?? {}, targetKey)) {
324
+ throw deliveryTargetError('unknown-target', 'Unknown target');
325
+ }
326
+ const { targetId: _targetId, ...stored } = replacement;
327
+ const next = {
328
+ ...this.#deliveryTargets,
329
+ [id]: { ...this.#deliveryTargets[id], [targetKey]: stored },
330
+ };
331
+ await this.#persist(this.#contextEnhancement, next, 2);
332
+ this.#deliveryTargets = next;
333
+ this.#version = 2;
334
+ return normalizeDeliveryTarget(stored, { targetId: targetKey });
335
+ });
336
+ }
337
+
338
+ async deleteDeliveryTarget(botId, targetId) {
339
+ const id = botIdOf(botId);
340
+ const targetKey = targetIdOf(targetId);
341
+ return this.#enqueue(id, async () => {
342
+ if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
343
+ if (!Object.hasOwn(this.#deliveryTargets[id] ?? {}, targetKey)) {
344
+ throw deliveryTargetError('unknown-target', 'Unknown target');
345
+ }
346
+ const botTargets = { ...this.#deliveryTargets[id] };
347
+ delete botTargets[targetKey];
348
+ const next = { ...this.#deliveryTargets };
349
+ if (Object.keys(botTargets).length > 0) next[id] = botTargets;
350
+ else delete next[id];
351
+ await this.#persist(this.#contextEnhancement, next, 2);
352
+ this.#deliveryTargets = next;
353
+ this.#version = 2;
354
+ return true;
355
+ });
356
+ }
357
+
184
358
  generationFor(botId) {
185
359
  return this.#generations.get(botIdOf(botId)) ?? null;
186
360
  }
@@ -471,6 +645,7 @@ export class BotWorkspaceStore {
471
645
  ...Object.keys(this.#workspaces),
472
646
  ...Object.keys(this.#agentPresets),
473
647
  ...Object.keys(this.#contextEnhancement),
648
+ ...Object.keys(this.#deliveryTargets),
474
649
  ...this.#dirtyRemovals,
475
650
  ]);
476
651
  for (const botId of candidates) {
@@ -518,10 +693,13 @@ export class BotWorkspaceStore {
518
693
  const hadWorkspace = Object.hasOwn(this.#workspaces, id);
519
694
  const hadPreset = Object.hasOwn(this.#agentPresets, id);
520
695
  const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
521
- const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement || this.#dirtyRemovals.has(id);
696
+ const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
697
+ const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
698
+ || hadDeliveryTargets || this.#dirtyRemovals.has(id);
522
699
  delete this.#workspaces[id];
523
700
  delete this.#agentPresets[id];
524
701
  delete this.#contextEnhancement[id];
702
+ delete this.#deliveryTargets[id];
525
703
  this.#generations.delete(id);
526
704
  this.#incarnations.delete(id);
527
705
  if (!needsCleanup) return {
@@ -551,14 +729,21 @@ export class BotWorkspaceStore {
551
729
  return queued;
552
730
  }
553
731
 
554
- async #persist(contextEnhancement = this.#contextEnhancement) {
555
- const document = { version: 1, workspaces: this.#workspaces };
732
+ async #persist(
733
+ contextEnhancement = this.#contextEnhancement,
734
+ deliveryTargets = this.#deliveryTargets,
735
+ version = this.#version,
736
+ ) {
737
+ const document = { version, workspaces: this.#workspaces };
556
738
  if (Object.keys(this.#agentPresets).length > 0) {
557
739
  document.agentPresets = this.#agentPresets;
558
740
  }
559
741
  if (Object.keys(contextEnhancement).length > 0) {
560
742
  document.contextEnhancement = contextEnhancement;
561
743
  }
744
+ if (version >= 2 && Object.keys(deliveryTargets).length > 0) {
745
+ document.deliveryTargets = deliveryTargets;
746
+ }
562
747
  await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
563
748
  const temporary = `${this.#path}.tmp`;
564
749
  await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
@@ -572,7 +757,8 @@ export class BotWorkspaceStore {
572
757
  async #persistCurrentDocument() {
573
758
  if (Object.keys(this.#workspaces).length > 0
574
759
  || Object.keys(this.#agentPresets).length > 0
575
- || Object.keys(this.#contextEnhancement).length > 0) {
760
+ || Object.keys(this.#contextEnhancement).length > 0
761
+ || Object.keys(this.#deliveryTargets).length > 0) {
576
762
  await this.#persist();
577
763
  return;
578
764
  }
@@ -223,6 +223,8 @@ export default {
223
223
  '📊 系统状态': '📊 System status',
224
224
  '📋 会话 / 工作区': '📋 Sessions / workspace',
225
225
  '/sessionlist 列出工作区会话': '/sessionlist List workspace sessions',
226
+ '/sessionlist 或 /sessions 列出工作区会话':
227
+ '/sessionlist or /sessions List workspace sessions',
226
228
  '/session ID 绑定已有会话': '/session ID Bind an existing session',
227
229
  '/workspacelist 列出工作区': '/workspacelist List workspaces',
228
230
  '/workspace 路径 切换工作区': '/workspace PATH Switch workspace',
@@ -239,12 +241,14 @@ export default {
239
241
  '📦 批量输入(仅私聊)': '📦 Batch input (direct messages only)',
240
242
  '🤖 预设 / 模型': '🤖 Presets / models',
241
243
  '/models 列出模型': '/models List models',
244
+ '/presetlist 或 /presets 列出可用 Agent Preset':
245
+ '/presetlist or /presets List available Agent Presets',
242
246
  '🎮 任务控制': '🎮 Task controls',
243
247
  '/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
244
248
  '**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
245
249
  '**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
246
- '**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
247
- '**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
250
+ '**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/workspace 路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` 或 `/presets` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
251
+ '**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/workspace PATH` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` or `/presets` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
248
252
  '**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
249
253
  '**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Complete permissions · **6** Help',
250
254
  '从下方下拉选择补充指令;最后一项可自定义输入。':
@@ -124,6 +124,8 @@ export default {
124
124
  '/workspacelist 列出工作区绝对路径': '/workspacelist List absolute workspace paths',
125
125
  '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题':
126
126
  '/sessionlist [workspace index or absolute path] List session IDs and titles',
127
+ '/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题':
128
+ '/sessionlist or /sessions [workspace index or absolute path] List session IDs and titles',
127
129
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
128
130
  '/session <Session ID or workspace index> Bind this chat to the specified session',
129
131
  '/models 按序号列出所有可用模型': '/models List all available models by index',
@@ -139,6 +141,8 @@ export default {
139
141
  '示例:先发 /models,再发 /model 2 [推理等级ID]':
140
142
  'Example: send /models first, then /model 2 [reasoning effort ID]',
141
143
  '/presetlist 按序号列出可用 Agent Preset': '/presetlist List available Agent Presets by index',
144
+ '/presetlist 或 /presets 按序号列出可用 Agent Preset':
145
+ '/presetlist or /presets List available Agent Presets by index',
142
146
  '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset':
143
147
  '/preset [index or full ID] Show or set the Agent Preset of this bot',
144
148
  '纯数字 ID:/preset id:<ID>': 'Numeric-only ID: /preset id:<ID>',
@@ -8,7 +8,7 @@ import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
8
8
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
9
9
 
10
10
  const PRESET_COMMAND = /^\/preset(?=$|\s)/iu;
11
- const PRESET_LIST_COMMAND = /^\/presetlist(?=$|\s)/iu;
11
+ const PRESET_LIST_COMMAND = /^\/(?:presetlist|presets)(?=$|\s)/iu;
12
12
  const PRESET_LIST_USAGE = '用法:/presetlist(不带参数)';
13
13
  const PRESET_USAGE = [
14
14
  '用法:',
@@ -257,7 +257,7 @@ export async function runPresetCommand(text, harness, state, key, options = {})
257
257
  const requestOptions = rpcOptions(options.signal);
258
258
 
259
259
  if (PRESET_LIST_COMMAND.test(command)) {
260
- if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
260
+ if (!/^\/(?:presetlist|presets)[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
261
261
  try {
262
262
  const current = await settings(harness, requestOptions);
263
263
  saveSnapshot(state, key, current.agentPresetCatalog.items);
@@ -469,6 +469,11 @@ export class TextHarnessBridge {
469
469
  });
470
470
  }
471
471
 
472
+ sendProactiveText(target, text, { signal } = {}) {
473
+ signal?.throwIfAborted();
474
+ return this.#bot.sendText(target, text);
475
+ }
476
+
472
477
  async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt) {
473
478
  const delivery = await deliverOutboundArtifacts({
474
479
  artifacts,
@@ -542,14 +547,14 @@ export class TextHarnessBridge {
542
547
  t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
543
548
  t('/workspace 工作区绝对路径 切换工作区'),
544
549
  t('/workspacelist 列出工作区绝对路径'),
545
- t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
550
+ t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
546
551
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
547
552
  t('/models 按序号列出所有可用模型'),
548
553
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
549
554
  t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
550
555
  t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
551
556
  t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
552
- t('/presetlist 按序号列出可用 Agent Preset'),
557
+ t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
553
558
  t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
554
559
  t('纯数字 ID:/preset id:<ID>'),
555
560
  t('/preset --default 跟随 Host 默认'),
@@ -227,6 +227,22 @@ export class TokenBotController {
227
227
  });
228
228
  }
229
229
 
230
+ async sendProactiveText(botId, target, text, options = {}) {
231
+ const config = this.#configStore.get(botId);
232
+ if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
233
+ return this.#withBotTransition(botId, async () => {
234
+ const runtime = this.#runtimes.get(botId);
235
+ if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
236
+ const error = new Error(t('{label}机器人尚未连接', {
237
+ label: this.#descriptor.label,
238
+ }));
239
+ error.code = 'bot-not-connected';
240
+ throw error;
241
+ }
242
+ return runtime.sendProactiveText(target, text, options);
243
+ });
244
+ }
245
+
230
246
  async deleteBot(botId) {
231
247
  const config = this.#configStore.get(botId);
232
248
  if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
@@ -6,7 +6,7 @@ import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
6
6
 
7
7
  const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
8
8
  const WORKSPACE_LIST_COMMAND = /^\/workspacelist(?:\s+([\s\S]+))?$/i;
9
- const SESSION_LIST_COMMAND = /^\/sessionlist(?:\s+([\s\S]+))?$/i;
9
+ const SESSION_LIST_COMMAND = /^\/(?:sessionlist|sessions)(?:\s+([\s\S]+))?$/i;
10
10
  const SESSION_BIND_PREFIX = /^\/session(?=$|\s)/i;
11
11
  const SESSION_BIND_COMMAND = /^\/session[ \t]+([^\s]+)$/i;
12
12
  const MAX_WORKSPACE_PATH_LENGTH = 4_096;
@@ -189,6 +189,20 @@ export class SlackController {
189
189
  });
190
190
  }
191
191
 
192
+ async sendProactiveText(botId, target, text, options = {}) {
193
+ const config = this.#configStore.get(botId);
194
+ if (!config) throw new Error('Unknown Slack bot');
195
+ return this.#withBotTransition(botId, async () => {
196
+ const runtime = this.#runtimes.get(botId);
197
+ if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
198
+ const error = new Error(t('Slack机器人尚未连接'));
199
+ error.code = 'bot-not-connected';
200
+ throw error;
201
+ }
202
+ return runtime.sendProactiveText(target, text, options);
203
+ });
204
+ }
205
+
192
206
  async deleteBot(botId) {
193
207
  const config = this.#configStore.get(botId);
194
208
  if (!config) throw new Error('Unknown Slack bot');
@@ -407,6 +407,30 @@ export class SlackRuntime {
407
407
  return this.#bridge.sendConnectionTest(text);
408
408
  }
409
409
 
410
+ async sendProactiveText(target, text, options = {}) {
411
+ if (!this.#status.ready || !this.#bridge) {
412
+ const error = new Error('Slack bot is not connected');
413
+ error.code = 'bot-not-connected';
414
+ throw error;
415
+ }
416
+ const channelId = typeof target?.route?.channelId === 'string'
417
+ ? target.route.channelId.trim() : '';
418
+ const threadTs = typeof target?.route?.threadTs === 'string'
419
+ ? target.route.threadTs.trim() : '';
420
+ if (!channelId
421
+ || (target?.kind !== 'conversation' && target?.kind !== 'thread')
422
+ || (target.kind === 'thread' && !threadTs)
423
+ || (target.kind === 'conversation' && threadTs)) {
424
+ const error = new TypeError('Invalid Slack proactive delivery target');
425
+ error.code = 'invalid-target';
426
+ throw error;
427
+ }
428
+ return this.#bridge.sendProactiveText({
429
+ channelId,
430
+ ...(threadTs ? { threadTs } : {}),
431
+ }, text, options);
432
+ }
433
+
410
434
  async start() {
411
435
  if (this.#status.ready && this.#socket) return this.status;
412
436
  if (this.#starting) return this.#starting;
@@ -22,10 +22,12 @@ export const TELEGRAM_COMMAND_MENU = Object.freeze([
22
22
  { command: 'workspace', description: '切换工作区' },
23
23
  { command: 'workspacelist', description: '列出工作区绝对路径' },
24
24
  { command: 'sessionlist', description: '列出会话 ID 和标题' },
25
+ { command: 'sessions', description: '列出会话 ID 和标题' },
25
26
  { command: 'session', description: '将当前聊天绑定到指定会话' },
26
27
  { command: 'models', description: '按序号列出所有可用模型' },
27
28
  { command: 'model', description: '查看或切换当前会话模型' },
28
29
  { command: 'presetlist', description: '列出可用 Agent Preset' },
30
+ { command: 'presets', description: '列出可用 Agent Preset' },
29
31
  { command: 'preset', description: '查看或设置新会话 Agent Preset' },
30
32
  { command: 'stop', description: '停止当前任务' },
31
33
  { command: 'steer', description: '纠偏当前任务' },
@@ -717,6 +719,30 @@ export class TelegramRuntime {
717
719
  return this.#bridge.sendConnectionTest(text);
718
720
  }
719
721
 
722
+ async sendProactiveText(target, text, options = {}) {
723
+ if (!this.#status.ready || !this.#bridge) {
724
+ const error = new Error('Telegram bot is not connected');
725
+ error.code = 'bot-not-connected';
726
+ throw error;
727
+ }
728
+ const chatIdText = typeof target?.route?.chatId === 'string'
729
+ ? target.route.chatId.trim() : '';
730
+ const chatId = Number(chatIdText);
731
+ const messageThreadId = target?.route?.messageThreadId;
732
+ if (!/^-?\d+$/.test(chatIdText) || !Number.isSafeInteger(chatId)
733
+ || (target?.kind !== 'chat' && target?.kind !== 'topic')
734
+ || (target.kind === 'topic' && (!Number.isSafeInteger(messageThreadId) || messageThreadId <= 0))
735
+ || (target.kind === 'chat' && messageThreadId !== undefined)) {
736
+ const error = new TypeError('Invalid Telegram proactive delivery target');
737
+ error.code = 'invalid-target';
738
+ throw error;
739
+ }
740
+ return this.#bridge.sendProactiveText({
741
+ chatId,
742
+ ...(target.kind === 'topic' ? { messageThreadId } : {}),
743
+ }, text, options);
744
+ }
745
+
720
746
  async start() {
721
747
  if (this.#status.ready && this.#pollTask) return this.status;
722
748
  if (this.#starting) return this.#starting;
@@ -72,6 +72,10 @@ export class WecomStateStore {
72
72
  await this.#persist();
73
73
  }
74
74
 
75
+ snapshot() {
76
+ return structuredClone(this.#state);
77
+ }
78
+
75
79
  async remove() {
76
80
  try {
77
81
  await unlink(this.#path);