Merge pull request #120 from IronRookieCoder/personal/plugin-auto-update-config
fix(plugin): make marketplace auto-update configurable by default
This commit is contained in:
commit
99f99b74e8
184
docs/features/plugin-auto-update.md
Normal file
184
docs/features/plugin-auto-update.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Plugin Auto-Update 机制说明
|
||||
|
||||
本文记录当前代码中的 plugin auto-update 机制,包括启动链路、配置来源、UI 配置项、手动更新行为,以及复核时发现的配置注意点。
|
||||
|
||||
## 结论摘要
|
||||
|
||||
Plugin auto-update 是启动后的后台任务,用于刷新开启了 `autoUpdate` 的 marketplaces,并把这些 marketplaces 下已安装且与当前项目相关的 plugins bump 到新版本缓存。
|
||||
|
||||
它由两层配置决定:
|
||||
|
||||
1. Plugin auto-update 门禁不能处于跳过状态:`DISABLE_AUTOUPDATER=1` 会关闭它,除非设置 `FORCE_AUTOUPDATE_PLUGINS=true` 强制运行。
|
||||
2. 单个 marketplace 必须被判定为 `autoUpdate: true`。
|
||||
|
||||
更新是 disk-only:后台任务只更新版本缓存和 `installed_plugins.json`,当前会话内存中的 plugin 列表不会立即替换。用户需要运行 `/reload-plugins` 或重启后才会使用新版本。
|
||||
|
||||
## 启动链路
|
||||
|
||||
后台入口在 `src/utils/backgroundHousekeeping.ts`:
|
||||
|
||||
- `startBackgroundHousekeeping()` 会调用 `autoUpdateMarketplacesAndPluginsInBackground()`。
|
||||
- REPL 中第一次提交后会启动 housekeeping。
|
||||
- headless 主流程在非 bare 模式下也会动态导入并启动 housekeeping。
|
||||
|
||||
核心实现位于 `src/utils/plugins/pluginAutoupdate.ts`:
|
||||
|
||||
1. `autoUpdateMarketplacesAndPluginsInBackground()` 先调用 `shouldSkipPluginAutoupdate()`。
|
||||
2. 如果未跳过,读取启用 auto-update 的 marketplaces。
|
||||
3. 对这些 marketplaces 调用 `refreshMarketplace()`。
|
||||
4. 调用 `updatePluginsForMarketplaces()` 更新这些 marketplaces 下已安装的 plugins。
|
||||
5. 如果有 plugin 被更新,通过 `onPluginsAutoUpdated()` 通知 UI。
|
||||
|
||||
后台刷新 marketplace 时会传入 `disableCredentialHelper: true`,避免启动后台任务触发交互式凭据提示。
|
||||
|
||||
## Plugin Auto-Update 门禁
|
||||
|
||||
门禁在 `src/utils/config.ts` 的 `shouldSkipPluginAutoupdate()`:
|
||||
|
||||
- 默认不跳过 plugin auto-update,因此 `/plugin marketplace` 中的 `Enable/Disable auto-update` 默认可见。
|
||||
- `DISABLE_AUTOUPDATER=1` 会跳过 plugin auto-update,并隐藏 marketplace 详情里的 `Enable/Disable auto-update`。
|
||||
- `FORCE_AUTOUPDATE_PLUGINS=1` 可以在 `DISABLE_AUTOUPDATER=1` 时重新启用 plugin auto-update 及其 UI 配置入口。
|
||||
|
||||
这与官方插件文档中的配置方式一致:如果用户禁用了自动更新,但仍希望插件自动更新可配置/可运行,可同时设置:
|
||||
|
||||
```bash
|
||||
export DISABLE_AUTOUPDATER=1
|
||||
export FORCE_AUTOUPDATE_PLUGINS=1
|
||||
```
|
||||
|
||||
注意:`/config` 中的 CLI binary auto-update 状态、development build 限制、`ENABLE_AUTOUPDATER` 门禁和 `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` 不再决定 plugin marketplace 的 per-marketplace auto-update 开关是否可见。
|
||||
|
||||
## Marketplace `autoUpdate` 判定
|
||||
|
||||
判定逻辑在 `src/utils/plugins/schemas.ts` 的 `isMarketplaceAutoUpdate()`:
|
||||
|
||||
- 如果 marketplace entry 明确设置了 `autoUpdate`,使用显式值。
|
||||
- 否则,官方 marketplace 默认开启 auto-update。
|
||||
- 第三方 marketplace 默认关闭 auto-update。
|
||||
- `knowledge-work-plugins` 虽在官方名称集合中,但默认不自动更新。
|
||||
|
||||
官方名称集合包括:
|
||||
|
||||
- `claude-code-marketplace`
|
||||
- `claude-code-plugins`
|
||||
- `claude-plugins-official`
|
||||
- `anthropic-marketplace`
|
||||
- `anthropic-plugins`
|
||||
- `agent-skills`
|
||||
- `life-sciences`
|
||||
- `knowledge-work-plugins`
|
||||
|
||||
其中 `knowledge-work-plugins` 在 `NO_AUTO_UPDATE_OFFICIAL_MARKETPLACES` 中,因此默认关闭。
|
||||
|
||||
## 配置来源和优先级
|
||||
|
||||
Plugin auto-update 涉及两类 marketplace 配置:
|
||||
|
||||
1. 状态层:`known_marketplaces.json`
|
||||
- schema 在 `KnownMarketplaceSchema`。
|
||||
- 字段包括 `source`、`installLocation`、`lastUpdated`、可选 `autoUpdate`。
|
||||
- 记录已 materialized 的 marketplace 缓存位置和刷新状态。
|
||||
|
||||
2. 意图层:settings 中的 `extraKnownMarketplaces`
|
||||
- schema 在 `ExtraKnownMarketplaceSchema`。
|
||||
- 也支持可选 `autoUpdate`。
|
||||
- 用于 user/project/local/managed settings 声明希望存在的 marketplace。
|
||||
|
||||
自动更新读取启用 marketplaces 时,会先读取 `known_marketplaces.json`,再用 settings 声明的 `extraKnownMarketplaces[name].autoUpdate` 覆盖状态层值。也就是说,settings 声明的 `autoUpdate` 优先于 `known_marketplaces.json` 中的 `autoUpdate`。
|
||||
|
||||
`getDeclaredMarketplaces()` 的合并顺序是:
|
||||
|
||||
1. 隐式官方 marketplace 声明。
|
||||
2. `--add-dir` 里的 extra marketplaces。
|
||||
3. merged settings 里的 `extraKnownMarketplaces`。
|
||||
|
||||
## `/plugin marketplace` UI
|
||||
|
||||
Marketplace 管理 UI 在 `src/commands/plugin/ManageMarketplaces.tsx`。
|
||||
|
||||
详情菜单包含:
|
||||
|
||||
- `Browse plugins`
|
||||
- `Update marketplace`
|
||||
- `Enable auto-update` 或 `Disable auto-update`
|
||||
- `Remove marketplace`
|
||||
|
||||
`Enable/Disable auto-update` 只有在 `shouldSkipPluginAutoupdate()` 为 false 时才显示。默认情况下该条件为 false,因此 marketplace auto-update 配置默认可见。只有显式设置 `DISABLE_AUTOUPDATER=1` 且未设置 `FORCE_AUTOUPDATE_PLUGINS=1` 时才会隐藏。
|
||||
|
||||
点击该开关后会调用 `setMarketplaceAutoUpdate()`:
|
||||
|
||||
- 更新 `known_marketplaces.json` 中该 marketplace 的 `autoUpdate`。
|
||||
- 如果该 marketplace 是 settings 声明的,还会写回声明它的同一个 settings source,避免下次被 settings 覆盖。
|
||||
- seed-managed marketplace 不允许切换 auto-update,会报错提示 seed 内容由管理员控制。
|
||||
|
||||
当某 marketplace 的 auto-update 已启用时,详情页底部会显示说明:CoStrict 会自动更新该 marketplace 及其已安装 plugins。
|
||||
|
||||
## `/config` UI
|
||||
|
||||
`/config` 中的 `Auto-update channel` 属于全局 CLI auto-updater 配置,不是 plugin 专用配置。
|
||||
|
||||
它会展示:
|
||||
|
||||
- `latest`
|
||||
- `stable`
|
||||
- `disabled` 和 disabled reason
|
||||
|
||||
Plugin auto-update 不使用 `latest/stable` channel 来选择插件版本,也不通过 `/config` 配置。插件版本来自 marketplace 刷新后的 plugin source、manifest version、git commit 等版本计算逻辑。
|
||||
|
||||
## 手动更新和自动更新的区别
|
||||
|
||||
手动更新路径包括:
|
||||
|
||||
- `/plugin marketplace update`
|
||||
- marketplace 详情页中的 `Update marketplace`
|
||||
|
||||
手动更新会:
|
||||
|
||||
1. 调用 `refreshMarketplace()` 刷新指定 marketplace。
|
||||
2. 调用 `updatePluginsForMarketplaces()` bump 该 marketplace 下已安装的 plugins。
|
||||
3. 清理缓存并刷新 UI 状态。
|
||||
|
||||
手动更新不依赖该 marketplace 的 `autoUpdate` 标记,也不通过后台 auto-update 的通知回调。它是用户主动操作。
|
||||
|
||||
自动更新只处理 auto-update enabled marketplaces,并在后台静默执行;失败通常只写 debug log,不阻塞用户。
|
||||
|
||||
## Plugin 更新细节
|
||||
|
||||
实际 plugin bump 由 `src/services/plugins/pluginOperations.ts` 的 `updatePluginOp()` 完成:
|
||||
|
||||
1. 解析 plugin ID。
|
||||
2. 从 marketplace 找到 plugin entry。
|
||||
3. 从 `installed_plugins.json` 找到对应 scope 和 projectPath 的安装记录。
|
||||
4. 对远程 source 下载临时副本;对本地 source 使用 marketplace 中的相对路径。
|
||||
5. 调用 `calculatePluginVersion()` 计算新版本。
|
||||
6. 如果当前安装已经是新版本,返回 already up to date。
|
||||
7. 否则复制到 versioned cache。
|
||||
8. 调用 `updateInstallationPathOnDisk()` 写回 `installed_plugins.json`。
|
||||
9. 如果旧版本路径不再被任何安装引用,标记为 orphaned。
|
||||
|
||||
`updateInstallationPathOnDisk()` 只改磁盘文件,并清掉 installed plugins cache;它不会更新当前会话中的 in-memory installed plugins。这个设计保证后台更新不会在运行中替换正在使用的 plugin。
|
||||
|
||||
## Pending 更新和通知
|
||||
|
||||
后台 auto-update 成功后:
|
||||
|
||||
- 如果 REPL 已注册 callback,立即调用 callback。
|
||||
- 如果 auto-update 先完成而 REPL hook 尚未注册,会先保存到 `pendingNotification`,注册后补发。
|
||||
|
||||
UI hook 在 `src/hooks/notifs/usePluginAutoupdateNotification.tsx`:
|
||||
|
||||
- remote mode 下不显示。
|
||||
- 通知内容为 plugin 名称和 `Run /reload-plugins to apply`。
|
||||
- 通知优先级为 low,默认 10 秒超时。
|
||||
|
||||
`getAutoUpdatedPluginNames()` 会通过 `hasPendingUpdates()` 判断磁盘安装记录和内存安装记录是否不同,并返回 pending update 的 plugin 名称。
|
||||
|
||||
## 关键注意点
|
||||
|
||||
- Plugin marketplace auto-update 配置默认可见。
|
||||
- `DISABLE_AUTOUPDATER=1` 会关闭 plugin auto-update;`FORCE_AUTOUPDATE_PLUGINS=1` 可专门恢复 plugin auto-update。
|
||||
- 官方 marketplaces 默认 auto-update,第三方 marketplaces 默认不 auto-update。
|
||||
- settings 中 `extraKnownMarketplaces[name].autoUpdate` 优先于 `known_marketplaces.json`。
|
||||
- 手动 marketplace update 会同步 bump 已安装 plugins,但不等于开启 auto-update。
|
||||
- auto-update 是 disk-only;当前会话需要 `/reload-plugins` 或重启才能应用。
|
||||
- `/config` 的 `Auto-update channel` 不决定 plugin 版本来源。
|
||||
47
src/utils/__tests__/pluginAutoupdateConfig.test.ts
Normal file
47
src/utils/__tests__/pluginAutoupdateConfig.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { shouldSkipPluginAutoupdate } from '../config'
|
||||
|
||||
describe('shouldSkipPluginAutoupdate', () => {
|
||||
const originalDisableAutoUpdater = process.env.DISABLE_AUTOUPDATER
|
||||
const originalForcePluginAutoupdate = process.env.FORCE_AUTOUPDATE_PLUGINS
|
||||
const originalEnableAutoUpdater = process.env.ENABLE_AUTOUPDATER
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.DISABLE_AUTOUPDATER
|
||||
delete process.env.FORCE_AUTOUPDATE_PLUGINS
|
||||
delete process.env.ENABLE_AUTOUPDATER
|
||||
if (originalDisableAutoUpdater !== undefined) {
|
||||
process.env.DISABLE_AUTOUPDATER = originalDisableAutoUpdater
|
||||
}
|
||||
if (originalForcePluginAutoupdate !== undefined) {
|
||||
process.env.FORCE_AUTOUPDATE_PLUGINS = originalForcePluginAutoupdate
|
||||
}
|
||||
if (originalEnableAutoUpdater !== undefined) {
|
||||
process.env.ENABLE_AUTOUPDATER = originalEnableAutoUpdater
|
||||
}
|
||||
})
|
||||
|
||||
test('does not skip plugin autoupdate by default', () => {
|
||||
expect(shouldSkipPluginAutoupdate()).toBe(false)
|
||||
})
|
||||
|
||||
test('does not inherit the CLI auto-updater config gate', () => {
|
||||
delete process.env.ENABLE_AUTOUPDATER
|
||||
|
||||
expect(shouldSkipPluginAutoupdate()).toBe(false)
|
||||
})
|
||||
|
||||
test('skips plugin autoupdate when DISABLE_AUTOUPDATER is set', () => {
|
||||
process.env.DISABLE_AUTOUPDATER = '1'
|
||||
|
||||
expect(shouldSkipPluginAutoupdate()).toBe(true)
|
||||
})
|
||||
|
||||
test('FORCE_AUTOUPDATE_PLUGINS overrides DISABLE_AUTOUPDATER', () => {
|
||||
process.env.DISABLE_AUTOUPDATER = '1'
|
||||
process.env.FORCE_AUTOUPDATE_PLUGINS = '1'
|
||||
|
||||
expect(shouldSkipPluginAutoupdate()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1719,15 +1719,15 @@ export function isAutoUpdaterDisabled(): boolean {
|
|||
|
||||
/**
|
||||
* Returns true if plugin autoupdate should be skipped.
|
||||
* This checks if the auto-updater is disabled AND the FORCE_AUTOUPDATE_PLUGINS
|
||||
* env var is not set to 'true'. The env var allows forcing plugin autoupdate
|
||||
* even when the auto-updater is otherwise disabled.
|
||||
* Plugin marketplace auto-update is independently configurable from the CLI
|
||||
* binary updater. The official hard stop is DISABLE_AUTOUPDATER; users can
|
||||
* opt plugin updates back in with FORCE_AUTOUPDATE_PLUGINS.
|
||||
*/
|
||||
export function shouldSkipPluginAutoupdate(): boolean {
|
||||
return (
|
||||
isAutoUpdaterDisabled() &&
|
||||
!isEnvTruthy(process.env.FORCE_AUTOUPDATE_PLUGINS)
|
||||
)
|
||||
if (isEnvTruthy(process.env.FORCE_AUTOUPDATE_PLUGINS)) {
|
||||
return false
|
||||
}
|
||||
return isEnvTruthy(process.env.DISABLE_AUTOUPDATER)
|
||||
}
|
||||
|
||||
export type AutoUpdaterDisabledReason =
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user