Merge pull request #91 from lKolabrodl/feat/session-preset-aliases

feat: add /sessions and /presets aliases
This commit is contained in:
xiemanR 2026-08-30 08:31:57 +08:00 • committed by GitHub
commit 98da9a2f24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 121 additions and 59 deletions

View file

@ -181,7 +181,7 @@ Do not modify the same profile through a terminal or plugin market during instal
| `/reasoning` | Show the current Session model and reasoning effort. |
| `/reasoning <number or effort ID>` | Switch the current model's reasoning effort. |
| `/reasoning --default` | Restore the current model's default reasoning effort. |
| `/presetlist` | List the Host's currently available Agent Presets, marking the Host default and this bot's selection. |
| `/presetlist`, `/presets` | Equivalent aliases that list the Host's currently available Agent Presets, marking the Host default and this bot's selection. |
| `/preset` | Show this bot's Agent Preset setting for new Sessions. |
| `/preset <number or Preset ID>` | Set this bot's Agent Preset; use `/preset id:<ID>` for a numeric ID. |
| `/preset --default` | Clear this bot's explicit selection so later new Sessions follow the Host default. |
@ -194,7 +194,7 @@ Do not modify the same profile through a terminal or plugin market during instal
| `/compact` | Immediately compact older context in the Session bound to the current chat. |
| `/workspace <absolute workspace path>` | Switch the current bot's Harness workspace. |
| `/workspacelist` | List workspace absolute paths that still exist on the current Harness Host. |
| `/sessionlist [workspace number or absolute path]` | List every registered session ID and title in the selected workspace; omit the argument to use the current workspace. |
| `/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. |
| `/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. |
@ -214,7 +214,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
- Bare `/model` shows the current Session model and reasoning effort. With arguments, it accepts a number from `/models` or an exact full model ID, followed optionally by an exact reasoning effort ID published in that target model's metadata, for example `/model 2 max`. When the effort is omitted, Harness resolves the target model's current default. If the chat has no Session yet, a valid switch creates and binds a blank Session without prompting the model.
- `/reasoninglist` and `/reasonings` are equivalent. They list the efforts from the current model's metadata and mark the current and default values. `/reasoning` shows the current value; `/reasoning <number or effort ID>` accepts a listed number or an exact metadata ID; `/reasoning --default` lets Harness resolve the current model's default again. Every `/reasoning...` command requires an existing Session and never creates one or prompts the model.
- The model or reasoning effort cannot be changed while a task is running or waiting for an approval or question answer. Wait for it to finish or use `/stop` first. A change takes effect on the next model request and keeps Harness's default-saving semantics: Harness attempts to save the accepted model and effort as the default selection for future Sessions, while other existing Sessions remain unchanged. A Session containing images cannot switch to a model that does not accept image input.
- `/presetlist` takes no arguments and never creates a Session. It reads the Host's currently available Agent Presets every time, showing their names, stable IDs, the Host default, and this bot's selection. A selected Preset that has been deleted or become broken is retained and marked unavailable instead of being cleared automatically. Only safe names and IDs are shown; paths, errors, and other Host internals are never exposed.
- `/presetlist` and `/presets` are equivalent. They take no arguments and never create a Session. Each reads the Host's currently available Agent Presets, showing their names, stable IDs, the Host default, and this bot's selection. A selected Preset that has been deleted or become broken is retained and marked unavailable instead of being cleared automatically. Only safe names and IDs are shown; paths, errors, and other Host internals are never exposed.
- Bare `/preset` shows this bot's setting for future new Sessions; it does not inspect or change the current Session. With an argument, it accepts a number from the most recent `/presetlist` in this chat or an exact ID; use `/preset id:<ID>` for a numeric ID. A numbered selection resolves the ID from that displayed list and then validates it against the latest Host catalog, asking for a fresh list if it has changed.
- `/preset --default` clears this bot's explicit override so future Sessions resolve the Host default when they are created. Explicitly selecting an ID that currently matches the Host default pins that ID instead. Following the Host default remains available even while the catalog cannot be read.
- An Agent Preset change is bot-wide: it affects future new Sessions in every chat for this bot, but never modifies, stops, unbinds, or rebuilds an existing Session and never runs `/new` automatically. If this chat already has a Session, ordinary messages keep using it; the first ordinary message after `/new` creates a Session with the new setting. Presets can still be queried or changed while a task is running or awaiting interaction because the command does not touch that Session.
@ -225,8 +225,8 @@ If the Slack desktop app has no native Slash Command registered with the same na
- `/compact` acts only on the Harness Session already bound to the current chat and is never sent to the model. The bot reports the applicable status when the chat has no Session yet, the Session is generating a reply, or there is no compactable history.
- 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. Any listed path can be copied directly into `/workspace`.
- A numeric `/sessionlist` 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` includes 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`.
- `/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.
- 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.
- A user message or a final assistant reply counts as one history item, not one turn or day. The latest N items are displayed oldest first. Tool events, reasoning, injected content, and unfinished assistant output are omitted; old attachments are not downloaded or resent. Long text is marked as truncated, with at most 3 text segments per reply and no automatic pagination. After binding a Session, send `/history` manually; binding never replays history automatically. Message text may still contain sensitive information from the original conversation, so expose the bot only to trusted users.

View file

@ -184,7 +184,7 @@ dsh plugin --profile web add -w --save-exact @xmanrui/dsh-im@3.1.0 --registry=ht
| `/reasoning` | 查看当前会话的模型和推理等级。 |
| `/reasoning <序号或等级ID>` | 切换当前模型的推理等级。 |
| `/reasoning --default` | 恢复当前模型的默认推理等级。 |
| `/presetlist` | 按序号列出 Host 当前可用的 Agent Preset,并标记 Host 默认项和当前机器人的选择。 |
| `/presetlist`、`/presets` | 两个等价命令;按序号列出 Host 当前可用的 Agent Preset,并标记 Host 默认项和当前机器人的选择。 |
| `/preset` | 查看当前机器人的新会话 Agent Preset 设置。 |
| `/preset <序号或 Preset ID>` | 设置当前机器人的 Agent Preset;纯数字 ID 使用 `/preset id:<ID>`。 |
| `/preset --default` | 清除当前机器人的显式选择,让后续新 Session 跟随 Host 默认。 |
@ -197,7 +197,7 @@ dsh plugin --profile web add -w --save-exact @xmanrui/dsh-im@3.1.0 --registry=ht
| `/compact` | 立即压缩当前聊天绑定会话的较早上下文。 |
| `/workspace <工作区绝对路径>` | 切换当前机器人的 Harness 工作区。 |
| `/workspacelist` | 列出当前 Harness Host 上仍然存在的工作区绝对路径。 |
| `/sessionlist [工作区序号或绝对路径]` | 列出指定工作区登记的所有会话 ID 和标题;省略参数时使用当前工作区。 |
| `/sessionlist [工作区序号或绝对路径]`、`/sessions [...]` | 两个等价命令;列出指定工作区登记的所有会话 ID 和标题,省略参数时使用当前工作区。 |
| `/session <Session ID>` | 将当前聊天绑定到指定的已有 Harness 会话。 |
| `/history [数量]` | 在私聊中查看当前绑定会话的最近历史消息,默认 3 条,最多 5 条。 |
| 交互式提问 | 回复选项序号、选项文字或自定义文字;多选时用逗号分隔。 |
@ -217,7 +217,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
- `/model` 不带参数时查看当前会话的模型和推理等级;带参数时接受 `/models` 列出的序号或精确完整模型 ID,并可追加目标模型元数据公布的精确推理等级 ID,例如 `/model 2 max`。省略推理等级时,由 Harness 解析目标模型的当前默认值。聊天尚无会话时,有效的切换命令会创建并绑定一个空白会话,但不会触发模型回复。
- `/reasoninglist` 和 `/reasonings` 完全等价,按当前模型的元数据列出可选推理等级并标记当前值和默认值。`/reasoning` 查看当前值;`/reasoning <序号或等级ID>` 接受列表序号或元数据中的精确 ID;`/reasoning --default` 让 Harness 重新采用当前模型的默认推理等级。所有 `/reasoning...` 命令都要求当前聊天已有 Session,不会自行创建 Session 或触发模型回复。
- 正在运行任务或等待审批、问题回答时不能修改模型或推理等级;请等待完成,或先使用 `/stop`。修改从下一次模型请求起生效,并沿用 Harness 的默认保存语义:Harness 会尝试把已接受的模型和推理等级保存为以后新会话的默认选择,已有其他会话不受影响。含图片的会话无法切换到不支持图片输入的模型。
- `/presetlist` 不需要参数,也不会创建会话。它每次都读取 Host 当前可用的 Agent Preset,显示名称、稳定 ID、Host 默认项和当前机器人的选择;已删除或损坏的当前选择会保留并标记为“已不可用”,不会被自动清除。列表只公开安全的名称和 ID,不公开 Preset 路径、错误或其他 Host 内部字段。
- `/presetlist` 和 `/presets` 完全等价,不需要参数,也不会创建会话。它们每次都读取 Host 当前可用的 Agent Preset,显示名称、稳定 ID、Host 默认项和当前机器人的选择;已删除或损坏的当前选择会保留并标记为“已不可用”,不会被自动清除。列表只公开安全的名称和 ID,不公开 Preset 路径、错误或其他 Host 内部字段。
- `/preset` 不带参数时查看当前机器人的“新会话设置”,不是查看或修改当前 Session。带参数时接受最近一次 `/presetlist` 在当前聊天中显示的序号或完整 ID;纯数字 ID 使用 `/preset id:<ID>`。选择序号时会先按该次列表解析 ID,再用 Host 最新目录复验,目录已经变化时会要求重新列出。
- `/preset --default` 清除当前机器人的显式覆盖值,让以后新建的 Session 在创建时跟随 Host 当前默认;显式选择一个恰好等于 Host 默认的 ID 则会固定该 ID。目录暂时不可读时仍可恢复为跟随 Host 默认。
- Agent Preset 修改是机器人级配置,会影响该机器人所有聊天以后创建的新 Session,但不会修改、停止、解绑或重建已有 Session,也不会自动执行 `/new`。若当前聊天已有会话,继续发送消息仍使用原 Session;发送 `/new` 后的下一条普通消息才会按新设置创建 Session。任务正在运行或等待交互时也可查询或修改 Preset,因为命令不会触碰当前 Session。
@ -228,8 +228,8 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
- `/compact` 只作用于当前聊天已经绑定的 Harness 会话,不会把命令发送给模型。当前聊天尚未创建会话、会话正在生成回复或没有可压缩历史时,机器人会直接返回对应状态。
- 只接受已经存在的绝对目录;路径无效时机器人会返回具体提示和正确用法。
- `/workspacelist` 不需要参数。它合并 Harness 全局登记项与当前机器人的路径;当前路径仍存在且可安全显示时会排在首位并标记为“当前”。结果可直接复制到 `/workspace` 命令。
- `/sessionlist` 的数字参数按命令执行时与 `/workspacelist` 相同的最新顺序解析;也可使用绝对路径直接指定工作区。结果会回显最终选中的路径。
- `/sessionlist` 会列出该工作区登记的所有会话。已归档会话会标记为“已归档”;空白会话和子代理会话在它们归属该工作区时也会列出;没有标题的会话显示为“暂无标题”。结果中的 ID 可直接用于 `/session Session ID`。
- `/sessionlist` 和 `/sessions` 完全等价。数字参数按命令执行时与 `/workspacelist` 相同的最新顺序解析;也可使用绝对路径直接指定工作区。结果会回显最终选中的路径。
- 两个会话列表命令都会列出该工作区登记的所有会话。已归档会话会标记为“已归档”;空白会话和子代理会话在它们归属该工作区时也会列出;没有标题的会话显示为“暂无标题”。结果中的 ID 可直接用于 `/session Session ID`。
- `/session` 只接受一个由 `/sessionlist` 获得的 Session ID。它不会新建会话或立即向模型发送消息;绑定成功后,当前聊天的后续消息会继续该会话。普通归档会话可以绑定但不会自动取消归档,子代理会话不能绑定。
- `/history` 在九个渠道的私聊中统一可用,只读取当前聊天已经绑定的会话,不新建会话、不调用模型,也不影响正在运行的任务或待处理交互。默认返回最近 3 条;`/history N` 接受正整数,超过 5 自动按 5 条处理,数量不足时返回实际条数。零、负数、小数、非数字和多个参数会提示用法,附带图片或文件时会拒绝处理;批量输入收集中请先 `/send` 或 `/cancel`。
- 历史预览中,一条用户消息或一条助手最终回复各算一条,不按轮次或天数计数。先取最新 N 条,再按从旧到新的顺序显示;不展示工具、推理、注入内容或尚未完成的助手片段,不下载或重发历史附件。长正文会截断并注明,全部结果最多发送 3 段文字,不自动翻页。绑定会话后可手动发送 `/history`,不会自动重发历史。正文仍可能包含会话原有的敏感信息,请只向可信用户开放机器人。

View file

@ -72,7 +72,7 @@ dsh-im 已经是一个具有九渠道多机器人管理、工作区与 Session
| 图片交付 | `dsh_im_return_file` 返回 `image/*` 时,九渠道均优先使用现有平台原生图片路径;确定性失败才复用既有文件附件降级,不增加配置、白名单或双消息补发 |
| Harness 会话 | 按机器人与聊天持久绑定 Session;支持工作区切换、Session 列表与绑定、模型查询与切换、停止、纠偏和上下文压缩 |
| Agent Preset | 每机器人独立选择或跟随 Host 默认;只影响之后新建 Session,不改变已有会话和在途回复 |
| 控制命令 | `/help`、`/new`、`/status`、`/models`、`/model`、`/presetlist`、`/preset`、`/stop`、`/steer`、`/compact`、`/workspace`、`/workspacelist`、`/sessionlist`、`/session` |
| 控制命令 | `/help`、`/new`、`/status`、`/models`、`/model`、`/presetlist`(别名 `/presets`)、`/preset`、`/stop`、`/steer`、`/compact`、`/workspace`、`/workspacelist`、`/sessionlist`(别名 `/sessions`)、`/session` |
| Harness 交互 | 九渠道已经能够用文字完成问题与审批;Actor/Route 所有权、FIFO、重放去重、跨端解决、取消和失败重试已有大量自动化覆盖 |
| 回复体验 | 飞书/钉钉卡片流、企业微信/Slack 原生或准原生流、QQ Markdown 与按渠道收敛的 Turn Feed、Telegram/Discord 编辑消息、WhatsApp typing + 最终消息;失败时已有安全恢复路径 |
| 运维可靠性 | 连接测试、自动重连、状态持久化、多机器人生命周期、并发竞态、工作区代际隔离、删除清理和敏感错误脱敏均已有测试资产 |

View file

@ -605,6 +605,10 @@ var package_default = {
{
name: "divingleee",
url: "https://github.com/divingleee"
},
{
name: "lKolabrodl",
url: "https://github.com/lKolabrodl"
}
],
license: "MIT",

File diff suppressed because one or more lines are too long

View file

@ -34,6 +34,10 @@
{
"name": "divingleee",
"url": "https://github.com/divingleee"
},
{
"name": "lKolabrodl",
"url": "https://github.com/lKolabrodl"
}
],
"license": "MIT",

View file

@ -72,14 +72,14 @@ const HELP_TEXT_LINES = [
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
'/workspace 工作区绝对路径 切换工作区',
'/workspacelist 列出工作区绝对路径',
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
'/models 按序号列出所有可用模型',
'/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
'/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型',
'示例:先发 /models,再发 /model 2 [推理等级ID]',
'/presetlist 按序号列出可用 Agent Preset',
'/presetlist 或 /presets 按序号列出可用 Agent Preset',
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
'纯数字 ID:/preset id:<ID>',
'/preset --default 跟随 Host 默认',

View file

@ -93,7 +93,7 @@ const REPAIR_COMMAND = /^\/repair(?:\s+(qr|status|cancel|verify))?\s*$/i;
const WATCH_COMMAND = /^\/watch(?:\s+([^\s]+))?$/i;
const UNWATCH_COMMAND = /^\/unwatch(?:\s+([^\s]+))?$/i;
const WATCHLIST_COMMAND = /^\/watchlist$/i;
const SESSION_LIST_PREFIX = /^\/sessionlist(?:\s|$)/i;
const SESSION_LIST_PREFIX = /^\/(?:sessionlist|sessions)(?:\s|$)/i;
const WORKSPACE_LIST_COMMAND = /^\/workspacelist$/i;
const NUMBER_REPLY = /^\d{1,2}$/;
/** A displayed menu stays number-tappable for this long. */
@ -133,13 +133,13 @@ const REPAIR_URL_HOSTS = new Set([
const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
/** Matches fast card commands that should not be queued behind a running task. */
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|sessionlist(?:\s|$)|workspacelist|watchlist|archived(?:\s+(on|off))?)$/i;
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|(?:sessionlist|sessions)(?:\s|$)|workspacelist|watchlist|archived(?:\s+(on|off))?)$/i;
/** Canonical workspace/session help advertised by every bridge family. */
const WORKSPACE_HELP_LINES = [
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
'/workspacelist 列出工作区绝对路径',
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
];
/** Safe user-facing text for bind/workspace failures (no raw messages). */
@ -1005,7 +1005,7 @@ export class FeishuHarnessBridge {
return;
}
if (SESSION_LIST_PREFIX.test(commandText)) {
const selector = commandText.slice('/sessionlist'.length).trim() || null;
const selector = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim() || null;
await this.#showSessions({ chatId: event.message.chat_id, key }, selector, 0);
return;
}

View file

@ -487,7 +487,7 @@ export function menuHelpText() {
'🤖 助手菜单(回复数字即可,无需记命令)',
'',
'📋 会话 / 工作区',
'/sessionlist 列出工作区会话',
'/sessionlist 或 /sessions 列出工作区会话',
'/session ID 绑定已有会话',
'/workspacelist 列出工作区',
'/workspace 路径 切换工作区',
@ -506,7 +506,7 @@ export function menuHelpText() {
'/unwatch ID 取消关注',
'',
'🤖 预设 / 模型',
'/presetlist 列出可用 Agent Preset',
'/presetlist 或 /presets 列出可用 Agent Preset',
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
'纯数字 ID:/preset id:<ID>',
'/preset --default 跟随 Host 默认',
@ -553,7 +553,7 @@ const HELP_TEXT_COMMANDS = [
'`/m` — 打开菜单卡片',
'`/new` — 开启全新会话',
'`/session ID` — 绑定已有会话',
'`/sessionlist [工作区]` — 列出会话',
'`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话',
'`/workspace 路径` — 切换工作区',
'`/workspacelist` — 列出工作区',
'`/status` — 查看连接状态',
@ -564,7 +564,7 @@ const HELP_TEXT_COMMANDS = [
'`/watchlist` — 关注列表',
'`/unwatch ID` — 取消关注',
'`/archived on/off` — 归档显隐',
'`/presetlist` — 列出预设',
'`/presetlist` 或 `/presets` — 列出预设',
'`/preset [序号/ID]` — 切换预设',
'`/preset --default` — 跟随默认',
'`/models` — 列出模型',

View file

@ -83,14 +83,14 @@ function helpText() {
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
t('/workspace 工作区绝对路径 切换工作区'),
t('/workspacelist 列出工作区绝对路径'),
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),
t('/preset --default 跟随 Host 默认'),

View file

@ -223,6 +223,8 @@ export default {
'📊 系统状态': '📊 System status',
'📋 会话 / 工作区': '📋 Sessions / workspace',
'/sessionlist 列出工作区会话': '/sessionlist List workspace sessions',
'/sessionlist 或 /sessions 列出工作区会话':
'/sessionlist or /sessions List workspace sessions',
'/session ID 绑定已有会话': '/session ID Bind an existing session',
'/workspacelist 列出工作区': '/workspacelist List workspaces',
'/workspace 路径 切换工作区': '/workspace PATH Switch workspace',
@ -239,12 +241,14 @@ export default {
'📦 批量输入(仅私聊)': '📦 Batch input (direct messages only)',
'🤖 预设 / 模型': '🤖 Presets / models',
'/models 列出模型': '/models List models',
'/presetlist 或 /presets 列出可用 Agent Preset':
'/presetlist or /presets List available Agent Presets',
'🎮 任务控制': '🎮 Task controls',
'/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 [工作区]` — 列出会话\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` — 补全飞书权限与卡片回调':
'**⌨️ 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',
'**⌨️ 文本命令**\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 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',
'从下方下拉选择补充指令;最后一项可自定义输入。':

View file

@ -124,6 +124,8 @@ export default {
'/workspacelist 列出工作区绝对路径': '/workspacelist List absolute workspace paths',
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题':
'/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',
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话':
'/session <Session ID or workspace index> Bind this chat to the specified session',
'/models 按序号列出所有可用模型': '/models List all available models by index',
@ -139,6 +141,8 @@ export default {
'示例:先发 /models,再发 /model 2 [推理等级ID]':
'Example: send /models first, then /model 2 [reasoning effort ID]',
'/presetlist 按序号列出可用 Agent Preset': '/presetlist List available Agent Presets by index',
'/presetlist 或 /presets 按序号列出可用 Agent Preset':
'/presetlist or /presets List available Agent Presets by index',
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset':
'/preset [index or full ID] Show or set the Agent Preset of this bot',
'纯数字 ID:/preset id:<ID>': 'Numeric-only ID: /preset id:<ID>',

View file

@ -8,7 +8,7 @@ import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
const PRESET_COMMAND = /^\/preset(?=$|\s)/iu;
const PRESET_LIST_COMMAND = /^\/presetlist(?=$|\s)/iu;
const PRESET_LIST_COMMAND = /^\/(?:presetlist|presets)(?=$|\s)/iu;
const PRESET_LIST_USAGE = '用法:/presetlist(不带参数)';
const PRESET_USAGE = [
'用法:',
@ -257,7 +257,7 @@ export async function runPresetCommand(text, harness, state, key, options = {})
const requestOptions = rpcOptions(options.signal);
if (PRESET_LIST_COMMAND.test(command)) {
if (!/^\/presetlist[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
if (!/^\/(?:presetlist|presets)[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
try {
const current = await settings(harness, requestOptions);
saveSnapshot(state, key, current.agentPresetCatalog.items);

View file

@ -542,14 +542,14 @@ export class TextHarnessBridge {
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
t('/workspace 工作区绝对路径 切换工作区'),
t('/workspacelist 列出工作区绝对路径'),
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),
t('/preset --default 跟随 Host 默认'),

View file

@ -6,7 +6,7 @@ import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
const WORKSPACE_LIST_COMMAND = /^\/workspacelist(?:\s+([\s\S]+))?$/i;
const SESSION_LIST_COMMAND = /^\/sessionlist(?:\s+([\s\S]+))?$/i;
const SESSION_LIST_COMMAND = /^\/(?:sessionlist|sessions)(?:\s+([\s\S]+))?$/i;
const SESSION_BIND_PREFIX = /^\/session(?=$|\s)/i;
const SESSION_BIND_COMMAND = /^\/session[ \t]+([^\s]+)$/i;
const MAX_WORKSPACE_PATH_LENGTH = 4_096;

View file

@ -22,10 +22,12 @@ export const TELEGRAM_COMMAND_MENU = Object.freeze([
{ command: 'workspace', description: '切换工作区' },
{ command: 'workspacelist', description: '列出工作区绝对路径' },
{ command: 'sessionlist', description: '列出会话 ID 和标题' },
{ command: 'sessions', description: '列出会话 ID 和标题' },
{ command: 'session', description: '将当前聊天绑定到指定会话' },
{ command: 'models', description: '按序号列出所有可用模型' },
{ command: 'model', description: '查看或切换当前会话模型' },
{ command: 'presetlist', description: '列出可用 Agent Preset' },
{ command: 'presets', description: '列出可用 Agent Preset' },
{ command: 'preset', description: '查看或设置新会话 Agent Preset' },
{ command: 'stop', description: '停止当前任务' },
{ command: 'steer', description: '纠偏当前任务' },

View file

@ -66,14 +66,14 @@ function helpText() {
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
t('/workspace 工作区绝对路径 切换工作区'),
t('/workspacelist 列出工作区绝对路径'),
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),
t('/preset --default 跟随 Host 默认'),

View file

@ -73,14 +73,14 @@ const HELP_TEXT = () => [
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
t('/workspace 工作区绝对路径 切换工作区'),
t('/workspacelist 列出工作区绝对路径'),
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
t('/models 按序号列出所有可用模型'),
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
t('/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级'),
t('/model [序号或完整模型ID] [推理等级ID] 查看或切换当前会话模型'),
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
t('/presetlist 按序号列出可用 Agent Preset'),
t('/presetlist 或 /presets 按序号列出可用 Agent Preset'),
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
t('纯数字 ID:/preset id:<ID>'),
t('/preset --default 跟随 Host 默认'),

View file

@ -3768,7 +3768,7 @@ test('preset card selection does not expose internal update errors', async () =>
function cards(messages) { return messages.filter((m) => m.msgType === 'interactive'); }
test('session list paginates by page number across 25 sessions', async () => {
test('/sessions alias uses the interactive session list and paginates across 25 sessions', async () => {
const fixture = stateFixture();
const sent = [];
const bridge = new FeishuHarnessBridge({
@ -3782,7 +3782,7 @@ test('session list paginates by page number across 25 sessions', async () => {
allowedSenderOpenIds: new Set(['ou_owner']),
});
await bridge.accept(event('sessions-open', '/sessionlist', { senderOpenId: 'ou_owner' }));
await bridge.accept(event('sessions-open', ' /SESSIONS ', { senderOpenId: 'ou_owner' }));
await bridge.waitForIdle();
assert.equal(cards(sent).length, 1);
const page0 = cards(sent).at(-1).content;
@ -3870,7 +3870,7 @@ test('session pagination preserves an explicitly selected workspace', async () =
allowedSenderOpenIds: new Set(['ou_owner']),
});
await bridge.accept(event('selected-workspace-open', '/sessionlist 2', { senderOpenId: 'ou_owner' }));
await bridge.accept(event('selected-workspace-open', '/sessions 2', { senderOpenId: 'ou_owner' }));
await bridge.waitForIdle();
assert.equal(useActionsFromCard(cards(sent).at(-1).content)[0], 'selected-01');

View file

@ -67,6 +67,8 @@ test('menu exposes the increased command set and keeps permission completion num
test('menu and card help advertise Agent Preset, reasoning, and batch commands', () => {
const help = menuHelpText();
assert.match(help, /\/presetlist/);
assert.match(help, /\/presets/);
assert.match(help, /\/sessions/);
assert.match(help, /\/preset \[序号或完整ID\]/);
assert.match(help, /\/preset id:<ID>/);
assert.match(help, /\/preset --default/);
@ -79,6 +81,8 @@ test('menu and card help advertise Agent Preset, reasoning, and batch commands',
assert.match(help, /\/version/);
const card = helpCard();
assert.match(card, /\/presets/);
assert.match(card, /\/sessions/);
assert.match(card, /\/reasoninglist/);
assert.match(card, /\/reasonings/);
assert.match(card, /\/reasoning \[序号、等级ID或 --default\]/);

View file

@ -218,9 +218,10 @@ test('Telegram API registers the command menu and commands-type menu button', as
await api.setChatMenuButton();
assert.equal(calls.length, 2);
assert.deepEqual(
TELEGRAM_COMMAND_MENU.filter(({ command }) => command === 'presetlist' || command === 'preset'),
TELEGRAM_COMMAND_MENU.filter(({ command }) => ['presetlist', 'presets', 'preset'].includes(command)),
[
{ command: 'presetlist', description: '列出可用 Agent Preset' },
{ command: 'presets', description: '列出可用 Agent Preset' },
{ command: 'preset', description: '查看或设置新会话 Agent Preset' },
],
);

View file

@ -46,20 +46,35 @@ function fixture({
return { calls, harness, state: {}, selected: () => selected };
}
test('isPresetCommand recognizes only /presetlist and /preset command prefixes', () => {
test('isPresetCommand recognizes /presets, /presetlist, and /preset command prefixes', () => {
for (const command of [
'/presetlist', ' /PRESETLIST ', '/presetlist ignored',
'/presetlist', ' /PRESETLIST ', '/presetlist ignored', '/presets', ' /PRESETS ',
'/preset', '/PrEsEt coding',
]) {
assert.equal(isPresetCommand(command), true, command);
}
for (const value of [
null, '', 'preset', '/presetx', '/presetlisting', 'hello /preset',
null, '', 'preset', '/presetx', '/presetlisting', '/presetss', 'hello /preset',
]) {
assert.equal(isPresetCommand(value), false, String(value));
}
});
test('/presets reuses /presetlist validation, listing, and snapshot behavior', async () => {
const { calls, harness, state } = fixture({ agentPreset: 'coding' });
const listed = await runPresetCommand(' /PRESETS ', harness, state, 'direct:alias');
assert.match(listed.message, /2\. Coding(coding)(当前选择)/);
assert.deepEqual(calls, [['agentPresetSettings', {}]]);
const selected = await runPresetCommand('/preset 1', harness, state, 'direct:alias');
assert.match(selected.message, /Standard(standard)/);
assert.deepEqual(calls.at(-1), ['updateAgentPreset', 'standard', {}]);
const invalid = await runPresetCommand('/presets unexpected', harness, state, 'direct:alias');
assert.match(invalid.message, /用法:\/presetlist/);
});
test('/presetlist dynamically lists, annotates, and snapshots presets without updating', async () => {
const { calls, harness, state } = fixture({ agentPreset: 'coding' });
const signal = new AbortController().signal;

View file

@ -1068,6 +1068,30 @@ test('/sessionlist supports the current workspace, list numbers, and absolute pa
assert.equal(workspaceListCalls, 1, 'only numeric selection needs the workspace registry order');
});
test('/sessions reuses /sessionlist parsing for current, numbered, and absolute workspaces', async (t) => {
const { defaultWorkspace, alternateWorkspace } = await fixture(t);
const calls = [];
const harness = {
currentWorkspace() { return defaultWorkspace; },
async listWorkspaces() { return [alternateWorkspace]; },
async listWorkspaceSessions(workspace) {
calls.push(workspace);
return { workspace, sessions: [] };
},
};
await runWorkspaceCommand(' /SESSIONS ', harness);
await runWorkspaceCommand('/sessions 2', harness);
await runWorkspaceCommand(`/sessions ${alternateWorkspace}`, harness);
assert.deepEqual(calls, [defaultWorkspace, alternateWorkspace, alternateWorkspace]);
assert.equal(await runWorkspaceCommand('/sessionsx', harness), null);
assert.match(
(await runWorkspaceCommand('/sessions relative/path', harness)).message,
/工作区必须是绝对路径/,
);
});
test('/sessionlist returns actionable and safe errors', async (t) => {
const { root, defaultWorkspace } = await fixture(t);
const file = join(root, 'not-a-workspace.txt');
@ -1226,7 +1250,7 @@ test('all nine channel bridge families advertise and fan out workspace command r
for (const file of bridgeFiles) {
const source = await readFile(new URL(file, import.meta.url), 'utf8');
assert.match(source, /\/workspacelist 列出工作区绝对路径/);
assert.match(source, /\/sessionlist \[工作区序号或绝对路径\] 列出会话 ID 和标题/);
assert.match(source, /\/sessionlist 或 \/sessions \[工作区序号或绝对路径\] 列出会话 ID 和标题/);
assert.match(source, /workspaceCommand\.messages \?\? \[workspaceCommand\.message\]/);
}
});