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=10&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 BOT_DATA_PATH = resolve(__dirname, "..", "data", "bot-characters.json"); const MODEL = process.env.DEEPSEEK_MODEL || "deepseek-chat"; const FORUM_SLUGS = [ "ec-platform", "ec-livestream", "ec-private", "ec-group", "ec-dtc", "ec-supply", "ai-tools-app", "ai-startup-forum", "ai-saas", "ai-hardware", "ai-llm", "ct-short-video", "ct-live", "ct-writing", "ct-mcn", "ct-podcast", "fi-stock", "fi-crypto", "fi-insurance", "fi-pe", "fi-forex", "cb-ecommerce", "cb-factory", "cb-logistics", "cb-payment", "cb-brand", "re-residential", "re-commercial", "re-renovation", "re-property", "fd-restaurant", "fd-tea", "fd-prepared", "fd-supply", "fd-delivery", "ll-beauty", "ll-fitness", "ll-pet", "ll-housekeeping", "ll-repair", "ll-edu-local", "hl-cosmetic", "hl-wellness", "hl-elderly", "hl-rehab", "hl-mental", "edu-skills", "edu-knowledge", "edu-abroad", "edu-corporate", "edu-k12", ]; const FORUM_NAMES = { "ec-platform": "平台电商", "ec-livestream": "直播带货", "ec-private": "私域电商", "ec-group": "社区团购", "ec-dtc": "独立站/DTC", "ec-supply": "选品与供应链", "ai-tools-app": "AI工具与应用", "ai-startup-forum": "AI创业", "ai-saas": "SaaS出海", "ai-hardware": "智能硬件", "ai-llm": "大模型与开源", "ct-short-video": "短视频创作", "ct-live": "直播运营", "ct-writing": "图文内容", "ct-mcn": "MCN机构", "ct-podcast": "播客与音频", "fi-stock": "股票与基金", "fi-crypto": "加密货币/Web3", "fi-insurance": "保险与理财", "fi-pe": "PE与VC", "fi-forex": "外汇与期货", "cb-ecommerce": "跨境电商平台", "cb-factory": "外贸工厂/B2B", "cb-logistics": "海外仓与物流", "cb-payment": "跨境支付合规", "cb-brand": "品牌出海/DTC", "re-residential": "住宅买卖", "re-commercial": "商业地产", "re-renovation": "装修与家居", "re-property": "物业与租赁", "fd-restaurant": "餐饮开店", "fd-tea": "茶饮与咖啡", "fd-prepared": "预制菜加工", "fd-supply": "食品供应链", "fd-delivery": "外卖运营", "ll-beauty": "美容美发", "ll-fitness": "健身运动", "ll-pet": "宠物经济", "ll-housekeeping": "家政服务", "ll-repair": "维修与数码", "ll-edu-local": "本地教培", "hl-cosmetic": "医疗美容", "hl-wellness": "健康管理", "hl-elderly": "养老服务", "hl-rehab": "运动康复", "hl-mental": "心理健康", "edu-skills": "职业技能", "edu-knowledge": "知识付费", "edu-abroad": "留学与语言", "edu-corporate": "企业培训", "edu-k12": "K12与家庭", }; const SUB_CATEGORY_MAP = { "ec-platform": ["ec-platform"], "ec-livestream": ["ec-livestream"], "ec-private": ["ec-private"], "ec-group": ["ec-group"], "ec-dtc": ["ec-dtc"], "ec-supply": ["ec-supply"], "ai-tools-app": ["ai-tools-app"], "ai-startup-forum": ["ai-startup-forum"], "ai-saas": ["ai-saas"], "ai-hardware": ["ai-hardware"], "ai-llm": ["ai-llm"], "ct-short-video": ["ct-short-video"], "ct-live": ["ct-live"], "ct-writing": ["ct-writing"], "ct-mcn": ["ct-mcn"], "ct-podcast": ["ct-podcast"], "fi-stock": ["fi-stock"], "fi-crypto": ["fi-crypto"], "fi-insurance": ["fi-insurance"], "fi-pe": ["fi-pe"], "fi-forex": ["fi-forex"], "cb-ecommerce": ["cb-ecommerce"], "cb-factory": ["cb-factory"], "cb-logistics": ["cb-logistics"], "cb-payment": ["cb-payment"], "cb-brand": ["cb-brand"], "re-residential": ["re-residential"], "re-commercial": ["re-commercial"], "re-renovation": ["re-renovation"], "re-property": ["re-property"], "fd-restaurant": ["fd-restaurant"], "fd-tea": ["fd-tea"], "fd-prepared": ["fd-prepared"], "fd-supply": ["fd-supply"], "fd-delivery": ["fd-delivery"], "ll-beauty": ["ll-beauty"], "ll-fitness": ["ll-fitness"], "ll-pet": ["ll-pet"], "ll-housekeeping": ["ll-housekeeping"], "ll-repair": ["ll-repair"], "ll-edu-local": ["ll-edu-local"], "hl-cosmetic": ["hl-cosmetic"], "hl-wellness": ["hl-wellness"], "hl-elderly": ["hl-elderly"], "hl-rehab": ["hl-rehab"], "hl-mental": ["hl-mental"], "edu-skills": ["edu-skills"], "edu-knowledge": ["edu-knowledge"], "edu-abroad": ["edu-abroad"], "edu-corporate": ["edu-corporate"], "edu-k12": ["edu-k12"], }; function ts() { return `[${new Date().toISOString()}]`; } function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function slugify(str) { const clean = str.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-|-$/g, "").substring(0, 80); return `${clean}-${Date.now().toString(36)}`; } async function logTask(action, status, detail) { try { await prisma.taskLog.create({ data: { taskKey: "bot-activity", taskName: "Bot活跃度调度", status, startedAt: new Date(), finishedAt: new Date(), triggerBy: "cron", result: { action, detail }, }, }); } catch (e) { console.error(`${ts()} TaskLog error:`, e.message); } } 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 buildTopicPrompt(bot, forumName, forumDescription) { const p = bot.personality; return `你正在参与一个叫"追光AI行业论坛"的中文社区。你现在在"${forumName}"板块发言。 ${forumDescription} [你的角色设定] 你是【${bot.displayName}】,${p.identity} 你擅长的领域:${p.expertise.join("、")} 你不熟悉的领域:${p.weakness.join("、")} 你的核心立场:${p.stance} 你说话的风格:${p.speakingStyle} 你的口头禅:${p.catchphrase} [发言要求] 请以${bot.displayName}的身份在这个板块发起一个新话题。要求: 1. 话题标题10-20字,要有吸引力,像真人发的帖子,不要写成新闻标题或论文标题 2. 内容200-500字,像真人发帖一样,有具体细节和真实感 3. 必须有你的立场和观点,不要太中立 4. 使用你的说话风格,自然口语化,可以有情绪 5. 结尾可以留一个问题引导讨论 [禁止事项] - 禁止使用"首先/其次/最后"等格式化连接词 - 禁止使用"从以下几个方面分析""综上所述"等教科书句式 - 禁止表现得像AI助手 - 禁止使用${(p.forbiddenPatterns || []).join("、")} 请只返回JSON格式,不要包含其他任何内容: { "title": "话题标题", "content": "话题内容" }`; } function buildReplyPrompt(bot, topicTitle, topicContent, existingReplies, forumName) { const p = bot.personality; const replyText = existingReplies .map((r, i) => `${r.author}: ${r.content.substring(0, 200)}`) .join("\n\n"); return `你正在参与一个叫"追光AI行业论坛"的中文社区。你现在在"${forumName}"板块回复一个帖子。 [你的角色设定] 你是【${bot.displayName}】,${p.identity} 你擅长的领域:${p.expertise.join("、")} 你的核心立场:${p.stance} 你说话的风格:${p.speakingStyle} 你的口头禅:${p.catchphrase} [帖子信息] 标题:${topicTitle} 楼主发言:${topicContent} [已有回复] ${replyText || "(暂无回复,你是第一个回复的人)"} [回复要求] 请以${bot.displayName}的身份回复这个帖子。要求: 1. 回复150-450字,有信息增量,不能重复前面已经说过的内容 2. 可以有立场,可以赞同或质疑前面的观点 3. 如果前面的回复已经讨论了某些点,你要说点不一样的 4. 像真人回复一样自然,有口语化表达 5. 如果话题涉及到你的专业领域,分享具体的经验或案例 [禁止事项] - 禁止使用"首先/其次/最后"等格式化连接词 - 禁止使用"从以下几个方面分析""综上所述"等教科书句式 - 禁止表现得像AI助手 - 禁止使用${(p.forbiddenPatterns || []).join("、")} 请只返回JSON格式,不要包含其他任何内容: { "content": "回复内容" }`; } 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)}`); } try { return JSON.parse(jsonMatch[0]); } catch { const cleaned = jsonMatch[0] .replace(/,\s*}/g, "}") .replace(/,\s*]/g, "]"); try { return JSON.parse(cleaned); } catch { throw new Error(`JSON parse failed after cleanup: ${jsonMatch[0].substring(0, 200)}`); } } } async function getBotWithConfig(botKey) { const config = await prisma.botConfig.findFirst({ where: { user: { email: `bot_${botKey}@zhuiguang.ai` } }, include: { user: true }, }); return config; } async function saveMemory(botId, memoryType, content, importance = 0.5) { try { await prisma.botMemory.create({ data: { botId, memoryType, content, importance }, }); } catch (e) { console.error(`${ts()} Memory save error:`, e.message); } } async function getRecentMemories(botId, limit = 3) { try { return await prisma.botMemory.findMany({ where: { botId }, orderBy: { createdAt: "desc" }, take: limit, }); } catch { return []; } } function buildMemoryContext(memories, forumName) { if (memories.length === 0) return ""; const lines = memories.map(m => { const c = m.content; if (m.memoryType === "experience") { return `• 你最近在"${c.forum}"板块发起了话题"${c.title}"`; } return `• 你最近在"${c.forum}"板块回复了关于"${c.topicTitle}"的讨论`; }); return `\n[你最近的讨论记忆]\n${lines.join("\n")}\n`; } async function getCategoryBySlug(slug) { return prisma.forumCategory.findUnique({ where: { slug } }); } async function getRandomSubCategory(forumSlug) { const subs = SUB_CATEGORY_MAP[forumSlug] || []; if (subs.length === 0) return null; const slug = pick(subs); return prisma.forumCategory.findUnique({ where: { slug } }); } async function getRecentTopicsInForum(forumSlug, limit = 5) { const cat = await getCategoryBySlug(forumSlug); if (!cat) return []; return prisma.forumTopic.findMany({ where: { categoryId: cat.id }, include: { user: { select: { id: true, name: true } }, posts: { include: { user: { select: { id: true, name: true } } }, orderBy: { createdAt: "asc" }, }, }, orderBy: { createdAt: "desc" }, take: limit, }); } async function createTopic(categoryId, userId, title, content) { return prisma.forumTopic.create({ data: { categoryId, userId, title, slug: slugify(title), content, }, }); } async function createPost(topicId, userId, content) { return prisma.forumPost.create({ data: { topicId, userId, content }, }); } async function processForum(forumSlug, botCharacters, forceNewTopic = false) { const category = await getCategoryBySlug(forumSlug); if (!category) { console.log(`${ts()} Forum "${forumSlug}" not found, skipping.`); return; } console.log(`${ts()} Processing forum: ${category.name}`); const forumBots = botCharacters.filter( c => c.primaryForums && c.primaryForums.includes(forumSlug) ); if (forumBots.length === 0) { console.log(`${ts()} No bots assigned to this forum.`); return; } const existingTopics = await getRecentTopicsInForum(forumSlug, 10); const needsNewTopic = forceNewTopic || existingTopics.length < 3 || existingTopics[0] && (Date.now() - existingTopics[0].createdAt.getTime()) > 24 * 60 * 60 * 1000; if (needsNewTopic) { const topicAuthor = pick(forumBots.filter(c => c.activityLevel === "high" || c.activityLevel === "medium")); const botConf = await getBotWithConfig(topicAuthor.key); if (!botConf) { console.log(`${ts()} Bot "${topicAuthor.key}" not found in DB.`); return; } console.log(`${ts()} ${topicAuthor.displayName} creating new topic...`); try { const recentMemories = await getRecentMemories(botConf.id, 2); const memoryCtx = buildMemoryContext(recentMemories, category.name); let prompt = buildTopicPrompt(topicAuthor, category.name, category.description); 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}"`); await saveMemory(botConf.id, "experience", { action: "create_topic", forum: category.name, title: result.title, summary: result.content.substring(0, 100), }, 0.6); await logTask("create_topic", "success", `${topicAuthor.displayName} created topic in ${category.name}: ${result.title}`); } catch (err) { console.error(`${ts()} Error creating topic:`, err.message); await logTask("create_topic", "error", err.message); } } if (existingTopics.length > 0) { const repliers = forumBots .filter(() => Math.random() < 0.5) .slice(0, 3); for (const replierBot of repliers) { const botConf = await getBotWithConfig(replierBot.key); if (!botConf) continue; const targetTopic = pick(existingTopics); const authorName = targetTopic.user.name; const topicContent = targetTopic.content; const replies = targetTopic.posts.map(p => ({ author: p.user.name, content: p.content, })); const alreadyReplied = targetTopic.posts.some(p => p.userId === botConf.userId); if (alreadyReplied) { console.log(`${ts()} ${replierBot.displayName} already replied to "${targetTopic.title}", skipping.`); continue; } console.log(`${ts()} ${replierBot.displayName} replying to "${targetTopic.title}"...`); try { const recentMemories = await getRecentMemories(botConf.id, 3); const memoryCtx = buildMemoryContext(recentMemories, category.name); let prompt = buildReplyPrompt(replierBot, targetTopic.title, topicContent, replies, category.name); if (memoryCtx) { prompt = prompt.replace("[你的角色设定]", memoryCtx + "[你的角色设定]"); } else { prompt = prompt.replace("[你的角色设定]", "\n[你的角色设定]"); } const result = await callDeepSeek(prompt); const post = await createPost(targetTopic.id, botConf.userId, result.content); console.log(`${ts()} Reply saved (${result.content.length} chars).`); await saveMemory(botConf.id, "interaction", { action: "reply", forum: category.name, topicTitle: targetTopic.title, repliedTo: authorName, summary: result.content.substring(0, 100), }, 0.5); await logTask("create_reply", "success", `${replierBot.displayName} replied to ${authorName}'s topic in ${category.name}`); } catch (err) { console.error(`${ts()} Error creating reply:`, err.message); await logTask("create_reply", "error", err.message); } } } } async function main() { console.log(`${ts()} Bot Activity Engine starting...`); const { characters: botCharacters } = await loadBotCharacters(); console.log(`${ts()} Loaded ${botCharacters.length} bot characters.`); const activeForums = FORUM_SLUGS.sort(() => Math.random() - 0.5).slice(0, 2); console.log(`${ts()} Processing ${activeForums.length} forums: ${activeForums.join(", ")}`); for (const forumSlug of activeForums) { await processForum(forumSlug, botCharacters); } console.log(`${ts()} Bot Activity Engine done.`); } main() .catch((e) => { console.error(`${ts()} Fatal error:`, e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });