# 接口契约与参考资料 这个目录放**外部接口的契约文档**,供人和 agent 共同查阅。 原则:参考材料显式收进仓库,避免依赖聊天上下文或人的记忆。 ## 先看哪份 | 文档 | 状态 | 什么时候读 | |---|---|---| | `api-chat.md` | ✅ **现行契约** | 改任何聊天相关代码之前 | | `api-chat-fields-zh.md` | ✅ **现行契约** | 需要字段中英对照、状态取值释义时 | | `company-classification.md` | ⚠️ **现行契约(后端有实测问题)** | 做企业信息分类同步(company_info → 1888/1886)时 | | `company-info-dms-mapping.md` | 📄 **字段对应关系**(脚本生成) | 要查「company_info 的某个字段落到 DMS 哪一列 / 哪些没落」时 | | `legacy/API.md` | 📦 **交接快照**(非规格、不更新) | 只在需要翻"当初调了什么接口"时读 | | `DMS_API.md` | 📄 **外部服务契约**(含实测结论) | 要调 DMS(数据管理服务)时 | | `DMS_COLUMNS.md` | 📄 **栏目现状** | 查 DMS 五个栏目的 columnId / 字段时 | | `DMS_MAPPING.md` | 📄 **表→栏目映射** | 做数据迁移、核对字段映射时 | | `current-api-call-sites.md` | 📋 **现状清单** | 想知道「还有哪些功能在用老接口 / 哪些是死代码」时 | > **DMS 三份文档的来路**:它们出自**另一个项目** > `F:\yysk\AI_zhaoshang\DMS_Data_Migration\`(那边有 `harness/artifacts/dms_openapi.json`、 > `harness/tools/create_dms_columns.py`、`harness/docs/RUNBOOK.md`)。 > 复制进来时没带上这些同伴,**本文档里指向它们的链接已改为绝对路径**。 > 那边的项目才是 DMS 工作的主场;这里保留它们,是为了让本仓库的问题排查能查到 DMS 契约。 > > **栏目现状**(2026-09-18 更新,原「五个栏目全空」的说法已过期): > - **1887 助手会话 / 1889 助手问答记录**:前端已接入(会话、整段对话、反馈都写这里), > 属于**在用的在线数据源**。见 [`../exec-plans/active/dms-chat-storage.md`](../exec-plans/active/dms-chat-storage.md) > (注:1887 的写入已于 2026-09-18 按用户指示**停用**,只保留读取与删除清理) > - **1886 企业荣誉信息 / 1888 企业基础信息**:**两栏目都已有数据**(2026-09-17 18:12 由 > DMS 侧 `ams_user` 灌入,不再是空的——此前文档里「仍是空的」已过期)。 > 1888 的行是完整工商信息(企业名/状态/地址/经营范围等),**没有** `c_tag_*`; > 1886 的行是真实荣誉记录(`c_level` 国家级/区级、系统 `title` 存荣誉名、 > `c_source` 存来源通知名),**没有** `c_honor` / `c_tag_name`(那两列是后来加的)。 > - **前端的企业信息分类同步**会写这两栏目(见 > [`company-classification.md`](company-classification.md) 与 > [`../exec-plans/active/company-classify-dms-sync.md`](../exec-plans/active/company-classify-dms-sync.md)): > 1888 按 `c_credit_code` **更新已存在的行**(只动身份三字段 + 6 个 `c_tag_*`,其余工商字段不碰), > 库里没有的企业才新增; > 1886 以 `company_info.honors.records` 为准,与该企业的行按「荣誉名+级别+来源」**全量对齐** > (补新增、更新差异、删多余;`honors.complete=false` 时只增改不删)—— > **灌入的荣誉行同样参与对齐**(它们就是同一份数据的旧快照),对齐后会被补上 `c_honor`(荣誉名称) > - **1885 页面浏览**:仍是空的(统计埋点已于 2026-09-18 删除,前端不再写) ## 现行契约:`POST /api/chat` ```bash curl --request POST \ --url http://192.168.2.23:8000/api/chat \ --header 'Content-Type: application/json' \ --data '{ "thread_id": "1111", "question": "高新技术企业的奖励是多少钱" }' ``` **最容易踩的坑:请求体只接受 `thread_id` 与 `question` 两个字段,多一个就返回 422。** 旧的 `transmission.files` / `file_pos` 等字段在新接口上发不出去(这正是"文件上传"功能阻塞的原因)。 - 返回的是 SSE 命名事件流:`accepted` / `progress` / `heartbeat` / `answer` / `source` / `item` / `summary` / `interrupt` / `result` / `done` / `error` - 每个事件的 `data` 统一封装为 `{thread_id, request_id, data}` - `answer.text` 与 `result.response` 相同,**只能展示一次**(result 是快照,不要重复追加) - 未收到 `done`/`error` 就断开 → 按连接中断处理,**不自动重试** 前端侧的适配层在 `src/components/api-chat-coordinator.ts`:它把新协议翻译成既有渲染组件 能识别的内容标记(`` / `` / `` / ``), 因此渲染层(BusinessRecord / PolicyMatch / QuestionCard / ScopeContent)不需要感知新协议。 接口地址由 `VITE_CHAT_API` 配置,两种写法都支持(服务前缀会自动追加 `/api/chat`, 已是完整地址则原样使用),见 `src/utils/runtime-config.ts` 的 `getChatApiBaseUrl()`。 开发环境走同源代理 `/chat-api` → `http://192.168.2.23:8000`(见 `vite.config.ts`)。 ### ⚠️ 已知的有意偏离:公司候选提交的是公司名,不是序号 `api-chat.md` 的 interrupt 一节写的是: > | 用户操作 | 下一次 question | > | --- | --- | > | 选择显示的第1家公司 | `"1"`,从1开始;不要重排候选后仍按原序号提交 | > | 换关键词 | 新名称或代码 | > > 不能直接把 actions 或 KeyNo 对象放入 question;当前 question 必须是字符串, > **API 由序号映射到原候选**。 **当前前端不这么做。** 公司候选提交的是**选项全部内容拼接成的文本** (公司名 + 信用代码 + 状态 + 负责人,见 `src/components/api-chat-coordinator.ts` 的 `toChatQuestionAnswer`)。这是产品明确要求。 > 🚨 **已实际发生的事故:死循环。** 提交公司名后再次调用接口,落进了协议里 > "换关键词"那条路径 → 后端重新检索 → 返回同一批候选 → 用户再选 → 再循环。 > 这与上表的语义冲突是**预期内**的:协议把"文本"这种输入形式保留给了 refine, > 文本在语义上无法表达"我选这一个"。 **若死循环再次出现**,只有两条路: 1. **改回提交序号**(协议的做法)——序号取自**当前显示顺序**(1 起), 不要重排候选后仍按原序号提交 2. **后端扩语义**——让后端能识别这种"全字段文本"等价于选中该候选 **验证方式**:在真实后端走一遍公司候选流程,选中一家后确认返回的是选定公司后的 结果,而不是再次弹出候选列表。 > 📌 **判据以跑着的后端为准。** 本文档(含 `api-chat.md`)描述的是**某一时刻**的约定, > 接口会持续演进。文档与后端行为不一致时,以**后端实际行为**为准, > 并回头更新本文档——不要拿文档去否定实际观测到的行为。 固定动作选项不受影响,仍映射为协议控制串:查看更多 → `/next`、重新查询 → `/retry`、 取消公司查询 → `/cancel`、不需要公司信息 → `不需要`。 自由输入(用户手打名称/代码)原样发送,那本就是协议的"换关键词"路径。 ### interrupt 的全部状态(后端确认) 补问用 `kind` + `status` 两个字段描述,**四种情况都可能出现**: | kind | status | 含义 | 前端形态 | |---|---|---|---| | `company_selection` | `found` | 有候选待选择 | 候选列表 + 查看更多 / 取消公司查询 | | `company_need` | `not_found` | 未找到候选,需要补充信息 | 上方常驻输入框 + 跳过公司查询 | | `company_need` | `failed` | **查询服务失败** | 上方常驻输入框 + 跳过公司查询(文案须说明是失败) | | `company_need` | 无 `status` | **初次询问是否需要公司信息** | 上方常驻输入框 + 不需要公司信息 | 两条容易踩的规则: 1. **`failed` 不能说成"未查询到"**。接口文档明确要求"空候选与查询失败需分别展示"、 "有此错误时,不把空候选说成查无公司"。失败时要给出原因(`result.error` 经 `describeCompanyError` 翻译),或至少说"公司查询服务暂时失败"。 2. **不要用 `input_help` 的文案做判断**。曾经用 `/跳过|skip/` 正则去猜按钮文案, 后端改一个字就会失效 —— `status` 才是为这件事准备的字段。 > 实现见 `src/components/api-chat-coordinator.ts` 的 `buildQuestionCardsContent`。 > `status` 缺失时退回"有没有候选"判断,以兼容旧版本。 ## `legacy/API.md` 是什么 **它是交接时根据项目源码生成的快照,不是规格。** - **来源**:接手这个项目时,由当时的项目源码整理生成,涵盖那一刻前端调用的全部后端接口 - **版本可追溯性**:**没有**。接口和功能后续一直在改,这份文档不会跟着更新, 从那之后新增/修改的接口它一律不知道 - **性质**:它是"当时的代码长这样"的记录,**不是"接口应该长这样"的约定** 保留它的理由只有一条:需要翻"某个前端模块(上传、会话管理、反馈等)**当初**调了什么接口、 传了什么参数"时,它是个方便的索引。 > ⚠️ **两件事都不要做:** > 1. **不要照着它实现新功能。** 新增或修改接口调用一律以 `api-chat.md` 为准。 > 2. **不要拿它当"原来行为是什么"的判据。** 它是二手整理,可能遗漏或失真; > 要精确判断原行为,直接 diff 备份目录里的源码(见下)。 ## 相关位置 - [`../architecture.md`](../architecture.md) —— 架构与关键约束 - [`../exec-plans/tech-debt-tracker.md`](../exec-plans/tech-debt-tracker.md) —— 已知技术债(含接口相关的未决项) - [`../../README.md`](../../README.md) —— `harness/` 的结构导航 - `F:\yysk\AI_zhaoshang\备份\zhaoshang-llm` —— **改动之前的原始代码备份(仓库外)**。 判断"原来怎么写的/怎么做的"以**源码**为准,直接 diff 那里,不要凭记忆、也不要只信 `legacy/API.md`(它是生成物,会失真)。 - `F:\yysk\AI_zhaoshang\接口文档\` —— 上述文档在仓库外的原件,未随仓库维护。 注意其中 `API.md` 同样是交接时的快照,不是持续维护的规格。