{"repo": "butiter/beautifullife", "files": {"server/index.js": {"53d16b974c9f0c5a66e331d640089247572ae9a3": {"t": 1785746586.3483303, "n": 48300, "h": "45745efdb8e0b6ba", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是创世纪输入审查员。你的核心目标是审核玩家输入。\n 请仅输出 JSON 对象,不要输出任何多余文字。\n 我将会给你两段文本,分别是用户的世界描述输入和用户的角色描述输入。\n 你需要完成四项检查:\n 1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n 2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n 3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n 4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n 输出的 JSON 结构必须为(下面只展示结构示例,value 只是占位,禁止照抄!!!!!!!): \n {\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"这里填安全性判定说明\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"这里填效用判定说明\" },\n \"expansion\":{ \"status\": \"pass|warn\", \"message\": \"这里填扩展性判定说明\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"这里填是否可直接构建的说明\" }\n }\n }\n\n 注意:\n 1. 不允许原样返回示例里的占位字符串(例如以“这里填……”)。\n`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return { checks };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n }\n };\n }\n\n const { checks } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks\n };\n};\n\nconst optimizeGenesisInput = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n\n if (!apiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n\n try {\n const parsed = await callJsonModel({\n apiKey,\n model: highQuality.model,\n provider: highQuality.provider,\n systemPrompt: `你是创世纪输入优化 agent。你的任务是整理并优化玩家的世界描述与角色描述。\n请仅输出 JSON 对象,不要输出任何多余文字。\n要求:\n1) 过滤过于精确或过度量化的细节,保留核心设定与氛围。\n2) 统一人称与叙述视角,使描述更连贯。\n3) 语言要有故事感,但避免强指令、硬性任务或具体数值。\n4) 必须保留玩家的核心意图与主题。\n输出 JSON 结构必须为:\n{\n \"worldDesc\": \"优化后的世界描述\",\n \"charDesc\": \"优化后的角色描述\"\n}`,\n userPayload: inputPayload,\n logTag: 'genesis_optimizer'\n });\n\n return {\n worldDesc:\n parsed?.worldDesc || sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.charDesc || sanitizePrompt(charDescRaw)\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: highQuality.model,\n input: { ...inputPayload, provider: highQuality.provider },\n output: `ERROR: ${message}`,\n error: true,\n provider: highQuality.provider\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst runInputCheck = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 InputCheckAgent。只输出 JSON,不要输出任何多余文字。\n你需要审查玩家输入的行动描述,并执行以下规则:\n1) 合规性检查:如果包含明确违法、仇恨、成人露骨内容或其他违规内容,设置 pass=false,并给出 error_message。\n2) 动作/结果分离:如果文本包含行动结果(例如“我挥剑砍死了哥布林”),必须删除结果,仅保留动作。\n3) 清洗提示:如果发生了结果删除,warning_message 写明“结果由系统判定,已提取你的动作”,否则为 null。\n\n输出 JSON 结构必须为:\n{\n \"pass\": true/false,\n \"cleaned_action\": \"清洗后的动作文字\",\n \"warning_message\": \"提示语或 null\",\n \"error_message\": \"违规原因或 null\"\n}`;\n\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { action: input },\n logTag: 'input_check'\n });\n};\n\nconst runDeduction = async ({\n apiKey,\n provider,\n model,\n world_desp_and_user_desp,\n goal,\n task_desp,\n task_detail,\n action_history,\n current_action\n}) => {\n const systemPrompt = `你是 DeductionAgent。只输出 JSON,不要输出任何多余文字。\n你负责根据世界观、任务目标、历史行动来推演玩家当前行动的直接结果。\n\n输出 JSON 结构必须为:\n{\n \"is_completed\": 1 或 0,\n \"result_description\": \"行动造成的直接结果描述\",\n \"next_possible_actions\": [\"建议行动1\", \"建议行动2\"]\n}\n要求:\n- result_description 必须简洁、具体、只描述本次行动的结果。\n- next_possible_actions 必须提供 2 个建议。\n- is_completed 仅在任务达成时为 1,否则为 0。`;\n\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: {\n world_desp_and_user_desp,\n goal,\n task_desp,\n task_detail,\n action_history,\n current_action\n },\n logTag: 'deduction'\n });\n};\n\nconst normalizeNextActions = (actions) => {\n const safeActions = Array.isArray(actions)\n ? actions.map((action) => String(action || '').trim()).filter(Boolean).slice(0, 2)\n : [];\n const fallback = ['观察周遭动静', '重新评估下一步行动'];\n while (safeActions.length < 2) {\n safeActions.push(fallback[safeActions.length]);\n }\n return safeActions;\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n\n try {\n const optimizedInput = await optimizeGenesisInput({\n worldDescRaw,\n charDescRaw,\n modelSettings\n });\n addProcessLog('创世纪输入优化完成');\n const input = {\n worldDesc: optimizedInput.worldDesc || worldDescRaw,\n charDesc: optimizedInput.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload,\n playerInput: {\n ...buildPayload.playerInput,\n raw: { worldDesc: worldDescRaw, charDesc: charDescRaw }\n }\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/input-check', async (req, res) => {\n const payload = req.body || {};\n const action = payload.action?.trim();\n if (!action) {\n return res.status(400).json({ ok: false, error: '动作不能为空。' });\n }\n\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少低质量文本生成 API Key。' });\n }\n\n try {\n const result = await runInputCheck({\n apiKey,\n input: action,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n\n if (result?.pass === false) {\n return res\n .status(400)\n .json({ ok: false, pass: false, error: result?.error_message || '输入包含违规内容。' });\n }\n\n return res.json({\n ok: true,\n pass: true,\n cleaned_action: result?.cleaned_action || action,\n warning_message: result?.warning_message || null\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({ ok: false, error: `输入审查失败:${message}` });\n }\n});\n\napp.post('/api/deduction', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少低质量文本生成 API Key。' });\n }\n\n try {\n const result = await runDeduction({\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model,\n world_desp_and_user_desp: payload.world_desp_and_user_desp,\n goal: payload.goal,\n task_desp: payload.task_desp,\n task_detail: payload.task_detail,\n action_history: Array.isArray(payload.action_history) ? payload.action_history : [],\n current_action: payload.current_action\n });\n\n const normalized = {\n is_completed: Number(result?.is_completed) === 1 ? 1 : 0,\n result_description: result?.result_description || '行动产生了新的变化。',\n next_possible_actions: normalizeNextActions(result?.next_possible_actions)\n };\n\n return res.json({ ok: true, result: normalized });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({ ok: false, error: `推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n try {\n const optimizedInput = await optimizeGenesisInput({\n worldDescRaw,\n charDescRaw,\n modelSettings\n });\n addProcessLog('创世纪输入优化完成');\n const input = {\n worldDesc: optimizedInput.worldDesc || worldDescRaw,\n charDesc: optimizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload,\n playerInput: {\n ...buildPayload.playerInput,\n raw: { worldDesc: worldDescRaw, charDesc: charDescRaw }\n }\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "b1a860e62d14b8c7d06e971281cd8cede7c68d06": {"t": 1785766001.577807, "n": 48300, "h": "45745efdb8e0b6ba", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是创世纪输入审查员。你的核心目标是审核玩家输入。\n 请仅输出 JSON 对象,不要输出任何多余文字。\n 我将会给你两段文本,分别是用户的世界描述输入和用户的角色描述输入。\n 你需要完成四项检查:\n 1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n 2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n 3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n 4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n 输出的 JSON 结构必须为(下面只展示结构示例,value 只是占位,禁止照抄!!!!!!!): \n {\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"这里填安全性判定说明\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"这里填效用判定说明\" },\n \"expansion\":{ \"status\": \"pass|warn\", \"message\": \"这里填扩展性判定说明\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"这里填是否可直接构建的说明\" }\n }\n }\n\n 注意:\n 1. 不允许原样返回示例里的占位字符串(例如以“这里填……”)。\n`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return { checks };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n }\n };\n }\n\n const { checks } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks\n };\n};\n\nconst optimizeGenesisInput = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n\n if (!apiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n\n try {\n const parsed = await callJsonModel({\n apiKey,\n model: highQuality.model,\n provider: highQuality.provider,\n systemPrompt: `你是创世纪输入优化 agent。你的任务是整理并优化玩家的世界描述与角色描述。\n请仅输出 JSON 对象,不要输出任何多余文字。\n要求:\n1) 过滤过于精确或过度量化的细节,保留核心设定与氛围。\n2) 统一人称与叙述视角,使描述更连贯。\n3) 语言要有故事感,但避免强指令、硬性任务或具体数值。\n4) 必须保留玩家的核心意图与主题。\n输出 JSON 结构必须为:\n{\n \"worldDesc\": \"优化后的世界描述\",\n \"charDesc\": \"优化后的角色描述\"\n}`,\n userPayload: inputPayload,\n logTag: 'genesis_optimizer'\n });\n\n return {\n worldDesc:\n parsed?.worldDesc || sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.charDesc || sanitizePrompt(charDescRaw)\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: highQuality.model,\n input: { ...inputPayload, provider: highQuality.provider },\n output: `ERROR: ${message}`,\n error: true,\n provider: highQuality.provider\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst runInputCheck = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 InputCheckAgent。只输出 JSON,不要输出任何多余文字。\n你需要审查玩家输入的行动描述,并执行以下规则:\n1) 合规性检查:如果包含明确违法、仇恨、成人露骨内容或其他违规内容,设置 pass=false,并给出 error_message。\n2) 动作/结果分离:如果文本包含行动结果(例如“我挥剑砍死了哥布林”),必须删除结果,仅保留动作。\n3) 清洗提示:如果发生了结果删除,warning_message 写明“结果由系统判定,已提取你的动作”,否则为 null。\n\n输出 JSON 结构必须为:\n{\n \"pass\": true/false,\n \"cleaned_action\": \"清洗后的动作文字\",\n \"warning_message\": \"提示语或 null\",\n \"error_message\": \"违规原因或 null\"\n}`;\n\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { action: input },\n logTag: 'input_check'\n });\n};\n\nconst runDeduction = async ({\n apiKey,\n provider,\n model,\n world_desp_and_user_desp,\n goal,\n task_desp,\n task_detail,\n action_history,\n current_action\n}) => {\n const systemPrompt = `你是 DeductionAgent。只输出 JSON,不要输出任何多余文字。\n你负责根据世界观、任务目标、历史行动来推演玩家当前行动的直接结果。\n\n输出 JSON 结构必须为:\n{\n \"is_completed\": 1 或 0,\n \"result_description\": \"行动造成的直接结果描述\",\n \"next_possible_actions\": [\"建议行动1\", \"建议行动2\"]\n}\n要求:\n- result_description 必须简洁、具体、只描述本次行动的结果。\n- next_possible_actions 必须提供 2 个建议。\n- is_completed 仅在任务达成时为 1,否则为 0。`;\n\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: {\n world_desp_and_user_desp,\n goal,\n task_desp,\n task_detail,\n action_history,\n current_action\n },\n logTag: 'deduction'\n });\n};\n\nconst normalizeNextActions = (actions) => {\n const safeActions = Array.isArray(actions)\n ? actions.map((action) => String(action || '').trim()).filter(Boolean).slice(0, 2)\n : [];\n const fallback = ['观察周遭动静', '重新评估下一步行动'];\n while (safeActions.length < 2) {\n safeActions.push(fallback[safeActions.length]);\n }\n return safeActions;\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n\n try {\n const optimizedInput = await optimizeGenesisInput({\n worldDescRaw,\n charDescRaw,\n modelSettings\n });\n addProcessLog('创世纪输入优化完成');\n const input = {\n worldDesc: optimizedInput.worldDesc || worldDescRaw,\n charDesc: optimizedInput.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload,\n playerInput: {\n ...buildPayload.playerInput,\n raw: { worldDesc: worldDescRaw, charDesc: charDescRaw }\n }\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/input-check', async (req, res) => {\n const payload = req.body || {};\n const action = payload.action?.trim();\n if (!action) {\n return res.status(400).json({ ok: false, error: '动作不能为空。' });\n }\n\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少低质量文本生成 API Key。' });\n }\n\n try {\n const result = await runInputCheck({\n apiKey,\n input: action,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n\n if (result?.pass === false) {\n return res\n .status(400)\n .json({ ok: false, pass: false, error: result?.error_message || '输入包含违规内容。' });\n }\n\n return res.json({\n ok: true,\n pass: true,\n cleaned_action: result?.cleaned_action || action,\n warning_message: result?.warning_message || null\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({ ok: false, error: `输入审查失败:${message}` });\n }\n});\n\napp.post('/api/deduction', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少低质量文本生成 API Key。' });\n }\n\n try {\n const result = await runDeduction({\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model,\n world_desp_and_user_desp: payload.world_desp_and_user_desp,\n goal: payload.goal,\n task_desp: payload.task_desp,\n task_detail: payload.task_detail,\n action_history: Array.isArray(payload.action_history) ? payload.action_history : [],\n current_action: payload.current_action\n });\n\n const normalized = {\n is_completed: Number(result?.is_completed) === 1 ? 1 : 0,\n result_description: result?.result_description || '行动产生了新的变化。',\n next_possible_actions: normalizeNextActions(result?.next_possible_actions)\n };\n\n return res.json({ ok: true, result: normalized });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({ ok: false, error: `推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n try {\n const optimizedInput = await optimizeGenesisInput({\n worldDescRaw,\n charDescRaw,\n modelSettings\n });\n addProcessLog('创世纪输入优化完成');\n const input = {\n worldDesc: optimizedInput.worldDesc || worldDescRaw,\n charDesc: optimizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload,\n playerInput: {\n ...buildPayload.playerInput,\n raw: { worldDesc: worldDescRaw, charDesc: charDescRaw }\n }\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "5f9c6d11cc732224ce74d852aeb37e2b03e6a477": {"t": 1785766003.200185, "n": 43937, "h": "559ebb6bd649fcb3", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是创世纪输入审查员。你的核心目标是审核玩家输入。\n 请仅输出 JSON 对象,不要输出任何多余文字。\n 我将会给你两段文本,分别是用户的世界描述输入和用户的角色描述输入。\n 你需要完成四项检查:\n 1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n 2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n 3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n 4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n 输出的 JSON 结构必须为(下面只展示结构示例,value 只是占位,禁止照抄!!!!!!!): \n {\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"这里填安全性判定说明\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"这里填效用判定说明\" },\n \"expansion\":{ \"status\": \"pass|warn\", \"message\": \"这里填扩展性判定说明\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"这里填是否可直接构建的说明\" }\n }\n }\n\n 注意:\n 1. 不允许原样返回示例里的占位字符串(例如以“这里填……”)。\n`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return { checks };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n }\n };\n }\n\n const { checks } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks\n };\n};\n\nconst optimizeGenesisInput = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n\n if (!apiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n\n try {\n const parsed = await callJsonModel({\n apiKey,\n model: highQuality.model,\n provider: highQuality.provider,\n systemPrompt: `你是创世纪输入优化 agent。你的任务是整理并优化玩家的世界描述与角色描述。\n请仅输出 JSON 对象,不要输出任何多余文字。\n要求:\n1) 过滤过于精确或过度量化的细节,保留核心设定与氛围。\n2) 统一人称与叙述视角,使描述更连贯。\n3) 语言要有故事感,但避免强指令、硬性任务或具体数值。\n4) 必须保留玩家的核心意图与主题。\n输出 JSON 结构必须为:\n{\n \"worldDesc\": \"优化后的世界描述\",\n \"charDesc\": \"优化后的角色描述\"\n}`,\n userPayload: inputPayload,\n logTag: 'genesis_optimizer'\n });\n\n return {\n worldDesc:\n parsed?.worldDesc || sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.charDesc || sanitizePrompt(charDescRaw)\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: highQuality.model,\n input: { ...inputPayload, provider: highQuality.provider },\n output: `ERROR: ${message}`,\n error: true,\n provider: highQuality.provider\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n\n try {\n const optimizedInput = await optimizeGenesisInput({\n worldDescRaw,\n charDescRaw,\n modelSettings\n });\n addProcessLog('创世纪输入优化完成');\n const input = {\n worldDesc: optimizedInput.worldDesc || worldDescRaw,\n charDesc: optimizedInput.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload,\n playerInput: {\n ...buildPayload.playerInput,\n raw: { worldDesc: worldDescRaw, charDesc: charDescRaw }\n }\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n try {\n const optimizedInput = await optimizeGenesisInput({\n worldDescRaw,\n charDescRaw,\n modelSettings\n });\n addProcessLog('创世纪输入优化完成');\n const input = {\n worldDesc: optimizedInput.worldDesc || worldDescRaw,\n charDesc: optimizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload,\n playerInput: {\n ...buildPayload.playerInput,\n raw: { worldDesc: worldDescRaw, charDesc: charDescRaw }\n }\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "21e5ae203201ec11d386f9a888ec6d8a962c1073": {"t": 1785766004.7576995, "n": 42889, "h": "c998af6bee91121f", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是创世纪输入审查与整理员。你的核心目标是审核玩家输入,并将玩家输入过滤为安全的版本。\n 请仅输出 JSON 对象,不要输出任何多余文字。\n 我将会给你两段文本,分别是用户的世界描述输入和用户的角色描述输入。\n 你需要完成四项检查并给出新的过滤后的世界观描述/角色描述:\n 1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n 2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n 3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n 4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n 同时返回 sanitizedInput。sanitizedInput是过滤后的世界描述与角色描述(而不是审查结果)。\n 你需要在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇,也可以简单扩展。\n 输出的 JSON 结构必须为(下面只展示结构示例,value 只是占位,禁止照抄!!!!!!!): \n {\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"这里填安全性判定说明\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"这里填效用判定说明\" },\n \"expansion\":{ \"status\": \"pass|warn\", \"message\": \"这里填扩展性判定说明\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"这里填是否可直接构建的说明\" }\n },\n \"sanitizedInput\": {\n \"worldDesc\": \"<根据用户 worldDesc 清理后的世界描述>\",\n \"charDesc\": \"<根据用户 charDesc 清理后的角色描述>\"\n }\n }\n\n 注意:\n 1. 你必须根据用户输入实际生成 sanitizedInput.worldDesc / sanitizedInput.charDesc。\n 2. 不允许原样返回示例里的占位字符串(例如以“这里填……”或带尖括号 <> 的内容)。\n`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "1618cfa7686b538567d81ad0a9c0fe8d2afa6515": {"t": 1785766006.8792975, "n": 43871, "h": "ad8c5f08d21d621a", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是创世纪输入审查员。你的核心目标是审核玩家输入的安全性与可用性。\n 请仅输出 JSON 对象,不要输出任何多余文字。\n 我将会给你两段文本,分别是用户的世界描述输入和用户的角色描述输入。\n 你需要完成四项检查并给出判定结果:\n 1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n 2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n 3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n 4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n 输出的 JSON 结构必须为(下面只展示结构示例,value 只是占位,禁止照抄!!!!!!!): \n {\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"这里填安全性判定说明\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"这里填效用判定说明\" },\n \"expansion\":{ \"status\": \"pass|warn\", \"message\": \"这里填扩展性判定说明\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"这里填是否可直接构建的说明\" }\n }\n }\n\n 注意:\n 1. 不允许原样返回示例里的占位字符串(例如以“这里填……”或带尖括号 <> 的内容)。\n`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n }\n };\n }\n\n const { checks } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks\n };\n};\n\nconst optimizeGenesisInput = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const systemPrompt = `你是创世纪文本优化器。只输出 JSON,不要输出任何多余文字或代码块。\n你将收到玩家的世界描述与角色描述,请优化整理文本:\n1) 保留原始意图,增强故事感与叙事连贯性。\n2) 过滤过于精确的描述(如具体数值、坐标、绝对命令、过度细节)。\n3) 统一人称为第三人称叙述,避免自我指令化语气。\n4) 输出精炼且可用于生成世界与角色设定的文本。\n5) 提取或生成玩家名字:如果输入中明确出现名字,直接使用;如果没有,则根据故事设定起一个不突兀的名字。\n\n输出 JSON 结构:\n{\n \"playerName\": \"...\",\n \"worldDesc\": \"...\",\n \"charDesc\": \"...\"\n}`;\n\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'genesis_optimizer'\n });\n\n return {\n playerName: parsed?.playerName || `旅者${rng(100, 999)}`,\n worldDesc:\n parsed?.worldDesc || sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.charDesc || sanitizePrompt(charDescRaw)\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\n { \"name\": \"技能1\", \"effect\": \"效果描述\" },\n { \"name\": \"技能2\", \"effect\": \"效果描述\" },\n { \"name\": \"技能3\", \"effect\": \"效果描述\" }\n ],\n \"item\": [\n { \"name\": \"道具1\", \"effect\": \"效果描述\" },\n { \"name\": \"道具2\", \"effect\": \"效果描述\" },\n { \"name\": \"道具3\", \"effect\": \"效果描述\" }\n ]\n}\n技能与道具必须与世界观一致,效果描述需清楚说明用途或增益。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc\n },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: input.playerName || `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n let optimizedInput;\n try {\n optimizedInput = await optimizeGenesisInput({\n worldDescRaw: input.worldDesc,\n charDescRaw: input.charDesc,\n apiKey: generationApiKey,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('创世纪输入优化完成', {\n provider: highQuality.provider,\n model: highQuality.model\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪输入优化失败,使用本地清理结果', { error: message });\n optimizedInput = {\n playerName: `旅者${rng(100, 999)}`,\n worldDesc: sanitizePrompt(input.worldDesc, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(input.charDesc)\n };\n }\n\n const generationInput = {\n ...optimizedInput,\n rawWorldDesc: input.worldDesc,\n rawCharDesc: input.charDesc,\n settings: input.settings\n };\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input: generationInput,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input: generationInput,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input: generationInput,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input: generationInput,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(generationInput, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: generationInput,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const input = {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "cd0ccb6b447d2ca1c4cb7f57457fb03d8c5029a2": {"t": 1785766008.325653, "n": 42889, "h": "c998af6bee91121f", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是创世纪输入审查与整理员。你的核心目标是审核玩家输入,并将玩家输入过滤为安全的版本。\n 请仅输出 JSON 对象,不要输出任何多余文字。\n 我将会给你两段文本,分别是用户的世界描述输入和用户的角色描述输入。\n 你需要完成四项检查并给出新的过滤后的世界观描述/角色描述:\n 1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n 2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n 3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n 4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n 同时返回 sanitizedInput。sanitizedInput是过滤后的世界描述与角色描述(而不是审查结果)。\n 你需要在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇,也可以简单扩展。\n 输出的 JSON 结构必须为(下面只展示结构示例,value 只是占位,禁止照抄!!!!!!!): \n {\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"这里填安全性判定说明\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"这里填效用判定说明\" },\n \"expansion\":{ \"status\": \"pass|warn\", \"message\": \"这里填扩展性判定说明\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"这里填是否可直接构建的说明\" }\n },\n \"sanitizedInput\": {\n \"worldDesc\": \"<根据用户 worldDesc 清理后的世界描述>\",\n \"charDesc\": \"<根据用户 charDesc 清理后的角色描述>\"\n }\n }\n\n 注意:\n 1. 你必须根据用户输入实际生成 sanitizedInput.worldDesc / sanitizedInput.charDesc。\n 2. 不允许原样返回示例里的占位字符串(例如以“这里填……”或带尖括号 <> 的内容)。\n`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "6ec46d513d62525be4281b7f656433ec50ad1ae5": {"t": 1785766010.0492594, "n": 42360, "h": "bab18af7800f9ba3", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst generateTaskGene = async ({ apiKey, input, provider, model }) => {\n const systemPrompt = `你是 TASK_GENE Agent。只输出 JSON,不要输出任何多余文字。\n你将基于世界观与起始任务,生成当前任务的详细说明。\n输入字段:\n- world_setting\n- origin_story\n- Power_level\n- task_desp\n\n输出 JSON 必须包含以下字段:\n- world_desp_and_user_desp:中等长度文本,包含 world_setting、origin_story、Power_level,在当前任务视角下的叙述\n- goal:完整、可执行的完成判定条件\n- task_desp:任务的玩家视角描述(不包含玩家未知信息)\n- task_detail:任务的系统视角描述(全视角)\n- firs_opt:玩家可能进行的第一个行动\n- seco_opt:玩家可能进行的第二个行动\n- thir_opt:玩家可能进行的第三个行动`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: input,\n logTag: 'task_gene'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/task-gene', async (req, res) => {\n addProcessLog('进入任务推演流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const apiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return res.status(400).json({ ok: false, error: '缺少高质量文本生成 API Key。' });\n }\n\n try {\n const taskGene = await generateTaskGene({\n apiKey,\n input: {\n world_setting: payload.world_setting,\n origin_story: payload.origin_story,\n Power_level: payload.Power_level,\n task_desp: payload.task_desp\n },\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('任务推演完成', { provider: highQuality.provider, model: highQuality.model });\n return res.json({ ok: true, taskGene });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('任务推演失败', { error: message });\n return res.status(502).json({ ok: false, error: `任务推演失败:${message}` });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "c6f35028acfe6a83beea568d5204c73d1e9fa0f2": {"t": 1785766011.554452, "n": 40563, "h": "4fe750172eaaeece", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `生成一张花在羊皮卷上的虚拟世界地图,该世界的世界观包含一定的魔法和武力。你的地图里需要包含${mapPlaces}等地点,每个地点用是一个图标,像是游戏里的那种,不要有任何文字。每个图标位置有一个明确的圆或小点点或者什么东西作为地点标识。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "476c5c873dd55f478dc08e1c0e23f0bb42456f78": {"t": 1785766013.1303377, "n": 40476, "h": "9ce095e888d14a84", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst MAX_PROCESS_LOGS = 80;\nconst llmLogs = [];\nconst processLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addProcessLog = (message, meta = {}) => {\n processLogs.push({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n message,\n meta\n });\n if (processLogs.length > MAX_PROCESS_LOGS) {\n processLogs.shift();\n }\n};\n\nconst addImageSuccessLog = ({ prompt, size, output, model, provider }) => {\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output\n });\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n addProcessLog('进入资产生成流程', { provider, model });\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n addProcessLog('头像图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: avatarPrompt,\n size: imageSize,\n output: avatarResult.value,\n model,\n provider\n });\n addProcessLog('头像图片生成完成', { provider, model });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n addProcessLog('地图图片生成失败', { provider, model });\n } else {\n addImageSuccessLog({\n prompt: mapPrompt,\n size: imageSize,\n output: mapResult.value,\n model,\n provider\n });\n addProcessLog('地图图片生成完成', { provider, model });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n addProcessLog('进入创世纪构建流程');\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n addProcessLog('世界构建完成', { provider: highQuality.provider, model: highQuality.model });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n addProcessLog('多智能体剧情生成完成', { provider: highQuality.provider, model: highQuality.model });\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n addProcessLog('创世纪资产生成完成', {\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n addProcessLog('创世纪构建完成');\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ processLogs, llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n addProcessLog('进入创世纪校验流程');\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n addProcessLog('创世纪校验失败');\n return res.status(statusCode).json(checkPayload);\n }\n\n addProcessLog('创世纪校验通过');\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n addProcessLog('进入创世纪生成流程');\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n addProcessLog('创世纪生成成功');\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addProcessLog('创世纪生成失败', { error: message });\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "8383533dd0088d2e0597daa4ad58eefa596f3351": {"t": 1785766014.8735852, "n": 38821, "h": "6fbf0dcaff1d561c", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n textHigh: { provider: 'doubao', model: 'doubao-seed-1-8-251228' },\n image: { provider: 'doubao', model: 'doubao-seedream-4-5-251128' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "012df8f5938120bb99df0ef42a13d16c349debed": {"t": 1785766016.485828, "n": 38781, "h": "4c9a9d87eff2fe98", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'openai', model: 'gpt-4.1-nano' },\n textHigh: { provider: 'openai', model: 'gpt-5.2' },\n image: { provider: 'openai', model: 'gpt-image-1' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const messages =\n provider === 'qwen'\n ? [\n { role: 'assistant', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ]\n : [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages,\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "7ac4e45da8774c5b7cd023e0ed262cb5388e1ded": {"t": 1785766017.9432268, "n": 38569, "h": "d819433e23048c66", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'openai', model: 'gpt-4.1-nano' },\n textHigh: { provider: 'openai', model: 'gpt-5.2' },\n image: { provider: 'openai', model: 'gpt-image-1' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ],\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst getImageTestSize = (provider) =>\n provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n\nconst runSingleModelTest = async ({ provider, apiKey, model, type }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (type === 'image') {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n return;\n }\n await testTextModel({ provider, apiKey, model });\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = getImageTestSize(provider);\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/settings/test-item', async (req, res) => {\n const { provider, model, type, providers = {} } = req.body || {};\n if (!provider || !model || !type) {\n return res.status(400).json({ ok: false, error: 'Missing test parameters.' });\n }\n const apiKey = getProviderApiKey(provider, { providers });\n if (!apiKey) {\n return res.json({\n ok: true,\n result: { provider, model, type, ok: false, message: 'Missing API key' }\n });\n }\n try {\n await runSingleModelTest({ provider, apiKey, model, type });\n return res.json({ ok: true, result: { provider, model, type, ok: true } });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.json({ ok: true, result: { provider, model, type, ok: false, message } });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "12a6c653e87545b8d317e2fdbe18bfb3a2a9acae": {"t": 1785766019.5527039, "n": 37348, "h": "3c3b4c5d40b267b9", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1-8-251228',\n 'doubao-seed-1-6-flash-250828'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5-mini']\n },\n textHigh: {\n doubao: ['doubao-seed-1-8-251228', 'doubao-seed-1-6-lite-251015'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4-5-251128',\n 'doubao-seedream-4-0-250828'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'openai', model: 'gpt-4.1-nano' },\n textHigh: { provider: 'openai', model: 'gpt-5.2' },\n image: { provider: 'openai', model: 'gpt-image-1' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ],\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = provider === 'qwen' ? '1024*1024' : provider === 'doubao' ? '2K' : '1024x1024';\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "58fd65ee339faa0f92193292949576ade22d08d8": {"t": 1785766022.4815423, "n": 37481, "h": "341f02acda74b7d3", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst PROVIDER_ENV_KEYS = {\n openai: 'OPENAI_API_KEY',\n doubao: 'ARK_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n qwen: 'DASHSCOPE_API_KEY'\n};\n\nconst PROVIDER_BASE_URLS = {\n openai: 'https://api.openai.com/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n deepseek: 'https://api.deepseek.com/v1',\n qwenText: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',\n qwenImage: 'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'\n};\n\nconst MODEL_CATALOG = {\n textLow: {\n doubao: [\n 'doubao-seed-1.8',\n 'doubao-seed-1.6-lite',\n 'doubao-seed-1.6-flash',\n 'doubao-1.5-pro-32k',\n 'doubao-1.5-lite-32k'\n ],\n qwen: ['qwen-mt-lite', 'qwen-flash', 'qwen-mt-plus'],\n deepseek: ['deepseek-chat'],\n openai: ['gpt-4.1-nano', 'gpt-5-nano', 'gpt-5.2-nano']\n },\n textHigh: {\n doubao: ['doubao-seed-1.8', 'doubao-1.5-pro-32k'],\n qwen: ['qwen3-max', 'qwen-plus'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n openai: ['gpt-4.1-mini', 'gpt-5-mini', 'gpt-5.2-mini', 'gpt-5.2']\n },\n image: {\n doubao: [\n 'doubao-seedream-4.5',\n 'doubao-seedream-4.0',\n 'doubao-seedream-3.0-t2i',\n 'doubao-seededit-3.0-i2i'\n ],\n qwen: ['qwen-image-plus', 'qwen-image'],\n openai: ['gpt-image-1-mini', 'gpt-image-1', 'gpt-image-1.5', 'gpt-5', 'gpt-5-mini']\n }\n};\n\nconst DEFAULT_MODEL_SELECTIONS = {\n textLow: { provider: 'openai', model: 'gpt-4.1-nano' },\n textHigh: { provider: 'openai', model: 'gpt-5.2' },\n image: { provider: 'openai', model: 'gpt-image-1' }\n};\n\nconst getModelSettings = (payload) => {\n const raw = payload?.modelSettings || payload?.settings?.modelSettings || {};\n const selections = raw.selections || {};\n const normalizedSelections = {};\n\n Object.keys(DEFAULT_MODEL_SELECTIONS).forEach((key) => {\n const defaultSelection = DEFAULT_MODEL_SELECTIONS[key];\n const providerOptions = MODEL_CATALOG[key];\n const requestedProvider = selections[key]?.provider || defaultSelection.provider;\n const provider = providerOptions[requestedProvider] ? requestedProvider : defaultSelection.provider;\n const models = providerOptions[provider] || [];\n const requestedModel = selections[key]?.model || defaultSelection.model;\n const model = models.includes(requestedModel) ? requestedModel : models[0];\n normalizedSelections[key] = { provider, model };\n });\n\n return {\n providers: raw.providers || {},\n selections: normalizedSelections\n };\n};\n\nconst getProviderApiKey = (provider, modelSettings) => {\n const fromSettings = modelSettings?.providers?.[provider]?.apiKey;\n if (fromSettings) return fromSettings;\n const envKey = PROVIDER_ENV_KEYS[provider];\n return envKey ? process.env[envKey] : undefined;\n};\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst extractChatCompletionText = (data) => data?.choices?.[0]?.message?.content || '';\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error, model, provider }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model,\n input: { tag: 'image_generation', prompt, size, provider },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag, provider }) => {\n const startedAt = new Date().toISOString();\n if (!apiKey) throw new Error('Missing API key');\n\n if (provider === 'openai' || provider === 'doubao') {\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: JSON.stringify(userPayload) }\n ],\n stream: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractChatCompletionText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload, provider },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst callOpenAiImage = async ({ apiKey, prompt, size, model }) => {\n if (model.startsWith('gpt-5')) {\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: prompt,\n tools: [{ type: 'image_generation' }]\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageCall = data?.output?.find((item) => item.type === 'image_generation_call');\n if (imageCall?.result) {\n return `data:image/png;base64,${imageCall.result}`;\n }\n throw new Error('OpenAI response image data missing.');\n }\n\n const response = await fetch(`${PROVIDER_BASE_URLS.openai}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst callDoubaoImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(`${PROVIDER_BASE_URLS.doubao}/images/generations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n prompt,\n size,\n response_format: 'url',\n watermark: false\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Doubao image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n if (imageData?.url) {\n return imageData.url;\n }\n if (imageData?.b64_json) {\n return `data:image/png;base64,${imageData.b64_json}`;\n }\n throw new Error('Doubao image response missing image data.');\n};\n\nconst callQwenImage = async ({ apiKey, prompt, size, model }) => {\n const response = await fetch(PROVIDER_BASE_URLS.qwenImage, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n input: {\n messages: [\n {\n role: 'user',\n content: [{ text: prompt }]\n }\n ]\n },\n parameters: {\n size,\n watermark: false,\n prompt_extend: true\n }\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`QWEN image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const content = data?.output?.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const imageItem = content.find((item) => item.image || item.image_url);\n if (imageItem?.image) return imageItem.image;\n if (imageItem?.image_url) return imageItem.image_url;\n }\n const imageUrl = data?.output?.image || data?.output?.image_url;\n if (imageUrl) return imageUrl;\n throw new Error('QWEN image response missing image data.');\n};\n\nconst callImageModel = async ({ apiKey, prompt, size, model, provider }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai') {\n return callOpenAiImage({ apiKey, prompt, size, model });\n }\n if (provider === 'doubao') {\n return callDoubaoImage({ apiKey, prompt, size, model });\n }\n if (provider === 'qwen') {\n return callQwenImage({ apiKey, prompt, size, model });\n }\n throw new Error(`Unsupported image provider: ${provider}`);\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async ({ worldDescRaw, charDescRaw, apiKey, provider, model }) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n try {\n const parsed = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`,\n userPayload: {\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n },\n logTag: 'prompt_check'\n });\n\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { ...inputPayload, provider },\n output: `ERROR: ${message}`,\n error: true,\n provider\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const lowQuality = resolvedSettings.selections.textLow;\n const apiKey = getProviderApiKey(lowQuality.provider, resolvedSettings);\n\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks({\n worldDescRaw,\n charDescRaw,\n apiKey,\n provider: lowQuality.provider,\n model: lowQuality.model\n });\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings, provider, model }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder, provider, model }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model,\n provider,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async ({ worldBuilder, apiKey, provider, model }) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = provider === 'qwen' ? '768*768' : '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize, model, provider }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize, model, provider })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({\n prompt: avatarPrompt,\n size: imageSize,\n error: avatarResult.reason,\n model,\n provider\n });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({\n prompt: mapPrompt,\n size: imageSize,\n error: mapResult.reason,\n model,\n provider\n });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ input, modelSettings }) => {\n const resolvedSettings = getModelSettings({ modelSettings });\n const highQuality = resolvedSettings.selections.textHigh;\n const imageSelection = resolvedSettings.selections.image;\n const generationApiKey = getProviderApiKey(highQuality.provider, resolvedSettings);\n const imageApiKey = getProviderApiKey(imageSelection.provider, resolvedSettings);\n\n if (!generationApiKey) {\n throw new Error('缺少高质量文本生成 API Key。');\n }\n if (!imageApiKey) {\n throw new Error('缺少图像生成 API Key。');\n }\n\n const worldBuilder = await generateWorldBuilder({\n apiKey: generationApiKey,\n input,\n settings: input.settings,\n provider: highQuality.provider,\n model: highQuality.model\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateQuestMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n }),\n generateSkillMaster({\n apiKey: generationApiKey,\n input,\n worldBuilder,\n provider: highQuality.provider,\n model: highQuality.model\n })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets({\n worldBuilder,\n apiKey: imageApiKey,\n provider: imageSelection.provider,\n model: imageSelection.model\n });\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\nconst testTextModel = async ({ provider, apiKey, model }) => {\n if (!apiKey) throw new Error('Missing API key');\n if (provider === 'openai' || provider === 'doubao') {\n const baseUrl = provider === 'openai' ? PROVIDER_BASE_URLS.openai : PROVIDER_BASE_URLS.doubao;\n const response = await fetch(`${baseUrl}/responses`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({ model, input: 'ping' })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n if (provider === 'deepseek' || provider === 'qwen') {\n const baseUrl = provider === 'deepseek' ? PROVIDER_BASE_URLS.deepseek : PROVIDER_BASE_URLS.qwenText;\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model,\n messages: [{ role: 'user', content: 'ping' }],\n stream: false\n })\n });\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`${provider} text test failed: ${response.status} ${errorText}`);\n }\n return;\n }\n\n throw new Error(`Unsupported provider: ${provider}`);\n};\n\nconst runModelTests = async ({ providers }) => {\n const results = [];\n const providerIds = Object.keys(providers || {});\n\n for (const provider of providerIds) {\n const apiKey = providers?.[provider]?.apiKey || process.env[PROVIDER_ENV_KEYS[provider]];\n if (!apiKey) continue;\n const textSets = [\n { type: 'textLow', models: MODEL_CATALOG.textLow[provider] || [] },\n { type: 'textHigh', models: MODEL_CATALOG.textHigh[provider] || [] }\n ];\n for (const set of textSets) {\n for (const model of set.models) {\n try {\n await testTextModel({ provider, apiKey, model });\n results.push({ provider, model, type: set.type, ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: set.type, ok: false, message });\n }\n }\n }\n\n const imageModels = MODEL_CATALOG.image[provider] || [];\n for (const model of imageModels) {\n try {\n const size = provider === 'qwen' ? '512*512' : provider === 'doubao' ? '2K' : '512x512';\n await callImageModel({ provider, apiKey, model, prompt: '测试图片', size });\n results.push({ provider, model, type: 'image', ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n results.push({ provider, model, type: 'image', ok: false, message });\n }\n }\n }\n\n return results;\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/settings/test', async (req, res) => {\n const providers = req.body?.providers || {};\n try {\n const results = await runModelTests({ providers });\n res.json({ ok: true, results });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n res.status(500).json({ ok: false, error: message });\n }\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const modelSettings = payload.modelSettings || {};\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settingsPayload = req.body || {};\n const settings = settingsPayload.settings || settingsPayload;\n const modelSettings = settingsPayload.modelSettings || settings.modelSettings || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, modelSettings });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ input, modelSettings });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "2dce47e74b21b6799bbb8e3ecc9927d7496ecb67": {"t": 1785766023.9506629, "n": 26060, "h": "67c75763a1d6daef", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst OPENAI_IMAGE_URL = 'https://api.openai.com/v1/images/generations';\nconst CHECK_MODEL = 'gpt-5-mini';\nconst GENERATION_MODEL = 'gpt-5.2';\nconst IMAGE_MODEL = 'gpt-image-1';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model: IMAGE_MODEL,\n input: { tag: 'image_generation', prompt, size },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag }) => {\n const startedAt = new Date().toISOString();\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n};\n\nconst callImageModel = async ({ apiKey, prompt, size }) => {\n const response = await fetch(OPENAI_IMAGE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: IMAGE_MODEL,\n prompt,\n size\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const imageData = data?.data?.[0];\n const b64 = imageData?.b64_json;\n if (b64) {\n return `data:image/png;base64,${b64}`;\n }\n if (imageData?.url) {\n return imageData.url;\n }\n throw new Error('OpenAI image response missing image data.');\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, apiKey }) => {\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async (worldBuilder, apiKey) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({ prompt: avatarPrompt, size: imageSize, error: avatarResult.reason });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({ prompt: mapPrompt, size: imageSize, error: mapResult.reason });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ apiKey, input }) => {\n const worldBuilder = await generateWorldBuilder({\n apiKey,\n input,\n settings: input.settings\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({ apiKey, input, worldBuilder }),\n generateQuestMaster({ apiKey, input, worldBuilder }),\n generateSkillMaster({ apiKey, input, worldBuilder })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets(worldBuilder, apiKey);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const apiKey = getApiKey(req);\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n error: '缺少 API Key,无法执行创世纪构建。'\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "4e53d4f3012601c45e2b79b2e634a04f9188059f": {"t": 1785766025.3975275, "n": 26001, "h": "0329b37cdef88c7f", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst OPENAI_IMAGE_URL = 'https://api.openai.com/v1/images/generations';\nconst CHECK_MODEL = 'gpt-5-mini';\nconst GENERATION_MODEL = 'gpt-5.2';\nconst IMAGE_MODEL = 'gpt-image-1';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst addImageErrorLog = ({ prompt, size, error }) => {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: new Date().toISOString(),\n model: IMAGE_MODEL,\n input: { tag: 'image_generation', prompt, size },\n output: `ERROR: ${message}`,\n error: true\n });\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag }) => {\n const startedAt = new Date().toISOString();\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n};\n\nconst callImageModel = async ({ apiKey, prompt, size }) => {\n const response = await fetch(OPENAI_IMAGE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: IMAGE_MODEL,\n prompt,\n size,\n response_format: 'b64_json'\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const b64 = data?.data?.[0]?.b64_json;\n if (!b64) throw new Error('OpenAI image response missing image data.');\n return `data:image/png;base64,${b64}`;\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, apiKey }) => {\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async (worldBuilder, apiKey) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n const imageSize = '1024x1024';\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: imageSize }),\n callImageModel({ apiKey, prompt: mapPrompt, size: imageSize })\n ]);\n\n if (avatarResult.status === 'rejected') {\n addImageErrorLog({ prompt: avatarPrompt, size: imageSize, error: avatarResult.reason });\n }\n if (mapResult.status === 'rejected') {\n addImageErrorLog({ prompt: mapPrompt, size: imageSize, error: mapResult.reason });\n }\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ apiKey, input }) => {\n const worldBuilder = await generateWorldBuilder({\n apiKey,\n input,\n settings: input.settings\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({ apiKey, input, worldBuilder }),\n generateQuestMaster({ apiKey, input, worldBuilder }),\n generateSkillMaster({ apiKey, input, worldBuilder })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets(worldBuilder, apiKey);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const apiKey = getApiKey(req);\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n error: '缺少 API Key,无法执行创世纪构建。'\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "e7e1ac54ec209a5c9225f9f93c88710e5331a979": {"t": 1785766027.0351229, "n": 25357, "h": "6dce34099c257865", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst OPENAI_IMAGE_URL = 'https://api.openai.com/v1/images/generations';\nconst CHECK_MODEL = 'gpt-5-mini';\nconst GENERATION_MODEL = 'gpt-5.2';\nconst IMAGE_MODEL = 'gpt-image-1';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag }) => {\n const startedAt = new Date().toISOString();\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n};\n\nconst callImageModel = async ({ apiKey, prompt, size }) => {\n const response = await fetch(OPENAI_IMAGE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: IMAGE_MODEL,\n prompt,\n size,\n response_format: 'b64_json'\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const b64 = data?.data?.[0]?.b64_json;\n if (!b64) throw new Error('OpenAI image response missing image data.');\n return `data:image/png;base64,${b64}`;\n};\n\nconst buildPlaceholderImage = ({ label, width, height }) => {\n const safeLabel = label?.replace(//g, '>') || 'IMAGE';\n const svg = `\n \n `;\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`;\n};\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, apiKey }) => {\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async (worldBuilder, apiKey) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n\n const [avatarResult, mapResult] = await Promise.allSettled([\n callImageModel({ apiKey, prompt: avatarPrompt, size: '1024x1024' }),\n callImageModel({ apiKey, prompt: mapPrompt, size: '1024x1024' })\n ]);\n\n const avatarImage =\n avatarResult.status === 'fulfilled'\n ? avatarResult.value\n : buildPlaceholderImage({ label: 'Avatar', width: 1024, height: 1024 });\n const mapImage =\n mapResult.status === 'fulfilled'\n ? mapResult.value\n : buildPlaceholderImage({ label: 'Map', width: 1024, height: 1024 });\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ apiKey, input }) => {\n const worldBuilder = await generateWorldBuilder({\n apiKey,\n input,\n settings: input.settings\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({ apiKey, input, worldBuilder }),\n generateQuestMaster({ apiKey, input, worldBuilder }),\n generateSkillMaster({ apiKey, input, worldBuilder })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets(worldBuilder, apiKey);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const apiKey = getApiKey(req);\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n error: '缺少 API Key,无法执行创世纪构建。'\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "559ac71ced045ee6625687157e8ccdf9d59c7800": {"t": 1785766028.387934, "n": 23914, "h": "5f2631dce27d9d05", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst OPENAI_IMAGE_URL = 'https://api.openai.com/v1/images';\nconst CHECK_MODEL = 'gpt-5-mini';\nconst GENERATION_MODEL = 'gpt-5.2';\nconst IMAGE_MODEL = 'gpt-image-1.5';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag }) => {\n const startedAt = new Date().toISOString();\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n};\n\nconst callImageModel = async ({ apiKey, prompt, size }) => {\n const response = await fetch(OPENAI_IMAGE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: IMAGE_MODEL,\n prompt,\n size,\n response_format: 'b64_json'\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const b64 = data?.data?.[0]?.b64_json;\n if (!b64) throw new Error('OpenAI image response missing image data.');\n return `data:image/png;base64,${b64}`;\n};\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst runGenesisChecks = async ({ worldDescRaw, charDescRaw, apiKey }) => {\n if (!apiKey) {\n return {\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return {\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n };\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n return {\n ok: !shouldBlock,\n checks,\n sanitizedInput\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async (worldBuilder, apiKey) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n\n const [avatarImage, mapImage] = await Promise.all([\n callImageModel({ apiKey, prompt: avatarPrompt, size: '512x512' }),\n callImageModel({ apiKey, prompt: mapPrompt, size: '1024x768' })\n ]);\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\nconst buildGenesisPayload = async ({ apiKey, input }) => {\n const worldBuilder = await generateWorldBuilder({\n apiKey,\n input,\n settings: input.settings\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n let questMasterRaw;\n const [narrator, questMaster, skillMaster] = await Promise.all([\n generateNarrator({ apiKey, input, worldBuilder }),\n generateQuestMaster({ apiKey, input, worldBuilder }),\n generateSkillMaster({ apiKey, input, worldBuilder })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMaster, fallbackTasks);\n const assets = await buildAssets(worldBuilder, apiKey);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMasterRaw);\n\n return {\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster: questMasterRaw,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMasterRaw),\n firstQuest\n };\n};\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis/checks', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n res.json(checkPayload);\n});\n\napp.post('/api/genesis/build', async (req, res) => {\n const payload = req.body || {};\n const apiKey = getApiKey(req);\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n error: '缺少 API Key,无法执行创世纪构建。'\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const worldDescRaw = payload.worldDesc?.trim() || '';\n const charDescRaw = payload.charDesc?.trim() || '';\n const sanitized = payload.sanitizedInput || {};\n const input = {\n worldDesc: sanitized.worldDesc || worldDescRaw,\n charDesc: sanitized.charDesc || charDescRaw,\n settings: {\n magic: payload.settings?.magic || 'mid',\n physics: payload.settings?.physics || 'mid',\n tech: payload.settings?.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n return res.json({\n ok: true,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n const checkPayload = await runGenesisChecks({ worldDescRaw, charDescRaw, apiKey });\n if (!checkPayload.ok) {\n const statusCode = checkPayload.error ? 502 : 400;\n return res.status(statusCode).json(checkPayload);\n }\n\n const input = {\n worldDesc: checkPayload.sanitizedInput.worldDesc || worldDescRaw,\n charDesc: checkPayload.sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n try {\n const buildPayload = await buildGenesisPayload({ apiKey, input });\n res.json({\n ok: true,\n checks: checkPayload.checks,\n ...buildPayload\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "249ff4f26d1a085b94475d787928bc465bab82df": {"t": 1785766029.8359103, "n": 22025, "h": "8d0b35156c8d4338", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst OPENAI_IMAGE_URL = 'https://api.openai.com/v1/images';\nconst CHECK_MODEL = 'gpt-5-mini';\nconst GENERATION_MODEL = 'gpt-5.2';\nconst IMAGE_MODEL = 'gpt-image-1.5';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst parseJsonOutput = (outputText) => {\n const trimmed = outputText?.trim();\n if (!trimmed) throw new Error('OpenAI response missing output text.');\n return JSON.parse(trimmed);\n};\n\nconst callJsonModel = async ({ apiKey, model, systemPrompt, userPayload, logTag }) => {\n const startedAt = new Date().toISOString();\n const payload = {\n model,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [{ type: 'input_text', text: systemPrompt }]\n },\n {\n role: 'user',\n content: [{ type: 'input_text', text: JSON.stringify(userPayload) }]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model,\n input: { tag: logTag, payload: userPayload },\n output: outputText\n });\n\n return parseJsonOutput(outputText);\n};\n\nconst callImageModel = async ({ apiKey, prompt, size }) => {\n const response = await fetch(OPENAI_IMAGE_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: IMAGE_MODEL,\n prompt,\n size,\n response_format: 'b64_json'\n })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI image request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const b64 = data?.data?.[0]?.b64_json;\n if (!b64) throw new Error('OpenAI image response missing image data.');\n return `data:image/png;base64,${b64}`;\n};\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst generateWorldBuilder = async ({ apiKey, input, settings }) => {\n const systemPrompt = `你是世界构建器(World Builder Agent)。只输出 JSON,不要输出任何多余文字或代码块。\n\n根据输入内容生成世界构建结果,JSON 必须包含:\n- world_setting: 详细世界观补全(中文,2-4 段)\n- factions: 4 个主要势力的简述数组,每个元素包含 name 与 summary\n- map: 8~12 个地点数组,每个元素包含 {id, name, x, y, type},x/y 为 0-100 的整数,type 只能是 start|neutral|danger|secret\n- faction_1~faction_4: 四个势力的详细设定对象(background, territory, conflicts, abilities, organization)\n- stats_allocation: {str, dex, int, cha} 总和必须为 15\n- inventory: 初始道具数组(3-6 项)\n- Power_level: 一段描述当前世界的魔法/科技/武力水平,并举 1-2 个例子\n- pic_style: 3-6 个词,表示绘画资产风格\n- character_appearance: 一段外貌描述(用于头像生成)\n\n注意:输出必须紧扣用户世界观与角色描述,避免模板化与默认措辞。`;\n\n const userPayload = {\n worldDesc: input.worldDesc,\n charDesc: input.charDesc,\n settings\n };\n\n const raw = await callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload,\n logTag: 'world_builder'\n });\n\n const fallbackStats = allocateStats(settings);\n return {\n ...raw,\n stats_allocation: normalizeStats(raw?.stats_allocation, fallbackStats),\n map: normalizeMapNodes(raw?.map),\n factions: Array.isArray(raw?.factions) ? raw.factions.slice(0, 4) : [],\n pic_style: Array.isArray(raw?.pic_style) ? raw.pic_style.slice(0, 6) : ['氛围感']\n };\n};\n\nconst generateNarrator = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Narrator。只输出 JSON,不要输出任何多余文字。\n输出格式:\n{\"origin_story\":\"...\"}\n请基于世界观、势力信息与人物描述,写出合理出身故事,中文 120-220 字。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'narrator'\n });\n};\n\nconst generateQuestMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Quest Master。只输出 JSON。\n生成一条线性主线任务(5-8 个节点),并给出最终势力目标。\n输出结构:\n{\n \"task_num\": 5-8,\n \"task_1\": \"...\",\n \"task_2\": \"...\",\n \"...\": \"...\",\n \"final_goal\": \"...\"\n}\n任务描述为一小段中文。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'quest_master'\n });\n};\n\nconst generateSkillMaster = async ({ apiKey, input, worldBuilder }) => {\n const systemPrompt = `你是身份与剧情编排器中的 Skill Master。只输出 JSON。\n输出结构:\n{\n \"skill\": [\"技能1\",\"技能2\",\"技能3\"],\n \"item\": [\"道具1\",\"道具2\",\"道具3\"]\n}\n技能与道具必须与世界观一致。`;\n return callJsonModel({\n apiKey,\n model: GENERATION_MODEL,\n systemPrompt,\n userPayload: { input, worldBuilder },\n logTag: 'skill_master'\n });\n};\n\nconst normalizeStats = (stats, fallback) => {\n const base = { ...fallback };\n if (!stats || typeof stats !== 'object') return base;\n ['str', 'dex', 'int', 'cha'].forEach((key) => {\n const value = Number(stats[key]);\n if (Number.isFinite(value)) base[key] = Math.max(0, Math.round(value));\n });\n const total = base.str + base.dex + base.int + base.cha;\n if (total === 15) return base;\n const diff = 15 - total;\n const order = ['str', 'dex', 'int', 'cha'];\n let idx = 0;\n let remaining = Math.abs(diff);\n while (remaining > 0) {\n const key = order[idx % order.length];\n base[key] = Math.max(0, base[key] + Math.sign(diff));\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst normalizeMapNodes = (nodes) => {\n if (!Array.isArray(nodes)) return buildMapNodes(rng(8, 12));\n return nodes.slice(0, 12).map((node, index) => ({\n id: Number(node?.id) || index + 1,\n name: node?.name || `区域 ${index + 1}`,\n x: Math.min(100, Math.max(0, Number(node?.x) || rng(10, 90))),\n y: Math.min(100, Math.max(0, Number(node?.y) || rng(10, 90))),\n type: node?.type || (index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral')\n }));\n};\n\nconst normalizeQuestMaster = (questMaster, fallbackTasks) => {\n const tasks = [];\n if (Array.isArray(questMaster?.tasks)) {\n questMaster.tasks.forEach((task, index) => {\n tasks.push({\n id: Number(task?.id) || index + 1,\n summary: task?.summary || task?.text || `任务 ${index + 1}`\n });\n });\n } else if (Number.isFinite(questMaster?.task_num)) {\n const count = Math.min(8, Math.max(5, questMaster.task_num));\n for (let i = 1; i <= count; i += 1) {\n tasks.push({ id: i, summary: questMaster[`task_${i}`] || `任务 ${i}` });\n }\n }\n\n const safeTasks = tasks.length ? tasks : fallbackTasks;\n return {\n task_num: safeTasks.length,\n final_goal: questMaster?.final_goal || '最终目标尚待明确。',\n tasks: safeTasks\n };\n};\n\nconst buildAssets = async (worldBuilder, apiKey) => {\n const avatarPrompt = `角色头像:${worldBuilder.character_appearance || '神秘旅者,目光坚定'}。风格关键词:${worldBuilder.pic_style.join(',')}。半身近景,单一角色,清晰面部细节。`;\n const mapPlaces = worldBuilder.map\n .slice(0, 10)\n .map((node) => node.name)\n .join('、');\n const mapPrompt = `世界地图:包含${mapPlaces}等地点,适度标注文字说明。风格关键词:${worldBuilder.pic_style.join(',')}。简洁清晰、易读。`;\n\n const [avatarImage, mapImage] = await Promise.all([\n callImageModel({ apiKey, prompt: avatarPrompt, size: '512x512' }),\n callImageModel({ apiKey, prompt: mapPrompt, size: '1024x768' })\n ]);\n\n return {\n avatar: {\n prompt: avatarPrompt,\n image: avatarImage\n },\n map: {\n prompt: mapPrompt,\n image: mapImage\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return res.status(502).json({\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n if (shouldBlock) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedInput.worldDesc || worldDescRaw,\n charDesc: sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n let worldBuilder;\n let narrator;\n let questMasterRaw;\n let skillMaster;\n let assets;\n try {\n worldBuilder = await generateWorldBuilder({\n apiKey,\n input,\n settings: input.settings\n });\n\n const fallbackTasks = worldBuilder.map.slice(0, 5).map((node, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${node.name},调查与${worldBuilder.factions[index % 4]?.name || '未知势力'}有关的异动。`\n }));\n\n [narrator, questMasterRaw, skillMaster] = await Promise.all([\n generateNarrator({ apiKey, input, worldBuilder }),\n generateQuestMaster({ apiKey, input, worldBuilder }),\n generateSkillMaster({ apiKey, input, worldBuilder })\n ]);\n\n questMasterRaw = normalizeQuestMaster(questMasterRaw, fallbackTasks);\n assets = await buildAssets(worldBuilder, apiKey);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return res.status(502).json({\n ok: false,\n error: `创世纪生成失败:${message}`\n });\n }\n\n const questMaster = questMasterRaw;\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "5dc77020e643455a58f6677885ab834be4add90a": {"t": 1785766031.1310792, "n": 16154, "h": "2691cd7af6fc12eb", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst CHECK_MODEL = 'gpt-5-mini';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst buildNarrator = (input, worldBuilder) => ({\n origin_story: `你出生在${worldBuilder.map[0].name}的边缘街区,曾在${worldBuilder.faction_3.name}的船团做过短暂学徒。${input.charDesc || '你始终记得那场改变命运的风暴。'}`\n});\n\nconst buildQuestMaster = (input, worldBuilder) => {\n const taskCount = rng(5, 8);\n const tasks = Array.from({ length: taskCount }, (_, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${worldBuilder.map[index % worldBuilder.map.length].name},调查与${worldBuilder.factions[index % 4].name}有关的异动。`\n }));\n return {\n task_num: taskCount,\n final_goal: `协助${worldBuilder.faction_1.name}赢得关键战役,获得影响世界格局的席位。`,\n tasks\n };\n};\n\nconst buildSkillMaster = (worldBuilder) => ({\n skill: ['雾影潜行', '短距跃迁', '星环解读'],\n item: worldBuilder.inventory\n});\n\nconst buildAssets = (worldBuilder) => {\n const avatarSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n return {\n avatar: {\n prompt: `角色头像,风格:${worldBuilder.pic_style.join(',')}`,\n image: avatarSvg\n },\n map: {\n prompt: `地图概览,风格:${worldBuilder.pic_style.join(',')}`,\n image: mapSvg\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return res.status(502).json({\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n if (shouldBlock) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedInput.worldDesc || worldDescRaw,\n charDesc: sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n const worldBuilder = buildWorldBuilder(input.settings, input);\n const narrator = buildNarrator(input, worldBuilder);\n const questMaster = buildQuestMaster(input, worldBuilder);\n const skillMaster = buildSkillMaster(worldBuilder);\n const assets = buildAssets(worldBuilder);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "f95dfd0bd4d7cd13c60f6d40c52ff2e51392280d": {"t": 1785766033.0047965, "n": 16176, "h": "81d8bc86b84b3dec", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst CHECK_MODEL = 'gpt-5-mini';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n temperature: 0.2,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'input_text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'input_text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst buildNarrator = (input, worldBuilder) => ({\n origin_story: `你出生在${worldBuilder.map[0].name}的边缘街区,曾在${worldBuilder.faction_3.name}的船团做过短暂学徒。${input.charDesc || '你始终记得那场改变命运的风暴。'}`\n});\n\nconst buildQuestMaster = (input, worldBuilder) => {\n const taskCount = rng(5, 8);\n const tasks = Array.from({ length: taskCount }, (_, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${worldBuilder.map[index % worldBuilder.map.length].name},调查与${worldBuilder.factions[index % 4].name}有关的异动。`\n }));\n return {\n task_num: taskCount,\n final_goal: `协助${worldBuilder.faction_1.name}赢得关键战役,获得影响世界格局的席位。`,\n tasks\n };\n};\n\nconst buildSkillMaster = (worldBuilder) => ({\n skill: ['雾影潜行', '短距跃迁', '星环解读'],\n item: worldBuilder.inventory\n});\n\nconst buildAssets = (worldBuilder) => {\n const avatarSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n return {\n avatar: {\n prompt: `角色头像,风格:${worldBuilder.pic_style.join(',')}`,\n image: avatarSvg\n },\n map: {\n prompt: `地图概览,风格:${worldBuilder.pic_style.join(',')}`,\n image: mapSvg\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return res.status(502).json({\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n if (shouldBlock) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedInput.worldDesc || worldDescRaw,\n charDesc: sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n const worldBuilder = buildWorldBuilder(input.settings, input);\n const narrator = buildNarrator(input, worldBuilder);\n const questMaster = buildQuestMaster(input, worldBuilder);\n const skillMaster = buildSkillMaster(worldBuilder);\n const assets = buildAssets(worldBuilder);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "404ff175870b73e11403b78856490e925d9f1439": {"t": 1785766034.8619401, "n": 16164, "h": "451484bb96cc6244", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst CHECK_MODEL = 'gpt-5-mini';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n temperature: 0.2,\n text: { format: { type: 'json_object' } },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst buildNarrator = (input, worldBuilder) => ({\n origin_story: `你出生在${worldBuilder.map[0].name}的边缘街区,曾在${worldBuilder.faction_3.name}的船团做过短暂学徒。${input.charDesc || '你始终记得那场改变命运的风暴。'}`\n});\n\nconst buildQuestMaster = (input, worldBuilder) => {\n const taskCount = rng(5, 8);\n const tasks = Array.from({ length: taskCount }, (_, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${worldBuilder.map[index % worldBuilder.map.length].name},调查与${worldBuilder.factions[index % 4].name}有关的异动。`\n }));\n return {\n task_num: taskCount,\n final_goal: `协助${worldBuilder.faction_1.name}赢得关键战役,获得影响世界格局的席位。`,\n tasks\n };\n};\n\nconst buildSkillMaster = (worldBuilder) => ({\n skill: ['雾影潜行', '短距跃迁', '星环解读'],\n item: worldBuilder.inventory\n});\n\nconst buildAssets = (worldBuilder) => {\n const avatarSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n return {\n avatar: {\n prompt: `角色头像,风格:${worldBuilder.pic_style.join(',')}`,\n image: avatarSvg\n },\n map: {\n prompt: `地图概览,风格:${worldBuilder.pic_style.join(',')}`,\n image: mapSvg\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return res.status(502).json({\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n if (shouldBlock) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedInput.worldDesc || worldDescRaw,\n charDesc: sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n const worldBuilder = buildWorldBuilder(input.settings, input);\n const narrator = buildNarrator(input, worldBuilder);\n const questMaster = buildQuestMaster(input, worldBuilder);\n const skillMaster = buildSkillMaster(worldBuilder);\n const assets = buildAssets(worldBuilder);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "62f28666fb9a661aa76d36f06e400d189dde5900": {"t": 1785766036.9667296, "n": 16163, "h": "5820e672c04befc1", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\nconst MAX_LLM_LOGS = 50;\nconst llmLogs = [];\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst CHECK_MODEL = 'gpt-5-mini';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst addLlmLog = (entry) => {\n llmLogs.push(entry);\n if (llmLogs.length > MAX_LLM_LOGS) {\n llmLogs.shift();\n }\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const startedAt = new Date().toISOString();\n const inputPayload = { worldDesc: worldDescRaw, charDesc: charDescRaw };\n const payload = {\n model: CHECK_MODEL,\n temperature: 0.2,\n response_format: { type: 'json_object' },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n try {\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: outputText\n });\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc:\n parsed?.sanitizedInput?.worldDesc ||\n sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n addLlmLog({\n id: nanoid(8),\n createdAt: startedAt,\n model: CHECK_MODEL,\n input: inputPayload,\n output: `ERROR: ${message}`,\n error: true\n });\n throw error;\n }\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst buildNarrator = (input, worldBuilder) => ({\n origin_story: `你出生在${worldBuilder.map[0].name}的边缘街区,曾在${worldBuilder.faction_3.name}的船团做过短暂学徒。${input.charDesc || '你始终记得那场改变命运的风暴。'}`\n});\n\nconst buildQuestMaster = (input, worldBuilder) => {\n const taskCount = rng(5, 8);\n const tasks = Array.from({ length: taskCount }, (_, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${worldBuilder.map[index % worldBuilder.map.length].name},调查与${worldBuilder.factions[index % 4].name}有关的异动。`\n }));\n return {\n task_num: taskCount,\n final_goal: `协助${worldBuilder.faction_1.name}赢得关键战役,获得影响世界格局的席位。`,\n tasks\n };\n};\n\nconst buildSkillMaster = (worldBuilder) => ({\n skill: ['雾影潜行', '短距跃迁', '星环解读'],\n item: worldBuilder.inventory\n});\n\nconst buildAssets = (worldBuilder) => {\n const avatarSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n return {\n avatar: {\n prompt: `角色头像,风格:${worldBuilder.pic_style.join(',')}`,\n image: avatarSvg\n },\n map: {\n prompt: `地图概览,风格:${worldBuilder.pic_style.join(',')}`,\n image: mapSvg\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.get('/api/logs', (req, res) => {\n res.json({ logs: llmLogs });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return res.status(502).json({\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n if (shouldBlock) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedInput.worldDesc || worldDescRaw,\n charDesc: sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n const worldBuilder = buildWorldBuilder(input.settings, input);\n const narrator = buildNarrator(input, worldBuilder);\n const questMaster = buildQuestMaster(input, worldBuilder);\n const skillMaster = buildSkillMaster(worldBuilder);\n const assets = buildAssets(worldBuilder);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "5daf46003606cc455e0d50de91797c5485f094d5": {"t": 1785766038.5977225, "n": 15254, "h": "e4061b783a15517d", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst OPENAI_API_URL = 'https://api.openai.com/v1/responses';\nconst CHECK_MODEL = 'gpt-5-mini';\n\nconst getApiKey = (req) => req.headers['x-api-key'] || process.env.OPENAI_API_KEY;\n\nconst extractResponseText = (data) => {\n if (typeof data?.output_text === 'string') return data.output_text;\n const message = data?.output?.find((item) => item.type === 'message') || data?.output?.[0];\n if (!message?.content) return '';\n const textItem = message.content.find((item) => item.type === 'output_text');\n return textItem?.text || '';\n};\n\nconst normalizeStatus = (value, allowed, fallback = 'fail') =>\n allowed.includes(value) ? value : fallback;\n\nconst runPromptChecks = async (worldDescRaw, charDescRaw, apiKey) => {\n const payload = {\n model: CHECK_MODEL,\n temperature: 0.2,\n response_format: { type: 'json_object' },\n input: [\n {\n role: 'system',\n content: [\n {\n type: 'text',\n text: `你是内容审核与创世纪输入审查员。请仅输出 JSON 对象,不要输出任何多余文字。\n\n需要完成四项检查并给出简短中文说明:\n1) safety:若包含攻击/入侵/绕过/漏洞/脚本注入等黑客内容,或政治/色情/赌博/恐怖/毒品/枪支/邪教/极端等敏感主题,则 fail;否则 pass。\n2) utility:若世界观描述与角色描述足够明确、可用于生成故事,则 pass;否则 fail。\n3) expansion:若包含数值化属性、强指向性任务/目标/必须完成的指令等,应标记 warn;否则 pass。\n4) builder:若输入足以直接进入世界构建(无需继续追问),则 pass;否则 fail。\n\n同时返回 sanitizedInput:在保留用户意图的前提下,移除攻击/敏感内容与过强指令化表述,必要时简化数值化词汇。\n\n输出 JSON 结构:\n{\n \"checks\": {\n \"safety\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"utility\": { \"status\": \"pass|fail\", \"message\": \"...\" },\n \"expansion\": { \"status\": \"pass|warn\", \"message\": \"...\" },\n \"builder\": { \"status\": \"pass|fail\", \"message\": \"...\" }\n },\n \"sanitizedInput\": { \"worldDesc\": \"...\", \"charDesc\": \"...\" }\n}`\n }\n ]\n },\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n worldDesc: worldDescRaw,\n charDesc: charDescRaw\n })\n }\n ]\n }\n ]\n };\n\n const response = await fetch(OPENAI_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(payload)\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`OpenAI request failed: ${response.status} ${errorText}`);\n }\n\n const data = await response.json();\n const outputText = extractResponseText(data);\n if (!outputText) {\n throw new Error('OpenAI response missing output text.');\n }\n\n const parsed = JSON.parse(outputText);\n const rawChecks = parsed?.checks || {};\n const checks = {\n safety: {\n status: normalizeStatus(rawChecks?.safety?.status, ['pass', 'fail']),\n message: rawChecks?.safety?.message || '安全检查完成。'\n },\n utility: {\n status: normalizeStatus(rawChecks?.utility?.status, ['pass', 'fail']),\n message: rawChecks?.utility?.message || '效用检查完成。'\n },\n expansion: {\n status: normalizeStatus(rawChecks?.expansion?.status, ['pass', 'warn']),\n message: rawChecks?.expansion?.message || '扩展检查完成。'\n },\n builder: {\n status: normalizeStatus(rawChecks?.builder?.status, ['pass', 'fail']),\n message: rawChecks?.builder?.message || '构建检查完成。'\n }\n };\n\n return {\n checks,\n sanitizedInput: {\n worldDesc: parsed?.sanitizedInput?.worldDesc || sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: parsed?.sanitizedInput?.charDesc || sanitizePrompt(charDescRaw)\n }\n };\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst buildNarrator = (input, worldBuilder) => ({\n origin_story: `你出生在${worldBuilder.map[0].name}的边缘街区,曾在${worldBuilder.faction_3.name}的船团做过短暂学徒。${input.charDesc || '你始终记得那场改变命运的风暴。'}`\n});\n\nconst buildQuestMaster = (input, worldBuilder) => {\n const taskCount = rng(5, 8);\n const tasks = Array.from({ length: taskCount }, (_, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${worldBuilder.map[index % worldBuilder.map.length].name},调查与${worldBuilder.factions[index % 4].name}有关的异动。`\n }));\n return {\n task_num: taskCount,\n final_goal: `协助${worldBuilder.faction_1.name}赢得关键战役,获得影响世界格局的席位。`,\n tasks\n };\n};\n\nconst buildSkillMaster = (worldBuilder) => ({\n skill: ['雾影潜行', '短距跃迁', '星环解读'],\n item: worldBuilder.inventory\n});\n\nconst buildAssets = (worldBuilder) => {\n const avatarSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n return {\n avatar: {\n prompt: `角色头像,风格:${worldBuilder.pic_style.join(',')}`,\n image: avatarSvg\n },\n map: {\n prompt: `地图概览,风格:${worldBuilder.pic_style.join(',')}`,\n image: mapSvg\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n const apiKey = getApiKey(req);\n\n await wait(SIMULATION_DELAY);\n\n if (!apiKey) {\n return res.status(401).json({\n ok: false,\n checks: {\n safety: { status: 'fail', message: '缺少 API Key,无法执行安全检查。' },\n utility: { status: 'fail', message: '缺少 API Key,无法执行效用检查。' },\n expansion: { status: 'fail', message: '缺少 API Key,无法执行扩展检查。' },\n builder: { status: 'fail', message: '缺少 API Key,无法执行构建检查。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n let checkPayload;\n try {\n checkPayload = await runPromptChecks(worldDescRaw, charDescRaw, apiKey);\n } catch (error) {\n return res.status(502).json({\n ok: false,\n error: '安全检查服务不可用,请稍后重试。',\n checks: {\n safety: { status: 'fail', message: '安全检查失败。' },\n utility: { status: 'fail', message: '效用检查失败。' },\n expansion: { status: 'fail', message: '扩展检查失败。' },\n builder: { status: 'fail', message: '构建检查失败。' }\n },\n sanitizedInput: {\n worldDesc: sanitizePrompt(worldDescRaw, { removeTaskDirectives: true }),\n charDesc: sanitizePrompt(charDescRaw)\n }\n });\n }\n\n const { checks, sanitizedInput } = checkPayload;\n const shouldBlock =\n checks.safety.status !== 'pass' ||\n checks.utility.status !== 'pass' ||\n checks.builder.status !== 'pass';\n\n if (shouldBlock) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedInput.worldDesc || worldDescRaw,\n charDesc: sanitizedInput.charDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n const worldBuilder = buildWorldBuilder(input.settings, input);\n const narrator = buildNarrator(input, worldBuilder);\n const questMaster = buildQuestMaster(input, worldBuilder);\n const skillMaster = buildSkillMaster(worldBuilder);\n const assets = buildAssets(worldBuilder);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "dfe7ae3531afde80ddd1e402d00b2bae6f7e4301": {"t": 1785766040.5954847, "n": 12520, "h": "1edff5e546afea7b", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 700;\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst HACK_PATTERNS = [/hack/i, /入侵/, /渗透/, /攻击/, /绕过/, /漏洞/, /病毒/, /木马/, /脚本注入/];\nconst SENSITIVE_PATTERNS = [\n /政治/,\n /敏感/,\n /色情/,\n /赌博/,\n /恐怖/,\n /暴恐/,\n /毒品/,\n /枪支/,\n /邪教/,\n /极端/\n];\nconst TASK_DIRECTIVE_PATTERNS = [/任务/, /目标/, /最终目的/, /必须完成/, /强制/];\nconst STAT_PATTERNS = [\n /武力值/,\n /力量/,\n /敏捷/,\n /智力/,\n /魅力/,\n /属性/,\n /数值/,\n /点数/,\n /战力/\n];\n\nconst sanitizePrompt = (text, options = {}) => {\n if (!text) return '';\n let cleaned = text;\n if (options.removeTaskDirectives) {\n cleaned = cleaned\n .split(/(?<=[。!?.!?])/)\n .filter((sentence) => !TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(sentence)))\n .join('');\n }\n\n cleaned = cleaned.replace(/[0-9]+/g, '');\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n return cleaned;\n};\n\nconst detectSafetyIssues = (text) =>\n HACK_PATTERNS.some((pattern) => pattern.test(text)) ||\n SENSITIVE_PATTERNS.some((pattern) => pattern.test(text));\n\nconst checkUtility = (worldDesc, charDesc) => {\n const worldOk =\n worldDesc.length >= 8 &&\n /(世界|王国|城市|星球|大陆|时代|文明|社会|荒原|王朝|未来|历史)/.test(worldDesc);\n const charOk =\n charDesc.length >= 6 &&\n /(角色|主角|身份|背景|出身|职业|旅人|调查员|佣兵|学者|刺客|猎人|工程师|船长)/.test(charDesc);\n return { worldOk, charOk };\n};\n\nconst checkExpansion = (text) => {\n const hasNumbers = /\\d/.test(text);\n const hasStatWords = STAT_PATTERNS.some((pattern) => pattern.test(text));\n const hasStrongDirective = TASK_DIRECTIVE_PATTERNS.some((pattern) => pattern.test(text));\n return hasNumbers || hasStatWords || hasStrongDirective;\n};\n\nconst allocateStats = (settings) => {\n const base = { str: 3, dex: 3, int: 3, cha: 3 };\n let remaining = 3;\n if (settings.physics === 'high') {\n base.str += 1;\n remaining -= 1;\n }\n if (settings.magic === 'high') {\n base.int += 1;\n remaining -= 1;\n }\n if (settings.tech === 'high') {\n base.dex += 1;\n remaining -= 1;\n }\n const order = ['cha', 'str', 'dex', 'int'];\n let idx = 0;\n while (remaining > 0) {\n base[order[idx % order.length]] += 1;\n remaining -= 1;\n idx += 1;\n }\n return base;\n};\n\nconst buildMapNodes = (count) => {\n const nodes = Array.from({ length: count }, (_, index) => ({\n id: index + 1,\n name: `区域 ${index + 1}`,\n x: rng(10, 90),\n y: rng(10, 90),\n type: index === 0 ? 'start' : index % 4 === 0 ? 'danger' : 'neutral'\n }));\n nodes[0].name = '起始点: 旧港区';\n nodes[1].name = '中立区: 交易拱廊';\n nodes[2].name = '禁区: 破碎塔群';\n return nodes;\n};\n\nconst buildWorldBuilder = (settings, input) => {\n const factionNames = ['星环议会', '赤曜军团', '雾海协会', '银钥秘盟'];\n const mapNodes = buildMapNodes(rng(8, 12));\n const stats = allocateStats(settings);\n\n return {\n world_setting: `${input.worldDesc || pick(MOCK_WORLD_CONTEXTS)} 世界的科技与魔法并行,势力之间处于微妙平衡。`,\n factions: factionNames.map((name, index) => ({\n name,\n summary: `势力 ${index + 1} 掌控着不同的资源与情报网络,彼此保持着脆弱的同盟关系。`\n })),\n map: mapNodes,\n faction_1: {\n name: factionNames[0],\n background: '由学者与议员组成的高层议会,掌控高阶魔法与政治秩序。',\n territory: '主城天穹港及其周边浮空环。',\n conflicts: '与赤曜军团在能源源泉上长期对峙。',\n abilities: '擅长信息控制与法术封印。',\n organization: '议会-内廷-执行官三级结构。'\n },\n faction_2: {\n name: factionNames[1],\n background: '由前线士兵与重装骑士组成的军事集团。',\n territory: '边境钢城与战线前哨。',\n conflicts: '与银钥秘盟争夺武器原型。',\n abilities: '重装作战、武力压制。',\n organization: '军团长-战团-战士三级结构。'\n },\n faction_3: {\n name: factionNames[2],\n background: '民间航运与情报商的联合体。',\n territory: '雾海群岛与贸易航线。',\n conflicts: '与星环议会在情报垄断上摩擦。',\n abilities: '隐秘交易、情报渗透。',\n organization: '船团-交易所-密探网络。'\n },\n faction_4: {\n name: factionNames[3],\n background: '神秘学派系,传承远古科技遗迹的钥匙。',\n territory: '沉眠遗迹与地下迷城。',\n conflicts: '与赤曜军团争夺遗迹控制权。',\n abilities: '古代机关、秘仪研究。',\n organization: '掌钥者-祭司-侦行者。'\n },\n stats_allocation: stats,\n inventory: ['折叠光刃', '信标晶片', '旧式通讯终端'],\n Power_level:\n settings.magic === 'high'\n ? '魔法与科技高度融合,常见的战斗手段包括符文装甲与能量护盾。'\n : '科技与魔法仍在磨合阶段,民间更多依赖冷兵器与简易工具。',\n pic_style: ['低饱和', '雾感', '赛博幻想', '柔和霓虹']\n };\n};\n\nconst buildNarrator = (input, worldBuilder) => ({\n origin_story: `你出生在${worldBuilder.map[0].name}的边缘街区,曾在${worldBuilder.faction_3.name}的船团做过短暂学徒。${input.charDesc || '你始终记得那场改变命运的风暴。'}`\n});\n\nconst buildQuestMaster = (input, worldBuilder) => {\n const taskCount = rng(5, 8);\n const tasks = Array.from({ length: taskCount }, (_, index) => ({\n id: index + 1,\n summary: `任务 ${index + 1}:前往${worldBuilder.map[index % worldBuilder.map.length].name},调查与${worldBuilder.factions[index % 4].name}有关的异动。`\n }));\n return {\n task_num: taskCount,\n final_goal: `协助${worldBuilder.faction_1.name}赢得关键战役,获得影响世界格局的席位。`,\n tasks\n };\n};\n\nconst buildSkillMaster = (worldBuilder) => ({\n skill: ['雾影潜行', '短距跃迁', '星环解读'],\n item: worldBuilder.inventory\n});\n\nconst buildAssets = (worldBuilder) => {\n const avatarSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n \n `)}`;\n\n return {\n avatar: {\n prompt: `角色头像,风格:${worldBuilder.pic_style.join(',')}`,\n image: avatarSvg\n },\n map: {\n prompt: `地图概览,风格:${worldBuilder.pic_style.join(',')}`,\n image: mapSvg\n }\n };\n};\n\nconst buildCharacter = (input, worldBuilder, assets) => ({\n name: `流浪者 ${rng(100, 999)}`,\n title: '星港游民',\n avatarSeed: rng(1, 9999),\n portrait: assets.avatar.image,\n stats: {\n str: worldBuilder.stats_allocation.str,\n dex: worldBuilder.stats_allocation.dex,\n int: worldBuilder.stats_allocation.int,\n cha: worldBuilder.stats_allocation.cha,\n money: rng(10, 50)\n },\n inventory: worldBuilder.inventory,\n originStory: input.charDesc\n});\n\nconst buildWorld = (worldBuilder) => ({\n name: '艾瑞斯 · 零号扇区',\n description: worldBuilder.world_setting,\n factions: worldBuilder.factions.map((faction) => faction.name),\n mapNodes: worldBuilder.map\n});\n\nconst buildFirstQuest = (questMaster) => {\n if (!questMaster?.tasks?.length) return { text: pick(MOCK_INTRO_QUESTS), options: [] };\n return {\n text: `【起始任务】${questMaster.tasks[0].summary}`,\n options: ['谨慎观察周围环境', '寻找可接触的势力线人', '检查装备并规划路线']\n };\n};\n\nconst baseTaskLine = (questMaster) =>\n questMaster.tasks.map((task) => `【节点 ${task.id}】${task.summary}`);\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n const worldDescRaw = settings.worldDesc?.trim() || '';\n const charDescRaw = settings.charDesc?.trim() || '';\n\n await wait(SIMULATION_DELAY);\n\n const safetyFailed = detectSafetyIssues(`${worldDescRaw} ${charDescRaw}`);\n const utilityCheck = checkUtility(worldDescRaw, charDescRaw);\n\n const expansionFlag = checkExpansion(`${worldDescRaw} ${charDescRaw}`);\n const sanitizedWorldDesc = sanitizePrompt(worldDescRaw, { removeTaskDirectives: true });\n const sanitizedCharDesc = sanitizePrompt(charDescRaw, { removeTaskDirectives: false });\n\n const checks = {\n safety: {\n status: safetyFailed ? 'fail' : 'pass',\n message: safetyFailed\n ? '检测到可能的攻击或敏感信息,请调整描述后再试。'\n : '安全检查通过。'\n },\n utility: {\n status: utilityCheck.worldOk && utilityCheck.charOk ? 'pass' : 'fail',\n message:\n utilityCheck.worldOk && utilityCheck.charOk\n ? '效用检查通过。'\n : '世界观或角色描述不足,请补充更清晰的设定。'\n },\n expansion: {\n status: expansionFlag ? 'warn' : 'pass',\n message: expansionFlag\n ? '检测到强指向性或数值化描述,已自动弱化处理。'\n : '扩展检查通过。'\n }\n };\n\n if (safetyFailed || !utilityCheck.worldOk || !utilityCheck.charOk) {\n return res.status(400).json({\n ok: false,\n checks,\n sanitizedInput: {\n worldDesc: sanitizedWorldDesc,\n charDesc: sanitizedCharDesc\n }\n });\n }\n\n await wait(SIMULATION_DELAY);\n\n const input = {\n worldDesc: sanitizedWorldDesc || worldDescRaw,\n charDesc: sanitizedCharDesc || charDescRaw,\n settings: {\n magic: settings.magic || 'mid',\n physics: settings.physics || 'mid',\n tech: settings.tech || 'mid'\n }\n };\n\n const worldBuilder = buildWorldBuilder(input.settings, input);\n const narrator = buildNarrator(input, worldBuilder);\n const questMaster = buildQuestMaster(input, worldBuilder);\n const skillMaster = buildSkillMaster(worldBuilder);\n const assets = buildAssets(worldBuilder);\n\n const character = buildCharacter(input, worldBuilder, assets);\n const world = buildWorld(worldBuilder);\n const firstQuest = buildFirstQuest(questMaster);\n\n res.json({\n ok: true,\n checks,\n playerInput: input,\n worldBuilder,\n multiAgent: {\n narrator,\n questMaster,\n skillMaster\n },\n assets,\n character,\n world,\n taskLine: baseTaskLine(questMaster),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}, "750a3c9e6871cd26731096ba68b6a5b6003f060d": {"t": 1785766042.0213237, "n": 4219, "h": "3d57178dd2c80c83", "c": "import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { nanoid } from 'nanoid';\nimport fs from 'fs/promises';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\nconst PORT = process.env.PORT || 3001;\n\napp.use(cors());\napp.use(express.json({ limit: '1mb' }));\n\nconst DATA_DIR = path.join(__dirname, 'data');\nconst CLIENT_DIST = path.join(__dirname, '../client/dist');\n\nconst SIMULATION_DELAY = 900;\n\nconst MOCK_WORLD_CONTEXTS = [\n '在旧日的灰烬中,霓虹灯与古老的符文交织。巨型企业掌握着魔法源,而底层的黑客们试图解开神灵的防火墙。',\n '这是一个由浮空岛屿构成的世界,重力由于千年前的“大崩坏”而紊乱。飞空艇是唯一的交通工具。',\n '永夜笼罩的冰封废土,蒸汽核心提供的热量是生存的唯一货币。机械教会统治着地下避难所。'\n];\n\nconst MOCK_INTRO_QUESTS = [\n '你在这个混乱的世界中醒来,头痛欲裂。你手里紧握着一枚不知名的徽章。你的直觉告诉你,必须先去“铁锈酒馆”找到老杰克。',\n '作为新晋的调查员,你的第一个任务是潜入下层区,调查关于“以太泄漏”的传闻。如果你失败了,没人会记得你。',\n '逃亡已经持续了三天。你的补给耗尽了,前方是戒备森严的边境哨所。你必须想办法通过,或者绕过去。'\n];\n\nconst rng = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;\nconst wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst pick = (items) => items[rng(0, items.length - 1)];\n\nconst buildCharacter = (settings) => ({\n name: `流浪者 No.${rng(100, 999)}`,\n title: '迷失的灵魂',\n avatarSeed: rng(1, 9999),\n stats: {\n str: rng(5, 15) + (settings.physics === 'high' ? 5 : 0),\n int: rng(5, 15) + (settings.magic === 'high' ? 5 : 0),\n dex: rng(5, 15) + (settings.tech === 'high' ? 5 : 0),\n cha: rng(5, 15),\n money: rng(10, 100)\n },\n inventory: ['生锈的匕首', '半块压缩饼干', '神秘的芯片']\n});\n\nconst buildWorld = (settings) => ({\n name: '艾瑞斯 · 零号扇区',\n description: settings.worldDesc || pick(MOCK_WORLD_CONTEXTS),\n factions: ['赛博神教', '废土游骑兵', '奥术辛迪加'],\n mapNodes: [\n { id: 1, name: '起始点: 贫民窟', x: 18, y: 80, type: 'start' },\n { id: 2, name: '中立区: 贸易站', x: 50, y: 50, type: 'neutral' },\n { id: 3, name: '禁区: 核心塔', x: 82, y: 18, type: 'danger' }\n ]\n});\n\nconst buildFirstQuest = () => ({\n text: pick(MOCK_INTRO_QUESTS),\n options: ['低调行事,观察周围环境', '大声询问,试图寻找线索', '检查装备,准备战斗']\n});\n\nconst baseTaskLine = (character, world) => [\n `【起始线】你被指派成为“${world.factions[0]}”的临时联络人,需要在一周内获取情报。`,\n `【身份】你伪装成${character.title},携带一份未解密的数据卷轴。`,\n '【目标】侦测“核心塔”的动向,为派系争取先机。'\n];\n\napp.get('/api/health', (req, res) => {\n res.json({ ok: true });\n});\n\napp.post('/api/genesis', async (req, res) => {\n const settings = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const character = buildCharacter(settings);\n const world = buildWorld(settings);\n const firstQuest = buildFirstQuest();\n\n res.json({\n character,\n world,\n taskLine: baseTaskLine(character, world),\n firstQuest\n });\n});\n\napp.post('/api/turn', async (req, res) => {\n const { action, stats } = req.body || {};\n await wait(SIMULATION_DELAY);\n\n const success = Math.random() > 0.4;\n const deltaMoney = success ? rng(10, 50) : -rng(0, 10);\n const newMoney = Math.max(0, (stats?.money || 0) + deltaMoney);\n\n const narrative = success\n ? `你的行动【${action}】取得了意想不到的成功。周围的人对你刮目相看,你发现了一些有价值的线索。`\n : `你的尝试【${action}】遇到阻碍。局势变得更加复杂,你需要更谨慎地行动。`;\n\n const mapUnlock = Math.random() > 0.7\n ? { id: rng(4, 99), name: '新区域: 秘密通道', x: rng(30, 70), y: rng(30, 70), type: 'secret' }\n : null;\n\n res.json({\n narrative,\n newOptions: ['继续深入调查', '寻找补给点休息', '尝试与当地势力接触'],\n statUpdates: {\n money: newMoney\n },\n mapUnlock\n });\n});\n\napp.post('/api/save', async (req, res) => {\n try {\n const id = nanoid(8);\n const payload = { ...req.body, savedAt: new Date().toISOString() };\n await fs.mkdir(DATA_DIR, { recursive: true });\n await fs.writeFile(path.join(DATA_DIR, `${id}.json`), JSON.stringify(payload, null, 2));\n res.json({ id });\n } catch (error) {\n res.status(500).json({ error: '保存失败' });\n }\n});\n\napp.get('/api/save/:id', async (req, res) => {\n try {\n const file = await fs.readFile(path.join(DATA_DIR, `${req.params.id}.json`), 'utf-8');\n res.json(JSON.parse(file));\n } catch (error) {\n res.status(404).json({ error: '未找到存档' });\n }\n});\n\napp.use(express.static(CLIENT_DIST));\napp.get('*', (req, res) => {\n res.sendFile(path.join(CLIENT_DIST, 'index.html'));\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on http://localhost:${PORT}`);\n});\n"}}}}