Skip to content

功能规格说明:自定义斜杠命令 ​

创建日期:2024-12-19

用户场景与测试 (必填) ​

用户故事:创建和使用基础自定义命令(优先级:P1) ​

用户可以通过将 markdown 文件放置在指定目录中来创建自定义斜杠命令,以自动化常用任务和工作流。

为什么是这个优先级:这是通过允许用户将复杂指令封装为简单命令来提供即时生产力价值的核心功能。

独立测试:可以通过创建一个简单的命令文件(例如 /project-info)并执行它来验证命令运行并产生预期输出来完整测试。

验收场景:

  1. 假设 用户的项目中有一个 .wave/commands/ 目录,当 他们创建一个名为 project-info.md 的 markdown 文件并包含命令内容,则 系统自动将其加载为 /project-info 命令
  2. 假设 存在一个自定义命令,当 用户在聊天中输入 /project-info,则 命令执行并在对话中显示结果
  3. 假设 自定义命令文件被修改,当 系统重新加载命令,则 更新的命令行为立即可用

用户故事:命令发现和自动补全(优先级:P1) ​

用户可以通过带有自动补全功能的交互式命令选择器发现可用的自定义命令。

为什么是这个优先级:对可用性至关重要 - 用户需要知道有哪些命令可用并能轻松找到它们。

独立测试:可以通过输入 / 并验证命令选择器出现,显示内置和自定义命令,以及功能搜索过滤来测试。

验收场景:

  1. 假设 自定义命令已加载,当 用户在输入框中输入 /,则 命令选择器出现,显示内置和自定义命令
  2. 假设 命令选择器已打开,当 用户在 / 后输入字符,则 列表过滤仅显示匹配的命令
  3. 假设 命令选择器显示过滤结果,当 用户用方向键导航并按 Enter,则 选中的命令被执行

用户故事:弹窗里的宿主命令只有一份表——由服务端下发并按入口过滤(优先级:P1) ​

作为在桌面端 / VS Code / JetBrains / 手机远控页之间来回用的用户,我希望同一个斜杠命令在四个入口都出现在弹窗里、点了也真的有效,而不是在电脑上看得见、在手机上只能靠手打、某一条又只在某一个宿主里有。

为什么是这个优先级:改动之前是三份宿主本地命令表各自维护(桌面 17 条、VS Code 18 条、JetBrains 17 条(均含此后下线的 /model),差异只有两条:桌面端整端没有 /clear、JB 没有 /config),而远控页那一侧一份都没有——它只从 bridge 拿到 SDK 那部分。于是同一批命令在四个入口分成三档:电脑上弹窗可见、手机上只能手打、还有一条在某个宿主里根本不存在。这不是观感问题,而是同一份知识被抄了四遍:每加一条要改三处(漏一处 = 某个入口的弹窗里少一条),而 webview 侧早就有过同类教训(弹窗分组与选中行为都因名单漂移出过缺陷,判据因此改成一律读宿主下发的 skillSource / runOnSelect)。

独立测试:在 VS Code / 桌面端 / JB / 手机远控页各打开一次弹窗,四个入口看到的宿主命令都来自同一张表(彼此只差各入口的开关差异),且每条点下去都在该入口真的执行;把表里某条改成只属于一个入口后,其余三个入口刷新后立即不再显示它。

验收场景:

  1. 假设 要看弹窗里的宿主本地命令(/config /plugin /reload-plugins /mcp /status /compact /tasks /workflows /agents /skills /hooks /rewind /resume /btw /plan /clear …)来自哪里,则 它来自服务端的一张表(packages/code 里与 AgentBridge 同侧的 stdio 服务端),随弹窗那次 getSlashCommands 回包一起下发。不放进 packages/webview(那是纯 UI 包,只准 import SDK 的 /types 与 /constants;把宿主能力表放进 UI 包等于让 UI 决定宿主能做什么),也不再由三个宿主各存一份。依据:四个入口本来就都连着同一个服务端——daemon 与 GUI 各端拉起的 --stdio 子进程都是 AgentBridge,手机请求最终也落到它上面;表放在这个交叉点上才谈得上「一份」。JB 是另一条理由:它是 Kotlin,import 不了 TS 模块,但能消费 JSON——经 RPC 下发是它唯一能共享到的形式。

  2. 假设 四个入口向服务端要这批命令,则 过滤依据是每次请求带的入口标识(desktop / vscode / jetbrains / remote-page),不是进程级宿主类型。依据:一台机器上,一个 GUI 宿主拉起的 --stdio 子进程可能同时服务它自己的会话区和手机(见 remote/remote-machine.md 边界的「--stdio 子进程与 daemon 都可能是服务方」)——进程级开关会让这两个消费方二选一,出现「手机上看到设置页命令」或「桌面上看不到」这类归属错误。

  3. 假设 回包过线,则 每条命令只带 id / name / description / skillSource? / runOnSelect? 这五个字段,不带 SlashCommand.handler(函数过不了线)。四个入口都只消费这个形状,谁都不再各自裁剪一遍。

  4. 假设 某个入口有「没有这条命令」的差异,则 差异写在表里(每条命令标出它属于哪些入口),照实记录今天的现状:桌面端没有 /clear(整端移除的既有拍板)、JB 没有 /config、远控页没有设置页 / 插件市场 / 计划预览 / 远程控制自身那几条(remote/remote-web.md 场景 21)。判据:这是能力差异而不是文案差异——谁不在表里给出的入口集合里,谁就不该看到它;反过来,看到了就意味着该入口真的能执行它。入口标识不够用时,收窄交给宿主:remote-page 是一个入口、却对应三种承接进程(daemon / GUI 的 --stdio 子进程 / 前台 CLI),能力面并不相同——/resume 只有前两种做得到(前台 CLI 只有一条会话槽位,恢复磁盘会话等于把用户正看着的那条换掉)。所以表的 surfaces 声明的是「这个入口的上限」,宿主的实现面可以在回包前再收窄一次:hostSlashCommandsFor("remote-page") 给全集,RemoteControlCliHost 删掉自己做不到的那条。判据不变——看到它就意味着这个宿主真的能执行它;页面照旧不存名单。

  5. 假设 用户在任一口味的弹窗里选中一条宿主命令,则 行为只由 runOnSelect 决定(真 = 选中即执行 / 交互;假 = 只把 /<名字> 插进输入框等用户补内容,/btw 是后者),执行本身仍在各入口自己的拦截 switch 里。因此每个入口要有用例钉住「表声称它支持的集合 == 它真的实现了的集合」;远控页那一档还要与远端白名单对齐(/btw↔askBtw、/rewind↔rewindToMessage…),避免出现「弹窗里有、点了回 -32601」。依据:表与执行点分居两处之后,靠同文件审查已经不够,只能用用例把两者钉在一起。

  6. 假设 服务端是旧版本(回包里没有这批命令,例如机器还在跑老 CLI),则 四个入口不显示它们、也不报错,更不回落成一份本地默认表(与 remote/remote-web.md 场景 23 的「缺字段就不写」同一条口径)。

  7. 假设 用户打开的是手机远控页的弹窗,则 页面上确实能用的那批命令(/btw /clear /resume /rewind /tasks /workflows /status /compact)出现在弹窗里,页面上必然无效的那批(设置页 / 插件市场 / /plan / /remote-control …)不出现——同一次回包就决定了,页面不再靠「手打才知道它存在」,也不再自己判断哪些命令归自己。例外只有一条:承接进程是前台 CLI 时,这同一份集合里少了 /resume(场景 4 的「宿主再收窄」)——同一个入口、不同宿主,页面拿到的清单因此可以不同;页面只需按收到的那份渲染。


用户故事:参数化命令(优先级:P2) ​

用户可以创建接受参数并使用参数替换的命令,以创建灵活、可复用的命令模板。

为什么是这个优先级:通过允许动态内容和减少对多个相似命令的需求来显著提高命令实用性。

独立测试:可以通过创建带有 $ARGUMENTS 或 $1, $2 占位符的命令并验证在调用命令时正确替换参数来测试。

验收场景:

  1. 假设 命令内容包含 $ARGUMENTS,当 用户执行 /command-name some arguments,则 $ARGUMENTS 被替换为 "some arguments"
  2. 假设 命令包含 $1 $2 占位符,当 用户执行 /command-name first second,则 $1 变为 "first",$2 变为 "second"
  3. 假设 命令期望参数,当 用户提供带空格的引号参数,则 引号内容被视为单个参数
  4. 假设 命令不包含参数占位符,当 用户执行 /command-name some arguments,则 "some arguments" 自动追加到命令内容末尾

用户故事:插件命令正文可用插件根目录占位符(优先级:P2) ​

作为插件作者,我希望在插件自带的 commands/*.md 正文里写 ${WAVE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_ROOT} 就能指向随插件分发的脚本,以便「命令 + 脚本绝对路径」这种写法在命令里同样成立,而不必为了拿到这段路径把能力改写成技能。

为什么是这个优先级:技能正文早就能替换这两个占位符,命令正文不能——于是插件作者只有两条路:把命令改写成技能(命令名被迫带插件命名空间 /<插件>:<技能>,用户还得多打一段),或把绝对路径硬编码进命令(插件装到别人机器上就失效)。命令与技能本来就共用同一套注入命令执行与权限判定,唯独这一处替换缺席,是同一件事只做了一半。

独立测试:在一个插件里写 commands/run.md,正文含 !`node ${WAVE_PLUGIN_ROOT}/scripts/x.mjs` 与一句对 ${WAVE_SKILL_DIR} 的引用;执行 /插件:run,断言脚本确实被执行、交给模型的正文里命令部分是插件根下的绝对路径,而 ${WAVE_SKILL_DIR} 未被替换并带一条可见提示。

验收场景:

  1. 假设 插件的 commands/<名字>.md 正文含 ${WAVE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_ROOT},当 该命令被调用,则 两者都被替换为该插件根目录的绝对路径;替换发生在参数替换($ARGUMENTS/$1)之后、注入命令执行之前,用的是与技能正文同一个替换实现(命令侧不另立一份)。
  2. 假设 插件的命令 frontmatter 在 allowed-tools 里写了含 ${WAVE_PLUGIN_ROOT} 的模式(如 Bash(node ${WAVE_PLUGIN_ROOT}/scripts/x.mjs*)),当 该命令被调用、这些模式被注册为临时放行规则时,则 注册的是替换成绝对路径后的规则:声明的那条命令直接执行、不提示确认——把永不匹配的占位符留在规则里比没有规则更糟,作者看到的是一条无法解释的确认提示(与 ecosystem/agent-skills.md 场景 4 同一口径)。
  3. 假设 自定义命令(~/.wave/commands/、<workdir>/.wave/commands/)正文含这两个占位符,当 它被调用,则 不替换(没有插件根可指向),并在正文里给出一条可见提示说明它不会被替换——不允许静默留下一个指向空路径的 token(与 ecosystem/agent-skills.md 场景 5 同一口径)。
  4. 假设 命令正文含 ${WAVE_SKILL_DIR}/${CLAUDE_SKILL_DIR},当 它被调用,则 一律不替换:命令不是技能,没有「技能目录」这个来源,提示同场景 3。

用户故事:命令配置和模型选择(优先级:P2) ​

用户可以使用 YAML frontmatter 为自定义命令配置特定的 AI 模型。

为什么是这个优先级:支持针对特定用例优化命令。

独立测试:可以通过创建 frontmatter 中指定模型的命令,然后验证 AI 在执行期间使用指定配置来测试。

验收场景:

  1. 假设 命令的 frontmatter 中有 model: gpt-4,当 命令执行,则 AI 使用指定模型而非默认值
  2. 假设 命令在 frontmatter 中有自定义描述,当 显示命令选择器,则 显示自定义描述而非自动生成的文本
  3. 假设 命令的 frontmatter 用 YAML 块标量写多行值(>/>-/|/|- 等),当 命令被加载,则 model/description 等字段必须是折叠/保留后的真实文本(不含 >、|、- 这类指示符),续行不得被解析成独立的 frontmatter 键;命令选择器里展示的描述就是解析后的结果

用户故事:项目和用户级命令作用域(优先级:P3) ​

用户可以在项目级(.wave/commands/)和用户级(~/.wave/commands/)定义命令,项目命令优先。

为什么是这个优先级:为项目特定工作流和跨项目的个人生产力命令提供灵活性。

独立测试:可以通过在两个位置创建同名命令并验证项目级命令覆盖用户级命令来测试。

验收场景:

  1. 假设 用户在 ~/.wave/commands/ 中有命令,当 他们使用任何项目,则 这些命令全局可用
  2. 假设 用户和项目目录都包含同名命令,当 命令被执行,则 使用项目级版本
  3. 假设 项目没有 .wave/commands/ 目录,当 用户级命令存在,则 它们仍被加载并可用

用户故事:自动批准的工具执行(优先级:P1) ​

作为用户,我希望在触发斜杠命令时 AI 自动执行特定工具,这样我就不必手动确认已知工作流的每一步。

为什么是这个优先级:这是一个高价值增强功能,减少常见自动化任务的摩擦。

独立测试:可以通过触发带有 allowed-tools 的斜杠命令并验证 AI 执行这些工具时不提示用户确认来测试。

验收场景:

  1. 假设 斜杠命令定义了 allowed-tools: Bash(git commit *),当 用户触发此命令且 AI 调用 Bash(git commit -m "test"),则 工具应立即执行,无需确认提示,因为它匹配模式
  2. 假设 斜杠命令定义了 allowed-tools: Bash(git commit *),当 用户触发此命令且 AI 调用 Bash(git commit -m "test" && rm -rf /),则 工具必须被阻止或需要确认,因为命令链的第二部分不被允许
  3. 假设 斜杠命令有 allowed-tools,当 AI 调用不在列表中的工具(例如 Write),则 系统必须像往常一样提示用户确认(除非它已在 settings.json 中被允许)
  4. 假设 带有 allowed-tools 的活跃斜杠命令会话,当 AI 完成任务且响应周期结束,则 所有后续工具执行必须需要手动确认

边界情况 ​

  • 命令文件包含无效 YAML frontmatter 时会怎样?
  • 系统如何处理具有无限循环或长时间运行操作的命令?
  • 命令引用不存在的参数时会怎样(例如只有 2 个参数但引用 $5)?
  • 重载操作期间如何处理同名命令?
  • 自定义命令中的 bash 命令失败或超时时会怎样? 不中止调用:该命令的错误输出以 <error>Command failed (exit code N): <输出></error> 形式留在正文里供模型看到,其余命令与后续处理继续执行。要把它与权限判定失败区分开——后者会中止整次调用(判定入口与语义见 ecosystem/agent-skills.md 的「注入命令执行前的权限校验」用户故事;技能与自定义命令共用同一套注入实现)。
  • 空允许工具:如果斜杠命令未定义 allowed-tools,所有工具执行必须需要手动确认
  • 无效模式语法:如果 allowed-tools 模式语法无效,系统应忽略该特定模式并对匹配工具默认使用手动确认
  • 会话持久性:如果用户开始新任务或切换上下文,之前斜杠命令的任何活跃 allowed-tools 权限必须被撤销
  • 重叠模式:如果多个模式匹配工具执行,最宽松的一个(自动批准)优先