Skip to content

Commit d679bf1

Browse files
committed
feat(bdsm): 新增 BDSM 模块全流程端到端测试
本次提交引入了一个完整的端到端测试 (`test_bdsm_full_journey.ts`),用于验证 BDSM 模块的核心用户旅程,包括论坛发现、NPC 解锁、私聊、任务生成与执行、见面协商和关系阶段推进。 - **新增** `test_bdsm_full_journey.ts` 文件,模拟从头到尾的完整 BDSM 交互流程。 - **扩展** `test_bdsm_workflow.ts`,加入了论坛帖子生成/解析、影响计算、私聊和见面场景等多个单元测试。 - **重构** `bdsmMeetingWorkflow.ts`,移除了已废弃的日常指令管理函数。 - **增强** `deviceRefreshMonitor.ts`,在设备刷新时统一处理校园论坛(普通和 BDSM)的刷新逻辑。
1 parent 66b8d5b commit d679bf1

4 files changed

Lines changed: 590 additions & 26 deletions

File tree

hooks/useGame/bdsmMeetingWorkflow.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export function 解析见面结果(rawText: string): 见面结果 {
158158
}
159159

160160
// ============================================================
161-
// 日常指令管理
161+
// 日常指令类型定义
162162
// ============================================================
163163

164164
export type 日常指令 = {
@@ -170,29 +170,6 @@ export type 日常指令 = {
170170
惩罚提示: string;
171171
};
172172

173-
export function 刷新日常指令(
174-
现有指令: 日常指令[],
175-
_当前回合: number,
176-
_指令持续时间回合: number = 3
177-
): 日常指令[] {
178-
return 现有指令.filter(指令 => {
179-
// 已完成的指令移除,未完成的保留
180-
return !指令.是否完成;
181-
});
182-
}
183-
184-
export function 更新指令完成状态(
185-
指令列表: 日常指令[],
186-
完成的内容: string
187-
): 日常指令[] {
188-
return 指令列表.map(指令 => {
189-
if (指令.内容 === 完成的内容 && !指令.是否完成) {
190-
return { ...指令, 是否完成: true };
191-
}
192-
return 指令;
193-
});
194-
}
195-
196173
// ============================================================
197174
// 任务管理辅助函数
198175
// ============================================================

hooks/useGame/deviceRefreshMonitor.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { DeviceMode, DeviceGameContext } from '../../models/mobileDevice';
88
import type { 校园系统数据 } from '../../models/campusPhone';
99
import type { 校园NSFW设置 } from '../../models/campusNSFW';
1010
import { 生成设备原始消息, 解析AI论坛帖子, 解析AIBDSM帖子 } from './deviceAiWorkflow';
11+
import { 刷新校园论坛 } from './campusForumWorkflow';
1112

1213
export interface 设备刷新任务 {
1314
id: string;
@@ -80,6 +81,35 @@ export const useDeviceRefreshMonitor = (deps: UseDeviceRefreshMonitorDeps) => {
8081
// 根据当前 app 决定刷新哪些内容
8182
const 需要刷新论坛 = app === 'forum' || app === 'confession';
8283
const 需要刷新BDSM = app === 'bdsn';
84+
const 需要刷新校园 = app === 'campus';
85+
86+
// 校园论坛统一刷新路径(普通论坛 + BDSM)
87+
if (需要刷新校园) {
88+
try {
89+
const 论坛结果 = await 刷新校园论坛({
90+
eraId, mode, apiConfig, apiSettings: apiConfig, gameContext,
91+
校园系统: { 论坛帖子列表: [], 私聊会话列表: [], 欲望系统: null, 暴露风险值: 0, 谣言等级: '无' } as any,
92+
nsfw设置, count: 8,
93+
});
94+
if (论坛结果.论坛帖子.length > 0) {
95+
deps.set校园系统(prev => {
96+
const existing = prev.论坛帖子列表 || [];
97+
return { ...prev, 论坛帖子列表: [...论坛结果.论坛帖子, ...existing].slice(0, 50) };
98+
});
99+
论坛帖子数 = 论坛结果.论坛帖子.length;
100+
}
101+
if (论坛结果.BDSM帖子.length > 0) {
102+
deps.set校园系统(prev => {
103+
const existing = prev.BDSM帖子列表 || [];
104+
return { ...prev, BDSM帖子列表: [...论坛结果.BDSM帖子, ...existing].slice(0, 50) };
105+
});
106+
BDSM帖子数 = 论坛结果.BDSM帖子.length;
107+
}
108+
errors.push(...论坛结果.errors);
109+
} catch (err) {
110+
errors.push(`校园刷新失败: ${err instanceof Error ? err.message : String(err)}`);
111+
}
112+
}
83113

84114
if (需要刷新论坛) {
85115
try {

test_bdsm_full_journey.ts

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/**
2+
* BDSM 模块全流程端到端测试
3+
* 模拟完整用户旅程:论坛发现 → NPC解锁 → 私聊 → 任务 → 见面 → 阶段推进
4+
*
5+
* 使用测试 API: https://gcli.ggchan.dev/
6+
*/
7+
8+
import type { 当前可用接口结构 } from './utils/apiConfig';
9+
import { 生成调教任务, 生成日常指令, 评价任务完成, 判定关系阶段推进 } from './hooks/useGame/bdsmTaskWorkflow';
10+
import { 触发任务生成 } from './hooks/useGame/bdsmTaskTrigger';
11+
import { 执行私聊发送工作流 } from './hooks/useGame/privateChatWorkflow';
12+
import { 构建见面场景提示词, 解析见面结果 } from './hooks/useGame/bdsmMeetingWorkflow';
13+
import { 判定寻主召奴联系结果, 生成联系初始对话, 计算BDSM帖子总影响 } from './hooks/useGame/bdsmForumEngine';
14+
import { 请求模型文本, 规范化文本补全消息链 } from './services/ai/chatCompletionClient';
15+
16+
const TEST_API: 当前可用接口结构 = {
17+
id: 'test-api',
18+
名称: '测试API',
19+
供应商: 'openai_compatible',
20+
协议覆盖: 'openai',
21+
baseUrl: 'https://gcli.ggchan.dev/',
22+
apiKey: 'gg-gcli-RALFsIs47kRn7m3HKh98dTj0R48ccM2ln8sIVDc3OSA',
23+
model: 'gemini-2.5-flash',
24+
maxTokens: 4096,
25+
temperature: 0.7,
26+
};
27+
28+
function sleep(ms: number) {
29+
return new Promise(resolve => setTimeout(resolve, ms));
30+
}
31+
32+
interface JourneyState {
33+
npcId: string;
34+
npcName: string;
35+
服从度: number;
36+
关系阶段: string;
37+
完成任务数: number;
38+
完美服从数: number;
39+
违约次数: number;
40+
任务列表: any[];
41+
指令列表: any[];
42+
私聊历史: { sender: string; content: string; isMe: boolean }[];
43+
日志: string[];
44+
}
45+
46+
function 初始状态(): JourneyState {
47+
return {
48+
npcId: 'npc-journey-001',
49+
npcName: '苏婉',
50+
服从度: 20,
51+
关系阶段: '初识',
52+
完成任务数: 0,
53+
完美服从数: 0,
54+
违约次数: 0,
55+
任务列表: [],
56+
指令列表: [],
57+
私聊历史: [],
58+
日志: [],
59+
};
60+
}
61+
62+
function log(state: JourneyState, step: string, msg: string) {
63+
console.log(`\n[${step}] ${msg}`);
64+
state.日志.push(`[${step}] ${msg}`);
65+
}
66+
67+
async function step1_论坛发现(state: JourneyState): Promise<boolean> {
68+
log(state, '步骤1: 论坛发现', '浏览深夜板块,发现寻主召奴帖子...');
69+
70+
const mockPosts = [{ id: 'post-recruit', 标题: '寻找温柔的主人', 内容: '...', 子分类: '寻主召奴', 影响等级: '严重' as const }] as any;
71+
const 影响结果 = 计算BDSM帖子总影响({ 帖子列表: mockPosts, 内容强度: '中度' });
72+
log(state, '步骤1', `帖子影响值: +${影响结果.总推进值}`);
73+
74+
const 联系结果 = 判定寻主召奴联系结果({ 玩家欲望阶段: '试探', 内容强度: '中度', 玩家社交NPC数: 5 });
75+
log(state, '步骤1', `联系结果: ${联系结果.结果} (概率: ${(联系结果.成功概率 * 100).toFixed(1)}%)`);
76+
77+
const 初始对话 = 生成联系初始对话({
78+
id: 'post-recruit', 标题: '寻找温柔的主人', 内容: '...', 子分类: '寻主召奴', 影响等级: '严重',
79+
寻主召奴信息: { 招募方角色: '召奴', 期望关系类型: '主/奴', 是否已联系: false, 联系状态: '未联系' },
80+
} as any);
81+
log(state, '步骤1', `初始对话: ${初始对话.slice(0, 40)}...`);
82+
83+
state.私聊历史.push({ sender: 'npc', content: 初始对话, isMe: false });
84+
return true;
85+
}
86+
87+
async function step2_NPC解锁(state: JourneyState): Promise<boolean> {
88+
log(state, '步骤2: NPC解锁', `从帖子创建 NPC「${state.npcName}」,好感度 20,关系初识`);
89+
return true;
90+
}
91+
92+
async function step3_私聊建立(state: JourneyState): Promise<boolean> {
93+
log(state, '步骤3: 私聊建立', `向「${state.npcName}」发送第一条消息...`);
94+
95+
const 私聊上下文 = {
96+
npcId: state.npcId, npcName: state.npcName, 玩家姓名: '测试玩家',
97+
会话历史: state.私聊历史,
98+
校园系统: {
99+
欲望系统: {
100+
NPC欲望档案: {
101+
[state.npcId]: {
102+
姓名: state.npcName, 性格特征: '温柔内向', 身份: '文学社成员', 欲望阶段: '试探',
103+
服从度: { 当前值: state.服从度, 未完成指令数: 0 },
104+
BDSM关系: {
105+
阶段: state.关系阶段 as any, 服从度: state.服从度, 安全词: '月光', 权力倾向: '支配',
106+
契约记录: [], 日常指令: [], 调教任务: [], 里程碑: [],
107+
违约次数: state.违约次数, 完美服从次数: state.完美服从数, 完成任务数: state.完成任务数,
108+
},
109+
},
110+
},
111+
},
112+
},
113+
apiConfig: TEST_API,
114+
};
115+
116+
try {
117+
const 回复 = await 执行私聊发送工作流(私聊上下文, '你好,我是从论坛看到你的帖子的。');
118+
log(state, '步骤3', `NPC 回复: ${回复.npcReply.slice(0, 60)}...`);
119+
state.私聊历史.push({ sender: 'player', content: '你好,我是从论坛看到你的帖子的。', isMe: true });
120+
state.私聊历史.push({ sender: 'npc', content: 回复.npcReply, isMe: false });
121+
return true;
122+
} catch (err) {
123+
log(state, '步骤3', `私聊失败: ${err instanceof Error ? err.message : String(err)}`);
124+
return false;
125+
}
126+
}
127+
128+
async function step4_任务生成(state: JourneyState): Promise<boolean> {
129+
log(state, '步骤4: 任务生成', `为「${state.npcName}」生成调教任务...`);
130+
131+
try {
132+
const 任务列表 = await 生成调教任务({
133+
契约类型: '口头约定', 契约状态: '口头约定', 服从度: state.服从度,
134+
权力倾向: '支配', 关系阶段: state.关系阶段, 已解锁场景: [],
135+
历史任务数量: state.任务列表.length, NPC性格特征: state.npcName,
136+
}, TEST_API);
137+
138+
if (任务列表.length === 0) { log(state, '步骤4', '未生成任何任务'); return false; }
139+
140+
state.任务列表 = 任务列表.map((t, i) => ({ ...t, id: `task-${i}`, 状态: '待接受' as const }));
141+
log(state, '步骤4', `生成 ${任务列表.length} 个任务:`);
142+
任务列表.forEach((t, i) => log(state, '步骤4', ` 任务${i + 1}: [${t.类型}] ${t.标题} (${t.难度})`));
143+
return true;
144+
} catch (err) {
145+
log(state, '步骤4', `任务生成失败: ${err instanceof Error ? err.message : String(err)}`);
146+
return false;
147+
}
148+
}
149+
150+
async function step5_任务执行(state: JourneyState): Promise<boolean> {
151+
log(state, '步骤5: 任务执行', '接受并完成第一个任务...');
152+
153+
if (state.任务列表.length === 0) { log(state, '步骤5', '无可用任务,跳过'); return true; }
154+
155+
const 任务 = state.任务列表[0];
156+
try {
157+
const 评价 = await 评价任务完成(
158+
{ 类型: 任务.类型, 难度: 任务.难度, 描述: 任务.描述 },
159+
'玩家成功完成了任务,表现得比预期更好。',
160+
state.服从度, state.npcName, TEST_API
161+
);
162+
163+
log(state, '步骤5', `评价: ${评价.评价}, 服从度变化: ${评价.服从度变化}`);
164+
state.服从度 = Math.max(0, Math.min(100, state.服从度 + 评价.服从度变化));
165+
state.完成任务数 += 1;
166+
if (评价.评价 === '完美' || (评价.评价 as string) === '完美') state.完美服从数 += 1;
167+
state.任务列表[0].状态 = '已完成';
168+
return true;
169+
} catch (err) {
170+
log(state, '步骤5', `任务评价失败: ${err instanceof Error ? err.message : String(err)}`);
171+
return false;
172+
}
173+
}
174+
175+
async function step6_日常指令(state: JourneyState): Promise<boolean> {
176+
log(state, '步骤6: 日常指令', '刷新日常指令...');
177+
178+
try {
179+
const 新指令 = await 生成日常指令({
180+
服从度: state.服从度, 契约状态: '口头约定', 关系阶段: state.关系阶段,
181+
已发布指令数: state.指令列表.length, NPC性格特征: state.npcName,
182+
}, TEST_API);
183+
184+
if (新指令.length === 0) { log(state, '步骤6', '未生成任何指令'); return false; }
185+
186+
state.指令列表 = 新指令;
187+
log(state, '步骤6', `生成 ${新指令.length} 条指令:`);
188+
新指令.forEach((d, i) => log(state, '步骤6', ` 指令${i + 1}: [${d.分类}] ${d.内容.slice(0, 30)}...`));
189+
return true;
190+
} catch (err) {
191+
log(state, '步骤6', `指令生成失败: ${err instanceof Error ? err.message : String(err)}`);
192+
return false;
193+
}
194+
}
195+
196+
async function step7_见面协商(state: JourneyState): Promise<boolean> {
197+
log(state, '步骤7: 见面协商', '安排第一次见面...');
198+
199+
const 提示词 = 构建见面场景提示词({
200+
NPC姓名: state.npcName, NPC性格特征: '温柔内向,喜欢文学',
201+
关系阶段: state.关系阶段 as any, 服从度: state.服从度, 权力倾向: '支配',
202+
契约类型: '未缔结', 安全词: '月光',
203+
底线列表: ['不伤害身体', '不涉及第三人'],
204+
见面地点: '咖啡厅', 见面时间: '3 回合后',
205+
私聊协商摘要: '双方通过私聊协商确定了见面时间和地点',
206+
历史任务摘要: `- 完成任务数: ${state.完成任务数}\n- 服从度: ${state.服从度}`,
207+
已解锁场景: [],
208+
});
209+
210+
try {
211+
const messages = 规范化文本补全消息链([
212+
{ role: 'system', content: '你是一个互动小说生成器。请根据给定的上下文生成一段第三人称见面场景描述。要有氛围感和情绪张力,给出 2-3 个互动选项供玩家选择。以第三人称叙事,不超过 500 字。' },
213+
{ role: 'user', content: 提示词 },
214+
]);
215+
216+
const 场景文本 = await 请求模型文本(TEST_API, messages, { temperature: 0.8 });
217+
log(state, '步骤7', `场景长度: ${场景文本.length} 字符`);
218+
219+
const 解析 = 解析见面结果(场景文本);
220+
log(state, '步骤7', `见面成功: ${解析.见面成功}`);
221+
return true;
222+
} catch (err) {
223+
log(state, '步骤7', `见面场景生成失败: ${err instanceof Error ? err.message : String(err)}`);
224+
return false;
225+
}
226+
}
227+
228+
async function step8_阶段推进(state: JourneyState): Promise<boolean> {
229+
log(state, '步骤8: 阶段推进判定', `当前阶段: ${state.关系阶段}, 服从度: ${state.服从度}`);
230+
231+
try {
232+
const 结果 = await 判定关系阶段推进(
233+
state.关系阶段, state.服从度, state.完成任务数,
234+
state.完美服从数, state.违约次数, '口头约定',
235+
state.日志.map(l => l.slice(0, 40)).join('; '),
236+
TEST_API
237+
);
238+
239+
log(state, '步骤8', `是否推进: ${结果.是否推进}`);
240+
log(state, '步骤8', `下一阶段: ${结果.下一阶段 || '无'}`);
241+
log(state, '步骤8', `理由: ${结果.理由.slice(0, 80)}`);
242+
243+
if (结果.下一阶段) state.关系阶段 = 结果.下一阶段;
244+
return true;
245+
} catch (err) {
246+
log(state, '步骤8', `阶段判定失败: ${err instanceof Error ? err.message : String(err)}`);
247+
return false;
248+
}
249+
}
250+
251+
async function main() {
252+
console.log('BDSM 模块全流程端到端测试');
253+
console.log('API: https://gcli.ggchan.dev/');
254+
console.log('='.repeat(50));
255+
256+
const state = 初始状态();
257+
const results: Record<string, boolean> = {};
258+
const startTime = Date.now();
259+
260+
const steps: [string, (s: JourneyState) => Promise<boolean>][] = [
261+
['步骤1: 论坛发现', step1_论坛发现],
262+
['步骤2: NPC解锁', step2_NPC解锁],
263+
['步骤3: 私聊建立', step3_私聊建立],
264+
['步骤4: 任务生成', step4_任务生成],
265+
['步骤5: 任务执行', step5_任务执行],
266+
['步骤6: 日常指令', step6_日常指令],
267+
['步骤7: 见面协商', step7_见面协商],
268+
['步骤8: 阶段推进', step8_阶段推进],
269+
];
270+
271+
for (const [name, step] of steps) {
272+
try {
273+
results[name] = await step(state);
274+
await sleep(2000);
275+
} catch (err) {
276+
log(state, name, `异常: ${err instanceof Error ? err.message : String(err)}`);
277+
results[name] = false;
278+
}
279+
}
280+
281+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
282+
283+
console.log('\n' + '='.repeat(50));
284+
console.log('旅程结果:');
285+
let passCount = 0;
286+
for (const [name, ok] of Object.entries(results)) {
287+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}`);
288+
if (ok) passCount++;
289+
}
290+
291+
console.log(`\n最终状态: ${state.npcName} | ${state.关系阶段} | 服从度 ${state.服从度} | 完成任务 ${state.完成任务数} | ${elapsed}s`);
292+
console.log(`总计: ${passCount}/${Object.keys(results).length} 通过`);
293+
294+
if (passCount === Object.keys(results).length) {
295+
console.log('\n全程通过!BDSM 模块端到端流程正常工作。');
296+
} else {
297+
console.log(`\n${Object.keys(results).length - passCount} 步失败。`);
298+
}
299+
}
300+
301+
main().catch(err => {
302+
console.error('测试异常:', err);
303+
process.exit(1);
304+
});

0 commit comments

Comments
 (0)