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:
@@ -55,6 +55,40 @@ export function CharacterCardItem({
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{onEdit || card.appearance ? (
|
||||
<div className="mb-1 flex items-start gap-1 text-ink-soft">
|
||||
<span className="shrink-0 text-ink">外貌:</span>
|
||||
{onEdit ? (
|
||||
<TextInput
|
||||
value={card.appearance ?? ""}
|
||||
onChange={(e) => onEdit({ appearance: e.target.value })}
|
||||
aria-label="外貌形象"
|
||||
controlSize="sm"
|
||||
className="min-w-0 flex-1"
|
||||
/>
|
||||
) : (
|
||||
<span>{card.appearance}</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{onEdit || card.motive ? (
|
||||
<div className="mb-1 flex items-start gap-1 text-ink-soft">
|
||||
<span className="shrink-0 text-ink">动机:</span>
|
||||
{onEdit ? (
|
||||
<TextInput
|
||||
value={card.motive ?? ""}
|
||||
onChange={(e) => onEdit({ motive: e.target.value })}
|
||||
aria-label="核心动机"
|
||||
controlSize="sm"
|
||||
className="min-w-0 flex-1"
|
||||
/>
|
||||
) : (
|
||||
<span>{card.motive}</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="mb-1 text-ink-soft">
|
||||
<span className="text-ink">背景:</span>
|
||||
{card.backstory}
|
||||
|
||||
14
apps/web/lib/api/schema.d.ts
vendored
14
apps/web/lib/api/schema.d.ts
vendored
@@ -922,9 +922,21 @@ export interface components {
|
||||
role: string;
|
||||
/**
|
||||
* Traits
|
||||
* @description 性格特质清单
|
||||
* @description 性格特质清单(性格三层)
|
||||
*/
|
||||
traits?: string[];
|
||||
/**
|
||||
* Appearance
|
||||
* @description 外貌形象(缺则空串)
|
||||
* @default
|
||||
*/
|
||||
appearance: string;
|
||||
/**
|
||||
* Motive
|
||||
* @description 核心动机/欲望/恐惧(缺则空串)
|
||||
* @default
|
||||
*/
|
||||
motive: string;
|
||||
/**
|
||||
* Backstory
|
||||
* @description 背景故事
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
const card = (over: Partial<CharacterCardView> = {}): CharacterCardView => ({
|
||||
name: "甲",
|
||||
role: "主角",
|
||||
appearance: "白衣如雪",
|
||||
motive: "为家族正名",
|
||||
backstory: "出身寒门",
|
||||
arc: "由怯转勇",
|
||||
...over,
|
||||
@@ -86,6 +88,18 @@ describe("updateCard", () => {
|
||||
expect(original.name).toBe("甲");
|
||||
expect(next).not.toBe(original);
|
||||
});
|
||||
|
||||
it("patches appearance/motive without dropping the other fields", () => {
|
||||
const original = card();
|
||||
const next = updateCard(original, {
|
||||
appearance: "玄色劲装",
|
||||
motive: "复仇",
|
||||
});
|
||||
expect(next.appearance).toBe("玄色劲装");
|
||||
expect(next.motive).toBe("复仇");
|
||||
expect(next.backstory).toBe(original.backstory);
|
||||
expect(original.appearance).toBe("白衣如雪"); // no mutation
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractIngestConflicts", () => {
|
||||
|
||||
Reference in New Issue
Block a user