#!/usr/bin/env node /** * Deploy MeInput tutorial + case pages to 103 production under 唐 user. * Usage: node scripts/deploy-tang-meinput-pages-103.mjs [--send-wechat] */ import { execSync } from 'node:child_process'; import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const HOST = 'john@58.38.22.103'; const REMOTE_ROOT = '/Users/john/Project/Memind'; const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db'; const JOHN_LOCAL = '1c99b83b-0454-474f-a5d2-129d34506a32'; const FILES = [ 'meinput-tkmind-portrait-tutorial.html', 'meinput-tkmind-portrait-tutorial-wechat.html', 'behavior-pattern-analysis.html', ]; const PUBLIC_BASE = `https://m.tkmind.cn/MindSpace/${TANG}/public`; const NODE103 = '/opt/homebrew/opt/node@24/bin/node'; const sendWechat = process.argv.includes('--send-wechat'); function sh(cmd) { execSync(cmd, { stdio: 'inherit' }); } const remoteScript = ` import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import mysql from 'mysql2/promise'; const TANG = '${TANG}'; const REMOTE_ROOT = '${REMOTE_ROOT}'; const FILES = ${JSON.stringify(FILES)}; const REQUEST_ID = 'deploy-meinput-tutorial-20260904'; const PUBLIC_BASE = '${PUBLIC_BASE}'; process.loadEnvFile(path.join(REMOTE_ROOT, '.env')); const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 }); async function ensureReady(relativePath) { const now = Date.now(); const id = crypto.randomUUID(); await pool.query( \`INSERT INTO h5_page_delivery_contracts (id, user_id, request_id, workspace_relative_path, data_mode, status, ready_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'static', 'ready', ?, ?, ?) ON DUPLICATE KEY UPDATE status = 'ready', ready_at = VALUES(ready_at), failure_reason = NULL, updated_at = VALUES(updated_at)\`, [id, TANG, REQUEST_ID, relativePath, now, now, now], ); } async function main() { const results = []; for (const name of FILES) { const relativePath = 'public/' + name; const abs = path.join(REMOTE_ROOT, 'MindSpace', TANG, relativePath); const exists = fs.existsSync(abs); const size = exists ? fs.statSync(abs).size : 0; if (exists) await ensureReady(relativePath); results.push({ file: name, exists, size, url: PUBLIC_BASE + '/' + name }); } console.log(JSON.stringify({ ok: true, pages: results }, null, 2)); await pool.end(); } main().catch((e) => { console.error(e); process.exit(1); }); `.trim(); async function sendWechatLinks() { const remoteWechat = ` import path from 'node:path'; import mysql from 'mysql2/promise'; const TANG = '${TANG}'; const PUBLIC_BASE = '${PUBLIC_BASE}'; const FILES = ${JSON.stringify(FILES)}; process.loadEnvFile('${REMOTE_ROOT}/.env'); const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 }); const appId = process.env.H5_WECHAT_MP_APP_ID ?? process.env.WECHAT_MP_APP_ID; const appSecret = process.env.H5_WECHAT_MP_APP_SECRET ?? process.env.WECHAT_MP_APP_SECRET; if (!appId || !appSecret) throw new Error('missing wechat credentials'); const [ident] = await pool.query( 'SELECT openid FROM h5_user_wechat_identities WHERE user_id = ? AND app_id = ? LIMIT 1', [TANG, appId], ); const openid = ident?.[0]?.openid; if (!openid) throw new Error('tang not bound to wechat'); const tokenRes = await fetch('https://api.weixin.qq.com/cgi-bin/stable_token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credential', appid: appId, secret: appSecret }), }); const tokenPayload = await tokenRes.json(); if (!tokenPayload.access_token) throw new Error(JSON.stringify(tokenPayload)); const lines = [ 'MeInput × TKMind 教程与案例已发布:', '', '📱 教程(公众号发布版)', PUBLIC_BASE + '/meinput-tkmind-portrait-tutorial-wechat.html', '', '📖 教程(网页阅读版)', PUBLIC_BASE + '/meinput-tkmind-portrait-tutorial.html', '', '🧭 案例:用户A 全景画像', PUBLIC_BASE + '/behavior-pattern-analysis.html', ]; const text = lines.join('\\n').slice(0, 2048); const sendRes = await fetch('https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=' + encodeURIComponent(tokenPayload.access_token), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ touser: openid, msgtype: 'text', text: { content: text } }), }); const sendPayload = await sendRes.json(); if (Number(sendPayload.errcode ?? 0) !== 0) throw new Error(JSON.stringify(sendPayload)); console.log(JSON.stringify({ ok: true, sent: true, openid: openid.slice(0, 8) + '...' })); await pool.end(); `.trim(); const tmp = `${REMOTE_ROOT}/.tmp-tang-meinput-wechat.mjs`; fs.writeFileSync(path.join(root, '.tmp-tang-wechat.mjs'), remoteWechat); sh(`scp -q ${path.join(root, '.tmp-tang-wechat.mjs')} ${HOST}:${tmp}`); sh(`ssh -o BatchMode=yes ${HOST} 'cd ${REMOTE_ROOT} && ${NODE103} ${tmp} && rm -f ${tmp}'`); fs.unlinkSync(path.join(root, '.tmp-tang-wechat.mjs')); } async function main() { const localDir = path.join(root, 'MindSpace', JOHN_LOCAL, 'public'); for (const f of FILES) { const src = path.join(localDir, f); if (!fs.existsSync(src)) throw new Error(`missing local file: ${src}`); } const remotePublic = `${REMOTE_ROOT}/MindSpace/${TANG}/public`; sh(`ssh -o BatchMode=yes ${HOST} 'mkdir -p ${remotePublic}'`); for (const f of FILES) { sh(`scp -q ${path.join(localDir, f)} ${HOST}:${remotePublic}/${f}`); } const tmpLocal = path.join(root, '.tmp-tang-deploy-103.mjs'); fs.writeFileSync(tmpLocal, remoteScript); const tmpRemote = `${REMOTE_ROOT}/.tmp-tang-meinput-deploy.mjs`; sh(`scp -q ${tmpLocal} ${HOST}:${tmpRemote}`); sh(`ssh -o BatchMode=yes ${HOST} 'cd ${REMOTE_ROOT} && ${NODE103} ${tmpRemote} && rm -f ${tmpRemote}'`); fs.unlinkSync(tmpLocal); console.log('\n=== Production URLs ==='); for (const f of FILES) console.log(`${PUBLIC_BASE}/${f}`); for (const f of FILES) { const code = execSync( `curl -sS -o /dev/null -w '%{http_code}' 'https://m.tkmind.cn/MindSpace/${TANG}/public/${f}'`, { encoding: 'utf8' }, ).trim(); console.log(`${f}: HTTP ${code}`); } if (sendWechat) { await sendWechatLinks(); } } main().catch((e) => { console.error(e); process.exit(1); });