@xmanrui/dsh-im 4.6.0 → 4.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.6.0",
3
+ "version": "4.8.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -34,6 +34,10 @@
34
34
  {
35
35
  "name": "divingleee",
36
36
  "url": "https://github.com/divingleee"
37
+ },
38
+ {
39
+ "name": "Chan-0312",
40
+ "url": "https://github.com/Chan-0312"
37
41
  }
38
42
  ],
39
43
  "license": "MIT",
@@ -17,6 +17,8 @@ const FIELD_LABELS = Object.freeze({
17
17
  senderId: '发送者标识',
18
18
  senderName: '发送者昵称',
19
19
  conversationTitle: '会话标题',
20
+ chatId: '会话标识',
21
+ threadId: '话题标识',
20
22
  botId: '机器人标识',
21
23
  });
22
24
 
@@ -29,6 +31,14 @@ const FIELD_HELP = Object.freeze({
29
31
  labelKey: 'conversationTitleHelpLabel',
30
32
  text: '该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<dsh_im_source> 中也会省略 conversationTitle。',
31
33
  }),
34
+ chatId: Object.freeze({
35
+ labelKey: 'chatIdHelpLabel',
36
+ text: '该字段不是每个渠道都能提供。会话标识用于区分不同的群组或私聊,飞书群聊会带上群 ID。当前消息没有会话标识时,即使已选择该字段,<dsh_im_source> 中也会省略 chatId。',
37
+ }),
38
+ threadId: Object.freeze({
39
+ labelKey: 'threadIdHelpLabel',
40
+ text: '该字段不是每个渠道都能提供。飞书话题群的消息会带上话题 ID,用于区分同一群组内的不同话题;当前消息不在话题中时,即使已选择该字段,<dsh_im_source> 中也会省略 threadId。',
41
+ }),
32
42
  });
33
43
 
34
44
  const SCOPE_COPY = Object.freeze({
@@ -38,6 +48,8 @@ const SCOPE_COPY = Object.freeze({
38
48
  fieldsHelpLabel: '查看群聊来源字段说明',
39
49
  senderNameHelpLabel: '查看群聊发送者昵称字段说明',
40
50
  conversationTitleHelpLabel: '查看群聊会话标题字段说明',
51
+ chatIdHelpLabel: '查看群聊会话标识字段说明',
52
+ threadIdHelpLabel: '查看群聊话题标识字段说明',
41
53
  guidanceLabel: '增强提示词',
42
54
  guidanceHelpLabel: '查看群聊增强提示词使用说明',
43
55
  guidanceUsage: '用于告诉模型如何使用当前群聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
@@ -49,6 +61,8 @@ const SCOPE_COPY = Object.freeze({
49
61
  fieldsHelpLabel: '查看私聊来源字段说明',
50
62
  senderNameHelpLabel: '查看私聊发送者昵称字段说明',
51
63
  conversationTitleHelpLabel: '查看私聊会话标题字段说明',
64
+ chatIdHelpLabel: '查看私聊会话标识字段说明',
65
+ threadIdHelpLabel: '查看私聊话题标识字段说明',
52
66
  guidanceLabel: '增强提示词',
53
67
  guidanceHelpLabel: '查看私聊增强提示词使用说明',
54
68
  guidanceUsage: '用于告诉模型如何使用当前私聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
@@ -292,6 +292,14 @@ const EN = Object.freeze({
292
292
  '查看群聊会话标题字段说明': 'View group conversation title field details',
293
293
  '查看私聊会话标题字段说明': 'View direct conversation title field details',
294
294
  '该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<dsh_im_source> 中也会省略 conversationTitle。': 'This field is not available on every channel. DingTalk group chats include the group name. If the current message has no conversation title, <dsh_im_source> omits conversationTitle even when the field is selected.',
295
+ '会话标识': 'Chat ID',
296
+ '查看群聊会话标识字段说明': 'View group chat ID field details',
297
+ '查看私聊会话标识字段说明': 'View direct chat ID field details',
298
+ '该字段不是每个渠道都能提供。会话标识用于区分不同的群组或私聊,飞书群聊会带上群 ID。当前消息没有会话标识时,即使已选择该字段,<dsh_im_source> 中也会省略 chatId。': 'This field is not available on every channel. The chat ID distinguishes different groups or direct chats, and Feishu group chats include the group ID. If the current message has no chat ID, <dsh_im_source> omits chatId even when the field is selected.',
299
+ '话题标识': 'Topic ID',
300
+ '查看群聊话题标识字段说明': 'View group topic ID field details',
301
+ '查看私聊话题标识字段说明': 'View direct topic ID field details',
302
+ '该字段不是每个渠道都能提供。飞书话题群的消息会带上话题 ID,用于区分同一群组内的不同话题;当前消息不在话题中时,即使已选择该字段,<dsh_im_source> 中也会省略 threadId。': 'This field is not available on every channel. Messages in a Feishu topic chat carry the topic ID, which distinguishes different topics inside the same group. If the current message is not inside a topic, <dsh_im_source> omits threadId even when the field is selected.',
295
303
  '机器人标识': 'Bot ID',
296
304
  '增强提示词': 'Guidance',
297
305
  '查看增强提示词使用说明': 'View guidance instructions',
@@ -319,7 +327,7 @@ const EN = Object.freeze({
319
327
  '上下文增强保存失败,请重试。': 'Could not save context enhancement. Try again.',
320
328
  '请提交完整的上下文增强设置。': 'Submit the complete context enhancement settings.',
321
329
  '群聊和私聊开关必须是布尔值。': 'Group and direct switches must be booleans.',
322
- '来源字段只能选择已定义的六个字段。': 'Source fields must be chosen from the six defined fields.',
330
+ '来源字段只能选择已定义的八个字段。': 'Source fields must be chosen from the eight defined fields.',
323
331
  '保存中…': 'Saving…',
324
332
  '未设置': 'Not set',
325
333
  '工作区修改失败,请重试。': 'Could not update the workspace. Try again.',
@@ -7,6 +7,17 @@ function pickerErrorCode(error) {
7
7
  return error?.rpcError?.code ?? error?.code;
8
8
  }
9
9
 
10
+ const PICKER_ERROR_KINDS = new Map([
11
+ ['directory-picker/unavailable', 'unavailable'],
12
+ ['directory-picker-unavailable', 'unavailable'],
13
+ ['directory-picker/unreadable', 'unreadable'],
14
+ ['directory-unreadable', 'unreadable'],
15
+ ]);
16
+
17
+ function pickerErrorKind(error) {
18
+ return PICKER_ERROR_KINDS.get(pickerErrorCode(error));
19
+ }
20
+
10
21
  function pickerErrorDetails(error) {
11
22
  return error?.rpcError?.details ?? error?.details;
12
23
  }
@@ -110,9 +121,9 @@ export function WorkspaceDirectoryPicker({
110
121
  const initialPath = initialPathRef.current;
111
122
  const initial = await loadDirectory(initialPath || undefined, { reportError: false });
112
123
  if (!active || initial.aborted || initial.value) return;
113
- const code = pickerErrorCode(initial.error);
124
+ const kind = pickerErrorKind(initial.error);
114
125
  const details = pickerErrorDetails(initial.error);
115
- if (code === 'directory-picker-unavailable'
126
+ if (kind === 'unavailable'
116
127
  && details?.capability === 'native'
117
128
  && typeof picker.pickDirectory === 'function') {
118
129
  setLoading(true);
@@ -128,7 +139,7 @@ export function WorkspaceDirectoryPicker({
128
139
  }
129
140
  return;
130
141
  }
131
- if (initialPath && code === 'directory-unreadable') {
142
+ if (initialPath && kind === 'unreadable') {
132
143
  const home = await loadDirectory(undefined, { reportError: false });
133
144
  if (!active || home.aborted || home.value) return;
134
145
  setError(pickerErrorMessage(home.error));
@@ -82,6 +82,7 @@ const HELP_TEXT_LINES = [
82
82
  '/workspace 工作区序号或绝对路径 切换工作区',
83
83
  '/workspacelist 列出工作区绝对路径',
84
84
  '/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
85
+ '/sessionlist --limit N 仅列出当前工作区前 N 个会话',
85
86
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
86
87
  '/models 按序号列出所有可用模型',
87
88
  '/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
@@ -1135,6 +1136,7 @@ export class DingtalkHarnessBridge {
1135
1136
  senderId: sender,
1136
1137
  senderName: message.senderNick,
1137
1138
  conversationTitle: message.conversationTitle,
1139
+ chatId: message.conversationId,
1138
1140
  }));
1139
1141
  contextEnhanced = content !== originalContent;
1140
1142
  }
@@ -47,7 +47,12 @@ import {
47
47
  isPresetCommand,
48
48
  runPresetCommand,
49
49
  } from '../shared/preset-command.mjs';
50
- import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
50
+ import {
51
+ parseSessionListArgument,
52
+ resolveSessionListWorkspace,
53
+ runWorkspaceCommand,
54
+ workspacePathSnapshot,
55
+ } from '../shared/workspace-command.mjs';
51
56
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
52
57
  import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
53
58
  import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
@@ -160,6 +165,7 @@ const WORKSPACE_HELP_LINES = [
160
165
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
161
166
  '/workspacelist 列出工作区绝对路径',
162
167
  '/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
168
+ '/sessionlist --limit N 仅列出当前工作区前 N 个会话',
163
169
  ];
164
170
 
165
171
  /** Safe user-facing text for bind/workspace failures (no raw messages). */
@@ -1085,8 +1091,18 @@ export class FeishuHarnessBridge {
1085
1091
  return;
1086
1092
  }
1087
1093
  if (SESSION_LIST_PREFIX.test(commandText)) {
1088
- const selector = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim() || null;
1089
- await this.#showSessions({ chatId: event.message.chat_id, key, replyTo: event.message.message_id }, selector, 0);
1094
+ const argument = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim();
1095
+ const request = parseSessionListArgument(argument);
1096
+ if (request.error) {
1097
+ await this.#send(event.message.chat_id, request.error, { replyTo: event.message.message_id });
1098
+ return;
1099
+ }
1100
+ await this.#showSessions(
1101
+ { chatId: event.message.chat_id, key, replyTo: event.message.message_id },
1102
+ request.selector || null,
1103
+ 0,
1104
+ { limit: request.limit },
1105
+ );
1090
1106
  return;
1091
1107
  }
1092
1108
  if (WORKSPACE_LIST_COMMAND.test(commandText)) {
@@ -1850,6 +1866,7 @@ export class FeishuHarnessBridge {
1850
1866
  messageId = null,
1851
1867
  sessionWorkspace = null,
1852
1868
  sessionPage = 0,
1869
+ sessionLimit = null,
1853
1870
  selections = [],
1854
1871
  }) {
1855
1872
  // Confirmations triggered by a card interaction stay anchored to the
@@ -1861,7 +1878,7 @@ export class FeishuHarnessBridge {
1861
1878
  { chatId, key, replyTo: messageId },
1862
1879
  sessionWorkspace,
1863
1880
  page,
1864
- { updateMessageId: messageId },
1881
+ { updateMessageId: messageId, limit: sessionLimit },
1865
1882
  );
1866
1883
  return;
1867
1884
  }
@@ -2040,7 +2057,7 @@ export class FeishuHarnessBridge {
2040
2057
  { chatId, key, replyTo: messageId },
2041
2058
  sessionWorkspace,
2042
2059
  sessionPage,
2043
- { updateMessageId: messageId },
2060
+ { updateMessageId: messageId, limit: sessionLimit },
2044
2061
  );
2045
2062
  }
2046
2063
  return;
@@ -2056,7 +2073,7 @@ export class FeishuHarnessBridge {
2056
2073
  { chatId, key, replyTo: messageId },
2057
2074
  sessionWorkspace,
2058
2075
  sessionPage,
2059
- { updateMessageId: messageId },
2076
+ { updateMessageId: messageId, limit: sessionLimit },
2060
2077
  );
2061
2078
  }
2062
2079
  }
@@ -2141,7 +2158,7 @@ export class FeishuHarnessBridge {
2141
2158
  { chatId, key, replyTo = null },
2142
2159
  selector,
2143
2160
  page = 0,
2144
- { updateMessageId = null } = {},
2161
+ { updateMessageId = null, limit = null } = {},
2145
2162
  ) {
2146
2163
  try {
2147
2164
  const signal = this.#cardDataSignal();
@@ -2151,7 +2168,11 @@ export class FeishuHarnessBridge {
2151
2168
  return;
2152
2169
  }
2153
2170
  const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
2154
- const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
2171
+ const visibleSessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
2172
+ const sessionLimit = Number.isSafeInteger(limit) && limit > 0 ? limit : null;
2173
+ const sessions = sessionLimit === null
2174
+ ? visibleSessions
2175
+ : visibleSessions.slice(0, sessionLimit);
2155
2176
  const workspace = listed?.workspace ?? resolved.workspace;
2156
2177
  if (sessions.length === 0) {
2157
2178
  await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }), { replyTo });
@@ -2178,6 +2199,7 @@ export class FeishuHarnessBridge {
2178
2199
  // list response's workspace is display data and is not authoritative.
2179
2200
  sessionWorkspace: resolved.workspace,
2180
2201
  sessionPage: safePage,
2202
+ sessionLimit,
2181
2203
  },
2182
2204
  );
2183
2205
  } catch (error) {
@@ -2246,6 +2268,9 @@ export class FeishuHarnessBridge {
2246
2268
  sessionPage: Number.isSafeInteger(options.sessionPage) && options.sessionPage >= 0
2247
2269
  ? options.sessionPage
2248
2270
  : 0,
2271
+ sessionLimit: Number.isSafeInteger(options.sessionLimit) && options.sessionLimit > 0
2272
+ ? options.sessionLimit
2273
+ : null,
2249
2274
  });
2250
2275
  if (this.#cardKeys.size > 200) {
2251
2276
  const oldest = this.#cardKeys.keys().next().value;
@@ -3305,6 +3330,8 @@ export class FeishuHarnessBridge {
3305
3330
  content = enhanceContextContent(originalContent, snapshot, () => ({
3306
3331
  channel: 'feishu',
3307
3332
  senderId: senderOpenId(event),
3333
+ chatId: event.message.chat_id,
3334
+ threadId: event.message.thread_id,
3308
3335
  }));
3309
3336
  contextEnhanced = content !== originalContent;
3310
3337
  }
@@ -488,6 +488,7 @@ export function menuHelpText() {
488
488
  '',
489
489
  '📋 会话 / 工作区',
490
490
  '/sessionlist 或 /sessions 列出工作区会话',
491
+ '/sessionlist --limit N 仅列出当前工作区前 N 个会话',
491
492
  '/session ID 绑定已有会话',
492
493
  '/workspacelist 列出工作区',
493
494
  '/workspace 工作区序号或绝对路径 切换工作区',
@@ -554,6 +555,7 @@ const HELP_TEXT_COMMANDS = [
554
555
  '`/new` — 开启全新会话',
555
556
  '`/session ID` — 绑定已有会话',
556
557
  '`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话',
558
+ '`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话',
557
559
  '`/workspace 工作区序号或绝对路径` — 切换工作区',
558
560
  '`/workspacelist` — 列出工作区',
559
561
  '`/status` — 查看连接状态',
@@ -91,6 +91,7 @@ function helpText() {
91
91
  t('/workspace 工作区序号或绝对路径 切换工作区'),
92
92
  t('/workspacelist 列出工作区绝对路径'),
93
93
  t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
94
+ t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
94
95
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
95
96
  t('/models 按序号列出所有可用模型'),
96
97
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
@@ -889,6 +890,7 @@ export class QqHarnessBridge {
889
890
  channel: 'qq',
890
891
  senderId: sender,
891
892
  senderName: message.kind === 'group' ? message.senderName : undefined,
893
+ chatId: message.kind === 'group' ? message.groupOpenid : message.senderId,
892
894
  }));
893
895
  contextEnhanced = content !== originalContent;
894
896
  }
@@ -1,6 +1,7 @@
1
1
  // Shared by the Host and settings UI; keep this module browser-compatible.
2
2
  export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
3
- 'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle', 'botId',
3
+ 'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle',
4
+ 'chatId', 'threadId', 'botId',
4
5
  ]);
5
6
 
6
7
  export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
@@ -38,7 +39,7 @@ const CHANNELS = new Set([
38
39
  ]);
39
40
  const SOURCE_LIMITS = {
40
41
  channel: 16, conversationType: 6, senderId: 256, senderName: 256,
41
- conversationTitle: 256, botId: 128,
42
+ conversationTitle: 256, chatId: 256, threadId: 256, botId: 128,
42
43
  };
43
44
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
44
45
 
@@ -64,7 +65,7 @@ function validateContextEnhancementScope(input) {
64
65
  throw invalidConfig('群聊和私聊开关必须是布尔值。');
65
66
  }
66
67
  if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
67
- throw invalidConfig('来源字段只能选择已定义的六个字段。');
68
+ throw invalidConfig('来源字段只能选择已定义的八个字段。');
68
69
  }
69
70
  if (typeof guidance !== 'string' || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
70
71
  throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
@@ -150,7 +151,7 @@ function sourceString(value, field) {
150
151
  function sourceBlock(snapshot, sourceFactory) {
151
152
  const { fields } = snapshot.config;
152
153
  const needsSource = fields.some((field) => [
153
- 'channel', 'senderId', 'senderName', 'conversationTitle',
154
+ 'channel', 'senderId', 'senderName', 'conversationTitle', 'chatId', 'threadId',
154
155
  ].includes(field));
155
156
  const source = needsSource ? sourceFactory?.() : null;
156
157
  const projected = {};
@@ -246,8 +246,8 @@ export default {
246
246
  '/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
247
247
  '**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
248
248
  '**📋 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',
249
- '**⌨️ 文本命令**\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` — 补全飞书权限与卡片回调':
250
- '**⌨️ 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 <workspace index or absolute 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',
249
+ '**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话\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` — 补全飞书权限与卡片回调':
250
+ '**⌨️ 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`/sessionlist --limit N` or `/sessions --limit N` — list only the first N sessions in the current workspace\n`/workspace <workspace index or absolute 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',
251
251
  '**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
252
252
  '**💡 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',
253
253
  '从下方下拉选择补充指令;最后一项可自定义输入。':
@@ -127,6 +127,8 @@ export default {
127
127
  '/sessionlist [workspace index or absolute path] List session IDs and titles',
128
128
  '/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题':
129
129
  '/sessionlist or /sessions [workspace index or absolute path] List session IDs and titles',
130
+ '/sessionlist --limit N 仅列出当前工作区前 N 个会话':
131
+ '/sessionlist --limit N List only the first N sessions in the current workspace',
130
132
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
131
133
  '/session <Session ID or workspace index> Bind this chat to the specified session',
132
134
  '/models 按序号列出所有可用模型': '/models List all available models by index',
@@ -3,8 +3,8 @@ export default {
3
3
  // workspace-command.mjs
4
4
  '用法:/session Session ID 或当前工作区序号(/session N)':
5
5
  'Usage: /session Session ID or the session index in the current Workspace (/session N)',
6
- '用法:\n/sessionlist 列出当前工作区会话\n/sessionlist 工作区序号 按 /workspacelist 序号列出会话\n/sessionlist 工作区绝对路径 列出指定工作区会话':
7
- 'Usage:\n/sessionlist List Sessions in the current Workspace\n/sessionlist Workspace index List Sessions by /workspacelist index\n/sessionlist Workspace absolute path List Sessions in the given Workspace',
6
+ '用法:\n/sessionlist 列出当前工作区会话\n/sessionlist --limit N 列出当前工作区前 N 个会话(N 为正整数)\n/sessionlist 工作区序号 按 /workspacelist 序号列出会话\n/sessionlist 工作区绝对路径 列出指定工作区会话':
7
+ 'Usage:\n/sessionlist List Sessions in the current Workspace\n/sessionlist --limit N List the first N Sessions in the current Workspace (N must be a positive integer)\n/sessionlist Workspace index List Sessions by /workspacelist index\n/sessionlist Workspace absolute path List Sessions in the given Workspace',
8
8
  '工作区必须是绝对路径。\n{usage}': 'The Workspace must be an absolute path.\n{usage}',
9
9
  '工作区路径包含不支持的字符或长度超过限制。\n{usage}':
10
10
  'The Workspace path contains unsupported characters or exceeds the length limit.\n{usage}',
@@ -597,6 +597,7 @@ export class TextHarnessBridge {
597
597
  t('/workspace 工作区序号或绝对路径 切换工作区'),
598
598
  t('/workspacelist 列出工作区绝对路径'),
599
599
  t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
600
+ t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
600
601
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
601
602
  t('/models 按序号列出所有可用模型'),
602
603
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
@@ -682,11 +683,14 @@ export class TextHarnessBridge {
682
683
  let contextEnhanced = false;
683
684
  if (snapshot) {
684
685
  const originalContent = content ?? text;
686
+ const contextSource = message.contextSource?.();
685
687
  content = enhanceContextContent(originalContent, snapshot, () => ({
686
688
  channel: this.#descriptor.key,
687
689
  senderId,
688
- senderName: message.contextSource?.()?.senderName,
689
- conversationTitle: message.contextSource?.()?.conversationTitle,
690
+ senderName: contextSource?.senderName,
691
+ conversationTitle: contextSource?.conversationTitle,
692
+ chatId: contextSource?.chatId ?? message.conversationId,
693
+ threadId: contextSource?.threadId,
690
694
  }));
691
695
  contextEnhanced = content !== originalContent;
692
696
  }
@@ -18,6 +18,7 @@ const SESSION_BIND_USAGE = '用法:/session Session ID 或当前工作区序
18
18
  const SESSION_LIST_USAGE = [
19
19
  '用法:',
20
20
  '/sessionlist 列出当前工作区会话',
21
+ '/sessionlist --limit N 列出当前工作区前 N 个会话(N 为正整数)',
21
22
  '/sessionlist 工作区序号 按 /workspacelist 序号列出会话',
22
23
  '/sessionlist 工作区绝对路径 列出指定工作区会话',
23
24
  ].join('\n');
@@ -45,6 +46,20 @@ function validSessionId(value) {
45
46
  && !UNSAFE_DISPLAY_TEXT.test(value);
46
47
  }
47
48
 
49
+ export function parseSessionListArgument(value) {
50
+ const argument = typeof value === 'string' ? value.trim() : '';
51
+ if (!argument) return { selector: '', limit: null };
52
+ if (!argument.toLowerCase().startsWith('--limit')) {
53
+ return { selector: argument, limit: null };
54
+ }
55
+ const match = /^--limit[ \t]+(\d+)$/iu.exec(argument);
56
+ const limit = match ? Number(match[1]) : null;
57
+ if (!Number.isSafeInteger(limit) || limit < 1) {
58
+ return { error: t(SESSION_LIST_USAGE) };
59
+ }
60
+ return { selector: '', limit };
61
+ }
62
+
48
63
  async function existingWorkspacePaths(values) {
49
64
  const checked = await Promise.all(values.map(async (value) => {
50
65
  const workspace = normalizedWorkspacePath(value);
@@ -235,12 +250,13 @@ async function currentSessionListWorkspace(harness) {
235
250
  }
236
251
 
237
252
  async function runSessionListCommand(match, harness) {
253
+ const request = parseSessionListArgument(match[1]);
254
+ if (request.error) return commandResult(request.error);
238
255
  if (typeof harness?.listWorkspaceSessions !== 'function') {
239
256
  return commandResult(t('当前机器人暂不支持列出工作区会话。'));
240
257
  }
241
- const selector = match[1]?.trim() ?? '';
242
258
  try {
243
- const resolved = await resolveSessionListWorkspace(selector, harness);
259
+ const resolved = await resolveSessionListWorkspace(request.selector, harness);
244
260
  if (resolved.error) return commandResult(resolved.error);
245
261
  const listed = await harness.listWorkspaceSessions(resolved.workspace);
246
262
  if (!listed || !Array.isArray(listed.sessions)) {
@@ -249,7 +265,10 @@ async function runSessionListCommand(match, harness) {
249
265
  harness.assertWorkspaceScope?.();
250
266
  const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
251
267
  const currentWorkspace = await currentSessionListWorkspace(harness);
252
- const message = sessionListMessage(workspace, listed.sessions, {
268
+ const sessions = request.limit === null
269
+ ? listed.sessions
270
+ : listed.sessions.slice(0, request.limit);
271
+ const message = sessionListMessage(workspace, sessions, {
253
272
  currentWorkspace: workspace === currentWorkspace,
254
273
  });
255
274
  return commandResult(message, splitWorkspaceCommandMessage(message));
@@ -172,6 +172,10 @@ export function normalizeSlackEvent(payload, botUserId, {
172
172
  senderIsBot: String(event.user) === String(botUserId),
173
173
  kind: direct ? 'direct' : 'group',
174
174
  conversationId: direct ? String(event.channel) : `${event.channel}:${threadTs}`,
175
+ contextSource: () => ({
176
+ chatId: String(event.channel),
177
+ threadId: event.thread_ts ? String(event.thread_ts) : undefined,
178
+ }),
175
179
  content: stripBotMention(event.text ?? '', botUserId),
176
180
  plainText: !Array.isArray(event.files) || event.files.length === 0,
177
181
  images: Array.isArray(event.files)
@@ -266,6 +266,8 @@ export function normalizeTelegramUpdate(update, {
266
266
  .filter((value) => typeof value === 'string' && value.trim())
267
267
  .map((value) => value.trim()).join(' ') || message.from?.username,
268
268
  conversationTitle: direct ? undefined : message.chat?.title,
269
+ chatId: String(chatId),
270
+ threadId: messageThreadId === undefined ? undefined : String(messageThreadId),
269
271
  }),
270
272
  senderIsBot: message.from?.is_bot === true,
271
273
  kind: direct ? 'direct' : 'group',
@@ -74,6 +74,7 @@ function helpText() {
74
74
  t('/workspace 工作区序号或绝对路径 切换工作区'),
75
75
  t('/workspacelist 列出工作区绝对路径'),
76
76
  t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
77
+ t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
77
78
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
78
79
  t('/models 按序号列出所有可用模型'),
79
80
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
@@ -1055,6 +1056,7 @@ export class WecomHarnessBridge {
1055
1056
  content = enhanceContextContent(originalContent, snapshot, () => ({
1056
1057
  channel: 'wecom',
1057
1058
  senderId,
1059
+ chatId,
1058
1060
  }));
1059
1061
  contextEnhanced = content !== originalContent;
1060
1062
  }
@@ -88,6 +88,7 @@ const HELP_TEXT = () => [
88
88
  t('/workspace 工作区序号或绝对路径 切换工作区'),
89
89
  t('/workspacelist 列出工作区绝对路径'),
90
90
  t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
91
+ t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
91
92
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
92
93
  t('/models 按序号列出所有可用模型'),
93
94
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
@@ -785,6 +786,7 @@ export class WeixinHarnessBridge {
785
786
  content = enhanceContextContent(originalContent, snapshot, () => ({
786
787
  channel: 'weixin',
787
788
  senderId: sender,
789
+ chatId: sender,
788
790
  }));
789
791
  contextEnhanced = content !== originalContent;
790
792
  }