#!/usr/bin/env node
/**
* 部署唐用户学习小助手(HTML + 后端 + 种子数据)到 103
* Usage: node scripts/deploy-tang-learning-assistant-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@180.159.29.143';
const REMOTE_ROOT = '/Users/john/Project/Memind';
const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db';
const NODE103 = '/opt/homebrew/opt/node@24/bin/node';
const FILES = ['learning-assistant.html', 'learning-assistant-parent.html'];
const BACKEND_FILES = ['learn-assistant-service.mjs', 'learn-assistant-routes.mjs'];
const PUBLIC_BASE = `https://m.tkmind.cn/MindSpace/${TANG}/public`;
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';
import { createLearnAssistantService } from './learn-assistant-service.mjs';
const TANG = '${TANG}';
const REMOTE_ROOT = '${REMOTE_ROOT}';
const FILES = ${JSON.stringify(FILES)};
const REQUEST_ID = 'deploy-learning-assistant-20260911';
const PUBLIC_BASE = '${PUBLIC_BASE}';
process.loadEnvFile(path.join(REMOTE_ROOT, '.env'));
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
const service = createLearnAssistantService({ dataRoot: path.join(REMOTE_ROOT, 'data', 'learn-assistant') });
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 seed = await service.seedTangDefaults();
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, seed, 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}';
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 text = [
'学习小助手已发布:',
'',
'👧 小朋友端',
PUBLIC_BASE + '/learning-assistant.html',
'',
'🔐 家长后台(默认密码 123456)',
PUBLIC_BASE + '/learning-assistant-parent.html',
].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-learning-wechat.mjs`;
fs.writeFileSync(path.join(root, '.tmp-tang-learning-wechat.mjs'), remoteWechat);
sh(`scp -q ${path.join(root, '.tmp-tang-learning-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-learning-wechat.mjs'));
}
async function main() {
sh(`node ${path.join(root, 'scripts/build-tang-learning-assistant-pages.mjs')}`);
const localPublic = path.join(root, 'MindSpace', TANG, 'public');
for (const f of FILES) {
if (!fs.existsSync(path.join(localPublic, f))) throw new Error(`missing ${f}`);
}
const remotePublic = `${REMOTE_ROOT}/MindSpace/${TANG}/public`;
sh(`ssh -o BatchMode=yes ${HOST} 'mkdir -p ${remotePublic} ${REMOTE_ROOT}/data/learn-assistant/${TANG}'`);
for (const f of FILES) {
sh(`scp -q ${path.join(localPublic, f)} ${HOST}:${remotePublic}/${f}`);
}
for (const f of BACKEND_FILES) {
sh(`scp -q ${path.join(root, f)} ${HOST}:${REMOTE_ROOT}/${f}`);
}
sh(`scp -q ${path.join(root, 'scripts/patch-portal-learn-assistant-103.mjs')} ${HOST}:${REMOTE_ROOT}/scripts/patch-portal-learn-assistant-103.mjs`);
sh(`ssh -o BatchMode=yes ${HOST} 'cd ${REMOTE_ROOT} && ${NODE103} scripts/patch-portal-learn-assistant-103.mjs'`);
const tmpLocal = path.join(root, '.tmp-tang-learning-deploy.mjs');
fs.writeFileSync(tmpLocal, remoteScript);
const tmpRemote = `${REMOTE_ROOT}/.tmp-tang-learning-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);
sh(`ssh -o BatchMode=yes ${HOST} 'launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal || true'`);
console.log('\n=== Production URLs ===');
for (const f of FILES) console.log(`${PUBLIC_BASE}/${f}`);
console.log(`API: https://m.tkmind.cn/api/learn/tang/children`);
console.log('家长默认密码: 123456');
await new Promise((r) => setTimeout(r, 5000));
for (const f of FILES) {
const code = execSync(
`curl -sS -o /dev/null -w '%{http_code}' '${PUBLIC_BASE}/${f}'`,
{ encoding: 'utf8' },
).trim();
console.log(`${f}: HTTP ${code}`);
}
const apiCode = execSync(
`curl -sS -o /dev/null -w '%{http_code}' 'https://m.tkmind.cn/api/learn/tang/children'`,
{ encoding: 'utf8' },
).trim();
console.log(`API children: HTTP ${apiCode}`);
if (sendWechat) await sendWechatLinks();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});