跳到主要内容
开发者中心 · 开发文档

开发文档与接口查询

这套系统的完整开发资料:从快速上手、数据模型、全部 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 devnuxt dev
npm run gatenode scripts/gate-compile.mjs
npm run gate:typesnode scripts/gate-typecheck.mjs
npm run gate:auto-importstsx scripts/gate-auto-imports.ts
npm run log:500powershell -NoProfile -Command "Select-String '\[500\]' .dev.log | Select-Object -Last 20"
npm run prebuildnpm 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 buildnuxt build
npm run generatenuxt generate
npm run previewnuxt preview
npm run postinstallprisma generate
npm run db:pushprisma db push
npm run db:pullprisma db pull
npm run db:studioprisma studio
npm run db:seedtsx prisma/seed.ts
npm run db:doctornode scripts/check-migrations.cjs
npm run og:buildnode scripts/generate-og-image.mjs
npm run docs:buildnpm run engine:spec && node scripts/generate-dev-docs.mjs && npm run docs:guide
npm run docs:guidetsx scripts/generate-dev-guide.ts
npm run api:difftsx scripts/api-changelog.ts diff
npm run api:changelogtsx scripts/api-changelog.ts write
npm run enginetsx scripts/engine.ts
npm run engine:listtsx scripts/engine.ts list
npm run ci:validatetsx scripts/engine.ts validate
npm run engine:validatetsx scripts/engine.ts validate
npm run engine:spectsx scripts/engine.ts spec
npm run engine:gctsx scripts/engine.ts gc
npm run engine:newtsx scripts/engine.ts new
npm run engine:packtsx scripts/engine.ts pack
npm run engine:publishtsx scripts/engine-publish.ts
npm run engine:keygentsx scripts/engine.ts keygen
npm run engine:removetsx scripts/engine.ts remove
npm run engine:schematsx scripts/engine.ts schema
npm run engine:licensetsx scripts/engine.ts license
npm run typechecknuxt typecheck
npm run testvitest run
npm run auditnode scripts/site-audit.mjs
npm run audit:fullnode 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/5Nuxt 插件(客户端动效、Web Vitals、SEO 配置预取等)
prisma/13数据库 schema 与种子数据
public/8静态资源(logo、默认分享图、开发文档 JSON)
scripts/32构建期脚本(分享图生成、开发文档生成)
server/254服务端:api 接口 / middleware 鉴权 / routes(sitemap、robots)/ utils / tasks
stores/0Pinia 状态(目前仅站点配置)
tests/60单元测试(vitest)
types/1全局类型声明

数据模型

来自 prisma/schema.prisma。 改结构后执行 npx prisma db push 同步到数据库。

AdminUser管理员用户36 字段
字段类型必填说明
idString是
usernameString是
passwordHashString是
nameString是
roleIdString是
roleAdminRole是关联字段
rememberMeBoolean是
/*=====是
下面三组字段回答的是同一个问题:**这个账号能碰什么**。undefined是
⚠️【空是
(沿用角色的全部权限)✓。理由:站主给自己账号加了限制、或者批量给老账号一个默认值,undefined是
都可能**把自己关在外面**✓是
/*找回密码的两个投递地址是
它挡的是"两个管理员写同一个手机号⇒是
可空列的多NULL是
emailString?否
phoneString?否
/*active是
statusString是
/*操作权限(站主要的那三项:新增是
存的是CREATE是
opAllowString[]是关联字段
/*指定栏目:**内容板块**(页面内容是
取值见server/utils/admin/scopes.ts是
columnScopeString[]是关联字段
/*指定功能:**后台功能模块**(站点配置是
取值见同一文件的FEATURE_SCOPES是
featureScopeString[]是关联字段
/*指定语言。⚠️是
这一维现在**先落库**,存的是多语言功能开放后要用的判据(现在还没有任何出口读它)——界面上必须写明这一点是
不许让人以为设了就有用✓。是
localeScopeString[]是关联字段
lastLoginAtDateTime?否
createdAtDateTime是
updatedAtDateTime是
operationLogsOperationLog[]是关联字段
AdminRole角色权限 RBAC11 字段
字段类型必填说明
idString是
/*角色标识:**登录令牌里存的就是它**(见是
nameString是
/*显示名(中文,界面上给站主看)。英文名是
labelString?否
permissionsString[]是关联字段
/*内置组排在前面(顺序即选项顺序,稳定是
sortInt是
usersAdminUser[]是关联字段
createdAtDateTime是
updatedAtDateTime是
PageContent页面静态内容(首页、关于、共建等;基座 M3 起支持后台新建与发布状态)11 字段
字段类型必填说明
idString是
pageKeyString是页面标识:同时是路由 /{pageKey} 与导航候选
titleString是
summaryString?否一句话摘要(列表与 SEO description 用)
contentString是
publishedBoolean是
settingsJson?否
localeString?否
blocksJson?否
createdAtDateTime是
updatedAtDateTime是
MediaAsset媒体库(基座 M3):上传即登记,删除前查引用(产品图集/页面内容/SEO 分享图/站点配置)7 字段
字段类型必填说明
idString是
urlString是/uploads/xxx.png
mimeString是
sizeInt是字节
altString?否
uploaderIdString?否
createdAtDateTime是
Form防灌水:提交接口带蜜罐字段 _website(前台表单隐藏填写,机器人会填)+ 按 IP 限频。8 字段
字段类型必填说明
idString是
keyString是提交接口 /api/forms/{key}/submit
nameString是
fieldsJson是
enabledBoolean是
createdAtDateTime是
updatedAtDateTime是
submissionsFormSubmission[]是关联字段
SiteConfig否则会随公开配置一起下发到浏览器。5 字段
字段类型必填说明
idString是
keyString是
valueString?否
descriptionString?否
updatedAtDateTime是
SeoPageSetting单页 SEO 覆盖(后台「SEO 设置」按页面维护;留空则用页面上写死的默认值)9 字段
字段类型必填说明
idString是
pageKeyString是站点内的规范路径:/ , /about , /systems ...
titleString是
descriptionString是
keywordsString是
robotsString是留空表示沿用页面默认(通常 index, follow)
ogImageString是
createdAtDateTime是
updatedAtDateTime是
SeoPushLog搜索引擎推送记录(每次主动推送留痕,便于判断是否被收录前先确认推送成功)6 字段
字段类型必填说明
idString是
scopeString是all / urls / auto
urlCountInt是
enginesString是各引擎结果的 JSON 摘要
okBoolean是
createdAtDateTime是
NavItem(server/utils/nav-defaults.ts),所以「从没配置过导航」与「今天的站点」完全一致。11 字段
字段类型必填说明
idString是
scopeString是main / footer / admin
parentIdString?否二级菜单挂在一级的 id 上(只支持两层)
labelString是
hrefString是
descriptionString?否下拉里的说明文字(可选)
iconString?否下拉卡片里的单字标识(可选)
sortInt是
enabledBoolean是
createdAtDateTime是
updatedAtDateTime是
EnginePackage文件清单以构建期扫描结果为准(避免两边不一致时不知道该信谁)。11 字段
字段类型必填说明
idString是
typeString是template / module / plugin
keyString是包的 id(目录名,与 manifest.id 一致)
nameString是
versionString是
enabledBoolean是
settingsJson?否
signerKeyIdString?否
installedFromString?否
createdAtDateTime是
updatedAtDateTime是
EngineTrustedKey吊销必须能随手做且立刻生效,环境变量做不到;环境变量保留为首次引导。9 字段
字段类型必填说明
idString是
keyIdString是
publicKeyString是
authorString是
noteString?否
sourceString是
revokedAtDateTime?否
createdAtDateTime是
updatedAtDateTime是
EngineLicense两者不一致时以 raw 为准 —— 解析字段只是缓存,不参与判定。14 字段
字段类型必填说明
idString是
typeString是
keyString是
rawString是
licensedToString?否
domainString?否
majorString?否
expiresAtString?否
issuedAtString?否
issuerKeyIdString?否
uploadedByString?否
noteString?否
createdAtDateTime是
updatedAtDateTime是
OperationLog管理员操作日志12 字段
字段类型必填说明
idString是
/*【2026-10-07是
任何登录过的管理员都会留下`login`是
表现是"删除按钮点了报错",而真正的原因是"他登录过"✓是
现在:**删账号不再删日志**✓(`detail`是
审计要的是"发生过什么",不是"那个用户名还在不在"✓。是
adminUserIdString?否
adminUserAdminUser?否关联字段
actionString是
detailString?否
ipString?否
createdAtDateTime是
WebVitalRecordWeb Vitals性能指标记录6 字段
字段类型必填说明
idString是
metricString是LCP / INP / CLS
valueFloat是
pagePathString是
userAgentString?否
createdAtDateTime是
ProductLog版本日志(按产品分组)8 字段
字段类型必填说明
idString是
productIdString是
productProduct是关联字段
versionString是
updateTypeString是新增 / 优化 / 修复
contentString是
createdAtDateTime是
updatedAtDateTime是
DemoApplication系统演示申请 / 开通记录10 字段
字段类型必填说明
idString是
productIdString是
productProduct是关联字段
leadNameString是
contactString是
tempAccountString?否
expireAtDateTime?否
statusString是pending / active / expired
createdAtDateTime是
updatedAtDateTime是
Faq常见问题(FAQ):后台可维护,/faq 页与「联合共建」页共用同一数据源9 字段
字段类型必填说明
idString是
questionString是
answerString是
categoryString是前台按分类分组展示
sortWeightInt是越大越靠前
statusBoolean是true 前台显示 false 隐藏
deletedAtDateTime?否
createdAtDateTime是
updatedAtDateTime是
Lead表里只存字符串 —— 状态集合是业务约定,不是数据库约束。13 字段
字段类型必填说明
idString是
nameString是
companyString?否
contactString是
memberIdString?否
memberMember?否关联字段
interestSlugString?否
descriptionString?否
statusString是new / contacted / demo / negotiating / closed / archived
remarkString?否
deletedAtDateTime?否
createdAtDateTime是
updatedAtDateTime是
Tag产品标签:只用于给产品分类,所以与产品同属一个模块6 字段
字段类型必填说明
idString是
nameString是
slugString是
productsProduct[]是关联字段
createdAtDateTime是
updatedAtDateTime是
Product自研业务系统(产品)22 字段
字段类型必填说明
idString是
nameString是
slugString是
shortDescriptionString是
descriptionString是
painPointsString是
targetCustomerString是
featuresString[]是关联字段
imageGalleryString[]是关联字段
deliveryTypeString是
deploymentMethodString是
statusBoolean是true上线 false隐藏
sortWeightInt是
seoTitleString是
seoDescriptionString是
deletedAtDateTime?否
deletedSlugString?否
tagsTag[]是关联字段
changelogsProductLog[]是关联字段
demoApplicationsDemoApplication[]是关联字段
createdAtDateTime是
updatedAtDateTime是
Member空串是一个**值**,两个空串就会撞唯一索引。10 字段
字段类型必填说明
idString是
emailString?否
phoneString?否
passwordHashString是
nameString是
localeString?否
statusString是
createdAtDateTime是
updatedAtDateTime是
leadsLead[]是关联字段
Term现有 ProductTag 数据由迁移脚本搬到这里(Term + TermLink),Tag 模型保留待模块下个版本切换。6 字段
字段类型必填说明
idString是
taxonomyString是
nameString是
slugString是
sortInt是
linksTermLink[]是关联字段
TermLink分类与内容的通用关联:不外联具体表(targetType + targetId),一张表服务全部内容类型。5 字段
字段类型必填说明
idString是
termIdString是
termTerm是关联字段
targetTypeString是
targetIdString是
SiteTextkey 清单见 composables/text-registry.ts。4 字段
字段类型必填说明
keyString是
valueString是
noteString是
updatedAtDateTime是
SearchTerm· 不做任何用户身份记录(没有 IP、没有会员 id):这里统计的是"词",不是"人"。4 字段
字段类型必填说明
idString是
termString是
countInt是
lastAtDateTime是
Notification/ `audience` 区分收件人类型:`admin` = 后台管理员(当前唯一在用)、`member` = 会员(预留)。10 字段
字段类型必填说明
idString是
audienceString是admin / member
adminUserIdString?否
memberIdString?否
eventString是
titleString是
bodyString是
levelString是info / warn
readAtDateTime?否
createdAtDateTime是
NotifyLog/ `target` 存收件人标识,但**只留尾部**(邮箱/手机号是个人信息,别整串进库与日志 ✗)。11 字段
字段类型必填说明
idString是
eventString是
channelString是
targetString是
okBoolean是
attemptsInt是
errorString是
durationMsInt是
titleString是
bodyString是
createdAtDateTime是

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**(不留孤儿文件 ✗)。

    路径参数 name

    server/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/blocksJWT

    server/api/admin/blocks.get.ts

/api/admin/content-snippets2 个

  • GET/api/admin/content-snippetsJWT

    server/api/admin/content-snippets.get.ts

  • PUT/api/admin/content-snippetsRBACpage:edit

    server/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/enginesJWT

    server/api/admin/engines.get.ts

  • DELETE/api/admin/engines/:idJWT
    路径参数 id

    server/api/admin/engines/[id].delete.ts

  • PUT/api/admin/engines/:idJWT
    路径参数 id 请求体: type:enumenabled?:booleansettings?:record

    server/api/admin/engines/[id].put.ts

  • DELETE/api/admin/engines/:id/licenseJWT
    路径参数 id

    server/api/admin/engines/[id]/license.delete.ts

  • PUT/api/admin/engines/:id/licenseRBACengine:install
    路径参数 id 请求体: type:enumlicense:string

    server/api/admin/engines/[id]/license.put.ts

  • POST/api/admin/engines/:id/rollbackRBACengine:install
    路径参数 id 请求体: type:enumname:string

    server/api/admin/engines/[id]/rollback.post.ts

  • POST/api/admin/engines/disable-third-partyRBACengine:install

    server/api/admin/engines/disable-third-party.post.ts

  • POST/api/admin/engines/installRBACengine:install

    server/api/admin/engines/install.post.ts

  • GET/api/admin/engines/marketJWT

    server/api/admin/engines/market.get.ts

  • POST/api/admin/engines/reloadJWT

    server/api/admin/engines/reload.post.ts

  • GET/api/admin/engines/themeJWT

    server/api/admin/engines/theme.get.ts

  • PUT/api/admin/engines/themeJWT
    请求体: id?:stringvalues?:record

    server/api/admin/engines/theme.put.ts

  • GET/api/admin/engines/trustRBACengine:install

    server/api/admin/engines/trust.get.ts

  • POST/api/admin/engines/trustRBACengine:install
    请求体: publicKey:stringauthor?:stringnote?:string

    server/api/admin/engines/trust.post.ts

  • DELETE/api/admin/engines/trust/:keyIdJWT
    路径参数 keyId

    server/api/admin/engines/trust/[keyId].delete.ts

  • PUT/api/admin/engines/trust/:keyIdJWT
    路径参数 keyId 请求体: action:enum

    server/api/admin/engines/trust/[keyId].put.ts

/api/admin/forgot1 个

  • POST/api/admin/forgotJWT
    请求体: username:stringchannel:enum

    server/api/admin/forgot.post.ts

/api/admin/forms6 个

  • GET/api/admin/formsRBACform:manage

    server/api/admin/forms.get.ts

  • POST/api/admin/formsRBACform:manage
    请求体: key:unknownname:stringfields:unknownenabled?:boolean

    server/api/admin/forms.post.ts

  • DELETE/api/admin/forms/:idJWT
    路径参数 id

    server/api/admin/forms/[id].delete.ts

  • PUT/api/admin/forms/:idJWT
    路径参数 id 请求体: name?:stringfields?:unknownenabled?:boolean

    server/api/admin/forms/[id].put.ts

  • GET/api/admin/forms/:id/exportJWT
    路径参数 id

    server/api/admin/forms/[id]/export.get.ts

  • GET/api/admin/forms/:id/submissionsJWT
    路径参数 id

    server/api/admin/forms/[id]/submissions.get.ts

/api/admin/friend-links2 个

  • GET/api/admin/friend-linksJWT

    server/api/admin/friend-links.get.ts

  • PUT/api/admin/friend-linksRBACnav:manage

    server/api/admin/friend-links.put.ts

/api/admin/home-content2 个

  • GET/api/admin/home-contentJWT

    server/api/admin/home-content.get.ts

  • PUT/api/admin/home-contentRBACpage:edit
    请求体: content:record

    server/api/admin/home-content.put.ts

/api/admin/login1 个

  • POST/api/admin/loginJWT

    server/api/admin/login.post.ts

/api/admin/logout1 个

  • POST/api/admin/logoutJWT

    server/api/admin/logout.post.ts

/api/admin/me2 个

  • GET/api/admin/meJWT

    server/api/admin/me.get.ts

  • POST/api/admin/me/passwordJWT
    请求体: currentPassword:stringnewPassword:string

    server/api/admin/me/password.post.ts

/api/admin/media3 个

  • GET/api/admin/mediaJWT

    server/api/admin/media.get.ts

  • DELETE/api/admin/media/:idJWT
    路径参数 id

    server/api/admin/media/[id].delete.ts

  • PUT/api/admin/media/:idJWT
    路径参数 id 请求体: alt?:string

    server/api/admin/media/[id].put.ts

/api/admin/members4 个

  • GET/api/admin/membersRBACmember:manage

    server/api/admin/members/index.get.ts

  • GET/api/admin/members/:idJWT
    路径参数 id

    server/api/admin/members/[id].get.ts

  • PUT/api/admin/members/:idJWT
    路径参数 id

    server/api/admin/members/[id].put.ts

  • GET/api/admin/members/exportJWT

    server/api/admin/members/export.get.ts

/api/admin/metrics1 个

  • GET/api/admin/metricsJWT

    server/api/admin/metrics.get.ts

/api/admin/nav5 个

  • GET/api/admin/navJWT

    server/api/admin/nav.get.ts

  • POST/api/admin/navRBACnav:manage
    请求体: scope:enumlabel:stringhref:stringparentId?:stringdescription?:stringicon?:stringsort?:numberenabled?:boolean

    server/api/admin/nav.post.ts

  • DELETE/api/admin/nav/:idJWT
    路径参数 id

    server/api/admin/nav/[id].delete.ts

  • PUT/api/admin/nav/:idJWT
    路径参数 id 请求体: label?:stringhref?:stringparentId?:stringdescription?:stringicon?:stringsort?:numberenabled?:boolean

    server/api/admin/nav/[id].put.ts

  • POST/api/admin/nav/:id/moveJWT
    路径参数 id 请求体: dir:enumwhere:unknownorderBy:unknown

    server/api/admin/nav/[id]/move.post.ts

/api/admin/notify7 个

  • POST/api/admin/notify/broadcastRBACnotify:broadcast

    server/api/admin/notify/broadcast.post.ts

  • GET/api/admin/notify/channelsJWT

    server/api/admin/notify/channels.get.ts

  • GET/api/admin/notify/logsJWT

    server/api/admin/notify/logs.get.ts

  • POST/api/admin/notify/logs/:id/retryJWT
    路径参数 id

    server/api/admin/notify/logs/[id]/retry.post.ts

  • GET/api/admin/notify/messagesJWT

    server/api/admin/notify/messages.get.ts

  • PATCH/api/admin/notify/messagesJWT

    server/api/admin/notify/messages.patch.ts

  • POST/api/admin/notify/testJWT

    server/api/admin/notify/test.post.ts

/api/admin/operation-logs1 个

  • GET/api/admin/operation-logsRBAClog:view

    server/api/admin/operation-logs.get.ts

/api/admin/pages4 个

  • GET/api/admin/pagesJWT

    server/api/admin/pages.get.ts

  • POST/api/admin/pagesRBACpage:edit
    请求体: pageKey:stringtitle:stringsummary?:stringcontent?:stringpublished?:boolean

    server/api/admin/pages.post.ts

  • DELETE/api/admin/pages/:keyJWT
    路径参数 key

    server/api/admin/pages/[key].delete.ts

  • PUT/api/admin/pages/:keyJWT
    路径参数 key 请求体: title?:stringsummary?:stringcontent?:stringblocks:array[]settings?:recordpublished?:boolean

    server/api/admin/pages/[key].put.ts

/api/admin/performance1 个

  • GET/api/admin/performanceJWT

    server/api/admin/performance.get.ts

/api/admin/permissions1 个

  • GET/api/admin/permissionsJWT

    server/api/admin/permissions.get.ts

/api/admin/reset1 个

  • POST/api/admin/resetJWT
    请求体: ticket:stringcode?:unionpassword:string

    server/api/admin/reset.post.ts

/api/admin/roles4 个

  • GET/api/admin/rolesJWT

    server/api/admin/roles.get.ts

  • POST/api/admin/rolesRBACrbac:manage
    请求体: name:stringpermissions?:array[]

    server/api/admin/roles.post.ts

  • DELETE/api/admin/roles/:idJWT
    路径参数 id

    server/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-termsJWT

    server/api/admin/search-terms.get.ts

/api/admin/seo4 个

  • GET/api/admin/seoJWT

    server/api/admin/seo.get.ts

  • PUT/api/admin/seo/pages/:pageKeyJWT
    路径参数 pageKey 请求体: title?:stringdescription?:stringkeywords?:stringrobots?:stringogImage?:string

    server/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?:string

    server/api/admin/seo/site.put.ts

/api/admin/sidebar1 个

  • GET/api/admin/sidebarJWT

    server/api/admin/sidebar.get.ts

/api/admin/site-config2 个

  • GET/api/admin/site-configJWT

    server/api/admin/site-config.get.ts

  • PUT/api/admin/site-config/:keyJWT
    路径参数 key 请求体: value?:stringwhere:unknownupdate:unknowncreate:unknown

    server/api/admin/site-config/[key].put.ts

/api/admin/site-pages1 个

  • GET/api/admin/site-pagesJWT

    server/api/admin/site-pages.get.ts

/api/admin/site-texts1 个

  • PUT/api/admin/site-texts/:keyJWT
    路径参数 key 请求体: value:stringnote?:string

    server/api/admin/site-texts/[key].put.ts

/api/admin/staff5 个

  • GET/api/admin/staffRBACstaff:manage

    server/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
    路径参数 id

    server/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:manage

    server/api/admin/staff/options.get.ts

/api/admin/stats1 个

  • GET/api/admin/statsJWT

    server/api/admin/stats.get.ts

/api/admin/upload1 个

  • POST/api/admin/uploadRBACmedia:manage

    server/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路径参数 id

    server/api/engine/packages/[type]/[id]/download.get.ts

  • GET/api/engine/packages/:type/:id/source公开
    路径参数 type路径参数 id

    server/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公开
    路径参数 key

    server/api/forms/[key].get.ts

  • POST/api/forms/:key/submit公开
    路径参数 key

    server/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?:string

    server/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公开
    路径参数 slug

    server/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

兼容变更

建立变更记录基线

本次没有接口 / 模型 / 页面层面的变化

下载变更记录(JSON)

内容契约(主题/模块可依赖的数据)

二次开发与主题制作时,以下数据源是稳定承诺:字段只增不改名;弃用会先在「接口变更记录」公告。 主题侧统一经 useSiteContent(type) 读取(已做归一化与空值兜底),也可直接调用对应 API。

资源API稳定字段
产品/api/products、/api/products/:slugslug、name、summary、description、cover、features、tags、changelogCount
页面/api/pages、/api/pages/:pageKeypageKey、title、summary、content、published、settings(仅公开键)
站点配置/api/site-config注册表内键(server/utils/site-config-registry.ts),新增键须先注册
FAQ/api/faqsquestion、answer、category
版本日志/api/products/:slug/changelogsversion、updateType、content、createdAt
导航/api/navscope(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
/:slugpages/[slug].vue已接入index
/aboutpages/about.vue已接入index
/accountpages/account/index.vue已接入index
/account/securitypages/account/security.vue已接入index
/adminpages/admin/index.vue已接入noindex
/admin/accountpages/admin/account.vue已接入noindex
/admin/backuppages/admin/backup.vue已接入noindex
/admin/engine-marketpages/admin/engine-market.vue已接入noindex
/admin/engine-trustpages/admin/engine-trust.vue已接入noindex
/admin/enginespages/admin/engines.vue已接入noindex
/admin/formspages/admin/forms.vue已接入noindex
/admin/homepages/admin/home.vue已接入noindex
/admin/loginpages/admin/login.vue已接入noindex
/admin/mediapages/admin/media.vue已接入noindex
/admin/memberspages/admin/members.vue已接入index
/admin/navpages/admin/nav.vue已接入noindex
/admin/notifypages/admin/notify.vue已接入index
/admin/operation-logspages/admin/operation-logs.vue已接入noindex
/admin/pagespages/admin/pages.vue已接入noindex
/admin/performancepages/admin/performance.vue已接入noindex
/admin/resetpages/admin/reset.vue已接入noindex
/admin/rolespages/admin/roles.vue已接入noindex
/admin/seopages/admin/seo.vue已接入noindex
/admin/site-configpages/admin/site-config.vue已接入noindex
/admin/site-textspages/admin/site-texts.vue已接入index
/admin/staffpages/admin/staff.vue已接入noindex
/admin/themespages/admin/themes.vue已接入noindex
/co-developpages/co-develop.vue已接入index
/consolepages/console/index.vue已接入index
/console/demopages/console/demo.vue已接入index
/console/messagespages/console/messages.vue已接入index
/contactpages/contact.vue已接入index
/demopages/demo.vue已接入index
/developerpages/developer.vue已接入index
/docspages/docs.vue已接入index
/faqpages/faq.vue已接入index
/loginpages/login.vue已接入index
/privacypages/privacy.vue已接入index
/registerpages/register.vue已接入index
/reset-passwordpages/reset-password.vue已接入noindex
/searchpages/search.vue已接入noindex
/systemspages/systems/index.vue已接入index
/systems/:slugpages/systems/[slug]/index.vue已接入index
/systems/:slug/changelogpages/systems/[slug]/changelog.vue已接入index
/tag/:slugpages/tag/[slug].vue已接入index
/tagspages/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.ts

    registerEngineBlockDefs()allBlockDefs()blockDef()isEngineBlockDef()

    —

  • content-tokens.ts

    renderContentTokens()renderTokensDeep()

    内容占位符 / 片段 **唯一注册表**(2026-10-07 · P2-7 · 站主 ②) 站主原话:「页面内容升级为 CMS 全功能,可自定义页面内容,并可**生成自定义引用的标签给主题调用**」✓。

  • csv.ts

    csvCell()csvSafe()csvRow()csvText()

    CSV 生成(纯函数 ✓ 可单测 ✓)—— 2026-10-06 · P2-10 【为什么单独一处】后台的"导出"正在从一条变成多条(线索 ✓、会员 ✓,以后还会有别的 ✓)。

  • dev-guide.ts

    useDevGuide()

    开发者中心的「作者指南」内容(2026-10-06) 【为什么是数据、不是页面上的一堆模板】`docs/ecosystem-plan.md` §问题 4 的诊断是:

  • error-content.ts

    safeLinkTarget()parseErrorContent()

    404 / 500 错误页的内容(可配)—— 2026-10-07 · SEO 程序第 4 项 【为什么要可配】错误页不光给访客"出路" ✓,也是搜索引擎判定"这个站有没有用"的地方之一 ✓

  • friend-links.ts

    scopeOfPath()isSafeLinkUrl()normalizeFriendLinks()groupByScope()

    友情链接(2026-10-07 · SEO 第 2 项)—— 作用域注册表 + 规范化(**纯函数** ✓ 可单测 ✓) ## 站主要什么

  • media-types.ts

    extOf()mediaTypeOfName()deniedReason()mimeOfName()mediaKindOfMime()mimesOfKind()

    媒体类型白名单(**唯一真源**,前后端共用) 【为什么放在 composables/ 而不是 server/utils/】

  • search-pinyin.generated.ts

    pinyinInitials()

    站内搜索用的「汉字 → 拼音首字母」映射 —— **自动生成,请勿手改**。 生成器:scripts/gen-search-pinyin.mjs(构建期跑,需要 pinyin-pro 这个 devDependency)。

  • source-label.ts

    sourceLabel()

    「扩展从哪来」的中文标签(**唯一一处**) 数据源:`EnginePackage.installedFrom` —— 由安装路径写入:

  • text-registry.ts

    —

  • useAuth.ts

    —

  • useCsrf.ts

    useCsrfHeader()

    —

  • useEngineSettings.ts

    读某个扩展(主题 / 区块 / 模块)在后台配置的**公开设置**,可选按页面覆盖(基座 M2) 【为什么主题参数走运行时而不是构建期】主题的结构(布局、页面、样式)天生是构建期确定的,

  • useFormDraft.ts

    useFormDraft()

    —

  • useMember.ts

    useMember()

    —

  • useSeo.ts

    useSeoConfigState()useSeo()

    —

  • useSiteContent.ts

    normalizeSiteContent()mergeSettings()

    站内内容的统一读取入口 —— **基座对外内容契约**(主题与核心页面共用) 【它解决什么】主题(尤其由 HTML 模板改来的)要展示站内内容时,不该去记

  • useSiteSearch.ts

    tokenize()useSiteSearch()

    —

  • useSiteTexts.ts

    useSiteTexts()

    —

服务端构成

鉴权中间件

  • server/middleware/admin-auth.ts

  • server/middleware/csrf.ts

  • server/middleware/indexnow-key.ts

  • server/middleware/maintenance.ts

  • server/middleware/request-id.ts

    每个请求一个短编号(8 位十六进制),挂在 `event.context.requestId` 并回写成响应头 `x-request-id`。 【为什么需要】以前出 500 时,日志里只有一行 `[req] GET /api/search → 500 57ms`

  • server/middleware/request-log.ts

  • server/middleware/security.ts

  • server/middleware/seo-verify-file.ts

独立路由(非 /api)

  • api/v1/[...path].ts

  • engine-assets/[...path].get.ts

  • robots.txt.get.ts

    robots.txt(PRD SEO 规范 7.2) 之前全站没有这个文件,/robots.txt 直接 404 —— 但 sitemap.xml 已经做好了,

  • sitemap.txt.get.ts

  • sitemap.xml.get.ts

服务端工具

  • admin/dashboard.ts

    仪表盘卡片注册表(P1-6,**唯一真源**) 【站主要什么】"仪表盘卡片要能注册、能开关、能排序" ✓。

  • admin/grants.ts

    「这个管理员这一次请求能不能做」的**裁决层**(纯函数,单测直接 import ✓) 【为什么单独一层,不把判断散在中间件里】站主要的四件事(新增 / 修改 / 删除 · 指定语言 ·

  • admin/nav.ts

    P1-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.ts

  • api-changelog.ts

    API 与数据结构的**变更记录**(纯逻辑,可单测) 【它解决什么问题】接口是二次开发者的合同,但合同是会改的:

  • api-error.ts

  • audit.ts

  • auth.ts

  • backup/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.ts

  • content-cache.ts

    内容缓存内核(基座)—— **服务端那一半的内容契约** 【它补的是什么】前端有 `useSiteContent`(契约 v1.0.0,M1 完成)把接口结果归一化成稳定形状;

  • content-snippets.ts

  • csp.ts

  • csrf.ts

  • db.ts

  • engine/api-perms.ts

  • engine/auto-import-names.ts

    扩展接口里可以直接使用的「自动导入」名字(唯一权威清单) 【为什么需要这个概念】

  • engine/auto-imports.ts

  • engine/backup.ts

  • engine/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.ts

  • engine/graph.ts

  • engine/heavy-deps.ts

    重型编译依赖的延迟加载(**必须**用「变量说明符 + 动态 import」,不要改成静态 import) 【为什么这条规则这么硬 —— 一次整站 500 的教训】

  • engine/hooks.ts

  • engine/hosts.ts

  • engine/index-format.ts

  • engine/install.ts

  • engine/license-store.ts

  • engine/license.ts

  • engine/manifest-schema.ts

  • engine/manifest.ts

  • engine/market-catalog.ts

  • engine/market-index.ts

  • engine/package-source.ts

  • engine/perm-table.ts

    扩展权限表汇总(纯函数,无 IO) 单独拆出来的原因与 graph.ts 相同:这段逻辑原来与数据库查询写在一起,

  • engine/pure-frontend.ts

  • engine/regions.ts

  • engine/registry.ts

  • engine/reload.ts

  • engine/routes.ts

  • engine/runtime.ts

  • engine/scan.ts

  • engine/settings.ts

    扩展设置的归一化(基座 C 项收口) 【解决什么】`EnginePackage.settings` 原本是"无 schema 的 JSON":值一律按字符串存

  • engine/sfc.ts

  • engine/signing.ts

  • engine/submission-review.ts

  • engine/surface.ts

    运行面(surface)判据 —— **后台轨道与前台轨道的唯一分界点**。 【为什么必须有这个文件】站主 2026-09-30 的判断:「后台的页面会涉及到前台的显示,这个明显就是

  • engine/theme-assembly.ts

  • engine/theme-contract.ts

  • engine/theme-validate.ts

  • engine/theme.ts

  • engine/trust-store.ts

  • engine/trust.ts

  • engine/ui-bridge.ts

    引擎 UI 运行时桥(**刻意零依赖**:Nitro 侧与 Nuxt 应用侧都会 import 它) 【为什么需要这个文件】

  • engine/ui-inventory.ts

    运行时 UI 产物的「清单守卫」(纯函数:不碰 fs / nitro / vue,可直接单测) 管两件底层边界的事:

  • engine/ui-shims.ts

  • engine/ui-table.ts

  • engine/ui-usable.ts

    运行时 UI 产物「是否可渲染」的判定(纯函数,可单测) 【为什么需要它】扩展的组件/页面是服务端就地编译出来的(server/utils/engine/sfc.ts),

  • engine/ui.ts

  • engine/uninstall.ts

  • forms.ts

  • friend-links.ts

  • home-content.ts

    首页内容(形状 + 解析 + 规范化) 【它是什么】首页各段内容存在 `SiteConfig.home_content`(JSON 字符串,见 site-config-registry.ts),

  • jobs.ts

  • member-auth.ts

  • member-identity.ts

    会员登录标识:双通道(手机号或邮箱)。 【为什么单独一个文件】注册、登录、会员改资料、以及"线索按手机号挂到会员"都要用同一套

  • member-reset.ts

  • metrics.ts

  • nav-defaults.ts

    内置默认导航(基座 M1) 【唯一事实在服务端】公开接口 `/api/nav` 在某个 scope 没有任何启用条目时回落到这里,

  • notify-templates.ts

    通知**事件**与**正文模板**(唯一真源,2026-10-06 P0-3) 【站主要什么】"通知没做对应要有设置和开关" —— 上一轮(P0-2 之前)已经把**通道**开关做完了 ✓

  • notify.ts

  • perm-match.ts

    「方法 + 路径 → 权限键」的匹配规则(纯函数,核心与扩展**共用同一套**) 【为什么必须共用】核心的 route-perms.ts 与扩展的 api-perms.ts(模块在清单里声明的权限点)

  • permissions.ts

    RBAC 权限清单(PRD 5.7 颗粒度:页面访问 / 新增 / 编辑 / 删除 / 导出) ⚠️ 与 server/middleware/admin-auth.ts 的 ROUTE_PERMS 一一对应:

  • public-content.ts

  • push-paths.ts

  • push-plan.ts

  • push-quota.ts

    推送配额相关的纯逻辑(2026-09-30) 【为什么会有这个文件】站主第一次手动全量推送就撞上百度的 `over quota`:

  • rate-limit.ts

  • redis.ts

  • route-perms.ts

    后台路由 → 所需权限 的映射与匹配(PRD 5.7) 抽成独立纯函数是为了能直接单测 —— 这段逻辑一旦写错,

  • search-push.ts

  • seo-config.ts

  • seo-page-keys.ts

  • seo-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.ts

  • site-urls.ts

  • sms-aliyun.ts

  • source-package.ts

  • wechat-mp.ts

    微信公众号模板消息(2026-10-06) 【门槛先写清楚】这条通道需要**已认证的服务号**(订阅号没有模板消息接口 ✗),

Nitro 插件与定时任务

  • csp-hashes.ts

  • engine.ts

  • error-report.ts

    500(及其它未处理的服务端异常)的统一留痕。 【为什么要这一层】此前唯一的留痕是 request-log 里的一行摘要:

  • front-css.ts

  • noise-filter.ts

    dev 日志降噪:过滤「同名组件重复注册」这一条 Vue 警告。 【为什么这条是噪音,不是问题】

  • seo-autopush.ts

  • startup-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.ts

  • server/tasks/demo-recycle.ts

  • server/tasks/health-alert.ts

  • server/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-secret
  • NUXT_JWT_EXPIRES_IN默认 7d
  • NUXT_BCRYPT_ROUNDS默认 12

站点公开信息

⚠️ 生产必须改成正式域名:canonical / og:url / sitemap.xml / robots.txt 全部以它为准。

  • NUXT_PUBLIC_SITE_URL默认 https://www.hejun1.com

SEO 站长平台验证(可选)

也可以直接在后台「SEO 设置」里填,那边优先级更高、改完即时生效;

  • NUXT_PUBLIC_GOOGLE_SITE_VERIFICATION
  • NUXT_PUBLIC_BING_SITE_VERIFICATION
  • NUXT_PUBLIC_BAIDU_SITE_VERIFICATION

搜索引擎主动推送

百度普通收录 token 与 IndexNow key 都只在后台「SEO 设置」里填,

消息推送 Webhook(企微 / 飞书),enabled 控制总开关

  • NUXT_WEBHOOK_WECOM
  • NUXT_WEBHOOK_FEISHU
  • NUXT_WEBHOOK_ENABLED默认 false

邮件通知(Nodemailer)

  • NUXT_MAIL_HOST
  • NUXT_MAIL_PORT默认 465
  • NUXT_MAIL_SECURE默认 true
  • NUXT_MAIL_USER
  • NUXT_MAIL_PASS
  • NUXT_MAIL_FROM

临时演示账号有效期(小时)

  • NUXT_DEMO_ACCOUNT_EXPIRE_HOURS默认 24

初始管理员账号(prisma/seed.ts / 巡检 scripts/site-audit.mjs 共用)

⚠️ 改名或改密码时**三处必须一起改**:这里、prisma/seed.ts(读同一对变量)、数据库里那一行。

  • SEED_ADMIN_USERNAME默认 chaos
  • SEED_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 文件。下面五步能把全流程走通, 不需要读内核代码,也不需要改任何核心文件。

  1. ① 生成骨架npm run engine:new -- --type plugin --id my-plugin --name "我的插件"

    生成 engines/plugins/my-plugin/,包含 manifest.json、hooks.ts、README.md。骨架本身是干净的(零错误零告警)。

    已生成 engines\plugins\my-plugin
  2. ② 自检npm run engine:validate

    必须「零错误零告警」再往下走。这一步同时会校验规范快照与源码是否一致,所以它也能挡住「文档过期」。

    插件(2)
      ✓ lead-dup-guard   1.0.0  线索重复提示
      ✓ my-plugin        1.0.0  我的插件
    共 2 个包,其中 0 个清单有问题
  3. ③ 写实现编辑 hooks.ts 与 manifest.json

    在 hooks.ts 里 on('lead:created', …);同时把 lead:created 写进 manifest.json 的 hooks。两边必须一致:声明了没实现会被拒绝,实现了没声明则不会被注册。

  4. ④ 启用npm run dev → 后台「引擎与扩展」→ 重新扫描 → 启用

    插件与钩子是运行时加载的:改完源码只需要再点一次「重新扫描」,不用重新构建、也不用重启。新装的包默认是停用的。

  5. ⑤ 验证前台 /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 直接生成 —— 它永远等于引擎真正执行的那套规则,不存在「文档落后于代码」。 未知字段会直接报错,不会被静默忽略。

字段类型必填默认说明
$schemastring否—(给编辑器用)指向 engines/manifest.schema.json,如 "../../manifest.schema.json";引擎会忽略它
idstring是—扩展 id。必须与包目录名一致(引擎会核对,不一致直接判为清单非法) 匹配 ^[a-z][a-z0-9-]{1,40}$
typeenum(theme | module | plugin)是—扩展类型。必须与所在目录一致:themes/ → theme,modules/ → module,plugins/ → plugin
namestring是—显示名称,出现在后台「引擎与扩展」列表与文档里(1–40 字) 1–40 字
versionstring是—语义化版本,形如 1.0.0。它决定运行时产物目录(.engine-build/<id>@<version>/) 匹配 ^\d+\.\d+\.\d+(-[a-z0-9.]+)?$
descriptionstring是—一句话说明这个扩展解决什么问题(1–300 字) 1–300 字
authorstring否""作者署名,会写进打包描述文件 最多 60 字
licensestring否"MIT"开源协议,默认 MIT 最多 40 字
homepagestring否""项目主页或仓库地址 最多 300 字
activationobject否{}授权声明(离线授权码)。**只有付费扩展需要**:required 为 true 时,站主必须贴进一张由你签发的授权码才能启用。免费扩展不要加这个字段 —— 加了就是白给自己加一道门。授权码用 npm run engine:license 签发
compatobject否{}兼容性声明
requiresstring[]否[]依赖的其他扩展,写「类型/id」,可加版本范围(@>=1.2.0 / @^1.2.0 / @~1.2.0 / 多个用空格分隔)。依赖未启用、或版本不满足时本扩展会被判为不可用(不会半启动);引擎会检测循环依赖
hooksstring[]否[]声明监听的扩展点。只有列在这里的钩子才会被注册;写未埋点的钩子会被拒绝
entrystring否""入口组件名(不含 .vue)。包内只有一个组件时可省略,引擎会自动取那一个 最多 60 字
regionsstring[]否[]自动注入区域,如 layout.top、home.after-systems。留空表示只支持被 <TemplateSection> 点名使用
adminNavobject[]否[]后台侧边栏追加项。与核心导航重复时保留核心项
permissionsobject[]否[]扩展自带的权限点。声明后模块的后台接口无需改核心的 route-perms.ts
settingsobject[]否[]可在后台配置的设置项
blocksobject[]否[]本扩展提供的页面区块类型。声明后:后台页面编辑器能选到它 ✓、前台由引擎按 component 解析渲染 ✓ —— 模块作者不需要改核心的 block-registry.ts
builtinboolean否false是否随源码交付的内置扩展。内置项默认启用且不允许卸载,只允许停用

settings 数组里每一项的子字段

设置项会在后台「引擎与扩展」里按 type 渲染成表单。 组件里靠键名与 props 同名自动接收:清单里声明 key: "title", 组件里写 defineProps<{ title?: string }>() 即可,不用自己取数据。 只有 public: true 的项才会下发给前台接口。

字段类型默认说明
keystring—设置键。必须与组件 props 同名才会被自动传入;只允许小写字母、数字、下划线
labelstring—后台表单里显示的标签(1–40 字)
typeenum(text | textarea | bool | number | url)"text"表单控件类型,决定后台如何渲染这一项
defaultstring""默认值。清单里统一写字符串;引擎按 type 归一化成 boolean / number 后才下发给组件(bool 写 "true" / "false")
hintstring""填写提示,显示在输入框下方
publicbooleanfalse是否允许前台 /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')。

来源可直接使用的名字
h3defineEventHandler / 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. 1. 在包目录下建 schema.prisma,只写自己的 model(不要写 datasource / generator)。
  2. 2. 执行 npm run engine:schema:片段会以标记块形式合并进 prisma/schema.prisma,审查 diff 时一眼可见。
  3. 3. 执行 npx prisma db push 建表,然后 停掉 dev server 跑 npx prisma generate(Windows 上 dev server 占用着查询引擎 dll,不停服会报 EPERM)。
  4. 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.prismaengine: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。

前台某处加一个可配置的展示区块很合适theme(区块)

components/ + entry + regions + settings,声明区域后由页面自动注入。

每个页面顶部加一条公告很合适theme(区块)/ module

组件 + regions: ['layout.top'];要后台管理就用模块(带页面与接口)。

提交线索时做校验 / 去重 / 通知很合适plugin

hooks.ts 里监听 lead:created;不占路由、不建表。

把数据同步到外部系统、埋点上报合适plugin

hooks.ts 里用注入的 prisma 读数据,再发出去。

后台加一个管理页(列表 / 新增 / 编辑)很合适module

pages/admin/*.vue + server/api/admin/** + permissions + adminNav。

代价:页面要用 definePageMeta(布局/鉴权)就得随源码构建一次

对外提供一组公开 API合适module

server/api/engine/*.ts,路径原样映射为 /api/engine/…。

代价:接口文件不能与核心接口重名(核心优先,冲突会被跳过并告警)

长期存自己的业务数据(建表)能做,有代价module

schema.prisma + 自己的页面与接口。

代价:安装时要走一次「合并 schema → db push → 停服 generate → 重启」

依赖另一个扩展的数据或能力合适任意

requires: ['module/xxx@>=1.2.0'],引擎会校验「已启用 + 版本满足」。

代价:不会替你安装依赖;不满足就把你判为不可用

做一个完整业务子系统技术上可以module

组件 + 页面 + 接口 + 权限点 + 数据表,一套齐全。

代价:受「建表要停服重建」与「没有沙箱」两条约束

替换或改造核心页面 / 核心接口做不到—

核心优先是有意设计:扩展只能「另加」,不能「覆盖」。

代价:需要这种能力时应当改核心,而不是硬写扩展

做不到的六件事(有意如此)

  • 不能改核心 —— 与核心接口冲突的扩展接口会被跳过并告警;扩展页面撞上核心页面路径会永远命中不到。 对策:换路径(公开接口建议放 /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-tests28 个测试文件 / 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. 1. 在 pages/ 下建 .vue 文件,文件名就是路由。
  2. 2. 第一行调用 useSeo({'{ title, description, keywords, breadcrumb }'})。
  3. 3. 用 <PageHero> 做页头,外层用 bm-container + bm-sections-compact。
  4. 4. 若希望它出现在 sitemap,把路径加进 server/utils/seo-pages.ts 的 SEO_PAGES。
  5. 5. 执行 npm run docs:build,本文档的页面清单会自动更新。
② 新增一个后台接口
  1. 1. 在 server/api/admin/ 下建文件,命名 资源.方法.ts(如 orders.post.ts)。
  2. 2. 用 zod 校验请求体,返回前调用 logOperation() 记操作日志。
  3. 3. 在 server/utils/route-perms.ts 登记权限点(不登记则只校验登录态)。
  4. 4. 在 server/utils/permissions.ts 把权限点加进分组,后台「角色权限」里才会出现。
  5. 5. 前台写操作记得带 CSRF 头:headers: useCsrfHeader()。
③ 新增一张数据表
  1. 1. 在 prisma/schema.prisma 里加 model,并在注释里写清用途。
  2. 2. 执行 npx prisma db push(开发)或 migrate(生产)。
  3. 3. 软删除是本站约定:加 deletedAt DateTime?,前台查询一律过滤它。
  4. 4. 重新生成本文档:npm run docs:build。
④ 新增一套业务系统(不用改代码)
  1. 1. 后台「系统产品」→ 新建,填名称、slug、简介、痛点、目标客户、功能、交付方式、SEO 标题描述。
  2. 2. 状态设为上线:前台系统矩阵、首页、站内搜索、sitemap、结构化数据会自动带上它。
  3. 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 沟通。

文档里没找到答案?

接口、部署与交付口径都可以直接问;常见问题里也有更细的回答。