Skip to content

Commit 93440c1

Browse files
committed
✨ feat(photography): 集成写真系统核心状态管理与LLM交互
- 【新功能】添加写真系统状态的解析、应用与回写逻辑,实现与LLM的双向数据同步 - 【新功能】实现写真系统、模特和摄影师档案的懒初始化,允许LLM在剧情中动态创建 - 【新功能】在主剧情请求中加入完整的写真NSFW参数,增强LLM对拍摄场景的感知与控制 - 【新功能】在运行时提示词中加入写真系统叙事约束,指导LLM生成符合玩法的内容 - 【新功能】为写真系统添加NPC和玩家的姓名映射,确保LLM状态输出的准确性 ♻️ refactor(photography): 优化写真系统状态解析与参数构建 - 【重构】增强`<写真系统状态>`标签的解析逻辑,兼容多种LLM输出的JSON格式 - 【重构】调整`构建写真NSFW参数`逻辑,确保在系统未初始化时也能提供基础参数 🐛 fix(send): 修正主工作流中的系统状态传递 - 【修复】确保`写真系统`和`都市网约车系统`等状态在发送工作流中正确传递和更新 🔧 chore(dev): 添加诊断日志 - 【调试】在`PhotographyDashboard`和发送工作流中增加诊断日志,便于追踪状态变化
1 parent fa32683 commit 93440c1

9 files changed

Lines changed: 432 additions & 32 deletions

File tree

components/features/PhotographyDashboard.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,15 @@ interface Props {
353353
export const PhotographyDashboard: React.FC<Props> = ({
354354
模特档案, 摄影师档案, 进行中的拍摄项目, 历史拍摄记录, 泄露事件列表, onClose
355355
}) => {
356+
// 诊断日志:确认 props 是否正确传递
357+
console.log('[写真Dashboard诊断] Props:', {
358+
模特档案Keys: Object.keys(模特档案),
359+
摄影师档案Keys: Object.keys(摄影师档案),
360+
进行中的拍摄项目数: 进行中的拍摄项目.length,
361+
历史拍摄记录数: 历史拍摄记录.length,
362+
泄露事件列表数: 泄露事件列表.length,
363+
});
364+
356365
const 活跃泄露 = useMemo(() => 泄露事件列表.filter(e => e.状态 === '活跃' || e.状态 === '已发酵'), [泄露事件列表]);
357366

358367
const 模特姓名映射 = useMemo(() => {

hooks/useGame/domains/sendDomain.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export function createSendDomain(input: SendDomainInput) {
261261
玩家门派, 任务列表, 约定列表, 剧情, 剧情规划,
262262
女主剧情规划, 同人剧情规划, 同人女主剧情规划,
263263
开局配置, 校规系统, 催眠系统, 校园系统,
264+
写真系统, 都市网约车系统,
264265
currentEra, loading, gameConfig, apiConfig, memoryConfig,
265266
visualConfig, 场景图片档案, prompts,
266267
内置提示词列表, 世界书列表,
@@ -269,6 +270,7 @@ export function createSendDomain(input: SendDomainInput) {
269270
setLoading, setShowSettings,
270271
设置剧情, 设置历史记录,
271272
应用并同步记忆系统,
273+
设置写真系统, 设置校园系统, 设置都市网约车系统,
272274
构建系统提示词,
273275
processResponseCommands,
274276
performAutoSave: (...args: any[]) => performAutoSaveRef.current?.(...args),

hooks/useGame/mainStoryRequest.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,20 @@ export const 构建主剧情请求参数 = (
181181
实际尺度: string;
182182
拍摄阶段: string;
183183
泄露风险值: number;
184+
[key: string]: unknown;
184185
};
185186
模特数量?: number;
186187
摄影师数量?: number;
187188
泄露事件数量?: number;
188189
内容强度?: '微暗' | '暧昧' | '露骨';
189190
主要玩法层?: '经营管理' | '人际关系' | '灰色地带';
190191
启用道德选择?: boolean;
192+
启用尺度递进?: boolean;
193+
启用越界识别?: boolean;
194+
启用安全词系统?: boolean;
195+
启用照片交付?: boolean;
196+
启用泄露事件?: boolean;
197+
泄露事件频率?: '低' | '中' | '高';
191198
};
192199
}
193200
): 主剧情请求构建结果 => {
@@ -224,9 +231,9 @@ export const 构建主剧情请求参数 = (
224231
)
225232
: '';
226233
const normalizedRuntimeExtraPrompt = !tavernPresetModeEnabled
227-
? 构建运行时额外提示词(runtimeGameConfig.额外提示词 || '', { ...runtimeGameConfig, 时代配置ID: params.时代配置ID, 校园NSFW参数: params.校园NSFW参数, 都市网约车NSFW参数: params.都市网约车NSFW参数 })
234+
? 构建运行时额外提示词(runtimeGameConfig.额外提示词 || '', { ...runtimeGameConfig, 时代配置ID: params.时代配置ID, 校园NSFW参数: params.校园NSFW参数, 都市网约车NSFW参数: params.都市网约车NSFW参数, 写真NSFW参数: params.写真NSFW参数 })
228235
: '';
229-
const tavernRuntimeExtraPrompt = 构建运行时额外提示词(runtimeGameConfig.额外提示词 || '', { ...runtimeGameConfig, 时代配置ID: params.时代配置ID, 校园NSFW参数: params.校园NSFW参数, 都市网约车NSFW参数: params.都市网约车NSFW参数 });
236+
const tavernRuntimeExtraPrompt = 构建运行时额外提示词(runtimeGameConfig.额外提示词 || '', { ...runtimeGameConfig, 时代配置ID: params.时代配置ID, 校园NSFW参数: params.校园NSFW参数, 都市网约车NSFW参数: params.都市网约车NSFW参数, 写真NSFW参数: params.写真NSFW参数 });
230237
const recallScriptAppend = params.recallTag ? `\n\n【剧情回忆】\n${params.recallTag}` : '';
231238
const scriptSectionText = `【即时剧情回顾】\n${formatHistoryToScript(params.updatedContextHistory) || '暂无'}${recallScriptAppend}`;
232239
const latestUserInputAsModel = [

hooks/useGame/photographyNSFWIntegration.ts

Lines changed: 229 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,35 @@ export const 解析写真系统状态更新 = (
3030

3131
try {
3232
const parsed = JSON.parse(match[1]);
33-
return parsed as {
33+
34+
// 兼容两种字段名:LLM可能输出"更新拍摄项目"(数组)或"更新项目状态"(对象/平铺对象)
35+
const raw项目更新 = (parsed as any).更新拍摄项目 || (parsed as any).更新项目状态;
36+
let 标准化项目更新: Partial<拍摄项目状态>[] | undefined;
37+
if (raw项目更新) {
38+
if (Array.isArray(raw项目更新)) {
39+
标准化项目更新 = raw项目更新;
40+
} else if (typeof raw项目更新 === 'object') {
41+
// 判断是平铺对象(含"项目ID"字段)还是嵌套对象({"ID":{数据}})
42+
if (raw项目更新.项目ID || raw项目更新.id) {
43+
// 平铺对象:直接作为单个项目更新
44+
标准化项目更新 = [raw项目更新];
45+
} else {
46+
// 嵌套对象:{"项目ID": {数据}}
47+
标准化项目更新 = Object.entries(raw项目更新).map(([id, data]: [string, any]) => ({
48+
id,
49+
项目ID: id,
50+
...data,
51+
}));
52+
}
53+
}
54+
}
55+
56+
return {
57+
更新模特档案: parsed.更新模特档案,
58+
更新摄影师档案: parsed.更新摄影师档案,
59+
更新拍摄项目: 标准化项目更新,
60+
新泄露事件: parsed.新泄露事件,
61+
} as {
3462
更新模特档案?: Record<string, Partial<模特核心状态>>;
3563
更新摄影师档案?: Record<string, any>;
3664
更新拍摄项目?: Partial<拍摄项目状态>[];
@@ -48,6 +76,165 @@ export const 移除写真系统状态标签 = (rawText: string): string => {
4876
return rawText.replace(/<>[\s\S]*?<\/>/g, '').trim();
4977
};
5078

79+
/**
80+
* 处理 AI 响应中的写真系统状态更新
81+
* 1. 解析 <写真系统状态> 标签
82+
* 2. 调用回调应用状态变更
83+
* 3. 返回清理后的纯文本(不含状态标签)
84+
*/
85+
export const 处理写真系统状态更新 = (
86+
rawAiText: string,
87+
callback: (result: NonNullable<ReturnType<typeof 解析写真系统状态更新>>) => void
88+
): string => {
89+
const 解析结果 = 解析写真系统状态更新(rawAiText);
90+
if (解析结果) {
91+
callback(解析结果);
92+
}
93+
return 移除写真系统状态标签(rawAiText);
94+
};
95+
96+
/**
97+
* 应用写真系统状态更新到游戏状态
98+
* 支持懒初始化:如果写真系统尚未创建但有状态更新,会自动创建初始系统
99+
*/
100+
export const 应用写真系统状态更新 = (
101+
current写真系统: any,
102+
更新: NonNullable<ReturnType<typeof 解析写真系统状态更新>>
103+
): any => {
104+
// 懒初始化:如果写真系统不存在但有更新,创建初始系统
105+
const 基础系统 = current写真系统 || {
106+
模特档案: {},
107+
摄影师档案: {},
108+
进行中的拍摄项目: [],
109+
历史拍摄记录: [],
110+
泄露事件列表: [],
111+
};
112+
113+
const 新系统 = { ...基础系统 };
114+
115+
// 应用模特档案更新(不存在的自动创建)
116+
if (更新.更新模特档案) {
117+
const 新模特档案 = { ...(新系统.模特档案 || {}) };
118+
for (const [id, 档案] of Object.entries(更新.更新模特档案)) {
119+
if (新模特档案[id]) {
120+
新模特档案[id] = { ...新模特档案[id], ...档案 };
121+
} else {
122+
// 新模特:创建完整最小档案,补充 Dashboard 所需的所有字段
123+
const 基础模特 = {
124+
id,
125+
姓名: id,
126+
类型: '素人模特' as const,
127+
职业状态: '新人' as const,
128+
保护意识: '适度保护' as const,
129+
信任度: 50,
130+
安全感: 60,
131+
自我认同: 50,
132+
羞耻度: 50,
133+
拍摄总次数: 0,
134+
正规拍摄次数: 0,
135+
擦边拍摄次数: 0,
136+
越界拍摄次数: 0,
137+
当前底线: 'G级' as const,
138+
底线历史: [],
139+
被偷拍次数: 0,
140+
被泄露次数: 0,
141+
投诉次数: 0,
142+
累计收入: 0,
143+
单次报价: 0,
144+
拍摄经历: [] as any[],
145+
};
146+
新模特档案[id] = { ...基础模特, ...档案 };
147+
}
148+
}
149+
新系统.模特档案 = 新模特档案;
150+
}
151+
152+
// 应用摄影师档案更新(不存在的自动创建)
153+
if (更新.更新摄影师档案) {
154+
const 新摄影师档案 = { ...(新系统.摄影师档案 || {}) };
155+
for (const [id, 档案] of Object.entries(更新.更新摄影师档案)) {
156+
if (新摄影师档案[id]) {
157+
新摄影师档案[id] = { ...新摄影师档案[id], ...档案 };
158+
} else {
159+
// 新摄影师:创建完整最小档案
160+
const 基础摄影师 = {
161+
id,
162+
姓名: id,
163+
类型: '独立摄影师' as const,
164+
动机: '纯艺术' as const,
165+
信誉: '普通摄影师' as const,
166+
技术水平: 50,
167+
沟通能力: 50,
168+
越界倾向: 30,
169+
偷拍倾向: 10,
170+
传播倾向: 10,
171+
口碑评分: 50,
172+
投诉累计: 0,
173+
拍摄总次数: 0,
174+
回头客数量: 0,
175+
作品发布数量: 0,
176+
擅长写真类型: [] as any[],
177+
擅长拍摄风格: [] as any[],
178+
};
179+
新摄影师档案[id] = { ...基础摄影师, ...档案 };
180+
}
181+
}
182+
新系统.摄影师档案 = 新摄影师档案;
183+
}
184+
185+
// 应用拍摄项目状态更新(不存在的自动创建)
186+
if (更新.更新拍摄项目) {
187+
const 当前项目 = 新系统.进行中的拍摄项目 || [];
188+
const 新项目列表 = [...当前项目];
189+
190+
for (const raw更新 of 更新.更新拍摄项目 as any[]) {
191+
const 项目ID = raw更新.id || raw更新.项目ID;
192+
const 已有索引 = 新项目列表.findIndex((p: any) => p.id === 项目ID || p.项目ID === 项目ID);
193+
194+
if (已有索引 >= 0) {
195+
新项目列表[已有索引] = { ...新项目列表[已有索引], ...raw更新 };
196+
} else {
197+
// 新项目:创建完整最小项目
198+
const 基础项目 = {
199+
id: 项目ID,
200+
项目ID,
201+
模特Id: Object.keys(更新.更新模特档案 || {})[0] || 'unknown',
202+
摄影师Id: 'unknown',
203+
约定写真类型: '商业写真' as const,
204+
约定场所: '影棚' as const,
205+
约定风格: '清新自然' as const,
206+
约定尺度: 'G级' as const,
207+
约定服装: '日常便装' as const,
208+
约定交付时间: 0,
209+
实际场所: '影棚' as const,
210+
实际尺度: 'G级' as const,
211+
实际服装: '日常便装' as const,
212+
当前回合: 1,
213+
最大回合: 10,
214+
拍摄阶段: '未开始' as const,
215+
尺度变更历史: [],
216+
越界行为记录: [],
217+
泄露风险评分: 0,
218+
交付状态: '待交付' as const,
219+
交付方式: null,
220+
后期处理方式: '纯自然' as const,
221+
违规记录: [],
222+
};
223+
新项目列表.push({ ...基础项目, ...raw更新 });
224+
}
225+
}
226+
227+
新系统.进行中的拍摄项目 = 新项目列表;
228+
}
229+
230+
// 应用新泄露事件
231+
if (更新.新泄露事件) {
232+
新系统.泄露事件列表 = [...(新系统.泄露事件列表 || []), ...更新.新泄露事件];
233+
}
234+
235+
return 新系统;
236+
};
237+
51238
/**
52239
* 构建写真约拍 NSFW 运行时参数(供主剧情请求使用)
53240
*/
@@ -60,16 +247,26 @@ export const 构建写真NSFW参数 = (state: {
60247
出身背景?: {
61248
名称?: string;
62249
};
250+
姓名?: string;
63251
};
64252
时代配置ID?: string;
253+
社交列表?: Array<{ id: string; 姓名: string; [key: string]: any }>;
65254
}): {
66255
活跃拍摄项目?: 拍摄项目状态;
67256
模特数量?: number;
68257
摄影师数量?: number;
69258
泄露事件数量?: number;
70259
内容强度?: '微暗' | '暧昧' | '露骨';
71260
主要玩法层?: '经营管理' | '人际关系' | '灰色地带';
261+
NPC姓名映射?: Record<string, string>;
262+
摄影师姓名映射?: Record<string, string>;
72263
启用道德选择?: boolean;
264+
启用尺度递进?: boolean;
265+
启用越界识别?: boolean;
266+
启用安全词系统?: boolean;
267+
启用照片交付?: boolean;
268+
启用泄露事件?: boolean;
269+
泄露事件频率?: '低' | '中' | '高';
73270
} | undefined => {
74271
// 检查时代配置 - 必须是 contemporary_ 开头的现代纪元
75272
const 时代ID = state.时代配置ID || '';
@@ -83,30 +280,50 @@ export const 构建写真NSFW参数 = (state: {
83280
return undefined;
84281
}
85282

86-
// 检查写真系统是否存在
283+
// 写真系统可能为空(新游戏尚未创建约拍项目),但仍需返回基本设置参数
284+
// 让 LLM 知道写真系统已激活,可以在剧情中触发约拍场景
87285
const 写真系统 = state.写真系统;
88-
if (!写真系统) {
89-
return undefined;
90-
}
91286

92287
// 获取活跃拍摄项目
93-
const 进行中项目 = 写真系统.进行中的拍摄项目;
94-
const 活跃项目 = 进行中项目 && 进行中项目.length > 0
95-
? 进行中项目[进行中项目.length - 1]
288+
const 进行中项目 = 写真系统?.进行中的拍摄项目;
289+
const 活跃项目 = 进行中项目 && 进行中项目.length > 0
290+
? 进行中项目[进行中项目.length - 1]
96291
: undefined;
97292

98-
// 统计数量
99-
const 模特数量 = 写真系统.模特档案 ? Object.keys(写真系统.模特档案).length : 0;
100-
const 摄影师数量 = 写真系统.摄影师档案 ? Object.keys(写真系统.摄影师档案).length : 0;
101-
const 泄露事件数量 = 写真系统.泄露事件列表?.length || 0;
293+
// 统计数量(系统为空时均为 0)
294+
const 模特数量 = 写真系统?.模特档案 ? Object.keys(写真系统.模特档案).length : 0;
295+
const 摄影师数量 = 写真系统?.摄影师档案 ? Object.keys(写真系统.摄影师档案).length : 0;
296+
const 泄露事件数量 = 写真系统?.泄露事件列表?.length || 0;
297+
298+
// 构建 NPC ID -> 姓名的映射,供 LLM 在输出状态时使用真实姓名
299+
const NPC姓名映射: Record<string, string> = {};
300+
if (state.社交列表) {
301+
state.社交列表.forEach(npc => {
302+
NPC姓名映射[npc.id] = npc.姓名 || npc.id;
303+
});
304+
}
305+
306+
// 玩家角色如果是摄影师背景,用角色名作为摄影师姓名
307+
const 摄影师姓名映射: Record<string, string> = {};
308+
if (state.角色?.姓名) {
309+
摄影师姓名映射['player'] = state.角色.姓名;
310+
}
102311

103312
return {
104313
活跃拍摄项目: 活跃项目,
105314
模特数量,
106315
摄影师数量,
107316
泄露事件数量,
317+
NPC姓名映射,
318+
摄影师姓名映射,
108319
内容强度: nsfw设置.NSFW内容强度,
109320
主要玩法层: nsfw设置.主要玩法层,
110321
启用道德选择: nsfw设置.启用道德选择,
322+
启用尺度递进: nsfw设置.启用尺度递进,
323+
启用越界识别: nsfw设置.启用越界识别,
324+
启用安全词系统: nsfw设置.启用安全词系统,
325+
启用照片交付: nsfw设置.启用照片交付,
326+
启用泄露事件: nsfw设置.启用泄露事件,
327+
泄露事件频率: nsfw设置.泄露事件频率,
111328
};
112329
};

0 commit comments

Comments
 (0)