Files
writer-work-flow/apps/api/ww_api/schemas/projects.py
Yaojia Wang 345cc73965 fix(txn+security): 仓储改 flush + 启动校验/兜底 + job.error 脱敏 + SSE 异常硬化
P0-1 SqlCredentialStore/save_draft 由自提交改 flush,端点/服务统一 commit
  (新增 CredentialStore.commit() 统一提交点;token 刷新落库显式提交);
  补多凭据一请求中途失败整体回滚集成测试。
P0-2 启动校验 _fernet(enc_key) 快速失败 + catch-all Exception → ErrorEnvelope;
  credential_enc_key 改 SecretStr。
P0-3 run_job 异常分类:AppError 存 code+message,其余存通用文案不泄 str(exc)。
P0-4 评审/正文 SSE 失败先发 error 事件,尾部 commit 包 try/except。
P1-4 max_version 加 FOR UPDATE 行锁消除 TOCTOU。
P1-5 scan_overdue 谓词下推 + 批量 UPDATE RETURNING。
P1-10 移除 OAuth user_code 日志。
P2 provider_deps 改调网关 build_adapter;accept_service Committable Protocol;
  CORS 白名单收窄;request_id 安全字符集白名单;stdlib 日志接管;读端点 404 校验;
  httpx timeout;测试用合法 Fernet key;类型化响应模型(JobResponse/DimensionEntry/
  ReviewConflictView/selling_points)+路由 ErrorEnvelope responses(供 codegen)。
2026-06-21 19:32:24 +02:00

165 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""项目(立项)与章节草稿的请求/响应 schemaC3 / ARCH §7.2)。
snake_case前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
"""
from __future__ import annotations
import uuid
from typing import Any, Literal
from pydantic import BaseModel, Field
class ProjectCreateRequest(BaseModel):
"""POST /projects立项向导字段owner_id 由后端补 stub不入参"""
title: str = Field(min_length=1)
genre: str | None = None
logline: str | None = None
premise: str | None = None
theme: str | None = None
selling_points: list[str] = Field(default_factory=list)
structure: str | None = None
class ProjectResponse(BaseModel):
"""项目视图(创建/列表/详情共用)。"""
id: uuid.UUID
title: str
genre: str | None = None
logline: str | None = None
premise: str | None = None
theme: str | None = None
selling_points: list[str] = Field(default_factory=list)
structure: str | None = None
class ProjectListResponse(BaseModel):
"""GET /projects项目列表。"""
projects: list[ProjectResponse] = Field(default_factory=list)
class DraftStreamRequest(BaseModel):
"""POST /projects/:id/chapters/:no/draft本章生成的可选输入T4-b
`directive` = 作者本章指令(覆盖/补充大纲节拍)。临时输入,不持久化、无迁移;
后端只入 volatile守缓存前缀不变量 #9。无 body 时退化为纯按大纲生成(向后兼容)。
"""
directive: str | None = None
class DraftSaveRequest(BaseModel):
"""PUT /projects/:id/chapters/:no/draft自动保存草稿正文。"""
text: str
class DraftResponse(BaseModel):
"""草稿保存结果(脱敏:只回元信息 + 长度,不回灌正文以外的衍生)。"""
project_id: uuid.UUID
chapter_no: int
volume: int
status: str
version: int
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----
class ReviewRequest(BaseModel):
"""POST /projects/:id/chapters/:no/review可选携带待审草稿正文。
不传 `draft` 时端点回退到已保存的草稿chapter_repo
"""
draft: str | None = None
class ReviewConflictView(BaseModel):
"""单条一致性冲突chapter_reviews.conflicts JSONB 子项;形对齐 SSE `conflict` 事件)。
强类型化此前的裸 `dict[str, Any]`,给 TS 客户端真实字段类型。容忍历史/缺省(默认值)。
"""
type: str = ""
where: str = ""
refs: list[str] = Field(default_factory=list)
suggestion: str = ""
class ReviewHistoryItem(BaseModel):
"""单条审稿留痕GET .../reviews 历史项snake_case"""
id: uuid.UUID
project_id: uuid.UUID
chapter_no: int
chapter_version: int | None = None
conflicts: list[ReviewConflictView] = Field(default_factory=list)
foreshadow_sug: list[dict[str, Any]] = Field(default_factory=list)
style: dict[str, Any] | None = None
pace: dict[str, Any] | None = None
health_score: int | None = None
decisions: dict[str, Any] | None = None
class ReviewHistoryResponse(BaseModel):
"""GET /projects/:id/chapters/:no/reviews审稿历史新→旧"""
reviews: list[ReviewHistoryItem] = Field(default_factory=list)
# ---- 验收T2.4----
# 每个冲突的裁决:采纳改法 / 忽略 / 手改R5
Verdict = Literal["accept", "ignore", "manual"]
class ConflictDecision(BaseModel):
"""对最近一次审稿留痕里**某个冲突**(按其在 conflicts 列表的下标定位)的裁决。"""
conflict_index: int = Field(ge=0, description="冲突在最近审稿 conflicts 列表中的下标")
verdict: Verdict = Field(description="采纳改法 / 忽略 / 手改")
note: str | None = Field(default=None, description="可选裁决备注(如手改说明)")
class AcceptRequest(BaseModel):
"""POST /projects/:id/chapters/:no/accept裁决清单 + 可能改过的终稿。"""
final_text: str = Field(min_length=1, description="作者裁决/改稿后的最终验收文本")
decisions: list[ConflictDecision] = Field(
default_factory=list, description="对最近审稿每个冲突的裁决每冲突必有其一R5"
)
class AcceptResponse(BaseModel):
"""验收「本次将更新」清单ARCH §7.2 写回结果)。"""
project_id: uuid.UUID
chapter_no: int
accepted_version: int = Field(description="晋升到的 accepted 版次max+1")
digest_added: bool = Field(description="是否新增了一行 chapter_digests")
decisions_recorded: int = Field(description="本次写回的裁决条数")
review_id: uuid.UUID | None = Field(default=None, description="写回裁决的审稿留痕行 id")