/* ============================================================ * debug_rail_replay_wuyuan.js —— 用真实失败案例回放,验证修复有效 * * 背景:2026-09-30 生成「婺源」时返回 payload_invalid,唯一 error 是 * 「第一天缺「夜逛」环节(night_stroll)」——但 AI 其实排了 19:00 的夜逛。 * 根因是校验器写死了 day.label 含「第一天」,而 AI 标的是「周六/周日」。 * * 本脚本把那次失败响应的 payload 原样取出来,走一遍「修复 → 校验」, * 用来证明修复后不再误报(同时也顺带检查其余 warning 是否已消除)。 * * 用法: * node dev_test_scripts/debug/debug_rail_replay_wuyuan.js 只回放校验,不写任何文件 * node dev_test_scripts/debug/debug_rail_replay_wuyuan.js --save 额外把这份结果写回 * 技能私有 data/last_result.json, * 这样用户可在页面上点「载入最近一次生成」 * 直接看到并导出这份婺源行程(不必重烧 token) * * 为什么需要 --save:服务端只在「校验 0 errors」时才落盘,那次因误判被丢弃, * 用户 4 分钟的生成白费了。这个开关是给「已经拿到完整 payload、只想补落盘」的补救场景用的。 * ============================================================ */ const fs = require('fs') const path = require('path') const os = require('os') const SRC = path.join( os.homedir(), 'AppData', 'Roaming', 'Trae CN', 'User', 'workspaceStorage', '540d38674b58f7802a478abf6956abc7', 'long-text', '6ab1e0ff40ac82e5fecf1de9', 'munlta58-u92r', '当前状态__失败__....txt' ) const SKILL = path.join(process.cwd(), 'src', 'server', 'thought_lab', 'labs', 'travel_citytour', 'skills', 'sh_2d_rail_hike') const skillIndex = require(path.join(SKILL, 'index.js')) const { validatePayload } = require(path.join(SKILL, 'validate.js')) const { repairPayload } = require(path.join(SKILL, 'repair.js')) if (!fs.existsSync(SRC)) { console.log('找不到失败响应文件(可能已被清理):' + SRC) process.exit(0) } const raw = fs.readFileSync(SRC, 'utf-8') // 响应体从「返回结果」后的第一个 '{' 开始,到「错误信息」之前结束 const startMark = raw.indexOf('返回结果') const start = raw.indexOf('{', startMark) const endMark = raw.indexOf('错误信息', start) const text = raw.slice(start, endMark > 0 ? endMark : undefined).trim() let envelope = null try { envelope = JSON.parse(text) } catch (e) { // 兜底:按大括号配平截取 let depth = 0, endIdx = -1 for (let i = start; i < raw.length; i++) { if (raw[i] === '{') depth++ else if (raw[i] === '}') { depth--; if (depth === 0) { endIdx = i; break } } } envelope = JSON.parse(raw.slice(start, endIdx + 1)) } const payload = envelope.payload const bundle = skillIndex.loadBundle() console.log('===== 婺源失败案例回放 =====') console.log('原文 error:' + JSON.stringify(envelope.check && envelope.check.errors)) console.log('day.label 实际写法:' + ((payload.timeline && payload.timeline.days) || []).map((d) => JSON.stringify(d.label)).join(' / ')) console.log('AI 是否排了夜逛行:' + ((payload.timeline && payload.timeline.days) || []) .reduce((a, d) => a.concat(d.rows || []), []) .some((r) => r.anchor_id === 'night_stroll')) console.log('') const repaired = repairPayload(JSON.parse(JSON.stringify(payload)), bundle) const chk = validatePayload(repaired.payload, bundle) console.log('— 修复说明 —') repaired.fixes.forEach((f) => console.log(' · ' + f)) console.log('') console.log('— 修复后校验结果 —') console.log('errors : ' + chk.errors.length + (chk.errors.length ? '\n ✗ ' + chk.errors.join('\n ✗ ') : '')) console.log('warnings: ' + chk.warnings.length + (chk.warnings.length ? '\n ! ' + chk.warnings.join('\n ! ') : '')) console.log('') console.log(chk.errors.length === 0 ? '✓ 回放通过:这份真实失败的 payload 修复后 0 errors,不再被误判' : '✗ 仍有 error,需继续排查') // --save:把修复后的结果补落盘,供页面「载入最近一次生成」使用 if (process.argv.indexOf('--save') >= 0) { if (chk.errors.length) { console.log('\n⚠ 仍有 error,拒绝落盘(避免把不合格结果写进 last_result)') process.exit(1) } const outEnvelope = { ok: true, payload: repaired.payload, report_spec: bundle.report_spec || {}, destination_override: null, check: chk, meta: Object.assign({}, envelope.meta, { repairs: repaired.fixes, recovered_from: 'payload_invalid 误判(校验器已修复)' }) } const dataDir = path.join(SKILL, 'data') fs.mkdirSync(dataDir, { recursive: true }) const out = path.join(dataDir, 'last_result.json') fs.writeFileSync(out, JSON.stringify(outEnvelope), 'utf-8') console.log('\n✓ 已补落盘:' + out) console.log(' 下一步:打开本技能页面 → 点「载入最近一次生成」即可查看/导出这份婺源行程') } process.exit(chk.errors.length === 0 ? 0 : 1)