Files
memind/scripts/seed-plaza-demo.mjs
T
John 6d99d762da Add image-designer skill, align billing to DeepSeek ×1, and enrich Plaza demo.
Introduce AI image generation with chat shortcut and agent API, improve MindSpace chat-to-page save resolution, and seed Plaza covers with production deploy scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:29:19 -07:00

358 lines
11 KiB
JavaScript

#!/usr/bin/env node
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createDbPool, initSchema } from '../db.mjs';
import { createPlazaPostService } from '../plaza-posts.mjs';
import { initializeDefaultSpace } from '../mindspace.mjs';
import { EXTRA_DEMO_POSTS } from './plaza-demo-posts.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));
const BASE_DEMO_POSTS = [
{
category: 'work-report',
title: 'Q2 产品增长复盘:从 0 到 10 万 DAU',
summary: '拆解增长漏斗、渠道投放 ROI 与留存曲线,附可复用的周报模板。',
tags: ['增长', '复盘', '产品'],
likes: 128,
comments: 24,
views: 3200,
author: 'john',
},
{
category: 'work-report',
title: 'AI 客服落地 30 天执行报告',
summary: '从需求澄清、知识库搭建到上线监控,记录真实踩坑与指标变化。',
tags: ['AI', '客服', '落地'],
likes: 86,
comments: 11,
views: 1840,
author: 'john',
},
{
category: 'work-report',
title: '供应链数字化周报 · 第 18 期',
summary: '库存周转、缺货预警与供应商协同进展,适合制造业团队参考。',
tags: ['供应链', '周报'],
likes: 45,
comments: 6,
views: 920,
author: 'admin',
},
{
category: 'study-notes',
title: 'Rust 所有权模型 15 分钟图解',
summary: '用生活类比讲清 borrow / move / lifetime,附练习题与答案。',
tags: ['Rust', '编程', '笔记'],
likes: 203,
comments: 37,
views: 5100,
author: 'john',
},
{
category: 'study-notes',
title: 'LLM Prompt 工程速查表',
summary: '角色设定、Few-shot、CoT、结构化输出,一张表搞定常见场景。',
tags: ['Prompt', 'LLM'],
likes: 312,
comments: 58,
views: 8900,
author: 'john',
},
{
category: 'creative',
title: '赛博水墨城市概念海报',
summary: 'MindSpace 生成的东方未来主义视觉,适合活动主 KV 参考。',
tags: ['设计', '海报', 'AI绘画'],
likes: 167,
comments: 19,
views: 4200,
author: 'john',
},
{
category: 'creative',
title: '独立音乐人首张 EP 视觉方案',
summary: '封面、社媒九宫格与演出海报的统一视觉语言。',
tags: ['音乐', '品牌'],
likes: 74,
comments: 9,
views: 1600,
author: 'admin',
},
{
category: 'business',
title: '社区咖啡店开业 7 天运营手记',
summary: '选址理由、爆款 SKU、会员拉新与坪效测算一页看懂。',
tags: ['咖啡', '开店'],
likes: 96,
comments: 15,
views: 2400,
author: 'john',
},
{
category: 'business',
title: '普拉提工作室 618 活动页',
summary: '课程包组合、限时优惠与预约转化话术,可直接复用结构。',
tags: ['健身', '618'],
likes: 58,
comments: 7,
views: 1300,
author: 'john',
},
{
category: 'travel',
title: '马来西亚 5 日轻量旅行攻略',
summary: '吉隆坡 + 槟城路线、美食清单与预算表,适合周末出发。',
tags: ['马来西亚', '攻略'],
likes: 241,
comments: 42,
views: 7600,
author: 'john',
},
{
category: 'travel',
title: '京都红叶季小众散步地图',
summary: '避开人潮的 8 条街道与 3 家深夜食堂,附交通换乘。',
tags: ['日本', '红叶'],
likes: 189,
comments: 31,
views: 5400,
author: 'admin',
},
{
category: 'data-analysis',
title: '电商大促 GMV 归因看板',
summary: '渠道、品类、客单三层下钻,附 SQL 与可视化截图说明。',
tags: ['电商', 'BI'],
likes: 112,
comments: 18,
views: 2900,
author: 'john',
},
{
category: 'data-analysis',
title: '用户留存 Cohort 分析模板',
summary: '按注册周看 D1/D7/D30 留存,含异常波动排查 checklist。',
tags: ['留存', '分析'],
likes: 77,
comments: 12,
views: 2100,
author: 'john',
},
{
category: 'lifestyle',
title: '极简桌面改造:提升专注的 12 个细节',
summary: '线缆收纳、光照、显示器高度与数字断舍离清单。',
tags: ['桌面', '效率'],
likes: 134,
comments: 21,
views: 3600,
author: 'john',
},
{
category: 'lifestyle',
title: '晨间写作 21 天挑战记录',
summary: '每天 20 分钟自由书写,记录情绪变化与可执行习惯。',
tags: ['写作', '习惯'],
likes: 63,
comments: 14,
views: 1500,
author: 'admin',
},
{
category: 'other',
title: '给未来的自己:2026 愿望清单',
summary: '学习、健康、关系与创作四个维度的年度目标墙。',
tags: ['年度规划'],
likes: 39,
comments: 8,
views: 980,
author: 'john',
},
{
category: 'other',
title: '开源副业项目冷启动笔记',
summary: '从 0 Star 到首批付费用户的 6 个关键动作。',
tags: ['开源', '副业'],
likes: 91,
comments: 16,
views: 2200,
author: 'john',
},
];
const DEMO_POSTS = [...BASE_DEMO_POSTS, ...EXTRA_DEMO_POSTS];
async function loadUser(pool, username) {
const [rows] = await pool.query(
`SELECT id, username, slug, display_name FROM h5_users WHERE username = ? LIMIT 1`,
[username],
);
return rows[0] ?? null;
}
async function ensureUserSpace(pool, userId) {
const [[space]] = await pool.query(
`SELECT id FROM h5_user_spaces WHERE user_id = ? LIMIT 1`,
[userId],
);
if (space?.id) return;
await initializeDefaultSpace(pool, userId);
}
async function createDemoPublication(pool, userId, demo, index) {
const now = Date.now() - index * 3_600_000;
const publicationId = crypto.randomUUID();
const pageId = crypto.randomUUID();
const pageVersionId = crypto.randomUUID();
const contentAssetId = crypto.randomUUID();
const assetVersionId = crypto.randomUUID();
const scanId = crypto.randomUUID();
const slug = `demo-${demo.category}-${index}-${publicationId.slice(0, 6)}`;
const [[space]] = await pool.query(
`SELECT id FROM h5_user_spaces WHERE user_id = ? LIMIT 1`,
[userId],
);
const [[category]] = await pool.query(
`SELECT id FROM h5_space_categories WHERE user_id = ? AND category_code = 'draft' LIMIT 1`,
[userId],
);
if (!space?.id || !category?.id) throw new Error(`用户 ${userId} 缺少空间或分类`);
const [[user]] = await pool.query(`SELECT slug, username FROM h5_users WHERE id = ? LIMIT 1`, [userId]);
const ownerSlug = user?.slug || user?.username || 'user';
const publicUrl = `/u/${ownerSlug}/pages/${slug}`;
await pool.query(
`INSERT INTO h5_assets
(id, user_id, space_id, category_id, asset_type, mime_type, original_filename, display_name,
current_version_id, size_bytes, checksum, visibility, status, source_type, created_at, updated_at)
VALUES (?, ?, ?, ?, 'content', 'text/markdown', 'page.md', ?, ?, 128, ?, 'private', 'ready', 'upload', ?, ?)`,
[contentAssetId, userId, space.id, category.id, demo.title, assetVersionId, 'demo-seed', now, now],
);
await pool.query(
`INSERT INTO h5_asset_versions
(id, asset_id, version_no, storage_key, size_bytes, checksum, mime_type, created_by, created_at)
VALUES (?, ?, 1, ?, 128, ?, 'text/markdown', ?, ?)`,
[assetVersionId, contentAssetId, `users/${userId}/assets/${contentAssetId}/v1`, 'demo-seed', userId, now],
);
await pool.query(
`INSERT INTO h5_page_records
(id, user_id, space_id, category_id, title, summary, page_type, template_id, status, visibility, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'article', 'editorial', 'published', 'public', ?, ?)`,
[pageId, userId, space.id, category.id, demo.title, demo.summary, now, now],
);
await pool.query(
`INSERT INTO h5_security_scans
(id, user_id, target_type, target_id, scanner_version, status, risk_level, findings_count, started_at, completed_at)
VALUES (?, ?, 'page_version', ?, 'seed', 'passed', 'none', 0, ?, ?)`,
[scanId, userId, pageVersionId, now, now],
);
await pool.query(
`INSERT INTO h5_page_versions
(id, page_id, version_no, content_asset_id, created_by, immutable, created_at)
VALUES (?, ?, 1, ?, ?, 1, ?)`,
[pageVersionId, pageId, contentAssetId, userId, now],
);
await pool.query(
`UPDATE h5_page_records SET current_version_id = ?, updated_at = ? WHERE id = ?`,
[pageVersionId, now, pageId],
);
await pool.query(
`INSERT INTO h5_publish_records
(id, user_id, page_id, page_version_id, url_slug, public_url, access_mode, published_at, status,
view_count, security_scan_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'public', ?, 'online', 0, ?, ?, ?)`,
[publicationId, userId, pageId, pageVersionId, slug, publicUrl, now, scanId, now, now],
);
return { publicationId, publicUrl };
}
async function bumpPostStats(pool, postId, demo) {
await pool.query(
`UPDATE plaza_posts
SET view_count = ?, like_count = ?, comment_count = ?, hot_score = ?, hot_updated_at = ?, updated_at = ?
WHERE id = ?`,
[demo.views, demo.likes, demo.comments, demo.likes * 3 + demo.comments * 5, Date.now(), Date.now(), postId],
);
}
const pool = createDbPool();
await initSchema(pool);
await createPlazaPostService(pool).ensureCategories();
const users = {};
for (const name of ['john', 'admin']) {
const user = await loadUser(pool, name);
if (!user) {
console.warn(`跳过:用户 ${name} 不存在`);
continue;
}
await ensureUserSpace(pool, user.id);
users[name] = user;
}
const plazaPosts = createPlazaPostService(pool);
let created = 0;
let skipped = 0;
for (const [index, demo] of DEMO_POSTS.entries()) {
const user = users[demo.author] ?? users.john;
if (!user) continue;
const { publicationId } = await createDemoPublication(pool, user.id, demo, index);
try {
const post = await plazaPosts.createPost(user.id, {
publication_id: publicationId,
category_slug: demo.category,
tags: demo.tags,
allow_comment: true,
});
await plazaPosts.reviewPost(post.id, 'approve');
await bumpPostStats(pool, post.id, demo);
created += 1;
console.log(`✓ [${demo.category}] ${demo.title}`);
} catch (error) {
if (error?.code === 'ALREADY_PUBLISHED') {
skipped += 1;
continue;
}
throw error;
}
}
const [pendingRows] = await pool.query(
`SELECT id FROM plaza_posts WHERE status = 'pending_review'`,
);
if (pendingRows.length > 0) {
for (const row of pendingRows) {
await plazaPosts.reviewPost(row.id, 'approve');
}
console.log(`✓ 已通过 ${pendingRows.length} 条待审核帖子`);
}
await pool.end();
console.log(`\n完成:新增 ${created} 条演示帖,跳过 ${skipped} 条。`);
console.log('刷新查看:http://localhost:3001/plaza');
console.log('职场报告:http://localhost:3001/plaza/cat/work-report');