import { readFileSync, writeFileSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const BOT_DATA_PATH = resolve(__dirname, "..", "data", "bot-characters.json"); // 行业板块配置 const FORUM_CONFIGS = { money: { name: "赚钱与副业", slugs: ["money-projects", "money-pitfalls", "money-models", "money-side-hustle", "money-freelance"] }, ecommerce: { name: "电商运营", slugs: ["ec-platform", "ec-livestream", "ec-product", "ec-supply", "ec-private", "ec-dtc"] }, "ai-tech": { name: "AI科技", slugs: ["ai-tools-app", "ai-startup-forum", "ai-llm", "ai-saas", "ai-agent"] }, career: { name: "职场创业", slugs: ["career-grow", "career-startup", "career-35", "career-transition", "career-remote"] }, "cross-border": { name: "跨境电商", slugs: ["cb-platform", "cb-logistics", "cb-local", "cb-brand", "cb-supply"] }, "real-estate": { name: "房产投资", slugs: ["re-buy", "re-invest", "re-market", "re-rent", "re-renovation"] }, content: { name: "自媒体内容", slugs: ["ct-short-video", "ct-live", "ct-mcn", "ct-writing", "ct-podcast"] }, "offline-biz": { name: "实体生意", slugs: ["ob-shop", "ob-food", "ob-service", "ob-franchise", "ob-location"] }, education: { name: "教育成长", slugs: ["edu-skills", "edu-mindset", "edu-books", "edu-online", "edu-kids"] }, "fire-life": { name: "FIRE与生活", slugs: ["fl-fire", "fl-invest", "fl-lifestyle", "fl-retire", "fl-balance"] } }; // 路人角色模板 const ROLE_TEMPLATES = [ { type: "newbie", suffix: "新手", identity: "刚入行{industry}不到半年,还在摸索中", stance: "大家好,我是新人,想请教一下{topic}的问题", expertise: ["学习能力", "信息搜集"], weakness: ["实战经验", "行业人脉", "资金"], catchphrase: "这个我还不懂", activityLevel: "high", replyChance: 0.5 }, { type: "observer", suffix: "观察者", identity: "关注{industry}动态{years}年,喜欢从旁观者角度看问题", stance: "我虽然不直接做这个,但看了很多案例,发现{insight}", expertise: ["案例分析", "趋势观察", "信息整合"], weakness: ["实操经验", "执行细节"], catchphrase: "我观察到一个现象", activityLevel: "medium", replyChance: 0.3 }, { type: "questioner", suffix: "求问者", identity: "在{industry}遇到瓶颈,想听听大家的建议", stance: "我现在的情况是{situation},不知道该怎么办", expertise: ["问题描述", "反思总结"], weakness: ["解决方案", "决策能力"], catchphrase: "大家怎么看", activityLevel: "medium", replyChance: 0.4 }, { type: "enthusiast", suffix: "爱好者", identity: "对{industry}有浓厚兴趣,业余时间都在研究这个", stance: "虽然这不是我的主业,但我真的很喜欢研究{topic}", expertise: ["热情", "自学能力", "信息搜集"], weakness: ["商业化", "实战经验"], catchphrase: "我最近在看", activityLevel: "high", replyChance: 0.4 }, { type: "skeptic", suffix: "质疑者", identity: "对{industry}的一些说法持怀疑态度,喜欢追问本质", stance: "你们说的这个我真的不太信,{doubt}", expertise: ["批判性思维", "逻辑分析"], weakness: ["执行力", "乐观主义"], catchphrase: "这真的靠谱吗", activityLevel: "low", replyChance: 0.2 }, { type: "learner", suffix: "学习者", identity: "正在系统学习{industry},报了好几个课还在消化中", stance: "我刚学了这个知识点,不知道理解得对不对{learning}", expertise: ["学习方法", "知识整理"], weakness: ["实战应用", "经验积累"], catchphrase: "我学到的是", activityLevel: "high", replyChance: 0.5 }, { type: "comparer", suffix: "对比者", identity: "喜欢对比不同{industry}玩法,研究各自的优劣", stance: "我对比了A和B两种方式,发现{comparison}", expertise: ["对比分析", "优劣势判断"], weakness: ["深度执行", "选择困难"], catchphrase: "我对比了一下", activityLevel: "medium", replyChance: 0.3 }, { type: "pragmatist", suffix: "务实派", identity: "在{industry}只关心能落地的方法,不喜欢空谈", stance: "别整那些虚的,就说{practical}", expertise: ["实操经验", "成本控制"], weakness: ["创新思维", "理论学习"], catchphrase: "具体怎么做", activityLevel: "medium", replyChance: 0.4 } ]; // 行业描述 const INDUSTRIES = { money: ["副业", "赚钱", "理财", "投资"], ecommerce: ["电商", "淘宝", "抖音", "拼多多"], "ai-tech": ["AI", "人工智能", "大模型", "AI应用"], career: ["职场", "创业", "自由职业", "远程工作"], "cross-border": ["跨境电商", "亚马逊", "独立站", "海外仓"], "real-estate": ["房产", "买房", "投资房", "装修"], content: ["自媒体", "短视频", "直播", "内容创作"], "offline-biz": ["实体店", "餐饮", "服务业", "线下生意"], education: ["学习", "教育", "培训", "知识付费"], "fire-life": ["FIRE", "理财", "提前退休", "生活平衡"] }; // 话题示例 const TOPICS = { money: ["副业选择", "理财规划", "成本控制", "盈利模式"], ecommerce: ["选品策略", "流量获取", "转化率优化", "供应链管理"], "ai-tech": ["AI工具使用", "AI创业方向", "大模型应用", "AI变现"], career: ["职业规划", "创业方向", "技能提升", "人脉建立"], "cross-border": ["平台选择", "物流方案", "本地化运营", "合规问题"], "real-estate": ["买房时机", "投资回报", "装修预算", "租金收益"], content: ["内容定位", "涨粉方法", "变现模式", "平台选择"], "offline-biz": ["选址策略", "成本控制", "获客方法", "复购提升"], education: ["学习方法", "课程选择", "知识管理", "技能变现"], "fire-life": ["理财策略", "被动收入", "生活成本", "退休规划"] }; // 洞察示例 const INSIGHTS = { money: ["真正赚钱的人都很低调", "副业最重要的是坚持", "理财要先学会省钱"], ecommerce: ["流量越来越贵了", "供应链才是核心", "复购比拉新重要"], "ai-tech": ["AI工具更新太快了", "真正落地的案例不多", "技术门槛在降低"], career: ["自由职业比想象的难", "人脉比能力重要", "持续学习是必须的"], "cross-border": ["合规成本越来越高", "本地化是关键", "物流时效很重要"], "real-estate": ["地段真的决定一切", "现金流比升值重要", "装修是个大坑"], content: ["内容同质化太严重了", "人设比内容重要", "变现要趁早"], "offline-biz": ["选址定生死", "成本控制是核心", "服务体验很重要"], education: ["学以致用最难", "系统学习比碎片学习有效", "实践出真知"], "fire-life": ["被动收入不容易", "生活成本要控制", "心态很重要"] }; // 情境描述 const SITUATIONS = { money: ["想做副业但不知道从哪开始", "有点闲钱但不知道怎么理财", "主业收入不够想增加收入来源"], ecommerce: ["店铺流量上不去", "转化率很低", "供应链经常出问题"], "ai-tech": ["想用AI但不知道怎么用", "学了几个工具但不知道能干嘛", "想创业但不知道方向"], career: ["工作遇到瓶颈想转行", "想创业但没方向", "想提升但不知道怎么学"], "cross-border": ["不知道选哪个平台", "物流成本太高", "不知道怎么做本地化"], "real-estate": ["不知道现在该不该买房", "投资房不知道选哪里", "装修预算超支了"], content: ["不知道做什么内容", "有流量但不知道怎么变现", "内容没灵感了"], "offline-biz": ["店里没客人", "成本太高不赚钱", "想开分店但没经验"], education: ["学了很多但用不上", "不知道学什么有用", "想系统学习但没时间"], "fire-life": ["不知道FIRE是否可行", "被动收入不够", "生活成本降不下来"] }; // 质疑点 const DOUBTS = { money: ["哪有那么多副业能赚钱", "理财不都是骗人的吗", "富人越富穷人越穷是真的吗"], ecommerce: ["现在做电商还来得及吗", "那些成功案例都是幸存者偏差吧", "平台规则改来改去谁能跟上"], "ai-tech": ["AI真的能替代人吗", "那些AI创业的不都是套壳吗", "大模型有什么实际用处"], career: ["自由职业真的自由吗", "创业九死一生是真的吗", "35岁危机是真的吗"], "cross-border": ["跨境电商真的能赚钱吗", "亚马逊越来越难做了吧", "独立站流量从哪来"], "real-estate": ["房价真的会跌吗", "投资房真的能赚钱吗", "装修真的能省钱吗"], content: ["自媒体真的能赚钱吗", "那些大V都是运气好吧", "内容创业是不是已经饱和了"], "offline-biz": ["实体店真的没出路了吗", "餐饮是不是太卷了", "选址真的那么重要吗"], education: ["知识付费是不是割韭菜", "学那么多真的有用吗", "在线教育真的好吗"], "fire-life": ["FIRE真的可行吗", "提前退休不会无聊吗", "被动收入真的稳定吗"] }; // 学习内容 const LEARNINGS = { money: ["理财要先学会记账", "副业要从兴趣开始", "投资要分散风险"], ecommerce: ["选品要看市场需求", "流量要多元化", "供应链要稳定"], "ai-tech": ["AI工具要从简单开始", "prompt很重要", "AI要结合实际场景"], career: ["职业规划要先了解自己", "创业要找对合伙人", "技能要持续更新"], "cross-border": ["平台规则要研究透", "物流要提前规划", "本地化要深入"], "real-estate": ["买房要看地段", "投资要算现金流", "装修要控制预算"], content: ["内容要有定位", "人设要一致", "变现要多元化"], "offline-biz": ["选址要人流量大", "成本要控制", "服务要好"], education: ["学习要有目标", "知识要系统化", "实践出真知"], "fire-life": ["理财要早开始", "被动收入要多元", "生活要简约"] }; // 对比内容 const COMPARISONS = { money: ["理财和投资的差别很大", "副业和主业的逻辑不一样", "省钱和赚钱同样重要"], ecommerce: ["淘宝和抖音的玩法完全不同", "标品和非标品的策略不一样", "流量和转化要平衡"], "ai-tech": ["不同AI工具有不同用途", "开源和闭源各有优劣", "技术和应用是两回事"], career: ["打工和创业差别很大", "自由职业和创业不一样", "技能和人脉都重要"], "cross-border": ["亚马逊和独立站逻辑不同", "FBA和自发货各有优劣", "欧美和东南亚市场差异大"], "real-estate": ["买房和租房逻辑不同", "新房和二手房各有优劣", "投资和自住考虑不同"], content: ["短视频和长视频逻辑不同", "图文和视频各有优劣", "涨粉和变现要平衡"], "offline-biz": ["餐饮和零售逻辑不同", "直营和加盟各有优劣", "选址和运营都重要"], education: ["线上和线下学习不同", "系统学习和碎片学习各有优劣", "理论和实践要结合"], "fire-life": ["激进和保守策略不同", "理财和生活要平衡", "提前退休和正常工作各有优劣"] }; // 务实内容 const PRACTICALS = { money: ["怎么低成本开始副业", "怎么控制日常开支", "怎么找到靠谱的理财渠道"], ecommerce: ["怎么找到好的供应商", "怎么提高转化率", "怎么降低物流成本"], "ai-tech": ["怎么用AI提高工作效率", "怎么找到AI落地场景", "怎么学习AI不落后"], career: ["怎么找到靠谱合伙人", "怎么提升核心竞争力", "怎么建立行业人脉"], "cross-border": ["怎么选择适合的平台", "怎么降低物流成本", "怎么做本地化运营"], "real-estate": ["怎么判断房价走势", "怎么计算投资回报", "怎么控制装修成本"], content: ["怎么找到内容定位", "怎么提高内容质量", "怎么实现内容变现"], "offline-biz": ["怎么选择好的位置", "怎么控制成本", "怎么提高复购率"], education: ["怎么高效学习", "怎么选择课程", "怎么把知识变现"], "fire-life": ["怎么增加被动收入", "怎么控制生活成本", "怎么规划退休生活"] }; // 名字生成 const SURNAMES = ["张", "王", "李", "赵", "刘", "陈", "杨", "黄", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "林", "罗"]; const GIVEN_NAMES_MALE = ["伟", "强", "磊", "军", "勇", "杰", "涛", "明", "辉", "鹏", "飞", "超", "浩", "志", "亮", "刚", "平", "琴", "林", "云"]; const GIVEN_NAMES_FEMALE = ["芳", "娜", "敏", "静", "丽", "强", "洁", "霞", "玲", "燕", "红", "萍", "琴", "云", "琳", "婷", "雪", "倩", "悦", "欣"]; // 生成随机名字 function generateName() { const surname = SURNAMES[Math.floor(Math.random() * SURNAMES.length)]; const isMale = Math.random() > 0.5; const givenNames = isMale ? GIVEN_NAMES_MALE : GIVEN_NAMES_FEMALE; const givenName = givenNames[Math.floor(Math.random() * givenNames.length)]; return surname + givenName; } // 生成头像提示 function generateAvatarPrompt(role, industry, age) { const gender = Math.random() > 0.5 ? "男性" : "女性"; const industryDesc = INDUSTRIES[industry][0]; const appearances = { newbie: `看起来比较年轻,穿着朴素,眼神中带着好奇`, observer: `穿着休闲,眼神敏锐,看起来善于观察`, questioner: `看起来有些焦虑,穿着普通,表情认真`, enthusiast: `穿着有个性,眼神热情,看起来很投入`, skeptic: `穿着简约,表情严肃,眼神质疑`, learner: `穿着学生气,拿着笔记本,看起来很认真`, comparer: `穿着商务休闲,拿着手机,看起来在对比什么`, pragmatist: `穿着朴实,表情务实,看起来很接地气` }; return `${age}岁中国${gender},${industryDesc}${role.suffix},${appearances[role.type]}`; } // 生成路人角色 function generatePasserby(index, forumSlug, roleTemplate) { const industry = INDUSTRIES[forumSlug]; const industryName = industry[Math.floor(Math.random() * industry.length)]; const forumName = FORUM_CONFIGS[forumSlug].name; const displayName = generateName(); const key = `passerby_${forumSlug}_${roleTemplate.type}_${index}`; const email = `bot_${key}@zhuiguang.ai`; const age = 22 + Math.floor(Math.random() * 20); const avatarPrompt = generateAvatarPrompt(roleTemplate, forumSlug, age); const years = 1 + Math.floor(Math.random() * 5); const topic = TOPICS[forumSlug][Math.floor(Math.random() * TOPICS[forumSlug].length)]; const insight = INSIGHTS[forumSlug][Math.floor(Math.random() * INSIGHTS[forumSlug].length)]; const situation = SITUATIONS[forumSlug][Math.floor(Math.random() * SITUATIONS[forumSlug].length)]; const doubt = DOUBTS[forumSlug][Math.floor(Math.random() * DOUBTS[forumSlug].length)]; const learning = LEARNINGS[forumSlug][Math.floor(Math.random() * LEARNINGS[forumSlug].length)]; const comparison = COMPARISONS[forumSlug][Math.floor(Math.random() * COMPARISONS[forumSlug].length)]; const practical = PRACTICALS[forumSlug][Math.floor(Math.random() * PRACTICALS[forumSlug].length)]; // 替换模板中的变量 const identity = roleTemplate.identity .replace("{industry}", industryName) .replace("{years}", years.toString()); let stance = roleTemplate.stance .replace("{industry}", industryName) .replace("{topic}", topic) .replace("{insight}", insight) .replace("{situation}", situation) .replace("{doubt}", doubt) .replace("{learning}", learning) .replace("{comparison}", comparison) .replace("{practical}", practical); const speakingStyles = { newbie: "谦虚好学,喜欢问问题,语气比较谨慎", observer: "客观冷静,喜欢从旁观者角度分析,不下结论", questioner: "焦虑但真诚,喜欢描述自己的困境,寻求建议", enthusiast: "热情洋溢,喜欢分享自己的发现,语气兴奋", skeptic: "质疑一切,喜欢追问本质,语气比较直接", learner: "认真记录,喜欢分享学习心得,语气谦虚", comparer: "理性分析,喜欢对比不同方案,语气客观", pragmatist: "直接务实,不喜欢空谈,语气接地气" }; // 选择2-3个论坛 const forumSlugs = FORUM_CONFIGS[forumSlug].slugs; const selectedForums = []; const forumCount = 2 + Math.floor(Math.random() * 2); const shuffled = [...forumSlugs].sort(() => Math.random() - 0.5); for (let i = 0; i < Math.min(forumCount, shuffled.length); i++) { selectedForums.push(shuffled[i]); } // 活跃时间 const activeHours = { weekday: [], weekend: [] }; const timeSlots = [ "07:00-09:00", "09:00-12:00", "12:00-14:00", "14:00-18:00", "18:00-20:00", "20:00-23:00" ]; const weekdayCount = 2 + Math.floor(Math.random() * 3); const shuffledSlots = [...timeSlots].sort(() => Math.random() - 0.5); for (let i = 0; i < Math.min(weekdayCount, shuffledSlots.length); i++) { activeHours.weekday.push(shuffledSlots[i]); } if (Math.random() > 0.3) { const weekendSlots = ["09:00-12:00", "14:00-18:00", "19:00-22:00"]; const weekendCount = 1 + Math.floor(Math.random() * 2); const shuffledWeekend = [...weekendSlots].sort(() => Math.random() - 0.5); for (let i = 0; i < Math.min(weekendCount, shuffledWeekend.length); i++) { activeHours.weekend.push(shuffledWeekend[i]); } } else { activeHours.weekend.push("不定时"); } return { key, displayName, email, role: "passerby", avatarPrompt, personality: { identity, expertise: [...roleTemplate.expertise], weakness: [...roleTemplate.weakness], stance, speakingStyle: speakingStyles[roleTemplate.type], catchphrase: roleTemplate.catchphrase, forbiddenPatterns: [], wordLimit: { min: 120 + Math.floor(Math.random() * 30), max: 350 + Math.floor(Math.random() * 100) }, passerbyType: roleTemplate.type }, primaryForums: selectedForums, activeHours, activityLevel: roleTemplate.activityLevel, replyChance: roleTemplate.replyChance }; } async function main() { console.log("开始生成新的路人角色..."); // 读取现有数据 const raw = readFileSync(BOT_DATA_PATH, "utf-8"); const data = JSON.parse(raw); const existingPasserbyCount = data.characters.filter(c => c.role === "passerby").length; console.log(`当前路人角色数量: ${existingPasserbyCount}`); // 需要增加的路人数量(2倍) const targetCount = existingPasserbyCount * 2; const newCount = targetCount - existingPasserbyCount; console.log(`需要新增路人角色: ${newCount}`); const newPasserbies = []; const forumSlugs = Object.keys(FORUM_CONFIGS); // 均匀分配到各个行业和角色类型 let index = 0; while (newPasserbies.length < newCount) { const forumSlug = forumSlugs[index % forumSlugs.length]; const roleTemplate = ROLE_TEMPLATES[Math.floor(index / forumSlugs.length) % ROLE_TEMPLATES.length]; const passerby = generatePasserby(index, forumSlug, roleTemplate); newPasserbies.push(passerby); index++; } console.log(`生成了 ${newPasserbies.length} 个新路人角色`); // 合并到现有数据 data.characters = [...data.characters, ...newPasserbies]; // 写回文件 writeFileSync(BOT_DATA_PATH, JSON.stringify(data, null, 2), "utf-8"); console.log(`完成!总角色数: ${data.characters.length}`); console.log(`路人角色数: ${data.characters.filter(c => c.role === "passerby").length}`); } main().catch(console.error);