feat(skills): Scope B A2 — 4 竞品生成器注册表+上下文分派+端点接线

注册表 TOOLBOX +4 entry(continue/expand/de-ai/teardown,全 preview-only writes=[]);
ContextStrategy +with_prior_chapter/text_input;ToolGenerateRequest +text 字段。
toolbox_context 分派新策略(续写→build_continuation_context、原文→build_text_input_context);
路由续写经 chapter_repo 读最新 accepted/draft 正文注入 builder(core 不 import apps/api,仿 accept_op)。
预览仅 commit ledger 不写业务表(#3);只声明 tier(#2);system_prompt 进缓存前缀(#9)。
单测:4 生成器 generate 预览 + 续写读前文/草稿路由 + 扩写/降AI text 路由 + 拆书结构化;
更新 registry/strategy 计数测试(15 工具 / 6 策略)。门禁绿:ruff/format/mypy 196 files/pytest 168(api)+32(skills)。
This commit is contained in:
Yaojia Wang
2026-06-23 19:10:35 +02:00
parent 87dd09a797
commit a5351320a2
9 changed files with 417 additions and 15 deletions

View File

@@ -20,6 +20,7 @@ from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
from ww_agents import AgentSpec, Conflict, ContinuityReview, continuity_spec
from ww_core.domain.chapter_repo import ChapterRepo
from ww_core.domain.outline_write_repo import OutlineWriteRepo
from ww_core.domain.project_repo import ProjectRepo
from ww_core.domain.repositories import MemoryRepos, OutlineRepo
@@ -47,6 +48,7 @@ from ww_api.schemas.toolbox import (
from ww_api.services.credentials import STUB_OWNER_ID
from ww_api.services.project_deps import (
TierGatewayBuilder,
get_chapter_repo,
get_memory_repos,
get_outline_read_repo,
get_outline_write_repo,
@@ -74,6 +76,7 @@ _INGEST_ERRORS: dict[int | str, dict[str, Any]] = {
ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)]
MemoryReposDep = Annotated[MemoryRepos, Depends(get_memory_repos)]
OutlineReadRepoDep = Annotated[OutlineRepo, Depends(get_outline_read_repo)]
ChapterRepoDep = Annotated[ChapterRepo, Depends(get_chapter_repo)]
WorldWriteRepoDep = Annotated[WorldEntityWriteRepo, Depends(get_world_entity_write_repo)]
OutlineWriteRepoDep = Annotated[OutlineWriteRepo, Depends(get_outline_write_repo)]
GatewayBuilderDep = Annotated[TierGatewayBuilder, Depends(get_tier_gateway_builder)]
@@ -138,6 +141,23 @@ async def _chapter_beats(
return list(raw.get("beats", [])) if isinstance(raw, dict) else []
async def _prior_chapter_text(
chapter_repo: ChapterRepo, project_id: uuid.UUID, chapter_no: int
) -> str:
"""读续写承接的前文正文:优先该章最新 accepted无则草稿皆无 → 空串(端点降级)。
core 的 builder 不 import apps/api——正文在端点读 chapter 领域表后经参数注入
(仿链 accept_op 的注入模式),守不变量 #1DB 为真相源)。
"""
accepted = await chapter_repo.latest_accepted(project_id, chapter_no)
if accepted is not None and accepted.content.strip():
return accepted.content
draft = await chapter_repo.get_draft(project_id, chapter_no)
if draft is not None and draft.content.strip():
return draft.content
return ""
@router.post("/projects/{project_id}/skills/{tool_key}/generate", responses=_TOOL_ERRORS)
async def generate_with_tool(
project_id: uuid.UUID,
@@ -147,6 +167,7 @@ async def generate_with_tool(
project_repo: ProjectRepoDep,
memory: MemoryReposDep,
outline_repo: OutlineReadRepoDep,
chapter_repo: ChapterRepoDep,
build_gateway: GatewayBuilderDep,
session: SessionDep,
) -> ToolGeneratePreviewResponse:
@@ -170,11 +191,17 @@ async def generate_with_tool(
world_context = ""
beats: list[str] = []
prior_text = ""
if tool.context_strategy == "with_world":
world_context = await _world_context(memory, project_id)
elif tool.context_strategy == "with_outline_chapter":
chapter_no = body.chapter_no or 1
beats = await _chapter_beats(outline_repo, project_id, chapter_no)
elif tool.context_strategy == "with_prior_chapter":
# 续写:端点先读该章最新 accepted/draft 正文DB 为真相源,#1+ 该章节拍,注入 builder。
chapter_no = body.chapter_no or 1
prior_text = await _prior_chapter_text(chapter_repo, project_id, chapter_no)
beats = await _chapter_beats(outline_repo, project_id, chapter_no)
context = build_toolbox_context(
tool.context_strategy,
@@ -183,6 +210,8 @@ async def generate_with_tool(
world_context=world_context,
chapter_no=body.chapter_no,
beats=beats,
prior_text=prior_text,
text=body.text or "",
)
gateway = await build_gateway(spec.tier)