{"repo": "486-king/mobious", "files": {".env": {"349621bd052684362b2e92bcb079712dc67adfb4": {"t": 1785745074.5165844, "n": 135, "h": "9a1a40a882ac05fc", "c": "DEEPSEEK_API_KEY=sk-a41f9dc6487146dbb66cce62b7e9ee53\nDEEPSEEK_BASE_URL=https://api.deepseek.com\nDEEPSEEK_MODEL=deepseek-chat\nPORT=3001\n"}, "30bb1653954d32fcf972b7ea1c43bd18f14739e7": {"t": 1785747637.900126, "n": 135, "h": "9a1a40a882ac05fc", "c": "DEEPSEEK_API_KEY=sk-a41f9dc6487146dbb66cce62b7e9ee53\nDEEPSEEK_BASE_URL=https://api.deepseek.com\nDEEPSEEK_MODEL=deepseek-chat\nPORT=3001\n"}}, "cloudbase/functions/ai-api/index.js": {"349621bd052684362b2e92bcb079712dc67adfb4": {"t": 1785746670.050726, "n": 23153, "h": "62282fcfde3cd0b5", "c": "import cors from \"cors\";\nimport express from \"express\";\nimport OpenAI from \"openai\";\nimport { buildAnalyzePrompt, buildAnchorReplyPrompt, buildFinalArchivePrompt, buildGhostCommandPrompt } from \"./prompts.js\";\n\n/* ─── 环境变量(CloudBase 控制台配置,不留真实 Key 在代码中)─── */\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL || \"https://api.deepseek.com\";\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || \"deepseek-chat\";\nconst PORT = process.env.PORT || 9000;\nconst ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || \"*\";\n\n/* ─── 初始化 ─── */\nconst app = express();\nconst client = DEEPSEEK_API_KEY ? new OpenAI({ apiKey: DEEPSEEK_API_KEY, baseURL: DEEPSEEK_BASE_URL }) : null;\n\n/* ─── CORS ─── */\nconst corsOrigins = ALLOWED_ORIGIN === \"*\" ? \"*\" : [ALLOWED_ORIGIN, \"http://localhost:5173\", \"http://localhost:4173\", \"http://127.0.0.1:5173\", \"http://127.0.0.1:4173\"];\napp.use(cors({ origin: corsOrigins }));\napp.options(\"*\", cors({ origin: corsOrigins }));\napp.use(express.json({ limit: \"1mb\" }));\n\n/* ═══════════════════════════════════════════════════════════\n 4 个 AI API 接口(请求体/响应格式与 server/index.js 完全兼容)\n ═══════════════════════════════════════════════════════════ */\n\n/* ─── POST /api/analyze-answer ─── */\napp.post(\"/api/analyze-answer\", async (req, res) => {\n const fallback = fallbackAnalyze(req.body);\n if (!client) return sendFallback(res, \"analyze-answer\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const parsed = await completeJson(buildAnalyzePrompt(req.body), \"analyze-answer\");\n res.json({ ok: true, fallback: false, data: normalizeAnalyze(parsed, fallback) });\n } catch (error) {\n logAIError(\"analyze-answer\", error);\n sendFallback(res, \"analyze-answer\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ─── POST /api/anchor-reply ─── */\napp.post(\"/api/anchor-reply\", async (req, res) => {\n const context = normalizeAnchorContext(req.body);\n const fallback = fallbackAnchorReply(context);\n if (!client) return sendFallback(res, \"anchor-reply\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const first = normalizeAnchorReply(await completeJson(buildAnchorReplyPrompt(context), \"anchor-reply\"), fallback);\n if (validateAnchorReply(first, context).ok) {\n return res.json({ ok: true, fallback: false, data: first });\n }\n\n const second = normalizeAnchorReply(await completeJson(buildAnchorReplyPrompt(context, true), \"anchor-reply-retry\"), fallback);\n const validation = validateAnchorReply(second, context);\n if (validation.ok) {\n return res.json({ ok: true, fallback: false, data: second });\n }\n\n console.warn(\"[AI VALIDATION FALLBACK]\", { endpoint: \"anchor-reply\", reason: validation.reason });\n sendFallback(res, \"anchor-reply\", validation.reason, \"Anchor reply did not match scene constraints after retry.\", fallback);\n } catch (error) {\n logAIError(\"anchor-reply\", error);\n sendFallback(res, \"anchor-reply\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ─── POST /api/ghost-command ─── */\napp.post(\"/api/ghost-command\", async (req, res) => {\n const context = normalizeGhostContext(req.body);\n const fallback = fallbackGhostCommand(context);\n if (!client) return sendFallback(res, \"ghost-command\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const first = normalizeGhostCommand(await completeJson(buildGhostCommandPrompt(context), \"ghost-command\"), fallback, context);\n const validation = validateGhostCommand(first, context);\n if (validation.ok) {\n return res.json({ ok: true, fallback: false, data: first });\n }\n\n const second = normalizeGhostCommand(await completeJson(buildGhostCommandPrompt(context, true), \"ghost-command-retry\"), fallback, context);\n const retryValidation = validateGhostCommand(second, context);\n if (retryValidation.ok) {\n return res.json({ ok: true, fallback: false, data: second });\n }\n\n console.warn(\"[AI VALIDATION FALLBACK]\", { endpoint: \"ghost-command\", reason: retryValidation.reason });\n sendFallback(res, \"ghost-command\", retryValidation.reason, \"Ghost command did not match scene constraints after retry.\", fallback);\n } catch (error) {\n logAIError(\"ghost-command\", error);\n sendFallback(res, \"ghost-command\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ─── POST /api/final-archive ─── */\napp.post(\"/api/final-archive\", async (req, res) => {\n const fallback = fallbackArchive(req.body);\n if (!client) return sendFallback(res, \"final-archive\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const parsed = await completeJson(buildFinalArchivePrompt(req.body), \"final-archive\");\n res.json({ ok: true, fallback: false, data: normalizeArchive(parsed, fallback) });\n } catch (error) {\n logAIError(\"final-archive\", error);\n sendFallback(res, \"final-archive\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ═══════════════════════════════════════════════════════════\n 辅助函数(逻辑与 server/index.js 一致)\n ═══════════════════════════════════════════════════════════ */\n\n/* ─── DeepSeek 调用 ─── */\nasync function completeJson(messages, endpoint) {\n const response = await client.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages,\n temperature: 0.62,\n response_format: { type: \"json_object\" },\n });\n const content = response.choices?.[0]?.message?.content ?? \"\";\n const parsed = safeJsonParse(content);\n if (!parsed.ok) {\n const error = new Error(\"JSON parsing failed\");\n error.code = \"JSON_PARSE_FAILED\";\n error.endpoint = endpoint;\n error.raw = content.slice(0, 800);\n throw error;\n }\n return parsed.data;\n}\n\nfunction safeJsonParse(text) {\n try {\n return { ok: true, data: JSON.parse(text) };\n } catch {\n const start = text.indexOf(\"{\");\n const end = text.lastIndexOf(\"}\");\n if (start >= 0 && end > start) {\n try {\n return { ok: true, data: JSON.parse(text.slice(start, end + 1)) };\n } catch {\n return { ok: false };\n }\n }\n return { ok: false };\n }\n}\n\n/* ─── Fallback 响应 ─── */\nfunction sendFallback(res, endpoint, error, hint, fallbackData) {\n res.json({\n ok: false,\n fallback: true,\n error,\n hint,\n data: attachFallbackMessage(endpoint, fallbackData),\n });\n}\n\nfunction attachFallbackMessage(endpoint, data) {\n const reason = \"云端的回声暂时沉默了。\\n但这段记忆仍会按照本地残响继续。\";\n if (endpoint === \"analyze-answer\") return { ...data, echoComment: `${reason}\\n${data.echoComment}` };\n if (endpoint === \"anchor-reply\") return { ...data, reply: `${reason}\\n${data.reply}` };\n if (endpoint === \"ghost-command\") return { ...data, deviationText: `${reason}\\n${data.deviationText}` };\n if (endpoint === \"final-archive\") return { ...data, archiveText: `${reason}\\n${data.archiveText}` };\n return data;\n}\n\n/* ─── 日志与错误 ─── */\nfunction logAIError(endpoint, error) {\n console.error(\"[AI ERROR]\", { endpoint, message: error?.message, status: error?.status, code: error?.code, raw: error?.raw });\n}\n\nfunction getErrorMessage(error) {\n if (error?.code === \"JSON_PARSE_FAILED\") return \"JSON parsing failed\";\n return error?.message || String(error);\n}\n\nfunction buildHint(error) {\n if (error?.code === \"JSON_PARSE_FAILED\") return \"DeepSeek returned text that could not be parsed as JSON.\";\n if (error?.status === 401 || error?.status === 403) return \"Check DEEPSEEK_API_KEY permissions and redeploy the cloud function.\";\n if (error?.status === 404) return \"Check DEEPSEEK_BASE_URL and DEEPSEEK_MODEL.\";\n if (error?.status === 429) return \"DeepSeek rate limit or quota issue.\";\n return \"Check CloudBase environment variables: DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL.\";\n}\n\nfunction missingKeyHint() {\n return \"In CloudBase console, set environment variable DEEPSEEK_API_KEY and redeploy the cloud function.\";\n}\n\n/* ─── 通用辅助 ─── */\nfunction clamp(value, min, max) {\n const number = Number(value);\n if (!Number.isFinite(number)) return min;\n return Math.max(min, Math.min(max, Math.round(number)));\n}\n\nfunction asText(value, fallback) {\n return typeof value === \"string\" && value.trim() ? value.trim() : fallback;\n}\n\n/* ─── analyze-answer 相关 ─── */\nfunction normalizeAnalyze(data, fallback) {\n return {\n sincerity: clamp(data.sincerity, 0, 100),\n dependency: clamp(data.dependency, 0, 100),\n sacrifice: clamp(data.sacrifice, 0, 100),\n avoidance: clamp(data.avoidance, 0, 100),\n control: clamp(data.control, 0, 100),\n emotionalAnchorDelta: clamp(data.emotionalAnchorDelta, -5, 25),\n anchorTrait: asText(data.anchorTrait, fallback.anchorTrait).slice(0, 10),\n memorySeed: asText(data.memorySeed, fallback.memorySeed).slice(0, 10),\n echoComment: asText(data.echoComment, fallback.echoComment).slice(0, 60),\n };\n}\n\nfunction fallbackAnalyze({ question, answer }) {\n const text = String(answer || \"\");\n const short = text.trim().length < 5;\n const concrete = /雨|光|手|等|抱|名字|咖啡|伞|教室|记得|消失|活下去/.test(text);\n const sacrifice = /消失|活下去|代价|接受|愿意|牺牲/.test(text);\n const avoidance = short || /不知道|随便|都行|无所谓/.test(text);\n const promptText = typeof question === \"string\" ? question : question?.prompt || \"这段回声\";\n return {\n sincerity: clamp((concrete ? 62 : 42) - (avoidance ? 22 : 0), 0, 100),\n dependency: clamp(/陪|抱|靠近|理解|记得|等/.test(text) ? 62 : 36, 0, 100),\n sacrifice: clamp(sacrifice ? 72 : 28, 0, 100),\n avoidance: clamp(avoidance ? 62 : 18, 0, 100),\n control: clamp(/必须|不要|擦掉|证明|只属于/.test(text) ? 58 : 18, 0, 100),\n emotionalAnchorDelta: clamp((concrete ? 14 : 8) + (sacrifice ? 6 : 0) - (avoidance ? 6 : 0), -5, 25),\n anchorTrait: concrete ? \"安静陪伴型\" : \"笨拙守护型\",\n memorySeed: promptText.includes(\"唯一\") ? \"未说出口\" : promptText.includes(\"无助\") ? \"雨夜等待\" : \"黄昏回声\",\n echoComment: concrete ? `这段回声记住了\"${text.slice(0, 12)}\"。` : \"这段回声有些模糊,但仍留下一点温度。\",\n };\n}\n\n/* ─── anchor-reply 相关 ─── */\nfunction normalizeAnchorContext(body) {\n const playerAnswers = body.playerAnswers || summarizePlayerAnswers(body.history || []);\n return {\n sceneId: normalizeSceneId(body.sceneId),\n chapter: body.chapter || \"memory\",\n speaker: body.speaker || \"Anchor\",\n playerMessage: String(body.playerMessage || \"\"),\n currentSceneSummary: body.currentSceneSummary || sceneSummary(body.sceneId),\n recentDialogue: Array.isArray(body.recentDialogue) ? body.recentDialogue.slice(-10) : [],\n anchorProfile: body.anchorProfile || {},\n playerAnswers,\n gameState: body.gameState || extractGameState(body.history || []),\n };\n}\n\nfunction normalizeAnchorReply(data, fallback) {\n const emotions = new Set([\"calm\", \"smile\", \"serious\", \"shy\", \"sad\", \"distant\"]);\n const intents = new Set([\"memory\", \"emotion\", \"daily\", \"question\", \"gibberish\"]);\n return {\n intent: intents.has(data.intent) ? data.intent : fallback.intent || \"emotion\",\n reply: asText(data.reply, fallback.reply),\n emotion: emotions.has(data.emotion) ? data.emotion : fallback.emotion,\n emotionalAnchorDelta: clamp(data.emotionalAnchorDelta, -3, 8),\n why: asText(data.why, \"\"),\n };\n}\n\nfunction validateAnchorReply(result, context) {\n if (!result.reply) return { ok: false, reason: \"reply missing\" };\n const reply = String(result.reply);\n if (result.intent === \"gibberish\") {\n if (reply.length < 4 || reply.length > 40) return { ok: false, reason: \"gibberish reply length invalid\" };\n return { ok: true };\n }\n if (reply.length < 4 || reply.length > 120) return { ok: false, reason: \"reply length invalid\" };\n const forbidden = [\"作为AI\", \"我是一个AI\", \"无法\", \"根据你的输入\", \"系统\", \"审\" + \"判\", \"JSON\"];\n if (forbidden.some((word) => reply.includes(word))) return { ok: false, reason: \"reply contains out-of-role wording\" };\n return { ok: true };\n}\n\nfunction fallbackAnchorReply(context) {\n const sceneId = normalizeSceneId(context.sceneId);\n const message = String(context.playerMessage || \"\").trim();\n if (isGibberish(message)) {\n return {\n intent: \"gibberish\",\n reply: Math.random() > 0.5 ? \"不太明白,你可以再说一次吗?\" : \"我没听懂,但我还在这里。\",\n emotion: \"calm\",\n emotionalAnchorDelta: 0,\n };\n }\n if (sceneId.includes(\"classroom\")) {\n return {\n intent: \"emotion\",\n reply: \"你看,连这张账单都会把 1+1 写成 3。\\n所以你偶尔弄错什么,也真的没关系。\",\n emotion: \"smile\",\n emotionalAnchorDelta: 4,\n };\n }\n if (sceneId.includes(\"cafe\")) {\n return {\n intent: \"emotion\",\n reply: \"雨声有点大。\\n你可以慢慢说,我会靠近一点听。\",\n emotion: \"shy\",\n emotionalAnchorDelta: 4,\n };\n }\n if (sceneId.includes(\"ferrisWheel\")) {\n return {\n intent: \"memory\",\n reply: \"如果这句话现在说不出口,也没关系。\\n我会把这一刻先替你记住。\",\n emotion: \"smile\",\n emotionalAnchorDelta: 5,\n };\n }\n return {\n intent: \"memory\",\n reply: \"记忆像远处的光一样晃了一下。\\n我听见了,只是还没能走到你身边。\",\n emotion: \"distant\",\n emotionalAnchorDelta: 2,\n };\n}\n\nfunction isGibberish(text) {\n const value = String(text || \"\").trim();\n if (!value) return true;\n if (value.length <= 2 && !/[\\u4e00-\\u9fa5a-zA-Z0-9]/.test(value)) return true;\n if (/^[^\\u4e00-\\u9fa5a-zA-Z0-9]{3,}$/.test(value)) return true;\n if (/^[a-zA-Z]{1,3}$/.test(value)) return true;\n if (/(.)\\1{5,}/.test(value)) return true;\n return false;\n}\n\n/* ─── ghost-command 相关 ─── */\nfunction normalizeGhostContext(body) {\n const availableObjects = Array.isArray(body.availableObjects) ? body.availableObjects : [];\n return {\n sceneId: normalizeSceneId(body.sceneId),\n playerCommand: String(body.playerCommand || body.command || \"\"),\n currentSceneSummary: body.currentSceneSummary || sceneSummary(body.sceneId),\n availableObjects,\n memoryCards: body.memoryCards || [],\n gameState: body.gameState || {},\n };\n}\n\nfunction normalizeGhostCommand(data, fallback, context) {\n const outcomes = new Set([\"success\", \"partial\", \"failed\", \"rejected\"]);\n const outcome = outcomes.has(data.outcome) ? data.outcome : fallback.outcome || \"partial\";\n const intensity = [\"light\", \"medium\", \"heavy\"].includes(data.intensity) ? data.intensity : fallback.intensity;\n const targetObject = context.availableObjects.includes(data.targetObject) ? data.targetObject : fallback.targetObject;\n const table = ghostInterferenceTable(context.playerCommand, targetObject, intensity, outcome);\n return {\n outcome,\n understoodAction: asText(data.understoodAction, fallback.understoodAction || inferGhostAction(context.playerCommand, targetObject, intensity)),\n targetObject,\n intensity,\n cost: table.cost,\n fateDriftDelta: table.fateDriftDelta,\n memoryStatus: table.memoryStatus,\n deviationText: asText(data.deviationText, fallback.deviationText),\n resultText: asText(data.resultText, fallback.resultText),\n cinematicText: asText(data.cinematicText, fallback.cinematicText || fallback.resultText),\n };\n}\n\nfunction validateGhostCommand(result, context) {\n if (!context.availableObjects.includes(result.targetObject)) return { ok: false, reason: \"targetObject not available\" };\n if (![\"light\", \"medium\", \"heavy\"].includes(result.intensity)) return { ok: false, reason: \"intensity invalid\" };\n if (![\"success\", \"partial\", \"failed\", \"rejected\"].includes(result.outcome)) return { ok: false, reason: \"outcome invalid\" };\n if (result.cost < 1 || result.cost > 70) return { ok: false, reason: \"cost out of range\" };\n if (result.outcome === \"failed\" || result.outcome === \"rejected\") {\n if (result.fateDriftDelta !== 0 || result.memoryStatus !== \"faded\") return { ok: false, reason: \"non-success outcome must not awaken\" };\n return { ok: true };\n }\n if (result.fateDriftDelta < 1 || result.fateDriftDelta > 45) return { ok: false, reason: \"awakening delta out of range\" };\n if (![\"faded\", \"distorted\", \"erased\"].includes(result.memoryStatus)) return { ok: false, reason: \"memoryStatus invalid\" };\n return { ok: true };\n}\n\nfunction fallbackGhostCommand(context) {\n const command = context.playerCommand;\n const targetObject = inferGhostTarget(command, context.availableObjects);\n const intensity = inferGhostIntensity(command);\n const outcome = intensity === \"light\" ? \"partial\" : \"success\";\n const table = ghostInterferenceTable(command, targetObject, intensity, outcome);\n return {\n outcome,\n understoodAction: inferGhostAction(command, targetObject, intensity),\n targetObject,\n intensity,\n ...table,\n deviationText: buildGhostDeviation(command, targetObject),\n resultText: `过去的男主看着${targetObject},低声说:为什么这里会有这句话?`,\n cinematicText: buildGhostCinematicText(command, targetObject, intensity, outcome),\n };\n}\n\nfunction inferGhostTarget(command, availableObjects) {\n return availableObjects.find((object) => command.includes(object) || command.includes(object.replace(/上的|的/g, \"\"))) || availableObjects[0] || \"记忆锚点\";\n}\n\nfunction inferGhostIntensity(command) {\n if (/不要相信咨询师|轮回是假的|她会被夺走|不要进入咨询室|咨询师是未来|爱意会被夺走/.test(command)) return \"heavy\";\n if (/警告|提醒|告诉|写下|刻下|留下/.test(command)) return \"medium\";\n return \"light\";\n}\n\nfunction inferGhostAction(command, targetObject, intensity) {\n const value = String(command || \"\")\n .replace(targetObject, \"\")\n .replace(/^[^::]*[::]/, \"\")\n .replace(/^(把|将|让|使|给|对|在)/, \"\")\n .trim();\n return value ? value.slice(0, 24) : `${ghostVerb(intensity)}${targetObject}`;\n}\n\nfunction ghostVerb(intensity) {\n if (intensity === \"heavy\") return \"写下完整警告:\";\n if (intensity === \"medium\") return \"留下警告于\";\n return \"轻触\";\n}\n\nfunction ghostInterferenceTable(command, target, intensity, outcome) {\n const text = String(command || \"\");\n if (outcome === \"failed\") return { cost: 6 + Math.min(4, Math.floor(text.length / 18)), fateDriftDelta: 0, memoryStatus: \"faded\" };\n if (outcome === \"rejected\") return { cost: 14 + Math.min(10, Math.floor(text.length / 12)), fateDriftDelta: 0, memoryStatus: \"faded\" };\n const base =\n intensity === \"heavy\"\n ? { cost: 42, fateDriftDelta: 30, memoryStatus: \"erased\" }\n : intensity === \"medium\"\n ? { cost: 26, fateDriftDelta: 18, memoryStatus: \"distorted\" }\n : { cost: 12, fateDriftDelta: 7, memoryStatus: \"faded\" };\n const truthBonus = /不要相信咨询师|轮回是假的|她会被夺走|不要进入咨询室|咨询师是未来|爱意会被夺走/.test(text) ? 8 : 0;\n const clarityBonus = /写下|刻下|留下|提醒|告诉|警告/.test(text) ? 4 : 0;\n const objectCost = /发绳|门票|账单|音乐盒/.test(target) ? 4 : /咖啡杯|车票|粉笔/.test(target) ? 2 : 0;\n const partialPenalty = outcome === \"partial\" ? -4 : 0;\n return {\n cost: Math.max(1, base.cost + truthBonus + clarityBonus + objectCost + Math.min(8, Math.floor(text.length / 18))),\n fateDriftDelta: Math.max(0, base.fateDriftDelta + Math.floor(truthBonus / 2) + Math.floor(clarityBonus / 2) + partialPenalty),\n memoryStatus: base.memoryStatus,\n };\n}\n\nfunction buildGhostDeviation(command, target) {\n if (target.includes(\"账单\")) return \"账单背面浮出一行很浅的字,只有过去的你低头时才会看见。\";\n if (target.includes(\"粉笔\") || target.includes(\"黑板擦\") || target.includes(\"板擦\")) return \"粉尘里浮出一小段不该存在的字,像有人把未来藏进了旧日的黄昏。\";\n if (target.includes(\"伞\") || target.includes(\"车票\") || target.includes(\"咖啡杯\")) return \"雨声停顿了一拍,警告沿着物件边缘慢慢显形。\";\n if (target.includes(\"音乐盒\") || target.includes(\"发绳\") || target.includes(\"门票\")) return \"旋律里混入不属于这一刻的杂音,过去的你因此皱起眉。\";\n if (!command.trim()) return \"这段记忆只泛起了很轻的涟漪。\";\n return \"警告留下了痕迹,但过去仍在犹豫。\";\n}\n\nfunction buildGhostCinematicText(command, target, intensity, outcome) {\n const warning = String(command || \"\")\n .replace(target, \"\")\n .replace(/^[^::]*[::]/, \"\")\n .trim();\n if (outcome === \"failed\") {\n return `你试着把声音压进${target},可这段过去没有听懂。物件安静地留在原位,像一封没有地址的信。过去的男主没有回头,夏央的声音仍沿着原来的轨迹落下。`;\n }\n if (outcome === \"rejected\") {\n return `你的愿望触到${target}的一瞬,整段记忆像被无形的手按住。黄昏把过于直接的因果弹开,只剩一点刺痛留在空气里。过去的男主短暂皱眉,却没有真正听懂。`;\n }\n if (intensity === \"heavy\") {\n return `${target}承住了你的警告,连空气都像被压低了一层。${warning ? `\"${warning.slice(0, 28)}\"` : \"那句警告\"}在过去的男主眼前亮起,他终于停下原本要走向黄昏咨询室的脚步。夏央回过头,像也察觉到命运的齿轮迟疑了一瞬。`;\n }\n if (outcome === \"partial\" || intensity === \"light\") {\n return `${target}上浮出一段很浅的痕迹,又很快被旧日的光擦淡。过去的男主看见了,却只能抓住其中一半意思。他低声问自己为什么会在这里停下,像第一次怀疑这段幸福并不完整。`;\n }\n return `${target}轻轻改变了位置,仿佛有人从记忆背面推了它一下。过去的男主注意到那句警告,指尖停在半空。那一刻,他没有立刻相信咨询师,也没有立刻忘记你的声音。`;\n}\n\n/* ─── final-archive 相关 ─── */\nfunction normalizeArchive(data, fallback) {\n return { archiveText: asText(data.archiveText, fallback.archiveText) };\n}\n\nfunction fallbackArchive({ endingId, gameState, playerAnswers, memoryCards }) {\n const warnings = Array.isArray(memoryCards) ? memoryCards.filter((card) => card.status !== \"intact\").length : 0;\n const awakening = gameState?.fateDrift ?? 0;\n const life = gameState?.life ?? 100;\n const answers = summarizePlayerAnswers(Array.isArray(playerAnswers) ? playerAnswers : []);\n const remembered = answers.uniqueMoment || answers.sacrificeChoice || answers.comfortNeed || answers.fear;\n const trace = Array.isArray(memoryCards) ? memoryCards.find((card) => card.status === \"erased\" || card.status === \"distorted\" || card.status === \"faded\")?.objectName : \"\";\n const playerTrace = remembered ? `你曾说过\"${String(remembered).slice(0, 24)}\",这句话被${trace || \"某段记忆\"}留下。` : `${trace || \"某段记忆\"}替你保存了没有说完的警告。`;\n const endingLine =\n endingId === \"E\"\n ? \"过去的你拒绝进入轮回,夏央终于回头,看见了一直在这里的你。\"\n : endingId === \"D\"\n ? \"轮回被打断了,但你的存在被黎明冲淡,夏央只记得有人替她挡住黄昏。\"\n : endingId === \"C\"\n ? \"你用生命把警告送回过去,轮回裂开,却也让自己的声音变得透明。\"\n : endingId === \"B\"\n ? \"过去的你发现异常,却仍未能完全阻止轮回。黄昏门缝里留下了一道裂光。\"\n : \"警告没有抵达,过去的你走进咨询室。你成为下一任咨询师。\";\n return { archiveText: `终章归档:觉醒度 ${awakening},生命值 ${life},留下警告 ${warnings} 处。${playerTrace}${endingLine}` };\n}\n\n/* ─── 通用场景辅助 ─── */\nfunction normalizeSceneId(sceneId) {\n if (sceneId === \"ferris-wheel\") return \"ferrisWheel\";\n return String(sceneId || \"classroom\");\n}\n\nfunction sceneSummary(sceneId) {\n const id = normalizeSceneId(sceneId);\n if (id.includes(\"classroom\")) return \"夕阳空教室,讲台上有算错的账单、半截粉笔、旧黑板擦,主题是被理解。\";\n if (id.includes(\"cafe\")) return \"雨夜咖啡馆,玻璃窗外是街灯和雨,桌上有冷掉的咖啡杯,门边有坏掉的透明伞,还有折皱车票,主题是被偏爱。\";\n if (id.includes(\"ferrisWheel\")) return \"夜晚摩天轮接近最高点,城市灯火在脚下,掌心有音乐盒发条、门票存根与蓝色发绳,主题是被选择。\";\n return \"幽灵阶段的记忆场景,颜色低饱和,Anchor 在远处,过去正在被轻轻改写。\";\n}\n\nfunction summarizePlayerAnswers(history) {\n const answers = history.filter((item) => item && item.questionId && item.answer);\n return {\n fear: answers.find((item) => item.questionId === \"q1\")?.answer || \"\",\n comfortNeed: answers.find((item) => item.questionId === \"q2\")?.answer || \"\",\n uniqueMoment: answers.find((item) => item.questionId === \"q3\")?.answer || \"\",\n sacrificeChoice: answers.find((item) => item.questionId === \"q4\")?.answer || \"\",\n };\n}\n\nfunction extractGameState(history) {\n return history.find((item) => item && typeof item === \"object\" && \"emotionalAnchor\" in item) || {};\n}\n\n/* ─── 启动 ─── */\napp.listen(PORT, () => {\n console.log(\"Mobius AI CloudBase HTTP function started\");\n console.log(`PORT: ${PORT}`);\n console.log(`DEEPSEEK_API_KEY loaded: ${DEEPSEEK_API_KEY ? \"yes\" : \"no\"}`);\n console.log(`DEEPSEEK_BASE_URL: ${DEEPSEEK_BASE_URL}`);\n console.log(`DEEPSEEK_MODEL: ${DEEPSEEK_MODEL}`);\n console.log(`ALLOWED_ORIGIN: ${ALLOWED_ORIGIN}`);\n console.log(`Endpoints:`);\n console.log(` POST /api/analyze-answer`);\n console.log(` POST /api/anchor-reply`);\n console.log(` POST /api/ghost-command`);\n console.log(` POST /api/final-archive`);\n});\n"}, "eed294cf011fb85e2da00932e5441f5540bd3262": {"t": 1785747638.1921773, "n": 23153, "h": "62282fcfde3cd0b5", "c": "import cors from \"cors\";\nimport express from \"express\";\nimport OpenAI from \"openai\";\nimport { buildAnalyzePrompt, buildAnchorReplyPrompt, buildFinalArchivePrompt, buildGhostCommandPrompt } from \"./prompts.js\";\n\n/* ─── 环境变量(CloudBase 控制台配置,不留真实 Key 在代码中)─── */\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL || \"https://api.deepseek.com\";\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || \"deepseek-chat\";\nconst PORT = process.env.PORT || 9000;\nconst ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || \"*\";\n\n/* ─── 初始化 ─── */\nconst app = express();\nconst client = DEEPSEEK_API_KEY ? new OpenAI({ apiKey: DEEPSEEK_API_KEY, baseURL: DEEPSEEK_BASE_URL }) : null;\n\n/* ─── CORS ─── */\nconst corsOrigins = ALLOWED_ORIGIN === \"*\" ? \"*\" : [ALLOWED_ORIGIN, \"http://localhost:5173\", \"http://localhost:4173\", \"http://127.0.0.1:5173\", \"http://127.0.0.1:4173\"];\napp.use(cors({ origin: corsOrigins }));\napp.options(\"*\", cors({ origin: corsOrigins }));\napp.use(express.json({ limit: \"1mb\" }));\n\n/* ═══════════════════════════════════════════════════════════\n 4 个 AI API 接口(请求体/响应格式与 server/index.js 完全兼容)\n ═══════════════════════════════════════════════════════════ */\n\n/* ─── POST /api/analyze-answer ─── */\napp.post(\"/api/analyze-answer\", async (req, res) => {\n const fallback = fallbackAnalyze(req.body);\n if (!client) return sendFallback(res, \"analyze-answer\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const parsed = await completeJson(buildAnalyzePrompt(req.body), \"analyze-answer\");\n res.json({ ok: true, fallback: false, data: normalizeAnalyze(parsed, fallback) });\n } catch (error) {\n logAIError(\"analyze-answer\", error);\n sendFallback(res, \"analyze-answer\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ─── POST /api/anchor-reply ─── */\napp.post(\"/api/anchor-reply\", async (req, res) => {\n const context = normalizeAnchorContext(req.body);\n const fallback = fallbackAnchorReply(context);\n if (!client) return sendFallback(res, \"anchor-reply\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const first = normalizeAnchorReply(await completeJson(buildAnchorReplyPrompt(context), \"anchor-reply\"), fallback);\n if (validateAnchorReply(first, context).ok) {\n return res.json({ ok: true, fallback: false, data: first });\n }\n\n const second = normalizeAnchorReply(await completeJson(buildAnchorReplyPrompt(context, true), \"anchor-reply-retry\"), fallback);\n const validation = validateAnchorReply(second, context);\n if (validation.ok) {\n return res.json({ ok: true, fallback: false, data: second });\n }\n\n console.warn(\"[AI VALIDATION FALLBACK]\", { endpoint: \"anchor-reply\", reason: validation.reason });\n sendFallback(res, \"anchor-reply\", validation.reason, \"Anchor reply did not match scene constraints after retry.\", fallback);\n } catch (error) {\n logAIError(\"anchor-reply\", error);\n sendFallback(res, \"anchor-reply\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ─── POST /api/ghost-command ─── */\napp.post(\"/api/ghost-command\", async (req, res) => {\n const context = normalizeGhostContext(req.body);\n const fallback = fallbackGhostCommand(context);\n if (!client) return sendFallback(res, \"ghost-command\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const first = normalizeGhostCommand(await completeJson(buildGhostCommandPrompt(context), \"ghost-command\"), fallback, context);\n const validation = validateGhostCommand(first, context);\n if (validation.ok) {\n return res.json({ ok: true, fallback: false, data: first });\n }\n\n const second = normalizeGhostCommand(await completeJson(buildGhostCommandPrompt(context, true), \"ghost-command-retry\"), fallback, context);\n const retryValidation = validateGhostCommand(second, context);\n if (retryValidation.ok) {\n return res.json({ ok: true, fallback: false, data: second });\n }\n\n console.warn(\"[AI VALIDATION FALLBACK]\", { endpoint: \"ghost-command\", reason: retryValidation.reason });\n sendFallback(res, \"ghost-command\", retryValidation.reason, \"Ghost command did not match scene constraints after retry.\", fallback);\n } catch (error) {\n logAIError(\"ghost-command\", error);\n sendFallback(res, \"ghost-command\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ─── POST /api/final-archive ─── */\napp.post(\"/api/final-archive\", async (req, res) => {\n const fallback = fallbackArchive(req.body);\n if (!client) return sendFallback(res, \"final-archive\", \"DEEPSEEK_API_KEY is missing\", missingKeyHint(), fallback);\n\n try {\n const parsed = await completeJson(buildFinalArchivePrompt(req.body), \"final-archive\");\n res.json({ ok: true, fallback: false, data: normalizeArchive(parsed, fallback) });\n } catch (error) {\n logAIError(\"final-archive\", error);\n sendFallback(res, \"final-archive\", getErrorMessage(error), buildHint(error), fallback);\n }\n});\n\n/* ═══════════════════════════════════════════════════════════\n 辅助函数(逻辑与 server/index.js 一致)\n ═══════════════════════════════════════════════════════════ */\n\n/* ─── DeepSeek 调用 ─── */\nasync function completeJson(messages, endpoint) {\n const response = await client.chat.completions.create({\n model: DEEPSEEK_MODEL,\n messages,\n temperature: 0.62,\n response_format: { type: \"json_object\" },\n });\n const content = response.choices?.[0]?.message?.content ?? \"\";\n const parsed = safeJsonParse(content);\n if (!parsed.ok) {\n const error = new Error(\"JSON parsing failed\");\n error.code = \"JSON_PARSE_FAILED\";\n error.endpoint = endpoint;\n error.raw = content.slice(0, 800);\n throw error;\n }\n return parsed.data;\n}\n\nfunction safeJsonParse(text) {\n try {\n return { ok: true, data: JSON.parse(text) };\n } catch {\n const start = text.indexOf(\"{\");\n const end = text.lastIndexOf(\"}\");\n if (start >= 0 && end > start) {\n try {\n return { ok: true, data: JSON.parse(text.slice(start, end + 1)) };\n } catch {\n return { ok: false };\n }\n }\n return { ok: false };\n }\n}\n\n/* ─── Fallback 响应 ─── */\nfunction sendFallback(res, endpoint, error, hint, fallbackData) {\n res.json({\n ok: false,\n fallback: true,\n error,\n hint,\n data: attachFallbackMessage(endpoint, fallbackData),\n });\n}\n\nfunction attachFallbackMessage(endpoint, data) {\n const reason = \"云端的回声暂时沉默了。\\n但这段记忆仍会按照本地残响继续。\";\n if (endpoint === \"analyze-answer\") return { ...data, echoComment: `${reason}\\n${data.echoComment}` };\n if (endpoint === \"anchor-reply\") return { ...data, reply: `${reason}\\n${data.reply}` };\n if (endpoint === \"ghost-command\") return { ...data, deviationText: `${reason}\\n${data.deviationText}` };\n if (endpoint === \"final-archive\") return { ...data, archiveText: `${reason}\\n${data.archiveText}` };\n return data;\n}\n\n/* ─── 日志与错误 ─── */\nfunction logAIError(endpoint, error) {\n console.error(\"[AI ERROR]\", { endpoint, message: error?.message, status: error?.status, code: error?.code, raw: error?.raw });\n}\n\nfunction getErrorMessage(error) {\n if (error?.code === \"JSON_PARSE_FAILED\") return \"JSON parsing failed\";\n return error?.message || String(error);\n}\n\nfunction buildHint(error) {\n if (error?.code === \"JSON_PARSE_FAILED\") return \"DeepSeek returned text that could not be parsed as JSON.\";\n if (error?.status === 401 || error?.status === 403) return \"Check DEEPSEEK_API_KEY permissions and redeploy the cloud function.\";\n if (error?.status === 404) return \"Check DEEPSEEK_BASE_URL and DEEPSEEK_MODEL.\";\n if (error?.status === 429) return \"DeepSeek rate limit or quota issue.\";\n return \"Check CloudBase environment variables: DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL.\";\n}\n\nfunction missingKeyHint() {\n return \"In CloudBase console, set environment variable DEEPSEEK_API_KEY and redeploy the cloud function.\";\n}\n\n/* ─── 通用辅助 ─── */\nfunction clamp(value, min, max) {\n const number = Number(value);\n if (!Number.isFinite(number)) return min;\n return Math.max(min, Math.min(max, Math.round(number)));\n}\n\nfunction asText(value, fallback) {\n return typeof value === \"string\" && value.trim() ? value.trim() : fallback;\n}\n\n/* ─── analyze-answer 相关 ─── */\nfunction normalizeAnalyze(data, fallback) {\n return {\n sincerity: clamp(data.sincerity, 0, 100),\n dependency: clamp(data.dependency, 0, 100),\n sacrifice: clamp(data.sacrifice, 0, 100),\n avoidance: clamp(data.avoidance, 0, 100),\n control: clamp(data.control, 0, 100),\n emotionalAnchorDelta: clamp(data.emotionalAnchorDelta, -5, 25),\n anchorTrait: asText(data.anchorTrait, fallback.anchorTrait).slice(0, 10),\n memorySeed: asText(data.memorySeed, fallback.memorySeed).slice(0, 10),\n echoComment: asText(data.echoComment, fallback.echoComment).slice(0, 60),\n };\n}\n\nfunction fallbackAnalyze({ question, answer }) {\n const text = String(answer || \"\");\n const short = text.trim().length < 5;\n const concrete = /雨|光|手|等|抱|名字|咖啡|伞|教室|记得|消失|活下去/.test(text);\n const sacrifice = /消失|活下去|代价|接受|愿意|牺牲/.test(text);\n const avoidance = short || /不知道|随便|都行|无所谓/.test(text);\n const promptText = typeof question === \"string\" ? question : question?.prompt || \"这段回声\";\n return {\n sincerity: clamp((concrete ? 62 : 42) - (avoidance ? 22 : 0), 0, 100),\n dependency: clamp(/陪|抱|靠近|理解|记得|等/.test(text) ? 62 : 36, 0, 100),\n sacrifice: clamp(sacrifice ? 72 : 28, 0, 100),\n avoidance: clamp(avoidance ? 62 : 18, 0, 100),\n control: clamp(/必须|不要|擦掉|证明|只属于/.test(text) ? 58 : 18, 0, 100),\n emotionalAnchorDelta: clamp((concrete ? 14 : 8) + (sacrifice ? 6 : 0) - (avoidance ? 6 : 0), -5, 25),\n anchorTrait: concrete ? \"安静陪伴型\" : \"笨拙守护型\",\n memorySeed: promptText.includes(\"唯一\") ? \"未说出口\" : promptText.includes(\"无助\") ? \"雨夜等待\" : \"黄昏回声\",\n echoComment: concrete ? `这段回声记住了\"${text.slice(0, 12)}\"。` : \"这段回声有些模糊,但仍留下一点温度。\",\n };\n}\n\n/* ─── anchor-reply 相关 ─── */\nfunction normalizeAnchorContext(body) {\n const playerAnswers = body.playerAnswers || summarizePlayerAnswers(body.history || []);\n return {\n sceneId: normalizeSceneId(body.sceneId),\n chapter: body.chapter || \"memory\",\n speaker: body.speaker || \"Anchor\",\n playerMessage: String(body.playerMessage || \"\"),\n currentSceneSummary: body.currentSceneSummary || sceneSummary(body.sceneId),\n recentDialogue: Array.isArray(body.recentDialogue) ? body.recentDialogue.slice(-10) : [],\n anchorProfile: body.anchorProfile || {},\n playerAnswers,\n gameState: body.gameState || extractGameState(body.history || []),\n };\n}\n\nfunction normalizeAnchorReply(data, fallback) {\n const emotions = new Set([\"calm\", \"smile\", \"serious\", \"shy\", \"sad\", \"distant\"]);\n const intents = new Set([\"memory\", \"emotion\", \"daily\", \"question\", \"gibberish\"]);\n return {\n intent: intents.has(data.intent) ? data.intent : fallback.intent || \"emotion\",\n reply: asText(data.reply, fallback.reply),\n emotion: emotions.has(data.emotion) ? data.emotion : fallback.emotion,\n emotionalAnchorDelta: clamp(data.emotionalAnchorDelta, -3, 8),\n why: asText(data.why, \"\"),\n };\n}\n\nfunction validateAnchorReply(result, context) {\n if (!result.reply) return { ok: false, reason: \"reply missing\" };\n const reply = String(result.reply);\n if (result.intent === \"gibberish\") {\n if (reply.length < 4 || reply.length > 40) return { ok: false, reason: \"gibberish reply length invalid\" };\n return { ok: true };\n }\n if (reply.length < 4 || reply.length > 120) return { ok: false, reason: \"reply length invalid\" };\n const forbidden = [\"作为AI\", \"我是一个AI\", \"无法\", \"根据你的输入\", \"系统\", \"审\" + \"判\", \"JSON\"];\n if (forbidden.some((word) => reply.includes(word))) return { ok: false, reason: \"reply contains out-of-role wording\" };\n return { ok: true };\n}\n\nfunction fallbackAnchorReply(context) {\n const sceneId = normalizeSceneId(context.sceneId);\n const message = String(context.playerMessage || \"\").trim();\n if (isGibberish(message)) {\n return {\n intent: \"gibberish\",\n reply: Math.random() > 0.5 ? \"不太明白,你可以再说一次吗?\" : \"我没听懂,但我还在这里。\",\n emotion: \"calm\",\n emotionalAnchorDelta: 0,\n };\n }\n if (sceneId.includes(\"classroom\")) {\n return {\n intent: \"emotion\",\n reply: \"你看,连这张账单都会把 1+1 写成 3。\\n所以你偶尔弄错什么,也真的没关系。\",\n emotion: \"smile\",\n emotionalAnchorDelta: 4,\n };\n }\n if (sceneId.includes(\"cafe\")) {\n return {\n intent: \"emotion\",\n reply: \"雨声有点大。\\n你可以慢慢说,我会靠近一点听。\",\n emotion: \"shy\",\n emotionalAnchorDelta: 4,\n };\n }\n if (sceneId.includes(\"ferrisWheel\")) {\n return {\n intent: \"memory\",\n reply: \"如果这句话现在说不出口,也没关系。\\n我会把这一刻先替你记住。\",\n emotion: \"smile\",\n emotionalAnchorDelta: 5,\n };\n }\n return {\n intent: \"memory\",\n reply: \"记忆像远处的光一样晃了一下。\\n我听见了,只是还没能走到你身边。\",\n emotion: \"distant\",\n emotionalAnchorDelta: 2,\n };\n}\n\nfunction isGibberish(text) {\n const value = String(text || \"\").trim();\n if (!value) return true;\n if (value.length <= 2 && !/[\\u4e00-\\u9fa5a-zA-Z0-9]/.test(value)) return true;\n if (/^[^\\u4e00-\\u9fa5a-zA-Z0-9]{3,}$/.test(value)) return true;\n if (/^[a-zA-Z]{1,3}$/.test(value)) return true;\n if (/(.)\\1{5,}/.test(value)) return true;\n return false;\n}\n\n/* ─── ghost-command 相关 ─── */\nfunction normalizeGhostContext(body) {\n const availableObjects = Array.isArray(body.availableObjects) ? body.availableObjects : [];\n return {\n sceneId: normalizeSceneId(body.sceneId),\n playerCommand: String(body.playerCommand || body.command || \"\"),\n currentSceneSummary: body.currentSceneSummary || sceneSummary(body.sceneId),\n availableObjects,\n memoryCards: body.memoryCards || [],\n gameState: body.gameState || {},\n };\n}\n\nfunction normalizeGhostCommand(data, fallback, context) {\n const outcomes = new Set([\"success\", \"partial\", \"failed\", \"rejected\"]);\n const outcome = outcomes.has(data.outcome) ? data.outcome : fallback.outcome || \"partial\";\n const intensity = [\"light\", \"medium\", \"heavy\"].includes(data.intensity) ? data.intensity : fallback.intensity;\n const targetObject = context.availableObjects.includes(data.targetObject) ? data.targetObject : fallback.targetObject;\n const table = ghostInterferenceTable(context.playerCommand, targetObject, intensity, outcome);\n return {\n outcome,\n understoodAction: asText(data.understoodAction, fallback.understoodAction || inferGhostAction(context.playerCommand, targetObject, intensity)),\n targetObject,\n intensity,\n cost: table.cost,\n fateDriftDelta: table.fateDriftDelta,\n memoryStatus: table.memoryStatus,\n deviationText: asText(data.deviationText, fallback.deviationText),\n resultText: asText(data.resultText, fallback.resultText),\n cinematicText: asText(data.cinematicText, fallback.cinematicText || fallback.resultText),\n };\n}\n\nfunction validateGhostCommand(result, context) {\n if (!context.availableObjects.includes(result.targetObject)) return { ok: false, reason: \"targetObject not available\" };\n if (![\"light\", \"medium\", \"heavy\"].includes(result.intensity)) return { ok: false, reason: \"intensity invalid\" };\n if (![\"success\", \"partial\", \"failed\", \"rejected\"].includes(result.outcome)) return { ok: false, reason: \"outcome invalid\" };\n if (result.cost < 1 || result.cost > 70) return { ok: false, reason: \"cost out of range\" };\n if (result.outcome === \"failed\" || result.outcome === \"rejected\") {\n if (result.fateDriftDelta !== 0 || result.memoryStatus !== \"faded\") return { ok: false, reason: \"non-success outcome must not awaken\" };\n return { ok: true };\n }\n if (result.fateDriftDelta < 1 || result.fateDriftDelta > 45) return { ok: false, reason: \"awakening delta out of range\" };\n if (![\"faded\", \"distorted\", \"erased\"].includes(result.memoryStatus)) return { ok: false, reason: \"memoryStatus invalid\" };\n return { ok: true };\n}\n\nfunction fallbackGhostCommand(context) {\n const command = context.playerCommand;\n const targetObject = inferGhostTarget(command, context.availableObjects);\n const intensity = inferGhostIntensity(command);\n const outcome = intensity === \"light\" ? \"partial\" : \"success\";\n const table = ghostInterferenceTable(command, targetObject, intensity, outcome);\n return {\n outcome,\n understoodAction: inferGhostAction(command, targetObject, intensity),\n targetObject,\n intensity,\n ...table,\n deviationText: buildGhostDeviation(command, targetObject),\n resultText: `过去的男主看着${targetObject},低声说:为什么这里会有这句话?`,\n cinematicText: buildGhostCinematicText(command, targetObject, intensity, outcome),\n };\n}\n\nfunction inferGhostTarget(command, availableObjects) {\n return availableObjects.find((object) => command.includes(object) || command.includes(object.replace(/上的|的/g, \"\"))) || availableObjects[0] || \"记忆锚点\";\n}\n\nfunction inferGhostIntensity(command) {\n if (/不要相信咨询师|轮回是假的|她会被夺走|不要进入咨询室|咨询师是未来|爱意会被夺走/.test(command)) return \"heavy\";\n if (/警告|提醒|告诉|写下|刻下|留下/.test(command)) return \"medium\";\n return \"light\";\n}\n\nfunction inferGhostAction(command, targetObject, intensity) {\n const value = String(command || \"\")\n .replace(targetObject, \"\")\n .replace(/^[^::]*[::]/, \"\")\n .replace(/^(把|将|让|使|给|对|在)/, \"\")\n .trim();\n return value ? value.slice(0, 24) : `${ghostVerb(intensity)}${targetObject}`;\n}\n\nfunction ghostVerb(intensity) {\n if (intensity === \"heavy\") return \"写下完整警告:\";\n if (intensity === \"medium\") return \"留下警告于\";\n return \"轻触\";\n}\n\nfunction ghostInterferenceTable(command, target, intensity, outcome) {\n const text = String(command || \"\");\n if (outcome === \"failed\") return { cost: 6 + Math.min(4, Math.floor(text.length / 18)), fateDriftDelta: 0, memoryStatus: \"faded\" };\n if (outcome === \"rejected\") return { cost: 14 + Math.min(10, Math.floor(text.length / 12)), fateDriftDelta: 0, memoryStatus: \"faded\" };\n const base =\n intensity === \"heavy\"\n ? { cost: 42, fateDriftDelta: 30, memoryStatus: \"erased\" }\n : intensity === \"medium\"\n ? { cost: 26, fateDriftDelta: 18, memoryStatus: \"distorted\" }\n : { cost: 12, fateDriftDelta: 7, memoryStatus: \"faded\" };\n const truthBonus = /不要相信咨询师|轮回是假的|她会被夺走|不要进入咨询室|咨询师是未来|爱意会被夺走/.test(text) ? 8 : 0;\n const clarityBonus = /写下|刻下|留下|提醒|告诉|警告/.test(text) ? 4 : 0;\n const objectCost = /发绳|门票|账单|音乐盒/.test(target) ? 4 : /咖啡杯|车票|粉笔/.test(target) ? 2 : 0;\n const partialPenalty = outcome === \"partial\" ? -4 : 0;\n return {\n cost: Math.max(1, base.cost + truthBonus + clarityBonus + objectCost + Math.min(8, Math.floor(text.length / 18))),\n fateDriftDelta: Math.max(0, base.fateDriftDelta + Math.floor(truthBonus / 2) + Math.floor(clarityBonus / 2) + partialPenalty),\n memoryStatus: base.memoryStatus,\n };\n}\n\nfunction buildGhostDeviation(command, target) {\n if (target.includes(\"账单\")) return \"账单背面浮出一行很浅的字,只有过去的你低头时才会看见。\";\n if (target.includes(\"粉笔\") || target.includes(\"黑板擦\") || target.includes(\"板擦\")) return \"粉尘里浮出一小段不该存在的字,像有人把未来藏进了旧日的黄昏。\";\n if (target.includes(\"伞\") || target.includes(\"车票\") || target.includes(\"咖啡杯\")) return \"雨声停顿了一拍,警告沿着物件边缘慢慢显形。\";\n if (target.includes(\"音乐盒\") || target.includes(\"发绳\") || target.includes(\"门票\")) return \"旋律里混入不属于这一刻的杂音,过去的你因此皱起眉。\";\n if (!command.trim()) return \"这段记忆只泛起了很轻的涟漪。\";\n return \"警告留下了痕迹,但过去仍在犹豫。\";\n}\n\nfunction buildGhostCinematicText(command, target, intensity, outcome) {\n const warning = String(command || \"\")\n .replace(target, \"\")\n .replace(/^[^::]*[::]/, \"\")\n .trim();\n if (outcome === \"failed\") {\n return `你试着把声音压进${target},可这段过去没有听懂。物件安静地留在原位,像一封没有地址的信。过去的男主没有回头,夏央的声音仍沿着原来的轨迹落下。`;\n }\n if (outcome === \"rejected\") {\n return `你的愿望触到${target}的一瞬,整段记忆像被无形的手按住。黄昏把过于直接的因果弹开,只剩一点刺痛留在空气里。过去的男主短暂皱眉,却没有真正听懂。`;\n }\n if (intensity === \"heavy\") {\n return `${target}承住了你的警告,连空气都像被压低了一层。${warning ? `\"${warning.slice(0, 28)}\"` : \"那句警告\"}在过去的男主眼前亮起,他终于停下原本要走向黄昏咨询室的脚步。夏央回过头,像也察觉到命运的齿轮迟疑了一瞬。`;\n }\n if (outcome === \"partial\" || intensity === \"light\") {\n return `${target}上浮出一段很浅的痕迹,又很快被旧日的光擦淡。过去的男主看见了,却只能抓住其中一半意思。他低声问自己为什么会在这里停下,像第一次怀疑这段幸福并不完整。`;\n }\n return `${target}轻轻改变了位置,仿佛有人从记忆背面推了它一下。过去的男主注意到那句警告,指尖停在半空。那一刻,他没有立刻相信咨询师,也没有立刻忘记你的声音。`;\n}\n\n/* ─── final-archive 相关 ─── */\nfunction normalizeArchive(data, fallback) {\n return { archiveText: asText(data.archiveText, fallback.archiveText) };\n}\n\nfunction fallbackArchive({ endingId, gameState, playerAnswers, memoryCards }) {\n const warnings = Array.isArray(memoryCards) ? memoryCards.filter((card) => card.status !== \"intact\").length : 0;\n const awakening = gameState?.fateDrift ?? 0;\n const life = gameState?.life ?? 100;\n const answers = summarizePlayerAnswers(Array.isArray(playerAnswers) ? playerAnswers : []);\n const remembered = answers.uniqueMoment || answers.sacrificeChoice || answers.comfortNeed || answers.fear;\n const trace = Array.isArray(memoryCards) ? memoryCards.find((card) => card.status === \"erased\" || card.status === \"distorted\" || card.status === \"faded\")?.objectName : \"\";\n const playerTrace = remembered ? `你曾说过\"${String(remembered).slice(0, 24)}\",这句话被${trace || \"某段记忆\"}留下。` : `${trace || \"某段记忆\"}替你保存了没有说完的警告。`;\n const endingLine =\n endingId === \"E\"\n ? \"过去的你拒绝进入轮回,夏央终于回头,看见了一直在这里的你。\"\n : endingId === \"D\"\n ? \"轮回被打断了,但你的存在被黎明冲淡,夏央只记得有人替她挡住黄昏。\"\n : endingId === \"C\"\n ? \"你用生命把警告送回过去,轮回裂开,却也让自己的声音变得透明。\"\n : endingId === \"B\"\n ? \"过去的你发现异常,却仍未能完全阻止轮回。黄昏门缝里留下了一道裂光。\"\n : \"警告没有抵达,过去的你走进咨询室。你成为下一任咨询师。\";\n return { archiveText: `终章归档:觉醒度 ${awakening},生命值 ${life},留下警告 ${warnings} 处。${playerTrace}${endingLine}` };\n}\n\n/* ─── 通用场景辅助 ─── */\nfunction normalizeSceneId(sceneId) {\n if (sceneId === \"ferris-wheel\") return \"ferrisWheel\";\n return String(sceneId || \"classroom\");\n}\n\nfunction sceneSummary(sceneId) {\n const id = normalizeSceneId(sceneId);\n if (id.includes(\"classroom\")) return \"夕阳空教室,讲台上有算错的账单、半截粉笔、旧黑板擦,主题是被理解。\";\n if (id.includes(\"cafe\")) return \"雨夜咖啡馆,玻璃窗外是街灯和雨,桌上有冷掉的咖啡杯,门边有坏掉的透明伞,还有折皱车票,主题是被偏爱。\";\n if (id.includes(\"ferrisWheel\")) return \"夜晚摩天轮接近最高点,城市灯火在脚下,掌心有音乐盒发条、门票存根与蓝色发绳,主题是被选择。\";\n return \"幽灵阶段的记忆场景,颜色低饱和,Anchor 在远处,过去正在被轻轻改写。\";\n}\n\nfunction summarizePlayerAnswers(history) {\n const answers = history.filter((item) => item && item.questionId && item.answer);\n return {\n fear: answers.find((item) => item.questionId === \"q1\")?.answer || \"\",\n comfortNeed: answers.find((item) => item.questionId === \"q2\")?.answer || \"\",\n uniqueMoment: answers.find((item) => item.questionId === \"q3\")?.answer || \"\",\n sacrificeChoice: answers.find((item) => item.questionId === \"q4\")?.answer || \"\",\n };\n}\n\nfunction extractGameState(history) {\n return history.find((item) => item && typeof item === \"object\" && \"emotionalAnchor\" in item) || {};\n}\n\n/* ─── 启动 ─── */\napp.listen(PORT, () => {\n console.log(\"Mobius AI CloudBase HTTP function started\");\n console.log(`PORT: ${PORT}`);\n console.log(`DEEPSEEK_API_KEY loaded: ${DEEPSEEK_API_KEY ? \"yes\" : \"no\"}`);\n console.log(`DEEPSEEK_BASE_URL: ${DEEPSEEK_BASE_URL}`);\n console.log(`DEEPSEEK_MODEL: ${DEEPSEEK_MODEL}`);\n console.log(`ALLOWED_ORIGIN: ${ALLOWED_ORIGIN}`);\n console.log(`Endpoints:`);\n console.log(` POST /api/analyze-answer`);\n console.log(` POST /api/anchor-reply`);\n console.log(` POST /api/ghost-command`);\n console.log(` POST /api/final-archive`);\n});\n"}}}}