#!/usr/bin/env node import crypto from 'node:crypto'; const baseUrl = process.env.AIHR_BASE_URL || 'http://127.0.0.1:8080'; const phone = process.env.AIHR_VERIFY_PHONE || '13900000001'; const smsCode = process.env.AIHR_VERIFY_SMS_CODE || '123456'; const clientId = '428a8310cd442757ae699df5d894f051'; async function request(endpoint, { method = 'GET', body, token } = {}) { const response = await fetch(`${baseUrl}${endpoint}`, { method, headers: { clientid: clientId, 'Content-Language': 'zh_CN', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body ? { 'Content-Type': 'application/json;charset=utf-8' } : {}) }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(30_000) }); const text = await response.text(); let payload; try { payload = JSON.parse(text); } catch { throw new Error(`${endpoint} 返回了非 JSON 响应(HTTP ${response.status})`); } if (!response.ok || Number(payload.code) !== 200) { throw new Error(`${endpoint} 失败:HTTP ${response.status} / ${payload.code} ${payload.msg || ''}`); } return payload.data; } function assert(condition, message) { if (!condition) throw new Error(message); } await request(`/resource/sms/code?phonenumber=${encodeURIComponent(phone)}`); const login = await request('/auth/mobile/sms-login', { method: 'POST', body: { phonenumber: phone, smsCode, tenantId: '000000' } }); const token = login?.access_token; assert(token, '手机号登录未返回 access_token'); const conversationId = crypto.randomUUID(); const first = await request('/api/knowledge/query', { method: 'POST', token, body: { queryText: '报修受理后第一步应该做什么?', category: 'sop', position: '生活顾问', limit: 5, conversationId, contextVersion: 0 } }); assert(first?.conversationId === conversationId, '首问没有返回同一 conversationId'); assert(Number(first?.contextVersion) === 1, '首问 contextVersion 未从 0 递增到 1'); assert(first?.intent === 'QA', `首问意图错误:${first?.intent}`); assert(!first?.noEvidence, '首问未召回已授权的报修依据'); const followup = await request('/api/knowledge/query', { method: 'POST', token, body: { queryText: '把对应原文件给我', category: 'sop', position: '生活顾问', limit: 5, conversationId, contextVersion: first.contextVersion } }); assert(followup?.conversationId === conversationId, '追问没有保持同一 conversationId'); assert(Number(followup?.contextVersion) === 2, '追问 contextVersion 未从 1 递增到 2'); assert(followup?.intent === 'FILE', `追问意图错误:${followup?.intent}`); assert(String(followup?.rewrittenQuery || '').includes('报修'), '追问没有结合首问主题重写'); assert(Array.isArray(followup?.resources) && followup.resources.length > 0, '追问未返回授权原文件'); const resource = followup.resources[0]; assert(resource?.contentUrl?.startsWith('/api/knowledge/resources/'), '原文件没有使用受保护内容地址'); const download = await fetch(`${baseUrl}${resource.contentUrl}`, { headers: { clientid: clientId, Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(30_000) }); assert(download.ok, `原文件下载失败:HTTP ${download.status}`); assert((await download.arrayBuffer()).byteLength > 0, '原文件下载内容为空'); const videoFollowup = await request('/api/knowledge/query', { method: 'POST', token, body: { queryText: '有对应操作视频吗?', category: 'sop', position: '生活顾问', limit: 5, conversationId, contextVersion: followup.contextVersion } }); assert(Number(videoFollowup?.contextVersion) === 3, '视频追问 contextVersion 未递增到 3'); assert(videoFollowup?.intent === 'VIDEO', `视频追问意图错误:${videoFollowup?.intent}`); assert(Array.isArray(videoFollowup?.resources) && videoFollowup.resources.length === 0, '无视频时返回了伪造资源'); assert(String(videoFollowup?.answer || '').includes('未找到'), '无视频时没有明确说明未找到'); const staleResponse = await fetch(`${baseUrl}/api/knowledge/query`, { method: 'POST', headers: { clientid: clientId, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json;charset=utf-8' }, body: JSON.stringify({ queryText: '再说一遍', category: 'sop', conversationId, contextVersion: first.contextVersion }), signal: AbortSignal.timeout(30_000) }); const stalePayload = await staleResponse.json(); assert(Number(stalePayload.code) === 409, `旧上下文版本未被拒绝:${stalePayload.code}`); console.log('PASS 手机号真实登录'); console.log('PASS 首问召回有依据答案,contextVersion=1'); console.log(`PASS 追问识别 FILE 并重写为:${followup.rewrittenQuery}`); console.log(`PASS 授权原文件下载:${resource.title}`); console.log('PASS 视频追问无匹配时明确返回空资源'); console.log('PASS 旧 contextVersion 返回 409');