feat(character): appearance/motive 全栈穿线 + motive/traits 口径重切
打通 appearance/motive 从生成→schema→契约→落库→前端整条穿线 (DB 列早在初始迁移、此前一直 NULL,无新迁移),并原子重切人物 内驱口径。 - @llm: CharacterCard/character-gen.md 加 appearance/motive(给默认值守 解析韧性),重切——动机唯一落 motive、traits 只留核心/表层/阴影三层、 arc 引用 motive;regen prompt_hashes.json 金标准。 - @backend: CharacterCardView + CharacterWriteRepo/SqlCharacterWriteRepo (create + update backfill 分支)+ CharacterWriteFields + 路由双向形变 与 _existing_characters 带上两字段。 - @frontend: pnpm gen:api 重生成 TS 客户端;CharacterCardItem 展示/编辑 两字段。 - 测试: schema 默认值/解析、动机不双写重切回归、card_to_view 双向、 ingest 透传、list 暴露、SqlCharacterWriteRepo create/backfill 真 pg 集成。
This commit is contained in:
@@ -75,6 +75,8 @@ class _FakeCharacterWriteRepo:
|
||||
name: str,
|
||||
role: str,
|
||||
traits: list[str],
|
||||
appearance: str,
|
||||
motive: str,
|
||||
backstory: str,
|
||||
arc: str,
|
||||
speech_tics: list[str],
|
||||
@@ -91,6 +93,8 @@ class _FakeCharacterWriteRepo:
|
||||
{
|
||||
"role": role,
|
||||
"traits": list(traits),
|
||||
"appearance": appearance,
|
||||
"motive": motive,
|
||||
"arc": arc,
|
||||
"speech_tics": list(speech_tics),
|
||||
"tags": list(tags),
|
||||
@@ -106,6 +110,8 @@ class _FakeCharacterWriteRepo:
|
||||
"name": name,
|
||||
"role": role,
|
||||
"traits": list(traits),
|
||||
"appearance": appearance,
|
||||
"motive": motive,
|
||||
"arc": arc,
|
||||
"speech_tics": list(speech_tics),
|
||||
"tags": list(tags),
|
||||
@@ -463,6 +469,62 @@ async def test_ingest_relations_persist_on_write() -> None:
|
||||
assert char_repo.rows[0]["relations"] == relations
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ingest_persists_appearance_and_motive() -> None:
|
||||
# ⑧ 穿线:入库端点须把 appearance/motive 透传给写侧 repo(否则新列永远 NULL)。
|
||||
repo = FakeProjectRepo()
|
||||
pid = await _seed_project(repo)
|
||||
gateway = _SchemaRoutingGateway({ContinuityReview: _no_conflicts()})
|
||||
char_repo = _FakeCharacterWriteRepo()
|
||||
app, _ = _make_app(project_repo=repo, gateway=gateway, char_repo=char_repo)
|
||||
|
||||
async with _client(app) as client:
|
||||
resp = await client.post(
|
||||
f"/projects/{pid}/characters",
|
||||
json={
|
||||
"cards": [
|
||||
{
|
||||
"name": "苏璃",
|
||||
"role": "CP",
|
||||
"traits": ["清冷毒舌"],
|
||||
"appearance": "银发赤瞳,玄色劲装",
|
||||
"motive": "替亡兄查清族灭真相",
|
||||
"backstory": "玄霜阁遗孤",
|
||||
"arc": "由敌转盟",
|
||||
"speech_tics": [],
|
||||
"tags": [],
|
||||
"relations": [],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 201
|
||||
assert char_repo.rows[0]["appearance"] == "银发赤瞳,玄色劲装"
|
||||
assert char_repo.rows[0]["motive"] == "替亡兄查清族灭真相"
|
||||
|
||||
|
||||
def test_card_to_view_roundtrips_new_fields() -> None:
|
||||
# ⑧ 穿线:schema↔view 双向形变须带上 appearance/motive(否则前后端静默丢字段)。
|
||||
from ww_api.routers.generation import _card_to_view, _view_to_card
|
||||
|
||||
card = CharacterCard(
|
||||
name="苏璃",
|
||||
role="CP",
|
||||
traits=["清冷毒舌"],
|
||||
appearance="银发赤瞳,玄色劲装",
|
||||
motive="替亡兄查清族灭真相",
|
||||
backstory="玄霜阁遗孤",
|
||||
arc="由敌转盟",
|
||||
)
|
||||
view = _card_to_view(card)
|
||||
assert view.appearance == "银发赤瞳,玄色劲装"
|
||||
assert view.motive == "替亡兄查清族灭真相"
|
||||
back = _view_to_card(view)
|
||||
assert back.appearance == card.appearance
|
||||
assert back.motive == card.motive
|
||||
|
||||
|
||||
def test_relations_from_jsonb_parses_and_skips_dirty() -> None:
|
||||
# Arrange:混入脏条目(非 dict / 缺 name / 缺 kind)。
|
||||
from ww_api.routers.generation import _relations_from_jsonb
|
||||
@@ -572,6 +634,8 @@ def _codex_memory() -> Any:
|
||||
name="叶寒",
|
||||
role="主角",
|
||||
traits={"items": ["腹黑", "护短"]},
|
||||
appearance="剑眉星目,一袭青衫",
|
||||
motive="为母复仇、登临帝位",
|
||||
backstory="孤儿出身",
|
||||
arc={"text": "从孤儿到帝王"},
|
||||
speech_tics={"items": ["哼"]},
|
||||
@@ -668,6 +732,9 @@ async def test_list_characters_unpacks_jsonb_to_api_shape() -> None:
|
||||
assert card["speech_tics"] == ["哼"]
|
||||
assert card["arc"] == "从孤儿到帝王"
|
||||
assert card["tags"] == ["天才"]
|
||||
# ⑧ 穿线:读端点暴露 appearance/motive(Text 列直落,不形变)。
|
||||
assert card["appearance"] == "剑眉星目,一袭青衫"
|
||||
assert card["motive"] == "为母复仇、登临帝位"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user