perf(backend): 角色/世界观入库改批量——create_many 一次 flush 落全部行

角色入库端点与 toolbox world_entities 入库端点原逐卡/逐条 create(每行一次
SELECT/flush/refresh,N+1 round-trip)。新增 create_many:世界观用 add_all,
角色用一次 IN 查已有名 + 一次 flush(保持 (project_id,name) upsert 幂等与批内
重名去重、保序返回)。单条 create 薄封装 create_many,行为不变(既有调用方/E2E
复用)。复活并复用 CharacterWriteFields、新增 WorldEntityFields 作批量入参。
补两端点「多行一次落库」测试;既有幂等/冲突 gate 测试与 m5 真 pg E2E 全绿。
This commit is contained in:
Yaojia Wang
2026-07-08 12:44:07 +02:00
parent 0892ff6cda
commit bf3dabea82
7 changed files with 221 additions and 71 deletions

View File

@@ -28,7 +28,7 @@ from ww_agents import (
WorldEntityCard,
WorldGenResult,
)
from ww_core.domain.character_repo import CharacterWriteView
from ww_core.domain.character_repo import CharacterWriteFields, CharacterWriteView
from ww_core.domain.project_repo import ProjectCreate
from ww_core.domain.rule_repo import RuleListItemView
from ww_llm_gateway.types import LlmRequest, LlmResponse, ServedBy, Usage
@@ -120,6 +120,27 @@ class _FakeCharacterWriteRepo:
)
return CharacterWriteView(id=row_id, name=name, role=role)
async def create_many(
self, project_id: uuid.UUID, cards: list[CharacterWriteFields]
) -> list[CharacterWriteView]:
# 镜像 Sql逐卡 upsert含批内重名去重保序返回逐卡 View。
return [
await self.create(
project_id,
name=c.name,
role=c.role,
traits=list(c.traits),
appearance=c.appearance,
motive=c.motive,
backstory=c.backstory,
arc=c.arc,
speech_tics=list(c.speech_tics),
tags=list(c.tags),
relations=[dict(r) for r in c.relations],
)
for c in cards
]
class _FakeRulesReadRepo:
"""实现规则 repo 的读侧 `list_for_project`(带 id供 list_rules 端点)。"""
@@ -352,6 +373,39 @@ async def test_ingest_characters_no_conflict_writes_and_201() -> None:
assert session.commits == 1
@pytest.mark.asyncio
async def test_ingest_multiple_characters_all_land_in_one_batch() -> None:
# 批量入库:多张卡一次 create_many 全部落库created 保序,单次 commit。
repo = FakeProjectRepo()
pid = await _seed_project(repo)
gateway = _SchemaRoutingGateway({ContinuityReview: _no_conflicts()})
char_repo = _FakeCharacterWriteRepo()
app, session = _make_app(project_repo=repo, gateway=gateway, char_repo=char_repo)
def _card(name: str) -> dict[str, Any]:
return {
"name": name,
"role": "配角",
"traits": [],
"backstory": "x",
"arc": "y",
"speech_tics": [],
"tags": [],
"relations": [],
}
async with _client(app) as client:
resp = await client.post(
f"/projects/{pid}/characters",
json={"cards": [_card(""), _card(""), _card("")]},
)
assert resp.status_code == 201
assert resp.json()["created"] == ["", "", ""]
assert [r["name"] for r in char_repo.rows] == ["", "", ""]
assert session.commits == 1
@pytest.mark.asyncio
async def test_ingest_characters_with_conflict_blocks_409() -> None:
repo = FakeProjectRepo()