443 lines
20 KiB
JavaScript
443 lines
20 KiB
JavaScript
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);
|