From 4fc59729ee222734628dca001e172c127c84488f Mon Sep 17 00:00:00 2001 From: john Date: Wed, 1 Jul 2026 21:10:56 +0800 Subject: [PATCH] fix: add session origin tracking + idle rotation foundation (schema/auth/config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend infrastructure for WeChat/H5 session unification: - schema.sql: h5_user_sessions adds origin ENUM('h5','wechat') column - db.mjs: idempotent migration for origin column (adds after user_id) - user-auth.mjs: - setSessionOrigin(sessionId, origin) - tag where session was created - getSessionOrigins(sessionIds) - batch lookup for history labeling - touchWechatAgentRoute(appId, openid) - refresh activity timestamp - wechat-mp.mjs: - DEFAULT_SESSION_IDLE_ROTATE_MS = 30min (env-configurable) - loadWechatMpConfig includes sessionIdleRotateMs Tests: 50/50 pass ✓ Still TODO: - ensureWechatAgentSession idle rotation check + setSessionOrigin call - runIntentMessage grantedSkills flow + touchWechatAgentRoute calls - Frontend type/UI component updates - Integration test for idle rotation Co-Authored-By: Claude --- db.mjs | 6 ++++++ schema.sql | 1 + user-auth.mjs | 29 +++++++++++++++++++++++++++++ wechat-mp.mjs | 5 +++++ 4 files changed, 41 insertions(+) diff --git a/db.mjs b/db.mjs index 832f5a2..cfb5d89 100644 --- a/db.mjs +++ b/db.mjs @@ -387,6 +387,12 @@ export async function migrateSchema(pool) { ); } + if (!(await columnExists(pool, 'h5_user_sessions', 'origin'))) { + await pool.query( + `ALTER TABLE h5_user_sessions ADD COLUMN origin ENUM('h5', 'wechat') NOT NULL DEFAULT 'h5' AFTER user_id`, + ); + } + if (!(await columnExists(pool, 'h5_session_snapshots', 'display_title'))) { await pool.query( `ALTER TABLE h5_session_snapshots ADD COLUMN display_title VARCHAR(512) NOT NULL DEFAULT '' AFTER name`, diff --git a/schema.sql b/schema.sql index 9db7a7f..cd0505e 100644 --- a/schema.sql +++ b/schema.sql @@ -350,6 +350,7 @@ CREATE TABLE IF NOT EXISTS h5_user_path_grants ( CREATE TABLE IF NOT EXISTS h5_user_sessions ( agent_session_id VARCHAR(128) NOT NULL PRIMARY KEY, user_id CHAR(36) NOT NULL, + origin ENUM('h5', 'wechat') NOT NULL DEFAULT 'h5', created_at BIGINT NOT NULL, KEY idx_h5_user_sessions_user (user_id), CONSTRAINT fk_h5_session_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE diff --git a/user-auth.mjs b/user-auth.mjs index 819b7a6..f68b322 100644 --- a/user-auth.mjs +++ b/user-auth.mjs @@ -764,6 +764,24 @@ export function createUserAuth(pool, options = {}) { return new Set(rows.map((row) => row.agent_session_id)); }; + const setSessionOrigin = async (agentSessionId, origin) => { + if (!agentSessionId || (origin !== 'h5' && origin !== 'wechat')) return; + await pool.query( + `UPDATE h5_user_sessions SET origin = ? WHERE agent_session_id = ?`, + [origin, agentSessionId], + ); + }; + + const getSessionOrigins = async (agentSessionIds = []) => { + const ids = [...new Set(agentSessionIds)].filter(Boolean); + if (ids.length === 0) return new Map(); + const [rows] = await pool.query( + `SELECT agent_session_id, origin FROM h5_user_sessions WHERE agent_session_id IN (?)`, + [ids], + ); + return new Map(rows.map((row) => [row.agent_session_id, row.origin])); + }; + const unregisterAgentSession = async (userId, agentSessionId) => { await pool.query( `DELETE FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ?`, @@ -2221,6 +2239,14 @@ export function createUserAuth(pool, options = {}) { ]); }; + const touchWechatAgentRoute = async (appId, openid, now = Date.now()) => { + if (!appId || !openid) return; + await pool.query( + `UPDATE h5_wechat_agent_routes SET updated_at = ? WHERE app_id = ? AND openid = ?`, + [now, appId, openid], + ); + }; + const recordWechatMpMessage = async ({ appId, openid, @@ -2806,6 +2832,7 @@ export function createUserAuth(pool, options = {}) { getWechatAgentRoute, upsertWechatAgentRoute, clearWechatAgentRoute, + touchWechatAgentRoute, recordWechatMpMessage, finishWechatMpMessage, insertWechatMpMessageDetail, @@ -2825,6 +2852,8 @@ export function createUserAuth(pool, options = {}) { unregisterAgentSession, ownsSession, listOwnedSessionIds, + setSessionOrigin, + getSessionOrigins, canUseChat, getUserById, getUserPublic, diff --git a/wechat-mp.mjs b/wechat-mp.mjs index d2488fc..41f5304 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -25,6 +25,7 @@ const DEFAULT_STATUS_TEXT = const DEFAULT_UNSUPPORTED_TEXT = '当前先支持文字消息,你可以直接发文字给我。'; const DEFAULT_UNBOUND_TEXT = '先点这里完成绑定,再继续和专属 Agent 对话:'; const DEFAULT_PROGRESS_DELAY_MS = 8000; +const DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1000; const PUBLIC_HTML_LINK_PATTERN = /https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi; const SESSION_WORKSPACE_TOOL_NAMES = new Set([ @@ -1254,6 +1255,10 @@ export function loadWechatMpConfig(env = process.env) { 0, Number(env.H5_WECHAT_MP_PROGRESS_DELAY_MS ?? DEFAULT_PROGRESS_DELAY_MS), ), + sessionIdleRotateMs: Math.max( + 0, + Number(env.H5_WECHAT_MP_SESSION_IDLE_ROTATE_MS ?? DEFAULT_SESSION_IDLE_ROTATE_MS), + ), statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT, unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT, unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT,