# 知识图谱后台管理功能

## Context

当前系统入库时 LLM 自动抽取实体/关系（7 种实体、11 种关系），前端可查看文档图谱和实体搜索。但管理员无法审核纠错、合并重复实体、批量重建图谱。需要新增完整的图谱后台管理 API（P0-P2 全部实现）。

**去重策略**：编辑距离初筛 + 同义词别名精确匹配（别名存在 Neo4j 节点 `aliases` 属性上）。

**类型定义可配置**：实体类型和关系类型从硬编码改为动态管理，存储在 Neo4j 元节点中。

---

## 架构决策

1. **新建独立路由** `admin_graph.py`（~24 个接口，不混入现有 `admin.py`）
2. **新建服务层** `graph_admin_service.py`（业务逻辑 + Cypher），`neo4j_client.py` 只加少量通用方法
3. **别名存储在 Neo4j 节点** `aliases: string[]` 属性——合并时自动将被合并名加入别名
4. **重建复用现有 Celery 原语**，扩展 `graph_task.py`
5. **实体改名不同步 ES**——ES 元数据来自 OA 权威源，Neo4j 是派生层，改名时返回 warning
6. **类型定义存储在 Neo4j 元节点**——`(:_EntityType {name, description, icon, color, properties_schema})` 和 `(:_RelationType {name, description, source_labels, target_labels})`。启动时加载到内存缓存，代替硬编码的 `_VALID_NODE_LABELS` / `_VALID_REL_TYPES`。前缀 `_` 表示系统内部元节点。
7. **LLM 抽取 prompt 动态生成**——`graph_builder.py` 读取当前类型定义生成 prompt，而非硬编码类型列表

---

## 新建文件（3 个）

### 1. `backend/app/api/schemas/admin_graph.py`

所有请求/响应 Pydantic 模型：

| 模型 | 用途 |
|------|------|
| `GraphStatsResponse` | 分类节点数、关系数、孤立节点数、占位符数 |
| `GraphHealthResponse` | 无关系实体比例、重复候选数、图谱缺失文档数 |
| `EntityListItem / EntityListResponse` | 分页实体列表（含 connection_count） |
| `EntityDetail` | 实体详情 + 关联文档 + 邻域 |
| `EntityCreateRequest / EntityUpdateRequest` | 创建/编辑 |
| `DuplicateCandidate / DuplicateListResponse` | 重复候选对（含相似度、匹配类型） |
| `EntityMergeRequest / EntityMergeResponse` | 合并（迁移关系数、别名列表） |
| `RelationshipItem / RelationshipListResponse` | 关系列表 |
| `RelationshipCreateRequest` | 创建关系 |
| `RebuildRequest / RebuildResponse / RebuildAllResponse / RebuildStatusResponse` | 重建任务 |
| `PlaceholderItem / PlaceholderListResponse` | 占位符列表（含引用方） |
| `PlaceholderLinkRequest / PlaceholderLinkResponse` | 关联占位符 |
| `EntityTypeItem / EntityTypeListResponse` | 实体类型列表 |
| `EntityTypeCreateRequest / EntityTypeUpdateRequest` | 创建/编辑实体类型（name, description, icon, color, properties_schema） |
| `RelTypeItem / RelTypeListResponse` | 关系类型列表 |
| `RelTypeCreateRequest / RelTypeUpdateRequest` | 创建/编辑关系类型（name, description, source_labels, target_labels） |

### 2. `backend/app/core/graph_admin_service.py`

```
class GraphAdminService:
    # ── P0: 统计 ──
    get_graph_stats()         → UNION ALL 分类计数 + 关系类型计数 + 孤立节点 + 占位符
    get_graph_health(es_ids)  → 无关系比例 / 重复候选数 / ES-Neo4j 不一致文档

    # ── P0: 实体 CRUD ──
    list_entities(label, name, page, sort_by)
        → MATCH (n:Label) WHERE n.name CONTAINS $name
          WITH n, size([(n)-[]-() | 1]) AS conn ORDER BY conn DESC SKIP/LIMIT
    get_entity_detail(id)     → 属性 + 连接文档 + 1-hop 邻域
    create_entity(label, props)  → CREATE (n:Label $props)（捕获约束冲突→409）
    update_entity(id, props)  → SET n += $props（连接 Document 时加 warning）
    delete_entity(id)         → DETACH DELETE

    # ── P1: 去重合并 ──
    detect_duplicates(label, threshold=0.85, limit=50)
        → 查同类实体名 + aliases → rapidfuzz Jaro-Winkler 初筛 → 别名精确补充
        → 按首字符分桶减少 O(n²)
    merge_entities(primary_id, secondary_id, add_alias=True)
        → 事务：查 secondary 关系 → 在 primary 上创建等效关系 → 加别名 → 删 secondary

    # ── P2: 关系 ──
    list_relationships(type, source_id, target_id, page)
    create_relationship(source_id, target_id, type, props)  → 验证 type ∈ _VALID_REL_TYPES
    delete_relationship(rel_id)

    # ── P2: 占位符 ──
    list_placeholders()       → MATCH (d:Document {is_placeholder: true}) + 引用方
    link_placeholder(id, real_doc_id)  → 迁移 REFERENCES 关系 → 删占位符
    delete_placeholder(id)

    # ── 类型定义管理 ──
    list_entity_types()       → MATCH (t:_EntityType) RETURN t ORDER BY t.name
    create_entity_type(name, desc, icon, color, properties_schema)
        → CREATE (t:_EntityType $props)
        → 同时在 Neo4j 创建 uniqueness constraint: CREATE CONSTRAINT IF NOT EXISTS FOR (n:Label) REQUIRE n.name IS UNIQUE
        → 刷新内存缓存
    update_entity_type(name, props)
        → 普通属性（description/icon/color）: SET t += $props → 刷新缓存
        → 重命名（props 含 new_name）:
          1. MATCH (n:OldLabel) REMOVE n:OldLabel SET n:NewLabel（批量迁移所有实例）
          2. DROP CONSTRAINT old_unique → CREATE CONSTRAINT new_unique
          3. 更新 _EntityType 节点 name
          4. 刷新缓存
    delete_entity_type(name)
        → 检查该类型下是否有实例节点，有则拒绝（400）
        → 删除 _EntityType 节点 + 对应 constraint → 刷新缓存

    list_rel_types()          → MATCH (t:_RelationType) RETURN t
    create_rel_type(name, desc, source_labels, target_labels) → CREATE (t:_RelationType $props) → 刷新缓存
    update_rel_type(name, props)
        → 普通属性: SET t += $props → 刷新缓存
        → 重命名（props 含 new_name）:
          1. MATCH (a)-[r:OLD_TYPE]->(b) CREATE (a)-[r2:NEW_TYPE]->(b) SET r2 = properties(r) DELETE r
             （Neo4j 不支持原地改 type，需逐条重建）
          2. 更新 _RelationType 节点 name
          3. 刷新缓存
          4. 返回迁移的关系数量
    delete_rel_type(name)     → 检查该类型是否有实例关系，有则拒绝 → 删 _RelationType → 刷新缓存

    # 内存缓存
    _entity_types_cache: dict[str, dict]   # 启动时从 Neo4j 加载
    _rel_types_cache: dict[str, dict]
    refresh_type_cache()      → 重新从 Neo4j 加载
```

**类型缓存集成**：
- `neo4j_client.py` 中的 `_VALID_NODE_LABELS` 和 `_VALID_REL_TYPES` 改为从 `GraphAdminService` 获取动态值
- `graph_builder.py` 的 LLM prompt 从缓存读取当前类型列表动态生成
- 应用启动时（lifespan）调用 `init_type_cache()` 加载，同时执行 `ensure_default_types()` 写入 7 个默认实体类型 + 11 个默认关系类型的 `_EntityType` / `_RelationType` 元节点（如果不存在）

### 3. `backend/app/api/v1/admin_graph.py`

```
GET    /admin/graph/stats                    # 图谱统计
GET    /admin/graph/health                   # 数据质量
GET    /admin/graph/entities                 # 实体列表（?label=&name=&page=&sort_by=）
GET    /admin/graph/entities/{id}            # 实体详情
POST   /admin/graph/entities                 # 创建实体
PUT    /admin/graph/entities/{id}            # 编辑实体
DELETE /admin/graph/entities/{id}            # 删除实体
GET    /admin/graph/duplicates               # 重复检测（?label=&threshold=&limit=）
POST   /admin/graph/entities/merge           # 合并实体
POST   /admin/graph/rebuild                  # 指定文档重建（body: {doc_ids}）
POST   /admin/graph/rebuild-all              # 全量重建→task_id
GET    /admin/graph/rebuild-status/{task_id} # 重建进度
GET    /admin/graph/relationships            # 关系列表
POST   /admin/graph/relationships            # 创建关系
DELETE /admin/graph/relationships/{rel_id}   # 删除关系
GET    /admin/graph/placeholders             # 占位符列表
POST   /admin/graph/placeholders/{id}/link   # 关联真实文档
DELETE /admin/graph/placeholders/{id}        # 删除占位符

# ── 类型定义管理 ──
GET    /admin/graph/entity-types              # 实体类型列表
POST   /admin/graph/entity-types              # 创建实体类型
PUT    /admin/graph/entity-types/{name}       # 编辑实体类型
DELETE /admin/graph/entity-types/{name}       # 删除实体类型（无实例时）
GET    /admin/graph/relationship-types        # 关系类型列表
POST   /admin/graph/relationship-types        # 创建关系类型
PUT    /admin/graph/relationship-types/{name} # 编辑关系类型
DELETE /admin/graph/relationship-types/{name} # 删除关系类型（无实例时）
```

所有接口需 JWT（`Depends(get_current_user)`）。
路由注册顺序：`/entities/merge` 和 `/duplicates` 在 `/entities/{id}` 之前。

---

## 修改文件（3 个）

### 4. `backend/app/infrastructure/neo4j_client.py`

新增 3 个通用方法：

| 方法 | Cypher |
|------|--------|
| `update_node(entity_id, props)` | `MATCH (n) WHERE elementId(n) = $eid SET n += $props RETURN ...` |
| `delete_node(entity_id)` | `MATCH (n) WHERE elementId(n) = $eid DETACH DELETE n RETURN rel_count` |
| `delete_relationship_by_id(rel_id)` | `MATCH ()-[r]->() WHERE elementId(r) = $rid DELETE r` |

`_VALID_NODE_LABELS` 和 `_VALID_REL_TYPES` 改为动态加载（从 `GraphAdminService` 的类型缓存获取），保留为模块级变量但初始值从 Neo4j `_EntityType` / `_RelationType` 元节点加载。增加 `reload_valid_types()` 方法供类型变更后刷新。

### 5. `backend/app/tasks/graph_task.py`

**新增 2 个 Celery 任务**：

```python
rebuild_document_graphs_task(self, doc_ids: list[str])
    # 每个 doc_id: delete_document_graph → 从 ES 取 meta+content → build_graph
    # 带进度上报: update_state(PROCESSING, {current, total, doc_id})

rebuild_all_graph_task()
    # 从 ES 查所有 completed doc_ids → 调用 rebuild_document_graphs_task
```

**修复现有 `_reconstruct_content()`**：
当前用 `{"term": {"doc_id": doc_id}}` 查 chunks，但新 schema 已无 `doc_id` 字段。
改为：先从 meta 获取 `content_hash`，再 `{"term": {"content_hash": content_hash}}` 查 chunks。

### 6. `backend/app/api/v1/router.py`

```python
from app.api.v1.admin_graph import router as admin_graph_router
v1_router.include_router(admin_graph_router)
```

---

## 新依赖

- `rapidfuzz` — Jaro-Winkler / Levenshtein 字符串相似度

---

## 额外修改文件

### 7. `backend/app/core/graph_builder.py` + `backend/app/prompts/entity_extraction.py`

- `entity_extraction.py` 中的硬编码实体/关系类型列表改为接收参数，由 `graph_builder.py` 传入
- `graph_builder.py` 的 `_VALID_ENTITY_TYPES` 改为从类型缓存读取
- `build_graph()` 调用时从缓存获取当前类型列表，动态拼装 prompt

### 8. 应用启动（lifespan）

在 `app/main.py` 或 startup hook 中调用：
```python
admin_service = GraphAdminService(neo4j_client)
await admin_service.ensure_default_types()  # 写入 7+11 个默认元节点（如不存在）
await admin_service.refresh_type_cache()    # 加载到内存
```

---

## 前端实现

### 新建文件（5 个）

#### F1. `frontend/src/api/admin-graph.ts`

API 调用模块，封装所有后端 admin graph 接口：

```typescript
const BASE = '/api/ai/v1/admin/graph'

// 统计
getGraphStats()                         → GET /stats
getGraphHealth()                        → GET /health

// 实体 CRUD
listEntities(params)                    → GET /entities?label=&name=&page=&sort_by=
getEntityDetail(id)                     → GET /entities/{id}
createEntity(body)                      → POST /entities
updateEntity(id, body)                  → PUT /entities/{id}
deleteEntity(id)                        → DELETE /entities/{id}

// 去重合并
detectDuplicates(params)                → GET /duplicates?label=&threshold=&limit=
mergeEntities(body)                     → POST /entities/merge

// 重建
rebuildDocGraphs(body)                  → POST /rebuild
rebuildAllGraphs()                      → POST /rebuild-all
getRebuildStatus(taskId)                → GET /rebuild-status/{taskId}

// 关系
listRelationships(params)              → GET /relationships
createRelationship(body)               → POST /relationships
deleteRelationship(relId)              → DELETE /relationships/{relId}

// 占位符
listPlaceholders()                     → GET /placeholders
linkPlaceholder(id, body)              → POST /placeholders/{id}/link
deletePlaceholder(id)                  → DELETE /placeholders/{id}

// 类型定义
listEntityTypes()                      → GET /entity-types
createEntityType(body)                 → POST /entity-types
updateEntityType(name, body)           → PUT /entity-types/{name}
deleteEntityType(name)                 → DELETE /entity-types/{name}
listRelTypes()                         → GET /relationship-types
createRelType(body)                    → POST /relationship-types
updateRelType(name, body)              → PUT /relationship-types/{name}
deleteRelType(name)                    → DELETE /relationship-types/{name}
```

#### F2. `frontend/src/types/admin-graph.d.ts`

TypeScript 类型定义，与后端 Pydantic schema 对应。

#### F3. `frontend/src/views/admin/GraphManager.vue`

图谱管理主页面，采用 Tab 页签布局：

```
┌─ 图谱管理 ─────────────────────────────────────────────────────┐
│ [统计概览] [实体管理] [类型定义] [去重合并] [关系管理] [重建]   │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  （各 Tab 内容如下详述）                                        │
│                                                                │
└────────────────────────────────────────────────────────────────┘
```

**Tab 1: 统计概览**
- 上方 4 张统计卡片（`a-statistic`）：总节点数、总关系数、孤立节点、占位符
- 下方分类统计表（各实体类型节点数、各关系类型数量）
- 数据质量指标卡（无关系比例、重复候选数、图谱缺失文档数）

**Tab 2: 实体管理**
- 顶部筛选栏：类型选择（`a-select`）+ 名称搜索（`a-input-search`）+ 排序
- 新建实体按钮 → 打开 Modal（选类型、填属性）
- 分页表格：名称、类型（`a-tag` 带颜色）、关联数、操作列
- 操作列：[编辑] [删除(`a-popconfirm`)]
- 编辑 → Modal 预填属性（含 name、aliases 等）
- 点击行 → 抽屉展开实体详情（属性 + 关联文档列表 + 邻域小图）

**Tab 3: 类型定义**
- 左右分栏：左侧实体类型列表，右侧关系类型列表
- 每侧：表格 + 新建按钮
- 实体类型表列：名称、描述、颜色（色块预览）、图标、实例数、操作
- 操作列：[重命名(`a-modal` 含输入框)] [编辑属性] [删除(`a-popconfirm`)]
- 关系类型表列：名称、描述、来源类型、目标类型、实例数、操作
- 操作列同上（重命名、编辑、删除）

**Tab 4: 去重合并**
- 顶部：类型选择 + 相似度阈值滑块（`a-slider`）+ [检测] 按钮
- 候选列表：卡片式布局，每对候选显示：
  - 实体 A 名称 ← 相似度(%) → 实体 B 名称
  - 匹配类型标签（编辑距离 / 别名匹配）
  - [合并到 A] [合并到 B] [忽略] 按钮
- 合并确认 Modal：显示将迁移的关系数、将添加的别名

**Tab 5: 关系管理**
- 筛选：关系类型 + 来源/目标实体搜索
- 新建关系按钮 → Modal（来源实体搜索选择、目标实体搜索选择、关系类型下拉）
- 分页表格：来源名称、关系类型（`a-tag`）、目标名称、操作[删除]

**Tab 6: 重建 & 占位符**
- 上半部分——图谱重建：
  - [重建选定文档] 按钮 → Modal 选择文档（搜索 + 多选）
  - [重建全部图谱] 按钮 → `a-popconfirm` 确认
  - 重建任务进度卡片（`a-progress` 进度条 + 当前文档 + 状态）
- 下半部分——占位符管理：
  - 表格：占位符名称、引用方列表、操作
  - 操作列：[关联真实文档(`a-modal` 含搜索)] [删除]

#### F4. `frontend/src/components/admin/EntityDetailDrawer.vue`

实体详情抽屉组件（从实体管理 Tab 点击行展开）：
- 属性表格（`a-descriptions`）
- 关联文档列表
- 1-hop 邻域小图（复用 G6 渲染逻辑，简化版）

#### F5. `frontend/src/components/admin/RebuildProgressCard.vue`

重建任务进度卡片（轮询 rebuild-status 接口）：
- 状态：PENDING → PROCESSING → COMPLETED / FAILED
- 进度条 + 当前处理文档 + 已完成/总数

### 修改文件（3 个）

#### F6. `frontend/src/router/index.ts`

新增路由：
```typescript
{
  path: '/admin/graph',
  name: 'AdminGraphManager',
  component: () => import('@/views/admin/GraphManager.vue'),
  meta: { title: '图谱管理', menuKey: 'admin' },
}
```

#### F7. `frontend/src/views/admin/Dashboard.vue`

在管理后台首页增加入口卡片/链接，点击跳转到 `/admin/graph`：
- 在统计卡片行下方或旁边加一张 "图谱管理" 入口卡（`router-link`）

#### F8. `frontend/src/utils/graphLabels.ts`

类型映射改为动态：
- 导出 `loadTypesFromApi()` 函数，从 `GET /admin/graph/entity-types` 和 `/relationship-types` 加载
- 保留当前硬编码作为 fallback
- `GraphExplorer.vue` 在 mount 时调用一次加载最新类型

---

## 更新后的实施顺序

| 步骤 | 内容 | 文件 |
|------|------|------|
| 1 | Pydantic 模型（含类型定义模型） | `schemas/admin_graph.py` (新) |
| 2 | Neo4j 通用方法 + 类型动态化 + 修复 _reconstruct_content | `neo4j_client.py` + `graph_task.py` (改) |
| 3 | GraphAdminService — 类型管理 + 缓存 + 默认类型初始化 | `graph_admin_service.py` (新) |
| 4 | GraphAdminService — 统计 + 实体 CRUD | `graph_admin_service.py` (续) |
| 5 | Admin 路由 — 类型管理 + P0 接口 + 路由注册 | `admin_graph.py` (新) + `router.py` (改) |
| 6 | GraphAdminService — 去重合并 | `graph_admin_service.py` (续) |
| 7 | Rebuild Celery 任务 | `graph_task.py` (改) |
| 8 | Admin 路由 P1（去重+重建） | `admin_graph.py` (改) |
| 9 | GraphAdminService — 关系 + 占位符 | `graph_admin_service.py` (续) |
| 10 | Admin 路由 P2（关系+占位符） | `admin_graph.py` (改) |
| 11 | graph_builder.py 动态 prompt 适配 | `graph_builder.py` + `entity_extraction.py` (改) |
| 12 | 前端 API + 类型定义 | `api/admin-graph.ts` + `types/admin-graph.d.ts` (新) |
| 13 | 前端图谱管理主页面（Tab 1-3: 统计 + 实体 + 类型） | `views/admin/GraphManager.vue` (新) |
| 14 | 前端 Tab 4-6（去重 + 关系 + 重建） + 组件 | `GraphManager.vue` + 组件 (新) |
| 15 | 前端路由 + Dashboard 入口 + graphLabels 动态化 | `router` + `Dashboard.vue` + `graphLabels.ts` (改) |

---

## 验证方案

**后端**：
1. **类型管理**：创建新实体类型 "法规" → 用该类型创建实体 → 验证成功；删除有实例的类型 → 返回 400
2. **统计**：`/admin/graph/stats` 数值与 Neo4j Browser 一致
3. **CRUD**：创建→查询→编辑→删除循环；唯一约束冲突返回 409
4. **去重**：预设 "发改委" / "国家发展和改革委员会"，验证出现在候选中
5. **合并**：合并后关系迁移正确、别名已记录、从实体已删
6. **重建**：单文档重建验证旧图谱清除 + 新图谱生成
7. **关系**：手动添加/删除关系，Neo4j 数据正确
8. **占位符**：link 后验证 REFERENCES 关系迁移到真实文档
9. **动态 prompt**：新增实体类型后重建图谱，验证 LLM 可抽取新类型实体

**前端**：
10. **页面可达**：Dashboard → 点击图谱管理卡片 → 跳转 `/admin/graph` → 6 个 Tab 正常切换
11. **统计 Tab**：数据加载显示，刷新按钮正常
12. **实体 CRUD**：筛选/搜索/分页 → 新建 → 编辑（含重命名）→ 删除全流程
13. **类型定义**：新建/重命名/删除实体类型和关系类型；重命名后刷新列表显示新名
14. **去重合并**：调整阈值检测 → 候选列表显示 → 点击合并 → 确认 → 列表刷新
15. **重建**：选择文档部分重建 → 进度轮询 → 完成；全量重建 → 确认 → 进度追踪
16. **占位符**：关联真实文档 → 占位符消失；删除占位符 → 列表刷新
