Files
memind/scripts/deploy-tang-learning-assistant-103.mjs
T
john 348e192800 feat(learn): add family learning assistant API routes and public pages
Wire /learn static delivery, parent/child portal routes, and Tang page build scripts for 103 deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 17:09:52 +08:00

185 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
});