#!/usr/bin/env node /** * Standalone one-shot WeChat resend for a completed scheduled task page. * Designed to run on 103 with only mysql2 from runtime node_modules. */ import process from 'node:process'; const TANG = process.argv.includes('--user-id') ? process.argv[process.argv.indexOf('--user-id') + 1] : 'a70ff537-8908-486e-9b6c-042e07cc25db'; const RELATIVE = process.argv.includes('--relative-path') ? process.argv[process.argv.indexOf('--relative-path') + 1] : 'public/daily-news-0817.html'; const TASK_ID = '21799936-0532-420c-b144-65ea3846cde1'; async function getStableAccessToken(config) { const response = 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: config.appId, secret: config.appSecret, }), }); const payload = await response.json(); if (!payload?.access_token) { throw new Error(`stable_token failed: ${JSON.stringify(payload)}`); } return payload.access_token; } async function main() { const { default: mysql } = await import('mysql2/promise'); 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 ?? process.env.WX_APP_ID; const appSecret = process.env.H5_WECHAT_MP_APP_SECRET ?? process.env.WECHAT_MP_APP_SECRET ?? process.env.WX_APP_SECRET; if (!appId || !appSecret) throw new Error('缺少 WECHAT_MP_APP_ID / WECHAT_MP_APP_SECRET'); 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('用户未绑定微信'); const [tasks] = await pool.query( 'SELECT title, last_result_json FROM h5_scheduled_tasks WHERE id = ? LIMIT 1', [TASK_ID], ); const task = tasks?.[0]; const deliveryText = String(task?.last_result_json?.deliveryText ?? '').trim(); if (!deliveryText) throw new Error('缺少 deliveryText'); const url = `https://m.tkmind.cn/MindSpace/${TANG}/${RELATIVE.replace(/^\/+/, '')}`; const text = `定时任务完成:${task.title}\n\n${deliveryText.includes(url) ? deliveryText : `${deliveryText}\n\n页面链接:\n${url}`}`.trim(); const accessToken = await getStableAccessToken({ appId, appSecret }); const response = await fetch(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${encodeURIComponent(accessToken)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ touser: openid, msgtype: 'text', text: { content: text.slice(0, 2048) }, }), }); const payload = await response.json(); if (Number(payload?.errcode ?? 0) !== 0) { throw new Error(`微信发送失败 errcode=${payload?.errcode} errmsg=${payload?.errmsg}`); } const now = Date.now(); const nextResult = { ...(task.last_result_json ?? {}), wechatDelivery: { sentAt: now, relativePaths: [RELATIVE], source: 'manual_resend', }, }; await pool.query( 'UPDATE h5_scheduled_tasks SET last_result_json = ?, updated_at = ? WHERE id = ?', [JSON.stringify(nextResult), now, TASK_ID], ); console.log(JSON.stringify({ ok: true, openid: `${openid.slice(0, 8)}...`, url }, null, 2)); await pool.end(); } main().catch((error) => { console.error(error); process.exit(1); });