feat(db): ai_messages 表 + 迁移——作者↔AI 往复聊天记录 append-only 侧表(AC-1)

计划 §2 canonical DDL:AiMessage(UuidPk, CreatedAt, Base),project_id(FK CASCADE)/
chapter_no(nullable=项目级)/thread_id/seq(repo 批内定序)/kind/tool_key/role(CHECK)/
content/meta(JSONB '{}'),append-only 无 updated_at。复合索引 (project_id,chapter_no,
created_at DESC) 手写 sa.text DESC + FK 单列索引。迁移 ceabd3c2c00d(down=f6a7b8c9d0e1),
autogenerate 仅捕获 ai_messages 无杂散漂移。T0.2 后第一张新业务表;旁路侧记录非手稿真源,
assemble()/agent 永不读(守 #1/#3/#6)。契约 C2 扩已「稳定」。
This commit is contained in:
Yaojia Wang
2026-07-09 16:39:24 +02:00
parent 9da938d39b
commit 1a188e6e5b
4 changed files with 118 additions and 1 deletions

View File

@@ -15,7 +15,7 @@
| 任务 | 状态 | 负责 | 依赖 | 备注 | | 任务 | 状态 | 负责 | 依赖 | 备注 |
|---|---|---|---|---| |---|---|---|---|---|
| AC-1 `ai_messages` 表 + 迁移 | | @db | — | 计划 §2 canonical DDL | | AC-1 `ai_messages` 表 + 迁移 | @db 2026-07-09 | @db | — | 计划 §2 canonical DDL;迁移 `ceabd3c2c00d`(down=`f6a7b8c9d0e1`)C2 扩已「稳定」AC-2 可动工 |
| AC-2 repo + append/list 端点 + schema + 契约 | ⬜ | @backend | AC-1 | 计划 §3批级 append 信封、newest-first 分页 | | AC-2 repo + append/list 端点 + schema + 契约 | ⬜ | @backend | AC-1 | 计划 §3批级 append 信封、newest-first 分页 |
| AC-3 gen:api + AiConversationDrawer + useAiConversation + 接线 5 处交互 | ⬜ | @frontend | AC-2 | 计划 §4本章对话 + 项目级生成两段 | | AC-3 gen:api + AiConversationDrawer + useAiConversation + 接线 5 处交互 | ⬜ | @frontend | AC-2 | 计划 §4本章对话 + 项目级生成两段 |
| AC-4 E2E + repo/端点/hook 测试 | ⬜ | @qa | AC-3 | 计划 §6 | | AC-4 E2E + repo/端点/hook 测试 | ⬜ | @qa | AC-3 | 计划 §6 |
@@ -208,6 +208,7 @@ T0.1 monorepo 骨架 ✅ @devops · T0.2 16 MVP 表迁移无漂移users st
> 格式:`- [YYYY-MM-DD] @skill 完成/进展 Txx — 一句话结果 + 影响的契约/文件` > 格式:`- [YYYY-MM-DD] @skill 完成/进展 Txx — 一句话结果 + 影响的契约/文件`
- [2026-07-09] @db**✅ AC-1 `ai_messages` 表 + 迁移**(计划 §2 canonical DDL`packages/db/ww_db/models.py``AiMessage(UuidPk, CreatedAt, Base)`(补 `Uuid` 导入):`project_id`(FK projects CASCADE, NOT NULL, indexed)/`chapter_no`(Integer nullable=项目级)/`thread_id`(Uuid NOT NULL)/`seq`(Integer NOT NULL, repo 批内 0 基定序)/`kind`(Text 无 CHECK)/`tool_key`(Text nullable)/`role`(Text + `ck_ai_messages_role` CHECK author|ai)/`content`(Text NOT NULL 全文)/`meta`(JSONB NOT NULL server_default `'{}'`)/`id`+`created_at`(mixin)append-only 无 updated_at。复合索引 `ix_ai_messages_project_chapter=(project_id,chapter_no,created_at DESC)` 手写 `sa.text` DESC + FK 单列索引。迁移 `ceabd3c2c00d`down=`f6a7b8c9d0e1`**autogenerate 仅捕获 ai_messages 无杂散漂移**DESC 表达式归一为 `sa.text`)。**T0.2 MVP schema 后第一张新业务表**——对话历史是 18 表都无的能力、正交于 review/job/手稿,属真新增(非重复建表);是**旁路侧记录非手稿真源**`assemble()`/agent 永不读(守 #1/#3/#6,围栏留 AC-4/P4 收口)。**门禁全绿**ruff/format 干净 · mypy **233 文件** · `alembic upgrade head` + **`alembic check` 无漂移** + **downgrade→upgrade 往复干净** · pytest **971 passed**(基线不变,无代码用新表)。契约 **C2 扩AC-1已登记「稳定」** → AC-2 @backend 可动工。
- [2026-07-08] @devops**✅ QA-CR CR-D1 抬高后端依赖下界至 lockfile 主次版本(保可复现)**。8 个 pyproject`apps/api` + `packages/{core,agents,config,db,llm_gateway,shared,skills}`)的后端运行时依赖下界从过旧值抬到实锁 MAJOR.MINOR`langgraph>=1.2`/`langgraph-checkpoint-postgres>=3.1`/`openai>=2.43`/`anthropic>=0.111`/`google-genai>=2.9`/`instructor>=1.15`/`tenacity>=9.1`/`pydantic>=2.13`/`pydantic-settings>=2.14`/`structlog>=26.1`/`asyncpg>=0.31`/`alembic>=1.18`/`fastapi>=0.137`/`uvicorn>=0.49`/`cryptography>=49`(每条 ≤ lockfile 实锁,同小版本线的 sqlalchemy/psycopg 留原样)。`uv.lock` 变动为**纯元数据刷新**——仅各 workspace 成员 `[package.metadata].requires-dist` 的 specifier 字符串随下界更新,**零 pinned `version=` 变动、零 re-resolve、零第三方版本 bump**。root dev 工具下界pytest/ruff/mypy/httpx+ 前端 vitest 3.x/tailwind 4.x 升级评估**明确不纳入**(非运行时可复现关键,超范围)。**门禁**`uv lock --check` 绿(元数据刷新后一致)· `uv run ruff check .` 干净 · pytest **971 passed**(与基线一致,无回归)。无源码/测试改动。 - [2026-07-08] @devops**✅ QA-CR CR-D1 抬高后端依赖下界至 lockfile 主次版本(保可复现)**。8 个 pyproject`apps/api` + `packages/{core,agents,config,db,llm_gateway,shared,skills}`)的后端运行时依赖下界从过旧值抬到实锁 MAJOR.MINOR`langgraph>=1.2`/`langgraph-checkpoint-postgres>=3.1`/`openai>=2.43`/`anthropic>=0.111`/`google-genai>=2.9`/`instructor>=1.15`/`tenacity>=9.1`/`pydantic>=2.13`/`pydantic-settings>=2.14`/`structlog>=26.1`/`asyncpg>=0.31`/`alembic>=1.18`/`fastapi>=0.137`/`uvicorn>=0.49`/`cryptography>=49`(每条 ≤ lockfile 实锁,同小版本线的 sqlalchemy/psycopg 留原样)。`uv.lock` 变动为**纯元数据刷新**——仅各 workspace 成员 `[package.metadata].requires-dist` 的 specifier 字符串随下界更新,**零 pinned `version=` 变动、零 re-resolve、零第三方版本 bump**。root dev 工具下界pytest/ruff/mypy/httpx+ 前端 vitest 3.x/tailwind 4.x 升级评估**明确不纳入**(非运行时可复现关键,超范围)。**门禁**`uv lock --check` 绿(元数据刷新后一致)· `uv run ruff check .` 干净 · pytest **971 passed**(与基线一致,无回归)。无源码/测试改动。
- [2026-07-08] @llm**✅ QA-CR MEDIUM/LOW 网关+链整改批CR-M2/M3/L1@llm/M1.4TDD 逐项提交)**。**完成 6**`32451a9` pricing 未知 model 发 `pricing_unknown_model` warning成本仍 0`9439b97` 三 adapter 瞬时错误消息经 `provider_error_summary(exc)` 只留类名+status不泄 vendor 响应体);`832ab7d` `OpenAICompatAdapter.probe_connection()` 公开探测 + provider_deps 弃私有 `_client` 反手CR-M1.4`c9ffada` 续写链前一章无已验收终稿发 `chain_prior_accepted_missing` warningCR-L1`ec91c8f` 链 runner 改用公开 `graph.aget_state(config).interrupts` 判暂停(弃私有 `__interrupt__``6a739ca` review 节点五审 `asyncio.gather` 并行扇出(复用主图 add-only 记账并发范式 T3.8collect 一次落库)。**SKIP 2**CR-M2 `is_open()` CQS惰性半开转移是唯一转半开点、拆分漏发命令会致熔断永不复位风险>收益CR-M3 `run_overdue_scan` 幂等(`scan_overdue` 本已是原子 `UPDATE...RETURNING`+`NOT IN[CLOSED,OVERDUE]`+commit-only-if-changed且下一章 current+1 扫描自愈漏标,再动会触碰验收事务边界)。**门禁全绿**:后端 ruff/format 干净 · mypy **232 文件** · pytest **971 passed**+11 新测pricing 2 / 错误脱敏 4 / probe 2 / 续写缺前文告警 2 / 并行审 1· 无 DDL/无迁移。守不变量 #1/#3/#4(熔断/验收事务/add-only 记账 sink 未动)。 - [2026-07-08] @llm**✅ QA-CR MEDIUM/LOW 网关+链整改批CR-M2/M3/L1@llm/M1.4TDD 逐项提交)**。**完成 6**`32451a9` pricing 未知 model 发 `pricing_unknown_model` warning成本仍 0`9439b97` 三 adapter 瞬时错误消息经 `provider_error_summary(exc)` 只留类名+status不泄 vendor 响应体);`832ab7d` `OpenAICompatAdapter.probe_connection()` 公开探测 + provider_deps 弃私有 `_client` 反手CR-M1.4`c9ffada` 续写链前一章无已验收终稿发 `chain_prior_accepted_missing` warningCR-L1`ec91c8f` 链 runner 改用公开 `graph.aget_state(config).interrupts` 判暂停(弃私有 `__interrupt__``6a739ca` review 节点五审 `asyncio.gather` 并行扇出(复用主图 add-only 记账并发范式 T3.8collect 一次落库)。**SKIP 2**CR-M2 `is_open()` CQS惰性半开转移是唯一转半开点、拆分漏发命令会致熔断永不复位风险>收益CR-M3 `run_overdue_scan` 幂等(`scan_overdue` 本已是原子 `UPDATE...RETURNING`+`NOT IN[CLOSED,OVERDUE]`+commit-only-if-changed且下一章 current+1 扫描自愈漏标,再动会触碰验收事务边界)。**门禁全绿**:后端 ruff/format 干净 · mypy **232 文件** · pytest **971 passed**+11 新测pricing 2 / 错误脱敏 4 / probe 2 / 续写缺前文告警 2 / 并行审 1· 无 DDL/无迁移。守不变量 #1/#3/#4(熔断/验收事务/add-only 记账 sink 未动)。
- [2026-07-08] @llm/@backend/@devops/@frontend**✅ QA-CR CRITICAL + HIGH 整改批全部落地**14 项 + 4 低危收尾)。多 agent 工作流:先 14 个只读 agent 逐项**扎根真实代码**产精确修复+测试规格(纠出 3 处台账前提漂移),再按 owner 四簇**串行整合**入单工作树(避免并发写树 + repo 级 `ruff format .` 竞态),各 TDD 红→绿、逐项补回归测试、逐 fix 小步提交,末尾 14 个 skeptic **对抗式复核**(实证每条 test 非 vacuous0 problem。**关键落地/纠偏**C1 next 15.1.3→**15.5.20**+react **19.2.7**`pnpm audit` 清、`dependencyFloors.ts` 下界守卫,**CVE 号无法对真库核验+密钥轮换为用户运维事项**C2 **前提证伪**langgraph 默认上限 10007 非 25无实活崩溃仍加显式顶层 `recursion_limit`H4 request_id **条件透传**(否则覆盖 contextvars 反退化H2 修请求级 httpx 泄漏时**揪出 E2E 会误触真网**override 跟随修H9 全文本字段上限+2 MiB body 中间件 413 兜底(契约 C3`gen:api` 无漂移H8 `.env.example` 换不可用占位符(**真 key 仍在 git 历史,曾用环境须轮换**)。**门禁全绿**:后端 mypy 229/pytest **934**/alembic 无漂移;前端 vitest **669**/build/coverage 95.41%。契约 C1-扩LlmRequest.request_id+ C3H9 caps。**MEDIUM/LOW 择批后清理。** 提交 `cb8fddf``dd6d2fc`19 提交)。 - [2026-07-08] @llm/@backend/@devops/@frontend**✅ QA-CR CRITICAL + HIGH 整改批全部落地**14 项 + 4 低危收尾)。多 agent 工作流:先 14 个只读 agent 逐项**扎根真实代码**产精确修复+测试规格(纠出 3 处台账前提漂移),再按 owner 四簇**串行整合**入单工作树(避免并发写树 + repo 级 `ruff format .` 竞态),各 TDD 红→绿、逐项补回归测试、逐 fix 小步提交,末尾 14 个 skeptic **对抗式复核**(实证每条 test 非 vacuous0 problem。**关键落地/纠偏**C1 next 15.1.3→**15.5.20**+react **19.2.7**`pnpm audit` 清、`dependencyFloors.ts` 下界守卫,**CVE 号无法对真库核验+密钥轮换为用户运维事项**C2 **前提证伪**langgraph 默认上限 10007 非 25无实活崩溃仍加显式顶层 `recursion_limit`H4 request_id **条件透传**(否则覆盖 contextvars 反退化H2 修请求级 httpx 泄漏时**揪出 E2E 会误触真网**override 跟随修H9 全文本字段上限+2 MiB body 中间件 413 兜底(契约 C3`gen:api` 无漂移H8 `.env.example` 换不可用占位符(**真 key 仍在 git 历史,曾用环境须轮换**)。**门禁全绿**:后端 mypy 229/pytest **934**/alembic 无漂移;前端 vitest **669**/build/coverage 95.41%。契约 C1-扩LlmRequest.request_id+ C3H9 caps。**MEDIUM/LOW 择批后清理。** 提交 `cb8fddf``dd6d2fc`19 提交)。

View File

@@ -80,6 +80,23 @@
- **迁移**`packages/db/migrations/versions/1f011c42bd4d_provider_credentials_oauth_columns.py`down_revision=`220ca2e3d53f`。up: add_column auth_type/oauth_enc + alter_column api_key_enc→nullabledown 逆向。`alembic check` 无漂移。 - **迁移**`packages/db/migrations/versions/1f011c42bd4d_provider_credentials_oauth_columns.py`down_revision=`220ca2e3d53f`。up: add_column auth_type/oauth_enc + alter_column api_key_enc→nullabledown 逆向。`alembic check` 无漂移。
- **⚠️ K1.3 @backend 必读**`api_key_enc` 现为 `bytes | None``apps/api/ww_api/services/credentials.py``StoredCredential.api_key_enc: bytes`dataclass 字段 line 28与 store 的 `r.api_key_enc` 赋值line 75/97mypy 报 `arg-type``bytes | None``bytes`)。**K1.1 不动 @backend 文件(目录所有权)**K1.3 接 OAuth store 时须把 `StoredCredential.api_key_enc` 改成 `bytes | None`(并加 `auth_type`/`oauth_enc` 字段/读写路径),届时 mypy 即绿。当前全仓 mypy 唯余此 2 处错(都在 credentials.py其余门禁绿ruff/format/alembic check/pytest 400 passed - **⚠️ K1.3 @backend 必读**`api_key_enc` 现为 `bytes | None``apps/api/ww_api/services/credentials.py``StoredCredential.api_key_enc: bytes`dataclass 字段 line 28与 store 的 `r.api_key_enc` 赋值line 75/97mypy 报 `arg-type``bytes | None``bytes`)。**K1.1 不动 @backend 文件(目录所有权)**K1.3 接 OAuth store 时须把 `StoredCredential.api_key_enc` 改成 `bytes | None`(并加 `auth_type`/`oauth_enc` 字段/读写路径),届时 mypy 即绿。当前全仓 mypy 唯余此 2 处错(都在 credentials.py其余门禁绿ruff/format/alembic check/pytest 400 passed
### C2 扩展AC-1, 2026-07-09· `ai_messages` 表——作者↔AI 往复聊天记录 append-only 侧表 owner @db 状态: 稳定
- 背景:`docs/design/ai-chat-history-plan.md §2`。持久化 refine/rewrite/clarify/continue/generator 五类往复交换供每章「AI 对话」抽屉重放。**T0.2 MVP schema 后第一张新业务表**(对话历史是 18 张表都无的能力,正交于 review/job/手稿,塞进 `jobs.result`/`chapter_reviews` 会污染其真源语义)。消费方:`@backend`(AC-2 repo+router据本条建勿自拟 DDL)、`@frontend`(AC-3 经 gen:api 消费码生成名)。
- **侧记录语义(守 #1/#3/#6AC-2/AC-3 必读)**`ai_messages` 是「说了什么」的真源,**不是手稿真源**。`assemble()` 与任何 agent/orchestrator 节点**永不读它**——不喂 prompt、不改正文、无记忆注入(#6)/裁决(#3/#4)权威。**append-only**:无 `updated_at`、无 PATCH/PUT、v1 不删。
- **模型(`packages/db/ww_db/models.py` `AiMessage``UuidPk, CreatedAt, Base`**——列与类型snake_case即 OpenAPI/码生成表面):
- `id` UuidPk / `created_at` TIMESTAMPTZ(`func.now()`)——来自 mixin。
- `project_id: uuid.UUID` FK `projects.id ON DELETE CASCADE`, NOT NULL, `index=True`(→ `ix_ai_messages_project_id`)。
- `chapter_no: int | None` `Integer` **nullable****NULL = 项目级/工具箱生成**;有意偏离 `chapter_reviews` 等 bare-int 的 NOT NULL 先例)。**无 FK**:改章号/删章会孤儿化,同 `chapter_reviews`**属设计,勿加 cascade「修」**。
- `thread_id: uuid.UUID` `Uuid` NOT NULL客户端每次开面板 `crypto.randomUUID()` 生成,归组一次多轮交换;无 threads 表)。
- `seq: int` `Integer` NOT NULL**repo 赋值 = append 批内 0 基位置**(无 Identity/无客户端值/无 server_default/无 UNIQUE。**Postgres `now()` 是事务起始时刻→同批共享 `created_at`,靠 `seq` 批内定序**;跨批 `created_at` 不同自然定序。定序键统一 `(created_at, seq)`——DB list 与前端展示都不许单靠 `created_at`
- `kind: str` `Text` NOT NULL**无 CHECK**(同 `jobs.kind`/`characters.role` 自由 Text边界由 Pydantic `Literal["refine","rewrite","clarify","continue","generator"]` 校验;加新 kind 零迁移)。
- `tool_key: str | None` `Text`continue→`"continue"`generator→其 keyrefine/rewrite/clarify→NULL`prompt_templates.tool_key` 无 FK/CHECK
- `role: str` `Text` NOT NULL + `CheckConstraint("role IN ('author','ai')", name="ck_ai_messages_role")`二元封闭枚举CHECK 兜底防御)。
- `content: str` `Text` NOT NULL人读 bubble 全文clarify ai 行存问题文本,结构化选项进 meta被拒候选无 chapters 行,须存全文不截断)。
- `meta: dict[str, Any]` `JSONB` NOT NULL `server_default '{}'`per-kind 形状见 §3.4AC-2 登记 C3 时随端点定;长文进 content 不进 meta
- **索引**(1) `ix_ai_messages_project_id`FK 单列,`index=True`)。(2) `ix_ai_messages_project_chapter = (project_id, chapter_no, created_at DESC)`——**迁移里手写 `sa.text("created_at DESC")`**autogenerate 无法可靠还原 DESC沿用 `chapter_reviews` 证明可过 `alembic check` 的形态)。服务抽屉 union 查询 + newest-first。**无 UNIQUE**。
- **迁移**`packages/db/migrations/versions/ceabd3c2c00d_add_ai_messages_table_chat_history.py`**revision=`ceabd3c2c00d`, down_revision=`f6a7b8c9d0e1`**)。仅建 `ai_messages` + 两索引 + role CHECK + FK CASCADE无其它漂移。DoD 全绿:`alembic upgrade head`**`alembic check` 无漂移** → **downgrade→upgrade 往复干净** → ruff/format/mypy(233 files)/pytest(**971 passed**) 绿。
## C3 · API / OpenAPI 端点 owner @backend 状态: 稳定M1 端点全落T1.7 settings + T1.4 projects/chapters, 2026-06-18 ## C3 · API / OpenAPI 端点 owner @backend 状态: 稳定M1 端点全落T1.7 settings + T1.4 projects/chapters, 2026-06-18
- 来源:`ARCHITECTURE.md §7.2` 端点清单(章节端点统一 `/projects/:id/chapters/:no/...`;含 `PUT .../draft`(自动保存)、`/refine``/jobs/:id``/reviews`)。 - 来源:`ARCHITECTURE.md §7.2` 端点清单(章节端点统一 `/projects/:id/chapters/:no/...`;含 `PUT .../draft`(自动保存)、`/refine``/jobs/:id``/reviews`)。
- 消费方:`@frontend`(经 OpenAPI→TS 客户端)。**改端点/字段 → 前端必须 `pnpm gen:api` 重生成客户端。** - 消费方:`@frontend`(经 OpenAPI→TS 客户端)。**改端点/字段 → 前端必须 `pnpm gen:api` 重生成客户端。**

View File

@@ -0,0 +1,66 @@
"""add ai_messages table (chat history)
作者↔AI 往复留痕 append-only 侧表AC-1——非手稿真源永不进 assemble()/agent 生成链。
复合索引 ix_ai_messages_project_chapter 手写 created_at DESC 表达式(沿用 chapter_reviews
证明可过 alembic check 的形态autogenerate 无法可靠还原 DESC。role CHECK 兜底二元枚举。
Revision ID: ceabd3c2c00d
Revises: f6a7b8c9d0e1
Create Date: 2026-07-09 16:35:14.948502
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "ceabd3c2c00d"
down_revision: str | None = "f6a7b8c9d0e1"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"ai_messages",
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("chapter_no", sa.Integer(), nullable=True),
sa.Column("thread_id", sa.Uuid(), nullable=False),
sa.Column("seq", sa.Integer(), nullable=False),
sa.Column("kind", sa.Text(), nullable=False),
sa.Column("tool_key", sa.Text(), nullable=True),
sa.Column("role", sa.Text(), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column(
"meta",
postgresql.JSONB(astext_type=sa.Text()),
server_default=sa.text("'{}'"),
nullable=False,
),
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.CheckConstraint("role IN ('author', 'ai')", name="ck_ai_messages_role"),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_ai_messages_project_chapter",
"ai_messages",
["project_id", "chapter_no", sa.text("created_at DESC")],
unique=False,
)
op.create_index(op.f("ix_ai_messages_project_id"), "ai_messages", ["project_id"], unique=False)
def downgrade() -> None:
op.drop_index(op.f("ix_ai_messages_project_id"), table_name="ai_messages")
op.drop_index("ix_ai_messages_project_chapter", table_name="ai_messages")
op.drop_table("ai_messages")

View File

@@ -19,6 +19,7 @@ from sqlalchemy import (
LargeBinary, LargeBinary,
Text, Text,
UniqueConstraint, UniqueConstraint,
Uuid,
text, text,
) )
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
@@ -231,6 +232,38 @@ class ChapterReview(UuidPk, CreatedAt, Base):
decisions: Mapped[dict[str, Any] | None] = mapped_column(JSONB) decisions: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
class AiMessage(UuidPk, CreatedAt, Base):
"""作者↔AI 往复留痕append-only 辅助侧记录,非手稿真源)。
不喂 prompt、不改正文、永不被 assemble()/agent 读回生成链(守 #1/#3/#6
一行一条 bubble一次多轮交换用 thread_id 归组;同一 append 批次内 created_at
相同,靠 seq(批内 0 基位置) 稳定定序。仅 CreatedAt无 updated_at不可改写
"""
__tablename__ = "ai_messages"
__table_args__ = (
CheckConstraint("role IN ('author', 'ai')", name="ck_ai_messages_role"),
Index(
"ix_ai_messages_project_chapter",
"project_id",
"chapter_no",
text("created_at DESC"),
),
)
project_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
chapter_no: Mapped[int | None] = mapped_column(Integer) # NULL = 项目级(工具箱)
thread_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) # 客户端生成,归组一次交换
seq: Mapped[int] = mapped_column(Integer, nullable=False) # repo 赋值:批内 0 基位置
kind: Mapped[str] = mapped_column(Text, nullable=False) # 自由 Text边界 Literal 校验
tool_key: Mapped[str | None] = mapped_column(Text) # generator/continue其余 NULL
role: Mapped[str] = mapped_column(Text, nullable=False) # author | ai (有 CHECK)
content: Mapped[str] = mapped_column(Text, nullable=False) # 人读 bubble 文本(完整)
meta: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, server_default=text("'{}'"))
# id, created_at 来自 UuidPk / CreatedAt (TIMESTAMPTZ func.now())
# ---- 运营/系统表(续)---- # ---- 运营/系统表(续)----