Files
blockflow-workbench/Design_Spec.md
gamewhale 589ff15213 chore: 初始化 BlockFlow Workbench 仓库
建立前端与 Tauri 桌面端的首个版本提交,包含核心编辑器、项目文件读写、测试与构建配置。

补充 Git 忽略规则和换行规范,排除依赖、构建产物、本地运行日志与临时验证文件,方便在其他电脑继续开发。
2026-05-29 17:23:43 +08:00

52 KiB
Raw Blame History

BlockFlow Workbench 产品与开发规格文档

文档用途:本文档用于指导 Codex 或其他代码生成/辅助开发工具进行应用开发。
项目定位:个人高级生产力工具,本地桌面端,可视化文本结构编排与模板渲染工具。
当前版本目标v0.1 可用原型。
重要约束:不引入 AI 能力,不做云协作,不做企业低代码平台,不做传统无限画布节点图优先的产品。


0. 总览

BlockFlow Workbench 是一款面向个人高级生产力场景的桌面端文本结构编排工具。它允许用户通过接近 Markdown 的轻量 DSL 快速创建变量、条件、循环、容器、片段引用等结构化块,并以可视化块/容器嵌套的方式维护复杂文本模板。最终,模板可以在输入 JSON 数据后被纯函数式渲染为文本、Markdown、JSON、YAML、HTML 或自定义扩展名文本。

它不是普通 Markdown 编辑器,也不是 Notion也不是传统低代码平台。它的核心是

高级文本编辑器体验
+ 可视化块系统
+ 容器嵌套
+ 轻量模板 DSL
+ JSON 数据驱动
+ 自动表单辅助
+ 实时预览
+ 调试日志
+ 本地项目文件夹

一句话定义:

BlockFlow Workbench 是一款本地化、面向高级用户的结构化文本生成 IDE。


1. 产品目标

1.1 核心目标

本应用的目标是帮助用户构建、维护和复用复杂文本结构。用户可以将常用文本、变量、条件逻辑、循环结构、输出格式、文档骨架、Prompt 片段、代码片段、角色设定段落、邮件模板、报告模板等统一抽象为可组合的 BlockFlow 模板。

最终应实现以下体验:

  1. 用户可以像写 Markdown 一样快速输入文本。
  2. 输入 {变量名} 后,文本立即转换为可视化变量胶囊。
  3. 输入 @if condition 并回车后,自动转换为条件容器。
  4. 输入 @for item in items 并回车后,自动转换为循环容器。
  5. 输入 / 打开命令菜单,插入容器、变量、条件、循环、片段引用、注释、输出配置等块。
  6. 用户可以选中已有文本,将其包裹为容器、条件、循环,或保存为片段。
  7. 用户可以使用 JSON 数据集实时渲染模板输出。
  8. 用户可以查看缺失变量、条件命中、循环展开、片段引用等调试信息。
  9. 用户可以将常用结构保存为片段,并在其他模板中保持引用。
  10. 用户可以导出单个渲染结果文件或复制最终输出到剪贴板。

1.2 长期价值

长期来看BlockFlow Workbench 不只是一个模板编辑器,而是一个“个人文本组件库”和“文本生产工作台”。用户长期积累后,将拥有自己的:

  • 常用表达库
  • Prompt 组件库
  • 文档结构库
  • 角色/世界观设定片段库
  • 代码注释/配置/文档模板库
  • 邮件、报告、合同、客服话术模板库
  • 条件逻辑和循环文本结构库

它的核心价值不是一次性生成文本,而是将复杂文本结构资产化、组件化、可维护化。


2. 非目标 / 暂不实现内容

v0.1 必须保持边界清晰,避免过早膨胀。

2.1 明确不做

v0.1 不做:

  • AI 生成能力
  • AI Prompt 自动优化
  • 云端同步
  • 多人协作
  • 用户账号系统
  • 权限系统
  • 插件市场
  • 企业低代码平台式工作流
  • 任意 JavaScript 执行
  • 外部命令执行
  • 网络请求节点
  • 文件读写节点
  • 异步渲染副作用
  • 传统无限画布节点连线作为主交互
  • 内置完整 Git 客户端
  • 内置版本历史快照系统
  • 批量导出
  • 多文件生成
  • 目录生成
  • SQLite 主存储

2.2 可以后置的能力

以下能力可在 v0.2 或更晚版本考虑:

  • 完整插槽系统
  • 片段 props 的复杂 UI
  • 批量导出
  • 多文件输出
  • 目录生成
  • SQLite 缓存索引
  • 依赖图谱可视化
  • 完整表单生成器
  • 插件系统
  • 源码模式高级编辑
  • 更复杂表达式函数库
  • 模板市场/模板包管理

3. 用户定位

3.1 第一目标用户

第一目标用户是开发者本人,或者类似的高级个人生产力用户。

这类用户具备以下特征:

  • 能接受一定学习成本
  • 喜欢结构化、工程化、可维护的工具
  • 偏好本地文件、项目目录、Git 管理
  • 熟悉 JSON、Markdown、代码编辑器风格界面
  • 需要生成多种类型的文本代码、文档、Prompt、创作文本、邮件、报告、设定等
  • 不需要软件为普通大众降低到极致简单

3.2 产品气质

产品应当更像:

VS Code
+ Markdown 编辑器
+ 可视化结构块系统
+ 文本模板渲染器

不应像:

Notion 大留白文档
企业低代码拖拽平台
传统流程图工具
纯 Markdown 编辑器
普通剪贴板模板管理器

核心气质:

像写 Markdown 一样轻,像搭积木一样直观,像 IDE 一样可维护。


4. 核心设计原则

4.1 写作体验优先

主体验是高级文本编辑器,而不是传统节点图或低代码画布。用户应能连续输入、快速改写、键盘优先操作。

4.2 结构化能力隐藏在块系统中

软件底层具有接近低代码的结构表达能力,但界面不能显得笨重。结构应表现为可折叠、可嵌套、可拖拽的块和容器。

4.3 AST 是唯一真相

项目文件中保存的是自定义 BlockFlow AST而不是 HTML、ProseMirror Doc、TipTap JSON 或其他编辑器私有结构。

BlockFlow AST ←→ Editor Adapter ←→ TipTap / ProseMirror

渲染器、导出器、引用索引、变量收集、DSL 序列化都基于 BlockFlow AST。

4.4 纯函数式渲染

渲染模型必须是:

模板结构 + 输入数据 = 输出文本

同一模板与同一数据必须得到相同输出。渲染过程不允许副作用,不允许修改外部状态,不允许文件读写、网络请求、外部命令、任意脚本执行。

4.5 项目文件夹是项目本体

项目以真实文件夹保存。软件的资源树只是对文件夹的增强展示。所有核心内容应可被 Git 管理、可手动查看、可手动修复。

4.6 可视化是为了维护,不是为了牺牲效率

可视化块应低调、高信息密度。变量胶囊、条件容器、循环容器等应帮助用户维护结构,而不能破坏文本编辑效率。


5. 产品核心概念

5.1 Workspace / Project

项目文件夹是一个 BlockFlow Project。它包含模板、片段、数据集、Schema、导出文件等。

5.2 Template

模板是可渲染入口。一个模板由 BlockNode 树组成。

5.3 Fragment

片段是可复用组件。片段与模板本质相同,都是 BlockTree但片段通常被其他模板或片段引用。

5.4 Dataset

数据集是用于渲染模板的 JSON 数据。模板与数据集分离。

5.5 BlockNode

块级节点。用于表达段落、容器、条件、循环、片段引用、插槽、注释、输出配置等结构。

5.6 InlineNode

行内节点。用于表达一行内部的文本、变量、表达式、行内片段、占位符等。

5.7 Renderer

渲染器读取 TemplateDocument、FragmentDocuments 和 Dataset输出 RenderResult。

5.8 Reference Index

引用索引用于追踪模板与片段之间的关系,包括谁引用谁、缺失片段、循环引用等。


6. v0.1 功能范围

6.1 v0.1 必须实现

v0.1 应至少实现:

  • 创建/打开项目文件夹
  • 读取 project.json
  • 扫描 templates、fragments、datasets
  • JSON AST 存储
  • 三栏 UI
  • TipTap 基础编辑器
  • 普通文本段落
  • 变量胶囊
  • 条件容器
  • 循环容器
  • 普通容器
  • 片段引用
  • JSON 数据输入
  • 实时预览
  • 调试日志
  • 复制输出
  • 单文件导出
  • 自动保存
  • 外部修改检测
  • 引用追踪
  • 缺失变量提示
  • 缺失片段提示
  • 片段循环引用检测

6.2 v0.1 推荐实现但可降级

以下能力重要,但如果时间不足可在 v0.1 后半段实现:

  • 选区包裹为容器
  • 选区包裹为条件
  • 选区包裹为循环
  • 选区保存为片段
  • DSL 源码模式
  • 局部源码编辑
  • 表单模式数据输入
  • 输出文件名模板

6.3 v0.1 不必完整实现

  • 完整插槽系统
  • 完整片段 props 编辑 UI
  • 批量导出
  • 依赖图谱
  • SQLite 索引缓存
  • 内置历史快照
  • 插件系统

7. 项目文件夹结构

7.1 推荐结构

MyBlockFlowProject/
  project.json

  templates/
    main.json
    code-template.json
    prompt-template.json
    character-template.json

  fragments/
    common-header.json
    common-footer.json
    prompt-rules.json
    markdown-output-rules.json

  datasets/
    default.json
    vip-user.json
    empty-list.json
    debug-case.json

  schemas/
    common-inputs.json

  exports/
    latest-output.md
    latest-output.txt

7.2 v0.1 最小结构

MyBlockFlowProject/
  project.json
  templates/
  fragments/
  datasets/

7.3 project.json 示例

{
  "id": "project_main",
  "name": "My BlockFlow Project",
  "version": "0.1.0",
  "createdAt": "2026-05-24T00:00:00.000Z",
  "updatedAt": "2026-05-24T00:00:00.000Z",
  "entryTemplateId": "template_main"
}

7.4 文件原则

  1. 所有核心内容使用 JSON 保存。
  2. 一个模板一个文件。
  3. 一个片段一个文件。
  4. 一个数据集一个文件。
  5. 项目可被 Git 管理。
  6. 不依赖数据库也能打开项目。
  7. 不保存编辑器私有结构作为主数据。
  8. 可以从 JSON 完整恢复项目。
  9. 外部手动修改文件后,软件刷新应能识别。
  10. 软件内新建/重命名/删除资源时,应真实操作文件系统。

8. 技术栈

v0.1 技术栈锁定为:

桌面壳Tauri
语言TypeScript
前端React
编辑器TipTap / ProseMirror
状态管理Zustand
样式Tailwind CSS
主存储:项目文件夹 + JSON
缓存索引:内存索引,暂不引入 SQLite

8.1 Tauri

用于桌面壳、本地文件访问、窗口管理。相比 Electron 更轻量,适合个人本地工具。

8.2 React + TypeScript

用于复杂界面、检查器、资源树、编辑器外壳、状态管理。

8.3 TipTap / ProseMirror

用于编辑器视图层,处理光标、选区、输入法、快捷键、撤销重做、自定义行内节点、自定义块级节点等。

重要约束TipTap Doc 不是项目文件格式。必须通过 Adapter 与 BlockFlow AST 转换。

8.4 Zustand

管理应用状态,包括当前项目、打开文件、选中块、当前数据集、保存状态、右侧面板 Tab 等。

8.5 Tailwind CSS

用于快速构建高信息密度、低装饰、深色优先的 UI。


9. 推荐代码目录结构

src/
  app/
    App.tsx
    routes/
    store/
      projectStore.ts
      editorStore.ts
      renderStore.ts
      uiStore.ts
    layout/
      MainLayout.tsx
      TopBar.tsx
      StatusBar.tsx
      LeftResourcePanel.tsx
      RightInspectorPanel.tsx

  core/
    types/
      common.ts
      project.ts
      document.ts
      dataset.ts
      nodes.ts
      schema.ts
      output.ts
      render.ts
      reference.ts
    renderer/
      renderTemplate.ts
      renderBlock.ts
      renderInline.ts
      renderContext.ts
      missingVariable.ts
    expression/
      tokenizer.ts
      parser.ts
      evaluator.ts
      types.ts
    reference/
      buildReferenceIndex.ts
      detectReferenceCycles.ts
    dsl/
      parseDsl.ts
      serializeDsl.ts
    schema/
      collectVariables.ts
      inferInputSchema.ts

  editor/
    BlockFlowEditor.tsx
    adapter/
      astToTiptap.ts
      tiptapToAst.ts
    extensions/
      VariableInlineExtension.ts
      ConditionBlockExtension.ts
      LoopBlockExtension.ts
      ContainerBlockExtension.ts
      FragmentRefExtension.ts
    commands/
      insertVariable.ts
      wrapSelectionAsCondition.ts
      wrapSelectionAsLoop.ts
      saveSelectionAsFragment.ts
    inputRules/
      variableInputRule.ts
      conditionInputRule.ts
      loopInputRule.ts
    components/
      VariablePill.tsx
      ConditionBlockView.tsx
      LoopBlockView.tsx
      ContainerBlockView.tsx
      FragmentRefView.tsx

  project/
    createProject.ts
    openProject.ts
    scanProject.ts
    readDocument.ts
    writeDocument.ts
    watchProjectFiles.ts
    validateProject.ts
    fileNaming.ts

  ui/
    resource-tree/
    inspector/
    data-panel/
    preview-panel/
    debug-panel/
    reference-panel/
    command-palette/
    context-menu/

  utils/
    id.ts
    path.ts
    json.ts
    debounce.ts

10. TypeScript 核心类型定义

以下类型是 v0.1 的基础数据模型。Codex 开发时应以这些类型为主线,不要让 UI 私有类型污染核心模型。

10.1 common.ts

export type ID = string
export type FilePath = string
export type ISODateString = string

export type BlockId = ID
export type TemplateId = ID
export type FragmentId = ID
export type DataSetId = ID

10.2 project.ts

import type { FilePath, ISODateString, TemplateId } from "./common"

export type BlockFlowProject = {
  id: string
  name: string
  version: string

  createdAt: ISODateString
  updatedAt: ISODateString

  entryTemplateId?: TemplateId

  paths: {
    root: FilePath
    templatesDir: FilePath
    fragmentsDir: FilePath
    datasetsDir: FilePath
    exportsDir?: FilePath
  }
}

10.3 document.ts

import type { FilePath, ISODateString, TemplateId, FragmentId } from "./common"
import type { BlockNode } from "./nodes"
import type { InputSchema } from "./schema"
import type { OutputConfig } from "./output"

export type TemplateDocument = {
  kind: "template"

  id: TemplateId
  name: string
  description?: string

  filePath?: FilePath
  tags?: string[]

  createdAt?: ISODateString
  updatedAt?: ISODateString

  inputSchema?: InputSchema
  outputConfig?: OutputConfig

  children: BlockNode[]
}

export type FragmentDocument = {
  kind: "fragment"

  id: FragmentId
  name: string
  description?: string

  filePath?: FilePath
  tags?: string[]

  createdAt?: ISODateString
  updatedAt?: ISODateString

  inputSchema?: InputSchema

  children: BlockNode[]
}

10.4 dataset.ts

import type { DataSetId, FilePath, ISODateString } from "./common"

export type DataSetDocument = {
  kind: "dataset"

  id: DataSetId
  name: string
  description?: string

  filePath?: FilePath
  tags?: string[]

  createdAt?: ISODateString
  updatedAt?: ISODateString

  data: Record<string, unknown>
}

10.5 nodes.ts

import type { BlockId, FragmentId } from "./common"
import type { OutputConfig } from "./output"

export type BlockNode =
  | ParagraphBlock
  | ContainerBlock
  | ConditionBlock
  | LoopBlock
  | FragmentRefBlock
  | SlotBlock
  | CommentBlock
  | OutputBlock

export type InlineNode =
  | TextInline
  | VariableInline
  | ExpressionInline
  | InlineFragmentRef
  | PlaceholderInline

export type BaseBlock = {
  id: BlockId
  type: string

  name?: string
  enabled?: boolean

  ui?: BlockUiState
  rendering?: RenderingOptions
}

export type BlockUiState = {
  collapsed?: boolean
  selected?: boolean
  color?: string
  note?: string
}

export type RenderingOptions = {
  prefix?: string
  suffix?: string
  separator?: string

  trimStart?: boolean
  trimEnd?: boolean

  preserveEmptyLine?: boolean
}

export type ParagraphBlock = BaseBlock & {
  type: "paragraph"
  inlines: InlineNode[]
}

export type TextInline = {
  type: "text"
  content: string
}

export type VariableInline = {
  type: "variable"
  path: string

  displayName?: string
  fallback?: unknown
  format?: string

  required?: boolean
  missingStrategy?: MissingVariableStrategy
}

export type ExpressionInline = {
  type: "expression"
  expression: string
  fallback?: unknown
  format?: string
}

export type InlineFragmentRef = {
  type: "inlineFragment"
  fragmentId: FragmentId
  props?: Record<string, unknown>
}

export type PlaceholderInline = {
  type: "placeholder"
  name: string
  description?: string
}

export type MissingVariableStrategy =
  | "global"
  | "error"
  | "keep-placeholder"
  | "empty-string"
  | "fallback"

export type ContainerBlock = BaseBlock & {
  type: "container"
  name: string
  children: BlockNode[]
}

export type ConditionBlock = BaseBlock & {
  type: "condition"
  expression: string
  children: BlockNode[]
  elseChildren?: BlockNode[]
}

export type LoopBlock = BaseBlock & {
  type: "loop"
  source: string
  itemName: string
  indexName?: string
  children: BlockNode[]
  emptyChildren?: BlockNode[]
  separator?: string
}

export type FragmentRefBlock = BaseBlock & {
  type: "fragmentRef"
  fragmentId: FragmentId
  props?: Record<string, unknown>
  fills?: Record<string, BlockNode[]>
}

export type SlotBlock = BaseBlock & {
  type: "slot"
  slotName: string
  fallbackChildren?: BlockNode[]
}

export type CommentBlock = BaseBlock & {
  type: "comment"
  content: string
  multiline?: boolean
}

export type OutputBlock = BaseBlock & {
  type: "output"
  config: OutputConfig
}

10.6 output.ts

export type OutputFormat =
  | "text"
  | "markdown"
  | "json"
  | "yaml"
  | "html"
  | "custom"

export type OutputConfig = {
  format: OutputFormat

  customExtension?: string
  fileNameTemplate?: string

  missingVariableStrategy?: ExportMissingVariableStrategy

  trimFinalOutput?: boolean
  ensureTrailingNewline?: boolean
}

export type ExportMissingVariableStrategy =
  | "error"
  | "keep-placeholder"
  | "empty-string"

10.7 schema.ts

export type InputSchema = {
  fields: InputField[]
}

export type InputField = {
  path: string
  type: InputFieldType

  label?: string
  description?: string

  required?: boolean
  defaultValue?: unknown

  enumOptions?: string[]
}

export type InputFieldType =
  | "string"
  | "number"
  | "boolean"
  | "array"
  | "object"
  | "date"
  | "unknown"

10.8 render.ts

import type { TemplateId, FragmentId, DataSetId, BlockId } from "./common"
import type { TemplateDocument, FragmentDocument } from "./document"
import type { DataSetDocument } from "./dataset"
import type { OutputConfig } from "./output"

export type RenderInput = {
  template: TemplateDocument
  fragments: Record<FragmentId, FragmentDocument>
  dataset?: DataSetDocument
  data?: Record<string, unknown>
  options?: RenderOptions
}

export type RenderOptions = {
  outputConfig?: OutputConfig
  mode?: RenderMode
  targetBlockId?: BlockId
  maxFragmentDepth?: number
}

export type RenderMode = "preview" | "export"

export type RenderResult = {
  ok: boolean
  output: string
  logs: RenderLog[]
  warnings: RenderWarning[]
  errors: RenderError[]
  stats: RenderStats
}

export type RenderStats = {
  usedVariables: string[]
  missingVariables: string[]
  usedFragments: FragmentId[]
  missingFragments: FragmentId[]
  renderedBlockCount: number
}

export type RenderLog =
  | VariableRenderLog
  | ConditionRenderLog
  | LoopRenderLog
  | FragmentRenderLog
  | SlotRenderLog
  | OutputRenderLog

export type VariableRenderLog = {
  type: "variable"
  blockId?: BlockId
  path: string
  resolved: boolean
  value?: unknown
  fallbackUsed?: boolean
}

export type ConditionRenderLog = {
  type: "condition"
  blockId: BlockId
  expression: string
  result: boolean
}

export type LoopRenderLog = {
  type: "loop"
  blockId: BlockId
  source: string
  count: number
}

export type FragmentRenderLog = {
  type: "fragment"
  blockId: BlockId
  fragmentId: FragmentId
  resolved: boolean
}

export type SlotRenderLog = {
  type: "slot"
  blockId: BlockId
  slotName: string
  filled: boolean
  fallbackUsed?: boolean
}

export type OutputRenderLog = {
  type: "output"
  format: string
}

export type RenderWarning = {
  code: RenderWarningCode
  message: string
  blockId?: BlockId
  path?: string
  fragmentId?: FragmentId
}

export type RenderError = {
  code: RenderErrorCode
  message: string
  blockId?: BlockId
  path?: string
  fragmentId?: FragmentId
  cause?: unknown
}

export type RenderWarningCode =
  | "MISSING_VARIABLE"
  | "FALLBACK_USED"
  | "EMPTY_LOOP"
  | "UNFILLED_SLOT"
  | "UNKNOWN_TYPE"

export type RenderErrorCode =
  | "INVALID_EXPRESSION"
  | "MISSING_VARIABLE_EXPORT"
  | "MISSING_FRAGMENT"
  | "FRAGMENT_CYCLE"
  | "LOOP_SOURCE_NOT_ARRAY"
  | "INVALID_BLOCK"
  | "RENDER_DEPTH_EXCEEDED"

10.9 reference.ts

import type { TemplateId, FragmentId, FilePath } from "./common"

export type DocumentRefId = TemplateId | FragmentId

export type ReferenceIndex = {
  outgoing: Record<DocumentRefId, ReferenceItem[]>
  incoming: Record<DocumentRefId, ReferenceItem[]>
  missingFragments: MissingFragmentRef[]
  cycles: ReferenceCycle[]
}

export type ReferenceItem = {
  fromId: DocumentRefId
  fromPath?: FilePath
  toFragmentId: FragmentId
  blockId?: string
}

export type MissingFragmentRef = {
  fromId: DocumentRefId
  fromPath?: FilePath
  fragmentId: FragmentId
  blockId?: string
}

export type ReferenceCycle = {
  chain: DocumentRefId[]
}

10.10 editorState.ts

该类型不写入项目文件,只用于前端状态。

import type { BlockId, DataSetId, TemplateId, FragmentId, FilePath } from "@/core/types/common"

export type OpenDocumentId = TemplateId | FragmentId

export type EditorState = {
  currentProjectPath?: FilePath

  openDocumentId?: OpenDocumentId
  openDocumentKind?: "template" | "fragment"

  selectedBlockId?: BlockId
  currentDataSetId?: DataSetId

  rightPanelTab: RightPanelTab
  saveStatus: SaveStatus
  externalFileStatus?: ExternalFileStatus
}

export type RightPanelTab =
  | "properties"
  | "data"
  | "preview"
  | "debug"
  | "references"

export type SaveStatus =
  | "saved"
  | "dirty"
  | "saving"
  | "error"

export type ExternalFileStatus =
  | "clean"
  | "changed-externally"
  | "conflict"

11. AST 设计规则

11.1 BlockNode 与 InlineNode 分离

所有普通文字都在 ParagraphBlock 中。ParagraphBlock 内部由 InlineNode 组成。

示例:

你好,{user.name},你的订单是 {order.id}

保存为:

{
  "id": "block_p1",
  "type": "paragraph",
  "inlines": [
    { "type": "text", "content": "你好," },
    { "type": "variable", "path": "user.name" },
    { "type": "text", "content": ",你的订单是 " },
    { "type": "variable", "path": "order.id" }
  ]
}

变量必须是一等 InlineNode而不是藏在 TextInline 的字符串里。

11.2 块级结构

块级结构用于组织、控制、复用文本。

典型结构:

Template
  └─ BlockNode[]
        ├─ ParagraphBlock
        ├─ ContainerBlock
        │    └─ BlockNode[]
        ├─ ConditionBlock
        │    └─ BlockNode[]
        ├─ LoopBlock
        │    └─ BlockNode[]
        ├─ FragmentRefBlock
        ├─ SlotBlock
        ├─ CommentBlock
        └─ OutputBlock

11.3 enabled 字段

所有 BaseBlock 可拥有 enabled?: boolean。若 enabled === false,渲染器应跳过该块。

11.4 ui 字段

ui 可保存折叠状态、颜色、备注等 UI 信息,但不参与核心渲染逻辑。

11.5 rendering 字段

rendering 用于控制 prefix、suffix、separator、trim 等渲染细节。v0.1 可只实现最基础行为,但数据结构应预留。


12. DSL 设计

DSL 表层像 Markdown逻辑像轻量模板语言。

12.1 语法总表

{name}                变量
{user.name}           路径变量
{user.name ?? "未命名"} 带 fallback 的表达式/变量

# 容器名              普通容器

@if condition         条件开始
@else                 条件否则
@endif                条件结束

@for item in items    循环开始
@endfor               循环结束

@use fragmentName     引用片段

@slot name            定义插槽
@fill name            填充插槽
@endfill              结束填充

// comment            单行注释

@comment              多行注释开始
@endcomment           多行注释结束

@output markdown      输出配置

12.2 变量

{name}
{user.name}
{items[0].title}

可视化后显示为变量胶囊:

[user.name]

12.3 容器

# 角色基础信息
这里写角色姓名、年龄、身份等内容。

转换为 ContainerBlock。

12.4 条件

@if user.isVip
感谢你作为 VIP 用户长期支持我们。
@endif

支持 else

@if user.type == "developer"
输出技术版本。
@else
输出普通版本。
@endif

12.5 循环

@for item in items
- {item.title}: {item.price}
@endfor

转换为 LoopBlock。

12.6 片段引用

@use common-footer

转换为 FragmentRefBlock。

12.7 插槽

v0.1 可预留,不必完整实现。

@slot content

填充语法:

@use wrapper
  @fill content
    这里是填充内容。
  @endfill
@enduse

12.8 注释

// 这是单行注释

或:

@comment
这里是多行注释。
@endcomment

注释不参与输出。


13. 输入转换规则

13.1 转换策略

采用混合策略:

变量:实时转换
块级语法:回车转换
复杂结构:命令确认

13.2 变量实时转换

用户输入:

{user.name}

当输入 } 后立即转换为变量胶囊。

13.3 块级语法回车转换

用户输入:

@if user.isVip

按 Enter 后转换为条件容器。

用户输入:

@for item in items

按 Enter 后转换为循环容器。

13.4 复杂结构命令确认

输入 / 打开命令菜单:

/变量
/容器
/条件
/循环
/片段引用
/插槽
/注释
/输出

选择后生成对应结构。


14. 选区转块功能

选区转块是核心交互,不是附加功能。

14.1 v0.1 应支持

选区 → 包裹为容器
选区 → 包裹为条件
选区 → 包裹为循环
选区 → 保存为片段
选区 → 转为注释
选区 → 禁用输出

14.2 包裹为条件

原始内容:

这段内容只在 VIP 用户下显示。

执行“包裹为条件”后:

▾ if 条件表达式
  这段内容只在 VIP 用户下显示。

应自动聚焦条件表达式。

14.3 包裹为循环

原始内容:

- {item.title}: {item.price}

执行“包裹为循环”后:

▾ for item in list
  - [item.title]: [item.price]

应自动聚焦 list 或 source 字段。

14.4 保存为片段

选中内容后执行“保存为片段”,弹出:

片段名称
保存位置
是否替换原选区为片段引用

如果选择替换,原选区变为:

use fragment-name

15. 片段系统

15.1 默认保持引用关系

片段引用不是复制内容,而是保持链接。

@use common-footer

保存为:

{
  "type": "fragmentRef",
  "fragmentId": "common-footer"
}

修改片段本体会影响所有引用处。

15.2 支持展开片段

用户可右键片段引用,选择“展开为普通内容”。展开后:

  • 片段内容复制到当前模板
  • 原引用关系断开
  • 后续修改片段本体不影响已展开内容

15.3 片段状态显示

片段引用块应显示状态:

use common-footer      ✓
use old-footer         missing
use cycle-test         cycle

15.4 循环引用检测

必须检测:

A 引用 B
B 引用 C
C 引用 A

渲染器或引用索引应报错:

检测到循环片段引用A → B → C → A

15.5 片段 props

v0.1 模型预留 propsUI 可先简单实现或后置。

@use standard-title {
  title = "角色设定"
  description = "以下是角色基础信息。"
}

保存为:

{
  "type": "fragmentRef",
  "fragmentId": "standard-title",
  "props": {
    "title": "角色设定",
    "description": "以下是角色基础信息。"
  }
}

15.6 片段作用域规则

片段 props 是局部作用域,优先级高于外层数据。

变量查找顺序:

循环局部变量
↓
片段 props
↓
模板局部输入
↓
全局数据集
↓
默认值 / fallback

未传入的变量继续向外层查找。


16. 表达式系统

16.1 表达式原则

第一版表达式系统必须安全、轻量、无副作用。不得执行任意 JavaScript。

表达式只允许:

  • 读取数据
  • 比较判断
  • 简单逻辑计算
  • 空值合并
  • length 访问
  • contains 判断

表达式不允许:

  • 赋值
  • 函数定义
  • 任意 JS 执行
  • 网络请求
  • 文件读写
  • 外部命令
  • 异步调用
  • 修改状态

16.2 v0.1 支持表达式

user.name
user.level >= 3
user.type == "developer"
tags contains "important"
items.length > 0
!disabled
isVip && level >= 3
type == "code" || type == "doc"
user.name ?? "未命名"

16.3 支持能力表

路径访问      user.name
数组访问      items[0].title
比较          == != > >= < <=
逻辑          && || !
包含          contains
空值合并      ??
长度          items.length
字符串字面量  "developer"
数字          123
布尔          true / false
null          null

16.4 表达式模块建议

建议自研小型 parser/evaluator或使用安全表达式库但不得直接 eval()

模块:

expression/
  tokenizer.ts
  parser.ts
  evaluator.ts
  types.ts

17. 渲染模型

17.1 纯函数式渲染

渲染公式:

BlockTree + DataSet + Fragments + Options => RenderResult

渲染结果不仅包含 output还包含 logs、warnings、errors、stats。

17.2 渲染流程

1. 加载入口模板
2. 创建根渲染上下文
3. 从根 Block 开始递归渲染
4. ParagraphBlock 渲染所有 InlineNode
5. VariableInline 从作用域读取值
6. ConditionBlock 判断表达式
7. LoopBlock 遍历数组并创建局部作用域
8. FragmentRefBlock 加载片段并创建片段作用域
9. SlotBlock 接收 fill 或 fallback
10. CommentBlock 忽略
11. OutputBlock 影响输出配置
12. 合并文本
13. 返回输出、日志、警告、错误、统计

17.3 核心函数建议

export function renderTemplate(input: RenderInput): RenderResult

function renderBlocks(blocks: BlockNode[], context: RenderContext): RenderChunk

function renderBlock(block: BlockNode, context: RenderContext): RenderChunk

function renderInlines(inlines: InlineNode[], context: RenderContext): string

function renderInline(inline: InlineNode, context: RenderContext): string

17.4 RenderContext 建议

type RenderContext = {
  mode: "preview" | "export"
  scopes: ScopeFrame[]
  fragments: Record<string, FragmentDocument>
  options: RenderOptions
  logs: RenderLog[]
  warnings: RenderWarning[]
  errors: RenderError[]
  stats: RenderStats
  fragmentStack: string[]
}

type ScopeFrame = {
  type: "data" | "template" | "fragment" | "loop" | "slot"
  values: Record<string, unknown>
}

17.5 作用域解析

查找变量时,从内到外查找 scopes。

顺序:

最近的 loop scope
fragment props scope
template scope
data scope
fallback

17.6 Paragraph 渲染

ParagraphBlock 渲染所有 inlines 并默认追加换行。具体策略可在 rendering 中控制。

建议 v0.1 默认:

ParagraphBlock => renderInlines(...) + "\n"
ContainerBlock => 递归渲染 children不额外增加换行
ConditionBlock => 命中时渲染 children不命中且无 else 时不输出
LoopBlock => 渲染每个 item使用 separator 或默认空字符串/换行策略
CommentBlock => 输出空字符串

换行策略必须尽早统一,避免条件隐藏后产生多余空行。


18. 缺失变量策略

18.1 全局规则

预览:缺失变量保留占位符,并显示警告
导出:缺失变量默认报错,需要确认才能继续
变量级 fallback 优先于全局策略

18.2 预览示例

模板:

你好,{user.name}

数据:

{
  "user": {}
}

预览输出:

你好,{user.name}

调试面板:

⚠ 缺失变量user.name

18.3 导出示例

导出时若变量缺失,默认阻止导出:

存在缺失变量:
- user.name
- order.id

是否继续导出?

操作:

返回编辑
继续导出
复制带占位符结果

18.4 fallback

模板:

你好,{user.name ?? "未命名"}

输出:

你好,未命名

调试日志:

user.name 缺失,已使用 fallback"未命名"

19. 数据输入系统

19.1 双入口

数据输入采用:

JSON 是高级主入口
表单是自动生成的辅助入口

19.2 JSON 数据面板

右侧 Data Tab 以 JSON 编辑为主。

{
  "user": {
    "name": "张三",
    "isVip": true
  },
  "items": [
    {
      "title": "产品 A",
      "price": 99
    }
  ]
}

19.3 自动表单

软件可根据变量与 InputSchema 生成表单。

user.name     [张三]
user.isVip    [true]
items[0].title [产品 A]

v0.1 可先重点完成 JSON 模式,表单模式可简化。

19.4 数据集

一个模板可以保存多组测试数据:

默认数据
空数据
VIP 用户
普通用户
极端长文本
多项目列表

数据集文件单独保存于 datasets 目录。


20. 输出系统

20.1 v0.1 输出范围

v0.1 只做单模板单输出。

支持:

复制到剪贴板
导出 .txt
导出 .md
导出 .json
导出 .yaml
导出 .html
导出自定义扩展名文本

不做:

批量导出
多文件生成
目录生成

20.2 OutputBlock

DSL

@output markdown

对应 OutputBlock。

右侧属性:

输出格式
文件名模板
缺失变量策略
trimFinalOutput
ensureTrailingNewline

20.3 文件名模板

支持:

{title}-{date}.md

使用当前数据集渲染得到文件名。

20.4 预览范围

右侧 Preview Tab 支持:

完整模板
当前块

21. UI 设计

21.1 总体布局

三栏布局:

┌──────────────────────────────────────────────────────────────┐
│ 顶部工具栏:项目名 / 当前模板 / 保存状态 / 预览 / 导出 / 搜索 │
├───────────────┬───────────────────────────┬──────────────────┤
│ 左侧资源区    │ 中间 BlockFlow 编辑器      │ 右侧检查器       │
│               │                           │                  │
│ Templates     │ ▾ 模板main              │ 属性             │
│ Fragments     │   你好,[user.name]       │ 数据             │
│ Datasets      │                           │ 预览             │
│ Schemas       │   ▾ if user.isVip         │ 调试             │
│ Exports       │     感谢支持              │ 引用             │
├───────────────┴───────────────────────────┴──────────────────┤
│ 底部状态栏:已保存 / 缺失变量 2 / 引用错误 0 / 当前数据集     │
└──────────────────────────────────────────────────────────────┘

21.2 视觉风格

v0.1 采用:

深色优先
类 IDE
低饱和
细线框
高信息密度
低装饰
键盘友好

不要大面积卡片,不要花哨彩色块。

21.3 顶部工具栏

包含:

项目名
当前打开文件
保存状态
当前数据集选择
复制输出
导出
全局搜索
命令面板

快捷键建议:

Ctrl + P          快速打开资源
Ctrl + Shift + P  命令面板
Ctrl + S          手动保存
Ctrl + Z          撤销
Ctrl + Y          重做

21.4 左侧资源区

显示真实项目文件树和增强信息。

Project: MyProject

Templates
  main
  prompt-generator
  code-template

Fragments
  common-footer       12 refs
  output-rule          5 refs
  role-block           2 refs

Datasets
  default
  empty
  full-case

Schemas
  common-inputs

Exports
  latest-output.md

右键操作:

新建
重命名
删除
复制
移动
在文件夹中显示
刷新

21.5 中间编辑器

示例视觉:

▾ # 用户通知模板

你好,[user.name]

▾ if user.isVip
  感谢你作为 VIP 用户长期支持我们。

▾ for item in items
  - [item.title]: [item.price]

use common-footer

变量胶囊样式应低调:

[user.name]
[user.email !]
[user.age ?]

容器应像代码折叠区:

▾ if user.isVip
  ...

而不是巨大卡片。

21.6 右侧检查器

Tab

属性 | 数据 | 预览 | 调试 | 引用

属性 Tab

选中变量:

Variable
Path        user.name
Fallback    未命名
Required    true

选中条件:

Condition
Expression  user.isVip
Current     true

选中循环:

Loop
Source      items
Item        item
Index       index
Separator   newline

选中片段:

Fragment Ref
Fragment    common-footer
Status      found
Actions     Open / Expand

数据 Tab

数据集default [切换] [新建] [复制]
模式JSON | Form

预览 Tab

完整模板 | 当前块
复制 | 导出 | 重新渲染

调试 Tab

✓ variable user.name = "张三"
✓ condition user.isVip => true
✓ loop items => 2 items
✓ fragment common-footer loaded
⚠ variable user.email missing

引用 Tab

当前文件fragments/common-footer.json

被引用于:
- templates/main.json
- templates/prompt-generator.json

引用了:
- fragments/signature.json

状态:
✓ 无缺失引用
✓ 无循环引用

21.7 底部状态栏

已保存 | default dataset | missing vars: 1 | refs: ok | main.json

错误状态:

保存失败 | 缺失变量 2 | 缺失片段 1 | 外部文件已修改

22. 资源管理系统

22.1 真实文件系统优先

项目资源以真实文件夹为准。软件左侧资源区显示文件树,并提供搜索、标签、引用状态等增强信息。

22.2 索引只是缓存

软件增强索引负责:

搜索模板
搜索片段
搜索变量
搜索文本内容
显示标签
显示引用状态
显示缺失片段
显示循环引用
显示最近使用

但 JSON 文件是真相,索引不是主数据。

22.3 标签系统

标签写在模板/片段/数据集 JSON 中。

{
  "id": "fragment_output_rule",
  "name": "输出格式约束",
  "tags": ["prompt", "format", "rule"],
  "children": []
}

目录负责物理组织,标签负责逻辑组织。


23. 引用追踪

23.1 v0.1 只做引用追踪面板

不做大型依赖图。先解决实际维护问题:

谁引用了我?
我引用了谁?
有没有缺失片段?
有没有循环引用?

23.2 索引构建

扫描所有 TemplateDocument 和 FragmentDocument找出 FragmentRefBlock 和 InlineFragmentRef。

建立:

outgoing: 当前文档引用了哪些片段
incoming: 当前片段被哪些文档引用
missingFragments: 缺失片段引用
cycles: 循环引用链

23.3 循环检测

可使用 DFS 检测有向图环。

示例:

A → B → C → A

输出 ReferenceCycle。


24. 自动保存与外部修改检测

24.1 自动保存

默认自动保存。

建议策略:

停止编辑 800ms 后自动保存
保存前做 JSON 校验
保存失败时提示
保留最近一次成功保存状态

状态显示:

已保存
保存中...
保存失败templates/main.json 无法写入

24.2 撤销重做

必须支持:

Ctrl + Z
Ctrl + Y / Ctrl + Shift + Z

包括:

输入文本
插入变量
创建容器
拖拽块
修改属性
包裹选区
展开片段

24.3 外部修改检测

软件应监听项目文件夹变化。

如果当前文件在外部被修改:

  • 当前无未保存内容:提示重新加载/忽略
  • 当前有未保存内容:进入 conflict 状态

冲突操作:

使用外部版本
使用当前版本覆盖
另存为副本
查看差异

v0.1 不做自动合并。


25. Editor Adapter 设计

25.1 核心原则

TipTap/ProseMirror 只做编辑视图层。项目文件只保存 BlockFlow AST。

需要双向转换:

BlockFlow AST → TipTap Doc
TipTap Doc → BlockFlow AST

25.2 AST 到 TipTap

示例:

  • ParagraphBlock → paragraph node
  • TextInline → text
  • VariableInline → custom inline atom node
  • ContainerBlock → custom block node with content
  • ConditionBlock → custom block node with attrs.expression
  • LoopBlock → custom block node with attrs.source / itemName / indexName
  • FragmentRefBlock → custom block atom or block node

25.3 TipTap 到 AST

保存时将当前 editor doc 转换回 BlockFlow AST。不要直接保存 TipTap JSON。

25.4 节点扩展

v0.1 至少需要:

VariableInlineExtension
ContainerBlockExtension
ConditionBlockExtension
LoopBlockExtension
FragmentRefExtension
CommentBlockExtension

26. 命令菜单

26.1 / 菜单 v0.1 项

/变量
/容器
/条件
/循环
/片段引用
/插槽
/注释
/输出

26.2 模糊搜索

支持中文和英文:

/if
/条件
/var
/变量
/loop
/循环

26.3 创建后默认行为

创建变量:立即聚焦变量名。
创建条件:自动选中条件表达式。
创建循环:自动选中 source。
创建容器:光标进入容器内容。
创建片段引用:弹出片段选择器。


27. 快捷键建议

v0.1 可内置以下快捷键,后续再支持自定义:

Ctrl + P              快速打开模板/片段/数据集
Ctrl + Shift + P      命令面板
Ctrl + S              手动保存
Ctrl + Z              撤销
Ctrl + Y              重做
Ctrl + Shift + Z      重做
Ctrl + /              转为注释
Ctrl + Alt + G        包裹为容器
Ctrl + Alt + I        包裹为条件
Ctrl + Alt + L        包裹为循环
Ctrl + Alt + F        保存为片段

28. 开发里程碑

Milestone 1核心 AST + 渲染器

先不做 UI直接用 JSON 测试。

目标:

手写 template.json
手写 dataset.json
运行 renderer
得到 output string + logs + errors

支持:

paragraph
text inline
variable inline
condition
loop
container
fragment
comment

完成标准:

变量能替换
条件能判断
循环能展开
片段能引用
缺失变量能产生 warning/error
循环引用能检测

Milestone 2项目文件夹系统

目标:

打开项目文件夹
读取 project.json
扫描 templates/
扫描 fragments/
扫描 datasets/
构建内存索引

完成标准:

左侧能看到真实文件树
能打开模板
能切换数据集
能保存 JSON
能检测缺失片段

Milestone 3最小三栏 UI

先做壳子:

顶部工具栏
左侧项目资源区
中间占位编辑区
右侧属性/数据/预览/调试/引用
底部状态栏

Milestone 4TipTap 编辑器接入

第一批节点:

ParagraphBlock
VariableInline
ContainerBlock
ConditionBlock
LoopBlock
FragmentBlock
CommentBlock

第一批输入规则:

{user.name}          自动变变量胶囊
@if user.isVip       回车变条件块
@for item in items   回车变循环块
/                    打开命令菜单

完成标准:

能写普通文本
能插入变量
能插入条件
能插入循环
能保存为 BlockFlow AST
能重新打开不丢结构

Milestone 5右侧数据与实时预览

目标:

右侧 JSON 数据编辑
实时渲染
显示完整模板预览
显示当前块预览
显示缺失变量
显示渲染日志

Milestone 6片段系统与引用追踪

目标:

创建片段
引用片段
打开片段
展开片段
检测缺失片段
检测循环引用
显示被哪些文件引用

Milestone 7选区转块与片段化

目标:

选区包裹为容器
选区包裹为条件
选区包裹为循环
选区保存为片段
选区转为注释

Milestone 8导出与源码模式

目标:

复制最终输出
导出 txt / md / json / yaml / 任意扩展名
AST → DSL
DSL → AST
全文源码模式
局部源码编辑

29. 验收标准

v0.1 可以被认为可用时,应满足:

  1. 可以创建或打开一个项目文件夹。
  2. 可以在左侧资源树看到模板、片段、数据集。
  3. 可以打开模板并编辑普通文本。
  4. 输入 {user.name} 可以生成变量胶囊。
  5. 输入 @if user.isVip 回车可以生成条件块。
  6. 输入 @for item in items 回车可以生成循环块。
  7. 可以通过 / 菜单插入基础块。
  8. 可以输入或编辑 JSON 数据集。
  9. 预览面板能实时显示渲染结果。
  10. 缺失变量在预览时保留占位符并显示警告。
  11. 导出时缺失变量默认报错。
  12. 片段引用能正常渲染。
  13. 缺失片段能被提示。
  14. 循环片段引用能被检测。
  15. 可以复制最终输出。
  16. 可以导出单个文件。
  17. 项目内容保存为 JSON AST而不是 TipTap JSON。
  18. 自动保存可用。
  19. 外部修改检测可用。
  20. 关闭重开项目后结构不丢失。

30. 测试用例建议

30.1 基础变量

模板:

你好,{user.name}

数据:

{
  "user": {
    "name": "张三"
  }
}

期望输出:

你好,张三

30.2 缺失变量预览

模板:

你好,{user.name}

数据:

{
  "user": {}
}

预览输出:

你好,{user.name}

warnings 包含 MISSING_VARIABLE。

30.3 fallback

模板:

你好,{user.name ?? "未命名"}

数据:

{
  "user": {}
}

输出:

你好,未命名

30.4 条件命中

模板:

@if user.isVip
VIP 用户
@endif

数据:

{
  "user": {
    "isVip": true
  }
}

输出包含:

VIP 用户

30.5 条件未命中

数据:

{
  "user": {
    "isVip": false
  }
}

输出不包含条件内容。

30.6 循环

模板:

@for item in items
- {item.title}: {item.price}
@endfor

数据:

{
  "items": [
    { "title": "A", "price": 1 },
    { "title": "B", "price": 2 }
  ]
}

输出:

- A: 1
- B: 2

30.7 循环源不是数组

如果 items 不是数组,导出模式应产生 LOOP_SOURCE_NOT_ARRAY 错误。

30.8 片段引用

模板引用 common-footer,片段存在时正常渲染。

30.9 缺失片段

模板引用不存在片段时,产生 MISSING_FRAGMENT 错误。

30.10 循环片段引用

A 引用 BB 引用 A应产生 FRAGMENT_CYCLE 错误。


31. Codex 开发注意事项

31.1 不要偏离产品定位

不要把产品实现成:

  • 普通 Markdown 编辑器
  • 纯富文本编辑器
  • 传统低代码平台
  • AI 工具
  • 数据库驱动工具
  • 单文件封闭项目格式

31.2 不要让 TipTap 成为主数据模型

TipTap 只是编辑器视图层。保存文件时必须转回 BlockFlow AST。

31.3 不要使用 eval

表达式系统不得使用 eval()new Function() 执行用户输入。

31.4 不要在 v0.1 引入复杂依赖

v0.1 应先完成核心闭环。不要过早引入 SQLite、插件系统、依赖图、批量导出等。

31.5 保持模块纯净

Renderer、Expression、Reference Index、Project IO 应尽量与 React UI 解耦,方便测试。

31.6 优先写单元测试

核心模块应优先测试:

  • renderTemplate
  • renderBlock
  • renderInline
  • expression evaluator
  • missing variable handling
  • fragment cycle detection
  • AST <-> DSL
  • AST <-> TipTap Doc

31.7 先实现核心闭环

开发顺序优先:

类型定义
渲染器
表达式求值
项目文件夹读取
最小 UI
编辑器接入
实时预览
片段引用
导出

不要先花太多时间在视觉细节、复杂动画或非核心扩展上。


32. 一份完整模板 JSON 示例

{
  "kind": "template",
  "id": "template_user_notice",
  "name": "用户通知",
  "tags": ["demo", "notice"],
  "children": [
    {
      "id": "block_1",
      "type": "container",
      "name": "用户通知",
      "children": [
        {
          "id": "block_2",
          "type": "paragraph",
          "inlines": [
            {
              "type": "text",
              "content": "你好,"
            },
            {
              "type": "variable",
              "path": "user.name",
              "fallback": "未命名"
            }
          ]
        },
        {
          "id": "block_3",
          "type": "condition",
          "expression": "user.isVip",
          "children": [
            {
              "id": "block_4",
              "type": "paragraph",
              "inlines": [
                {
                  "type": "text",
                  "content": "感谢你作为 VIP 用户长期支持我们。"
                }
              ]
            }
          ]
        },
        {
          "id": "block_5",
          "type": "loop",
          "source": "items",
          "itemName": "item",
          "indexName": "index",
          "children": [
            {
              "id": "block_6",
              "type": "paragraph",
              "inlines": [
                {
                  "type": "text",
                  "content": "- "
                },
                {
                  "type": "variable",
                  "path": "item.title"
                },
                {
                  "type": "text",
                  "content": ": "
                },
                {
                  "type": "variable",
                  "path": "item.price"
                }
              ]
            }
          ]
        },
        {
          "id": "block_7",
          "type": "fragmentRef",
          "fragmentId": "fragment_common_footer"
        }
      ]
    }
  ]
}

33. 一份完整数据集 JSON 示例

{
  "kind": "dataset",
  "id": "dataset_default",
  "name": "默认数据",
  "tags": ["demo"],
  "data": {
    "user": {
      "name": "张三",
      "isVip": true
    },
    "items": [
      {
        "title": "产品 A",
        "price": 99
      },
      {
        "title": "产品 B",
        "price": 199
      }
    ]
  }
}

34. 一份片段 JSON 示例

{
  "kind": "fragment",
  "id": "fragment_common_footer",
  "name": "通用结尾",
  "tags": ["common", "footer"],
  "children": [
    {
      "id": "footer_p1",
      "type": "paragraph",
      "inlines": [
        {
          "type": "text",
          "content": "以上。"
        }
      ]
    }
  ]
}

35. v0.1 最终定义

v0.1 的最终形态应是:

一个可以打开本地项目文件夹、编辑结构化文本模板、插入变量/条件/循环/片段、输入 JSON 数据、实时预览、查看调试日志、追踪片段引用并复制或导出单文件结果的桌面端高级文本结构编排工具。

它的核心闭环是:

创建项目
↓
创建模板
↓
输入文本
↓
输入特殊符号转换为块
↓
嵌套组织容器
↓
引用片段
↓
输入 JSON 数据
↓
实时预览
↓
调试错误
↓
复制/导出结果

只要这个闭环足够顺畅v0.1 就是成功的。


36. 后续版本方向

v0.2

  • 完整片段 props UI
  • 完整插槽系统
  • 自动表单生成增强
  • DSL 源码模式增强
  • 快捷键自定义
  • 搜索正文与变量
  • 更好的文件 diff

v0.3

  • 批量导出
  • 多文件生成
  • 依赖图谱
  • SQLite 缓存索引
  • 主题系统
  • 模板包导入导出

v1.0

  • 稳定项目格式
  • 完整文档
  • 插件 API 初版
  • 大型项目性能优化
  • 可靠测试覆盖

37. 最重要的开发准则

开发时永远优先保护以下原则:

  1. BlockFlow AST 是唯一真相。
  2. 渲染器必须纯函数式、无副作用。
  3. 项目文件夹是真实项目本体。
  4. 可视化块服务于文本编辑效率。
  5. 变量、条件、循环、片段是核心,不要被 UI 花活稀释。
  6. v0.1 先做单模板单输出闭环,不要扩张到低代码平台。
  7. 高信息密度、键盘友好、类 IDE 是产品气质。
  8. 所有复杂功能都应围绕“复杂文本结构更容易维护”这个目标。