全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除

This commit is contained in:
ZhuiGuangAI Dev
2026-06-12 17:27:47 +08:00
parent 464924aea0
commit 4c325c8699
454 changed files with 33647 additions and 980 deletions
+311
View File
@@ -0,0 +1,311 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
import OpenAI from "openai";
import "dotenv/config";
import { withRetry } from "./lib/retry.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://");
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=5&pool_timeout=30`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
const openai = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.deepseek.com/v1",
});
const MODEL = process.env.DEEPSEEK_MODEL || "deepseek-chat";
const BOT_DATA_PATH = resolve(__dirname, "..", "data", "bot-characters.json");
const ENGAGEMENT_WEIGHTS = {
REPLY_PER_TOPIC: 0.4,
HUMAN_RATIO: 0.3,
LIKES_LOG: 0.3,
};
const TIER_THRESHOLDS = {
high: 1.0,
medium: 0.5,
};
function ts() {
return `[${new Date().toISOString()}]`;
}
function startOfToday() {
// 用 UTC 午夜避开 Prisma @db.Date 写入时的时区截断
const d = new Date();
return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
}
async function loadBotCharacters() {
const raw = readFileSync(BOT_DATA_PATH, "utf-8");
const { characters } = JSON.parse(raw);
const map = {};
for (const c of characters) {
map[c.key] = c;
}
return { characters, map };
}
function calcEngagementScore(stats) {
const { topicsCreated, repliesReceived, humanInteractions, likesReceived, repliesSent } = stats;
const replyPerTopic = topicsCreated > 0 ? repliesReceived / topicsCreated : 0;
const humanRatio =
repliesSent + topicsCreated > 0 ? humanInteractions / (repliesSent + topicsCreated) : 0;
const likesLog = Math.log(1 + likesReceived);
return (
ENGAGEMENT_WEIGHTS.REPLY_PER_TOPIC * Math.min(replyPerTopic, 5) +
ENGAGEMENT_WEIGHTS.HUMAN_RATIO * Math.min(humanRatio, 1) * 5 +
ENGAGEMENT_WEIGHTS.LIKES_LOG * Math.min(likesLog, 5)
);
}
function decideTier(score, baseTier) {
const baseRank = { low: 1, medium: 2, high: 3 }[baseTier] || 2;
if (score >= TIER_THRESHOLDS.high) return "high";
if (score >= TIER_THRESHOLDS.medium) return baseRank >= 2 ? "medium" : "high";
if (score >= TIER_THRESHOLDS.medium * 0.6) return baseRank >= 2 ? "low" : "medium";
return "low";
}
function buildReflectionPrompt(botChar, stats, score, nextTier) {
const p = botChar.personality;
return `你是【${botChar.displayName}】。本周你的论坛表现总结:
[本周数据]
- 发帖 ${stats.topicsCreated} 个
- 回复 ${stats.repliesSent} 条
- 收到回复 ${stats.repliesReceived} 条
- 真人互动 ${stats.humanInteractions} 次
- 收到点赞 ${stats.likesReceived} 个
- 互动得分 ${score.toFixed(2)}
[下周配额] ${nextTier}
[你的角色]
${p.identity}
风格:${p.speakingStyle}
[任务]
请以第一人称写一段50-120字的本周复盘:
1. 数据怎么解读(不要数字罗列,要有你的判断)
2. 哪类话题效果好、哪类效果差
3. 下周你最想尝试的一个调整是什么
要求:保持你【${botChar.displayName}】的说话风格,简短口语化。
只输出JSON:{"reflection": "你的复盘内容"}`;
}
async function callDeepSeek(prompt) {
const response = await withRetry(() =>
openai.chat.completions.create({
model: MODEL,
messages: [{ role: "user", content: prompt }],
temperature: 0.85,
max_tokens: 400,
})
);
const text = response.choices[0]?.message?.content?.trim() || "";
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error("无法解析JSON");
return JSON.parse(jsonMatch[0]);
}
async function getWeekRange() {
const today = startOfToday();
const dayOfWeek = today.getUTCDay();
// weekStart = today - 6 天(统一用 UTC 午夜)
const weekStart = new Date(today);
weekStart.setUTCDate(weekStart.getUTCDate() - 6);
const weekEnd = today;
return { weekStart, weekEnd };
}
async function reviewBot(botChar) {
const botUser = await prisma.user.findFirst({
where: { email: `bot_${botChar.key}@zhuiguang.ai` },
});
if (!botUser) return;
const botConfig = await prisma.botConfig.findUnique({
where: { userId: botUser.id },
});
if (!botConfig) return;
const { weekStart, weekEnd } = await getWeekRange();
// weekStart / weekEnd 已经是 UTC 午夜了,无需再 setHours
const weekStartDate = new Date(weekStart);
const weekEndDate = new Date(weekEnd);
const stats = await prisma.botDailyStat.aggregate({
where: {
botId: botConfig.id,
date: { gte: weekStartDate, lte: weekEndDate },
},
_sum: {
topicCreated: true,
replySent: true,
followUpSent: true,
repliesReceived: true,
likesReceived: true,
humanInteractions: true,
feedbackProcessed: true,
},
});
const sum = stats._sum;
const flatStats = {
topicsCreated: sum.topicCreated || 0,
repliesSent: sum.replySent || 0,
repliesReceived: sum.repliesReceived || 0,
humanInteractions: sum.humanInteractions || 0,
likesReceived: sum.likesReceived || 0,
};
if (flatStats.topicsCreated === 0 && flatStats.repliesSent === 0) {
console.log(`${ts()} ${botChar.displayName}: 本周无活动,跳过`);
return;
}
const score = calcEngagementScore(flatStats);
const baseTier = botChar.activityLevel || "medium";
const nextTier = decideTier(score, baseTier);
let reflection = null;
try {
const prompt = buildReflectionPrompt(botChar, flatStats, score, nextTier);
const result = await callDeepSeek(prompt);
reflection = (result.reflection || "").trim().slice(0, 500) || null;
} catch (err) {
console.warn(`${ts()} ${botChar.displayName} 反思生成失败:`, err.message);
}
const review = await prisma.botWeeklyReview.upsert({
where: { botId_weekStart: { botId: botConfig.id, weekStart: weekStartDate } },
create: {
botId: botConfig.id,
weekStart: weekStartDate,
weekEnd: weekEndDate,
topicsCreated: flatStats.topicsCreated,
repliesSent: flatStats.repliesSent,
repliesReceived: flatStats.repliesReceived,
humanRatio:
flatStats.repliesSent + flatStats.topicsCreated > 0
? flatStats.humanInteractions / (flatStats.repliesSent + flatStats.topicsCreated)
: 0,
likesReceived: flatStats.likesReceived,
engagementScore: score,
nextWeekQuota: nextTier,
reflection,
},
update: {
weekEnd: weekEndDate,
topicsCreated: flatStats.topicsCreated,
repliesSent: flatStats.repliesSent,
repliesReceived: flatStats.repliesReceived,
humanRatio:
flatStats.repliesSent + flatStats.topicsCreated > 0
? flatStats.humanInteractions / (flatStats.repliesSent + flatStats.topicsCreated)
: 0,
likesReceived: flatStats.likesReceived,
engagementScore: score,
nextWeekQuota: nextTier,
reflection,
},
});
await prisma.botMemory.create({
data: {
botId: botConfig.id,
memoryType: "reflection",
layer: "WORKING",
content: {
action: "weekly_review",
score,
topicsCreated: flatStats.topicsCreated,
repliesReceived: flatStats.repliesReceived,
humanInteractions: flatStats.humanInteractions,
likesReceived: flatStats.likesReceived,
nextTier,
reflection,
},
importance: 0.7,
contextTags: {
reviewId: review.id,
weekStart: weekStartDate.toISOString().slice(0, 10),
source: "weekly_review",
},
},
});
console.log(
`${ts()} ${botChar.displayName}: 得分 ${score.toFixed(2)} | ${baseTier} → ${nextTier} | 帖${flatStats.topicsCreated}/收${flatStats.repliesReceived}/真人${flatStats.humanInteractions}`
);
}
async function main() {
console.log(`${ts()} Bot Weekly Review starting...`);
if (!process.env.DEEPSEEK_API_KEY) {
throw new Error("DEEPSEEK_API_KEY 未配置");
}
const { characters } = await loadBotCharacters();
const botChars = characters.filter((c) => !c.role || c.role !== "passerby");
console.log(`${ts()} Loaded ${botChars.length} expert bots.`);
let processed = 0;
for (const botChar of botChars) {
try {
await reviewBot(botChar);
processed++;
} catch (err) {
console.error(`${ts()} Error processing ${botChar.key}:`, err.message);
}
}
await prisma.taskLog.create({
data: {
taskKey: "bot-weekly-review",
taskName: "Bot周度复盘",
status: "success",
startedAt: new Date(),
finishedAt: new Date(),
triggerBy: "cron",
result: { processedBots: processed },
},
});
console.log(`${ts()} Bot Weekly Review done. Processed ${processed} bots.`);
}
main()
.catch(async (e) => {
console.error(`${ts()} Fatal error:`, e);
try {
await prisma.taskLog.create({
data: {
taskKey: "bot-weekly-review",
taskName: "Bot周复盘",
status: "failed",
startedAt: new Date(),
finishedAt: new Date(),
triggerBy: "cron",
error: (e && e.message ? e.message : String(e)).slice(0, 1000),
},
});
} catch {}
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});