const fs = require('fs'); const path = require('path'); const express = require('express'); const next = require('next'); const dotenv = require('dotenv'); dotenv.config(); const dev = process.env.NODE_ENV !== 'production'; const port = Number(process.env.PORT || 3200); const app = next({ dev, dir: __dirname }); const handle = app.getRequestHandler(); const sessions = new Map(); const opening = '开始前,我想先简单认识你一下。就像朋友聊天,想到什么说什么就行,没有标准答案。咱们先从几个轻松的小问题开始,可以吗?'; const questionBank = [ '你今年大概在哪个年龄段?比如20岁以下、20-29、30-39这样。', '你平时怎么称呼自己更舒服?', '你现在主要在做什么?上学、上班、自己做事,还是在休息调整?', '最近这几天,有没有一件小事让你心情变好?', '如果只说一件事,你现在最发愁的是什么?', '最近一周,你心情大多数时候是轻松、一般,还是有点压着?', '你更容易从一个人待着恢复,还是和人聊天后恢复?', '忙完一天后,你最想做什么来放松?', '什么场景最容易让你觉得被掏空?', '你做什么事时最容易忘记时间?', '最近一次你状态特别好的那天,发生了什么?', '最近一次你状态特别差的那天,发生了什么?', '遇到新任务,你习惯先列计划,还是先做再调整?', '你做决定时更看重稳妥还是可能性更大?', '你拖延通常是因为不会做、怕做错,还是没兴趣?', '有压力时你会先自己扛,还是找人聊聊?', '你更喜欢一次做一件事,还是好几件事一起推?', '过去一个月,你最满意的一次决定是什么?', '和亲近的人有分歧时,你更常沉默、解释,还是直接顶回去?', '你会不会因为怕别人失望,就先答应再后悔?', '别人一句话让你不舒服时,你通常会说出来吗?', '最近一次你明明很累但还在撑的场景是什么?', '你觉得自己做得不够好的念头,最近常出现吗?', '如果给现在的自己一句鼓励,你最想说什么?', '接下来3个月,你最想改善的一件事是什么?', '你觉得最大的拦路点是什么?', '如果只做一个小动作,哪件事你愿意这周就开始?', '谁能给你一点支持?你愿不愿意主动开口?', '你希望我给你稳一点的方案还是冲一点的方案?', '今天聊完,你最想先记住的一句话是什么?', ]; 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); } catch { return '你是灵镜,一个温暖、自然的一问一答助手。'; } } async function callAI(messages) { const key = process.env.YCAPIS_API_KEY; if (!key) return null; const res = await fetch('https://ycapis.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}`, }, body: JSON.stringify({ model: 'gpt-5.3-codex', temperature: 0.7, messages, }), }); if (!res.ok) return null; const data = await res.json(); return data?.choices?.[0]?.message?.content || null; } function previewReport(session) { 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: '继续查看完整报告' }, }; } function fullReport(session) { return { version: 'pro_v1', reportType: 'pro', profileSummary: { oneLineDiagnosis: '你是行动意愿强、但容易被多目标分散的人。先聚焦一个主线,会明显提速。', currentStage: '调整期', }, strengths: [ { name: '自我觉察', description: '你能描述自己的状态变化,便于快速纠偏。' }, { name: '现实感', description: '你会考虑真实条件,不容易盲目冲动。' }, { name: '执行意愿', description: '你愿意从小步开始,这对长期成长很关键。' }, ], routes: [ { routeType: 'stable', title: '稳妥线', fitReason: '先稳住节奏与作息,每周完成固定小目标。' }, { routeType: 'growth', title: '成长线', fitReason: '围绕一个能力做30天刻意练习,建立优势杠杆。' }, { routeType: 'sprint', title: '冲刺线', fitReason: '聚焦一件高价值目标,用短周期冲刺验证上限。' }, ], sourceAnswers: session.answers, }; } app.prepare().then(() => { const server = express(); server.use(express.json({ limit: '1mb' })); server.post('/api/session/new', (req, res) => { const sessionId = `lgj_${Date.now().toString(36)}`; sessions.set(sessionId, { index: 0, answers: [], skips: 0, prompt: loadSystemPrompt() }); res.json({ sessionId, opening, total: 30 }); }); server.post('/api/chat', async (req, res) => { const { sessionId, answer } = req.body || {}; const s = sessions.get(sessionId); if (!s) return res.status(404).json({ error: 'session_not_found' }); const userAnswer = (answer || '').trim(); if (userAnswer) { s.answers.push(userAnswer); s.skips = userAnswer === '跳过' ? s.skips + 1 : 0; s.index += 1; } const done = s.index >= questionBank.length; if (done) { return res.json({ done: true, reply: '你已经完成全部对话。接下来我会为你生成专属灵镜报告。' }); } const progress = s.index > 0 && s.index % 5 === 0 ? `你已经完成第${Math.ceil(s.index / 6)}/5阶段了。` : ''; const bridge = userAnswer ? '我听懂了。' : '我们继续。'; const nextQ = questionBank[s.index]; let aiReply = `${bridge}${progress ? ` ${progress}` : ''} ${nextQ}`.trim(); const ds = await callAI([ { role: 'system', content: s.prompt }, { role: 'assistant', content: opening }, ...s.answers.slice(-8).map((a) => ({ role: 'user', content: a })), { role: 'user', content: `请按规则继续下一问。当前问题建议:${nextQ}` }, ]); if (ds) aiReply = ds; return res.json({ done: false, reply: aiReply, index: s.index + 1, total: 30 }); }); 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/full', (req, res) => { const s = sessions.get(req.query.sessionId); if (!s) return res.json(fullReport({ answers: [] })); return res.json(fullReport(s)); }); server.use((req, res) => handle(req, res)); server.listen(port, () => { console.log(`Lingjing listening on ${port}`); }); });