ReactiveMD 功能与架构简化方案
状态:用户后续确认的收缩范围已完成本地实现与验收(2026-11-02);未推送、未部署。 本站核对基线:
9a9b99b7;当时使用reactive-md@0.7.0。 上游核对提交:dc276866cc53f33fab099a1f1eb439debe24fa62。 本方案保留统一 Markdown / ReactiveMD 的产品目标,调整快照、发布、源码升级和部署 的职责。此前实施记录保留在docs/plan/reactive-md-unification-plan.md,其中持久快照、 原文与快照同事务、全站编译通过后才完成迁移的要求,在本方案实施后退出。 本文不是上线记录;没有修改生产数据库、部署脚本或当前正文。2026-11 校订(R3-703):§5(保存、发布和错误处理)与 §11(验收矩阵)里的两项 未实现 —— ①「允许语法中间态」:剪贴板保存仍整篇编译、失败返回
reactive_invalid(clipboard-service.ts);②「worker 可终止」:全仓worker_threads零命中。 文首「收缩范围已完成」只指其余项。
本次执行范围(用户后续确认)
本次按“替换正文解释器”的边界做减法:阅读直接使用现有编译与内存缓存;保存只写 源码与必要元数据;SQL up 仅做结构;移除持久快照和回填,保留旧源码必要兼容与独立 升级命令。旧表暂留,不改已应用 SQL。
现有编辑器、Markdown 兼容、引用权限、首屏、交互和导出保留;保存 / 发布的现有语法 与格式校验保留为临时检查,不再持久化结果。本次不新增服务器 worker、不重做保存 语义、不重新开发编辑器。下文是完整调研提案,其更大范围的候选不等于本轮承诺。
1. 结论与产品定位
ReactiveMD 是 Markdown 的交互增强:普通作者正常写 Markdown,需要时直接加入控件、 表达式、图表和条件内容。博客与云剪贴板共用编辑器与渲染规则,不增加模式切换步骤。
推荐采用:持久保存源码与必要元数据,按需编译,浏览器运行交互,编译结果只做可丢弃缓存。
服务端仍承担正文授权、站内引用取数、静态首屏和搜索正文提取。保留这些能力不需要在 数据库里长期保存编译树,也不需要把所有文章重新编译作为部署成功的条件。
2. 上游调研与本站差异
核对上游文档和本地已安装包的声明文件,以下能力均有实际公开接口依据:
| 上游设计 | 对本站的含义 |
|---|---|
compileLocal 生成 LocalDocument,React 视图消费该结果 |
编译与显示可分离;同一次编译结果可直接经 RSC 传给客户端 |
文档明确说明 LocalDocument 不应作为持久化格式 |
源码是长期数据;编译树应随包版本和缓存生命周期失效 |
LocalDocumentView 与 createLocalRuntime |
可保留现有静态首屏与客户端交互,无须重建一套渲染器 |
LocalCompileScheduler、worker 和增量编译会话 |
编辑预览可复用现有实现,不必每次按键请求服务器 |
源码修订与 migrateLocalSource 是显式协议 |
删除数据库快照不能顺便忽略旧语法的真实语义变化 |
| 运行状态默认属于当前文档实例 | 滑块、输入值与计时器不需要数据库表或自动同步接口 |
| 宿主 schema、hostData、hostRenderer 是公共扩展点 | 站内引用沿用宿主组件,不另造一个 RMD 方言 |
| 编辑器与作者工具已有 CM6、目录、补全和模板能力 | 复用其语法事实与功能,避免维护第二份组件功能清单 |
上游还明确限定:表达式不执行任意 JavaScript,权限和业务数据由宿主负责;开启作者 CSS / JavaScript / 扩展 HTML 会改变整篇文档的解释路径,不能拿这些开关修补 Markdown 兼容。本站继续关闭这些能力,不增加面向作者的脚本执行入口。
现有实现的问题不是缺少缓存:compile.ts 已有有界内存 LRU,阅读页已有现编译路径。
冗余主要来自额外的持久快照契约、摘要与签名复核、引用再验证、快照回填、写入事务绑定,
以及又把这些工作接入 SQL 迁移。另有 snapshot.ts 的旧 HTML 快照契约,并不参与生产路径。
2.1 调研来源
以下链接固定在本次核对的上游提交,避免 main 之后变化影响判断:
上游 main 的描述不自动代表已发布包具备全部相同细节。实施按锁文件里的包验证,使用
现有 compileLocal + LocalDocumentView 路径,不依赖未确认的新属性。上游通用 SSR
水合保证有限,本站已有包装组件的 SSR / 无 JS / 实际交互仍须独立验收。
3. 必须保留的完整能力
| 范围 | 保留的用户体验 |
|---|---|
| 普通 Markdown | 标题、列表、任务列表、引用、代码、GFM 表格、脚注、公式、链接、图片、既有软换行及历史 HTML 的可读结果 |
| RMD | 当前版本支持的输入控件、派生计算、校验、条件、集合、图表、绘图、图示、布局、主题、导航及其合法别名 |
| 编辑 | CM6、撤销重做、选区、快捷键、IME、补全、源码诊断、编辑 / 并排 / 预览、草稿恢复 |
| 工具栏 | Insert、Components、上传图片、插入引用四个主入口,手机上可用;低频功能留在菜单或快捷键 |
| 阅读 | 博客和云剪贴板统一渲染,首屏有正文,无 JS 有初始值,有 JS 可交互;实例之间状态隔离 |
| 站内引用 | 图床、音频、剪贴板、投票、收藏夹;保留当前内联 / 卡片语义与权限边界 |
| 输出 | 原文复制 / 下载、静态与交互 HTML 导出、打印,保持授权范围与实际资源可用性 |
| 消费者 | API、草稿、摘要、公开搜索、旧引用消费者能准确辨别增强内容 |
不把评论、讨论、故事和站内文档一并重构。不重写已验收的工具栏,不用“简化”为由删去 云剪贴板、SSR、引用权限或导出。上游运行状态导入 / 导出只复用其现有能力,不新增云同步、 协作编辑、文档脚本平台、组件市场或多套编译器常驻系统。
4. 数据与渲染职责
4.1 长期数据
正文表保留 content、现有 format、必要的 syntax_version 和业务元数据。format
服务于现有 API 与消费者兼容,不再决定是否要保存快照,也不呈现为作者必须选择的模式。
源码版本与编译器包版本分别管理;不得把升级包版本等同于改写所有正文。
文章复制、编辑、下载原文始终使用作者源码;兼容改写只发生在解释用的临时副本上。
不持久保存 LocalDocument、hostData、HTML 首屏或读者运行时状态。
公开搜索索引仍可留在数据库:它是独立的检索派生数据,缺失时应降级检索,而不影响 正文是否存在、能否保存或能否部署。删除 AST 快照不等于删除搜索索引。
4.2 唯一内容管线
数据库源码 → 当前请求授权 → RMD 编译(可命中内存缓存)
→ 在编译后的文档节点中按权限展开引用
→ SSR 初始正文 → 浏览器实例接管交互
编辑中的源码 → 浏览器 worker 编译 → 展开引用 → 相同文档视图
复用 compile.ts、cache.ts 和现有文档视图。服务器和浏览器共用编译配置与引用展开逻辑。
RSC 传输本次编译的普通数据是正常请求处理,不是数据库快照。首屏沿用已验收的
staticFirst 路径;客户端不为接管交互再把同一份源码完整编译一次。
4.3 缓存与资源成本
先复用现有有界 LRU,不新增 Redis、磁盘缓存、常驻队列服务或必须运行的预热任务。 缓存键覆盖实际编译输入、精确编译版本、语法版本、兼容规则和选项;同输入并发编译合并。 缓存清空、进程重启、首次访问均应正确,只影响速度。
权限判定和引用准备发生在命中缓存之前;hostData 不进入跨请求共享的编译缓存。带成员 数据的页面不进入公共整页缓存,登出、换账号、撤权后必须重新授权。
删除数据库快照不消除 CPU 成本。原有测量显示结构复杂的长文可能达到秒级,不能直接 把所有冷编译同步放回 Next 主事件循环。推荐一个惰性启动的进程内 Node worker 执行器: 只接收纯编译输入、有界并发与排队、真实超时终止 worker,重建后可继续服务;不是一个 新后端服务,也不持久保存任务。先完成 Next 生产构建及部署打包可行性验证。
沿用当前源码、节点与引用展开预算,依据冷编译、事件循环延迟、内存和排队数据标定
执行超时,不凭“20 万字几毫秒”放开。Promise.race 无法中断同步解析,不能冒充超时
隔离。浏览器沿用已有 worker / 调度器,Worker 出错时有界回退并保留编辑内容。
5. 保存、发布和错误处理
核心改变是取消“正文必须与可信快照同事务写入”。校验、保存源码、生成阅读结果三者 分别有明确结果;编译结果被丢弃不影响已保存的正文。
| 动作 | 推荐行为 |
|---|---|
| 编辑与本地草稿 | 允许标签、表达式暂未完成;立即显示诊断,不阻止记录源码 |
| 云剪贴板手动 / 自动保存 | 保存合法权限与大小范围内的当前源码,允许语法中间态;显示已保存与渲染诊断,不伪报保存失败 |
| 博客显式发布 | 保留发布前增强语法检查;有阻断诊断时定位问题并保留草稿,检查结果不落快照表 |
| 普通 Markdown 保存 | 历史 HTML / 兼容告警不应被升级成 RMD 发布错误 |
| 历史正文阅读 | 不重新追究“当年是否通过当前发布规则”;尽量显示可恢复内容,失败只影响该篇 |
| 数据库迁移 | 不读取全站正文做编译验收,不因某篇文章错误判数据库迁移失败 |
剪贴板现在的保存即更新共享源码,本方案不额外引入“已发布副本 + 云端草稿副本”系统。 允许语法中间态意味着读者暂时可能看到诊断,这是源码保存产品的明确取舍;本地草稿仍 保留为断网 / 冲突恢复。实现必须同时调整服务写入闸门与编辑器提交前阻断,不能只改后端。
前端检查不替代服务器权限、资源大小和协议校验。博客发布的诊断检查也可在服务器 复用同一个编译器执行,完成后丢弃结果;无需保存编译树来证明检查发生过。
区分恢复能力:上游提供局部运行错误时只影响该组件;整篇语法编译失败没有可靠 AST 时,不能承诺自动救出所有普通段落。显示明确的文档不可用提示,并在正文权限允许时 提供转义原文。保留页面其它部分,不把失败源码送进旧 Markdown 解析器,也不额外开发 一套“抢救半篇 AST”的解析器。
6. Markdown 兼容与组件保护
这次 radio-group 事故说明:一个 HTML 标签清洗名单不能同时充当 RMD 语法定义。
- 先用上游语法分析事实 / 组件 schema 保护增强组件范围及合法结构子项,再处理历史
HTML;名字同为
option不意味着可以剥除。别名也要保护,目录模板覆盖不了全部别名。 - 当前阶段保留已验证的兼容层修复和长度守恒,逐条收敛适配,不一次性重写全部 HTML。 优先使用上游已支持的行为;确实需要站点补齐的规则集中存放。
- 仅对本站历史净化行为需要的内容做适配,代码、数学、属性、转义范围保持正确。 所有清洗都只处理临时编译副本,源码和编辑器选区不被永久改变。
- 上游分析在歧义输入上无法提供可靠范围时,保留原内容交给诊断;不能猜测剥掉组件 来制造“编译成功”。普通 Markdown 的字面量容错仍按已识别格式处理。
- 完整覆盖原生目录、合法别名、混合文档和历史同名 HTML。以 AST / 实际 DOM 和
最终行为判断,通过诊断数量或仅看
ok不能代替功能验收。
永久保留本次最小复现:相同的 radio-group 示例用于博客、云剪贴板、预览、阅读、
发布检查、旧版本适配、静态和交互导出;同时保留旧 HTML select/option 不误激活的反例。
7. 源码版本升级
上游 0.7.0 仅接受当前 local-draft-3;显式 draft-2 不能原样交给它。这是必须处理的
真实协议约束,不能通过删掉版本字段、直接替换注释或取消所有检查来隐藏。
推荐分离为两层:
- **阅读兼容:**对已知旧修订调用现有安全升级适配(复用
source-migration.ts的上游 检查、站点预算和动态约束保护),只在内存中生成当前编译输入。确认可安全升级的 文档正常阅读,数据库和下载原文保持不变。所有入口,包括预览、搜索和导出,复用它。 - **持久升级:**保留独立运维命令。默认只读报告;显式应用时逐篇事务、核对原文未变化, 只保存确认安全的变更,失败文章保留原文和位置报告。完成标记不能替代每篇源版本判断, 不因一篇失败回滚无关文章,也不再绑定快照或公开索引的成功。
存在新增标签激活、动态约束变化、声明与字段冲突或未知修订时,只阻止该文档的解释 / 升级;不自动同意语义变化。作者修复并显式保存后才更新持久版本。升级依赖前先报告 受影响文章;确有不兼容时先修正文或暂缓换包,不常驻一整套旧编译器。
内存兼容成功不代表源码已持久升级,状态与日志必须区分;它也不应自动更新文章时间、 触发通知、改变内容摘要或写入数据库。
8. 引用、状态、导出和检索
8.1 站内引用
继续用已有 site-embed 公共宿主扩展,引用选择器插入 [@...],不要求作者手写内部标签。
成员视图按当前查看者权限取数;访客视图保持既有外部规则,对剪贴板、投票、收藏夹
保留字面量,连正文和标题也不读取。图片、音频继续由各自资源接口授权。
保持 Markdown 剪贴板内联一层、Reactive 剪贴板卡片及现有数量 / 深度预算;引用无法 访问时局部占位,不让父文档失败。不要把引用内容或会话数据持久写进父文章编译结果。 预览中的投票等业务操作继续只读,阅读中的业务动作仍由原 API 授权。
8.2 浏览器状态与输出
每份文档独立 runtime,销毁时释放计时器和订阅;默认刷新回到作者初值。状态保留 / 导入导出只采用上游显式机制,不自动上传读者填写值。条件隐藏只是显示行为,不能保护 秘密数据;权限不足的数据从一开始就不能下发。
源码导出使用原文;静态 / 交互 HTML 导出按当前授权准备内容后临时编译。默认使用作者 初值,需要分享当前参数时由用户显式选择。无法访问的引用保留引用标记 / 占位并说明; 离线资源与允许内联的图片使用既有处理,不以“导出成功”掩盖资源缺失或权限越界。
8.3 摘要与公开搜索
提取公开可见的静态文字,不索引声明值、条件隐藏数据、读者状态或引用资源正文。
索引版本从独立编译配置 / 提取规则派生,去掉对 snapshot-store.ts 的依赖。
正文写入先完成,索引随后尽力更新;失败不回滚正文。保留既有可恢复状态和
search:rebuild 独立重建。版本升级后的索引不完整时明确降级为元数据查询,不用原始
正文 LIKE 兜底;可见性过滤、软删除撤索引及计数 / 分页一致性保持不变。
9. 部署与具体删改范围
npm run migrate -- up 恢复为手写 SQL 结构迁移,不再自动进入旧源码全量检查、快照
回填或搜索重建。status / verify 保持只读。保留已经改进的数据库路径解析、参数数组
子进程调用和明确失败提示,不为撤销数据编排退回有跨库风险的旧实现。
| 现有部分 | 处理 |
|---|---|
snapshot-store.ts、snapshot-revalidation.ts |
从生产读写路径移除,职责不搬到新文件继续维持 |
snapshot.ts 的 HTML 快照骨架 |
核对零业务调用后删除接口和仅守着它的测试 |
publish.ts |
拆出可复用的发布诊断,删除生成 / 写入持久快照的要求 |
read-render.ts |
收敛成授权后的按需编译与明确错误处理,删除快照状态分支 |
backfill.ts / reactive:backfill |
退出常规流程,先给废弃说明,再随快照代码退出;不再是发布前置 |
source-migration.ts / 旧语法命令 |
保留真实语义检查,改成独立兼容与逐篇显式升级 |
compile.ts / cache.ts / worker / 预览 |
复用有用部分,补有界服务器执行,不新增持久编译层 |
flags.ts / 渲染选择器 |
去掉已不用的 percent、shadow 和逐篇 override 分支;只保留必要事故开关 |
| 博客 / 剪贴板 service | 删除源码与快照同事务写入;按各自保存 / 发布语义处理诊断 |
| 搜索 | 保留独立索引及修复命令,与快照和 SQL up 解耦 |
首次上线先停止快照表的读写,保留表与数据作为兼容期历史,不在这次切换中 DROP 表。 已有 SQL 迁移文件和 checksum 不改写。物理清理另列维护事项,在备份与回滚窗口安排后 单独执行;不把“少几张表”作为作者能正常阅读的前置条件。
部署继续安装锁定依赖、生成 Prisma Client、执行 SQL up、构建并重启。与当前包兼容 性有关的源码预检和搜索索引修复是独立、可观察的操作,发布脚本不得暗中把它们重新 拼成“任一历史文档失败则部署失败”的强依赖。
10. 实施顺序与退出条件
P0:固定用户行为与技术可行性
- 固定普通 Markdown、完整 RMD、radio-group 别名、混合引用、复杂长文和旧修订夹具。
- 核对已安装包与上游主分支的 API 差异;验证 Node worker 在 Next 生产产物中的路径、 ESM 加载与终止重建,并测首次编译和事件循环延迟。
- 证明清空持久快照后,现有按需编译 +
staticFirst可完成真实 SSR / 水合 / 无 JS 阅读。 - 明确剪贴板中间态保存和博客发布校验的 UI / API 行为,先锁定契约再改 service。
P1:解除部署、发布与快照绑定
- 让 SQL up 仅做结构;删除两个 service 的持久快照写入。
- 阅读页使用按需编译和现有 LRU;完成服务器有界执行及故障恢复。
- 旧修订临时适配覆盖阅读 / 预览 / 导出 / 检索,保留原始源码不变。
- 出口:新旧文章均无需快照即可保存 / 阅读;单篇错误不阻塞 SQL up;真实首屏可读。
P2:收敛兼容、编辑和消费者
- 保护所有合法组件与别名,删除无业务作用的快照骨架和重复选择分支。
- 保留四入口工具栏,统一诊断和保存状态;处理剪贴板自动保存中间态。
- 摘要、索引、导出使用相同版本与兼容准备,不依赖持久快照。
- 出口:功能矩阵逐项通过,不为了减少文件数丢弃正确边界。
P3:文档与运维收口
- 更新
docs/architecture.md、docs/deploy.md、../../CLAUDE.md、../../README.md和作者指南。 - 废弃快照命令有明确退出说明;旧表保留兼容期,不篡改历史迁移。
- 做一次停服务 / 冷启动 / 无快照 / 独立索引失败的部署演练,不操作生产正文验证方案。
- 出口:正常部署没有 RMD 全量回填步骤;空缓存能运行,失败能定位到单篇。
各阶段独立提交并运行相关测试和类型检查;实现完成后一次全量回归、生产构建,再按 实际页面做桌面 / 手机验收。该方案不要求再次重写已完成的编辑器或重跑无关功能开发。
11. 验收矩阵
| 场景 | 必须满足 |
|---|---|
| 快照表为空或无当前行 | 博客 / 剪贴板可正常阅读;保存不写 AST;部署无回填要求 |
| 首次访问、服务重启、LRU 淘汰 | 结果正确,不要求手动 warm-up |
| 普通 Markdown 加一个滑块 | 作者无需切模式,预览与保存后正文软换行、排版一致 |
| 完整组件示例与 radio-group 别名 | 合法结构不被清洗,所有入口可编译且真实交互有效 |
| 旧 HTML select/option 与代码里的 RMD 示例 | 不误激活、不损坏子文本或源码范围 |
| draft-2 安全 / 不安全升级 | 安全者临时可读且数据库源码不变;不安全者仅该篇报告,不强改声明 |
| 一篇故意错误的历史 RMD | SQL up 成功,其它文章可读,错误文章仍可编辑恢复 |
| 剪贴板未闭合标签时自动保存 | 源码已保存、诊断可见;恢复编辑不丢内容,恢复合法后正常渲染 |
| 无 JavaScript与水合 | 初始 HTTP 响应含真实正文;启用 JS 后控件计算正确,无重复正文 |
| 换账号、撤权、私密 / 外部引用 | 不串缓存;访客不请求成员资源;占位不带私密标题与正文 |
| 导出与打印 | 静态件可读,交互件可运行;离线资源与权限限制说明准确 |
| 搜索失败、旧索引或索引缺失 | 正文写入成功;搜索按规则降级、过滤与分页一致、可独立修复 |
| 复杂长文、病态输入、并发冷编译 | 保留现有预算;worker 可终止恢复;同输入合并;其它请求不被秒级同步解析拖住 |
性能记录应区分冷 / 热编译、引用读取、RSC 传输、DOM 渲染和事件循环延迟。没有实际 测量前,不宣称任意 20 万字正文只需几毫秒,也不以新增持久快照来回避冷编译验证。
本次实现与验证记录(2026-11-02)
代码提交:9013ea7e。本轮按上文“本次执行范围”完成,未实施更大范围的候选设计。
- 阅读由当前源码按需编译,复用现有内存 LRU 与静态首屏 / 浏览器交互。
- 博客和云剪贴板保存仅写源码与必要元数据;发布语法 / 格式检查保留,结果不落库。
- 删除持久快照读写、HTML 快照骨架、回填脚本和自动数据阶段;旧表与历史 SQL 不变。
- SQL up 只处理结构;真实测试确认错误正文不阻塞,重复 up 不写正文、快照或索引。
- 已知旧修订可安全临时解释,数据库原文和版本不变;独立源码升级和检索修复可运行。
- 公开索引使用独立编译身份;运维脚本统一用已有 ESM loader,兼容 ESM-only RMD 包。
验证结果:
| 检查 | 结果 |
|---|---|
| 类型检查 | 通过 |
| 相关编译 / 引用 / 版本 / 搜索 / 文档回归 | 12 文件、123 项通过;收尾另有 6 文件、86 项通过 |
| 无快照访问、冷读、原文保持与旧版安全拒绝 | 4 项通过 |
| 真实 SQL 入口及 URL / 只读检查 | 8 项通过,1 项非本平台路径用例跳过 |
| 全量回归(一次) | 288 文件通过、1 文件失败;5,702 项通过、1 项失败、1 项跳过,240.75 秒 |
| 生产构建 | 隔离工作树通过,159 个静态页面生成成功 |
| 真实浏览器 | 16 个不同场景通过:冷启动无 JS 首屏 1,既有渲染 / 引用 / 预览 / 媒体 / 导出 13,旧 RMD 剪贴板桌面与窄屏 2 |
| 独立运维 CLI | 匿名 QA 库重建 2 篇公开索引成功;旧源码预检返回已完成、0 阻塞 |
全量唯一失败是未修改的 tests/unit/wand2.test.ts 断输入停步位置断言(预期 91、
得到 99.11)。未修改 HEAD 的独立树连续三次单文件均 7 项通过;当前项目单文件复跑
也 7 项通过。本轮未修改游戏实现,不把这次全量描述为全绿。
本机 Prisma SQL 引擎对系统 TEMP 数据库报 readonly,独立实验确认标准测试目录可写; 入口测试改用项目既有的独立测试库目录,并保留每轮唯一文件和路径 / 诱饵库检查。 浏览器测试用全新合成库,未读取生产数据;没有执行 SSH、推送或部署。
完整验证日志保存在本地 TEMP:raricy-rmd-simple-full-20261102.log、
raricy-rmd-simple-build-20261102.log、raricy-rmd-simple-e2e-cold3.log、
raricy-rmd-simple-e2e-main.log、raricy-rmd-simple-e2e-clipboard.log。
首轮新增浏览器用例因 API 包装字段断言写错失败,按实际 clip 对象校正后桌面与窄屏均
通过;既有 13 个场景不重复计数。所有失败及完整输出均保留,未截断测试日志。