f57e080b49
Memind CI / Test, build, and release guards (push) Successful in 3m44s
Document branch disposition after 103 release, and add standalone/manual helpers to resend scheduled-task WeChat delivery when reconcile cannot replay. Co-authored-by: Cursor <cursoragent@cursor.com>
95 lines
3.5 KiB
JavaScript
95 lines
3.5 KiB
JavaScript
#!/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);
|
|
});
|