项目:运营搭建平台(vue3-course) | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
本篇是仓库关键决策的"考古现场":把散落在 tiny-engine-borrow-plan.md、docs/plans/(16 份设计文档)、源码注释与 README 里的决策证据收拢为 12 条 ADR。每条按「背景 → 备选方案 → 决策 → 源码证据 → 后果」组织——背景讲当时面对什么问题,备选讲放弃过什么,决策讲选了什么,证据给出可复核的文件与行号,后果讲这条决策今天让接手者付出或得到了什么。与既有篇目的分工:01 篇讲"架构现在是什么形状",本篇讲"为什么是这个形状";机制细节(如密钥如何轮换、白名单如何校验)见《08-安全与权限设计》《05-核心流程》,本篇只记录决策本身。ADR 编号按决策发生的大致时序排列,标注状态:已采纳(现行行为)、已采纳·有边界(采纳但明确了适用边界)、已决策·暂不执行(分析已付、实施挂起)。
tiny-engine-borrow-plan.md「架构决策点」节原文记录)。packages/work-front/src/pages/manage/modules/page-editor.tsx,物料是独立 npm 包,经构建发布到 mock CDN,编辑器内 requirejs 以 AMD 运行时加载直渲染)与看板线(dashboard-editor,物料是本地 @my/charts 元表,编辑器内同应用直渲染)。借鉴 TinyEngine 时必须回答:两条线要不要收敛成一套编辑器?PropSchemaItem 的 widget 化 schema 与属性分组)与配置器注册表(packages/work-front/src/ui/prop-configurator/registry.ts),让"配置面板"成为两边可复用的同一模块;画布层是否统一到 iframe 留到协议统一后再评估。用 borrow-plan 的原话说,这是把「协议层统一」与「渲染层隔离」拆成两个独立话题。tiny-engine-borrow-plan.md 架构决策点节(结论原文);packages/shared/src/schema/index.ts 的 PropSchemaItem/WidgetConfig/getPropGroups()(协议落点,注释多处标注"对齐官方 xxx");packages/work-front/src/ui/prop-configurator/registry.ts(注册表);packages/work-front/src/ui/dynamic-form.vue(页面线管理端表单复用同一注册表模式)。props.schema.json(scripts/build-materials.ts 用 typescript-json-schema 生成)与看板线的 propSchema 是同一协议思想在两种物料形态上的投影。负面——两条渲染链路(AMD 远程物料 vs 本地元表)长期并存,文档、测试、心智都要双份;13 篇记录的"图表物料面板移除、运行时保留"双口径就是这种并存成本的实例。packages/build/vite-config(buildFrontLibEntry + copyVueRuntimeLib),scripts/build-work-front.ts 与 build-portal-front.ts 消费同一份配置。tiny-engine-borrow-plan.md 〇节与任务 7(验收记录:"已用 shasum 逐字节比对通过,work-front + portal-front 共 19 个产物文件");packages/build/vite-config/index.ts(package.json 注明 main 指向 ./index.ts,构建脚本以相对路径引用——borrow-plan 注释解释了原因:pnpm workspace 显式列举 + 当时环境 store 不匹配,待注册 workspace 后可改包名引用);根 package.json 的 devDependencies 中 vite ^7.3.6、@vitejs/plugin-vue、@vitejs/plugin-vue-jsx;vitest.config.js 复用同一 vite 插件链。PluginVue()/PluginJsx() 一份);页面线发布能在服务端进程内直接调 vite.build(packages/work-server/src/service/page-static.ts),这在 Webpack 时代要难得多的多。负面/遗留——packages/build/vite-config 以相对路径引用是显式妥协(borrow-plan 注释自认);vite 7 的 lib 模式必须显式声明 cssFileName(06 篇构建链提到的坑);SSR 配置形状不同仍留在原脚本,构成 13 篇记录的"SSR 半成品"债务的一部分。docs/plans/dashboard-publish-2026-09.md 设计决策 1)。dashboard_info_test / dashboard_info_pre / dashboard_info_prod 三张同构表;推进顺序强制 test → pre → prod(pre 需 test 存在,prod 需 pre 存在);prod 允许重复发布(事务内行锁自增版本号 + 自动打 source='publish' 快照);portal 只读 prod 表,不碰草稿主表。与页面线的三点差异(prod 不锁版本、不回写主表、版本服务端自增)都写在 plan 文档里。docs/plans/dashboard-publish-2026-09.md 设计决策 1-3;packages/work-server/src/service/dashboard-stage.ts 72-205 行 pushDashboardStage(test 注入白名单、pre 复制 test、prod 事务复制 pre);packages/work-server/src/model/dashboard-stage.ts(nextProdVersion 行锁自增);packages/init-database/src/sql/dashboard_info_test.sql(三表 DDL)。unpublishStage 置 status=0)即门户 404,无需缓存清理;版本号只在 prod 递增,test/pre 只是"晋级中的副本",语义干净。负面——三张表 × 两条线 = 六张发布表,DDL 与软删级联都要覆盖(packages/work-server/src/service/dashboard.ts 删除事务级联置零三阶段表);"跳阶段发布被拒"对只想试发布的用户多一步操作(12 篇 FAQ 高频条目之一)。JWT_SECRET(HS256 登录 cookie,生产缺失拒绝启动)、SECRET_BOX_KEY(AES-256-GCM 主密钥,库中有密文而密钥缺失则启动即退)、INTERNAL_TOKEN(portal→work-server 通道共享密钥,未配置即取数通道整体禁用,fail-closed)、PASSWORD_SALT(旧 HMAC 口令哈希迁移期保留,🔴 已废弃)。三道启动自检集中在 packages/work-server/src/index.ts,报错文案自带处理动作。packages/work-server/src/config.ts 5/6/11 行(三个配置读取点,11 行注释明写"fail-closed");packages/work-server/src/util/secret-box.ts 30-46 行 masterKey()(延迟读取——注释解释是为测试注入保留的余地);packages/work-server/src/index.ts 38-47 行(JWT_SECRET 自检)与 101-118 行(SECRET_BOX_KEY 异步预检);packages/portal-server/src/util/access-gate.ts 25 行注释("不 fail-open");.env.example 18-20 行(PASSWORD_SALT 废弃标注)。scripts/migrate-secrets.ts);"密钥错误在启动期爆炸而非第一个请求爆炸"把最难排查的 GCM 认证失败挡在生产流量之前。负面——SECRET_BOX_KEY 托管完全依赖运维纪律(无 KMS,13 篇第 1 节 #2 入账);四把密钥 + 一堆限流/开关变量让 .env 成为事实上的配置契约(14 篇全清单就是为了治这个)。docs/plans/dashboard-publish-2026-09.md 设计决策 4"安全设计核心")。x-internal-token 转发 work-server 的 /api/post/dashboard/portal-data → work-server 不信 portal 的判断再校验一次白名单 → queryPublishedDataSource 以 queryParams 固定空对象执行。解密(openConfigSecrets)只发生在 executeDataSourceQuery 一处——它是全仓唯一取数出口,编辑器路径与访客路径在此汇聚。docs/plans/dashboard-publish-2026-09.md 决策 4(含攻击面分析:"公网只能触发已发布看板白名单内、配置已固化的数据源查询";queryParams 写死 {} "杜绝参数注入面");packages/work-server/src/service/data-query.ts 27-72 行(唯一出口注释原文:"解密放在这里可以保证没有任何一条下游拿不到明文,也不会把明文带出本函数");packages/work-server/src/service/dashboard-stage.ts 282-310 行 queryPortalDashboardData(双层校验终点 + queryParams 固定空);packages/portal-server/src/service/dashboard.ts(转发与 15 秒超时)。Record<方法名, 函数体字符串>),运行时要在浏览器里编译执行,方法体内可 await。难点:构建器会把源码里的 async function 字面量按构建 target 降级转译(es2015 下取 Object.getPrototypeOf(async function(){}).constructor 拿到的是普通 Function),方法体里的 await 直接报错。Object.getPrototypeOf(async function(){}).constructor——被转译击穿,否决;② 限定构建 target ≥ es2017 保留 async 字面量——全局约束太脆,任何一个共享包降级就坏;③ 经运行时 new Function 求值拿到原生 AsyncFunction——采纳;④ 沙箱(iframe/Worker/独立 origin)——长期方向,当期未做。const AsyncFunction = new Function('return (async function(){}).constructor')(),编译签名固定 (state, methods)(对齐 TinyEngine 官方 script 插件的约定),单方法编译失败不阻断其余方法、错误归集 errors 数组。packages/shared/src/runtime.ts 35-39 行(注释原文:"打包器会把 async function 字面量按 target 降级转译(es2015 下取 prototype 会拿到 Function,导致方法体 await 报错)")与 45-63 行 compilePageMethods;packages/work-front/src/pages/dashboard-editor/services/ 的语法校验(js-syntax.test.ts 保证保存时与运行时同一语义)。packages/work-server/src/util/page-methods-guard.ts 的公开闸门(prod 发布 + 切 public 两处拦截)就是这条决策的边界条款(08 篇第 6 节);CSP 刻意不下发 script-src 也是同一让步(packages/server-kit/src/security-headers.ts 注释);13 篇路线图把"独立 origin 沙箱"列为第二阶段最长周期项,落地后本 ADR 的边界条款可整体放松。ComponentMeta(type/label/category/defaultSize/defaultProps/propSchema/propGroups/container/dataBinding/events)为物料元表;53 个 Element Plus 组件在 elementPlusMetas.ts 按 meta 构造器批量声明;属性面板按 propSchema 每项的 widget: { component, props } 查注册表渲染,未声明 widget 按 type 内置兜底;容器语义 container.acceptChildren 对齐官方 configure.isContainer。packages/charts/src/meta/elementPlusMetas.ts 头注释(2026-09 审查后的两条硬约束:type 必须对应真实存在的 el-* 组件;propSchema 每项 name 必须与 defaultProps 键一致——"缺 name 会导致所有属性写入 model[''] 空键(已修)");tiny-engine-study/designer-demo/public/mock/bundle.json(协议参考源);packages/work-front/src/ui/prop-configurator/registry.ts + builtin.ts(5 个内置配置器);packages/work-front/src/ui/prop-configurator 的注册表测试。/page/dashboard-canvas + postMessage 协议)——采纳。canvas:* 消息协议双向通信;位置字段以画布为唯一上传方,编辑器下发的 schema 签名不含位置字段,杜绝"下发→渲染→上报→再下发"的双向同步回环。borrow-plan 记录的取舍理由:"开发/生产行为一致、无需动 Koa 模板与多入口构建、改动面小;代价是画布与编辑器共享同一 Vue 版本。"tiny-engine-borrow-plan.md 任务 3(实现方式与理由原文);packages/work-front/src/pages/dashboard-canvas/protocol.ts 头注释("位置字段以画布为唯一上传方,编辑器下发的 schema 签名不含位置,避免双向同步回环");packages/work-front/src/pages/dashboard-editor/services/canvas-service.ts(iframe 挂载与协议接线);borrow-plan 任务 9 对官方 canvas 构建黑科技的"不实施"结论。canvas:rects 高频上报需依赖协议的批量 items[] 设计(rAF 节流是 10 篇预留的优化)。docs/plans/merge-servers-2026-09.md 开头原文:"已决策——暂不执行")。docs/plans/merge-servers-2026-09.md 全文(含"已否决的方案"节与目标架构目录树);packages/server-kit/src/security-headers.ts(frame 策略二选一的实现,两服务各传不同参数);scripts/build-work-server.ts / build-portal-server.ts(桶隔离的拷贝执行点)。docs/plans/page-line-freeze-2026-09.md)。page.vue + page-editor.tsx + @my/business 拖拽体系 + 两个 materials 物料)何去何从需要显式决策——此前 .workbuddy 有过 D-4"长期并存"的注记,本次覆盖。/p/:uuid)与物料运行时必须永久兼容;② 长期并存、两线同步演进——否决:投入产出不成立;③ 归档冻结——采纳。work-front/src/ssr/、pages/preview-amd/、pages/home/app.vue、page-flow.vue、materials 两物料)——清理时勿动。docs/plans/page-line-freeze-2026-09.md 全文(含"本批已清理的教学残留"清单:零引用 bootstrap、portal 侧死键、未接线的 controller 等);packages/charts/src/index.ts 23-26 行注释(同月的配套决策:"面板移除图表物料、运行时保留兼容"是同一收敛期的姊妹决策);packages/business/src/(冻结包,仍被页面线消费)。@my/business 的 TODO(structuredClone)等小修也随冻结降级为低优先级。layout[] + components[] 的网格模型(grid-layout-plus 的 24 列栅格)。借鉴时是否换范式?tiny-engine-borrow-plan.md 任务 8(结论原文);packages/work-front/src/pages/dashboard-canvas/app.vue(grid-layout-plus 消费点,grep 证据);packages/dashboard-codegen/index.ts(出码用 CSS Grid 复刻"24 列 / 行高 30px / 间距 10px"网格语义——模型一致性延伸到了出码);packages/shared/src/schema/index.ts 的 ComponentInstance(x/y/w/h 栅格字段注释:"绝对布局模式下的坐标;文档流模式不使用"——后续 layoutMode 双模式是网格模型的内聚扩展而非范式切换)。applyFlowMigration 等迁移函数存在说明模型经历过一次 flow/absolute 双模式扩展,新场景若再偏离网格需要重新评估本 ADR 而不是继续打补丁。packages/dashboard-codegen/index.ts 文件头注释):图表渲染层整体 vendor @my/charts 源码(与画布同源、视觉天然一致);数据源只带出码当下的快照(toSafeDataSources 字段白名单,连接配置一律不落导出包);生成工程的 package.json 依赖钉死为工作区实际安装版本(controller/codegen.ts 传入 versions 表)。tiny-engine-borrow-plan.md 任务 6(含"已真机验证:样例 schema 生成 20 个文件,独立 pnpm install + vite build 成功"的验收记录);packages/dashboard-codegen/index.ts 77-97 行(本地样式白名单副本及其"改动两处需同步"的自认——13 篇第 3 节已入账为债务);packages/work-server/src/controller/codegen.ts(jszip 打包与 versions 传参)。CHART_IMPORTS,漏了出码即坏);样式白名单双份在依赖 shared 之前是持续利息(13 篇第一阶段止血项)。把 12 条 ADR 连起来看,能读出这个仓库决策风格的三个特征。其一,安全决策先于功能决策成型(ADR-003/004/005 都在 2026-09 的发布闭环设计里同批敲定,白名单、双层校验、唯一解密出口是同一份 plan 文档的四个小节),安全不是补丁而是发布语义的一部分。其二,借鉴有账本、不抄有理由:ADR-001/007/008/011 四条都源于同一份 borrow-plan,每条都同时记录"借了什么"与"刻意不抄什么"(渲染方式、独立构建入口、组件树模型),这是它能保持"课程仓外壳、产品内核"而不失控的原因。其三,否决方案留档:ADR-009 的"已否决的方案"节、ADR-010 覆盖 D-4 注记、borrow-plan 的进度基线表,都在为"将来有人再提同样的问题"省一次重新分析——这也是本篇存在的理由:接手者想推翻任何一条决策前,先在这里找到它当初为什么被采纳。
逐条与既有篇目的映射:ADR-001/007/008/011 的机制细节见《01-系统架构》《07-模块详解》;ADR-003/005 的执行链路见《05-核心流程》;ADR-004/006 的安全边界见《08-安全与权限设计》;ADR-002 的构建链路见《06-部署运行》与《14-配置与环境详解》;ADR-010/012 与债务的关联见《13-技术债务与演进建议》。本篇与它们互为索引:读机制去篇目,读动机回本篇。