#!/usr/bin/env node /** * Local E2E: agent run terminal → h5_experience via Experience Extractor. * * Usage: * node scripts/verify-experience-agent-run-local.mjs * * Requires: .env MySQL, goosed or direct-chat LLM path. */ import crypto from 'node:crypto'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadH5Environment } from './load-env.mjs'; import { createAgentRunGateway } from '../agent-run-gateway.mjs'; import { createManagedChatIntentRouter } from '../chat-intent-router.mjs'; import { createConversationMemoryService } from '../conversation-memory.mjs'; import { createDbPool } from '../db.mjs'; import { createEpisodicMemoryService } from '../episodic-memory.mjs'; import { createExperienceService } from '../experience-service.mjs'; import { createLlmProviderService } from '../llm-providers.mjs'; import { createMemoryV2AdminConfigService } from '../memory-v2-admin-config.mjs'; import { createManagedMemoryV2Runtime } from '../memory-v2-runtime.mjs'; import { createTkmindProxy } from '../tkmind-proxy.mjs'; import { createToolGateway } from '../tool-gateway.mjs'; import { createUserAuth } from '../user-auth.mjs'; import { createSessionSnapshotService } from '../session-snapshot.mjs'; import { createDirectChatService } from '../direct-chat-service.mjs'; import { createSessionAccess } from '../session-broker.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const PORTAL = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`; const USERNAME = process.env.MEMIND_E2E_USERNAME ?? 'john'; const PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888'; const VERIFY_MESSAGE = process.env.EXPERIENCE_E2E_MESSAGE ?? 'Experience E2E 验证:请用一句话回复收到即可'; const MAX_WAIT_MS = Number(process.env.EXPERIENCE_E2E_WAIT_MS ?? 180_000); loadH5Environment(import.meta.dirname); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function loginViaApi(pool) { const response = await fetch(`${PORTAL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: USERNAME, password: PASSWORD }), }); const body = await response.json().catch(() => ({})); if (response.ok && body?.authenticated && body.user?.id) { return { userId: body.user.id }; } const auth = createUserAuth(pool, { usersRoot: process.env.H5_USERS_ROOT ?? path.join(root, 'users'), h5Root: root, }); const result = await auth.login({ username: USERNAME, password: PASSWORD, ip: '127.0.0.1' }); if (!result.ok || !result.user?.id) { throw new Error(`登录失败: ${result.message ?? body?.message ?? 'unknown'}`); } return { userId: result.user.id }; } async function resolveUserId(pool) { const forcedUserId = String(process.env.EXPERIENCE_E2E_USER_ID ?? '').trim(); if (forcedUserId) return forcedUserId; try { const { userId } = await loginViaApi(pool); if (userId) return userId; } catch (error) { console.warn('[experience-e2e] API/DB login skipped:', error instanceof Error ? error.message : error); } const [rows] = await pool.query( 'SELECT id FROM h5_users WHERE username = ? LIMIT 1', [USERNAME], ); if (rows[0]?.id) { console.log(`[experience-e2e] using user id from DB lookup: ${USERNAME}`); return rows[0].id; } throw new Error(`找不到用户 ${USERNAME},可设置 EXPERIENCE_E2E_USER_ID`); } async function waitForTerminal(pool, runId, timeoutMs) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const [rows] = await pool.query( 'SELECT status, error_message FROM h5_agent_runs WHERE id = ? LIMIT 1', [runId], ); const status = rows[0]?.status; if (status === 'succeeded' || status === 'failed') { return rows[0]; } await sleep(1000); } throw new Error(`run ${runId} 未在 ${timeoutMs}ms 内进入终态`); } async function findExperienceForRun(pool, runId) { const [rows] = await pool.query( `SELECT id, kind, title, problem, result, status, JSON_EXTRACT(evidence_json, '$.provenance.run_id') AS run_id, created_at FROM h5_experience WHERE JSON_UNQUOTE(JSON_EXTRACT(evidence_json, '$.provenance.run_id')) = ? ORDER BY created_at DESC LIMIT 1`, [runId], ); return rows[0] ?? null; } async function bootstrapGateway(pool) { const userAuth = createUserAuth(pool, { usersRoot: process.env.H5_USERS_ROOT ?? path.join(root, 'users'), h5Root: root, }); const llmProviderService = createLlmProviderService(pool, { apiTarget: process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006', apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret', }); const memoryV2ConfigService = createMemoryV2AdminConfigService(pool); const getEffectiveEnv = async () => { const state = await memoryV2ConfigService.getRuntimeState().catch(() => null); return { ...process.env, ...(state?.overrides ?? {}) }; }; const conversationMemoryService = createConversationMemoryService(pool, { llmProviderService, getEffectiveEnv, }); const memoryV2 = await createManagedMemoryV2Runtime({ legacyMemoryService: conversationMemoryService, configService: memoryV2ConfigService, mysqlPool: pool, }); const episodicMemoryService = createEpisodicMemoryService(pool, { getEffectiveEnv, logger: console, }); const sessionSnapshotService = createSessionSnapshotService(pool, { conversationMemoryService, memoryV2, episodicMemoryService, }); const sessionAccess = createSessionAccess({ userAuth, enabled: false }); const directChatService = createDirectChatService({ userAuth, sessionAccess, llmProviderService, sessionSnapshotService, memoryV2, conversationMemoryService, episodicMemoryService, }); const chatIntentRouter = createManagedChatIntentRouter({ llmProviderService, memoryV2, conversationMemoryService, episodicMemoryService, configService: memoryV2ConfigService, }); const tkmindProxy = createTkmindProxy({ apiTarget: process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006', apiTargets: String(process.env.TKMIND_API_TARGETS ?? '').split(',').filter(Boolean).length ? String(process.env.TKMIND_API_TARGETS).split(',').map((s) => s.trim()).filter(Boolean) : [process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006'], apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret', userAuth, }); const experienceService = createExperienceService(pool); return createAgentRunGateway({ pool, userAuth, sessionAccess, tkmindProxy, toolGateway: createToolGateway({ llmProviderService }), directChatService, sessionSnapshotService, conversationMemoryService, chatIntentRouter, experienceService, autoDispatch: false, retryDelaysMs: [], }); } async function main() { const pool = createDbPool(); try { console.log('[experience-e2e] login...'); const userId = await resolveUserId(pool); const gateway = await bootstrapGateway(pool); console.log('[experience-e2e] create + dispatch run (Experience Extractor gateway)...'); const run = await gateway.createRun(userId, { requestId: crypto.randomUUID(), userMessage: { id: crypto.randomUUID(), role: 'user', content: [{ type: 'text', text: VERIFY_MESSAGE }], metadata: { userVisible: true, displayText: VERIFY_MESSAGE, }, }, }); const runId = run.id; console.log('[experience-e2e] run created:', runId, 'status:', run.status); gateway.dispatchRun(runId); await sleep(500); while ((await gateway.getQueueStatus()).inFlight > 0) { await sleep(500); } const terminal = await waitForTerminal(pool, runId, MAX_WAIT_MS); console.log('[experience-e2e] run terminal:', terminal.status, terminal.error_message ?? ''); const experience = await findExperienceForRun(pool, runId); if (!experience) { console.error('[experience-e2e] FAIL: no h5_experience row for run', runId); process.exit(1); } console.log('[experience-e2e] PASS'); console.log(JSON.stringify({ runId, runStatus: terminal.status, experience: { id: experience.id, kind: experience.kind, problem: experience.problem, result: experience.result, status: experience.status, run_id: experience.run_id, }, }, null, 2)); } finally { await pool.end(); } } main().catch((error) => { console.error('[experience-e2e] failed:', error instanceof Error ? error.message : error); process.exit(1); });