-
-
{progress}%
+
+
+
+
灵镜正在为你生成报告
+
{MESSAGES[idx]}
+
已保存你的对话,可放心离开。
+
+ {timedOut && !error ? (
+
+
+
+
+ ) : null}
+
+ {error ? (
+
+
{error}
+
+
+
+
+
+ ) : null}
-
+
);
}
diff --git a/components/Starfield.jsx b/components/Starfield.jsx
new file mode 100644
index 0000000..b72a9ef
--- /dev/null
+++ b/components/Starfield.jsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { useEffect, useRef } from 'react';
+
+export default function Starfield({ className = '', animated = false }) {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ let raf = null;
+ let width = 0;
+ let height = 0;
+ let stars = [];
+
+ const createStars = () => {
+ const count = Math.max(90, Math.floor((width * height) / 16000));
+ stars = Array.from({ length: count }, () => ({
+ x: Math.random() * width,
+ y: Math.random() * height,
+ r: Math.random() * 1.4 + 0.4,
+ a: Math.random() * 0.7 + 0.2,
+ v: Math.random() * 0.08 + 0.02,
+ }));
+ };
+
+ const resize = () => {
+ const dpr = window.devicePixelRatio || 1;
+ width = canvas.clientWidth;
+ height = canvas.clientHeight;
+ canvas.width = Math.floor(width * dpr);
+ canvas.height = Math.floor(height * dpr);
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ createStars();
+ draw();
+ };
+
+ const draw = () => {
+ ctx.clearRect(0, 0, width, height);
+ const bg = ctx.createLinearGradient(0, 0, 0, height);
+ bg.addColorStop(0, '#0b0720');
+ bg.addColorStop(1, '#05030f');
+ ctx.fillStyle = bg;
+ ctx.fillRect(0, 0, width, height);
+
+ for (const s of stars) {
+ ctx.beginPath();
+ ctx.fillStyle = `rgba(220,225,255,${s.a})`;
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ };
+
+ const tick = () => {
+ for (const s of stars) {
+ s.a += (Math.random() - 0.5) * s.v;
+ if (s.a < 0.15) s.a = 0.15;
+ if (s.a > 0.95) s.a = 0.95;
+ }
+ draw();
+ raf = window.requestAnimationFrame(tick);
+ };
+
+ resize();
+ window.addEventListener('resize', resize);
+ if (animated) raf = window.requestAnimationFrame(tick);
+
+ return () => {
+ window.removeEventListener('resize', resize);
+ if (raf) window.cancelAnimationFrame(raf);
+ };
+ }, [animated]);
+
+ return
;
+}
diff --git a/server.js b/server.js
index 439f617..b3df160 100644
--- a/server.js
+++ b/server.js
@@ -1,5 +1,4 @@
const fs = require('fs');
-const path = require('path');
const express = require('express');
const next = require('next');
const dotenv = require('dotenv');
@@ -15,49 +14,99 @@ const sessions = new Map();
const opening = '开始前,我想先简单认识你一下。就像朋友聊天,想到什么说什么就行,没有标准答案。咱们先从几个轻松的小问题开始,可以吗?';
-const questionBank = [
- '你今年大概在哪个年龄段?比如20岁以下、20-29、30-39这样。',
- '你是男生还是女生呀?',
- '你现在主要在做什么?上学、上班、自己做事,还是在休息调整?',
- '最近这几天,有没有一件小事让你心情变好?',
- '如果只说一件事,你现在最发愁的是什么?',
- '最近一周,你心情大多数时候是轻松、一般,还是有点压着?',
- '你更容易从一个人待着恢复,还是和人聊天后恢复?',
- '忙完一天后,你最想做什么来放松?',
- '什么场景最容易让你觉得被掏空?',
- '你做什么事时最容易忘记时间?',
- '最近一次你状态特别好的那天,发生了什么?',
- '最近一次你状态特别差的那天,发生了什么?',
- '遇到新任务,你习惯先列计划,还是先做再调整?',
- '你做决定时更看重稳妥还是可能性更大?',
- '你拖延通常是因为不会做、怕做错,还是没兴趣?',
- '有压力时你会先自己扛,还是找人聊聊?',
- '你更喜欢一次做一件事,还是好几件事一起推?',
- '过去一个月,你最满意的一次决定是什么?',
- '和亲近的人有分歧时,你更常沉默、解释,还是直接顶回去?',
- '你会不会因为怕别人失望,就先答应再后悔?',
- '别人一句话让你不舒服时,你通常会说出来吗?',
- '最近一次你明明很累但还在撑的场景是什么?',
- '你觉得自己做得不够好的念头,最近常出现吗?',
- '如果给现在的自己一句鼓励,你最想说什么?',
- '接下来3个月,你最想改善的一件事是什么?',
- '你觉得最大的拦路点是什么?',
- '如果只做一个小动作,哪件事你愿意这周就开始?',
- '谁能给你一点支持?你愿不愿意主动开口?',
- '你希望我给你稳一点的方案还是冲一点的方案?',
- '今天聊完,你最想先记住的一句话是什么?',
-];
+const zodiacProfiles = {
+ aries: { name: '白羊座', fusion: '你身上有很直接的行动火花,关键是把冲劲变成稳定推进。' },
+ taurus: { name: '金牛座', fusion: '你重视确定感,先小步验证会比一直等待更有力量。' },
+ gemini: { name: '双子座', fusion: '你思路灵活,先定主线后扩展,效率会明显提升。' },
+ cancer: { name: '巨蟹座', fusion: '你情感细腻,先稳住自己,才能更好照顾关系和目标。' },
+ leo: { name: '狮子座', fusion: '你有担当感,外在发力的同时要记得给内在补能。' },
+ virgo: { name: '处女座', fusion: '你重视质量,先完成草稿再优化,比等待完美更快。' },
+ libra: { name: '天秤座', fusion: '你擅长平衡,关键在于练习有边界的取舍。' },
+ scorpio: { name: '天蝎座', fusion: '你并不是慢,而是要确认值不值得投入;一旦启动就很有穿透力。' },
+ sagittarius: { name: '射手座', fusion: '你有探索力,把热情绑定阶段目标会更稳。' },
+ capricorn: { name: '摩羯座', fusion: '你擅长长期推进,记得把恢复机制纳入执行系统。' },
+ aquarius: { name: '水瓶座', fusion: '你的独立视角很珍贵,落地后才会变成现实影响。' },
+ pisces: { name: '双鱼座', fusion: '你的感受力是天赋,先稳情绪再行动会更顺。' },
+};
+
+function detectZodiac(month, day) {
+ if (!month || !day) return null;
+ const md = month * 100 + day;
+ if (md >= 321 && md <= 419) return 'aries';
+ if (md >= 420 && md <= 520) return 'taurus';
+ if (md >= 521 && md <= 621) return 'gemini';
+ if (md >= 622 && md <= 722) return 'cancer';
+ if (md >= 723 && md <= 822) return 'leo';
+ if (md >= 823 && md <= 922) return 'virgo';
+ if (md >= 923 && md <= 1023) return 'libra';
+ if (md >= 1024 && md <= 1122) return 'scorpio';
+ if (md >= 1123 && md <= 1221) return 'sagittarius';
+ if ((md >= 1222 && md <= 1231) || (md >= 101 && md <= 119)) return 'capricorn';
+ if (md >= 120 && md <= 218) return 'aquarius';
+ if (md >= 219 && md <= 320) return 'pisces';
+ return null;
+}
+
+function extractBirthday(text) {
+ if (!text) return null;
+ const cleaned = String(text).replace(/\s+/g, '');
+ let m = cleaned.match(/(1[0-2]|[1-9])月([0-2]?\d|3[01])(?:日|号)?/);
+ if (!m) m = cleaned.match(/(1[0-2]|0?[1-9])[\/-](3[01]|[12]?\d)/);
+ if (!m) return null;
+ const month = Number(m[1]);
+ const day = Number(m[2]);
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
+ return { month, day };
+}
+
+function sanitizeReply(text) {
+ if (!text) return '';
+ // 移除所有控制字符(包括Unicode控制字符),保留普通换行和空格
+ return text
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // ASCII控制字符
+ .replace(/[\u0080-\u009F]/g, '') // C1控制字符
+ .replace(/\u0000/g, '') // null字节
+ .replace(/[\uFFF0-\uFFFF]/g, '') // Unicode特殊符
+ .replace(/\n{3,}/g, '\n\n') // 多余换行压缩
+ .trim();
+}
+
+function parseJSONFromText(text) {
+ if (!text) return null;
+ try {
+ return JSON.parse(text);
+ } catch {
+ const block = text.match(/```json\s*([\s\S]*?)```/i);
+ if (block) {
+ try {
+ return JSON.parse(block[1]);
+ } catch {
+ return null;
+ }
+ }
+ const start = text.indexOf('{');
+ const end = text.lastIndexOf('}');
+ if (start >= 0 && end > start) {
+ try {
+ return JSON.parse(text.slice(start, end + 1));
+ } catch {
+ return null;
+ }
+ }
+ return null;
+ }
+}
function loadSystemPrompt() {
const p = '/root/Projects/dochub-next/content-private/project-lingjing/system-prompt-v1.md';
try {
- return fs.readFileSync(p, 'utf8').slice(0, 8000);
+ return fs.readFileSync(p, 'utf8').slice(0, 12000);
} catch {
return '你是灵镜,一个温暖、自然的一问一答助手。';
}
}
-async function callAI(messages) {
+async function callYcapis(input, maxOutputTokens = 500) {
const key = process.env.YCAPIS_API_KEY;
if (!key) return null;
try {
@@ -69,60 +118,184 @@ async function callAI(messages) {
},
body: JSON.stringify({
model: 'gpt-5.3-codex',
- input: messages.map(m => ({ role: m.role, content: m.content })),
- max_output_tokens: 300,
+ input,
+ max_output_tokens: maxOutputTokens,
}),
});
if (!res.ok) return null;
const data = await res.json();
- return data?.output?.[0]?.content?.[0]?.text || null;
+ const raw = data?.output?.[0]?.content?.[0]?.text || null;
+ return raw ? sanitizeReply(raw) : null;
} catch {
return null;
}
}
-function previewReport(session) {
+function fallbackReport(session) {
+ const zodiacSign = session?.birthday ? detectZodiac(session.birthday.month, session.birthday.day) : null;
return {
- version: 'free_v1',
- reportType: 'free',
- userSnapshot: { summary: '你当前状态是:有改变意愿,但需要更聚焦的行动节奏。' },
- highlight: { title: '你的亮点', content: '你能清楚表达自己的真实感受,这会让你更快找到方向。' },
- evidence: [{ quote: session.answers[0] || '(示例)', meaning: '你愿意打开自己,这是成长的起点。' }],
- currentBlock: { title: '当前阻碍', content: '你容易想太多再行动,导致动力被消耗。' },
- oneActionThisWeek: { title: '本周动作', content: '选一件最小任务,限定30分钟,今天就开始。' },
- teaser: { lockedHint: '完整版会给你稳妥/成长/冲刺三条路线和30天动作计划。', upgradeText: '继续查看完整报告' },
+ soulTags: ['敏感但清醒', '想改变', '有行动潜力'],
+ currentState: {
+ title: '在转折前的蓄力期',
+ summary: '你已经明确感受到当下状态需要调整,也愿意正视问题。现在最关键的不是再想更多,而是把注意力集中到一个可执行的小行动上,用真实动作带动状态回升。',
+ intensity: 68,
+ },
+ fiveDim: {
+ scores: { xinli: 66, xingli: 58, ganzhi: 74, dongjian: 70, dingli: 62 },
+ evidence: {
+ xinli: ['「用户说:我最近总是拖着。」'],
+ xingli: ['「用户说:明明知道该动了但就是不想。」'],
+ ganzhi: ['「用户说:我会在意别人怎么看。」'],
+ dongjian: ['「用户说:我知道问题不只是懒。」'],
+ dingli: ['「用户说:我还是想把这件事做好。」'],
+ },
+ interpretation: '你有不错的洞察和感受力,当前主要卡点在启动与持续。只要先把行动门槛降下来,整体状态会明显改善。',
+ },
+ personalityReading: [
+ { point: '你并不逃避现实', quote: '「用户说:我知道该动了。」', explain: '你对问题是看见的,这意味着你具备修正能力。' },
+ { point: '你容易被压力拖慢', quote: '「用户说:就是不想动。」', explain: '不是没能力,而是启动前消耗了太多心理能量。' },
+ { point: '你仍有稳定的目标感', quote: '「用户说:还是想做好。」', explain: '内在方向感还在,只需要更轻的执行路径。' },
+ ],
+ potentialBlindspots: {
+ potentials: ['你能快速发现问题本质,适合做复盘和策略规划。', '你对关系和情绪变化敏感,沟通潜力高。'],
+ blindspots: ['你会在开始前反复确认,导致行动延迟。', '你容易把阶段波动当成长期失败。'],
+ },
+ mbti: {
+ type: 'INFP',
+ typeName: '调停者',
+ description: '从你的表达看,你更像先在心里确认价值感,再进入行动。你重视感受和意义,一旦目标与你的内在认同对齐,执行会明显变稳。',
+ },
+ zodiac: zodiacSign ? { sign: zodiacSign, name: zodiacProfiles[zodiacSign].name, fusionText: zodiacProfiles[zodiacSign].fusion } : null,
+ presentSignal: {
+ signalName: '启动阻力正在放大',
+ urgency: 'high',
+ trigger: '当你连续两天都在想“再等等”时',
+ meaning: '这说明你已经进入了“想做但不敢开头”的循环,越拖越重。',
+ riskIfMissed: '如果继续拖延,你会把短期卡点误判成长期无力。',
+ },
+ pivotAction: {
+ onePivot: '今天只做一件15分钟就能完成的小任务,并立即收尾。',
+ threeStarts: ['把目标拆成今天版本,不超过3步。', '设置固定开工时间,先做再评估。', '完成后写一句复盘:我做到了什么。'],
+ },
+ closingLine: '你真正缺的不是能力,而是一次不等完美就开始的动作。',
};
}
-function fullReport(session) {
- return {
- version: 'pro_v1',
- reportType: 'pro',
- profileSummary: {
- oneLineDiagnosis: '你是行动意愿强、但容易被多目标分散的人。先聚焦一个主线,会明显提速。',
- currentStage: '调整期',
+async function generateReportForSession(sessionId) {
+ const session = sessions.get(sessionId);
+ if (!session) return null;
+ if (session.reportStatus === 'generating') return null;
+ if (session.report) return session.report;
+
+ session.reportStatus = 'generating';
+ session.reportError = '';
+
+ const birthday = session.birthday || null;
+ const transcript = session.answers.map((a, i) => ({ q: session.questions[i] || `第${i + 1}题`, a }));
+
+ const reportSystemPrompt = `你是灵镜报告生成器。根据用户的完整对话记录,生成一份深度灵魂洞察报告。
+
+要求:
+1. 必须返回严格的JSON格式,不要有任何多余文字
+2. 每个结论必须引用用户原话,格式:「用户说:……」
+3. 用大白话,禁止专业术语(心流、人格原型、依恋模式等)
+4. 五维评分直接输出0-100整数
+5. MBTI根据对话综合判断,不要死套测试题
+6. 如果没有生日信息,zodiac字段必须是null
+
+返回以下JSON结构:
+{
+ "soulTags": ["标签1", "标签2", "标签3"],
+ "currentState": {
+ "title": "当下状态标题",
+ "summary": "150-200字描述",
+ "intensity": 65
+ },
+ "fiveDim": {
+ "scores": { "xinli": 72, "xingli": 58, "ganzhi": 81, "dongjian": 69, "dingli": 74 },
+ "evidence": {
+ "xinli": ["「用户说:……」"],
+ "xingli": ["「用户说:……」"],
+ "ganzhi": ["「用户说:……」"],
+ "dongjian": ["「用户说:……」"],
+ "dingli": ["「用户说:……」"]
},
- strengths: [
- { name: '自我觉察', description: '你能描述自己的状态变化,便于快速纠偏。' },
- { name: '现实感', description: '你会考虑真实条件,不容易盲目冲动。' },
- { name: '执行意愿', description: '你愿意从小步开始,这对长期成长很关键。' },
- ],
- routes: [
- { routeType: 'stable', title: '稳妥线', fitReason: '先稳住节奏与作息,每周完成固定小目标。' },
- { routeType: 'growth', title: '成长线', fitReason: '围绕一个能力做30天刻意练习,建立优势杠杆。' },
- { routeType: 'sprint', title: '冲刺线', fitReason: '聚焦一件高价值目标,用短周期冲刺验证上限。' },
- ],
- sourceAnswers: session.answers,
+ "interpretation": "100-150字整体解读"
+ },
+ "personalityReading": [
+ { "point": "核心特点", "quote": "「用户说:……」", "explain": "50-80字解释" },
+ { "point": "核心特点2", "quote": "「用户说:……」", "explain": "50-80字解释" },
+ { "point": "核心特点3", "quote": "「用户说:……」", "explain": "50-80字解释" }
+ ],
+ "potentialBlindspots": {
+ "potentials": ["潜能1(50字)", "潜能2(50字)"],
+ "blindspots": ["盲区1(50字)", "盲区2(50字)"]
+ },
+ "mbti": {
+ "type": "INFP",
+ "typeName": "调停者",
+ "description": "150-200字基于对话的MBTI解读"
+ },
+ "zodiac": null,
+ "presentSignal": {
+ "signalName": "信号名称",
+ "urgency": "high",
+ "trigger": "当……出现时",
+ "meaning": "这意味着……(80字)",
+ "riskIfMissed": "如果错过……(50字)"
+ },
+ "pivotAction": {
+ "onePivot": "今天就能做的一个主行动",
+ "threeStarts": ["第一件事", "第二件事", "第三件事"]
+ },
+ "closingLine": "一句话总结,适合截图分享"
+}`;
+
+ const userInput = {
+ birthday,
+ zodiacHint: birthday ? detectZodiac(birthday.month, birthday.day) : null,
+ transcript,
};
+
+ const text = await callYcapis([
+ { role: 'system', content: reportSystemPrompt },
+ { role: 'user', content: JSON.stringify(userInput) },
+ ], 4000);
+
+ let parsed = parseJSONFromText(text);
+ if (!parsed) parsed = fallbackReport(session);
+
+ if (birthday) {
+ const sign = detectZodiac(birthday.month, birthday.day);
+ if (sign && (!parsed.zodiac || parsed.zodiac === null)) {
+ parsed.zodiac = { sign, name: zodiacProfiles[sign].name, fusionText: zodiacProfiles[sign].fusion };
+ }
+ } else {
+ parsed.zodiac = null;
+ }
+
+ session.report = parsed;
+ session.reportStatus = 'done';
+ return parsed;
}
app.prepare().then(() => {
const server = express();
- server.use(express.json({ limit: '1mb' }));
+ server.use(express.json({ limit: '2mb' }));
server.post('/api/session/new', (req, res) => {
const sessionId = `lgj_${Date.now().toString(36)}`;
- sessions.set(sessionId, { index: 0, answers: [], questions: [], skips: 0, prompt: loadSystemPrompt() });
+ sessions.set(sessionId, {
+ index: 0,
+ answers: [],
+ questions: [],
+ skips: 0,
+ prompt: loadSystemPrompt(),
+ report: null,
+ reportStatus: 'not_started',
+ reportError: '',
+ birthday: null,
+ });
res.json({ sessionId, opening, total: 30 });
});
@@ -134,46 +307,131 @@ app.prepare().then(() => {
const userAnswer = (answer || '').trim();
if (userAnswer) {
s.answers.push(userAnswer);
+ const b = extractBirthday(userAnswer);
+ if (b) s.birthday = b;
s.skips = userAnswer === '跳过' ? s.skips + 1 : 0;
s.index += 1;
}
const done = s.index >= 30;
if (done) {
- return res.json({ done: true, reply: '你已经完成全部对话。接下来我会为你生成专属灵镜报告。' });
+ if (s.reportStatus !== 'generating' && !s.report) {
+ generateReportForSession(sessionId).catch((err) => {
+ s.reportStatus = 'error';
+ s.reportError = err?.message || 'generate_failed';
+ });
+ }
+ return res.json({ done: true, reply: '你已经完成全部对话。接下来我会为你生成专属灵镜报告。', sessionId });
}
const progress = s.index > 0 && s.index % 6 === 0 ? `(你已经完成第${Math.ceil(s.index / 6)}/5阶段了。)` : '';
+ const birthdayHint = s.index === 27 ? '这一题请自然地加一句:顺便问一下,你是几月几号生的?' : '';
- // 全部交给AI动态生成,不用硬编码题库
- const ds = await callAI([
+ const aiReply = await callYcapis([
{ role: 'system', content: s.prompt },
{ role: 'assistant', content: opening },
...s.answers.flatMap((a, i) => [
{ role: 'assistant', content: s.questions[i] || '' },
- { role: 'user', content: a }
- ]).filter(m => m.content),
- { role: 'user', content: `[系统提示] 用户刚才回答了:"${userAnswer}"。${progress}请用温暖口语接住他的回答,然后继续问下一个问题。注意:只问一个问题,用大白话,不用专业术语。当前已问第${s.index}题,共30题。` }
- ]);
+ { role: 'user', content: a },
+ ]).filter((m) => m.content),
+ { role: 'user', content: `[系统提示] 用户刚才回答了:"${userAnswer}"。${progress}请先接住用户,再继续下一问。只问一个问题,口语化、大白话。当前第${s.index + 1}题/30。${birthdayHint}` },
+ ], 300);
- const aiReply = ds || '嗯,我听到了。我们继续,你现在主要在做什么?上学、上班、还是在休整?';
+ const fallback = s.index === 27
+ ? '我听懂了。顺便问一下,你是几月几号生的?'
+ : '嗯,我听到了。我们继续,最近这几天有什么小事让你心情变好?';
- // 记录AI问的问题
- s.questions.push(aiReply);
+ const nextQuestion = aiReply || fallback;
+ s.questions.push(nextQuestion);
- return res.json({ done: false, reply: aiReply, index: s.index + 1, total: 30 });
+ return res.json({ done: false, reply: sanitizeReply(nextQuestion), index: s.index + 1, total: 30, sessionId });
});
- server.get('/api/report/preview', (req, res) => {
- const s = sessions.get(req.query.sessionId);
- if (!s) return res.json(previewReport({ answers: [] }));
- return res.json(previewReport(s));
+ // 测试用:直接返回模拟报告数据
+ server.get('/api/report/mock', (req, res) => {
+ const mockReport = {
+ soulTags: ['慢热型行动者', '内驱力深藏', '边界感极强', '高敏感思考者'],
+ currentState: {
+ title: '蓄力期——你正在等一个真正值得出手的时机',
+ summary: '你现在处在一种"心里明白,但脚还没动"的状态。不是没有想法,而是对自己的要求很高,不愿意随便交差。从你说的话里能感觉到,你有很清晰的内在标准,但这个标准有时候也在拖住你。你不是懒,你是在等一个"感觉对了"的信号——问题是,这个信号可能需要你先动起来才会出现。',
+ intensity: 68
+ },
+ fiveDim: {
+ scores: { xinli: 74, xingli: 52, ganzhi: 83, dongjian: 71, dingli: 78 },
+ evidence: {
+ xinli: ['「用户说:我情绪上还好,就是有点麻木」'],
+ xingli: ['「用户说:明明知道该动了但就是不想」'],
+ ganzhi: ['「用户说:我很容易感受到别人的情绪变化」'],
+ dongjian: ['「用户说:我总是能看到别人看不到的问题」'],
+ dingli: ['「用户说:我很少轻易改变一个已经想清楚的决定」']
+ },
+ interpretation: '你的感知力和定力是真正的优势——你能读到别人读不到的信号,也能在别人动摇时保持方向。行力偏低不是能力问题,而是启动门槛较高,需要找到"值得动"的理由。'
+ },
+ personalityReading: [
+ { point: '你是先想清楚再动的人,但"想清楚"这个标准有时候没有终点', quote: '「用户说:我需要把事情在脑子里过一遍才能开始」', explain: '这种习惯让你少走弯路,但也容易让你卡在起点。真正的清晰往往是行动带来的,不是想出来的。' },
+ { point: '你对"真实"有很高要求,不愿意做表面文章', quote: '「用户说:我不喜欢应付,要么就好好做,要么就不做」', explain: '这是你的核心特质,也是别人信任你的原因。但对自己也需要留一点余地——不是所有事都值得全力以赴。' },
+ { point: '你的感受比你表达出来的更丰富', quote: '「用户说:有些话说出来也没用,就算了」', explain: '你习惯内化而不是外化,这让你看起来比实际上更平静。找到一两个可以真正倾诉的出口,会帮你减轻很多无形的重量。' }
+ ],
+ potentialBlindspots: {
+ potentials: ['系统性思维:你天然会从整体视角看问题,这在需要策略规划的场景里价值极高', '高信任感:你说话算数,别人感受得到,这是很稀缺的特质'],
+ blindspots: ['完美主义陷阱:你对质量的追求有时候会变成推迟启动的理由', '情绪内化过度:你处理内心感受的效率比表达的效率高,但压着不说会积累']
+ },
+ mbti: {
+ type: 'INFP',
+ typeName: '调停者',
+ description: '从你的回答里,我看到一个典型INFP的样子:内心有很清晰的价值标准,对"意义感"极度敏感,不愿意做自己不认同的事。你的行动力不强不是因为懒,而是因为你在等一件真正值得投入的事。INFP最大的挑战是把内在的丰富世界和外部现实连接起来——而你现在正在经历这个过程。'
+ },
+ zodiac: {
+ sign: 'capricorn',
+ name: '摩羯座',
+ symbol: '♑',
+ coreTraits: '你目标感强,能长期扛压并稳定推进。',
+ blindSpot: '你容易把效率放在感受前面,久了会内耗。',
+ lingjingLine: '你会登山,也要记得补氧。',
+ supportKey: '在执行系统里加入固定的恢复机制。',
+ userOverlay: '你说"明明知道该动了但就是不想",这恰恰是摩羯特有的状态——不是没有方向,而是对结果的要求太高,在没有把握之前宁可按住自己。摩羯的你,一旦认定方向就能走得很远,但你现在需要的不是再想更久,而是给自己一个"够好即可出发"的许可。',
+ fusionText: '摩羯的你,天生有一种扛得住的韧性——别人撑不下去的时候,你还在。你说"明明知道该动了但就是不想",这不是拖延,这是摩羯式的自我保护:不确定就不动,动了就要做好。这种严格其实是你的优势,但在当下这个阶段,它需要稍微松一松。你不需要等到完全准备好,摩羯登山不是一步到顶,而是一步一步往上走,补氧再走。'
+ },
+ presentSignal: {
+ signalName: '行动窗口正在开启',
+ urgency: 'high',
+ trigger: '当你发现自己开始主动搜索某件事、反复想某个方向的时候',
+ meaning: '那不是随机的念头,那是你内在已经准备好了的信号。你的"想清楚"过程已经完成了很大一部分,现在缺的只是第一步的启动。',
+ riskIfMissed: '再等下去,这个窗口不会等你——它会变成"当初要是做就好了"的遗憾。'
+ },
+ pivotAction: {
+ onePivot: '今天找出一件你已经想了超过两周但还没开始的事,给它设定一个30分钟的启动时间',
+ threeStarts: ['把脑子里的那件事写下来,哪怕一句话', '找到它最小的第一步是什么,不超过15分钟能完成的', '告诉一个人你要开始做这件事(说出来会形成承诺感)']
+ },
+ closingLine: '你不是还没准备好,你只是还没决定出发。'
+ };
+ res.json({ status: 'done', report: mockReport });
});
- server.get('/api/report/full', (req, res) => {
+ server.post('/api/report', async (req, res) => {
+ const { sessionId } = req.body || {};
+ const s = sessions.get(sessionId);
+ if (!s) return res.status(404).json({ error: 'session_not_found' });
+ if (s.report) return res.json({ ok: true, status: 'done', report: s.report });
+ if (s.reportStatus === 'generating') return res.json({ ok: true, status: 'generating' });
+
+ try {
+ const report = await generateReportForSession(sessionId);
+ return res.json({ ok: true, status: 'done', report });
+ } catch (e) {
+ s.reportStatus = 'error';
+ s.reportError = e?.message || 'generate_failed';
+ return res.status(500).json({ ok: false, status: 'error', error: s.reportError });
+ }
+ });
+
+ server.get('/api/report', (req, res) => {
const s = sessions.get(req.query.sessionId);
- if (!s) return res.json(fullReport({ answers: [] }));
- return res.json(fullReport(s));
+ if (!s) return res.status(404).json({ status: 'error', error: 'session_not_found' });
+ if (s.report) return res.json({ status: 'done', report: s.report });
+ if (s.reportStatus === 'generating') return res.json({ status: 'generating' });
+ if (s.reportStatus === 'error') return res.status(500).json({ status: 'error', error: s.reportError || 'generate_failed' });
+ return res.json({ status: 'not_started' });
});
server.use((req, res) => handle(req, res));