feat(web): 角色群像按定位配比生成(role_mix 缩减版)

- 契约: CharacterGenerateRequest 加 role_mix{定位:数量},model_validator 归一
  count=各值之和(每项≥1、总和≤12),缺省退回单一 role+count
- @llm: build_character_gen_context/run_character_gen 加 role_mix 参并注入配比块
  (确定性、无时间戳,守 assemble 纯函数);router 穿参
- character-gen.md 加"按配比严格分配 role"纪律 + regen prompt_hashes 金标准
- 前端: sanitizeRoleMix/roleMixTotal + buildCharacterGenerateRequest 第4参 +
  useCharacterGen 穿参 + RoleMixEditor + CharacterGenerator 单一/配比双模
This commit is contained in:
Yaojia Wang
2026-07-06 17:24:16 +02:00
parent 72b3c81146
commit 7babb4854b
16 changed files with 428 additions and 50 deletions

View File

@@ -181,6 +181,39 @@ def test_build_character_gen_context_injects_existing_and_generated() -> None:
assert "苏黎" in ctx # 本批已生成卡
def test_build_character_gen_context_injects_role_mix() -> None:
# ⑦ 群像按定位配比role_mix 各定位 + 数量都进上下文,指示模型按配比分配
ctx = build_character_gen_context(
brief="给我一组群像",
count=6,
role=None,
world_context="",
existing_chars=[],
generated_so_far=[],
role_mix={"主角": 1, "对手": 2, "配角": 3},
)
assert "配比" in ctx # 配比块标识
assert "主角" in ctx and "对手" in ctx and "配角" in ctx
assert "1" in ctx and "2" in ctx and "3" in ctx
def test_build_character_gen_context_role_mix_overrides_single_role() -> None:
# role_mix 在场时用配比块不再输出单一「角色定位role」行
ctx = build_character_gen_context(
brief="群像",
count=3,
role="主角", # 应被 role_mix 覆盖
world_context="",
existing_chars=[],
generated_so_far=[],
role_mix={"对手": 3},
)
assert "角色定位:主角" not in ctx
assert "对手" in ctx
def test_build_character_gen_context_handles_empty_anti_dup() -> None:
# 首次单生成:无已有/已生成时仍产出可用上下文(不崩)
ctx = build_character_gen_context(
@@ -243,6 +276,29 @@ async def test_run_character_gen_injects_anti_dup_into_request() -> None:
assert "九转炼气" in req.input # 世界观约束注入
async def test_run_character_gen_injects_role_mix_into_request() -> None:
# ⑦ role_mix 穿参run → build_character_gen_context → 请求 input 含各定位
gateway = FakeRunGateway(_CARDS)
await run_character_gen(
character_gen_spec,
brief="群像",
count=5,
role=None,
world_context="",
existing_chars=[],
generated_so_far=[],
gateway=gateway,
user_id=USER,
project_id=PROJECT,
role_mix={"主角": 2, "配角": 3},
)
req = gateway.last_request
assert req is not None
assert "配角" in req.input # 配比定位注入
async def test_run_character_gen_raises_when_gateway_fails() -> None:
gateway = FailingRunGateway(RuntimeError("provider down"))