210 lines
18 KiB
JavaScript
210 lines
18 KiB
JavaScript
// 样板数据:三城大师墙 + 高端文旅路线(成都 / 苏州 / 徽州)。
|
|
// 用法:node scripts/seed-masters.js (幂等,可反复跑)
|
|
import 'dotenv/config';
|
|
import { ObjectId } from 'mongodb';
|
|
import { connectDb, getDb, closeDb } from '../src/db.js';
|
|
|
|
const SEED = 'chengdu-master-wall'; // 沿用同一 seedTag,一键清旧重灌
|
|
|
|
// ── 大师(跨三城)────────────────────────────────────────────────
|
|
const MASTERS = [
|
|
// 成都
|
|
{ slug: 'zhucheng', displayName: '朱成', honorific: '朱成', domain: 'diaosu', region: '成都', masterTier: 'flagship',
|
|
tagline: '用石头留住一座城的记忆', masterworks: ['朱成石刻艺术博物馆·馆长', '万佛墙', '抢救性民间石刻造像收藏'], poolWeight: 2,
|
|
characterPrompt: '我是朱成,雕塑家,朱成石刻艺术博物馆馆长。几十年来我在抢救、收藏民间石刻造像,把散落各处的民间美学与信仰留下来。我谈艺术、谈收藏、谈一座城的记忆,语气朴实、有温度。',
|
|
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4', // 占位样片,替换为大师真实短片
|
|
tags: ['雕塑', '石刻', '民间博物馆', '成都'] },
|
|
{ slug: 'lijiang-art', displayName: '李江', honorific: '李江教授', domain: 'meishu', region: '成都', masterTier: 'resident',
|
|
tagline: '笔墨见性,画如其人', masterworks: ['美术学院系主任', '当代绘画'], poolWeight: 1,
|
|
characterPrompt: '我是李江,画家,美术学院系主任。我谈绘画语言、创作观、如何看一幅画。面对习作我会具体点评构图、笔性、色调、气韵,温和而犀利。',
|
|
tags: ['美术', '绘画', '当代艺术', '成都'] },
|
|
{ slug: 'tangjie', displayName: '鲁杰', honorific: '鲁杰先生', domain: 'gujian', region: '成都', masterTier: 'flagship',
|
|
tagline: '营造之道,藏在斗拱之间', masterworks: ['中国古建泰斗', '营造法式研究'], poolWeight: 1,
|
|
characterPrompt: '我是鲁杰,专研中国古建筑。我讲斗拱、营造法式、会馆与庙宇的形制,也讲古建的当代传承。语气沉稳、爱"论道"。',
|
|
tags: ['古建', '营造法式', '历史建筑', '成都'] },
|
|
// 苏州
|
|
{ slug: 'shenyun-su', displayName: '沈云', honorific: '沈云', domain: 'feiyi', region: '苏州', masterTier: 'flagship',
|
|
tagline: '以针代笔,一线一世界', masterworks: ['苏绣非遗代表性传承人', '双面绣'], poolWeight: 1,
|
|
characterPrompt: '我是沈云,苏绣非遗传承人。我谈刺绣的针法、丝理、配色与"以针代笔"的意境,也讲一幅绣品背后几个月的功夫与心境,温婉细致。',
|
|
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4', // 占位样片
|
|
tags: ['苏绣', '非遗', '刺绣', '苏州'] },
|
|
{ slug: 'chenyuan-su', displayName: '陈园', honorific: '陈园先生', domain: 'gujian', region: '苏州', masterTier: 'resident',
|
|
tagline: '移步换景,咫尺山林', masterworks: ['苏州园林营造研究', '叠山理水'], poolWeight: 1,
|
|
characterPrompt: '我是陈园,研究苏州园林营造。我讲叠山、理水、借景、移步换景,如何在方寸间造出咫尺山林,也讲文人园林的意趣。',
|
|
tags: ['园林', '造园', '古建', '苏州'] },
|
|
// 徽州
|
|
{ slug: 'humo-hui', displayName: '胡墨', honorific: '胡墨', domain: 'feiyi', region: '徽州', masterTier: 'flagship',
|
|
tagline: '一两徽墨一两金', masterworks: ['徽墨制作技艺传承人', '歙砚鉴藏'], poolWeight: 1,
|
|
characterPrompt: '我是胡墨,做徽墨、赏歙砚。我谈点烟、和胶、捣杵、描金的制墨古法,也讲文房四宝里的门道与文人风骨。',
|
|
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4', // 占位样片
|
|
tags: ['徽墨', '歙砚', '文房', '非遗', '徽州'] },
|
|
{ slug: 'fangying-hui', displayName: '方营', honorific: '方营先生', domain: 'gujian', region: '徽州', masterTier: 'resident',
|
|
tagline: '粉墙黛瓦马头墙', masterworks: ['徽派建筑营造研究', '徽州三雕'], poolWeight: 1,
|
|
characterPrompt: '我是方营,研究徽派建筑。我讲马头墙、天井、三雕(木雕/石雕/砖雕)与聚族而居的宗族空间,也讲徽商衣锦还乡如何营造一座村落。',
|
|
tags: ['徽派建筑', '古村落', '三雕', '徽州'] },
|
|
];
|
|
|
|
// ── 文旅路线(引用大师 slug)────────────────────────────────────
|
|
const TOURS = [
|
|
{
|
|
title: '成都·大师人文之旅(5天·私宴接待级)', region: '成都', tier: 'private',
|
|
priceHint: 50000, depositRate: 0.5, seats: 8,
|
|
masters: [['zhucheng', 2], ['lijiang-art', 1], ['tangjie', 1]],
|
|
highlights: ['三位泰斗级大师亲自陪同 + 私宴(市面不可复制)', '顶奢文化酒店(大慈寺旁·博舍级)+ 专属管家', '8 人极致小团 · 数字分身行前讲解 + 行后永续问道'],
|
|
included: ['5 晚顶奢文化住宿', '全程专属管家', '三场大师私宴/茶叙', '博物馆/画室/古建深度接待', '数字传承卡'],
|
|
itinerary: [
|
|
{ day: 1, title: '入蓉·序章:大慈千年', desc: '专属管家接机 → 入住文化酒店 → 大慈寺·太古里人文漫步 → 欢迎晚宴;朱成馆长出席茶叙破冰。' },
|
|
{ day: 2, title: '朱成·石头里的众生', desc: '朱成石刻艺术博物馆深度导览(万佛墙/民间造像),馆长亲自讲解 → 拓片/雕塑手作、大师点评 → 私宴。' },
|
|
{ day: 3, title: '李江·笔墨见性', desc: '蓝顶艺术区·李江画室,看画论画、大师现场示范 → 命题写生一对一指点 → 当代艺术下午茶雅集。' },
|
|
{ day: 4, title: '鲁杰·营造之道', desc: '洛带古镇客家会馆群(清代古建),鲁杰现场解构斗拱与营造法式 → "论道"沙龙 → 宽窄巷子夜话·私宴。' },
|
|
{ day: 5, title: '历史建筑·故事收官', desc: '安仁公馆群 / 都江堰-青城山古建,专家讲背后的家族与时代故事 → 结业茶会,赠"数字传承卡"。' },
|
|
],
|
|
masterMoments: ['D1 朱成茶叙破冰', 'D2 朱成馆长亲自导览 + 私宴论道', 'D3 李江面对面:示范、点评习作', 'D4 鲁杰论道:古建营造现场解构'],
|
|
guides: [
|
|
{ name: '朱明', note: '朱成之子', titles: ['博物馆研究博士', '一人博物馆创始人', '成都美术家协会副秘书长'] },
|
|
{ name: '熊婧彤', titles: ['省级青年建筑专家', '教授级高工', '成都历史建筑口述编导', '主播', '成都美术家协会副秘书长'] },
|
|
],
|
|
},
|
|
{
|
|
title: '姑苏·园林与绣艺之旅(4天·私宴接待级)', region: '苏州', tier: 'private',
|
|
priceHint: 38000, depositRate: 0.5, seats: 8,
|
|
masters: [['shenyun-su', 1], ['chenyuan-su', 1]],
|
|
highlights: ['苏绣非遗传承人亲授 + 园林营造大师同行', '拙政园旁精品园景酒店 · 专属管家', '昆曲雅集 · 数字分身行前讲解 + 行后问道'],
|
|
included: ['3 晚精品园景住宿', '全程专属管家', '苏绣工坊亲授', '名园深度私导 + 昆曲雅集', '数字传承卡'],
|
|
itinerary: [
|
|
{ day: 1, title: '入苏·序章:平江烟雨', desc: '专属管家接机 → 入住园景酒店 → 拙政园·平江路人文漫步 → 欢迎晚宴。' },
|
|
{ day: 2, title: '沈云·一针一世界', desc: '苏绣非遗工坊,沈云大师亲授看绣/试针 → 论"以针代笔"的意境 → 苏帮菜私宴。' },
|
|
{ day: 3, title: '陈园·园林之道', desc: '网师园/留园,陈园现场解构叠山理水、移步换景 → 夜游拙政园·昆曲雅集。' },
|
|
{ day: 4, title: '姑苏·故事收官', desc: '虎丘/寒山寺,专家讲背后故事 → 结业茶会 → 送机。' },
|
|
],
|
|
masterMoments: ['D2 沈云亲授苏绣 + 私宴', 'D3 陈园园林论道 + 昆曲雅集'],
|
|
},
|
|
{
|
|
title: '徽州·古村与文房之旅(5天·私宴接待级)', region: '徽州', tier: 'private',
|
|
priceHint: 45000, depositRate: 0.5, seats: 8,
|
|
masters: [['humo-hui', 1], ['fangying-hui', 1]],
|
|
highlights: ['徽墨非遗传承人亲授 + 徽派建筑大师同行', '宏村旁徽派古宅精品酒店 · 专属管家', '世界文化遗产古村落深度私导'],
|
|
included: ['4 晚徽派古宅住宿', '全程专属管家', '徽墨/歙砚工坊亲授', '宏村·西递深度私导', '数字传承卡'],
|
|
itinerary: [
|
|
{ day: 1, title: '入徽·序章:屯溪老街', desc: '专属管家接机 → 入住徽派古宅酒店 → 屯溪老街人文漫步 → 欢迎晚宴。' },
|
|
{ day: 2, title: '胡墨·文房四宝', desc: '徽墨/歙砚工坊,胡墨大师亲授点烟描金制墨、赏砚 → 论文房之道 → 徽菜私宴。' },
|
|
{ day: 3, title: '方营·粉墙黛瓦', desc: '宏村/西递古村落,方营现场解构徽派三雕与马头墙、天井 → 南湖书院茶叙。' },
|
|
{ day: 4, title: '徽州人文·宗族故事', desc: '徽州古城/棠樾牌坊群,专家讲徽商与宗族的时代故事。' },
|
|
{ day: 5, title: '徽州·山水收官', desc: '黟县木坑竹海 → 结业茶会,赠"数字传承卡" → 送机。' },
|
|
],
|
|
masterMoments: ['D2 胡墨亲授制墨 + 私宴', 'D3 方营徽派建筑现场论道 + 书院茶叙'],
|
|
},
|
|
];
|
|
|
|
// ── 众生墙:普通人示例分身(非大师,isMaster:false)──
|
|
const PEOPLE = [
|
|
{ slug: 'linxiaoman', displayName: '林小满', tagline: '一杯手冲,慢煮生活', tags: ['咖啡', '手冲', '独立小店', '生活方式'], likeCount: 7, likeSum: 32,
|
|
characterPrompt: '我是林小满,开了家只有八个座位的独立咖啡馆。我聊手冲、豆子、慢生活,也聊一个人开小店的酸甜。语气温柔、不急不躁。' },
|
|
{ slug: 'chenjiu', displayName: '陈九', tagline: '写代码的深夜,和你聊技术也聊人生', tags: ['程序员', '技术', '副业', '效率'], likeCount: 9, likeSum: 40,
|
|
characterPrompt: '我是陈九,写了八年后端。能陪你聊架构、聊 bug、聊程序员的副业和焦虑。理性、爱打比方,偶尔毒舌。' },
|
|
{ slug: 'suqing', displayName: '苏晴', tagline: '呼吸之间,找回身体的觉知', tags: ['瑜伽', '冥想', '身心', '疗愈'], likeCount: 6, likeSum: 29,
|
|
characterPrompt: '我是苏晴,教瑜伽和冥想。我引导你把注意力放回呼吸和身体,聊体式,也聊如何和自己相处。温和、笃定。' },
|
|
{ slug: 'amay', displayName: '阿May', tagline: '记录女儿的每一天,也记录我自己', tags: ['育儿', '亲子', '成长记录', '日常'], likeCount: 8, likeSum: 36,
|
|
characterPrompt: '我是阿May,一个一岁女儿的新手妈妈。分享育儿的手忙脚乱和小确幸,也想被人问问"你自己还好吗"。真诚、有点碎碎念。' },
|
|
{ slug: 'laozhou', displayName: '老周', tagline: '退休了,想把读过的书讲给你听', tags: ['读书', '文史', '退休生活', '讲书'], likeCount: 5, likeSum: 24,
|
|
characterPrompt: '我是老周,教了三十五年语文,刚退休。爱讲书、讲诗词、讲历史里的小人物,也想有人陪我聊聊天。慈祥、爱掉书袋。' },
|
|
{ slug: 'dapeng', displayName: '大鹏', tagline: '别急,我们从第一个深蹲开始', tags: ['健身', '增肌', '减脂', '自律'], likeCount: 7, likeSum: 30,
|
|
characterPrompt: '我是大鹏,健身教练。陪你从零开始练,聊动作、聊饮食、聊怎么坚持下去。热血、直接、爱鼓励人。' },
|
|
{ slug: 'xiawan', displayName: '夏晚', tagline: '背包走过 23 国,故事讲不完', tags: ['旅行', '摄影', '背包客', '攻略'], likeCount: 9, likeSum: 43,
|
|
characterPrompt: '我是夏晚,背包走过二十多个国家。聊路上的故事、省钱攻略、一个人旅行的胆量与孤独。松弛、爱讲段子。' },
|
|
{ slug: 'linyisheng', displayName: '林医生', tagline: '有些话,先说给我的分身听听', tags: ['心理', '情绪', '倾诉', '疗愈'], likeCount: 8, likeSum: 38,
|
|
characterPrompt: '我是林医生,心理咨询师。不评判,只陪你把情绪慢慢说清楚。温和、会倾听、常反问。注:我不做诊断,严重困扰请找线下专业帮助。' },
|
|
{ slug: 'xiaoye', displayName: '小野', tagline: '把家的味道,做给更多人', tags: ['美食', '家常菜', '下厨', '治愈'], likeCount: 6, likeSum: 27,
|
|
characterPrompt: '我是小野,爱做家常菜。聊怎么把一顿普通饭做得有温度,分享菜谱和厨房里的治愈时刻。热心、烟火气十足。' },
|
|
{ slug: 'azheng', displayName: '阿正', tagline: '教室到舞房,都是我的主场', tags: ['街舞', '校园', '青春', '兴趣'], likeCount: 7, likeSum: 31,
|
|
characterPrompt: '我是阿正,大三,跳街舞。聊舞蹈、校园、怎么在忙碌里坚持热爱。活力满满、爱用网络梗。' },
|
|
];
|
|
|
|
async function run() {
|
|
await connectDb();
|
|
const db = getDb();
|
|
|
|
// 幂等:清掉上次种子(只清本 seedTag 相关,不动后台手工建的数据)。
|
|
const oldTwins = await db.collection('twins').find({ seedTag: SEED }).project({ _id: 1 }).toArray();
|
|
const oldIds = oldTwins.map((t) => t._id);
|
|
const oldMasters = await db.collection('masters').find({ twinId: { $in: oldIds } }).project({ _id: 1 }).toArray();
|
|
await db.collection('agreements').deleteMany({ masterId: { $in: oldMasters.map((m) => m._id) } });
|
|
await db.collection('twins').deleteMany({ seedTag: SEED });
|
|
await db.collection('masters').deleteMany({ twinId: { $in: oldIds } });
|
|
await db.collection('tours').deleteMany({ seedTag: SEED });
|
|
|
|
const twinIdBySlug = {};
|
|
for (const m of MASTERS) {
|
|
const twinDoc = {
|
|
ownerId: new ObjectId(),
|
|
displayName: m.displayName, kind: 'clone', persona: '', characterPrompt: m.characterPrompt,
|
|
avatarUrl: null, voiceId: null, voiceModel: null, lang: 'zh',
|
|
isPublic: true, inPlaza: true, slug: m.slug, tagline: m.tagline, tags: m.tags,
|
|
verified: true, isMaster: true, masterTier: m.masterTier, domain: m.domain, region: m.region,
|
|
honorific: m.honorific, masterworks: m.masterworks, profession: null, videoUrl: m.videoUrl || null,
|
|
talents: { sub_talents: [], unlockedAt: null }, likeSum: 0, likeCount: 0,
|
|
seedTag: SEED, createdAt: new Date(), updatedAt: new Date(),
|
|
};
|
|
const r = await db.collection('twins').insertOne(twinDoc);
|
|
twinIdBySlug[m.slug] = r.insertedId;
|
|
const mr = await db.collection('masters').insertOne({
|
|
twinId: r.insertedId, status: 'live',
|
|
// ⚠ knowledge 必须是 false。这个脚本【一处知识库都没写】(全文 0 处 ingest)——
|
|
// 种子大师只有一句 characterPrompt 人设,知识库是空的。
|
|
// 此前这里写死 knowledge:true,等于代码在对自己的仪表盘撒谎:后台看到"知识库已就绪",
|
|
// 实际上任何人派分身来"向大师学习",学到的都只是 Qwen 借着人设即兴生成的通识。
|
|
// 要让它变成 true,只有一条路:真的把大师的资料喂进去(masters.js addMasterKnowledge)。
|
|
buildProgress: { photo: true, voice: true, knowledge: false, review: true },
|
|
poolWeight: m.poolWeight, offlineCommit: '每团出席茶叙/论道 1 次;私宴按排期配合。',
|
|
intro: m.characterPrompt, contact: {}, createdAt: new Date(), updatedAt: new Date(),
|
|
});
|
|
await db.collection('twins').updateOne({ _id: r.insertedId }, { $set: { masterId: mr.insertedId } });
|
|
await db.collection('agreements').insertMany([
|
|
{ masterId: mr.insertedId, kind: 'authorization',
|
|
scope: { useImage: true, useVoice: true, useKnowledge: true, canSay: [m.domain], cannotSay: ['政治', '医疗诊断', '投资建议'] },
|
|
revocable: true, term: { from: new Date(), to: null },
|
|
inheritance: '授权范围内,数字分身可在本人年迈/离世后继续用于文化传承讲解。',
|
|
aiDisclosure: '本页为大师授权的数字分身,非本人实时应答;深度交流以线下为准。',
|
|
signedAt: new Date(), createdAt: new Date() },
|
|
{ masterId: mr.insertedId, kind: 'cooperation',
|
|
cooperationSLA: { teaPerTour: 1, banquetPerYear: 4, receptionNote: '按团配合接待、茶叙/论道,私宴择期。' },
|
|
createdAt: new Date() },
|
|
]);
|
|
}
|
|
|
|
for (const t of TOURS) {
|
|
const ids = t.masters.map(([slug]) => twinIdBySlug[slug]);
|
|
await db.collection('tours').insertOne({
|
|
title: t.title, cover: null, region: t.region, masterIds: ids, tier: t.tier,
|
|
priceHint: t.priceHint, depositRate: t.depositRate,
|
|
revShare: { platform: 20, masters: 30, organizer: 50 },
|
|
masterShares: t.masters.map(([slug, weight]) => ({ masterId: twinIdBySlug[slug], weight })),
|
|
seats: t.seats, seatsLeft: t.seats, status: 'open',
|
|
highlights: t.highlights, included: t.included, itinerary: t.itinerary, masterMoments: t.masterMoments,
|
|
guides: t.guides || [],
|
|
seedTag: SEED, createdAt: new Date(), updatedAt: new Date(),
|
|
});
|
|
}
|
|
|
|
// ── 众生墙:普通人示例分身 ──
|
|
for (const p of PEOPLE) {
|
|
await db.collection('twins').insertOne({
|
|
ownerId: new ObjectId(),
|
|
displayName: p.displayName, kind: 'clone', persona: '', characterPrompt: p.characterPrompt,
|
|
avatarUrl: null, voiceId: null, voiceModel: null, lang: 'zh',
|
|
isPublic: true, inPlaza: true, slug: p.slug, tagline: p.tagline, tags: p.tags,
|
|
verified: false, isMaster: false, profession: null,
|
|
talents: { sub_talents: [], unlockedAt: null }, likeSum: p.likeSum, likeCount: p.likeCount,
|
|
seedTag: SEED, createdAt: new Date(), updatedAt: new Date(),
|
|
});
|
|
}
|
|
|
|
console.log('✓ 种子完成');
|
|
console.log(' 大师:', MASTERS.map((m) => `${m.displayName}(${m.region})`).join('、'));
|
|
console.log(' 路线:', TOURS.map((t) => `${t.region}¥${t.priceHint}`).join('、'));
|
|
console.log(' 众生墙普通人:', PEOPLE.map((p) => p.displayName).join('、'));
|
|
await closeDb();
|
|
}
|
|
|
|
run().catch((e) => { console.error(e); process.exit(1); });
|