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:
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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 1–2', desc: '抵达与市区精华,控制步行强度。', tag: '行程' },
|
||||
{ title: 'Day 3–4', 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);
|
||||
}
|
||||
|
||||
Executable
+23
@@ -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
|
||||
Executable
+37
@@ -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
|
||||
@@ -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 = ?`, [
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user