chore: 工作台产品化进行中的改动

把工作区里其余在制品一并入库,主要是工作台产品化的推进:

  后端:新增 capability_definition / project / my_app_center / office_skill
        接口与 action_definition / skill_definition / project / user_app_center
        模型,config 加路由健康上报。
  前端:新增 frontend/src/skills(Office 技能与 workbuddy 复刻)、
        项目管理、应用中心、能力目录页,以及配套 api / store / config;
        聊天侧新增 SpecialistChip / SpecialistPanel / SkillStrip / AppChatRail
        等组件。
  清理:移除旧 views/tools 下的单页工具(已并入工作台)、_frozen 冻结组件、
        cmd/inspect_oa_debug 调试入口,以及两份调试笔记。
  其它:文档与启动脚本同步。

(这批改动与上一提交的 SY23 工作并行进行,此前已在同一工作区内交织。)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-17 21:32:35 +08:00
co-authored by Claude Code
parent 8b136d4a10
commit 16d63de4e1
180 changed files with 22283 additions and 13850 deletions
+255 -117
View File
@@ -1,8 +1,8 @@
# BE05 — 知识入库体系总设计(Knowledge Ingest)
> **版本:V1.1 | 定稿**
> **定位**:把「素材入库」与「结构化知识入库」统一为一套可复用的知识入库体系,覆盖上传 → 生成 → 审批 → 入库全链路。
> **配套**:BE03(素材转换管线)、BE04(AI 检索)、`docs/knowledge_source/README.md`(知识源格式契约)。
> **版本:V1.3 | 当前实现规范**
> **技术栈**:Go 后端 + SQLite + Vue3。
> **落点**:素材上传/审批/摄入、知识源(media_file/knowledge_source)审批与入库、切块与检索均由 Go 实现(`eai_agentplatform/backend-go/internal/api/media.go`、`knowledge.go`、`knowledge_pipeline.go` 等);知识分类/入库已彻底迁移到 Go,Python 的 `knowledge_service`(分类/FAISS 索引)已删除;检索为 Go 原生 brute-force 向量/关键词召回(对齐 D07/D13)。
---
@@ -19,10 +19,10 @@
---
## 2. 完整目录结构(定稿)
## 2. 完整目录结构
```
eai_agentplatform_app/
eai_agentplatform/
├── docs/
│ └── knowledge_source/ # 知识源文档(权威源,纳入 git 版本管理)
│ ├── README.md # 格式契约 + 答案契约 + 审批状态机
@@ -32,40 +32,41 @@ eai_agentplatform_app/
│ ├── 04_AI咨询与实施类.md
│ └── 05_企业级AI工具与平台.md
│
├── backend/
│ ├── app/
├── backend-go/
│ ├── config/ai_config.json # AI 路由配置(含 embed_gen/llm_fallback)
│ ├── internal/
│ │ ├── api/
│ │ │ ├── media.py # 【已有】素材上传/审批/预览
│ │ │ └── knowledge.py # 【新增】知识源扫描/审批/摄入
│ │ ├── models/
│ │ │ ├── media_file.py # 【已有】素材表
│ │ │ ├── knowledge_chunk.py # 【改造】来源扩展(见 §4)
│ │ │ └── knowledge_source.py # 【新增】知识源文档表
│ │ ├── services/
│ │ │ ├── media_service.py # 【已有】LibreOffice/PyMuPDF 转换管线
│ │ │ └── knowledge_service.py # 【新增】md 解析 + 摄入
│ │ └── scripts/
│ │ ├── init_db.py # 【已有】建表 + 种子
│ │ └── ingest_knowledge.py # 【新增】知识源扫描/摄入脚本
│ │ │ ├── media.go # 素材上传/审批/预览/提取管线
│ │ │ ├── knowledge.go # 知识源扫描/审批/摄入/解析
│ │ │ ├── knowledge_pipeline.go # 知识检索/分类/问答管线
│ │ │ └── knowledge_index.go # 知识索引重建(Go 原生)
│ │ ├── model/
│ │ │ ├── media_file.go # 素材表
│ │ │ ├── knowledge_source.go # 知识源文档表
│ │ │ ├── knowledge_chunk.go # 知识块表
│ │ │ └── question.go # 考试题目
│ │ └── store/ # DB 初始化 + GORM 连接
│ └── data/
│ ├── media/ # 【已有】素材物理文件(git 忽略)
│ │ ├── upload/
│ │ └── _preview_cache/
│ └── logs/ # 【已有】special_trace 按天日志
│
└── docker-compose.yml # 【待创建】
│ ├── eai_agentplatform.db # SQLite 单文件数据(自动建表)
│ ├── kb_data/ # 知识库物理文件(git 忽略)
│ │ ├── approved/
│ │ ├── pending/
│ │ └── rejected/
│ └── backups/ # 定期备份(git 忽略)
└── deploy/eai_agentplatform.env # 部署环境变量
```
**约定**:
- `docs/knowledge_source/` 是知识源 md 的唯一入库口(权威源,git 管理,可 diff 可回滚)。
- 运行时摄入**直接读取**该目录,不复制到 backend/data(单一事实源,避免双份漂移)。
- 素材物理文件仍在 `backend/data/media/`(git 忽略)。
- 运行时摄入**直接读取**该目录,不复制(单一事实源,避免双份漂移)。
- 素材物理文件存储在 `data/kb_data/`(git 忽略),分为 `approved/pending/rejected` 三个子目录。
- 数据库为 SQLite 单文件(`data/eai_agentplatform.db`),GORM 首次启动自动建表。
---
## 3. 两条流程的状态机(定稿)
## 3. 两条流程的状态机
### 流程 A:非结构化素材(BE03 已实现)
### 流程 A:非结构化素材
```
员工上传 ──▶ media_file(pending) ──审批──▶ approved ──▶ 异步转换
@@ -73,20 +74,20 @@ eai_agentplatform_app/
▼
┌──────────────────────────┐
│ 文档: LibreOffice→PDF→ │
│ PyMuPDF 提取→切片 │
│ pdftotext 提取→切片 │
│ 视频/图片: 仅预览 │
└──────────┬───────────────┘
▼
knowledge_chunk
```
### 流程 B:结构化知识源(本次新增)
### 流程 B:结构化知识源(知识源 md)
```
知识源 md(docs/knowledge_source/)
│
▼
【扫描】ingest_knowledge.py / POST /api/knowledge/scan
【扫描】POST /api/knowledge/scan
│ 解析 front-matter → 为每个 md 建 knowledge_source 记录
▼
knowledge_source(pending) ──审批──▶ approved ──▶ 解析摄入
@@ -102,125 +103,262 @@ knowledge_source(pending) ──审批──▶ approved ──▶ 解析摄入
---
## 4. 数据模型变更
## 4. 数据模型
### 4.1 新增 `knowledge_source` 表
### 4.1 `knowledge_source` 表
```sql
CREATE TABLE knowledge_source (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(256) NOT NULL COMMENT '文档标题',
file_path VARCHAR(512) NOT NULL UNIQUE COMMENT 'md 相对路径(docs/knowledge_source/ 下)',
category VARCHAR(64) NOT NULL COMMENT '分类:general/capital_consulting/qualification_counseling/ai_consulting/ai_tools_platform',
domain ENUM('company','product','sales') NOT NULL DEFAULT 'product',
source_version VARCHAR(32) NOT NULL COMMENT '源版本,如 V1.0',
audit_status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
audit_by BIGINT UNSIGNED DEFAULT NULL COMMENT '审批人ID',
audit_at DATETIME(6) DEFAULT NULL COMMENT '审批时间',
reject_reason VARCHAR(512) DEFAULT NULL COMMENT '驳回理由',
ingested TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已摄入',
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
INDEX idx_ks_status (audit_status),
INDEX idx_ks_category (category),
CONSTRAINT fk_ks_auditor FOREIGN KEY (audit_by) REFERENCES user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
table: knowledge_source
columns:
id uint GORM primary key (auto-increment)
title string(256) 文档标题
file_path string(512) unique 相对路径(docs/knowledge_source/ 下)
category string(64) general/capital_consulting/qualification_counseling/ai_consist/ai_tools_platform
domain string(16) company / product / sales(默认 product)
source_version string(32) 源版本,如 V1.0
audit_status string(16) pending / approved / rejected(默认 pending)
audit_by *uint 审批人 ID
audit_at *time.Time 审批时间
reject_reason string(512) 驳回理由
ingested bool 是否已摄入(默认 false)
knowledge_space_key string(64) 知识空间键
created_at time.Time
updated_at time.Time
indexes:
idx_ks_status (audit_status)
idx_ks_category (category)
```
### 4.2 改造 `knowledge_chunk` 表(来源扩展)
### 4.2 `knowledge_chunk` 表(知识块,AI 检索最小单元)
现状 `media_file_id BIGINT UNSIGNED NOT NULL` 强绑定素材。改造后:
```sql
media_file_id BIGINT UNSIGNED DEFAULT NULL, -- 由 NOT NULL 改为可空
knowledge_source_id BIGINT UNSIGNED DEFAULT NULL, -- 新增
-- 约束:media_file_id 与 knowledge_source_id 二者必居其一(应用层校验,Fail Fast)
```
table: knowledge_chunk
columns:
id uint GORM primary key
media_file_id *uint 二选一(素材来源)
knowledge_source_id *uint 二选一(知识源来源)
source_type string(32) pdf/doc/md(来源类型标识)
source_id string(64) 来源 ID 字符串
knowledge_space_key string(64) 知识空间键
chunk_index int 块序号
content text 内容正文
created_at time.Time
indexes:
idx_mf (media_file_id)
idx_ks (knowledge_source_id)
idx_key (knowledge_space_key)
```
对应 ORM `models/knowledge_chunk.py`:`media_file_id` 改为可空,新增 `knowledge_source_id` 外键与 relationship。
### 4.3 `media_file` 表(素材文件)
```
table: media_file
columns:
id uint GORM primary key
file_name string(512) 文件名
file_path string(512) 物理路径(data/kb_data/...)
file_size int64 文件大小
file_type string(64) MIME 类型
status string(16) pending / approved / rejected(默认 pending)
approved_at *time.Time 审批时间
audit_by *uint 审批人 ID
audit_status string(16) 审批状态
file_category string(64) 分类(培训/产品/规则等)
extract_status string(64) 提取状态(pending / extracting / completed / failed)
created_at time.Time
updated_at time.Time
indexes:
idx_status (status)
idx_category (file_category)
```
---
## 5. 模块清单(8 模块)
## 5. 模块清单
| # | 模块 | 状态 | 职责 | 落点 |
|---|------|------|------|------|
| 1 | 上传模块 | ✅ 已有 | 直传 + 分片(>100MB) | `api/media.py` |
| 2 | 转换模块 | ✅ 已有 | LibreOffice → PDF | `services/media_service.py` |
| 3 | 提取切片模块 | ✅ 已有 | PyMuPDF 提取 + 段落切片 | `services/media_service.py` |
| 4 | **摄入模块** | 🆕 新增 | 解析知识源 md → product/question/chunk | `services/knowledge_service.py` |
| 5 | 审批模块 | 🔧 扩展 | 素材审批(已有)+ 知识源审批(新增) | `api/media.py` + `api/knowledge.py` |
| 6 | 入库模块 | 🔧 扩展 | 写 product/question/knowledge_chunk | `services/knowledge_service.py` |
| 7 | 预览模块 | ✅ 已有 | approved 素材预览 | `api/media.py` |
| 8 | 检索模块 | ✅ 已有 | MySQL FULLTEXT 召回 | `services/ai_service.py` |
| 1 | 上传模块 | ✅ | 直传 + 分片(>100MB) | `api/media.go` |
| 2 | 转换模块 | ✅ | LibreOffice→PDF,pdftotext→文本 | `api/media.go` |
| 3 | 提取切片模块 | ✅ | PDF→文本→段落切片 | `api/media.go` |
| 4 | 摄入模块 | ✅ | 解析知识源 md → product/question/chunk | `api/knowledge.go` |
| 5 | 审批模块 | ✅ | 素材审批 + 知识源审批 | `api/media.go` + `api/knowledge.go` |
| 6 | 检索模块 | ✅ | brute-force 向量/关键词召回 | `api/knowledge_pipeline.go` |
| 7 | 索引重建 | ✅ | 知识索引异步重建(Go 原生) | `api/knowledge_index.go` |
| 8 | 问答管线 | ✅ | 意图分类 → FAQ 匹配 → 向量/关键词检索 → LLM 兜底 | `api/knowledge_pipeline.go` |
---
## 6. 新增 API(knowledge.py)
## 6. API 端点
```python
router = APIRouter(prefix="/api/knowledge", tags=["knowledge"])
### 知识源管理
# ── 扫描(管理员)──
POST /api/knowledge/scan
# 扫描 docs/knowledge_source/*.md
# 为新增/变更的 md 建 knowledge_source 记录(status=pending)
# 已存在且未摄入的记录跳过;返回扫描结果列表
| 端点 | 方法 | 职责 |
|------|------|------|
| `POST /api/knowledge/scan` | 管理员 | 扫描 `docs/knowledge_source/*.md`,为新增/变更的 md 建 `knowledge_source` 记录(status=pending)。已存在且未摄入的记录跳过。 |
| `GET /api/knowledge/audit-list?status=&page=&size=` | 管理员 | 按 `audit_status` 过滤,分页返回知识源列表。 |
| `POST /api/knowledge/audit/{sourceId}` | 管理员 | `{ "action": "approve" | "reject", "reject_reason": "..." }`。approve → 触发摄入(同步解析写入 product/question/chunk,标记 `ingested=1`,然后异步重建索引);reject → 必填 `reject_reason`,不摄入。 |
| `GET /api/knowledge/status/{sourceId}` | 管理员 | 返回 `{ "audit_status", "ingested", "reject_reason", "knowledge_space_key" }`。 |
# ── 审批(管理员)──
GET /api/knowledge/audit-list?status=pending
POST /api/knowledge/audit/{source_id}
# { "action": "approve" | "reject", "reject_reason": "..." }
# approve → 触发摄入(同步解析写入 product/question/chunk,标记 ingested=1)
# reject → 必填 reject_reason,不摄入
### 素材管理
# ── 状态查询 ──
GET /api/knowledge/status/{source_id}
# { "audit_status", "ingested", "reject_reason" }
| 端点 | 方法 | 职责 |
|------|------|------|
| `POST /api/media/upload` | 通用 | 文件上传(直传) |
| `POST /api/media/upload/init` | 通用 | 分片上传初始化(>100MB) |
| `POST /api/media/upload/chunk` | 通用 | 分片上传分片 |
| `POST /api/media/upload/complete` | 通用 | 分片上传完成 |
| `POST /api/media/audit/{fileId}` | 管理员 | `{ "action": "approve" | "reject" }`。approve → 触发异步提取;reject → 素材标记 rejected。 |
| `GET /api/media/preview/{fileId}` | 通用 | 预览素材(PDF/图片/视频) |
| `GET /api/knowledge/index/rebuild` | 管理员 | 手动触发知识索引重建(Go 原生,读取 `knowledge_chunk` + `media_file` + `knowledge_source` 全量数据重建索引)。返回 `{ "rebuild": true, "chunk_count": N, "service_enabled": false }`。 |
---
## 7. 摄入逻辑(`api/knowledge.go:ingestSource`)
### 解析规则
知识源 md 采用 **YAML front-matter + 三个固定 `## ` section** 格式,Go 后端按如下规则解析:
```
---
title: 文档标题
category: 分类
domain: domain(product/company/sales)
version: V1.0
---
```
---
然后按 `## ` 二级标题切分三个 section:
## 7. 摄入脚本(ingest_knowledge.py)
| Section 名 | 内容 | 解析 |
|-----------|------|------|
| `结构化产品数据` | 每个 `### code name` 键值对列表 → product | 解析 `code`/`name`/`category`/`tags`/`description`/`pricing`/`commission_recommend`/`commission_negotiate`/`public_course_bonus`/`version_risk`/`report_rules` |
| `AI 检索知识` | 每个 `### 标题` 段落 → knowledge_chunk | 段落正文写入 content,source_type=md |
| `考试题目` | 每个 `### Qn` 字段列表 → question | 解析 `type`/`stem`/`options`(JSON)/`answer`(JSON)/`explanation`/`domain` |
```python
"""知识源摄入脚本:扫描 md → 建记录 → (审批通过后)解析入库
### 断点摄入
用法:
cd backend && source venv/bin/activate
python -m app.scripts.ingest_knowledge --scan # 仅扫描建 pending 记录
python -m app.scripts.ingest_knowledge --ingest <source_id> # 摄入单个已审批源
"""
`ingestSource` 先检查 `src.Ingested`:若已为 true,直接返回(跳过重复解析写入)。
# 解析规则(严格对齐 knowledge_source/README.md 格式契约):
# 1. 读 YAML front-matter:category / domain / source_version
# 2. 按 "## " 二级标题切块:
# ## 结构化产品数据 → 解析 "### code name" + "key: value" 列表 → product
# ## AI 检索知识 → 每个 "### 标题" 段落 → knowledge_chunk
# ## 考试题目 → 每个 "### Qn" 的字段列表 → question
# 3. 摄入后 product/question 置 status=active
# 4. knowledge_chunk 写入 knowledge_source_id,media_file_id 留空
### 写入逻辑
```go
// ingestSource 伪代码
func ingestSource(src *model.KnowledgeSource) ([3]int, error) {
data, _ := os.ReadFile(filepath.Join(Cfg.KnowledgeSourceDir, src.FilePath))
fm := parseFrontMatter(string(data))
sections := splitSections(string(data))
// 1. 产品数据 → product 表(upsert:按 code 查,已存在则更新)
for _, b := range splitBlocks(sections["结构化产品数据"]) {
kv := parseKV(b.body)
p := model.Product{Code: kv["code"], Name: kv["name"], ...}
store.DB.Save(&p) // upsert by code
}
// 2. AI 检索知识 → knowledge_chunk
for i, b := range splitBlocks(sections["AI 检索知识"]) {
store.DB.Create(&model.KnowledgeChunk{
KnowledgeSourceID: &src.ID,
SourceType: "md",
SourceID: strconv.FormatUint(uint64(src.ID), 10),
Content: b.body,
})
}
// 3. 考试题目 → question
for _, b := range splitBlocks(sections["考试题目"]) {
q := model.Question{...}
store.DB.Create(&q)
}
}
```
**Fail Fast 约束(G02)**:解析失败(front-matter 缺失、section 缺块、字段缺失)立即抛错并中断该源摄入,不静默跳过、不写半截数据。
### 断点约束
解析失败(front-matter 缺失、section 缺块、字段缺失)静默跳过对应块,不抛错中断。每个 section 的解析独立于其他 section。
---
## 8. 格式契约(已定,见 knowledge_source/README.md)
## 8. 格式契约
- 每个 md:YAML front-matter(`category` / `domain` / `source_version`)+ 三个固定 `## ` section。
- 题目答案契约:`judge=[bool]`、`single=[索引]`、`multiple=[索引列表]`。
- 已生成 5 个知识源:`01_通用规则` + `02/03/04/05` 四大分类,共 28 产品 + 20 题。
- 题目格式:
- 判断题:`- judge: true/false` + `- stem: 题干` + `- explanation: 解析`
- 单选题:`- type: single` + `- stem: 题干` + `- options: ["A. ...", "B. ..."]` + `- answer: [1]` + `- explanation: 解析`
- 多选题:`- type: multiple` + `- stem: 题干` + `- options: [...]` + `- answer: [1,2]` + `- explanation: 解析`
- 选项格式:`- 选项文本`(Go 端自动编号 A/B/C/D...)
- 已生成 5 个知识源:`01_通用规则` + `02/03/04/05` 四大分类。
---
## 9. 实现清单(代码侧,已完成)
## 9. 知识检索管线(`api/knowledge_pipeline.go`)
- [x] `models/knowledge_source.py` 新建
- [x] `models/knowledge_chunk.py` 来源扩展(media_file_id 可空 + knowledge_source_id)
- [x] `services/knowledge_service.py`(md 解析 + 摄入)
- [x] `api/knowledge.py`(scan / audit / status)
- [x] `scripts/ingest_knowledge.py`
- [x] `scripts/init_db.py` 追加 knowledge_source 建表 + knowledge_chunk 字段迁移
- [x] `docs/db_schema.md`、`docs/api.md` 同步更新
知识检索采用 **四层管线**,逐层降级:
```
用户提问
│
▼ ① 意图分类器(heuristicKnowledgeIntent)
│ 关键词规则匹配 → intent: invalid / smalltalk / out_of_scope / faq / document
│ 命中 smalltalk/out_of_scope → 直接返回答案,管线终止
│
▼ ② FAQ 匹配(matchKnowledgeFAQ)
│ 问题归一化 + 相似问题匹配 + 关键词重叠评分
│ 最佳分 >= 70 → 返回标准答案,管线终止
│
▼ ③ 向量检索(vectorRetrieveCitations)
│ 1) 调用 embed_gen 路由嵌入 query + 候选块(brute-force)
│ 2) 计算余弦相似度,降序排序
│ 3) 最高分 >= 0.35 且不需要 LLM 兜底 → 直接给出结果
│
▼ ④ LLM 兜底(llm_fallback)
│ 将检索结果 + 系统提示词发送给 LLM
│ LLM 基于知识片段生成最终回答
```
### 检索技术细节
- **嵌入路由**:通过 `config.GetRoute("embed_gen")` 获取嵌入模型配置,调用 Ollama embedding API。
- **向量检索**:brute-force 全量候选块嵌入 + 余弦相似度排序(对齐 D07/D13),不依赖外部向量库。
- **关键词兜底**:当嵌入模型不可用时(`embed_gen` 路由缺失/调用失败),降级为 `keywordRetrieveCitations`——term 重叠评分排序。
- **空间过滤**:支持按 `knowledge_space_key` 过滤候选块(如选择某知识库后再提问)。
- **直接回答判定**:`canDirectAnswerFromVector` 判定:最高分 ≥ 0.35 且不含 LLM 兜底关键词 → 直接返回。
- **LLM 兜底判定**:`needsLLMFallback` 检测 query 含「总结/梳理/分析/对比/归纳/起草/生成/提纲/报告」等关键词 → 强制 LLM 处理。
---
## 10. 知识索引重建(`api/knowledge_index.go`)
知识索引为 Go 原生实现,异步触发:
```
triggerKnowledgeIndexRebuild()
└─ go func()
└─ rebuildKnowledgeIndexNow()
└─ buildKnowledgeIndexItems()
├─ 读取 knowledge_chunk 全量
├─ 读取 approved media_file
├─ 读取 approved knowledge_source
└─ 合并 → 构建索引项列表
```
- 触发时机:知识源审批通过摄入后(`ingestSource` 成功后)、素材提取管线完成切片后。
- 锁机制:`knowledgeIndexRebuildLock` 防止并发重建。
- 索引项字段:`ID, Title, Content, ChunkIndex, SourceType, SourceID, KnowledgeSpaceKey, KnowledgeSpaceName`。
- 管理员也可通过 `POST /api/knowledge/index/rebuild` 手动触发重建。
---
## 11. 实现清单(Go 代码,已完成)
- [x] `internal/model/knowledge_source.go` — 知识源表
- [x] `internal/model/knowledge_chunk.go` — 知识块表(来源扩展:media_file_id/knowledge_source_id 二选一)
- [x] `internal/model/media_file.go` — 素材表(status + extract_status 字段)
- [x] `api/knowledge.go` — scan / audit / status / ingestSource(md 解析 + 摄入)
- [x] `api/media.go` — 上传 / 审批 / 提取管线 / 预览
- [x] `api/knowledge_pipeline.go` — 意图分类 / FAQ / 向量检索 / LLM 兜底
- [x] `api/knowledge_index.go` — 知识索引重建(Go 原生)
- [x] `internal/store/` — DB 初始化 + 自动建表
- [x] `config/ai_config.json` — AI 路由配置(embed_gen / llm_fallback)