57 KiB
| title | status | date | deepened | type | origin |
|---|---|---|---|---|---|
| feat: 多维表格(Bitable)伴生服务 v1 | active | 2026-06-24 | 2026-06-24 | feat | 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 逐字段合并:
-- 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-* 样式冲突。隔离策略:
- vxe-table 样式通过
@import局部引入到BitableGrid.vue的<style scoped>不可行(vxe-table 用全局类),改为在main.ts中import 'vxe-table/lib/style.css'且只在 bitable 路由组件挂载时确保已加载 - bitable 网格容器用
.bitable-grid-scope包裹,vxe-table 的样式覆盖限定在该 scope 下(.bitable-grid-scope .vxe-table { ... }) - 字体/颜色变量对齐 Ant Design Vue 的 token(
var(--ant-primary-color)等),避免视觉割裂
理由:vxe-table 和 Ant Design Vue 都是全局样式注入型组件库,不隔离会导致样式互相污染。
KTD11: 内部 Agent→bitable HTTP 服务间认证
Agent 通过 BitableTool 调用 bitable REST API 时,不走用户 JWT 认证(Agent 无用户会话)。改用内部服务令牌:
agentkit.yaml配置bitable.internal_token(启动时生成或手动配置)- BitableTool 请求头携带
X-Internal-Token: <token> - bitable 路由的
require_authenticated依赖增加内部令牌分支:Authorization: Bearer <jwt>或X-Internal-Token: <token>二选一 - 内部令牌仅授权 bitable 端点,不授权其他 API
理由:伴生服务架构要求 REST API 边界(KTD5),但 Agent 无用户会话。内部令牌是服务间认证的标准模式,比禁用认证安全,比共享 JWT 简单。
ponytail: 内部令牌是静态共享密钥,适合单实例部署;未来多实例/独立部署时升级为 mTLS 或 OAuth2 client credentials。
High-Level Technical Design
组件架构
flowchart LR
subgraph AgentKit
Agent[Agent Loop] -->|HTTP API| BitableRoutes[Bitable Routes]
CLI[agentkit bitable CLI] -->|direct call| BitableService
BitableRoutes --> BitableService[BitableService]
BitableService --> BitableRepo[Repository]
BitableService --> FormulaEngine[Formula Engine]
BitableService --> RecalcQueue[Recalc Queue]
RecalcWorker[Recalc Worker] -->|consume| RecalcQueue
RecalcWorker --> FormulaEngine
RecalcWorker --> BitableRepo
BitableRepo --> PG[(PostgreSQL\nbitable schema)]
end
subgraph Ingestion
Agent -->|Excel/DB/Crawler| BitableTool[BitableTool]
BitableTool -->|HTTP| BitableRoutes
end
subgraph Frontend
GridView[Grid View\nvxe-table] -->|HTTP| BitableRoutes
GridStore[Pinia Store] --> GridView
end
数据模型 ERD
erDiagram
bitable_tables ||--o{ bitable_fields : has
bitable_tables ||--o{ bitable_records : has
bitable_tables ||--o{ bitable_views : has
bitable_fields ||--o{ bitable_recalc_queue : triggers
bitable_tables {
string id PK
string name
string description
string primary_key_field_id FK
string owner_user_id
timestamp created_at
timestamp updated_at
}
bitable_fields {
string id PK
string table_id FK
string name
string field_type "text/number/date/select/attachment/image/formula/lookup"
jsonb config "options, formula_expr, lookup_target"
string owner "agent|user"
timestamp created_at
}
bitable_records {
string id PK
string table_id FK
jsonb values "{field_id: value}"
timestamp created_at
timestamp updated_at
}
bitable_views {
string id PK
string table_id FK
string name
string view_type "grid|kanban|gantt|gallery|form"
jsonb config "filters, sorts, groupings, hidden_fields"
timestamp created_at
}
bitable_recalc_queue {
string id PK
string table_id FK
string record_id FK
string field_id FK
string status "pending|calculating|done|error"
string error_message
timestamp queued_at
timestamp completed_at
}
公式异步重算流程
sequenceDiagram
participant Agent
participant API
participant Service
participant Queue as Recalc Queue
participant Worker as Recalc Worker
participant Engine as Formula Engine
participant DB
Agent->>API: POST /records (upsert, data columns)
API->>Service: upsert_records(table_id, records, pk)
Service->>DB: upsert (update agent-owned columns only)
Service->>Service: detect affected formula fields (DAG lookup)
Service->>DB: mark formula cells "calculating"
Service->>Queue: enqueue recalc tasks (record_id, field_id)
Service-->>API: 202 Accepted (records saved, formulas calculating)
API-->>Agent: 202 + recalc pending count
loop async
Worker->>Queue: dequeue task
Worker->>DB: load source values + formula expr
Worker->>Engine: evaluate(formula_expr, source_values)
Engine-->>Worker: result | error
Worker->>DB: write result to record values JSONB
Worker->>DB: mark cell "done" | "error"
end
Note over Agent,API: Frontend polls or gets WS update for "done" status
Scope Boundaries
本次范围内(v1)
- bitable 伴生服务骨架(领域模型、PostgreSQL schema、REST API、CLI)
- 字段所有权模型 + 按主键 upsert 语义
- 三类采集落地(Excel/DB/爬虫API)通过 BitableTool
- 网格视图(vxe-table,排序/筛选/分页/编辑)
- 基础公式列(算术/字符串/SUM/AVG/COUNT)+ 基础引用列(lookup)
- 图片/附件字段类型
- 异步公式重算 + "计算中"状态
延后(v2/v3,见需求文档 §5)
- 看板/甘特/画廊/表单视图
- 高级公式(日期/条件/跨表 rollup)+ 函数库扩展
- 分析能力(分组/透视)
- 大规模优化(列式/分区/物化视图)
- 多人实时协作
本产品身份之外
- 不做通用电子表格(字段化记录模型,非单元格自由编辑)
- 不做 ETL/数据管道平台(采集是 Agent 按需执行,非定时调度)
- 不做 BI 仪表盘产品
- 不替代知识库 RAG
Deferred to Follow-Up Work
- bitable 数据导出为 Excel/CSV(现有
excel_renderer.py可后续适配) - bitable 记录的语义检索(pgvector 索引,类似 episodic memory)
- 多维表格与 Agent 记忆系统的联动(bitable 作为 episodic memory 的结构化补充)
- WebSocket 实时推送公式重算完成事件(v1 用轮询)
Implementation Units
U1. 领域模型 + PostgreSQL Schema + 服务骨架
Goal: 搭建 bitable 子系统的领域模型、数据库 schema 和服务骨架,为后续所有单元提供基础。
Requirements: R1, R8
Dependencies: 无(基础单元)
Files:
src/agentkit/bitable/__init__.py(新建)src/agentkit/bitable/models.py(新建,Pydantic v2 数据模型)src/agentkit/bitable/db.py(新建,PostgreSQL schema + init 函数 + 迁移机制)src/agentkit/bitable/repository.py(新建,数据访问层)src/agentkit/bitable/service.py(新建,业务逻辑层骨架)src/agentkit/server/app.py(修改,lifespan 中初始化 bitable)tests/unit/bitable/test_models.py(新建)tests/unit/bitable/test_db.py(新建)
Approach:
- Pydantic 模型:
Table、Field(含FieldType枚举:text/number/date/select/multiselect/attachment/image/formula/lookup)、Record、View、RecalcTask。所有模型用model_config = ConfigDict(...)。 - PostgreSQL schema:5 张表(
bitable_tables、bitable_fields、bitable_records、bitable_views、bitable_recalc_queue)+ 1 张元数据表(bitable_meta),置于独立 schemabitable。bitable_records.values用 JSONB + GIN 索引。 - 主键唯一约束:
bitable_tables的primary_key_field_id指定的字段在bitable_records.values中对应值必须唯一。通过在bitable_records上建函数索引CREATE UNIQUE INDEX ... ON bitable_records (table_id, (values->>'{pk_field_id}')) WHERE values ? '{pk_field_id}'实现。upsert 按此索引匹配。 - Recalc queue 索引:
bitable_recalc_queue在(status, queued_at)上建索引(worker 按 status=pending + queued_at 排序消费);在(record_id, field_id)上建唯一索引防重复入队。 - Schema 迁移机制:采用
src/agentkit/server/auth/models.py的_SCHEMA_VERSION模式。bitable_meta表存schema_version,init_bitable_db()读取当前版本,按版本号顺序执行迁移脚本(migrations/v1__init.sql、migrations/v2__add_index.sql等)。首次创建版本=1,后续每次 init 检查版本并执行未应用的迁移。 - init 函数
init_bitable_db():参考src/agentkit/evolution/pg_store.py的延迟初始化 + 锁防并发模式。CREATE SCHEMA IF NOT EXISTS bitable+ 按版本执行迁移。 - service 骨架:
BitableService类,注入 repository,暴露后续单元将实现的方法签名。 - app.py lifespan:
try/except包裹初始化,失败时logger.exception不崩溃(参考 calendar 子系统初始化模式,src/agentkit/server/app.py第 406-428 行)。
Patterns to follow:
- PostgreSQL 模式:
src/agentkit/evolution/pg_store.py(PGBase 独立、延迟初始化、锁防并发) - SQLAlchemy 2 declarative:
src/agentkit/server/auth/models.py(DeclarativeBase+Mapped+mapped_column+_SCHEMA_VERSION迁移机制) - JSONB 元数据:
src/agentkit/memory/models.py(metadata_字段用 JSONB) - 表名安全校验:
src/agentkit/evolution/experience_store.py(_SAFE_TABLE_NAME_PATTERN防 SQL 注入)
Test scenarios:
- Happy path:
init_bitable_db()创建 schema 和 6 张表(含bitable_meta),幂等重复调用不报错 - Happy path: Pydantic 模型序列化/反序列化 round-trip 正确(Table/Field/Record/View)
- Edge case:
Field的configJSONB 在不同 field_type 下结构正确(formula 类型有formula_expr,lookup 类型有lookup_target,select 类型有options) - Edge case:
Record.valuesJSONB 为空{}时合法(新记录无值) - Covers 迁移:
bitable_meta.schema_version初始为 1;模拟 v2 迁移脚本存在时,第二次 init 执行 v2 迁移并更新版本号 - Covers 主键约束: 设置主键字段后,插入两条相同主键值的记录触发唯一约束冲突
- Covers 队列去重:
bitable_recalc_queue的(record_id, field_id)唯一索引阻止重复入队 - Error path: PostgreSQL 不可用时
init_bitable_db()抛出明确异常,app.py lifespan 捕获后 bitable 降级 - Integration: app.py lifespan 初始化后
app.state.bitable_service存在
Verification: init_bitable_db() 成功创建 schema + 6 张表 + 迁移版本记录;主键唯一约束生效;Pydantic 模型可正确序列化;app 启动后 bitable service 可用(或降级记日志)。
U2. CRUD API + 字段所有权 + Upsert 语义
Goal: 实现 bitable 的 REST API(表/字段/记录/视图 CRUD),含字段所有权模型和按主键 upsert 语义。
Requirements: R1, R2, R8
Dependencies: U1
Files:
src/agentkit/server/routes/bitable.py(新建,FastAPI 路由)src/agentkit/bitable/service.py(修改,实现 CRUD + upsert 逻辑)src/agentkit/bitable/repository.py(修改,实现数据访问)src/agentkit/server/app.py(修改,注册路由app.include_router(bitable_routes.router, prefix="/api/v1"))tests/unit/bitable/test_service.py(新建)tests/unit/bitable/test_routes.py(新建)
Approach:
- 路由:
router = APIRouter(prefix="/bitable", tags=["bitable"]),最终前缀/api/v1/bitable。参考src/agentkit/server/routes/calendar.py。 - 端点:
- 表:
POST /tables、GET /tables、GET /tables/{id}、PATCH /tables/{id}、DELETE /tables/{id} - 字段:
POST /tables/{id}/fields、GET /tables/{id}/fields、PATCH /fields/{id}、DELETE /fields/{id} - 记录:
POST /tables/{id}/records(批量插入)、GET /tables/{id}/records(cursor 分页+筛选+排序,见 KTD9)、PATCH /records/{id}、DELETE /tables/{id}/records(批量删除) - 视图:
POST /tables/{id}/views、GET /tables/{id}/views、PATCH /views/{id}
- 表:
- 字段所有权:创建字段时
owner默认user;Agent 通过 BitableTool 写入时声明owner=agent。upsert 时只更新owner=agent的字段值。 - Upsert 实现(KTD8):upsert 更新阶段禁止
UPDATE ... SET values = :new_values。必须用jsonb_set逐字段合并——对每条记录的每个 agent 列执行jsonb_set(values, '{field_id}', :value, true)。user 列值绝不出现在 UPDATE 语句中。批量 upsert 用单事务包裹,失败回滚。 - 字段删除依赖检查:删除字段前检查:(1) 是否有公式字段引用该字段(DAG 反向查找);(2) 是否是表的主键字段;(3) 是否有视图的 filter/sort 配置引用该字段。有依赖时返回 409 Conflict + 依赖列表,不直接删除。强制删除时(
?force=true)级联清理:公式字段标记为 error、视图配置移除该字段引用、记录 JSONB 中移除该字段 key(values - '{field_id}')。 - 视图过滤翻译:视图的
config.filters(如[{field_id, op, value}])在查询记录时翻译为 JSONB 查询条件。op支持eq/ne/contains/gt/lt/is_empty。翻译为WHERE values->>'{field_id}' {op_sql} :value。排序翻译为ORDER BY values->>'{field_id}'。注意:JSONB->>返回 text,number/date 比较需 cast(CAST(values->>'{field_id}' AS NUMERIC))。 - 认证:
require_authenticated依赖(参考src/agentkit/server/auth/dependencies.py)+ 内部令牌分支(KTD11)。 - 服务访问:路由通过
request.app.state.bitable_service获取 service(参考 calendar.py 模式)。
Patterns to follow:
- 路由模块:
src/agentkit/server/routes/calendar.py(APIRouter+app.state服务访问 + 503 降级) - 认证依赖:
src/agentkit/server/auth/dependencies.py(require_authenticated、require_permission) - 表名/字段名校验:
_SAFE_TABLE_NAME_PATTERN模式防注入 - JSONB 查询:
src/agentkit/memory/models.py的 JSONB 操作模式
Test scenarios:
- Happy path: 创建表 → 添加字段 → 插入记录 → 查询记录(cursor 分页)完整流程
- Happy path: upsert 模式——首次插入 3 条记录,第二次 upsert 同主键不同数据列值,记录数不变、数据列更新
- Covers R2 + KTD8: upsert 时用户列(
owner=user)值不被覆盖——先手动设置 user 列值,再 upsert,user 列值不变。验证 SQL 层面用jsonb_set而非整行替换 - Covers 字段删除: 删除被公式引用的字段返回 409 + 依赖列表;
?force=true后级联清理公式字段状态 - Covers 字段删除: 删除主键字段返回 409
- Covers 视图过滤: 视图配置
filter: [{field_id, op: "gt", value: 100}]查询时正确过滤 number 字段(CAST 为 NUMERIC) - Edge case: 主键字段未设置时 upsert 报 400
- Edge case: 批量插入空数组返回成功且 count=0
- Edge case: cursor 分页——第一页返回 next_cursor,第二页用该 cursor 获取后续记录,无更多数据时 next_cursor 为 null
- Covers 并发: 两个并发 upsert 同主键不同 agent 列——一个成功一个等待,最终两列都更新(行级锁)
- Error path: 表不存在时所有操作返回 404
- Error path: 字段类型不匹配(往 number 字段写非数字)的校验行为
- Integration: 字段所有权自动推断——Agent 声明
owner=agent的字段,upsert 后该字段更新;owner=user的字段不更新
Verification: API 端点可 CRUD 表/字段/记录/视图;upsert 用 jsonb_set 正确保留用户列;字段删除有依赖检查;视图过滤正确翻译为 JSONB 查询;cursor 分页正确。
U3. 公式引擎 + 异步重算管道 + 基础引用列
Goal: 实现自研 Python 公式引擎(解析、依赖图、重算)和异步重算管道,支持基础公式(算术/字符串/SUM/AVG/COUNT)和基础引用列(lookup)。
Requirements: R5, R7
Dependencies: U1, U2
Files:
src/agentkit/bitable/formula/__init__.py(新建)src/agentkit/bitable/formula/parser.py(新建,公式解析为 AST)src/agentkit/bitable/formula/engine.py(新建,DAG + 拓扑排序 + 求值)src/agentkit/bitable/formula/functions.py(新建,内置函数库)src/agentkit/bitable/recalc_worker.py(新建,异步重算 worker)src/agentkit/bitable/service.py(修改,写入时触发重算入队)src/agentkit/server/app.py(修改,lifespan 启动 recalc worker)tests/unit/bitable/test_formula_parser.py(新建)tests/unit/bitable/test_formula_engine.py(新建)tests/unit/bitable/test_recalc.py(新建)
Approach:
- 解析器(
parser.py):用ast模块解析公式字符串(如=SUM({field_abc}) + {field_xyz} * 2)为 AST。字段引用用{field_id}语法。支持算术运算符、字符串拼接、函数调用。 - AST 安全约束(KTD7):
ast.parse后实现受限NodeVisitor,仅允许白名单节点(Expression/BinOp/UnaryOp/BoolOp/Compare/Call仅已注册函数/Name仅字段引用/Constant/IfExp)。禁用Attribute/Subscript/Lambda/Import/Assign/For/While/FunctionDef/ClassDef/Await/Yield。遇到禁用节点抛FormulaSecurityError。禁止eval()/exec()。 - 引擎(
engine.py):- 构建 DAG:遍历 AST 提取字段依赖,建立字段间依赖图
- 拓扑排序:Kahn 算法,确定重算顺序
- 循环检测:DFS 检测循环引用,抛出
CircularReferenceError - 求值:按拓扑序遍历 AST,解析字段引用(从 record values 读取),调用函数库
- 函数库(
functions.py):v1 实现SUM、AVG、COUNT、MIN、MAX、CONCAT、ABS、ROUND、IF、LEN。每个函数注册到FUNCTION_REGISTRY。 - 聚合函数语义边界:
SUM({field_id})中{field_id}引用整列(聚合上下文),返回标量;{field_id} + 1中{field_id}引用当前记录的值(行上下文),返回标量。区分规则:聚合函数的参数为列引用时聚合整列,否则按行求值。SUM({f1} + {f2})= 对每行f1+f2求和(聚合);{f1} + SUM({f2})= 当前行 f1 + f2 列总和(混合)。解析器在 AST 层面标记聚合上下文,引擎按标记决定取列值还是取行值。 - 引用列(lookup):lookup 字段的
config.lookup_target = {table_id, field_id, filter_field_id, filter_value}。求值时从目标表查询匹配记录的指定字段值。复用引擎的字段引用解析机制。lookup 是只读引用,不参与 DAG 环检测(不会形成环)。 - 异步重算管道(
recalc_worker.py):- 数据列写入后,service 检测受影响的公式字段(DAG 反向查找下游)
- 标记
bitable_recalc_queue为pending,记录 JSONB 中公式字段值置为{__status: "calculating"}。入队时利用(record_id, field_id)唯一索引去重(已有 pending 任务则跳过,ON CONFLICT DO NOTHING) - worker 从队列消费任务,按拓扑序重算,结果写回 JSONB,状态置
done/error - 事务边界:每个 recalc task 的"读取源值→求值→写回结果→标记完成"在单事务中完成,避免读到半更新状态。写回用
jsonb_set只更新公式字段值,不影响其他字段。 - Worker 崩溃恢复:worker 启动时扫描
status='calculating'的任务(上次崩溃残留),重置为pending重新入队。app.py lifespan 中 worker 作为 asyncio task 启动,关闭时发 sentinel 优雅停止。 - Reaper 机制:定时任务(每 5 分钟)扫描
status='pending'且queued_at超过 10 分钟的任务,重置为pending并重新入队(防 worker 卡死)。 - worker 在 app.py lifespan 中作为 asyncio task 启动,关闭时优雅停止
- 异步生成器安全:若 worker 用
async def+yield消费队列,遵守return; yield模式(见.trae/rules/project_rules.md)。
Execution note: 公式引擎核心逻辑(解析 + AST 安全 + DAG + 求值)建议测试先行——先写解析器、安全约束和循环检测的测试,再实现。
Patterns to follow:
- 异步任务生命周期:
src/agentkit/server/app.pylifespan 中 calendar/evolution 子系统的启动/关闭模式 - 异步生成器安全:
.trae/rules/project_rules.md(return; yield模式) - 队列消费模式:
src/agentkit/experts/team.py的HandoffTransport(bounded queue + sentinel 关闭) - AST 安全检查:Python
ast.NodeVisitor白名单模式
Test scenarios:
- Happy path:
=1+2*3解析为 AST 并求值得 7 - Happy path:
=SUM({f1})其中 f1 列值为 [1,2,3] 求值得 6(聚合上下文) - Happy path:
=CONCAT({f1}, "-", {f2})求值得 "a-b"(行上下文) - Happy path:
={f1} + SUM({f2})混合上下文——当前行 f1 + f2 列总和 - Happy path: lookup 字段从目标表查询匹配记录的值
- Covers KTD7 安全: 公式
=__import__('os')抛出FormulaSecurityError(Attribute 节点被禁) - Covers KTD7 安全: 公式
=(lambda: 1)()抛出FormulaSecurityError(Lambda 节点被禁) - Covers KTD7 安全: 公式
=eval('1+1')抛出FormulaSecurityError(Call 节点函数名不在注册表) - Covers 聚合语义:
SUM({f1})聚合整列 vs{f1} + 1引用当前行——两者语义不同,测试验证 - Edge case: 公式引用不存在的 field_id 报错
- Edge case: 空值参与运算的语义(SUM 忽略空值,算术遇空值报错)
- Covers R7: 数据列写入后,公式字段进入 "calculating" 状态,worker 重算后变为 "done" 且值正确
- Covers 崩溃恢复: 模拟 worker 在
calculating状态崩溃 → 重启后任务重置为pending并重算成功 - Covers 去重: 同一 (record_id, field_id) 并发入队两次 → 队列中只有一个任务(唯一索引)
- Covers 事务: recalc task 执行期间另一请求更新源字段 → task 在自己的事务中读到一致快照
- Error path: 循环引用
f1 = f2 + 1、f2 = f1 + 1抛出CircularReferenceError - Error path: 公式语法错误(括号不匹配)抛出
FormulaParseError - Error path: 函数不存在(
=UNKNOWN())抛出UnknownFunctionError - Integration: 多条记录批量 upsert 后,所有受影响公式字段被入队重算,最终值正确
Verification: 公式可解析、求值;AST 安全约束阻止注入;聚合函数语义正确;循环引用被检测;异步重算管道正确更新公式值且支持崩溃恢复;lookup 引用列正确跨表取值。
U4. Agent BitableTool + 三类采集落地
Goal: 实现 BitableTool(Agent 工具),支持三类数据采集写入:Excel 上传/URL、数据库导入、爬虫/API 采集。
Requirements: R3, R8
Dependencies: U2
Files:
src/agentkit/tools/bitable_tool.py(新建,BitableTool(Tool))src/agentkit/bitable/ingestion/__init__.py(新建)src/agentkit/bitable/ingestion/excel.py(新建,Excel 解析+写入)src/agentkit/bitable/ingestion/database.py(新建,数据库导入)src/agentkit/bitable/ingestion/api_collector.py(新建,API/爬虫采集)src/agentkit/server/app.py(修改,注册 BitableTool 到 tool_registry)tests/unit/bitable/test_bitable_tool.py(新建)tests/unit/bitable/test_ingestion_excel.py(新建)
Approach:
- BitableTool:继承
Tool(src/agentkit/tools/base.py),input_schema用action枚举区分操作。通过 HTTP 调用 bitable REST API(KTD5:即使共部署也走 HTTP)。- actions:
create_table、import_excel、import_database、collect_api、upsert_records、query_records - 工具描述用英文(供 LLM function calling)
- 内部认证(KTD11):HTTP 请求头携带
X-Internal-Token: <token>,token 从agentkit.yaml的bitable.internal_token读取。BitableTool 初始化时注入 token。
- actions:
- 批量分块:
upsert_records和import_excel/import_database写入时,单次 HTTP 请求最多 500 条记录(BATCH_SIZE=500)。超过则分块发送,每块独立 HTTP 请求。失败时返回已成功块数 + 失败块详情,支持断点续传(调用方传入resume_from跳过已成功块)。 - Excel 导入(
excel.py):复用src/agentkit/memory/document_loader.py的_parse_xlsx逻辑(openpyxl),但改为返回结构化数据({sheet_name: [{col: val}]})而非 Markdown 文本。支持文件上传和 URL 两种输入。按 sheet 创建表,首行为字段名,自动推断字段类型。 - 数据库导入(
database.py):接收连接串 + 表名列表,用 SQLAlchemy 反射读取表结构,按表生成 bitable 表+字段,批量导入数据。字段类型映射(DB int→number、varchar→text、timestamp→date)。 - API 采集(
api_collector.py):Agent 已有爬虫/API 调用能力(工具系统),此模块只负责"把 Agent 采集到的结构化数据按字段写入 bitable"。接收{records: [...], field_mapping: {...}},调用 upsert API 写入。 - 注册:app.py lifespan 中
app.state.tool_registry.register(BitableTool(...)),参考 calendar_tool 注册模式(src/agentkit/server/app.py第 422 行)。
Patterns to follow:
- 工具基类:
src/agentkit/tools/base.py(Tool抽象类 +safe_execute) - 工具实现:
src/agentkit/tools/calendar_tool.py(action枚举 + service 注入 + input_schema) - Excel 解析:
src/agentkit/memory/document_loader.py的_parse_xlsx(openpyxl +MAX_ROWS_PER_SHEET) - 工具注册:
src/agentkit/server/app.pylifespan(tool_registry.register)
Test scenarios:
- Happy path:
import_excel上传 .xlsx 文件 → 创建表 + 字段 + 记录,数据正确 - Happy path:
import_excel提供 URL → 下载 + 解析 + 写入 - Happy path:
import_database指定表名 → 生成 bitable 表,字段类型映射正确 - Happy path:
collect_api接收 records + field_mapping → upsert 写入 - Covers KTD11 认证: BitableTool 请求头携带
X-Internal-Token,无 token 时 bitable API 返回 401 - Covers 批量分块: 1200 条记录的 upsert → 分 3 次 HTTP 请求(500+500+200),全部成功
- Covers 批量分块: 1200 条记录,第 2 块失败 → 返回已成功 500 条 + 失败详情,
resume_from=500续传 - Covers R3: 三类采集场景各自端到端写入成功
- Edge case: Excel 空表(只有表头无数据行)→ 创建表+字段,0 条记录
- Edge case: Excel 合并单元格——仅左上角有值(已知局限,参考
document_loader.py) - Edge case: 数据库表无主键——bitable 表自动生成 id 字段作为主键
- Error path: Excel 文件格式损坏 → 明确错误信息
- Error path: 数据库连接失败 → 错误返回,不创建 bitable 表
- Error path: bitable REST API 不可用(503)→ 工具返回明确错误
- Integration: Agent 通过 BitableTool 创建表并写入数据后,前端网格视图可查看到数据
Verification: Agent 能通过 BitableTool 执行三类采集写入;内部认证正确;批量分块写入可靠;数据正确落地到 bitable 表。
U5a. 前端网格视图 + Store + API 客户端 + 公式轮询
Goal: 实现前端网格视图(vxe-table),支持排序/筛选/cursor 分页/单元格编辑,配套 Pinia store、API 客户端和公式重算状态轮询。
Requirements: R4, R7(前端侧)
Dependencies: U2
Files:
src/agentkit/server/frontend/src/api/bitable.ts(新建,BitableApiClient extends BaseApiClient+ 类型定义)src/agentkit/server/frontend/src/stores/bitable.ts(新建,defineStore('bitable', ...))src/agentkit/server/frontend/src/views/BitableView.vue(新建,主视图,全屏布局)src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue(新建,vxe-table 网格组件)src/agentkit/server/frontend/src/components/bitable/TableViewList.vue(新建,表列表侧栏)src/agentkit/server/frontend/src/router/index.ts(修改,添加/agent/bitable路由,全屏布局)src/agentkit/server/frontend/src/main.ts(修改,import 'vxe-table/lib/style.css')src/agentkit/server/frontend/package.json(修改,添加 vxe-table 依赖)
Approach:
- API 客户端(
bitable.ts):BitableApiClient extends BaseApiClient(src/agentkit/server/frontend/src/api/base.ts),API_BASE = '/api/v1/bitable'。类型定义同文件(IBitableTable、IBitableField、IBitableRecord、IBitableView),参考src/agentkit/server/frontend/src/api/calendar.ts模式。 - Pinia store(
bitable.ts):Composition API 风格,state(tables、currentTable、fields、records、views、loading、recalcPendingCount),actions(loadTables、selectTable、loadRecords、updateCell、addField、pollRecalcStatus)。参考src/agentkit/server/frontend/src/stores/calendar.ts。 - 网格组件(
BitableGrid.vue):vxe-table v4,配置:- 虚拟滚动:
virtualXConfig+virtualYConfig(支持 10k+ 行) - 可编辑单元格:
edit-config,按字段类型渲染编辑器(text/number/date/select) - 自定义列渲染器:附件/图片列用插槽渲染(U6 实现),公式列显示值或"计算中"标记
- 排序/筛选:vxe-table 内置 + 服务端 cursor 分页
- CSS 隔离(KTD10):容器用
.bitable-grid-scope包裹,vxe-table 样式覆盖限定在该 scope 下
- 虚拟滚动:
- 公式轮询策略:store 中
pollRecalcStatusaction——当记录中存在__status: "calculating"的公式字段时,每 2s 轮询GET /tables/{id}/records?fields=calculating_only。所有公式字段变为done/error后停止轮询。切换表或组件卸载时清理定时器。 - 主视图(
BitableView.vue):左侧表列表 + 右侧网格。全屏布局(非 AgentLayout 的 55% 象限),bitable 需要完整宽度展示网格。 - 路由:
/agentchildren 中添加{ path: 'bitable', name: 'agent-bitable', meta: { title: '多维表格', panel: 'full' }, component: () => import('@/views/BitableView.vue') }。meta.panel = 'full'表示全屏(需在 AgentLayout 中支持 full panel 类型,或直接用独立 layout)。 - 依赖:
npm install vxe-table(MIT)。
Patterns to follow:
- API 客户端:
src/agentkit/server/frontend/src/api/calendar.ts(BaseApiClient继承 + 类型同文件 +isXxx类型守卫) - Pinia store:
src/agentkit/server/frontend/src/stores/calendar.ts(Composition API + 错误中文化 + notification) - 路由注册:
src/agentkit/server/frontend/src/router/index.ts(/agentchildren +meta.panel) - 组件结构:
src/agentkit/server/frontend/src/components/calendar/(主视图 + 子组件分层)
Test scenarios:
- Happy path: 打开
/agent/bitable→ 显示表列表 → 选择表 → 网格加载数据(cursor 分页) - Happy path: 双击单元格 → 编辑 → 保存 → 数据持久化到后端
- Happy path: 点击列头排序 → 服务端排序 → 数据刷新
- Happy path: 滚动到底部 → cursor 分页加载下一页
- Covers R7 轮询: 公式列显示"计算中"标记 → 2s 后轮询 → 重算完成更新为值 → 轮询停止
- Covers KTD10 隔离: vxe-table 样式不污染 Ant Design Vue 组件(视觉检查)
- Edge case: 10k 行数据虚拟滚动流畅(无卡顿)
- Edge case: 空表(0 条记录)显示空状态
- Edge case: 切换表时轮询定时器被清理(无内存泄漏)
- Error path: 后端 503(bitable 未初始化)→ 显示降级提示
- Integration: Agent 通过 BitableTool 写入数据后,前端刷新可见
Verification: 前端可查看/编辑 bitable 数据;10k 行虚拟滚动流畅;公式列正确显示计算状态并轮询更新;CSS 隔离无污染。
U5b. 表/字段管理 UI
Goal: 实现表创建、字段管理(新增/编辑/删除/类型配置)的前端 UI,使用户能自主建表和配置字段(不依赖 Agent 采集)。
Requirements: R1, R2
Dependencies: U5a
Files:
src/agentkit/server/frontend/src/components/bitable/TableCreateModal.vue(新建,创建表对话框)src/agentkit/server/frontend/src/components/bitable/FieldManagePanel.vue(新建,字段管理面板)src/agentkit/server/frontend/src/components/bitable/FieldConfigForm.vue(新建,字段配置表单——按类型动态渲染)src/agentkit/server/frontend/src/stores/bitable.ts(修改,添加 createTable、addField、updateField、deleteField actions)src/agentkit/server/frontend/src/api/bitable.ts(修改,补充表/字段 CRUD 方法)
Approach:
- 创建表对话框(
TableCreateModal.vue):a-modal+a-form,输入表名、描述、主键字段名。提交后调用POST /tables。 - 字段管理面板(
FieldManagePanel.vue):侧滑面板(a-drawer),列出当前表所有字段(名称、类型、owner 标签),支持新增/编辑/删除。删除时调用 API,若返回 409(有依赖)则显示依赖列表确认框。 - 字段配置表单(
FieldConfigForm.vue):按field_type动态渲染配置项:- text/number:无额外配置
- date:日期格式
- select/multiselect:选项列表(可增删)
- formula:公式表达式输入框 + 实时语法校验(调用后端
POST /bitable/fields/validate-formula) - lookup:目标表+字段+过滤条件选择器
- attachment/image:无额外配置
- owner 标签:字段列表中
owner=agent显示蓝色"Agent"标签,owner=user显示绿色"用户"标签。
Patterns to follow:
- 对话框/抽屉:Ant Design Vue 的
a-modal/a-drawer模式 - 动态表单:
a-form+v-if按 type 渲染 - 表单校验:
src/agentkit/server/frontend/src/components/calendar/中的表单校验模式
Test scenarios:
- Happy path: 点击"新建表" → 填写表名+主键字段 → 提交 → 表列表刷新
- Happy path: 打开字段管理 → 新增 formula 字段 → 输入公式 → 语法校验通过 → 保存
- Happy path: 编辑 select 字段 → 增删选项 → 保存 → 网格中该列下拉选项更新
- Covers 字段删除: 删除被公式引用的字段 → 显示 409 依赖列表 → 确认强制删除 → 公式字段标记 error
- Edge case: 公式语法错误 → 实时校验显示错误提示,保存按钮禁用
- Edge case: lookup 字段配置——选择目标表后加载该表字段列表
- Edge case: owner 标签正确显示(agent 蓝色 / user 绿色)
- Error path: 网络错误 → notification 提示
Verification: 用户可通过 UI 创建表、管理字段(含公式语法校验)、删除字段(含依赖检查);字段 owner 标签正确。
U5c. 视图配置 UI
Goal: 实现视图管理(创建/切换/配置筛选排序)的前端 UI,使用户能保存和切换不同的数据查看视角。
Requirements: R4
Dependencies: U5a
Files:
src/agentkit/server/frontend/src/components/bitable/ViewSwitcher.vue(新建,视图切换器)src/agentkit/server/frontend/src/components/bitable/ViewConfigPanel.vue(新建,视图配置面板——筛选/排序/隐藏字段)src/agentkit/server/frontend/src/components/bitable/FilterBuilder.vue(新建,筛选条件构建器)src/agentkit/server/frontend/src/stores/bitable.ts(修改,添加 createView、updateView、switchView actions)src/agentkit/server/frontend/src/api/bitable.ts(修改,补充视图 CRUD 方法)
Approach:
- 视图切换器(
ViewSwitcher.vue):网格顶部 tab 栏,列出当前表的所有视图,点击切换。"+"按钮创建新视图(输入名称+类型,v1 仅 grid)。 - 视图配置面板(
ViewConfigPanel.vue):a-drawer,三个 tab:筛选、排序、隐藏字段。- 筛选:
FilterBuilder组件,支持增删筛选条件(field + op + value),op 选项按字段类型动态变化 - 排序:字段选择 + 升序/降序
- 隐藏字段:字段列表 checkbox,勾选隐藏
- 筛选:
- FilterBuilder(
FilterBuilder.vue):条件列表,每行a-select(字段)+a-select(操作符)+a-input/a-date-picker(值)。op 选项:text→eq/ne/contains/is_empty;number→eq/ne/gt/lt/gte/lte;date→eq/gt/lt/between。 - 配置变更实时保存到视图
config(PATCH /views/{id}),网格立即重新查询。
Patterns to follow:
- Tab 栏:Ant Design Vue
a-tabs - 条件构建器:动态表单行 +
a-select联动
Test scenarios:
- Happy path: 创建视图"高价订单" → 配置筛选
amount > 1000→ 网格只显示匹配记录 - Happy path: 切换视图 → 网格数据按视图配置刷新
- Happy path: 配置排序
created_at desc→ 网格按创建时间倒序 - Happy path: 隐藏字段 → 网格中该列消失
- Edge case: 筛选 number 字段 → op 选项为 eq/ne/gt/lt(非 contains)
- Edge case: 筛选 date 字段 → 值输入为
a-date-picker - Edge case: 多个筛选条件 AND 组合
- Edge case: 视图配置变更实时保存 + 网格刷新
- Error path: 筛选值类型不匹配 → 校验提示
Verification: 用户可创建/切换视图;筛选/排序/隐藏字段配置生效并实时保存;不同字段类型的筛选操作符正确。
U6. 图片/附件字段类型
Goal: 实现图片和附件字段类型,复用现有文件上传能力,支持上传、存储、预览。
Requirements: R6
Dependencies: U2, U5a
Files:
src/agentkit/server/routes/bitable.py(修改,添加附件上传端点)src/agentkit/bitable/service.py(修改,附件存储逻辑 + 记录删除时附件清理)src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue(新建,附件单元格渲染)src/agentkit/server/frontend/src/components/bitable/ImageCell.vue(新建,图片单元格渲染,懒加载)tests/unit/bitable/test_attachment.py(新建)
Approach:
- 后端:复用现有上传端点模式(
src/agentkit/server/routes/chat.py第 1240-1276 行的POST /api/v1/chat/upload),新建POST /api/v1/bitable/tables/{id}/upload,存储到data/uploads/bitable/。附件值在记录 JSONB 中存为[{filename, stored_name, url, size, mime_type}]数组。 - 记录删除时附件清理:
DELETE /tables/{id}/records和DELETE /records/{id}时,service 层先读取待删记录的 attachment/image 字段值,删除对应物理文件,再删除记录。用try/except包裹文件删除(文件丢失不阻断记录删除,只记日志)。 - 前端:
AttachmentCell.vue渲染附件列表(文件名+下载链接),ImageCell.vue渲染图片缩略图(点击预览)。作为 vxe-table 的自定义渲染器插槽。 - 图片懒加载:
ImageCell.vue用IntersectionObserver或 vxe-table 的虚拟滚动可见性回调——仅视口内的图片加载src,视口外的用占位图。缩略图用?thumbnail=true参数请求后端生成缩略图(或前端 CSS 缩放)。 - 字段类型:
attachment(通用文件)和image(仅图片,前端限制 accept)。
Patterns to follow:
- 文件上传:
src/agentkit/server/routes/chat.py(MAX_UPLOAD_SIZE+data/uploads/存储 + 下载端点) - 前端上传组件:
src/agentkit/server/frontend/src/components/kb/DocumentUpload.vue(a-upload-dragger) - 文件展示:
src/agentkit/server/frontend/src/components/chat/messages/FileAttachment.vue(文件类型识别)
Test scenarios:
- Happy path: 上传图片 → 记录 image 字段存为
[{filename, url, ...}]→ 前端显示缩略图 - Happy path: 上传 PDF 附件 → attachment 字段存值 → 前端显示文件名+下载链接
- Covers 附件清理: 删除含附件的记录 → 物理文件被删除 → 记录被删除
- Covers 附件清理: 删除记录时文件已丢失 → 记录仍被删除(不阻断),日志记录
- Covers 懒加载: 10k 行含图片记录 → 仅视口内图片加载,滚动流畅
- Edge case: 上传超过大小限制的文件 → 413 错误
- Edge case: image 字段上传非图片文件 → 校验拒绝
- Edge case: 一条记录的 attachment 字段上传多个文件 → 数组存储多个
- Error path: 上传时磁盘满 → 明确错误
- Integration: Agent 通过 BitableTool 写入含附件的记录 → 前端正确渲染
Verification: 图片/附件可上传、存储、预览、下载;记录删除时附件文件被清理;图片懒加载保证大表滚动流畅;字段值正确存为 JSONB 数组。
U7. CLI 子命令
Goal: 实现 agentkit bitable CLI 子命令组,供运维和脚本化操作。
Requirements: R1, R8
Dependencies: U2
Files:
src/agentkit/cli/bitable.py(新建,bitable_app = typer.Typer(...))src/agentkit/cli/main.py(修改,app.add_typer(bitable_app, name="bitable"))tests/unit/bitable/test_cli.py(新建)
Approach:
- 子命令:
list-tables、create-table、import-excel、export-excel(复用excel_renderer.py)、query。 - CLI 直接调用
BitableService(KTD5 例外:CLI 是运维工具,非运行时调用路径)。 - 参考
src/agentkit/cli/task.py的task_app模式。
Patterns to follow:
- CLI 子命令组:
src/agentkit/cli/task.py(typer.Typer+@app.command) - 注册:
src/agentkit/cli/main.py(app.add_typer) - 输出格式:Typer + Rich(项目 CLI 已用 Rich)
Test scenarios:
- Happy path:
agentkit bitable list-tables列出所有表 - Happy path:
agentkit bitable create-table --name "测试"创建表 - Happy path:
agentkit bitable import-excel --file data.xlsx --table "导入表"导入 Excel - Edge case: 表不存在时
query报错 - Error path: bitable 未初始化时 CLI 报明确错误
Verification: CLI 子命令可执行表管理和数据导入操作。
Test Infrastructure
PG Schema Fixture
所有涉及 PostgreSQL 的测试用 @pytest.mark.postgres 标记。conftest.py 提供 bitable_db fixture:
- 每个测试函数前
DROP SCHEMA bitable CASCADE+ 重新init_bitable_db(),保证隔离 - 测试后自动清理(teardown)
- PG 不可用时
pytest.skip("PostgreSQL not available"),不报错
文件:tests/unit/bitable/conftest.py(新建,bitable_db fixture + bitable_service fixture)
测试标记
| 标记 | 用途 | 命令 |
|---|---|---|
@pytest.mark.postgres |
需要 PG 的测试 | pytest -m postgres |
@pytest.mark.integration |
端到端集成测试 | pytest -m integration |
| 无标记 | 纯单元测试(公式解析、Pydantic 模型等) | pytest -m "not postgres and not integration" |
HTTP Mock 策略
BitableTool 测试(U4)不真实调用 HTTP——用 httpx.MockTransport 或 unittest.mock.patch mock bitable REST API 响应:
- Happy path:mock 返回 202 + 正常数据
- Error path:mock 返回 503 / 401 / 500
- 批量分块:mock 验证请求被正确分块
不 mock 的场景:U2 路由测试用 FastAPI TestClient 真实调用(但 service 层可 mock repository)。
工厂 Fixture
conftest.py 提供工厂函数快速创建测试数据:
make_table(name="test_table", ...)→ 创建表 + 返回 table_idmake_field(table_id, name="f1", field_type="text", owner="agent", ...)→ 创建字段 + 返回 field_idmake_record(table_id, values={"f1": "value1", ...})→ 创建记录 + 返回 record_idmake_formula_field(table_id, name="calc", formula_expr="=SUM({f1})", ...)→ 创建公式字段
并发测试
U2/U3 的并发场景(并发 upsert、并发入队)用 asyncio.gather 并发执行多个操作,验证:
- 并发 upsert 同主键不同列 → 行级锁,最终两列都更新
- 并发入队同 (record_id, field_id) → 唯一索引去重,队列中只有一个任务
异步生成器安全测试
若 recalc worker 用 async def + yield,测试验证 return; yield 模式(项目规则要求):
- worker 在队列为空时
return→ 不触发'async for' requires __aiter__错误 - worker 正常消费 → yield 每个任务
Risks & Dependencies
风险
| 风险 | 影响 | 缓解 |
|---|---|---|
| 公式引擎循环引用/跨表引用边界 case 多 | 重算错误或死循环 | DFS 循环检测 + 超时保护 + v1 限制跨表引用仅 lookup(只读引用,不形成环) |
| 公式 AST 安全约束遗漏 | 代码注入(__import__/eval) |
KTD7 白名单 AST walker + 安全测试覆盖(=__import__/=eval/=lambda 用例) |
| vxe-table 虚拟滚动 + 可编辑 + 自定义列三者集成复杂 | 前端开发周期拉长 | U5a 前先做最小原型验证(单表 + 1万行 + 编辑) |
| vxe-table 与 Ant Design Vue CSS 冲突 | 样式互相污染 | KTD10 CSS 隔离策略(.bitable-grid-scope 包裹 + token 对齐) |
| PostgreSQL 部署依赖 | 无 PG 环境无法用 bitable | lifespan 降级处理(参考 calendar 模式),CLI doctor 检查 PG 可用性 |
| Agent 通过 HTTP 调用 bitable 的延迟 | 采集写入慢 | 本地 HTTP 延迟可忽略;批量写入用批量端点 + 分块(U4 BATCH_SIZE=500) |
| JSONB 记录存储在 100k 行时的查询性能 | 分页/筛选慢 | GIN 索引 + cursor 分页(KTD9)+ v3 评估列式存储 |
| Recalc worker 崩溃后任务卡在 calculating | 公式永远不更新 | U3 崩溃恢复(启动时重置 calculating→pending)+ reaper 定时清理 |
| 内部令牌泄露 | 未授权访问 bitable API | 令牌存 agentkit.yaml(不进 git),未来升级 mTLS(KTD11 ponytail 注明) |
| Schema 迁移失败 | bitable 不可用 | U1 采用 _SCHEMA_VERSION 模式,迁移在事务中执行,失败回滚 |
依赖
- PostgreSQL 已配置且可连接(bitable 不像 calendar/documents 有 SQLite 回退)
- 现有文件上传基础设施(
data/uploads/+ 上传端点模式)可复用 - Agent 工具系统(
src/agentkit/tools/base.py)可扩展 - vxe-table npm 包可安装(MIT,无许可证风险)
System-Wide Impact
- 部署:bitable 要求 PostgreSQL(与 calendar/documents 的 SQLite 零依赖不同)。部署文档需注明 PG 是 bitable 的硬依赖。
agentkit.yaml需新增bitable.internal_token配置。 - Agent 能力:Agent 获得"结构化数据落地"能力,可通过 BitableTool 把采集结果持久化为可编辑表格。这改变了 Agent 输出的形态(从纯文本/文件 → 持久化结构化数据)。
- 前端:新增
/agent/bitable路由(全屏布局,非象限),引入 vxe-table 依赖(与现有 Ant Design Vue 并存,KTD10 CSS 隔离)。前端工作量从 1 个单元扩展到 3 个单元(U5a/U5b/U5c)。 - CLI:新增
agentkit bitable子命令组。 - 数据库:新增
bitableschema(6 张表含bitable_meta),不影响现有 schema。采用_SCHEMA_VERSION迁移机制。
Open Questions
公式列的"计算中"状态前端感知方式:已解决——U5a 采用 2s 间隔轮询,公式字段全 done/error 后停止。WebSocket 推送延后到 v2。- 数据库导入的字段类型映射完整性:v1 覆盖常见类型(int/varchar/text/timestamp/bool/decimal),复杂类型(json/array/enum)如何处理?实现时按需扩展。
- 多用户并发编辑同一记录:v1 不做多人实时协作(v3),但 Agent 写入和用户编辑可能同时发生。v1 用乐观锁(
updated_at版本号)还是 last-write-wins?倾向 last-write-wins(upsert 语义已保证用户列不被覆盖,冲突面小)。实现时确认。 - bitable 全屏路由的 layout 实现:
meta.panel = 'full'需要在 AgentLayout 中新增 full panel 类型,还是为 bitable 用独立 layout(脱离 AgentLayout)?实现时确认——倾向独立 layout(bitable 是伴生服务,UI 独立性更好)。 - 公式语法校验端点:U5b 提到
POST /bitable/fields/validate-formula端点用于前端实时校验。这个端点在 U2 还是 U3 实现?倾向 U3(公式引擎在 U3),U2 只做 CRUD。
Sources & Research
- 本地模式研究:
src/agentkit/server/app.py(路由注册 + lifespan)、src/agentkit/server/auth/models.py(SQLAlchemy 2 模式)、src/agentkit/evolution/pg_store.py(PostgreSQL 模式)、src/agentkit/cli/task.py(CLI 模式)、src/agentkit/server/frontend/src/api/calendar.ts(API 客户端模式)、src/agentkit/server/frontend/src/stores/calendar.ts(Pinia store 模式)、src/agentkit/tools/calendar_tool.py(工具实现模式) - 公式引擎选型:Hyperformula(GPLv3/商业双授权,商业需付费)、formulas(EUPL 1.1)、pycel(GPL-3.0,不推荐)、Univer(Apache-2.0,升级路径)。结论:自研避免许可证风险。
- 网格组件选型:vxe-table(MIT,Vue 3 原生,首选)、ag-grid Community(MIT,但分组/透视需 Enterprise 付费)、Handsontable(商业付费)、Univer(Apache-2.0,完整套件,备选)。结论:vxe-table。
- 公式重算算法:业界标准为"标记脏 cell → 拓扑序增量重算",Luckysheet/Univer/HyperFormula 均采用此模式。