Browse Source

fix(dms): 1887 会话写入恢复——四个必填运营字段传空值/默认值

用户拍板「先传空值/默认值」。先直连实测确认空值能过 must 校验
(summary:"" + 三个 false → 200 成功;must 只校验字段在不在,不校验非空)。

改动(chat-sessions-dms.ts):
- 新增 buildRequiredSessionFields() → {summary:"", qpyszx:false, qyzt:false, rzqpyx:false}
- **只在新增时带**;更新一律不带 —— 否则会把后端将来回填的值冲成空
- 注释写清:这四个是 must(不带就 214),但故意传空;**不要顺手填业务判断**
  (按登录态推 qyzt、拿首问当 summary 之类),那会把「没数据」变成「有数据但可能是错的」
- 1889 不带这些字段(实测无此约束,保持干净)

验证(token 有效,打真实 DMS):
- verify-dms-chat-storage.mjs **43/43 通过** —— 1887 写入已恢复;
  新建带 title、改名后 c_title 与系统 title 都跟随、四个字段写入且为空、
  记录 title = 问题文本
- verify-dms-payload-fields.mjs 10/10(打桩抓请求体,不需要 token)
- npm run build 通过

过程中修正两条我自己写错的断言(过期的「摘要应为会话标题」、
payload 脚本头部仍写着旧策略)。feature_list 的 dms-chat-storage 恢复 passing。

⚠️ 四个字段目前是空值:运营侧看到的都是 false/空。业务口径定了再改。
另一:DMS token 有效期至今晚 21:33。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 2 days ago
parent
commit
0b2e54be5c

+ 4 - 3
README.md

@@ -174,9 +174,10 @@
     原本只在创建时初始化,**改 `c_title` 不会带动它**,所以前端改名后 DMS 里看到的标题不变
   - 问答记录(1889):`title` 用**问题文本**(原先库里是字符串 "null",
     在 DMS 列表里看不出是哪一轮)
-  - 写入**只带业务字段 + `title`**:不发 `summary` / `qpyszx` / `qyzt` / `rzqpyx`
-    这类运营分析字段(用户明确要求「传 title,不要传类似于 summary 的多余字段」;
-    要传哪些由业务方指定,前端不替它们猜值)
+  - 写入带 `title`(用户需求);1887 另有四个 `must=true` 的运营字段
+    (`summary` / `qpyszx` / `qyzt` / `rzqpyx`)——**不带就 `214 数据错误`,四个必须全带**,
+    但**只传空值/默认值**(取值属业务口径,前端不猜;用户拍板「先传空值」)。
+    ⚠️ **只在新增时带、更新时一律不带**——否则会把后端将来回填的值冲成空
   - 新增 `harness/tools/verify-dms-payload-fields.mjs`(9 项断言):**打桩 fetch 抓真实请求体**,
     断言字段集合「不多不少」且 `title` 的值正确 —— 这条**不需要 DMS token** 就能跑
 - 环境变量整理:**地址类配置收进 env,不再写死在代码里**

File diff suppressed because it is too large
+ 2 - 3
harness/feature_list.json


+ 37 - 0
harness/progress.md

@@ -1777,3 +1777,40 @@
     ③**维持现状** → 1887 会话写入持续失败
 - **下一步最佳动作**:等用户在①②③中选一个
 
+## Session 042
+
+- **日期**:2026-09-18
+- **本轮目标**:用户拍板「先传空值/默认值」—— 让写入先通,业务口径后续再定
+- **先验证空值能不能过 must 校验**(直连实测):`summary:""` + 三个 `false` → **200 成功** ✓
+  (must 只校验"字段在不在",不校验非空)
+- **改动**(`chat-sessions-dms.ts`):
+  - 新增 `buildRequiredSessionFields()`,返回 `{summary:"", qpyszx:false, qyzt:false, rzqpyx:false}`
+  - **只在新增(addContent)时带**;⚠️ **更新时一律不带** —— 否则会把后端将来回填的值冲成空
+  - 注释写清:这四个是 must(不带就 214),但**故意传空值**;
+    **不要顺手填业务判断**(如按登录态推 qyzt、拿首问当 summary)——
+    那会把「没数据」变成「有数据但可能是错的」,比空值更糟
+  - 1889(问答记录)**不带**这些字段:实测它没这个约束,保持干净
+- **运行过的验证(全部打真实 DMS,token 有效)**:
+  - `verify-dms-chat-storage.mjs` **43/43 通过** —— **1887 写入已恢复**
+    关键项:新建会话带 title ✓、改名后 c_title 与系统 title **都跟着变** ✓、
+    四个必填字段确实写进去了**且为空/默认** ✓、记录 title = 问题文本 ✓
+  - `verify-dms-payload-fields.mjs` **10/10 通过**(打桩抓请求体,不需要 token):
+    会话新增字段集合「不多不少」、更新**不带**那四个字段、记录不带
+  - `npm run build` 通过
+- **过程中修正两条我自己写错的断言**:
+  1. `verify-dms-chat-storage.mjs` 里「摘要应为会话标题」——已过期,改成
+     「写入且为空(不猜业务值)」+「三个布尔为默认 false」
+  2. `verify-dms-payload-fields.mjs` 头部说明还写着旧的「不要传 summary」,已更正
+- **已记录证据**:本文件 Session 042;两个脚本的通过输出(43/43、10/10)
+- **更新过的文件或工件**:`src/network/api/dms/chat-sessions-dms.ts`、
+  `harness/tools/{verify-dms-chat-storage.mjs,verify-dms-payload-fields.mjs}`、
+  根 `README.md`、`harness/feature_list.json`、本文件
+- **已知风险或未解决问题**:
+  - ⚠️ **这四个字段目前是空值**:运营侧看到的 `qpyszx/qyzt/rzqpyx` 都是 false、`summary` 为空。
+    若业务口径定了(或该由后端回填),需要再改这里
+  - ⚠️ **更新不带它们**是刻意的:保护后端/他人回填的值不被冲掉。若将来发现更新后这些值被清空,
+    先看这条
+  - ⚠️ DMS token 有效期至今晚 21:33;过期后打真实 DMS 的脚本不可复跑
+    (但 payload 脚本不需要 token,随时可跑)
+- **下一步最佳动作**:等业务方给这几个字段的取值口径(或确认由后端回填)
+

+ 6 - 3
harness/tools/verify-dms-chat-storage.mjs

@@ -192,9 +192,12 @@ console.log('\n【7】title 字段');
   await upsertDmsSession({ sessionId: SID2, title: '标题甲', creditCode: TEST_CREDIT });
   let row = await rowOf(DMS_COLUMN_SESSION, 'c_session_id', SID2);
   check('新建会话带上了系统 title', row?.title === '标题甲', JSON.stringify(row?.title));
-  check('摘要字段已写入', row?.summary === '标题甲', JSON.stringify(row?.summary));
-  // 注:qpyszx / qyzt / rzqpyx 三个字段按要求**前端不传值**(2026-09-18 用户指示),
-  // 它们在 DMS 模型里是 must=true —— 写入若出现 214,先看这里,别急着改业务逻辑
+  // 四个必填运营字段(summary/qpyszx/qyzt/rzqpyx)**故意传空值/默认值**:
+  // 它们是 must=true(不带就 214),但取值属业务口径、前端不猜。
+  // 所以这里断言的是「写进去了、且确实是空的」——而不是断言某个业务值。
+  check('摘要字段已写入**且为空**(不猜业务值)', row?.summary === '', JSON.stringify(row?.summary));
+  check('三个布尔运营字段为默认 false', row?.qpyszx === false && row?.qyzt === false && row?.rzqpyx === false,
+    JSON.stringify([row?.qpyszx, row?.qyzt, row?.rzqpyx]));
 
   await upsertDmsSession({ sessionId: SID2, title: '标题乙', creditCode: TEST_CREDIT });
   row = await rowOf(DMS_COLUMN_SESSION, 'c_session_id', SID2);

+ 15 - 6
harness/tools/verify-dms-payload-fields.mjs

@@ -1,9 +1,10 @@
 /**
  * 验证写 DMS 时**到底发了哪些字段**(不需要 token,也不碰真实 DMS)。
  *
- * 为什么要单独测这个:用户明确要求 —— **「我的需求是传 title,不要传类似于 summary
- * 的多余的字段」**。字段多发/少发是很容易在后续改动里被"顺手"改掉的,
- * 而且它不像业务逻辑那样有直观的行为反馈,所以直接把请求体抓下来断言。
+ * 为什么要单独测这个:写入字段的规矩是**踩出来的**——
+ * 1887 的模型把 summary/qpyszx/qyzt/rzqpyx 设为 must=true(不带就 214),
+ * 但它们**只能传空值**(业务口径由业务方定,前端不猜);而 1889 不需要这些字段。
+ * 多发/少发字段没有直观的行为反馈、极易被"顺手"改掉,所以直接把请求体抓下来断言。
  *
  * 做法:把 `globalThis.fetch` 打桩,捕获模块真实发出的 form body,
  * 解析出 `content` 的键集合,断言「不多不少」。
@@ -64,10 +65,18 @@ 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();
+const EXPECT_SESSION_ADD = ['c_created_at', 'c_credit_code', 'c_session_id', 'c_source', 'c_title', 'c_updated_at', 'title',
+  // 1887 的模型把这四个设为 must=true,四个全带才写得进去(少一个就 214);
+  // 值故意留空 —— 运营口径由业务方定,前端不猜
+  'summary', 'qpyszx', 'qyzt', 'rzqpyx'].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));
+check('带上四个必填运营字段(值必须为空/默认,不是业务值)', ['summary','qpyszx','qyzt','rzqpyx'].every((k) => keys?.includes(k)), JSON.stringify(keys));
+check('这四个字段的值是空值/默认(不是猜的业务值)', (() => {
+  const hit = [...captured].reverse().find((c) => c.url.includes('/content/addContent'));
+  const c = JSON.parse(new URLSearchParams(hit.body).get('content'));
+  return c.summary === '' && c.qpyszx === false && c.qyzt === false && c.rzqpyx === false;
+})(), '');
 
 console.log('\n【2】会话改名(更新):也要带 title');
 captured.length = 0;
@@ -84,7 +93,7 @@ await upsertDmsSession({ sessionId: 'sess-A', title: '会话标题乙', creditCo
 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));
+check('**更新不带这四个字段**(否则会把后端回填的值冲成空)', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
 
 console.log('\n【3】问答记录新增:title = 问题文本,且不多发字段');
 captured.length = 0;

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

@@ -44,16 +44,29 @@ export const resolveDmsCreditCode = (): string =>
   enterpriseInfo.value?.credit_code || `访客_${getOrCreateAssistantVisitorId()}`;
 
 /**
- * ⚠️ 写入**只带业务字段 + `title`**,不发 `summary` / `qpyszx` / `qyzt` / `rzqpyx`
- * 之类的运营分析字段 —— 用户的明确要求(2026-09-18):
- * **「我的需求是传 title,不要传类似于 summary 的多余的字段」**。
+ * 1887 助手会话的**必填运营字段** —— 只能传空值/默认值。
  *
- * 需要什么字段由用户指定,**不要在代码里替它们猜值或顺手填**(此前一度传过
- * summary/qpyszx/qyzt/rzqpyx,已全部去掉)。
+ * 背景(2026-09-18 实测二分):
+ * DMS 的 1887 模型把 `summary` / `qpyszx` / `qyzt` / `rzqpyx` 设为 **`must=true`**,
+ * **四个全带才写得进去**,少任何一个都会被 `214 数据错误` 拒绝:
  *
- * 另注:DMS 的栏目模型里那几个是 `must=true`,若写入出现 `214 数据错误`,
- * 先确认是不是缺了 must 字段 —— 但那属于「要不要传」的决策,先问用户,别自行补。
+ * | 载荷 | 结果 |
+ * |---|---|
+ * | 全不带 / +summary / +summary+qpyszx / +…+qyzt | 都 214 |
+ * | **四个都带** | **200** |
+ *
+ * ⚠️ 这里**故意传空值**(用户拍板「先传空值/默认值」):它们是运营分析口径,
+ * 取值由业务方决定。**不要顺手填业务判断**(比如按登录态推 `qyzt`、拿首问当 `summary`)——
+ * 那会把「没数据」变成「有数据但可能是错的」,比空值更糟。等口径定了再改这里。
+ *
+ * ⚠️ **只在新增(addContent)时带**:更新时不带 —— 否则会把后端将来回填的值冲成空。
  */
+const buildRequiredSessionFields = (): DmsRow => ({
+  summary: "",
+  qpyszx: false,
+  qyzt: false,
+  rzqpyx: false,
+});
 
 /** 写 DMS 的时间格式(Step-0 实测:`YYYY-MM-DD HH:mm:ss` 被接受) */
 export const nowDmsTimestamp = (): string => {
@@ -144,6 +157,8 @@ export const upsertDmsSession = (params: {
     const existing = await findRowBy(DMS_COLUMN_SESSION, "c_session_id", params.sessionId);
     if (!existing) {
       content.c_created_at = nowDmsTimestamp();
+      // 必填的运营字段:只在新增时带空值(见 buildRequiredSessionFields 的说明)
+      Object.assign(content, buildRequiredSessionFields());
       const id = await addDmsContent(DMS_COLUMN_SESSION, DMS_MODEL_SESSION, content);
       if (!id) console.warn("[dms] 会话写入失败", params.sessionId);
       return !!id;

Some files were not shown because too many files changed in this diff