按类型开发指南

主题(theme)开发指南

主题包是声明式的:设计令牌、品牌图片、布局槽位和无逻辑模板。主控把它渲染成同源的静态 CSS/JSON,不带脚本、不改任何 CSP。

← 全部开发指南

快速开始

  1. 新建插件,类型选 theme,插件 ID 如 acme.dev.ocean。
  2. 写 theme.json:id 必须是 market.<插件 ID>,version 与插件版本相同。
  3. 把 theme.json 的 SHA-256 写进 spec.entrypoint.sha256,权限只写 platform.themes.provide,签名并上传。
{
  "schema": "cdnca.theme/v1",
  "id": "market.acme.dev.ocean",
  "version": "1.0.0",
  "name": {"zh-CN": "海蓝", "en-US": "Ocean"},
  "description": {"zh-CN": "蓝色系主控外观"},
  "tokens": {"colorPrimary": "#1769aa", "borderRadius": 6},
  "layout_slots": {"login_layout": "centered", "sidebar": "dark"},
  "templates": {
    "styles": [{"selector": ".cf-page", "declarations": {"background-color": "token:colorBgContainer"}}],
    "error_page": "<!doctype html><html><head><title>{{title}}</title></head><body><main class=\"cf-page\"><h1>{{status}} {{title}}</h1><p>{{message}}</p></main></body></html>",
    "email": "<!doctype html><html><head><title>{{subject}}</title></head><body><main class=\"cf-page\">{{#lines}}<p>{{.}}</p>{{/lines}}</main></body></html>"
  }
}

签名与公钥登记见插件签名工具页。 签名工具与步骤 →

manifest 示例与字段

manifest 与规则包相同,只有下表这些字段不同。

字段要求
spec.entrypoint{"type": "theme", "path": "theme.json", "sha256": "<theme.json 的 SHA-256>"}
spec.permissions恰好一个:platform.themes.provide
spec.categories通常为 ["theme"]
metadata.version纯 major.minor.patch,每段最多 5 位数字

协议与接口

字段说明
schema / id / versioncdnca.theme/v1;market.<插件 ID>;与插件版本相同
name / description必须有 zh-CN,可加 zh-TW、en-US;名称最多 64 字,说明最多 512 字
tokens白名单令牌(颜色、长度、数字、字体栈、阴影),未列出的令牌名一律拒绝;长度与数字有范围,最多两位小数
tokens_by_role只对 admin 或 user 控制台覆盖令牌
brand_slotsfavicon(image/x-icon、image/vnd.microsoft.icon 或 image/png)、logo / logo_dark / app_icon(image/png)、login_ad / login_background(image/jpeg);body_base64 为标准 base64,每张最多 2 MiB,文件头必须与 MIME 一致
layout_slotslogin_layout:split / centered / background;sidebar:light / dark;可选 look:classic / tech、motion:off / subtle / rich、最多 8 个 header_links(只能 https)、footer_text、user_home_blocks
templatesstyles(选择器 + 声明,值可写 token:<令牌名>)、error_page、email;模板只支持 {{name}}、{{#name}}…{{/name}}、{{^name}}…{{/name}},错误页变量 status、title、message、footer_text,邮件变量 subject、lines(列表)、footer_text

任何字符串里出现 url(、@import、expression(、javascript:、<script、<link、<iframe、<object、<embed、src=、href=、srcset= 都会被拒绝(header_links 的 url 与 base64 图片除外)。主题包最大 8 MiB。

打包与上传字段

表单字段必填内容与校验
manifest是签名后的 manifest,1 字节到 256 KiB;身份必须是本插件 ID、本命名空间,integrity.key_id 必须是该命名空间 active 的公钥并验签通过
configuration_schema是配置 JSON Schema 文件,1 字节到 64 KiB;SHA-256 必须等于 spec.configuration_schema.sha256
changelog_md否更新说明 Markdown,最多 65536 字节
channel否stable(默认)或 beta
auto_publish否true / false:审核通过后自动上架
content是theme.json 本身,1 字节到 8 MiB,SHA-256 等于 spec.entrypoint.sha256
package:<platform>不能有theme 版本不带可执行包

接口:POST /seller/v1/plugins/{id}/versions,Content-Type: multipart/form-data,必须带一个 1–128 字符的 Idempotency-Key 请求头;同一 key 重试返回同一结果。请求体默认上限 200 MiB。除上表与本类型专属字段外,出现任何其他字段名都返回 400 invalid_upload。

同一插件的版本号不能重复,且必须高于任何已审核过的版本(已通过、已上架、已隐藏或已下架)。上传成功后版本进入“预检中”,卖家站版本详情显示预检报告。

审核规则

上传后审核 worker 先跑“校验”阶段(manifest、入口、权限、本类型专属检查),出现任何拒绝项就不再扫描;再跑“扫描”阶段。结论:有拒绝项 → 需修改(修好后点“重新提交”,每个版本最多 10 次扫描);有隔离项 → 待人工审核;都没有 → 视下列条件直接通过或进入人工审核。

  • 首个版本(该插件还没有任何已审核版本)
  • 权限与上一个已审核版本不同(增加或减少)
  • 高风险类型:module、engine_module、provider
  • 入驻未满 90 天的卖家(市场默认值,运营可调)
  • 扫描连续 3 次出错

满足任一条即进入人工审核。完整规则表(规则 ID、级别、拒绝或隔离、是否试运行)见审核规则页。

  • theme.invalid:不是合法的 cdnca.theme/v1(令牌、槽位、模板、外部引用),detail 里有 code 和 field(拒绝)。
  • theme.id_mismatch:id 不是 market.<插件 ID>(拒绝)。
  • theme.version_mismatch:主题 version 与插件版本不同(拒绝)。

本类型相关的规则见审核规则清单: metadata.* entrypoint.* permissions.* theme.*

版本与兼容

metadata.version 是规范 SemVer(不带前导 v),每次上传必须比已审核版本高;同一版本号不能重复上传。

spec.compatibility.control_api 是 1–8 个以空格分隔的比较式(>=、<=、=、>、<,后接规范 SemVer),总长不超过 128;全部满足才算兼容。不满足的主控不能安装或升级到该版本,已安装的保持最近兼容版本。

"compatibility": {"control_api": ">=1.0.35 <2.0.0"}

新版本增加权限时,客户管理员在主控里要重新确认;权限变化的版本一律人工审核。

主题 version 必须等于插件版本。客户启用新版本主题后,主控会为所有租户重新发布错误页。

本地调试

审核用的就是主控的渲染器 libs/themepack 的 Render。在仓库根目录用它预检,报错的 code / field 与审核报告一致:

mkdir -p libs/themepack/cmd/thc && cat > libs/themepack/cmd/thc/main.go <<'EOF'
package main

import (
	"fmt"
	"os"

	"github.com/cdnca/cdnca/libs/themepack"
)

func main() {
	raw, _ := os.ReadFile(os.Args[1])
	r, err := themepack.Render(raw)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println(r.Package.ID, r.Package.Version, r.PackageSHA256)
}
EOF
GOWORK=off go -C libs/themepack run ./cmd/thc "$PWD/theme.json"

官方内置主题 libs/themepack/builtin/*.json 可作为完整写法参考(id 需改成 market.<你的插件 ID>)。