Files
zhuiguang-ai/scripts/generate-passerby-roles.mjs

443 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);