| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- /**
- * 端到端验证「会话 / 整段对话 / 反馈」在 DMS 里的读写(栏目 1887 / 1889)。
- *
- * 存储粒度约定(用户 2026-09-17 定的):
- * - 1887 助手会话:**一个会话一行**(标题、归属、更新时间)
- * - 1889 助手问答记录:**也是一个会话一行**,整段对话的消息数组以 JSON 存在 `c_answer`;
- * 反馈记在 JSON 里对应那条消息上
- *
- * 验证什么:前端真实调用的那些函数(引用 src 里同一份实现,不是复制逻辑)
- * 打到真实 DMS 上能否正确 增/改/查/删,尤其是:
- * ① 反复保存同一会话**只有一行**(不是一轮问答一行)
- * ② 反馈能在 JSON 里精确定位到某条消息
- * ③ 历史读回能与写入的消息一一对应
- *
- * 怎么跑(需要 dev server 起着,代理会注入 token):
- * 1) npm run dev
- * 2) npx esbuild harness/tools/_entry-dms.ts --bundle --format=esm \
- * --outfile=harness/tools/_dms.mjs --alias:@=./src --define:import.meta.env='{}'
- * 3) node harness/tools/verify-dms-chat-storage.mjs https://localhost:8083/dms-api
- *
- * 脚本**不持有 token**——和浏览器一样只打代理地址。
- */
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
- const base = process.argv[2] || 'https://localhost:8083/dms-api';
- globalThis.VITE_DMS_API = base;
- const {
- upsertDmsSession,
- saveDmsTranscript,
- fetchDmsSessions,
- fetchDmsSessionRecords,
- deleteDmsSession,
- writeDmsFeedback,
- resolveDmsCreditCode,
- parseDmsTimestamp,
- nowDmsTimestamp,
- searchDmsContents,
- deleteDmsContent,
- DMS_COLUMN_SESSION,
- DMS_COLUMN_RECORD,
- } = await import('./_dms.mjs');
- let pass = 0;
- let fail = 0;
- const check = (name, ok, extra = '') => {
- if (ok) {
- pass++;
- console.log(' ok ' + name);
- } else {
- fail++;
- console.log(' FAIL ' + name + ' ' + extra);
- }
- };
- const TEST_CREDIT = `verify_cc_${Date.now().toString(36)}`;
- const SESSION_ID = `verify_sess_${Date.now().toString(36)}`;
- const MSG_USER_1 = 'msg_u1_' + Date.now().toString(36);
- const MSG_AI_1 = 'msg_a1_' + Date.now().toString(36);
- const MSG_USER_2 = 'msg_u2_' + Date.now().toString(36);
- const MSG_AI_2 = 'msg_a2_' + Date.now().toString(36);
- const TITLE_A = '验证会话标题';
- const TITLE_B = '验证会话标题(改过)';
- const Q1 = '高新技术企业的奖励是多少?';
- const A1 = '<!-- POLICY_TABLE {"data":[]} -->\n<scope title="思考中">\n</scope>\n第一轮回答。';
- const Q2 = '那申报条件呢?';
- const A2 = '第二轮回答,带换行。\n第二行。';
- /** 模拟前端的消息列表:两轮问答 */
- const transcriptAfterRound1 = [
- { id: MSG_USER_1, role: 'user', content: Q1 },
- { id: MSG_AI_1, role: 'ai', content: A1 },
- ];
- const transcriptAfterRound2 = [
- ...transcriptAfterRound1,
- { id: MSG_USER_2, role: 'user', content: Q2 },
- { id: MSG_AI_2, role: 'ai', content: A2 },
- ];
- console.log(`\nDMS 会话/整段对话/反馈 端到端验证\n 代理地址: ${base}\n 测试标记: ${TEST_CREDIT}\n`);
- const rowsForSession = (columnId) =>
- searchDmsContents(columnId, {
- search: [{ field: 'c_session_id', searchType: 1, content: { value: SESSION_ID } }],
- page: 0,
- pageSize: 20,
- });
- const cleanup = async () => {
- await deleteDmsSession(SESSION_ID);
- for (const col of [DMS_COLUMN_SESSION, DMS_COLUMN_RECORD]) {
- const rows = await searchDmsContents(col, {
- search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT } }],
- page: 0,
- pageSize: 50,
- });
- for (const row of rows) if (row.id) await deleteDmsContent(col, String(row.id));
- }
- };
- try {
- // ── 1. 会话行 ──────────────────────────────────────────────────────
- console.log('【1】会话(1887)');
- check('upsertDmsSession 新增', (await upsertDmsSession({ sessionId: SESSION_ID, title: TITLE_A, creditCode: TEST_CREDIT })) === true);
- let sessions = await fetchDmsSessions(TEST_CREDIT);
- check('列表能查到', sessions.some((s) => s.session_id === SESSION_ID));
- check('标题正确', sessions.find((s) => s.session_id === SESSION_ID)?.title === TITLE_A);
- check('updated_at 合理', Math.abs((sessions.find((s) => s.session_id === SESSION_ID)?.updated_at || 0) - Date.now()) < 10 * 60 * 1000);
- await upsertDmsSession({ sessionId: SESSION_ID, title: TITLE_B, creditCode: TEST_CREDIT });
- sessions = await fetchDmsSessions(TEST_CREDIT);
- check('改名后只有一行', sessions.filter((s) => s.session_id === SESSION_ID).length === 1);
- check('标题已更新', sessions.find((s) => s.session_id === SESSION_ID)?.title === TITLE_B);
- // ── 2. 整段对话:一轮一问一答 → 一行 ───────────────────────────────
- console.log('\n【2】整段对话(1889)—— 粒度必须是「一会话一行」');
- await saveDmsTranscript({ sessionId: SESSION_ID, messages: transcriptAfterRound1, creditCode: TEST_CREDIT });
- let rows = await rowsForSession(DMS_COLUMN_RECORD);
- check('第一轮后:只有一行', rows.length === 1, `实际 ${rows.length} 行`);
- // ── 3. 第二轮:仍是同一行(不能变成两行)───────────────────────────
- await saveDmsTranscript({ sessionId: SESSION_ID, messages: transcriptAfterRound2, creditCode: TEST_CREDIT });
- rows = await rowsForSession(DMS_COLUMN_RECORD);
- check('第二轮后:仍是同一行(不是一封问答一行)', rows.length === 1, `实际 ${rows.length} 行`);
- const stored = JSON.parse(rows[0]?.c_answer || '{}');
- check('存的是包装对象 {version, messages}', stored.version === 1 && Array.isArray(stored.messages), Object.keys(stored).join(','));
- check('整段 4 条消息都在', stored.messages?.length === 4, `实际 ${stored.messages?.length} 条`);
- check('消息顺序与角色正确', stored.messages?.map((m) => m.role).join(',') === 'user,ai,user,ai', stored.messages?.map((m) => m.role).join(','));
- check('第一轮回答完整往返(含协议标记)', stored.messages?.[1]?.content === A1, `长 ${String(stored.messages?.[1]?.content || '').length}/${A1.length}`);
- check('第二轮回答完整往返', stored.messages?.[3]?.content === A2);
- check('消息 id 保留(反馈定位依赖)', stored.messages?.map((m) => m.id).join(',') === [MSG_USER_1, MSG_AI_1, MSG_USER_2, MSG_AI_2].join(','));
- check('c_question 存了首问', rows[0]?.c_question === Q1, JSON.stringify(rows[0]?.c_question));
- // ── 4. 历史读回 ───────────────────────────────────────────────────
- console.log('\n【4】历史读回');
- const records = await fetchDmsSessionRecords(SESSION_ID);
- check('读出两轮问答', records.length === 2, `实际 ${records.length} 轮`);
- check('第一轮 question/answer 正确', records[0]?.question === Q1 && records[0]?.answer === A1);
- check('第二轮 question/answer 正确', records[1]?.question === Q2 && records[1]?.answer === A2);
- check('record_id 回填的是 AI 消息 id(反馈闭环)', records[0]?.record_id === MSG_AI_1 && records[1]?.record_id === MSG_AI_2,
- `${records[0]?.record_id} / ${records[1]?.record_id}`);
- // ── 5. 反馈:定位到 JSON 里那一条消息 ──────────────────────────────
- console.log('\n【5】反馈(写进该会话行 JSON 的第二条 AI 消息)');
- await writeDmsFeedback({ recordId: MSG_AI_2, sessionId: SESSION_ID, status: 2, option: '没有回答我的问题', remark: '验证备注' });
- rows = await rowsForSession(DMS_COLUMN_RECORD);
- check('反馈后仍是一行', rows.length === 1, `实际 ${rows.length} 行`);
- const after = JSON.parse(rows[0]?.c_answer || '{}');
- const fbMsg = after.messages?.find((m) => m.id === MSG_AI_2);
- const untouched = after.messages?.find((m) => m.id === MSG_AI_1);
- check('命中的那条消息带上了反馈', fbMsg?.feedback === 2, String(fbMsg?.feedback));
- check('feedbackOption 正确', fbMsg?.feedbackOption === '没有回答我的问题', JSON.stringify(fbMsg?.feedbackOption));
- check('feedbackRemark 正确', fbMsg?.feedbackRemark === '验证备注', JSON.stringify(fbMsg?.feedbackRemark));
- check('feedbackAt 已写入', typeof fbMsg?.feedbackAt === 'number' && fbMsg.feedbackAt > 0, String(fbMsg?.feedbackAt));
- check('**另一条消息没被误改**', untouched && untouched.feedback === undefined, JSON.stringify(untouched?.feedback));
- check('对话内容未被反馈覆盖', after.messages?.length === 4 && after.messages[1].content === A1);
- // 取消反馈
- await writeDmsFeedback({ recordId: MSG_AI_2, sessionId: SESSION_ID, status: 0 });
- rows = await rowsForSession(DMS_COLUMN_RECORD);
- const cancelled = JSON.parse(rows[0]?.c_answer || '{}').messages?.find((m) => m.id === MSG_AI_2);
- check('取消反馈 status=0', cancelled?.feedback === 0, String(cancelled?.feedback));
- // 反馈定位不到时不能造出空行
- const beforeCount = (await rowsForSession(DMS_COLUMN_RECORD)).length;
- await writeDmsFeedback({ recordId: 'not_exist_msg', sessionId: SESSION_ID, status: 1 });
- await writeDmsFeedback({ recordId: MSG_AI_1, sessionId: 'not_exist_session', status: 1 });
- check('定位不到时不新增行', (await rowsForSession(DMS_COLUMN_RECORD)).length === beforeCount);
- // ── 6. 删除 ───────────────────────────────────────────────────────
- console.log('\n【6】删除会话');
- check('deleteDmsSession 返回 true', (await deleteDmsSession(SESSION_ID)) === true);
- check('会话行已删', !(await fetchDmsSessions(TEST_CREDIT)).some((s) => s.session_id === SESSION_ID));
- check('对话行已连带删除', (await rowsForSession(DMS_COLUMN_RECORD)).length === 0);
- // ── 7. 工具函数 ───────────────────────────────────────────────────
- console.log('\n【7】时间戳与访客归属');
- check('nowDmsTimestamp 格式', /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(nowDmsTimestamp()), nowDmsTimestamp());
- check('parseDmsTimestamp 毫秒', parseDmsTimestamp(1789651800000) === 1789651800000);
- check('parseDmsTimestamp 秒', parseDmsTimestamp(1789651800) === 1789651800000);
- check('parseDmsTimestamp 字符串', parseDmsTimestamp('2026-09-17 21:30:00') === 1789651800000, String(parseDmsTimestamp('2026-09-17 21:30:00')));
- check('访客归属形态 访客_<id>', /^访客_.+/.test(resolveDmsCreditCode()), resolveDmsCreditCode());
- } finally {
- console.log('\n【清理】');
- await cleanup();
- const left = [
- ...(await searchDmsContents(DMS_COLUMN_SESSION, { search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT } }], page: 0, pageSize: 50 })),
- ...(await searchDmsContents(DMS_COLUMN_RECORD, { search: [{ field: 'c_credit_code', searchType: 1, content: { value: TEST_CREDIT } }], page: 0, pageSize: 50 })),
- ];
- console.log(` 测试数据已清理(残留 ${left.length} 行)`);
- }
- console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
- process.exit(fail ? 1 : 0);
|