fischer-agentkit/docs/plans/2026-06-24-001-feat-bitable...

869 lines
57 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 |
**成功标准**(源自需求文档 §9Agent 能把 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商业付费、pycelGPL 传染风险、formulasEUPL 边界模糊)。自研,因为 v1 函数集小10-50 个)。
**架构**`ast`/`pyparsing` 解析公式为 AST → 构建 DAG字段依赖关系→ Kahn 算法拓扑排序 → DFS 检测循环引用 → 增量重算(仅重算受影响下游)。
**重算策略**:数据列写入 → 标记依赖该列的公式列为"计算中" → 异步队列按拓扑序重算 → 结果写回记录 JSONB → 状态置"完成"。
`ponytail:` 自研引擎的 O(V+E) 拓扑重算在万级公式单元格下足够;若未来公式量到十万级或需 Excel 100% 兼容,升级路径为迁移到 Univer 引擎Apache-2.0,免费商用)。
### KTD4: 网格视图组件——vxe-tableMIT
不选 Handsontable商业付费、ag-grid Enterprise付费功能、a-table 裸用10k+ 行无虚拟滚动)。选 vxe-tableVue 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_setO(字段数) 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``<style scoped>` 不可行vxe-table 用全局类),改为在 `main.ts``import 'vxe-table/lib/style.css'` 且**只在 bitable 路由组件挂载时确保已加载**
2. bitable 网格容器用 `.bitable-grid-scope` 包裹vxe-table 的样式覆盖限定在该 scope 下(`.bitable-grid-scope .vxe-table { ... }`
3. 字体/颜色变量对齐 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
### 组件架构
```mermaid
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
```mermaid
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
}
```
### 公式异步重算流程
```mermaid
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 schema5 张表(`bitable_tables`、`bitable_fields`、`bitable_records`、`bitable_views`、`bitable_recalc_queue`+ 1 张元数据表(`bitable_meta`),置于独立 schema `bitable`。`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``config` JSONB 在不同 field_type 下结构正确formula 类型有 `formula_expr`lookup 类型有 `lookup_target`select 类型有 `options`
- Edge case: `Record.values` JSONB 为空 `{}` 时合法(新记录无值)
- 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 `->>` 返回 textnumber/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 列值,再 upsertuser 列值不变。验证 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.py` lifespan 中 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 APIKTD5即使共部署也走 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。
- **批量分块**`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.py` lifespan`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 风格statetables、currentTable、fields、records、views、loading、recalcPendingCountactionsloadTables、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 中 `pollRecalcStatus` action——当记录中存在 `__status: "calculating"` 的公式字段时,每 2s 轮询 `GET /tables/{id}/records?fields=calculating_only`。所有公式字段变为 `done`/`error` 后停止轮询。切换表或组件卸载时清理定时器。
- **主视图**`BitableView.vue`):左侧表列表 + 右侧网格。**全屏布局**(非 AgentLayout 的 55% 象限bitable 需要完整宽度展示网格。
- **路由**`/agent` children 中添加 `{ 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``/agent` children + `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: 后端 503bitable 未初始化)→ 显示降级提示
- 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 + valueop 选项按字段类型动态变化
- 排序:字段选择 + 升序/降序
- 隐藏字段:字段列表 checkbox勾选隐藏
- **FilterBuilder**`FilterBuilder.vue`):条件列表,每行 `a-select`(字段)+ `a-select`(操作符)+ `a-input`/`a-date-picker`。op 选项text→eq/ne/contains/is_emptynumber→eq/ne/gt/lt/gte/ltedate→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 pathmock 返回 202 + 正常数据
- Error pathmock 返回 503 / 401 / 500
- 批量分块mock 验证请求被正确分块
**不 mock 的场景**U2 路由测试用 FastAPI `TestClient` 真实调用(但 service 层可 mock repository
### 工厂 Fixture
`conftest.py` 提供工厂函数快速创建测试数据:
- `make_table(name="test_table", ...)` → 创建表 + 返回 table_id
- `make_field(table_id, name="f1", field_type="text", owner="agent", ...)` → 创建字段 + 返回 field_id
- `make_record(table_id, values={"f1": "value1", ...})` → 创建记录 + 返回 record_id
- `make_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未来升级 mTLSKTD11 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` 子命令组。
- **数据库**:新增 `bitable` schema6 张表含 `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-winsupsert 语义已保证用户列不被覆盖,冲突面小)。实现时确认。
- **bitable 全屏路由的 layout 实现**`meta.panel = 'full'` 需要在 AgentLayout 中新增 full panel 类型,还是为 bitable 用独立 layout脱离 AgentLayout实现时确认——倾向独立 layoutbitable 是伴生服务UI 独立性更好)。
- **公式语法校验端点**U5b 提到 `POST /bitable/fields/validate-formula` 端点用于前端实时校验。这个端点在 U2 还是 U3 实现?倾向 U3公式引擎在 U3U2 只做 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`(工具实现模式)
- **公式引擎选型**HyperformulaGPLv3/商业双授权商业需付费、formulasEUPL 1.1、pycelGPL-3.0不推荐、UniverApache-2.0,升级路径)。结论:自研避免许可证风险。
- **网格组件选型**vxe-tableMITVue 3 原生首选、ag-grid CommunityMIT但分组/透视需 Enterprise 付费、Handsontable商业付费、UniverApache-2.0完整套件备选。结论vxe-table。
- **公式重算算法**:业界标准为"标记脏 cell → 拓扑序增量重算"Luckysheet/Univer/HyperFormula 均采用此模式。