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:
+152
-15
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user