From d7cdbd7a918fc6b42a8b7c4c10702cf994952771 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Wed, 12 Aug 2026 19:04:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=BF=87=E6=97=B6=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ss_536d35da-2849-4d9d-a255-aa9149c33a32.md | 132 -------- docs/theme-guide.md | 135 -------- docs/theme-system-refactor.md | 301 ------------------ 3 files changed, 568 deletions(-) delete mode 100644 .zcode/plans/plan-sess_536d35da-2849-4d9d-a255-aa9149c33a32.md delete mode 100644 docs/theme-guide.md delete mode 100644 docs/theme-system-refactor.md diff --git a/.zcode/plans/plan-sess_536d35da-2849-4d9d-a255-aa9149c33a32.md b/.zcode/plans/plan-sess_536d35da-2849-4d9d-a255-aa9149c33a32.md deleted file mode 100644 index 6678519..0000000 --- a/.zcode/plans/plan-sess_536d35da-2849-4d9d-a255-aa9149c33a32.md +++ /dev/null @@ -1,132 +0,0 @@ -# Pika 主题系统整改方案 - -## 背景与决策 - -你的目标:做"可自定义公共页面"。上一个 AI 把它实现成了一套**完整的第三方主题分发生态**(市场 + ZIP 安装 + GitHub 更新 + SSRF + SDK 契约),1497 行单体 service + 缺 repo 层。 - -经讨论确认以下决策: -1. **不隔离 origin**(部署要简单),但**砍掉主题市场**(拒绝"任何人能投毒主题"的公开入口),保留「本地上传 + 管理员自填 URL 安装」——同源 JS 风险仍在但收敛到"你信错某个 URL",攻击面可控。 -2. **拆分 `theme_service.go`** 1497 行单体为同包多文件,解决可维护性。 -3. **新增 repo 层** `ThemeConfigurationRepo`,收口 DB 访问,与项目 agent/monitor/api_key 风格统一。 -4. **重写设计文档**,与"砍市场后"的实际架构对齐。 - -审计确认:`InstallFromURL`(从 URL 安装)+ `validSHA256` + `download*` 与市场逻辑完全独立,砍市场后照常可用。 - ---- - -## 阶段一:砍掉主题市场(前端 + 后端 + 配置) - -### 1.1 前端 `web/admin/src/api/theme.ts` -- 删除 `ThemeMarketEntry`(17-20)、`ThemeMarket`(21)两个接口 -- 删除 `getThemeMarket`(40)、`installMarketTheme`(41-44)两个函数 -- 保留 `installTheme`(29-30,从 URL 安装,与市场无关) - -### 1.2 前端 `web/admin/src/admin/pages/Settings/ThemeSettings.tsx` -- import(9-10):移除 `getThemeMarket, installMarketTheme` -- 删除 `marketQuery`(53)、`refresh()` 里的 `['theme-market']` 失效(56) -- 删除 `marketCards` 整段(164-177) -- `Tabs items`(199-207):删除 `market` Tab,只保留 `installed` Tab,Tabs 简化为单页或直接渲染 `installedCards` 的 Row -- 其余(已安装卡片/上传/URL安装/配置 Modal/安全提示)全部保留 - -### 1.3 后端路由 `internal/app.go` -- 删除 268-269 两行:`GET /themes/market`、`POST /themes/market/install` - -### 1.4 后端 handler `internal/handler/theme_handler.go` -- 删除 `Market`(118-124)、`InstallMarket`(126-139)两个方法 - -### 1.5 后端 service `internal/service/theme_service.go`(在阶段二拆分前的原文件上操作) -- 删除 `Market` 方法(1064-1089) -- 删除 `ThemeMarket`(90-93)、`ThemeMarketEntry`(95-105)两个类型 -- 删除结构体字段 `marketURL`(75)和构造赋值 `marketURL: ...`(129) -- **保留**:`validSHA256`(1091-1097,InstallFromURL 仍用)、`download`/`downloadWithLimit`/`validateRemoteURL`/`githubReleaseURL`(Update 仍用) - -### 1.6 配置 -- `internal/config/config.go`:删除 `ThemeConfig.MarketURL`(17) -- 三份 yaml(`config.yaml:21` / `config.sqlite.yaml:25` / `config.postgresql.yaml:25`):删除 `MarketURL: ""` 行 - ---- - -## 阶段二:拆分 `theme_service.go`(同包多文件 + repo 层) - -### 2.1 拆分原则(关键约束) -- **保持 `package service` 不变**,文件拆到 `internal/service/` 下(不引入 `internal/theme/` 子包)。原因:测试 `theme_service_test.go` 同包直接访问未导出字段 `themeDir`(L229)、`mu`(L339)、未导出方法 `installArchiveLocked`(L340),换子包会破坏测试。 -- **`ThemeService` struct 保持单一**(字段不变,除删除 marketURL),作为共享状态容器。所有方法仍以 `*ThemeService` 为接收者,只是定义分散到不同文件。Go 原生支持同包多文件。 -- **公开接口零变更**:10 个被 handler 调用的导出方法、`NewThemeService`、4 个错误值、3 个导出常量、`ThemeInstallResult` 类型——签名一个不改。 - -### 2.2 新增 repo 层 `internal/repo/theme_configuration_repo.go` -``` -type ThemeConfigurationRepo struct{ db *gorm.DB } -func NewThemeConfigurationRepo(db *gorm.DB) *ThemeConfigurationRepo -func (r *ThemeConfigurationRepo) Get(ctx, themeID) (models.ThemeConfiguration, error) // ErrRecordNotFound 透传 -func (r *ThemeConfigurationRepo) Save(ctx, db *gorm.DB, record) error // 支持传入 tx,保留事务原子性 -func (r *ThemeConfigurationRepo) Delete(ctx, themeID) error -func (r *ThemeConfigurationRepo) Migrate(ctx, db *gorm.DB, manifest) (map[string]any, error) // 支持传入 tx -``` -> 关键:`Save`/`Migrate` 必须接收外部 `*gorm.DB`(既可是 `r.db` 也可是事务 `tx`),因为 `installArchiveLocked`(L649)要求"文件 rename + 配置迁移"同事务原子提交。现有 `migrateConfigurationWithDB(ctx, database *gorm.DB, ...)` 签名已为此设计,repo 直接照搬语义。 - -### 2.3 文件拆分映射(`internal/service/` 下新建) - -| 新文件 | 来源行号 | 内容 | -|---|---|---| -| `theme.go` | 保留原 `theme_service.go` 重命名 | import + const + var(错误/正则) + `ThemeService` struct + `ThemeInstallResult` + `NewThemeService` | -| `theme_manager.go` | 原 226-351, 574-672, 674-689, 760-814, 816-882, 1010-1062, 1183-1198 | List/Get/Active/Activate/Delete/Install/InstallFromURL/Update/ActiveDistDir/PreviewPath + installArchiveLocked/findInstalledThemeID/getUnlocked/infoFromRoot/repairActiveTheme/ActiveThemeID + 配置存取方法(改为调 repo) | -| `theme_manifest.go` | 原 353-537, 884-1008 | loadThemeManifest/validateThemeManifest/validateThemeIndex/validateConfigSpec/validThemeRedirect/validThemeID + defaultThemeConfiguration/sanitizeThemeConfiguration/migrateThemeConfiguration/validateManagedField | -| `theme_compatibility.go` | 原 1377-1496 | versionConstraint/parseVersionRange/versionMatchesRange/parseSemver/compareSemver/compareSemverParts | -| `theme_archive.go` | 原 539-598, 691-758 | readThemeArchive/extractThemeArchive | -| `theme_downloader.go` | 原 160-189, 1091-1181 | dialRemoteContext/unsafeRemoteIP/download/downloadWithLimit/validateRemoteURL/githubReleaseURL/validSHA256 | -| `theme_filesystem.go` | 原 191-224, 539-572, 1311-1375 | validateOfficialWebBuild/themeRoot/validateRelativeFile/recoverFilesystem/themeRootValid | -| `theme_renderer.go` | 原 1200-1309 | RenderIndex/applySystemMetadata/replaceBeforeFirst/invalidateRenderCache | - -### 2.4 DB 访问收口(5 处 `s.db` → `s.repo`) -| 原行号 | 方法 | 改动 | -|---|---|---| -| 649 | `installArchiveLocked` | `s.db.Transaction(...)` → 事务体内 `migrateConfigurationWithDB` 改调 `s.repo.Migrate(ctx, tx, manifest)` | -| 805 | `Delete` | `s.db...Delete(...)` → `s.repo.Delete(ctx, id)` | -| 824 | `GetConfiguration` | `s.db...First(...)` → `s.repo.Get(ctx, id)` | -| 854 | `SaveConfiguration` | `s.db...Save(...)` → `s.repo.Save(ctx, s.db, record)` | -| 867/878 | `migrateConfigurationWithDB` | 整体移入 repo 层,service 侧 `migrateConfiguration` 改为转发 `s.repo.Migrate(ctx, s.db, manifest)` | - -### 2.5 Wire 注入调整 -- `wire.go` / `wire_gen.go`:新增 `repo.NewThemeConfigurationRepo` provider;`NewThemeService` 签名**不变**(内部 `db *gorm.DB` → 改为持有 `repo *repo.ThemeConfigurationRepo`,构造时 `cfg` 仍传入)。 -- 构造顺序保持 `PropertyService` → `ThemeService`。 - ---- - -## 阶段三:重写设计文档 - -### 3.1 重写 `docs/theme-system-refactor.md` -- 标题改为反映实际形态(如《Pika 可安装主题系统》),删除"状态:待确认" -- 删除 §8.3 主题市场整章、所有 market/marketURL 相关行 -- 在安全章节明确写清**当前的安全定位**:不隔离 origin + 不开放市场,自定义来源限于"管理员本地上传或自填 URL",同源 JS 风险由管理员对所选来源负责;并保留 origin 隔离作为"未来若开放无审核市场的前置条件"的说明 -- 同步目录结构、文件拆分(去掉 `internal/theme/` 改为 `internal/service/theme_*.go` + repo 层)、配置项(去 MarketURL) - -### 3.2 更新 `docs/theme-guide.md` -- 删除"管理员使用"里的市场项(11)、配置说明的市场项(31)、yaml 示例的 MarketURL(25) -- 保留上传 ZIP / 从 URL 安装 / 配置 / 更新 / 删除 的完整说明 - ---- - -## 阶段四:验证 - -1. 后端:`go build ./...` + `go vet ./...` + `go test ./internal/service/... ./internal/handler/...`(确认 441 行测试全绿,尤其 `TestThemeUpdateRollsBackFilesystemWhenDatabaseCommitFails` 这个验证 repo 收口后事务回滚仍正确) -2. 前端:`cd web/admin && npm run build`(确认 TS 编译无 market 残留引用) -3. 全局 grep 确认无 `market`/`Market`/`市场` 残留(除文档历史段落和 origin 隔离讨论) -4. `make build-web` 确认组装+校验脚本仍通过 - ---- - -## 风险与回退 - -- **风险点**:阶段二拆文件 + repo 收口是改动面最大的一步。测试同包访问 `mu`/`installArchiveLocked`/`themeDir` 的约束决定了**必须保持同包**(不引入子包),这一点已在方案中遵守。 -- **回退**:每个阶段独立可提交。若阶段二出问题,可单独 revert 拆分提交回到"单体 service + repo 层未抽"状态,不影响阶段一砍市场的成果。 -- **顺序**:阶段一(砍市场)→ 阶段二(拆分+repo)→ 阶段三(文档)→ 阶段四(验证)。建议每阶段单独 commit,便于 review 和回退。 - ---- - -## 不做的事(明确边界) - -- 不引入 origin 隔离 / 双子域名(你已明确拒绝) -- 不改前端 admin/portal/theme-sdk 的项目结构和构建(已经做得很好,无需动) -- 不改 theme-sdk 的 API 契约 -- 不补下载链路的 httptest 集成测试(属于增强项,不在本次硬伤修复范围;本次只保证现有测试全绿) -- 不动 `appearance_config` Property、`/api/config` 响应结构、默认主题清单 \ No newline at end of file diff --git a/docs/theme-guide.md b/docs/theme-guide.md deleted file mode 100644 index e4657a1..0000000 --- a/docs/theme-guide.md +++ /dev/null @@ -1,135 +0,0 @@ -# Pika 主题使用与开发指南 - -Pika 的第三方主题是一个完整的公开页面 SPA。它可以替换服务器列表、服务器详情、监控列表和监控详情,但不能替换登录页、OAuth/OIDC 回调或管理后台。 - -## 管理员使用 - -进入“管理后台 → 系统设置 → 主题管理”后,可以: - -- 上传本地 ZIP 安装主题; -- 查看预览、兼容状态、作者和版本; -- 启用、配置或删除主题。 - -默认主题 `default` 随 Pika 构建发布,不允许覆盖或删除。当前主题目录、清单或入口损坏时,Pika 会整体回退到默认主题并修正活动主题配置。当前启用的第三方主题必须先切换到其他主题才能删除。 - -主题切换不会刷新管理后台。打开或刷新公开页面即可看到新主题。系统名称、自定义 CSS/JS 等公开配置保存后同样在公开页面刷新时生效。 - -### 服务端配置 - -```yaml -App: - Theme: - Dir: "./data/themes" -``` - -- `PIKA_THEME_DIR` 非空时覆盖 `Dir`; -- SQLite 与 PostgreSQL Docker 部署都必须持久化 `/app/data`。 - -## 安全边界 - -第三方主题包含 JavaScript,并与 Pika 在同一 origin 执行。它是“受信任插件”,不是 CSS 皮肤,也不是安全沙箱。主题可以读取浏览器同源存储并访问同源 API,因此只应安装完全信任、能审查源码和发布流程的主题。 - -Pika 会校验 ZIP 路径、符号链接、文件数量、解压大小、清单、入口和预览图,但这些检查不能证明主题 JavaScript 没有恶意行为。 - -登录页和管理后台始终从官方 `/admin/assets/*` 加载;第三方主题只从 `/theme-assets/*` 加载。管理员设置的 `customCSS` 和 `customJS` 也只注入公开主题入口。 - -## 主题包格式 - -ZIP 根目录必须直接包含以下内容,不能额外套一层目录: - -```text -pika-theme.json -preview.png -dist/ -├── index.html -└── assets/ - ├── index-.js - └── index-.css -``` - -`dist/index.html` 是固定入口。所有构建资源必须使用 `/theme-assets/` 绝对前缀,并建议使用内容 hash 文件名。 - -Vite 示例: - -```ts -import {defineConfig} from 'vite'; - -export default defineConfig(({command}) => ({ - base: command === 'serve' ? '/' : '/theme-assets/', - build: { - outDir: 'dist', - assetsDir: 'assets', - }, -})); -``` - -入口 HTML 建议保留注入点: - -```html - - - - - - -
- - -``` - -服务端会在响应入口时注入 `window.PikaRuntime`,并设置站点标题、description 和 `/api/logo` favicon。没有注入标记时,会在 `` 和 `` 前注入。 - -## 清单示例 - -```json -{ - "schemaVersion": 1, - "id": "pika-minimal", - "name": "Pika Minimal", - "description": "简洁状态页", - "version": "1.2.0", - "author": "example", - "homepage": "https://github.com/example/pika-minimal", - "license": "MIT", - "preview": "preview.png", - "entry": "dist/index.html", - "apiVersion": "v1", - "capabilities": [ - "server-list", - "server-detail", - "monitor-list", - "monitor-detail" - ] -} -``` - -`id` 只能包含字母、数字、下划线和连字符,最长 64 字符;`default`、`admin`、`official`、`system`、`api`、`assets` 和 `theme-assets` 是保留名称。四项核心 capability 缺一时主题不可启用。 - -## 公开 API 与 SDK - -主题启动时可读取: - -```ts -window.PikaRuntime -``` - -也可以请求: - -```http -GET /api/config -``` - -该接口只返回公开字段,不返回 Logo Base64、自定义 CSS/JS 原文、JWT/OAuth Secret、API Key、通知或 DNS 凭据。 - -稳定 TypeScript 契约位于 `web/theme-sdk`。官方默认主题 `web/portal` 也使用同一个 SDK,通过 `pika` 客户端取得公开数据,通过 `getRuntimeConfig()` 读取注入配置。第三方主题不得引用 `web/admin` 或 `web/portal` 源码,也不应复制管理 API 或自行维护第二套请求封装。站内链接使用以 `/` 开头的绝对路径,以保证直接刷新 SPA 路由时仍由 Pika 正确回退到主题入口。 - -## 打包前检查 - -发布 ZIP 前至少确认: - -1. `pika-theme.json` 位于 ZIP 根目录,ID 与主题一致; -2. `dist/index.html` 和预览图片存在; -3. JS/CSS 引用 `/theme-assets/` 且文件名包含内容 hash; -4. `/`、`/servers/:id`、`/monitors`、`/monitors/:id` 可直接刷新; -5. 不依赖管理后台源码或未公开 API; -6. 在 light、dark、system 和移动宽度下测试。 diff --git a/docs/theme-system-refactor.md b/docs/theme-system-refactor.md deleted file mode 100644 index 658b348..0000000 --- a/docs/theme-system-refactor.md +++ /dev/null @@ -1,301 +0,0 @@ -# Pika 可安装主题系统 - -Pika 的「主题」是一套可安装的公开页面 SPA。它可以替换公开首页、服务器详情、监控列表和监控详情,但**不能**替换登录页、OAuth/OIDC 回调或管理后台。 - -本文描述当前已落地的实现形态,是代码改动的验收基线。 - -## 1. 设计取舍与安全定位 - -### 1.1 当前形态:单 origin,无主题市场 - -Pika 公开主题和管理后台运行在**同一 origin**,由单个 Pika 进程通过路由前缀区分: - -```text -/api/*、/ws/* → Pika Go API / WebSocket -/admin/* → 官方管理 SPA(web/dist/admin),不允许主题覆盖 -/admin/assets/* → 官方管理静态资源 -/theme-assets/* → 当前公开主题静态资源 -其他公开 GET 路径 → 当前公开主题入口(运行时渲染注入) -``` - -主题来源**只有一种**:管理员本地上传 ZIP。Pika 不从远程地址下载主题,也不提供主题市场。 - -**没有主题市场、没有远程下载、没有自动更新**。曾考虑过可配置的远程主题索引(市场)和 URL/GitHub Release 安装,但由于下面的安全约束,这些能力在单 origin 模型下会引入不可接受的攻击面,因此全部移除。 - -### 1.2 同源执行风险(必须理解的前提) - -第三方主题包含 JavaScript,并在 Pika 公开站点的**同一 origin** 执行。它本质上是「受信任插件」,不是无权限皮肤,也不是安全沙箱: - -- 主题可以读取浏览器同源存储(包括 localStorage 中的登录 token); -- 主题可以带着浏览器自动附加的凭证发起同源 API 请求; -- 即使把登录态从 localStorage 换成 HttpOnly Cookie,HttpOnly 只阻止「JS 读取 Cookie 值」,**不阻止**「JS 发起带凭证的认证请求」。 - -Pika 是单用户系统,登录 token 即全站最高权限。因此**恶意主题 = 管理后台被完全接管**,进而等于所有被监控服务器的信息暴露。 - -正因如此,当前的安全策略是: - -1. **不提供公开主题市场,不做远程下载**,避免「任何人都能投毒主题」的入口; -2. 主题来源限于「管理员本地上传 ZIP」,安全责任收敛到「管理员是否信任该来源」; -3. 安装、启用时强制展示「主题是同源可信代码、非沙箱」的警告并二次确认; -4. 管理后台、登录页、认证回调**始终**从官方 `/admin/assets/*` 加载,第三方主题只从 `/theme-assets/*` 加载。 - -### 1.3 未来若要开放无审核市场的前置条件 - -如果未来确实需要开放「任何人可提交」的主题市场,**必须先完成 origin 隔离**,例如: - -```text -status.example.com # 公开主题(低信任) -admin.example.com # 管理后台 + 认证(高信任) -``` - -不同 origin 后,公开主题的 JS 物理上无法读取 admin origin 的存储或发起带凭证的 admin API 请求。 - -实现方式推荐「单进程 + Host 分流」(仍是单个 Pika 二进制,按 Host 头选择返回 admin SPA 还是公开主题),而非双进程。当前代码的主题管理逻辑(安装/ZIP 安全/清单校验/渲染缓存/文件系统回滚)完全不关心 origin,origin 隔离只需要调整路由层,绝大多数代码可原样复用。 - -**在完成 origin 隔离之前,不应重新引入主题市场。** - -## 2. 目录结构 - -### 2.1 源码目录 - -```text -web/ -├── admin/ # 独立的官方管理 React/Vite 项目(base: /admin/) -├── portal/ # 独立的官方默认主题 React/Vite 项目(生产 base: /theme-assets/) -├── theme-sdk/ # 第三方主题开发契约和 TypeScript 类型 -└── scripts/ # 组装并校验服务端 Web 产物 -``` - -`admin`、`portal`、`theme-sdk` 分别维护依赖、锁文件和构建产物,根 `web` 不是 npm workspace。admin 与 portal 不共享源码,也不能相互 import;admin 不依赖 theme-sdk,portal 通过 `file:../theme-sdk` 依赖 theme-sdk。第三方主题只能依赖 `theme-sdk`。 - -### 2.2 构建产物 - -```text -web/dist/ -├── admin/ -│ ├── index.html -│ └── assets/ -└── default-theme/ - ├── pika-theme.json - ├── preview.png(或 dist/logo.png) - └── dist/ - ├── index.html - └── assets/ -``` - -`web/scripts/assemble-web.mjs` 把 admin 和 portal 各自的构建产物组装到上述结构;`web/scripts/verify-dist.mjs` 校验 admin 的 HTML 含 `/admin/assets/` 且不含 `/theme-assets/`,portal 的 HTML 含 `/theme-assets/` 且不含 `/admin/assets/`——这是防止主题与后台资源前缀串台的硬护栏。 - -### 2.3 运行时安装目录 - -```text -data/themes/ -├── pika-minimal/ -│ ├── pika-theme.json -│ ├── preview.png -│ └── dist/ -└── ... -``` - -`default` 主题不复制到 `data/themes`,始终从 `web/dist/default-theme` 读取,且不可删除或覆盖。 - -## 3. 后端模块结构 - -主题后端逻辑按职责拆分在 `internal/service/` 下,共享状态集中在 `ThemeService` 结构体: - -```text -internal/service/ -├── theme_service.go # 常量/错误/正则、ThemeService 结构体、构造函数 -├── theme_manager.go # 列表/安装/启用/删除/活动主题回退 -├── theme_manifest.go # 清单解析与字段校验 -├── theme_archive.go # ZIP 安全解压 -├── theme_filesystem.go # 默认主题访问、路径校验、启动恢复 -├── theme_renderer.go # 入口 HTML 运行时注入与缓存 -└── theme_service_test.go # 上述能力的单元测试 - -internal/models/theme.go # 清单与外观的数据模型 -internal/handler/theme_handler.go # 主题管理 HTTP handler -internal/handler/web_handler.go # 公开配置 / 静态资源 / SPA fallback -``` - -主题数据全部存放在文件系统,**不经过数据库**。`ThemeService` 不持有 `*gorm.DB`,所有操作(列表/安装/启用/删除)都是对 `data/themes` 目录的扫描与文件操作。唯一需要持久化的运行时状态是「当前启用哪个主题」,通过 `appearance_config` Property 存储。 - -## 4. 主题包协议 - -### 4.1 ZIP 结构 - -ZIP 根目录必须直接包含清单,不允许额外套一层目录: - -```text -pika-minimal.zip -├── pika-theme.json -├── preview.png -└── dist/ - ├── index.html - └── assets/ -``` - -### 4.2 `pika-theme.json` - -```json -{ - "schemaVersion": 1, - "id": "pika-minimal", - "name": "Pika Minimal", - "description": "简约服务器状态主题", - "version": "1.2.0", - "author": "example", - "homepage": "https://github.com/example/pika-minimal", - "license": "MIT", - "preview": "preview.png", - "entry": "dist/index.html", - "apiVersion": "v1", - "capabilities": ["server-list", "server-detail", "monitor-list", "monitor-detail"] -} -``` - -### 4.3 字段约束 - -- `schemaVersion`:必须为 `1`。 -- `id`:正则 `^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$`,保留名 `default`/`admin`/`official`/`system`/`api`/`assets`/`theme-assets`,大小写不敏感。 -- `version`:主题版本号,仅作展示。 -- `entry`:必须是 `dist/index.html`。 -- `apiVersion`:必须为 `v1`。 -- `capabilities`:四项核心能力 `server-list`/`server-detail`/`monitor-list`/`monitor-detail` 缺一不可。 -- `preview`:包内相对路径,最大 5 MiB。 - -## 5. 数据模型 - -主题数据全部存放在文件系统,不使用数据库表。`pika-theme.json` 是主题元数据的唯一事实来源,运行时直接扫描目录读取。唯一需要持久化的是「当前启用哪个主题」: - -```go -const PropertyIDAppearanceConfig = "appearance_config" - -type AppearanceConfig struct { - ActiveTheme string `json:"activeTheme"` // 默认 "default" - DefaultColorMode string `json:"defaultColorMode"` // 默认 "system" -} -``` - -浏览器用户的个人明暗偏好保存在浏览器本地;`defaultColorMode` 只在用户没有个人偏好时生效。 - -## 6. HTTP API - -### 6.1 公开配置 - -```http -GET /api/config -``` - -只返回公开字段(系统名称、Logo 路径、ICP、版本、默认视图、默认明暗模式、当前主题 id/version、功能开关)。不返回 Logo Base64 原文、customJS/customCSS 原文、JWT/OAuth Secret、API Key、通知渠道、DNS 凭据。 - -### 6.2 管理接口(均在 `/api/admin/themes` 下,需管理员认证) - -| 方法 | 路径 | 说明 | -|---|---|---| -| GET | `/` | 列出所有主题(含默认) | -| POST | `/upload` | 上传 ZIP 安装(multipart `file`) | -| PUT | `/:id/activate` | 启用主题(重新校验清单/入口/兼容性) | -| DELETE | `/:id` | 删除主题(default 和当前启用主题不可删) | -| GET | `/:id/preview` | 读取预览图 | - -上传新主题时 id 已存在返回 `409`;当前启用主题不可删除返回 `409`;默认主题不可删除/修改返回 `400`。 - -## 7. 静态资源与 SPA 路由 - -路由优先级(由具体前缀到通配): - -1. `/api/*`、`/ws/*`:API 与 WebSocket。 -2. `/admin/assets/*`:官方管理静态资源,禁止目录浏览/穿越/HTML5 fallback。 -3. `/theme-assets/*`:当前主题 `dist/*`,缺失文件返回 404,不逐文件回退默认主题。 -4. `/admin`、`/admin/*`:官方管理 SPA,返回管理 `index.html`(含登录与认证回调)。 -5. 公开 GET 请求:路径对应主题真实文件则返回文件;带扩展名但文件不存在返回 404;其余返回渲染后的当前主题 `index.html`;当前主题入口损坏则整体回退默认主题。 -6. 非 GET 或未知 `/api/*`:JSON 404,绝不返回 SPA HTML。 - -全局静态中间件已删除,改为显式 handler。官方和主题静态文件统一设置 `X-Content-Type-Options: nosniff` 和 `Referrer-Policy: strict-origin-when-cross-origin`;HTML 入口 `Cache-Control: no-cache`,hash 资源 `immutable`。 - -## 8. 入口 HTML 运行时渲染 - -服务端读取主题 `index.html`,在内存注入 `window.PikaRuntime`(只含公开配置),并设置 ``、`<meta description>`、`<link icon>`。主题可在 HTML 中放 `<!-- pika:head -->`/`<!-- pika:body -->` 标记控制注入点,否则注入到 `</head>`/`</body>` 前。`customCSS`/`customJS` 单独注入到公开入口(不注入管理入口)。渲染结果按 `theme id + version + index 内容 + SystemConfig/Appearance UpdatedAt + pika version` 缓存,主题切换后主动清缓存。 - -HTML 转义使用 `html/template` 和 `encoding/json`,禁止字符串拼接生成 `<script>` 内 JSON。 - -## 9. 安装、更新与回滚 - -### 9.1 安装流程 - -```text -接收 ZIP → 写入 staging → 校验大小/文件数 → 安全解压 → 校验清单/入口/预览 → 检查 id 冲突 → rename 到正式目录 → 健康检查 → 清理 staging -``` - -任一步失败删除 staging,不触碰已安装主题。 - -### 9.2 覆盖安装 - -上传的 ZIP 与已安装主题 id 相同时,走覆盖安装(`installArchiveLocked` 的 replace 模式): - -```text -取写锁 → 校验且新包 id 与目标一致 → 原目录 rename 为 .backup-* → 新目录 rename 为正式 → 健康检查 → 删 backup -``` - -健康检查失败时移走新目录、backup rename 回正式、继续用旧主题、记录日志。 - -### 9.3 启动恢复 - -进程可能在原子替换期间被杀,启动时执行:清理 24h 以上的 staging;唯一有效 `.backup-*` 恢复;正式与备份并存时以校验通过的正式为准;状态不明则保留全部并告警,活动主题回退默认。 - -### 9.4 活动主题回退 - -`appearance_config.activeTheme` 为空、目录缺失、清单损坏、入口缺失时,自动回退默认主题并修正配置(用 `fallbackMu` 串行化,避免每个请求都写库)。 - -## 10. ZIP 安全 - -### 10.1 硬上限(代码内固定,不可配置关闭) - -```text -压缩包最大 64 MiB | 最大文件数 5000 | 单文件最大 64 MiB | 解压总大小 256 MiB | 清单 1 MiB | 预览图 5 MiB -``` - -### 10.2 路径与文件类型防御 - -ZIP Slip(`../`)、绝对路径、Windows 盘符、反斜杠、符号链接、硬链接、设备文件、命名管道、大小写重复、Unicode 归一化重复、清单路径越界、文件数/总大小溢出。只解压普通文件和目录,权限归一化为目录 0755 / 文件 0644。 - -## 11. 前端构建 - -- `web/admin`:`base: '/admin/'`,独立 React + antd 项目,自带 HTTP 客户端和 `ColorModeContext`,不依赖 theme-sdk。 -- `web/portal`:`base: 生产 '/theme-assets/' / 开发 '/'`,默认主题,所有数据访问经 `@pika-monitor/theme-sdk` 的 `pika` 客户端。 -- `web/theme-sdk`:导出 `pika` 客户端、`PikaRuntimeConfig`/`PikaThemeManifest` 等类型、`getRuntimeConfig`/`resolveColorMode` 工具。源码即产物(`tsc --noEmit` 仅类型检查)。 - -`make build-web` 依次构建 theme-sdk(类型检查)→ admin → portal,再 assemble + verify。 - -## 12. 管理后台交互 - -「系统设置 → 主题管理」包含: - -- 当前主题信息、已安装主题卡片(预览图、名称、版本、作者、兼容状态); -- 启用、删除; -- 上传 ZIP; -- 所有安装/启用统一经「主题是同源可信代码、非沙箱」的红色二次确认 Modal; -- 页面顶部常驻安全提示。 - -按钮约束:当前主题不显示删除;默认主题不显示删除;不兼容主题可查看可删除但不能启用;操作期间禁止重复提交;启用成功不刷新管理 SPA,只提示公开页面已切换。 - -## 13. 配置 - -```yaml -App: - Theme: - Dir: "./data/themes" -``` - -- `PIKA_THEME_DIR` 非空时覆盖 `Dir`; -- SQLite 与 PostgreSQL Docker 部署都必须持久化 `/app/data`。 - -## 14. 测试覆盖 - -Go 单元测试覆盖:ZIP 安全(slip/绝对路径/盘符/symlink/大小写·Unicode 重复/文件数超限)、清单校验(保留 id/非法 schema·api/缺 capability)、HTML 元数据转义、文件系统恢复、完整安装/启用/删除生命周期。 - -## 15. 不做的事(明确边界) - -- 不提供主题市场、不做远程下载、不做自动更新(见 §1.1); -- 不做 origin 隔离(当前单进程单 origin;未来开放市场的前置条件,见 §1.3); -- 不改前端 admin/portal/theme-sdk 的项目结构和构建。