feat: 首次推送到Gitea - 完整项目代码 + 安全加固 + 知识库
This commit is contained in:
@@ -6,6 +6,8 @@ import { fileURLToPath } from "url";
|
||||
import OpenAI from "openai";
|
||||
import "dotenv/config";
|
||||
import { withRetry } from "./lib/retry.mjs";
|
||||
import { pickCommentStyle, getStyleHint } from "./lib/topic-diversity.mjs";
|
||||
import { validateContextRelevance, detectBadPatterns } from "./lib/reply-quality.mjs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://");
|
||||
@@ -16,11 +18,11 @@ const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
baseURL: "https://api.deepseek.com",
|
||||
});
|
||||
|
||||
const BOT_DATA_PATH = resolve(__dirname, "..", "data", "bot-characters.json");
|
||||
const MODEL = process.env.DEEPSEEK_MODEL || "deepseek-chat";
|
||||
const MODEL = process.env.DEEPSEEK_MODEL || "deepseek-v4-pro";
|
||||
|
||||
const LAYER = {
|
||||
SESSION: "SESSION",
|
||||
@@ -91,10 +93,13 @@ async function incrementStat(botUserId, field, by = 1) {
|
||||
}
|
||||
|
||||
async function getProcessedReplyIds(botUserId) {
|
||||
// 只回看最近 7 天的反馈记忆,避免全量扫描随数据增长而变慢
|
||||
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const memories = await prisma.botMemory.findMany({
|
||||
where: {
|
||||
botId: botUserId,
|
||||
memoryType: MEMORY_TYPE.FEEDBACK,
|
||||
createdAt: { gte: since },
|
||||
},
|
||||
select: { contextTags: true },
|
||||
});
|
||||
@@ -108,12 +113,19 @@ async function getProcessedReplyIds(botUserId) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
function buildFollowUpPrompt(bot, topicTitle, topicContent, originalPost, allReplies) {
|
||||
// 📄 核心 Prompt 模板参考: scripts/prompts/bot-feedback.txt
|
||||
// 实际 prompt 构建逻辑保留了动态参数注入(角色设定、回复风格、上下文等)
|
||||
function buildFollowUpPrompt(bot, topicTitle, topicContent, originalPost, allReplies, options = {}) {
|
||||
const { commentStyle = null } = options;
|
||||
const p = bot.personality;
|
||||
const repliesText = allReplies
|
||||
.map((r, i) => `${i + 1}. ${r.author}: ${r.content.slice(0, 250)}`)
|
||||
.join("\n");
|
||||
|
||||
const styleHint = commentStyle
|
||||
? `\n[本次回复风格指引] ${getStyleHint(commentStyle)}\n`
|
||||
: "";
|
||||
|
||||
return `你正在参与"追光AI行业论坛"。你之前在"${topicTitle}"这个话题下发了主帖,现在有用户回复了你,你需要用你的人格接着聊。
|
||||
|
||||
[你的角色设定]
|
||||
@@ -128,7 +140,7 @@ ${topicContent}
|
||||
|
||||
[新收到的回复]
|
||||
${repliesText}
|
||||
|
||||
${styleHint}
|
||||
[二次回复要求]
|
||||
1. 像真人接话,120-350字,不能太长刷屏
|
||||
2. 必须对前面至少一个具体观点有回应(赞同/质疑/补充案例/反问)
|
||||
@@ -150,7 +162,8 @@ async function callDeepSeek(prompt) {
|
||||
model: MODEL,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0.88,
|
||||
max_tokens: 800,
|
||||
// DeepSeek V4 推理 token 计入 max_tokens,预算不足会导致正文截断/为空
|
||||
max_tokens: 4096,
|
||||
})
|
||||
);
|
||||
const text = response.choices[0].message.content.trim();
|
||||
@@ -272,7 +285,9 @@ async function processBotFeedback(botKey, botChar) {
|
||||
await incrementStat(botConfig.id, "humanInteractions", humanCount);
|
||||
}
|
||||
|
||||
// 只有当有真人回复时,Bot才进行二次回复(避免Bot之间互相回复)
|
||||
if (
|
||||
humanCount > 0 &&
|
||||
newReplies.length >= MIN_FEEDBACK_TO_FOLLOWUP &&
|
||||
Math.random() < FOLLOW_UP_PROBABILITY &&
|
||||
followUpsSent < MAX_FOLLOWUP_PER_RUN_PER_BOT &&
|
||||
@@ -283,12 +298,16 @@ async function processBotFeedback(botKey, botChar) {
|
||||
author: p.user.name || "匿名",
|
||||
content: p.content,
|
||||
}));
|
||||
|
||||
// 选择回复风格,确保多样性
|
||||
const commentStyle = pickCommentStyle();
|
||||
const prompt = buildFollowUpPrompt(
|
||||
botChar,
|
||||
topic.title,
|
||||
topic.content,
|
||||
null,
|
||||
repliesForPrompt
|
||||
repliesForPrompt,
|
||||
{ commentStyle }
|
||||
);
|
||||
const result = await callDeepSeek(prompt);
|
||||
const followUpContent = (result.content || "").trim();
|
||||
@@ -297,6 +316,19 @@ async function processBotFeedback(botKey, botChar) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 质量检查:反模式检测
|
||||
const badPatterns = detectBadPatterns(followUpContent);
|
||||
if (badPatterns.isGeneric) {
|
||||
console.warn(`${ts()} ⚠️ ${botChar.displayName} 回复过于通用 (pattern_matches=${badPatterns.matchCount}), 但仍保留`);
|
||||
}
|
||||
|
||||
// 关联度检查
|
||||
const contextForRelevance = repliesForPrompt.map(r => r.content).join(" ");
|
||||
const { score: relevanceScore } = validateContextRelevance(followUpContent, contextForRelevance);
|
||||
|
||||
// 多样性度量日志
|
||||
console.log(`${ts()} 📊 ${botChar.displayName}: comment_style=${commentStyle}, relevance=${(relevanceScore / 100).toFixed(2)}, generic=${badPatterns.isGeneric}`);
|
||||
|
||||
const post = await prisma.forumPost.create({
|
||||
data: {
|
||||
topicId: topic.id,
|
||||
@@ -324,6 +356,12 @@ async function processBotFeedback(botKey, botChar) {
|
||||
topicId: topic.id,
|
||||
triggeredBy: "feedback_loop",
|
||||
summary: followUpContent.slice(0, 100),
|
||||
qualityMetrics: {
|
||||
commentStyle,
|
||||
relevance: relevanceScore / 100,
|
||||
isGeneric: badPatterns.isGeneric,
|
||||
patternMatches: badPatterns.matchCount,
|
||||
},
|
||||
},
|
||||
0.6,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user