feat(h5): web 联网能力、实时查询路由与 session Finish 对齐

- 新增 web 能力并挂载 platform/web(web_search/fetch_url)
- 实时查询强制 web skill,router fallback 与 await session Finish
- Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-06 16:06:26 +08:00
parent 08feae8bef
commit 14a00774d9
41 changed files with 3501 additions and 126 deletions
+34 -9
View File
@@ -22,6 +22,7 @@ import {
} from './agent-run-routes.mjs';
import { createTkmindProxy, sanitizeSessionConversationPublicHtmlLinks } from './tkmind-proxy.mjs';
import { createSessionAccess, isSessionBrokerEnabled } from './session-broker.mjs';
import { isSessionBrokerMetricsEnabled } from './session-broker-metrics.mjs';
import {
clearUserSessionCookie,
createUserAuth,
@@ -173,7 +174,9 @@ import { createLlmProviderService, RELAY_BOOTSTRAP } from './llm-providers.mjs';
import { createDirectChatService, isDirectChatSessionId, isPortalDirectChatSnapshot, sendDirectChatSessionEvents, shouldExpirePortalDirectChatSnapshot } from './direct-chat-service.mjs';
import { repairSessionConversationFromDb } from './conversation-repair.mjs';
import { filterNonemptyUserVisibleMessages } from './conversation-transcript-persist.mjs';
import { createManagedChatIntentRouter } from './chat-intent-router.mjs';
import { createSessionStreamStore } from './session-stream-store.mjs';
import { isSessionStreamReplayEnabled } from './session-stream.mjs';
import { createManagedChatIntentRouter, resolveNormalizedRouterDecisionMode } from './chat-intent-router.mjs';
import { createSessionSnapshotService } from './session-snapshot.mjs';
import { createConversationMemoryService } from './conversation-memory.mjs';
import { createManagedMemoryV2Runtime } from './memory-v2-runtime.mjs';
@@ -293,22 +296,27 @@ if (ACCESS_PASSWORD && !isDatabaseConfigured()) {
let userAuth = null;
let sessionAccess = null;
let sessionStreamStore = null;
let tkmindProxy = null;
async function resolveActiveSessionAccess() {
if (sessionAccess) return sessionAccess;
if (!userAuth) return null;
return createSessionAccess({ userAuth, enabled: isSessionBrokerEnabled() });
}
async function ownsAgentSession(userId, sessionId) {
if (!userId || !sessionId) return false;
if (sessionAccess) return sessionAccess.validateOwnership(userId, sessionId);
if (userAuth) return userAuth.ownsSession(userId, sessionId);
return false;
const access = await resolveActiveSessionAccess();
if (!access) return false;
return access.validateOwnership(userId, sessionId);
}
async function unregisterAgentSessionForUser(userId, sessionId) {
if (!userId || !sessionId) return;
if (sessionAccess) {
await sessionAccess.unregisterSession({ userId, sessionId });
return;
}
if (userAuth) await unregisterAgentSessionForUser(userId, sessionId);
const access = await resolveActiveSessionAccess();
if (!access) return;
await access.unregisterSession({ userId, sessionId });
}
let agentRunGateway = null;
let chatIntentRouter = null;
@@ -470,6 +478,19 @@ async function bootstrapUserAuth() {
if (sessionAccess.enabled) {
console.log('[Portal] Session Broker enabled (MEMIND_SESSION_BROKER_ENABLED=1)');
}
const routerDecisionMode = resolveNormalizedRouterDecisionMode(process.env);
const h5SessionFlags = [
sessionAccess.enabled && 'MEMIND_SESSION_BROKER_ENABLED',
isSessionBrokerMetricsEnabled() && 'MEMIND_SESSION_BROKER_METRICS',
routerDecisionMode !== 'off' && `MEMIND_ROUTER_NORMALIZED_DECISION=${routerDecisionMode}`,
isSessionStreamReplayEnabled() && 'MEMIND_SESSION_STREAM_REPLAY',
['1', 'true', 'yes', 'on'].includes(String(process.env.MEMIND_SSE_EVENT_TAXONOMY ?? '').trim().toLowerCase()) && 'MEMIND_SSE_EVENT_TAXONOMY',
['1', 'true', 'yes', 'on'].includes(String(process.env.MEMIND_RUN_STREAM_REPLAY ?? '').trim().toLowerCase()) && 'MEMIND_RUN_STREAM_REPLAY',
['1', 'true', 'yes', 'on'].includes(String(process.env.MEMIND_H5_HTML_FINISH_GUARD ?? '').trim().toLowerCase()) && 'MEMIND_H5_HTML_FINISH_GUARD',
].filter(Boolean);
if (h5SessionFlags.length) {
console.log(`[Portal] H5 session flags: ${h5SessionFlags.join(', ')}`);
}
wechatPayClient = createWechatPayClient(loadWechatPayConfig());
wechatOAuthService = createWechatOAuthService(pool, loadWechatOAuthConfig(), { userAuth });
rechargeService = createRechargeService(pool, {
@@ -575,6 +596,9 @@ async function bootstrapUserAuth() {
conversationMemoryService,
memoryV2,
});
if (isSessionStreamReplayEnabled()) {
sessionStreamStore = createSessionStreamStore({ pool });
}
directChatService = createDirectChatService({
userAuth,
sessionAccess,
@@ -595,6 +619,7 @@ async function bootstrapUserAuth() {
apiSecret: API_SECRET,
userAuth,
sessionAccess,
sessionStreamStore,
llmProviderService,
subscriptionService,
sessionSnapshotService,