# 网文创作工作流 · 工程架构文档（Architecture）

> 把 [PRODUCT_SPEC.md](./PRODUCT_SPEC.md)（产品规格）下沉到**可实现的技术架构**：组件划分、接口契约、完整 DDL、LLM 网关内部设计、编排引擎、API 清单、横切关注点、部署与测试。
> UX/UI 见 [UX_SPEC.md](./UX_SPEC.md)。本文按 PRODUCT_SPEC 章节顺序逐节展开，每节末标注回溯锚点 `← PRODUCT_SPEC §x`。

---

## 0. 文档说明

| 项 | 内容 |
|---|---|
| 文档类型 | 软件架构文档（SAD），面向工程实现 |
| 目标读者 | 后端/前端工程师、技术负责人、SRE |
| 上游 | `PRODUCT_SPEC.md`（产品规格）、`UX_SPEC.md`（界面规格） |
| 范围 | 技术架构与设计；**不含**业务代码与脚手架（属 M1 实现阶段） |
| 状态 | 设计定稿待评审；评审通过后据此搭 M1 骨架 |
| 关键约束 | 提供商中立（不绑定单一 LLM 厂商）；记忆即真相源；AI 不黑箱（产出经验收 gate） |

**与上游的关系**：PRODUCT_SPEC 回答"做什么/为什么"，本文回答"怎么实现"。本文不重复产品论证，只在每节顶部用一句话点出对应的产品意图，随后给技术方案。如展开中发现规格需调整，集中列于 [§12 后 · 待回写规格的发现](#待回写规格的发现)，不擅自改上游。

---

## 1. 架构总览　← PRODUCT_SPEC §1 / §1.5 / §2

### 1.1 质量属性（NFR）与架构驱动

产品的四大痛点 + 工程哲学，转译为可度量的架构驱动力：

| 质量属性 | 目标 | 架构手段 |
|---|---|---|
| **一致性**（首要） | 几十万字不崩设定/不忘伏笔 | 记忆即真相源 + 每章事实摘要 + 写前检索 + 四审 gate |
| **可扩展（提供商）** | 任意接入/切换 LLM 厂商 | LLM 网关 + 适配器 + 能力档位抽象 |
| **可扩展（能力）** | 新增 Agent/题材模板零侵入 | 声明式 Skill + registry + 表权限契约 |
| **成本可控** | 长记忆注入不烧钱 | 前缀缓存抽象 + **确定性按需注入**（显式+近况，无向量） + 档位分流 |
| **可用性/韧性** | 单厂商故障不致瘫 | 故障回退链 + 重试/熔断 + 能力降级 |
| **安全/隔离** | 密钥与作品数据隔离 | 密钥托管 + 用户 Skill 沙箱（**原型单用户**；多租户行级隔离为后续，见 §9.1） |
| **可掌控（产品约束）** | AI 改动经作者裁决 | 四审只读 + 验收事务写回 |
| **可观测** | 每次 LLM 调用可追踪可计费 | 结构化日志 + LLM trace + 按 provider+model 记账 |

### 1.2 架构风格与关键决策（ADR 摘要）

| # | 决策 | 选择 | 理由 / 取舍 |
|---|---|---|---|
| ADR-1 | 总体形态 | 前后端分离：**后端为模块化单体**（Modular Monolith）起步 | 前端 Next、后端 FastAPI 两个可部署单元；后端内部编排器/网关/记忆为清晰模块、预留拆服务接缝，避免过早微服务化。 |
| ADR-2 | 真相源 | **数据库为唯一真相源**，Agent 经库交换 | 对齐 PRODUCT_SPEC §5.4；Agent 间零直连，降耦合、可独立替换。 |
| ADR-3 | LLM 接入 | **自建网关 + 适配器**，直连各家 API | 对齐 §3.4；提供商中立、确定性编排；不用厂商 CLI/托管运行时（锁定单厂商）。 |
| ADR-4 | Agent 形态 | **声明式 Skill = 单一职责 Agent**；用 **LangGraph** 做确定性编排 | 对齐 §5；非自治 agent loop，编排为固定图（节点/并行/验收 HITL 中断/checkpoint），可测、可回放。 |
| ADR-5 | 写章模型 | **章 = 纯函数 f(大纲, 选择的状态, 文风)**，状态变更集中在验收 | 可缓存、可重放；副作用隔离到验收事务。 |
| ADR-6 | 检索 | **Postgres 单库 + 确定性记忆选择**（无向量） | 原型设定集不大，"本章有谁"大纲已点名，确定性选择（显式+主角+近况）足够且可调试；**向量检索降为 P2**，规模化再在同一接口后接入。 |
| ADR-7 | 结构化输出 | **网关统一封装**，原生优先、JSON+校验兜底 | 各家能力参差（§3.3 能力协商），上层无感。 |
| ADR-8 | 前后端 | **前端 Next.js（TS）+ 后端 FastAPI（Python）分离** | 前端 SSR/流式友好；后端 Python 是 LLM 编排/工具的强生态；契约经 OpenAPI→TS 代码生成。 |
| ADR-9 | 长任务 | **FastAPI BackgroundTasks + `jobs` 表**（原型不上专门队列） | 学文风/全书扫描走后台任务 + 状态表；写章/审稿走 SSE 即时反馈。规模化再上 arq/Celery。 |
| ADR-10 | 编排实现 | **LangGraph（Python）** | 写章→四审→验收天然是跨请求的暂停-恢复（HITL）；LangGraph 的并行/中断/checkpoint 正合，且 Python 版成熟。 |

### 1.3 系统上下文图（C4 L1）

```
          ┌───────────────┐
          │   网文作者     │ 浏览器
          └──────┬────────┘
                 │ HTTPS
        ┌────────▼─────────────────────────────┐
        │      网文创作工作流（本系统）           │
        │  立项·设定·大纲·写章·四审·验收·伏笔      │
        └───┬──────────────────────────────────┘
            │ 各家 API(经网关)
   ┌────────▼────────┐
   │  LLM 提供商      │
   │ Claude/DeepSeek │
   │ Kimi/GPT/GLM/…  │
   └─────────────────┘
```

外部依赖：①LLM 提供商（多家，经网关）；②对象存储（可选，存文风样本原文/导出稿）。
> 注：原型**不依赖 Embedding 服务**（砍向量检索，改确定性记忆选择，§3.4）；向量检索为 P2，届时再引入嵌入服务。

### 1.4 容器图（C4 L2）

```
┌──── 前端 Next.js(TS) ────┐         ┌──────────── 后端 FastAPI(Python) ────────────┐
│ 作品库 立项 工作台 审稿  │  fetch  │                                              │
│ 设定库 伏笔看板 设置页   │ ─/SSE─▶ │ API 层 ─▶ 编排器(LangGraph) ─▶ LLM 网关       │
└──────────────────────────┘         │   │           │                  │ ┌────────┐│
   OpenAPI→TS 客户端类型              │   │           ▼              ├─▶│OpenAI兼容││ DeepSeek…
                                      │   │     记忆服务(选择/注入/写回) ├─▶│Anthropic ││
                                      │   ▼           │                └─▶│Gemini    ││
                                      │ BackgroundTasks  数据访问层(Repo)  密钥管理   │
                                      └────┬───────────────┬───────────────┬─────────┘
                                           │(jobs)         │               │
                                    ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
                                    │ jobs 表(同库)│ │ Postgres    │ │ 密钥存储     │
                                    │             │ │ (真相源,无向量)│ │ (KMS/加密列) │
                                    └─────────────┘ └─────────────┘ └─────────────┘
```

**容器职责**
- **前端（Next.js/TS）**：UX_SPEC 的页面与交互；流式渲染、冲突标注、乐观更新；经 OpenAPI 生成的 TS 客户端调后端。
- **后端（FastAPI/Python）**：API 层（入参校验、SSE）、编排器（LangGraph）、网关、记忆服务同进程。
- **编排器（LangGraph）**：确定性图串联 Agent（写章→四审→验收 HITL），并行四审、验收中断/恢复。
- **记忆服务**：写前**确定性选择**（显式+主角+近况，无向量）、prompt 组装（缓存断点）、验收写回（事务）。
- **LLM 网关**：档位路由、适配器、能力协商/降级、回退、缓存、记账（§4 详）。图中 **OpenAI 兼容适配器一套覆盖 DeepSeek/Kimi/Qwen/GLM/OpenAI**，Anthropic/Gemini 各一适配器（§4.2）。
- **数据访问层**：Repository 模式封装所有表读写（原型单用户，多租户行级隔离为后续）。
- **BackgroundTasks + `jobs` 表**：学文风、全书扫描等长任务（原型不上专门队列）。
- **密钥管理**：各提供商 API Key 加密托管。

---

## 2. 技术栈与工程结构　← PRODUCT_SPEC §3.1

### 2.1 选型与理由

| 层 | 选型 | 理由 |
|---|---|---|
| 前端语言/框架 | **TypeScript + Next.js App Router + React** | SSR/流式/编辑器友好；纯 UI，调后端 API |
| 后端语言/框架 | **Python + FastAPI**（async） | LLM 编排/工具的强生态；async + SSE + Pydantic 校验天然契合 |
| 样式 | Tailwind + CSS 变量（设计 token） | 纸感主题 token 化（§8.4）；快速一致 |
| 编排 | **LangGraph**（Python） | 写章→四审→验收的并行 + HITL 中断 + checkpoint（ADR-10） |
| 数据库 | **PostgreSQL**（无向量） | 关系一库；事务保证验收写回原子性；向量检索为 P2 再引入 |
| ORM/迁移 | **SQLAlchemy 2.0（async）+ Alembic** | 类型化 + 迁移管理；贴近 SQL/索引控制 |
| 长任务 | **FastAPI BackgroundTasks + `jobs` 表** | 原型免专门队列；规模化再上 arq/Celery |
| LLM SDK | `anthropic` + `openai`(baseURL 覆盖 DeepSeek/Kimi/Qwen/GLM) + `google-genai` | 网关内适配器按需选用 |
| 校验/结构化输出 | **Pydantic + `instructor`** | 入参 + 结构化输出 schema 单一来源 |
| 前后端契约 | **FastAPI OpenAPI → 生成 TS 客户端**（openapi-typescript/orval） | 找回"共享类型"，跨语言不漂移 |
| 鉴权 | **原型单用户 stub**（多租户 + Auth 为后续） | 先做原型；§9.1 |
| 观测 | OpenTelemetry + 结构化日志（structlog） | LLM 调用 trace、成本指标 |

> 说明：本架构约束**模块边界与契约**；上表为原型锁定栈，向量检索/专门队列/多租户均为后续可选（标注于各节）。

### 2.2 代码组织（monorepo 模块边界）

```
writer-work-flow/
├── apps/
│   ├── web/                      # 前端 Next.js(TS)：纯 UI，调后端 API
│   │   ├── app/                  # App Router 页面(对齐 UX_SPEC §6)
│   │   │   ├── (dashboard)/      # 作品库
│   │   │   ├── projects/[id]/    # 工作台: write/outline/codex/foreshadow/review/style/rules
│   │   │   └── settings/         # 模型与提供商设置(UX §6.10)
│   │   ├── components/           # UI 组件(对齐 UX §7)
│   │   └── lib/api/              # OpenAPI 生成的 TS 客户端
│   └── api/                      # 后端 FastAPI(Python)
│       ├── routers/             # 端点(对齐 §7)
│       └── main.py              # 应用入口 + OpenAPI
├── packages/                     # Python 包(后端共享)
│   ├── core/                     # 领域核心(无 IO)
│   │   ├── domain/               # 实体/状态机(伏笔状态机/一致性规则)
│   │   ├── orchestrator/         # 编排器(LangGraph 图)
│   │   └── memory/               # 确定性选择/注入/prompt 组装
│   ├── llm_gateway/              # LLM 网关(适配器/路由/降级/回退/缓存/记账)
│   │   └── adapters/             # openai_compatible / anthropic / gemini
│   ├── agents/                   # 8 个内置 Agent 的声明(prompt+schema+reads/writes)
│   ├── skills/                   # Skill registry + loader + 权限沙箱
│   ├── db/                       # SQLAlchemy 模型 / Alembic 迁移 / Repository
│   ├── shared/                   # Pydantic schema / 错误信封(后端; 经 OpenAPI 暴露给前端)
│   └── config/                   # 提供商配置/档位映射/env 解析
└── PRODUCT_SPEC.md / UX_SPEC.md / ARCHITECTURE.md / DEV_PLAN.md
```

**模块依赖方向**（高→低，禁止反向）：
`apps/api(routers) → core / agents / skills → llm_gateway / memory → db → shared`。
`core` 不依赖 `db` 的具体实现，只依赖 Repository 接口（依赖倒置，便于测试 mock）。
**前后端契约**：`apps/api` 的 FastAPI 自动产出 OpenAPI → 生成 `apps/web/lib/api` 的 TS 类型，跨语言不漂移。

### 2.3 运行时拓扑

- **后端进程（FastAPI）**：处理 HTTP/SSE，承载 API 层 + 编排器(LangGraph) + 网关 + 记忆服务（同进程，低延迟）；长任务用 **BackgroundTasks** 在同进程后台跑，状态落 `jobs` 表（原型无独立 Worker；常驻服务部署即可，避免 serverless 杀长函数）。
- **前端进程（Next.js）**：SSR + 静态资源；调后端 API。
- **Postgres**：单主库起步；读多可加只读副本（看板/列表查询走副本）。
- **横向扩展**：原型单实例即可；后端无状态，规模化可多实例 + 抽独立 Worker（arq/Celery）。

---

## 3. 数据架构　← PRODUCT_SPEC §3.2

产品意图：记忆即真相源，每章写前检索注入、验收后增量更新；不可变追加保留历史。

### 3.1 完整 DDL（11 张创作表 + 运营表）

> 类型以 PostgreSQL 表述；`jsonb` 用于结构化但 schema 演进频繁的字段。所有业务表带 `project_id` 外键（原型单用户，多租户行级隔离为后续）。**原型无向量列**——记忆选择走确定性逻辑（§3.4）；向量检索为 P2，届时再加 `embedding vector(N)` 列与 HNSW 索引。
> 下面先列 **11 张创作数据表**（对齐 PRODUCT_SPEC §3.2；其中 `timeline`/`decisions` 为 P2，MVP 可不建），再列 **运营/系统表**（用户/凭据/用量/技能/jobs）。

```sql
-- 作品（根表，承载总纲/卖点/结构）
CREATE TABLE projects (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id      uuid NOT NULL REFERENCES users(id),
  title         text NOT NULL,
  genre         text,
  logline       text,                      -- 一句话故事
  premise       text,                      -- 总纲
  theme         text,                      -- 立意
  selling_points jsonb DEFAULT '[]',       -- 卖点数组
  structure     text,                      -- 故事结构选型(三幕/故事圈/雪花)
  created_at    timestamptz NOT NULL DEFAULT now(),
  updated_at    timestamptz NOT NULL DEFAULT now()
);

-- 人物（设定库 / 角色生成器产出）
CREATE TABLE characters (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  name          text NOT NULL,
  role          text,                      -- 定位: 主角/CP/对手/导师/工具人
  traits        jsonb,                     -- 性格(核心-表层-阴影/大五)
  appearance    text,
  motive        text,
  backstory     text,                      -- 背景故事
  arc           jsonb,                     -- 人物弧光: 起点→转变→终点
  speech_tics   jsonb,                     -- 口癖/语言风格
  tags          jsonb DEFAULT '[]',        -- 人设标签/萌点
  relations     jsonb DEFAULT '[]',        -- 关系网边: [{target, type}]
  first_chapter int,
  latest_state  text,                      -- 最新状态(随验收更新)
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 世界观实体(势力/地理/力量体系/物品)
CREATE TABLE world_entities (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  type          text NOT NULL,             -- faction/place/power_system/item/rule
  name          text NOT NULL,
  rules         jsonb,                     -- 硬规则(供一致性校验引用)
  first_chapter int,
  latest_state  text,
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 大纲(分卷分章 + 场景)
CREATE TABLE outline (
  id                 uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id         uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  volume             int NOT NULL,
  chapter_no         int NOT NULL,
  beats              jsonb,                -- 节拍/场景清单
  foreshadow_windows jsonb DEFAULT '[]',  -- 本章关联的伏笔回收窗口(可多条)
  UNIQUE (project_id, chapter_no)
);

-- 章节事实摘要(append-only, 防矛盾的关键)
CREATE TABLE chapter_digests (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  chapter_no    int NOT NULL,
  facts         jsonb NOT NULL,            -- 结构化事实: 谁做了什么/状态变化/新增设定
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 伏笔账本
CREATE TABLE foreshadow (
  id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id          uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  code                text NOT NULL,       -- F-012
  title               text NOT NULL,
  status              text NOT NULL DEFAULT 'OPEN',  -- OPEN/PARTIAL/CLOSED/OVERDUE
  planted_at          int,                 -- 埋设章号
  content             text,
  expected_close_from int,
  expected_close_to   int,
  importance          text,                -- 主线/支线
  links               jsonb DEFAULT '[]',  -- 关联人物/势力
  progress            jsonb DEFAULT '[]',  -- [{chapter, note, status}] append
  updated_at          timestamptz NOT NULL DEFAULT now(),
  UNIQUE (project_id, code)
);

-- 文风指纹
CREATE TABLE style_fingerprint (
  id             uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id     uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  dimensions_json jsonb NOT NULL,          -- 16 维: {dim: value}
  evidence_json  jsonb NOT NULL,           -- 每维原文证据
  version        int NOT NULL DEFAULT 1,   -- 增量更新版本
  created_at     timestamptz NOT NULL DEFAULT now()
);

-- 四级规则
CREATE TABLE rules (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid REFERENCES projects(id) ON DELETE CASCADE,  -- global 级可为 NULL
  level         text NOT NULL,             -- global/genre/style/project
  content       text NOT NULL,
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 章节正文(带版本, 支持改稿回溯)
CREATE TABLE chapters (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  volume        int NOT NULL,
  chapter_no    int NOT NULL,
  content       text,
  status        text NOT NULL DEFAULT 'draft',  -- draft/accepted
  version       int NOT NULL DEFAULT 1,
  created_at    timestamptz NOT NULL DEFAULT now(),
  UNIQUE (project_id, chapter_no, version)
);

-- 审稿留痕(四审报告 + 裁决; 支撑 UX 审稿历史; append 每次审稿)
CREATE TABLE chapter_reviews (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id      uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  chapter_no      int NOT NULL,
  chapter_version int,                        -- 审的是哪个草稿版本
  conflicts       jsonb DEFAULT '[]',         -- continuity 冲突清单
  foreshadow_sug  jsonb DEFAULT '[]',         -- 新埋/回收建议
  style           jsonb,                      -- 漂移段 + 评分
  pace            jsonb,                      -- 注水/钩子/节拍图
  health_score    int,                        -- 0-100
  decisions       jsonb,                      -- 作者裁决(采纳/忽略/回炉)留痕
  created_at      timestamptz NOT NULL DEFAULT now()
);

-- 时间线(P2; MVP 先由 chapter_digests 推导)
CREATE TABLE timeline (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  event         text NOT NULL,
  chapter_no    int,
  story_time    text,                      -- 故事内时间(可模糊)
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 设定讨论决议留痕(P2)
CREATE TABLE decisions (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id    uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  topic         text NOT NULL,
  decision      text NOT NULL,
  created_at    timestamptz NOT NULL DEFAULT now()
);
```

**运营/系统表（非创作数据）**

```sql
-- 用户(多租户主体; owner_id 外键指向此)
CREATE TABLE users (
  id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email        text UNIQUE NOT NULL,
  display_name text,
  created_at   timestamptz NOT NULL DEFAULT now()
);

-- 提供商凭据(各家 API Key, 加密存储; 按用户/作品)
CREATE TABLE provider_credentials (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id    uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  project_id  uuid REFERENCES projects(id) ON DELETE CASCADE,  -- NULL=用户级默认
  provider    text NOT NULL,                  -- anthropic/deepseek/kimi/openai/...
  api_key_enc bytea NOT NULL,                  -- 加密密文(KMS/加密列), 永不回显
  created_at  timestamptz NOT NULL DEFAULT now(),
  UNIQUE (owner_id, project_id, provider)
);

-- 档位路由配置(全局默认在 config; 作品级覆盖落库)
CREATE TABLE tier_routing (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id  uuid REFERENCES projects(id) ON DELETE CASCADE,  -- NULL=用户/全局
  tier        text NOT NULL,                  -- writer/analyst/light
  provider    text NOT NULL,
  model       text NOT NULL,
  fallback    jsonb DEFAULT '[]',             -- [{provider,model}] 回退链
  UNIQUE (project_id, tier)
);

-- 用量记账(按 provider+model 计费, 支持多币种)
CREATE TABLE usage_ledger (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id      uuid NOT NULL REFERENCES users(id),
  project_id    uuid REFERENCES projects(id),
  provider      text NOT NULL,
  model         text NOT NULL,
  input_tokens  int NOT NULL,
  output_tokens int NOT NULL,
  cache_read    int DEFAULT 0,
  cost_minor    bigint NOT NULL,              -- 最小货币单位
  currency      text NOT NULL,                -- USD/CNY...(混币种)
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 技能 registry(内置/自定义/社区; 对齐 PRODUCT_SPEC §5.5)
CREATE TABLE skills (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  scope         text NOT NULL,                -- builtin/custom/community
  owner_id      uuid REFERENCES users(id),    -- custom 时归属
  name          text NOT NULL,
  description   text,
  tier          text NOT NULL,                -- 档位 或 provider:model
  system_prompt text NOT NULL,
  input_schema  jsonb,
  output_schema jsonb,
  reads         jsonb DEFAULT '[]',           -- 声明式表读权限
  writes        jsonb DEFAULT '[]',           -- 声明式表写权限
  genre         text,
  examples      jsonb DEFAULT '[]',
  created_at    timestamptz NOT NULL DEFAULT now()
);

-- 异步长任务状态(BackgroundTasks + 轮询; §7.4)
CREATE TABLE jobs (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id  uuid REFERENCES projects(id) ON DELETE CASCADE,
  kind        text NOT NULL,                -- style_learn / full_scan / batch_chars
  status      text NOT NULL DEFAULT 'queued',  -- queued/running/done/failed
  progress    int DEFAULT 0,                -- 0-100
  result      jsonb,
  error       text,
  created_at  timestamptz NOT NULL DEFAULT now(),
  updated_at  timestamptz NOT NULL DEFAULT now()
);
```

### 3.2 索引设计

```sql
-- 租户 + 高频过滤
CREATE INDEX idx_characters_proj   ON characters(project_id);
CREATE INDEX idx_world_proj        ON world_entities(project_id);
CREATE INDEX idx_digests_proj_ch   ON chapter_digests(project_id, chapter_no);
CREATE INDEX idx_chapters_proj_ch  ON chapters(project_id, chapter_no);
CREATE INDEX idx_foreshadow_proj_status ON foreshadow(project_id, status);  -- 看板/到期扫描
-- 可选: 实体按名匹配(确定性检索的 FTS 兜底, §3.4)
CREATE INDEX idx_char_name_trgm  ON characters     USING gin (name gin_trgm_ops);
CREATE INDEX idx_world_name_trgm ON world_entities USING gin (name gin_trgm_ops);
-- (P2 引入向量时再加 embedding 列 + HNSW 索引)
```

### 3.3 不可变与版本化策略

| 数据 | 策略 | 说明 |
|---|---|---|
| 章节摘要 `chapter_digests` | **append-only** | 每章一行，从不改；历史可回溯"第 X 章发生了什么" |
| 伏笔进展 `foreshadow.progress` | **数组 append** | 状态字段可变，但进展只追加 |
| 章节正文 `chapters` | **多版本行** | 改稿写新 version 行，旧版保留；`(project_id, chapter_no, version)` 唯一 |
| 文风指纹 | **版本号递增** | 增量补样本生成新 version |
| 设定（人物/世界观） | **就地更新 + latest_state** | 冲突不静默覆盖，标 `[CONFLICT]` 待裁决；重大变更可在 `decisions` 留痕 |

> 不可变原则呼应用户全局编码规范（不就地覆盖、保留历史）。验收写回为**单事务**（§5.5），保证摘要/伏笔/状态一致落库或全回滚。

### 3.4 确定性记忆选择（无向量）

原型用**确定性选择**取代向量检索——"本章有谁"大纲已点名，设定集也不大，无需语义相似度去猜，且完全可调试。

- **选择来源（并集去重）**：
  1. **显式引用**——本章大纲 `beats` 点名的人物/世界观实体（最精准）。
  2. **主角常驻**——`role` 为主角/核心的角色永远纳入。
  3. **近况实体**——近 N 章 `chapter_digests.facts` 里出现过的实体（N 可配，默认 3–5）。
  4. **（可选）按名匹配**——正文/大纲提到的实体名，用 Postgres `pg_trgm`/全文匹配兜底（§3.2 trgm 索引）。
  5. 命中本章 `outline.foreshadow_windows` 的伏笔。
- **渲染为卡片**：每个选中实体行渲染成一张"角色/设定卡"文本块（喂给 AI 的"MD"由行生成，非手管文件）。
- **拼装**：选中卡 + latest_state → "易变状态块"，置于 prompt 缓存断点**之后**（§4.6）。
- **目的**：只注入相关设定（控成本）+ 覆盖关键实体（抗上下文衰减），对齐 PRODUCT_SPEC §3.5。
- **已知局限（覆盖盲区）**：若本章涉及一个**既未在 beats 点名、又久未出场（不在近 N 章）**的实体（如回收 50 章前的伏笔、重提冷门角色），确定性选择会**漏注入**→ AI 可能写出与旧设定冲突的内容——这正是产品要防的失败。缓解：①命中 `outline.foreshadow_windows` 的伏笔关联实体强制纳入；②对正文/大纲做按名 `pg_trgm` 匹配兜底；③**允许作者在工作台手动 pin 实体**进本章注入。这是砍向量换简单的代价，向量检索（P2）能从语义上补这个盲区。
- **P2 升级位**：设定集膨胀或盲区频发时，在 `select_relevant_entities()` 同一接口后接入**向量检索**（加 `embedding` 列 + HNSW + 嵌入服务），上层无感。

### 3.5 数据访问层（Repository）

- 每张表一个 Repository（`CharacterRepo`/`ForeshadowRepo`…），封装 SQL，**统一强制 `project_id` 过滤**（数据隔离；多租户化时此层再加 `owner_id` 校验）。
- `core` 依赖 Repository **接口**而非实现（依赖倒置），测试时注入内存实现。
- 迁移：**Alembic** 版本化迁移，CI 校验 schema 与 SQLAlchemy 模型一致。

---

## 4. LLM 网关架构　← PRODUCT_SPEC §3.3 / §3.4 / §3.5

产品意图：不绑定单一厂商；Agent 只声明**能力档位**，网关映射到具体 provider+model，并屏蔽各家能力差异。

### 4.1 统一内部接口契约

网关对上层（编排器/Agent）暴露单一接口，屏蔽各家差异：

```python
class Block(BaseModel):
    text: str
    cache: bool = False                      # 稳定块可标缓存断点

class LlmRequest(BaseModel):
    tier: Literal["writer", "analyst", "light"]  # 档位(或显式 provider:model 锁定)
    system: list[Block] = []                 # 稳定块在前
    input: str | list[Block]                 # 易变内容(断点之后)
    stream: bool = False
    output_schema: type[BaseModel] | None = None  # 需结构化输出时(Pydantic 模型/instructor)
    thinking: bool = False                   # 是否启用思考/推理(有则启用)
    max_tokens: int | None = None
    scope: "Scope"                           # {project_id; user_id 原型可固定}

class Usage(BaseModel):
    provider: str; model: str
    input_tokens: int; output_tokens: int
    cache_read_tokens: int = 0
    cost_minor: int; currency: str           # 混币种: USD/CNY…

class LlmResponse(BaseModel):
    text: str
    parsed: BaseModel | None = None          # output_schema 命中时的结构化结果
    usage: Usage
    served_by: "ServedBy"                    # {provider; model; fell_back}
```
- 流式：`stream=True` 返回 `AsyncIterator[Delta]`，归一各家 SSE 事件为统一 `Delta`。
- 上层永远不碰具体厂商字段——换厂商对 Agent 透明。

### 4.2 适配器设计

```
            LlmRequest(统一)
                 │
        ┌────────▼─────────┐
        │  网关核心          │  路由→能力协商→(缓存)→调用→(回退)→记账
        └────────┬─────────┘
                 │ 归一化请求
   ┌─────────────┼──────────────┐
   ▼             ▼              ▼
OpenAI兼容适配器  Anthropic适配器  Gemini适配器
(DeepSeek/Kimi/   (Messages API   (generateContent)
 Qwen/GLM/OpenAI)  形态)
```

- **适配器职责**：把 `LlmRequest` 翻译成目标家 API 请求；把响应/流/usage 翻译回统一形态；声明**能力矩阵**（见 4.4）。
- **OpenAI 兼容适配器一套覆盖多家**：DeepSeek/Kimi/Qwen/GLM/OpenAI 仅 baseURL + model + key 不同。
- Anthropic、Gemini 各一适配器（请求形态不同）。
- 新增厂商 = 新增一个适配器 + 在 `config` 注册，零侵入上层。

### 4.3 档位路由与三级配置解析

```
解析优先级(高→低)：单 Agent 锁定 > 作品级覆盖 > 全局默认
tier ──▶ resolve(scope) ──▶ { provider, model, fallback[] }
```

- 配置来源：`config`（全局默认）+ 作品设置（DB）+ Skill 内 `tier/provider:model`（§5.6）。
- 解析结果含**主模型 + 回退链**（UX §6.10 的"主/回退"）。
- 切换厂商只改配置，不改 Agent 代码（对齐 ADR-3）。

### 4.4 能力协商与降级

各家能力参差，网关维护**能力矩阵**并优雅降级：

| 能力 | 支持时 | 不支持时（降级） |
|---|---|---|
| 结构化输出(JSON Schema) | 走原生 | JSON 提示 + Pydantic/instructor 校验 + 重试（**上限 2-3 次**，超限标"该审未完成"不死循环） |
| 前缀缓存 | 标记缓存断点 | 跳过缓存（不影响正确性，仅成本） |
| 思考/推理 | 启用 | 普通生成 |
| 工具调用 | 原生 | 本系统四审/生成均用结构化输出实现，不强依赖工具调用 |

- 能力矩阵由适配器声明（`capabilities()`），网关据此决定调用形态。
- 降级对上层透明，仅在 `usage`/日志标注实际行为。
- **轻量档可靠性风险**：最便宜模型（Qwen-turbo/GLM-flash 类）JSON 结构化输出不稳，易触发重试。故 pace/style 这类轻量档审稿要用上面的重试上限兜底；若某 provider 在轻量档反复失败，运营可在档位路由把它换成更稳的模型（仍属轻量档）。

### 4.5 故障回退与重试

```
调用主模型
  ├─ 成功 ────────────────────────▶ 返回(标 served_by.fell_back=false)
  ├─ 限流(429)/超时/5xx ─▶ 指数退避重试(SDK/自实现, 上限 R 次)
  │                          └─ 仍失败 ─▶ 切回退链下一个 provider
  └─ 内容策略拒绝 ─────────▶ 按策略可切回退或上抛(让作者知情)
回退链耗尽 ─▶ 抛 LlmUnavailable(上层降级: 提示作者换档位/稍后重试; 不丢正文)
```

- **中途失败**：流式写章已吐部分 token 后回退耗尽 → 已生成部分由自动保存留在 `chapters(draft)`，LangGraph write 节点报错使图停在 write 前的 checkpoint；前端提示"生成中断，可重试或基于已存部分续写"，不丢稿。
- **熔断**：某 provider 连续失败超阈值，短时熔断、直接走回退，避免雪崩。
- **缓存隔离**：回退到不同 provider 时缓存失效（缓存按 `provider+model` 维度，§4.6）。
- 重试只对幂等的生成/校验；写库副作用在编排层、不在网关。

### 4.6 缓存抽象（稳定前缀）

- **原则跨厂商通用**：`system` 中的稳定内核（世界观硬规则 + 文风指纹 + 少量真正定型的角色）置于缓存断点前；易变（活跃角色的 `latest_state` + 本章大纲）置于断点后。
- **务实预期**：真正稳定的多是世界观硬规则 + 文风指纹；角色 `latest_state` 每次出场验收都变、新角色随时加入，所以"稳定内核"会随连载缩水、失效更频繁。**"省约 90%" 仅在稳定内核占比高时成立**，不是通用承诺；缓存收益应按命中率实测评估（§9.3）。
- **机制按厂商封装**：Claude 显式 `cache_control`；OpenAI/DeepSeek 自动前缀缓存；不支持者跳过。
- **失效面**：缓存键含 `provider+model`；上游稳定块任一字节变更即失效（故组装时排序序列化、不混入时间戳/UUID）。
- 命中指标回传 `usage.cache_read_tokens`，供观测（§9.3）。

### 4.7 凭据管理与隔离

- 各提供商 API Key 加密存储（KMS 或 DB 加密列），**前端永不回显明文**（UX §6.10）。
- 作用域：平台级默认 Key（可选）+ 用户/作品级自带 Key；解析时按 scope 选取。
- `测试连接`：发一个最小探测请求验证 Key 有效 + 拉取能力矩阵。

### 4.8 用量记账

- 每次调用落 `usage_ledger` 表（provider/model/in/out/cache_read/cost），按 `owner_id+project_id+provider+model+day` 聚合。
- 成本换算表随 provider 配置维护（单价随厂商而异，§3.3）。
- 供 UX §6.10 月度概览 + 按档位/作品下钻。

---

## 5. Agent 编排引擎　← PRODUCT_SPEC §3.6 / §5

产品意图：单一职责 Agent + 确定性编排；Agent 经记忆库交换、不直连；四审只读、产出经验收 gate。

### 5.1 Agent / Skill 声明式抽象

内置 Agent 与用户 Skill 同构——都是一份**声明**，由编排器加载后经网关执行：

```python
class AgentSpec(BaseModel):
    name: str                       # worldbuilder / writer / continuity ...
    tier: Tier                      # 能力档位(网关解析 provider+model)
    system_prompt: str              # 角色与约束(值由 load_prompt(name) import 期注入, 见下)
    input_schema: type[BaseModel]   # 入参契约
    output_schema: type[BaseModel] | None  # 结构化产出契约(网关保证; writer 为 None=纯文本)
    reads:  list[TableName]         # 声明式表读权限
    writes: list[TableName]         # 声明式表写权限(经验收才生效)
    genre: str | None = None        # 题材适用(Skill 用)
```

- 内置 21 Agent 即 `scope=builtin` 的 AgentSpec；用户 Skill 为 `custom/community`（§5.6）。
- `reads/writes` 是**契约**：运行时强制，越权拒绝（安全见 §5.6 / §9.2）。
- **Prompt 外置（方案A，2026-06）**：`system_prompt` 散文不再内联于 Python 常量，而外置为 `packages/agents/ww_agents/prompts/<spec.name>.md`，由 `load_prompt(name)`（`prompt_loader.py`）在 **import 期确定性加载**（去 BOM/LF 归一/NFC/`rstrip` 尾 LF + 内存缓存 + fail-fast `PromptNotFoundError`，无任何运行期插值）。`system_prompt` 仍是字节稳定的整块 `str`，进 `system` 且 `cache=True`——缓存断点前块字节不变（不变量 #9）；改稿只动 `.md`，金标准 fixture `tests/fixtures/prompt_hashes.json` 守字节回归。Pydantic 类型**永留 Python**：`SCHEMA_CATALOG[name]→output type` 是唯一真相源，`output_schema` 由它派生。
- **注册表 `SPECS: dict[name, AgentSpec]`** 是内置 agent 的集中名册（name 为唯一主键，`SPECS[name]`/`prompts/<name>.md`/`SCHEMA_CATALOG[name]` 三者由 name 对齐，缺一即 fail-fast）。四审受信名锚在显式白名单 `REVIEW_RESERVED_NAMES={continuity,foreshadow,style,pace}`。
- **统一解析入口 `SpecResolver.get(name)`**（`packages/skills/ww_skills/spec_resolver.py`）：内置走纯内存 `SPECS`（零 DB），用户 skill 走 `SkillRegistry`（DB）。同读接口、不同信任级别；内置 name 为保留命名空间，用户 skill 同名在 **SkillRegistry 入库校验期**即被拒（不在读路径），守不变量 #3。

### 5.2 编排器（LangGraph 图）

编排器是一张 **LangGraph 状态图**（非自治 agent loop），节点固定、边确定；用其**并行**跑四审、用 **interrupt（HITL）+ checkpoint** 表达"写章→作者裁决→验收"的跨请求暂停-恢复。

```python
# 状态: { project_id, chapter_no, ctx, draft, reviews, decisions }
g = StateGraph(ChapterState)

g.add_node("assemble", lambda s: memory.assemble(s.project_id, s.chapter_no))  # §5.3
g.add_node("write",    lambda s: gateway.run(writer_spec, s.ctx, stream=True)) # 纯函数产草稿
# 四审为并行分支(无依赖), 汇入 collect
for spec in (continuity_spec, foreshadow_spec, style_spec, pace_spec):
    g.add_node(spec.name, make_review_node(spec))           # 只读, 结构化输出
g.add_node("collect", collect_reviews)                      # 汇总四审 → 落 chapter_reviews 表(留痕)

g.add_edge("assemble", "write")
for spec in REVIEW_SPECS:                                   # write ─並行→ 四审
    g.add_edge("write", spec.name); g.add_edge(spec.name, "collect")

# 验收 = HITL 中断: 图在此 interrupt; 审稿结果已落 chapter_reviews 表, 前端从表读取并裁决;
# 作者 POST /accept 携 decisions(+ 可能改过的终稿) 后, 从 checkpoint 恢复, 进 accept 节点
g.add_node("accept", commit_accept)                         # 单事务(含「从终稿提炼 digest」): 见 §5.5
graph = g.compile(checkpointer=PostgresSaver(...), interrupt_before=["accept"])
```

- **确定性**：图固定、可单测、可回放（给定输入 + mock 网关，输出确定；checkpoint 落 Postgres）。
- **并行**：四审为并行分支汇入 collect；任一失败不阻塞其余，报告标"该审未完成"。
- **HITL 恢复 + 真相源边界**：用 `interrupt_before=["accept"]` + checkpointer 支持"写章请求返回、验收请求恢复"的跨请求流程。**正文以 `chapters` 表、审稿结果以 `chapter_reviews` 表为权威真相源；checkpoint 只存"图走到哪 + 待裁决句柄"，不作正文/审稿的真相源**——恢复时按 `chapter_no` 从领域表重读，避免双真相源（对齐 ADR-2）。
- **编排 vs Agent**：图做控制流与事务边界；Agent 节点只做单次认知任务、经记忆库交换（不直连）。

#### 5.2.1 实现现状（单章流程）与多章链图　← 回写自实现审计 + Chain Workflow 交付（2026-06-23）

**单章流程的实现现状（与上面"一张图串全程"的理想形态有别）**：上图是设计意图；实际**单章流程未编译成这一张图**——
- **写章**：`POST /projects/{id}/chapters/{no}/draft` 直接 `gateway.stream()` 流式产草稿（`stream_chapter_draft`），**不经图**。（曾有 `build_write_graph` 单节点图但无端点调用，已在多章链图落地时删除；`write_node` 本体保留供链复用。）
- **四审**：`POST /projects/{id}/chapters/{no}/review` 编译 `build_review_graph()` 做**一次性 `ainvoke`**（START→四审并行→collect→END），**不带 checkpointer**。
- **验收**：`POST /projects/{id}/chapters/{no}/accept` 是**确定性事务代码** `run_accept_transaction`（晋升终稿→从终稿提炼 digest→裁决留痕，§5.5），**不是图节点**；其 HITL 闸在 API/DB 层（冲突未裁决→`CONFLICT_UNRESOLVED`），**非 langgraph `interrupt`**。
- 因此单章流程是线性的，**`interrupt` / Postgres checkpointer 在单章流程未实际启用**——线性流无需图。

**多章工作流链（Chain Workflow）= 本项目首个真正用 LangGraph cyclic 图 + Postgres checkpointer + `interrupt` 的场景。** 设计契约见 `docs/design/chain-workflow.md`。对标竞品「一键多章」，差异化在「每章过四审、遇冲突才停人」。

```python
# ChainState(只存控制流, 不存正文/上下文/冲突明文): {project_id, chain_key,
#   start/last/current_chapter_no, written:[], has_conflicts}
# 节点各自用独立短 session(每章 accept 章原子提交); gateway 经 builder 按节点 session 重建
g = StateGraph(ChainState)
g.add_node("write_chapter",  write_chapter)   # assemble→收集版写章(gateway.run,非SSE)→落 chapters draft
g.add_node("review_chapter", review_chapter)  # 四审→落 chapter_reviews(只读留痕)
g.add_node("decide",         decide)          # 纯逻辑: 读最近审稿 → has_conflicts
g.add_node("accept_chapter", accept_chapter)  # interrupt-on-conflict; 复用 run_accept_transaction + 伏笔扫描
g.add_edge("write_chapter", "review_chapter"); g.add_edge("review_chapter", "decide")
g.add_conditional_edges("decide", ...)        # 有冲突→interrupt()暂停交人; 无冲突→直接 accept
g.add_conditional_edges("accept_chapter", ...) # current<last→回 write_chapter(循环); 否则→END
graph = g.compile(checkpointer=AsyncPostgresSaver(...))  # thread_id = job_id
```

- **跑在 `run_job` 长任务壳里**：`POST .../chains/{key}/run`→202 `{job_id}` + BackgroundTask；`GET /jobs/{id}` 轮询进度（`written`）；某章四审报冲突→`interrupt()` 暂停（job=`awaiting_input`）→`POST .../chains/runs/{job_id}/resume` 携裁决 `Command(resume=decisions)` 续跑。
- **守真相源边界（同 §5.2）**：checkpoint 只存控制流位置 + 待裁决句柄；正文/审稿/冲突明文以领域表为权威，resume 时重读 `chapters`/`chapter_reviews`（不变量 #5）。
- **checkpointer 建表**：`setup_checkpointer` 在 alembic 迁移建 langgraph 检查点 4 表（DDL 只在 migrations/CI，绝不 app-runtime）；E2E 用 `MemorySaver`（单进程，零联网）。

### 5.3 记忆注入与 prompt 组装

```python
def assemble(project_id, chapter_no):
    outline_row = outline_repo.get(project_id, chapter_no)
    entities = select_relevant_entities(project_id, outline_row)  # 确定性, §3.4
               # = 显式点名 ∪ 主角常驻 ∪ 近况实体 ∪ (可选)按名匹配
    forewin     = foreshadow_repo.windows_for(chapter_no)
    digests     = digest_repo.recent(project_id, k)              # 近况摘要
    fingerprint = style_repo.latest(project_id)
    rules       = rules_repo.effective(project_id)               # 四级合并

    stable_core = serialize_sorted(world_hard_rules + settled_chars + fingerprint + rules)
    volatile    = serialize_sorted(render_cards(entities) + forewin + digests + outline_row.beats)
    return LlmRequest(system=[Block(text=stable_core, cache=True)], input=volatile)
```

- **缓存断点**：stableCore 标 `cache:true`（断点前），volatile 在后（§4.6）。
- **确定性序列化**：排序 + 无时间戳/UUID，保证缓存字节稳定。
- 四级规则合并：`global → genre → style → project`，越具体越优先。

### 5.4 8 个 Agent 的 I/O 契约（对齐 §5.2 花名册）

| Agent | 档位 | reads | writes(经验收) | 输出 schema 要点 |
|---|---|---|---|---|
| worldbuilder | 写手 | projects | world_entities | `{entities:[{type,name,rules}]}` 硬规则显式 |
| character-gen | 写手 | world_entities, characters | characters | `{cards:[{name,role,traits,backstory,arc,speech_tics,tags,relations}]}` |
| outliner | 分析 | projects, foreshadow, characters, world_entities | outline | `{chapters:[{no,beats,foreshadow_windows}]}` |
| writer | 写手 | (注入) | —(产草稿，持久化见下注) | 纯文本流(stream) |
| continuity | 分析 | chapter_digests, characters, world_entities | —(只读) | `{conflicts:[{type,where,refs,suggestion}]}`（仅冲突；digest 在验收时从终稿另提，见下注） |
| foreshadow-analyst | 分析 | foreshadow | —(只读) | `{planted:[...], resolved:[...]}` 建议 |
| style-auditor | 轻量(打分)/分析(提取) | style_fingerprint | style_fingerprint(仅学文风提取) | 提取`{dims,evidence}` / 打分`{segments:[{idx,score}]}` |
| pace-checker | 轻量 | rules(genre) | —(只读) | `{water:[...], hook:bool, beat_map:[...]}` |

- 所有非 writer 的 Agent **结构化输出**（网关保证，§4.4），便于程序消费与就地裁决。
- **writer 不直接写库**：产草稿(stream)→自动保存落 `chapters(draft)`(确定性代码，即时)→验收晋升为 `accepted` 新 version（§5.5）。四审 `writes` 为空（只读）。
- **style-auditor 的写入是例外**：发生在**学文风**流程（写 `style_fingerprint`），不在写章流水线；写章时它只读指纹做漂移打分。
- **digest 从终稿提取，不从审稿草稿**：审稿时 continuity 只出冲突；作者裁决/改稿后，验收事务里用**终稿**另跑一次轻量提炼得 `digest` 再追加 `chapter_digests`（§5.5/§6.1）——否则改稿前的草稿事实会污染真相源。

### 5.5 验收与状态写回（事务）

- 验收是**确定性代码**，非 Agent。把作者裁决后的变更**单事务**落库：
  1. 章节晋升 `accepted` 新 version；
  2. **从终稿提炼 `digest`**（轻量 LLM 调用，输入是最终验收文本而非审稿草稿）→ 追加 `chapter_digests`；
  3. 应用伏笔状态变更/新登记、更新人物 latest_state、可选加规则；
  4. 写入本次裁决留痕（`chapter_reviews.decisions`）。
- 事务保证一致：全成功或全回滚，杜绝"摘要更了但伏笔没更"的半态。
- 写回后触发：伏笔到期扫描（§6.2）、看板/仪表盘缓存失效。

### 5.6 技能系统运行时（Skill loader + 沙箱）

- **加载**：从 `skills` registry 读 AgentSpec（builtin/custom/community），按 `tier` 经网关执行（复用 §5.1 机制）。
- **表权限强制（执行点说明）**：这**不是进程级沙箱**——Skill 是声明式 prompt，本身不执行代码。强制点在**编排/写库层**：① 注入时只把声明 `reads` 的表数据喂给该 skill；② 验收/写库代码只把该 skill 产出**应用到其声明的 `writes` 表**（白名单），越权的产出字段丢弃并审计。即"声明式权限 + apply 层白名单"，而非隔离运行时。
- **纯声明式**：Skill 不含可执行代码，仅 prompt + schema + 权限，杜绝任意代码执行。
- **写库仍经验收 gate**：自定义 Skill 产出同样要过作者验收/四审才入库，不开后门。
- 安全细节见 §9.2。

---

## 6. 核心模块详细设计　← PRODUCT_SPEC §4

### 6.1 长篇一致性

- **摘要提炼**：验收时从**终稿**（作者裁决/改稿后的最终文本，非审稿草稿）提炼 `digest`（结构化事实），事务追加 `chapter_digests`——避免改稿前的草稿事实污染真相源。
- **实体状态**：人物/世界观维护 `latest_state`；验收按裁决更新（带 `[CONFLICT]` 的不自动覆盖）。
- **冲突检测**：审稿时 continuity 比对草稿 vs（近况摘要 + 人物卡 + 世界观硬规则），产出结构化冲突清单：
  ```
  conflict = { type: 性格漂移|能力不符|设定违例|地理矛盾|时间线倒错,
               where: 本章定位, refs: [冲突来源章/设定], suggestion: 改法 }
  ```
- **裁决流**：前端就地高亮 + 报告条目 → 作者选 `采纳改法 / 忽略 / 手改`；忽略可沉淀为规则。**未决冲突不允许验收**（UX §9 状态）。
- **校验清单**：参考 *Lost in Stories 2026* 分类（人物/设定/时间三类）。
- 时间线：MVP 由 `chapter_digests` 推导；P2 落 `timeline` 表加速。

### 6.2 伏笔账本

**状态机**：
```
        登记            首次照应          完成回收
  ─────▶ OPEN ──────▶ PARTIAL ─────────▶ CLOSED
           │             │
           └─────────────┴──▶ OVERDUE  (章号 > expected_close_to 且未 CLOSED)
                              (到期扫描置位; 回收后可转 CLOSED)
```
- **到期扫描**：验收后触发（确定性代码，非 Agent）：`current_ch > expected_close_to AND status≠CLOSED → OVERDUE`，写回并在看板/报告提醒。
- **新埋/回收建议**：审稿时 foreshadow-analyst 检测本章新埋线 / 疑似回收 → 建议，作者确认后登记/改状态。
- **依赖图/窗口**：`outline.foreshadow_windows` 关联章与伏笔；排大纲时提示接近回收窗口的伏笔。
- **看板**：按 `(project_id, status)` 索引查询，四泳道 + OVERDUE 强调（UX §6.8）。

### 6.3 文风模仿

- **提取（一次性，分析档）**：3–5 样本（≥5 万字）→ 16 维指纹（9 通用 + 7 中文），**每维附原文证据**，存 `style_fingerprint`（版本化、可增量合并）。
- **生成约束**：写章注入指纹（缓存稳定块）。
- **漂移打分（高频，轻量档）**：每段对照指纹打相似度分；低于阈值标漂移段。
- **回炉**：仅重写漂移段、保留上下文，新旧 diff（UX §8.3）。阈值可配（§10 风险：需校准）。

### 6.4 节奏引擎

- **模板 DSL**（存 `rules` genre 级）：黄金三章、章末钩子、爽点密度（每 N 千字一拍）、情绪曲线。
- **检测（pace-checker，轻量档）**：注水段（信息密度过低/重复铺陈）、章末钩子有无、爽点节拍图、情绪曲线。
- **产出**：`{water:[段], hook:bool, beat_map:[...]}` → 报告以"爽点节拍图 ▁▃▅"可视化（UX §6.4）。
- 这是对英文工具的差异化（中文网文模板）。

### 6.5 角色生成器

- **单/批量**：输入一句话需求 + 数量 + 定位 → character-gen（写手档）产出结构化角色卡。
- **防雷同（批量）**：一次性生成一组时，prompt 注入"已生成卡 + 已有角色"，要求差异化定位/性格；可对产出做相似度去重复核。
- **一致性校验接入**：入库前由**编排器**追加一道 continuity 检查（非 character-gen 直接互调，符合 §5.4 数据流），确认不与世界观/力量体系冲突。
- **入库**：过校验 → 写 `characters`（角色行即注入单元，§3.4 渲染成卡片）→ 进入后续确定性选择注入。

---

## 7. API 设计　← PRODUCT_SPEC §6

### 7.1 REST 约定与错误信封

- **作用域嵌套**：章节端点统一 `/projects/:id/chapters/:no/...`（修正 PRODUCT_SPEC §6 中 `:no` 跨项目不唯一的 nit，见文末发现）。
- **鉴权**：**原型单用户**（无登录/归属校验）；多租户 + 按 `owner_id` 校验为后续（§9.1）。
- **统一成功**：`200/201 { data, meta? }`。
- **统一错误信封**：
  ```json
  { "error": { "code": "CONFLICT_UNRESOLVED", "message": "...", "details": {} } }
  ```
  常见码：`NOT_FOUND / VALIDATION / CONFLICT_UNRESOLVED(未决冲突禁验收) / LLM_UNAVAILABLE(回退耗尽) / RATE_LIMITED`（多租户上线后补 `UNAUTHORIZED/FORBIDDEN`）。

### 7.2 端点清单（对齐 §6 + 设置/伏笔/看板）

| Method | Path | 请求 | 响应 | 码 |
|---|---|---|---|---|
| POST | `/projects` | 立项向导各字段 | project | 201 |
| GET | `/projects` | — | 作品列表(含待办徽标) | 200 |
| GET | `/projects/:id` | — | 作品详情 | 200 |
| POST | `/projects/:id/world/generate` | `{需求}` | world_entities(预览) | 200 |
| POST | `/projects/:id/characters/generate` | `{需求, count?, role?}` | 角色卡(预览,待入库) | 200 |
| POST | `/projects/:id/characters` | 角色卡 | 入库结果 | 201 |
| POST | `/projects/:id/style` | 样本(`mode=update?`) | `{job_id}`(异步见 7.4) | 202 |
| GET | `/projects/:id/style` | — | 最新文风指纹 `{dimensions, evidence, version}` | 200 (无指纹 404) |
| GET | `/jobs/:id` | — | 任务状态/进度/结果 | 200 |
| POST | `/projects/:id/outline` | `{范围}` | 大纲(含伏笔窗口) | 200 |
| POST | `/projects/:id/chapters/:no/draft` | `{}` | **SSE 流**草稿 | 200(stream) |
| PUT | `/projects/:id/chapters/:no/draft` | `{text}` | 自动保存草稿(幂等，写 `chapters.draft`，断流兜底) | 200 |
| POST | `/projects/:id/chapters/:no/review` | `{draft}` | **SSE 流**四审报告 | 200(stream) |
| POST | `/projects/:id/chapters/:no/accept` | `{裁决清单}` | 写回结果(将更新清单) | 200 |
| GET | `/projects/:id/chapters/:no/reviews` | — | 审稿历史(`chapter_reviews`) | 200 |
| POST | `/projects/:id/chapters/:no/refine` | `{段落, 指令}` | 回炉重写段(diff) | 200 |
| GET | `/projects/:id/foreshadow` | `?status=` | 伏笔看板数据 | 200 |
| POST | `/projects/:id/rules` | `{level, content}` | rule | 201 |
| GET | `/skills/toolbox` | — | 生成器描述符列表(key/标题/input_fields/ingestable/legacy_route) | 200 |
| POST | `/projects/:id/skills/:tool_key/generate` | `{brief, chapter_no?, count?, kind?}` | 结构化预览(不写库,仅记账) | 200 (未知 key 404/无 provider 503) |
| POST | `/projects/:id/skills/:tool_key/ingest` | `{world_entities?\|scenes?, chapter_no?, acknowledge_conflicts}` | 入库结果 | 201 (continuity 冲突 409) |
| GET/PUT | `/settings/providers` | 凭据/档位配置 | 配置(Key 脱敏) | 200 |
| POST | `/settings/providers/test` | `{provider}` | 连接+能力矩阵 | 200 |

### 7.3 流式接口（SSE）

- `draft` / `review` 用 **Server-Sent Events**：网关流式 `Delta` → 归一 SSE 事件 → 前端打字机渲染。
- 事件类型：`token`（正文增量）、`section`（四审分项开始/完成）、`conflict`（冲突命中）、`done`、`error`。
- 断流可重连：前端按已收 token 续接；正文自动保存兜底（不丢稿）。

### 7.4 异步长任务（BackgroundTasks + jobs 表）

- **长任务**：学文风（解析数万字样本）、全书一致性回归扫描、批量群像 → **FastAPI BackgroundTasks** 在同进程后台跑，先写 `jobs` 行返回 `{job_id}`（202），前端轮询 `GET /jobs/:id` 取进度/结果。
- 幂等以 `job_id` 去重；失败置 `jobs.status=failed` 可重试。
- **持久性局限 + 缓解**：BackgroundTasks 在进程内跑，**进程重启/部署会丢任务**、`jobs.status` 卡在 `running`。原型缓解：**启动时把所有 `running` 僵尸 job 标 `failed`**，让用户看到失败并重试（而非进度条永转）。真正的持久重试/跨进程恢复属规模化项——换 arq/Celery（接口不变）。
- **原型不上专门队列**。
- 写章/审稿**不走后台**（要即时流式反馈），走 SSE 同步流。

---

## 8. 前端架构　← UX_SPEC

### 8.1 应用结构（Next.js App Router；前端独立于后端）

- 路由对齐 UX §3.1 导航：`app/(dashboard)` 作品库；`app/projects/[id]/{write,outline,codex,foreshadow,review,style,rules}`；`app/settings/providers`。
- **Server Components** 取数（列表/看板/设定库只读视图）；**Client Components** 承载编辑器、流式、交互。
- **调用独立 FastAPI 后端**（§7 端点），经 **OpenAPI 生成的 TS 客户端**（`app/lib/api`）；SSE 用 `EventSource`/`fetch` 流读。无 Next API Routes 业务逻辑（仅可留 BFF 代理/鉴权透传）。

### 8.2 状态管理

| 状态类别 | 方案 |
|---|---|
| 服务端数据(列表/设定/看板) | React Query / SWR（缓存 + 失效） |
| 编辑器本地态(正文/光标/未保存) | 局部 store（Zustand 或 RSC + local） |
| 流式增量(draft/review) | SSE 订阅 → 增量 reducer |
| 乐观更新(验收/裁决) | 先改 UI、失败回滚 + Toast |

### 8.3 编辑器与流式渲染

- 正文编辑器：宋体 / 720px / 行高 1.9（UX §2.3）；contentEditable 或 ProseMirror/TipTap（富文本 + 锚点标注）。
- **流式打字机**：SSE `token` 逐字淡入 + 朱砂光标；尊重 `prefers-reduced-motion`（关动效）。
- **冲突标注**：`conflict` 事件 → 正文对应区间挂朱砂波浪线 + 悬浮卡 + 报告联动跳转。
- **自动保存**：debounce 持续保存草稿（写 `chapters` 当前 version），显示"保存于 hh:mm"。

### 8.4 设计 token 落地

- UX §2 的色板/字体/间距 → CSS 变量（`--paper`, `--ink`, `--vermilion` …）+ Tailwind theme。
- 纸感主题集中在 `:root`；预留 `[data-theme="night"]` 夜读模式扩展位（UX §2.1 备注）。
- 组件库（UX §7）一一实现：AppShell / Editor / ReviewCard / ForeshadowCard / TierRouter / ProviderRow …

---

## 9. 横切关注点　← PRODUCT_SPEC §10 / §5.5

### 9.1 鉴权与多租户隔离（原型：单用户）

- **原型阶段**：单用户，无登录/无归属校验；`users` 表与 `projects.owner_id` 保留为接缝（可写死一个 stub 用户），暂不强制。
- **多租户化（后续）**：接入 Auth（邮箱/OAuth）→ 所有 Repository 强制 `project_id + owner_id` 行级隔离 → 越权返回 `FORBIDDEN` + 审计；再远期多人协作预留 `project_members`。
- 因 Repository 已统一封装表访问，多租户化时只在该层加归属校验，不动业务逻辑。

### 9.2 安全

| 面 | 措施 |
|---|---|
| 密钥 | 提供商 Key 加密存储（KMS/加密列）；前端永不回显；仅后端解密用于调用 |
| 用户 Skill（不可信输入） | 纯声明式（无代码执行）；`reads/writes` 表权限白名单强制；产出经验收 gate（§5.6） |
| 提示注入 | 用户/作品文本以**数据**注入，不与系统指令混层；结构化输出 + 校验抑制越权产出；Skill prompt 不得提权 |
| 输入校验 | 所有端点 Pydantic 校验（系统边界），失败 `VALIDATION` |
| 数据外发 | LLM 调用即把作品内容发往第三方——UX §6.10 标境内/境外，由作者/运营按合规取舍 |
| 注入/越权审计 | 关键操作（验收/改设定/Key 变更/Skill 越权拦截）留审计日志 |

### 9.3 可观测性

- **结构化日志**（structlog）：请求、编排步骤、网关调用（provider/model/usage/fell_back/cache_read）。
- **追踪**（OpenTelemetry）：一次写章 = 一条 trace，跨"检索→writer→四审→验收"span，定位慢点。
- **指标**：四审耗时、缓存命中率、回退率、每作品成本；驱动 UX §6.10 成本看板与告警。

### 9.4 韧性与配置

- **韧性**：网关回退/熔断/重试（§4.5）；SSE 断流可续；正文自动保存兜底；长任务可重试幂等。
- **配置**：`config` 模块集中——提供商注册、档位默认映射、env 解析、特性开关（P1/P2 功能灰度）。

---

## 10. 部署与运维　← PRODUCT_SPEC §3.1 / §10

- **环境**：dev / staging / prod 三套；env 与密钥分环境管理。
- **部署拓扑**：前端 Next.js + 后端 FastAPI + Postgres；**常驻服务**（Fly.io/Render + 托管 PG，或 Docker 自托管）——因后端有 BackgroundTasks 长任务，避免 serverless 杀长函数。后端无状态可水平扩。
- **数据库**：托管 Postgres（**无需 pgvector 扩展**；P2 引入向量时再开）；定时备份 + PITR；Alembic 迁移随发布执行、CI 校验。
- **扩展性/配额**：写章并发受 LLM 限流约束——按作品**速率配额**；网关侧聚合各 provider 限流（429 退避 + 回退）。规模化再抽独立 Worker（arq/Celery）削峰。
- **发布**：CI 跑测试 + 迁移校验 + 类型检查；蓝绿/滚动；DB 迁移向后兼容（先加列后用）。

---

## 11. 测试策略

对齐团队规范（≥80% 覆盖，单元/集成/E2E 三类，TDD）：

| 层 | 范围 | 要点 |
|---|---|---|
| 单元 | 领域逻辑/状态机/纯函数 | 伏笔状态机、四级规则合并、prompt 组装确定性、档位解析 |
| 集成 | Repository + DB、LangGraph 图 + mock 网关、端点 | 验收事务原子性、HITL 恢复、SSE 流 |
| 契约 | **LLM 结构化输出 schema** | 每个 Agent 的 outputSchema 用录制样例做契约测试；适配器能力矩阵测试 |
| E2E | 关键流程 | 写一章闭环（写→审→裁决→验收）、生成群像、切换提供商 |

- **LLM mock**：网关可注入 mock provider（返回固定/录制响应），使编排器/Agent 可确定性测试，不烧真实 token。
- **降级/回退测试**：模拟 429/不支持结构化输出，验证降级与回退路径。
- TDD：新模块先写测试（RED→GREEN→重构）。

---

## 12. 实施路线（架构视角）　← PRODUCT_SPEC §9

按依赖关系排架构落地顺序（与产品 M1–M5 对齐）：

| 里程碑 | 架构交付 | 依赖 |
|---|---|---|
| **M1** | `db`(**建全部表**+迁移，含 rules) → `llm_gateway`(单 provider 起 + 档位) → `memory`(确定性选择+组装) → `orchestrator`(写章) → API(立项/写本章/SSE) → 前端 AppShell+工作台 + **设置页(至少一个 provider)** | 无 |
| **M2** | continuity Agent + review SSE + 验收事务 + 冲突裁决 UI | M1 |
| **M3** | 伏笔状态机 + 到期扫描 + 看板 + outliner 伏笔窗口 + pace-checker | M2 |
| **M4** | 文风提取(异步任务) + style-auditor 双轨 + 回炉 | M1 网关 |
| **M5** | worldbuilder/character-gen 生成 + 多 provider 回退/降级完善 + Skill 运行时 + 规则/命令面板 | M2–M4 |

> 网关的多 provider/回退/降级在 M1 先打**接口与单 provider**，M5 补齐多家与韧性——避免一开始过度工程。

---

## 附录 A · ADR 关键决策记录

见 §1.2 摘要表；正式实现期每条 ADR 单独建档（背景/选项/决策/后果）。核心十条：模块化单体、库为真相源、自建网关、声明式 Agent、写章纯函数、Postgres 单库（确定性选择/无向量）、网关统一结构化输出、前端 Next + 后端 FastAPI 分离、BackgroundTasks 长任务、LangGraph 编排。

## 附录 B · ARCHITECTURE ↔ PRODUCT_SPEC 覆盖矩阵

| PRODUCT_SPEC 章节 | 对应 ARCHITECTURE |
|---|---|
| §1 问题 / §1.5 哲学 / §2 目标 | §1.1 NFR / §1.2 决策 |
| §2.5 功能总览 | §12 路线（按 P0–P2 落地） |
| §3.1 技术栈 | §2 技术栈与工程结构 / §10 部署 |
| §3.2 数据模型 | §3 数据架构（完整 DDL） |
| §3.3/§3.4/§3.5 网关/直连/缓存 | §4 LLM 网关架构 |
| §3.6 编排示意 | §5.2 编排器 |
| §4 四大模块 + 角色生成 | §6 核心模块详细设计 |
| §5 多 Agent | §5 Agent 编排引擎 |
| §6 接口 | §7 API 设计 |
| §7 规则积累 | §5.3 四级规则合并 + §7 rules 端点 |
| §8 差异化 | （产品论证，不在架构展开） |
| §9 实施路线 | §12 实施路线（架构视角） |
| §10 风险 | §9 横切关注点（安全/韧性/合规） |
| UX_SPEC | §8 前端架构 |

## 附录 C · 术语表

| 术语 | 含义 |
|---|---|
| 档位(tier) | 能力等级（写手/分析/轻量），网关映射到 provider+model |
| 适配器 | 把统一 LlmRequest 翻译成某厂商 API 的模块 |
| 真相源 | 数据库——Agent 间唯一交换媒介 |
| 四审 | continuity/foreshadow/style/pace 四个并行质检 Agent |
| 验收 gate | 作者裁决后的事务性写回，AI 产出入库唯一入口 |
| 稳定内核 | prompt 中可缓存的稳定前缀（世界观硬规则+已定型角色+文风指纹） |

---

## 待回写规格的发现（已同步三份文档一致）

下列在架构下沉中暴露的上游缺口**已回写** PRODUCT_SPEC / UX_SPEC，三份文档现已一致：

1. ✅ **API 作用域**：章节端点统一为 `/projects/:id/chapters/:no/...`（PRODUCT_SPEC §6 + UX §3.2 已改）。
2. ✅ **向量检索砍除**：原型改确定性记忆选择（§3.4），删除 `embedding` 列/HNSW；PRODUCT_SPEC §3.2 已注明"向量检索为 P2，届时再加 embedding 列 + 嵌入服务"。
3. ✅ **回炉端点**：PRODUCT_SPEC §6 已增 `POST /projects/:id/chapters/:no/refine`（本架构 §7.2）。
4. ✅ **users 表**：PRODUCT_SPEC §3.2 已补 `users` 与 `projects.owner_id`（本架构 §3.1 运营表）。




