跳转至

科研表格

文件树右键「新建科研表格」创建 *.table.json,普通文件夹(包括 data/)均可使用。侧栏文件行不显示 .table.json 或实验记录的 .md,类型只由表格图标和笔记图标区分。工作区自动识别并使用 Glide Data Grid 打开。旧的独立 PoC、模拟数据、性能按钮和环境变量入口已删除。

编辑与保存

  • 标题点击进入编辑,Enter 或失焦确认,Escape 取消;类别为标题旁的菜单标签。保存状态(已保存、待保存、正在保存、未保存)始终以文字显示,窄窗格也不隐藏;已保存带勾,未保存用错误色和图标。手动保存、复制引用和 CSV 导出收进操作菜单。
  • 「添加行」仍追加一行。下拉提供 10 与 100,自定义数量最多 500。一次添加 100 行或更多空行会先确认,因为空行会作为记录写入文件。列尾 + 和操作菜单里的「添加字段」添加字段。底栏统计未归档的已存储行和字段,不含末尾录入行,也不含已归档行。只要存在已归档行,底栏同时给出它们的数量。
  • 网格高度按当前视图里的数据行再加一行录入行计算,并限制在窗格高度内。多出来的窗格是纯背景,不再铺满空白行线。在录入行输入或粘贴会追加真实记录,不必先点「添加行」。
  • 点击列头在升序、降序和不排序之间切换,只改变当前视图,不改写 JSON 里的行序;排序中的列头有方向标记。字段编辑在列头菜单,不在普通点击上。拖拽列边界会把宽度写入字段的可选 width(80 到 480 的整数)。有单位的列默认 220,其它列默认 200;拖回默认宽度时省略该键,因此没改过列宽的旧文件保持原样。工具栏的一个筛选框按单元格文本过滤视图,隐藏的行仍留在文件里。是否显示已归档行也只存在于当前视图,不写回 JSON。筛选和排序都作用在这条可见性规则之后。底栏行数是未归档的已存储行。
  • 网格使用应用主题变量,表头区分类型图标、字段名与单位;数值右对齐,保留原始精度。Notebook 内嵌预览仅保留打开入口和含刷新操作的菜单。
  • 表格区分「科研对象」和「实验结果」;这仍是两种表格语义标签。每张表可以有自己的注册编号前缀。还没有执行 Run 或库存业务。
  • 字段有稳定 ID,支持文本、有限数值、布尔、ISO 日期、单选和链接。数值字段可配置单位,显示在列名中;单选和日期使用独立编辑控件。数值只接受有限十进制(含科学计数法),拒绝 0x10 这类十六进制。空布尔值在网格里显示为空白文本,存储仍是 null,不会画成未勾选;输入 true/false 或 1/0 后才显示复选框,Delete 清空回到 null。链接单元格是注册编号字符串或空值,格式为前缀加至少四位数字(ID-0001),拒绝 0x10 和 nope。目标路径留空表示指向本表,编辑时可以点选本表已有编号及其第一段文本;非空目标是另一份 Vault 相对的 *.table.json,编号手动输入。解析时不要求目标行或目标文件存在,悬空链接合法。链接字段不保存单位和选项。已有取值的列改成链接仍被既有的类型迁移规则拦住。
  • 字段改名保留 ID。已有非空数据时,对话框会说明无法直接改类型或单位,并禁用这次保存,不静默换算。单选选项是可添加、重命名、移除和上下移动的列表,不允许空名称或重复。重命名会改写仍使用旧名称的单元格,但不会因此改写该行的 updatedAt。若选项仍被使用,移除会被拒绝,并说明还有多少行在用它。删除字段要确认。删除行的确认改为归档:记录离开当前工作集,注册编号不释放,之后可以恢复。永久删除只针对已归档行,另有一次确认,并且不回收编号。确认文案不把会话内撤销说成可追溯的审计记录。保留至少一列。
  • 可以直接在录入行编辑,也可以从 Excel 复制矩形区域粘贴;必要时自动追加行。整个批次先校验再提交,一个错误值或额外列会拒绝整批,不截断后悄悄丢数据。排序和筛选不会改变这次粘贴所依据的存储行,粘贴落在视图中的对应记录上,超出录入行的部分追加到表尾。
  • 行有内部稳定 ID,界面不展示这串 UUID。行标在有注册编号时显示编号(如 ID-0001),否则显示当前视图序号。选中一行或多行已存储记录时,工具栏用弹出层显示注册编号、创建时间、更新时间,以及最后编辑者是人还是智能体。缺哪一项就不显示哪一项。时间按当前界面语言格式化。同一选择可以复制:副本追加在表尾,单元格取值相同,使用新的 UUID、新的注册编号、actor: "human" 和当时的时间戳,不复制 archivedAt,也不复用已有编号。少于 100 行会立即复制;100 行或更多先确认一次。选中行标后 Delete 会确认并归档这些尚未归档的行;已归档行在工具栏里恢复或永久删除。普通单元格选区 Delete 清空值。已归档行在网格里使用更弱的颜色。新行,包括末尾录入行提交出的记录,会领到注册编号、创建时间和 actor: "human"。只改单元格的行更新 updatedAt 和 actor,不重新发号。
  • 最近 20 次内存编辑可以撤销/重做,包括字段、行和批量粘贴。Cmd/Ctrl+Z 操作网格事务;浮层输入框保留自身文本撤销。它不是持久化审计历史。
  • 800 ms 防抖保存、手动保存和关闭前 flush 共用工作区文本保存链路,维护 textDirty、种子和外部重载。表格保存严格比较上次保存内容,不使用 Markdown 的自身写入宽免;文件读失败时拒绝写入。当前沿用既有文件读后比较机制,不提供跨进程文件 CAS。
  • 可导出 CSV,或复制 Markdown 引用。CSV 只含未归档行,第一列是 registryId,其后是字段名(数值带单位)。引号按 CSV 转义。它仍是扁平交换格式,不包含字段类型和行的内部 UUID;完整保留结构应复制原 JSON 文件。

Notebook 引用

复制引用后粘贴到普通 Markdown,或使用 /table(嵌入科研表格)。空嵌入会列出已有的 *.table.json(路径、表名、注册前缀或 id);列表被 200 份上限截断时,仍可手填路径。复制引用同时写入 id 和 path。表格不在当前 Vault 内时提示错误,不再静默返回。

::research-table{id="table-uuid" path="data/measurements.table.json"}

只写 path 的旧笔记仍然加载。path 在出现时仍须通过 safeTableReference。id 是表格 JSON 的 id。编辑器保存时,remark-directive 可能把不含空格的 id 写成 {#id ...};这和 id="..." 是同一个属性,两种写法都会读回。预览先按 path 读取。路径缺失、文件无法读取,或文件里的 id 与指令里的 id 不同时,按该 id 在 research_table_list 的结果里查找(结果含 path、id、name)。恰好一条匹配就改写指令的 path 并保留原来的 id,然后预览那份文件。笔记只读或正在导出时不改指令,回到可编辑后再查找这一对。多条匹配时显示不匹配说明和这些路径,不从中挑一份。没有匹配则保持当前的不匹配或无法读取状态。每一对 id 与 path 只查找一次,不会在每次按键时重复查找。

Plate 中显示实时只读预览,在未归档行里取最后 20 行(记录追加在表尾,因此是较新的实验),并显示预览行数/未归档总行数;「打开表格」进入完整编辑器。笔记和表格各自保存,笔记不复制整份数据。导出笔记时输出同一窗口的 HTML 表格。没有固定历史快照语义,实验定稿应另导出结果文件。

引用路径禁止绝对路径、..、隐藏目录及跨平台分隔符歧义。重命名或移动表格时,已有的 id 属性保持不变,只改写跟随移动的 path。当前窗口保存表格会立刻刷新其嵌入。文件监听里的表格变更(包括 Agent 写入)也会刷新已经打开的笔记预览,窗口在后台时先暂存,回到前台再刷新。预览上仍可手动刷新。

底栏右侧用一行显示有多少笔记通过 path 或 id 使用了这张表。没有笔记时不显示。点开后列出这些笔记的路径,点击后打开笔记并收起列表。从文件树删除 *.table.json 时,若至少一篇笔记嵌入了它,先确认并写明数量。查找失败时仍按原来的方式删除,并用 notifyError 提示,不会把文件留在原地。查找跳过隐藏目录,并跳过超过 2 MiB 的 Markdown。

实验记录

实验记录是普通 Markdown,不是新的文件类型。文件树在「新建科研表格」旁提供「新建实验记录」。确认名称后,在所选文件夹写入同名的 *.table.json 和 .md。表名来自文件名(按 TABLE_SUFFIX.length 去掉后缀),第一列名称与新建科研表格相同。任一路径已存在则 notifyError 并且两个文件都不写。

新建的笔记不带 frontmatter。正文按当前界面语言写入目的、步骤、结果三节,结果节嵌入新建的表(同时带 id 和 path)。需要状态或其它元数据时,用编辑器里已有的「属性」自己添加,不另做状态菜单。

已经写成 type: experiment 且 status: locked 的旧笔记,file_write 仍拒绝替换或追加,稳定错误码是 experiment_locked,并且不写盘。判断只扫描第一个 frontmatter 块的行,不使用 YAML 库。新建文件允许。*.table.json 不受这条规则影响。桌面端不再因为这个状态把正文设成只读。

文件契约与 Agent

src/lib/research-table/model.ts 是契约与校验入口,格式名仍是 agentero-table,version 仍是字面量 1,不引入第二版。结构文件本身是真源,没有为这些记录另建 SQLite 或目录。每个 Vault 可包含多份表格;字段和行的内部 ID 在编辑中不变,revision 随成功保存递增。注册编号只在这一份文件内唯一,不会扫描 Vault,也不会写入 Catalog;归档和永久删除都不回收已发出的号码。空值为 null 或缺省字段,零和 false 不等于空值。网格里的空布尔是空白文本,不是未勾选框。

缺少下列新键的旧文件仍能 parseResearchTable。打开后不做编辑再保存,不会把这些键补进 JSON。新表才会写入 idPrefix: "ID" 和 nextSequence: 1。

  • idPrefix:1–12 个字符,以字母开头。缺省时,下一次真正追加记录才会写成 ID。
  • nextSequence:下一个将要发出的正整数。若它小于已有同前缀后缀,发号时改用已有最大后缀加一,并记下新值。计数偏小不是解析错误。
  • schemaPath:另一份 Vault 相对 *.table.json,规则与表格引用相同(拒绝空路径、非 .table.json、\、:、#、控制字符、空段和以 . 开头的段,因此 .. 和隐藏目录失败)。调用方知道当前文件时,还要拒绝指向自己。确认时读取该文件并做 checkSchema:结构表里的每个字段 ID 都必须在当前表中,共有字段的名称、类型、单位和选项必须一致,当前表可以多出字段。不一致时提示错误且不保留这次路径。文件不存在时保留路径,并在工具栏显示一行状态,不在侧栏挂错误条。「补齐表结构字段」只追加缺失字段(同一 ID、名称、类型、单位和选项),不删除多余字段,也不覆盖类型不一致的列。
  • 行上的 registryId 形如 ID-0001。createdAt、updatedAt、archivedAt 使用 Date.prototype.toISOString() 这种 Zod 接受的 UTC 时间。actor 只取 human 或 agent;界面在创建记录、复制行或改单元格时写 human,没有作者字符串,也没有用户资料。选中行的详情只读这些字段。
  • 字段可选 width:80–480 的整数。网格在缺省时用 220(字段有单位)或 200(其它)。等于该默认值时不写键。table_update_cells 重写文件时保留已有的 width。
  • appliedKeys:可选字符串数组,最多 50 项,每项 1–80 个字符。Agent 行操作用它记住最近的幂等键。界面没有这一项的编辑控件。人类保存会展开整张表再写回,因此 replaceField 重新解析后这个键还在。旧文件没有该键时仍能解析,不做编辑就不会把它写进去。
  • 字段类型增加 link。target 省略或空字符串表示本表;非空时是 Vault 相对 *.table.json。非链接字段不保存 target。链接值是现有单元格联合里的 string | null。
{
  "format": "agentero-table",
  "version": 1,
  "id": "table-stable-id",
  "name": "循环测量",
  "kind": "result",
  "revision": 1,
  "fields": [
    {"id":"sample","name":"样品","type":"text","unit":"","options":[]},
    {"id":"capacity","name":"容量","type":"number","unit":"mAh","options":[]}
  ],
  "idPrefix": "ID",
  "nextSequence": 2,
  "rows": [{
    "id": "row-stable-id",
    "registryId": "ID-0001",
    "createdAt": "2026-11-07T00:00:00.000Z",
    "updatedAt": "2026-11-07T00:00:00.000Z",
    "actor": "human",
    "values": {"sample": "BAT-001", "capacity": 2.31}
  }]
}

version 仍是字面量 1。Agent 与 CLI 的行操作(table_query / table_upsert / table_update_cells,CLI 为 agentero table query|upsert|update-cells)和桌面网格共用这一份文件模式,不另建 SQLite 索引。Vault 关键词搜索会包含 *.table.json(见下),但读出匹配行仍用 table_query。查询按文件里的存储顺序返回。单元格用字段名定位,行用注册编号定位。写入带 expectedRevision:与当前 revision 不一致时返回 revision_conflict 和当前修订号,不写盘;成功则把 revision 加一。省略注册编号的 upsert 会追加一行,并按 idPrefix(缺省 ID)签发下一个编号,序号取 nextSequence 与同前缀已用后缀加一的较大者。可选幂等键写入 appliedKeys;键已经存在时不写盘、不增加修订号,并返回 replayed: true。Agent 创建或改值的行写 actor: "agent"。本步不提供归档、恢复或永久删除。

file_read 仍只能整文件读取不超过 2 MiB 的表格;更大的表用 table_query(行操作读取上限 32 MiB)。file_write 不能追加或覆盖已有的 *.table.json。创建一份还不存在的表格时,正文必须能通过同一套解析,非法 JSON 不会落盘。桌面网格继续通过 model.ts 编辑,不改走 IPC。保存时如果磁盘内容既不是上次保存的快照、也不是即将写入的正文,已有的字节冲突检查会拒绝覆盖,因此打开中的编辑器不会被行操作静默盖掉。

重新打开时严格校验版本、身份唯一性、注册编号和字段值;非法内容显示无法读取的说明,并附上已有的简短本地化原因,不展示堆栈,也不当作空表覆盖。列排序、关键字筛选和「显示已归档」只存在于打开表格时的视图,不写回 JSON,也不改存储行序。空布尔值在界面上是空白文本,文件里仍是 null 或缺省,不是 false。

命令面板的正文搜索同样覆盖 *.table.json。命中标题是表的 name,不是文件名;匹配落在某一行时,摘要带上该行的注册编号和字段名,否则摘要是表名。年份、期刊、DOI、已读这些 catalog 条件会丢掉表格命中。其它 JSON 文件不进入索引。隐藏目录和既有的体积上限仍然有效。命令面板里表格命中使用表格图标。

边界:最多 100,000 行、100 个字段、单次粘贴/编辑 100,000 单元格;整文件读写,不代表已完成最大规模性能验收。链接只保存注册编号,不在解析时解析目标行。注册编号的唯一性停在单个文件,没有跨表目录。原始文件逐值来源、科学单位换算、可保存的查询视图、图表、库存和不可变审计历史属于后续步骤。当前列排序、筛选和显示已归档都是临时视图。本步没有图表、库存、分子工具,也没有第二套文件版本。

同步

S3 / WebDAV 同步时,双侧都改动同一张 .table.json 会保留原文件与 name (conflict 时间).table.json,两份均可由表格编辑器打开,需人工核对合并;不按普通 JSON 的 LWW 丢弃一侧。Git 继续使用 Git 的冲突流程。文件夹内原始数据和普通笔记仍按原有规则同步。

验证

最小回归:test/research-table.test.ts、test/research-table-markdown.test.ts、test/workspace-viewer-modes.test.ts、test/editor-slash-command.test.ts;Rust merge_divergent_research_tables_preserves_both_readable_files 验证两种时间顺序下都保留可识别的完整扩展名。

Markdown 回归走实际的 deserializeMarkdownBody,覆盖预处理、插件选择和往返序列化,避免仅在裸 Plate 测试里通过、实际笔记却把指令大括号转成普通文本。

macOS Tauri 实测:打开真实 .table.json,新增行、撤销、自动保存后重载;粘贴包含中文与数值的 TSV 矩形并核对磁盘内容;Cmd+Z / Shift+Cmd+Z;非法数值弹出错误且拒绝整批;Plate 引用加载及刷新。当前回归 6 个测试文件、51 个用例通过,另有同步冲突 Rust 回归通过。中文粘贴不等于真实输入法组合输入验收;输入法候选词、Windows WebView2 和 100,000 行性能仍需专项验收。