import { spawn } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { stopProcess } from './controlBridge'; /** Explicit live-test helper: keys stay in Python memory, only the ephemeral bridge * token crosses to the browser. Tighten (never bypass) production admission limits. */ export async function startLiveDecisionService() { const token = randomBytes(32).toString('hex'); const directory = await mkdtemp(join(tmpdir(), 'lekiwi-live-decision-')); const script = ` import asyncio, json, os, signal, sys from pathlib import Path from aiohttp import web from decision_server.server import create_app, STATE from decision_server.credentials import deepseek_llm, openrouter_jev from decision_server.protocol import DecisionError async def main(): app = create_app(Path(sys.argv[1]), os.environ.pop('DECISION_SMOKE_TOKEN'), origins={'http://127.0.0.1:4176'}, port=0) service = app[STATE] service.connections.values['llm'] = deepseek_llm(Path('.env')) service.connections.values['jev'] = openrouter_jev(Path('.env')) original = service.admit counts = {'llm': 0, 'jev': 0} def admit(role, stamp): if counts[role] >= {'llm': 1, 'jev': 12}[role]: raise DecisionError('live_smoke_budget', 429) original(role, stamp) counts[role] += 1 service.admit = admit runner = web.AppRunner(app, access_log=None, handler_cancellation=True) await runner.setup() site = web.TCPSite(runner, '127.0.0.1', 0) await site.start() port = site._server.sockets[0].getsockname()[1] service.hosts = {f'127.0.0.1:{port}'} stop = asyncio.Event() for sig in (signal.SIGTERM, signal.SIGINT): asyncio.get_running_loop().add_signal_handler(sig, stop.set) print('DECISION_READY ' + json.dumps({'endpoint': f'http://127.0.0.1:{port}'}), flush=True) try: await stop.wait() finally: await runner.cleanup() asyncio.run(main()) `; const child = spawn(resolve('.venv/bin/python'), ['-u', '-c', script, directory], { env: { ...process.env, DECISION_SMOKE_TOKEN: token, PYTHONNOUSERSITE: '1', PYTHONPATH: '' }, stdio: ['ignore', 'pipe', 'ignore'], }); const stop = async () => { if (child.pid) await stopProcess(child); await rm(directory, { recursive: true, force: true }); }; try { const endpoint = await new Promise((resolve, reject) => { let output = ''; const timer = setTimeout( () => reject(new Error('真实模型服务启动超时(不输出认证日志)')), 15000, ); child.once('error', () => { clearTimeout(timer); reject(new Error('真实模型服务启动失败')); }); child.once('exit', (code) => { clearTimeout(timer); reject(new Error(`真实模型服务退出:${code}`)); }); child.stdout.on('data', (chunk: Buffer) => { output = (output + chunk.toString()).slice(-8192); const match = output.match(/DECISION_READY (\{[^\n]+\})/); if (match) { clearTimeout(timer); resolve((JSON.parse(match[1]) as { endpoint: string }).endpoint); } }); }); return { endpoint, token, stop }; } catch (error) { await stop(); throw error; } }