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:
Yaojia Wang
2026-06-20 11:56:54 +02:00
parent e8cccf7389
commit 0d473e726e
16 changed files with 681 additions and 41 deletions

View File

@@ -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