/* ============================================================ * test_travel_citytour_generate_integration.js * 「游山玩水」生成链路集成测试:不依赖服务重启,直接调用 lab 内的 ai.js * * 验证目标: * 1. prompt 组装 + 百炼调用是否通 * 2. qwen-plus-latest 能否产出符合 output_schema 的整份行程(会不会被截断) * 3. 结构校验(validate.js)出来多少 errors / warnings * 4. 端到端耗时(决定前端要不要长 loading 提示) * * 运行:node dev_test_scripts/integration/test_travel_citytour_generate_integration.js * 产物:payload 落在系统临时目录(不进仓库),仅用于人工查看 * ============================================================ */ const fs = require('fs') const path = require('path') // 载入本机私有凭据(与服务端同一份文件) const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local') if (fs.existsSync(envPath)) { for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) { if (!line || /^\s*#/.test(line)) continue const i = line.indexOf('=') if (i < 0) continue const k = line.slice(0, i).trim() const v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '') if (k && !(k in process.env)) process.env[k] = v } } const LAB = path.join(process.cwd(), 'src', 'server', 'thought_lab', 'labs', 'travel_citytour') const SKILL_ID = process.argv[2] || 'sh_2d_car_hike' const DEST = process.argv[3] || '南通如东' const MODEL_OVERRIDE = process.argv[4] || '' const MAXTOK_OVERRIDE = Number(process.argv[5]) || 0 // 技能全栈自包含:引擎三件套与技能包都在 skills// 内 const SKILL_DIR = path.join(LAB, 'skills', SKILL_ID) const skillIndex = require(path.join(SKILL_DIR, 'index.js')) const { validatePayload } = require(path.join(SKILL_DIR, 'validate.js')) const { repairPayload } = require(path.join(SKILL_DIR, 'repair.js')) const ai = require(path.join(SKILL_DIR, 'ai.js')) // 红线能力(callLLM / 北京时间 / JSON 提取)由 lab 层提供,测试直接复用同一实现,避免漂移 const { makeCtx } = require(path.join(LAB, 'index.js')) const ctx = makeCtx(SKILL_ID) const main = async () => { const cfg = JSON.parse(fs.readFileSync(path.join(SKILL_DIR, 'config.json'), 'utf8')) const aiConfig = Object.assign({}, cfg.ai || {}) if (MODEL_OVERRIDE) aiConfig.model = MODEL_OVERRIDE if (MAXTOK_OVERRIDE) aiConfig.max_tokens = MAXTOK_OVERRIDE const bundle = skillIndex.loadBundle() const dest = skillIndex.findDestination(bundle, DEST) const answers = { destination: DEST, departure_origin: bundle.skill.base_origin || '', departure_date: '2026-09-19', departure_time: '06:40', vehicle_type: 'fuel', headcount: 1 } console.log('===== 生成链路集成测试 =====') console.log('skill :', bundle.skill.id, 'v' + bundle.skill.version, '(' + (bundle.skill.anchors || []).length + ' 槽位)') console.log('skill_dir :', SKILL_DIR) console.log('destination:', DEST, dest ? '(技能包内有覆盖数据)' : '(首次生成,无覆盖数据)') console.log('model :', aiConfig.model, ' temp=', aiConfig.temperature, ' max_tokens=', aiConfig.max_tokens) console.log('ai_ready :', ctx.aiReady() ? '已配置(密钥由 lab 层持有)' : '缺失!') console.log('system 长度:', ai.buildMessages(bundle, answers, dest)[0].content.length, '字符') console.log('开始生成…\n') const t0 = Date.now() let result = null try { result = await ai.generate({ bundle, answers, dest, aiConfig, ctx }) } catch (e) { console.log('❌ 生成失败:' + String(e.message || e)) if (e.raw) console.log('原始输出前 1200 字:\n' + String(e.raw).slice(0, 1200)) process.exit(1) } // 与服务端一致:先无损修复,再校验 const repaired = repairPayload(result.payload, bundle) const check = validatePayload(repaired.payload, bundle) const outDir = path.join(process.env.TEMP || 'C:\\Windows\\Temp', 'travel_citytour_out') fs.mkdirSync(outDir, { recursive: true }) const outFile = path.join(outDir, 'payload_' + SKILL_ID + '.json') fs.writeFileSync(outFile, JSON.stringify(repaired.payload, null, 2), 'utf8') console.log('✅ 生成成功') console.log('耗时 :', result.elapsed_ms + ' ms(含 ' + result.attempts + ' 次尝试)') console.log('总墙钟耗时 :', (Date.now() - t0) + ' ms') console.log('finish_reason:', result.finish_reason || '(空)') console.log('usage :', JSON.stringify(result.usage || {})) console.log('\n结构规模 :', JSON.stringify(check.stats)) console.log('无损修复 :', repaired.fixes.length, repaired.fixes.length ? '\n - ' + repaired.fixes.slice(0, 12).join('\n - ') : '') console.log('errors :', check.errors.length, check.errors.length ? '\n - ' + check.errors.slice(0, 12).join('\n - ') : '') console.log('warnings :', check.warnings.length, check.warnings.length ? '\n - ' + check.warnings.slice(0, 12).join('\n - ') : '') console.log('\npayload 落盘:', outFile, '(' + (fs.statSync(outFile).size / 1024).toFixed(1) + ' KB)') console.log('summary 标题:', (repaired.payload.summary || {}).title || '(无)') console.log('\n' + (check.errors.length === 0 ? '✓ 结构校验通过,可直接渲染' : '✗ 结构校验未通过,需按上面 errors 调整规则或 prompt')) process.exit(check.errors.length === 0 ? 0 : 2) } main().catch((e) => { console.error('FATAL', e && e.message); process.exit(1) })