docs(ops): register wechat delivery fix branch and add manual resend scripts
Memind CI / Test, build, and release guards (push) Successful in 3m44s
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>
This commit is contained in:
@@ -3,6 +3,29 @@
|
||||
本文件记录已经完成迁移、但仍可能因为 Git 拓扑或遗留 worktree 被误判为“尚未进入 `main`”的分支。
|
||||
它是分支复用、合并、cherry-pick 和清理前的必查清单。
|
||||
|
||||
## `feature/scheduled-task-wechat-delivery-fix`
|
||||
|
||||
**状态:禁止再次引用。改动已提交并进入 `origin/main`,该分支保留仅用于只读追溯,不是待合并开发分支。**
|
||||
|
||||
审计日期:2026-08-17
|
||||
分支 HEAD:`1ca06e58`
|
||||
`origin/main` 对应提交:`1ca06e58`
|
||||
|
||||
### 原始用途
|
||||
|
||||
修复唐用户定时新闻任务微信链接被 guard 误杀、页面未落盘即推送、以及 reconcile 后未补发微信的问题。
|
||||
|
||||
### 验证摘要
|
||||
|
||||
- `node --test scheduled-task-executor.test.mjs scheduled-task-worker.test.mjs notification-dispatcher.test.mjs`:22 passed
|
||||
- `node --test wechat-mp.test.mjs`:100 passed(含 verifiedHtmlUrls 用例)
|
||||
- 103 快速发布绑定 `1ca06e58`
|
||||
|
||||
### 最终处置
|
||||
|
||||
- 保留本地分支名用于审计追溯。
|
||||
- 不要从该分支继续开发、merge、cherry-pick 或构建 runtime/artifact。
|
||||
|
||||
## `feature/production-inspection-fixes`
|
||||
|
||||
**状态:禁止再次引用。改动已提交并进入 `origin/main`,该分支保留仅用于只读追溯,不是待合并开发分支。**
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* One-shot proactive WeChat resend for a completed scheduled task page delivery.
|
||||
* Usage:
|
||||
* node scripts/manual-resend-scheduled-task-wechat.mjs --user-id <uuid> --relative-path public/foo.html [--dry-run]
|
||||
*/
|
||||
import process from 'node:process';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import {
|
||||
buildScheduledTaskVerifiedHtmlUrls,
|
||||
formatScheduledTaskDeliveryMessage,
|
||||
resendScheduledTaskWechatForReadyPage,
|
||||
} from '../scheduled-task-executor.mjs';
|
||||
import { loadWechatMpConfig, createWechatMpService } from '../wechat-mp.mjs';
|
||||
import { createNotificationDispatcher } from '../notification-dispatcher.mjs';
|
||||
import { createUserAuth } from '../user-auth.mjs';
|
||||
|
||||
loadH5Environment(new URL('.', import.meta.url).pathname);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
userId: '',
|
||||
relativePath: '',
|
||||
dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (token === '--user-id') args.userId = String(argv[++i] ?? '').trim();
|
||||
else if (token === '--relative-path') args.relativePath = String(argv[++i] ?? '').trim();
|
||||
else if (token === '--dry-run') args.dryRun = true;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { userId, relativePath, dryRun } = parseArgs(process.argv.slice(2));
|
||||
if (!userId || !relativePath) {
|
||||
throw new Error('用法: node scripts/manual-resend-scheduled-task-wechat.mjs --user-id <uuid> --relative-path public/foo.html');
|
||||
}
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error('缺少 DATABASE_URL');
|
||||
}
|
||||
|
||||
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
|
||||
const userAuth = createUserAuth(pool);
|
||||
const wechatMpService = createWechatMpService({
|
||||
config: loadWechatMpConfig(),
|
||||
userAuth,
|
||||
apiFetch: async () => {
|
||||
throw new Error('manual resend does not proxy chat sessions');
|
||||
},
|
||||
mysqlPool: pool,
|
||||
});
|
||||
const notificationDispatcher = createNotificationDispatcher({
|
||||
sendWechatTextToUser: wechatMpService?.enabled
|
||||
? (targetUserId, text, options) => wechatMpService.sendTextToUser(targetUserId, text, options)
|
||||
: null,
|
||||
});
|
||||
|
||||
if (dryRun) {
|
||||
const verifiedHtmlUrls = buildScheduledTaskVerifiedHtmlUrls(userId, [relativePath]);
|
||||
console.log(JSON.stringify({ mode: 'dry-run', userId, relativePath, verifiedHtmlUrls }, null, 2));
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const sent = await resendScheduledTaskWechatForReadyPage({
|
||||
pool,
|
||||
userId,
|
||||
relativePath,
|
||||
notificationDispatcher,
|
||||
logger: console,
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
mode: 'apply',
|
||||
userId,
|
||||
relativePath,
|
||||
sent,
|
||||
}, null, 2));
|
||||
await pool.end();
|
||||
if (!sent) process.exitCode = 1;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user