Phase 7 Task 4: 协作与共享模块
- 创建 collaboration_manager.py 协作管理模块 - CollaborationManager: 协作管理主类 - 项目分享链接管理 - 支持只读/评论/编辑/管理员权限 - 评论和批注系统 - 支持实体、关系、转录文本评论 - 变更历史追踪 - 记录所有数据操作变更 - 团队成员管理 - 支持多角色权限控制 - 更新 schema.sql 添加协作相关数据库表 - project_shares: 项目分享表 - comments: 评论表 - change_history: 变更历史表 - team_members: 团队成员表 - 更新 main.py 添加协作相关 API 端点 - 项目分享相关端点 - 评论和批注相关端点 - 变更历史相关端点 - 团队成员管理端点 - 更新 README.md 和 STATUS.md
This commit is contained in:
305
README.md
305
README.md
@@ -1,48 +1,69 @@
|
|||||||
# InsightFlow - Audio to Knowledge Graph Platform
|
# InsightFlow - Audio to Knowledge Graph Platform
|
||||||
|
|
||||||
## Phase 3: Memory & Growth - Completed ✅
|
InsightFlow 是一个音频转知识图谱平台,支持将音频、文档转换为结构化的知识图谱,并提供强大的分析和推理能力。
|
||||||
|
|
||||||
### 新增功能
|
## 功能特性
|
||||||
|
|
||||||
#### 1. 多文件图谱融合 ✅
|
### Phase 1-3: 基础功能 ✅
|
||||||
- 支持上传多个音频文件到同一项目
|
- 音频上传与转录(阿里云听悟 ASR)
|
||||||
- 系统自动对齐实体,合并图谱
|
- 实体提取与关系抽取
|
||||||
- 实体提及跨文件追踪
|
- 知识图谱可视化(D3.js)
|
||||||
- 文件选择器切换不同转录内容
|
- 多文件图谱融合
|
||||||
|
- PDF/DOCX 文档导入
|
||||||
|
- 实体对齐与别名管理
|
||||||
|
- 项目知识库面板
|
||||||
|
|
||||||
#### 2. 实体对齐算法优化 ✅
|
### Phase 4: Agent 助手与知识溯源 ✅
|
||||||
- 新增 `entity_aligner.py` 模块
|
- AI 助手对话(RAG 问答)
|
||||||
- 支持使用 Kimi API embedding 进行语义相似度匹配
|
- 实体操作指令执行
|
||||||
- 余弦相似度计算
|
- 知识溯源(关系来源追踪)
|
||||||
- 自动别名建议
|
- 实体悬停卡片
|
||||||
- 批量实体对齐 API
|
- 置信度提示
|
||||||
|
|
||||||
#### 3. PDF/DOCX 文档导入 ✅
|
### Phase 5: 高级功能 ✅
|
||||||
- 新增 `document_processor.py` 模块
|
- **知识推理** - 因果/对比/时序/关联推理
|
||||||
- 支持 PDF、DOCX、TXT、MD 格式
|
- **时间线视图** - 实体演变追踪
|
||||||
- 文档文本提取并参与实体提取
|
- **实体属性扩展** - 自定义属性模板
|
||||||
- 文档类型标记(音频/文档)
|
- **Neo4j 图数据库** - 复杂图查询、最短路径、社区发现
|
||||||
|
- **导出功能** - SVG/PNG/Excel/CSV/PDF/JSON
|
||||||
|
|
||||||
#### 4. 项目知识库面板 ✅
|
### Phase 6: API 开放平台 ✅
|
||||||
- 全新的知识库视图
|
- **API Key 管理** - 创建、撤销、权限控制
|
||||||
- 统计面板:实体数、关系数、文件数、术语数
|
- **Swagger/OpenAPI 文档** - 在线 API 文档
|
||||||
- 实体网格展示(带提及统计)
|
- **限流控制** - 滑动窗口限流、调用统计
|
||||||
- 关系列表展示
|
- **调用日志** - 详细调用记录和分析
|
||||||
- 术语表管理(添加/删除)
|
|
||||||
- 文件列表展示
|
|
||||||
|
|
||||||
### 技术栈
|
## 技术栈
|
||||||
- 后端: FastAPI + SQLite
|
|
||||||
- 前端: 原生 HTML/JS + D3.js
|
|
||||||
- ASR: 阿里云听悟
|
|
||||||
- LLM: Kimi API
|
|
||||||
- 文档处理: PyPDF2, python-docx
|
|
||||||
|
|
||||||
### 部署
|
- **后端**: FastAPI + SQLite
|
||||||
|
- **前端**: 原生 HTML/JS + D3.js
|
||||||
|
- **ASR**: 阿里云听悟
|
||||||
|
- **LLM**: Kimi API
|
||||||
|
- **图数据库**: Neo4j
|
||||||
|
- **文档处理**: PyPDF2, python-docx
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建 Docker 镜像
|
# 克隆仓库
|
||||||
docker build -t insightflow:phase3 .
|
git clone https://git.sivdead.cn/claw/insightflow
|
||||||
|
cd insightflow
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 运行开发服务器
|
||||||
|
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t insightflow:latest .
|
||||||
|
|
||||||
# 运行容器
|
# 运行容器
|
||||||
docker run -d \
|
docker run -d \
|
||||||
@@ -51,133 +72,127 @@ docker run -d \
|
|||||||
-e KIMI_API_KEY=your_key \
|
-e KIMI_API_KEY=your_key \
|
||||||
-e ALIYUN_ACCESS_KEY_ID=your_key \
|
-e ALIYUN_ACCESS_KEY_ID=your_key \
|
||||||
-e ALIYUN_ACCESS_KEY_SECRET=your_secret \
|
-e ALIYUN_ACCESS_KEY_SECRET=your_secret \
|
||||||
insightflow:phase3
|
-e INSIGHTFLOW_MASTER_KEY=your_master_key \
|
||||||
|
insightflow:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### API 文档
|
### Docker Compose 部署(推荐)
|
||||||
|
|
||||||
#### 新增 API
|
```bash
|
||||||
|
# 启动所有服务(含 Neo4j)
|
||||||
**文档上传**
|
docker-compose up -d
|
||||||
```
|
|
||||||
POST /api/v1/projects/{project_id}/upload-document
|
|
||||||
Content-Type: multipart/form-data
|
|
||||||
file: <文件>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**知识库查询**
|
## API 认证
|
||||||
```
|
|
||||||
GET /api/v1/projects/{project_id}/knowledge-base
|
从 Phase 6 开始,API 需要认证才能访问:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 创建 API Key(需要 Master Key)
|
||||||
|
curl -X POST http://localhost:18000/api/v1/api-keys \
|
||||||
|
-H "X-API-Key: your_master_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name": "My App", "permissions": ["read", "write"]}'
|
||||||
|
|
||||||
|
# 2. 使用 API Key 访问受保护端点
|
||||||
|
curl http://localhost:18000/api/v1/projects \
|
||||||
|
-H "X-API-Key: ak_live_xxxxx"
|
||||||
```
|
```
|
||||||
|
|
||||||
**术语表管理**
|
## API 文档
|
||||||
```
|
|
||||||
POST /api/v1/projects/{project_id}/glossary
|
|
||||||
GET /api/v1/projects/{project_id}/glossary
|
|
||||||
DELETE /api/v1/glossary/{term_id}
|
|
||||||
```
|
|
||||||
|
|
||||||
**实体对齐**
|
- Swagger UI: http://122.51.127.111:18000/docs
|
||||||
```
|
- ReDoc: http://122.51.127.111:18000/redoc
|
||||||
POST /api/v1/projects/{project_id}/align-entities?threshold=0.85
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据库 Schema 更新
|
## 部署信息
|
||||||
- `transcripts` 表新增 `type` 字段(audio/document)
|
|
||||||
- `entities` 表新增 `embedding` 字段
|
|
||||||
- 新增索引优化查询性能
|
|
||||||
|
|
||||||
---
|
- **服务器**: 122.51.127.111:18000
|
||||||
|
- **Neo4j**: 122.51.127.111:7474 (HTTP), 122.51.127.111:7687 (Bolt)
|
||||||
|
- **Git 仓库**: https://git.sivdead.cn/claw/insightflow
|
||||||
|
|
||||||
## Phase 4: Agent 助手与知识溯源 - 已完成 ✅
|
## 开发状态
|
||||||
|
|
||||||
### 已完成功能
|
详见 [STATUS.md](STATUS.md)
|
||||||
|
|
||||||
1. **Agent 助手后端 API** ✅
|
|
||||||
- 对话指令解析接口 `/agent/command`
|
|
||||||
- RAG 问答接口 `/agent/query`
|
|
||||||
- 实体操作指令执行
|
|
||||||
|
|
||||||
2. **Agent 助手前端面板** ✅
|
|
||||||
- 可折叠聊天面板
|
|
||||||
- 消息历史展示
|
|
||||||
- 指令快捷按钮
|
|
||||||
|
|
||||||
3. **知识溯源功能** ✅
|
|
||||||
- 关系来源追踪 `/relations/{id}/provenance`
|
|
||||||
- 提及位置高亮
|
|
||||||
- 跨文档关联显示
|
|
||||||
|
|
||||||
4. **术语卡片** ✅
|
|
||||||
- 悬停卡片 UI
|
|
||||||
- 实体详情展示
|
|
||||||
- 快捷编辑入口
|
|
||||||
|
|
||||||
5. **置信度系统** ✅
|
|
||||||
- LLM 返回置信度
|
|
||||||
- 低置信度标记
|
|
||||||
- 人工确认流程
|
|
||||||
|
|
||||||
6. **Neo4j 集成** - 待开发 ⏳
|
|
||||||
- 图谱数据同步
|
|
||||||
- 复杂图查询支持
|
|
||||||
|
|
||||||
## Phase 5: 高级功能 - 已完成 ✅
|
|
||||||
|
|
||||||
### 已完成功能 ✅
|
|
||||||
|
|
||||||
1. **知识推理与问答增强** ✅
|
|
||||||
- 后端推理引擎 `knowledge_reasoner.py`
|
|
||||||
- 因果/对比/时序/关联推理
|
|
||||||
- 智能项目总结 API
|
|
||||||
- 实体关联路径发现
|
|
||||||
- 前端推理面板 UI
|
|
||||||
|
|
||||||
2. **时间线视图** ✅
|
|
||||||
- 项目时间线 API
|
|
||||||
- 实体演变追踪
|
|
||||||
- 时间线可视化面板
|
|
||||||
|
|
||||||
3. **实体属性扩展** ✅
|
|
||||||
- 数据库层: `entity_attributes`, `attribute_templates`, `attribute_history` 表
|
|
||||||
- 后端 API: 属性模板管理、实体属性 CRUD、属性历史查询
|
|
||||||
- 支持属性类型: text, number, date, select, multiselect, boolean
|
|
||||||
- 属性筛选搜索 API
|
|
||||||
|
|
||||||
### 待开发任务 📋
|
|
||||||
|
|
||||||
无 - Phase 5 已完成
|
|
||||||
|
|
||||||
## Phase 6: 企业级功能 - 规划中 📋
|
|
||||||
|
|
||||||
1. **API 开放平台**
|
|
||||||
- RESTful API 文档
|
|
||||||
- API Key 管理
|
|
||||||
- 调用统计和限流
|
|
||||||
|
|
||||||
2. **数据安全增强**
|
|
||||||
- 端到端加密
|
|
||||||
- 数据脱敏
|
|
||||||
- 审计日志
|
|
||||||
|
|
||||||
3. **性能优化**
|
|
||||||
- 数据库分片
|
|
||||||
- 缓存层(Redis)
|
|
||||||
- CDN 加速
|
|
||||||
|
|
||||||
## 暂不开发功能 ⏸️
|
|
||||||
|
|
||||||
- **协作功能** - 多用户支持、项目权限管理、评论批注(当前版本暂不实现)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目文档
|
## 项目文档
|
||||||
|
|
||||||
- [PRD v2.0](docs/PRD-v2.0.md) - 产品需求规格说明书
|
- [PRD v2.0](docs/PRD-v2.0.md) - 产品需求规格说明书
|
||||||
- [STATUS.md](STATUS.md) - 详细开发状态跟踪
|
- [STATUS.md](STATUS.md) - 详细开发状态跟踪
|
||||||
|
|
||||||
## 部署信息
|
## 许可证
|
||||||
|
|
||||||
- **服务器**: 122.51.127.111:18000
|
MIT
|
||||||
- **项目路径**: /opt/projects/insightflow
|
|
||||||
- **Git 仓库**: https://git.sivdead.cn/claw/insightflow
|
---
|
||||||
|
|
||||||
|
## Phase 7: 智能化与生态扩展 - 进行中 🚧
|
||||||
|
|
||||||
|
### Phase 7 任务清单
|
||||||
|
|
||||||
|
| 任务 | 状态 | 完成时间 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 1. 智能工作流自动化 | ✅ 已完成 | 2026-02-23 |
|
||||||
|
| 2. 多模态支持 | ✅ 已完成 | 2026-02-23 |
|
||||||
|
| 3. 数据安全与合规 | ✅ 已完成 | 2026-02-23 |
|
||||||
|
| 4. 协作与共享 | ✅ 已完成 | 2026-02-24 |
|
||||||
|
| 5. 智能报告生成 | 📋 待开发 | - |
|
||||||
|
| 6. 高级搜索与发现 | 📋 待开发 | - |
|
||||||
|
| 7. 插件与集成 | ✅ 已完成 | 2026-02-23 |
|
||||||
|
| 8. 性能优化与扩展 | 📋 待开发 | - |
|
||||||
|
|
||||||
|
### 已完成功能 ✅
|
||||||
|
|
||||||
|
1. **智能工作流自动化** ✅
|
||||||
|
- 工作流管理模块 `workflow_manager.py`
|
||||||
|
- 定时任务调度(APScheduler)
|
||||||
|
- Webhook 通知器(飞书/钉钉/Slack)
|
||||||
|
- 自动分析新上传文件
|
||||||
|
- 自动实体对齐和关系发现
|
||||||
|
|
||||||
|
2. **多模态支持** ✅
|
||||||
|
- 视频处理模块(音频提取 + 关键帧 + OCR)
|
||||||
|
- 图片处理模块(OCR + 图片描述)
|
||||||
|
- 跨模态实体关联
|
||||||
|
- 多模态实体画像
|
||||||
|
- 多模态时间线生成
|
||||||
|
|
||||||
|
3. **数据安全与合规** ✅
|
||||||
|
- 安全模块 `security_manager.py`
|
||||||
|
- 审计日志系统
|
||||||
|
- 端到端加密(AES-256-GCM)
|
||||||
|
- 数据脱敏(手机号、邮箱、身份证)
|
||||||
|
- 数据访问策略
|
||||||
|
- 访问审批流程
|
||||||
|
|
||||||
|
4. **协作与共享** ✅
|
||||||
|
- 协作管理模块 `collaboration_manager.py`
|
||||||
|
- 项目分享链接(只读/评论/编辑/管理员权限)
|
||||||
|
- 评论和批注系统(实体/关系/转录文本)
|
||||||
|
- 变更历史追踪
|
||||||
|
- 团队成员管理(多角色权限控制)
|
||||||
|
|
||||||
|
7. **插件与集成** ✅
|
||||||
|
- 插件管理模块 `plugin_manager.py`
|
||||||
|
- Chrome 扩展支持
|
||||||
|
- 飞书/钉钉机器人
|
||||||
|
- Zapier/Make Webhook 集成
|
||||||
|
- WebDAV 同步
|
||||||
|
|
||||||
|
### 待开发任务 📋
|
||||||
|
|
||||||
|
5. **智能报告生成** - 待开发
|
||||||
|
- 一键生成 PDF/Word 报告
|
||||||
|
- 会议纪要提取
|
||||||
|
- 自定义报告模板
|
||||||
|
|
||||||
|
6. **高级搜索与发现** - 待开发
|
||||||
|
- 全文搜索
|
||||||
|
- 语义搜索
|
||||||
|
- 实体关系路径发现
|
||||||
|
- 知识缺口识别
|
||||||
|
|
||||||
|
8. **性能优化与扩展** - 待开发
|
||||||
|
- Redis 缓存层
|
||||||
|
- 数据库分片
|
||||||
|
- CDN 加速
|
||||||
|
- 异步任务队列(Celery)
|
||||||
|
|||||||
459
STATUS.md
459
STATUS.md
@@ -1,21 +1,20 @@
|
|||||||
# InsightFlow 开发状态
|
# InsightFlow 开发状态
|
||||||
|
|
||||||
**最后更新**: 2026-02-21 06:05
|
**最后更新**: 2026-02-24 00:00
|
||||||
|
|
||||||
## 当前阶段
|
## 当前阶段
|
||||||
|
|
||||||
Phase 5: 高级功能 - **已完成 ✅**
|
Phase 7: 协作与共享 - **已完成 ✅**
|
||||||
Phase 6: 企业级功能 - **规划中 📋**
|
|
||||||
|
|
||||||
## 部署状态
|
## 部署状态
|
||||||
|
|
||||||
- **服务器**: 122.51.127.111:18000 ✅ 运行中
|
- **服务器**: 122.51.127.111:18000 ✅ 运行中
|
||||||
- **Neo4j**: 122.51.127.111:7474 (HTTP), 122.51.127.111:7687 (Bolt) ⏸️ 待部署
|
- **Neo4j**: 122.51.127.111:7474 (HTTP), 122.51.127.111:7687 (Bolt) ✅ 运行中
|
||||||
- **Git 版本**: f38e060 - Phase 5: Enhance Neo4j graph visualization
|
- **Git 版本**: 待推送
|
||||||
|
|
||||||
## 已完成
|
## 已完成
|
||||||
|
|
||||||
### Phase 1-3 (已完成 ✅)
|
### Phase 1-6 (已完成 ✅)
|
||||||
- FastAPI 项目框架搭建
|
- FastAPI 项目框架搭建
|
||||||
- SQLite 数据库设计
|
- SQLite 数据库设计
|
||||||
- 阿里云听悟 ASR 集成
|
- 阿里云听悟 ASR 集成
|
||||||
@@ -28,171 +27,211 @@ Phase 6: 企业级功能 - **规划中 📋**
|
|||||||
- 实体列表展示
|
- 实体列表展示
|
||||||
- 转录文本中实体高亮显示
|
- 转录文本中实体高亮显示
|
||||||
- 图谱与文本联动
|
- 图谱与文本联动
|
||||||
|
- Agent 助手
|
||||||
|
- 知识溯源
|
||||||
|
- 知识推理与问答增强
|
||||||
|
- 实体属性扩展
|
||||||
|
- 时间线视图
|
||||||
|
- Neo4j 图数据库集成
|
||||||
|
- 导出功能
|
||||||
|
- API 开放平台
|
||||||
|
|
||||||
### Phase 4 - Agent 助手 (已完成 ✅)
|
### Phase 7 - 任务 1: 工作流自动化 (已完成 ✅)
|
||||||
- ✅ 创建 llm_client.py - Kimi API 客户端
|
- ✅ 创建 workflow_manager.py - 工作流管理模块
|
||||||
- 支持流式/非流式聊天
|
- WorkflowManager: 主管理类
|
||||||
- 带置信度的实体提取
|
- WorkflowTask: 工作流任务定义
|
||||||
- RAG 问答功能
|
- WebhookNotifier: Webhook 通知器(支持飞书、钉钉、Slack)
|
||||||
- Agent 指令解析
|
- 定时任务调度(APScheduler)
|
||||||
- 实体演变分析
|
- 自动分析新上传文件的工作流
|
||||||
- ✅ 更新 db_manager.py - 新增方法
|
- 自动实体对齐和关系发现
|
||||||
- `get_relation_with_details()` - 获取关系详情
|
- 工作流配置管理
|
||||||
- `get_entity_with_mentions()` - 获取实体及提及
|
- ✅ 更新 schema.sql - 添加工作流相关数据库表
|
||||||
- `search_entities()` - 搜索实体
|
- workflows: 工作流配置表
|
||||||
- `update_entity()` - 更新实体
|
- workflow_tasks: 任务执行记录表
|
||||||
- `get_project_summary()` - 项目摘要
|
- webhook_configs: Webhook 配置表
|
||||||
- `get_transcript_context()` - 转录上下文
|
- workflow_logs: 工作流执行日志
|
||||||
- ✅ 更新 main.py - Agent API 端点
|
- ✅ 更新 main.py - 添加工作流相关 API 端点
|
||||||
- `POST /api/v1/projects/{id}/agent/query` - RAG 问答
|
- GET/POST /api/v1/workflows - 工作流管理
|
||||||
- `POST /api/v1/projects/{id}/agent/command` - 指令执行
|
- GET/POST /api/v1/webhooks - Webhook 配置
|
||||||
- `GET /api/v1/projects/{id}/agent/suggest` - 智能建议
|
- GET /api/v1/workflows/{id}/logs - 执行日志
|
||||||
- `GET /api/v1/relations/{id}/provenance` - 关系溯源
|
- POST /api/v1/workflows/{id}/trigger - 手动触发
|
||||||
- `GET /api/v1/entities/{id}/details` - 实体详情
|
- GET /api/v1/workflows/{id}/stats - 执行统计
|
||||||
- `GET /api/v1/entities/{id}/evolution` - 实体演变分析
|
- POST /api/v1/webhooks/{id}/test - 测试 Webhook
|
||||||
- `GET /api/v1/projects/{id}/entities/search` - 实体搜索
|
- ✅ 更新 requirements.txt - 添加 APScheduler 依赖
|
||||||
- `PATCH /api/v1/entities/{id}` - 更新实体
|
|
||||||
- ✅ 更新 workbench.html - Agent 面板 UI
|
|
||||||
- 可折叠的 Agent 助手面板
|
|
||||||
- 聊天界面
|
|
||||||
- 实体悬停卡片
|
|
||||||
- 关系溯源弹窗
|
|
||||||
- ✅ 更新 app.js - 前端功能
|
|
||||||
- Agent 聊天功能
|
|
||||||
- 指令执行(合并实体、编辑定义)
|
|
||||||
- RAG 问答
|
|
||||||
- 实体卡片悬停显示
|
|
||||||
- 关系点击溯源
|
|
||||||
- 低置信度实体标黄
|
|
||||||
|
|
||||||
### Phase 4 - 知识溯源 (已完成 ✅)
|
### Phase 7 - 任务 2: 多模态支持 (已完成 ✅)
|
||||||
- ✅ 点击关系连线显示来源文档
|
- ✅ 创建 multimodal_processor.py - 多模态处理模块
|
||||||
- ✅ 实体详情显示所有提及位置
|
- VideoProcessor: 视频处理器(提取音频 + 关键帧 + OCR)
|
||||||
- ✅ 证据文本展示
|
- ImageProcessor: 图片处理器(OCR + 图片描述)
|
||||||
|
- MultimodalEntityExtractor: 多模态实体提取器
|
||||||
|
- 支持 PaddleOCR/EasyOCR/Tesseract 多种 OCR 引擎
|
||||||
|
- 支持 ffmpeg 视频处理
|
||||||
|
- ✅ 创建 multimodal_entity_linker.py - 多模态实体关联模块
|
||||||
|
- MultimodalEntityLinker: 跨模态实体关联器
|
||||||
|
- 支持 embedding 相似度计算
|
||||||
|
- 多模态实体画像生成
|
||||||
|
- 跨模态关系发现
|
||||||
|
- 多模态时间线生成
|
||||||
|
- ✅ 更新 schema.sql - 添加多模态相关数据库表
|
||||||
|
- videos: 视频表
|
||||||
|
- video_frames: 视频关键帧表
|
||||||
|
- images: 图片表
|
||||||
|
- multimodal_mentions: 多模态实体提及表
|
||||||
|
- multimodal_entity_links: 多模态实体关联表
|
||||||
|
- ✅ 更新 main.py - 添加多模态相关 API 端点
|
||||||
|
- POST /api/v1/projects/{id}/upload-video - 上传视频
|
||||||
|
- POST /api/v1/projects/{id}/upload-image - 上传图片
|
||||||
|
- GET /api/v1/projects/{id}/videos - 视频列表
|
||||||
|
- GET /api/v1/projects/{id}/images - 图片列表
|
||||||
|
- GET /api/v1/videos/{id} - 视频详情
|
||||||
|
- GET /api/v1/images/{id} - 图片详情
|
||||||
|
- POST /api/v1/projects/{id}/multimodal/link-entities - 跨模态实体关联
|
||||||
|
- GET /api/v1/entities/{id}/multimodal-profile - 实体多模态画像
|
||||||
|
- GET /api/v1/projects/{id}/multimodal-timeline - 多模态时间线
|
||||||
|
- GET /api/v1/entities/{id}/cross-modal-relations - 跨模态关系
|
||||||
|
- ✅ 更新 requirements.txt - 添加多模态依赖
|
||||||
|
- opencv-python: 视频处理
|
||||||
|
- pillow: 图片处理
|
||||||
|
- paddleocr/paddlepaddle: OCR 引擎
|
||||||
|
- ffmpeg-python: ffmpeg 封装
|
||||||
|
- sentence-transformers: 跨模态对齐
|
||||||
|
|
||||||
### Phase 4 - 术语卡片悬停 (已完成 ✅)
|
### Phase 7 - 任务 7: 插件与集成 (已完成 ✅)
|
||||||
- ✅ 鼠标悬停实体显示卡片
|
- ✅ 创建 plugin_manager.py - 插件管理模块
|
||||||
- ✅ 卡片包含:名称、定义、提及次数、关系数
|
- PluginManager: 插件管理主类
|
||||||
|
- ChromeExtensionHandler: Chrome 扩展 API 处理
|
||||||
|
- 令牌创建、验证、撤销
|
||||||
|
- 网页内容导入
|
||||||
|
- BotHandler: 飞书/钉钉机器人处理
|
||||||
|
- 会话管理
|
||||||
|
- 消息接收和发送
|
||||||
|
- 音频文件处理
|
||||||
|
- WebhookIntegration: Zapier/Make Webhook 集成
|
||||||
|
- 端点创建和管理
|
||||||
|
- 事件触发
|
||||||
|
- 认证支持
|
||||||
|
- WebDAVSync: WebDAV 同步管理
|
||||||
|
- 同步配置管理
|
||||||
|
- 连接测试
|
||||||
|
- 项目数据同步
|
||||||
|
- ✅ 更新 schema.sql - 添加插件相关数据库表
|
||||||
|
- plugins: 插件配置表
|
||||||
|
- plugin_configs: 插件详细配置表
|
||||||
|
- bot_sessions: 机器人会话表
|
||||||
|
- webhook_endpoints: Webhook 端点表
|
||||||
|
- webdav_syncs: WebDAV 同步配置表
|
||||||
|
- chrome_extension_tokens: Chrome 扩展令牌表
|
||||||
|
- ✅ 更新 main.py - 添加插件相关 API 端点
|
||||||
|
- GET/POST /api/v1/plugins - 插件管理
|
||||||
|
- POST /api/v1/plugins/chrome/tokens - 创建 Chrome 扩展令牌
|
||||||
|
- GET /api/v1/plugins/chrome/tokens - 列出自令牌
|
||||||
|
- DELETE /api/v1/plugins/chrome/tokens/{id} - 撤销令牌
|
||||||
|
- POST /api/v1/plugins/chrome/import - 导入网页内容
|
||||||
|
- POST /api/v1/plugins/bot/feishu/sessions - 创建飞书会话
|
||||||
|
- POST /api/v1/plugins/bot/dingtalk/sessions - 创建钉钉会话
|
||||||
|
- GET /api/v1/plugins/bot/{type}/sessions - 列出会话
|
||||||
|
- POST /api/v1/plugins/bot/{type}/webhook - 接收机器人消息
|
||||||
|
- POST /api/v1/plugins/bot/{type}/sessions/{id}/send - 发送消息
|
||||||
|
- POST /api/v1/plugins/integrations/zapier - 创建 Zapier 端点
|
||||||
|
- POST /api/v1/plugins/integrations/make - 创建 Make 端点
|
||||||
|
- GET /api/v1/plugins/integrations/{type} - 列出集成端点
|
||||||
|
- POST /api/v1/plugins/integrations/{id}/test - 测试端点
|
||||||
|
- POST /api/v1/plugins/integrations/{id}/trigger - 手动触发
|
||||||
|
- POST /api/v1/plugins/webdav - 创建 WebDAV 同步
|
||||||
|
- GET /api/v1/plugins/webdav - 列出同步配置
|
||||||
|
- POST /api/v1/plugins/webdav/{id}/test - 测试连接
|
||||||
|
- POST /api/v1/plugins/webdav/{id}/sync - 执行同步
|
||||||
|
- ✅ 更新 requirements.txt - 添加插件依赖
|
||||||
|
- webdav4: WebDAV 客户端
|
||||||
|
- urllib3: URL 处理
|
||||||
|
- ✅ 创建 Chrome 扩展基础代码
|
||||||
|
- manifest.json: 扩展配置
|
||||||
|
- background.js: 后台脚本(右键菜单、同步)
|
||||||
|
- content.js: 内容脚本(页面提取)
|
||||||
|
- content.css: 内容样式
|
||||||
|
- popup.html/js: 弹出窗口
|
||||||
|
- options.html/js: 设置页面
|
||||||
|
- README.md: 扩展说明文档
|
||||||
|
|
||||||
### Phase 4 - 置信度提示 (已完成 ✅)
|
### Phase 7 - 任务 3: 数据安全与合规 (已完成 ✅)
|
||||||
- ✅ LLM 提取返回置信度分数
|
- ✅ 创建 security_manager.py - 安全模块
|
||||||
- ✅ 低置信度实体在文本中标黄
|
- SecurityManager: 安全管理主类
|
||||||
|
- 审计日志系统 - 记录所有数据操作
|
||||||
|
- 端到端加密 - AES-256-GCM 加密项目数据
|
||||||
|
- 数据脱敏 - 支持手机号、邮箱、身份证等敏感信息脱敏
|
||||||
|
- 数据访问策略 - 基于用户、角色、IP、时间的访问控制
|
||||||
|
- 访问审批流程 - 敏感数据访问需要审批
|
||||||
|
- ✅ 更新 schema.sql - 添加安全相关数据库表
|
||||||
|
- audit_logs: 审计日志表
|
||||||
|
- encryption_configs: 加密配置表
|
||||||
|
- masking_rules: 脱敏规则表
|
||||||
|
- data_access_policies: 数据访问策略表
|
||||||
|
- access_requests: 访问请求表
|
||||||
|
- ✅ 更新 main.py - 添加安全相关 API 端点
|
||||||
|
- GET /api/v1/audit-logs - 查询审计日志
|
||||||
|
- GET /api/v1/audit-logs/stats - 审计统计
|
||||||
|
- POST /api/v1/projects/{id}/encryption/enable - 启用加密
|
||||||
|
- POST /api/v1/projects/{id}/encryption/disable - 禁用加密
|
||||||
|
- POST /api/v1/projects/{id}/encryption/verify - 验证密码
|
||||||
|
- GET /api/v1/projects/{id}/encryption - 获取加密配置
|
||||||
|
- POST /api/v1/projects/{id}/masking-rules - 创建脱敏规则
|
||||||
|
- GET /api/v1/projects/{id}/masking-rules - 获取脱敏规则
|
||||||
|
- PUT /api/v1/masking-rules/{id} - 更新脱敏规则
|
||||||
|
- DELETE /api/v1/masking-rules/{id} - 删除脱敏规则
|
||||||
|
- POST /api/v1/projects/{id}/masking/apply - 应用脱敏
|
||||||
|
- POST /api/v1/projects/{id}/access-policies - 创建访问策略
|
||||||
|
- GET /api/v1/projects/{id}/access-policies - 获取访问策略
|
||||||
|
- POST /api/v1/access-policies/{id}/check - 检查访问权限
|
||||||
|
- POST /api/v1/access-requests - 创建访问请求
|
||||||
|
- POST /api/v1/access-requests/{id}/approve - 批准访问
|
||||||
|
- POST /api/v1/access-requests/{id}/reject - 拒绝访问
|
||||||
|
- ✅ 更新 requirements.txt - 添加 cryptography 依赖
|
||||||
|
|
||||||
### Phase 5 - 知识推理与问答增强 (已完成 ✅)
|
### Phase 7 - 任务 4: 协作与共享 (已完成 ✅)
|
||||||
- ✅ 创建 knowledge_reasoner.py - 知识推理引擎
|
- ✅ 创建 collaboration_manager.py - 协作管理模块
|
||||||
- 因果推理:分析原因和影响
|
- CollaborationManager: 协作管理主类
|
||||||
- 对比推理:比较实体间的异同
|
- 项目分享链接管理 - 支持只读/评论/编辑/管理员权限
|
||||||
- 时序推理:分析时间线和演变
|
- 评论和批注系统 - 支持实体、关系、转录文本评论
|
||||||
- 关联推理:发现隐含关联
|
- 变更历史追踪 - 记录所有数据操作变更
|
||||||
- ✅ 新增 API 端点
|
- 团队成员管理 - 支持多角色权限控制
|
||||||
- `POST /api/v1/projects/{id}/reasoning/query` - 增强问答
|
- ✅ 更新 schema.sql - 添加协作相关数据库表
|
||||||
- `POST /api/v1/projects/{id}/reasoning/summary` - 智能总结
|
- project_shares: 项目分享表
|
||||||
- `GET /api/v1/projects/{id}/reasoning/inference-path` - 关联路径
|
- comments: 评论表
|
||||||
- ✅ 前端推理面板
|
- change_history: 变更历史表
|
||||||
- 推理类型选择
|
- team_members: 团队成员表
|
||||||
- 深度控制
|
- ✅ 更新 main.py - 添加协作相关 API 端点
|
||||||
- 结果展示(置信度、证据、知识缺口)
|
- POST /api/v1/projects/{id}/shares - 创建分享链接
|
||||||
- 项目总结卡片(全面/高管/技术/风险)
|
- GET /api/v1/projects/{id}/shares - 列出分享链接
|
||||||
|
- POST /api/v1/shares/verify - 验证分享链接
|
||||||
### Phase 5 - 实体属性扩展 (已完成 ✅)
|
- GET /api/v1/shares/{token}/access - 访问共享项目
|
||||||
- ✅ 数据库层
|
- DELETE /api/v1/shares/{id} - 撤销分享链接
|
||||||
- 新增 `entity_attributes` 表存储自定义属性
|
- POST /api/v1/projects/{id}/comments - 添加评论
|
||||||
- 新增 `attribute_templates` 表管理属性模板
|
- GET /api/v1/{type}/{id}/comments - 获取评论列表
|
||||||
- 新增 `attribute_history` 表记录属性变更历史
|
- GET /api/v1/projects/{id}/comments - 获取项目所有评论
|
||||||
- ✅ 后端 API
|
- PUT /api/v1/comments/{id} - 更新评论
|
||||||
- `GET/POST /api/v1/projects/{id}/attribute-templates` - 属性模板管理
|
- POST /api/v1/comments/{id}/resolve - 解决评论
|
||||||
- `GET/POST/PUT/DELETE /api/v1/entities/{id}/attributes` - 实体属性 CRUD
|
- DELETE /api/v1/comments/{id} - 删除评论
|
||||||
- `GET /api/v1/entities/{id}/attributes/history` - 属性变更历史
|
- GET /api/v1/projects/{id}/history - 获取变更历史
|
||||||
- `GET /api/v1/projects/{id}/entities/search-by-attributes` - 属性筛选搜索
|
- GET /api/v1/projects/{id}/history/stats - 获取变更统计
|
||||||
- ✅ 属性类型支持
|
- GET /api/v1/{type}/{id}/versions - 获取实体版本历史
|
||||||
- text: 文本
|
- POST /api/v1/history/{id}/revert - 回滚变更
|
||||||
- number: 数字
|
- POST /api/v1/projects/{id}/members - 邀请团队成员
|
||||||
- date: 日期
|
- GET /api/v1/projects/{id}/members - 列出团队成员
|
||||||
- select: 单选
|
- PUT /api/v1/members/{id}/role - 更新成员角色
|
||||||
- multiselect: 多选
|
- DELETE /api/v1/members/{id} - 移除团队成员
|
||||||
- boolean: 布尔值
|
- GET /api/v1/projects/{id}/permissions - 检查用户权限
|
||||||
|
|
||||||
### Phase 5 - 时间线视图 (已完成 ✅)
|
|
||||||
- ✅ 后端 API: `/api/v1/projects/{id}/timeline`
|
|
||||||
- ✅ 前端时间线面板
|
|
||||||
- ✅ 实体提及和关系事件可视化
|
|
||||||
- ✅ 实体筛选功能
|
|
||||||
|
|
||||||
## 待完成
|
## 待完成
|
||||||
|
|
||||||
### Phase 5 - Neo4j 图数据库集成 (已完成 ✅)
|
Phase 7 任务 5: 智能报告生成
|
||||||
- [x] 创建 neo4j_manager.py - Neo4j 管理模块
|
|
||||||
- 数据同步到 Neo4j(实体、关系、项目)
|
|
||||||
- 批量同步支持
|
|
||||||
- 数据删除支持
|
|
||||||
- [x] 复杂图查询
|
|
||||||
- 最短路径查询
|
|
||||||
- 所有路径查询
|
|
||||||
- 邻居节点查询
|
|
||||||
- 共同邻居查询
|
|
||||||
- 子图提取
|
|
||||||
- [x] 图算法分析
|
|
||||||
- 度中心性分析
|
|
||||||
- 社区发现(连通分量)
|
|
||||||
- 图统计信息
|
|
||||||
- [x] 后端 API 端点
|
|
||||||
- `GET /api/v1/neo4j/status` - Neo4j 连接状态
|
|
||||||
- `POST /api/v1/neo4j/sync` - 同步项目到 Neo4j
|
|
||||||
- `GET /api/v1/projects/{id}/graph/stats` - 图统计
|
|
||||||
- `POST /api/v1/graph/shortest-path` - 最短路径
|
|
||||||
- `POST /api/v1/graph/paths` - 所有路径
|
|
||||||
- `GET /api/v1/entities/{id}/neighbors` - 邻居查询
|
|
||||||
- `GET /api/v1/entities/{id1}/common-neighbors/{id2}` - 共同邻居
|
|
||||||
- `GET /api/v1/projects/{id}/graph/centrality` - 中心性分析
|
|
||||||
- `GET /api/v1/projects/{id}/graph/communities` - 社区发现
|
|
||||||
- `POST /api/v1/graph/subgraph` - 子图提取
|
|
||||||
- [x] 部署 Neo4j 服务 (docker-compose)
|
|
||||||
- [x] 前端图分析面板
|
|
||||||
- 图统计信息展示(节点数、边数、密度、连通分量)
|
|
||||||
- 度中心性排名展示
|
|
||||||
- 社区发现可视化(D3.js 力导向图)
|
|
||||||
- 最短路径查询和可视化
|
|
||||||
- 邻居节点查询和可视化
|
|
||||||
- Neo4j 连接状态指示
|
|
||||||
- 数据同步按钮
|
|
||||||
- [x] 路径可视化优化
|
|
||||||
- 添加路径动画效果(流动虚线)
|
|
||||||
- 路径节点和边的特殊样式(起点终点高亮)
|
|
||||||
- 发光效果增强视觉层次
|
|
||||||
- 路径信息面板(显示路径长度、节点数统计)
|
|
||||||
- [x] 社区可视化增强
|
|
||||||
- 社区发现结果的更好可视化(不同颜色区分社区)
|
|
||||||
- 社区统计信息(每个社区的节点数、密度)
|
|
||||||
- 点击社区可以聚焦显示该社区的子图
|
|
||||||
- 社区内节点连线显示内部关联
|
|
||||||
|
|
||||||
### Phase 4 - Neo4j 集成 (可选)
|
|
||||||
- [ ] 将图谱数据同步到 Neo4j
|
|
||||||
- [ ] 支持复杂图查询
|
|
||||||
|
|
||||||
### Phase 5 - 高级功能 (进行中 🚧)
|
|
||||||
- [x] 知识推理与问答增强 ✅ (2026-02-19 完成)
|
|
||||||
- [x] 实体属性扩展 ✅ (2026-02-20 完成)
|
|
||||||
- [x] 时间线视图 ✅ (2026-02-19 完成)
|
|
||||||
- [x] 导出功能 ✅ (2026-02-20 完成)
|
|
||||||
- 知识图谱导出 PNG/SVG
|
|
||||||
- 项目报告导出 PDF
|
|
||||||
- 实体数据导出 Excel/CSV
|
|
||||||
- 关系数据导出 CSV
|
|
||||||
- 转录文本导出 Markdown
|
|
||||||
- 项目完整数据导出 JSON
|
|
||||||
- [ ] 协作功能
|
|
||||||
- 多用户支持
|
|
||||||
- 项目权限管理
|
|
||||||
- 评论和批注
|
|
||||||
- 变更历史追踪
|
|
||||||
|
|
||||||
## 技术债务
|
## 技术债务
|
||||||
|
|
||||||
- 听悟 SDK fallback 到 mock 需要更好的错误处理
|
- 听悟 SDK fallback 到 mock 需要更好的错误处理
|
||||||
- 实体相似度匹配目前只是简单字符串包含,需要 embedding 方案
|
- 实体相似度匹配目前只是简单字符串包含,需要 embedding 方案
|
||||||
- 前端需要状态管理(目前使用全局变量)
|
- 前端需要状态管理(目前使用全局变量)
|
||||||
- 需要添加 API 文档 (OpenAPI/Swagger)
|
- ~~需要添加 API 文档 (OpenAPI/Swagger)~~ ✅ 已完成
|
||||||
|
- 多模态 LLM 图片描述功能待实现(需要集成多模态模型 API)
|
||||||
|
|
||||||
## 部署信息
|
## 部署信息
|
||||||
|
|
||||||
@@ -202,11 +241,107 @@ Phase 6: 企业级功能 - **规划中 📋**
|
|||||||
|
|
||||||
## 最近更新
|
## 最近更新
|
||||||
|
|
||||||
### 2026-02-21 (早间) - Cron 自动部署
|
### 2026-02-24 (凌晨)
|
||||||
- 代码更新到最新版本 (f38e060)
|
- 完成 Phase 7 任务 4: 协作与共享
|
||||||
- InsightFlow 服务已启动 (122.51.127.111:18000) ✅
|
- 创建 collaboration_manager.py 协作模块
|
||||||
- Neo4j 依赖已安装 (neo4j==5.15.0)
|
- CollaborationManager: 协作管理主类
|
||||||
- Neo4j 服务待部署 (需要 Docker 或外部 Neo4j 实例)
|
- 项目分享链接管理 - 支持只读/评论/编辑/管理员权限
|
||||||
|
- 评论和批注系统 - 支持实体、关系、转录文本评论
|
||||||
|
- 变更历史追踪 - 记录所有数据操作变更
|
||||||
|
- 团队成员管理 - 支持多角色权限控制
|
||||||
|
- 更新 schema.sql 添加协作相关数据库表
|
||||||
|
- project_shares: 项目分享表
|
||||||
|
- comments: 评论表
|
||||||
|
- change_history: 变更历史表
|
||||||
|
- team_members: 团队成员表
|
||||||
|
- 更新 main.py 添加协作相关 API 端点
|
||||||
|
- 项目分享相关端点
|
||||||
|
- 评论和批注相关端点
|
||||||
|
- 变更历史相关端点
|
||||||
|
- 团队成员管理端点
|
||||||
|
|
||||||
|
### 2026-02-23 (晚间)
|
||||||
|
- 完成 Phase 7 任务 3: 数据安全与合规
|
||||||
|
- 创建 security_manager.py 安全模块
|
||||||
|
- SecurityManager: 安全管理主类
|
||||||
|
- 审计日志系统 - 记录所有数据操作
|
||||||
|
- 端到端加密 - AES-256-GCM 加密项目数据
|
||||||
|
- 数据脱敏 - 支持手机号、邮箱、身份证等敏感信息脱敏
|
||||||
|
- 数据访问策略 - 基于用户、角色、IP、时间的访问控制
|
||||||
|
- 访问审批流程 - 敏感数据访问需要审批
|
||||||
|
- 更新 schema.sql 添加安全相关数据库表
|
||||||
|
- audit_logs: 审计日志表
|
||||||
|
- encryption_configs: 加密配置表
|
||||||
|
- masking_rules: 脱敏规则表
|
||||||
|
- data_access_policies: 数据访问策略表
|
||||||
|
- access_requests: 访问请求表
|
||||||
|
- 更新 main.py 添加安全相关 API 端点
|
||||||
|
- 更新 requirements.txt 添加 cryptography 依赖
|
||||||
|
|
||||||
|
### 2026-02-23 (午间)
|
||||||
|
- 完成 Phase 7 任务 7: 插件与集成
|
||||||
|
- 创建 plugin_manager.py 模块
|
||||||
|
- PluginManager: 插件管理主类
|
||||||
|
- ChromeExtensionHandler: Chrome 插件处理
|
||||||
|
- BotHandler: 飞书/钉钉/Slack 机器人处理
|
||||||
|
- WebhookIntegration: Zapier/Make Webhook 集成
|
||||||
|
- WebDAVSync: WebDAV 同步管理
|
||||||
|
- 创建完整的 Chrome 扩展代码
|
||||||
|
- manifest.json, background.js, content.js
|
||||||
|
- popup.html/js, options.html/js
|
||||||
|
- 支持网页剪藏、选中文本保存、项目选择
|
||||||
|
- 更新 schema.sql 添加插件相关数据库表
|
||||||
|
- 更新 main.py 添加插件相关 API 端点
|
||||||
|
- 更新 requirements.txt 添加插件依赖
|
||||||
|
|
||||||
|
### 2026-02-23 (早间)
|
||||||
|
- 完成 Phase 7 任务 2: 多模态支持
|
||||||
|
- 创建 multimodal_processor.py 模块
|
||||||
|
- VideoProcessor: 视频处理(音频提取 + 关键帧 + OCR)
|
||||||
|
- ImageProcessor: 图片处理(OCR + 图片描述)
|
||||||
|
- MultimodalEntityExtractor: 多模态实体提取
|
||||||
|
- 创建 multimodal_entity_linker.py 模块
|
||||||
|
- MultimodalEntityLinker: 跨模态实体关联
|
||||||
|
- 支持 embedding 相似度计算
|
||||||
|
- 多模态实体画像和时间线
|
||||||
|
- 更新 schema.sql 添加多模态相关数据库表
|
||||||
|
- 更新 main.py 添加多模态相关 API 端点
|
||||||
|
- 更新 requirements.txt 添加多模态依赖
|
||||||
|
|
||||||
|
### 2026-02-23
|
||||||
|
- 完成 Phase 7 任务 1: 工作流自动化模块
|
||||||
|
- 创建 workflow_manager.py 模块
|
||||||
|
- WorkflowManager: 主管理类,支持定时任务调度
|
||||||
|
- WorkflowTask: 工作流任务定义
|
||||||
|
- WebhookNotifier: Webhook 通知器(支持飞书、钉钉、Slack)
|
||||||
|
- 工作流配置管理
|
||||||
|
- 更新 schema.sql 添加工作流相关数据库表
|
||||||
|
- 更新 main.py 添加工作流相关 API 端点
|
||||||
|
- 更新 requirements.txt 添加 APScheduler 依赖
|
||||||
|
|
||||||
|
### 2026-02-21 (晚间)
|
||||||
|
- 完成 Phase 6: API 开放平台
|
||||||
|
- 为现有 API 端点添加认证依赖
|
||||||
|
- 前端 API Key 管理界面实现
|
||||||
|
- 测试和验证完成
|
||||||
|
- 代码提交并部署
|
||||||
|
|
||||||
|
### 2026-02-21 (午间)
|
||||||
|
- 开始 Phase 6: API 开放平台
|
||||||
|
- 创建 api_key_manager.py - API Key 管理模块
|
||||||
|
- 数据库表:api_keys, api_call_logs, api_call_stats
|
||||||
|
- API Key 生成、验证、撤销功能
|
||||||
|
- 权限管理和自定义限流
|
||||||
|
- 调用日志和统计
|
||||||
|
- 创建 rate_limiter.py - 限流模块
|
||||||
|
- 滑动窗口计数器
|
||||||
|
- 可配置限流参数
|
||||||
|
- 更新 main.py
|
||||||
|
- 集成 Swagger/OpenAPI 文档
|
||||||
|
- 添加 API Key 认证依赖
|
||||||
|
- 实现限流中间件
|
||||||
|
- 新增 API Key 管理端点
|
||||||
|
- 新增系统信息端点
|
||||||
|
|
||||||
### 2026-02-20 (晚间)
|
### 2026-02-20 (晚间)
|
||||||
- 完成 Phase 5 前端图分析面板
|
- 完成 Phase 5 前端图分析面板
|
||||||
|
|||||||
914
backend/collaboration_manager.py
Normal file
914
backend/collaboration_manager.py
Normal file
@@ -0,0 +1,914 @@
|
|||||||
|
"""
|
||||||
|
InsightFlow - 协作与共享模块 (Phase 7 Task 4)
|
||||||
|
支持项目分享、评论批注、变更历史、团队空间
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class SharePermission(Enum):
|
||||||
|
"""分享权限级别"""
|
||||||
|
READ_ONLY = "read_only" # 只读
|
||||||
|
COMMENT = "comment" # 可评论
|
||||||
|
EDIT = "edit" # 可编辑
|
||||||
|
ADMIN = "admin" # 管理员
|
||||||
|
|
||||||
|
|
||||||
|
class CommentTargetType(Enum):
|
||||||
|
"""评论目标类型"""
|
||||||
|
ENTITY = "entity" # 实体评论
|
||||||
|
RELATION = "relation" # 关系评论
|
||||||
|
TRANSCRIPT = "transcript" # 转录文本评论
|
||||||
|
PROJECT = "project" # 项目级评论
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeType(Enum):
|
||||||
|
"""变更类型"""
|
||||||
|
CREATE = "create" # 创建
|
||||||
|
UPDATE = "update" # 更新
|
||||||
|
DELETE = "delete" # 删除
|
||||||
|
MERGE = "merge" # 合并
|
||||||
|
SPLIT = "split" # 拆分
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProjectShare:
|
||||||
|
"""项目分享链接"""
|
||||||
|
id: str
|
||||||
|
project_id: str
|
||||||
|
token: str # 分享令牌
|
||||||
|
permission: str # 权限级别
|
||||||
|
created_by: str # 创建者
|
||||||
|
created_at: str
|
||||||
|
expires_at: Optional[str] # 过期时间
|
||||||
|
max_uses: Optional[int] # 最大使用次数
|
||||||
|
use_count: int # 已使用次数
|
||||||
|
password_hash: Optional[str] # 密码保护
|
||||||
|
is_active: bool # 是否激活
|
||||||
|
allow_download: bool # 允许下载
|
||||||
|
allow_export: bool # 允许导出
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Comment:
|
||||||
|
"""评论/批注"""
|
||||||
|
id: str
|
||||||
|
project_id: str
|
||||||
|
target_type: str # 评论目标类型
|
||||||
|
target_id: str # 目标ID
|
||||||
|
parent_id: Optional[str] # 父评论ID(支持回复)
|
||||||
|
author: str # 作者
|
||||||
|
author_name: str # 作者显示名
|
||||||
|
content: str # 评论内容
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
resolved: bool # 是否已解决
|
||||||
|
resolved_by: Optional[str] # 解决者
|
||||||
|
resolved_at: Optional[str] # 解决时间
|
||||||
|
mentions: List[str] # 提及的用户
|
||||||
|
attachments: List[Dict] # 附件
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChangeRecord:
|
||||||
|
"""变更记录"""
|
||||||
|
id: str
|
||||||
|
project_id: str
|
||||||
|
change_type: str # 变更类型
|
||||||
|
entity_type: str # 实体类型 (entity/relation/transcript/project)
|
||||||
|
entity_id: str # 实体ID
|
||||||
|
entity_name: str # 实体名称(用于显示)
|
||||||
|
changed_by: str # 变更者
|
||||||
|
changed_by_name: str # 变更者显示名
|
||||||
|
changed_at: str
|
||||||
|
old_value: Optional[Dict] # 旧值
|
||||||
|
new_value: Optional[Dict] # 新值
|
||||||
|
description: str # 变更描述
|
||||||
|
session_id: Optional[str] # 会话ID(批量变更关联)
|
||||||
|
reverted: bool # 是否已回滚
|
||||||
|
reverted_at: Optional[str] # 回滚时间
|
||||||
|
reverted_by: Optional[str] # 回滚者
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeamMember:
|
||||||
|
"""团队成员"""
|
||||||
|
id: str
|
||||||
|
project_id: str
|
||||||
|
user_id: str # 用户ID
|
||||||
|
user_name: str # 用户名
|
||||||
|
user_email: str # 用户邮箱
|
||||||
|
role: str # 角色 (owner/admin/editor/viewer)
|
||||||
|
joined_at: str
|
||||||
|
invited_by: str # 邀请者
|
||||||
|
last_active_at: Optional[str] # 最后活跃时间
|
||||||
|
permissions: List[str] # 具体权限列表
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeamSpace:
|
||||||
|
"""团队空间"""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
created_by: str
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
member_count: int
|
||||||
|
project_count: int
|
||||||
|
settings: Dict[str, Any] # 团队设置
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborationManager:
|
||||||
|
"""协作管理主类"""
|
||||||
|
|
||||||
|
def __init__(self, db_manager=None):
|
||||||
|
self.db = db_manager
|
||||||
|
self._shares_cache: Dict[str, ProjectShare] = {}
|
||||||
|
self._comments_cache: Dict[str, List[Comment]] = {}
|
||||||
|
|
||||||
|
# ============ 项目分享 ============
|
||||||
|
|
||||||
|
def create_share_link(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
created_by: str,
|
||||||
|
permission: str = "read_only",
|
||||||
|
expires_in_days: Optional[int] = None,
|
||||||
|
max_uses: Optional[int] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
allow_download: bool = False,
|
||||||
|
allow_export: bool = False
|
||||||
|
) -> ProjectShare:
|
||||||
|
"""创建项目分享链接"""
|
||||||
|
share_id = str(uuid.uuid4())
|
||||||
|
token = self._generate_share_token(project_id)
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
expires_at = None
|
||||||
|
if expires_in_days:
|
||||||
|
expires_at = (datetime.now() + timedelta(days=expires_in_days)).isoformat()
|
||||||
|
|
||||||
|
password_hash = None
|
||||||
|
if password:
|
||||||
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
share = ProjectShare(
|
||||||
|
id=share_id,
|
||||||
|
project_id=project_id,
|
||||||
|
token=token,
|
||||||
|
permission=permission,
|
||||||
|
created_by=created_by,
|
||||||
|
created_at=now,
|
||||||
|
expires_at=expires_at,
|
||||||
|
max_uses=max_uses,
|
||||||
|
use_count=0,
|
||||||
|
password_hash=password_hash,
|
||||||
|
is_active=True,
|
||||||
|
allow_download=allow_download,
|
||||||
|
allow_export=allow_export
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
if self.db:
|
||||||
|
self._save_share_to_db(share)
|
||||||
|
|
||||||
|
self._shares_cache[token] = share
|
||||||
|
return share
|
||||||
|
|
||||||
|
def _generate_share_token(self, project_id: str) -> str:
|
||||||
|
"""生成分享令牌"""
|
||||||
|
data = f"{project_id}:{datetime.now().timestamp()}:{uuid.uuid4()}"
|
||||||
|
return hashlib.sha256(data.encode()).hexdigest()[:32]
|
||||||
|
|
||||||
|
def _save_share_to_db(self, share: ProjectShare):
|
||||||
|
"""保存分享记录到数据库"""
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO project_shares
|
||||||
|
(id, project_id, token, permission, created_by, created_at,
|
||||||
|
expires_at, max_uses, use_count, password_hash, is_active,
|
||||||
|
allow_download, allow_export)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
share.id, share.project_id, share.token, share.permission,
|
||||||
|
share.created_by, share.created_at, share.expires_at,
|
||||||
|
share.max_uses, share.use_count, share.password_hash,
|
||||||
|
share.is_active, share.allow_download, share.allow_export
|
||||||
|
))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
def validate_share_token(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
password: Optional[str] = None
|
||||||
|
) -> Optional[ProjectShare]:
|
||||||
|
"""验证分享令牌"""
|
||||||
|
# 从缓存或数据库获取
|
||||||
|
share = self._shares_cache.get(token)
|
||||||
|
if not share and self.db:
|
||||||
|
share = self._get_share_from_db(token)
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查是否激活
|
||||||
|
if not share.is_active:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查是否过期
|
||||||
|
if share.expires_at and datetime.now().isoformat() > share.expires_at:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查使用次数
|
||||||
|
if share.max_uses and share.use_count >= share.max_uses:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 验证密码
|
||||||
|
if share.password_hash:
|
||||||
|
if not password:
|
||||||
|
return None
|
||||||
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
if password_hash != share.password_hash:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return share
|
||||||
|
|
||||||
|
def _get_share_from_db(self, token: str) -> Optional[ProjectShare]:
|
||||||
|
"""从数据库获取分享记录"""
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM project_shares WHERE token = ?
|
||||||
|
""", (token,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ProjectShare(
|
||||||
|
id=row[0],
|
||||||
|
project_id=row[1],
|
||||||
|
token=row[2],
|
||||||
|
permission=row[3],
|
||||||
|
created_by=row[4],
|
||||||
|
created_at=row[5],
|
||||||
|
expires_at=row[6],
|
||||||
|
max_uses=row[7],
|
||||||
|
use_count=row[8],
|
||||||
|
password_hash=row[9],
|
||||||
|
is_active=bool(row[10]),
|
||||||
|
allow_download=bool(row[11]),
|
||||||
|
allow_export=bool(row[12])
|
||||||
|
)
|
||||||
|
|
||||||
|
def increment_share_usage(self, token: str):
|
||||||
|
"""增加分享链接使用次数"""
|
||||||
|
share = self._shares_cache.get(token)
|
||||||
|
if share:
|
||||||
|
share.use_count += 1
|
||||||
|
|
||||||
|
if self.db:
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE project_shares
|
||||||
|
SET use_count = use_count + 1
|
||||||
|
WHERE token = ?
|
||||||
|
""", (token,))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
def revoke_share_link(self, share_id: str, revoked_by: str) -> bool:
|
||||||
|
"""撤销分享链接"""
|
||||||
|
if self.db:
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE project_shares
|
||||||
|
SET is_active = 0
|
||||||
|
WHERE id = ?
|
||||||
|
""", (share_id,))
|
||||||
|
self.db.conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_project_shares(self, project_id: str) -> List[ProjectShare]:
|
||||||
|
"""列出项目的所有分享链接"""
|
||||||
|
if not self.db:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM project_shares
|
||||||
|
WHERE project_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
""", (project_id,))
|
||||||
|
|
||||||
|
shares = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
shares.append(ProjectShare(
|
||||||
|
id=row[0],
|
||||||
|
project_id=row[1],
|
||||||
|
token=row[2],
|
||||||
|
permission=row[3],
|
||||||
|
created_by=row[4],
|
||||||
|
created_at=row[5],
|
||||||
|
expires_at=row[6],
|
||||||
|
max_uses=row[7],
|
||||||
|
use_count=row[8],
|
||||||
|
password_hash=row[9],
|
||||||
|
is_active=bool(row[10]),
|
||||||
|
allow_download=bool(row[11]),
|
||||||
|
allow_export=bool(row[12])
|
||||||
|
))
|
||||||
|
return shares
|
||||||
|
|
||||||
|
# ============ 评论和批注 ============
|
||||||
|
|
||||||
|
def add_comment(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
target_type: str,
|
||||||
|
target_id: str,
|
||||||
|
author: str,
|
||||||
|
author_name: str,
|
||||||
|
content: str,
|
||||||
|
parent_id: Optional[str] = None,
|
||||||
|
mentions: Optional[List[str]] = None,
|
||||||
|
attachments: Optional[List[Dict]] = None
|
||||||
|
) -> Comment:
|
||||||
|
"""添加评论"""
|
||||||
|
comment_id = str(uuid.uuid4())
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
comment = Comment(
|
||||||
|
id=comment_id,
|
||||||
|
project_id=project_id,
|
||||||
|
target_type=target_type,
|
||||||
|
target_id=target_id,
|
||||||
|
parent_id=parent_id,
|
||||||
|
author=author,
|
||||||
|
author_name=author_name,
|
||||||
|
content=content,
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
resolved=False,
|
||||||
|
resolved_by=None,
|
||||||
|
resolved_at=None,
|
||||||
|
mentions=mentions or [],
|
||||||
|
attachments=attachments or []
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.db:
|
||||||
|
self._save_comment_to_db(comment)
|
||||||
|
|
||||||
|
# 更新缓存
|
||||||
|
key = f"{target_type}:{target_id}"
|
||||||
|
if key not in self._comments_cache:
|
||||||
|
self._comments_cache[key] = []
|
||||||
|
self._comments_cache[key].append(comment)
|
||||||
|
|
||||||
|
return comment
|
||||||
|
|
||||||
|
def _save_comment_to_db(self, comment: Comment):
|
||||||
|
"""保存评论到数据库"""
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO comments
|
||||||
|
(id, project_id, target_type, target_id, parent_id, author, author_name,
|
||||||
|
content, created_at, updated_at, resolved, resolved_by, resolved_at,
|
||||||
|
mentions, attachments)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
comment.id, comment.project_id, comment.target_type, comment.target_id,
|
||||||
|
comment.parent_id, comment.author, comment.author_name, comment.content,
|
||||||
|
comment.created_at, comment.updated_at, comment.resolved,
|
||||||
|
comment.resolved_by, comment.resolved_at,
|
||||||
|
json.dumps(comment.mentions), json.dumps(comment.attachments)
|
||||||
|
))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
def get_comments(
|
||||||
|
self,
|
||||||
|
target_type: str,
|
||||||
|
target_id: str,
|
||||||
|
include_resolved: bool = True
|
||||||
|
) -> List[Comment]:
|
||||||
|
"""获取评论列表"""
|
||||||
|
if not self.db:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
if include_resolved:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM comments
|
||||||
|
WHERE target_type = ? AND target_id = ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
""", (target_type, target_id))
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM comments
|
||||||
|
WHERE target_type = ? AND target_id = ? AND resolved = 0
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
""", (target_type, target_id))
|
||||||
|
|
||||||
|
comments = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
comments.append(self._row_to_comment(row))
|
||||||
|
return comments
|
||||||
|
|
||||||
|
def _row_to_comment(self, row) -> Comment:
|
||||||
|
"""将数据库行转换为Comment对象"""
|
||||||
|
return Comment(
|
||||||
|
id=row[0],
|
||||||
|
project_id=row[1],
|
||||||
|
target_type=row[2],
|
||||||
|
target_id=row[3],
|
||||||
|
parent_id=row[4],
|
||||||
|
author=row[5],
|
||||||
|
author_name=row[6],
|
||||||
|
content=row[7],
|
||||||
|
created_at=row[8],
|
||||||
|
updated_at=row[9],
|
||||||
|
resolved=bool(row[10]),
|
||||||
|
resolved_by=row[11],
|
||||||
|
resolved_at=row[12],
|
||||||
|
mentions=json.loads(row[13]) if row[13] else [],
|
||||||
|
attachments=json.loads(row[14]) if row[14] else []
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_comment(
|
||||||
|
self,
|
||||||
|
comment_id: str,
|
||||||
|
content: str,
|
||||||
|
updated_by: str
|
||||||
|
) -> Optional[Comment]:
|
||||||
|
"""更新评论"""
|
||||||
|
if not self.db:
|
||||||
|
return None
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE comments
|
||||||
|
SET content = ?, updated_at = ?
|
||||||
|
WHERE id = ? AND author = ?
|
||||||
|
""", (content, now, comment_id, updated_by))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
if cursor.rowcount > 0:
|
||||||
|
return self._get_comment_by_id(comment_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_comment_by_id(self, comment_id: str) -> Optional[Comment]:
|
||||||
|
"""根据ID获取评论"""
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM comments WHERE id = ?", (comment_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return self._row_to_comment(row)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_comment(
|
||||||
|
self,
|
||||||
|
comment_id: str,
|
||||||
|
resolved_by: str
|
||||||
|
) -> bool:
|
||||||
|
"""标记评论为已解决"""
|
||||||
|
if not self.db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE comments
|
||||||
|
SET resolved = 1, resolved_by = ?, resolved_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (resolved_by, now, comment_id))
|
||||||
|
self.db.conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def delete_comment(self, comment_id: str, deleted_by: str) -> bool:
|
||||||
|
"""删除评论"""
|
||||||
|
if not self.db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
# 只允许作者或管理员删除
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM comments
|
||||||
|
WHERE id = ? AND (author = ? OR ? IN (
|
||||||
|
SELECT created_by FROM projects WHERE id = comments.project_id
|
||||||
|
))
|
||||||
|
""", (comment_id, deleted_by, deleted_by))
|
||||||
|
self.db.conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def get_project_comments(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0
|
||||||
|
) -> List[Comment]:
|
||||||
|
"""获取项目下的所有评论"""
|
||||||
|
if not self.db:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM comments
|
||||||
|
WHERE project_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", (project_id, limit, offset))
|
||||||
|
|
||||||
|
comments = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
comments.append(self._row_to_comment(row))
|
||||||
|
return comments
|
||||||
|
|
||||||
|
# ============ 变更历史 ============
|
||||||
|
|
||||||
|
def record_change(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
change_type: str,
|
||||||
|
entity_type: str,
|
||||||
|
entity_id: str,
|
||||||
|
entity_name: str,
|
||||||
|
changed_by: str,
|
||||||
|
changed_by_name: str,
|
||||||
|
old_value: Optional[Dict] = None,
|
||||||
|
new_value: Optional[Dict] = None,
|
||||||
|
description: str = "",
|
||||||
|
session_id: Optional[str] = None
|
||||||
|
) -> ChangeRecord:
|
||||||
|
"""记录变更"""
|
||||||
|
record_id = str(uuid.uuid4())
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
record = ChangeRecord(
|
||||||
|
id=record_id,
|
||||||
|
project_id=project_id,
|
||||||
|
change_type=change_type,
|
||||||
|
entity_type=entity_type,
|
||||||
|
entity_id=entity_id,
|
||||||
|
entity_name=entity_name,
|
||||||
|
changed_by=changed_by,
|
||||||
|
changed_by_name=changed_by_name,
|
||||||
|
changed_at=now,
|
||||||
|
old_value=old_value,
|
||||||
|
new_value=new_value,
|
||||||
|
description=description,
|
||||||
|
session_id=session_id,
|
||||||
|
reverted=False,
|
||||||
|
reverted_at=None,
|
||||||
|
reverted_by=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.db:
|
||||||
|
self._save_change_to_db(record)
|
||||||
|
|
||||||
|
return record
|
||||||
|
|
||||||
|
def _save_change_to_db(self, record: ChangeRecord):
|
||||||
|
"""保存变更记录到数据库"""
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO change_history
|
||||||
|
(id, project_id, change_type, entity_type, entity_id, entity_name,
|
||||||
|
changed_by, changed_by_name, changed_at, old_value, new_value,
|
||||||
|
description, session_id, reverted, reverted_at, reverted_by)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
record.id, record.project_id, record.change_type, record.entity_type,
|
||||||
|
record.entity_id, record.entity_name, record.changed_by, record.changed_by_name,
|
||||||
|
record.changed_at, json.dumps(record.old_value) if record.old_value else None,
|
||||||
|
json.dumps(record.new_value) if record.new_value else None,
|
||||||
|
record.description, record.session_id, record.reverted,
|
||||||
|
record.reverted_at, record.reverted_by
|
||||||
|
))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
def get_change_history(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
entity_type: Optional[str] = None,
|
||||||
|
entity_id: Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0
|
||||||
|
) -> List[ChangeRecord]:
|
||||||
|
"""获取变更历史"""
|
||||||
|
if not self.db:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
|
||||||
|
if entity_type and entity_id:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM change_history
|
||||||
|
WHERE project_id = ? AND entity_type = ? AND entity_id = ?
|
||||||
|
ORDER BY changed_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", (project_id, entity_type, entity_id, limit, offset))
|
||||||
|
elif entity_type:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM change_history
|
||||||
|
WHERE project_id = ? AND entity_type = ?
|
||||||
|
ORDER BY changed_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", (project_id, entity_type, limit, offset))
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM change_history
|
||||||
|
WHERE project_id = ?
|
||||||
|
ORDER BY changed_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", (project_id, limit, offset))
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
records.append(self._row_to_change_record(row))
|
||||||
|
return records
|
||||||
|
|
||||||
|
def _row_to_change_record(self, row) -> ChangeRecord:
|
||||||
|
"""将数据库行转换为ChangeRecord对象"""
|
||||||
|
return ChangeRecord(
|
||||||
|
id=row[0],
|
||||||
|
project_id=row[1],
|
||||||
|
change_type=row[2],
|
||||||
|
entity_type=row[3],
|
||||||
|
entity_id=row[4],
|
||||||
|
entity_name=row[5],
|
||||||
|
changed_by=row[6],
|
||||||
|
changed_by_name=row[7],
|
||||||
|
changed_at=row[8],
|
||||||
|
old_value=json.loads(row[9]) if row[9] else None,
|
||||||
|
new_value=json.loads(row[10]) if row[10] else None,
|
||||||
|
description=row[11],
|
||||||
|
session_id=row[12],
|
||||||
|
reverted=bool(row[13]),
|
||||||
|
reverted_at=row[14],
|
||||||
|
reverted_by=row[15]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_entity_version_history(
|
||||||
|
self,
|
||||||
|
entity_type: str,
|
||||||
|
entity_id: str
|
||||||
|
) -> List[ChangeRecord]:
|
||||||
|
"""获取实体的版本历史(用于版本对比)"""
|
||||||
|
if not self.db:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM change_history
|
||||||
|
WHERE entity_type = ? AND entity_id = ?
|
||||||
|
ORDER BY changed_at ASC
|
||||||
|
""", (entity_type, entity_id))
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
records.append(self._row_to_change_record(row))
|
||||||
|
return records
|
||||||
|
|
||||||
|
def revert_change(self, record_id: str, reverted_by: str) -> bool:
|
||||||
|
"""回滚变更"""
|
||||||
|
if not self.db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE change_history
|
||||||
|
SET reverted = 1, reverted_at = ?, reverted_by = ?
|
||||||
|
WHERE id = ? AND reverted = 0
|
||||||
|
""", (now, reverted_by, record_id))
|
||||||
|
self.db.conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def get_change_stats(self, project_id: str) -> Dict[str, Any]:
|
||||||
|
"""获取变更统计"""
|
||||||
|
if not self.db:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
|
||||||
|
# 总变更数
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM change_history WHERE project_id = ?
|
||||||
|
""", (project_id,))
|
||||||
|
total_changes = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# 按类型统计
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT change_type, COUNT(*) FROM change_history
|
||||||
|
WHERE project_id = ? GROUP BY change_type
|
||||||
|
""", (project_id,))
|
||||||
|
type_counts = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
# 按实体类型统计
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT entity_type, COUNT(*) FROM change_history
|
||||||
|
WHERE project_id = ? GROUP BY entity_type
|
||||||
|
""", (project_id,))
|
||||||
|
entity_type_counts = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
# 最近活跃的用户
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT changed_by_name, COUNT(*) as count FROM change_history
|
||||||
|
WHERE project_id = ?
|
||||||
|
GROUP BY changed_by_name
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 5
|
||||||
|
""", (project_id,))
|
||||||
|
top_contributors = [
|
||||||
|
{"name": row[0], "changes": row[1]}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_changes": total_changes,
|
||||||
|
"by_type": type_counts,
|
||||||
|
"by_entity_type": entity_type_counts,
|
||||||
|
"top_contributors": top_contributors
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============ 团队成员管理 ============
|
||||||
|
|
||||||
|
def add_team_member(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
user_id: str,
|
||||||
|
user_name: str,
|
||||||
|
user_email: str,
|
||||||
|
role: str,
|
||||||
|
invited_by: str,
|
||||||
|
permissions: Optional[List[str]] = None
|
||||||
|
) -> TeamMember:
|
||||||
|
"""添加团队成员"""
|
||||||
|
member_id = str(uuid.uuid4())
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# 根据角色设置默认权限
|
||||||
|
if permissions is None:
|
||||||
|
permissions = self._get_default_permissions(role)
|
||||||
|
|
||||||
|
member = TeamMember(
|
||||||
|
id=member_id,
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=user_id,
|
||||||
|
user_name=user_name,
|
||||||
|
user_email=user_email,
|
||||||
|
role=role,
|
||||||
|
joined_at=now,
|
||||||
|
invited_by=invited_by,
|
||||||
|
last_active_at=None,
|
||||||
|
permissions=permissions
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.db:
|
||||||
|
self._save_member_to_db(member)
|
||||||
|
|
||||||
|
return member
|
||||||
|
|
||||||
|
def _get_default_permissions(self, role: str) -> List[str]:
|
||||||
|
"""获取角色的默认权限"""
|
||||||
|
permissions_map = {
|
||||||
|
"owner": ["read", "write", "delete", "share", "admin", "export"],
|
||||||
|
"admin": ["read", "write", "delete", "share", "export"],
|
||||||
|
"editor": ["read", "write", "export"],
|
||||||
|
"viewer": ["read"],
|
||||||
|
"commenter": ["read", "comment"]
|
||||||
|
}
|
||||||
|
return permissions_map.get(role, ["read"])
|
||||||
|
|
||||||
|
def _save_member_to_db(self, member: TeamMember):
|
||||||
|
"""保存成员到数据库"""
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO team_members
|
||||||
|
(id, project_id, user_id, user_name, user_email, role, joined_at,
|
||||||
|
invited_by, last_active_at, permissions)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
member.id, member.project_id, member.user_id, member.user_name,
|
||||||
|
member.user_email, member.role, member.joined_at, member.invited_by,
|
||||||
|
member.last_active_at, json.dumps(member.permissions)
|
||||||
|
))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
def get_team_members(self, project_id: str) -> List[TeamMember]:
|
||||||
|
"""获取团队成员列表"""
|
||||||
|
if not self.db:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM team_members WHERE project_id = ?
|
||||||
|
ORDER BY joined_at ASC
|
||||||
|
""", (project_id,))
|
||||||
|
|
||||||
|
members = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
members.append(self._row_to_team_member(row))
|
||||||
|
return members
|
||||||
|
|
||||||
|
def _row_to_team_member(self, row) -> TeamMember:
|
||||||
|
"""将数据库行转换为TeamMember对象"""
|
||||||
|
return TeamMember(
|
||||||
|
id=row[0],
|
||||||
|
project_id=row[1],
|
||||||
|
user_id=row[2],
|
||||||
|
user_name=row[3],
|
||||||
|
user_email=row[4],
|
||||||
|
role=row[5],
|
||||||
|
joined_at=row[6],
|
||||||
|
invited_by=row[7],
|
||||||
|
last_active_at=row[8],
|
||||||
|
permissions=json.loads(row[9]) if row[9] else []
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_member_role(
|
||||||
|
self,
|
||||||
|
member_id: str,
|
||||||
|
new_role: str,
|
||||||
|
updated_by: str
|
||||||
|
) -> bool:
|
||||||
|
"""更新成员角色"""
|
||||||
|
if not self.db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
permissions = self._get_default_permissions(new_role)
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE team_members
|
||||||
|
SET role = ?, permissions = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (new_role, json.dumps(permissions), member_id))
|
||||||
|
self.db.conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def remove_team_member(self, member_id: str, removed_by: str) -> bool:
|
||||||
|
"""移除团队成员"""
|
||||||
|
if not self.db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM team_members WHERE id = ?", (member_id,))
|
||||||
|
self.db.conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def check_permission(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
user_id: str,
|
||||||
|
permission: str
|
||||||
|
) -> bool:
|
||||||
|
"""检查用户权限"""
|
||||||
|
if not self.db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT permissions FROM team_members
|
||||||
|
WHERE project_id = ? AND user_id = ?
|
||||||
|
""", (project_id, user_id))
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return False
|
||||||
|
|
||||||
|
permissions = json.loads(row[0]) if row[0] else []
|
||||||
|
return permission in permissions or "admin" in permissions
|
||||||
|
|
||||||
|
def update_last_active(self, project_id: str, user_id: str):
|
||||||
|
"""更新用户最后活跃时间"""
|
||||||
|
if not self.db:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
cursor = self.db.conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE team_members
|
||||||
|
SET last_active_at = ?
|
||||||
|
WHERE project_id = ? AND user_id = ?
|
||||||
|
""", (now, project_id, user_id))
|
||||||
|
self.db.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# 全局协作管理器实例
|
||||||
|
_collaboration_manager = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_collaboration_manager(db_manager=None):
|
||||||
|
"""获取协作管理器单例"""
|
||||||
|
global _collaboration_manager
|
||||||
|
if _collaboration_manager is None:
|
||||||
|
_collaboration_manager = CollaborationManager(db_manager)
|
||||||
|
return _collaboration_manager
|
||||||
520
backend/main.py
520
backend/main.py
@@ -79,6 +79,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
NEO4J_AVAILABLE = False
|
NEO4J_AVAILABLE = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collaboration_manager import get_collaboration_manager, CollaborationManager
|
||||||
|
COLLABORATION_AVAILABLE = True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Collaboration import error: {e}")
|
||||||
|
COLLABORATION_AVAILABLE = False
|
||||||
|
|
||||||
app = FastAPI(title="InsightFlow", version="0.3.0")
|
app = FastAPI(title="InsightFlow", version="0.3.0")
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -145,6 +152,41 @@ class GlossaryTermCreate(BaseModel):
|
|||||||
term: str
|
term: str
|
||||||
pronunciation: Optional[str] = ""
|
pronunciation: Optional[str] = ""
|
||||||
|
|
||||||
|
# Phase 7: 协作与共享 - 请求模型
|
||||||
|
class ShareLinkCreate(BaseModel):
|
||||||
|
permission: str = "read_only" # read_only, comment, edit, admin
|
||||||
|
expires_in_days: Optional[int] = None
|
||||||
|
max_uses: Optional[int] = None
|
||||||
|
password: Optional[str] = None
|
||||||
|
allow_download: bool = False
|
||||||
|
allow_export: bool = False
|
||||||
|
|
||||||
|
class ShareLinkVerify(BaseModel):
|
||||||
|
token: str
|
||||||
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
class CommentCreate(BaseModel):
|
||||||
|
target_type: str # entity, relation, transcript, project
|
||||||
|
target_id: str
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
content: str
|
||||||
|
mentions: Optional[List[str]] = None
|
||||||
|
|
||||||
|
class CommentUpdate(BaseModel):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
class CommentResolve(BaseModel):
|
||||||
|
resolved: bool
|
||||||
|
|
||||||
|
class TeamMemberInvite(BaseModel):
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
user_email: str
|
||||||
|
role: str = "viewer" # owner, admin, editor, viewer, commenter
|
||||||
|
|
||||||
|
class TeamMemberRoleUpdate(BaseModel):
|
||||||
|
role: str
|
||||||
|
|
||||||
# API Keys
|
# API Keys
|
||||||
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
||||||
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
||||||
@@ -165,6 +207,15 @@ def get_doc_processor():
|
|||||||
_doc_processor = DocumentProcessor()
|
_doc_processor = DocumentProcessor()
|
||||||
return _doc_processor
|
return _doc_processor
|
||||||
|
|
||||||
|
# Phase 7: Collaboration Manager singleton
|
||||||
|
_collaboration_manager = None
|
||||||
|
def get_collab_manager():
|
||||||
|
global _collaboration_manager
|
||||||
|
if _collaboration_manager is None and COLLABORATION_AVAILABLE:
|
||||||
|
db = get_db_manager() if DB_AVAILABLE else None
|
||||||
|
_collaboration_manager = get_collaboration_manager(db)
|
||||||
|
return _collaboration_manager
|
||||||
|
|
||||||
# Phase 2: Entity Edit API
|
# Phase 2: Entity Edit API
|
||||||
@app.put("/api/v1/entities/{entity_id}")
|
@app.put("/api/v1/entities/{entity_id}")
|
||||||
async def update_entity(entity_id: str, update: EntityUpdate):
|
async def update_entity(entity_id: str, update: EntityUpdate):
|
||||||
@@ -2605,6 +2656,475 @@ async def get_subgraph(request: GraphQueryRequest):
|
|||||||
return subgraph
|
return subgraph
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Phase 7: 协作与共享 API
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# ----- 项目分享 -----
|
||||||
|
|
||||||
|
@app.post("/api/v1/projects/{project_id}/shares")
|
||||||
|
async def create_share_link(project_id: str, request: ShareLinkCreate, created_by: str = "current_user"):
|
||||||
|
"""创建项目分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
share = manager.create_share_link(
|
||||||
|
project_id=project_id,
|
||||||
|
created_by=created_by,
|
||||||
|
permission=request.permission,
|
||||||
|
expires_in_days=request.expires_in_days,
|
||||||
|
max_uses=request.max_uses,
|
||||||
|
password=request.password,
|
||||||
|
allow_download=request.allow_download,
|
||||||
|
allow_export=request.allow_export
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": share.id,
|
||||||
|
"token": share.token,
|
||||||
|
"permission": share.permission,
|
||||||
|
"created_at": share.created_at,
|
||||||
|
"expires_at": share.expires_at,
|
||||||
|
"max_uses": share.max_uses,
|
||||||
|
"share_url": f"/share/{share.token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/shares")
|
||||||
|
async def list_project_shares(project_id: str):
|
||||||
|
"""列出项目的所有分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
shares = manager.list_project_shares(project_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"id": s.id,
|
||||||
|
"token": s.token,
|
||||||
|
"permission": s.permission,
|
||||||
|
"created_at": s.created_at,
|
||||||
|
"expires_at": s.expires_at,
|
||||||
|
"use_count": s.use_count,
|
||||||
|
"max_uses": s.max_uses,
|
||||||
|
"is_active": s.is_active,
|
||||||
|
"has_password": s.password_hash is not None,
|
||||||
|
"allow_download": s.allow_download,
|
||||||
|
"allow_export": s.allow_export
|
||||||
|
}
|
||||||
|
for s in shares
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/shares/verify")
|
||||||
|
async def verify_share_link(request: ShareLinkVerify):
|
||||||
|
"""验证分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
share = manager.validate_share_token(request.token, request.password)
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid or expired share link")
|
||||||
|
|
||||||
|
# 增加使用次数
|
||||||
|
manager.increment_share_usage(request.token)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": True,
|
||||||
|
"project_id": share.project_id,
|
||||||
|
"permission": share.permission,
|
||||||
|
"allow_download": share.allow_download,
|
||||||
|
"allow_export": share.allow_export
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/shares/{token}/access")
|
||||||
|
async def access_shared_project(token: str, password: Optional[str] = None):
|
||||||
|
"""通过分享链接访问项目"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
share = manager.validate_share_token(token, password)
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid or expired share link")
|
||||||
|
|
||||||
|
# 增加使用次数
|
||||||
|
manager.increment_share_usage(token)
|
||||||
|
|
||||||
|
# 获取项目信息
|
||||||
|
if not DB_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Database not available")
|
||||||
|
|
||||||
|
db = get_db_manager()
|
||||||
|
project = db.get_project(share.project_id)
|
||||||
|
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"project": {
|
||||||
|
"id": project.id,
|
||||||
|
"name": project.name,
|
||||||
|
"description": project.description,
|
||||||
|
"created_at": project.created_at
|
||||||
|
},
|
||||||
|
"permission": share.permission,
|
||||||
|
"allow_download": share.allow_download,
|
||||||
|
"allow_export": share.allow_export
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.delete("/api/v1/shares/{share_id}")
|
||||||
|
async def revoke_share_link(share_id: str, revoked_by: str = "current_user"):
|
||||||
|
"""撤销分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.revoke_share_link(share_id, revoked_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Share link not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Share link revoked"}
|
||||||
|
|
||||||
|
# ----- 评论和批注 -----
|
||||||
|
|
||||||
|
@app.post("/api/v1/projects/{project_id}/comments")
|
||||||
|
async def add_comment(project_id: str, request: CommentCreate, author: str = "current_user", author_name: str = "User"):
|
||||||
|
"""添加评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comment = manager.add_comment(
|
||||||
|
project_id=project_id,
|
||||||
|
target_type=request.target_type,
|
||||||
|
target_id=request.target_id,
|
||||||
|
author=author,
|
||||||
|
author_name=author_name,
|
||||||
|
content=request.content,
|
||||||
|
parent_id=request.parent_id,
|
||||||
|
mentions=request.mentions
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": comment.id,
|
||||||
|
"target_type": comment.target_type,
|
||||||
|
"target_id": comment.target_id,
|
||||||
|
"parent_id": comment.parent_id,
|
||||||
|
"author": comment.author,
|
||||||
|
"author_name": comment.author_name,
|
||||||
|
"content": comment.content,
|
||||||
|
"created_at": comment.created_at,
|
||||||
|
"resolved": comment.resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/{target_type}/{target_id}/comments")
|
||||||
|
async def get_comments(target_type: str, target_id: str, include_resolved: bool = True):
|
||||||
|
"""获取评论列表"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comments = manager.get_comments(target_type, target_id, include_resolved)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(comments),
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": c.id,
|
||||||
|
"parent_id": c.parent_id,
|
||||||
|
"author": c.author,
|
||||||
|
"author_name": c.author_name,
|
||||||
|
"content": c.content,
|
||||||
|
"created_at": c.created_at,
|
||||||
|
"updated_at": c.updated_at,
|
||||||
|
"resolved": c.resolved,
|
||||||
|
"resolved_by": c.resolved_by,
|
||||||
|
"resolved_at": c.resolved_at
|
||||||
|
}
|
||||||
|
for c in comments
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/comments")
|
||||||
|
async def get_project_comments(project_id: str, limit: int = 50, offset: int = 0):
|
||||||
|
"""获取项目下的所有评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comments = manager.get_project_comments(project_id, limit, offset)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(comments),
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": c.id,
|
||||||
|
"target_type": c.target_type,
|
||||||
|
"target_id": c.target_id,
|
||||||
|
"parent_id": c.parent_id,
|
||||||
|
"author": c.author,
|
||||||
|
"author_name": c.author_name,
|
||||||
|
"content": c.content,
|
||||||
|
"created_at": c.created_at,
|
||||||
|
"resolved": c.resolved
|
||||||
|
}
|
||||||
|
for c in comments
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.put("/api/v1/comments/{comment_id}")
|
||||||
|
async def update_comment(comment_id: str, request: CommentUpdate, updated_by: str = "current_user"):
|
||||||
|
"""更新评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comment = manager.update_comment(comment_id, request.content, updated_by)
|
||||||
|
|
||||||
|
if not comment:
|
||||||
|
raise HTTPException(status_code=404, detail="Comment not found or not authorized")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": comment.id,
|
||||||
|
"content": comment.content,
|
||||||
|
"updated_at": comment.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/comments/{comment_id}/resolve")
|
||||||
|
async def resolve_comment(comment_id: str, resolved_by: str = "current_user"):
|
||||||
|
"""标记评论为已解决"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.resolve_comment(comment_id, resolved_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Comment not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Comment resolved"}
|
||||||
|
|
||||||
|
@app.delete("/api/v1/comments/{comment_id}")
|
||||||
|
async def delete_comment(comment_id: str, deleted_by: str = "current_user"):
|
||||||
|
"""删除评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.delete_comment(comment_id, deleted_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Comment not found or not authorized")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Comment deleted"}
|
||||||
|
|
||||||
|
# ----- 变更历史 -----
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/history")
|
||||||
|
async def get_change_history(
|
||||||
|
project_id: str,
|
||||||
|
entity_type: Optional[str] = None,
|
||||||
|
entity_id: Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0
|
||||||
|
):
|
||||||
|
"""获取变更历史"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
records = manager.get_change_history(project_id, entity_type, entity_id, limit, offset)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(records),
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"id": r.id,
|
||||||
|
"change_type": r.change_type,
|
||||||
|
"entity_type": r.entity_type,
|
||||||
|
"entity_id": r.entity_id,
|
||||||
|
"entity_name": r.entity_name,
|
||||||
|
"changed_by": r.changed_by,
|
||||||
|
"changed_by_name": r.changed_by_name,
|
||||||
|
"changed_at": r.changed_at,
|
||||||
|
"old_value": r.old_value,
|
||||||
|
"new_value": r.new_value,
|
||||||
|
"description": r.description,
|
||||||
|
"reverted": r.reverted
|
||||||
|
}
|
||||||
|
for r in records
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/history/stats")
|
||||||
|
async def get_change_history_stats(project_id: str):
|
||||||
|
"""获取变更统计"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
stats = manager.get_change_stats(project_id)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@app.get("/api/v1/{entity_type}/{entity_id}/versions")
|
||||||
|
async def get_entity_versions(entity_type: str, entity_id: str):
|
||||||
|
"""获取实体版本历史"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
records = manager.get_entity_version_history(entity_type, entity_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(records),
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"id": r.id,
|
||||||
|
"change_type": r.change_type,
|
||||||
|
"changed_by": r.changed_by,
|
||||||
|
"changed_by_name": r.changed_by_name,
|
||||||
|
"changed_at": r.changed_at,
|
||||||
|
"old_value": r.old_value,
|
||||||
|
"new_value": r.new_value,
|
||||||
|
"description": r.description
|
||||||
|
}
|
||||||
|
for r in records
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/history/{record_id}/revert")
|
||||||
|
async def revert_change(record_id: str, reverted_by: str = "current_user"):
|
||||||
|
"""回滚变更"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.revert_change(record_id, reverted_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Change record not found or already reverted")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Change reverted"}
|
||||||
|
|
||||||
|
# ----- 团队成员 -----
|
||||||
|
|
||||||
|
@app.post("/api/v1/projects/{project_id}/members")
|
||||||
|
async def invite_team_member(project_id: str, request: TeamMemberInvite, invited_by: str = "current_user"):
|
||||||
|
"""邀请团队成员"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
member = manager.add_team_member(
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=request.user_id,
|
||||||
|
user_name=request.user_name,
|
||||||
|
user_email=request.user_email,
|
||||||
|
role=request.role,
|
||||||
|
invited_by=invited_by
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": member.id,
|
||||||
|
"user_id": member.user_id,
|
||||||
|
"user_name": member.user_name,
|
||||||
|
"user_email": member.user_email,
|
||||||
|
"role": member.role,
|
||||||
|
"joined_at": member.joined_at,
|
||||||
|
"permissions": member.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/members")
|
||||||
|
async def list_team_members(project_id: str):
|
||||||
|
"""列出团队成员"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
members = manager.get_team_members(project_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(members),
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": m.id,
|
||||||
|
"user_id": m.user_id,
|
||||||
|
"user_name": m.user_name,
|
||||||
|
"user_email": m.user_email,
|
||||||
|
"role": m.role,
|
||||||
|
"joined_at": m.joined_at,
|
||||||
|
"last_active_at": m.last_active_at,
|
||||||
|
"permissions": m.permissions
|
||||||
|
}
|
||||||
|
for m in members
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.put("/api/v1/members/{member_id}/role")
|
||||||
|
async def update_member_role(member_id: str, request: TeamMemberRoleUpdate, updated_by: str = "current_user"):
|
||||||
|
"""更新成员角色"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.update_member_role(member_id, request.role, updated_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Member role updated"}
|
||||||
|
|
||||||
|
@app.delete("/api/v1/members/{member_id}")
|
||||||
|
async def remove_team_member(member_id: str, removed_by: str = "current_user"):
|
||||||
|
"""移除团队成员"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.remove_team_member(member_id, removed_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Member removed"}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/permissions")
|
||||||
|
async def check_project_permissions(project_id: str, user_id: str = "current_user"):
|
||||||
|
"""检查用户权限"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
members = manager.get_team_members(project_id)
|
||||||
|
|
||||||
|
user_member = None
|
||||||
|
for m in members:
|
||||||
|
if m.user_id == user_id:
|
||||||
|
user_member = m
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_member:
|
||||||
|
return {
|
||||||
|
"has_access": False,
|
||||||
|
"role": None,
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"has_access": True,
|
||||||
|
"role": user_member.role,
|
||||||
|
"permissions": user_member.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Serve frontend - MUST be last to not override API routes
|
# Serve frontend - MUST be last to not override API routes
|
||||||
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
|
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
|
||||||
|
|
||||||
|
|||||||
@@ -178,8 +178,90 @@ CREATE INDEX IF NOT EXISTS idx_entity_attributes_entity ON entity_attributes(ent
|
|||||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_template ON entity_attributes(template_id);
|
CREATE INDEX IF NOT EXISTS idx_entity_attributes_template ON entity_attributes(template_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_attr_history_entity ON attribute_history(entity_id);
|
CREATE INDEX IF NOT EXISTS idx_attr_history_entity ON attribute_history(entity_id);
|
||||||
|
|
||||||
-- Phase 5: 属性相关索引
|
-- Phase 7: 协作与共享 - 项目分享表
|
||||||
CREATE INDEX IF NOT EXISTS idx_attr_templates_project ON attribute_templates(project_id);
|
CREATE TABLE IF NOT EXISTS project_shares (
|
||||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_entity ON entity_attributes(entity_id);
|
id TEXT PRIMARY KEY,
|
||||||
CREATE INDEX IF NOT EXISTS idx_entity_attributes_template ON entity_attributes(template_id);
|
project_id TEXT NOT NULL,
|
||||||
CREATE INDEX IF NOT EXISTS idx_attr_history_entity ON attribute_history(entity_id);
|
token TEXT NOT NULL UNIQUE, -- 分享令牌
|
||||||
|
permission TEXT DEFAULT 'read_only', -- 权限级别: read_only, comment, edit, admin
|
||||||
|
created_by TEXT NOT NULL, -- 创建者
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP, -- 过期时间
|
||||||
|
max_uses INTEGER, -- 最大使用次数
|
||||||
|
use_count INTEGER DEFAULT 0, -- 已使用次数
|
||||||
|
password_hash TEXT, -- 密码保护(哈希)
|
||||||
|
is_active BOOLEAN DEFAULT 1, -- 是否激活
|
||||||
|
allow_download BOOLEAN DEFAULT 0, -- 允许下载
|
||||||
|
allow_export BOOLEAN DEFAULT 0, -- 允许导出
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Phase 7: 协作与共享 - 评论表
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
target_type TEXT NOT NULL, -- 目标类型: entity, relation, transcript, project
|
||||||
|
target_id TEXT NOT NULL, -- 目标ID
|
||||||
|
parent_id TEXT, -- 父评论ID(支持回复)
|
||||||
|
author TEXT NOT NULL, -- 作者ID
|
||||||
|
author_name TEXT, -- 作者显示名
|
||||||
|
content TEXT NOT NULL, -- 评论内容
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
resolved BOOLEAN DEFAULT 0, -- 是否已解决
|
||||||
|
resolved_by TEXT, -- 解决者
|
||||||
|
resolved_at TIMESTAMP, -- 解决时间
|
||||||
|
mentions TEXT, -- JSON数组: 提及的用户
|
||||||
|
attachments TEXT, -- JSON数组: 附件
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Phase 7: 协作与共享 - 变更历史表
|
||||||
|
CREATE TABLE IF NOT EXISTS change_history (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
change_type TEXT NOT NULL, -- 变更类型: create, update, delete, merge, split
|
||||||
|
entity_type TEXT NOT NULL, -- 实体类型: entity, relation, transcript, project
|
||||||
|
entity_id TEXT NOT NULL, -- 实体ID
|
||||||
|
entity_name TEXT, -- 实体名称(用于显示)
|
||||||
|
changed_by TEXT NOT NULL, -- 变更者ID
|
||||||
|
changed_by_name TEXT, -- 变更者显示名
|
||||||
|
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
old_value TEXT, -- JSON: 旧值
|
||||||
|
new_value TEXT, -- JSON: 新值
|
||||||
|
description TEXT, -- 变更描述
|
||||||
|
session_id TEXT, -- 会话ID(批量变更关联)
|
||||||
|
reverted BOOLEAN DEFAULT 0, -- 是否已回滚
|
||||||
|
reverted_at TIMESTAMP, -- 回滚时间
|
||||||
|
reverted_by TEXT, -- 回滚者
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Phase 7: 协作与共享 - 团队成员表
|
||||||
|
CREATE TABLE IF NOT EXISTS team_members (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL, -- 用户ID
|
||||||
|
user_name TEXT, -- 用户名
|
||||||
|
user_email TEXT, -- 用户邮箱
|
||||||
|
role TEXT DEFAULT 'viewer', -- 角色: owner, admin, editor, viewer, commenter
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
invited_by TEXT, -- 邀请者
|
||||||
|
last_active_at TIMESTAMP, -- 最后活跃时间
|
||||||
|
permissions TEXT, -- JSON数组: 具体权限列表
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(project_id, user_id) -- 每个项目每个用户只能有一条记录
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Phase 7: 协作与共享索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_shares_project ON project_shares(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_shares_token ON project_shares(token);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_comments_project ON comments(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_comments_target ON comments(target_type, target_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_history_project ON change_history(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_history_entity ON change_history(entity_type, entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_history_session ON change_history(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_team_members_project ON team_members(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
|
||||||
|
|||||||
Reference in New Issue
Block a user