185 lines
7.8 KiB
JavaScript
185 lines
7.8 KiB
JavaScript
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;
|
||
try {
|
||
const res = await fetch('https://ycapis.com/v1/responses', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: `Bearer ${key}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'gpt-5.3-codex',
|
||
input: messages.map(m => ({ role: m.role, content: m.content })),
|
||
max_output_tokens: 300,
|
||
}),
|
||
});
|
||
if (!res.ok) return null;
|
||
const data = await res.json();
|
||
return data?.output?.[0]?.content?.[0]?.text || null;
|
||
} catch {
|
||
return 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: [], questions: [], 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 >= 30;
|
||
if (done) {
|
||
return res.json({ done: true, reply: '你已经完成全部对话。接下来我会为你生成专属灵镜报告。' });
|
||
}
|
||
|
||
const progress = s.index > 0 && s.index % 6 === 0 ? `(你已经完成第${Math.ceil(s.index / 6)}/5阶段了。)` : '';
|
||
|
||
// 全部交给AI动态生成,不用硬编码题库
|
||
const ds = await callAI([
|
||
{ 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题。` }
|
||
]);
|
||
|
||
const aiReply = ds || '嗯,我听到了。我们继续,你现在主要在做什么?上学、上班、还是在休整?';
|
||
|
||
// 记录AI问的问题
|
||
s.questions.push(aiReply);
|
||
|
||
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}`);
|
||
});
|
||
});
|