Files
Toolbox/dev_test_scripts/integration/test_travel_citytour_generate_integration.js

105 lines
5.5 KiB
JavaScript

/* ============================================================
* 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/<skill_id>/ 内
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) })