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>
This commit is contained in:
John
2026-06-16 16:29:19 -07:00
parent 03690ee354
commit 6d99d762da
155 changed files with 2748 additions and 190 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ if (!process.env.DATABASE_URL) {
/** wx_mk4zzaps duplicate Finish billing (2026-06-16 session 20260616_600). */
const DUPLICATE_COST_CENTS = 1735;
const DUPLICATE_TOKENS = 838567;
/** Unique bills at 2/6 分/1k vs documented DeepSeek ×3 default (0.302/0.605 分/1k). */
/** Unique bills at 2/6 分/1k vs documented DeepSeek ×1 default (0.101/0.202 分/1k). */
const RATE_OVERCHARGE_CENTS = 1467;
async function main() {
+14 -2
View File
@@ -103,13 +103,15 @@ async function ensureBundle(conn, row, html) {
async function main() {
const pool = createDbPool();
const [rows] = await pool.query(
`SELECT pp.id AS post_id, pp.title,
`SELECT pp.id AS post_id, pp.title, pp.summary, pp.tags,
c.slug AS category_slug,
pr.id AS publication_id, pr.page_id, pr.page_version_id, pr.user_id, pr.status AS pub_status,
p.space_id, p.category_id, p.title AS page_title,
pv.bundle_asset_id,
(SELECT av.id FROM h5_asset_versions av
WHERE av.asset_id = pv.bundle_asset_id AND av.version_no = 1 LIMIT 1) AS bundle_version_id
FROM plaza_posts pp
JOIN plaza_categories c ON c.id = pp.category_id
JOIN h5_publish_records pr ON pr.id = pp.publication_id
JOIN h5_page_records p ON p.id = pr.page_id
JOIN h5_page_versions pv ON pv.id = pr.page_version_id
@@ -122,7 +124,17 @@ async function main() {
try {
await conn.beginTransaction();
for (const row of rows) {
const { summary, html, skipHtml } = getEnrichment(row.post_id, row.title);
let tags = [];
try {
tags = typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags ?? [];
} catch {
tags = [];
}
const { summary, html, skipHtml } = getEnrichment(row.post_id, row.title, {
categorySlug: row.category_slug,
summary: row.summary,
tags,
});
const summaryOnly = skipHtml && row.bundle_asset_id;
if (!summaryOnly) {
await ensureBundle(conn, row, html);
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# 安装 g2.tkmind.cn + plaza.tkmind.cn 生产 LaunchAgent(登录自启 + 崩溃自动重启)
#
# 架构:
# cloudflared → g2 :8090 (Caddy) → Portal :8081
# cloudflared → plaza :3001 (Next.js) → Portal :8081
#
# 用法:pnpm setup:prod-services
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOG_DIR="${HOME}/Library/Logs"
PORTAL_PLIST="${HOME}/Library/LaunchAgents/cn.tkmind.memind-portal.plist"
PLAZA_PLIST="${HOME}/Library/LaunchAgents/cn.tkmind.plaza.plist"
G2_LB_PLIST="${HOME}/Library/LaunchAgents/cn.tkmind.g2-lb.plist"
CF_PLIST="${HOME}/Library/LaunchAgents/com.cloudflare.cloudflared.plist"
PORTAL_RUN="${ROOT}/scripts/run-memind-portal-prod.sh"
PLAZA_RUN="${ROOT}/scripts/run-plaza-prod.sh"
CADDYFILE="${ROOT}/scripts/g2-lb.Caddyfile"
UID_NUM="$(id -u)"
GUI="gui/${UID_NUM}"
PATH_ENV="/opt/homebrew/bin:/opt/homebrew/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin"
chmod +x "${PORTAL_RUN}" "${PLAZA_RUN}" "${ROOT}/scripts/install-g2-lb.sh"
mkdir -p "${LOG_DIR}"
cat >"${PORTAL_PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>cn.tkmind.memind-portal</string>
<key>ProgramArguments</key>
<array>
<string>${PORTAL_RUN}</string>
</array>
<key>WorkingDirectory</key>
<string>${ROOT}</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>${PATH_ENV}</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>${LOG_DIR}/memind-portal.log</string>
<key>StandardErrorPath</key>
<string>${LOG_DIR}/memind-portal.log</string>
</dict>
</plist>
EOF
cat >"${PLAZA_PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>cn.tkmind.plaza</string>
<key>ProgramArguments</key>
<array>
<string>${PLAZA_RUN}</string>
</array>
<key>WorkingDirectory</key>
<string>${ROOT}</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>${PATH_ENV}</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>15</integer>
<key>StandardOutPath</key>
<string>${LOG_DIR}/plaza-prod.log</string>
<key>StandardErrorPath</key>
<string>${LOG_DIR}/plaza-prod.log</string>
</dict>
</plist>
EOF
if [[ ! -f "${G2_LB_PLIST}" ]]; then
cat >"${G2_LB_PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>cn.tkmind.g2-lb</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/caddy</string>
<string>run</string>
<string>--config</string>
<string>${CADDYFILE}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>${LOG_DIR}/g2-lb.log</string>
<key>StandardErrorPath</key>
<string>${LOG_DIR}/g2-lb.log</string>
</dict>
</plist>
EOF
fi
install_agent() {
local label="$1"
local plist="$2"
if launchctl print "${GUI}/${label}" >/dev/null 2>&1; then
launchctl bootout "${GUI}/${label}" 2>/dev/null || true
sleep 1
fi
launchctl bootstrap "${GUI}" "${plist}"
launchctl enable "${GUI}/${label}"
launchctl kickstart -k "${GUI}/${label}"
}
echo "==> 释放端口 8081 / 3001(避免与手动进程冲突)…"
for port in 8081 3001; do
lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
done
sleep 1
echo "==> 安装 LaunchAgent…"
install_agent "cn.tkmind.memind-portal" "${PORTAL_PLIST}"
sleep 2
install_agent "cn.tkmind.plaza" "${PLAZA_PLIST}"
install_agent "cn.tkmind.g2-lb" "${G2_LB_PLIST}"
if [[ -f "${CF_PLIST}" ]]; then
install_agent "com.cloudflare.cloudflared" "${CF_PLIST}"
else
echo "⚠ 未找到 ${CF_PLIST},请确认 cloudflared LaunchAgent 已配置" >&2
fi
echo "==> 等待服务就绪…"
for _ in $(seq 1 60); do
portal_ok=0
plaza_ok=0
g2_ok=0
curl -sf "http://127.0.0.1:8081/api/status" >/dev/null 2>&1 && portal_ok=1
curl -sf "http://127.0.0.1:3001/plaza" >/dev/null 2>&1 && plaza_ok=1
curl -sf "http://127.0.0.1:8090/api/status" >/dev/null 2>&1 && g2_ok=1
if [[ "${portal_ok}" -eq 1 && "${plaza_ok}" -eq 1 && "${g2_ok}" -eq 1 ]]; then
break
fi
sleep 2
done
echo ""
echo "=== 生产服务状态 ==="
launchctl list | grep -E "cn.tkmind\.(memind-portal|plaza|g2-lb)|com.cloudflare.cloudflared" || true
curl -s -o /dev/null -w "Portal :8081 → %{http_code}\n" "http://127.0.0.1:8081/api/status" || true
curl -s -o /dev/null -w "g2 LB :8090 → %{http_code}\n" "http://127.0.0.1:8090/api/status" || true
curl -s -o /dev/null -w "Plaza :3001 → %{http_code}\n" "http://127.0.0.1:3001/plaza" || true
curl -s -o /dev/null -w "外网 g2 → %{http_code}\n" "https://g2.tkmind.cn/api/status" || true
curl -s -o /dev/null -w "外网 plaza → %{http_code}\n" "https://plaza.tkmind.cn/plaza" || true
echo ""
echo "✅ 生产 LaunchAgent 已安装(登录自启 + KeepAlive"
echo " Portal cn.tkmind.memind-portal → ~/Library/Logs/memind-portal.log"
echo " Plaza cn.tkmind.plaza → ~/Library/Logs/plaza-prod.log"
echo " g2 LB cn.tkmind.g2-lb → ~/Library/Logs/g2-lb.log"
echo " 隧道 com.cloudflare.cloudflared"
echo ""
echo "管理命令:"
echo " launchctl kickstart -k ${GUI}/cn.tkmind.memind-portal"
echo " launchctl kickstart -k ${GUI}/cn.tkmind.plaza"
echo " tail -f ~/Library/Logs/memind-portal.log ~/Library/Logs/plaza-prod.log"
+578
View File
@@ -0,0 +1,578 @@
/** 各分类补充演示帖(每类 7 条,与 seed-plaza-demo.mjs 原有条目合并使用) */
export const EXTRA_DEMO_POSTS = [
// work-report +7
{
category: 'work-report',
title: '跨部门项目协同周报 · 模板与实践',
summary: '研发、产品、运营三方对齐模板,附 RACI 矩阵与风险升级机制。',
tags: ['协同', '周报', '模板'],
likes: 72,
comments: 14,
views: 2100,
author: 'john',
},
{
category: 'work-report',
title: '销售团队 Q3 业绩复盘与目标拆解',
summary: '区域完成率、客单价变化与 Q4 冲刺计划,含 Top10 客户分析。',
tags: ['销售', '复盘', 'Q3'],
likes: 54,
comments: 9,
views: 1680,
author: 'admin',
},
{
category: 'work-report',
title: '远程办公效率调研 · 2025 秋季',
summary: '200 人样本:专注时长、会议占比与工具偏好,给管理者的 5 条建议。',
tags: ['远程', '效率', '调研'],
likes: 98,
comments: 22,
views: 3400,
author: 'john',
},
{
category: 'work-report',
title: '产品经理年度 OKR 回顾与反思',
summary: '目标达成率 78%,未达成项根因与下年度优先级调整思路。',
tags: ['OKR', '产品', '复盘'],
likes: 115,
comments: 19,
views: 2800,
author: 'john',
},
{
category: 'work-report',
title: 'B2B SaaS 客户成功月报 · 9 月',
summary: 'NRR 112%、健康度红黄绿分布与 3 个流失预警案例复盘。',
tags: ['SaaS', '客户成功'],
likes: 67,
comments: 11,
views: 1950,
author: 'admin',
},
{
category: 'work-report',
title: '研发团队 Sprint 回顾 · 第 42 期',
summary: 'Velocity 趋势、阻塞项统计与下一 Sprint 容量规划建议。',
tags: ['敏捷', 'Sprint'],
likes: 43,
comments: 8,
views: 1200,
author: 'john',
},
{
category: 'work-report',
title: '品牌升级方案执行进度追踪',
summary: 'VI 落地、渠道物料与内部培训三节进度,附里程碑甘特图。',
tags: ['品牌', '项目管理'],
likes: 61,
comments: 10,
views: 1750,
author: 'admin',
},
// study-notes +7
{
category: 'study-notes',
title: 'TypeScript 类型体操 10 道经典题解析',
summary: 'Conditional、Mapped、Template Literal 从入门到面试高频。',
tags: ['TypeScript', '类型'],
likes: 186,
comments: 34,
views: 6200,
author: 'john',
},
{
category: 'study-notes',
title: '系统设计面试:分布式缓存怎么答',
summary: 'Cache-Aside、一致性、热点 Key 与穿透防护的标准答题框架。',
tags: ['系统设计', '面试'],
likes: 224,
comments: 41,
views: 7800,
author: 'john',
},
{
category: 'study-notes',
title: '线性代数直觉笔记 · 特征值与 PCA',
summary: '不用公式堆:用图像旋转理解特征分解与降维直觉。',
tags: ['数学', 'PCA'],
likes: 142,
comments: 28,
views: 4100,
author: 'admin',
},
{
category: 'study-notes',
title: 'Docker 容器化从零到部署',
summary: 'Dockerfile 最佳实践、Compose 多服务与生产环境注意事项。',
tags: ['Docker', 'DevOps'],
likes: 167,
comments: 31,
views: 5300,
author: 'john',
},
{
category: 'study-notes',
title: 'Pandas 数据分析速查手册',
summary: '读写、分组、透视、合并与可视化 30 个常用片段,可复制即用。',
tags: ['Python', 'Pandas'],
likes: 198,
comments: 36,
views: 6500,
author: 'john',
},
{
category: 'study-notes',
title: 'Git 团队分支规范与 Code Review',
summary: 'GitFlow vs Trunk、PR 模板与 Review 清单,适合 10 人以内团队。',
tags: ['Git', '规范'],
likes: 89,
comments: 17,
views: 2600,
author: 'admin',
},
{
category: 'study-notes',
title: '技术文档英语阅读训练计划',
summary: '21 天计划:RFC 段落拆解、术语表与每日 15 分钟精读练习。',
tags: ['英语', '文档'],
likes: 76,
comments: 15,
views: 2200,
author: 'john',
},
// creative +7
{
category: 'creative',
title: '国潮茶饮品牌 VIS 视觉系统',
summary: 'Logo 变体、杯身纹样与门店导视的统一设计语言。',
tags: ['品牌', 'VIS'],
likes: 134,
comments: 18,
views: 3800,
author: 'john',
},
{
category: 'creative',
title: '科幻短片《归途》分镜脚本',
summary: '12 场关键镜头描述、色调参考与音效设计备忘。',
tags: ['分镜', '科幻'],
likes: 92,
comments: 14,
views: 2400,
author: 'admin',
},
{
category: 'creative',
title: '手作 ceramics 工作室品牌摄影',
summary: '自然光布光、肌理特写与产品叙事的三组拍摄方案。',
tags: ['摄影', '陶瓷'],
likes: 108,
comments: 12,
views: 2900,
author: 'john',
},
{
category: 'creative',
title: '城市夜景长曝光摄影系列',
summary: '车轨、霓虹反射与 30s 曝光参数对照,附后期调色流程。',
tags: ['摄影', '夜景'],
likes: 156,
comments: 24,
views: 4500,
author: 'john',
},
{
category: 'creative',
title: '极简 UI 图标集 · 200 Icons',
summary: '2px 线宽、24×24 网格的线性图标库,含 Figma 组件规范。',
tags: ['UI', '图标'],
likes: 178,
comments: 29,
views: 5100,
author: 'admin',
},
{
category: 'creative',
title: '水墨风 AI 插画实验合集',
summary: 'Prompt 控制笔触、留白与构图的 8 组可复现参数。',
tags: ['AI绘画', '水墨'],
likes: 143,
comments: 21,
views: 3900,
author: 'john',
},
{
category: 'creative',
title: '复古胶片人像调色预设包',
summary: 'Kodak / Fuji 模拟 LUT + Lightroom 预设,适合户外人像。',
tags: ['调色', '胶片'],
likes: 87,
comments: 13,
views: 2100,
author: 'admin',
},
// business +7
{
category: 'business',
title: '精酿啤酒馆开业 30 天数据复盘',
summary: '坪效、复购率与爆款酒款分析,第二个月优化动作清单。',
tags: ['精酿', '复盘'],
likes: 82,
comments: 12,
views: 2300,
author: 'john',
},
{
category: 'business',
title: '日式美甲沙龙会员体系设计',
summary: '储值梯度、生日礼遇与转介绍奖励的完整规则说明。',
tags: ['美甲', '会员'],
likes: 64,
comments: 9,
views: 1700,
author: 'admin',
},
{
category: 'business',
title: '社区健身房私教转化漏斗优化',
summary: '体验课到年卡的三步话术与异议处理,转化率 +18pp。',
tags: ['健身', '转化'],
likes: 71,
comments: 11,
views: 2000,
author: 'john',
},
{
category: 'business',
title: '独立书店文创周边选品策略',
summary: '本地作者、手账与联名款组合,提升客单而不损品牌调性。',
tags: ['书店', '文创'],
likes: 93,
comments: 16,
views: 2600,
author: 'john',
},
{
category: 'business',
title: '宠物洗护上门服务 SOP 手册',
summary: '预约、消毒、服务流程与客户沟通标准,可直接培训使用。',
tags: ['宠物', 'SOP'],
likes: 55,
comments: 8,
views: 1500,
author: 'admin',
},
{
category: 'business',
title: '轻食沙拉店午餐高峰运营方案',
summary: '备料节奏、动线设计与套餐组合,12:00–13:30 翻台率提升 35%。',
tags: ['轻食', '运营'],
likes: 68,
comments: 10,
views: 1850,
author: 'john',
},
{
category: 'business',
title: '民宿旺季定价与房源包装指南',
summary: '动态定价区间、首图拍摄与评价回复模板,适合 5 间以内体量。',
tags: ['民宿', '定价'],
likes: 79,
comments: 14,
views: 2200,
author: 'admin',
},
// travel +7
{
category: 'travel',
title: '冰岛环岛 10 日自驾攻略',
summary: 'Ring Road 路线、天气应对与必看瀑布/冰川清单,含预算表。',
tags: ['冰岛', '自驾'],
likes: 267,
comments: 48,
views: 9200,
author: 'john',
},
{
category: 'travel',
title: '清迈慢生活 4 日深度游',
summary: '古城寺庙、宁曼路咖啡与周日夜市,避开网红排队点。',
tags: ['泰国', '清迈'],
likes: 198,
comments: 35,
views: 6800,
author: 'admin',
},
{
category: 'travel',
title: '西藏林芝桃花季摄影路线',
summary: '3 月下旬最佳机位、海拔适应与装备清单,附样片参数。',
tags: ['西藏', '摄影'],
likes: 223,
comments: 39,
views: 7500,
author: 'john',
},
{
category: 'travel',
title: '新加坡亲子 3 日轻松游',
summary: '动物园、滨海湾与本地美食,推车友好路线与交通卡攻略。',
tags: ['新加坡', '亲子'],
likes: 156,
comments: 27,
views: 4900,
author: 'john',
},
{
category: 'travel',
title: '云南大理丽江 6 日省钱攻略',
summary: '淡季住宿、拼车与必去小众点,人均 ¥2800 舒适玩法。',
tags: ['云南', '省钱'],
likes: 184,
comments: 33,
views: 6100,
author: 'admin',
},
{
category: 'travel',
title: '釜山海鲜美食 48 小时',
summary: '札嘎其市场、甘川文化村与海边 Cafe,周末快闪指南。',
tags: ['韩国', '美食'],
likes: 142,
comments: 24,
views: 4300,
author: 'john',
},
{
category: 'travel',
title: '阿尔卑斯徒步 · 少女峰周边',
summary: 'Easy / Moderate 两条路线、齿轮火车时刻与登山装备建议。',
tags: ['瑞士', '徒步'],
likes: 201,
comments: 36,
views: 6700,
author: 'admin',
},
// data-analysis +7
{
category: 'data-analysis',
title: '用户行为漏斗分析实战案例',
summary: '注册→激活→付费五步漏斗,定位最大流失环节与优化假设。',
tags: ['漏斗', '行为分析'],
likes: 134,
comments: 23,
views: 3600,
author: 'john',
},
{
category: 'data-analysis',
title: 'A/B 测试显著性检验指南',
summary: '样本量计算、p-value 误读与 Bayesian 备选方案对照。',
tags: ['AB测试', '统计'],
likes: 98,
comments: 17,
views: 2800,
author: 'admin',
},
{
category: 'data-analysis',
title: '销售预测模型:ARIMA vs Prophet',
summary: '同一数据集两种模型对比,含 Python 代码与误差指标。',
tags: ['预测', '时序'],
likes: 87,
comments: 15,
views: 2500,
author: 'john',
},
{
category: 'data-analysis',
title: '文本情感分析:评论洞察报告',
summary: '电商评论 NLP pipeline,主题聚类与负面词云解读。',
tags: ['NLP', '情感分析'],
likes: 76,
comments: 13,
views: 2200,
author: 'john',
},
{
category: 'data-analysis',
title: '实时大屏指标设计与 SQL 模板',
summary: 'GMV、DAU、转化率三类大屏的指标定义与刷新策略。',
tags: ['大屏', 'SQL'],
likes: 112,
comments: 19,
views: 3100,
author: 'admin',
},
{
category: 'data-analysis',
title: '营销归因:Shapley vs Markov',
summary: '多触点归因两种方法适用场景与结果差异案例。',
tags: ['归因', '营销'],
likes: 69,
comments: 12,
views: 1900,
author: 'john',
},
{
category: 'data-analysis',
title: '数据质量监控规则库搭建',
summary: '空值、重复、时效性三类规则 + 告警分级与值班流程。',
tags: ['数据质量', '监控'],
likes: 83,
comments: 14,
views: 2400,
author: 'admin',
},
// lifestyle +7
{
category: 'lifestyle',
title: '厨房收纳改造:小空间大利用',
summary: '垂直收纳、标签系统与动线优化,周末 4 小时完成。',
tags: ['收纳', '厨房'],
likes: 121,
comments: 19,
views: 3300,
author: 'john',
},
{
category: 'lifestyle',
title: '30 天早睡挑战执行记录',
summary: '23:30 前入睡、屏幕宵禁与睡眠质量变化曲线。',
tags: ['睡眠', '习惯'],
likes: 88,
comments: 16,
views: 2400,
author: 'admin',
},
{
category: 'lifestyle',
title: '周末徒步装备清单与路线',
summary: '一日轻徒步:鞋服、补给与安全装备,附 3 条城市周边路线。',
tags: ['徒步', '户外'],
likes: 102,
comments: 18,
views: 2900,
author: 'john',
},
{
category: 'lifestyle',
title: '阳台花园从零搭建指南',
summary: '光照评估、盆器选择与前 10 种好养植物推荐。',
tags: ['园艺', '阳台'],
likes: 95,
comments: 17,
views: 2700,
author: 'john',
},
{
category: 'lifestyle',
title: '断舍离:衣柜 100 件到 40 件',
summary: '胶囊衣橱原则、捐赠流程与 30 天不再购衣挑战记录。',
tags: ['断舍离', '极简'],
likes: 113,
comments: 22,
views: 3200,
author: 'admin',
},
{
category: 'lifestyle',
title: '手冲咖啡入门 7 日练习',
summary: '研磨度、水温与注水节奏对照表,从酸到平衡的味觉笔记。',
tags: ['咖啡', '手冲'],
likes: 79,
comments: 14,
views: 2100,
author: 'john',
},
{
category: 'lifestyle',
title: '周末窑烤面包新手日记',
summary: '天然酵母起种、折叠节奏与第一次成功欧包的全记录。',
tags: ['烘焙', '面包'],
likes: 91,
comments: 16,
views: 2500,
author: 'admin',
},
// other +7
{
category: 'other',
title: '自由职业者财务规划入门',
summary: '现金流缓冲、税筹与保险配置,适合刚转自由职业的前 12 个月。',
tags: ['自由职业', '财务'],
likes: 68,
comments: 12,
views: 1900,
author: 'john',
},
{
category: 'other',
title: '播客频道从 0 到 1000 订阅',
summary: '选题、剪辑流程与跨平台分发,第 8 集突破 1000 订阅复盘。',
tags: ['播客', '创作'],
likes: 74,
comments: 13,
views: 2100,
author: 'admin',
},
{
category: 'other',
title: '城市观鸟入门:常见 20 种鸟',
summary: '公园/湿地观测点、识别特征与入门望远镜选购建议。',
tags: ['观鸟', '自然'],
likes: 52,
comments: 9,
views: 1400,
author: 'john',
},
{
category: 'other',
title: '二手交易向可持续生活转型',
summary: '闲鱼/多抓鱼使用心得与「买前问三遍」决策清单。',
tags: ['可持续', '二手'],
likes: 61,
comments: 11,
views: 1700,
author: 'john',
},
{
category: 'other',
title: '个人知识库搭建方法论',
summary: 'PARA + 渐进式总结,Obsidian/Notion 选型与迁移经验。',
tags: ['知识管理', 'PKM'],
likes: 129,
comments: 24,
views: 3500,
author: 'admin',
},
{
category: 'other',
title: 'VR 健身 60 天体验报告',
summary: 'Beat Saber + Supernatural 组合,体重与体态变化数据。',
tags: ['VR', '健身'],
likes: 84,
comments: 15,
views: 2300,
author: 'john',
},
{
category: 'other',
title: '社群运营:从群聊到价值观',
summary: '冷启动、活跃机制与冲突处理,200 人社群 6 个月实践。',
tags: ['社群', '运营'],
likes: 97,
comments: 18,
views: 2700,
author: 'admin',
},
];
+225 -1
View File
@@ -845,9 +845,21 @@ export const ENRICHMENTS = {
},
};
export function getEnrichment(postId, fallbackTitle) {
export function getEnrichment(postId, fallbackTitle, meta = {}) {
const item = ENRICHMENTS[postId];
if (item) return item;
const categorySlug = meta.categorySlug ?? meta.category_slug ?? '';
const summary = meta.summary ?? '';
const tags = meta.tags ?? [];
if (categorySlug) {
return {
summary: summary || `${fallbackTitle} — MindSpace 精选内容`,
html: buildCategoryPage(categorySlug, fallbackTitle, summary || fallbackTitle, tags),
};
}
return {
summary: `${fallbackTitle} — MindSpace 精选`,
html: buildJohnPage({
@@ -860,3 +872,215 @@ export function getEnrichment(postId, fallbackTitle) {
}),
};
}
const CATEGORY_THEMES = {
'work-report': {
tag: '职场报告',
tagEmoji: '📊',
accent: '#2d7d6a',
accent2: '#4ecdc4',
images: [IMG.growth, IMG.ai, IMG.supply, IMG.guide],
},
'study-notes': {
tag: '学习笔记',
tagEmoji: '📚',
accent: '#f97316',
accent2: '#fb923c',
images: [IMG.rust, IMG.llm, IMG.guide, IMG.opensource],
},
creative: {
tag: '创意作品',
tagEmoji: '🎨',
accent: '#c084fc',
accent2: '#a855f7',
images: [IMG.cyber, IMG.music, IMG.guide],
},
business: {
tag: '门店展示',
tagEmoji: '🏪',
accent: '#d97706',
accent2: '#fbbf24',
images: [IMG.coffee, IMG.pilates, IMG.guide],
},
travel: {
tag: '旅行攻略',
tagEmoji: '✈️',
accent: '#2d7d6a',
accent2: '#4ecdc4',
images: [IMG.malaysia, IMG.kyoto, IMG.travel, IMG.guide],
},
'data-analysis': {
tag: '数据分析',
tagEmoji: '📈',
accent: '#38bdf8',
accent2: '#0ea5e9',
images: [IMG.data, IMG.growth, IMG.guide],
},
lifestyle: {
tag: '生活记录',
tagEmoji: '🌿',
accent: '#2d7d6a',
accent2: '#4ecdc4',
images: [IMG.desk, IMG.write, IMG.guide],
},
other: {
tag: '其他',
tagEmoji: '💡',
accent: '#34d399',
accent2: '#10b981',
images: [IMG.opensource, IMG.wish, IMG.guide],
},
};
function hashTitle(title) {
let hash = 0;
for (let i = 0; i < title.length; i += 1) {
hash = (hash * 31 + title.charCodeAt(i)) >>> 0;
}
return hash;
}
function buildHeroTitleHtml(title) {
const trimmed = String(title).trim();
if (trimmed.length <= 6) {
return `<em>${esc(trimmed)}</em>`;
}
const splitAt = Math.min(8, Math.ceil(trimmed.length * 0.42));
const head = trimmed.slice(0, splitAt);
const tail = trimmed.slice(splitAt);
return `${esc(head)}<em>${esc(tail)}</em>`;
}
function buildCategorySections(categorySlug, title, summary, tags) {
const tagList =
tags.length > 0
? `<ul>${tags.map((tag) => `<li>${esc(tag)}</li>`).join('')}</ul>`
: '';
const metricsPool = {
'work-report': [
{ value: '92%', label: '目标完成', note: '本期 OKR' },
{ value: '3', label: '关键实验', note: '已验证假设' },
{ value: '↓18%', label: '阻塞时长', note: '较上期' },
],
'study-notes': [
{ value: '15min', label: '阅读时长', note: '核心章节' },
{ value: '8', label: '要点', note: '可复用片段' },
{ value: '3', label: '练习题', note: '附参考答案' },
],
creative: [
{ value: '4K', label: '输出规格', note: '主视觉' },
{ value: '3', label: '延展场景', note: '社媒 / 印刷' },
{ value: '12', label: '配色', note: '品牌体系' },
],
business: [
{ value: '+24%', label: '转化提升', note: '活动期' },
{ value: '4.8', label: '评分', note: '5 分制' },
{ value: '86%', label: '复购意向', note: '问卷样本' },
],
travel: [
{ value: '5-10', label: '行程天数', note: '弹性安排' },
{ value: '¥680', label: '日均预算', note: '舒适型参考' },
{ value: '12+', label: '精选点位', note: '含美食' },
],
'data-analysis': [
{ value: '3层', label: '下钻维度', note: '渠道/品类/客单' },
{ value: '99%', label: '数据覆盖', note: '核心指标' },
{ value: 'T+1', label: '刷新频率', note: '看板更新' },
],
lifestyle: [
{ value: '21天', label: '挑战周期', note: '习惯养成' },
{ value: '12', label: '实践要点', note: '可执行清单' },
{ value: '↓32%', label: '焦虑自评', note: '挑战前后' },
],
other: [
{ value: '6', label: '关键动作', note: '冷启动路径' },
{ value: '1000+', label: '读者', note: '累计触达' },
{ value: '4.6', label: '满意度', note: '反馈均值' },
],
};
const cardsPool = {
'work-report': [
{ title: '背景与目标', desc: '明确本期业务目标与衡量口径,对齐 stakeholder 预期。', tag: '目标' },
{ title: '执行进展', desc: '按里程碑拆解完成度,标注风险与依赖项。', tag: '进展' },
{ title: '数据洞察', desc: '用 3 个核心指标讲清「发生了什么、为什么」。', tag: '洞察' },
{ title: '下一步', desc: '列出 2–3 个可验证的下一周期动作。', tag: '行动' },
],
'study-notes': [
{ title: '核心概念', desc: '用一句话 + 生活类比建立直觉,再展开细节。', tag: '概念' },
{ title: '常见误区', desc: '面试与实践中高频踩坑点对照表。', tag: '避坑' },
{ title: '代码/例题', desc: '可复制片段,标注输入输出与边界条件。', tag: '实践' },
],
travel: [
{ title: 'Day 12', desc: '抵达与市区精华,控制步行强度。', tag: '行程' },
{ title: 'Day 34', desc: '深度体验与小众点位,预留弹性时间。', tag: '深度' },
{ title: '美食地图', desc: '早/午/晚各 2 家备选,标注人均与排队时段。', tag: '美食' },
],
creative: [
{ title: '概念阐述', desc: '设计意图、受众与使用场景说明。', tag: '概念' },
{ title: '视觉规范', desc: '字体、配色、网格与留白规则。', tag: '规范' },
{ title: '延展应用', desc: '海报、社媒、印刷等多场景适配方案。', tag: '延展' },
],
};
const sections = [
{
type: 'metrics',
title: '核心亮点',
hint: title,
items: metricsPool[categorySlug] ?? metricsPool.other,
},
];
if (cardsPool[categorySlug]) {
sections.push({
type: 'cards',
title: '内容结构',
hint: summary.slice(0, 60),
items: cardsPool[categorySlug],
});
}
sections.push({
type: 'text',
title: '详细说明',
html: `<p>${esc(summary)}</p>${tagList}`,
});
sections.push({
type: 'quote',
html: `${esc(title)}」—— 由 <span style="color:#4ecdc4">MindSpace</span> 创作并公开发布到 TKMind 广场。`,
});
return sections;
}
export function buildCategoryPage(categorySlug, title, summary, tags = []) {
const theme = CATEGORY_THEMES[categorySlug] ?? CATEGORY_THEMES.other;
const heroImage = theme.images[hashTitle(title) % theme.images.length];
const bgDark = categorySlug === 'creative' ? '#0f0a1a' : '#0a1628';
return buildJohnPage({
title,
description: summary,
tag: theme.tag,
tagEmoji: theme.tagEmoji,
accent: theme.accent,
accent2: theme.accent2,
bgDark,
heroImage,
heroTitleHtml: buildHeroTitleHtml(title),
heroSubtitle: summary,
heroBtn: '开始阅读',
sections: buildCategorySections(categorySlug, title, summary, tags),
footer: 'TKMind Plaza · MindSpace 公开发布',
});
}
// legacy export — post_id keyed enrichments kept for existing seeded posts
export function getEnrichmentLegacy(postId, fallbackTitle) {
const item = ENRICHMENTS[postId];
if (item) return item;
return getEnrichment(postId, fallbackTitle);
}
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Memind Portal 生产进程(g2.tkmind.cn / Plaza API 共用 :8081
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ -f "${ROOT}/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "${ROOT}/.env"
set +a
fi
export NODE_ENV=production
export H5_PORT="${H5_PORT:-8081}"
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@22/bin/node}"
if [[ ! -x "${NODE_BIN}" ]]; then
NODE_BIN="$(command -v node)"
fi
exec "${NODE_BIN}" server.mjs
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Plaza Next.js 生产进程(plaza.tkmind.cn → Cloudflare Tunnel :3001
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ -f "${ROOT}/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "${ROOT}/.env"
set +a
fi
export NODE_ENV=production
PORTAL_PORT="${H5_PORT:-8081}"
PLAZA_PORT="${PLAZA_PORT:-3001}"
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@22/bin/node}"
if [[ ! -x "${NODE_BIN}" ]]; then
NODE_BIN="$(command -v node)"
fi
echo "==> 等待 Portal :${PORTAL_PORT}"
for _ in $(seq 1 120); do
if curl -sf "http://127.0.0.1:${PORTAL_PORT}/auth/status" >/dev/null 2>&1; then
break
fi
sleep 1
done
if ! curl -sf "http://127.0.0.1:${PORTAL_PORT}/auth/status" >/dev/null 2>&1; then
echo "Portal 未就绪 (:${PORTAL_PORT})Plaza 退出" >&2
exit 1
fi
exec "${NODE_BIN}" scripts/start-plaza-prod.mjs -- --skip-build --no-portal
+108 -8
View File
@@ -23,11 +23,96 @@ function loadEnvFile(filePath) {
loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));
const COVER_QUALITY = 'auto=format&fit=crop&w=1920&h=1200&q=90';
const CATEGORY_IMAGE_POOLS = {
'work-report': [
`https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1460925895917-afdab827c52f?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1551288049-bebda4e38f71?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1522071820081-009f0129c71c?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1504384308090-c894fdcc538d?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1542744173-8e7e53415bb0?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1600880292203-757bb62b4baf?${COVER_QUALITY}`,
],
'study-notes': [
`https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1516116216624-53e697fedbea?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1515879214367-846362d3526?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1676299085922-0a9a504459f8?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1516321318423-f06f85e504b3?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1434030216411-0b793f4b4173?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1503676260728-1c00da094a0b?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1524995998267-8169c4e50d05?${COVER_QUALITY}`,
],
creative: [
`https://images.unsplash.com/photo-1513364776144-60967b0f800f?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1514565131-fce0801e5785?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1511379938544-be09687a4d4b?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1541961017774-22349e4a1262?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1561998338-13ad7886534d?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?${COVER_QUALITY}`,
],
business: [
`https://images.unsplash.com/photo-1441986300917-64674bd600d8?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1518611012118-696072aa579a?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1554118811-1e0d58224f24?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1447933601403-0c6688de566e?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1560472354-b33ff0c44a43?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1414235077428-338989a2e8c0?${COVER_QUALITY}`,
],
travel: [
`https://images.unsplash.com/photo-1506905925346-21bda4d32df4?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1596422846544-75c675fcd5d0?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1493976040374-85c8e577f47e?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1488646953014-85cb44e25828?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1476514525535-07fb3b4fcc5f?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1501785888041-af3ef285b470?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1528127269322-539801943592?${COVER_QUALITY}`,
],
'data-analysis': [
`https://images.unsplash.com/photo-1551288049-bebda4e38f71?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1543286386-713bdd548da4?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1504868584819-f8e8b4b6d7e3?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1555949963-aa79dcee981c?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1518186285589-2f7649de83e0?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1573164713714-d95e436ab8d6?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1639322537504-6427a16b0a28?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1591696205602-2f950c417cb9?${COVER_QUALITY}`,
],
lifestyle: [
`https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1497215842964-222b430dc094?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1455390582260-0446deae2f3f?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1416879595882-3373a0480b5b?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1498805903192-2f734adc632a?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1517487881594-2787fef5ebf7?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1556911220-e15b29be8c8f?${COVER_QUALITY}`,
],
other: [
`https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1484480974693-6ca0a7823cce?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1516321318423-f06f85e504b3?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1529156069898-49953e39b3ac?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1522202176988-66273c2fd55f?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1515378792601-1a587fd37b2d?${COVER_QUALITY}`,
`https://images.unsplash.com/photo-1523240795612-9a054b0db644?${COVER_QUALITY}`,
],
};
const COVER_SPECS = [
{
match: 'Q2 产品增长复盘',
file: 'work-report-growth.jpg',
url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1280&h=800&q=85',
url: `https://images.unsplash.com/photo-1551288049-bebda4e38f71?${COVER_QUALITY}`,
},
{
match: 'AI 客服落地',
@@ -137,21 +222,36 @@ const CATEGORY_FALLBACKS = {
other: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1280&h=800&q=85',
};
function slugify(value) {
return String(value)
function hashTitle(title) {
let hash = 0;
for (let i = 0; i < title.length; i += 1) {
hash = (hash * 31 + title.charCodeAt(i)) >>> 0;
}
return hash;
}
function coverFileName(categorySlug, title) {
const hash = hashTitle(title).toString(36);
const asciiSlug = String(title)
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 48);
.slice(0, 32);
const suffix = asciiSlug ? `${asciiSlug}-${hash}` : hash;
return `${categorySlug}-${suffix}.jpg`;
}
function resolveSpec(title, categorySlug) {
const spec = COVER_SPECS.find((item) => title.includes(item.match));
if (spec) return spec;
const pool = CATEGORY_IMAGE_POOLS[categorySlug] ?? CATEGORY_IMAGE_POOLS.other;
const url = pool[hashTitle(title) % pool.length];
return {
match: title,
file: `${categorySlug}-${slugify(title)}.jpg`,
url: CATEGORY_FALLBACKS[categorySlug] ?? CATEGORY_FALLBACKS.other,
file: coverFileName(categorySlug, title),
url,
};
}
@@ -208,7 +308,7 @@ const [posts] = await pool.query(
let updated = 0;
for (const post of posts) {
const spec = resolveSpec(post.title, post.category_slug);
await downloadCover(spec, CATEGORY_FALLBACKS[post.category_slug]);
await downloadCover(spec, (CATEGORY_IMAGE_POOLS[post.category_slug] ?? CATEGORY_IMAGE_POOLS.other)[0]);
const coverUrl = `${coverBase}/${spec.file}`;
if (post.cover_url === coverUrl) continue;
await pool.query(`UPDATE plaza_posts SET cover_url = ?, updated_at = ? WHERE id = ?`, [
+4 -1
View File
@@ -6,6 +6,7 @@ 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)), '..');
@@ -25,7 +26,7 @@ function loadEnvFile(filePath) {
loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));
const DEMO_POSTS = [
const BASE_DEMO_POSTS = [
{
category: 'work-report',
title: 'Q2 产品增长复盘:从 0 到 10 万 DAU',
@@ -198,6 +199,8 @@ const DEMO_POSTS = [
},
];
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`,
+21 -2
View File
@@ -21,6 +21,22 @@ const plazaPublicBase = (
).replace(/\/$/, '');
const portalUrl = `http://127.0.0.1:${portalPort}`;
const skipBuild = process.argv.includes('--skip-build');
const noPortal = process.argv.includes('--no-portal');
function resolveNpmBin() {
const candidates = [
process.env.NPM_BIN,
'/opt/homebrew/opt/node@22/bin/npm',
'/opt/homebrew/bin/npm',
'/usr/local/bin/npm',
].filter(Boolean);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return 'npm';
}
const npmBin = resolveNpmBin();
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
@@ -127,6 +143,9 @@ freePort(plazaPort);
try {
if (!(await portOpen(portalPort))) {
if (noPortal) {
throw new Error(`Portal 未运行 (${portalUrl}),请先启动 cn.tkmind.memind-portal`);
}
console.log(`==> 启动 Portal @ ${portalUrl}`);
portal = spawnChild('node', ['server.mjs'], 'portal');
await waitFor(() => portOpen(portalPort), 'Portal');
@@ -136,14 +155,14 @@ try {
if (!skipBuild) {
console.log('==> 构建 Plaza Next.js(生产)…');
execSync('npm run build', { cwd: plazaDir, stdio: 'inherit', env: { ...process.env, ...plazaEnv } });
execSync(`"${npmBin}" run build`, { cwd: plazaDir, stdio: 'inherit', env: { ...process.env, ...plazaEnv }, shell: true });
} else {
console.log('==> 跳过 next build--skip-build');
}
console.log(`==> 启动 Plaza Next.js 正式服务 (0.0.0.0:${plazaPort})`);
plaza = spawnChild(
'npm',
npmBin,
['run', 'start', '--', '-H', '0.0.0.0', '-p', String(plazaPort)],
'plaza',
plazaDir,