--- title: "feat: 多维表格(Bitable)伴生服务 v1" status: active date: 2026-06-24 deepened: 2026-06-24 type: feat origin: docs/brainstorms/2026-06-24-bitable-module-requirements.md --- # 多维表格(Bitable)伴生服务 v1 实现规划 ## Summary 为 AgentKit 引入多维表格伴生服务,作为异构数据源(Excel/数据库/爬虫API)的统一持久化落地载体。Agent 是数据的主要作者(采集写入),用户在落地后的表上精修、配视图、做分析。v1 验证"采集→落地→网格视图→基础公式→附件"核心闭环。 本服务逻辑独立(自有 API/CLI/领域模型/存储),当前共部署、UI 级集成,未来可零成本抽离。 ## Problem Frame AgentKit 缺少统一的持久化结构化数据落地载体。Excel 导出是单向的(`src/agentkit/documents/renderers/excel_renderer.py`),Excel 解析只转文本进 RAG(`src/agentkit/memory/document_loader.py`),`SharedWorkspace` 是带 TTL 的临时 KV。没有模块能把异构数据源的结构化数据持久化为可编辑、可视图、可计算的多维表格。 详见 origin: `docs/brainstorms/2026-06-24-bitable-module-requirements.md`。 ## Requirements 源自需求文档 v1 范围: | ID | 需求 | 来源 | |----|------|------| | R1 | 服务骨架:领域模型(表/字段/记录/视图)、API/CLI、独立 schema 存储 | 需求文档 §5 v1 | | R2 | 字段所有权模型 + 按主键 upsert 语义(数据列归 Agent,用户列保留) | 需求文档 §4.1 | | R3 | 三类采集落地(Excel 上传/URL、数据库导入、爬虫/API 采集) | 需求文档 §2 | | R4 | 网格视图(排序/筛选/分页/单元格编辑) | 需求文档 §5 v1 | | R5 | 基础公式列(算术/字符串/SUM/AVG/COUNT)+ 基础引用列(lookup) | 需求文档 §4.2 | | R6 | 图片/附件字段类型(复用现有文件上传能力) | 需求文档 §5 v1 | | R7 | 异步公式重算 + "计算中"状态标记 | 需求文档 §4.2 | | R8 | 伴生服务架构:API/CLI 调用边界,不做进程内紧耦合 | 需求文档 §3 | **成功标准**(源自需求文档 §9):Agent 能把 Excel/DB/API 数据写入多维表格;用户能编辑单元格、新增公式列、看到异步重算结果;重复采集时按主键 upsert 保留用户列;服务通过 API/CLI 被调用。 --- ## Key Technical Decisions ### KTD1: 存储选用 PostgreSQL(非 SQLite),跟随 evolution/memory 模式 现有伴生子系统(calendar/documents/auth)用 SQLite + 独立 `.db` 文件。bitable **偏离此模式**,改用 PostgreSQL + 独立 schema,跟随 `src/agentkit/evolution/pg_store.py` 和 `src/agentkit/memory/models.py` 的 PostgreSQL 模式。 **理由**:需求文档要求可演进到单表 10万+行 + 并发写入(Agent 采集 + 用户编辑同时)。SQLite 的并发写锁和单文件规模是硬瓶颈。PostgreSQL 的 JSONB 查询能力、行级并发、索引支持是 bitable 的刚需。 **代价**:bitable 要求部署环境配置 PostgreSQL(不像 calendar/documents 开箱即用 SQLite)。这是可接受的——需求文档已明确"共享 PG + 独立 schema"。 **模式参考**:`src/agentkit/evolution/pg_store.py`(PGBase 独立 + 延迟初始化 + 锁防并发)、`src/agentkit/memory/models.py`(SQLAlchemy 2 declarative + JSONB + pgvector)。 ### KTD2: 存储模型——字段定义表 + 记录表(JSONB 存值) 不用 EAV(一行一单元格,100k×20=200万行太慢),不用动态列(加列要 DDL)。采用: - **字段定义表** `bitable_fields`:每行一个字段定义(名称、类型、配置、所有权) - **记录表** `bitable_records`:每行一条记录,`values` 列为 JSONB(`{field_id: value}`) 这是 Airtable/飞书多维表格的标准模式。JSONB 支持 GIN 索引和 `->>` 查询,兼顾灵活性与查询性能。加列/删列只改字段定义表,不动记录表结构。 ### KTD3: 公式引擎——自研 Python 轻量引擎 不引入 HyperFormula(商业付费)、pycel(GPL 传染风险)、formulas(EUPL 边界模糊)。自研,因为 v1 函数集小(10-50 个)。 **架构**:`ast`/`pyparsing` 解析公式为 AST → 构建 DAG(字段依赖关系)→ Kahn 算法拓扑排序 → DFS 检测循环引用 → 增量重算(仅重算受影响下游)。 **重算策略**:数据列写入 → 标记依赖该列的公式列为"计算中" → 异步队列按拓扑序重算 → 结果写回记录 JSONB → 状态置"完成"。 `ponytail:` 自研引擎的 O(V+E) 拓扑重算在万级公式单元格下足够;若未来公式量到十万级或需 Excel 100% 兼容,升级路径为迁移到 Univer 引擎(Apache-2.0,免费商用)。 ### KTD4: 网格视图组件——vxe-table(MIT) 不选 Handsontable(商业付费)、ag-grid Enterprise(付费功能)、a-table 裸用(10k+ 行无虚拟滚动)。选 vxe-table:Vue 3 原生 + TS、MIT、横向+纵向虚拟滚动、可编辑 CRUD、自定义渲染器(插槽实现附件/图片/公式列)。 公式列由后端计算后回填值,前端只渲染(不前端算公式)。 ### KTD5: 服务边界——REST API 即使共部署也走 HTTP 需求文档要求"API/CLI 调用边界,不做进程内紧耦合"。即使 bitable 与 AgentKit 共进程部署,Agent 调用 bitable 也走 localhost REST API(`/api/v1/bitable/*`),而非直接 import service 类。 **理由**:满足伴生服务契约,未来抽离为零成本。代价是本地 HTTP 往返开销(可忽略)。 **例外**:CLI 命令(`agentkit bitable ...`)可直接调用 service 层(CLI 是运维工具,不是运行时调用路径)。 ### KTD6: 字段所有权——field 元数据 `owner` 字段 + 自动推断 `bitable_fields` 表增加 `owner` 列(`agent` | `user`)。自动推断规则:公式列/引用列/手动标注列 → `user`;Agent 采集写入的列 → `agent`。Agent 采集时可显式声明覆盖推断。 upsert 时只更新 `owner=agent` 的字段值,`owner=user` 的字段值原样保留。 ### KTD7: 公式引擎安全约束——受限 AST walker + 白名单节点 `ast.parse` 后**禁止直接 `eval()`**。必须实现受限 AST walker,仅允许白名单节点类型:`Expression`、`BinOp`、`UnaryOp`、`BoolOp`、`Compare`、`Call`(仅已注册函数)、`Name`(仅字段引用)、`Constant`、`IfExp`。 **禁用节点**:`Attribute`(防 `__import__`)、`Subscript`、`Lambda`、`Import`/`ImportFrom`、`Assign`/`AugAssign`、`For`/`While`、`FunctionDef`/`ClassDef`、`Subscript`、`Await`、`Yield`。遇到禁用节点立即抛出 `FormulaSecurityError`。 **理由**:公式字符串来自用户输入和 Agent 输出,是信任边界。`ast.eval` 的 `eval` 模式仍允许 `__builtins__` 访问。受限 walker 是唯一安全方案。 **模式参考**:Python `ast` 模块的 `NodeVisitor` + 白名单校验,类似 bandit 的 AST 检查模式。 ### KTD8: Upsert 用 `jsonb_set` 逐字段合并,禁止整行替换 upsert 更新 agent 列时,**禁止** `UPDATE ... SET values = :new_values`(整行替换会覆盖 user 列)。必须用 `jsonb_set` 逐字段合并: ```sql -- ponytail: 逐字段 jsonb_set,O(字段数) per record,万级批量 upsert 可接受 UPDATE bitable_records SET values = jsonb_set(values, :field_path, :field_value, true) WHERE id = :record_id ``` 对每条记录的每个 agent 列执行一次 `jsonb_set`,或在单条 SQL 中嵌套多个 `jsonb_set`。user 列(`owner=user`)的值绝不出现在 UPDATE 语句中。 **理由**:整行替换是 upsert 语义破坏的最常见实现错误。`jsonb_set` 逐字段合并是唯一能保证"只更新 agent 列、保留 user 列"的正确实现。 ### KTD9: 记录分页用 cursor-based,非 offset-based `GET /tables/{id}/records` 分页用 cursor(`?cursor=...&limit=50`),非 `?offset=0&limit=50`。 **理由**:offset 分页在 100k 行时深翻页慢(`OFFSET 50000` 仍扫描前 5 万行)。cursor 分页用 `WHERE id > :cursor ORDER BY id LIMIT :limit`,恒定性能。代价是不支持随机跳页(v1 不需要——网格视图是连续滚动)。 `ponytail:` cursor 分页不支持跳页;未来若需"跳到第 N 页",升级路径为 keyset + 估算偏移或预计算页索引。 ### KTD10: vxe-table 与 Ant Design Vue CSS 隔离 vxe-table 引入全局 CSS(`.vxe-*` 前缀),可能与 Ant Design Vue 的 `.ant-*` 样式冲突。隔离策略: 1. vxe-table 样式通过 `@import` 局部引入到 `BitableGrid.vue` 的 `