Kaynağa Gözat

feat(dms): 只要对话就在 1887「助手会话」建行(用户决定)

用户要求(承接「为什么没写入助手会话栏目」的答复):「只要对话就要在助手会话栏目建行,
只是未登录 && 本轮没带回 company_info 时传 IP、登录状态、以及其他可填的字段,
先列出来这种情况下可传的字段,我判断可行后就修改」→ 列完清单后用户拍板:
**c_credit_code 留空;建行时机保持现状(回答完成时)**。

先查清再动手(用户要求先出清单):
- 拉了 1887 模型(2034)23 个字段:c_id / c_credit_code / summary / qpyszx / qyzt / rzqpyx
  标必填;summary、c_description、c_record_type、c_focus_points、c_core_needs、
  c_contact_clues、c_follow_up_departments、c_settlement_intent_level、jygjbm 是**运营分析
  系统(author=ams_user)回填**的,前端碰了会把值冲掉
- 可行性证据:c_credit_code 虽 must=true,但库里 200 行样本里有 86 行是空的
  (probe-batch-* 那些就是 addContent 建的)→ 未登录无企业信息建行在 DMS 侧可行
- 读侧安全:两个壳都只在登录后才拉远端会话(isLoggedIn 才 fetchAndMergeRemoteSessions),
  且 fetchDmsSessions 本就排除 c_is_login="0" 的访客行 → 新建的访客行不会串进企业列表

实现(只有一处逻辑改动):删掉 chat-sessions-dms.ts 里「访客且无企业信息 → 不建行」的
早返回;其余创建逻辑原样复用 —— 那种情况下行里是 c_ip(真 IP,取不到不写)+ c_is_login="0"
+ 时间戳 + 系统 title + 四个必填运营位(summary="" 与三个 false,见 buildRequiredSessionFields);
c_credit_code 留空、企业名称/图片留空、不碰运营分析回填列。
函数头注释与 docs/reference/current-api-call-sites.md 里那条「2026-09-18 起 1887 不再写入」
的旧说法一并改正。

验证:verify-dms-chat-storage.mjs 78 → 89/89(打真实 DMS,新增【9】11 项:确实建行、
c_is_login="0"、c_ip 是真 IP、c_credit_code 留空、企业名称/图片留空、系统 title 正确、
四个必填位是空值兜底、不碰运营回填列、重复 upsert 仍一行、后续轮次带回企业信息时该行
能被正常填上)。变异测试:注入「新建行一律不建」→ 断言大面积失败;还原后 89/89。
另跑 verify-dms-payload-fields 19/19(第 4 节正是「1887 只在 totalResponse 写一次」的
源码级断言,与「时机保持现状」一致)、附件链路 57/57、toast 通道 27/27、build 通过、
vue-tsc 改动文件无类型错误、validate-harness 通过。

⚠️ 中断/报错的轮次(停止生成、409、断流)仍不会在 1887 建行 —— 这是用户选定的
「建行时机保持现状」,不是漏做。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 1 ay önce
ebeveyn
işleme
abfc6afde6

+ 12 - 0
README.md

@@ -401,6 +401,18 @@
     **静默丢弃整个 patch**(返回仍是 200「成功」),所以更新路径只在「真有附件值」时才捎上这两列,
     最坏也只影响带附件的轮次,不会连累补 answer
   - 验证:`verify-dms-chat-storage.mjs` 65 → **78/78**(打真实 DMS 的写入-读回-补 answer 全链路)
+- **只要对话就在「助手会话」(1887)建行**(用户 2026-09-23,推翻 2026-09-18 那条
+  「访客且没有企业信息就不建行」):
+  - 未登录且本轮没带回企业资料时,行里放**能填的那些**:`c_ip`(真实 IP)、`c_is_login="0"`、
+    时间戳、系统 `title`,以及四个必填运营位(`summary` 空串 + 三个布尔 `false`,只做空值兜底、
+    不猜业务值);**`c_credit_code` 留空**(用户拍板:这一列只放真实信用代码,不写访客标签)
+  - **建行时机不变**:仍只在回答**正常完成**(`totalResponse`)时写一次 —— 中断/报错的轮次不建行
+  - 运营分析回填列(`c_title`、`c_description`、`c_focus_points`、`c_core_needs`、
+    `c_contact_clues`、`c_follow_up_departments`、`c_settlement_intent_level`、`jygjbm` 等)
+    前端**一律不碰**,否则会把分析系统的值冲掉
+  - 读侧不受影响:远端会话列表本来就**排除 `c_is_login="0"` 的访客行**,且只在登录后拉取
+  - 验证:`verify-dms-chat-storage.mjs` 78 → **89/89**(打真实 DMS 新增 11 项,含"后续轮次带回
+    企业信息时这个纯访客行能正常被填上");变异测试注入旧行为 → 断言大面积失败
 - **🔴 待处理(登记进技术债 #9)**:`.env.production` / `.env.qingpu` / `.env.test`
   **都没有配 `VITE_DMS_API`** —— 这些环境打出来的包里附件**传不上去**(直接抛「未配置 DMS 接口地址」)、
   DMS 会话落库也**静默跳过**。接生产要先定:DMS 从部署环境是否可达、**token 由谁注入**

+ 12 - 4
harness/docs/reference/current-api-call-sites.md

@@ -17,10 +17,18 @@
 | 政策卡片与协议内容 | 随 `/api/chat` 下发 | |
 | 惠企政策库数据 | 静态 json `policies_public.v1.json` | |
 
-> ⚠️ **2026-09-18 起:1887 助手会话不再写入**(用户指示「只传具体的会话记录,另一个接口不传了」)。
-> 新建会话、改名都不再同步;**前端会话列表退化为纯本地**
-> (`mergeRemoteSessions` 拿到空数组无害,行为与接 DMS 之前一致)。
-> 停用的调用点按约定注释保留在 `useBusinessAssistantChat.ts`,并有源码级断言防改回。
+> ⚠️ **1887 助手会话的写入范围(2026-09-23 时的准)**
+>
+> - **仍然写**,但只有一处:回答**正常完成**(`totalResponse`)时写一次,见 `useBusinessAssistantChat.ts`
+>   的 `void upsertDmsSession(...)`;有源码级断言钉着「活跃调用点只有 1 处、且在 totalResponse 内」
+>   (`verify-dms-payload-fields.mjs` 第 4 节)。**中断/报错的轮次不写**。
+> - **新建会话、改名不再同步**(2026-09-18 的决定只废了这两条),所以前端会话列表仍是纯本地为主,
+>   登录后另按 `c_credit_code` 拉远端会话合并(读侧会排除 `c_is_login="0"` 的访客行)。
+> - **建行策略**:2026-09-18 曾是「访客且本轮没有企业信息就不建行」,**2026-09-23 由用户推翻**——
+>   只要对话就建行;那种情况下行里放 `c_ip` / `c_is_login="0"` / 时间戳 / 标题与四个必填运营位,
+>   **`c_credit_code` 留空**(不写访客标签)。
+> - 停用的调用点(`updateSessionInfo` / `syncSessionTitleToServer`)按约定注释保留在
+>   `useBusinessAssistantChat.ts`。
 
 ## 二、还在用老接口的功能
 

Dosya farkı çok büyük olduğundan ihmal edildi
+ 3 - 2
harness/feature_list.json


+ 40 - 0
harness/progress.md

@@ -3943,4 +3943,44 @@ payload 里含模型里**不存在**的列时,`addContent` 会忽略该列照
 
 **⚠️ 仍需用户在浏览器确认**:带图提问一次,然后在 DMS 里看那条记录的 `c_image` 是否有值。
 
+### 续 30:只要对话就在 1887「助手会话」建行(用户要求)
+
+**用户要求**(承接上一条「为什么没写入助手会话栏目」的答复):
+「只要对话就要在助手会话栏目建行,只是未登录 && 本轮没带回 company_info 时传 IP、登录状态、
+以及其他可填的字段,**先列出可传字段**,我判断可行后就修改」→ 列完清单后用户拍板:
+**`c_credit_code` 留空;建行时机保持现状(回答完成时)**。
+
+**先查清楚再动手**(用户要求先出清单):
+- 拉了 1887 模型(2034)的 23 个字段 + 别名 + `must`:`c_id` / `c_credit_code` / `summary` /
+  `qpyszx` / `qyzt` / `rzqpyx` 标着必填;`summary`、`c_description`、`c_record_type`、`c_focus_points`、
+  `c_core_needs`、`c_contact_clues`、`c_follow_up_departments`、`c_settlement_intent_level`、`jygjbm`
+  是**运营分析系统(author=ams_user)回填**的,前端碰了会把值冲掉
+- **可行性证据**:`c_credit_code` 虽 must=true,但库里 200 行样本里 **86 行是空的**
+  (`probe-batch-*` 那些行就是 `addContent` 建的)→ **未登录无企业信息建行在 DMS 侧可行**
+- **读侧安全**:两个壳都只在**登录后**才拉远端会话(`isLoggedIn` 才 `fetchAndMergeRemoteSessions`),
+  查的是真实信用代码;且 `fetchDmsSessions` 本来就**排除 `c_is_login="0"` 的访客行**
+  → 新建的访客行不会串进任何企业的会话列表
+- 顺带提醒了用户一个未来风险:**用空串查 `c_credit_code` 会命中所有"空代码"行**
+  (实测返回 20 行)—— 今天不会发生(读侧只在登录后跑),但哪天放开访客读远端会话就会串号
+
+**实现**(改动只有一处逻辑):删掉 `chat-sessions-dms.ts` 里那道「访客且无企业信息 → 不建行」的
+早返回(原 279-281 行);其余创建逻辑原样复用 —— 那种情况下行里就是
+`c_ip`(真实 IP,取不到不写)+ `c_is_login="0"` + `c_created_at`/`c_updated_at` + 系统 `title`
++ 四个必填运营位(`summary=""`、三个布尔 `false`,见 `buildRequiredSessionFields`);
+**`c_credit_code` 留空**、企业名称/图片留空。函数头注释与
+`docs/reference/current-api-call-sites.md` 里那条「2026-09-18 起 1887 不再写入」的旧说法一并改正。
+
+**运行过的验证**:`verify-dms-chat-storage.mjs` 78 → **89/89**(打真实 DMS,新增【9】11 项):
+未登录无企业信息**确实建了行**、`c_is_login="0"`、`c_ip` 是真 IP、**`c_credit_code` 留空**、
+企业名称/图片留空、系统 `title` 正确、四个必填位是空值兜底、**不碰运营分析回填列**、
+重复 upsert 仍只有一行、**后续轮次带回企业信息时这个"纯访客行"能正常被填上**。
+变异测试:注入"新建行一律不建"(模拟旧行为)→ 大量断言失败;还原后 89/89。
+另跑:`verify-dms-payload-fields.mjs` **19/19**(第 4 节正是「1887 只在 totalResponse 写一次」的
+源码级断言 —— 与用户「时机保持现状」的决定一致)、`verify-attachment-question.mjs` 57/57、
+`verify-toast-channel.mjs` 27/27、`npm run build` 通过、`vue-tsc` 改动文件无类型错误、
+`validate-harness` 通过。
+
+**⚠️ 仍未做(等用户决定)**:中断/报错的轮次(停止生成、409、断流)**仍不会**在 1887 建行
+—— 这是用户选定的「建行时机保持现状」,不是漏做。
+
 

+ 57 - 0
harness/tools/verify-dms-chat-storage.mjs

@@ -448,5 +448,62 @@ console.log('\n【8】附件路径 → c_image / c_file');
   console.log('  (本节测试数据已清理)');
 }
 
+// ── 9. 只要对话就建行(用户 2026-09-23 决定,推翻 2026-09-18「访客不建行」)──
+console.log('\n【9】未登录 + 无企业信息:也要在 1887 建行');
+{
+  const SID11 = `verify_onlychat_${Date.now().toString(36)}`;
+  const sessionRowOf = async (sid) =>
+    (
+      await searchDmsContents(DMS_COLUMN_SESSION, {
+        search: [{ field: 'c_session_id', searchType: 1, content: { value: sid } }],
+        page: 0,
+        pageSize: 5,
+      })
+    )[0];
+
+  enterpriseInfo.value = null; // 未登录
+  await upsertDmsSession({ sessionId: SID11, sessionTitle: '随手一问' });
+  const row = await sessionRowOf(SID11);
+
+  check('**建了行**(此前这里直接 return,不建)', !!row, JSON.stringify(row && Object.keys(row).slice(0, 5)));
+  if (row) {
+    check('c_is_login = "0"', row.c_is_login === '0', JSON.stringify(row.c_is_login));
+    check('c_ip 存的是真实 IP(脚本里打桩成 203.0.113.9)', row.c_ip === STUB_IP, JSON.stringify(row.c_ip));
+    check('**c_credit_code 留空**(用户 2026-09-23 明确:不写访客标签)',
+      !String(row.c_credit_code ?? '').trim(), JSON.stringify(row.c_credit_code));
+    check('企业名称 / 企业图片留空', !String(row.c_ent_name ?? '').trim() && !String(row.c_ent_image_url ?? '').trim(),
+      JSON.stringify([row.c_ent_name, row.c_ent_image_url]));
+    check('系统 title = 会话标题', row.title === '随手一问', JSON.stringify(row.title));
+    check('必填运营位:summary 空串 + 三个布尔 false(空值兜底,不猜业务值)',
+      !String(row.summary ?? '').trim() && row.qpyszx === false && row.qyzt === false && row.rzqpyx === false,
+      JSON.stringify([row.summary, row.qpyszx, row.qyzt, row.rzqpyx]));
+    check('不碰运营分析回填列(c_title / c_description 仍空)',
+      !String(row.c_title ?? '').trim() && !String(row.c_description ?? '').trim(),
+      JSON.stringify([row.c_title, row.c_description]));
+  }
+
+  // 幂等:再写一次仍是同一行(不能每轮多一行)
+  await upsertDmsSession({ sessionId: SID11, sessionTitle: '随手一问' });
+  const rows = await searchDmsContents(DMS_COLUMN_SESSION, {
+    search: [{ field: 'c_session_id', searchType: 1, content: { value: SID11 } }],
+    page: 0,
+    pageSize: 5,
+  });
+  check('重复 upsert 仍只有一行(幂等键 c_session_id)', rows.length === 1, `实际 ${rows.length} 行`);
+
+  // 后续轮次带回企业信息时,这个"纯访客行"要能跟着被填上(别因为先建了行就写不进去)
+  await upsertDmsSession({
+    sessionId: SID11,
+    sessionTitle: '随手一问',
+    companyInfo: { company: { Name: '戊测试公司', CreditCode: '91310000EEEEEEEEEE' } },
+  });
+  const filled = await sessionRowOf(SID11);
+  check('后续轮次带回企业信息 → 企业名称被填上', filled?.c_ent_name === '戊测试公司', JSON.stringify(filled?.c_ent_name));
+  check('后续轮次带回企业信息 → 信用代码被填上', filled?.c_credit_code === '91310000EEEEEEEEEE', JSON.stringify(filled?.c_credit_code));
+
+  await deleteDmsSession(SID11);
+  console.log('  (本节测试数据已清理)');
+}
+
 console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
 process.exit(fail ? 1 : 0);

+ 16 - 9
src/network/api/dms/chat-sessions-dms.ts

@@ -190,9 +190,14 @@ const isSameRowValue = (a: unknown, b: unknown): boolean =>
 /**
  * 会话 upsert(栏目 1887,幂等键 `c_session_id`)。
  *
+ * 建行策略(用户 2026-09-23 定):**只要对话就在 1887 建行** —— 未登录、本轮也没带回
+ * 企业资料时同样建,行里就放 IP / 登录状态 / 时间戳 / 标题与四个必填运营位。
+ * (2026-09-18 那版是「访客且无企业信息就不建行」,2026-09-23 由用户推翻。)
+ *
  * 写哪些、谁能覆盖谁(用户 2026-09-22 定):
- * - **`c_credit_code` 只放真实信用代码**:登录 → 企业代码;未登录 → 对话带出的公司代码。
- *   访客身份(`访客_<IP>`)**不写这里**,它落在 1889 的 `c_credit_code` 上。
+ * - **`c_credit_code` 只放真实信用代码**:登录 → 企业代码;未登录 → 对话带出的公司代码;
+ *   两者都没有就**留空**(用户 2026-09-23 确认:「留空」而不是写访客标签)。
+ *   访客身份(`访客_<IP>`)落在 1889 的 `c_credit_code` 上,不进这里。
  * - **`c_ip` 放本机 IP**(用户 2026-09-22:IP 存在 c_ip)——只填空,拿不到 IP 就不写。
  * - **登录态的值优先级最高**:`c_ent_name` / `c_credit_code` / `c_is_login="1"` 无条件写(覆盖)。
  * - **对话带出的 `company_info`**(`c_ent_name` / `c_credit_code` / `c_ent_image_url`):
@@ -272,13 +277,15 @@ export const upsertDmsSession = (params: {
       chatOwnsCompany ||
       (!!logged && !!fromChat.creditCode && fromChat.creditCode === logged.credit_code);
     if (!existing) {
-      // 没有任何可写内容(访客且本轮没带回企业资料,也没有显式归属)→ **不建行**。
-      // 尊重 2026-09-18 的决定「只传具体的会话记录(1889),另一个栏目不传了」:
-      // 只有确实有企业信息可记时才落到 1887。
-      const hasChatContent = Object.keys(fromChatFields).length > 0;
-      if (!logged && !hasChatContent && !params.creditCode) {
-        return true;
-      }
+      // **只要对话就建行**(用户 2026-09-23 决定,推翻 2026-09-18 那条
+      // 「只传具体的会话记录(1889),另一个栏目不传了」)。
+      //
+      // 未登录且本轮没带回企业资料时,这一行仍然建,写进去的就是「可填的那些」:
+      // `c_ip`(真实 IP,拿不到则不写)、`c_is_login="0"`、时间戳、系统 `title`,
+      // 以及四个必填运营位(`summary` 空串 + 三个布尔 false,见 buildRequiredSessionFields)。
+      // **`c_credit_code` 留空**(用户 2026-09-23 明确)—— 这一列只放真实信用代码,
+      // 访客身份靠 `c_ip` 认,不往这列混非代码值。
+      // 企业名称/图片同理留空(没有就不带这两列)。
       const content: DmsRow = {
         ...base,
         ...fillIfEmpty,

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor