869 lines
57 KiB
Markdown
869 lines
57 KiB
Markdown
---
|
||
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 均采用此模式。
|