feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
apps/api/ww_api/schemas/generation.py
Normal file
163
apps/api/ww_api/schemas/generation.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""生成/入库端点的请求/响应 schema(C3 扩 / ARCH §6.5 / §7.2)。
|
||||
|
||||
snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
|
||||
|
||||
生成走**即时返回**(预览 → 作者确认 → 入库),非 jobs 长任务(M5-d):
|
||||
- `POST /world/generate` / `POST /characters/generate`:返回**预览**(不持久化)。
|
||||
- `POST /characters`(入库):作者确认的角色卡 → 过 continuity 预检 gate + 权限白名单 → 写库。
|
||||
|
||||
视图字段贴 `ww_agents.WorldEntityCard`/`CharacterCard`(保持生成产物形:traits/speech_tics
|
||||
是 list、arc 是 str;入库时由写侧 repo 转 DB JSONB 形,见 character_repo)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ww_api.schemas.rules import RuleView
|
||||
|
||||
# 批量生成数量上限(防一次性巨量调用;原型保守值)。
|
||||
MAX_GENERATE_COUNT = 12
|
||||
|
||||
|
||||
# ---- 世界观生成(预览)----
|
||||
|
||||
|
||||
class WorldGenerateRequest(BaseModel):
|
||||
"""POST /projects/:id/world/generate:据需求生成世界观实体(预览)。"""
|
||||
|
||||
brief: str = Field(min_length=1, description="世界观生成需求(题材/设定方向/约束)")
|
||||
|
||||
|
||||
class WorldEntityCardView(BaseModel):
|
||||
"""单个世界观实体卡(贴 ww_agents.WorldEntityCard;预览 + 入库共用形)。"""
|
||||
|
||||
type: str = Field(description="实体类型(势力 / 地理 / 力量体系 / 物品 / 概念 等)")
|
||||
name: str = Field(description="实体名")
|
||||
rules: list[str] = Field(default_factory=list, description="该实体的硬规则清单")
|
||||
|
||||
|
||||
class WorldGenPreviewResponse(BaseModel):
|
||||
"""世界观生成预览(不持久化;作者确认后另走入库,本期入库端点为角色)。"""
|
||||
|
||||
entities: list[WorldEntityCardView] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---- 角色生成(预览)----
|
||||
|
||||
|
||||
class CharacterGenerateRequest(BaseModel):
|
||||
"""POST /projects/:id/characters/generate:据需求生成角色卡(预览,群像防雷同)。"""
|
||||
|
||||
brief: str = Field(min_length=1, description="角色生成需求")
|
||||
count: int = Field(default=1, ge=1, le=MAX_GENERATE_COUNT, description="生成数量")
|
||||
role: str | None = Field(default=None, description="角色定位(主角/CP/对手/导师/工具人 等)")
|
||||
|
||||
|
||||
class CharacterRelationView(BaseModel):
|
||||
"""单条人物关系(贴 ww_agents.CharacterRelation)。"""
|
||||
|
||||
name: str
|
||||
kind: str
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class CharacterCardView(BaseModel):
|
||||
"""单张角色卡(贴 ww_agents.CharacterCard;预览 + 入库请求共用形)。"""
|
||||
|
||||
name: str = Field(description="角色名")
|
||||
role: str = Field(description="角色定位")
|
||||
traits: list[str] = Field(default_factory=list, description="性格特质清单")
|
||||
backstory: str = Field(description="背景故事")
|
||||
arc: str = Field(description="人物弧光(一句话)")
|
||||
speech_tics: list[str] = Field(default_factory=list, description="口癖/语言风格")
|
||||
tags: list[str] = Field(default_factory=list, description="人设标签/萌点")
|
||||
relations: list[CharacterRelationView] = Field(default_factory=list, description="关系网")
|
||||
|
||||
|
||||
class CharacterGenPreviewResponse(BaseModel):
|
||||
"""角色生成预览(不持久化;作者确认后经 POST /characters 入库)。"""
|
||||
|
||||
cards: list[CharacterCardView] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---- 角色入库(作者确认后;过 continuity gate + 权限白名单)----
|
||||
|
||||
|
||||
class CharacterIngestRequest(BaseModel):
|
||||
"""POST /projects/:id/characters:把作者确认的角色卡入库。
|
||||
|
||||
`acknowledge_conflicts`:作者已查看并接受预检冲突时置 `true`,越过 continuity gate
|
||||
(仿 accept 的冲突裁决:不静默入库,须作者确认;默认 false → 有冲突即 409,见端点)。
|
||||
"""
|
||||
|
||||
cards: list[CharacterCardView] = Field(min_length=1, description="待入库的角色卡(至少 1 张)")
|
||||
acknowledge_conflicts: bool = Field(
|
||||
default=False, description="作者已知悉并接受 continuity 冲突 → 放行入库"
|
||||
)
|
||||
|
||||
|
||||
class IngestConflictView(BaseModel):
|
||||
"""入库预检检出的单条 continuity 冲突(贴 ww_agents.Conflict 五类)。"""
|
||||
|
||||
type: str
|
||||
where: str
|
||||
refs: list[str] = Field(default_factory=list)
|
||||
suggestion: str
|
||||
|
||||
|
||||
class CharacterIngestResponse(BaseModel):
|
||||
"""角色入库结果(201):写入的角色 + 被白名单丢弃的越权表(审计)。"""
|
||||
|
||||
created: list[str] = Field(default_factory=list, description="写入的角色名(按入参顺序)")
|
||||
rejected_tables: list[str] = Field(
|
||||
default_factory=list, description="被权限白名单丢弃的越权写表名(审计;正常为空)"
|
||||
)
|
||||
|
||||
|
||||
# ---- 读端点:设定库 Codex(角色 / 世界观全量列表)----
|
||||
|
||||
|
||||
class CharacterListResponse(BaseModel):
|
||||
"""GET /projects/:id/characters:已入库角色全量列表(设定库 Codex 真源)。
|
||||
|
||||
DB JSONB dict 列 → API list/str 反向解包(入库形变的逆向,见 character_repo):
|
||||
`{"items":[...]}`→list、`{"text":...}`→str;tags/relations 直落 list。
|
||||
"""
|
||||
|
||||
characters: list[CharacterCardView] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WorldEntityListResponse(BaseModel):
|
||||
"""GET /projects/:id/world_entities:已入库世界观实体全量列表(设定库 Codex 真源)。
|
||||
|
||||
DB JSONB dict 列 `{"rules":[...]}`→裸 list(worldbuilder 形变的逆向)。
|
||||
"""
|
||||
|
||||
world_entities: list[WorldEntityCardView] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---- 读端点:规则列表 + 技能库(T5.6 前端)----
|
||||
|
||||
|
||||
class RuleListResponse(BaseModel):
|
||||
"""GET /projects/:id/rules:规则列表(规则页)。"""
|
||||
|
||||
rules: list[RuleView] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillView(BaseModel):
|
||||
"""单个 skill 的声明式视图(技能库 UI)。"""
|
||||
|
||||
name: str
|
||||
scope: str = Field(description="builtin / custom / community")
|
||||
tier: str = Field(description="能力档位(writer / analyst / light)")
|
||||
reads: list[str] = Field(default_factory=list)
|
||||
writes: list[str] = Field(default_factory=list)
|
||||
genre: str | None = None
|
||||
|
||||
|
||||
class SkillListResponse(BaseModel):
|
||||
"""GET /skills:技能库列表(按 name 升序)。"""
|
||||
|
||||
skills: list[SkillView] = Field(default_factory=list)
|
||||
39
apps/api/ww_api/schemas/kimi_oauth.py
Normal file
39
apps/api/ww_api/schemas/kimi_oauth.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Kimi Code OAuth device-flow 端点的响应 schema(C3 扩 K1.3 / ARCH §7.2 / §7.4)。
|
||||
|
||||
snake_case(命名契约,见 memory/gotchas)。前端经 OpenAPI→TS 客户端消费——改字段须
|
||||
`pnpm gen:api` 重生成。**响应绝不含 access/refresh token**(只 user_code + 轮询信息)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OAuthStartResponse(BaseModel):
|
||||
"""device flow 启动:返回 job_id(轮询 `GET /jobs/{id}`)+ 用户面展示信息。
|
||||
|
||||
前端展示 `user_code`、打开 `verification_uri`(或 `verification_uri_complete`),并按
|
||||
`interval` 轮询 job 直到 `done`(已连接)/`failed`(过期/拒绝)。**无 token**。
|
||||
"""
|
||||
|
||||
job_id: uuid.UUID
|
||||
user_code: str
|
||||
verification_uri: str
|
||||
verification_uri_complete: str | None = None
|
||||
expires_in: int
|
||||
interval: int
|
||||
|
||||
|
||||
class OAuthStatusResponse(BaseModel):
|
||||
"""连接状态:是否已连接 + access token 过期时刻(ISO8601;**无 token 本体**)。"""
|
||||
|
||||
connected: bool
|
||||
expires_at: str | None = None
|
||||
|
||||
|
||||
class OAuthDisconnectResponse(BaseModel):
|
||||
"""断开:是否删到凭据行。"""
|
||||
|
||||
disconnected: bool
|
||||
@@ -59,6 +59,22 @@ class DraftResponse(BaseModel):
|
||||
length: int
|
||||
|
||||
|
||||
class DraftView(BaseModel):
|
||||
"""GET .../draft:回灌已保存草稿(含正文,供工作台重载编辑器)。
|
||||
|
||||
与 PUT 的 `DraftResponse` 同元信息,**额外带 `content`**——读侧需要正文以重建编辑器,
|
||||
保存侧不回灌正文故不带。`length` 仍按 content 字符数派生。
|
||||
"""
|
||||
|
||||
project_id: uuid.UUID
|
||||
chapter_no: int
|
||||
volume: int
|
||||
status: str
|
||||
version: int
|
||||
content: str
|
||||
length: int
|
||||
|
||||
|
||||
# ---- 审稿(T2.5)----
|
||||
|
||||
|
||||
|
||||
28
apps/api/ww_api/schemas/rules.py
Normal file
28
apps/api/ww_api/schemas/rules.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""规则端点的请求/响应 schema(C3 扩 / PRODUCT_SPEC §7 POST /rules)。
|
||||
|
||||
snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
|
||||
`level` ∈ global/genre/style/project(四级合并优先级,见 memory `merge_rules`)。
|
||||
加规则是作者显式动作——审稿发现的问题/亮点随手沉淀为规则(非 AI 静默写库,不变量 #3)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
RuleLevel = Literal["global", "genre", "style", "project"]
|
||||
|
||||
|
||||
class RuleCreateRequest(BaseModel):
|
||||
"""POST /projects/:id/rules:新增一条规则。"""
|
||||
|
||||
level: RuleLevel = Field(description="规则级别(global/genre/style/project,越具体越优先)")
|
||||
content: str = Field(min_length=1, description="规则正文")
|
||||
|
||||
|
||||
class RuleView(BaseModel):
|
||||
"""规则视图(创建后回显;snake_case)。"""
|
||||
|
||||
level: str
|
||||
content: str
|
||||
47
apps/api/ww_api/schemas/style.py
Normal file
47
apps/api/ww_api/schemas/style.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""文风学习 + 回炉端点的请求/响应 schema(C3 扩 / ARCH §7.2 / UX §6.9 / §8.3)。
|
||||
|
||||
snake_case(命名契约,见 memory/gotchas)。前端经 OpenAPI→TS 客户端消费——改字段
|
||||
须 `pnpm gen:api` 重生成。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class StyleLearnRequest(BaseModel):
|
||||
"""学文风:上传样本正文(前端可读文件转文本)+ 模式(首学 / 更新)。"""
|
||||
|
||||
samples: list[str] = Field(min_length=1)
|
||||
mode: Literal["create", "update"] = "create"
|
||||
|
||||
|
||||
class StyleLearnResponse(BaseModel):
|
||||
"""学文风受理:返回 job_id(长任务,走 `GET /jobs/{id}` 轮询)。"""
|
||||
|
||||
job_id: uuid.UUID
|
||||
|
||||
|
||||
class StyleFingerprintResponse(BaseModel):
|
||||
"""最新文风指纹(`GET /style`):完整 16 维 + 证据 + 版本(对齐 UX §6.9)。"""
|
||||
|
||||
dimensions: dict[str, Any] = Field(default_factory=dict)
|
||||
evidence: dict[str, Any] = Field(default_factory=dict)
|
||||
version: int
|
||||
|
||||
|
||||
class RefineRequest(BaseModel):
|
||||
"""回炉:重写选中段(可选改写指令)。"""
|
||||
|
||||
segment: str = Field(min_length=1)
|
||||
instruction: str | None = None
|
||||
|
||||
|
||||
class RefineResponse(BaseModel):
|
||||
"""回炉结果:原段 + 重写段(不写库,作者采纳经既有 draft 自动保存合入)。"""
|
||||
|
||||
original: str
|
||||
refined: str
|
||||
Reference in New Issue
Block a user