first commit

This commit is contained in:
RISE
2026-05-21 10:28:29 +08:00
commit 5d3b2477c7
1159 changed files with 152655 additions and 0 deletions

19
src/config.js Normal file
View File

@@ -0,0 +1,19 @@
require("dotenv").config();
module.exports = {
port: parseInt(process.env.PORT, 10) || 3000,
wechat: {
appid: process.env.WECHAT_APPID || "",
secret: process.env.WECHAT_SECRET || "",
},
deepseek: {
apiKey: process.env.DEEPSEEK_API_KEY || "",
baseUrl: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com",
model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
},
jwtSecret: process.env.JWT_SECRET || "dev-secret-change-in-production",
jwtExpiresIn: "7d",
};

40
src/index.js Normal file
View File

@@ -0,0 +1,40 @@
const http = require("http");
const express = require("express");
const cors = require("cors");
const { WebSocketServer } = require("ws");
const config = require("./config");
const authRoutes = require("./routes/auth");
const posterRoutes = require("./routes/poster");
const { handleConnection } = require("./wsHandler");
const app = express();
app.use(cors());
app.use(express.json());
// HTTP 路由
app.use(authRoutes);
app.use(posterRoutes);
// 健康检查
app.get("/", (req, res) => {
res.json({ status: "ok", service: "周公解梦 API" });
});
const server = http.createServer(app);
// WebSocket 服务器
const wss = new WebSocketServer({ server, path: "/chat" });
wss.on("connection", (ws, req) => {
handleConnection(ws, req);
});
server.listen(config.port, () => {
console.log(`服务已启动: http://localhost:${config.port}`);
console.log(`WebSocket: ws://localhost:${config.port}/chat`);
console.log("接口列表:");
console.log(" GET /login?code=xxx — 微信登录");
console.log(" GET /generate-post?record_id= — 生成分享海报");
console.log(" WSS /chat — 解梦对话");
});

28
src/routes/auth.js Normal file
View File

@@ -0,0 +1,28 @@
const { Router } = require("express");
const jwt = require("jsonwebtoken");
const config = require("../config");
const { code2session } = require("../services/wechat");
const router = Router();
router.get("/login", async (req, res) => {
try {
const { code } = req.query;
if (!code) {
return res.status(400).json({ error: "缺少 code 参数" });
}
const { openid } = await code2session(code);
const token = jwt.sign({ openid }, config.jwtSecret, {
expiresIn: config.jwtExpiresIn,
});
res.json({ access_token: token });
} catch (err) {
console.error("登录失败:", err.message);
res.status(500).json({ error: "登录失败,请稍后重试" });
}
});
module.exports = router;

96
src/routes/poster.js Normal file
View File

@@ -0,0 +1,96 @@
const { Router } = require("express");
const sharp = require("sharp");
const store = require("../store");
const router = Router();
const POSTER_WIDTH = 750;
const POSTER_HEIGHT = 1200;
function wrapText(text, maxCharsPerLine) {
const lines = [];
let current = "";
for (const ch of text) {
if (ch === "\n") {
lines.push(current);
current = "";
} else if (current.length >= maxCharsPerLine) {
lines.push(current);
current = ch;
} else {
current += ch;
}
}
if (current) lines.push(current);
return lines;
}
function buildPosterSvg(dream, interpretation) {
const dreamLines = wrapText(`梦:${dream}`, 18).slice(0, 10);
const interpLines = wrapText(interpretation, 18);
const titleY = 80;
const dreamCardY = 140;
const interpCardY = 400;
const lineHeight = 36;
const dreamCardH = dreamLines.length * lineHeight + 80;
const interpCardH = interpLines.length * lineHeight + 80;
const dreamTextSvg = dreamLines
.map((l, i) => `<text x="48" y="${48 + i * lineHeight}" font-size="28" fill="#333">${escapeXml(l)}</text>`)
.join("\n");
const interpTextSvg = interpLines
.map((l, i) => `<text x="48" y="${48 + i * lineHeight}" font-size="28" fill="#333">${escapeXml(l)}</text>`)
.join("\n");
const footerY = interpCardY + interpCardH + 60;
return `<svg xmlns="http://www.w3.org/2000/svg" width="${POSTER_WIDTH}" height="${Math.max(POSTER_HEIGHT, footerY + 80)}">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#16213e"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#bg)"/>
<text x="375" y="${titleY}" text-anchor="middle" font-size="48" font-weight="bold" fill="#e0c77c">周公解梦</text>
<text x="375" y="${titleY + 32}" text-anchor="middle" font-size="20" fill="#a0a0b0">— AI 智能梦境解析 —</text>
<rect x="24" y="${dreamCardY}" width="702" height="${dreamCardH}" rx="16" fill="#ffffff" opacity="0.95"/>
<text x="48" y="${dreamCardY + 38}" font-size="24" font-weight="bold" fill="#1a1a2e">梦境描述</text>
${dreamTextSvg}
<rect x="24" y="${interpCardY}" width="702" height="${interpCardH}" rx="16" fill="#ffffff" opacity="0.95"/>
<text x="48" y="${interpCardY + 38}" font-size="24" font-weight="bold" fill="#1a1a2e">解梦分析</text>
${interpTextSvg}
<text x="375" y="${footerY}" text-anchor="middle" font-size="22" fill="#606070">— 周公解梦 · AI 智能解析 —</text>
</svg>`;
}
function escapeXml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
router.get("/generate-post", async (req, res) => {
try {
const { record_id } = req.query;
if (!record_id) {
return res.status(400).json({ error: "缺少 record_id 参数" });
}
const record = store.getRecord(record_id);
if (!record) {
return res.status(404).json({ error: "记录不存在" });
}
const svg = buildPosterSvg(record.dream, record.interpretation);
const png = await sharp(Buffer.from(svg)).png().toBuffer();
res.set("Content-Type", "image/png");
res.send(png);
} catch (err) {
console.error("生成海报失败:", err.message);
res.status(500).json({ error: "生成海报失败" });
}
});
module.exports = router;

68
src/services/deepseek.js Normal file
View File

@@ -0,0 +1,68 @@
const axios = require("axios");
const config = require("../config");
const SYSTEM_PROMPT = `你是一位专业的梦境解析师,精通《周公解梦》和中国传统文化中的解梦理论。用户会向你描述他们的梦境,请你:
1. 首先提取梦境中的关键元素和符号
2. 结合传统解梦理论进行分析
3. 给出通俗易懂、温暖鼓励的解析
4. 最后给出一些积极的生活建议
注意:
- 保持理性客观,不要散布迷信
- 语言优美流畅,富有文采
- 如果梦境内容负面,要给予正向引导和安抚
- 控制在300字以内`;
async function* streamDreamInterpretation(dream) {
const url = `${config.deepseek.baseUrl}/v1/chat/completions`;
const response = await axios.post(
url,
{
model: config.deepseek.model,
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: `我梦到了:${dream}` },
],
stream: true,
temperature: 0.8,
max_tokens: 1024,
},
{
headers: {
Authorization: `Bearer ${config.deepseek.apiKey}`,
"Content-Type": "application/json",
},
responseType: "stream",
timeout: 60000,
}
);
const stream = response.data;
let buffer = "";
for await (const chunk of stream) {
buffer += chunk.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith("data:")) continue;
const data = trimmed.slice(5).trim();
if (data === "[DONE]") return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) yield content;
} catch {
// 忽略无法解析的 chunk
}
}
}
}
module.exports = { streamDreamInterpretation };

18
src/services/wechat.js Normal file
View File

@@ -0,0 +1,18 @@
const axios = require("axios");
const config = require("../config");
async function code2session(code) {
const { appid, secret } = config.wechat;
const url = "https://api.weixin.qq.com/sns/jscode2session";
const { data } = await axios.get(url, {
params: { appid, secret, js_code: code, grant_type: "authorization_code" },
});
if (data.errcode) {
throw new Error(`微信登录失败: ${data.errmsg} (errcode=${data.errcode})`);
}
return { openid: data.openid, sessionKey: data.session_key };
}
module.exports = { code2session };

22
src/store.js Normal file
View File

@@ -0,0 +1,22 @@
const { v4: uuidv4 } = require("uuid");
// 内存存储record_id → { dream, interpretation, userId, createdAt }
const records = new Map();
module.exports = {
saveRecord(userId, dream, interpretation) {
const id = uuidv4();
records.set(id, {
id,
userId,
dream,
interpretation,
createdAt: new Date().toISOString(),
});
return id;
},
getRecord(id) {
return records.get(id) || null;
},
};

83
src/wsHandler.js Normal file
View File

@@ -0,0 +1,83 @@
const jwt = require("jsonwebtoken");
const config = require("./config");
const store = require("./store");
const { streamDreamInterpretation } = require("./services/deepseek");
function handleConnection(ws, req) {
// 从 url 参数或 header 中提取 token
const url = new URL(req.url, "http://localhost");
const token = url.searchParams.get("token") || req.headers["authorization"];
let userId = null;
try {
const payload = jwt.verify(token, config.jwtSecret);
userId = payload.openid;
} catch {
ws.send(
JSON.stringify({ act: "error", message: "请先登录后再解梦" })
);
ws.close(4001, "未授权");
return;
}
let fullInterpretation = "";
ws.on("message", async (raw) => {
let msg;
try {
msg = JSON.parse(raw.toString());
} catch {
ws.send(JSON.stringify({ act: "error", message: "消息格式错误" }));
return;
}
if (msg.act !== "start_generate") return;
const { message: dream, template_name } = msg.payload || {};
if (!dream || !dream.trim()) {
ws.send(JSON.stringify({ act: "error", message: "请填写梦境内容" }));
return;
}
if (template_name !== "jiemeng") {
ws.send(JSON.stringify({ act: "error", message: "不支持的模板" }));
return;
}
fullInterpretation = "";
try {
for await (const chunk of streamDreamInterpretation(dream.trim())) {
fullInterpretation += chunk;
ws.send(JSON.stringify({ act: "answer", message: chunk }));
}
const recordId = store.saveRecord(
userId,
dream.trim(),
fullInterpretation
);
ws.send(
JSON.stringify({
act: "answer_finish",
payload: { record_id: recordId },
})
);
} catch (err) {
console.error("DeepSeek 调用失败:", err.message);
ws.send(
JSON.stringify({
act: "error",
message: "AI 解析服务暂时不可用,请稍后重试",
})
);
}
});
ws.on("close", () => {
// 清理工作(如有需要)
});
}
module.exports = { handleConnection };