Improve WeChat MP replies and ship MindSpace/H5 production updates.

Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-19 23:06:43 +08:00
parent b0f5d6a51c
commit 229805a070
241 changed files with 13190 additions and 902 deletions
+152 -15
View File
@@ -1,6 +1,7 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from '@node-rs/argon2';
import { computeDeltaCostCents, loadBillingConfig, normalizeTokenState } from './billing.mjs';
import { buildInsufficientBalancePayload, loadRechargeConfig } from './billing-recharge.mjs';
import {
@@ -65,16 +66,14 @@ function hashPasswordPbkdf2(password, salt) {
}
function hashPasswordArgon2id(password, salt) {
return crypto
.argon2Sync(PASSWORD_ALGORITHM_ARGON2ID, {
message: password,
nonce: Buffer.from(salt, 'hex'),
parallelism: ARGON2_PARALLELISM,
tagLength: ARGON2_TAG_LENGTH,
memory: ARGON2_MEMORY,
passes: ARGON2_PASSES,
})
.toString('hex');
return argon2HashRawSync(password, {
salt: Buffer.from(salt, 'hex'),
parallelism: ARGON2_PARALLELISM,
outputLen: ARGON2_TAG_LENGTH,
memoryCost: ARGON2_MEMORY,
timeCost: ARGON2_PASSES,
algorithm: Argon2Algorithm.Argon2id,
}).toString('hex');
}
function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARGON2ID) {
@@ -627,15 +626,23 @@ export function createUserAuth(pool, options = {}) {
}
};
const registerAgentSession = async (userId, agentSessionId) => {
const registerAgentSession = async (userId, agentSessionId, goosedNode = 0) => {
await pool.query(
`INSERT INTO h5_user_sessions (agent_session_id, user_id, created_at)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE user_id = VALUES(user_id)`,
[agentSessionId, userId, Date.now()],
`INSERT INTO h5_user_sessions (agent_session_id, user_id, goosed_node, created_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), goosed_node = VALUES(goosed_node)`,
[agentSessionId, userId, goosedNode, Date.now()],
);
};
const getSessionNode = async (agentSessionId) => {
const [rows] = await pool.query(
`SELECT goosed_node FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[agentSessionId],
);
return rows[0]?.goosed_node ?? 0;
};
const ownsSession = async (userId, agentSessionId) => {
const [rows] = await pool.query(
`SELECT 1 FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ? LIMIT 1`,
@@ -1712,6 +1719,129 @@ export function createUserAuth(pool, options = {}) {
return rows[0]?.openid ?? null;
};
const findWechatUserByOpenid = async (appId, openid) => {
const [rows] = await pool.query(
`SELECT wi.user_id, wi.nickname, u.username, u.slug, u.display_name, u.status
FROM h5_user_wechat_identities wi
JOIN h5_users u ON u.id = wi.user_id
WHERE wi.app_id = ? AND wi.openid = ?
LIMIT 1`,
[appId, openid],
);
return rows[0]
? {
userId: rows[0].user_id,
status: rows[0].status,
nickname: rows[0].nickname,
username: rows[0].username,
slug: rows[0].slug,
displayName: rows[0].display_name,
}
: null;
};
const getWechatAgentRoute = async (appId, openid) => {
const [rows] = await pool.query(
`SELECT id, user_id, agent_session_id, status, created_at, updated_at
FROM h5_wechat_agent_routes
WHERE app_id = ? AND openid = ?
LIMIT 1`,
[appId, openid],
);
const row = rows[0];
if (!row) return null;
return {
id: row.id,
userId: row.user_id,
agentSessionId: row.agent_session_id,
status: row.status,
createdAt: Number(row.created_at ?? 0),
updatedAt: Number(row.updated_at ?? 0),
};
};
const upsertWechatAgentRoute = async ({
userId,
appId,
openid,
agentSessionId,
status = 'active',
now = Date.now(),
}) => {
const existing = await getWechatAgentRoute(appId, openid);
if (existing) {
await pool.query(
`UPDATE h5_wechat_agent_routes
SET user_id = ?, agent_session_id = ?, status = ?, updated_at = ?
WHERE app_id = ? AND openid = ?`,
[userId, agentSessionId, status, now, appId, openid],
);
return existing.id;
}
const id = crypto.randomUUID();
await pool.query(
`INSERT INTO h5_wechat_agent_routes
(id, user_id, app_id, openid, agent_session_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[id, userId, appId, openid, agentSessionId, status, now, now],
);
return id;
};
const clearWechatAgentRoute = async (appId, openid) => {
await pool.query(`DELETE FROM h5_wechat_agent_routes WHERE app_id = ? AND openid = ?`, [
appId,
openid,
]);
};
const recordWechatMpMessage = async ({
appId,
openid,
msgId,
now = Date.now(),
}) => {
if (!appId || !openid || !msgId) return { inserted: true };
const [result] = await pool.query(
`INSERT IGNORE INTO h5_wechat_mp_messages
(app_id, openid, msg_id, status, created_at, updated_at)
VALUES (?, ?, ?, 'processing', ?, ?)`,
[appId, openid, String(msgId), now, now],
);
if (Number(result?.affectedRows ?? 0) > 0) return { inserted: true };
const retryCutoff = now - 10 * 60 * 1000;
const [retryResult] = await pool.query(
`UPDATE h5_wechat_mp_messages
SET status = 'processing', agent_session_id = NULL, updated_at = ?
WHERE app_id = ? AND openid = ? AND msg_id = ?
AND (status = 'failed' OR (status = 'processing' AND updated_at < ?))`,
[now, appId, openid, String(msgId), retryCutoff],
);
return {
inserted: Number(retryResult?.affectedRows ?? 0) > 0,
duplicate: Number(retryResult?.affectedRows ?? 0) === 0,
};
};
const finishWechatMpMessage = async ({
appId,
openid,
msgId,
status = 'done',
agentSessionId = null,
now = Date.now(),
}) => {
if (!appId || !openid || !msgId) return;
const safeStatus = status === 'failed' ? 'failed' : 'done';
await pool.query(
`UPDATE h5_wechat_mp_messages
SET status = ?, agent_session_id = COALESCE(?, agent_session_id), updated_at = ?
WHERE app_id = ? AND openid = ? AND msg_id = ?`,
[safeStatus, agentSessionId, now, appId, openid, String(msgId)],
);
};
const bindWechatToUser = async ({
userId,
appId,
@@ -2183,6 +2313,12 @@ export function createUserAuth(pool, options = {}) {
getWechatPendingBind,
getWechatBindingStatus,
getWechatOpenidForUser,
findWechatUserByOpenid,
getWechatAgentRoute,
upsertWechatAgentRoute,
clearWechatAgentRoute,
recordWechatMpMessage,
finishWechatMpMessage,
resetPassword,
verify,
revoke,
@@ -2194,6 +2330,7 @@ export function createUserAuth(pool, options = {}) {
isPathAllowed,
repairAllUserPublishDirs,
registerAgentSession,
getSessionNode,
unregisterAgentSession,
ownsSession,
listOwnedSessionIds,