feat(knowledge): add short-term ask memory
This commit is contained in:
@@ -131,6 +131,7 @@ FROM (
|
||||
UNION ALL SELECT 'aihr_knowledge_app'
|
||||
UNION ALL SELECT 'aihr_knowledge_app_space'
|
||||
UNION ALL SELECT 'aihr_knowledge_query_log'
|
||||
UNION ALL SELECT 'aihr_knowledge_conversation'
|
||||
UNION ALL SELECT 'aihr_knowledge_admin_audit'
|
||||
UNION ALL SELECT 'aihr_learning_question'
|
||||
UNION ALL SELECT 'aihr_practice_question_feedback'
|
||||
@@ -162,7 +163,7 @@ REMOTE
|
||||
)" || fail "remote schema check failed: $remote_ssh:$remote_db"
|
||||
|
||||
[[ -z "$missing" ]] || fail "remote schema missing required tables: $(printf '%s' "$missing" | tr '\n' ', ' | sed 's/, $//')"
|
||||
echo "remote_schema=34/34 $remote_ssh:$remote_db"
|
||||
echo "remote_schema=35/35 $remote_ssh:$remote_db"
|
||||
}
|
||||
|
||||
changed_files="$(git status --porcelain)"
|
||||
|
||||
@@ -27,6 +27,7 @@ CREATE TABLE aihr_knowledge_app (tenant_id varchar(20), label varchar(20)) ENGIN
|
||||
CREATE TABLE aihr_knowledge_app_space (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
CREATE TABLE aihr_knowledge_query_log (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
CREATE TABLE aihr_knowledge_admin_audit (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
CREATE TABLE aihr_knowledge_conversation (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
CREATE TABLE aihr_learning_question (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
CREATE TABLE aihr_practice_question_feedback (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
CREATE TABLE aihr_onboard_exam (tenant_id varchar(20), label varchar(20)) ENGINE=InnoDB;
|
||||
@@ -53,6 +54,6 @@ aligned_columns="$(mysql "$DB" -N -B -e "
|
||||
AND column_name IN ('tenant_id', 'label')
|
||||
AND collation_name='utf8mb4_general_ci';
|
||||
")"
|
||||
test "$aligned_columns" = '36'
|
||||
test "$aligned_columns" = '38'
|
||||
|
||||
echo "PASS: AIHR release tables inherit the target database collation and the migration is idempotent"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/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');
|
||||
Reference in New Issue
Block a user