177 lines
6.3 KiB
JavaScript
177 lines
6.3 KiB
JavaScript
|
|
#!/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);
|
|||
|
|
});
|