Răsfoiți Sursa

change(dms): 写入只带业务字段 + title,去掉 summary 等多余字段

用户澄清:「我的需求是传 title,不要传类似于 summary 的多余的字段」
(上一轮我保留了 summary,是理解偏了)。

- chat-sessions-dms.ts 删掉 buildAnalyticsFields 及其两处调用,写入只发:
  · 会话(1887):c_credit_code / c_session_id / c_title / title / c_source /
    c_created_at / c_updated_at
  · 问答记录(1889):c_credit_code / c_session_id / c_record_id / c_question /
    title / c_source / c_created_at
- 注释改成明确规矩:只带业务字段 + title,不发运营分析字段;
  要传哪些由用户指定,**不替它们猜值或顺手填**;再遇 214 先问用户,别自行补

新增 verify-dms-payload-fields.mjs(9 项,**不需要 token**):打桩 globalThis.fetch
抓真实请求体,解析 content 的键集合断言「不多不少」,并断言 title 的值正确
(会话新增/改名带 title、记录 title = 问题文本)。
—— 这样即使 token 过期也能守住「字段不该多发」这条要求。

验证:verify-dms-payload-fields 9/9 通过;npm run build 通过;validate-harness 通过。
并补回上一轮误删的两条验证项(代理链路实测、DMS 残留复查),不削弱验证记录。

⚠️ 仍未验证:打真实 DMS 的 42 项脚本因 token 过期(2026-09-17 22:19)暂不可复跑;
去掉 must 字段后 DMS 是否拒收,要等新 token 才能确认。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 2 zile în urmă
părinte
comite
a715975b1e

+ 6 - 4
README.md

@@ -172,11 +172,13 @@
 - DMS 写入补齐 `title` 与运营分析字段:
   - 会话(1887):每次 upsert 都显式带 `title` = 会话标题 —— DMS 的系统字段 `title`
     原本只在创建时初始化,**改 `c_title` 不会带动它**,所以前端改名后 DMS 里看到的标题不变
-  - 问答记录(1889):`title` 与 `summary` 用**问题文本**(原先库里是字符串 "null",
+  - 问答记录(1889):`title` 用**问题文本**(原先库里是字符串 "null",
     在 DMS 列表里看不出是哪一轮)
-  - 补上 DMS 模型新增的 `must=true` 字段(`summary`)——不带会被 `214 数据错误` 拒绝
-  - `qpyszx`(是否青浦营商咨询)/ `qyzt`(是否识别到企业主体)/ `rzqpyx`(是否落户青浦意向)
-    这三个字段**前端不传值**(业务口径由业务方决定,前端不再替它们猜值)
+  - 写入**只带业务字段 + `title`**:不发 `summary` / `qpyszx` / `qyzt` / `rzqpyx`
+    这类运营分析字段(用户明确要求「传 title,不要传类似于 summary 的多余字段」;
+    要传哪些由业务方指定,前端不替它们猜值)
+  - 新增 `harness/tools/verify-dms-payload-fields.mjs`(9 项断言):**打桩 fetch 抓真实请求体**,
+    断言字段集合「不多不少」且 `title` 的值正确 —— 这条**不需要 DMS token** 就能跑
 - 环境变量整理:**地址类配置收进 env,不再写死在代码里**
   - 新增 `VITE_CHAT_TARGET`(聊天后端真实地址)——此前它硬编码在 `vite.config.ts` 的
     代理配置里;现由 `vite.config.ts` 从 env 读取(保留同值兜底)

Fișier diff suprimat deoarece este prea mare
+ 3 - 2
harness/feature_list.json


+ 32 - 0
harness/progress.md

@@ -1704,3 +1704,35 @@
     must 约束,还是这三个字段仍要传(那就得定取值口径)
 - **下一步最佳动作**:拿到新 token 后重跑 `verify-dms-chat-storage.mjs`,确认写入是否仍通
 
+## Session 040
+
+- **日期**:2026-09-18
+- **本轮目标**:用户澄清需求 —— **「我的需求是传 title,不要传类似于 summary 的多余的字段」**
+  (上一轮我把 summary 留下了,是理解偏了)
+- **已完成**:
+  - `chat-sessions-dms.ts` **删掉 `buildAnalyticsFields`** 及其两处调用;
+    写入只发业务字段 + `title`:
+    - 会话(1887):`c_credit_code` `c_session_id` `c_title` **`title`** `c_source` `c_created_at` `c_updated_at`
+    - 问答记录(1889):`c_credit_code` `c_session_id` `c_record_id` `c_question` **`title`** `c_source` `c_created_at`
+  - 代码注释改成**明确的规矩**:写入只带业务字段 + title,不发运营分析字段;
+    **要传哪些由用户指定,不要在代码里替它们猜值或顺手填**;若再遇 214 先确认是不是缺 must 字段,
+    但那属于"要不要传"的决策 —— **先问用户,别自行补**
+- **运行过的验证**(**不依赖 token**):
+  - 新增 `harness/tools/verify-dms-payload-fields.mjs` —— **打桩 fetch 抓真实请求体**,
+    解析 form 里的 `content` 键集合,断言「不多不少」:**9/9 通过**
+    - 会话新增字段集合与预期**完全一致**(含 `title`,不含 summary/qpyszx/qyzt/rzqpyx)
+    - 会话**更新也带 `title`**(原先不带 → DMS 里标题不跟随改名)
+    - 记录新增字段集合一致,且 **`title` 的值就是问题文本**
+  - 这个脚本绕开了 token 问题:把 `globalThis.fetch` 打桩即可断言发出的内容
+  - `npm run build` 通过;`validate-harness` 通过
+- **已记录证据**:本文件 Session 040;`harness/tools/verify-dms-payload-fields.mjs`
+- **更新过的文件或工件**:`src/network/api/dms/chat-sessions-dms.ts`、
+  `harness/tools/{verify-dms-payload-fields.mjs(新增),verify-dms-chat-storage.mjs}`、
+  根 `README.md`、`harness/feature_list.json`、本文件
+- **已知风险或未解决问题**:
+  - ⚠️ **打真实 DMS 的验证仍做不了**(token 2026-09-17 22:19 过期)。
+    已验的是「发出去的字段对不对」;**没验的是「DMS 收不收」**——
+    那三个字段在模型里是 must=true,去掉后是否被 214 拒绝,要等新 token
+  - ⚠️ 若写入被拒:是等 DMS 侧放宽 must,还是这些字段仍要传——**由用户定,前端不自行补**
+- **下一步最佳动作**:拿到新 token 后跑 `verify-dms-chat-storage.mjs` 确认真实写入是否通过
+

+ 109 - 0
harness/tools/verify-dms-payload-fields.mjs

@@ -0,0 +1,109 @@
+/**
+ * 验证写 DMS 时**到底发了哪些字段**(不需要 token,也不碰真实 DMS)。
+ *
+ * 为什么要单独测这个:用户明确要求 —— **「我的需求是传 title,不要传类似于 summary
+ * 的多余的字段」**。字段多发/少发是很容易在后续改动里被"顺手"改掉的,
+ * 而且它不像业务逻辑那样有直观的行为反馈,所以直接把请求体抓下来断言。
+ *
+ * 做法:把 `globalThis.fetch` 打桩,捕获模块真实发出的 form body,
+ * 解析出 `content` 的键集合,断言「不多不少」。
+ *
+ * 怎么跑(在项目根目录):
+ *   npx esbuild harness/tools/_entry-dms.ts --bundle --format=esm \
+ *     --outfile=harness/tools/_dms.mjs --alias:@=./src --define:import.meta.env='{}'
+ *   node harness/tools/verify-dms-payload-fields.mjs
+ */
+
+globalThis.VITE_DMS_API = 'http://dms.invalid/dms'; // 只为让 base 非空;请求会被打桩拦住
+
+// ── 打桩 fetch:捕获请求,返回可控响应 ────────────────────────────────
+const captured = [];
+const json = (obj) => new Response(JSON.stringify(obj), { status: 200, headers: { 'Content-Type': 'application/json' } });
+
+globalThis.fetch = async (url, init = {}) => {
+  const body = init.body ? String(init.body) : '';
+  captured.push({ url: String(url), body });
+
+  if (String(url).includes('/content/selectContentList')) {
+    return json({ code: 202, message: '数据不存在', content: '数据不存在' }); // 空 → 走新增分支
+  }
+  if (String(url).includes('/content/addContent')) {
+    return json({ code: 200, message: '成功', content: 'stub-uuid-0001' });
+  }
+  if (String(url).includes('/content/updateContent')) {
+    return json({ code: 200, message: '成功', content: '执行成功' });
+  }
+  return json({ code: 200, message: '成功', content: '' });
+};
+
+const { upsertDmsSession, upsertDmsRecord } = await import('./_dms.mjs');
+
+let pass = 0;
+let fail = 0;
+const check = (name, cond, extra = '') => {
+  if (cond) {
+    pass++;
+    console.log('  ok   ' + name);
+  } else {
+    fail++;
+    console.log('  FAIL ' + name + '  ' + extra);
+  }
+};
+
+/** 取出最近一次某端点的请求体里 content 的字段名(排序后) */
+const keysOfLast = (endpoint) => {
+  const hit = [...captured].reverse().find((c) => c.url.includes(endpoint));
+  if (!hit) return null;
+  const params = new URLSearchParams(hit.body);
+  const content = JSON.parse(params.get('content') || '{}');
+  return Object.keys(content).sort();
+};
+
+const DISALLOWED = ['summary', 'qpyszx', 'qyzt', 'rzqpyx'];
+
+console.log('\n【1】会话新增:字段必须「不多不少」');
+await upsertDmsSession({ sessionId: 'sess-A', title: '会话标题甲', creditCode: '91310118TEST' });
+let keys = keysOfLast('/content/addContent');
+const EXPECT_SESSION_ADD = ['c_created_at', 'c_credit_code', 'c_session_id', 'c_source', 'c_title', 'c_updated_at', 'title'].sort();
+check('字段集合与预期完全一致', JSON.stringify(keys) === JSON.stringify(EXPECT_SESSION_ADD), `实际 ${JSON.stringify(keys)}`);
+check('**带 title**(用户需求)', keys?.includes('title') === true);
+check('不含 summary/qpyszx/qyzt/rzqpyx 等多余字段', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
+
+console.log('\n【2】会话改名(更新):也要带 title');
+captured.length = 0;
+// 让这次走「更新」分支:先让 search 返回一行
+globalThis.fetch = async (url, init = {}) => {
+  const body = init.body ? String(init.body) : '';
+  captured.push({ url: String(url), body });
+  if (String(url).includes('/content/selectContentList')) {
+    return json({ code: 200, message: '成功', content: { data: [{ id: 'row-uuid', c_session_id: 'sess-A' }], count: 1 } });
+  }
+  return json({ code: 200, message: '成功', content: '执行成功' });
+};
+await upsertDmsSession({ sessionId: 'sess-A', title: '会话标题乙', creditCode: '91310118TEST' });
+keys = keysOfLast('/content/updateContent');
+check('更新走的是 updateContent', keys !== null);
+check('**更新也带 title**(原先不带,改名后 DMS 里标题不变)', keys?.includes('title') === true, JSON.stringify(keys));
+check('更新不带多余的运营字段', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
+
+console.log('\n【3】问答记录新增:title = 问题文本,且不多发字段');
+captured.length = 0;
+globalThis.fetch = async (url, init = {}) => {
+  const body = init.body ? String(init.body) : '';
+  captured.push({ url: String(url), body });
+  if (String(url).includes('/content/selectContentList')) return json({ code: 202, message: '数据不存在', content: '数据不存在' });
+  if (String(url).includes('/content/addContent')) return json({ code: 200, message: '成功', content: 'stub-uuid-0002' });
+  return json({ code: 200, message: '成功', content: '' });
+};
+await upsertDmsRecord({ recordId: 'rec-A', sessionId: 'sess-A', creditCode: '91310118TEST', question: '这是问题文本' });
+keys = keysOfLast('/content/addContent');
+const EXPECT_RECORD_ADD = ['c_created_at', 'c_credit_code', 'c_question', 'c_record_id', 'c_session_id', 'c_source', 'title'].sort();
+check('字段集合与预期完全一致', JSON.stringify(keys) === JSON.stringify(EXPECT_RECORD_ADD), `实际 ${JSON.stringify(keys)}`);
+check('不含 summary 等多余字段', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
+
+const hit = [...captured].reverse().find((c) => c.url.includes('/content/addContent'));
+const content = JSON.parse(new URLSearchParams(hit.body).get('content'));
+check('**title 的值就是问题文本**', content.title === '这是问题文本', JSON.stringify(content.title));
+
+console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
+process.exit(fail ? 1 : 0);

+ 7 - 14
src/network/api/dms/chat-sessions-dms.ts

@@ -44,19 +44,16 @@ export const resolveDmsCreditCode = (): string =>
   enterpriseInfo.value?.credit_code || `访客_${getOrCreateAssistantVisitorId()}`;
 
 /**
- * 写入时附带的「摘要」字段。
+ * ⚠️ 写入**只带业务字段 + `title`**,不发 `summary` / `qpyszx` / `qyzt` / `rzqpyx`
+ * 之类的运营分析字段 —— 用户的明确要求(2026-09-18):
+ * **「我的需求是传 title,不要传类似于 summary 的多余的字段」**。
  *
- * ⚠️ **只发 summary**。`qpyszx`(是否青浦营商咨询)、`qyzt`(是否识别到企业主体)、
- * `rzqpyx`(是否落户青浦意向)这三个字段**前端不传值** —— 属业务口径,
- * 由用户后续决定要不要传、怎么传(2026-09-18 明确)。
- * 代码里不要再替它们猜值(此前一度传过 true/false,已按要求去掉)。
+ * 需要什么字段由用户指定,**不要在代码里替它们猜值或顺手填**(此前一度传过
+ * summary/qpyszx/qyzt/rzqpyx,已全部去掉)。
  *
- * 另注:DMS 的栏目模型里这几个是 `must=true`,实测不带上会被 `214 数据错误` 拒绝;
- * 若写入再次出现 214,先看这里,别急着改业务逻辑
+ * 另注:DMS 的栏目模型里那几个是 `must=true`,若写入出现 `214 数据错误`,
+ * 先确认是不是缺了 must 字段 —— 但那属于「要不要传」的决策,先问用户,别自行补
  */
-const buildAnalyticsFields = (summary: string): DmsRow => ({
-  summary: summary || "",
-});
 
 /** 写 DMS 的时间格式(Step-0 实测:`YYYY-MM-DD HH:mm:ss` 被接受) */
 export const nowDmsTimestamp = (): string => {
@@ -147,8 +144,6 @@ export const upsertDmsSession = (params: {
     const existing = await findRowBy(DMS_COLUMN_SESSION, "c_session_id", params.sessionId);
     if (!existing) {
       content.c_created_at = nowDmsTimestamp();
-      // 运营分析字段是 must=true,**只有新增时才必须带**(更新时不校验,实测)
-      Object.assign(content, buildAnalyticsFields(params.title || "未命名会话"));
       const id = await addDmsContent(DMS_COLUMN_SESSION, DMS_MODEL_SESSION, content);
       if (!id) console.warn("[dms] 会话写入失败", params.sessionId);
       return !!id;
@@ -200,8 +195,6 @@ export const upsertDmsRecord = (params: {
       }
       if (params.answer !== undefined) content.c_answer = params.answer;
       content.c_created_at = nowDmsTimestamp();
-      // 运营分析字段(must=true):摘要用问题文本
-      Object.assign(content, buildAnalyticsFields(params.question || ""));
       const id = await addDmsContent(DMS_COLUMN_RECORD, DMS_MODEL_RECORD, content);
       if (!id) console.warn("[dms] 问答记录写入失败", params.recordId);
       return !!id;

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff