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"], }; const NEWS_SEARCH_QUERIES = { "ec-platform": "电商 淘宝 京东 拼多多 平台 最新", "ec-livestream": "直播带货 抖音直播 快手电商 最新", "ec-private": "私域流量 微信电商 社群运营 最新", "ec-group": "社区团购 美团优选 多多买菜 最新", "ec-dtc": "DTC品牌 Shopify 独立站 电商出海", "ec-supply": "供应链 选品 电商货源 库存管理", "ai-tools-app": "AI工具 ChatGPT Claude AI应用 最新", "ai-startup-forum": "AI创业 人工智能创业 融资 最新", "ai-saas": "SaaS AI出海 软件服务 最新", "ai-hardware": "AI硬件 智能设备 机器人 IoT", "ai-llm": "大模型 LLM 开源模型 人工智能", "ct-short-video": "短视频 抖音 B站 创作者 最新", "ct-live": "直播运营 直播带货 主播 最新", "ct-writing": "公众号 小红书 自媒体 图文创作", "ct-mcn": "MCN机构 达人孵化 网红经济", "ct-podcast": "播客 小宇宙 音频创作 最新", "fi-stock": "A股 港股 美股 股票 基金 最新", "fi-crypto": "比特币 加密货币 区块链 Web3", "fi-insurance": "保险 理财 资产配置 最新", "fi-pe": "PE VC 风险投资 融资 最新", "fi-forex": "外汇 期货 大宗商品 黄金 最新", "cb-ecommerce": "跨境电商 Amazon Temu TikTokShop", "cb-factory": "外贸工厂 制造业 出口 B2B 最新", "cb-logistics": "跨境物流 海外仓 FBA 供应链", "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教育 学科辅导 素质教育 家庭", }; 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 fetchGoogleNewsRSS(query, lang = "zh-CN", country = "CN") { try { const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=${lang}&gl=${country}&ceid=${country}:${lang === "zh-CN" ? "zh-Hans" : "en"}`; const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (compatible; PasserbyBot/1.0)" }, signal: AbortSignal.timeout(8000), }); if (!response.ok) return []; const xml = await response.text(); const items = []; const itemRegex = /([\s\S]*?)<\/item>/g; let match; while ((match = itemRegex.exec(xml)) !== null) { const block = match[1]; const titleMatch = block.match(/<!\[CDATA\[([\s\S]*?)\]\]><\/title>/) || block.match(/<title>([\s\S]*?)<\/title>/); const linkMatch = block.match(/<link>([\s\S]*?)<\/link>/); const pubDateMatch = block.match(/<pubDate>([\s\S]*?)<\/pubDate>/); if (titleMatch) { items.push({ title: titleMatch[1].trim(), url: linkMatch ? linkMatch[1].trim() : "", pubDate: pubDateMatch ? pubDateMatch[1].trim() : "", }); } } return items.filter(item => { if (!item.pubDate) return true; const pubTime = new Date(item.pubDate).getTime(); const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000; return pubTime > threeDaysAgo; }).slice(0, 8); } catch { return []; } } async function fetchRecentNewsForCategory(forumSlug) { const query = NEWS_SEARCH_QUERIES[forumSlug]; if (!query) return []; console.log(`${ts()} Searching news: "${query}"`); const cnNews = await fetchGoogleNewsRSS(query, "zh-CN", "CN"); console.log(`${ts()} Found ${cnNews.length} recent Chinese news items`); return cnNews; } function buildPasserbyTopicPrompt(bot, forumName, forumDescription, newsItems) { const p = bot.personality; const newsText = newsItems.length > 0 ? newsItems.map((n, i) => `${i + 1}. [${n.pubDate || "近期"}] ${n.title}`).join("\n") : "(当前无实时新闻,请根据你的角色知识和行业常识,提出一个结合当前季节/月份/年份背景的时效性强的话题。注意话题中必须体现当前时间背景,不能看起来像旧帖子)"; const today = new Date().toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" }); const passerbyType = p.passerbyType || "observer"; const typeInstructions = { news: "你是一个善于发现行业动态的人。请从上面的近期新闻中选一个最有话题性的,用你的视角提出讨论。话题必须引用具体新闻事件,不能泛泛而谈", observer: "你是一个观察敏锐的人。请结合上面新闻中反映的行业趋势,提出一个值得深挖的问题。话题要有洞察力,引导大家思考现象背后的原因", problem: "你是一个正在这个行业里遇到实际困难的人。请结合上面的近期新闻和行业现状,提出一个具体的、有代表性的困扰或难题。问题要有真实感,让人感同身受", }; const instruction = typeInstructions[passerbyType] || typeInstructions.observer; return `你正在参与一个叫"追光AI行业论坛"的中文社区。今天是${today}。你现在在"${forumName}"板块发言。 ${forumDescription} [你的角色设定] 你是【${bot.displayName}】,${p.identity} 你的角色定位:${p.stance} 你说话的风格:${p.speakingStyle} 你的口头禅:${p.catchphrase} [近期行业新闻参考] ${newsText} [发言要求] ${instruction} 请以${bot.displayName}的身份在这个板块发起一个新话题。要求: 1. 话题标题10-20字,要像真人发的帖子,有真实感,不能写成新闻标题或论文标题 2. 内容180-400字,结合上面的新闻素材,有具体细节和你的个人感受 3. 如果引用了具体新闻,用自己的话说出来,不要像官方报道 4. 使用你的说话风格,自然口语化,可以有情绪和困惑 5. 结尾留一个问题引导大家参与讨论 [禁止事项] - 禁止使用"首先/其次/最后"等格式化连接词 - 禁止使用"从以下几个方面分析""综上所述"等教科书句式 - 禁止表现得像AI助手或官方新闻报道 - 话题中必须让人感受到"这是最近/今天发生的事",不能像泛泛而谈的老话题 ${p.forbiddenPatterns ? `- 禁止使用${p.forbiddenPatterns.join("、")}` : ""} 请只返回JSON格式,不要包含其他任何内容: { "title": "话题标题", "content": "话题内容" }`; } 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) && c.role !== "passerby" ); 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 processPasserbyForum(forumSlug, botCharacters) { const category = await getCategoryBySlug(forumSlug); if (!category) { console.log(`${ts()} Passerby: Forum "${forumSlug}" not found, skipping.`); return; } console.log(`${ts()} Passerby processing: ${category.name}`); const passerbyBots = botCharacters.filter( c => c.role === "passerby" && c.primaryForums && c.primaryForums.includes(forumSlug) ); if (passerbyBots.length === 0) { console.log(`${ts()} No passerby bots for this forum.`); return; } const recentNews = await fetchRecentNewsForCategory(forumSlug); if (recentNews.length === 0) { console.log(`${ts()} No recent news found, will generate from bot knowledge.`); } const posterBot = pick(passerbyBots); const botConf = await getBotWithConfig(posterBot.key); if (!botConf) { console.log(`${ts()} Passerby bot "${posterBot.key}" not found in DB.`); return; } console.log(`${ts()} ${posterBot.displayName} (${posterBot.personality.passerbyType}) creating topic...`); try { const recentMemories = await getRecentMemories(botConf.id, 2); const memoryCtx = buildMemoryContext(recentMemories, category.name); let prompt = buildPasserbyTopicPrompt(posterBot, category.name, category.description, recentNews); 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()} Passerby topic created: "${result.title}"`); await saveMemory(botConf.id, "experience", { action: "create_topic_passerby", forum: category.name, title: result.title, summary: result.content.substring(0, 100), newsCount: recentNews.length, }, 0.5); await logTask("create_topic_passerby", "success", `${posterBot.displayName}(${posterBot.personality.passerbyType}) posted in ${category.name}: ${result.title}`); } catch (err) { console.error(`${ts()} Passerby topic error:`, err.message); await logTask("create_topic_passerby", "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 expertForums = FORUM_SLUGS.sort(() => Math.random() - 0.5).slice(0, 2); 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, 1); console.log(`${ts()} Processing ${passerbyForums.length} forums for passerbys: ${passerbyForums.join(", ")}`); for (const forumSlug of passerbyForums) { await processPasserbyForum(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(); });