lingjing/server.js

178 lines
7.4 KiB
JavaScript
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.

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}`);
});
});