开发文档与接口查询
这套系统的完整开发资料:从快速上手、数据模型、全部 API 接口,到扩展体系(主题 / 模块 / 插件的打包、签名、授权与分发)、错误契约与工程门禁。接口、模型、页面与组件清单由源码扫描生成,永远等于当前代码。
- 128
- 接口
- 27
- 数据模型
- 47
- 页面路由
- 46
- 全局组件
快速开始
需要 Node.js ≥ 20、PostgreSQL、Redis。全部依赖都在 package.json 里,没有私有源依赖。
git clone <仓库地址> && cd www.hejun1.com
npm ci
cp .env.example .env # 按注释填 DATABASE_URL / REDIS_URL / NUXT_PUBLIC_SITE_URL
npx prisma db push # 建表
npm run db:seed # 写入管理员、四套系统、FAQ 等初始数据
npm run dev # http://localhost:3000初始管理员账号与密码由部署时的环境变量决定,首次登录后请立即修改。 后台地址与账号信息不在这里公开 —— 这部分属于交付方与站主之间的运维信息, 不写进公开文档页。
可用命令
| npm run dev | nuxt dev |
| npm run gate | node scripts/gate-compile.mjs |
| npm run gate:types | node scripts/gate-typecheck.mjs |
| npm run gate:auto-imports | tsx scripts/gate-auto-imports.ts |
| npm run log:500 | powershell -NoProfile -Command "Select-String '\[500\]' .dev.log | Select-Object -Last 20" |
| npm run prebuild | npm run gate && npm run gate:types && npm run gate:auto-imports && npm run engine:spec && node scripts/generate-dev-docs.mjs && node scripts/gen-search-pinyin.mjs |
| npm run build | nuxt build |
| npm run generate | nuxt generate |
| npm run preview | nuxt preview |
| npm run postinstall | prisma generate |
| npm run db:push | prisma db push |
| npm run db:pull | prisma db pull |
| npm run db:studio | prisma studio |
| npm run db:seed | tsx prisma/seed.ts |
| npm run db:doctor | node scripts/check-migrations.cjs |
| npm run og:build | node scripts/generate-og-image.mjs |
| npm run docs:build | npm run engine:spec && node scripts/generate-dev-docs.mjs && npm run docs:guide |
| npm run docs:guide | tsx scripts/generate-dev-guide.ts |
| npm run api:diff | tsx scripts/api-changelog.ts diff |
| npm run api:changelog | tsx scripts/api-changelog.ts write |
| npm run engine | tsx scripts/engine.ts |
| npm run engine:list | tsx scripts/engine.ts list |
| npm run ci:validate | tsx scripts/engine.ts validate |
| npm run engine:validate | tsx scripts/engine.ts validate |
| npm run engine:spec | tsx scripts/engine.ts spec |
| npm run engine:gc | tsx scripts/engine.ts gc |
| npm run engine:new | tsx scripts/engine.ts new |
| npm run engine:pack | tsx scripts/engine.ts pack |
| npm run engine:publish | tsx scripts/engine-publish.ts |
| npm run engine:keygen | tsx scripts/engine.ts keygen |
| npm run engine:remove | tsx scripts/engine.ts remove |
| npm run engine:schema | tsx scripts/engine.ts schema |
| npm run engine:license | tsx scripts/engine.ts license |
| npm run typecheck | nuxt typecheck |
| npm run test | vitest run |
| npm run audit | node scripts/site-audit.mjs |
| npm run audit:full | node scripts/site-audit.mjs --with-static |
技术栈
选型原则是「一个人也能长期维护」:不要微服务、不要自建 SSR 框架、不引入需要专职运维的中间件。
运行时依赖
- @nuxt/image^1.11.0
- @nuxt/kit^4.5.2
- @nuxt/ui^3.2.0
- @pinia/nuxt^0.7.0
- @prisma/client^6.5.0
- @vue/compiler-sfc^3.5.43
- bcryptjs^2.4.3
- esbuild0.28.2
- h3^2.0.1-rc.32
- ioredis^5.4.2
- nanotar^0.3.0
- nodemailer^6.10.0
- nuxt^4.0.0
- pinia^4.0.3
- vue^3.5.13
- zod^3.24.2
开发与构建依赖
- @iconify-json/lucide^1.2.140
- @iconify-json/mdi^1.2.3
- @iconify-json/simple-icons^1.2.99
- @types/bcryptjs^2.4.6
- @types/nodemailer^6.4.17
- pinyin-pro^3.29.4
- prisma^6.5.0
- sharp^0.35.4
- tailwindcss^4.1.0
- tsx^4.19.2
- typescript^5.7.0
- vitest^2.1.8
- vue-tsc^3.3.11
目录结构
| 目录 | 文件数 | 用途 |
|---|---|---|
| assets/ | 2 | 全局样式(main.css 里是 .bm-* 设计系统与 Tailwind 主题) |
| components/ | 46 | 全局组件,Nuxt 自动导入,模板里直接写组件名即可 |
| composables/ | 19 | 组合式函数,自动导入(useSeo / useSiteSearch / useAuth 等) |
| engines/ | 357 | — |
| layouts/ | 3 | 布局:default 前台、admin 后台(含侧边栏与统一 noindex) |
| middleware/ | 1 | 前端路由中间件(admin 鉴权跳转) |
| modules/ | 1 | — |
| pages/ | 47 | 文件式路由,目录结构即 URL 结构 |
| plugins/ | 5 | Nuxt 插件(客户端动效、Web Vitals、SEO 配置预取等) |
| prisma/ | 13 | 数据库 schema 与种子数据 |
| public/ | 8 | 静态资源(logo、默认分享图、开发文档 JSON) |
| scripts/ | 32 | 构建期脚本(分享图生成、开发文档生成) |
| server/ | 254 | 服务端:api 接口 / middleware 鉴权 / routes(sitemap、robots)/ utils / tasks |
| stores/ | 0 | Pinia 状态(目前仅站点配置) |
| tests/ | 60 | 单元测试(vitest) |
| types/ | 1 | 全局类型声明 |
数据模型
来自 prisma/schema.prisma。 改结构后执行 npx prisma db push 同步到数据库。
AdminUser管理员用户36 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| username | String | 是 | |
| passwordHash | String | 是 | |
| name | String | 是 | |
| roleId | String | 是 | |
| role | AdminRole | 是 | 关联字段 |
| rememberMe | Boolean | 是 | |
| /* | ===== | 是 | |
| 下面三组字段回答的是同一个问题:**这个账号能碰什么**。 | undefined | 是 | |
| ⚠️ | 【空 | 是 | |
| (沿用角色的全部权限)✓。理由:站主给自己账号加了限制、或者批量给老账号一个默认值, | undefined | 是 | |
| 都可能**把自己关在外面** | ✓ | 是 | |
| /* | 找回密码的两个投递地址 | 是 | |
| 它挡的是"两个管理员写同一个手机号 | ⇒ | 是 | |
| 可空列的多 | NULL | 是 | |
| String? | 否 | ||
| phone | String? | 否 | |
| /* | active | 是 | |
| status | String | 是 | |
| /* | 操作权限(站主要的那三项:新增 | 是 | |
| 存的是 | CREATE | 是 | |
| opAllow | String[] | 是 | 关联字段 |
| /* | 指定栏目:**内容板块**(页面内容 | 是 | |
| 取值见 | server/utils/admin/scopes.ts | 是 | |
| columnScope | String[] | 是 | 关联字段 |
| /* | 指定功能:**后台功能模块**(站点配置 | 是 | |
| 取值见同一文件的 | FEATURE_SCOPES | 是 | |
| featureScope | String[] | 是 | 关联字段 |
| /* | 指定语言。⚠️ | 是 | |
| 这一维现在**先落库**,存的是多语言功能开放后要用的判据(现在还没有任何出口读它)—— | 界面上必须写明这一点 | 是 | |
| 不许让人以为设了就有用 | ✓。 | 是 | |
| localeScope | String[] | 是 | 关联字段 |
| lastLoginAt | DateTime? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 | |
| operationLogs | OperationLog[] | 是 | 关联字段 |
AdminRole角色权限 RBAC11 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| /* | 角色标识:**登录令牌里存的就是它**(见 | 是 | |
| name | String | 是 | |
| /* | 显示名(中文,界面上给站主看)。英文名 | 是 | |
| label | String? | 否 | |
| permissions | String[] | 是 | 关联字段 |
| /* | 内置组排在前面(顺序即选项顺序,稳定 | 是 | |
| sort | Int | 是 | |
| users | AdminUser[] | 是 | 关联字段 |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
PageContent页面静态内容(首页、关于、共建等;基座 M3 起支持后台新建与发布状态)11 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| pageKey | String | 是 | 页面标识:同时是路由 /{pageKey} 与导航候选 |
| title | String | 是 | |
| summary | String? | 否 | 一句话摘要(列表与 SEO description 用) |
| content | String | 是 | |
| published | Boolean | 是 | |
| settings | Json? | 否 | |
| locale | String? | 否 | |
| blocks | Json? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
MediaAsset媒体库(基座 M3):上传即登记,删除前查引用(产品图集/页面内容/SEO 分享图/站点配置)7 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| url | String | 是 | /uploads/xxx.png |
| mime | String | 是 | |
| size | Int | 是 | 字节 |
| alt | String? | 否 | |
| uploaderId | String? | 否 | |
| createdAt | DateTime | 是 |
Form防灌水:提交接口带蜜罐字段 _website(前台表单隐藏填写,机器人会填)+ 按 IP 限频。8 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| key | String | 是 | 提交接口 /api/forms/{key}/submit |
| name | String | 是 | |
| fields | Json | 是 | |
| enabled | Boolean | 是 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 | |
| submissions | FormSubmission[] | 是 | 关联字段 |
SiteConfig否则会随公开配置一起下发到浏览器。5 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| key | String | 是 | |
| value | String? | 否 | |
| description | String? | 否 | |
| updatedAt | DateTime | 是 |
SeoPageSetting单页 SEO 覆盖(后台「SEO 设置」按页面维护;留空则用页面上写死的默认值)9 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| pageKey | String | 是 | 站点内的规范路径:/ , /about , /systems ... |
| title | String | 是 | |
| description | String | 是 | |
| keywords | String | 是 | |
| robots | String | 是 | 留空表示沿用页面默认(通常 index, follow) |
| ogImage | String | 是 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
SeoPushLog搜索引擎推送记录(每次主动推送留痕,便于判断是否被收录前先确认推送成功)6 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| scope | String | 是 | all / urls / auto |
| urlCount | Int | 是 | |
| engines | String | 是 | 各引擎结果的 JSON 摘要 |
| ok | Boolean | 是 | |
| createdAt | DateTime | 是 |
NavItem(server/utils/nav-defaults.ts),所以「从没配置过导航」与「今天的站点」完全一致。11 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| scope | String | 是 | main / footer / admin |
| parentId | String? | 否 | 二级菜单挂在一级的 id 上(只支持两层) |
| label | String | 是 | |
| href | String | 是 | |
| description | String? | 否 | 下拉里的说明文字(可选) |
| icon | String? | 否 | 下拉卡片里的单字标识(可选) |
| sort | Int | 是 | |
| enabled | Boolean | 是 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
EnginePackage文件清单以构建期扫描结果为准(避免两边不一致时不知道该信谁)。11 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| type | String | 是 | template / module / plugin |
| key | String | 是 | 包的 id(目录名,与 manifest.id 一致) |
| name | String | 是 | |
| version | String | 是 | |
| enabled | Boolean | 是 | |
| settings | Json? | 否 | |
| signerKeyId | String? | 否 | |
| installedFrom | String? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
EngineTrustedKey吊销必须能随手做且立刻生效,环境变量做不到;环境变量保留为首次引导。9 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| keyId | String | 是 | |
| publicKey | String | 是 | |
| author | String | 是 | |
| note | String? | 否 | |
| source | String | 是 | |
| revokedAt | DateTime? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
EngineLicense两者不一致时以 raw 为准 —— 解析字段只是缓存,不参与判定。14 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| type | String | 是 | |
| key | String | 是 | |
| raw | String | 是 | |
| licensedTo | String? | 否 | |
| domain | String? | 否 | |
| major | String? | 否 | |
| expiresAt | String? | 否 | |
| issuedAt | String? | 否 | |
| issuerKeyId | String? | 否 | |
| uploadedBy | String? | 否 | |
| note | String? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
OperationLog管理员操作日志12 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| /* | 【2026-10-07 | 是 | |
| 任何登录过的管理员都会留下 | `login` | 是 | |
| 表现是"删除按钮点了报错",而真正的原因是"他登录过" | ✓ | 是 | |
| 现在:**删账号不再删日志** | ✓(`detail` | 是 | |
| 审计要的是"发生过什么",不是"那个用户名还在不在" | ✓。 | 是 | |
| adminUserId | String? | 否 | |
| adminUser | AdminUser? | 否 | 关联字段 |
| action | String | 是 | |
| detail | String? | 否 | |
| ip | String? | 否 | |
| createdAt | DateTime | 是 |
WebVitalRecordWeb Vitals性能指标记录6 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| metric | String | 是 | LCP / INP / CLS |
| value | Float | 是 | |
| pagePath | String | 是 | |
| userAgent | String? | 否 | |
| createdAt | DateTime | 是 |
ProductLog版本日志(按产品分组)8 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| productId | String | 是 | |
| product | Product | 是 | 关联字段 |
| version | String | 是 | |
| updateType | String | 是 | 新增 / 优化 / 修复 |
| content | String | 是 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
DemoApplication系统演示申请 / 开通记录10 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| productId | String | 是 | |
| product | Product | 是 | 关联字段 |
| leadName | String | 是 | |
| contact | String | 是 | |
| tempAccount | String? | 否 | |
| expireAt | DateTime? | 否 | |
| status | String | 是 | pending / active / expired |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
Faq常见问题(FAQ):后台可维护,/faq 页与「联合共建」页共用同一数据源9 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| question | String | 是 | |
| answer | String | 是 | |
| category | String | 是 | 前台按分类分组展示 |
| sortWeight | Int | 是 | 越大越靠前 |
| status | Boolean | 是 | true 前台显示 false 隐藏 |
| deletedAt | DateTime? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
Lead表里只存字符串 —— 状态集合是业务约定,不是数据库约束。13 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| name | String | 是 | |
| company | String? | 否 | |
| contact | String | 是 | |
| memberId | String? | 否 | |
| member | Member? | 否 | 关联字段 |
| interestSlug | String? | 否 | |
| description | String? | 否 | |
| status | String | 是 | new / contacted / demo / negotiating / closed / archived |
| remark | String? | 否 | |
| deletedAt | DateTime? | 否 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
Tag产品标签:只用于给产品分类,所以与产品同属一个模块6 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| name | String | 是 | |
| slug | String | 是 | |
| products | Product[] | 是 | 关联字段 |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
Product自研业务系统(产品)22 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| name | String | 是 | |
| slug | String | 是 | |
| shortDescription | String | 是 | |
| description | String | 是 | |
| painPoints | String | 是 | |
| targetCustomer | String | 是 | |
| features | String[] | 是 | 关联字段 |
| imageGallery | String[] | 是 | 关联字段 |
| deliveryType | String | 是 | |
| deploymentMethod | String | 是 | |
| status | Boolean | 是 | true上线 false隐藏 |
| sortWeight | Int | 是 | |
| seoTitle | String | 是 | |
| seoDescription | String | 是 | |
| deletedAt | DateTime? | 否 | |
| deletedSlug | String? | 否 | |
| tags | Tag[] | 是 | 关联字段 |
| changelogs | ProductLog[] | 是 | 关联字段 |
| demoApplications | DemoApplication[] | 是 | 关联字段 |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 |
Member空串是一个**值**,两个空串就会撞唯一索引。10 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| String? | 否 | ||
| phone | String? | 否 | |
| passwordHash | String | 是 | |
| name | String | 是 | |
| locale | String? | 否 | |
| status | String | 是 | |
| createdAt | DateTime | 是 | |
| updatedAt | DateTime | 是 | |
| leads | Lead[] | 是 | 关联字段 |
Term现有 ProductTag 数据由迁移脚本搬到这里(Term + TermLink),Tag 模型保留待模块下个版本切换。6 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| taxonomy | String | 是 | |
| name | String | 是 | |
| slug | String | 是 | |
| sort | Int | 是 | |
| links | TermLink[] | 是 | 关联字段 |
TermLink分类与内容的通用关联:不外联具体表(targetType + targetId),一张表服务全部内容类型。5 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| termId | String | 是 | |
| term | Term | 是 | 关联字段 |
| targetType | String | 是 | |
| targetId | String | 是 |
SiteTextkey 清单见 composables/text-registry.ts。4 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | String | 是 | |
| value | String | 是 | |
| note | String | 是 | |
| updatedAt | DateTime | 是 |
SearchTerm· 不做任何用户身份记录(没有 IP、没有会员 id):这里统计的是"词",不是"人"。4 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| term | String | 是 | |
| count | Int | 是 | |
| lastAt | DateTime | 是 |
Notification/ `audience` 区分收件人类型:`admin` = 后台管理员(当前唯一在用)、`member` = 会员(预留)。10 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| audience | String | 是 | admin / member |
| adminUserId | String? | 否 | |
| memberId | String? | 否 | |
| event | String | 是 | |
| title | String | 是 | |
| body | String | 是 | |
| level | String | 是 | info / warn |
| readAt | DateTime? | 否 | |
| createdAt | DateTime | 是 |
NotifyLog/ `target` 存收件人标识,但**只留尾部**(邮箱/手机号是个人信息,别整串进库与日志 ✗)。11 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | String | 是 | |
| event | String | 是 | |
| channel | String | 是 | |
| target | String | 是 | |
| ok | Boolean | 是 | |
| attempts | Int | 是 | |
| error | String | 是 | |
| durationMs | Int | 是 | |
| title | String | 是 | |
| body | String | 是 | |
| createdAt | DateTime | 是 |
API 接口
共 128 个,其中 95 个需要登录。 公开 接口前台可直接调用;JWT 需要登录态;RBAC 还要求角色具备对应权限点 (权限在后台「角色权限」里分配)。 说明取自源码注释:没写注释的接口不显示说明,只列出路径、鉴权与所在文件。
/api/admin/backup6 个
- GET
/api/admin/backupRBACbackup:manage备份中心:列表 + 体检(2026-10-07 · P1「安全与效率」第 1 条) 【权限】`backup:manage`(**GET 也要登记** ✓ —— 这个列表一按"下载"就是全站数据,
server/api/admin/backup/index.get.ts
- DELETE
/api/admin/backup/:nameJWT删除一份备份(2026-10-07 · P1) ⚠️ 删掉的是**站点数据的一份拷贝** ⇒ 写操作日志 ✓,且**连带删 meta**(不留孤儿文件 ✗)。
路径参数nameserver/api/admin/backup/[name].delete.ts
- POST
/api/admin/backup/createJWT生成一份备份(2026-10-07 · P1) 【单飞】同一时刻只允许一个备份在跑 ✗ ——
server/api/admin/backup/create.post.ts
- GET
/api/admin/backup/downloadJWT下载备份文件到本地(2026-10-07 · P1) 【为什么必须流式】备份里可能装的是几百 MB 的上传文件夹 ✗ ——
server/api/admin/backup/download.get.ts
- POST
/api/admin/backup/restoreRBACbackup:manage上传一份备份并**预览**(第一步:只看不动 ✓ —— 2026-10-07 · P1) 【分两步,为什么】恢复会**覆盖全站数据** ⇒ 必须先让站主看见"这份包里到底有什么、要覆盖什么",
server/api/admin/backup/restore.post.ts
- POST
/api/admin/backup/restore-commitRBACbackup:restore确认执行恢复(第二步:真的写 ✗ —— 2026-10-07 · P1) 【权限】独立权限点 `backup:restore` ✓ —— "备份"与"恢复(覆盖全站数据)"不是同一件事 ✗
server/api/admin/backup/restore-commit.post.ts
/api/admin/blocks1 个
- GET
/api/admin/blocksJWTserver/api/admin/blocks.get.ts
/api/admin/content-snippets2 个
- GET
/api/admin/content-snippetsJWTserver/api/admin/content-snippets.get.ts
- PUT
/api/admin/content-snippetsRBACpage:editserver/api/admin/content-snippets.put.ts
/api/admin/dashboard1 个
- PUT
/api/admin/dashboardRBACsite:config请求体:hiddenCards?:array[]hiddenShortcuts?:array[]cardOrder?:array[]shortcutOrder?:array[]server/api/admin/dashboard.put.ts
/api/admin/engines16 个
- GET
/api/admin/enginesJWTserver/api/admin/engines.get.ts
- DELETE
/api/admin/engines/:idJWT路径参数idserver/api/admin/engines/[id].delete.ts
- PUT
/api/admin/engines/:idJWT路径参数id请求体:type:enumenabled?:booleansettings?:recordserver/api/admin/engines/[id].put.ts
- DELETE
/api/admin/engines/:id/licenseJWT路径参数idserver/api/admin/engines/[id]/license.delete.ts
- PUT
/api/admin/engines/:id/licenseRBACengine:install路径参数id请求体:type:enumlicense:stringserver/api/admin/engines/[id]/license.put.ts
- POST
/api/admin/engines/:id/rollbackRBACengine:install路径参数id请求体:type:enumname:stringserver/api/admin/engines/[id]/rollback.post.ts
- POST
/api/admin/engines/disable-third-partyRBACengine:installserver/api/admin/engines/disable-third-party.post.ts
- POST
/api/admin/engines/installRBACengine:installserver/api/admin/engines/install.post.ts
- GET
/api/admin/engines/marketJWTserver/api/admin/engines/market.get.ts
- POST
/api/admin/engines/reloadJWTserver/api/admin/engines/reload.post.ts
- GET
/api/admin/engines/themeJWTserver/api/admin/engines/theme.get.ts
- PUT
/api/admin/engines/themeJWT请求体:id?:stringvalues?:recordserver/api/admin/engines/theme.put.ts
- GET
/api/admin/engines/trustRBACengine:installserver/api/admin/engines/trust.get.ts
- POST
/api/admin/engines/trustRBACengine:install请求体:publicKey:stringauthor?:stringnote?:stringserver/api/admin/engines/trust.post.ts
- DELETE
/api/admin/engines/trust/:keyIdJWT路径参数keyIdserver/api/admin/engines/trust/[keyId].delete.ts
- PUT
/api/admin/engines/trust/:keyIdJWT路径参数keyId请求体:action:enumserver/api/admin/engines/trust/[keyId].put.ts
/api/admin/forgot1 个
- POST
/api/admin/forgotJWT请求体:username:stringchannel:enumserver/api/admin/forgot.post.ts
/api/admin/forms6 个
- GET
/api/admin/formsRBACform:manageserver/api/admin/forms.get.ts
- POST
/api/admin/formsRBACform:manage请求体:key:unknownname:stringfields:unknownenabled?:booleanserver/api/admin/forms.post.ts
- DELETE
/api/admin/forms/:idJWT路径参数idserver/api/admin/forms/[id].delete.ts
- PUT
/api/admin/forms/:idJWT路径参数id请求体:name?:stringfields?:unknownenabled?:booleanserver/api/admin/forms/[id].put.ts
- GET
/api/admin/forms/:id/exportJWT路径参数idserver/api/admin/forms/[id]/export.get.ts
- GET
/api/admin/forms/:id/submissionsJWT路径参数idserver/api/admin/forms/[id]/submissions.get.ts
/api/admin/friend-links2 个
- GET
/api/admin/friend-linksJWTserver/api/admin/friend-links.get.ts
- PUT
/api/admin/friend-linksRBACnav:manageserver/api/admin/friend-links.put.ts
/api/admin/home-content2 个
- GET
/api/admin/home-contentJWTserver/api/admin/home-content.get.ts
- PUT
/api/admin/home-contentRBACpage:edit请求体:content:recordserver/api/admin/home-content.put.ts
/api/admin/login1 个
- POST
/api/admin/loginJWTserver/api/admin/login.post.ts
/api/admin/logout1 个
- POST
/api/admin/logoutJWTserver/api/admin/logout.post.ts
/api/admin/me2 个
- GET
/api/admin/meJWTserver/api/admin/me.get.ts
- POST
/api/admin/me/passwordJWT请求体:currentPassword:stringnewPassword:stringserver/api/admin/me/password.post.ts
/api/admin/media3 个
- GET
/api/admin/mediaJWTserver/api/admin/media.get.ts
- DELETE
/api/admin/media/:idJWT路径参数idserver/api/admin/media/[id].delete.ts
- PUT
/api/admin/media/:idJWT路径参数id请求体:alt?:stringserver/api/admin/media/[id].put.ts
/api/admin/members4 个
- GET
/api/admin/membersRBACmember:manageserver/api/admin/members/index.get.ts
- GET
/api/admin/members/:idJWT路径参数idserver/api/admin/members/[id].get.ts
- PUT
/api/admin/members/:idJWT路径参数idserver/api/admin/members/[id].put.ts
- GET
/api/admin/members/exportJWTserver/api/admin/members/export.get.ts
/api/admin/metrics1 个
- GET
/api/admin/metricsJWTserver/api/admin/metrics.get.ts
/api/admin/nav5 个
- GET
/api/admin/navJWTserver/api/admin/nav.get.ts
- POST
/api/admin/navRBACnav:manage请求体:scope:enumlabel:stringhref:stringparentId?:stringdescription?:stringicon?:stringsort?:numberenabled?:booleanserver/api/admin/nav.post.ts
- DELETE
/api/admin/nav/:idJWT路径参数idserver/api/admin/nav/[id].delete.ts
- PUT
/api/admin/nav/:idJWT路径参数id请求体:label?:stringhref?:stringparentId?:stringdescription?:stringicon?:stringsort?:numberenabled?:booleanserver/api/admin/nav/[id].put.ts
- POST
/api/admin/nav/:id/moveJWT路径参数id请求体:dir:enumwhere:unknownorderBy:unknownserver/api/admin/nav/[id]/move.post.ts
/api/admin/notify7 个
- POST
/api/admin/notify/broadcastRBACnotify:broadcastserver/api/admin/notify/broadcast.post.ts
- GET
/api/admin/notify/channelsJWTserver/api/admin/notify/channels.get.ts
- GET
/api/admin/notify/logsJWTserver/api/admin/notify/logs.get.ts
- POST
/api/admin/notify/logs/:id/retryJWT路径参数idserver/api/admin/notify/logs/[id]/retry.post.ts
- GET
/api/admin/notify/messagesJWTserver/api/admin/notify/messages.get.ts
- PATCH
/api/admin/notify/messagesJWTserver/api/admin/notify/messages.patch.ts
- POST
/api/admin/notify/testJWTserver/api/admin/notify/test.post.ts
/api/admin/operation-logs1 个
- GET
/api/admin/operation-logsRBAClog:viewserver/api/admin/operation-logs.get.ts
/api/admin/pages4 个
- GET
/api/admin/pagesJWTserver/api/admin/pages.get.ts
- POST
/api/admin/pagesRBACpage:edit请求体:pageKey:stringtitle:stringsummary?:stringcontent?:stringpublished?:booleanserver/api/admin/pages.post.ts
- DELETE
/api/admin/pages/:keyJWT路径参数keyserver/api/admin/pages/[key].delete.ts
- PUT
/api/admin/pages/:keyJWT路径参数key请求体:title?:stringsummary?:stringcontent?:stringblocks:array[]settings?:recordpublished?:booleanserver/api/admin/pages/[key].put.ts
/api/admin/performance1 个
- GET
/api/admin/performanceJWTserver/api/admin/performance.get.ts
/api/admin/permissions1 个
- GET
/api/admin/permissionsJWTserver/api/admin/permissions.get.ts
/api/admin/reset1 个
- POST
/api/admin/resetJWT请求体:ticket:stringcode?:unionpassword:stringserver/api/admin/reset.post.ts
/api/admin/roles4 个
- GET
/api/admin/rolesJWTserver/api/admin/roles.get.ts
- POST
/api/admin/rolesRBACrbac:manage请求体:name:stringpermissions?:array[]server/api/admin/roles.post.ts
- DELETE
/api/admin/roles/:idJWT路径参数idserver/api/admin/roles/[id].delete.ts
- PUT
/api/admin/roles/:idJWT路径参数id请求体:permissions:array[]server/api/admin/roles/[id].put.ts
/api/admin/search-terms1 个
- GET
/api/admin/search-termsJWTserver/api/admin/search-terms.get.ts
/api/admin/seo4 个
- GET
/api/admin/seoJWTserver/api/admin/seo.get.ts
- PUT
/api/admin/seo/pages/:pageKeyJWT路径参数pageKey请求体:title?:stringdescription?:stringkeywords?:stringrobots?:stringogImage?:stringserver/api/admin/seo/pages/[pageKey].put.ts
- POST
/api/admin/seo/pushJWT请求体:scope?:enumurls?:array[]server/api/admin/seo/push.post.ts
- PUT
/api/admin/seo/siteJWT请求体:items:array[]key:stringvalue?:stringserver/api/admin/seo/site.put.ts
/api/admin/sidebar1 个
- GET
/api/admin/sidebarJWTserver/api/admin/sidebar.get.ts
/api/admin/site-config2 个
- GET
/api/admin/site-configJWTserver/api/admin/site-config.get.ts
- PUT
/api/admin/site-config/:keyJWT路径参数key请求体:value?:stringwhere:unknownupdate:unknowncreate:unknownserver/api/admin/site-config/[key].put.ts
/api/admin/site-pages1 个
- GET
/api/admin/site-pagesJWTserver/api/admin/site-pages.get.ts
/api/admin/site-texts1 个
- PUT
/api/admin/site-texts/:keyJWT路径参数key请求体:value:stringnote?:stringserver/api/admin/site-texts/[key].put.ts
/api/admin/staff5 个
- GET
/api/admin/staffRBACstaff:manageserver/api/admin/staff.get.ts
- POST
/api/admin/staffRBACstaff:manage请求体:username:stringname:stringpassword:stringroleId:stringemail?:unionphone?:unionstatus?:enumopAllow?:array[]columnScope?:array[]featureScope?:array[]localeScope?:array[]server/api/admin/staff.post.ts
- DELETE
/api/admin/staff/:idJWT路径参数idserver/api/admin/staff/[id].delete.ts
- PUT
/api/admin/staff/:idJWT路径参数id请求体:username?:stringname?:stringpassword?:stringroleId?:stringemail?:unionphone?:unionstatus?:enumopAllow?:array[]columnScope?:array[]featureScope?:array[]localeScope?:array[]server/api/admin/staff/[id].put.ts
- GET
/api/admin/staff/optionsRBACstaff:manageserver/api/admin/staff/options.get.ts
/api/admin/stats1 个
- GET
/api/admin/statsJWTserver/api/admin/stats.get.ts
/api/admin/upload1 个
- POST
/api/admin/uploadRBACmedia:manageserver/api/admin/upload.post.ts
/api/auth/forgot1 个
- POST
/api/auth/forgot公开server/api/auth/forgot.post.ts
/api/auth/login1 个
- POST
/api/auth/login公开server/api/auth/login.post.ts
/api/auth/logout1 个
- POST
/api/auth/logout公开server/api/auth/logout.post.ts
/api/auth/me1 个
- GET
/api/auth/me公开server/api/auth/me.get.ts
/api/auth/register1 个
- POST
/api/auth/register公开server/api/auth/register.post.ts
/api/auth/reset1 个
- POST
/api/auth/reset公开server/api/auth/reset.post.ts
/api/csrf1 个
- GET
/api/csrf公开server/api/csrf.get.ts
/api/dev-docs1 个
- GET
/api/dev-docs公开server/api/dev-docs.get.ts
/api/developer/me1 个
- GET
/api/developer/me公开server/api/developer/me.get.ts
/api/developer/packages2 个
- POST
/api/developer/packages/commit公开server/api/developer/packages/commit.post.ts
- POST
/api/developer/packages/inspect公开server/api/developer/packages/inspect.post.ts
/api/developer/source1 个
- GET
/api/developer/source公开server/api/developer/source.get.ts
/api/engine/packages3 个
- GET
/api/engine/packages公开server/api/engine/packages.get.ts
- GET
/api/engine/packages/:type/:id/download公开路径参数type路径参数idserver/api/engine/packages/[type]/[id]/download.get.ts
- GET
/api/engine/packages/:type/:id/source公开路径参数type路径参数idserver/api/engine/packages/[type]/[id]/source.get.ts
/api/engine/ui1 个
- GET
/api/engine/ui公开server/api/engine/ui.get.ts
/api/engines1 个
- GET
/api/engines公开server/api/engines.get.ts
/api/forms/:key2 个
- GET
/api/forms/:key公开路径参数keyserver/api/forms/[key].get.ts
- POST
/api/forms/:key/submit公开路径参数keyserver/api/forms/[key]/submit.post.ts
/api/friend-links1 个
- GET
/api/friend-links公开server/api/friend-links.get.ts
/api/health1 个
- GET
/api/health公开server/api/health.get.ts
/api/home-content1 个
- GET
/api/home-content公开server/api/home-content.get.ts
/api/member/leads1 个
- GET
/api/member/leads会员server/api/member/leads.get.ts
/api/member/messages2 个
- GET
/api/member/messages会员server/api/member/messages.get.ts
- PATCH
/api/member/messages会员server/api/member/messages.patch.ts
/api/member/password1 个
- POST
/api/member/password会员server/api/member/password.post.ts
/api/member/profile1 个
- PATCH
/api/member/profile会员server/api/member/profile.patch.ts
/api/metrics/web-vitals1 个
- POST
/api/metrics/web-vitals公开请求体:metric:stringvalue:numberpagePath:stringuserAgent?:stringserver/api/metrics/web-vitals.post.ts
/api/nav1 个
- GET
/api/nav公开server/api/nav.get.ts
/api/pages1 个
- GET
/api/pages公开server/api/pages.get.ts
/api/search1 个
- GET
/api/search公开server/api/search.get.ts
/api/search/hit1 个
- POST
/api/search/hit公开server/api/search/hit.post.ts
/api/search/hot1 个
- GET
/api/search/hot公开server/api/search/hot.get.ts
/api/seo1 个
- GET
/api/seo公开server/api/seo.get.ts
/api/site-config1 个
- GET
/api/site-config公开server/api/site-config.get.ts
/api/site-texts1 个
- GET
/api/site-texts公开server/api/site-texts.get.ts
/api/taxonomy/tags2 个
- GET
/api/taxonomy/tags公开server/api/taxonomy/tags.get.ts
- GET
/api/taxonomy/tags/:slug公开路径参数slugserver/api/taxonomy/tags/[slug].get.ts
接口变更记录
接口是二次开发的合同,而合同会改。这里记录每次**接口 / 数据模型 / 页面 / 扩展规范**的变化 —— 它由 npm run api:changelog 从代码差异**生成**(不是手写的,所以不会过期)。 破坏性指老调用方或老扩展会坏:删接口、删字段、可选字段变必填、字段类型变化、 权限点变化、访问要求变严、删页面、扩展点或清单字段被移除;其余为兼容变更。 扩展作者可以用 GET /api/dev-docs?since=<引擎版本> 只取那之后的变化(返回里带 changelogSince)。
2026-10-05 · 引擎 1.0.0
破坏性 1 项补齐自 2026-09-30 以来未落账的清单(54 项)。内容基座:SiteText 文案覆盖、Term/TermLink 分类、SearchTerm 热搜词、首页内容接口;会员体系:Member 表与会员/开发者自助接口;通知中心:Notification 站内信表、NotifyLog 投递记录表、后台 4 条接口(channels/test/logs/messages),六条通道(站内信/邮件/企业微信/飞书/短信/公众号);另含站点配置与侧栏治理相关接口。⚠️ 含 1 项破坏性变更:PUT /api/admin/pages/:key 新增必填字段 blocks(array[]),不带该字段的旧调用方会被拒 —— 影响面是后台接口,扩展若有调用需同步。
- 破坏性
PUT /api/admin/pages/:key · 字段 blocks新增必填字段 blocks(array[]):旧调用方不带它会被拒 - 新增
GET /api/admin/home-content新增接口 - 新增
GET /api/admin/members新增接口(权限点 member:manage) - 新增
GET /api/admin/notify/channels新增接口 - 新增
GET /api/admin/notify/logs新增接口 - 新增
GET /api/admin/notify/messages新增接口 - 新增
GET /api/admin/search-terms新增接口 - 新增
GET /api/admin/sidebar新增接口 - 新增
GET /api/admin/site-pages新增接口 - 新增
GET /api/auth/me新增接口(公开) - 新增
GET /api/developer/me新增接口(公开) - 新增
GET /api/home-content新增接口(公开) - 新增
GET /api/member/leads新增接口 - 新增
GET /api/search新增接口(公开) - 新增
GET /api/search/hot新增接口(公开) - 新增
GET /api/site-texts新增接口(公开) - 新增
PATCH /api/admin/notify/messages新增接口 - 新增
PATCH /api/member/profile新增接口 - 新增
POST /api/admin/notify/test新增接口 - 新增
POST /api/auth/forgot新增接口(公开) - 新增
POST /api/auth/login新增接口(公开) - 新增
POST /api/auth/logout新增接口(公开) - 新增
POST /api/auth/register新增接口(公开) - 新增
POST /api/auth/reset新增接口(公开) - 新增
POST /api/developer/packages/commit新增接口(公开) - 新增
POST /api/developer/packages/inspect新增接口(公开) - 新增
POST /api/member/password新增接口 - 新增
POST /api/search/hit新增接口(公开) - 新增
PUT /api/admin/home-content新增接口 - 新增
PUT /api/admin/members/:id新增接口 - 新增
PUT /api/admin/site-texts/:key新增接口 - 变更
Lead · 字段 memberId新增可选字段 memberId(String?) - 新增
Member新增数据表 Member(9 个字段) - 新增
Notification新增数据表 Notification(10 个字段) - 新增
NotifyLog新增数据表 NotifyLog(10 个字段) - 变更
PageContent · 字段 blocks新增可选字段 blocks(Json?) - 变更
PageContent · 字段 locale新增可选字段 locale(String?) - 新增
SearchTerm新增数据表 SearchTerm(4 个字段) - 新增
SiteText新增数据表 SiteText(4 个字段) - 新增
Term新增数据表 Term(5 个字段) - 新增
TermLink新增数据表 TermLink(4 个字段) - 新增
/account新增页面路由 - 新增
/account/security新增页面路由 - 新增
/admin/home新增页面路由 - 新增
/admin/members新增页面路由 - 新增
/admin/notify新增页面路由 - 新增
/admin/site-texts新增页面路由 - 新增
/console新增页面路由 - 新增
/console/demo新增页面路由 - 新增
/developer新增页面路由 - 新增
/login新增页面路由 - 新增
/register新增页面路由 - 新增
/reset-password新增页面路由 - 新增
/search新增页面路由
2026-09-25 · 引擎 1.0.0
兼容变更M4 平台与运维:新增 GET /api/health(DB/Redis 探测,失败 503,compose 已挂 healthcheck);表单基座(POST/GET/PUT/DELETE /api/admin/forms 与提交记录/CSV 导出,form:manage 权限;公开 GET /api/forms/{key} 与 POST /api/forms/{key}/submit,蜜罐+IP 限频);请求日志中间件(≥400 或 ≥1s 才记);通知抽成通用通道 notifyText
- 新增
DELETE /api/admin/forms/:id新增接口 - 新增
GET /api/admin/forms新增接口(权限点 form:manage) - 新增
GET /api/admin/forms/:id/export新增接口 - 新增
GET /api/admin/forms/:id/submissions新增接口 - 新增
GET /api/forms/:key新增接口(公开) - 新增
GET /api/health新增接口(公开) - 新增
POST /api/admin/forms新增接口(权限点 form:manage) - 新增
POST /api/forms/:key/submit新增接口(公开) - 新增
PUT /api/admin/forms/:id新增接口 - 新增
Form新增数据表 Form(7 个字段) - 新增
/admin/forms新增页面路由
2026-09-25 · 引擎 1.0.0
破坏性 1 项M3 内容与媒体:新增后台页面 POST /api/admin/pages 与 DELETE /api/admin/pages/{key}(page:edit;保留字与保护页限制);公开 GET /api/pages 仅返回已发布页面;新增 GET /api/admin/pages(后台清单,此前缺失);新增媒体库 GET /api/admin/media、PUT/DELETE /api/admin/media/{id}(删除前查引用,409 提示引用位置);上传即登记 MediaAsset;sitemap 收录已发布内容页
- 破坏性
PageContent · 字段 published新增必填字段 published(Boolean):旧调用方不带它会被拒 - 新增
DELETE /api/admin/media/:id新增接口 - 新增
DELETE /api/admin/pages/:key新增接口 - 新增
GET /api/admin/media新增接口 - 新增
POST /api/admin/pages新增接口(权限点 page:edit) - 新增
PUT /api/admin/media/:id新增接口 - 变更
PUT /api/admin/pages/:key · 字段 published新增可选字段 published(boolean) - 变更
PUT /api/admin/pages/:key · 字段 summary新增可选字段 summary(string) - 新增
MediaAsset新增数据表 MediaAsset(7 个字段) - 变更
PageContent · 字段 summary新增可选字段 summary(String?) - 新增
/:slug新增页面路由 - 新增
/admin/media新增页面路由
2026-09-25 · 引擎 1.0.0
兼容变更M2 设置统一:站点配置后台读取按注册表下发(修复 GET /api/admin/site-config 缺失导致后台页一直为空);PageContent 新增页面级设置(PUT /api/admin/pages/{key} 可带 settings);内容契约 v1.0.0 增补 pages.settings 与 mergeSettings(页面级 > 包级);useEngineSettings 支持按页覆盖
- 变更
PUT /api/admin/pages/:key · 字段 content新增可选字段 content(string) - 变更
PUT /api/admin/pages/:key · 字段 settings新增可选字段 settings(record) - 变更
PageContent · 字段 settings新增可选字段 settings(Json?)
2026-09-25 · 引擎 1.0.0
兼容变更M1 契约与导航:新增公开导航 GET /api/nav?scope=main|footer(空库回落内置默认结构;停用父项=整支隐藏);新增后台导航管理 GET/POST /api/admin/nav、PUT/DELETE /api/admin/nav/{id}、POST /api/admin/nav/{id}/move(nav:manage 权限);内容契约升级 v1.0.0(新增 nav 形状与类型,changelog 增加 updateType/content/items)
- 新增
DELETE /api/admin/nav/:id新增接口 - 新增
GET /api/admin/nav新增接口 - 新增
GET /api/nav新增接口(公开) - 新增
POST /api/admin/nav新增接口(权限点 nav:manage) - 新增
POST /api/admin/nav/:id/move新增接口 - 新增
PUT /api/admin/nav/:id新增接口 - 新增
NavItem新增数据表 NavItem(11 个字段) - 新增
/admin/nav新增页面路由
2026-09-25 · 引擎 1.0.0
破坏性 5 项M0 止血:上传从公开端点 /api/upload 收敛到 /api/admin/upload(写文件的端点不能公开);删除与 WebVitalRecord 列不匹配、假成功的 /api/metrics 死接口(前端采集已改指 /api/metrics/web-vitals);站点配置接入注册表(未注册键拒绝写入、secret 键不公开、bool/number 归一化、系统键只读);新增自助改密 POST /api/admin/me/password
- 破坏性
POST /api/metrics接口被移除 - 破坏性
POST /api/upload接口被移除 - 破坏性
PUT /api/admin/site-config/:key · 字段 create新增必填字段 create(unknown):旧调用方不带它会被拒 - 破坏性
PUT /api/admin/site-config/:key · 字段 update新增必填字段 update(unknown):旧调用方不带它会被拒 - 破坏性
PUT /api/admin/site-config/:key · 字段 where新增必填字段 where(unknown):旧调用方不带它会被拒 - 新增
POST /api/admin/me/password新增接口 - 新增
POST /api/admin/upload新增接口 - 新增
/admin/account新增页面路由
2026-09-24 · 引擎 1.0.0
兼容变更建立变更记录基线
本次没有接口 / 模型 / 页面层面的变化
内容契约(主题/模块可依赖的数据)
二次开发与主题制作时,以下数据源是稳定承诺:字段只增不改名;弃用会先在「接口变更记录」公告。 主题侧统一经 useSiteContent(type) 读取(已做归一化与空值兜底),也可直接调用对应 API。
| 资源 | API | 稳定字段 |
|---|---|---|
| 产品 | /api/products、/api/products/:slug | slug、name、summary、description、cover、features、tags、changelogCount |
| 页面 | /api/pages、/api/pages/:pageKey | pageKey、title、summary、content、published、settings(仅公开键) |
| 站点配置 | /api/site-config | 注册表内键(server/utils/site-config-registry.ts),新增键须先注册 |
| FAQ | /api/faqs | question、answer、category |
| 版本日志 | /api/products/:slug/changelogs | version、updateType、content、createdAt |
| 导航 | /api/nav | scope(main/footer/admin)、label、href、sort、enabled |
| 搜索 | /api/search?q= | items[].type/title/hint/to(覆盖产品、页面、FAQ、版本日志) |
| 表单提交 | /api/forms/:key/submissions | 表单在后台「表单管理」定义,字段动态 |
错误与状态码契约
所有接口的错误响应都是同一个形状,前端按 data.code 分支、 按 data.message 显示文案:
HTTP 409
{
"statusCode": 409,
"message": "表单 key 已存在",
"data": { "code": "duplicate" }
}状态码语义(选码时按这张表)
| 400 | 请求不成形:body 不是合法 JSON、缺必填字段、没有上传文件 | bad_request |
| 401 | 未登录或令牌失效 | unauthorized |
| 403 | 已登录但无权限;或对象被标记为只读 | forbidden / read_only_target |
| 404 | 资源不存在 | not_found |
| 409 | 与当前状态冲突:重名、受保护或被引用不能删 | conflict / duplicate / protected_resource |
| 413 415 | 体积超限 / 类型不支持 | payload_too_large / unsupported_media_type |
| 422 | 值不合法:字段校验失败、枚举越界、字段组合非法 | validation_failed |
| 429 | 触发限流(登录、表单提交) | rate_limited |
| 500 | 服务端异常 | internal_error |
| 503 | 服务端自身配置缺失(例如未配置扩展索引地址) | server_misconfigured |
最常被搞错的两条:重名 / 受保护不能删是 409(不是 400);邮箱格式、枚举越界、父级不存在是 422(不是 400)。 「请求不成形」才用 400。
服务端怎么写
用 server/utils/api-error.ts 的构造器 (Nitro 自动导入,不必写 import)。业务代码里不要手写createError({ statusCode: 400 }) —— 巡检会拦下来,而且那样带不上机器可读的 code。
const parsed = schema.safeParse(await readBody(event))
if (!parsed.success) throwZodError(parsed.error) // 422 + validation_failed + 字段明细
if (!parent) throw validationError('父级菜单不存在或不合法')
if (exists) throw duplicate('表单 key 已存在') // 409
throw protectedResource(`${key} 的内容由固定页面消费,不能删除`) // 409
throw readOnlyTarget(`${key} 由系统写入,不能手工修改`) // 403前端与扩展怎么读
try {
await $fetch('/api/admin/forms', { method: 'POST', body })
}
catch (e) {
const msg = e?.data?.message || '操作失败' // 文案:读 message
if (e?.data?.code === 'duplicate') { /* 机器分支:提示改名 */ }
for (const issue of e?.data?.issues ?? []) markField(issue.path, issue.message)
}statusMessage。 h3 v2 已弃用该字段(只传 message 时它会被序列化成 "Server Error"),全站已统一改成 写 message、读 data.message。 扩展的后台页若还在读 err.data.statusMessage,请改掉。 页面路由
| 路由 | 文件 | SEO | 收录 |
|---|---|---|---|
| / | pages/index.vue | 已接入 | index |
| /:slug | pages/[slug].vue | 已接入 | index |
| /about | pages/about.vue | 已接入 | index |
| /account | pages/account/index.vue | 已接入 | index |
| /account/security | pages/account/security.vue | 已接入 | index |
| /admin | pages/admin/index.vue | 已接入 | noindex |
| /admin/account | pages/admin/account.vue | 已接入 | noindex |
| /admin/backup | pages/admin/backup.vue | 已接入 | noindex |
| /admin/engine-market | pages/admin/engine-market.vue | 已接入 | noindex |
| /admin/engine-trust | pages/admin/engine-trust.vue | 已接入 | noindex |
| /admin/engines | pages/admin/engines.vue | 已接入 | noindex |
| /admin/forms | pages/admin/forms.vue | 已接入 | noindex |
| /admin/home | pages/admin/home.vue | 已接入 | noindex |
| /admin/login | pages/admin/login.vue | 已接入 | noindex |
| /admin/media | pages/admin/media.vue | 已接入 | noindex |
| /admin/members | pages/admin/members.vue | 已接入 | index |
| /admin/nav | pages/admin/nav.vue | 已接入 | noindex |
| /admin/notify | pages/admin/notify.vue | 已接入 | index |
| /admin/operation-logs | pages/admin/operation-logs.vue | 已接入 | noindex |
| /admin/pages | pages/admin/pages.vue | 已接入 | noindex |
| /admin/performance | pages/admin/performance.vue | 已接入 | noindex |
| /admin/reset | pages/admin/reset.vue | 已接入 | noindex |
| /admin/roles | pages/admin/roles.vue | 已接入 | noindex |
| /admin/seo | pages/admin/seo.vue | 已接入 | noindex |
| /admin/site-config | pages/admin/site-config.vue | 已接入 | noindex |
| /admin/site-texts | pages/admin/site-texts.vue | 已接入 | index |
| /admin/staff | pages/admin/staff.vue | 已接入 | noindex |
| /admin/themes | pages/admin/themes.vue | 已接入 | noindex |
| /co-develop | pages/co-develop.vue | 已接入 | index |
| /console | pages/console/index.vue | 已接入 | index |
| /console/demo | pages/console/demo.vue | 已接入 | index |
| /console/messages | pages/console/messages.vue | 已接入 | index |
| /contact | pages/contact.vue | 已接入 | index |
| /demo | pages/demo.vue | 已接入 | index |
| /developer | pages/developer.vue | 已接入 | index |
| /docs | pages/docs.vue | 已接入 | index |
| /faq | pages/faq.vue | 已接入 | index |
| /login | pages/login.vue | 已接入 | index |
| /privacy | pages/privacy.vue | 已接入 | index |
| /register | pages/register.vue | 已接入 | index |
| /reset-password | pages/reset-password.vue | 已接入 | noindex |
| /search | pages/search.vue | 已接入 | noindex |
| /systems | pages/systems/index.vue | 已接入 | index |
| /systems/:slug | pages/systems/[slug]/index.vue | 已接入 | index |
| /systems/:slug/changelog | pages/systems/[slug]/changelog.vue | 已接入 | index |
| /tag/:slug | pages/tag/[slug].vue | 已接入 | index |
| /tags | pages/tags.vue | 已接入 | index |
新增页面时记得调用 useSeo(): 它会一次性输出 title / description / canonical / og / twitter / JSON-LD, 否则这一页在搜索结果里是裸链接。
组件与组合式函数
全局组件(46)
admin/Alert.vue—
admin/Badge.vue—
admin/BlockEditor.vue—
admin/Button.vue—
admin/Card.vue—
admin/Empty.vue—
admin/EngineSettings.vue—
admin/Field.vue—
admin/ImageField.vue—
admin/MediaPicker.vue—
admin/Page.vue—
admin/Pagination.vue—
admin/Resource.vue—
admin/Switch.vue—
admin/Table.vue—
AppFooter.vue—
AppHeader.vue—
AuthPanel.vue—
BlockRenderer.vue—
blocks/cta.vue—
blocks/faqs.vue—
blocks/features.vue—
blocks/hero.vue—
blocks/media.vue—
blocks/products.vue—
blocks/richtext.vue—
blocks/stats.vue—
BreadcrumbNav.vue—
ConsoleShell.vue—
engine/EnginePage.vue—
FriendLinks.vue—
HeroPanorama.vue—
HeroSlideshow.vue—
HomeIntro.vue—
HomeSections.vue—
IconBlock.vue—
MarkdownView.vue—
MemberBenefits.vue—
PageHero.vue—
SceneTabs.vue—
SiteActionBar.vue—
SiteHeader.vue—
SiteLogo.vue—
SystemThumb.vue—
TemplateSection.vue—
TemplateSlots.vue—
组合式函数(19)
block-registry.tsregisterEngineBlockDefs()allBlockDefs()blockDef()isEngineBlockDef()—
content-tokens.tsrenderContentTokens()renderTokensDeep()内容占位符 / 片段 **唯一注册表**(2026-10-07 · P2-7 · 站主 ②) 站主原话:「页面内容升级为 CMS 全功能,可自定义页面内容,并可**生成自定义引用的标签给主题调用**」✓。
csv.tscsvCell()csvSafe()csvRow()csvText()CSV 生成(纯函数 ✓ 可单测 ✓)—— 2026-10-06 · P2-10 【为什么单独一处】后台的"导出"正在从一条变成多条(线索 ✓、会员 ✓,以后还会有别的 ✓)。
dev-guide.tsuseDevGuide()开发者中心的「作者指南」内容(2026-10-06) 【为什么是数据、不是页面上的一堆模板】`docs/ecosystem-plan.md` §问题 4 的诊断是:
error-content.tssafeLinkTarget()parseErrorContent()404 / 500 错误页的内容(可配)—— 2026-10-07 · SEO 程序第 4 项 【为什么要可配】错误页不光给访客"出路" ✓,也是搜索引擎判定"这个站有没有用"的地方之一 ✓
friend-links.tsscopeOfPath()isSafeLinkUrl()normalizeFriendLinks()groupByScope()友情链接(2026-10-07 · SEO 第 2 项)—— 作用域注册表 + 规范化(**纯函数** ✓ 可单测 ✓) ## 站主要什么
media-types.tsextOf()mediaTypeOfName()deniedReason()mimeOfName()mediaKindOfMime()mimesOfKind()媒体类型白名单(**唯一真源**,前后端共用) 【为什么放在 composables/ 而不是 server/utils/】
search-pinyin.generated.tspinyinInitials()站内搜索用的「汉字 → 拼音首字母」映射 —— **自动生成,请勿手改**。 生成器:scripts/gen-search-pinyin.mjs(构建期跑,需要 pinyin-pro 这个 devDependency)。
source-label.tssourceLabel()「扩展从哪来」的中文标签(**唯一一处**) 数据源:`EnginePackage.installedFrom` —— 由安装路径写入:
text-registry.ts—
useAuth.ts—
useCsrf.tsuseCsrfHeader()—
useEngineSettings.ts读某个扩展(主题 / 区块 / 模块)在后台配置的**公开设置**,可选按页面覆盖(基座 M2) 【为什么主题参数走运行时而不是构建期】主题的结构(布局、页面、样式)天生是构建期确定的,
useFormDraft.tsuseFormDraft()—
useMember.tsuseMember()—
useSeo.tsuseSeoConfigState()useSeo()—
useSiteContent.tsnormalizeSiteContent()mergeSettings()站内内容的统一读取入口 —— **基座对外内容契约**(主题与核心页面共用) 【它解决什么】主题(尤其由 HTML 模板改来的)要展示站内内容时,不该去记
useSiteSearch.tstokenize()useSiteSearch()—
useSiteTexts.tsuseSiteTexts()—
服务端构成
鉴权中间件
server/middleware/admin-auth.tsserver/middleware/csrf.tsserver/middleware/indexnow-key.tsserver/middleware/maintenance.tsserver/middleware/request-id.ts每个请求一个短编号(8 位十六进制),挂在 `event.context.requestId` 并回写成响应头 `x-request-id`。 【为什么需要】以前出 500 时,日志里只有一行 `[req] GET /api/search → 500 57ms`
server/middleware/request-log.tsserver/middleware/security.tsserver/middleware/seo-verify-file.ts
独立路由(非 /api)
api/v1/[...path].tsengine-assets/[...path].get.tsrobots.txt.get.tsrobots.txt(PRD SEO 规范 7.2) 之前全站没有这个文件,/robots.txt 直接 404 —— 但 sitemap.xml 已经做好了,
sitemap.txt.get.tssitemap.xml.get.ts
服务端工具
admin/dashboard.ts仪表盘卡片注册表(P1-6,**唯一真源**) 【站主要什么】"仪表盘卡片要能注册、能开关、能排序" ✓。
admin/grants.ts「这个管理员这一次请求能不能做」的**裁决层**(纯函数,单测直接 import ✓) 【为什么单独一层,不把判断散在中间件里】站主要的四件事(新增 / 修改 / 删除 · 指定语言 ·
admin/nav.tsP1-5:侧栏可见性与接口鉴权**共用同一套权限**(`hasPermission` 认超管通配 `*` ✓)—— 这就是"双轨合一":不再有第二套 `role === 'super_admin'` 的判据 ✓。
admin/role-presets.ts内置用户组(站主 2026-10-07 需求①:创始人 / 管理员 / 内容管理员 / 优化推广员 / 自定义用户组) 【为什么要有这个文件】改前"谁能干什么"只存在于两个地方:
admin/scopes.ts管理员**可授予范围**的唯一真源(站主 2026-10-07 需求②:指定语言 / 指定栏目 / 指定功能) 【为什么要单独一个文件】这三个词在站主的话里是一句带过,落到系统上却是**三张各自独立的清单**:
admin/staff.ts管理员管理的**共用校验与格式化**(2026-10-07) 【为什么抽这一层】staff 有 5 个端点(列表 / 选项 / 新增 / 编辑 / 删除 / 代改密码),
admin-password-reset.tsapi-changelog.tsAPI 与数据结构的**变更记录**(纯逻辑,可单测) 【它解决什么问题】接口是二次开发者的合同,但合同是会改的:
api-error.tsaudit.tsauth.tsbackup/create.ts生成备份(2026-10-07 · P1「安全与效率」) 【站主要的三形态】单独备份数据库 / 单独备份上传文件夹 / 一键整站 —— 这三种只是 `parts` 不同 ✓,
backup/database.ts数据库备份 / 恢复基座(2026-10-07 · P1「安全与效率」) 【两条路线,为什么】
backup/restore.ts从备份恢复(2026-10-07 · P1「安全与效率」) 【这是全后台最危险的动作】它会**覆盖全站数据** ⇒ 三条纪律:
backup/store.ts备份文件的存放与清单(2026-10-07 · P1「安全与效率」) 【放哪儿】默认 `<项目根>/storage/backups`,`HEJUN_BACKUP_DIR` 可覆盖 ✗ **绝不放 public/ 下**:
backup/tar.ts流式 tar.gz —— 备份与恢复的底座(2026-10-07 · P1「安全与效率」) 【为什么不用项目里已有的 nanotar】
block-components.tscontent-cache.ts内容缓存内核(基座)—— **服务端那一半的内容契约** 【它补的是什么】前端有 `useSiteContent`(契约 v1.0.0,M1 完成)把接口结果归一化成稳定形状;
content-snippets.tscsp.tscsrf.tsdb.tsengine/api-perms.tsengine/auto-import-names.ts扩展接口里可以直接使用的「自动导入」名字(唯一权威清单) 【为什么需要这个概念】
engine/auto-imports.tsengine/backup.tsengine/build-gc.ts产物目录的通用回收工具(纯 fs 工具,不含业务判定) 判定「哪些该留」在各自的清单模块里:
engine/build-inventory.ts扩展**接口**(服务端)产物的「清单守卫」(纯函数:不碰 fs,可单测) 与 UI 线(ui-inventory.ts)同一套规则,区别只有产物命名:
engine/core-routes.generated.ts由 modules/engine.ts 在构建时自动生成,请勿手改(每次启动/构建都会覆盖)。 内容一:核心自带的接口路由清单(`METHOD /path`,动态段写成 `:param`)。
engine/dispatch.tsengine/graph.tsengine/heavy-deps.ts重型编译依赖的延迟加载(**必须**用「变量说明符 + 动态 import」,不要改成静态 import) 【为什么这条规则这么硬 —— 一次整站 500 的教训】
engine/hooks.tsengine/hosts.tsengine/index-format.tsengine/install.tsengine/license-store.tsengine/license.tsengine/manifest-schema.tsengine/manifest.tsengine/market-catalog.tsengine/market-index.tsengine/package-source.tsengine/perm-table.ts扩展权限表汇总(纯函数,无 IO) 单独拆出来的原因与 graph.ts 相同:这段逻辑原来与数据库查询写在一起,
engine/pure-frontend.tsengine/regions.tsengine/registry.tsengine/reload.tsengine/routes.tsengine/runtime.tsengine/scan.tsengine/settings.ts扩展设置的归一化(基座 C 项收口) 【解决什么】`EnginePackage.settings` 原本是"无 schema 的 JSON":值一律按字符串存
engine/sfc.tsengine/signing.tsengine/submission-review.tsengine/surface.ts运行面(surface)判据 —— **后台轨道与前台轨道的唯一分界点**。 【为什么必须有这个文件】站主 2026-09-30 的判断:「后台的页面会涉及到前台的显示,这个明显就是
engine/theme-assembly.tsengine/theme-contract.tsengine/theme-validate.tsengine/theme.tsengine/trust-store.tsengine/trust.tsengine/ui-bridge.ts引擎 UI 运行时桥(**刻意零依赖**:Nitro 侧与 Nuxt 应用侧都会 import 它) 【为什么需要这个文件】
engine/ui-inventory.ts运行时 UI 产物的「清单守卫」(纯函数:不碰 fs / nitro / vue,可直接单测) 管两件底层边界的事:
engine/ui-shims.tsengine/ui-table.tsengine/ui-usable.ts运行时 UI 产物「是否可渲染」的判定(纯函数,可单测) 【为什么需要它】扩展的组件/页面是服务端就地编译出来的(server/utils/engine/sfc.ts),
engine/ui.tsengine/uninstall.tsforms.tsfriend-links.tshome-content.ts首页内容(形状 + 解析 + 规范化) 【它是什么】首页各段内容存在 `SiteConfig.home_content`(JSON 字符串,见 site-config-registry.ts),
jobs.tsmember-auth.tsmember-identity.ts会员登录标识:双通道(手机号或邮箱)。 【为什么单独一个文件】注册、登录、会员改资料、以及"线索按手机号挂到会员"都要用同一套
member-reset.tsmetrics.tsnav-defaults.ts内置默认导航(基座 M1) 【唯一事实在服务端】公开接口 `/api/nav` 在某个 scope 没有任何启用条目时回落到这里,
notify-templates.ts通知**事件**与**正文模板**(唯一真源,2026-10-06 P0-3) 【站主要什么】"通知没做对应要有设置和开关" —— 上一轮(P0-2 之前)已经把**通道**开关做完了 ✓
notify.tsperm-match.ts「方法 + 路径 → 权限键」的匹配规则(纯函数,核心与扩展**共用同一套**) 【为什么必须共用】核心的 route-perms.ts 与扩展的 api-perms.ts(模块在清单里声明的权限点)
permissions.tsRBAC 权限清单(PRD 5.7 颗粒度:页面访问 / 新增 / 编辑 / 删除 / 导出) ⚠️ 与 server/middleware/admin-auth.ts 的 ROUTE_PERMS 一一对应:
public-content.tspush-paths.tspush-plan.tspush-quota.ts推送配额相关的纯逻辑(2026-09-30) 【为什么会有这个文件】站主第一次手动全量推送就撞上百度的 `over quota`:
rate-limit.tsredis.tsroute-perms.ts后台路由 → 所需权限 的映射与匹配(PRD 5.7) 抽成独立纯函数是为了能直接单测 —— 这段逻辑一旦写错,
search-push.tsseo-config.tsseo-page-keys.tsseo-pages.ts可在后台单独设置 SEO 的页面清单 只列「路径固定」的页面。系统详情页 /systems/:slug 与版本日志页不在这里 ——
seo-verify.ts搜索引擎「站点验证」的单一真源(2026-09-30) 【为什么需要这个文件】原先验证码是**三个写死的键**(google/bing/baidu)直接写在 `app.vue` 里,
site-config-registry.ts站点配置注册表(基座 M0) 【它解决什么】SiteConfig 原本是"无 schema 的 KV":键只存在于 seed、值全是字符串、
site-texts.tssite-urls.tssms-aliyun.tssource-package.tswechat-mp.ts微信公众号模板消息(2026-10-06) 【门槛先写清楚】这条通道需要**已认证的服务号**(订阅号没有模板消息接口 ✗),
Nitro 插件与定时任务
csp-hashes.tsengine.tserror-report.ts500(及其它未处理的服务端异常)的统一留痕。 【为什么要这一层】此前唯一的留痕是 request-log 里的一行摘要:
front-css.tsnoise-filter.tsdev 日志降噪:过滤「同名组件重复注册」这一条 Vue 警告。 【为什么这条是噪音,不是问题】
seo-autopush.tsstartup-checks.ts启动自检(基座 M0 止血) 【为什么必须有这一步】server/utils/auth.ts 在 NUXT_JWT_SECRET 未配置时会回退到
theme-css.ts主题样式按路由注入:**前台注入、后台绝不注入** 【为什么必须是这个形状】主题的 CSS 一旦进 Vite(不管是 `nuxt.options.css`,还是放在主题布局里
server/tasks/backup-auto.ts每天自动备份整站(2026-10-07 补完 P9) 【为什么要有它】`docs/runbook.md` 一直写着"建议每天 03:00 跑一次备份" ✓ ——
server/tasks/cleanup-webvitals.tsserver/tasks/demo-recycle.tsserver/tasks/health-alert.tsserver/tasks/seo-full-push.ts- 定时任务在
nuxt.config.ts的nitro.scheduledTasks注册, 且必须开启nitro.experimental.tasks,否则只会静默失效。
环境变量
完整清单见仓库根目录 .env.example。
数据库
DATABASE_URL默认 postgresql://hejun:CHANGE_ME_STRONG_PASSWORD@localhost:5432/hejun_website?schema=public
Redis(页面缓存 + 临时演示账号会话)
REDIS_URL默认 redis://localhost:6379
JWT 会话(后台登录令牌,生产环境务必替换为强随机值)
NUXT_JWT_SECRET默认 change-me-to-a-long-random-secretNUXT_JWT_EXPIRES_IN默认 7dNUXT_BCRYPT_ROUNDS默认 12
站点公开信息
⚠️ 生产必须改成正式域名:canonical / og:url / sitemap.xml / robots.txt 全部以它为准。
NUXT_PUBLIC_SITE_URL默认 https://www.hejun1.com
SEO 站长平台验证(可选)
也可以直接在后台「SEO 设置」里填,那边优先级更高、改完即时生效;
NUXT_PUBLIC_GOOGLE_SITE_VERIFICATIONNUXT_PUBLIC_BING_SITE_VERIFICATIONNUXT_PUBLIC_BAIDU_SITE_VERIFICATION
搜索引擎主动推送
百度普通收录 token 与 IndexNow key 都只在后台「SEO 设置」里填,
消息推送 Webhook(企微 / 飞书),enabled 控制总开关
NUXT_WEBHOOK_WECOMNUXT_WEBHOOK_FEISHUNUXT_WEBHOOK_ENABLED默认 false
邮件通知(Nodemailer)
NUXT_MAIL_HOSTNUXT_MAIL_PORT默认 465NUXT_MAIL_SECURE默认 trueNUXT_MAIL_USERNUXT_MAIL_PASSNUXT_MAIL_FROM
临时演示账号有效期(小时)
NUXT_DEMO_ACCOUNT_EXPIRE_HOURS默认 24
初始管理员账号(prisma/seed.ts / 巡检 scripts/site-audit.mjs 共用)
⚠️ 改名或改密码时**三处必须一起改**:这里、prisma/seed.ts(读同一对变量)、数据库里那一行。
SEED_ADMIN_USERNAME默认 chaosSEED_ADMIN_PASSWORD默认 change-me
引擎与扩展
本站的三套扩展机制。三者的边界是刻意划清的 —— 如果它们都能做同样的事, 社区贡献的扩展最后一定会互相打架。引擎版本 1.0.0, 当前已装 9 个扩展包,可声明的扩展点 4 个。 这一节的字段表、扩展点、示例清单全部由源码生成,不存在「文档写得过时」—— 另有同一份内容的离线版 docs/engine-spec.md 可直接交付给开发者。
主题 theme
engines/themes/<id>/
- 能做
- 整套前端展示:布局(layouts/)、页面(pages/)、样式(assets/),按后台参数渲染;只有组件、没有布局与页面的就是「区块」
- 不做
- 不建表、不加服务端接口(那是模块的事)
- 适合
- 最容易被社区贡献:会写 Vue 就能做一个区块,也能换掉整套外观
模块 module
engines/modules/<id>/
- 能做
- 组件 / 页面 / 接口 / 组合式函数 / 数据表片段 / 自带权限点
- 不做
- 不修改核心文件
- 适合
- 一套完整业务能力,装上就能用
插件 plugin
engines/plugins/<id>/
- 能做
- 在扩展点上插入行为(一个 hooks.ts 即可)
- 不做
- 不起路由、不建表、不占页面
- 适合
- 轻量增强:同步外部系统、埋点、通知
从零做一个插件(五分钟)
插件是最容易上手的类型:一个目录、一个清单、一个 TS 文件。下面五步能把全流程走通, 不需要读内核代码,也不需要改任何核心文件。
① 生成骨架
npm run engine:new -- --type plugin --id my-plugin --name "我的插件"生成 engines/plugins/my-plugin/,包含 manifest.json、hooks.ts、README.md。骨架本身是干净的(零错误零告警)。
已生成 engines\plugins\my-plugin② 自检
npm run engine:validate必须「零错误零告警」再往下走。这一步同时会校验规范快照与源码是否一致,所以它也能挡住「文档过期」。
插件(2) ✓ lead-dup-guard 1.0.0 线索重复提示 ✓ my-plugin 1.0.0 我的插件 共 2 个包,其中 0 个清单有问题③ 写实现
编辑 hooks.ts 与 manifest.json在 hooks.ts 里 on('lead:created', …);同时把 lead:created 写进 manifest.json 的 hooks。两边必须一致:声明了没实现会被拒绝,实现了没声明则不会被注册。
④ 启用
npm run dev → 后台「引擎与扩展」→ 重新扫描 → 启用插件与钩子是运行时加载的:改完源码只需要再点一次「重新扫描」,不用重新构建、也不用重启。新装的包默认是停用的。
⑤ 验证
前台 /contact 提交一条需求扩展点被触发。控制台里能看到带扩展名前缀的日志,可据此确认自己写的逻辑真的跑到了。
[engine:my-plugin] 收到新线索 …
包内部布局(名字是硬约束)
目录名必须等于清单里的 id; 下面这些位置是引擎识别能力的唯一依据 —— 放到别处不会生效,而且不会报错,所以照着放最省事。
| 路径 | 必需 | 作用 |
|---|---|---|
| manifest.json | 必需 | 清单。唯一必需文件,缺它整个包会被忽略(不会报错中断站点) |
| hooks.ts | 可选 | 插件的全部实现:导出默认函数 setup({ on, settings, log, prisma, version, engineVersion }) |
| components/*.vue | 可选 | 组件。全局注册,模板里直接写组件名即可(模板型扩展至少有一个)。运行时编译:改完点一次「重新扫描」即生效 |
| pages/**/*.vue | 可选 | 页面。按文件路径生成路由:新页面即时可用(默认布局);要用 definePageMeta 的能力需重新构建 |
| server/api/**/*.ts | 可选 | 服务端接口。包内 server/ 下的路径原样映射为 /api/…(运行时注册:改完点一次「重新扫描」即可,不用重新构建) |
| composables/*.ts | 可选 | 组合式函数:包内组件可直接用(运行时替身);要被项目代码自动导入则需重新构建 |
| schema.prisma | 可选 | 仅模块可用:数据表片段,用 npm run engine:schema 合并进主 schema(见下方「数据表」) |
| README.md | 可选 | 说明文档,engine:pack 时会一起打包 |
manifest.json 全部字段
共 20 个字段。这张表由 server/utils/engine/manifest.ts 直接生成 —— 它永远等于引擎真正执行的那套规则,不存在「文档落后于代码」。 未知字段会直接报错,不会被静默忽略。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| $schema | string | 否 | — | (给编辑器用)指向 engines/manifest.schema.json,如 "../../manifest.schema.json";引擎会忽略它 |
| id | string | 是 | — | 扩展 id。必须与包目录名一致(引擎会核对,不一致直接判为清单非法) 匹配 ^[a-z][a-z0-9-]{1,40}$ |
| type | enum(theme | module | plugin) | 是 | — | 扩展类型。必须与所在目录一致:themes/ → theme,modules/ → module,plugins/ → plugin |
| name | string | 是 | — | 显示名称,出现在后台「引擎与扩展」列表与文档里(1–40 字) 1–40 字 |
| version | string | 是 | — | 语义化版本,形如 1.0.0。它决定运行时产物目录(.engine-build/<id>@<version>/) 匹配 ^\d+\.\d+\.\d+(-[a-z0-9.]+)?$ |
| description | string | 是 | — | 一句话说明这个扩展解决什么问题(1–300 字) 1–300 字 |
| author | string | 否 | "" | 作者署名,会写进打包描述文件 最多 60 字 |
| license | string | 否 | "MIT" | 开源协议,默认 MIT 最多 40 字 |
| homepage | string | 否 | "" | 项目主页或仓库地址 最多 300 字 |
| activation | object | 否 | {} | 授权声明(离线授权码)。**只有付费扩展需要**:required 为 true 时,站主必须贴进一张由你签发的授权码才能启用。免费扩展不要加这个字段 —— 加了就是白给自己加一道门。授权码用 npm run engine:license 签发 |
| compat | object | 否 | {} | 兼容性声明 |
| requires | string[] | 否 | [] | 依赖的其他扩展,写「类型/id」,可加版本范围(@>=1.2.0 / @^1.2.0 / @~1.2.0 / 多个用空格分隔)。依赖未启用、或版本不满足时本扩展会被判为不可用(不会半启动);引擎会检测循环依赖 |
| hooks | string[] | 否 | [] | 声明监听的扩展点。只有列在这里的钩子才会被注册;写未埋点的钩子会被拒绝 |
| entry | string | 否 | "" | 入口组件名(不含 .vue)。包内只有一个组件时可省略,引擎会自动取那一个 最多 60 字 |
| regions | string[] | 否 | [] | 自动注入区域,如 layout.top、home.after-systems。留空表示只支持被 <TemplateSection> 点名使用 |
| adminNav | object[] | 否 | [] | 后台侧边栏追加项。与核心导航重复时保留核心项 |
| permissions | object[] | 否 | [] | 扩展自带的权限点。声明后模块的后台接口无需改核心的 route-perms.ts |
| settings | object[] | 否 | [] | 可在后台配置的设置项 |
| blocks | object[] | 否 | [] | 本扩展提供的页面区块类型。声明后:后台页面编辑器能选到它 ✓、前台由引擎按 component 解析渲染 ✓ —— 模块作者不需要改核心的 block-registry.ts |
| builtin | boolean | 否 | false | 是否随源码交付的内置扩展。内置项默认启用且不允许卸载,只允许停用 |
settings 数组里每一项的子字段
设置项会在后台「引擎与扩展」里按 type 渲染成表单。 组件里靠键名与 props 同名自动接收:清单里声明 key: "title", 组件里写 defineProps<{ title?: string }>() 即可,不用自己取数据。 只有 public: true 的项才会下发给前台接口。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| key | string | — | 设置键。必须与组件 props 同名才会被自动传入;只允许小写字母、数字、下划线 |
| label | string | — | 后台表单里显示的标签(1–40 字) |
| type | enum(text | textarea | bool | number | url) | "text" | 表单控件类型,决定后台如何渲染这一项 |
| default | string | "" | 默认值。清单里统一写字符串;引擎按 type 归一化成 boolean / number 后才下发给组件(bool 写 "true" / "false") |
| hint | string | "" | 填写提示,显示在输入框下方 |
| public | boolean | false | 是否允许前台 /api/engines 读到。放密钥或内部文案时保持 false |
扩展点清单
当前可声明 4 个扩展点。 标记「未实现」的不要声明 —— 它们还在设计里,声明会被校验直接拒绝: 宁可报错,也不让你写出一个「装上了但什么都不发生」的扩展。 扩展点里的异常会被引擎吞掉并记日志:「顺带做的事」不该让主流程(提交需求、保存产品)失败。
| 扩展点 | 状态 | 触发时机 | 负载 |
|---|---|---|---|
| app:ready | 可用 | 服务端进程启动、钩子注册完成后触发一次 | { engineVersion } |
| lead:created | 可用 | 客户通过 /contact 提交需求、线索写入成功后 | { lead: { id, name, contact, company, interestSlug }, source } |
| product:afterSave | 可用 | 后台新增或编辑系统产品成功后 | { product: { id, slug, name, status }, mode: "create" | "update" } |
| content:changed | 可用 | 后台改动内容(产品 / FAQ / 页面内容 / 版本日志)后 | { kind: "product" | "faq" | "page" | "changelog", paths: string[] } |
hooks.ts 的接口
导出默认函数,参数是引擎注入的能力;用不到的可以不接(解构里不写就行)。
export default function setup({
on, // (hook, handler, priority?) => void 注册扩展点;hook 必须在 manifest.hooks 里声明过
settings, // Record<string, string> 后台里填的设置(已合并清单默认值)
log, // (message, detail?) => void 带 [engine:<id>] 前缀的日志
prisma, // Prisma Client 可用已有表;模块自建表需先跑 engine:schema
version, // string 本扩展的版本号
engineVersion, // string 引擎版本,用于兼容判断
}) {
// priority 越小越先执行,不传默认 100
on('lead:created', async ({ lead, source }) => {
log('收到新线索 ' + lead.id, { source })
}, 100)
}生命周期回调(可选)
setup() 的返回值可以是生命周期回调。 只在状态切换时触发:第一次装载只建立基线 —— 否则每次重启都会重跑一遍初始化副作用 (重复发通知、重复写 seed 数据),这类问题最难查。
export default function setup(ctx) {
ctx.on('content:changed', async ({ kind, paths }) => { /* … */ })
return {
onEnable(ctx) { /* 启用时触发(首次启用也走这里,可当安装初始化) */ },
onDisable(ctx) { /* 停用时触发;此时它的扩展点已全部撤销 */ },
onUninstall(ctx) { /* 由 npm run engine:remove 调用,清理自己写入的数据 */ },
}
} 只做生命周期、不注册任何扩展点是合法的(此时 hooks 留空即可)。 依赖别的扩展用清单里的 requires 声明(写「类型/id」): 依赖没满足时本扩展会被判为不可用,而不是半启动;循环依赖会让环上全部失效。
模块自带后台接口与权限点
这是模块化的关键:不必改核心的 route-perms.ts。在清单里声明权限点, 接口放进包内 server/api/admin/, 核心的鉴权中间件就会按 routes 匹配;声明过的权限点会自动出现在后台「角色权限」里等待分配。
// manifest.json
"permissions": [
{
"key": "order:manage",
"name": "订单管理",
"routes": ["GET /api/admin/orders", "POST /api/admin/orders", "DELETE /api/admin/orders"]
}
]
// 包内 server/api/admin/orders/index.get.ts —— 不需要 import,prisma 等由 Nitro 自动导入
export default defineEventHandler(async () => {
return prisma.order.findMany({ orderBy: { createdAt: 'desc' } })
}) 注意:不要写 '../../server/utils/xx' 这类回到项目的相对路径 —— 包的位置一变层级数就错,写错一个会让整站 500。
接口里可以直接用的名字(不用 import)
引擎编译扩展接口时会注入下面这些名字,取的是运行中进程的同一批对象(同一份 Prisma 客户端)。 其它依赖请显式 import(例如 import { z } from 'zod')。
| 来源 | 可直接使用的名字 |
|---|---|
| h3 | defineEventHandler / createError / readBody / getQuery / getRouterParam / getHeader / setResponseHeader / setResponseStatus / getCookie / setCookie / sendRedirect |
| 项目工具 | prisma / logOperation / resolveEngines / resolveEnabled / emitHook / ENGINE_VERSION |
两条硬约束:接口必须放在包内 server/api/ 下; 不能占用核心已有的接口(核心优先,冲突时扩展接口会被跳过并告警)。 两者的检查结果都会出现在「重新扫描」的返回里。
组件与页面(运行时编译)
引擎在服务端把包里的 .vue 就地编译成 ESM: 改完点一次「重新扫描」即生效,不用重新构建。支持的写法: <script setup> + 模板、 <style scoped>、 Vue API(ref / computed / onMounted…)、 上面清单里的 Nuxt 组合式函数、包内 composables/*.ts 的导出、 组件之间互相 import(import Foo from './Foo.vue')、 其它 npm 依赖(会被打进产物)。
两条边界要知道:① 页面 运行时只保证「能跑」—— 落在默认布局、不认 definePageMeta(layout 与 route middleware 不生效), 要这些能力就重新构建一次(构建期注册那条路是完整语义); ② 组件 同名时会覆盖构建期注册的那份(所以改模板立刻生效),编译失败则不会被覆盖,自动退回构建期兜底。 不支持 SCSS 等预处理器、<style module>, 也不能与项目自带页面撞路径(核心页面优先)。
模块要建表怎么办
- 1. 在包目录下建
schema.prisma,只写自己的 model(不要写 datasource / generator)。 - 2. 执行
npm run engine:schema:片段会以标记块形式合并进prisma/schema.prisma,审查 diff 时一眼可见。 - 3. 执行
npx prisma db push建表,然后 停掉 dev server 跑npx prisma generate(Windows 上 dev server 占用着查询引擎 dll,不停服会报 EPERM)。 - 4. 重启服务。数据表是唯一绕不开重启的部分 —— Prisma Client 由构建期生成,运行时加不了 model。
随源码交付的示例扩展
这三张是「照着改就能变成你自己的扩展」的模板,分别覆盖三类扩展的完整写法。
theme禾君默认主题hejun-default · v1.0.0
本站现有的整套前台外观:首页、产品系统(列表/详情/版本日志)、FAQ、联系我们、二次开发、文档与隐私页,以及页头页脚与页面区块。作为可切换的主题包交付 —— 换主题即换整套外观。
engines/themes/hejun-default组件 13页面 10
themeModern Commercemoderncommerce · v1.0.0
第二套主题:由 HTML 模板 Moderncommerce 转换而来,含首页与 服务 / 案例 / 博客 / 团队 / 联系 / FAQ 共 13 个页面,自带完整样式与动效(jQuery + GSAP + Slick)。与「禾君默认主题」互斥,同时只能激活一个。
engines/themes/moderncommerce页面 14
module顶部公告条announcement-bar · v1.0.0
站点顶部可关闭的公告条:后台维护公告内容与生效时间,前台自动取「当前生效」的一条展示。包含自己的数据表、后台页面、公开接口与权限点,是模块引擎的完整示例。
engines/modules/announcement-bar组件 1页面 1接口 4schema.prisma
module版本日志changelog-center · v1.0.0
按产品分组的版本日志:后台维护(新增 / 编辑 / 删除),公开接口 GET /api/products/{slug}/changelogs 供前台的产品更新记录页消费。表通过外键挂在产品表上,所以依赖「系统产品」模块。
engines/modules/changelog-center页面 1接口 5schema.prisma
module演示账号demo-accounts · v1.0.0
给客户开的限时只读演示账号:后台按产品开通(写入 Redis 里的限时会话,到期自动失效),访客用临时口令换一个只读 Cookie 再进演示站。表通过外键挂在产品表上,所以依赖「系统产品」模块。
engines/modules/demo-accounts页面 1接口 5schema.prisma
moduleFAQ 管理faq-manager · v1.0.0
常见问题:后台维护(新增 / 编辑 / 排序 / 隐藏 / 回收站),公开接口 GET /api/faqs 供前台 FAQ 页与任意页面消费。自带数据表、后台页面与权限点,是「业务模块」的官方样板。
engines/modules/faq-manager页面 1接口 6schema.prisma
module客户线索lead-inbox · v1.0.0
访客留言的统一收件箱:前台表单写入(手机号 / 微信校验 + 通知 + lead:created 扩展点),后台可搜索、改状态、写跟进备注、进回收站、按状态导出 CSV。自带数据表、后台页面与权限点。
engines/modules/lead-inbox页面 1接口 6schema.prisma
module系统产品product-catalog · v1.0.0
自研业务系统的产品目录:后台维护产品(图集、卖点、交付方式、SEO)与分类标签,公开接口 GET /api/products 供前台系统页与留言表单消费。自带 products / tags 两张表、两个后台页与权限点 —— 官方模块里的「系统产品 / 版本日志 / 演示账号」三件套以它为基础。
engines/modules/product-catalog页面 2接口 12schema.prisma
plugin线索重复提示lead-dup-guard · v1.0.0
监听 lead:created,用同一联系方式在近 30 天内是否提交过需求,把「重复线索」写进操作日志,方便销售判断是不是同一波需求反复来问。
engines/plugins/lead-dup-guardhooks.ts
监听:lead:created
在页面里使用扩展区块
<!-- 主题自带的区块:直接当组件用(主题激活时它的组件就已注册) -->
<FeatureSteps :title="'交付流程'" />
<CaseStudies />
<PricingPlans />
<!-- 点名某个扩展包的区块(按包 id;块随包走时才这么写) -->
<TemplateSection id="扩展包 id" :props="{ title: '…' }" />
<!-- 页面只留位置,让声明了该区域的扩展自己注入(没有扩展时什么都不渲染) -->
<TemplateSlots region="home.after-systems" /> 区域清单以站点源码为准:完整可用区域在 npm run engine:validate 输出的末尾(当前 9 个);清单里声明了站点不存在的区域,校验会直接失败。
命令行
| npm run engine:keygen | 生成一对 ed25519 签名密钥:私钥留本机(ENGINE_PRIVATE_KEY),公钥交给站主加进「信任白名单」 |
| npm run engine:license -- issue --type module --id <id> --to "客户名" | 签发离线授权码(付费扩展);--domain 绑域名、--per-major 买断大版本、--days/--expires 定有效期 |
| npm run engine:license -- list | 看签发台账(dist/licenses/ledger.jsonl)—— 谁买了什么、什么时候、什么范围 |
| npm run engine:list | 列出全部扩展包、清单校验结果与它们携带的内容 |
| npm run engine:new -- --type plugin --id my-plugin | 生成扩展骨架(三类各有自己的骨架,一生成就能通过校验) |
| npm run engine:remove -- --type plugin --id my-plugin | 卸载:先跑扩展自己的 onUninstall 清理数据,再删目录与状态记录 |
| npm run engine:validate | 校验清单 + 规范一致性。零错误零告警才算通过,失败退出码 1,可直接进 CI |
| npm run engine:spec | 从 manifest.ts 生成 JSON Schema 与文档快照。加 --check 只校验不写盘 |
| npm run engine:pack -- --type plugin --id my-plugin | 打包为 tar.gz + 校验和描述文件,用于分发 |
| npm run engine:schema | 把模块的 schema.prisma 片段合并进主 schema |
| npm run engine:gc | 清理运行时编译产物的旧版本(每个包保留最近 2 个) |
打包、分发与安装
npm run engine:pack -- --type module --id <id> 产出两个文件:包本体 .tar.gz(包内路径是 <id>/…)与同名描述文件 .json(含 sha256、作者、引擎兼容范围与签名)。
签名与「信任白名单」
npm run engine:keygen 生成一对 ed25519 密钥: 私钥 ENGINE_PRIVATE_KEY 只留在你自己机器上, 公钥交给站主。打包时加 --sign 即签名 (描述文件里会带上 keyId,站主据此知道是谁签的)。
站主在后台 →「引擎与扩展 → 信任密钥」里把公钥加进白名单(可以有多把,一个合作者一把)。 白名单里只要有一把未吊销的密钥,安装就强制验签:没有签名、签名对不上、或签名者不在名单里的包 一律拒绝。白名单为空时只靠「仅超级管理员可安装」把关,后台会明确显示「未启用签名校验」。
吊销一把密钥会连带停用由它签名的扩展(可逆:不删文件、不删数据),它的接口也会一起下线; 重新信任不会自动启用它们。装进来的包都会记下签名者,出事时查得出「这个包是谁给的」。 注意:签名保证的是「装进来的东西 == 被签名的那个东西」与「谁给的」, 保证不了「签的人是好人」—— 只把公钥加给你真正信任的人。
要收费?用离线授权码
清单里声明 activation.required 之后, 站主必须贴进一张由你签发的授权码才能启用它:装得上、看得见,但没授权就不跑 (钩子、接口、组件一起下线)。授权码用签包那把私钥签发,离线可验 —— 你的服务器宕机也不影响客户站点。
# 签发一张授权码(买断 2.x、一年有效、绑客户域名、带 15 天宽限)
npm run engine:license -- issue --type module --id order-board \
--to "某某科技" --per-major --days 365 --grace 15 --domain client-a.com
# 自查(发出去之前先确认它是对的)与台账(卖出记录)
npm run engine:license -- verify --file dist/licenses/module-order-board-xxxx.license.txt
npm run engine:license -- list三个收费选择都是签发参数,不需要改代码:--domain 决定绑不绑域名、 --per-major 决定买断某个大版本还是永久、 --days / --expires 决定有效期。 贴进后台时会先校验再保存:读不出来、不是给这个包的、签名对不上、签发者不在信任列表、 域名或大版本不匹配 —— 都会当场说明原因;已过期的照收,但显示「已过期,请续期」。
它不是 DRM:校验跑在站主自己的服务器上,他拥有代码与数据库,有能力改掉。 它给的是「明确的契约状态 + 防误用」—— 真正的约束力来自合同与交付关系。
自建扩展源(索引)
把描述文件汇总成一份索引放到任何静态托管(字段与描述文件一致),再在服务器 .env 里配置 ENGINE_INDEX_URL 指向它: 「扩展市场」就会列出这些包,并标注本机已装版本、是否可更新、是否与引擎版本兼容。
{
"format": "hejun-engine-index/1",
"updatedAt": "2026-09-23T00:00:00.000Z",
"packages": [
{
"type": "module",
"id": "announcement-bar",
"name": "顶部公告条",
"version": "1.0.0",
"description": "页面顶部展示一条可关闭的公告",
"author": "禾君科技",
"license": "MIT",
"engine": ">=1.0.0",
"sha256": "…",
"signature": "…",
"tarball": "module-announcement-bar-1.0.0.tar.gz"
}
]
}三种安装方式
- ① 后台上传(推荐):扩展市场 → 上传安装,选
.tar.gz;服务器配了公钥还要贴签名。 - ② 后台从索引安装:配置了索引后,在扩展市场里点「安装 / 更新」。
- ③ 手工放目录:解压到
engines/<类型>/下,点一次「重新扫描」。
三者效果一样:接口、扩展点、组件与页面都立即生效,不需要重新构建; 只有带 schema.prisma(数据表片段)的模块需要额外走 engine:schema → db push → 停服 prisma generate → 重启。 第三方扩展装上默认停用,要在「引擎与扩展」里显式启用。
安装侧的安全:先校验 sha256 与签名,再解包——只接受普通文件与目录(符号链接一律拒绝)、拒绝绝对路径与 ..、包内只允许一个顶层目录且必须等于清单 id、体积有上限; 校验不过不会在磁盘上留下半个包。完整规则见仓库 docs/engine-spec.md §11。
改了什么之后要做什么
这一节是最容易踩坑的地方:扩展的加载方式分两类,做错了会以为「改了没反应」。
| 改动 | 要做什么 | 为什么 |
|---|---|---|
| hooks.ts 里的钩子逻辑 | 后台点「重新扫描」 | 钩子是运行时加载的:按内容 hash 重新编译并加载,不用重新构建、也不用重启 |
| manifest.json 的 name / hooks / settings / adminNav | 后台点「重新扫描」 | 清单由运行时读取;改 hooks 后必须重新扫描才会重新注册处理器 |
| 已有组件的模板与样式 | 什么都不用做 | 走 Vite 热更新 |
| 安装 / 更新扩展(后台上传或从索引) | 什么都不用做 | 安装接口装完会立刻做一次重新扫描;到「引擎与扩展」里启用即可。含 schema.prisma 的模块例外,需重新构建 |
| 新增 / 修改包内 server/api/** 接口 | 后台点「重新扫描」 | 接口由引擎在运行时注册进 Nitro 路由表:新增、改动、删除都不需要重新构建 |
| 新增 / 修改 components/*.vue 组件 | 后台点「重新扫描」 | 组件在服务端就地编译并注册到 Vue 应用;同名时覆盖构建期那份,所以改模板立刻生效 |
| 新增 pages/**/*.vue 页面 | 后台点「重新扫描」 | 运行时 catch-all 兜底渲染(默认布局,不认 definePageMeta) |
| 修改已有页面的模板(构建期已注册) | 重新构建 + 重启 | 具体路由由构建期注册,优先级高于运行时兜底;重新构建后才能拿到完整语义 |
| 新增 composables/*.ts 且要被项目代码自动导入 | 重启 dev server | 项目代码是构建期编译的,addImportsDir 只在构建期生效 |
| 包内的 schema.prisma | engine:schema → db push → 停服 generate → 重启 | Prisma Client 由构建期生成,运行时加不了 model |
| 引擎核心代码 server/utils/engine/** | 重启 dev server | 引擎自身的改动不在热更新范围内 |
常见报错对照
未知字段 region —— 清单里的未知字段会被引擎忽略,所以这里直接报错
字段名拼错了。报错里会列出全部可用字段;给 manifest.json 加一行 $schema 就能在编辑器里直接看到提示。
扩展点 xxx 不存在(可用清单见 docs/engine-spec.md)
名字写错了,或者用了已删除的扩展点。admin:nav 与 site:settings 在阶段 2 被移除:后台导航走清单的 adminNav 字段,站点配置走后台「站点配置」。
后台显示「不可用」并给出 xxx(未启用)
依赖没满足。清单 requires 里写的扩展没启用时,本扩展会被判为不可用(而不是半启动)。先启用被依赖的那个;提示循环依赖时需要拆环。
扩展接口 404
依次确认:文件在包内 server/api/ 下、做过一次「重新扫描」、没有与核心接口重名(重名会被跳过并在服务端日志里告警)。
扩展接口报 xxx is not a function
用了「可直接使用的名字」清单以外的标识符。改用清单里的名字,或显式 import。
组件在页面上没出现 / 还是旧内容
先点一次「重新扫描」。若返回里有 uiErrors,说明编译失败 —— 失败的组件不会覆盖构建期那份,所以表现为「还是旧的」。另外用 <component :is="名字"> 渲染时,名字要与文件名一致。
新加的页面 404
先点一次「重新扫描」(页面表是运行时填的);若路径与项目自带页面相同则不会生效,返回值里会告警。
页面的 definePageMeta 不生效
运行时页面拿不到 Nuxt 的编译期宏:落在默认布局、不跑 route middleware。要这些能力就重新构建一次。
manifest.hooks 声明了扩展点,但缺少 hooks.ts
两者必须同时存在:要么补一个 hooks.ts 并导出默认函数,要么删掉清单里的 hooks 声明。
提供了 hooks.ts 但 manifest.hooks 未声明扩展点,钩子不会被注册
把 hooks.ts 里 on() 用到的扩展点补进清单的 hooks 数组。
清单的 id(xxx)与目录名(yyy)不一致
目录名就是扩展标识,二者必须完全一致(含大小写与短横线)。
编译失败 / 加载 hooks.ts 失败
后台「引擎与扩展」会显示具体原因;看服务端日志里 [engine] 开头的行最快。
后台启用了扩展,但前台没有任何变化
按顺序检查:扩展是否真的启用、声明的扩展点是否是「可用」状态、设置项是否把功能关了。
扩展能做到什么深度
「我想做的事,扩展能不能做、做到什么程度、代价是什么」—— 按场景对着找即可。 字段级规范见 docs/engine-spec.md, 完整版(含每条天花板的成因)见 docs/engine-capabilities.md。
components/ + entry + regions + settings,声明区域后由页面自动注入。
组件 + regions: ['layout.top'];要后台管理就用模块(带页面与接口)。
hooks.ts 里监听 lead:created;不占路由、不建表。
hooks.ts 里用注入的 prisma 读数据,再发出去。
pages/admin/*.vue + server/api/admin/** + permissions + adminNav。
代价:页面要用 definePageMeta(布局/鉴权)就得随源码构建一次
server/api/engine/*.ts,路径原样映射为 /api/engine/…。
代价:接口文件不能与核心接口重名(核心优先,冲突会被跳过并告警)
schema.prisma + 自己的页面与接口。
代价:安装时要走一次「合并 schema → db push → 停服 generate → 重启」
requires: ['module/xxx@>=1.2.0'],引擎会校验「已启用 + 版本满足」。
代价:不会替你安装依赖;不满足就把你判为不可用
组件 + 页面 + 接口 + 权限点 + 数据表,一套齐全。
代价:受「建表要停服重建」与「没有沙箱」两条约束
核心优先是有意设计:扩展只能「另加」,不能「覆盖」。
代价:需要这种能力时应当改核心,而不是硬写扩展
做不到的六件事(有意如此)
- 不能改核心 —— 与核心接口冲突的扩展接口会被跳过并告警;扩展页面撞上核心页面路径会永远命中不到。 对策:换路径(公开接口建议放 /api/engine/ 下)或把改动提给核心。
- 带表的模块安装必须停服重建 —— Prisma 客户端是构建期生成的,运行时无法凭空多出一个 model。 对策:按安装结果的提示跑一遍 engine:schema → db push → 停服 generate → 重启。
- 运行时装上的页面不认 definePageMeta —— 它是 Nuxt 的编译期宏,运行时编译拿不到。 对策:需要完整后台语义(admin 布局 + 鉴权中间件)就随源码构建一次。
- 接口只能写在包内 server/api/ 下 —— 包内 server/ 路径原样映射到站点根,工具链与文档都以 server/api/** 为准。 对策:放对位置即可,路径就是 /api/**。
- 组合式函数只在自己包内可用 —— 引擎按包注入替身,避免全局命名空间互相覆盖。 对策:跨包复用做成插件 + 扩展点,或直接调对方接口。
- 没有沙箱 —— 运行时加载 = 执行装进来的代码,Node 里做不出可靠沙箱。 对策:用信任白名单 + 签名控制来源,只把你信任的人加进白名单。
改了什么时候生效
| 改了哪里 | 需要做什么 |
|---|---|
| 已有组件的模板 / 样式 | 什么都不用做(走热更新) |
| 包内 server/api/**(新增 / 改 / 删接口) | 后台点一次「重新扫描」 |
| 包内 components/ pages/(新增 / 改 / 删) | 后台点一次「重新扫描」 |
| hooks.ts | 后台点一次「重新扫描」 |
| manifest.json(名称 / 设置 / 权限 / 依赖 / 钩子声明) | 后台点一次「重新扫描」 |
| schema.prisma(数据表片段) | engine:schema → db push → 停服 generate → 重启 |
| 想彻底回收更新前的旧版本内存 | 重启服务(Node 无法卸载已加载模块) |
最快的判断方式:除了「带数据表的模块」与「彻底回收旧版本内存」,其余一切都只要一次「重新扫描」。
门禁与巡检
这套系统把踩过的坑都写成了可复跑的检查,一条命令跑完 15 个域: npm run audit(动态,打运行中的站点)与 npm run audit:full(再加静态闸门)。 退出码非 0 即失败,可直接进 CI。
它挡住的都是真实出过的问题
| engine-assets-allowlist | 扩展产物公开面只服务「当前产物表内」的地址。曾实测有 14 个孤儿产物公开可下载,其中含未激活主题的全部页面产物 |
| engine-build-orphan-dirs | 产物目录里不留「源码树已删除的包」的残渣(改名、停用、换主题都会产生) |
| theme-isolation | 未激活主题的样式与页面一律 404 —— 主题删掉不生效,才算真的能换回去 |
| api-contract / error-probes | 错误状态码语义:不成形=400、值不合法=422、冲突=409,并断言响应体里有 message |
| headers-page(CSP) | script-src 不含 unsafe-inline,且必须带内联脚本 sha256 指纹(只删不放=脚本被拦下) |
| static/api-drift | 接口快照与代码一致:破坏性变更必须走 npm run api:changelog 落账 |
| static/typecheck | 本仓类型错误基线 0(依赖内部那 1 个不计)——新代码必须干净 |
| static/unit-tests | 28 个测试文件 / 306 条用例,覆盖引擎编译、契约、错误构造、产物清单等纯逻辑 |
判定约定(读报告时按这个看)
- 失败:必须修。它代表某条不变量被破坏(产物漏出、契约漂移、类型回归…)。
- 存疑:需要人来判断,不是自动结论。例如安全头的取舍、依赖内部的类型缺陷。
- 信息 / 跳过:只做记录;跳过会写明原因(例如生产机只部署产物,源码类静态检查无从判定)。
node scripts/site-audit.mjs --base http://127.0.0.1:3000)—— 本地与生产的后台密码不同,从本机打域名会得到 3 条假失败(登录态相关),而且磁盘类检查只有在本机才准; ② 磁盘与源码类检查在只部署产物的机器上会如实跳过并写明原因,不要把它当失败。 部署
仓库自带多阶段 Dockerfile(Builder 装依赖并构建,Runner 只带 .output), 搭配 docker-compose.yml 可直接起完整环境。
docker compose up -d --build
docker compose exec app npx prisma db push # 首次部署建表
docker compose exec app npm run db:seed # 首次部署写入初始数据上线前必须确认
- NUXT_PUBLIC_SITE_URL 改成正式域名 —— canonical / og:url / sitemap 全部以它为准
- NUXT_JWT_SECRET 换成强随机值,不要用默认值
- 数据库与 Redis 不要暴露到公网,只在内网互通
- 后台入口 /admin 建议再做一层访问控制(IP 白名单或反代 Basic Auth)
- 在后台「SEO 设置」里填站长平台验证码与推送令牌,然后执行一次「推送全部地址」
二次开发指南
下面是四件最常做的事,按顺序照做即可,不需要先通读整个代码库。
① 新增一个前台页面
- 1. 在
pages/下建 .vue 文件,文件名就是路由。 - 2. 第一行调用
useSeo({'{ title, description, keywords, breadcrumb }'})。 - 3. 用
<PageHero>做页头,外层用bm-container+bm-sections-compact。 - 4. 若希望它出现在 sitemap,把路径加进
server/utils/seo-pages.ts的 SEO_PAGES。 - 5. 执行
npm run docs:build,本文档的页面清单会自动更新。
② 新增一个后台接口
- 1. 在
server/api/admin/下建文件,命名资源.方法.ts(如 orders.post.ts)。 - 2. 用 zod 校验请求体,返回前调用
logOperation()记操作日志。 - 3. 在
server/utils/route-perms.ts登记权限点(不登记则只校验登录态)。 - 4. 在
server/utils/permissions.ts把权限点加进分组,后台「角色权限」里才会出现。 - 5. 前台写操作记得带 CSRF 头:
headers: useCsrfHeader()。
③ 新增一张数据表
- 1. 在
prisma/schema.prisma里加 model,并在注释里写清用途。 - 2. 执行
npx prisma db push(开发)或 migrate(生产)。 - 3. 软删除是本站约定:加
deletedAt DateTime?,前台查询一律过滤它。 - 4. 重新生成本文档:
npm run docs:build。
④ 新增一套业务系统(不用改代码)
- 1. 后台「系统产品」→ 新建,填名称、slug、简介、痛点、目标客户、功能、交付方式、SEO 标题描述。
- 2. 状态设为上线:前台系统矩阵、首页、站内搜索、sitemap、结构化数据会自动带上它。
- 3. 有版本迭代就在「版本日志」里记录,前台自动生成版本日志页。
开发常见问题
改了页面文案,为什么分享出去还是旧标题?
标题的优先级是「后台 SEO 设置 › 页面里 useSeo 的默认值」。先确认后台「SEO 设置」里该页有没有单独填过;另外社交平台会缓存已抓取过的卡片,可用平台的调试工具强制重新抓取。
为什么 /systems?tag=game 的 canonical 指向 /systems?
筛选态不是独立页面,把它当成独立可收录页会产生重复内容。useSeo 统一只取 path 生成 canonical,后台筛选、搜索等 query 参数都会被收敛回干净地址。
后台保存后没有推送成功?
三个前提:站点级设置里「内容变更时自动推送」已开启、百度 token 或 IndexNow key 至少配了一个、NUXT_PUBLIC_SITE_URL 是正式域名。手动点一次「推送全部地址」能看到具体报错。
为什么 Google 没有主动推送?
Google 的 Indexing API 只对招聘(JobPosting)与直播(BroadcastEvent)两类结构化数据开放,普通网页用它属于违反使用条款且不生效。Google 的收录靠 sitemap 与自然抓取,这也是 sitemap 里 lastmod 必须真实的原因。
开发文档的数据会过期吗?
不会。public/dev-docs.json 由 scripts/generate-dev-docs.mjs 扫描源码生成,npm run build 前自动执行(prebuild)。如果本地改了代码想看最新文档,执行 npm run docs:build。
能不能不要 Redis?
可以,但演示账号临时会话与部分缓存会退化成内存实现,多实例部署时会不一致。单机部署且不用演示功能的话,把 NUXT_DEMO_ACCOUNT_EXPIRE_HOURS 之外的依赖绕开即可。
这套代码可以商用吗?
源码完整交付、可自行部署与二次开发。具体授权范围以与禾君科技的约定为准,欢迎通过 /contact 沟通。
