/** * 探测 /api/chat 下发的政策卡片(`item` 事件)原始载荷。 * * 为什么需要:用户报「两张卡片政策名相同、申报事项不同,但点『查看详情』内容一样」。 * 前端 openDetailByItem 是按 `originalData.id / policy_id / declaration_item` 反查卡片索引的, * 若后端给同一政策的多个申报事项下发了**相同的 id**,findIndex 就会永远命中第一张。 * * 排查协议问题必须看**原始载荷**,不能只看封装后的中间事件(踩过)。 * * 用法: * node harness/tools/probe-policy-cards.mjs "我想在青浦开一家奶茶店,有什么相关扶持政策吗" [chatApiBase] * 默认 chatApiBase = http://192.168.2.23:8000(可用 dev 代理 https://localhost:8083/chat-api) */ const question = process.argv[2] || '我想在青浦开一家奶茶店,有什么相关扶持政策吗'; const base = (process.argv[3] || 'http://192.168.2.23:8000').replace(/\/+$/, ''); const url = base.endsWith('/api/chat') ? base : `${base}/api/chat`; console.log(`POST ${url}\nquestion: ${question}\n`); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: JSON.stringify({ thread_id: `probe_${Date.now()}`, question }), }); if (!response.ok) { console.error('HTTP', response.status, await response.text()); process.exit(1); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; const items = []; let currentEvent = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const raw of lines) { const line = raw.replace(/\r$/, ''); if (line.startsWith('event:')) { currentEvent = line.slice(6).trim(); } else if (line.startsWith('data:')) { const payload = line.slice(5).trim(); if (currentEvent === 'item') { try { items.push(JSON.parse(payload)); } catch { /* 分片 JSON,忽略,靠完整帧 */ } } if (currentEvent === 'done' || currentEvent === 'error') { currentEvent = ''; } } } } console.log(`收到 ${items.length} 个 item 事件\n`); console.log('═'.repeat(70)); items.forEach((evt, i) => { const d = evt?.data ?? evt; const list = Array.isArray(d) ? d : d?.data || []; const arr = Array.isArray(list) ? list : [list]; arr.forEach((it, k) => { console.log(`\n【item ${i}.${k}】`); console.log(' id :', JSON.stringify(it?.id)); console.log(' policy_id :', JSON.stringify(it?.policy_id)); console.log(' name/title :', JSON.stringify(it?.name ?? it?.title ?? it?.['政策名称'])); console.log(' declaration :', JSON.stringify(it?.declaration_item ?? it?.['申报事项'])); console.log(' 企业收益 :', JSON.stringify(it?.data?.['企业收益'] ?? it?.['企业收益'])); console.log(' 顶层字段 :', Object.keys(it || {}).join(',')); }); }); // ── 关键判据:有没有两条 item 共用同一个 id/policy_id ────────────────── const ids = []; for (const evt of items) { const d = evt?.data ?? evt; const list = Array.isArray(d) ? d : d?.data || []; for (const it of Array.isArray(list) ? list : [list]) { ids.push({ id: it?.id, policy_id: it?.policy_id, decl: it?.declaration_item ?? it?.['申报事项'] }); } } const dupId = new Map(); for (const x of ids) { const key = x.id ?? x.policy_id; if (key === undefined || key === null) continue; if (!dupId.has(key)) dupId.set(key, []); dupId.get(key).push(x.decl); } const clashes = [...dupId.entries()].filter(([, v]) => v.length > 1); console.log('\n' + '═'.repeat(70)); console.log('关键判据 —— 是否有不同申报事项共用同一个 id:'); if (!clashes.length) { console.log(' ✅ 没有:每条 item 的 id 都是独立的(问题可能出在别处)'); } else { console.log(` ❌ 有 ${clashes.length} 组共用 id —— 这就是「点进去内容一样」的原因:`); clashes.forEach(([id, decls]) => console.log(` id=${id} 对应 ${decls.length} 个申报事项:${decls.join(' / ')}`)); }