|
| 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