---
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` 的 `<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 schema：5 张表（`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 `->>` 返回 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.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 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。
- **批量分块**：`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 风格，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 中 `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: 后端 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_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），未来升级 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` 子命令组。
- **数据库**：新增 `bitable` schema（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 均采用此模式。
