diff --git a/logs/phase3_log.md b/logs/phase3_log.md new file mode 100644 index 0000000..da6b27f --- /dev/null +++ b/logs/phase3_log.md @@ -0,0 +1,197 @@ +# Phase 3 开发日志 + +> 本文件记录 Phase 3(知识库基础设施)各 Task 的已完成事实。 +> 续接快照(待办/决策)见单独文件,不混入本 log。 + +--- + +## Task 1 — embedding 最小链路验证 ✅(2026-05-26 完成) + +### 目标 +用最小代价验证 Phase 3 的命脉:文本能否转成正确维度的向量、整条「解析→embedding→入库→语义检索」链路能否跑通。不碰 schema、不碰前端、不碰 docx。 + +### 关键结论(地基事实,后续 Task 依赖) +- **embedding 服务确定为 MiniMax embo-01**。前期查证已排除 DeepSeek(官方无 embedding 接口)。 +- **embo-01 输出维度 = 1536**,与 `001_init.sql` 中 `knowledge_embeddings.embedding vector(1536)` 完全匹配,无需改维度、无需改宪法。 +- **embo-01 真实接口结构**(实测确认,非凭印象): + - 请求:`{"model":"embo-01", "texts":[...], "type":"db"|"query"}` —— 注意是 `texts` 数组,不是 OpenAI 的 `input`。 + - 返回:向量在 `vectors` 字段(`data["vectors"][0]`),不是 OpenAI 的 `data[0].embedding`。 + - 存文档用 `type="db"`,查询用 `type="query"`,不可混。 + - 调用需 `Authorization: Bearer {API_KEY}` + `GroupId` 两个凭证(MiniMax 特有 GroupId)。 +- **检索方式**:使用 pgvector 数据库原生余弦距离算子 `<=>`,不在 Python 侧计算。 +- **凭证管理**:API Key + GroupId 存于 `backend/.env`(`MINIMAX_EMBED_API_KEY` / `MINIMAX_GROUP_ID`),代码从 .env 读取,未进任何代码、未进 git(.env 已在 .gitignore)。 +- 注意:embedding 接口地址 `https://api.minimax.chat/v1/embeddings`,与 Cline 写代码所用的 M2.7 chat 接口是两套独立配置,互不影响。 + +### 新增/改动文件(已 commit & push,commit 38873ac) +- `backend/requirements.txt`(+pgvector==0.2.5) +- `backend/app/core/config.py`(+引导凭证字段) +- `backend/app/models/knowledge.py`(新增,embedding 字段用 pgvector.Vector) +- `backend/app/services/embedding_service.py`(新增,embo-01 调用封装) +- `backend/app/services/knowledge_service.py`(新增,写库 + SQL 向量检索) +- `backend/scripts/test_embo01_api.py`(探路脚本) +- `backend/scripts/verify_embedding.py`(全链路验证脚本) +- `backend/sample_md/`(空目录,测试文件已清) + +### 收尾 +- 验证用测试数据已全部清除:knowledge_items = 0 行,knowledge_embeddings = 0 行,sample_md/ 空。 +- 知识库当前为干净空库,等待真实笔记导入。 + +### 顺手核实到的现状(未处理,留待办) +- episodes 表当前仅 7 行。 +- Phase 2 的「批量导入」前端有入口但从未真实导入过收视数据(制片人确认:是流程上没提醒导入,加上收视表里有测试数据不会删)。属 Phase 2 遗留债,记入 backlog,另行处理,不并入 Phase 3。 + +--- + +## Task 2 — 知识库管理后台(最小可用版)✅(2026-05-26 完成) + +### 目标 +做一条端到端跑通的链路:上传 .md 笔记 → 解析 yaml frontmatter → 入库(含语义向量)→ 列表展示 → 删除。不改 schema、不碰认证、不引向量索引以外的新依赖。 + +### 新增/改动文件(4 次独立 commit) + +| 文件 | 动作 | 说明 | +|------|------|------| +| `backend/requirements.txt` | 改 | +python-frontmatter==1.1.0 | +| `backend/app/services/knowledge_service.py` | 扩展 | +parse_md_file() + delete_item() + list_items() + get_distinct_sources();复用 EmbeddingService | +| `backend/app/api/knowledge.py` | 新增 | 4 个接口:POST /upload、GET /items、DELETE /items/{id}、GET /sources | +| `backend/app/main.py` | 改 | +app.include_router(knowledge_router) | +| `frontend/src/services/knowledgeService.js` | 新增 | 封装 4 个 API 调用 | +| `frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx` | 重写 | 上传区(Dragger) + 筛选栏(Select) + 列表(Table) + 删除(Popconfirm) | + +### yaml frontmatter 解析映射(按真实样本,写死) + +```python +# 类型 → source_type(硬映射,不猜测) +杂志文章/军报 → military_report +节目文稿 → manuscript +报题单 → baoti + +# 标题 +名称 或 标题 → title + +# 作者 +作者 或 编导 → author + +# 出处详情(存 JSONB 的 source_detail) +期刊 + 期号 → 拼接,如"航空知识 2026年第1期" + +# 播出日期 +容错"待补充"等非日期文本 → null + +# 权重 +原样存 JSONB(不展示不排序,Phase 4 排序用) + +# 双链 [[...]] +_extract_double_brackets() 原样存入 JSONB 预留给 Phase 4 +``` + +### 关键设计决策 + +1. **source_detail 存 JSONB 不 ALTER**:Task Brief 说"不要新增表字段",`tags` JSONB 列已用于存权重,故 source_detail 也放进来,不 ALTER TABLE。查询时从 `tags->>'source_detail'` 解压。 +2. **来源筛选动态从 DB 取**:`get_distinct_sources()` 查库里所有 `tags->>'source_detail'` DISTINCT 值,下拉选项不写死,新增一本杂志自动出现。 +3. **embedding 复用**:直接调用 `EmbeddingService.embed_single(content_md, embed_type="db")`,1536 维向量已验证,不重写。 +4. **删除 CASCADE**:数据库层已有 `ON DELETE CASCADE`,Python 侧只删 `KnowledgeItem` 即可。 + +### 测试结果(已验证) + +| 验收项 | 结果 | +|--------|------| +| 两篇 md 均成功入库 | ✅ id=27(光辉之路)、id=28(超级战舰) | +| 杂志篇显示作者"钱峰",无播出时间 | ✅ author=钱峰, publish_date=null | +| 节目篇显示编导"左鑫",播出日期"待补充"不显示 | ✅ author=左鑫, publish_date=null | +| source_detail 正确存下("航空知识 2026年第1期") | ✅ tags 中可见 | +| 来源筛选下拉可据此筛 | ✅ /api/knowledge/sources 返回 `航空知识 2026年第1期` | +| knowledge_embeddings 有对应行(向量已生成) | ✅ 两行,19207/19174 字符(1536 维) | +| 删除有二次确认,确认后条目+向量一并消失 | ✅ 删除 id=27 验证,embeddings 级联消失 | + +### commit 历史 +- `779429a` feat: 添加 python-frontmatter 依赖用于解析 md yaml frontmatter +- `7b6ae24` feat: 知识库管理后台上传/列表/删除API,含frontmatter解析 +- `7155a18` feat: 知识库管理前端页面,含上传/列表/筛选/删除 + +### 潜在风险 +1. **windows CRLF 警告**:每次 commit 都有 CRLF→LF 警告,不影响功能但难看,属 git 全局配置问题(core.autocrlf),下次提醒用户设一下。 +2. **"待补充"播出日期**:节目文稿的播出日期"待补充"正确存为 null,列表"播出/发表时间"格无值时不显示(render: val || null)。 +3. **删除后 sources 下拉残留**:删除后 `get_distinct_sources()` 重新查会去掉已删条目的 source_detail,正常。 +4. **embo-01 调用失败时整条上传失败**:当前是 try-except 包装,失败时返回 errors 数组,不污染已入库数据(因为单条原子),但前端需要展示 errors 反馈(已实现)。 + +--- + +## Task 3 — 知识库树形视图(路线B 按来源分组)✅(2026-05-27 完成) + +> 本段为追加内容,接续 phase3_log 的 Task 1 / Task 2 之后。 +> Task 3 实际分三轮(Task 3 主体 + 3.1 修复 + 3.2 调整)完成并验收。 + +### 目标 +在 Task 2 的扁平列表外,包一层"可展开/收起的树形导航",按"来源"两层分组, +点树节点 → 右侧列表联动过滤。保留原有上传/筛选/删除。不改 schema。 + +### 路线选择(已定) +- **走路线B:按来源分组**(制片人拍板)。理由:现有字段(source_type + source_detail)即可建树,零返工。 +- 路线A(按主题分组:涉及装备/技术等)的原料其实已在库(related_entities / tags JSONB), + 将来转 A 路大概率无需补字段重传,只改分组层 + 映射表。 +- **解耦红线**:交互层(KnowledgeTree.jsx)与数据分组层(useKnowledgeGrouping.js + 后端 get_grouped_items)分离, + 转 A 路只改分组层一处,树组件不动。 + +### 三轮各自做了什么 + +**Task 3(树主体)** +- 两层树:来源大类 → 二级(出处)。"全部"为根节点;空大类规则、_none_ 不泄漏。 +- 交互:展开/收起、全部展开/全部收起、节点高亮、右侧联动。 + +**Task 3.1(修复)** +- 修出处筛选下拉为空(根因:前端从未调用 listSources,用 items.length 误判;改为调 /sources 动态填充)。 +- 修页面宽度跳动(右侧容器加 minWidth:0 约束)。 +- 表格列宽规划;上传区收成一行按钮;隐藏"入库时间"列(仅前端隐藏,DB 字段保留)。 + +**Task 3.2(调整)** +- 大类固定排序:节目文稿 → 杂志文章 → 报题单 → 其他。 +- 节目文稿按作者(author)归堆二级(如"左鑫");映射表 SECONDARY_GROUP_FIELD 落地。 +- 左右两列对齐(曾尝试 flex→Grid 重构,连续改坏,最终回退 flex 止损,保留功能改进)。 +- 空大类(0条)不显示。 + +### 关键设计决策 +1. **二级分组维度按大类不同**:manuscript→author,military_report→source_detail,其余 None。 + 做成**可配置映射表**(前端 useKnowledgeGrouping.js + 后端 knowledge_service.py 同名映射),改一处即可调整。 + ⚠️ 此设计使"第二层维度"在不同大类下含义不同,是制片人知情拍板的临时方案; + 将来宜把"按编导看稿"迁为独立筛选(见快照 Backlog#3)。 +2. **点击筛选为前端本地计算**:displayedItems 基于已拉取全量 items + 选中节点实时算,不发网络请求,稳定。 +3. **过滤按大类用对应字段**:manuscript 比 author,military_report 比 source_detail(Task 3.2 最终修对的关键)。 +4. **空大类不显示根因**:初始化时 type_groups 预建所有大类空列表,致 continue 失效;改为动态收集真实出现的 source_type。 + +### 改动文件 +| 文件 | 动作 | +|------|------| +| `backend/app/services/knowledge_service.py` | 新增 get_grouped_items();SOURCE_TYPE_ORDER;SECONDARY_GROUP_FIELD;空大类动态收集 | +| `backend/app/api/knowledge.py` | 新增 GET /api/knowledge/grouped | +| `frontend/src/services/knowledgeService.js` | 新增 getGroupedItems() | +| `frontend/src/hooks/useKnowledgeGrouping.js` | 新增(数据分组层,解耦核心) | +| `frontend/src/components/KnowledgeTree/KnowledgeTree.jsx` | 新增(树交互组件) | +| `frontend/src/components/KnowledgeTree/KnowledgeTree.css` | 新增 | +| `frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx` | 重构左右分栏;按 type 分支过滤;上传区收小;隐藏入库时间列 | + +### 验收结果(制片人真实点页面逐条验,已通过) +| 验收项 | 结果 | +|--------|------| +| 树两层、大类固定顺序(节目文稿在前) | ✅ | +| 节目文稿 → 左鑫 → 点击右侧出现超级战舰 | ✅ | +| 杂志文章 → 航空知识2026年第1期 → 点击右侧出现对应2篇 | ✅ | +| 点"全部"/各节点联动正确 | ✅ | +| 全部展开/全部收起、宽度不跳、上传按钮收小 | ✅ | +| 空大类(报题单/其他 0条)不显示 | ✅ | +| 原有上传/筛选/删除无回归 | ✅ | + +### commit 历史 +- 树主体与修复多次提交,最终修复 commit `3409d48`(前序含 `769a048` 等)。 + +### 教训(重要,下次接手避雷) +1. **Cline 自检多次虚标 ✅**:完工报告打勾项实测多坏。纪律:不信自检,制片人真实点页面验;禁止用会误报的检查方式(如跨多行正则)。 +2. **key 两端对齐反复踩坑**:树节点 key 格式改动时生成端与过滤端未同步,致点节点筛不出。改 key 属高风险,改后必实测联动。 +3. **布局别轻易换实现方式**:flex→Grid 整体重构连续改坏,靠"回退 flex + 保留功能"止损。已验证好用的布局不为对齐而重构。 +4. **排查 > 盲改**:Cline 报告自相矛盾或与页面不符时,让制片人直接发源码文件给顾问看,一眼定位胜过反复试错。 +5. **见好就收**:像素级打磨边际收益递减,记 backlog 改天连同视觉规范统一弄。 + +### 潜在风险 / 遗留 +1. 界面像素级未到位:上传按钮框与筛选栏框上下精确对齐未做到;整体配色/字号属"能接受"非"精致",记 backlog。 +2. 200+ 笔记尚未批量录入:需先试 10 篇验证解析,且宜等 pdf 存储方案定后再全量(见快照 Backlog#1、#2)。 +3. Phase 3 内"语义搜索"尚未做(向量已就位,随时可开),是下一刀候选。