Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/486-king_mobious.json
T

1 line
55 KiB
JSON
Raw 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": "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"}}}}