feat: 首次推送到Gitea - 完整项目代码 + 安全加固 + 知识库

This commit is contained in:
ZhuiGuangAI Dev
2026-10-02 18:52:49 +08:00
parent b44601bb66
commit 8205ae709c
1185 changed files with 49841 additions and 12802 deletions
+215 -143
View File
@@ -9,6 +9,8 @@ import { withRetry } from "./lib/retry.mjs";
import { schedulePersonaRefresh, disconnectBotPersona } from "./lib/bot-persona.mjs";
import { pickVariantForPrompt, commitAssignment, buildVariantPersonaBlock, disconnectBotExperiment } from "./lib/bot-persona-experiment.mjs";
import { getActiveLearningForBot, buildAdversarialBlock, markLearningUsed, disconnectAdversarialLearning } from "./lib/bot-adversarial-learning.mjs";
import { getNextTemplate, getNextContentPattern, pickCommentStyle, getStyleHint } from "./lib/topic-diversity.mjs";
import { validateContextRelevance, detectBadPatterns, addPersonalityFlavor } from "./lib/reply-quality.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://");
@@ -19,11 +21,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 FORUM_SLUGS = [
"ec-platform", "ec-livestream", "ec-private", "ec-group", "ec-dtc", "ec-supply",
@@ -379,6 +381,8 @@ async function logTask(action, status, detail) {
finishedAt: new Date(),
triggerBy: "cron",
result: { action, detail },
// error 状态时把明细同步写入 error 字段,便于健康检查/审计直接读取
error: status === "error" || status === "failed" ? String(detail).slice(0, 1000) : undefined,
},
});
} catch (e) {
@@ -396,6 +400,8 @@ async function loadBotCharacters() {
return { characters, map };
}
// 📄 核心 Prompt 模板参考: scripts/prompts/bot-activity.txt
// 实际 prompt 构建逻辑保留了动态参数注入(板块信息、技能、人设、A/B 变体等)
function buildTopicPrompt(bot, forumName, forumDescription, options = {}) {
const { topSkills = [], persona = null, isCrossForum = false, styleSeed = 0, variantBlock = "", adversarialBlock = "" } = options;
const p = bot.personality;
@@ -464,7 +470,7 @@ ${topSkills.length > 0 ? "6. 可以参考上述套路的钩子方式,但内容
}
function buildReplyPrompt(bot, topicTitle, topicContent, existingReplies, forumName, options = {}) {
const { variantBlock = "", adversarialBlock = "" } = options;
const { variantBlock = "", adversarialBlock = "", commentStyle = null } = options;
const p = bot.personality;
const replyText = existingReplies
.map((r, i) => `${r.author}: ${r.content.substring(0, 200)}`)
@@ -482,6 +488,10 @@ function buildReplyPrompt(bot, topicTitle, topicContent, existingReplies, forumN
};
const strategyHint = strategyHints[strategy] || strategyHints.agree_with_evidence;
const styleHint = commentStyle
? `\n[本轮回复风格指引] ${getStyleHint(commentStyle)}\n`
: "";
const useCasual = Math.random() < STYLE_VARIATION.CASUAL_RATE;
const useEmotional = Math.random() < STYLE_VARIATION.EMOTIONAL_RATE;
const casualHint = useCasual
@@ -512,7 +522,7 @@ ${adversarialBlock}
${replyText || "(暂无回复,你是第一个回复的人)"}
[本次回复策略] ${strategyHint}
${casualHint}${emotionalHint}
${styleHint}${casualHint}${emotionalHint}
[回复要求]
请以${bot.displayName}的身份回复这个帖子。要求:
1. 回复${wordLimit.min}-${wordLimit.max}字,有信息增量,不能重复前面已经说过的内容
@@ -534,43 +544,62 @@ ${casualHint}${emotionalHint}
}
async function callDeepSeek(prompt) {
const response = await withRetry(() =>
openai.chat.completions.create({
model: MODEL,
messages: [{ role: "user", content: prompt }],
temperature: 0.85,
max_tokens: 1024,
})
);
const text = response.choices[0].message.content.trim();
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error(`Could not parse JSON from response: ${text.substring(0, 200)}`);
}
let parsed;
try {
parsed = JSON.parse(jsonMatch[0]);
} catch {
const cleaned = jsonMatch[0]
.replace(/,\s*}/g, "}")
.replace(/,\s*]/g, "]");
// 整体重试(最多 3 次):覆盖 JSON 截断/解析失败/质量不达标等非网络类偶发失败;
// 网络层已有 withRetry 兜底,这里针对"内容不合规"再做一次完整重生成
let lastErr;
for (let attempt = 0; attempt < 3; attempt++) {
try {
parsed = JSON.parse(cleaned);
} catch {
throw new Error(`JSON parse failed after cleanup: ${jsonMatch[0].substring(0, 200)}`);
}
}
const response = await withRetry(() =>
openai.chat.completions.create({
model: MODEL,
messages: [{ role: "user", content: prompt }],
temperature: 0.85,
// DeepSeek V4 推理 token 计入 max_tokens,预算不足会导致正文截断/为空
max_tokens: 4096,
})
);
// 质量检查(仅对含 content 字段的结果)
if (parsed.content) {
const qa = assessContentQuality(parsed.content, parsed.title);
if (!qa.pass) {
console.warn(`${ts()} ⚠️ 质量不达标 (score=${qa.score}): ${qa.issues.join(", ")}`);
const text = response.choices[0].message.content.trim();
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error(`Could not parse JSON from response: ${text.substring(0, 200)}`);
}
let parsed;
try {
parsed = JSON.parse(jsonMatch[0]);
} catch {
const cleaned = jsonMatch[0]
.replace(/,\s*}/g, "}")
.replace(/,\s*]/g, "]");
try {
parsed = JSON.parse(cleaned);
} catch {
throw new Error(`JSON parse failed after cleanup: ${jsonMatch[0].substring(0, 200)}`);
}
}
// 质量检查(仅对含 content 字段的结果)
if (parsed.content) {
const qa = assessContentQuality(parsed.content, parsed.title);
if (!qa.pass) {
console.warn(`${ts()} ⚠️ 质量不达标 (score=${qa.score}): ${qa.issues.join(", ")}`);
}
// 严重低质量内容(score < 40)直接丢弃,防止 AI 胡言乱语进入论坛
if (qa.score < 40) {
throw new Error(`Content quality critically low (score=${qa.score}): ${qa.issues.join(", ")}`);
}
parsed._qualityScore = qa.score;
}
return parsed;
} catch (e) {
lastErr = e;
if (attempt < 2) {
console.warn(`${ts()} ⚠️ 生成重试 ${attempt + 1}/3: ${String(e.message || e).slice(0, 120)}`);
await new Promise((r) => setTimeout(r, 600 * (attempt + 1)));
}
}
parsed._qualityScore = qa.score;
}
return parsed;
throw lastErr;
}
async function getBotWithConfig(botKey) {
@@ -898,9 +927,9 @@ async function getRecentTopicsInForum(forumSlug, limit = 5) {
return prisma.forumTopic.findMany({
where: { categoryId: cat.id },
include: {
user: { select: { id: true, name: true } },
user: { select: { id: true, name: true, isBot: true } },
posts: {
include: { user: { select: { id: true, name: true } } },
include: { user: { select: { id: true, name: true, isBot: true } } },
orderBy: { createdAt: "asc" },
},
},
@@ -1010,92 +1039,98 @@ async function processForum(forumSlug, botCharacters, forceNewTopic = false) {
if (todayStat.topicCreated >= finalQuota.topicPerDay) {
console.log(`${ts()} ${topicAuthor.displayName} 今日发帖配额已用完 (${todayStat.topicCreated}/${finalQuota.topicPerDay}, tier=${dynamicTier})`);
} else {
console.log(`${ts()} ${topicAuthor.displayName} creating new topic (tier=${dynamicTier})${isCrossForum ? " [跨板块]" : ""}...`);
console.log(`${ts()} ${topicAuthor.displayName} creating new topic (tier=${dynamicTier})${isCrossForum ? " [跨板块]" : ""}...`);
try {
const topSkills = await getTopSkills(botConf.id, forumSlug, 2);
const persona = await getPersona(botConf.id);
const recentMemories = await getRecentMemories(botConf.id, 2);
const memoryCtx = buildMemoryContext(recentMemories, category.name);
try {
// 并行拉取互不依赖的画像/记忆/A-B变体/对抗学习数据
const [topSkills, persona, recentMemories, variantPick, advLearning] = await Promise.all([
getTopSkills(botConf.id, forumSlug, 2),
getPersona(botConf.id),
getRecentMemories(botConf.id, 2),
pickVariantForPrompt(botConf.id),
getActiveLearningForBot(botConf.id),
]);
const memoryCtx = buildMemoryContext(recentMemories, category.name);
const variantBlock = variantPick.personaBlock || "";
const adversarialBlock = buildAdversarialBlock(advLearning);
// A/B 分流:选变体(不写库),注入 personaBlock 到 prompt
const variantPick = await pickVariantForPrompt(botConf.id);
const variantBlock = variantPick.personaBlock || "";
// 对抗学习:拉本周真人高赞帖洞察(如果存在)
const advLearning = await getActiveLearningForBot(botConf.id);
const adversarialBlock = buildAdversarialBlock(advLearning);
let prompt = buildTopicPrompt(topicAuthor, category.name, category.description, {
topSkills,
persona,
isCrossForum,
variantBlock,
adversarialBlock,
});
if (memoryCtx) {
prompt = prompt.replace("[你的角色设定]", memoryCtx + "[你的角色设定]");
} else {
prompt = prompt.replace("[你的角色设定]", "\n[你的角色设定]");
}
const result = await callDeepSeek(prompt);
const subCat = await getRandomSubCategory(forumSlug);
const catId = subCat ? subCat.id : category.id;
const topic = await createTopic(catId, botConf.userId, result.title, result.content);
console.log(`${ts()} Created topic: "${result.title}"`);
if (advLearning) {
try {
await markLearningUsed(advLearning.id);
console.log(`${ts()} 🎓 对抗学习引用 week=${advLearning.weekKey} topic#${advLearning.sourceRefId}`);
} catch (e) {
console.warn(`${ts()} ⚠️ markLearningUsed failed: ${e.message}`);
let prompt = buildTopicPrompt(topicAuthor, category.name, category.description, {
topSkills,
persona,
isCrossForum,
variantBlock,
adversarialBlock,
});
if (memoryCtx) {
prompt = prompt.replace("[你的角色设定]", memoryCtx + "[你的角色设定]");
} else {
prompt = prompt.replace("[你的角色设定]", "\n[你的角色设定]");
}
}
// 提交 A/B assignment
if (variantPick.variant) {
try {
await commitAssignment(botConf.id, variantPick.variant.id, "topic", topic.id, forumSlug);
console.log(`${ts()} 🅰️ variant=${variantPick.variant.variantKey} assigned to topic#${topic.id}`);
} catch (e) {
console.warn(`${ts()} ⚠️ commitAssignment failed: ${e.message}`);
const result = await callDeepSeek(prompt);
// 质量检查:反模式检测 & 关联度
const badPatterns = detectBadPatterns(result.content);
if (badPatterns.isGeneric) {
console.warn(`${ts()} ⚠️ ${topicAuthor.displayName} 话题过于通用 (pattern_matches=${badPatterns.matchCount}), 但仍保留`);
}
console.log(`${ts()} 📊 ${topicAuthor.displayName}: topic_pattern=${getNextContentPattern()}, generic=${badPatterns.isGeneric}`);
const subCat = await getRandomSubCategory(forumSlug);
const catId = subCat ? subCat.id : category.id;
const topic = await createTopic(catId, botConf.userId, result.title, result.content);
console.log(`${ts()} Created topic: "${result.title}"`);
if (advLearning) {
try {
await markLearningUsed(advLearning.id);
console.log(`${ts()} 🎓 对抗学习引用 week=${advLearning.weekKey} topic#${advLearning.sourceRefId}`);
} catch (e) {
console.warn(`${ts()} ⚠️ markLearningUsed failed: ${e.message}`);
}
}
// 提交 A/B assignment
if (variantPick.variant) {
try {
await commitAssignment(botConf.id, variantPick.variant.id, "topic", topic.id, forumSlug);
console.log(`${ts()} 🅰️ variant=${variantPick.variant.variantKey} assigned to topic#${topic.id}`);
} catch (e) {
console.warn(`${ts()} ⚠️ commitAssignment failed: ${e.message}`);
}
}
await prisma.forumCategory.update({
where: { id: catId },
data: { topicCount: { increment: 1 } },
});
await saveMemory(botConf.id, "experience", {
action: "create_topic",
forum: category.name,
title: result.title,
summary: result.content.substring(0, 100),
usedSkillIds: topSkills.map(s => s.id),
usedSkillNames: topSkills.map(s => s.name),
isCrossForum,
}, isCrossForum ? 0.7 : 0.6);
if (topSkills.length > 0) {
await recordSkillUsage(topSkills.map(s => s.id), topic.id);
}
await incrementStat(botConf.id, "topicCreated", 1);
// 异步触发 persona 刷新(5 分钟防抖,不阻塞主流程)
schedulePersonaRefresh(botConf.userId, botConf.id);
await logTask("create_topic", "success",
`${topicAuthor.displayName} created topic in ${category.name}: ${result.title}${isCrossForum ? " [跨板块]" : ""}`);
} catch (err) {
console.error(`${ts()} Error creating topic:`, err.message);
await logTask("create_topic", "error", err.message);
}
await prisma.forumCategory.update({
where: { id: catId },
data: { topicCount: { increment: 1 } },
});
await saveMemory(botConf.id, "experience", {
action: "create_topic",
forum: category.name,
title: result.title,
summary: result.content.substring(0, 100),
usedSkillIds: topSkills.map(s => s.id),
usedSkillNames: topSkills.map(s => s.name),
isCrossForum,
}, isCrossForum ? 0.7 : 0.6);
if (topSkills.length > 0) {
await recordSkillUsage(topSkills.map(s => s.id), topic.id);
}
await incrementStat(botConf.id, "topicCreated", 1);
// 异步触发 persona 刷新(5 分钟防抖,不阻塞主流程)
schedulePersonaRefresh(botConf.userId, botConf.id);
await logTask("create_topic", "success",
`${topicAuthor.displayName} created topic in ${category.name}: ${result.title}${isCrossForum ? " [跨板块]" : ""}`);
} catch (err) {
console.error(`${ts()} Error creating topic:`, err.message);
await logTask("create_topic", "error", err.message);
}
}
}
@@ -1122,6 +1157,12 @@ async function processForum(forumSlug, botCharacters, forceNewTopic = false) {
const targetTopic = pick(existingTopics);
// 防止 Bot 互回造成回音壁:90% 概率跳过 Bot 作者的话题(仅 10% 容忍度用于跨板块共性话题)
if (targetTopic.user.isBot && Math.random() > 0.1) {
console.log(`${ts()} ⏭️ ${replierBot.displayName} skipped bot-authored topic "${targetTopic.title}" (bot echo prevention)`);
continue;
}
const authorName = targetTopic.user.name;
const topicContent = targetTopic.content;
const replies = targetTopic.posts.map(p => ({
@@ -1138,18 +1179,24 @@ async function processForum(forumSlug, botCharacters, forceNewTopic = false) {
console.log(`${ts()} ${replierBot.displayName} replying to "${targetTopic.title}"...`);
try {
const recentMemories = await getRecentMemories(botConf.id, 3);
// 并行拉取互不依赖的记忆/A-B变体/对抗学习数据
const [recentMemories, variantPick, advLearning] = await Promise.all([
getRecentMemories(botConf.id, 3),
pickVariantForPrompt(botConf.id),
getActiveLearningForBot(botConf.id),
]);
const memoryCtx = buildMemoryContext(recentMemories, category.name);
// A/B 分流:选变体注入 personaBlock
const variantPick = await pickVariantForPrompt(botConf.id);
const variantBlock = variantPick.personaBlock || "";
// 对抗学习:拉本周真人高赞帖洞察
const advLearning = await getActiveLearningForBot(botConf.id);
const adversarialBlock = buildAdversarialBlock(advLearning);
let prompt = buildReplyPrompt(replierBot, targetTopic.title, topicContent, replies, category.name, { variantBlock, adversarialBlock });
// 选择回复风格,确保多样性
const commentStyle = pickCommentStyle();
let prompt = buildReplyPrompt(replierBot, targetTopic.title, topicContent, replies, category.name, {
variantBlock,
adversarialBlock,
commentStyle,
});
if (memoryCtx) {
prompt = prompt.replace("[你的角色设定]", memoryCtx + "[你的角色设定]");
} else {
@@ -1158,6 +1205,15 @@ async function processForum(forumSlug, botCharacters, forceNewTopic = false) {
const result = await callDeepSeek(prompt);
// 质量检查:反模式检测 & 关联度
const badPatterns = detectBadPatterns(result.content);
if (badPatterns.isGeneric) {
console.warn(`${ts()} ⚠️ ${replierBot.displayName} 回复过于通用 (pattern_matches=${badPatterns.matchCount}), 但仍保留`);
}
const contextForRelevance = (topicContent + " " + replies.map(r => r.content).join(" ")).slice(0, 500);
const { score: relevanceScore } = validateContextRelevance(result.content, contextForRelevance);
console.log(`${ts()} 📊 ${replierBot.displayName}: comment_style=${commentStyle}, relevance=${(relevanceScore / 100).toFixed(2)}, generic=${badPatterns.isGeneric}`);
const post = await createPost(targetTopic.id, botConf.userId, result.content);
console.log(`${ts()} Reply saved (${result.content.length} chars).`);
@@ -1265,11 +1321,11 @@ async function processPasserbyForum(forumSlug, botCharacters) {
console.log(`${ts()} ${posterBot.displayName} (${posterBot.personality.passerbyType}) creating topic...`);
try {
const recentMemories = await getRecentMemories(botConf.id, 2);
const [recentMemories, variantPick] = await Promise.all([
getRecentMemories(botConf.id, 2),
pickVariantForPrompt(botConf.id),
]);
const memoryCtx = buildMemoryContext(recentMemories, category.name);
// A/B 分流
const variantPick = await pickVariantForPrompt(botConf.id);
const variantBlock = variantPick.personaBlock || "";
let prompt = buildPasserbyTopicPrompt(posterBot, category.name, category.description, recentNews, { variantBlock });
@@ -1281,6 +1337,13 @@ async function processPasserbyForum(forumSlug, botCharacters) {
const result = await callDeepSeek(prompt);
// 质量检查:反模式检测
const badPatterns = detectBadPatterns(result.content);
if (badPatterns.isGeneric) {
console.warn(`${ts()} ⚠️ ${posterBot.displayName} 话题过于通用 (pattern_matches=${badPatterns.matchCount}), 但仍保留`);
}
console.log(`${ts()} 📊 ${posterBot.displayName}(passerby): topic_pattern=${getNextContentPattern()}, generic=${badPatterns.isGeneric}`);
const subCat = await getRandomSubCategory(forumSlug);
const catId = subCat ? subCat.id : category.id;
@@ -1422,7 +1485,7 @@ async function runBotAutoLikes(botCharacters) {
for (const bot of allBotUsers) {
if (totalApplied >= BOT_LIKE_CONFIG.MAX_LIKES_PER_RUN) break;
// 候选伙伴 = 其他 bot + 少量真人
// 候选伙伴 = 其他 bot(真人候选在下方单独按概率补充)
const partnerBotIds = botUserIds.filter((id) => id !== bot.id);
const { topics, posts } = await pickLikeCandidates(bot.id, partnerBotIds, BOT_LIKE_CONFIG.LOOKBACK_DAYS);
if (topics.length === 0 && posts.length === 0) continue;
@@ -1526,16 +1589,13 @@ function assessContentQuality(content, title) {
if (pat.test(content)) { score -= 15; issues.push(`AI句式: ${pat.source}`); }
}
// 3) 重复标题检测(与内容前100字相似度)
// 3) 重复标题检测(标题是否照搬内容开头)
if (title && content) {
const titleWords = new Set(title.split(""));
const contentStart = content.substring(0, 100);
let overlap = 0;
for (const w of titleWords) {
if (w.trim() && contentStart.includes(w)) overlap++;
}
const titleLen = [...titleWords].filter(w => w.trim()).length;
if (titleLen > 0 && overlap / titleLen > 0.8) {
const normalizedTitle = title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
const contentStart = content
.substring(0, Math.max(60, normalizedTitle.length))
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
if (normalizedTitle.length > 0 && contentStart.includes(normalizedTitle)) {
score -= 20; issues.push("标题与内容高度重复");
}
}
@@ -1549,6 +1609,18 @@ function assessContentQuality(content, title) {
return { score: Math.max(0, score), issues, pass: score >= 60 };
}
// 基于当前小时的确定性轮转选板块,保证覆盖均匀(替代随机 shuffle)
// startOffset 用于让专家/路人两批错开,避免每次都撞到相同板块
function getRotatedForums(count, startOffset = 0) {
const total = FORUM_SLUGS.length;
const offset = (new Date().getHours() + startOffset) % total;
const rotated = [];
for (let i = 0; i < count; i++) {
rotated.push(FORUM_SLUGS[(offset + i) % total]);
}
return rotated;
}
async function main() {
console.log(`${ts()} Bot Activity Engine starting...`);
@@ -1557,14 +1629,14 @@ async function main() {
const passerbyCount = botCharacters.filter((c) => c.role === "passerby").length;
console.log(`${ts()} Loaded ${botCharacters.length} bot characters (${expertCount} experts + ${passerbyCount} passersbys).`);
const expertForums = FORUM_SLUGS.sort(() => Math.random() - 0.5).slice(0, EXPERT_FORUMS_PER_RUN);
const expertForums = getRotatedForums(EXPERT_FORUMS_PER_RUN);
console.log(`${ts()} Processing ${expertForums.length} forums for experts: ${expertForums.join(", ")}`);
for (const forumSlug of expertForums) {
await processForum(forumSlug, botCharacters);
}
const passerbyForums = FORUM_SLUGS.sort(() => Math.random() - 0.5).slice(0, PASSERBY_FORUMS_PER_RUN);
const passerbyForums = getRotatedForums(PASSERBY_FORUMS_PER_RUN, EXPERT_FORUMS_PER_RUN);
console.log(`${ts()} Processing ${passerbyForums.length} forums for passerbys: ${passerbyForums.join(", ")}`);
for (const forumSlug of passerbyForums) {