
一个开源项目把 Typora 类的排版体验带到手机上块级 WYSIWYG、LaTeX 公式、Mermaid 图表以及面向 Agent 的诊断与 CLI 工具链。移动端 Markdown 编辑器并不少。真正困难的是如何让 Markdown 在触屏设备上变成真正可编辑的 Document而不是“源码 预览”的拼接。Tafcm 就是在解决这个问题。本文不重点介绍功能而是从工程实现出发讨论它的架构、Markdown AST、公式渲染、PDF 导出以及一次真实真机卡死问题是如何被定位和修复的。一、为什么做移动端 WYSIWYG传统 Markdown 编辑器通常是Markdown 源码 ↓ 编辑 ↓ Preview这套模型在桌面端没有太大问题但在手机上会明显放大交互成本。尤其是公式、代码、表格、Mermaid 等复杂内容出现以后用户需要不断在“写”和“看”之间切换。Tafcm 采用的是另一种模型Markdown ↓ Parser ↓ Document AST ↓ WYSIWYG Editor ↓ Render / Export也就是说用户操作的是 Document而不是 Markdown 源码。Markdown 仍然作为持久化格式但编辑过程中用户面对的是结构化文档。这也是 Tafcm 的核心产品假设.md是单一真相源Document 是用户的编辑对象。二、整体技术栈层选型UIFlutter 3.44 / Material 3状态管理RiverpodMarkdown 解析手写 Parser AST公式渲染MathJax → SVGPNG 回退图表Mermaid / WebView → SVGPDFdart_pdfCLIPython / Click测试Flutter Test pytest E2E为什么没有直接使用一个现成 Markdown Editor因为 Tafcm 的需求并不是“把 Markdown 显示出来。”而是让 Markdown 成为一个可以被编辑、渲染、导出、诊断和验证的结构化文档系统。这决定了 Parser、AST、Transaction、Renderer 和 Export 都必须掌握在自己的架构里。三、六层架构当前 Flutter 工程采用六层结构presentation/ UI、Screen、Theme providers/ Riverpod Provider domain/ 业务领域、导出服务 data/ Document、Template 等数据模型 core/ Parser、Renderer、Service main.dart App Entry依赖方向严格保持presentation ↓ providers ↓ domain ↓ data ↓ core核心原则是core 不允许反向依赖上层。同时通过架构测试阻止循环依赖。例如test/architecture/layer_dependency_test.dart不仅测试业务功能还直接测试代码结构本身。这类约束的价值在于架构规则不应该只存在于文档里而应该成为 CI 可以执行的约束。四、核心设计AST 驱动的 MarkdownTafcm 没有把 Markdown 当成一串字符串直接渲染。而是Markdown ↓ Parser ↓ Document AST ↓ Renderer核心数据结构使用 Dart 3 sealed classsealedclassDocumentElement{constDocumentElement();}finalclassHeadingElementextendsDocumentElement{finalint level;finalStringtext;constHeadingElement(this.level,this.text);}finalclassFormulaElementextendsInlineElement{finalStringlatex;constFormulaElement(this.latex);}渲染时可以直接利用模式匹配Widgetrender(InlineElemente)switch(e){TextElement()Text(e.text),FormulaElement()FormulaView(e.latex),BoldElement()Bold(child:render(e.children)),};这种设计最大的价值不是代码更“现代”。而是Parser 与 Renderer 完全解耦。同一个 AST 可以被多个下游消费者使用┌─ WYSIWYG Renderer Markdown → AST ├─ Export ├─ Analysis └─ Validation同时可以进行 AST 等价性测试和 Markdown round-trip 测试。例如parse(md) ↓ AST ↓ serialize(AST) ↓ Markdown要求AST(Markdown) AST(Markdown)这样 Markdown 文件就可以真正作为长期稳定的单一真相源。五、公式渲染为什么选择 SVG公式是移动端 WYSIWYG 最容易踩坑的部分之一。Tafcm 的编辑器内公式采用LaTeX ↓ MathJax ↓ SVG ↓ Flutter为什么优先 SVG因为论文、数学笔记等场景会频繁缩放。位图公式放大后容易出现清晰度问题而 SVG 属于矢量内容更适合排版和导出。同时导出环境和编辑器环境并不完全相同。因此导出链路没有假设“只要编辑器里渲染成功PDF 就一定成功。”而是建立独立的 Formula Plan缓存 SVG ↓失败 缓存 PNG ↓失败 离屏渲染 ↓失败 文本 fallback核心原则是单个公式失败不应该让整份文档导出失败。六、一次真实 BugPDF 导出为什么卡在 28%这是这个项目目前比较值得记录的一次工程案例。在真实 Android 设备上测试 PDF 导出时遇到过这样的场景Document 30 blocks 12 SVG formulas ↓ PDF addPage ↓ 28% ↓ 永久阻塞最初非常容易想到“是不是公式渲染太慢”但进一步对照测试发现同样密度的文档在纯 Dart 环境下可以在约 30 秒内完成。只有真实设备环境会出现永久阻塞。因此问题逐渐从“PDF 导出很慢”收敛为真实设备环境中的特定 SVG 密度触发了同步布局路径的 blocking primitive。这两个问题的工程处理方式完全不同。七、为什么 watchdog 不能真正解决问题第一反应通常是启动 watchdog ↓ 3 秒没有返回 ↓ 判定超时 ↓ 恢复但这里有一个关键问题如果addPage本身正在同步执行应用层 watchdog 并不能抢占正在执行的同步布局调用。因此Timeout ≠ Interruptwatchdog 可以发现问题却不能直接终止问题。这是这次 Bug 最有价值的工程结论之一。八、最终方案有限性保证因此 Tafcm 最终没有把解决方案建立在“卡住之后如何恢复”而是转向“如何保证危险路径不会无限进入。”最终形成三层机制。1. 预防性降级分片渲染时统计公式数量。当一个片段中的 SVG 公式达到阈值例如formula_count 8则对该片段中的公式进行文本 fallback。也就是进入危险布局路径之前 ↓ 检测复杂度 ↓ 主动降级这是主要机制。2. WatchdogaddPage执行期间仍然启动 watchdog。它的作用不是“杀死同步任务”而是记录 blocking 状态并形成诊断证据。3. 导出终态保证最终导出必须进入SUCCESS FAILED FALLBACK中的某一个终态。不能出现28% ↓ 无限等待 ↓ 用户不知道发生了什么最终优化结果原始危险场景 60s ↓ 有限性策略 270ms这个案例最终形成了一个比较明确的工程原则对于不可抢占的同步路径系统的有限性必须通过进入路径之前的约束来保证而不是依赖进入之后的 timeout recovery。这比“加一个 timeout”更接近真正的可靠性设计。九、CI 与可观测让工程约束自动执行Tafcm 的工程验证并不只有单元测试。目前 CI 门禁包括flutter analyze --fatal-warnings fluttertestflutter build apk flutter build web同时还有架构级测试例如Layer Dependency File Size Provider Uniqueness Presentation I/O Boundary其中一个典型规则是presentation 层禁止直接进行文件 I/O。原因很简单如果 UI 可以随意访问文件系统那么未来的状态管理、测试和多平台适配都会逐渐失控。所以这些规则直接进入自动化测试而不是只写进 README。十、ADR为什么记录“为什么这么设计”项目目前维护了 29 篇架构决策记录编号覆盖 ADR-0001 ADR-0032。ADR 中不仅记录现在是什么还记录为什么这么做 替代方案是什么 为什么没有选择替代方案 这个决策的代价是什么例如这次 PDF 有限性问题就形成了独立的架构决策。这样做的价值在于代码只能告诉后来者“系统现在这样运行。”而 ADR 可以告诉后来者“为什么系统必须这样运行。”对于 Agent 协作开发这一点尤其重要。十一、ADI让 Agent 参与诊断Tafcm 还有一个相对特殊的设计Agent Diagnostic InterfaceADI。它不是简单增加一个 AI Chat而是把运行时异常转化成 Agent 可以消费的诊断链。例如adi latest-error--jsonadi trace showidadi replayidadi validate --after-fix于是问题处理过程可以变成Observation ↓ Discovery ↓ Trace ↓ Replay ↓ Fix ↓ Validate这和传统的用户描述 Bug ↓ Agent 猜 ↓ 改代码 ↓ 用户测试有本质区别。Tafcm 的设计目标是让 Agent 尽量基于真实 Evidence 工作而不是基于自然语言猜测。十二、CLI-native让工程能力离开 GUITafcm 同时提供ffx-cli。例如ffx project status ffx analyzefilepathffx adi latest-error ffx adi replayidffx adi validate --after-fix ffx contract-syncCLI 的一个重要设计是Human-readable Machine-readable JSON例如ffx adi latest-error--json因此它既能服务开发者也能进入CI Agent Automation Regression Pipeline这也是 Tafcm 与传统 Markdown App 一个比较明显的差异GUI 面向人CLI / ADI 面向自动化系统与 Agent。十三、最终架构把上面的设计串起来可以得到Markdown ↓ Parser ↓ Document AST ↓ ┌────────────┴────────────┐ ↓ ↓ WYSIWYG Export ↓ ↓ Runtime State PDF / Word ↓ Observation ↓ ADI ↓ FFX CLI ↓ CI / Agent / Regression而底层运行时仍然遵循core ↓ data ↓ domain ↓ providers ↓ presentation最终形成的是一个“双轨系统”对人Document-first。对机器Evidence-first。十四、目前的状态Tafcm 当前已经完成核心编辑、解析、渲染、导出和验证基础设施。当前版本v0.1.1发布时间2026-08-31当前开发阶段Phase 3.12 · 信息架构重构项目开源协议MIT LicenseGitHubhttps://github.com/Thy985/TafcmReleasehttps://github.com/Thy985/Tafcm/releases/tag/v0.1.1十五、一些工程上的结论这个项目目前得到的几个比较明确的结论是1. WYSIWYG 的难点不是“把 Markdown 渲染出来”。真正困难的是Editing AST State Rendering Export五者之间如何保持一致。2. 同步阻塞路径不能只靠 timeout 保证可靠性。如果任务不可抢占必须在进入危险路径之前限制复杂度。3. Markdown-first 和结构化编辑并不矛盾。Markdown 可以继续作为持久化真相源而 AST 负责承担编辑和渲染过程。4. Agent-native 不应该等于“加一个 AI 聊天框”。更重要的是给 AgentObservation Trace Replay Evidence Validation这些真正可以参与工程闭环的接口。5. 架构规则最好由测试执行而不是由文档提醒。能自动检查的规则就不要只写在 README 里。结语移动端 WYSIWYG Markdown 是一个很容易低估的领域。桌面上的一套编辑模型直接搬到触屏设备上通常很快会遇到触控、光标、块操作、公式渲染、WebView、真机性能、导出一致性……Tafcm 目前还远没有到“最终完成”的阶段。但这个项目已经验证了一件事移动端 Markdown 编辑器可以不只是一个 Preview 工具而可以成为一个真正的 Document Runtime。在此基础上再让这个 Runtime 暴露 ADI 和 CLI让 Agent 能观察、诊断、复现和验证它。这也是 Tafcm 当前正在探索的方向Typeset · Agent-native · Formula-aware · CLI-native · Markdown-first希望诸君能感兴趣欢迎共建生态