Skip to content

Commit 2beb8cb

Browse files
authored
0.4.1
1 parent ba4fe32 commit 2beb8cb

44 files changed

Lines changed: 2066 additions & 1785 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# ADR 0030: 官方插件随宿主发版与启动时静默同步
2+
3+
- **状态**: Accepted
4+
- **日期**: 2026-08-31
5+
- **关联提交**: `db2128f`, `642e38b`
6+
- **关联**: **部分修订** [ADR 0016](./0016-round3-convergence-and-deprecated-removal.md)(撤销 `minEngineVersion` 运行时校验);延续 [ADR 0011](./0011-single-track-official-plugin-install.md) 单轨在线安装;延续 [ADR 0014](./0014-wallpaper-official-marketplace-only.md) 官方 catalog 分发
7+
- **范围**: `packages/core`, `apps/web`, `scripts/build-official-plugins.ts`, `apps/web/static/official-plugins`
8+
9+
---
10+
11+
## 背景与问题
12+
13+
ADR 0016 引入 `CHRONOS_ENGINE_VERSION` 与 manifest `minEngineVersion` semver 校验,并保留单插件「检查更新 / 重装」流程。实践中出现三套版本号并行维护:
14+
15+
| 版本号 | 示例 | 职责 |
16+
| ------ | ---- | ---- |
17+
| `apps/web` 发布版本 | `0.4.1` | 产品发版 |
18+
| `CHRONOS_ENGINE_VERSION` | `0.4.1` | 插件宿主 API 契约 |
19+
| `OFFICIAL_PLUGIN_VERSION` | `1.0.0` | 官方插件 manifest.version |
20+
21+
官方 ESM 插件已随 `apps/web` 静态资源同发(`static/official-plugins/`),不独立热更。单插件更新 UI 与引擎版本闸门增加心智负担,且与 PWA 整包更新路径重复。
22+
23+
另一缺口:已安装插件的 bundle 缓存在 `installed_plugins` 中,PWA 更新宿主后 `init()` 仍直接 `activate` 本地缓存,不会自动拉新 catalog bundle。
24+
25+
## 架构决策
26+
27+
### 1 — 版本单源:`apps/web` 发布版本
28+
29+
- 删除 `CHRONOS_ENGINE_VERSION``OFFICIAL_PLUGIN_VERSION``PluginManifest.minEngineVersion`
30+
- 删除 `comparePluginVersions` / `isPluginVersionNewer` / 引擎 semver 校验。
31+
- `scripts/build-official-plugins.ts``apps/web/package.json` 读取 `version` 写入各 manifest。
32+
- 保留 `catalog.version`(JSON 格式号,与发版无关)。
33+
34+
### 2 — 移除单插件更新流程
35+
36+
- 删除 `checkForUpdates``updateInstalled``PluginUpdateOffer`
37+
- 插件页移除「更新」按钮与版本对比 UI。
38+
- 不兼容场景统一走 PWA 整包更新(「关于 → 软件更新」)。
39+
40+
### 3 — 启动时静默同步已安装官方插件
41+
42+
[`sync-installed-plugins.ts`](../../apps/web/src/lib/services/official-plugins/sync-installed-plugins.ts) + `OfficialPluginService.syncInstalledWithHost()`
43+
44+
`init()``dedupeBuiltinOverlap()` 之后、`activate` 之前执行:
45+
46+
```
47+
load → dedupeBuiltinOverlap → syncInstalledWithHost → activate enabled
48+
```
49+
50+
同步规则:
51+
52+
| 条件 | 行为 |
53+
| ---- | ---- |
54+
| `manifest.version === APP_VERSION` | 跳过 |
55+
| `manifestUrl` 为外部 `http(s)` 链接 | 跳过(第三方插件自有版本) |
56+
| 官方 catalog 插件(`/official-plugins/manifests/…` 或无 manifestUrl 但 id 在 catalog 中) | 从 catalog 静默 `install({ silent: true })` |
57+
| catalog 拉取失败 | 整体跳过,继续用缓存(离线友好) |
58+
| 单个插件重装失败 | `console.error`,该插件继续用缓存 |
59+
| 插件已从 catalog 移除 | 保留本地缓存,不自动卸载 |
60+
61+
### 4 — 用户数据与安装缓存分离
62+
63+
同步仅替换 `installed_plugins` 中的 manifest 与 bundle(`code` / `cssCode` / `colorsJson` 等)。
64+
65+
**不触碰**插件命名空间下的业务数据,例如:
66+
67+
- `tool-wallpaper``wallpaper_image`
68+
- 各插件 → `PLUGIN_CONFIG_STORAGE_KEY` 配置
69+
70+
`uninstall()` 调用 `clearPluginData(pluginId)` 时才会清除。
71+
72+
## 非目标
73+
74+
- 不恢复单插件更新 UI
75+
- 不同步外部链接安装的插件
76+
- 不自动卸载 catalog 已移除的插件
77+
- 不为第三方插件重新引入引擎版本契约
78+
79+
## 验证
80+
81+
- `vp check` / `vp test` 全绿
82+
- 官方插件 manifest `version``apps/web` 一致,无 `minEngineVersion`
83+
- `init()` 在版本落后时静默重装官方插件,不触发 `engine.notify`
84+
- 同步后 `wallpaper_image` 与插件配置保留
85+
86+
## 修订记录
87+
88+
- 2026-08-31:初版 Accepted;撤销 ADR 0016 `minEngineVersion` 闭环策略,改为宿主发版 + 启动同步。

.agents/docs/adr/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
| [ADR 0027](./0027-round6-architecture-subtraction.md) | **Round 6 架构减法:颜色契约、引擎瘦面、宿主 i18n、传输管道与插件工厂** | Accepted | `packages/core`, `packages/ui-kit`, `packages/plugins/*`, `apps/web`, `scripts/*` | workbench 连字符键;删 engine.t/休眠端口;`hostT`;transfer-state 闭环;`defineChronosPlugin`;C1/C7 挂起缓议 |
3838
| [ADR 0028](./0028-today-plugin-default-launch-and-day-clock.md) | **今日插件、defaultLaunch 冷启动与 day-clock 收敛** | Accepted (§3–§4 Superseded by ADR 0029) | `packages/core`, `packages/plugins/today`, `apps/web` | `createDayClock`;Today controller;BottomSheet history;`queryCourses` 首消费者 |
3939
| [ADR 0029](./0029-shell-internal-tab-navigation.md) | **壳内 Tab 导航与底栏槽位去 URL 化** | Accepted | `packages/core`, `packages/plugins/today`, `apps/web` | `activeTabId``ShellTabPanels`;删除 `href`/`HOST_SHELL_TAB_ROUTES``backShellTab` |
40+
| [ADR 0030](./0030-official-plugin-version-co-shipping-and-host-sync.md) | **官方插件随宿主发版与启动时静默同步** | Accepted | `packages/core`, `apps/web`, `scripts/*`, `static/official-plugins` | 撤销 `minEngineVersion` 与单插件更新;manifest.version 单源 `apps/web``init` 静默 catalog 同步;用户插件数据保留 |
4041

4142
## 状态图例
4243

CONTRIBUTING.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,11 @@ export default defineChronosPlugin({
257257

258258
### 分发形态
259259

260-
| 形态 | 适合 | 要求 |
261-
| ----------------- | -------------------------------------- | ------------------------------------------------------------------------ |
262-
| Profile 内置 | 随应用发行的核心能力(如 source-cqut) | 进入 profile 清单,进程内加载 |
263-
| 官方在线 ESM 插件 | 含逻辑/富 UI 的扩展(如 wallpaper) | 构建为自包含 ESM bundle,manifest 带 SHA-256 与 `minEngineVersion` |
264-
| JSON-only 主题 | 纯配色/图标资源(如 theme-yumemita) | `ThemeManifest` 显式声明 `colorsUrl` / `iconThemeUrl` / `themeId`,无 JS |
260+
| 形态 | 适合 | 要求 |
261+
| ----------------- | -------------------------------------- | ---------------------------------------------------------------------------------- |
262+
| Profile 内置 | 随应用发行的核心能力(如 source-cqut) | 进入 profile 清单,进程内加载 |
263+
| 官方在线 ESM 插件 | 含逻辑/富 UI 的扩展(如 wallpaper) | 构建为自包含 ESM bundle,manifest 带 SHA-256`version``apps/web` 发布版本一致 |
264+
| JSON-only 主题 | 纯配色/图标资源(如 theme-yumemita) | `ThemeManifest` 显式声明 `colorsUrl` / `iconThemeUrl` / `themeId`,无 JS |
265265

266266
发布流程见[新增官方插件](#新增官方插件)
267267

@@ -300,7 +300,7 @@ vp run verify:official-plugins # 自校验检查:哈希、manifest 字段、c
300300
若插件含 JS:
301301

302302
1. bundle 必须自包含(Svelte 编译进产物),通过 Blob ESM 导入加载;
303-
2. manifest 声明 `cssUrl``cssSha256``jsSha256``minEngineVersion`
303+
2. manifest 声明 `cssUrl``cssSha256``jsSha256``version` 取自 `apps/web/package.json`
304304
3. 富 UI 暴露 mountable 包装器而非裸组件;
305305
4. 本地验证可在「我的 → 插件」中经 catalog 在线安装路径走一遍(双轨行为必须一致,见 [ADR 0011](.agents/docs/adr/0011-single-track-official-plugin-install.md))。
306306

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@chronos/web",
33
"private": true,
4-
"version": "0.4.0",
4+
"version": "0.4.1",
55
"type": "module",
66
"scripts": {
77
"dev": "vp dev .",

apps/web/src/lib/components/plugins/PluginsScreen.svelte

Lines changed: 3 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
getProfileBuiltinPlugins,
77
getAppController
88
} from '$lib/services/app-engine';
9-
import type {
10-
InstalledOfficialPluginRecord,
11-
PluginUpdateOffer
12-
} from '$lib/services/official-plugins/official-plugin-service';
9+
import type { InstalledOfficialPluginRecord } from '$lib/services/official-plugins/official-plugin-service';
1310
import type { PluginManifest, ConfigSchema } from '@chronos/core';
1411
import { resolveLocaleMapText } from '@chronos/core';
1512
import SegmentedControl from '$lib/components/ui/SegmentedControl.svelte';
@@ -40,7 +37,6 @@
4037
4138
let installedRecords = $state.raw<InstalledOfficialPluginRecord[]>([]);
4239
let catalogManifests = $state.raw<Array<{ url: string; manifest: PluginManifest }>>([]);
43-
let updateOffers = $state.raw<PluginUpdateOffer[]>([]);
4440
let loadingCatalog = $state(false);
4541
let catalogError = $state<string | null>(null);
4642
let operatingPluginId = $state<string | null>(null);
@@ -67,34 +63,6 @@
6763
installedRecords = [...officialPlugins.listInstalled()];
6864
}
6965
70-
const updateOfferById = $derived.by(() => {
71-
const map = new Map<string, PluginUpdateOffer>();
72-
for (const offer of updateOffers) {
73-
map.set(offer.pluginId, offer);
74-
}
75-
return map;
76-
});
77-
78-
function buildPrefetchedCatalog() {
79-
return new Map(
80-
catalogManifests.map((entry) => [
81-
entry.manifest.id,
82-
{ manifest: entry.manifest, manifestUrl: entry.url }
83-
])
84-
);
85-
}
86-
87-
async function refreshUpdateOffers() {
88-
try {
89-
updateOffers = await officialPlugins.checkForUpdates(
90-
BUILTIN_CATALOG_URL,
91-
buildPrefetchedCatalog()
92-
);
93-
} catch {
94-
updateOffers = [];
95-
}
96-
}
97-
9866
onMount(() => {
9967
void officialPlugins.init().then(async () => {
10068
refreshInstalled();
@@ -103,7 +71,6 @@
10371
10472
const sub = officialPlugins.onChanged(() => {
10573
refreshInstalled();
106-
void refreshUpdateOffers();
10774
});
10875
10976
return () => {
@@ -128,11 +95,9 @@
12895
})
12996
);
13097
catalogManifests = entries.filter((entry) => entry !== null);
131-
await refreshUpdateOffers();
13298
} catch (err: unknown) {
13399
const msg = err instanceof Error ? err.message : String(err);
134100
catalogError = msg;
135-
await refreshUpdateOffers();
136101
} finally {
137102
loadingCatalog = false;
138103
}
@@ -192,18 +157,6 @@
192157
}
193158
}
194159
195-
async function handleUpdate(pluginId: string, manifestUrl?: string) {
196-
operatingPluginId = pluginId;
197-
try {
198-
await officialPlugins.updateInstalled(pluginId, manifestUrl);
199-
} catch (err: unknown) {
200-
const msg = err instanceof Error ? err.message : String(err);
201-
snackbarKey('snackbar.update.failed', { message: msg });
202-
} finally {
203-
operatingPluginId = null;
204-
}
205-
}
206-
207160
function promptLinkInstall() {
208161
manifestUrlInput = '';
209162
linkInstallDialogOpen = true;
@@ -297,7 +250,7 @@
297250
{/snippet}
298251

299252
<div class="flex h-full min-h-0 flex-col text-on-surface">
300-
<div class="shrink-0 px-4 pt-3 pb-4">
253+
<div class="mx-auto w-full max-w-lg shrink-0 px-4 pt-3 pb-4">
301254
<SegmentedControl
302255
segments={tabSegments}
303256
value={activeTab}
@@ -401,7 +354,6 @@
401354
{@const desc = resolveManifestText(record.manifest.description)}
402355
{@const meta = getPluginCategoryMeta(record.manifest.type)}
403356
{@const isBusy = operatingPluginId === record.manifest.id}
404-
{@const updateOffer = updateOfferById.get(record.manifest.id)}
405357
<div
406358
class={[
407359
'flex flex-col gap-2 p-3 transition-colors hover:bg-surface-variant/30',
@@ -445,22 +397,6 @@
445397
</p>
446398
{/if}
447399
</div>
448-
449-
{#if updateOffer}
450-
<div class="flex shrink-0 flex-col items-end gap-1">
451-
<Button
452-
variant="filled"
453-
class="h-8 shrink-0 px-3.5 text-xs font-medium"
454-
disabled={isBusy}
455-
onclick={() => handleUpdate(record.manifest.id, updateOffer.manifestUrl)}
456-
>
457-
{isBusy ? hostT('plugins.action.updating') : hostT('plugins.action.update')}
458-
</Button>
459-
<span class="text-caption text-[10px] text-on-surface-variant">
460-
v{updateOffer.currentVersion} → v{updateOffer.latestVersion}
461-
</span>
462-
</div>
463-
{/if}
464400
</div>
465401

466402
<div class="flex items-center justify-between gap-2">
@@ -559,7 +495,6 @@
559495
{@const desc = resolveManifestText(manifest.description)}
560496
{@const installed = isInstalled(manifest.id)}
561497
{@const isBusy = operatingPluginId === manifest.id}
562-
{@const updateOffer = updateOfferById.get(manifest.id)}
563498
<div
564499
class="flex items-center justify-between gap-3 p-3 transition-colors hover:bg-surface-variant/30"
565500
>
@@ -591,21 +526,7 @@
591526
</div>
592527

593528
<div class="flex shrink-0 flex-col items-end gap-1">
594-
{#if installed && updateOffer}
595-
<Button
596-
variant="filled"
597-
class="h-8 shrink-0 px-3.5 text-xs font-medium"
598-
disabled={isBusy}
599-
onclick={() => handleUpdate(manifest.id, updateOffer.manifestUrl)}
600-
>
601-
{isBusy
602-
? hostT('plugins.action.updating')
603-
: hostT('plugins.action.update')}
604-
</Button>
605-
<span class="text-caption text-[10px] text-on-surface-variant">
606-
v{updateOffer.currentVersion} → v{updateOffer.latestVersion}
607-
</span>
608-
{:else if installed}
529+
{#if installed}
609530
<span
610531
class="inline-flex items-center gap-1 rounded-full bg-primary-container/50 px-2.5 py-1 text-xs font-medium text-primary"
611532
>

0 commit comments

Comments
 (0)