feat(b0): 注入透明可控版后端 — PUT override(pin/排除/recent_n) + 选择函数加参 + draft 同读
- selection.select_relevant_entities 加 pinned/excluded frozenset 入参;新理由 author_pin(末位) - assemble 加 override 关键字参(recent_n 覆盖回看章数 + pin/排除);GET/PUT/draft 同读同一覆盖(不变量 #6 作者兜底) - 新 domain/injection_repo.py(InjectionOverride/EntityRef/SqlInjectionOverrideRepo,upsert 只 flush) - 新表 chapter_injection(迁移 ad2c4c663daf,唯一 project_id+chapter_no;复用 outline 行已否决) - PUT/GET injection 端点 + InjectionOverrideRequest/回显 pinned/excluded;recent_n 1..20 校验 422 - 单测:selection pin/排除/exclude>pin + assemble override + 端点 PUT/404/422;regen TS 客户端 - 门禁绿:ruff/format · mypy 163 · alembic 无漂移 · pytest 476 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import httpx
|
||||
import pytest
|
||||
from fakes_projects import FakeProjectRepo
|
||||
from ww_api.services.credentials import STUB_OWNER_ID
|
||||
from ww_core.domain.injection_repo import InjectionOverride
|
||||
from ww_core.domain.project_repo import ProjectCreate
|
||||
from ww_core.domain.repositories import (
|
||||
CharacterView,
|
||||
@@ -81,6 +82,22 @@ class _SpecRepo:
|
||||
return ProjectSpecView(title="测试作品", premise="测试前提")
|
||||
|
||||
|
||||
class _FakeInjectionRepo:
|
||||
"""内存注入覆盖 repo(按 (project_id, chapter_no) 存最后一次 upsert)。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[tuple[str, int], InjectionOverride] = {}
|
||||
|
||||
async def get(self, project_id: uuid.UUID, chapter_no: int) -> InjectionOverride | None:
|
||||
return self._store.get((str(project_id), chapter_no))
|
||||
|
||||
async def upsert(
|
||||
self, project_id: uuid.UUID, chapter_no: int, override: InjectionOverride
|
||||
) -> InjectionOverride:
|
||||
self._store[(str(project_id), chapter_no)] = override
|
||||
return override
|
||||
|
||||
|
||||
def _memory_repos(
|
||||
*,
|
||||
outline: dict[int, OutlineView] | None = None,
|
||||
@@ -103,16 +120,25 @@ def _make_client(
|
||||
*,
|
||||
project_repo: FakeProjectRepo,
|
||||
memory_repos: MemoryRepos,
|
||||
injection_repo: _FakeInjectionRepo | None = None,
|
||||
) -> httpx.AsyncClient:
|
||||
import os
|
||||
|
||||
os.environ.setdefault("CREDENTIAL_ENC_KEY", "x" * 44)
|
||||
from fakes_projects import FakeSession
|
||||
from ww_api.main import create_app
|
||||
from ww_api.services.project_deps import get_memory_repos, get_project_repo
|
||||
from ww_api.services.project_deps import (
|
||||
get_injection_repo,
|
||||
get_memory_repos,
|
||||
get_project_repo,
|
||||
)
|
||||
from ww_db import get_session
|
||||
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_project_repo] = lambda: project_repo
|
||||
app.dependency_overrides[get_memory_repos] = lambda: memory_repos
|
||||
app.dependency_overrides[get_injection_repo] = lambda: injection_repo or _FakeInjectionRepo()
|
||||
app.dependency_overrides[get_session] = lambda: FakeSession()
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
return httpx.AsyncClient(transport=transport, base_url="http://test")
|
||||
|
||||
@@ -167,3 +193,68 @@ async def test_injection_no_outline_returns_empty() -> None:
|
||||
resp = await client.get(f"/projects/{pid}/chapters/9/injection")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["selected"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_injection_put_pin_excludes_and_reflects() -> None:
|
||||
repo = FakeProjectRepo()
|
||||
pid = await _create_project(repo)
|
||||
memory = _memory_repos(
|
||||
outline={3: OutlineView(volume=1, chapter_no=3, beats={})},
|
||||
characters=[
|
||||
CharacterView(name="林动", role="主角"),
|
||||
CharacterView(name="路人乙", role="龙套"),
|
||||
],
|
||||
)
|
||||
injection = _FakeInjectionRepo()
|
||||
client = _make_client(project_repo=repo, memory_repos=memory, injection_repo=injection)
|
||||
async with client:
|
||||
# pin 龙套路人乙(本无理由)、排除主角林动、近况回看设为 2。
|
||||
put = await client.put(
|
||||
f"/projects/{pid}/chapters/3/injection",
|
||||
json={
|
||||
"pinned": [{"kind": "character", "name": "路人乙"}],
|
||||
"excluded": [{"kind": "character", "name": "林动"}],
|
||||
"recent_n": 2,
|
||||
},
|
||||
)
|
||||
assert put.status_code == 200
|
||||
body = put.json()
|
||||
by_name = {e["name"]: e for e in body["selected"]}
|
||||
assert "路人乙" in by_name and "author_pin" in by_name["路人乙"]["reasons"]
|
||||
assert "林动" not in by_name # 排除生效
|
||||
assert body["recent_n"] == 2
|
||||
assert body["pinned"] == [{"kind": "character", "name": "路人乙"}]
|
||||
assert body["excluded"] == [{"kind": "character", "name": "林动"}]
|
||||
|
||||
# 覆盖已落 fake repo → GET 回放同一结果(看到的=写章用的)。
|
||||
got = await client.get(f"/projects/{pid}/chapters/3/injection")
|
||||
assert got.status_code == 200
|
||||
got_names = {e["name"] for e in got.json()["selected"]}
|
||||
assert "路人乙" in got_names and "林动" not in got_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_injection_put_unknown_project_404() -> None:
|
||||
repo = FakeProjectRepo()
|
||||
client = _make_client(project_repo=repo, memory_repos=_memory_repos())
|
||||
async with client:
|
||||
resp = await client.put(
|
||||
f"/projects/{uuid.uuid4()}/chapters/1/injection",
|
||||
json={"pinned": [], "excluded": [], "recent_n": None},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
assert resp.json()["error"]["code"] == ErrorCode.NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_injection_put_rejects_out_of_range_recent_n() -> None:
|
||||
repo = FakeProjectRepo()
|
||||
pid = await _create_project(repo)
|
||||
client = _make_client(project_repo=repo, memory_repos=_memory_repos())
|
||||
async with client:
|
||||
resp = await client.put(
|
||||
f"/projects/{pid}/chapters/1/injection",
|
||||
json={"pinned": [], "excluded": [], "recent_n": 999},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
@@ -67,6 +67,13 @@ class _StubProjectSpecRepo:
|
||||
return ProjectSpecView(title="测试作品", premise="测试前提")
|
||||
|
||||
|
||||
class _NoInjectionRepo:
|
||||
"""空注入覆盖 stub:draft 端点读它得 None(纯自动选择),不打真 DB。"""
|
||||
|
||||
async def get(self, project_id: uuid.UUID, chapter_no: int) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def _empty_memory_repos() -> MemoryRepos:
|
||||
return MemoryRepos(
|
||||
outline=_EmptyOutlineRepo(),
|
||||
@@ -92,6 +99,7 @@ def _make_client(
|
||||
from ww_api.main import create_app
|
||||
from ww_api.services.project_deps import (
|
||||
get_chapter_repo,
|
||||
get_injection_repo,
|
||||
get_memory_repos,
|
||||
get_project_repo,
|
||||
get_writer_gateway,
|
||||
@@ -106,6 +114,8 @@ def _make_client(
|
||||
app.dependency_overrides[get_chapter_repo] = lambda: chapter_repo
|
||||
app.dependency_overrides[get_memory_repos] = _empty_memory_repos
|
||||
app.dependency_overrides[get_writer_gateway] = lambda: gateway
|
||||
# draft 端点现读注入覆盖:注空 stub,避免单测打真 DB(无覆盖 → 纯自动选择)。
|
||||
app.dependency_overrides[get_injection_repo] = lambda: _NoInjectionRepo()
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
client = httpx.AsyncClient(transport=transport, base_url="http://test")
|
||||
return client, project_repo, chapter_repo, gateway
|
||||
|
||||
@@ -22,6 +22,7 @@ from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from ww_core.domain.chapter_repo import ChapterRepo
|
||||
from ww_core.domain.digest_repo import DigestAppendRepo
|
||||
from ww_core.domain.injection_repo import EntityRef, InjectionOverride, InjectionOverrideRepo
|
||||
from ww_core.domain.project_repo import ProjectCreate, ProjectRepo
|
||||
from ww_core.domain.repositories import MemoryRepos
|
||||
from ww_core.domain.review_repo import ReviewRepo
|
||||
@@ -41,7 +42,12 @@ 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.injection import (
|
||||
InjectionEntity,
|
||||
InjectionEntityRef,
|
||||
InjectionOverrideRequest,
|
||||
InjectionResponse,
|
||||
)
|
||||
from ww_api.schemas.projects import (
|
||||
AcceptRequest,
|
||||
AcceptResponse,
|
||||
@@ -67,6 +73,7 @@ from ww_api.services.project_deps import (
|
||||
get_chapter_repo,
|
||||
get_digest_append_repo,
|
||||
get_digest_gateway,
|
||||
get_injection_repo,
|
||||
get_memory_repos,
|
||||
get_project_repo,
|
||||
get_review_gateway,
|
||||
@@ -85,6 +92,7 @@ GatewayDep = Annotated[Gateway, Depends(get_writer_gateway)]
|
||||
ReviewGatewayDep = Annotated[Gateway, Depends(get_review_gateway)]
|
||||
DigestGatewayDep = Annotated[Gateway, Depends(get_digest_gateway)]
|
||||
MemoryReposDep = Annotated[MemoryRepos, Depends(get_memory_repos)]
|
||||
InjectionRepoDep = Annotated[InjectionOverrideRepo, Depends(get_injection_repo)]
|
||||
ReviewRepoDep = Annotated[ReviewRepo, Depends(get_review_repo)]
|
||||
DigestRepoDep = Annotated[DigestAppendRepo, Depends(get_digest_append_repo)]
|
||||
|
||||
@@ -126,36 +134,95 @@ async def get_project(project_id: uuid.UUID, repo: ProjectRepoDep) -> ProjectRes
|
||||
return _to_response(view)
|
||||
|
||||
|
||||
async def _injection_response(
|
||||
repos: MemoryRepos,
|
||||
project_id: uuid.UUID,
|
||||
chapter_no: int,
|
||||
override: InjectionOverride | None,
|
||||
) -> InjectionResponse:
|
||||
"""组装注入响应:以作者覆盖跑确定性 assemble → selected + override 回显(GET/PUT 共用)。"""
|
||||
context = await assemble(repos, project_id, chapter_no, override=override)
|
||||
selected = [
|
||||
InjectionEntity(kind=e.kind, name=e.name, reasons=list(e.reasons))
|
||||
for e in context.selection.selected
|
||||
]
|
||||
effective_n = override.recent_n if (override and override.recent_n) else RECENT_DIGEST_COUNT
|
||||
return InjectionResponse(
|
||||
project_id=project_id,
|
||||
chapter_no=chapter_no,
|
||||
selected=selected,
|
||||
recent_n=effective_n,
|
||||
pinned=[
|
||||
InjectionEntityRef(kind=r.kind, name=r.name)
|
||||
for r in (override.pinned if override else [])
|
||||
],
|
||||
excluded=[
|
||||
InjectionEntityRef(kind=r.kind, name=r.name)
|
||||
for r in (override.excluded if override else [])
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/chapters/{chapter_no}/injection")
|
||||
async def get_injection(
|
||||
project_id: uuid.UUID,
|
||||
chapter_no: int,
|
||||
repos: MemoryReposDep,
|
||||
project_repo: ProjectRepoDep,
|
||||
injection_repo: InjectionRepoDep,
|
||||
) -> InjectionResponse:
|
||||
"""本章注入透明(B0,读端点):回放确定性 SelectionTrace。无 LLM、无 commit。
|
||||
"""本章注入透明(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
|
||||
]
|
||||
override = await injection_repo.get(project_id, chapter_no)
|
||||
resp = await _injection_response(repos, project_id, chapter_no, override)
|
||||
log.info(
|
||||
"injection_read",
|
||||
project_id=str(project_id),
|
||||
chapter_no=chapter_no,
|
||||
selected_count=len(selected),
|
||||
selected_count=len(resp.selected),
|
||||
pinned_count=len(resp.pinned),
|
||||
excluded_count=len(resp.excluded),
|
||||
)
|
||||
return InjectionResponse(
|
||||
project_id=project_id,
|
||||
return resp
|
||||
|
||||
|
||||
@router.put("/{project_id}/chapters/{chapter_no}/injection")
|
||||
async def save_injection(
|
||||
project_id: uuid.UUID,
|
||||
chapter_no: int,
|
||||
body: InjectionOverrideRequest,
|
||||
repos: MemoryReposDep,
|
||||
project_repo: ProjectRepoDep,
|
||||
injection_repo: InjectionRepoDep,
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> InjectionResponse:
|
||||
"""本章注入覆盖(B0 可控版,写端点):存 pin/排除/recent_n → 回放新的确定性 selected。
|
||||
|
||||
项目不存在 → 404。upsert 只 flush,端点末尾 commit。覆盖是确定性输入,draft 端点同读,
|
||||
故「看到的=写章用的」仍成立(不变量 #6 的作者兜底)。
|
||||
"""
|
||||
if await project_repo.get(STUB_OWNER_ID, project_id) is None:
|
||||
raise AppError(ErrorCode.NOT_FOUND, f"project {project_id} not found")
|
||||
override = InjectionOverride(
|
||||
pinned=[EntityRef(kind=r.kind, name=r.name) for r in body.pinned],
|
||||
excluded=[EntityRef(kind=r.kind, name=r.name) for r in body.excluded],
|
||||
recent_n=body.recent_n,
|
||||
)
|
||||
saved = await injection_repo.upsert(project_id, chapter_no, override)
|
||||
await session.commit()
|
||||
log.info(
|
||||
"injection_saved",
|
||||
project_id=str(project_id),
|
||||
chapter_no=chapter_no,
|
||||
selected=selected,
|
||||
recent_n=RECENT_DIGEST_COUNT,
|
||||
pinned_count=len(saved.pinned),
|
||||
excluded_count=len(saved.excluded),
|
||||
recent_n=saved.recent_n,
|
||||
)
|
||||
return await _injection_response(repos, project_id, chapter_no, saved)
|
||||
|
||||
|
||||
def _encode_sse(event: SseEvent) -> str:
|
||||
@@ -171,11 +238,13 @@ async def stream_draft(
|
||||
request: Request,
|
||||
repos: MemoryReposDep,
|
||||
gateway: GatewayDep,
|
||||
injection_repo: InjectionRepoDep,
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> StreamingResponse:
|
||||
"""流式写章草稿:组装记忆 → 网关流 → 归一为 SSE 事件 → text/event-stream。"""
|
||||
"""流式写章草稿:组装记忆(含作者注入覆盖)→ 网关流 → 归一为 SSE 事件 → text/event-stream。"""
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
context = await assemble(repos, project_id, chapter_no)
|
||||
override = await injection_repo.get(project_id, chapter_no)
|
||||
context = await assemble(repos, project_id, chapter_no, override=override)
|
||||
log.info(
|
||||
"draft_stream_start",
|
||||
project_id=str(project_id),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""本章注入透明(B0)的响应 schema(C 扩 / ARCH §3.4、§5.3)。
|
||||
"""本章注入透明(B0)的请求/响应 schema(C 扩 / ARCH §3.4、§5.3)。
|
||||
|
||||
snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
|
||||
读取无 LLM、无 commit——仅回放 assemble() 的确定性 SelectionTrace(不变量 #6)。
|
||||
读取无 LLM、无 commit——回放 assemble() 的确定性 SelectionTrace(不变量 #6)。
|
||||
可控版(PUT)存作者覆盖:pin 强制纳入 / excluded 强制剔除 / recent_n 覆盖回看章数,
|
||||
本身仍是确定性输入,故「看到的=写章用的」成立。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -10,6 +12,10 @@ import uuid
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# recent_n 合法区间(回看近况摘要章数)——下限 1,上限防作者误填爆 token。
|
||||
RECENT_N_MIN = 1
|
||||
RECENT_N_MAX = 20
|
||||
|
||||
|
||||
class InjectionEntity(BaseModel):
|
||||
"""一个被注入本章上下文的实体 + 入选理由(喂给透明面板的徽标)。"""
|
||||
@@ -18,14 +24,39 @@ class InjectionEntity(BaseModel):
|
||||
name: str
|
||||
reasons: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="explicit_beat / main_character / recent_digest / foreshadow_window",
|
||||
description="explicit_beat/main_character/recent_digest/foreshadow_window/author_pin",
|
||||
)
|
||||
|
||||
|
||||
class InjectionEntityRef(BaseModel):
|
||||
"""作者 pin/排除点名的实体引用(kind + name)。"""
|
||||
|
||||
kind: str = Field(description="character / world_entity")
|
||||
name: str
|
||||
|
||||
|
||||
class InjectionOverrideRequest(BaseModel):
|
||||
"""PUT /projects/:id/chapters/:no/injection:作者对本章注入的覆盖。
|
||||
|
||||
pinned 强制纳入(加 author_pin 理由);excluded 强制剔除;recent_n 覆盖近况回看章数
|
||||
(None=用默认)。空覆盖 = 纯自动选择。
|
||||
"""
|
||||
|
||||
pinned: list[InjectionEntityRef] = Field(default_factory=list)
|
||||
excluded: list[InjectionEntityRef] = Field(default_factory=list)
|
||||
recent_n: int | None = Field(default=None, ge=RECENT_N_MIN, le=RECENT_N_MAX)
|
||||
|
||||
|
||||
class InjectionResponse(BaseModel):
|
||||
"""GET /projects/:id/chapters/:no/injection:本章确定性注入留痕。"""
|
||||
"""GET/PUT /projects/:id/chapters/:no/injection:本章注入留痕 + 作者覆盖回显。"""
|
||||
|
||||
project_id: uuid.UUID
|
||||
chapter_no: int
|
||||
selected: list[InjectionEntity] = Field(default_factory=list)
|
||||
recent_n: int = Field(description="近况摘要回看章数(确定性选择的默认参数)")
|
||||
recent_n: int = Field(description="生效的近况回看章数(默认或被 override 覆盖后的值)")
|
||||
pinned: list[InjectionEntityRef] = Field(
|
||||
default_factory=list, description="作者强制纳入的实体(回显)"
|
||||
)
|
||||
excluded: list[InjectionEntityRef] = Field(
|
||||
default_factory=list, description="作者强制剔除的实体(回显,供面板恢复)"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ from ww_core.domain import ForeshadowLedgerRepo, SqlForeshadowLedgerRepo
|
||||
from ww_core.domain.chapter_repo import ChapterRepo, SqlChapterRepo
|
||||
from ww_core.domain.character_repo import CharacterWriteRepo, SqlCharacterWriteRepo
|
||||
from ww_core.domain.digest_repo import DigestAppendRepo, SqlDigestAppendRepo
|
||||
from ww_core.domain.injection_repo import InjectionOverrideRepo, SqlInjectionOverrideRepo
|
||||
from ww_core.domain.job_repo import JobRepo, SqlJobRepo
|
||||
from ww_core.domain.outline_write_repo import OutlineWriteRepo, SqlOutlineWriteRepo
|
||||
from ww_core.domain.project_repo import ProjectRepo, SqlProjectRepo
|
||||
@@ -102,6 +103,16 @@ def get_memory_repos(
|
||||
return sql_memory_repos(session)
|
||||
|
||||
|
||||
def get_injection_repo(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> InjectionOverrideRepo:
|
||||
"""本章注入覆盖读/写 repo(B0 可控版 GET/PUT injection + draft 同读)。
|
||||
|
||||
upsert 只 flush,端点提交(与其它写侧一致)。测试经 `app.dependency_overrides` 注 fake。
|
||||
"""
|
||||
return SqlInjectionOverrideRepo(session)
|
||||
|
||||
|
||||
def get_review_repo(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> ReviewRepo:
|
||||
|
||||
93
apps/web/lib/api/schema.d.ts
vendored
93
apps/web/lib/api/schema.d.ts
vendored
@@ -99,12 +99,19 @@ export interface paths {
|
||||
};
|
||||
/**
|
||||
* Get Injection
|
||||
* @description 本章注入透明(B0,读端点):回放确定性 SelectionTrace。无 LLM、无 commit。
|
||||
* @description 本章注入透明(B0,读端点):回放确定性 SelectionTrace + 作者覆盖。无 LLM、无 commit。
|
||||
*
|
||||
* 项目不存在 → 404;无大纲 → selected: [](不报错)。看到的=写章用的(不变量 #6)。
|
||||
*/
|
||||
get: operations["get_injection_projects__project_id__chapters__chapter_no__injection_get"];
|
||||
put?: never;
|
||||
/**
|
||||
* Save Injection
|
||||
* @description 本章注入覆盖(B0 可控版,写端点):存 pin/排除/recent_n → 回放新的确定性 selected。
|
||||
*
|
||||
* 项目不存在 → 404。upsert 只 flush,端点末尾 commit。覆盖是确定性输入,draft 端点同读,
|
||||
* 故「看到的=写章用的」仍成立(不变量 #6 的作者兜底)。
|
||||
*/
|
||||
put: operations["save_injection_projects__project_id__chapters__chapter_no__injection_put"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -135,7 +142,7 @@ export interface paths {
|
||||
put: operations["save_draft_projects__project_id__chapters__chapter_no__draft_put"];
|
||||
/**
|
||||
* Stream Draft
|
||||
* @description 流式写章草稿:组装记忆 → 网关流 → 归一为 SSE 事件 → text/event-stream。
|
||||
* @description 流式写章草稿:组装记忆(含作者注入覆盖)→ 网关流 → 归一为 SSE 事件 → text/event-stream。
|
||||
*/
|
||||
post: operations["stream_draft_projects__project_id__chapters__chapter_no__draft_post"];
|
||||
delete?: never;
|
||||
@@ -994,13 +1001,41 @@ export interface components {
|
||||
name: string;
|
||||
/**
|
||||
* Reasons
|
||||
* @description explicit_beat / main_character / recent_digest / foreshadow_window
|
||||
* @description explicit_beat/main_character/recent_digest/foreshadow_window/author_pin
|
||||
*/
|
||||
reasons?: string[];
|
||||
};
|
||||
/**
|
||||
* InjectionEntityRef
|
||||
* @description 作者 pin/排除点名的实体引用(kind + name)。
|
||||
*/
|
||||
InjectionEntityRef: {
|
||||
/**
|
||||
* Kind
|
||||
* @description character / world_entity
|
||||
*/
|
||||
kind: string;
|
||||
/** Name */
|
||||
name: string;
|
||||
};
|
||||
/**
|
||||
* InjectionOverrideRequest
|
||||
* @description PUT /projects/:id/chapters/:no/injection:作者对本章注入的覆盖。
|
||||
*
|
||||
* pinned 强制纳入(加 author_pin 理由);excluded 强制剔除;recent_n 覆盖近况回看章数
|
||||
* (None=用默认)。空覆盖 = 纯自动选择。
|
||||
*/
|
||||
InjectionOverrideRequest: {
|
||||
/** Pinned */
|
||||
pinned?: components["schemas"]["InjectionEntityRef"][];
|
||||
/** Excluded */
|
||||
excluded?: components["schemas"]["InjectionEntityRef"][];
|
||||
/** Recent N */
|
||||
recent_n?: number | null;
|
||||
};
|
||||
/**
|
||||
* InjectionResponse
|
||||
* @description GET /projects/:id/chapters/:no/injection:本章确定性注入留痕。
|
||||
* @description GET/PUT /projects/:id/chapters/:no/injection:本章注入留痕 + 作者覆盖回显。
|
||||
*/
|
||||
InjectionResponse: {
|
||||
/**
|
||||
@@ -1014,9 +1049,19 @@ export interface components {
|
||||
selected?: components["schemas"]["InjectionEntity"][];
|
||||
/**
|
||||
* Recent N
|
||||
* @description 近况摘要回看章数(确定性选择的默认参数)
|
||||
* @description 生效的近况回看章数(默认或被 override 覆盖后的值)
|
||||
*/
|
||||
recent_n: number;
|
||||
/**
|
||||
* Pinned
|
||||
* @description 作者强制纳入的实体(回显)
|
||||
*/
|
||||
pinned?: components["schemas"]["InjectionEntityRef"][];
|
||||
/**
|
||||
* Excluded
|
||||
* @description 作者强制剔除的实体(回显,供面板恢复)
|
||||
*/
|
||||
excluded?: components["schemas"]["InjectionEntityRef"][];
|
||||
};
|
||||
/**
|
||||
* OAuthDisconnectResponse
|
||||
@@ -1689,6 +1734,42 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
save_injection_projects__project_id__chapters__chapter_no__injection_put: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
project_id: string;
|
||||
chapter_no: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["InjectionOverrideRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["InjectionResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_draft_projects__project_id__chapters__chapter_no__draft_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user