# 分子构建器 — 协议规范 **项目**:有机化学反应工作台 · 分子构建器 **版本**:0.1.0 **日期**:2026-09-13 **文档类型**:数据与接口协议规范 本规范定义分子构建器对外/对内的**数据交换格式、序列化协议、外部服务接口约定与错误协议**,是模块间集成与二次开发的契约。 --- ## 1. 术语 | 术语 | 含义 | |------|------| | 分子图(MoleculeGraph) | 以原子编号 + 连接键表示的分子结构,系统的唯一事实来源 | | 隐式氢(implicit_h) | 未显式建键、由价键规则推导出的氢原子数 | | 芳香键 | `aromatic = true` 的键,渲染为苯环,未实体化为凯库勒单双键 | | 凯库勒环 | 已实体化为单双交替的环(`aromatic = false`),可继续编辑 | | 描述符 | SMILES / InChI / InChIKey,由 RDKit 计算或自研兜底生成 | --- ## 2. 分子图数据协议 ### 2.1 Atom | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|:----:|------|------| | `atom_id` | int | ✓ | ≥ 1,分子内唯一,删除不回收 | 原子编号 | | `element` | string | ✓ | 见 §2.4 元素集合 | 元素符号 | | `charge` | int | ✓ | 整数,默认 0 | 形式电荷 | | `isotope` | int | ✗ | — | 同位素质量数 | | `radical` | int | ✓ | ≥ 0,默认 0 | 自由基电子数 | | `implicit_h` | int | ✓ | ≥ 0 | 隐式氢数(由系统重算) | | `stereo` | string | ✓ | 默认 `""` | 手性/构型标记(预留) | | `lone_pairs` | int | ✓ | ≥ 0 | 孤对电子数(由系统估算) | | `x` | number | ✓ | px | 渲染坐标(不入化学逻辑) | | `y` | number | ✓ | px | 渲染坐标(不入化学逻辑) | | `in_ring` | bool | ✗ | — | 环成员标记(渲染辅助) | | `manuallyPlaced` | bool | ✗ | — | 手动拖动锁定标记 | ### 2.2 Bond | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|:----:|------|------| | `bond_id` | int | ✓ | ≥ 1,分子内唯一,删除不回收 | 键编号 | | `atom1_id` | int | ✓ | 必须存在于 `atoms` | 起始原子 | | `atom2_id` | int | ✓ | 必须存在于 `atoms`,≠ `atom1_id` | 终止原子 | | `order` | 1 \| 2 \| 3 | ✓ | — | 键级(芳香键单独标记) | | `aromatic` | bool | ✓ | — | 是否芳香键 | | `stereo` | string | ✓ | 默认 `""` | 顺反/楔形标记(预留) | ### 2.3 MoleculeGraph ```json { "atoms": [ { "atom_id": 1, "element": "C", "charge": 0, "radical": 0, "implicit_h": 3, "stereo": "", "lone_pairs": 0, "x": 120, "y": 150 }, { "atom_id": 2, "element": "C", "charge": 0, "radical": 0, "implicit_h": 3, "stereo": "", "lone_pairs": 0, "x": 164, "y": 150 } ], "bonds": [ { "bond_id": 1, "atom1_id": 1, "atom2_id": 2, "order": 1, "aromatic": false, "stereo": "" } ], "nextAtomId": 3, "nextBondId": 2 } ``` | 字段 | 类型 | 说明 | |------|------|------| | `atoms` | Atom[] | 原子数组,顺序稳定 | | `bonds` | Bond[] | 键数组,顺序稳定 | | `nextAtomId` | int | 下一个可用原子编号 = 历史最大编号 + 1 | | `nextBondId` | int | 下一个可用键编号 = 历史最大编号 + 1 | ### 2.4 取值集合 - **可增加元素** `ADDABLE_ELEMENTS`:`H, O, N, S, P, F, Cl, Br, I`。 - **基团模板** `GroupKey`:`OH, NH2, CH3, COOH, CHO, NO2, SO3H, Ph`。 - **主价表**:`C:4`,`N:3|5`,`O:2`,`S:2|4|6`,`P:3|5`,`H|F|Cl|Br|I:1`。 ### 2.5 不变量 1. 编号唯一且单调递增,删除不回收。 2. 所有编辑出口统一执行「芳香性规范化 → 隐式氢/孤对重算」。 3. 芳香键按 Kekulé 计价:环内首根芳香键计 2,其余各计 1。 4. 坐标不参与任何化学判定。 --- ## 3. 操作结果协议 ### 3.1 编辑操作返回值 `OpsResult` ```ts type OpsResult = | { ok: true; graph: MoleculeGraph; message?: string } | { ok: false; error: string; atomIds?: number[] } ``` | 字段 | 说明 | |------|------| | `ok=true` | 操作成功,`graph` 为新图(不可变,原图不变) | | `ok=false` | 操作被拒绝;`error` 为面向用户的中文提示;`atomIds` 为需高亮的原子编号 | | `message` | 可选的成功提示(如「已按化学规范整理布局」) | **约定**:编辑失败**绝不抛出异常**,一律以 `ok=false` 返回,由 store 转成 Toast 与高亮。 ### 3.2 删除结果协议 `DeleteOutcome` ```ts type DeleteOutcome = | { kind: 'done'; graph: MoleculeGraph } // 直接完成 | { kind: 'ask-branch'; branches: BranchChoice[]; targetId: number } // 需用户选分支 | { kind: 'empty' } // 结果为分子为空 ``` `BranchChoice`: | 字段 | 类型 | 说明 | |------|------|------| | `branchAtomId` | int | 保留侧的相邻原子编号(确认参数) | | `comp` | int[] | 该分支包含的原子编号集合 | | `side` | enum | `left/right/up/down/up-left/up-right/down-left/down-right` | | `label` | string | 中文方位标签,如「保留右侧」 | 方位由相邻原子相对中心原子的方向按 8 扇区(每 45°)划分。 ### 3.3 典型错误码 | 场景 | `error` 文案 | |------|-------------| | 原子已连接 4 键 | `该原子已连接 4 个键` | | 芳香键用于非环 | `芳香键需在环中使用,请使用单键` | | 自连 | `不能连接同一个原子` | | 重复成键 | `这两个原子之间已有键` | | 价键超限 | `<元素> 最多 键,当前 键`(多个以「;」连接) | | 无可用方向 | `该原子已无可用方向` | --- ## 4. 持久化协议 ### 4.1 存储位置 | 键 | 内容 | 说明 | |----|------|------| | `molecule-library-v2` | `LibraryEntry[]` 的 JSON | 分子库;版本号变更即视为不兼容 | ### 4.2 LibraryEntry ```json { "id": "uuid", "name": "乙醇", "formula": "C2H6O", "smiles": "CCO", "inChI": "InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3", "inChIKey": "LFQSCWFLJHTTHZ-UHFFFAOYSA-N", "atoms": [ /* Atom[] */ ], "bonds": [ /* Bond[] */ ], "tags": [], "createdAt": 1757660000000 } ``` | 字段 | 类型 | 说明 | |------|------|------| | `id` | string | `crypto.randomUUID()`,不可用时回退时间戳 | | `name` | string | 保存时取「中文名 → IUPAC 名 → 分子式」首个非空 | | `formula` | string | 分子式 | | `smiles` | string | canonical SMILES | | `inChI` / `inChIKey` | string | 标准标识符 | | `atoms` / `bonds` | 数组 | 分子图快照(含坐标) | | `tags` | string[] | 预留标签 | | `createdAt` | number | 毫秒时间戳 | ### 4.3 去重规则 按顺序匹配: 1. 双方均有 `inChIKey` → 比较 `inChIKey`; 2. 否则双方均有 `smiles` → 比较 `smiles`; 3. 命中即返回 `{ status: 'duplicate', existing }`,不写入。 ### 4.4 载入还原协议 `entryToGraph(entry)`: 1. 深拷贝 `atoms` / `bonds`; 2. `nextAtomId = max(atom_id) + 1`,`nextBondId = max(bond_id) + 1`; 3. 重算隐式氢与孤对电子后返回。 > 该协议保证库中数据即使因历史版本字段缺失,也能被安全还原。 --- ## 5. 序列化协议 ### 5.1 MOL(V2000 molblock) 由 `graphToMolblock(graph)` 生成,用于导出 `.mol` 及 RDKit `get_mol` 输入。 ``` MoleculeBuilder 2026-09-12 0 0 0 0 0 0 0 0999 V2000 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 M CHG ... ← 仅当存在形式电荷 M END ``` | 约定 | 值 | |------|-----| | 坐标格式 | 定点 4 位小数,宽度 10 | | 芳香键 | 输出键级 `4` | | 形式电荷 | `M CHG` 行,原子序号为 **1 基** | | 编码 | UTF-8,行尾 `\n` | ### 5.2 SMILES | 来源 | 协议 | |------|------| | 首选 | RDKit `mol.get_smiles()`(canonical) | | 兜底 | `toFallbackSmiles(graph)`:自研 DFS 生成树 + 非树边环闭合编号;芳香环内碳用小写 `c`;导出 `.smi` 时首行为 SMILES,次行为名称/分子式 | ### 5.3 图形导出 | 格式 | 协议 | |------|------| | SVG | 克隆画布内容分组、**移除视变换与热区**、按未变换包围盒加 16px 留白,输出 `viewBox` 与白底,`XMLSerializer` 序列化 | | PNG | 同 SVG 流程后以 3 倍分辨率绘制到 Canvas 导出 | | 文件名 | `分子结构.svg` / `分子结构.png` / `分子结构.mol` / `分子.smi` | --- ## 6. RDKit 集成协议 ### 6.1 加载协议 | 项 | 约定 | |----|------| | 入口脚本 | `/rdkit/RDKit_minimal.js`(由 `scripts/copy-rdkit.mjs` 复制自 `@rdkit/rdkit`) | | wasm 定位 | `locateFile: f => '/rdkit/' + f` | | 初始化 | 调用全局 `initRDKitModule(options)`,返回 Promise | | 超时 | 10s;超时或失败 → 状态 `failed`,整体降级自研引擎 | | 复用 | 单例;已注入的 `