|
|
@@ -1,16 +1,17 @@
|
|
|
/**
|
|
|
- * 端到端验证「会话 / 整段对话 / 反馈」在 DMS 里的读写(栏目 1887 / 1889)。
|
|
|
+ * 端到端验证「会话 / 问答记录 / 反馈」在 DMS 里的读写(栏目 1887 / 1889)。
|
|
|
*
|
|
|
- * 存储粒度约定(用户 2026-09-17 定的):
|
|
|
- * - 1887 助手会话:**一个会话一行**(标题、归属、更新时间)
|
|
|
- * - 1889 助手问答记录:**也是一个会话一行**,整段对话的消息数组以 JSON 存在 `c_answer`;
|
|
|
- * 反馈记在 JSON 里对应那条消息上
|
|
|
+ * 存储粒度约定(2026-09-17 用户最终确认):
|
|
|
+ * - 1887 助手会话:一个会话一行(标题、归属、更新时间)
|
|
|
+ * - 1889 助手问答记录:**一问一答一条**(幂等键 c_record_id),
|
|
|
+ * c_question / c_answer 各存一半;反馈写在该行的 c_feedback_* 列上
|
|
|
+ * (曾一度改成「一个会话一行、整段对话 JSON」,按用户要求已改回本版)
|
|
|
*
|
|
|
* 验证什么:前端真实调用的那些函数(引用 src 里同一份实现,不是复制逻辑)
|
|
|
* 打到真实 DMS 上能否正确 增/改/查/删,尤其是:
|
|
|
- * ① 反复保存同一会话**只有一行**(不是一轮问答一行)
|
|
|
- * ② 反馈能在 JSON 里精确定位到某条消息
|
|
|
- * ③ 历史读回能与写入的消息一一对应
|
|
|
+ * ① 反复保存同一轮**只有一行**(不是每问一次多一行)
|
|
|
+ * ② 补 answer 时不覆盖 question、不变成两行
|
|
|
+ * ③ 反馈能命中该轮那一行
|
|
|
*
|
|
|
* 怎么跑(需要 dev server 起着,代理会注入 token):
|
|
|
* 1) npm run dev
|
|
|
@@ -28,7 +29,7 @@ globalThis.VITE_DMS_API = base;
|
|
|
|
|
|
const {
|
|
|
upsertDmsSession,
|
|
|
- saveDmsTranscript,
|
|
|
+ upsertDmsRecord,
|
|
|
fetchDmsSessions,
|
|
|
fetchDmsSessionRecords,
|
|
|
deleteDmsSession,
|
|
|
@@ -56,10 +57,8 @@ const check = (name, ok, 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 REC_1 = 'rec1_' + Date.now().toString(36);
|
|
|
+const REC_2 = 'rec2_' + Date.now().toString(36);
|
|
|
const TITLE_A = '验证会话标题';
|
|
|
const TITLE_B = '验证会话标题(改过)';
|
|
|
const Q1 = '高新技术企业的奖励是多少?';
|
|
|
@@ -67,24 +66,13 @@ const A1 = '<!-- POLICY_TABLE {"data":[]} -->\n<scope title="思考中">\n</scop
|
|
|
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, {
|
|
|
+console.log(`\nDMS 会话/问答记录/反馈 端到端验证\n 代理地址: ${base}\n 测试标记: ${TEST_CREDIT}\n`);
|
|
|
+
|
|
|
+const rowsForSession = () =>
|
|
|
+ searchDmsContents(DMS_COLUMN_RECORD, {
|
|
|
search: [{ field: 'c_session_id', searchType: 1, content: { value: SESSION_ID } }],
|
|
|
page: 0,
|
|
|
- pageSize: 20,
|
|
|
+ pageSize: 50,
|
|
|
});
|
|
|
|
|
|
const cleanup = async () => {
|
|
|
@@ -113,70 +101,67 @@ try {
|
|
|
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】历史读回');
|
|
|
+ // ── 2. 粒度:一问一答一条 ──────────────────────────────────────────
|
|
|
+ console.log('\n【2】问答记录(1889)—— 粒度必须是「一问一答一条」');
|
|
|
+ await upsertDmsRecord({ recordId: REC_1, sessionId: SESSION_ID, creditCode: TEST_CREDIT, question: Q1 });
|
|
|
+ let rows = await rowsForSession();
|
|
|
+ check('第 1 轮提交后:1 行', rows.length === 1, `实际 ${rows.length} 行`);
|
|
|
+ check('c_question 正确', rows[0]?.c_question === Q1, JSON.stringify(rows[0]?.c_question));
|
|
|
+ check('c_record_id 存的是该轮标识', rows[0]?.c_record_id === REC_1, String(rows[0]?.c_record_id));
|
|
|
+ check('此时 c_answer 为空', !rows[0]?.c_answer, JSON.stringify(rows[0]?.c_answer));
|
|
|
+
|
|
|
+ // 回答完成 → 补 answer,不能变成两行、不能冲掉 question
|
|
|
+ await upsertDmsRecord({ recordId: REC_1, sessionId: SESSION_ID, creditCode: TEST_CREDIT, answer: A1 });
|
|
|
+ rows = await rowsForSession();
|
|
|
+ check('补 answer 后仍是 1 行', rows.length === 1, `实际 ${rows.length} 行`);
|
|
|
+ check('answer 完整往返(含协议标记)', rows[0]?.c_answer === A1, `长 ${String(rows[0]?.c_answer || '').length}/${A1.length}`);
|
|
|
+ check('question 未被覆盖', rows[0]?.c_question === Q1, JSON.stringify(rows[0]?.c_question));
|
|
|
+
|
|
|
+ // 第 2 轮 → 应该是**新的一行**(一问一答一条)
|
|
|
+ await upsertDmsRecord({ recordId: REC_2, sessionId: SESSION_ID, creditCode: TEST_CREDIT, question: Q2 });
|
|
|
+ await upsertDmsRecord({ recordId: REC_2, sessionId: SESSION_ID, creditCode: TEST_CREDIT, answer: A2 });
|
|
|
+ rows = await rowsForSession();
|
|
|
+ check('第 2 轮后:2 行(一问一答一条)', rows.length === 2, `实际 ${rows.length} 行`);
|
|
|
+
|
|
|
+ // ── 3. 历史读回 ───────────────────────────────────────────────────
|
|
|
+ console.log('\n【3】历史读回(按创建时间正序)');
|
|
|
const records = await fetchDmsSessionRecords(SESSION_ID);
|
|
|
- check('读出两轮问答', records.length === 2, `实际 ${records.length} 轮`);
|
|
|
+ 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('record_id 回填(反馈闭环)', records[0]?.record_id === REC_1 && records[1]?.record_id === REC_2, `${records[0]?.record_id} / ${records[1]?.record_id}`);
|
|
|
+ check('created_at 被解析成合理时间戳', Math.abs((records[0]?.created_at || 0) - Date.now()) < 10 * 60 * 1000, String(records[0]?.created_at));
|
|
|
+
|
|
|
+ // ── 4. 反馈:写在该轮那一行 ────────────────────────────────────────
|
|
|
+ console.log('\n【4】反馈(写在第 2 轮那一行)');
|
|
|
+ await writeDmsFeedback({ recordId: REC_2, sessionId: SESSION_ID, status: 2, option: '没有回答我的问题', remark: '验证备注' });
|
|
|
+ rows = await rowsForSession();
|
|
|
+ const target = rows.find((r) => r.c_record_id === REC_2);
|
|
|
+ const untouched = rows.find((r) => r.c_record_id === REC_1);
|
|
|
+ check('行数不变(仍 2 行)', rows.length === 2, `实际 ${rows.length} 行`);
|
|
|
+ check('c_feedback_status = 2', Number(target?.c_feedback_status) === 2, String(target?.c_feedback_status));
|
|
|
+ check('c_feedback_option 正确', target?.c_feedback_option === '没有回答我的问题', JSON.stringify(target?.c_feedback_option));
|
|
|
+ check('c_feedback_remark 正确', target?.c_feedback_remark === '验证备注', JSON.stringify(target?.c_feedback_remark));
|
|
|
+ check('c_feedback_at 已写入', !!target?.c_feedback_at, String(target?.c_feedback_at));
|
|
|
+ check('**另一轮未被误改**', untouched && untouched.c_feedback_status == null, JSON.stringify(untouched?.c_feedback_status));
|
|
|
+ check('问答内容未被反馈覆盖', target?.c_question === Q2 && target?.c_answer === A2);
|
|
|
+
|
|
|
+ await writeDmsFeedback({ recordId: REC_2, sessionId: SESSION_ID, status: 0 });
|
|
|
+ rows = await rowsForSession();
|
|
|
+ check('取消反馈 status=0', Number(rows.find((r) => r.c_record_id === REC_2)?.c_feedback_status) === 0);
|
|
|
+
|
|
|
+ const before = (await rowsForSession()).length;
|
|
|
+ await writeDmsFeedback({ recordId: 'not_exist_rec', sessionId: SESSION_ID, status: 1 });
|
|
|
+ check('定位不到时不新增行', (await rowsForSession()).length === before);
|
|
|
+
|
|
|
+ // ── 5. 删除 ───────────────────────────────────────────────────────
|
|
|
+ console.log('\n【5】删除会话');
|
|
|
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);
|
|
|
+ check('问答记录已连带删除', (await rowsForSession()).length === 0);
|
|
|
|
|
|
- // ── 7. 工具函数 ───────────────────────────────────────────────────
|
|
|
- console.log('\n【7】时间戳与访客归属');
|
|
|
+ // ── 6. 工具函数 ───────────────────────────────────────────────────
|
|
|
+ console.log('\n【6】时间戳与访客归属');
|
|
|
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);
|