Files

1 line
913 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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, '&lt;').replace(/>/g, '&gt;') || 'IMAGE';\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#0f172a\" />\n <stop offset=\"100%\" stop-color=\"#1e293b\" />\n </linearGradient>\n </defs>\n <rect width=\"100%\" height=\"100%\" fill=\"url(#bg)\" />\n <rect x=\"6%\" y=\"6%\" width=\"88%\" height=\"88%\" fill=\"none\" stroke=\"#38bdf8\" stroke-width=\"3\" stroke-dasharray=\"10 8\" />\n <text x=\"50%\" y=\"50%\" fill=\"#e2e8f0\" font-family=\"Arial, sans-serif\" font-size=\"24\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n ${safeLabel}\n </text>\n <text x=\"50%\" y=\"62%\" fill=\"#94a3b8\" font-family=\"Arial, sans-serif\" font-size=\"14\" text-anchor=\"middle\" dominant-baseline=\"middle\">\n image unavailable\n </text>\n </svg>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"256\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#22d3ee\" />\n <stop offset=\"100%\" stop-color=\"#6366f1\" />\n </linearGradient>\n </defs>\n <rect width=\"256\" height=\"256\" fill=\"#0f172a\" />\n <circle cx=\"128\" cy=\"128\" r=\"100\" fill=\"url(#g)\" opacity=\"0.9\" />\n <text x=\"128\" y=\"140\" font-size=\"56\" text-anchor=\"middle\" fill=\"#0f172a\" font-family=\"sans-serif\">L</text>\n </svg>\n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"320\">\n <rect width=\"512\" height=\"320\" fill=\"#0f172a\" />\n <rect x=\"24\" y=\"24\" width=\"464\" height=\"272\" fill=\"#111827\" stroke=\"#22d3ee\" stroke-width=\"2\" />\n <text x=\"256\" y=\"60\" font-size=\"18\" text-anchor=\"middle\" fill=\"#94a3b8\" font-family=\"sans-serif\">世界概要地图</text>\n <text x=\"256\" y=\"92\" font-size=\"12\" text-anchor=\"middle\" fill=\"#64748b\" font-family=\"sans-serif\">${worldBuilder.pic_style.join(' · ')}</text>\n </svg>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"256\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#22d3ee\" />\n <stop offset=\"100%\" stop-color=\"#6366f1\" />\n </linearGradient>\n </defs>\n <rect width=\"256\" height=\"256\" fill=\"#0f172a\" />\n <circle cx=\"128\" cy=\"128\" r=\"100\" fill=\"url(#g)\" opacity=\"0.9\" />\n <text x=\"128\" y=\"140\" font-size=\"56\" text-anchor=\"middle\" fill=\"#0f172a\" font-family=\"sans-serif\">L</text>\n </svg>\n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"320\">\n <rect width=\"512\" height=\"320\" fill=\"#0f172a\" />\n <rect x=\"24\" y=\"24\" width=\"464\" height=\"272\" fill=\"#111827\" stroke=\"#22d3ee\" stroke-width=\"2\" />\n <text x=\"256\" y=\"60\" font-size=\"18\" text-anchor=\"middle\" fill=\"#94a3b8\" font-family=\"sans-serif\">世界概要地图</text>\n <text x=\"256\" y=\"92\" font-size=\"12\" text-anchor=\"middle\" fill=\"#64748b\" font-family=\"sans-serif\">${worldBuilder.pic_style.join(' · ')}</text>\n </svg>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"256\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#22d3ee\" />\n <stop offset=\"100%\" stop-color=\"#6366f1\" />\n </linearGradient>\n </defs>\n <rect width=\"256\" height=\"256\" fill=\"#0f172a\" />\n <circle cx=\"128\" cy=\"128\" r=\"100\" fill=\"url(#g)\" opacity=\"0.9\" />\n <text x=\"128\" y=\"140\" font-size=\"56\" text-anchor=\"middle\" fill=\"#0f172a\" font-family=\"sans-serif\">L</text>\n </svg>\n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"320\">\n <rect width=\"512\" height=\"320\" fill=\"#0f172a\" />\n <rect x=\"24\" y=\"24\" width=\"464\" height=\"272\" fill=\"#111827\" stroke=\"#22d3ee\" stroke-width=\"2\" />\n <text x=\"256\" y=\"60\" font-size=\"18\" text-anchor=\"middle\" fill=\"#94a3b8\" font-family=\"sans-serif\">世界概要地图</text>\n <text x=\"256\" y=\"92\" font-size=\"12\" text-anchor=\"middle\" fill=\"#64748b\" font-family=\"sans-serif\">${worldBuilder.pic_style.join(' · ')}</text>\n </svg>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"256\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#22d3ee\" />\n <stop offset=\"100%\" stop-color=\"#6366f1\" />\n </linearGradient>\n </defs>\n <rect width=\"256\" height=\"256\" fill=\"#0f172a\" />\n <circle cx=\"128\" cy=\"128\" r=\"100\" fill=\"url(#g)\" opacity=\"0.9\" />\n <text x=\"128\" y=\"140\" font-size=\"56\" text-anchor=\"middle\" fill=\"#0f172a\" font-family=\"sans-serif\">L</text>\n </svg>\n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"320\">\n <rect width=\"512\" height=\"320\" fill=\"#0f172a\" />\n <rect x=\"24\" y=\"24\" width=\"464\" height=\"272\" fill=\"#111827\" stroke=\"#22d3ee\" stroke-width=\"2\" />\n <text x=\"256\" y=\"60\" font-size=\"18\" text-anchor=\"middle\" fill=\"#94a3b8\" font-family=\"sans-serif\">世界概要地图</text>\n <text x=\"256\" y=\"92\" font-size=\"12\" text-anchor=\"middle\" fill=\"#64748b\" font-family=\"sans-serif\">${worldBuilder.pic_style.join(' · ')}</text>\n </svg>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"256\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#22d3ee\" />\n <stop offset=\"100%\" stop-color=\"#6366f1\" />\n </linearGradient>\n </defs>\n <rect width=\"256\" height=\"256\" fill=\"#0f172a\" />\n <circle cx=\"128\" cy=\"128\" r=\"100\" fill=\"url(#g)\" opacity=\"0.9\" />\n <text x=\"128\" y=\"140\" font-size=\"56\" text-anchor=\"middle\" fill=\"#0f172a\" font-family=\"sans-serif\">L</text>\n </svg>\n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"320\">\n <rect width=\"512\" height=\"320\" fill=\"#0f172a\" />\n <rect x=\"24\" y=\"24\" width=\"464\" height=\"272\" fill=\"#111827\" stroke=\"#22d3ee\" stroke-width=\"2\" />\n <text x=\"256\" y=\"60\" font-size=\"18\" text-anchor=\"middle\" fill=\"#94a3b8\" font-family=\"sans-serif\">世界概要地图</text>\n <text x=\"256\" y=\"92\" font-size=\"12\" text-anchor=\"middle\" fill=\"#64748b\" font-family=\"sans-serif\">${worldBuilder.pic_style.join(' · ')}</text>\n </svg>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"256\" height=\"256\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#22d3ee\" />\n <stop offset=\"100%\" stop-color=\"#6366f1\" />\n </linearGradient>\n </defs>\n <rect width=\"256\" height=\"256\" fill=\"#0f172a\" />\n <circle cx=\"128\" cy=\"128\" r=\"100\" fill=\"url(#g)\" opacity=\"0.9\" />\n <text x=\"128\" y=\"140\" font-size=\"56\" text-anchor=\"middle\" fill=\"#0f172a\" font-family=\"sans-serif\">L</text>\n </svg>\n `)}`;\n\n const mapSvg = `data:image/svg+xml;utf8,${encodeURIComponent(`\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"320\">\n <rect width=\"512\" height=\"320\" fill=\"#0f172a\" />\n <rect x=\"24\" y=\"24\" width=\"464\" height=\"272\" fill=\"#111827\" stroke=\"#22d3ee\" stroke-width=\"2\" />\n <text x=\"256\" y=\"60\" font-size=\"18\" text-anchor=\"middle\" fill=\"#94a3b8\" font-family=\"sans-serif\">世界概要地图</text>\n <text x=\"256\" y=\"92\" font-size=\"12\" text-anchor=\"middle\" fill=\"#64748b\" font-family=\"sans-serif\">${worldBuilder.pic_style.join(' · ')}</text>\n </svg>\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"}}}}