This commit is contained in:
2026-04-18 13:05:29 +08:00
parent 09d4c0c31b
commit 5032701c38
77 changed files with 8538 additions and 2413 deletions

View File

@@ -15,6 +15,9 @@ type ParsedRole = {
title: string;
role: string;
description: string;
visualDescription: string;
actionDescription: string;
sceneVisualDescription: string;
backstory: string;
personality: string;
motivation: string;
@@ -34,6 +37,7 @@ type ParsedLandmark = {
id: string;
name: string;
description: string;
visualDescription: string;
dangerLevel: string;
sceneNpcIds: string[];
connections: ParsedLandmarkConnection[];
@@ -220,6 +224,9 @@ function normalizeRole(value: unknown): ParsedRole | null {
title: title || role || '角色',
role,
description: toText(record.description),
visualDescription: toText(record.visualDescription),
actionDescription: toText(record.actionDescription),
sceneVisualDescription: toText(record.sceneVisualDescription),
backstory: toText(record.backstory),
personality: toText(record.personality),
motivation: toText(record.motivation),
@@ -275,6 +282,7 @@ function normalizeLandmark(value: unknown): ParsedLandmark | null {
id,
name,
description: toText(record.description),
visualDescription: toText(record.visualDescription),
dangerLevel: toText(record.dangerLevel, 'medium'),
sceneNpcIds: toStringArray(record.sceneNpcIds, 12),
connections,
@@ -326,7 +334,11 @@ function buildRoleReferenceText(roles: ParsedRole[], emptyText: string) {
role.backstory || '未写'
} / 性格:${role.personality || '未写'} / 动机:${
role.motivation || '未写'
} / 标签${role.tags.join('、') || '暂无'}`,
} / 形象${role.visualDescription || '未写'} / 动作表现:${
role.actionDescription || '未写'
} / 场景画面:${role.sceneVisualDescription || '未写'} / 标签:${
role.tags.join('、') || '暂无'
}`,
)
.join('\n');
}
@@ -361,7 +373,9 @@ function buildLandmarkReferenceText(profile: ParsedProfile) {
return `${index + 1}. ${landmark.name} / 危险度:${
landmark.dangerLevel || 'medium'
} / 描述:${landmark.description || '未写'} / 场景角色${
} / 描述:${landmark.description || '未写'} / 画面${
landmark.visualDescription || '未写'
} / 场景角色:${
sceneNpcNames || '暂无'
} / 连接:${connectionNames || '暂无'}`;
})
@@ -437,6 +451,24 @@ function buildFallbackRoleDraft(
: `长期活跃于当前世界暗面,能补足场景视角的关键角色。`,
60,
),
visualDescription: clampText(
kind === 'playable'
? `他保留着适合长期同行的鲜明外形识别点,服装、装备和体态都能直接看出其职责、出身和会如何与玩家并肩行动。`
: `他身上带着与当前局势强绑定的外观痕迹,衣着、器具和整体气质会暴露其长期活动环境与所站的位置。`,
96,
),
actionDescription: clampText(
kind === 'playable'
? '动作表现偏向协作推进与稳定压制,起手克制,发力明确,收招干净。'
: '动作表现偏向试探、牵制与借势,节奏谨慎,但关键时刻会突然加重攻击或位移。',
72,
),
sceneVisualDescription: clampText(
profile.landmarks[0]?.description
? `他的主要活动空间与${profile.landmarks[0].name}相连,场景里能看到${profile.landmarks[0].description}`
: `他的主要活动空间与${profile.name}当前冲突线直接相关,环境里会留下势力痕迹、旧装置和仍在运转的局势线索。`,
96,
),
backstory: clampText(
`他与${profile.name}当前正在扩张的冲突链紧密相连,知道一些还未公开的内情。`,
80,
@@ -535,6 +567,10 @@ function buildFallbackLandmarkDraft(profile: ParsedProfile) {
`承接${profile.name}当前主冲突的一处关键新场景,适合继续向外扩张世界关系网。`,
72,
),
visualDescription: clampText(
`这里延续${profile.name}当前主冲突的视觉气质,能看到明确的空间层次、可站立地面、核心建筑或地貌,以及仍在运转的局势痕迹。`,
88,
),
dangerLevel: 'medium',
sceneNpcNames,
connections: targetLandmarkNames.map((targetLandmarkName, index) => ({
@@ -560,6 +596,9 @@ function buildPlayablePrompt(profile: ParsedProfile) {
'- 必须与当前世界设定、已有可扮演角色、已有场景角色、已有场景形成互补,不要重复定位。',
'- 必须保留明确的协作价值、成长空间和入队理由。',
'- 不要生成泛用模板角色,必须让角色与当前世界的具体势力、地点、冲突或禁忌发生绑定。',
'- visualDescription 只写与角色设定相关的外形、服装、材质、武器、体态、色彩和识别特征,禁止写角色以外的周边环境等与角色不想管的设定。',
'- actionDescription 只写这个角色的动作表现与战斗气质,不要写镜头切换或参数。',
'- sceneVisualDescription 只写该角色首次登场或主要活动区域的场景画面感,不要写“提示词”字样。',
'- 只返回 JSON不要输出解释或 Markdown。',
'JSON 结构:',
'{',
@@ -568,6 +607,9 @@ function buildPlayablePrompt(profile: ParsedProfile) {
' "title": "称号",',
' "role": "身份",',
' "description": "一句到两句定位描述",',
' "visualDescription": "角色形象描述",',
' "actionDescription": "动作表现描述",',
' "sceneVisualDescription": "角色关联场景画面描述",',
' "backstory": "背景经历",',
' "personality": "性格特点",',
' "motivation": "当前动机",',
@@ -608,6 +650,9 @@ function buildStoryPrompt(profile: ParsedProfile) {
'- 必须与当前世界设定、已有可扮演角色、已有场景角色、已有场景形成互补,不要重复定位。',
'- 必须像能直接进入游戏的场景角色,而不是抽象设定条目。',
'- 角色应与具体场景、关系链或局势变化发生绑定。',
'- visualDescription 只写与角色设定匹配的外形、服装、材质、武器、体态、色彩和识别特征,不要写“提示词”、镜头参数或构图规则。',
'- actionDescription 只写这个角色的动作表现与战斗气质,不要写镜头切换或参数。',
'- sceneVisualDescription 只写该角色首次登场或主要活动区域的场景画面感,不要写“提示词”字样。',
'- 只返回 JSON不要输出解释或 Markdown。',
'JSON 结构:',
'{',
@@ -616,6 +661,9 @@ function buildStoryPrompt(profile: ParsedProfile) {
' "title": "称号",',
' "role": "身份",',
' "description": "一句到两句定位描述",',
' "visualDescription": "角色形象描述",',
' "actionDescription": "动作表现描述",',
' "sceneVisualDescription": "角色关联场景画面描述",',
' "backstory": "背景经历",',
' "personality": "性格特点",',
' "motivation": "当前动机",',
@@ -656,12 +704,14 @@ function buildLandmarkPrompt(profile: ParsedProfile) {
'- 必须与当前世界设定、已有可扮演角色、已有场景角色、已有场景形成互补,不要重复。',
'- 必须给出适合出现在这个新场景里的 sceneNpcNames且只能从已有场景角色里选择至少 3 个名字。',
'- 必须给出 connections且 targetLandmarkName 只能引用已有场景名,不要连向自己。',
'- visualDescription 只写这个场景的空间层次、地面、主体建筑或自然景观、氛围、色彩和可见装置,不要写“提示词”、镜头参数或构图规则。',
'- 只返回 JSON不要输出解释或 Markdown。',
'JSON 结构:',
'{',
' "landmark": {',
' "name": "场景名",',
' "description": "场景描述",',
' "visualDescription": "场景画面描述",',
' "dangerLevel": "low|medium|high|extreme",',
' "sceneNpcNames": ["场景角色1", "场景角色2", "场景角色3"],',
' "connections": [',
@@ -737,6 +787,21 @@ function sanitizeGeneratedRole(
toText(record?.description, fallbackDraft.description),
120,
),
visualDescription: clampText(
toText(record?.visualDescription, fallbackDraft.visualDescription),
180,
),
actionDescription: clampText(
toText(record?.actionDescription, fallbackDraft.actionDescription),
140,
),
sceneVisualDescription: clampText(
toText(
record?.sceneVisualDescription,
fallbackDraft.sceneVisualDescription,
),
180,
),
backstory: clampText(toText(record?.backstory, fallbackDraft.backstory), 260),
personality: clampText(
toText(record?.personality, fallbackDraft.personality),
@@ -962,6 +1027,10 @@ function sanitizeGeneratedLandmark(rawValue: unknown, profile: ParsedProfile) {
toText(record?.description, fallbackDraft.description),
140,
),
visualDescription: clampText(
toText(record?.visualDescription, fallbackDraft.visualDescription),
180,
),
dangerLevel: (() => {
const level = toText(record?.dangerLevel, fallbackDraft.dangerLevel);
return level === 'low' ||