feat(ux): B0 注入透明读端点 + F1 写作页右栏真面板
兑现「看到的=写章用的」信任牌(不变量 #6),替换写作页右栏过时假占位
(「M1 暂未接 / M2 开放」)。
后端:
- GET /projects/{id}/chapters/{no}/injection → InjectionResponse(selected
实体 + 入选理由 + recent_n);调既有 assemble() 回放确定性 SelectionTrace,
无 LLM / 无 commit / 无 DDL;项目不存在→404,无大纲→selected:[]。
- 新 schemas/injection.py;tests/test_injection.py(3 测)。
前端:
- gen:api 纳入端点;纯逻辑 lib/workbench/injection.ts(理由/类型→中文徽标
+ 4 vitest)+ useInjection 读 hook。
- ChapterAssistant 改 client 组件:列出选中实体 + 理由徽标 + 空/载/错三态,
四审段指向审稿页;Workbench 传 projectId/chapterNo。
门禁:后端 ruff/format/mypy 159/alembic 无漂移/pytest 454;
前端 lint/typecheck/vitest 167/build。
B0 可控版(PUT override pin/排除/recent_n + selection 加参 + draft 同读
override + 持久化)+ F1 可控控件待后续。
This commit is contained in:
@@ -26,6 +26,7 @@ from ww_core.domain.project_repo import ProjectCreate, ProjectRepo
|
||||
from ww_core.domain.repositories import MemoryRepos
|
||||
from ww_core.domain.review_repo import ReviewRepo
|
||||
from ww_core.memory import assemble
|
||||
from ww_core.memory.selection import RECENT_DIGEST_COUNT
|
||||
from ww_core.orchestrator import (
|
||||
ChapterState,
|
||||
SseEvent,
|
||||
@@ -40,6 +41,7 @@ from ww_llm_gateway import Gateway
|
||||
from ww_shared import AppError, ErrorCode
|
||||
|
||||
from ww_api.logging_config import get_logger
|
||||
from ww_api.schemas.injection import InjectionEntity, InjectionResponse
|
||||
from ww_api.schemas.projects import (
|
||||
AcceptRequest,
|
||||
AcceptResponse,
|
||||
@@ -124,6 +126,38 @@ async def get_project(project_id: uuid.UUID, repo: ProjectRepoDep) -> ProjectRes
|
||||
return _to_response(view)
|
||||
|
||||
|
||||
@router.get("/{project_id}/chapters/{chapter_no}/injection")
|
||||
async def get_injection(
|
||||
project_id: uuid.UUID,
|
||||
chapter_no: int,
|
||||
repos: MemoryReposDep,
|
||||
project_repo: ProjectRepoDep,
|
||||
) -> InjectionResponse:
|
||||
"""本章注入透明(B0,读端点):回放确定性 SelectionTrace。无 LLM、无 commit。
|
||||
|
||||
项目不存在 → 404;无大纲 → selected: [](不报错)。看到的=写章用的(不变量 #6)。
|
||||
"""
|
||||
if await project_repo.get(STUB_OWNER_ID, project_id) is None:
|
||||
raise AppError(ErrorCode.NOT_FOUND, f"project {project_id} not found")
|
||||
context = await assemble(repos, project_id, chapter_no)
|
||||
selected = [
|
||||
InjectionEntity(kind=e.kind, name=e.name, reasons=list(e.reasons))
|
||||
for e in context.selection.selected
|
||||
]
|
||||
log.info(
|
||||
"injection_read",
|
||||
project_id=str(project_id),
|
||||
chapter_no=chapter_no,
|
||||
selected_count=len(selected),
|
||||
)
|
||||
return InjectionResponse(
|
||||
project_id=project_id,
|
||||
chapter_no=chapter_no,
|
||||
selected=selected,
|
||||
recent_n=RECENT_DIGEST_COUNT,
|
||||
)
|
||||
|
||||
|
||||
def _encode_sse(event: SseEvent) -> str:
|
||||
"""把归一事件编码为 text/event-stream 帧:`event: <name>\\ndata: <json>\\n\\n`。"""
|
||||
payload = json.dumps(event.data, ensure_ascii=False)
|
||||
|
||||
31
apps/api/ww_api/schemas/injection.py
Normal file
31
apps/api/ww_api/schemas/injection.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""本章注入透明(B0)的响应 schema(C 扩 / ARCH §3.4、§5.3)。
|
||||
|
||||
snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
|
||||
读取无 LLM、无 commit——仅回放 assemble() 的确定性 SelectionTrace(不变量 #6)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class InjectionEntity(BaseModel):
|
||||
"""一个被注入本章上下文的实体 + 入选理由(喂给透明面板的徽标)。"""
|
||||
|
||||
kind: str = Field(description="character / world_entity")
|
||||
name: str
|
||||
reasons: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="explicit_beat / main_character / recent_digest / foreshadow_window",
|
||||
)
|
||||
|
||||
|
||||
class InjectionResponse(BaseModel):
|
||||
"""GET /projects/:id/chapters/:no/injection:本章确定性注入留痕。"""
|
||||
|
||||
project_id: uuid.UUID
|
||||
chapter_no: int
|
||||
selected: list[InjectionEntity] = Field(default_factory=list)
|
||||
recent_n: int = Field(description="近况摘要回看章数(确定性选择的默认参数)")
|
||||
Reference in New Issue
Block a user