be464a5b8d
Memind CI / Test, build, and release guards (push) Has been cancelled
Introduce rain-service orchestration, browser-safe chat skill filtering, MeInput adapter helpers, and verify/deploy scripts so Rain mode can summarize recent input without Memory V2 pollution. Co-authored-by: Cursor <cursoragent@cursor.com>
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);
|
||
});
|