mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
feat: support limiting session list results
This commit is contained in:
parent
52deb872aa
commit
37ec6abb06
16 changed files with 373 additions and 225 deletions
|
|
@ -205,6 +205,7 @@ Do not modify the same profile through a terminal or plugin market during instal
|
|||
| `/workspace <workspace index or absolute path>` | Switch the current bot's Harness workspace by `/workspacelist` index or absolute path. |
|
||||
| `/workspacelist` | List workspace absolute paths that still exist on the current Harness Host. |
|
||||
| `/sessionlist [workspace number or absolute path]`, `/sessions [...]` | Equivalent aliases that list every registered session ID and title in the selected workspace; omit the argument to use the current workspace. |
|
||||
| `/sessionlist --limit N`, `/sessions --limit N` | List the first N sessions in the current workspace's existing order; N must be a positive integer. |
|
||||
| `/session <Session ID>` | Bind the current chat to an existing Harness session. |
|
||||
| `/history [count]` | Preview recent messages from the bound Session in a direct chat; defaults to 3, capped at 5. |
|
||||
| Interactive question | Reply with an option number, option label, or custom text; separate multiple choices with commas. |
|
||||
|
|
@ -238,6 +239,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
|
|||
- The path must be an existing absolute directory. The bot returns an actionable error and the correct usage when validation fails.
|
||||
- `/workspacelist` takes no arguments. It combines the Harness global registry with the current bot's path. When that current path still exists and is safe to display, it appears first and is marked as current. `/workspace N` switches using the freshly resolved list order at execution time; absolute paths remain supported.
|
||||
- `/sessionlist` and `/sessions` are equivalent. A numeric argument uses the same freshly resolved order as `/workspacelist` at command execution time. An absolute path can also select a workspace directly, and the result echoes the resolved path.
|
||||
- `/sessionlist --limit N` and `/sessions --limit N` limit only that command's response. They do not change any global or bot setting, and omitting `--limit` still lists every session.
|
||||
- Both session-list aliases include every session registered to the selected workspace. Archived sessions are marked as archived; blank and subagent sessions are included when they belong to that workspace; sessions without a title are shown as `No title yet`. Any listed ID can be passed directly to `/session Session ID`.
|
||||
- `/session` accepts exactly one Session ID obtained from `/sessionlist`. It neither creates a session nor immediately prompts the model; later messages in the current chat continue the bound session. Regular archived sessions can be bound without being unarchived, while subagent sessions cannot be bound.
|
||||
- `/history` works identically in direct chats on all nine channels. It only reads the Session already bound to this chat: it never creates a Session, prompts the model, or interrupts running tasks or pending interactions. It returns the latest 3 messages by default. `/history N` accepts a positive integer, caps values above 5 at 5, and returns fewer when fewer are available. Zero, negative, fractional, nonnumeric, and multiple arguments show usage; commands with images or files are rejected. While collecting batch input, use `/send` or `/cancel` first.
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ dsh plugin --profile web add -w --save-exact @xmanrui/dsh-im@3.1.0 --registry=ht
|
|||
| `/workspace <工作区序号或绝对路径>` | 按 `/workspacelist` 序号或绝对路径切换当前机器人的 Harness 工作区。 |
|
||||
| `/workspacelist` | 列出当前 Harness Host 上仍然存在的工作区绝对路径。 |
|
||||
| `/sessionlist [工作区序号或绝对路径]`、`/sessions [...]` | 两个等价命令;列出指定工作区登记的所有会话 ID 和标题,省略参数时使用当前工作区。 |
|
||||
| `/sessionlist --limit N`、`/sessions --limit N` | 列出当前工作区现有顺序中的前 N 个会话;N 必须是正整数。 |
|
||||
| `/session <Session ID>` | 将当前聊天绑定到指定的已有 Harness 会话。 |
|
||||
| `/history [数量]` | 在私聊中查看当前绑定会话的最近历史消息,默认 3 条,最多 5 条。 |
|
||||
| 交互式提问 | 回复选项序号、选项文字或自定义文字;多选时用逗号分隔。 |
|
||||
|
|
@ -241,6 +242,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
|
|||
- 只接受已经存在的绝对目录;路径无效时机器人会返回具体提示和正确用法。
|
||||
- `/workspacelist` 不需要参数。它合并 Harness 全局登记项与当前机器人的路径;当前路径仍存在且可安全显示时会排在首位并标记为“当前”。`/workspace N` 会在执行时按最新列表顺序切换,也可继续使用绝对路径。
|
||||
- `/sessionlist` 和 `/sessions` 完全等价。数字参数按命令执行时与 `/workspacelist` 相同的最新顺序解析;也可使用绝对路径直接指定工作区。结果会回显最终选中的路径。
|
||||
- `/sessionlist --limit N` 和 `/sessions --limit N` 只限制本次命令的返回条数,不改变任何全局或机器人配置。未指定 `--limit` 时仍列出全部会话。
|
||||
- 两个会话列表命令都会列出该工作区登记的所有会话。已归档会话会标记为“已归档”;空白会话和子代理会话在它们归属该工作区时也会列出;没有标题的会话显示为“暂无标题”。结果中的 ID 可直接用于 `/session Session ID`。
|
||||
- `/session` 只接受一个由 `/sessionlist` 获得的 Session ID。它不会新建会话或立即向模型发送消息;绑定成功后,当前聊天的后续消息会继续该会话。普通归档会话可以绑定但不会自动取消归档,子代理会话不能绑定。
|
||||
- `/history` 在九个渠道的私聊中统一可用,只读取当前聊天已经绑定的会话,不新建会话、不调用模型,也不影响正在运行的任务或待处理交互。默认返回最近 3 条;`/history N` 接受正整数,超过 5 自动按 5 条处理,数量不足时返回实际条数。零、负数、小数、非数字和多个参数会提示用法,附带图片或文件时会拒绝处理;批量输入收集中请先 `/send` 或 `/cancel`。
|
||||
|
|
|
|||
421
lib/index.js
421
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -82,6 +82,7 @@ const HELP_TEXT_LINES = [
|
|||
'/workspace 工作区序号或绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/models 按序号列出所有可用模型',
|
||||
'/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ import {
|
|||
isPresetCommand,
|
||||
runPresetCommand,
|
||||
} from '../shared/preset-command.mjs';
|
||||
import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
|
||||
import {
|
||||
parseSessionListArgument,
|
||||
resolveSessionListWorkspace,
|
||||
runWorkspaceCommand,
|
||||
workspacePathSnapshot,
|
||||
} from '../shared/workspace-command.mjs';
|
||||
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
||||
import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
|
||||
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
||||
|
|
@ -160,6 +165,7 @@ const WORKSPACE_HELP_LINES = [
|
|||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
||||
];
|
||||
|
||||
/** Safe user-facing text for bind/workspace failures (no raw messages). */
|
||||
|
|
@ -1085,8 +1091,18 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
if (SESSION_LIST_PREFIX.test(commandText)) {
|
||||
const selector = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim() || null;
|
||||
await this.#showSessions({ chatId: event.message.chat_id, key, replyTo: event.message.message_id }, selector, 0);
|
||||
const argument = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim();
|
||||
const request = parseSessionListArgument(argument);
|
||||
if (request.error) {
|
||||
await this.#send(event.message.chat_id, request.error, { replyTo: event.message.message_id });
|
||||
return;
|
||||
}
|
||||
await this.#showSessions(
|
||||
{ chatId: event.message.chat_id, key, replyTo: event.message.message_id },
|
||||
request.selector || null,
|
||||
0,
|
||||
{ limit: request.limit },
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (WORKSPACE_LIST_COMMAND.test(commandText)) {
|
||||
|
|
@ -1850,6 +1866,7 @@ export class FeishuHarnessBridge {
|
|||
messageId = null,
|
||||
sessionWorkspace = null,
|
||||
sessionPage = 0,
|
||||
sessionLimit = null,
|
||||
selections = [],
|
||||
}) {
|
||||
// Confirmations triggered by a card interaction stay anchored to the
|
||||
|
|
@ -1861,7 +1878,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo: messageId },
|
||||
sessionWorkspace,
|
||||
page,
|
||||
{ updateMessageId: messageId },
|
||||
{ updateMessageId: messageId, limit: sessionLimit },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2040,7 +2057,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo: messageId },
|
||||
sessionWorkspace,
|
||||
sessionPage,
|
||||
{ updateMessageId: messageId },
|
||||
{ updateMessageId: messageId, limit: sessionLimit },
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -2056,7 +2073,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo: messageId },
|
||||
sessionWorkspace,
|
||||
sessionPage,
|
||||
{ updateMessageId: messageId },
|
||||
{ updateMessageId: messageId, limit: sessionLimit },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2141,7 +2158,7 @@ export class FeishuHarnessBridge {
|
|||
{ chatId, key, replyTo = null },
|
||||
selector,
|
||||
page = 0,
|
||||
{ updateMessageId = null } = {},
|
||||
{ updateMessageId = null, limit = null } = {},
|
||||
) {
|
||||
try {
|
||||
const signal = this.#cardDataSignal();
|
||||
|
|
@ -2151,7 +2168,11 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
|
||||
const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
||||
const visibleSessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
||||
const sessionLimit = Number.isSafeInteger(limit) && limit > 0 ? limit : null;
|
||||
const sessions = sessionLimit === null
|
||||
? visibleSessions
|
||||
: visibleSessions.slice(0, sessionLimit);
|
||||
const workspace = listed?.workspace ?? resolved.workspace;
|
||||
if (sessions.length === 0) {
|
||||
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }), { replyTo });
|
||||
|
|
@ -2178,6 +2199,7 @@ export class FeishuHarnessBridge {
|
|||
// list response's workspace is display data and is not authoritative.
|
||||
sessionWorkspace: resolved.workspace,
|
||||
sessionPage: safePage,
|
||||
sessionLimit,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -2246,6 +2268,9 @@ export class FeishuHarnessBridge {
|
|||
sessionPage: Number.isSafeInteger(options.sessionPage) && options.sessionPage >= 0
|
||||
? options.sessionPage
|
||||
: 0,
|
||||
sessionLimit: Number.isSafeInteger(options.sessionLimit) && options.sessionLimit > 0
|
||||
? options.sessionLimit
|
||||
: null,
|
||||
});
|
||||
if (this.#cardKeys.size > 200) {
|
||||
const oldest = this.#cardKeys.keys().next().value;
|
||||
|
|
|
|||
|
|
@ -488,6 +488,7 @@ export function menuHelpText() {
|
|||
'',
|
||||
'📋 会话 / 工作区',
|
||||
'/sessionlist 或 /sessions 列出工作区会话',
|
||||
'/sessionlist --limit N 仅列出当前工作区前 N 个会话',
|
||||
'/session ID 绑定已有会话',
|
||||
'/workspacelist 列出工作区',
|
||||
'/workspace 工作区序号或绝对路径 切换工作区',
|
||||
|
|
@ -554,6 +555,7 @@ const HELP_TEXT_COMMANDS = [
|
|||
'`/new` — 开启全新会话',
|
||||
'`/session ID` — 绑定已有会话',
|
||||
'`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话',
|
||||
'`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话',
|
||||
'`/workspace 工作区序号或绝对路径` — 切换工作区',
|
||||
'`/workspacelist` — 列出工作区',
|
||||
'`/status` — 查看连接状态',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ function helpText() {
|
|||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
t('/models 按序号列出所有可用模型'),
|
||||
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
||||
|
|
|
|||
|
|
@ -246,8 +246,8 @@ export default {
|
|||
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
|
||||
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
|
||||
'**📋 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',
|
||||
'**⌨️ 文本命令**\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` — 补全飞书权限与卡片回调':
|
||||
'**⌨️ 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',
|
||||
'**⌨️ 文本命令**\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` — 补全飞书权限与卡片回调':
|
||||
'**⌨️ 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',
|
||||
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
|
||||
'**💡 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',
|
||||
'从下方下拉选择补充指令;最后一项可自定义输入。':
|
||||
|
|
|
|||
|
|
@ -127,6 +127,8 @@ export default {
|
|||
'/sessionlist [workspace index or absolute path] List session IDs and titles',
|
||||
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题':
|
||||
'/sessionlist or /sessions [workspace index or absolute path] List session IDs and titles',
|
||||
'/sessionlist --limit N 仅列出当前工作区前 N 个会话':
|
||||
'/sessionlist --limit N List only the first N sessions in the current workspace',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
|
||||
'/session <Session ID or workspace index> Bind this chat to the specified session',
|
||||
'/models 按序号列出所有可用模型': '/models List all available models by index',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ export default {
|
|||
// workspace-command.mjs
|
||||
'用法:/session Session ID 或当前工作区序号(/session N)':
|
||||
'Usage: /session Session ID or the session index in the current Workspace (/session N)',
|
||||
'用法:\n/sessionlist 列出当前工作区会话\n/sessionlist 工作区序号 按 /workspacelist 序号列出会话\n/sessionlist 工作区绝对路径 列出指定工作区会话':
|
||||
'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',
|
||||
'用法:\n/sessionlist 列出当前工作区会话\n/sessionlist --limit N 列出当前工作区前 N 个会话(N 为正整数)\n/sessionlist 工作区序号 按 /workspacelist 序号列出会话\n/sessionlist 工作区绝对路径 列出指定工作区会话':
|
||||
'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',
|
||||
'工作区必须是绝对路径。\n{usage}': 'The Workspace must be an absolute path.\n{usage}',
|
||||
'工作区路径包含不支持的字符或长度超过限制。\n{usage}':
|
||||
'The Workspace path contains unsupported characters or exceeds the length limit.\n{usage}',
|
||||
|
|
|
|||
|
|
@ -597,6 +597,7 @@ export class TextHarnessBridge {
|
|||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
t('/models 按序号列出所有可用模型'),
|
||||
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const SESSION_BIND_USAGE = '用法:/session Session ID 或当前工作区序
|
|||
const SESSION_LIST_USAGE = [
|
||||
'用法:',
|
||||
'/sessionlist 列出当前工作区会话',
|
||||
'/sessionlist --limit N 列出当前工作区前 N 个会话(N 为正整数)',
|
||||
'/sessionlist 工作区序号 按 /workspacelist 序号列出会话',
|
||||
'/sessionlist 工作区绝对路径 列出指定工作区会话',
|
||||
].join('\n');
|
||||
|
|
@ -45,6 +46,20 @@ function validSessionId(value) {
|
|||
&& !UNSAFE_DISPLAY_TEXT.test(value);
|
||||
}
|
||||
|
||||
export function parseSessionListArgument(value) {
|
||||
const argument = typeof value === 'string' ? value.trim() : '';
|
||||
if (!argument) return { selector: '', limit: null };
|
||||
if (!argument.toLowerCase().startsWith('--limit')) {
|
||||
return { selector: argument, limit: null };
|
||||
}
|
||||
const match = /^--limit[ \t]+(\d+)$/iu.exec(argument);
|
||||
const limit = match ? Number(match[1]) : null;
|
||||
if (!Number.isSafeInteger(limit) || limit < 1) {
|
||||
return { error: t(SESSION_LIST_USAGE) };
|
||||
}
|
||||
return { selector: '', limit };
|
||||
}
|
||||
|
||||
async function existingWorkspacePaths(values) {
|
||||
const checked = await Promise.all(values.map(async (value) => {
|
||||
const workspace = normalizedWorkspacePath(value);
|
||||
|
|
@ -235,12 +250,13 @@ async function currentSessionListWorkspace(harness) {
|
|||
}
|
||||
|
||||
async function runSessionListCommand(match, harness) {
|
||||
const request = parseSessionListArgument(match[1]);
|
||||
if (request.error) return commandResult(request.error);
|
||||
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
||||
return commandResult(t('当前机器人暂不支持列出工作区会话。'));
|
||||
}
|
||||
const selector = match[1]?.trim() ?? '';
|
||||
try {
|
||||
const resolved = await resolveSessionListWorkspace(selector, harness);
|
||||
const resolved = await resolveSessionListWorkspace(request.selector, harness);
|
||||
if (resolved.error) return commandResult(resolved.error);
|
||||
const listed = await harness.listWorkspaceSessions(resolved.workspace);
|
||||
if (!listed || !Array.isArray(listed.sessions)) {
|
||||
|
|
@ -249,7 +265,10 @@ async function runSessionListCommand(match, harness) {
|
|||
harness.assertWorkspaceScope?.();
|
||||
const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
|
||||
const currentWorkspace = await currentSessionListWorkspace(harness);
|
||||
const message = sessionListMessage(workspace, listed.sessions, {
|
||||
const sessions = request.limit === null
|
||||
? listed.sessions
|
||||
: listed.sessions.slice(0, request.limit);
|
||||
const message = sessionListMessage(workspace, sessions, {
|
||||
currentWorkspace: workspace === currentWorkspace,
|
||||
});
|
||||
return commandResult(message, splitWorkspaceCommandMessage(message));
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ function helpText() {
|
|||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
t('/models 按序号列出所有可用模型'),
|
||||
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ const HELP_TEXT = () => [
|
|||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
t('/models 按序号列出所有可用模型'),
|
||||
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
||||
|
|
|
|||
|
|
@ -4739,6 +4739,50 @@ test('/sessions alias uses the interactive session list and paginates across 25
|
|||
assert.equal(useActionsFromCard(page1)[0], 'session-11');
|
||||
});
|
||||
|
||||
test('Feishu session limit caps card pages and survives paging and watch refresh', async () => {
|
||||
const { state } = await watchStoreFixture();
|
||||
const work = realpathSync(tmpdir());
|
||||
const sessions = Array.from({ length: 25 }, (_, index) => ({
|
||||
sessionId: `limited-card-${String(index + 1).padStart(2, '0')}`,
|
||||
title: `Limited Card ${index + 1}`,
|
||||
lastSeq: index,
|
||||
}));
|
||||
const harness = watchHarness({ current: work, sessionsByWorkspace: { [work]: sessions } });
|
||||
const sent = [];
|
||||
const patches = [];
|
||||
const bridge = new FeishuHarnessBridge({
|
||||
client: cardClient(
|
||||
async (outgoing) => sent.push(outgoing),
|
||||
async (request) => patches.push(request),
|
||||
),
|
||||
channel: {},
|
||||
harness,
|
||||
state,
|
||||
status: bridgeStatus(),
|
||||
allowedSenderOpenIds: new Set(['ou_owner']),
|
||||
});
|
||||
|
||||
await bridge.accept(event('limited-card-open', '/sessionlist --limit 12', { senderOpenId: 'ou_owner' }));
|
||||
await bridge.waitForIdle();
|
||||
const page0 = cards(sent).at(-1).content;
|
||||
assert.equal(useActionsFromCard(page0).length, 10);
|
||||
assert.match(page0.body.elements[0].text.content, /共 \*\*12\*\* 个会话/);
|
||||
|
||||
await bridge.onCardAction(cardActionEvent('om_card_1', 'sessions:1', 'ou_owner'));
|
||||
await bridge.waitForIdle();
|
||||
const page1 = JSON.parse(patches.at(-1).data.content);
|
||||
assert.deepEqual(useActionsFromCard(page1), ['limited-card-11', 'limited-card-12']);
|
||||
|
||||
await bridge.onCardAction(cardActionEvent('om_card_1', 'watch:limited-card-11', 'ou_owner'));
|
||||
await bridge.waitForIdle();
|
||||
const refreshed = JSON.parse(patches.at(-1).data.content);
|
||||
assert.deepEqual(useActionsFromCard(refreshed), ['limited-card-11', 'limited-card-12']);
|
||||
assert.equal(
|
||||
buttonsFromCard(refreshed).some((button) => callbackAction(button) === 'unwatch:limited-card-11'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('number replies on a later session page use page-local labels', async () => {
|
||||
const fixture = stateFixture();
|
||||
const sent = [];
|
||||
|
|
|
|||
|
|
@ -1149,6 +1149,51 @@ test('/sessionlist supports the current workspace, list numbers, and absolute pa
|
|||
assert.equal(workspaceListCalls, 1, 'only numeric selection needs the workspace registry order');
|
||||
});
|
||||
|
||||
test('/sessionlist and /sessions accept a one-off --limit for the current workspace', async (t) => {
|
||||
const { defaultWorkspace } = await fixture(t);
|
||||
const sessions = Array.from({ length: 4 }, (_, index) => ({
|
||||
sessionId: `limited-session-${index + 1}`,
|
||||
title: `Limited Session ${index + 1}`,
|
||||
archived: false,
|
||||
summaryAvailable: true,
|
||||
}));
|
||||
let listCalls = 0;
|
||||
const harness = {
|
||||
currentWorkspace() { return defaultWorkspace; },
|
||||
async listWorkspaceSessions(workspace) {
|
||||
listCalls += 1;
|
||||
return { workspace, sessions };
|
||||
},
|
||||
};
|
||||
|
||||
const limited = await runWorkspaceCommand('/sessionlist --limit 2', harness);
|
||||
assert.match(limited.message, /会话(2):/);
|
||||
assert.match(limited.message, /limited-session-1/);
|
||||
assert.match(limited.message, /limited-session-2/);
|
||||
assert.doesNotMatch(limited.message, /limited-session-3|limited-session-4/);
|
||||
|
||||
const alias = await runWorkspaceCommand('/SESSIONS --LIMIT 1', harness);
|
||||
assert.match(alias.message, /会话(1):/);
|
||||
assert.match(alias.message, /limited-session-1/);
|
||||
assert.doesNotMatch(alias.message, /limited-session-2/);
|
||||
|
||||
const oversized = await runWorkspaceCommand('/sessionlist --limit 99', harness);
|
||||
assert.match(oversized.message, /会话(4):/);
|
||||
assert.equal(listCalls, 3);
|
||||
|
||||
for (const command of [
|
||||
'/sessionlist --limit',
|
||||
'/sessionlist --limit 0',
|
||||
'/sessionlist --limit -1',
|
||||
'/sessionlist --limit many',
|
||||
'/sessionlist --limit 2 1',
|
||||
]) {
|
||||
const result = await runWorkspaceCommand(command, harness);
|
||||
assert.match(result.message, /\/sessionlist --limit N/);
|
||||
}
|
||||
assert.equal(listCalls, 3, 'invalid limits must not query Harness');
|
||||
});
|
||||
|
||||
test('/sessions reuses /sessionlist parsing for current, numbered, and absolute workspaces', async (t) => {
|
||||
const { defaultWorkspace, alternateWorkspace } = await fixture(t);
|
||||
const calls = [];
|
||||
|
|
@ -1332,6 +1377,7 @@ test('all nine channel bridge families advertise and fan out workspace command r
|
|||
const source = await readFile(new URL(file, import.meta.url), 'utf8');
|
||||
assert.match(source, /\/workspacelist 列出工作区绝对路径/);
|
||||
assert.match(source, /\/sessionlist 或 \/sessions \[工作区序号或绝对路径\] 列出会话 ID 和标题/);
|
||||
assert.match(source, /\/sessionlist --limit N 仅列出当前工作区前 N 个会话/);
|
||||
assert.match(source, /workspaceCommand\.messages \?\? \[workspaceCommand\.message\]/);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue