Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Session snapshot cache service.
|
||||
*
|
||||
* Stores a DB-side copy of each session's userVisible messages so that
|
||||
* repeated opens of historical chats skip the Goose round-trip entirely.
|
||||
*
|
||||
* Rollback: set SESSION_SNAPSHOT_CACHE_ENABLED=0 in env → all reads/writes
|
||||
* are no-ops; behaviour falls back to the existing Goose path unchanged.
|
||||
*
|
||||
* The table (h5_session_snapshots) is additive-only and has no FK deps on
|
||||
* conversation content, so it can be dropped/truncated safely at any time.
|
||||
*/
|
||||
|
||||
export function createSessionSnapshotService(pool) {
|
||||
function isEnabled() {
|
||||
return process.env.SESSION_SNAPSHOT_CACHE_ENABLED !== '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a snapshot for a session.
|
||||
* @param {string} sessionId
|
||||
* @param {string} userId
|
||||
* @param {object} session – raw Goose session object (includes message_count, updated_at, etc.)
|
||||
* @param {Array} messages – already-normalized userVisible Message[]
|
||||
*/
|
||||
async function save(sessionId, userId, session, messages) {
|
||||
if (!isEnabled() || !pool) return;
|
||||
try {
|
||||
const now = Date.now();
|
||||
const sessionMeta = {
|
||||
name: session.name ?? '',
|
||||
working_dir: session.working_dir ?? '',
|
||||
created_at_str: session.created_at ?? '',
|
||||
updated_at_str: session.updated_at ?? '',
|
||||
user_set_name: session.user_set_name ? 1 : 0,
|
||||
recipe_json: session.recipe != null ? JSON.stringify(session.recipe) : null,
|
||||
};
|
||||
await pool.query(
|
||||
`INSERT INTO h5_session_snapshots
|
||||
(agent_session_id, user_id, name, working_dir,
|
||||
created_at_str, updated_at_str, user_set_name, recipe_json,
|
||||
synced_msg_count, source_updated_at,
|
||||
messages_json, synced_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
working_dir = VALUES(working_dir),
|
||||
created_at_str = VALUES(created_at_str),
|
||||
updated_at_str = VALUES(updated_at_str),
|
||||
user_set_name = VALUES(user_set_name),
|
||||
recipe_json = VALUES(recipe_json),
|
||||
synced_msg_count = VALUES(synced_msg_count),
|
||||
source_updated_at = VALUES(source_updated_at),
|
||||
messages_json = VALUES(messages_json),
|
||||
synced_at = VALUES(synced_at)`,
|
||||
[
|
||||
sessionId,
|
||||
userId,
|
||||
sessionMeta.name,
|
||||
sessionMeta.working_dir,
|
||||
sessionMeta.created_at_str,
|
||||
sessionMeta.updated_at_str,
|
||||
sessionMeta.user_set_name,
|
||||
sessionMeta.recipe_json,
|
||||
session.message_count ?? 0,
|
||||
session.updated_at ?? '',
|
||||
JSON.stringify(messages),
|
||||
now,
|
||||
],
|
||||
);
|
||||
} catch (err) {
|
||||
// Non-fatal: log and continue; caller falls back to Goose.
|
||||
console.warn('[snapshot] save failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a snapshot. Returns null if not found or cache is disabled.
|
||||
* The returned object reconstructs what `loadSessionDetail` returns.
|
||||
*/
|
||||
async function get(sessionId) {
|
||||
if (!isEnabled() || !pool) return null;
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT agent_session_id, user_id, name, working_dir,
|
||||
created_at_str, updated_at_str, user_set_name, recipe_json,
|
||||
synced_msg_count, source_updated_at, messages_json, synced_at
|
||||
FROM h5_session_snapshots
|
||||
WHERE agent_session_id = ?
|
||||
LIMIT 1`,
|
||||
[sessionId],
|
||||
);
|
||||
if (!rows.length) return null;
|
||||
const row = rows[0];
|
||||
return {
|
||||
session: {
|
||||
id: sessionId,
|
||||
name: row.name,
|
||||
working_dir: row.working_dir,
|
||||
message_count: row.synced_msg_count,
|
||||
created_at: row.created_at_str || undefined,
|
||||
updated_at: row.updated_at_str || undefined,
|
||||
user_set_name: row.user_set_name === 1,
|
||||
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
|
||||
conversation: null,
|
||||
},
|
||||
messages: JSON.parse(row.messages_json),
|
||||
meta: {
|
||||
synced_msg_count: row.synced_msg_count,
|
||||
source_updated_at: row.source_updated_at,
|
||||
synced_at: row.synced_at,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[snapshot] get failed:', err instanceof Error ? err.message : err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a snapshot (called when the session itself is deleted).
|
||||
*/
|
||||
async function remove(sessionId) {
|
||||
if (!pool) return;
|
||||
try {
|
||||
await pool.query(
|
||||
`DELETE FROM h5_session_snapshots WHERE agent_session_id = ?`,
|
||||
[sessionId],
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn('[snapshot] remove failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch full session from Goose and write/update the snapshot.
|
||||
* Fire-and-forget safe: errors are swallowed.
|
||||
*/
|
||||
async function refresh(sessionId, userId, apiFetchFn) {
|
||||
if (!isEnabled() || !pool) return;
|
||||
try {
|
||||
const res = await apiFetchFn(`/sessions/${encodeURIComponent(sessionId)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const gooseSession = await res.json();
|
||||
// Store raw userVisible messages — same as write-through path.
|
||||
const messages = (gooseSession.conversation ?? [])
|
||||
.filter((m) => m.metadata?.userVisible);
|
||||
await save(sessionId, userId, gooseSession, messages);
|
||||
} catch (err) {
|
||||
console.warn('[snapshot] refresh failed:', sessionId, err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
return { save, get, remove, refresh, isEnabled };
|
||||
}
|
||||
Reference in New Issue
Block a user