---
url: /wave-agent/specs/ecosystem/plugin.md
description: 插件加载与市场生态：作用域管理、命令/技能/代理/钩子/LSP/MCP/工作流组件与就地重载
---

# 功能规格说明：插件支持与市场

## 背景

本规格说明合并并统一了本地插件支持、扩展插件能力（Skills、LSP、MCP、Hooks、Agents）、插件作用域管理以及插件市场生态系统（包括本地、GitHub 和内置市场支持，以及交互式 CLI 管理界面）的需求。它为 Wave 生态系统内插件的发现、安装和管理提供了单一事实来源。

## 远程插件获取

除内置官方市场（`wave-plugins-official`，走镜像 zip 快照，见下方「官方插件市场镜像 zip 快照获取与更新」用户故事）外，所有远程插件/市场获取都通过 `GitService` 使用 **`git clone --depth 1`**。没有直接的 HTTP 文件下载。Git 获取流程：

1. **市场注册** → 将市场仓库 `git clone`（HTTP/HTTPS/SSH）到 `~/.wave/plugins/marketplaces/<repo>/`
2. **插件安装** → `marketplace.json` 中插件条目的 `source` 支持两种形态：**字符串**（Git URL —— `http://`、`https://`、`git@`、`ssh://` —— 或市场检出目录内的相对路径）与**对象**（`{"source":"url","url":…}` 整仓，或 `{"source":"git-subdir","url":…,"path":…}` 仓库子目录，两者均可带 `ref` / `sha`）。Git URL 形态单独克隆插件仓库到临时目录后移入缓存；`git-subdir` 形态克隆后取 `path` 指向的子目录作为插件根；相对路径从市场检出目录解析。字段语义见 A-021。
3. **插件加载** → `PluginLoader` 从缓存的本地副本读取 `.wave-plugin/plugin.json` 和组件子目录。
4. **插件激活** → `PluginManager.loadSinglePlugin()` 向各自的管理器注册命令、技能、钩子等。

内置官方市场 zip 快照镜像获取流程（内容寻址 zip + latest 指针 + 本地哨兵）：

1. GET `{base}/latest`（~10s 超时）→ 得到内容寻址 sha
2. 读市场目录哨兵 `.wave-market-sha`；与 sha 相等 → no-op（幂等，不做任何下载）
3. GET `{base}/{sha}.zip`（~60s 超时）→ 解压到 `{市场目录}.staging`（拒绝路径穿越/绝对路径、文件与总量大小上限；按 zip external attrs 恢复可执行位）
4. 原子换入：删除旧市场目录 + 重命名 staging → 市场目录；写回 `.wave-market-sha` 哨兵
5. 任一步失败 → 本次镜像获取失败（不动现有市场内容）；若 git 兜底开关开启则回退既有 git pull/clone 路径（Git 不可用则跳过更新）

镜像仅作为内置官方市场的获取通道替换，settings/缓存中的 `source` 字段保持 `github` 不变（按市场名称特判），存量数据零迁移。

## 用户场景与测试

### 用户故事：开发者创建本地插件（优先级：P1）

作为开发者，我希望在本地创建自定义插件，以便我可以用自己的命令、技能和其他组件扩展 agent 的能力。

**验收场景**：

1. **假设**新目录 `my-plugin`，**当**我创建带有有效元数据的 `.wave-plugin/plugin.json` 时，**则**该目录被识别为有效的插件结构。
2. **假设**插件目录，**当**我在 `commands/` 目录中添加 Markdown 文件时，**则**它被识别为潜在的斜杠命令。
3. **假设**插件目录，**当**我添加带有有效 frontmatter 的 `skills/my-skill/SKILL.md` 文件时，**则**系统识别 "my-skill" 技能。
4. **假设**插件目录，**当**我添加 `workflows/scan.js`（首条语句为纯字面量 `meta`）时，**则**插件加载后可按 `Workflow({name:"my-plugin:scan"})` 调用它；**假设**该文件的 `meta` 非法，**则**只跳过这一个文件，插件其余组件与其它工作流照常加载（语义见 [workflow.md](../multi-agent/workflow.md)「调用插件携带的工作流」）。

### 用户故事：用户加载和管理插件（优先级：P1）

作为用户，我希望加载本地插件并在不同作用域（用户、项目、本地）中管理其启用状态。

**验收场景**：

1. **假设** `./my-plugin` 有有效插件，**当**我运行 `wave --plugin-dir ./my-plugin` 时，**则**插件被加载到会话中。
2. **假设**插件已安装，**当**我运行 `wave plugin enable <plugin-id> --scope project` 时，**则**插件为当前项目启用，其命令可用。
3. **假设**多个作用域对插件有冲突的设置，**当**系统加载插件时，**则**它必须遵守优先级：`local` > `project` > `user`。

### 用户故事：正确的插件结构验证（优先级：P3）

作为开发者，我希望系统将组件目录（如 `skills/` 或 `commands/`）放在 `.wave-plugin/` 目录内时警告我。

**验收场景**：

1. **假设**插件中 `skills/` 在 `.wave-plugin/` 内部，**当**插件被加载时，**则**系统应该忽略 `skills/` 目录或提供警告。

### 用户故事：发现和安装插件（优先级：P1）

作为用户，我希望从市场浏览可用插件并在不同作用域中安装它们，以便我可以为自己或团队扩展 Wave 的功能。

**验收场景**：

1. **假设**用户在 `wave plugin` UI 的"发现"部分，**当**他们选择插件时，**则**他们应该看到详情和三个安装选项：项目（默认）、用户和本地。
2. **假设**插件被选中，**当**选择"为所有协作者安装（项目作用域）"时，**则**插件被下载并在仓库配置中自动启用。
3. **假设**插件被选中，**当**选择"为你安装（用户作用域）"时，**则**插件被下载并全局自动启用给当前用户。
4. **假设**插件被选中，**当**选择"为你安装，仅在此仓库中（本地作用域）"时，**则**插件被下载并自动启用给用户，但仅在当前仓库中活动。
5. **假设**全新安装的 Wave，**当**我运行 `wave plugin marketplace list` 时，**则**我应该在已注册市场列表中看到 `wave-plugins-official`。

### 用户故事：管理已安装插件（优先级：P2）

作为用户，我希望看到我安装了哪些插件，并能够切换其状态或移除它们，以保持环境干净和功能正常。

**验收场景**：

1. **假设**用户在"已安装"部分，**当**他们选择插件时，**则**他们应该看到卸载、启用或禁用的选项。
2. **假设**一个插件，**当**选择"卸载"时，**则**只清除**当前作用域**的启用记录（配置链 `local` > `project` > `user` 中该作用域的那一层）与该作用域在本机的安装记录；该插件在其它作用域的启用记录与安装记录一律保留。**假设**这是该插件在本机的最后一条安装记录，**则**其在 `~/.wave/plugins` 下的缓存目录一并删除；否则缓存保留（同一插件的各作用域共用同一份按版本寻址的缓存，见 A-015）。
3. **假设**插件以用户作用域启用（`--scope user`），**当**我在主目录或任意目录运行 `wave plugin list` 时，**则**该插件的作用域标签显示 `(user)`，与安装输出一致。
4. **假设**某插件以项目作用域或本地作用域在另一个项目中安装，**当**我在当前目录运行 `wave plugin list` 时，**则**该插件显示为 `not installed`（安装状态以当前目录的配置链为准，本机缓存的存在不构成该目录中的「已安装」），也不展示作用域标签。
5. **假设**同一插件在项目 A 以项目作用域安装、在项目 B 也以项目作用域安装（两处各有一条安装记录、共用同一份本机缓存），**当**我在项目 A 卸载该插件时，**则**只有 A 的启用记录与 A 的安装记录被清除，B 的安装记录与本机缓存均保留——在 B 中该插件仍显示为已安装且作用域为 `(project)`；**假设**该插件同时以用户作用域在本机启用，**则**卸载项目 A 的项目作用域后，用户作用域的该插件在任意目录（含项目 A）仍照常可用。

### 用户故事：托管插件的卸载与禁用限制（优先级：P1）

作为组织管理员，我下发的插件不应被成员卸载或禁用；作为成员，我应当一眼看出该插件由组织管理、卸载入口不可用，而不是点了之后才被拒绝。

**为什么是这个优先级**：只下发而不限制，成员一次卸载就能绕过管控，「强制」不成立；同时限制必须是**可见的**（卸载入口置灰或移除 + 一句说明），否则会被当成功能故障。

**独立测试**：托管下发一个插件并确认已启用，在插件市场里查看该插件，验证卸载入口不可用并带一句「由组织管理」的说明；在 CLI 执行卸载该插件，验证命令被拒绝、错误信息可理解且不产生任何改动；随后管理员从托管配置里去掉该插件，验证卸载入口恢复可用。

**验收场景**：

1. **假设**某插件由托管配置启用，**当**用户在插件市场查看该插件时，**则**该插件的卸载入口不可用（置灰或移除，不是点击后才报错），并给出一句由组织管理的说明。
2. **假设**某插件由托管配置启用，**当**用户通过 CLI 执行卸载该插件时，**则**命令被拒绝并给出「该插件由组织管理」类的错误，命令不产生任何配置或安装记录的改动。
3. **假设**某插件由托管配置启用，**当**用户在本机切换该插件的安装作用域或执行更新时，**则**该插件仍保持启用——这两个操作不改变托管生效判定，允许执行。
4. **假设**管理员把该插件从托管配置中去掉，**当**界面重新拉取插件列表时，**则**卸载入口恢复可用、组织管理说明消失，插件变回用户可自行卸载的普通插件。
5. **假设**某插件由托管配置启用、而本机记录里它是禁用状态，**当**插件列表渲染时，**则**该插件仍显示为已安装且可用——界面不得出现在本机记录与实际生效状态之间说谎的「已禁用」状态（与 A-019 一致：插件视图本就不表达启用 / 停用态）。

判据见 A-024；托管侧的下发与合并语义见 [server-managed-config.md](../enterprise/server-managed-config.md)「托管配置下发插件市场与启用列表」。

### 用户故事：管理市场（优先级：P3）

作为用户，我希望添加和管理市场来源，以便我可以从各种提供商（GitHub、SSH 或本地路径）访问插件。

**验收场景**：

1. **假设**有效的 GitHub 仓库 `owner/repo`，**当**我运行 `wave plugin marketplace add owner/repo` 时，**则**市场成功注册。
2. **假设**有效的 Git 仓库 URL，**当**我运行 `wave plugin marketplace add [url]` 时，**则**市场成功注册。
3. **假设**带有有效 `marketplace.json` 的目录，**当**我运行 `wave plugin marketplace add [path]` 时，**则**市场成功注册。
4. **假设**现有市场，**当**在 CLI 插件管理器中选中时，**则**用户可以选择「批量更新插件」（按当前清单批量升级该市场内所有可更新插件，不拉取检出——清单刷新由打开插件管理器时的后台刷新承担）或「移除」市场。
5. **假设**我在插件管理器的市场视图中按 `a` 添加一个此前未添加过的市场，**当**添加成功并刷新回市场列表时，**则**新加的市场成为列表中的选中项（焦点落在新市场行，而不是回落列表首项）；**假设**添加失败（来源无效、清单缺失、网络失败或与已有市场重名），**则**保持原有选中项并展示失败原因。

### 用户故事：官方插件市场镜像 zip 快照获取与更新（优先级：P1）

作为用户，我希望内置官方插件市场（`wave-plugins-official`）通过网络可达的内容寻址 zip 快照镜像获取与增量更新（无需 git、无需访问 GitHub），以便在国内网络受限环境下也能获得官方插件市场内容。

**为什么是这个优先级**：官方市场托管在 GitHub（国内访问受限），镜像 zip 快照是国内用户获取官方市场内容的可靠通道（对齐 Claude Code 的 GCS zip 镜像模型）；它只替换市场获取这一步（git clone/pull → 内容寻址 zip），下游插件安装/加载代码零改动。镜像未配置或失败时保留 git 兜底，不破坏现有用户。

**验收场景**：

1. **假设** 镜像 base URL 已配置（环境变量 `WAVE_OFFICIAL_MARKET_MIRROR_BASE_URL` 优先，否则代码内置常量），且市场目录内哨兵 `.wave-market-sha` 内容等于 `latest` 指针返回的 sha，**当** 官方市场更新时，**则** 客户端判定已是最新（no-op）：不下载 zip、不改动市场目录。
2. **假设** 镜像 `latest` 指针返回新 sha，**当** 官方市场更新时，**则** 客户端下载 `{base}/{sha}.zip` 全量快照、解压到 staging 目录、原子换入市场目录并写回哨兵，市场 manifest 与 `plugins/` 目录结构原样保留、可被既有插件安装流程直接使用。
3. **假设** 市场目录由早期 git clone 得到（目录内无哨兵），**当** 官方市场更新时，**则** 客户端下载镜像 zip 整体替换该目录，不依赖目录内 git 历史。
4. **假设** 下载的 zip 损坏或解压被拒绝（路径穿越/绝对路径条目、单文件或总量超限），**当** 官方市场更新时，**则** 本次更新失败且不改动现有市场内容；git 兜底开启时回退既有 git pull/clone 路径，Git 不可用或兜底关闭则跳过本次更新。
5. **假设** `latest` 返回空 body 或镜像网络请求失败，**当** 官方市场更新时，**则** 与 zip 失败同路径处理：本次镜像获取失败并回退 git（若兜底开启）。
6. **假设** 镜像 URL 未配置（环境变量未设置且内置常量仍为占位空值），**当** 官方市场更新时，**则** 行为保持现状——直接走 git 路径，不发起任何 HTTP 请求。
7. **假设** 官方市场为内置市场（`source` 字段保持 `github` 不变），**当** 消费端需要决定获取通道时，**则** 仅按市场名称 `wave-plugins-official` 特判优先尝试镜像；其他 github/git 市场不受影响。
8. **假设** 镜像快照 zip 内含需要可执行位的脚本（hooks 等），**当** 解压落盘时，**则** 依据 zip external attrs 恢复可执行位（对齐 git clone 原生保留 +x），hooks 脚本可直接执行。

### 用户故事：兼容 Claude Code 生态的市场清单与插件（优先级：P2）

作为用户，我希望把 Claude Code 生态的插件市场（如 `anthropics/claude-plugins-official`）按原样添加进来、并安装其中的插件，以便我复用已经存在的插件生态，而不必等每个插件被逐条改写成本仓库的写法。

**为什么是这个优先级**：Claude Code 官方市场 310 条条目的 `source` 里有 258 条（84%）是对象形态（161 条 `url` + 97 条 `git-subdir`），而现行实现把 `source` 一律当字符串处理——在读取「最新版本」时对 `source` 直接调用 `startsWith`，对象传进去抛 `TypeError`，又被市场级 `catch` 静默吞掉。实测结果是**整个市场一条都不展示**（同一市场把来源过滤成只剩字符串条目后，同一次操作列出 52 条）。同理，Claude Code 的插件清单里 `version` 是可选的、`.mcp.json` 允许扁平形态（该生态 14 个 `.mcp.json` 里 9 个是扁平），照现状都会让插件「安装成功但加载失败」。这条兼容层只动市场清单与插件清单的**解析**，不动安装与加载的下游流程，风险面小、收益是整个第三方生态可用。

**独立测试**：添加 `anthropics/claude-plugins-official`，确认市场列表中该市场的条目数等于其清单条数（310 条，而非 0 条），且其中不可安装的条目逐条给出原因；安装一条 `git-subdir` 来源的插件，确认克隆后取到 `path` 子目录的内容；安装一条缺 `version` 的插件，确认安装后能正常加载；安装一条使用扁平 `.mcp.json` 的插件，确认 MCP 服务器被注册且该插件的命令 / 技能 / 钩子照常可用。

**验收场景**：

1. **假设** 市场清单里某条目的 `source` 是对象且其 `source` 字段为 `url`，**当** 读取该市场清单时，**则** 该条目按 Git URL 处理，与把 `source` 直接写成同一 URL 字符串等价；市场列表不因清单里存在对象形态条目而少列任何条目（A-021）。
2. **假设** 某条目的 `source` 为 `{"source":"git-subdir","url":…,"path":…}`，**当** 安装该插件时，**则** 克隆 `url` 指向的仓库后以 `path` 指向的子目录作为插件根（插件清单与组件目录都从该子目录读取）；**假设** `path` 在该仓库中不存在，**则** 该插件安装失败并给出原因。
3. **假设** 对象形态条目带 `sha`，**当** 安装该插件时，**则** 按该 `sha` 检出（不是只认 `ref` 的分支/标签）；**假设** 该 `sha` 无法检出，**则** 该插件安装失败并给出原因，不得静默改用默认分支。
4. **假设** 市场清单里某条目形态无法识别（未知的 `source` 取值、缺 `url`、字段类型不符等），**当** 读取该市场清单时，**则** 该条目照常出现在列表中，但安装它时明确失败并给出原因（指出该条目的 `source` 形态不受支持）；**该市场内其余条目照常列出且可正常安装**——不得因单条解析失败丢掉整个市场，也不得静默跳过而没有任何可见信息。
5. **假设** 某已注册市场的清单本身读取或解析失败，**当** 插件市场列表渲染时，**则** 该失败不得被静默吞掉：记录含市场名与失败原因的告警日志，且不影响其它市场的列出（失败的市场贡献零条目，但源码层面必须留下可诊断的记录，不允许 `catch {}` 空吞）。
6. **假设** 插件清单（`.wave-plugin/plugin.json`，兼容 `.claude-plugin/plugin.json`）没有 `version`，**当** 安装并加载该插件时，**则** 该插件正常加载、版本按 `1.0.0` 处理（与安装阶段既有的 `version || "1.0.0"` 回退一致），不得出现「安装成功、加载时报缺少 `version`」的组合（A-022）。
7. **假设** 插件根目录的 `.mcp.json` 使用扁平形态（`{"<serverName>": {…}}`，无外层 `mcpServers`），**当** 插件被加载时，**则** 其中的 MCP 服务器与包裹形态（`{"mcpServers": {"<serverName>": {…}}}`）等价地被注册，且该插件的命令 / 技能 / 子代理 / 钩子 / LSP 不受牵连——MCP 声明的形态不得让整个插件加载失败（A-023）。
8. **假设** 插件清单内联声明了 `mcpServers`（没有独立 `.mcp.json`），**当** 插件被加载时，**则** 该声明与独立 `.mcp.json` 等效；两者同时存在时按服务器名合并、同名以 `.mcp.json` 为准（A-023）。
9. **假设** 以上解析发生在 CLI 与 GUI 三端，**当** 呈现结果时，**则** 四端一致（同一份清单解析结果）：不得出现某端列出、某端不列出的分叉。

### 用户故事：插件市场（优先级：P1）

作为 GUI 用户（桌面端 / VS Code 扩展 / JetBrains 插件），我希望在「插件市场」中按市场浏览与筛选插件、安装 / 更新 / 卸载插件、更换安装作用域，并在执行当前市场的批量更新前先看到「到底会更新哪些插件」的确认弹窗，另外通过市场切换行右侧的入口添加市场、打开「管理插件市场」弹窗查看与移除市场，以便在不切换到 CLI 的情况下集中发现并启用产品能力；桌面端还要能从首页左侧边栏「新对话」下方一步进入插件市场整页，不必先钻进设置页。

**为什么是这个优先级**：插件市场是产品能力的集中入口（对齐 claude.ai / Claude Desktop 的能力市场定位）；CLI 的 `wave plugin` 界面在 GUI 宿主中不可用，而按市场组织、版本对比、安装作用域管理这些能力此前在 GUI 上缺失。桌面端把入口提到首页侧边栏，是因为「发现并安装能力」与「改配置」是两类操作——前者不该藏在设置页里。市场来源的增删是低频配置动作，故从市场切换行的文字按钮收敛成两个图标入口（添加 / 管理），让切换与浏览动作保持干净。批量更新是一次写动作（会改动本机已安装插件与磁盘上的启用记录），用户需要在下发前确认影响面，故按下去先给出待更新清单；它的作用范围是**当前正在看的这个市场**，故挨着该市场的筛选分段器摆，而不是横跨所有市场。

**独立测试**：桌面端点侧边栏「新对话」下方的「插件市场」进入整页视图，验证入口高亮、侧边栏「当前对话」条目在整页打开期间仍为常态选中样式（打开前后一致）、页面自带返回、返回后会话视图（分屏与选中会话）保持、整页打开期间从侧边栏选中另一条对话时整页自动关闭并切到该对话；在桌面端打开设置页，验证左侧导航「AI 与扩展」分组里**没有**「插件市场」项，而在桌面端输入 `/plugin` 进入的是插件市场整页（不是设置页）；在 VSCE / JetBrains 的设置页里「插件市场」项仍在、输入 `/plugin` 落到设置页的该视图；在任一 GUI 宿主的插件市场视图里切换市场 tab 验证列表与计数随市场变化、分段器复位为「全部」，且 tab 文案与宿主下发的市场名称一致；注册市场多到切换区放不下时，验证还有未展示切换项的一侧出现渐隐提示、滚到该端渐隐消失，且右侧图标按钮位置不变；点击市场切换行右侧的「管理」图标（悬浮该图标按钮时下方显示「管理插件市场」提示气泡，加号同理显示「添加插件市场」）打开弹窗，验证弹窗列出全部市场（官方行标「官方」且无可移除操作）、在弹窗内添加一个本地路径市场后列表与 tab 同步出现且自动切到该新市场（该新切换项落在切换条可视区之外时，切换条自动滚动使其完整可见）；注册市场多到列表超出弹窗可视高度时，验证滚动只发生在市场列表区（标题行与「添加插件市场」按钮位置不随滚动变化）；从市场切换行的加号入口与弹窗内同名入口分别打开对话框，验证两处打开的弹窗标题都是「添加插件市场」（与入口同名）；对未安装插件选择作用域并安装；悬浮可更新行的升级胶囊与「更新」按钮，验证两处都在元素下方给出同款气泡（文案分别为「已安装 v<旧>，最新版本 v<新>」「更新到最新版本 v<最新>」，且都没有浏览器原生 `title`），移开鼠标即消失；悬浮未安装行的「安装」按钮，验证**不出现**任何气泡（按钮文字已自明）；对已安装且有新版本的插件执行更新；添加 / 移除市场与行内单插件更新成功后，验证宿主给出「已添加市场「<市场名>」」「已移除市场「<市场名>」」「已更新「<插件名>」至 v<版本>」的全局成功反馈（批量更新后的「「<市场名>」已更新 N 个插件 / 「<市场名>」已是最新」文案不变）；在筛选行里、筛选分段器右侧看到带可更新数量的「更新」（数量只按**当前市场**统计，该市场没有可更新插件时按钮消失、也不受筛选与搜索影响），切换市场验证数量随之变化，点击后验证先弹出待更新清单（注明「<市场名> 市场下」、逐行含名称 / 版本变化 / 作用域），取消不产生任何更新、确认后才触发一次该市场的更新；更换安装作用域后确认旧作用域不再保留该插件；在弹窗内移除一个自定义市场时，验证确认框副文本按该市场下**已安装**插件计数、该市场下没有已安装插件时不显示副文本，移除后自动切到剩余市场；只剩官方市场时验证弹窗仍可打开并添加新市场；悬浮任一已安装插件行的作用域按钮，验证气泡只给出**当前锚点工程**的「工程名（工程根目录）」一行、不带「所属项目」这类标签、也不列出该插件在别的工程里的启用记录（只以用户作用域安装的插件显示「用户级安装（所有项目可用）」；没有锚点工程时显示「所属工程未知」），且行内作用域文案不变；桌面端在选中不同对话时进入插件市场，验证列表里的作用域呈现与「安装 / 更换安装作用域」对话框中 project、local 两档给出的工程名都以**当前选中对话所属工程**为准（不是上一次打开过的工程），锚点为空时 project / local 两档置灰不可选。在项目 A 以项目作用域安装某插件后在项目 B 打开，验证该插件显示为未安装；对同一插件分别在两个项目以不同作用域安装后，卸载任一侧并验证另一侧的安装状态、作用域标签与插件功能都不受影响。

**验收场景**：

1. **假设**桌面端首页左侧边栏可见，**当**查看「新对话」按钮下方，**则**存在「插件市场」入口；**当**用户点击该入口，**则**会话区切换为插件市场整页视图（会话侧边栏保留、页面左上提供返回按钮），入口呈高亮态；整页只替换会话区，**侧边栏中「当前对话」条目在整页打开期间必须保持常态选中样式**——既不因进入整页取消选中（该会话与聚焦分屏的绑定未变），也不为整页另设一套视觉（与未打开整页时一致）；若会话状态看板此刻正打开，**则**看板关闭（会话区同一处整页视图同时只呈现一个，后开者优先）。
2. **假设**插件市场整页已打开，**当**用户点击返回按钮、或再次点击侧边栏「插件市场」入口时，**则**回到打开前的会话视图（分屏布局与选中会话保持）、入口高亮态取消（与「活动」看板的开关语义一致）；**假设**用户在整页打开期间从侧边栏选中了另一条对话，**则**整页随之关闭、回到会话视图并切到该对话（与「活动」看板点击会话卡片的既有语义一致）——插件视图以打开时的当前对话所属工程为锚点（A-018），切对话必先离开整页，锚点才不会在页面停留期间漂移。
3. **假设**用户打开设置页，**当**查看左侧导航，**则**「AI 与扩展」分组里的各项按宿主区分：**桌面端不出现「插件市场」项**——桌面端已有侧边栏整页这一入口（场景 1），设置页不再重复摆一个；**VSCE / JetBrains 保留「插件市场」项**（这两个宿主没有侧边栏整页，设置页这项是它们唯一入口），点击后进入设置页内的插件市场视图（与整页共享同一套市场与插件列表）。
4. **假设**用户在输入框输入 `/plugin`，**当**命令执行时，**则**不弹出独立对话框，而是落到**当前宿主可用的那个入口**：桌面端打开插件市场整页（场景 1/2）；VSCE / JetBrains 打开设置页并直接选中「插件市场」项（行为不变）。
5. **假设**插件市场视图已打开（整页或设置页内），**当**渲染市场切换区时，**则**每个已注册市场显示为可切换项并带该市场内的插件数量，切换项文案即该市场自身的名称（见 A-011，内置官方市场即 `wave-plugins-official`），顺序即宿主下发顺序；默认选中第一个市场，插件列表与筛选计数均以当前市场为单位；**假设**市场名称较多导致切换区放不下，**则**切换区自身横向滚动、右侧入口位置固定不动（滚动条不占布局空间），**并在还有未展示切换项的那一端叠加渐隐提示**：右端尚有未展示项时右端渐隐，滚到最右端后右端渐隐消失；从起点向右滚开后左端同理出现渐隐，回到起点即消失——用户据此感知「这一侧还有市场」。渐隐是纯提示层：不占布局宽度、不改变任何切换项与右侧入口的位置，也不拦截鼠标（点渐隐覆盖处等于点其下的切换项，不改变当前选中的市场）。
6. **假设**当前市场已选中，**当**用户切换筛选（全部 / 已安装 / 未安装）时，**则**三个筛选各自带当前市场内的对应数量，列表仅展示命中筛选的插件；**假设**用户在某个市场里选过「已安装 / 未安装」后再切换到另一个市场，**则**分段器自动复位为「全部」——每个市场都从「全部」开始展示（不让新市场继承上一个市场的分组，避免切过去只看半个列表）；**当**复位发生时，关键词搜索框里已输入的内容保持不变（只复位分组，不清空搜索词）。
7. **假设**用户在搜索框输入关键词，**当**列表刷新时，**则**仅展示当前市场内名称或描述匹配关键词的插件。
8. **假设**当前市场的插件列表已渲染，**当**展示插件行时，**则**每行包含插件名称、功能描述、版本信息、当前作用域（已安装时），以及操作：未安装显示「安装」，已安装且有新版本显示「更新」——两者都是主色实心按钮、贴在该行最右端（已安装时作用域下拉在它左侧）；已安装且已是最新**不显示状态按钮**——「已安装」这一状态由作用域下拉本身表达（不再有「已安装」按钮）；右侧动作列与该行的名称行垂直对齐；**假设**鼠标悬浮或键盘聚焦到「更新」，**则**在按钮下方显示提示气泡（与作用域下拉、市场切换行的图标按钮同一套提示，见 A-020，不用浏览器原生 `title`），文案为「更新到最新版本 v<最新版本>」，鼠标移开或失焦即消失；「安装」**不带**提示气泡——按钮文字已把动作说清，气泡只会重复一遍（见 A-020）。
9. **假设**插件未安装，**当**渲染版本信息时，**则**显示该插件当前可安装版本的胶囊 `v<版本>`（不带「最新」字样），位于插件名右侧。
10. **假设**插件已安装且存在新版本，**当**渲染版本信息时，**则**在插件名右侧显示**一个**升级胶囊（而不是两个各自成形的版本胶囊）——胶囊内是「当前已安装版本 `v<旧版本>` → 最新版本 `v<新版本>`」，两段之间用向右箭头连接，整体读作「已安装 v旧 → 最新 v新」（哪个版本对应哪种状态由「左→右的箭头」承载，用户不必猜）；该胶囊与未安装 / 已是最新时的普通版本胶囊**是同一形制**，胶囊内的两个版本号与箭头一律不承载状态语义（不在胶囊内部重复表达「可更新」）；「可更新」的语义完全由紧跟在胶囊右侧的「可更新」徽标承载。**假设**鼠标悬浮或键盘聚焦到该胶囊，**则**在胶囊下方显示提示气泡（与场景 8 的两个操作按钮同一套提示，见 A-020，不用浏览器原生 `title`），文案为「已安装 v<旧版本>，最新版本 v<新版本>」，把胶囊里「哪段是已安装、哪段是最新」用文字说清。判据与行内「更新」按钮一致（见场景 14）。
11. **假设**插件已安装且已是最新，**当**渲染版本信息时，**则**仅显示当前已安装版本胶囊 `v<版本>`（单个灰胶囊）。
12. **假设**用户点击未安装插件的「安装」，**当**安装作用域对话框打开时，**则**提供三种作用域，每档的标题与说明文案逐字为：user——标题「用户」、说明「作为你的用户配置，所有项目可用」；project——标题「项目共享」、说明「写入当前项目配置，项目的其他协作者共享使用」；local——标题「项目本地」、说明「仅在设备本地仓库配置，当前项目可用，不影响其他项目」（三档标题与说明均为**定稿文案**，不得简写或改写其中任一句，例如 user 档不得写成「作为用户配置，所有项目可用」；标题在 UI 上追加作用域标识显示，如「用户（user）」），默认选中 user，确认后按所选作用域安装最新版本并记录安装作用域；**当**锚点工程可用（见 A-018），**则** project 与 local 两档在说明文案之外**必须标明配置将写进哪个工程**，写法为**一行**「当前项目：工程名（工程根目录）」（例：`当前项目：wave-agent（/Users/me/code/wave-agent）`，行首标签逐字为「当前项目：」，工程名取工程根目录的末级目录名，括号用全角）：只写「项目配置」「本地仓库」用户无法确认是哪个仓库，工程重名时靠路径区分；user 档**不显示**任何工程名（它不属于任何工程）；**假设**锚点为空（还没有任何工程上下文），**则** project 与 local 两档置灰不可选并说明需先选择项目目录，只有 user 档可选。
13. **假设**用户点击已安装插件的作用域按钮，**当**对话框打开时，**则**标题为「更换安装作用域」、预选当前作用域，并提供「保存」与「卸载」（三档作用域选项的标题与说明文案与场景 12 完全一致，不因是「更换」而换一套说法，**project / local 两档同样给出「当前项目：工程名（工程根目录）」一行、且与锚点同一个工程**）：保存后该插件仅在新作用域启用（旧作用域的启用记录被清除）；**「卸载」作用于该弹窗中所选的作用域**——只清除该作用域的启用记录与该作用域的安装记录，该插件在其它作用域的安装与启用保持不变（同一插件在一个项目以项目作用域、在另一个项目以用户作用域安装时，任一侧卸载都不影响另一侧；见 A-015）。
14. **假设**插件已安装且存在新版本，**当**用户点击「更新」时，**则**该插件升级至最新版本，安装作用域保持不变。
15. **假设**插件市场视图已打开，**当**查看筛选行时，**则**该行从左到右依次是筛选分段器（全部 / 已安装 / 未安装，各带计数）、紧挨分段器右侧的「更新」按钮（文字 + 可更新数量，语义见下）与贴在该行最右端的关键词搜索框（限定当前市场内）；**当**用户点击「更新」，**则**先弹出确认弹窗（清单与语义见场景 21），确认后才将**当前市场**中已安装且存在新版本的插件升级至最新（判据与行内「更新」一致）——按钮的统计与作用范围只跟当前选中的市场走（切换市场后数量随之变化），也不受该市场的筛选与搜索影响；**假设**当前市场没有可更新的插件，**则**该「更新」按钮不展示。该动作按当前清单把待更新插件升级（判据与行内「更新」一致），**自身不拉取检出**——清单的新鲜度由打开该视图时的后台刷新承担（见「市场清单自动刷新与插件升级解耦」A-013）。
16. **假设**插件市场视图已打开，**当**查看市场切换行右侧，**则**只有「添加插件市场」（加号图标）与「管理插件市场」（齿轮图标）两个入口——都不随当前市场变化，按钮内不出现可见文字，语义由 `aria-label` 与**悬浮提示**承载（不用浏览器原生 `title`）：**当**鼠标悬浮或键盘聚焦到这两个图标按钮，**则**在按钮下方显示提示气泡，文案分别为「添加插件市场」「管理插件市场」，鼠标移开或失焦即消失，气泡因位置固定而在贴视口边缘时自动内收、不被滚动容器裁切（「更新」不在此行，见场景 15）；**当**用户点击「管理插件市场」，**则**打开标题为「管理插件市场」的弹窗，弹窗内列出全部已注册市场（行文案即市场自身名称）——内置官方市场行标「官方」且不提供「移除」，各自定义市场提供「移除」；**假设**已注册市场较多导致列表超出弹窗可视高度，**则**只有市场列表区自身滚动，弹窗标题行与底部「添加插件市场」按钮的位置保持固定（不随列表滚出视野）；**当**用户点击「添加插件市场」，**则**直接打开同名对话框（标题与入口同名，见场景 18；与弹窗内的同名入口等价）。
17. **假设**用户在「管理插件市场」弹窗中点击某个自定义市场的「移除」，**当**确认框出现时，**则**提示将移除该市场及其全部插件，其中的副文本为「该市场下的 N 个已安装插件将一并移除。」（N = 该市场下**已安装**的插件数，与列表里「已安装」筛选的判据一致）；**假设**该市场下没有已安装插件，**则**不显示该副文本（确认框只保留标题与按钮）；确认后该市场与其插件一并移除、自动切换到剩余市场；**假设**已被移除的是当前选中的市场，**则**切换后选中项为宿主下发顺序里的第一个市场（市场列表只剩内置官方市场时即选中官方市场，其插件照常展示）；**假设**市场列表已空，**则**插件区提示无插件市场并引导从「添加插件市场」入口添加。
18. **假设**用户点击市场切换行右侧的「添加插件市场」图标，或在「管理插件市场」弹窗内点击「添加插件市场」，**当**对话框打开时，**则**弹窗标题与入口同名（同为「添加插件市场」，不使用「新建市场」这类别名——入口与弹窗共用一个名字，用户不用做名称映射），并提供「本地路径 / 远程仓库」两个来源类型（默认本地路径）与各自的填写方式：本地路径通过「选择文件夹」打开系统目录选择器，选定文件夹后即添加为市场；远程仓库填写 GitHub `owner/repo`（如 `netease/wave-plugins`）或完整 Git 地址后点击「添加」，且不提供文件夹选取。市场名称不由用户填写（见 A-011）；**假设**该来源此前已添加过，或该市场的名称与已有市场同名，**则**不允许添加并提示原因（来源重复时提示已存在的市场名）；**假设**添加成功，**则**新市场出现在市场切换行并自动切换为当前市场（列表与筛选随新市场刷新），**假设**切换区放不下、这个新切换项落在可视区之外，**则**切换条自动横向滚动使其完整进入可视区（只动切换条自身的横向滚动位置，不改变任何切换项的相对顺序、也不滚动设置页本身）。
19. **假设**用户在插件市场视图中执行安装 / 卸载 / 更新 / 更换作用域或批量更新并成功，**当**变更落盘时，**则**不重建任何会话、不弹重建确认框——变更只产生一个「待应用」信号；桌面端把它呈现为**强引导确认框**（标题「插件已变更，重新加载后生效」，按钮「稍后」/「立即重新加载」，点「立即重新加载」回发既有 `reloadPlugins` 命令），不再是会被忽略的一句轻提示；VS Code / JetBrains 沿用应用级提示提醒运行 `/reload-plugins`（就地重载的完整语义见下文「插件变更的就地重载」故事，逐字文案见「插件变更提示」）。
20. **假设**任一插件或市场操作失败（如网络错误），**当**失败发生时，**则**通过宿主提示告知失败原因；**假设**用户在插件市场内执行新建市场 / 移除市场 / 安装插件 / 卸载插件 / 更新插件 / 更换安装作用域 / 批量更新插件中的任一操作且成功，**当**操作完成时，**则**由宿主按下文「插件市场操作提示」的逐字文案给出一句结果提示。成功反馈与失败提示走同一渠道（桌面端应用级 toast、VS Code 原生通知、JetBrains 通知气泡），并随操作本身的收尾一起发生。
21. **假设**某插件已安装，**当**渲染该行时，**则**作用域按钮的文案维持现状（在**当前**工程的作用域：用户 / 项目 / 本地；未在当前工程启用时为「未知」——行内文案本轮不变），但**鼠标悬浮或键盘聚焦该按钮时在其下方显示提示气泡**（与场景 16 的两个图标按钮同一套悬浮提示，不用浏览器原生 `title`；按钮的无障碍名仍为「更换安装作用域」）：气泡**只说明该插件在当前锚点工程（A-018）的归属**，内容只有一行「工程名（工程根目录）」（例：`wave-agent（/Users/me/code/wave-agent）`，工程名取工程根目录的末级目录名，括号用全角），不加「所属项目」这类行首标签、也不列出在别的工程里的启用记录——气泡与行内胶囊看的是同一个工程，用户不必在两个工程视角之间做换算；**假设**该插件只以用户作用域安装（不属于任何工程），**则**气泡内容为「用户级安装（所有项目可用）」；**假设**宿主没有锚点工程（A-018 的空锚点情形），**则**气泡不给出工程名、只说明「所属工程未知」（此时 project / local 两档也不可选，见场景 12）。
22. **假设**插件市场视图已打开且**当前市场**存在可更新插件，**当**用户点击筛选行里的「更新」按钮（场景 15），**则**必须**先弹出确认弹窗、确认后才下发更新请求**：弹窗标题说明即将更新插件，正文注明作用范围是「**<当前市场名> 市场**下」并可汇总「共 N 个插件可更新」（市场名即市场自身清单里的名字，不由用户输入，见 A-011），再逐行列出该市场中可更新的插件——每行包含插件名称、版本变化（已安装 `v旧` → 最新 `v新`）与安装作用域（`user` / `project` / `local`，缺失时显示「未知」）；**当**用户点「更新」确认，**则**关闭弹窗并执行一次**该市场**的更新（带上该市场名下发，见场景 15），**当**用户点「取消」或按 `Esc`，**则**关闭弹窗且不产生任何更新请求；**假设**待更新插件较多导致清单超出弹窗可视高度，**则**清单区自身滚动，标题、汇总行与按钮位置保持固定；清单内容取弹窗打开时刻的插件快照（弹窗打开期间宿主刷新插件列表不改变已展示内容，也不改变确认后的下发语义）；插件行内的单插件「更新」不弹此确认框（场景 14 语义不变）。
23. **假设**某插件在项目 A 以项目作用域（或本地作用域）安装，**当**我在另一个项目 B 打开插件市场时，**则**该插件在 B 中显示为未安装（显示「安装」操作、计入「未安装」筛选、不展示作用域标签），不得出现「已安装 + 作用域未知」的组合；**假设**该插件以用户作用域安装，**则**它在任意项目中都显示为已安装且作用域为「用户」；以项目作用域安装的插件在 A 中显示为已安装且作用域为「项目」（安装状态与作用域标签的判据一致，见 A-012）。

### 插件市场操作提示（文案口径：2026-09-16 原型）

GUI 三端（桌面端 / VS Code / JetBrains）在插件市场内完成一次操作后，由**宿主**在操作结果确定时按下列逐字文案给出一句结果提示（仍在 `try/catch` 的成功分支发出，不以 webview 乐观时序为准）；三端各自的通道：桌面端 `showToast`、VS Code `showInformationMessage`、JetBrains `IdeService.showInfo`。

**成功文案（逐字，成功分支专用）**：

| 操作                   | 文案                                    |
| ---------------------- | --------------------------------------- |
| 新建市场               | `已添加市场「<市场名>」`                |
| 移除市场               | `已移除市场「<市场名>」`                |
| 安装插件               | `已安装「<插件名>」（作用域：<scope>）` |
| 卸载插件               | `已卸载「<插件名>」`                    |
| 更新插件               | `已更新「<插件名>」至 v<版本>`          |
| 更换安装作用域         | `已更新「<插件名>」的作用域：<scope>`   |
| 批量更新插件（有升级） | `「<市场名>」已更新 <N> 个插件`         |
| 批量更新插件（无升级） | `「<市场名>」已是最新`                  |

其中 `<scope>` 取作用域键原值（`user` / `project` / `local`，不做本地化翻译），`<版本>` 为升级后的版本号。

* **失败文案不变**：沿用本故事场景 20 的失败提示（经宿主提示告知失败原因）——新建市场来源重复/同名等由 SDK 直接抛出原因文本，宿主透传。
* **不在范围**：插件启用 / 禁用（原型未定义提示）、打开插件市场的 `/plugin` 提示（原型为聊天输入框的演示交互）与市场清单刷新的「检查更新中…」界面内状态（见「市场清单自动刷新与插件升级解耦」场景 12）。

### 用户故事：市场清单自动刷新与插件升级解耦（优先级：P1）

作为用户，我希望每当我打开插件市场界面（GUI 设置页的插件市场视图，或 CLI 插件管理器的市场列表）时，各市场的清单都被后台刷新到最新（由此得知有哪些插件有了新版本），而插件的实际升级必须由我主动触发——行内「更新」逐个升级，或点「批量更新插件」把该市场内所有可更新插件一次升掉；我不打开这些界面时（宿主启动、对话初始化、新建或恢复会话都算）不发生任何清单刷新，以便我对「新版本何时生效」保持控制，也不必为「要不要开自动刷新」「什么时候刷」做配置。

**为什么是这个优先级**：当前开启自动更新的市场把「拉清单」与「重装已安装插件」一步做完（`autoUpdateAll` 以 `updatePlugins: true` 调用市场更新），导致「已装版本 < 最新版本」这一状态在正常流程中无法稳定存在——本规格「插件市场」场景 8 / 10 / 12 描述的版本对比与「更新」按钮因此拿不到可达状态（git/github 来源的市场尤其如此：唯一的清单刷新入口会把插件一并升掉）。解耦后自动刷新只负责把清单带到最新，升级动作全部显式化，行内「更新」与「批量更新插件」批量升级两条路径才真正可用。

批量升级的「先刷新检出」也随之取消：既然清单的新鲜度已由「打开界面时的那次刷新」保证，升级动作再拉一次检出就是同一份清单拉两遍——它既让行内「更新」与「批量更新插件」的语义不一致（前者从不拉），也在用户连点该按钮时变成同一检出上的并发 `git pull`（必然失败）而报错。取消后「批量更新插件」与行内「更新」同口径：只按当前清单重装。由此带来的可预期后果是**清单的时效边界就在打开界面那一刻**：界面长时间停留期间上游若又发版，需重开视图（或退出重进 CLI 插件管理器）才会看到新版本——这与 apt 的「`update` 与 `upgrade` 分离、`upgrade` 只按上次 `update` 的索引执行」是同一种取舍；唯一保留「刷新 + 升级」一体的是无界面可依赖的非交互命令 `wave plugin marketplace update`（见 A-013），它同时也保留了 brew 式「升级前先更新索引」的便利。

刷新时机也从「宿主启动／Agent 初始化」改到「打开插件市场界面」：升级既然只能从这些界面发起（口子只有插件行的「更新」与市场级的「批量更新插件」／`wave plugin marketplace update`），启动时的那次刷新就只是把清单提前拉一次——结果不推送给任何人、之后也不升级，用户在界面里看不到任何差别，却白付一次网络与文件锁开销（刷新会全程持有 `~/.wave/plugins/.lock`，期间用户发起的安装/更新要排队）。挪到打开界面时刷新，既保证列表呈现的是最新清单（「更新」按钮可达），又不做用户没要求的取网络动作；GUI 设置页视图与 CLI 插件管理器（`/plugin`）照此一致处理，因此两侧都有「只刷清单、不装插件」的通道，与「升级必须显式触发」不再冲突。同时把「市场是否自动刷新」「何时自动刷新」从用户配置项收敛为固定行为（不再有开关，见 A-014）：既然它已不再改动已装插件，就不需要用户为它做选择。

**独立测试**：给某市场发布一个比已装版本更新的插件版本 → 重启宿主并新建会话，确认各市场检出目录未被触碰（无 `git pull`／clone、检出目录内容与 `installed_plugins.json` 均未变）→ 打开插件市场视图（桌面端：侧边栏「插件市场」；VSCE / JetBrains：设置 → 插件市场），确认无需任何额外操作，该行即从进入视图时的旧状态自动变为「已安装 vX · 最新 vY」且出现「更新」按钮，同时 `installed_plugins.json` 中该插件的版本与插件缓存目录仍未变、插件行为未变 → 在 CLI 中打开 `/plugin` 插件管理器，确认同样无需额外操作即呈现刷新后的清单、且未升级任何插件 → 点击行内「更新」后该插件升级到 vY 且安装作用域不变；另起一次只点「批量更新插件」，确认该市场内所有可更新插件被一次性升级、提示升级数量，且该动作自身未拉取检出（全程无 `git pull`）；连续快速点击该按钮两次，确认两次操作串行完成、均不报错。

**验收场景**：

1. **假设**某市场的上游发布了比已装版本更新的插件版本，**当**用户打开插件市场界面（GUI 设置页视图或 CLI 插件管理器）触发后台刷新时，**则**只刷新该市场的检出内容（`git pull`／官方市场镜像 zip 快照，必要时 `clone`），**不得**重装或改动任何已安装插件——`installed_plugins.json` 中对应插件的版本与缓存目录保持不变。
2. **假设**打开界面触发的后台刷新刚刷新了清单、某插件已装版本低于清单版本，**当**该界面仍处于打开状态时，**则**该行自动变为显示「已安装 vX · 最新 vY」并出现「更新」按钮（即「插件市场」场景 10 与场景 8 的按钮态在此流程下可达），用户不需要关闭重开界面、也不需要手动刷新——这是「打开界面」这一动作自身的结果，不属于场景 8 所禁止的主动提示。
3. **假设**用户点击该行的「更新」，**当**操作完成时，**则**该插件按当前市场检出内容升级至最新、安装作用域保持不变（等同「插件市场」场景 14）；该动作不刷新市场清单。
4. **假设**用户点击当前市场的「批量更新插件」，**当**操作完成时，**则**按当前清单把该市场内所有已安装且存在新版本的插件一次性升级（「插件市场」场景 15 语义不变），宿主提示`「<市场名>」已更新 <N> 个插件`；无可更新插件时提示「<市场名>」已是最新（GUI 三端逐字文案见「插件市场」故事的「插件市场操作提示」）。该动作的范围恒为当前市场，不做跨市场批量升级；**它自身不拉取检出**——「先刷新清单」这一步已由打开界面时的后台刷新承担（A-013），重复拉取既无必要、也是并发点击时两次 `git pull` 撞同一检出而报错的根源。
5. **假设**用户打开插件市场界面且存在已注册市场，**当**界面加载触发自动刷新时，**则**所有市场一律在后台执行清单刷新（与列表渲染并行、不阻塞界面、不要求用户等待），不区分市场、也不存在「某个市场已关闭自动刷新因而不刷新」的状态：本地路径来源的市场为无操作（清单本就在本地），远程仓库来源的市场执行 `git pull`／镜像获取。相应地，宿主启动、对话（Agent）初始化、新建会话与恢复会话都**不**触发任何清单刷新。
6. **假设**内置官方市场 `wave-plugins-official`，**当**用户打开插件市场界面触发自动刷新时，**则**与其它市场同样执行清单刷新（镜像 zip 快照通道，失败按既有语义回退 git 或跳过本次更新），已安装插件不被重装。
7. **假设**某市场的检出目录尚不存在（从未 clone），**当**打开界面触发自动刷新时，**则**仍按既有语义完成 clone 使该市场可用；此行为不因本次解耦而改变。刷新完成前该市场可能先呈现为清单不可用／无插件，刷新完成后按场景 2 的机制自行呈现最新内容。
8. **假设**用户未打开插件市场界面，**当**会话正常进行时，**则**既不产生任何「有 N 个插件可更新」的主动提示（不做角标、不做 toast），也不发生清单刷新；更新完全由用户进入插件市场界面后自行发现并触发。
9. **假设**打开界面触发的后台刷新失败（网络不可达、镜像失败且 git 不可用等），**当**失败发生时，**则**按「插件市场」场景 20 的既有处理后台静默记录日志、不打断会话与插件加载，界面仍以刷新前的清单正常展示，且不得回滚、清空或重装已安装插件。
10. **假设**存量用户配置中存在 `marketplaces.<name>.autoUpdate`（含显式 `false`），**当**本规格生效后，**则**该取值不再影响行为——市场仍在打开插件市场界面时按场景 5 固定执行清单刷新；同时所有切换入口都移除：GUI 三端设置页不提供该开关，CLI 交互式插件管理器（`/plugin` → 市场详情）的 `Enable / Disable auto-update` 项一并移除，也不新增任何非交互式子命令或环境变量作为替代入口。
11. **假设**用户在 CLI 中打开 `/plugin` 插件管理器，**当**市场列表加载时，**则**与 GUI 同样在后台刷新各市场检出、不升级任何插件，并在刷新完成后刷新列表呈现最新清单（无需用户操作）；此时 CLI 的插件升级入口仍只有市场详情的「批量更新插件」项（按当前清单批量升级该市场，不拉取检出）与单个插件的「Update plugin (reinstall)」，二者都是显式动作、且都不刷新检出。
12. **假设**打开界面触发的后台刷新尚未完成，**当**用户仍停留在该界面时，**则**界面内以轻量状态提示正在检查更新（GUI 为列表区/工具栏文案，CLI 为进行中提示），且不阻塞列表浏览与操作、不弹窗；该提示属界面内状态，不构成场景 8 所禁止的界面外主动提示。
13. **假设**同一次刷新仍在进行中，**当**用户重复打开该界面或界面重新挂载时，**则**复用进行中的那次刷新、不发起第二次市场拉取；单次刷新只使用一把进程内单飞（后续调用返回同一个进行中的 promise），刷新完成后每个打开的界面各自呈现最新清单。
14. **假设**同一市场的操作被连续多次触发（如连点「批量更新插件」、或在刷新尚未结束时发起该市场的安装 / 卸载 / 更新），**当**这些调用并发到达时，**则**它们在同一个进程内**串行执行**——后到的调用排队等待当前操作结束、不得跳过锁并行运行，因此不会出现在同一个市场检出或同一插件缓存上并发读写（并发两次 `git pull` 同一检出必然失败）的情况，也不会因此报错；每次触发的动作各自完成并按场景 13 给出自己的结果提示（排队执行，不复用进行中调用的结果）。

### 用户故事：插件变更的就地重载（优先级：P1）

作为正在对话中的用户，我希望插件变更由我主动触发一次「重载」就在**当前对话里**生效——不重启对话、不打断正在生成的回合，以便我在「插件何时生效」这件事上既能立刻拿到结果，又能完全掌控承担成本（一次提示词缓存失效）的时机。

**为什么是这个优先级**：现状下插件变更只能通过「销毁并重建 Agent」生效（见 [agent-config.md](../core/agent-config.md)「配置变更的构造期副作用与重建」），而重建会掐断正在生成的回合、清空排队消息，因此必须弹确认框让用户挑时机，并留下「稍后重启」这个悬空状态——选了之后既有对话永久停留在旧配置，用户只能自己新开对话。就地重载把「变更落盘」与「变更在会话内生效」解耦：落盘只产生一个待应用信号，真正换装由用户敲一次命令触发，在同一个会话内完成（会话 id 不变、消息与转录连续、正在生成的回合不被打断）。

**独立测试**：在已有多轮对话的会话里启停一个同时提供斜杠命令、技能、子代理与钩子的插件 → 确认变更不自动生效、桌面端弹出强引导确认框（其余宿主出现一次性提示）→ 点「立即重新加载」（或敲 `/reload-plugins`）→ 同一会话（会话 id 不变）内立即能使用该插件的命令 / 技能 / 子代理、钩子生效且转录连续 → 再卸载该插件并重载一次 → 确认该插件贡献的全部能力消失、会话仍可正常继续；另测一遍点「稍后」：弹窗关掉、什么都没发生，随后手动敲 `/reload-plugins` 才生效。

**验收场景**：

1. **假设**任一插件变更（安装 / 卸载 / 启用 / 禁用 / 更新 / 更换作用域、内置插件开关、批量更新）落盘，**当**变更完成时，**则**不得自动重建任何会话、不得弹重建确认框；只置一个会话内的「插件已变更、待应用」信号，并按下文「插件变更提示」呈现给用户——桌面端是一次强引导确认框（「稍后」/「立即重新加载」），其余宿主是一句一次性提示。
2. **假设**用户敲 `/reload-plugins`，**当**重载执行，**则**当前会话**就地**换装磁盘上已落盘的插件状态——不销毁、不重建 Agent；会话 id 不变、消息与转录连续、不新建会话、不清空排队消息。
3. **假设**重载时磁盘上的插件状态相对当前会话已装载的状态含七类能力的增删（斜杠命令、技能、子代理、钩子、MCP 服务器、LSP 服务器、工作流），**当**重载完成，**则**七类全部指向最新状态：新增的立即可用，被禁用 / 卸载 / 更新移出的**不再可用**（反注册与新增同等重要）；某类无变化时该类现行能力保持可用。工作流的注册名形制与注册表接线见 [workflow.md](../multi-agent/workflow.md)「调用插件携带的工作流」。
4. **假设**目标会话正在生成回复（含正在流式输出、有待确认权限、有运行中后台任务的情形），**当**用户敲 `/reload-plugins`，**则**重载照常执行且**不打断**该回合——与重建方案相反，此处不需要让用户选择时机、也不得因此跳过或延后重载。
5. **假设**重载后会话发出下一次模型请求，**当**该请求携带更新后的工具集时，**则**此前累积的提示词前缀缓存失效并在该轮重建——**这是刻意接受的代价**：它只发生在用户主动敲命令的那一次，不由系统在任何插件变更后自动触发，也不因此改变「不自动重载」的语义。
6. **假设**磁盘上的插件状态与当前会话已装载的状态一致（无任何变更），**当**用户敲 `/reload-plugins`，**则**重载幂等：不报错、不改变会话内任何能力、不打断任何回合，仍给出重载完成的提示。
7. **假设**一次重载完成，**当**用户继续对话，**则**「待应用」信号已清除、提示不再出现；**假设**此后又发生新的插件变更，**则**信号重新置位并按场景 1 再次提示（一次变更一次提示，不累计、不做持久标识）。
8. **假设**用户在当前进程内有多个 live 会话，**当**任一会话触发重载，**则**该进程内的全部 live 会话各自就地换装（共用同一份磁盘状态），不做「只重载当前会话」的区分。
9. **假设**用户未敲 `/reload-plugins`，**当**此时新开对话，**则**新会话直接按磁盘当前的插件状态装载（无需敲命令）；既有对话继续使用各自启动时的状态，直到被重载。
10. **假设**用户在待应用状态下修改了 `enabledPlugins` 启用记录（而非插件内容），**当**重载执行，**则**按修改后的启用记录换装——重载读的是磁盘当前的插件状态，不只是插件内容。
11. **假设**重载过程中某类能力的换装失败（如 MCP 重连失败、某插件清单解析失败），**当**重载结束，**则**如实提示失败原因；不得因单点失败回滚已经成功的其它类、不得使会话不可用（后续对话仍可正常进行），也不得留下「部分能力指向已失效路径」的状态。
12. **假设**某插件本次变更涉及缓存目录替换（更新会换用新的版本目录），**当**重载完成，**则**该插件贡献的各类能力必须指向新的版本目录，不得出现「重载后仍指向本次已被移除的旧目录」。
13. **假设** CLI TUI、桌面端、VS Code 扩展、JetBrains 插件中的任一端，**当**用户需要触发重载时，**则**该端提供同一入口（斜杠命令 `/reload-plugins`）；该命令不进入对话、不触发模型回复，只执行重载并给出提示。
14. **假设**桌面端刚完成一次插件变更（安装 / 卸载 / 启用 / 禁用 / 更新 / 更换作用域 / 批量更新 / 内置开关），**当**变更落盘时，**则**弹出一个强引导确认框（标题「插件已变更，重新加载后生效」，按钮「稍后」/「立即重新加载」），同一时刻不重建任何会话、不自动重载；**当**用户点「立即重新加载」，**则**按场景 2 就地换装并给出「插件已重载。」；**当**用户点「稍后」（或 `Esc` / 右上关闭），**则**只关掉弹窗、不产生任何重载请求，之后仍可在对话框里敲 `/reload-plugins` 手动补上；**假设**弹窗还开着又来一次插件变更（连续装卸两个插件），**则**不叠出第二个弹窗（同一次「待应用」状态只问一次）；**假设**用户在弹窗里点「立即重新加载」时目标会话正在生成，**则**照常重载、不打断该回合（与场景 4 一致）。

### 插件变更提示（文案口径：2026-09-18 拍板，2026-09-23 修订）

**逐字文案**：

| 时机                         | 文案                                                                |
| ---------------------------- | ------------------------------------------------------------------- |
| 变更落盘、待应用（桌面端）   | 弹窗标题 `插件已变更，重新加载后生效`；按钮 `稍后` / `立即重新加载` |
| 变更落盘、待应用（其余宿主） | `插件已变更。运行 /reload-plugins 使其生效。`                       |
| 重载完成（各端）             | `插件已重载。`                                                      |

**形态与通道**：

* **桌面端是强引导确认框，不是轻提示**（2026-09-23 修订，起因：桌面端原先只弹一句顶部 toast，用户会直接忽略，插件装完没人运行 `/reload-plugins`，插件一直不生效）：变更落盘后由 webview 弹一个居中确认框，正文说明「重新加载会把磁盘上最新的插件状态应用到当前对话，新插件的命令、技能与子代理随后即可使用；正在生成的回复不会被打断」；点「立即重新加载」回发既有的 `reloadPlugins` 命令（与 `/reload-plugins` 同一条就地重载路径），点「稍后」只关掉弹窗、不做任何重载（之后仍可手动敲命令）。宿主只发一条 `pluginChangePending` 信号，弹窗与按钮在 webview 里（三端共用同一套 UI 的成本不该由一条提示承担，桌面端先行）。
* **VS Code / JetBrains / CLI 维持各端既有的一次性提示通道**（VS Code `showInformationMessage`、JetBrains `IdeService.showInfo`、CLI 为 TUI 内提示）——它们的插件入口在设置页 / TUI，强引导弹窗留待各端 UI 有承载位时再对齐。
* **一次性事件，不提供持久可见标识**——不新增状态条 / 角标 / 列表标记，也不提供「查看哪些对话待应用」的入口（与 [desktop-settings.md](../desktop/desktop-settings.md) 既有的「不提供持久可见标识」拍板一致）；同一次落盘只弹一次，重复信号不叠成两个弹窗。
* 各端文案都是**中性**的：弹窗按钮不是危险动作（红色只留给删除 / 移除这类不可逆操作），toast 形态不占用成功 / 失败语义色（语义三色只用于设置页结果型提示，见 desktop-settings.md「toast 形态与路由」）。
* **不新增界面内进行态**：不做「正在重载中…」的常驻状态（重载是用户主动触发、结果即时可见）。

**两条边界的定论（2026-09-18 拍板）**：

* **重建路径完全废弃**：「销毁并重建 Agent」不再是插件变更的生效方式。随本故事生效，[agent-config.md](../core/agent-config.md)「配置变更不再需要重建会话」故事的构造期副作用清单为空、场景 3–4 明确禁止重建，[desktop-settings.md](../desktop/desktop-settings.md) 中对应的重建确认框条款（「立即重启 / 稍后重启」、`Esc` 等同「稍后重启」、N/M 文案）与 [builtin-sdd-plugin.md](../commands/builtin-sdd-plugin.md) 场景 2 的同一措辞一并废止；插件变更一律走「待应用信号 + 手动敲命令」，不保留重建作为兜底（就地重载的净效果严格包含重建，且不中断回合）。
* **跨进程变更不置位信号**：「待应用」信号只覆盖宿主**同进程**可感知的变更（GUI 三端设置页、CLI TUI 的 `/plugin`、宿主经 RPC 发起的插件变更）。在另一个终端执行非交互命令（如 `wave plugin install`）导致的落盘不会产生提示——`/reload-plugins` 命令本身无状态、随时可用，用户知道发生过变更即可敲命令应用；完整覆盖需对 `~/.wave/plugins` 做文件监视，不在本期范围。

## 假设

* **A-001**：已安装的插件如果未在任何 `enabledPlugins` 配置中明确提及，则默认**禁用**。
* **A-002**：`settings.json` 中的 `enabledPlugins` 设置优先于插件在缓存中的单纯存在。
* **A-003**：用户必须使用 `name@marketplace` 格式来唯一标识插件以进行作用域管理。
* **A-004**：底层插件安装逻辑由 SDK 服务通过 `PluginCore` 高级 API 处理。
* **A-005**："项目作用域"安装涉及修改通常提交到版本控制的文件（如 `.wave/settings.json`）。
* **A-006**：系统应安装 `git` 以使用 GitHub 或基于 Git 的市场。
* **A-007**：本地市场存储在与 `wave` 安装相同的文件系统上。
* **A-008**：除内置官方市场 `wave-plugins-official` 的镜像 zip 快照通道（见「官方插件市场镜像 zip 快照获取与更新」用户故事）外，所有远程获取使用 `git clone`——没有直接的 HTTP 下载插件文件机制。官方市场镜像失败或未配置时同样回退 git clone。
* **A-009**：GitHub 简写（`owner/repo`）自动解析为 `https://github.com/owner/repo.git`。
* **A-010**：插件市场视图展示的「最新版本」取自市场检出目录内该插件的插件清单（`.wave-plugin/plugin.json`，兼容 `.claude-plugin/plugin.json`）；插件来源为独立 Git 仓库等无法就地读取清单的情况，不展示最新版本，仅展示已安装版本（对象形态的 `url` 与 `git-subdir` 都指向市场之外的外部仓库、市场检出目录内没有其副本，因此与既有 Git URL 来源同属「不可就地读取」，见 A-021）。
* **A-011**：市场名称不由用户填写，由该市场自身的清单（`marketplace.json` 的 `name`）决定——本地路径来源同样读取所选文件夹内的该清单。该名称同时是插件 `name@marketplace` 标识、配置键与判重的依据，也是 UI 中的展示文案（市场切换项与「管理插件市场」列表行都直接显示它，见场景 5、16）；已注册市场的名称不因展示层改动而变化。同一来源重复添加，或名称与已有市场同名，都不允许添加（判重同时看作用域配置与市场缓存注册表，对 GUI 与 CLI 两条入口一致）。
* **A-012**：插件在某个工作目录中是否「已安装」，需两个条件同时成立——该目录的配置链（`local` > `project` > `user`）中存在该插件的启用记录，且本机已有该插件的安装产物（`~/.wave/plugins` 下的注册表条目与缓存）。安装产物单独存在只代表该插件曾被下载过，不构成任何目录中的「已安装」（与 A-002 一致）：以项目/本地作用域安装的插件在其它项目中是「未安装」，以用户作用域安装的插件在所有目录都是「已安装」。安装状态与作用域标签必须同源判断，设置页插件市场与 CLI `wave plugin list` 一致，不存在「已安装但作用域未知」的展示。
* **A-013**：市场清单的自动刷新只表示「刷新市场检出内容」（远程仓库来源 `git pull`／官方市场镜像 zip 快照获取，本地路径来源为无操作），与「插件升级」解耦——自动刷新不重装已安装插件。它的触发时机固定为**用户打开插件市场界面**（GUI 三端设置页的插件市场视图、CLI 交互式插件管理器的市场列表）：刷新在后台进行、与列表渲染并行，完成后把最新清单呈现给已打开的界面；宿主启动、对话（Agent）初始化、新建会话与恢复会话都不触发。插件升级只有显式入口，且**都按当前清单进行、自身不拉取检出**：插件行的「更新」（单个）与市场级的「批量更新插件」（该市场内所有可更新插件；CLI 交互式插件管理器的同名项同语义）——两者的清单新鲜度都由「打开界面时的那次刷新」保证。唯一的例外是非交互命令 `wave plugin marketplace update [name]`（不带名字时作用于全部市场）：它没有「打开界面」这一步可依赖，因此**保持「先刷新检出、再升级该市场插件」**，同时是 CLI-only 用户唯一的非交互刷新入口与唯一的非交互批量升级入口。
* **A-014**：市场清单的自动刷新是**固定行为**，不提供开关——GUI 三端设置页没有该开关，CLI 交互式插件管理器（`/plugin` → 市场详情）的 `Enable / Disable auto-update` 项移除，也不以非交互式子命令或环境变量作为替代入口。存量配置里 `marketplaces.<name>.autoUpdate` 的取值（含显式 `false`）不再影响任何行为，市场一律按 A-013 在打开插件市场界面时刷新清单（同样没有「何时刷新」的配置项）。
* **A-015**：插件的安装产物按**作用域**分别记账，各作用域相互独立：`~/.wave/plugins/installed_plugins.json` 中同一插件（`name@marketplace`）可存在多条安装记录，每条记录标明其作用域；项目作用域与本地作用域的记录另带所属项目的路径，用户作用域的记录不带项目路径。卸载因此是**按作用域**的删除操作——只清除指定作用域在配置链中那一层的启用记录与同作用域的安装记录，不触碰其它作用域（含其它项目的项目/本地作用域）；指定作用域没有该插件的安装记录时操作失败并提示该插件实际的安装作用域（项目/本地作用域另附所属仓库路径），不做静默全清。物理缓存目录由同一插件的各条安装记录共享（按市场/插件/版本寻址，与作用域无关），仅在该插件再无任何安装记录时才删除；仅剩用户作用域记录而卸载项目作用域这类常见情形下，缓存必须保留。GUI 三端的卸载入口把弹窗中所选作用域传给 SDK；CLI 的 `wave plugin uninstall <plugin>` 接受 `--scope`，缺省时按 `local` > `project` > `user` 探测当前生效作用域（与 `enable`/`disable` 的既有回落一致），CLI 交互式插件管理器的卸载沿用同一探测。更换作用域（「插件市场」场景 13 的「保存」）沿用「先在旧作用域清除、再在新作用域启用」的既有语义，不等同于卸载。
* **A-016**：插件市场整页（侧边栏入口形态）为桌面端独有——依赖首页左侧边栏，VSCE / JetBrains 不提供；两处宿主进入的是同一套市场与插件视图，能力与语义不得分叉。入口按宿主分工，**同一宿主只保留一个入口**：桌面端只有侧边栏整页一个（设置页左侧导航不出现「插件市场」项、`/plugin` 也落到整页），VSCE / JetBrains 没有侧边栏整页、沿用设置页导航项（`/plugin` 落到该视图）。
* **A-017**：插件行的作用域呈现与气泡都只看**锚点工程**（A-018）——气泡给出的是「当前这个工程」的工程名与根目录，不枚举该插件在别的工程里的启用记录。宿主因此**不**需要收集「最近 / 已打开的其它工程」作为候选，也**不**在文件系统里全局搜索 `.wave/settings.json`（那是为「跨工程列出归属」准备的，本轮不再做）。归属判定只看工程配置里的 `enabledPlugins`（`<工程>/.wave/settings.json` 与 `<工程>/.wave/settings.local.json`，即 `project` / `local` 作用域），用户级（`~/.wave/settings.json`）安装不产生工程归属。
* **A-018**：插件视图的**锚点**是「当前工程」——桌面端为当前选中对话所属工程（即该对话的工作目录），插件端为当前工作区文件夹。桌面端尚无选中对话时（刚新建、还没发过消息），锚点即该对话已绑定的工作目录（启动时按「最近打开」首项初始化的那个工程）；因此「新对话默认从哪个项目开始就以该项目为锚点」是预期行为，与现状一致。锚点在插件市场整页打开期间**不得变化**——切换对话会先关闭整页（场景 2），故插件列表的作用域呈现与随后的写操作（安装 / 更换作用域 / 卸载）用的是同一个锚点；写操作不得读「此刻的当前工程」（可能已被用户切走），而必须沿用打开时的锚点。锚点为空的唯一情形是宿主还没拿到任何工程目录（用户从未选择过目录）：此时 project / local 两档不可选（置灰并说明需先选择项目目录），作用域呈现与气泡不带工程名。作用域呈现本身仍是「当前工程视角」——胶囊只报该插件在锚点工程（或全局）有没有记录，气泡也只说明这个锚点工程的归属（A-017）。
* **A-019**：插件视图只表达两件事——**是否已安装**（判据同 A-012：当前工程配置链里有启用记录且本机有安装产物，安装产物单独存在不构成「已安装」），以及**作用域记录写在哪个文件**（`project` / `local` / `user`，判据是该插件是否出现在对应 `enabledPlugins` 里）。它**不表达**插件的实际启用 / 停用状态（`enabledPlugins` 的值是 `true` 还是 `false` 不改变任何呈现）：用 CLI 停用插件后，界面上的作用域文案与气泡维持原样。本轮不引入启用 / 停用态及其操作入口（CLI 的 `wave plugin enable/disable` 仍是唯一的启用开关）。
* **A-020**：插件视图里**同一层级的信息提示只用一套**——插件行内的提示（升级胶囊、行尾「更新」按钮）与市场切换行、作用域下拉一样走共享的自定义气泡（工具提示层，悬浮或键盘聚焦出现、在锚定元素下方、移开或失焦消失），**不用浏览器原生 `title`**（两者并存会同时弹出系统提示与自定义气泡，风格也不一致——原生提示是系统配色、与视图其余提示不同源）。**没有新增信息的提示一律不给**：按钮/胶囊文字已自明的（如「安装」按钮）不配气泡，「更新 N」按钮的原生 `title` 是既有例外，本轮不动。气泡文案里出现的版本号必须与胶囊、行内按钮同源（同一个插件版本数据），避免同一行两处版本号不一致。
* **A-021**：市场清单条目的 `source` 支持两种形态，字段语义如下：**字符串** = Git URL（`http://` / `https://` / `git@` / `ssh://`，可带 `url#ref`）或市场检出目录内的相对路径（既有语义不变）；**对象** = `{"source":"url","url":…}`（整仓）或 `{"source":"git-subdir","url":…,"path":…}`（仓库子目录），两者均可带 `ref`（分支/标签）与 `sha`（钉版本，存在时优先级高于 `ref`）。解析必须容错到**条目级**：形态无法识别的条目只使自己不可安装（并给出原因），不影响同一市场内其它条目，更不得让整个市场从列表中消失；解析失败不得静默吞掉。对象形态两种取值都指向**市场之外**的外部仓库，市场检出目录内没有其副本，因此「最新版本」判定与既有 Git URL 来源一致（不展示，见 A-010）。
* **A-022**：插件清单的 `version` 是**可选**字段，缺失时按 `1.0.0` 处理（与安装阶段既有的回退一致）；`name` 与 `description` 仍是加载的必要条件。不允许出现「安装阶段接受、加载阶段拒绝」的两套判据。
* **A-023**：插件声明 MCP 服务器有两处等价位置：插件根目录的 `.mcp.json`（兼容**包裹**形态 `{"mcpServers": {…}}` 与**扁平**形态 `{"<serverName>": {…}}`），与插件清单内的 `mcpServers` 字段。两者同时存在时按服务器名合并，同名以 `.mcp.json` 为准。清单内联 `mcpServers` 本期只支持**对象形态**（内联声明），Claude Code 的 string / array 形态（指向配置文件路径）不在本期范围。任何形态都不允许导致整个插件加载失败：单个服务器解析或注册失败只影响该服务器，插件其余能力照常加载。
* **A-024**：一个插件是否受托管限制（成员不可卸载），唯一依据是**托管配置（远端托管设置）的 `enabledPlugins` 中是否存在该插件 id**——`true` 为强制启用、`false` 为强制禁用（连加载都拒绝）。判定只看下发值本身，本机不落任何「已托管」标记，限制随管理员撤掉条目自动消失。托管层的合并顺序是「托管 > 本机 user / project / local」，且 `marketplaces` 与 `enabledPlugins` 按**键**合并——托管层只覆盖它自己声明的那些键，本机独有的插件与市场照常生效（对齐 Claude Code 的 policy 层语义，见 [server-managed-config.md](../enterprise/server-managed-config.md) 场景 5）；托管值不写入本机 `settings.json`（见 [server-managed-config.md](../enterprise/server-managed-config.md)）。
