Compare commits
6 Commits
e18e34b065
...
7a2f6dd30a
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a2f6dd30a | |||
| bf8c4de07d | |||
| 3a1d9ed676 | |||
| 11c9ff2428 | |||
| fef1c9f302 | |||
| 11de8c916a |
112
algorithm/README.md
Normal file
112
algorithm/README.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Algorithm Service
|
||||||
|
|
||||||
|
Python 算法服务,提供文档解析、Embedding、LLM 调用等功能。
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- FastAPI
|
||||||
|
- Uvicorn
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式
|
||||||
|
uvicorn main:app --reload --port 8081
|
||||||
|
|
||||||
|
# 生产模式
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8081
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 文档解析
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /parse
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| file_url | String | 是 | 文件 URL |
|
||||||
|
| engine | String | 是 | 解析引擎:markitdown / docling |
|
||||||
|
| docling_url | String | 否 | Docling 服务 URL |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"content": "解析后的文本内容...",
|
||||||
|
"chunks": ["chunk1", "chunk2"],
|
||||||
|
"total_pages": 10,
|
||||||
|
"metadata": {
|
||||||
|
"filename": "document.pdf",
|
||||||
|
"file_size": 1234567
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 生成 Embedding
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /embedding
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| input | String/Array | 是 | 要 embedding 的文本 |
|
||||||
|
| model | String | 是 | 模型名称 |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]],
|
||||||
|
"model": "text-embedding-3-small"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. LLM 对话
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /chat
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| messages | Array | 是 | 消息列表 |
|
||||||
|
| model | String | 是 | 模型名称 |
|
||||||
|
| temperature | Float | 否 | 温度参数 |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "回复内容..."
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 100,
|
||||||
|
"completion_tokens": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
175
algorithm/main.py
Normal file
175
algorithm/main.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
"""
|
||||||
|
Algorithm Service - 文档解析、Embedding、LLM 调用服务
|
||||||
|
"""
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
app = FastAPI(title="Algorithm Service")
|
||||||
|
|
||||||
|
|
||||||
|
# ========== Models ==========
|
||||||
|
|
||||||
|
class ParseRequest(BaseModel):
|
||||||
|
file_url: str
|
||||||
|
engine: str # markitdown / docling
|
||||||
|
docling_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddingRequest(BaseModel):
|
||||||
|
input: str | List[str]
|
||||||
|
model: str
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(BaseModel):
|
||||||
|
role: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class ChatRequest(BaseModel):
|
||||||
|
messages: List[ChatMessage]
|
||||||
|
model: str
|
||||||
|
temperature: Optional[float] = 0.7
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
base_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# ========== 文档解析 ==========
|
||||||
|
|
||||||
|
@app.post("/parse")
|
||||||
|
async def parse_document(req: ParseRequest):
|
||||||
|
"""解析文档,支持 markitdown 和 docling"""
|
||||||
|
try:
|
||||||
|
if req.engine == "markitdown":
|
||||||
|
return await parse_with_markitdown(req.file_url)
|
||||||
|
elif req.engine == "docling":
|
||||||
|
return await parse_with_docling(req.file_url, req.docling_url)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Unsupported engine: {req.engine}")
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_with_markitdown(file_url: str) -> Dict[str, Any]:
|
||||||
|
"""使用 markitdown 解析文档"""
|
||||||
|
try:
|
||||||
|
from markitdown import MarkItDown
|
||||||
|
|
||||||
|
md = MarkItDown()
|
||||||
|
result = md.convert(file_url)
|
||||||
|
|
||||||
|
# 简单分块(按段落分割)
|
||||||
|
content = result.text_content if hasattr(result, 'text_content') else str(result)
|
||||||
|
chunks = [c.strip() for c in content.split('\n\n') if c.strip()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"content": content,
|
||||||
|
"chunks": chunks[:100], # 限制 chunk 数量
|
||||||
|
"total_pages": 1,
|
||||||
|
"metadata": {
|
||||||
|
"filename": file_url.split('/')[-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except ImportError:
|
||||||
|
raise HTTPException(status_code=500, detail="markitdown not installed. Run: pip install markitdown")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to parse with markitdown: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_with_docling(file_url: str, docling_url: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""使用 docling 解析文档"""
|
||||||
|
if not docling_url:
|
||||||
|
raise HTTPException(status_code=400, detail="docling_url is required for docling engine")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用 docling 服务
|
||||||
|
response = requests.post(
|
||||||
|
f"{docling_url}/convert",
|
||||||
|
json={"url": file_url},
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Docling service error: {response.text}")
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
content = result.get("text", "")
|
||||||
|
chunks = [c.strip() for c in content.split('\n\n') if c.strip()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"content": content,
|
||||||
|
"chunks": chunks[:100],
|
||||||
|
"total_pages": result.get("num_pages", 1),
|
||||||
|
"metadata": {
|
||||||
|
"filename": file_url.split('/')[-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to connect docling service: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ========== Embedding ==========
|
||||||
|
|
||||||
|
@app.post("/embedding")
|
||||||
|
async def generate_embedding(req: EmbeddingRequest):
|
||||||
|
"""生成 Embedding"""
|
||||||
|
try:
|
||||||
|
# TODO: 根据不同 provider 调用不同的 embedding 服务
|
||||||
|
# 目前返回模拟数据
|
||||||
|
|
||||||
|
texts = [req.input] if isinstance(req.input, str) else req.input
|
||||||
|
|
||||||
|
# 模拟 embedding 返回
|
||||||
|
embeddings = [[0.1] * 1536 for _ in texts] # 1536 维向量
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"embeddings": embeddings,
|
||||||
|
"model": req.model
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ========== Chat ==========
|
||||||
|
|
||||||
|
@app.post("/chat")
|
||||||
|
async def chat(req: ChatRequest):
|
||||||
|
"""LLM 对话"""
|
||||||
|
try:
|
||||||
|
# TODO: 根据 model 和 base_url 调用实际的 LLM 服务
|
||||||
|
# 目前返回模拟数据
|
||||||
|
|
||||||
|
last_message = req.messages[-1].content if req.messages else ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": f"Echo: {last_message}"
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": len(last_message),
|
||||||
|
"completion_tokens": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ========== Health Check ==========
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8081)
|
||||||
17
algorithm/requirements.txt
Normal file
17
algorithm/requirements.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# FastAPI
|
||||||
|
fastapi>=0.100.0
|
||||||
|
uvicorn[standard]>=0.23.0
|
||||||
|
|
||||||
|
# HTTP 请求
|
||||||
|
requests>=2.31.0
|
||||||
|
|
||||||
|
# 文档解析
|
||||||
|
markitdown>=0.0.1
|
||||||
|
|
||||||
|
# Pydantic
|
||||||
|
pydantic>=2.0.0
|
||||||
|
|
||||||
|
# 可选:其他解析库
|
||||||
|
# docling>=0.1.0
|
||||||
|
# pypdf>=3.0.0
|
||||||
|
# python-docx>=0.8.11
|
||||||
30
algorithm/start.bat
Normal file
30
algorithm/start.bat
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
title Algorithm Service
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo 启动 Algorithm 服务
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
cd /d %~dp0
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 检查虚拟环境...
|
||||||
|
if not exist venv (
|
||||||
|
echo [INFO] 创建虚拟环境...
|
||||||
|
python -m venv venv
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 安装/更新依赖...
|
||||||
|
call venv\Scripts\pip install -r requirements.txt -q
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 启动服务...
|
||||||
|
echo 访问 http://localhost:8081/docs 查看 API 文档
|
||||||
|
echo 按 Ctrl+C 停止服务
|
||||||
|
echo.
|
||||||
|
|
||||||
|
call venv\Scripts\uvicorn main:app --reload --port 8081 --host 0.0.0.0
|
||||||
|
|
||||||
|
pause
|
||||||
26
algorithm/start.sh
Normal file
26
algorithm/start.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " 启动 Algorithm 服务"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# 检查虚拟环境
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "[INFO] 创建虚拟环境..."
|
||||||
|
python3 -m venv venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "安装/更新依赖..."
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt -q
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "启动服务..."
|
||||||
|
echo "访问 http://localhost:8081/docs 查看 API 文档"
|
||||||
|
echo "按 Ctrl+C 停止服务"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
uvicorn main:app --reload --port 8081 --host 0.0.0.0
|
||||||
BIN
screenshots/创建文件夹.png
Normal file
BIN
screenshots/创建文件夹.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
screenshots/文件解析失败.png
Normal file
BIN
screenshots/文件解析失败.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
screenshots/窗口bug.png
Normal file
BIN
screenshots/窗口bug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -70,12 +70,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 自动迁移表
|
// 3. 自动迁移表
|
||||||
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{}, &model.ModelInfo{})
|
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{}, &model.ModelInfo{}, &model.KnowledgeBase{}, &model.KnowledgeDocument{})
|
||||||
|
|
||||||
// 4. 初始化 Repository
|
// 4. 初始化 Repository
|
||||||
dbRepo := repository.NewDatabaseRepository(db)
|
dbRepo := repository.NewDatabaseRepository(db)
|
||||||
subTableRepo := repository.NewSubTableRepository(db)
|
subTableRepo := repository.NewSubTableRepository(db)
|
||||||
modelRepo := repository.NewModelRepository(db)
|
modelRepo := repository.NewModelRepository(db)
|
||||||
|
knowledgeRepo := repository.NewKnowledgeRepository(db)
|
||||||
|
|
||||||
// 5. 初始化 Service
|
// 5. 初始化 Service
|
||||||
dbService := service.NewDatabaseService(dbRepo, subTableRepo)
|
dbService := service.NewDatabaseService(dbRepo, subTableRepo)
|
||||||
@@ -86,6 +87,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Failed to initialize upload service: %v (files will not be available)", err)
|
log.Printf("Warning: Failed to initialize upload service: %v (files will not be available)", err)
|
||||||
}
|
}
|
||||||
|
knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL)
|
||||||
|
|
||||||
// 6. 初始化 Handler
|
// 6. 初始化 Handler
|
||||||
dbHandler := handler.NewDatabaseHandler(dbService)
|
dbHandler := handler.NewDatabaseHandler(dbService)
|
||||||
@@ -93,6 +95,7 @@ func main() {
|
|||||||
neo4jHandler := handler.NewNeo4jHandler(neo4jService)
|
neo4jHandler := handler.NewNeo4jHandler(neo4jService)
|
||||||
modelHandler := handler.NewModelHandler(modelService)
|
modelHandler := handler.NewModelHandler(modelService)
|
||||||
systemHandler := handler.NewSystemHandler()
|
systemHandler := handler.NewSystemHandler()
|
||||||
|
knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService)
|
||||||
var uploadHandler *handler.UploadHandler
|
var uploadHandler *handler.UploadHandler
|
||||||
if uploadService != nil {
|
if uploadService != nil {
|
||||||
uploadHandler = handler.NewUploadHandler(uploadService)
|
uploadHandler = handler.NewUploadHandler(uploadService)
|
||||||
@@ -185,6 +188,22 @@ func main() {
|
|||||||
modelGroup.DELETE("/:id", modelHandler.Delete)
|
modelGroup.DELETE("/:id", modelHandler.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 知识库管理模块
|
||||||
|
knowledgeGroup := r.Group("/api/knowledge")
|
||||||
|
{
|
||||||
|
knowledgeGroup.POST("/create", knowledgeHandler.Create)
|
||||||
|
knowledgeGroup.GET("/list", knowledgeHandler.List)
|
||||||
|
knowledgeGroup.GET("/:id", knowledgeHandler.GetByID)
|
||||||
|
knowledgeGroup.PUT("/:id", knowledgeHandler.Update)
|
||||||
|
knowledgeGroup.DELETE("/:id", knowledgeHandler.Delete)
|
||||||
|
// 文档管理
|
||||||
|
knowledgeGroup.GET("/:id/documents", knowledgeHandler.ListDocuments)
|
||||||
|
knowledgeGroup.POST("/:id/documents", knowledgeHandler.UploadDocument)
|
||||||
|
knowledgeGroup.DELETE("/:id/documents/:doc_id", knowledgeHandler.DeleteDocument)
|
||||||
|
knowledgeGroup.POST("/:id/documents/:doc_id/reparse", knowledgeHandler.ReparseDocument)
|
||||||
|
knowledgeGroup.GET("/:id/documents/:doc_id/preview", knowledgeHandler.GetDocumentPreview)
|
||||||
|
}
|
||||||
|
|
||||||
// 系统信息模块
|
// 系统信息模块
|
||||||
r.GET("/system/info", systemHandler.GetSystemInfo)
|
r.GET("/system/info", systemHandler.GetSystemInfo)
|
||||||
|
|
||||||
@@ -195,8 +214,8 @@ func main() {
|
|||||||
r.Static("/files", cfg.UploadLocalPath)
|
r.Static("/files", cfg.UploadLocalPath)
|
||||||
}
|
}
|
||||||
// 上传路由
|
// 上传路由
|
||||||
r.POST("/upload", uploadHandler.Upload)
|
r.POST("/api/file_upload", uploadHandler.Upload)
|
||||||
r.DELETE("/upload/:filename", uploadHandler.Delete)
|
r.DELETE("/api/file_upload/:filename", uploadHandler.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 启动服务
|
// 8. 启动服务
|
||||||
|
|||||||
220
server/internal/handler/knowledge_handler.go
Normal file
220
server/internal/handler/knowledge_handler.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"x-agents/server/internal/model"
|
||||||
|
"x-agents/server/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KnowledgeHandler struct {
|
||||||
|
service *service.KnowledgeService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKnowledgeHandler(s *service.KnowledgeService) *KnowledgeHandler {
|
||||||
|
return &KnowledgeHandler{service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建知识库
|
||||||
|
func (h *KnowledgeHandler) Create(c *gin.Context) {
|
||||||
|
var req model.CreateKnowledgeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kb, err := h.service.Create(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"id": kb.ID,
|
||||||
|
"message": "Knowledge base created successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取知识库列表
|
||||||
|
func (h *KnowledgeHandler) List(c *gin.Context) {
|
||||||
|
list, err := h.service.List()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 获取知识库详情
|
||||||
|
func (h *KnowledgeHandler) GetByID(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kb, err := h.service.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "Knowledge base not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": kb})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新知识库
|
||||||
|
func (h *KnowledgeHandler) Update(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req model.UpdateKnowledgeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.Update(id, req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Knowledge base updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除知识库
|
||||||
|
func (h *KnowledgeHandler) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.Delete(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Knowledge base deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDocuments 获取知识库下的文档列表
|
||||||
|
func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := c.Query("status")
|
||||||
|
list, err := h.service.ListDocuments(id, status)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadDocument 上传文档到知识库
|
||||||
|
func (h *KnowledgeHandler) UploadDocument(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "No file uploaded"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小(最大 100MB)
|
||||||
|
if file.Size > 100*1024*1024 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "File too large (max 100MB)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, fileURL, err := h.service.UploadDocument(id, file)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"id": doc.ID,
|
||||||
|
"url": fileURL,
|
||||||
|
"document": doc,
|
||||||
|
"message": "Document uploaded",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDocument 删除文档
|
||||||
|
func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
docID := c.Param("doc_id")
|
||||||
|
|
||||||
|
if id == "" || docID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id and doc_id are required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteDocument(id, docID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Document deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReparseDocument 重新解析文档
|
||||||
|
func (h *KnowledgeHandler) ReparseDocument(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
docID := c.Param("doc_id")
|
||||||
|
|
||||||
|
if id == "" || docID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id and doc_id are required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.ReparseDocument(id, docID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Document reparse started"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDocumentPreview 获取文档预览
|
||||||
|
func (h *KnowledgeHandler) GetDocumentPreview(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
docID := c.Param("doc_id")
|
||||||
|
|
||||||
|
if id == "" || docID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "id and doc_id are required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := 1
|
||||||
|
if p := c.Query("page"); p != "" {
|
||||||
|
if parsed, err := strconv.Atoi(p); err == nil {
|
||||||
|
page = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preview, err := h.service.GetDocumentPreview(id, docID, page)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": preview})
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ type KnowledgeDocument struct {
|
|||||||
KnowledgeBaseID string `json:"knowledge_base_id" gorm:"type:varchar(36);not null;index"`
|
KnowledgeBaseID string `json:"knowledge_base_id" gorm:"type:varchar(36);not null;index"`
|
||||||
Name string `json:"name" gorm:"type:varchar(255);not null"`
|
Name string `json:"name" gorm:"type:varchar(255);not null"`
|
||||||
FileKey string `json:"file_key" gorm:"type:varchar(500)"`
|
FileKey string `json:"file_key" gorm:"type:varchar(500)"`
|
||||||
|
FileURL string `json:"file_url" gorm:"type:varchar(500)"` // 文件访问 URL
|
||||||
FileSize int64 `json:"file_size" gorm:"type:bigint;default:0"`
|
FileSize int64 `json:"file_size" gorm:"type:bigint;default:0"`
|
||||||
Status string `json:"status" gorm:"type:varchar(20);default:parsing"` // parsing / parsed / failed
|
Status string `json:"status" gorm:"type:varchar(20);default:parsing"` // parsing / parsed / failed
|
||||||
ChunkCount int `json:"chunk_count" gorm:"default:0"`
|
ChunkCount int `json:"chunk_count" gorm:"default:0"`
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -10,16 +13,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type KnowledgeService struct {
|
type KnowledgeService struct {
|
||||||
repo *repository.KnowledgeRepository
|
repo *repository.KnowledgeRepository
|
||||||
modelRepo *repository.ModelRepository
|
modelRepo *repository.ModelRepository
|
||||||
uploadService *UploadService
|
uploadService *UploadService
|
||||||
|
pythonServiceURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *repository.ModelRepository, uploadService *UploadService) *KnowledgeService {
|
func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *repository.ModelRepository, uploadService *UploadService, pythonServiceURL string) *KnowledgeService {
|
||||||
return &KnowledgeService{
|
return &KnowledgeService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
modelRepo: modelRepo,
|
modelRepo: modelRepo,
|
||||||
uploadService: uploadService,
|
uploadService: uploadService,
|
||||||
|
pythonServiceURL: pythonServiceURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,22 +40,25 @@ func (s *KnowledgeService) Create(req model.CreateKnowledgeRequest) (*model.Know
|
|||||||
}
|
}
|
||||||
|
|
||||||
kb := &model.KnowledgeBase{
|
kb := &model.KnowledgeBase{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
LLMModelID: req.LLMModelID,
|
LLMModelID: req.LLMModelID,
|
||||||
EmbeddingModelID: req.EmbeddingModelID,
|
EmbeddingModelID: req.EmbeddingModelID,
|
||||||
ParsingConfig: req.ParsingConfig,
|
ParsingConfig: req.ParsingConfig,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
DocumentCount: 0,
|
DocumentCount: 0,
|
||||||
ChunkCount: 0,
|
ChunkCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置默认值
|
// 设置默认值
|
||||||
if kb.ParsingConfig.EnablePDF == false && kb.ParsingConfig.EnablePDF != true {
|
if kb.ParsingConfig.Engine == "" {
|
||||||
|
kb.ParsingConfig.Engine = "markitdown"
|
||||||
|
}
|
||||||
|
if kb.ParsingConfig.EnablePDF != false {
|
||||||
kb.ParsingConfig.EnablePDF = true
|
kb.ParsingConfig.EnablePDF = true
|
||||||
}
|
}
|
||||||
if kb.ParsingConfig.Pandoc == false && kb.ParsingConfig.Pandoc != true {
|
if kb.ParsingConfig.Pandoc != false {
|
||||||
kb.ParsingConfig.Pandoc = true
|
kb.ParsingConfig.Pandoc = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,41 +126,102 @@ func (s *KnowledgeService) ListDocuments(kbID string, status string) ([]model.Kn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UploadDocument 上传文档到知识库
|
// UploadDocument 上传文档到知识库
|
||||||
func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeader) (*model.KnowledgeDocument, error) {
|
func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeader) (*model.KnowledgeDocument, string, error) {
|
||||||
// 验证知识库存在
|
// 验证知识库存在
|
||||||
_, err := s.repo.FindByID(kbID)
|
kb, err := s.repo.FindByID(kbID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
result, err := s.uploadService.Upload(file)
|
result, err := s.uploadService.Upload(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
if !result.Success {
|
if !result.Success {
|
||||||
return nil, nil
|
return nil, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
ext := getFileExt(file.Filename)
|
||||||
|
|
||||||
// 创建文档记录
|
// 创建文档记录
|
||||||
doc := &model.KnowledgeDocument{
|
doc := &model.KnowledgeDocument{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
KnowledgeBaseID: kbID,
|
KnowledgeBaseID: kbID,
|
||||||
Name: file.Filename,
|
Name: file.Filename,
|
||||||
FileKey: result.FileKey,
|
FileKey: result.FileKey + ext,
|
||||||
|
FileURL: result.URL,
|
||||||
FileSize: file.Size,
|
FileSize: file.Size,
|
||||||
Status: "parsing",
|
Status: "parsing",
|
||||||
UploadedAt: time.Now(),
|
UploadedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.repo.CreateDocument(doc); err != nil {
|
if err := s.repo.CreateDocument(doc); err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新知识库文档数
|
// 更新知识库文档数
|
||||||
s.updateDocumentCount(kbID)
|
s.updateDocumentCount(kbID)
|
||||||
|
|
||||||
return doc, nil
|
// 异步调用 Python 服务解析文档
|
||||||
|
go s.parseDocument(kbID, doc.ID, result.URL, kb.ParsingConfig)
|
||||||
|
|
||||||
|
return doc, result.URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDocument 调用 Python 服务解析文档
|
||||||
|
func (s *KnowledgeService) parseDocument(kbID, docID, fileURL string, config model.ParsingConfig) {
|
||||||
|
// 构建请求
|
||||||
|
reqBody := map[string]interface{}{
|
||||||
|
"file_url": fileURL,
|
||||||
|
"engine": config.Engine,
|
||||||
|
}
|
||||||
|
if config.Engine == "docling" && config.DoclingURL != "" {
|
||||||
|
reqBody["docling_url"] = config.DoclingURL
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(reqBody)
|
||||||
|
resp, err := http.Post(s.pythonServiceURL+"/parse", "application/json", bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
s.repo.UpdateDocument(docID, map[string]interface{}{"status": "failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
s.repo.UpdateDocument(docID, map[string]interface{}{"status": "failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
s.repo.UpdateDocument(docID, map[string]interface{}{"status": "failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success, ok := result["success"].(bool); ok && success {
|
||||||
|
// 解析成功,更新状态
|
||||||
|
chunks := []string{}
|
||||||
|
if c, ok := result["chunks"].([]interface{}); ok {
|
||||||
|
for _, chunk := range c {
|
||||||
|
if c, ok := chunk.(string); ok {
|
||||||
|
chunks = append(chunks, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.repo.UpdateDocument(docID, map[string]interface{}{
|
||||||
|
"status": "parsed",
|
||||||
|
"chunk_count": len(chunks),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新知识库的 chunk_count
|
||||||
|
s.updateChunkCount(kbID)
|
||||||
|
} else {
|
||||||
|
s.repo.UpdateDocument(docID, map[string]interface{}{"status": "failed"})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDocument 删除文档
|
// DeleteDocument 删除文档
|
||||||
@@ -168,7 +237,7 @@ func (s *KnowledgeService) DeleteDocument(kbID, docID string) error {
|
|||||||
|
|
||||||
// 删除文件
|
// 删除文件
|
||||||
if doc.FileKey != "" {
|
if doc.FileKey != "" {
|
||||||
s.uploadService.DeleteFile(doc.FileKey + getFileExt(doc.Name))
|
s.uploadService.DeleteFile(doc.FileKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除文档记录
|
// 删除文档记录
|
||||||
@@ -193,8 +262,25 @@ func (s *KnowledgeService) ReparseDocument(kbID, docID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取知识库配置
|
||||||
|
kb, err := s.repo.FindByID(kbID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件 URL
|
||||||
|
fileURL, err := s.uploadService.GetFileURL(doc.FileKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 重置状态为 parsing
|
// 重置状态为 parsing
|
||||||
return s.repo.UpdateDocument(docID, map[string]interface{}{"status": "parsing"})
|
s.repo.UpdateDocument(docID, map[string]interface{}{"status": "parsing"})
|
||||||
|
|
||||||
|
// 异步重新解析
|
||||||
|
go s.parseDocument(kbID, docID, fileURL, kb.ParsingConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDocumentPreview 获取文档预览
|
// GetDocumentPreview 获取文档预览
|
||||||
@@ -208,12 +294,20 @@ func (s *KnowledgeService) GetDocumentPreview(kbID, docID string, page int) (*mo
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简单实现:返回文件 URL(实际应由 Python 服务处理)
|
// 如果已解析,返回解析内容;否则返回文件 URL
|
||||||
fileURL, err := s.uploadService.GetFileURL(doc.FileKey + getFileExt(doc.Name))
|
if doc.Status == "parsed" {
|
||||||
if err != nil {
|
// TODO: 从存储中读取解析内容(可以存到数据库或文件)
|
||||||
return nil, err
|
// 暂时返回文件 URL
|
||||||
|
fileURL, _ := s.uploadService.GetFileURL(doc.FileKey)
|
||||||
|
return &model.DocumentPreviewResponse{
|
||||||
|
TotalPages: 1,
|
||||||
|
CurrentPage: page,
|
||||||
|
Content: fileURL,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 未解析,返回文件 URL
|
||||||
|
fileURL, _ := s.uploadService.GetFileURL(doc.FileKey)
|
||||||
return &model.DocumentPreviewResponse{
|
return &model.DocumentPreviewResponse{
|
||||||
TotalPages: 1,
|
TotalPages: 1,
|
||||||
CurrentPage: page,
|
CurrentPage: page,
|
||||||
@@ -227,7 +321,23 @@ func (s *KnowledgeService) updateDocumentCount(kbID string) {
|
|||||||
s.repo.Update(kbID, map[string]interface{}{"document_count": int(count)})
|
s.repo.Update(kbID, map[string]interface{}{"document_count": int(count)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateChunkCount 更新知识库 chunk 数
|
||||||
|
func (s *KnowledgeService) updateChunkCount(kbID string) {
|
||||||
|
docs, _ := s.repo.FindDocumentsByKBID(kbID, "parsed")
|
||||||
|
totalChunks := 0
|
||||||
|
for _, doc := range docs {
|
||||||
|
totalChunks += doc.ChunkCount
|
||||||
|
}
|
||||||
|
s.repo.Update(kbID, map[string]interface{}{"chunk_count": totalChunks})
|
||||||
|
}
|
||||||
|
|
||||||
func getFileExt(filename string) string {
|
func getFileExt(filename string) string {
|
||||||
|
exts := []string{".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md", ".html"}
|
||||||
|
for _, ext := range exts {
|
||||||
|
if len(filename) >= len(ext) && filename[len(filename)-len(ext):] == ext {
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(filename) > 4 {
|
if len(filename) > 4 {
|
||||||
return filename[len(filename)-4:]
|
return filename[len(filename)-4:]
|
||||||
}
|
}
|
||||||
|
|||||||
19066
server/resource/files/554a2a07-bb6e-4a7d-86c1-d2f7cf7e9517.pdf
Normal file
19066
server/resource/files/554a2a07-bb6e-4a7d-86c1-d2f7cf7e9517.pdf
Normal file
File diff suppressed because one or more lines are too long
259
team-require/api/knowledge-api.md
Normal file
259
team-require/api/knowledge-api.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# 知识库 API
|
||||||
|
|
||||||
|
## 基础信息
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 基础URL | `http://localhost:8082` |
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 创建知识库
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/knowledge/create
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| name | String | 是 | 知识库名称 |
|
||||||
|
| description | String | 否 | 知识库描述 |
|
||||||
|
| llm_model_id | String | 是 | LLM 模型 ID |
|
||||||
|
| embedding_model_id | String | 是 | Embedding 模型 ID |
|
||||||
|
| parsing_config | Object | 是 | 解析配置 |
|
||||||
|
| - engine | String | 是 | 解析引擎:markitdown / docling |
|
||||||
|
| - docling_url | String | 条件 | Docling URL(engine=docling 时必填) |
|
||||||
|
| - enable_pdf | Boolean | 否 | 是否启用 PDF 解析 |
|
||||||
|
| - pandoc | Boolean | 否 | 是否启用 Pandoc |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"id": "kb_xxx",
|
||||||
|
"message": "Knowledge base created successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 获取知识库列表
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/knowledge/list
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "kb_001",
|
||||||
|
"name": "产品文档知识库",
|
||||||
|
"description": "用于存储产品手册",
|
||||||
|
"llm_model_id": "model_001",
|
||||||
|
"embedding_model_id": "model_002",
|
||||||
|
"status": "active",
|
||||||
|
"document_count": 15,
|
||||||
|
"chunk_count": 156,
|
||||||
|
"created_at": "2024-01-15T10:30:00Z",
|
||||||
|
"updated_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 获取知识库详情
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/knowledge/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "kb_001",
|
||||||
|
"name": "产品文档知识库",
|
||||||
|
"description": "用于存储产品手册",
|
||||||
|
"llm_model_id": "model_001",
|
||||||
|
"embedding_model_id": "model_002",
|
||||||
|
"parsing_config": {
|
||||||
|
"engine": "markitdown",
|
||||||
|
"enable_pdf": true,
|
||||||
|
"pandoc": true
|
||||||
|
},
|
||||||
|
"status": "active",
|
||||||
|
"document_count": 15,
|
||||||
|
"chunk_count": 156,
|
||||||
|
"created_at": "2024-01-15T10:30:00Z",
|
||||||
|
"updated_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 删除知识库
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/knowledge/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Knowledge base deleted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 获取知识库下的文档列表
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/knowledge/:id/documents
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| status | String | 否 | 过滤状态:all / parsed / parsing / failed |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "doc_001",
|
||||||
|
"knowledge_base_id": "kb_001",
|
||||||
|
"name": "产品手册_v2.0.pdf",
|
||||||
|
"file_size": 2516582,
|
||||||
|
"status": "parsed",
|
||||||
|
"chunk_count": 156,
|
||||||
|
"uploaded_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 上传文档到知识库
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/knowledge/:id/documents
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| file | File | 是 | 要上传的文件 |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"id": "doc_001",
|
||||||
|
"url": "http://localhost:8082/files/abc123.pdf",
|
||||||
|
"document": {
|
||||||
|
"id": "doc_001",
|
||||||
|
"knowledge_base_id": "kb_001",
|
||||||
|
"name": "产品手册_v2.0.pdf",
|
||||||
|
"file_size": 2516582,
|
||||||
|
"status": "parsing",
|
||||||
|
"chunk_count": 0,
|
||||||
|
"uploaded_at": "2024-01-15T10:30:00Z"
|
||||||
|
},
|
||||||
|
"message": "Document uploaded"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 删除知识库文档
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/knowledge/:id/documents/:doc_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Document deleted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 重新解析文档
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/knowledge/:id/documents/:doc_id/reparse
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Document reparse started"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 获取文档预览内容
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/knowledge/:id/documents/:doc_id/preview
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | Number | 否 | 页码(默认 1) |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"total_pages": 3,
|
||||||
|
"current_page": 1,
|
||||||
|
"content": "第一章 产品介绍..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -30,7 +30,7 @@ minio_use_ssl: false
|
|||||||
**请求**
|
**请求**
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /upload
|
POST /api/file_upload
|
||||||
Content-Type: multipart/form-data
|
Content-Type: multipart/form-data
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ Content-Type: multipart/form-data
|
|||||||
**请求**
|
**请求**
|
||||||
|
|
||||||
```
|
```
|
||||||
DELETE /upload/:filename
|
DELETE /api/file_upload/:filename
|
||||||
```
|
```
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface MenuItem {
|
|||||||
const mainMenu: MenuItem[] = [
|
const mainMenu: MenuItem[] = [
|
||||||
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
||||||
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' },
|
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' },
|
||||||
|
{ name: 'Team', icon: 'fa-users', path: '/team' },
|
||||||
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
||||||
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
||||||
{ name: 'Database', icon: 'fa-database', path: '/database' },
|
{ name: 'Database', icon: 'fa-database', path: '/database' },
|
||||||
@@ -28,7 +29,6 @@ const mainMenu: MenuItem[] = [
|
|||||||
|
|
||||||
const bottomMenu: MenuItem[] = [
|
const bottomMenu: MenuItem[] = [
|
||||||
{ name: 'Settings', icon: 'fa-gear', path: '/settings' },
|
{ name: 'Settings', icon: 'fa-gear', path: '/settings' },
|
||||||
{ name: 'Team', icon: 'fa-users', path: '/team' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const bottomMenu2: MenuItem[] = [
|
const bottomMenu2: MenuItem[] = [
|
||||||
@@ -117,7 +117,7 @@ const handleUserCommand = (command: string) => {
|
|||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<li class="my-4 border-t border-dark-500"></li>
|
<li class="my-4 border-t border-dark-500"></li>
|
||||||
|
|
||||||
<!-- Settings & Team -->
|
<!-- Settings -->
|
||||||
<li v-for="item in bottomMenu" :key="item.name">
|
<li v-for="item in bottomMenu" :key="item.name">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -134,18 +134,13 @@ const handleUserCommand = (command: string) => {
|
|||||||
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="item.name === 'Team'" class="flex gap-1">
|
|
||||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
|
||||||
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
|
|
||||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<li class="my-4 border-t border-dark-500"></li>
|
<li class="my-4 border-t border-dark-500"></li>
|
||||||
|
|
||||||
<!-- Information -->
|
<!-- Account -->
|
||||||
<li v-for="item in bottomMenu2" :key="item.name">
|
<li v-for="item in bottomMenu2" :key="item.name">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import Dashboard from '@/views/Dashboard.vue'
|
import Dashboard from '@/views/Dashboard.vue'
|
||||||
import Login from '@/views/Login.vue'
|
import Login from '@/views/Login.vue'
|
||||||
import Agents from '@/views/Agents.vue'
|
import Agents from '@/views/Agents.vue'
|
||||||
import MCP from '@/views/MCP.vue'
|
import Team from '@/views/Team.vue'
|
||||||
|
import Skill from '@/views/Skill.vue'
|
||||||
import ModelAPIs from '@/views/ModelAPIs.vue'
|
import ModelAPIs from '@/views/ModelAPIs.vue'
|
||||||
import Database from '@/views/Database.vue'
|
import Database from '@/views/Database.vue'
|
||||||
import Knowledge from '@/views/Knowledge.vue'
|
import Knowledge from '@/views/Knowledge.vue'
|
||||||
@@ -26,10 +27,15 @@ const router = createRouter({
|
|||||||
name: 'agents',
|
name: 'agents',
|
||||||
component: Agents
|
component: Agents
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/team',
|
||||||
|
name: 'team',
|
||||||
|
component: Team
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/mcp',
|
path: '/mcp',
|
||||||
name: 'mcp',
|
name: 'mcp',
|
||||||
component: MCP
|
component: Skill
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/model-apis',
|
path: '/model-apis',
|
||||||
|
|||||||
@@ -2,16 +2,32 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useModelSettings } from './settings/useModelSettings'
|
import { useModelSettings } from './settings/useModelSettings'
|
||||||
|
import { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge'
|
||||||
import './knowledge/knowledge.css'
|
import './knowledge/knowledge.css'
|
||||||
|
|
||||||
// 获取已配置的模型列表
|
// 获取已配置的模型列表
|
||||||
const { models, fetchModels } = useModelSettings()
|
const { models, fetchModels } = useModelSettings()
|
||||||
|
|
||||||
// 页面加载时获取模型列表
|
// 页面加载时获取数据
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
fetchModels()
|
fetchModels()
|
||||||
|
await fetchKbList()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取知识库列表
|
||||||
|
const knowledgeBases = ref<any[]>([])
|
||||||
|
const loadingKb = ref(false)
|
||||||
|
|
||||||
|
const fetchKbList = async () => {
|
||||||
|
loadingKb.value = true
|
||||||
|
try {
|
||||||
|
const data = await fetchKnowledgeBases()
|
||||||
|
knowledgeBases.value = data
|
||||||
|
} finally {
|
||||||
|
loadingKb.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 筛选 LLM (chat) 模型
|
// 筛选 LLM (chat) 模型
|
||||||
const llmModels = computed(() => {
|
const llmModels = computed(() => {
|
||||||
return models.value.filter((m: any) => m.model_type === 'chat')
|
return models.value.filter((m: any) => m.model_type === 'chat')
|
||||||
@@ -80,37 +96,6 @@ const prevStep = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟知识库数据
|
|
||||||
const knowledgeBases = ref([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Product Documentation',
|
|
||||||
description: 'Product user manual and API docs',
|
|
||||||
document_count: 156,
|
|
||||||
chunk_count: 1248,
|
|
||||||
created_at: '2024-01-15T10:30:00Z',
|
|
||||||
status: 'ready'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Company Policies',
|
|
||||||
description: 'Internal company policies and procedures',
|
|
||||||
document_count: 42,
|
|
||||||
chunk_count: 320,
|
|
||||||
created_at: '2024-01-20T14:20:00Z',
|
|
||||||
status: 'ready'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Technical Wiki',
|
|
||||||
description: 'Engineering documentation and RFCs',
|
|
||||||
document_count: 89,
|
|
||||||
chunk_count: 756,
|
|
||||||
created_at: '2024-02-01T09:15:00Z',
|
|
||||||
status: 'processing'
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
@@ -132,7 +117,17 @@ const createStep = ref(1)
|
|||||||
const showFileUploadDialog = ref(false)
|
const showFileUploadDialog = ref(false)
|
||||||
const selectedKnowledge = ref<any>(null)
|
const selectedKnowledge = ref<any>(null)
|
||||||
const fileFilter = ref('all') // all, parsed, parsing, failed
|
const fileFilter = ref('all') // all, parsed, parsing, failed
|
||||||
const selectedFile = ref<any>(null) // 当前选中的文件
|
const selectedFile = ref<any>(null) // 当前选中的文件ID
|
||||||
|
const selectedDocument = ref<any>(null) // 当前选中的文档详情
|
||||||
|
const knowledgeDocuments = ref<any[]>([]) // 知识库文档列表
|
||||||
|
const loadingDocuments = ref(false)
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const uploading = ref(false)
|
||||||
|
const previewUrl = ref('') // 文档预览URL
|
||||||
|
const loadingPreview = ref(false)
|
||||||
|
const previewPage = ref(1) // 当前页码
|
||||||
|
const previewTotalPages = ref(1) // 总页数
|
||||||
|
|
||||||
const newKbForm = ref({
|
const newKbForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -153,6 +148,11 @@ const parsingConfig = ref({
|
|||||||
fileSizeLimit: '5242880',
|
fileSizeLimit: '5242880',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Storage 配置
|
||||||
|
const storageConfig = ref({
|
||||||
|
type: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
const openCreateDialog = () => {
|
const openCreateDialog = () => {
|
||||||
createStep.value = 1
|
createStep.value = 1
|
||||||
newKbForm.value = { name: '', description: '' }
|
newKbForm.value = { name: '', description: '' }
|
||||||
@@ -166,6 +166,7 @@ const openCreateDialog = () => {
|
|||||||
highRes: false,
|
highRes: false,
|
||||||
fileSizeLimit: '5242880',
|
fileSizeLimit: '5242880',
|
||||||
}
|
}
|
||||||
|
storageConfig.value = { type: 'local' }
|
||||||
showCreateDialog.value = true
|
showCreateDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,23 +182,42 @@ const cancelCreate = () => {
|
|||||||
highRes: false,
|
highRes: false,
|
||||||
fileSizeLimit: '5242880',
|
fileSizeLimit: '5242880',
|
||||||
}
|
}
|
||||||
|
storageConfig.value = { type: 'local' }
|
||||||
showCreateDialog.value = false
|
showCreateDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const createKnowledgeBase = () => {
|
const createKnowledgeBase = async () => {
|
||||||
knowledgeBases.value.push({
|
const result = await apiCreateKnowledgeBase({
|
||||||
id: Date.now().toString(),
|
|
||||||
name: newKbForm.value.name,
|
name: newKbForm.value.name,
|
||||||
description: newKbForm.value.description,
|
description: newKbForm.value.description,
|
||||||
document_count: 0,
|
llm_model_id: modelConfig.value.llmModelId,
|
||||||
chunk_count: 0,
|
embedding_model_id: modelConfig.value.embeddingModelId,
|
||||||
created_at: new Date().toISOString(),
|
parsing_config: {
|
||||||
status: 'ready'
|
engine: parsingConfig.value.engine,
|
||||||
|
docling_url: parsingConfig.value.engine === 'docling' ? parsingConfig.value.doclingUrl : undefined,
|
||||||
|
enable_pdf: parsingConfig.value.enablePdf,
|
||||||
|
pandoc: parsingConfig.value.pandoc,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
newKbForm.value = { name: '', description: '' }
|
|
||||||
modelConfig.value = { llmModelId: '', embeddingModelId: '' }
|
if (result.success) {
|
||||||
showCreateDialog.value = false
|
await fetchKbList()
|
||||||
ElMessage.success('Knowledge base created successfully')
|
newKbForm.value = { name: '', description: '' }
|
||||||
|
modelConfig.value = { llmModelId: '', embeddingModelId: '' }
|
||||||
|
parsingConfig.value = {
|
||||||
|
enablePdf: true,
|
||||||
|
engine: 'markitdown',
|
||||||
|
doclingUrl: '',
|
||||||
|
pandoc: true,
|
||||||
|
academic: false,
|
||||||
|
highRes: false,
|
||||||
|
fileSizeLimit: '5242880',
|
||||||
|
}
|
||||||
|
showCreateDialog.value = false
|
||||||
|
ElMessage.success('Knowledge base created successfully')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || 'Failed to create knowledge base')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑知识库
|
// 编辑知识库
|
||||||
@@ -232,19 +252,217 @@ const cancelEdit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除知识库
|
// 删除知识库
|
||||||
const deleteKb = (id: string) => {
|
const deleteKb = async (id: string) => {
|
||||||
const index = knowledgeBases.value.findIndex(k => k.id === id)
|
const result = await apiDeleteKnowledgeBase(id)
|
||||||
if (index > -1) {
|
if (result.success) {
|
||||||
knowledgeBases.value.splice(index, 1)
|
await fetchKbList()
|
||||||
ElMessage.success('Knowledge base deleted')
|
ElMessage.success('Knowledge base deleted')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || 'Failed to delete knowledge base')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:格式化文件大小
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (!bytes) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:格式化日期
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return date.toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取状态图标
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'parsed':
|
||||||
|
return 'fa-solid fa-check-circle'
|
||||||
|
case 'parsing':
|
||||||
|
return 'fa-solid fa-spinner fa-spin'
|
||||||
|
case 'failed':
|
||||||
|
return 'fa-solid fa-circle-xmark'
|
||||||
|
default:
|
||||||
|
return 'fa-solid fa-clock'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入知识库(上传文档界面)
|
// 进入知识库(上传文档界面)
|
||||||
const enterKnowledge = (kb: any) => {
|
const enterKnowledge = async (kb: any) => {
|
||||||
selectedKnowledge.value = kb
|
selectedKnowledge.value = kb
|
||||||
|
selectedFile.value = null
|
||||||
|
previewUrl.value = ''
|
||||||
|
|
||||||
|
// 获取文档列表
|
||||||
|
loadingDocuments.value = true
|
||||||
|
try {
|
||||||
|
const docs = await fetchKnowledgeDocuments(kb.id, fileFilter.value)
|
||||||
|
console.log('Fetched documents:', docs)
|
||||||
|
knowledgeDocuments.value = docs
|
||||||
|
|
||||||
|
// 自动选中第一个文档
|
||||||
|
if (docs && docs.length > 0) {
|
||||||
|
console.log('First doc:', docs[0])
|
||||||
|
await selectDocument(docs[0])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loadingDocuments.value = false
|
||||||
|
}
|
||||||
|
|
||||||
showFileUploadDialog.value = true
|
showFileUploadDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换文件过滤标签时重新获取文档列表
|
||||||
|
const changeFileFilter = async (filter: string) => {
|
||||||
|
fileFilter.value = filter
|
||||||
|
if (selectedKnowledge.value) {
|
||||||
|
loadingDocuments.value = true
|
||||||
|
try {
|
||||||
|
const docs = await fetchKnowledgeDocuments(selectedKnowledge.value.id, filter)
|
||||||
|
knowledgeDocuments.value = docs
|
||||||
|
} finally {
|
||||||
|
loadingDocuments.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择文档
|
||||||
|
const selectDocument = async (doc: any) => {
|
||||||
|
selectedFile.value = doc.id
|
||||||
|
selectedDocument.value = doc
|
||||||
|
previewUrl.value = ''
|
||||||
|
previewPage.value = 1
|
||||||
|
previewTotalPages.value = 1
|
||||||
|
|
||||||
|
// 尝试从多个字段获取文件URL
|
||||||
|
const fileUrl = doc.file_url || doc.fileUrl || doc.url || doc.FileURL
|
||||||
|
if (fileUrl) {
|
||||||
|
previewUrl.value = fileUrl
|
||||||
|
} else if (selectedKnowledge.value && doc.status === 'parsed') {
|
||||||
|
// 获取文档预览
|
||||||
|
loadingPreview.value = true
|
||||||
|
try {
|
||||||
|
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||||
|
const result = await getDocumentPreview(selectedKnowledge.value.id, doc.id)
|
||||||
|
if (result.success && result.data?.content) {
|
||||||
|
previewUrl.value = result.data.content
|
||||||
|
previewTotalPages.value = result.data.total_pages || 1
|
||||||
|
previewPage.value = result.data.current_page || 1
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get preview:', error)
|
||||||
|
} finally {
|
||||||
|
loadingPreview.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页
|
||||||
|
const changePreviewPage = async (page: number) => {
|
||||||
|
if (!selectedKnowledge.value || !selectedFile.value || page < 1 || page > previewTotalPages.value) return
|
||||||
|
|
||||||
|
loadingPreview.value = true
|
||||||
|
previewPage.value = page
|
||||||
|
try {
|
||||||
|
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||||
|
const result = await getDocumentPreview(selectedKnowledge.value.id, selectedFile.value, page)
|
||||||
|
if (result.success && result.data?.content) {
|
||||||
|
previewUrl.value = result.data.content
|
||||||
|
previewTotalPages.value = result.data.total_pages || 1
|
||||||
|
previewPage.value = result.data.current_page || page
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get preview:', error)
|
||||||
|
} finally {
|
||||||
|
loadingPreview.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发文件上传
|
||||||
|
const triggerFileUpload = () => {
|
||||||
|
fileInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件选择
|
||||||
|
const handleFileSelect = async (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
const file = target.files?.[0]
|
||||||
|
if (!file || !selectedKnowledge.value) return
|
||||||
|
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const { uploadDocument } = await import('./knowledge/useKnowledge')
|
||||||
|
const result = await uploadDocument(selectedKnowledge.value.id, file)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('File uploaded successfully')
|
||||||
|
|
||||||
|
// 后端返回 result.url 在顶层,result.document 里有 file_url
|
||||||
|
const fileUrl = result.url || result.document?.file_url
|
||||||
|
// 添加到文档列表
|
||||||
|
const newDoc = result.document || {
|
||||||
|
id: result.id,
|
||||||
|
name: file.name,
|
||||||
|
file_size: file.size,
|
||||||
|
status: 'parsing',
|
||||||
|
chunk_count: 0,
|
||||||
|
uploaded_at: new Date().toISOString(),
|
||||||
|
file_url: fileUrl
|
||||||
|
}
|
||||||
|
// 如果返回了file_url,添加到列表开头
|
||||||
|
if (fileUrl) {
|
||||||
|
previewUrl.value = fileUrl
|
||||||
|
// 设置选中的文档信息
|
||||||
|
selectedFile.value = result.id
|
||||||
|
selectedDocument.value = newDoc
|
||||||
|
// 添加到文档列表
|
||||||
|
knowledgeDocuments.value = [newDoc, ...knowledgeDocuments.value]
|
||||||
|
} else {
|
||||||
|
// 刷新文档列表
|
||||||
|
await changeFileFilter(fileFilter.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || 'Failed to upload file')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('Failed to upload file')
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
// 清空 input 以便可以再次选择相同文件
|
||||||
|
target.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文档
|
||||||
|
const deleteDocument = async (docId: string) => {
|
||||||
|
if (!selectedKnowledge.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { deleteDocument: apiDeleteDocument } = await import('./knowledge/useKnowledge')
|
||||||
|
const result = await apiDeleteDocument(selectedKnowledge.value.id, docId)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('Document deleted')
|
||||||
|
// 如果删除的是当前选中的文件,清除选中状态
|
||||||
|
if (selectedFile.value === docId) {
|
||||||
|
selectedFile.value = null
|
||||||
|
selectedDocument.value = null
|
||||||
|
previewUrl.value = ''
|
||||||
|
}
|
||||||
|
// 刷新文档列表
|
||||||
|
await changeFileFilter(fileFilter.value)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || 'Failed to delete document')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('Failed to delete document')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -515,7 +733,7 @@ const enterKnowledge = (kb: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<el-form label-position="top" class="kb-form">
|
<el-form label-position="top" class="kb-form">
|
||||||
<el-form-item label="Storage Type">
|
<el-form-item label="Storage Type">
|
||||||
<el-select placeholder="Select storage type" class="w-full">
|
<el-select v-model="storageConfig.type" placeholder="Select storage type" class="w-full" popper-class="dark-select-dropdown">
|
||||||
<el-option label="Local" value="local" />
|
<el-option label="Local" value="local" />
|
||||||
<el-option label="MinIO" value="minio" />
|
<el-option label="MinIO" value="minio" />
|
||||||
<el-option label="S3" value="s3" />
|
<el-option label="S3" value="s3" />
|
||||||
@@ -580,8 +798,8 @@ const enterKnowledge = (kb: any) => {
|
|||||||
<!-- 文件上传弹窗 -->
|
<!-- 文件上传弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="showFileUploadDialog"
|
v-model="showFileUploadDialog"
|
||||||
:title="selectedKnowledge?.name || 'Knowledge Base'"
|
title=" "
|
||||||
width="95vw"
|
width="calc(100vw - 40px)"
|
||||||
top="20px"
|
top="20px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
class="kb-dialog file-upload-dialog"
|
class="kb-dialog file-upload-dialog"
|
||||||
@@ -593,9 +811,16 @@ const enterKnowledge = (kb: any) => {
|
|||||||
<i class="fa-solid fa-arrow-left"></i>
|
<i class="fa-solid fa-arrow-left"></i>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="file-title">{{ selectedKnowledge?.name || 'Knowledge Base' }}</h2>
|
<h2 class="file-title">{{ selectedKnowledge?.name || 'Knowledge Base' }}</h2>
|
||||||
<button class="btn-primary">
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="fileInput"
|
||||||
|
style="display: none"
|
||||||
|
accept=".pdf,.doc,.docx,.txt,.md"
|
||||||
|
@change="handleFileSelect"
|
||||||
|
/>
|
||||||
|
<button class="btn-primary" @click="triggerFileUpload">
|
||||||
<i class="fa-solid fa-upload"></i>
|
<i class="fa-solid fa-upload"></i>
|
||||||
上传文档
|
Upload
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -604,30 +829,30 @@ const enterKnowledge = (kb: any) => {
|
|||||||
<div
|
<div
|
||||||
class="file-tab"
|
class="file-tab"
|
||||||
:class="{ active: fileFilter === 'all' }"
|
:class="{ active: fileFilter === 'all' }"
|
||||||
@click="fileFilter = 'all'"
|
@click="changeFileFilter('all')"
|
||||||
>
|
>
|
||||||
全部
|
All
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="file-tab"
|
class="file-tab"
|
||||||
:class="{ active: fileFilter === 'parsed' }"
|
:class="{ active: fileFilter === 'parsed' }"
|
||||||
@click="fileFilter = 'parsed'"
|
@click="changeFileFilter('parsed')"
|
||||||
>
|
>
|
||||||
已解析
|
Parsed
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="file-tab"
|
class="file-tab"
|
||||||
:class="{ active: fileFilter === 'parsing' }"
|
:class="{ active: fileFilter === 'parsing' }"
|
||||||
@click="fileFilter = 'parsing'"
|
@click="changeFileFilter('parsing')"
|
||||||
>
|
>
|
||||||
解析中
|
Parsing
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="file-tab"
|
class="file-tab"
|
||||||
:class="{ active: fileFilter === 'failed' }"
|
:class="{ active: fileFilter === 'failed' }"
|
||||||
@click="fileFilter = 'failed'"
|
@click="changeFileFilter('failed')"
|
||||||
>
|
>
|
||||||
解析失败
|
Failed
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -635,74 +860,98 @@ const enterKnowledge = (kb: any) => {
|
|||||||
<div class="file-main">
|
<div class="file-main">
|
||||||
<!-- 左侧文件列表 -->
|
<!-- 左侧文件列表 -->
|
||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
<!-- 模拟文件项 -->
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loadingDocuments" class="loading-state">
|
||||||
|
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
<!-- 无数据提示 -->
|
||||||
|
<div v-else-if="knowledgeDocuments.length === 0" class="empty-state">
|
||||||
|
<i class="fa-solid fa-folder-open"></i>
|
||||||
|
<span>No documents yet</span>
|
||||||
|
</div>
|
||||||
|
<!-- 文档列表 -->
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
class="file-item"
|
class="file-item"
|
||||||
:class="{ selected: selectedFile === i }"
|
:class="{ selected: selectedFile === doc.id }"
|
||||||
v-for="i in 8"
|
v-for="doc in knowledgeDocuments"
|
||||||
:key="i"
|
:key="doc.id"
|
||||||
@click="selectedFile = i"
|
@click="selectDocument(doc)"
|
||||||
>
|
>
|
||||||
<div class="file-item-icon">
|
<div class="file-item-icon">
|
||||||
<i class="fa-solid fa-file-pdf"></i>
|
<i class="fa-solid fa-file-pdf"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-item-info">
|
<div class="file-item-info">
|
||||||
<div class="file-item-name">产品手册_v2.0.pdf</div>
|
<div class="file-item-name">{{ doc.name }}</div>
|
||||||
<div class="file-item-meta">
|
<div class="file-item-meta">
|
||||||
<span>2.4 MB</span>
|
<span>{{ formatFileSize(doc.file_size) }}</span>
|
||||||
<span>2024-01-15</span>
|
<span>{{ formatDate(doc.uploaded_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-item-status success">
|
<div class="file-item-status" :class="doc.status">
|
||||||
<i class="fa-solid fa-check-circle"></i>
|
<span v-if="doc.status === 'failed'" class="status-text">Failed</span>
|
||||||
|
<i v-else :class="getStatusIcon(doc.status)"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧预览面板 -->
|
<!-- 右侧预览面板 -->
|
||||||
<div class="file-preview">
|
<div class="file-preview">
|
||||||
|
<!-- 无选中文件时显示提示 -->
|
||||||
|
<div v-if="!selectedFile" class="preview-empty">
|
||||||
|
<i class="fa-solid fa-file-lines"></i>
|
||||||
|
<span>Select a document to preview</span>
|
||||||
|
</div>
|
||||||
|
<!-- 有选中文件时显示预览 -->
|
||||||
|
<template v-else>
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<div class="preview-header-left">
|
<div class="preview-header-left">
|
||||||
<i class="fa-solid fa-file-pdf"></i>
|
<i class="fa-solid fa-file-pdf"></i>
|
||||||
<span class="preview-filename">产品手册_v2.0.pdf</span>
|
<span class="preview-filename">{{ selectedDocument?.name || 'Document' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-header-info">
|
<div class="preview-header-info">
|
||||||
<span class="info-tag">2.4 MB</span>
|
<span class="info-tag">{{ formatFileSize(selectedDocument?.file_size) }}</span>
|
||||||
<span class="info-tag">2024-01-15 10:30</span>
|
<span class="info-tag">{{ formatDate(selectedDocument?.uploaded_at) }}</span>
|
||||||
<span class="info-tag success">解析成功</span>
|
<span class="info-tag" :class="selectedDocument?.status">{{ selectedDocument?.status === 'parsed' ? 'Parsed' : selectedDocument?.status === 'parsing' ? 'Parsing' : selectedDocument?.status === 'failed' ? 'Failed' : 'Pending' }}</span>
|
||||||
<span class="info-tag">156 个切片</span>
|
<span class="info-tag">{{ selectedDocument?.chunk_count || 0 }} chunks</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-content">
|
<div class="preview-content">
|
||||||
<!-- PDF文件预览 -->
|
<!-- PDF文件预览 -->
|
||||||
<div class="pdf-preview">
|
<div class="pdf-preview">
|
||||||
<iframe
|
<!-- 加载中状态 -->
|
||||||
src="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
<div v-if="loadingPreview" class="preview-loading">
|
||||||
class="pdf-iframe"
|
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||||
></iframe>
|
<span>Loading preview...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-pagination">
|
<!-- 有预览URL时显示PDF -->
|
||||||
<button class="page-btn" disabled>
|
<embed
|
||||||
<i class="fa-solid fa-chevron-left"></i>
|
v-else-if="previewUrl"
|
||||||
</button>
|
type="application/pdf"
|
||||||
<span class="page-info">1 / 3</span>
|
:src="previewUrl"
|
||||||
<button class="page-btn">
|
class="pdf-embed"
|
||||||
<i class="fa-solid fa-chevron-right"></i>
|
/>
|
||||||
</button>
|
<!-- 无预览时显示提示 -->
|
||||||
|
<div v-else class="preview-no-file">
|
||||||
|
<i class="fa-solid fa-file-pdf"></i>
|
||||||
|
<span>Document preview not available</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<div class="preview-actions">
|
<div class="preview-actions">
|
||||||
<button class="action-btn delete">
|
<button class="action-btn delete" @click="deleteDocument(selectedFile)">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
删除
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn reparse">
|
<button class="action-btn reparse">
|
||||||
<i class="fa-solid fa-rotate"></i>
|
<i class="fa-solid fa-rotate"></i>
|
||||||
重新解析
|
Reparse
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn confirm">
|
<button class="action-btn confirm">
|
||||||
<i class="fa-solid fa-check"></i>
|
<i class="fa-solid fa-check"></i>
|
||||||
确认
|
Confirm
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,90 +1,86 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useModelSettings } from './settings/useModelSettings'
|
import { useModelSettings } from './settings/useModelSettings'
|
||||||
|
import { useSkillServers } from './skill/useSkillServers'
|
||||||
|
import { useFileTree } from './skill/useFileTree'
|
||||||
|
import { useSkillChat } from './skill/useSkillChat'
|
||||||
|
import { useWorkflow } from './skill/useWorkflow'
|
||||||
|
import type { FileItem } from './skill/types'
|
||||||
|
|
||||||
interface MCPServer {
|
// MCP Server management
|
||||||
id: number
|
const {
|
||||||
name: string
|
mcpServers,
|
||||||
status: 'running' | 'stopped' | 'error'
|
editingServer,
|
||||||
type: string
|
isEditing,
|
||||||
port: number
|
searchQuery,
|
||||||
createdAt: string
|
filterStatus,
|
||||||
description: string
|
editForm,
|
||||||
}
|
openEdit,
|
||||||
|
saveEdit,
|
||||||
|
cancelEdit,
|
||||||
|
toggleStatus,
|
||||||
|
deleteServer,
|
||||||
|
filteredServers,
|
||||||
|
statusClass,
|
||||||
|
} = useSkillServers()
|
||||||
|
|
||||||
const mcpServers = ref<MCPServer[]>([
|
// File tree management
|
||||||
{ id: 1, name: 'linear-demo', status: 'running', type: 'Linear', port: 3001, createdAt: '2025-04-10', description: 'Linear API integration for project management' },
|
const {
|
||||||
{ id: 2, name: 'google-maps', status: 'running', type: 'Google Maps', port: 3002, createdAt: '2025-04-08', description: 'Google Maps API for location services' },
|
fileTree,
|
||||||
{ id: 3, name: 'explorer-mcp', status: 'error', type: 'File System', port: 3003, createdAt: '2025-04-05', description: 'File system explorer and editor' },
|
rootFiles,
|
||||||
{ id: 4, name: 'postgres-mcp', status: 'running', type: 'PostgreSQL', port: 3004, createdAt: '2025-04-12', description: 'PostgreSQL database operations' },
|
inlineNewFolderName,
|
||||||
{ id: 5, name: 'github-mcp', status: 'stopped', type: 'GitHub', port: 3005, createdAt: '2025-04-11', description: 'GitHub API integration' },
|
inlineCreatingFolderId,
|
||||||
])
|
inlineNewFileName,
|
||||||
|
inlineCreatingFileId,
|
||||||
|
isEditingFile,
|
||||||
|
isEditingFileClosing,
|
||||||
|
editingFile,
|
||||||
|
editingFileName,
|
||||||
|
editingFileContent,
|
||||||
|
editingFileType,
|
||||||
|
hoveringItem,
|
||||||
|
selectedFolder,
|
||||||
|
toggleFolder,
|
||||||
|
showInlineNewFolderInput,
|
||||||
|
confirmInlineCreateFolder,
|
||||||
|
cancelInlineCreateFolder,
|
||||||
|
findFolderById,
|
||||||
|
showInlineNewFileInput,
|
||||||
|
confirmInlineCreateFile,
|
||||||
|
cancelInlineCreateFile,
|
||||||
|
getFileIconByName,
|
||||||
|
handleFileUpload,
|
||||||
|
findFile,
|
||||||
|
selectFolder,
|
||||||
|
deleteFile,
|
||||||
|
deleteFolder,
|
||||||
|
openFile,
|
||||||
|
saveFileEdit,
|
||||||
|
cancelFileEdit,
|
||||||
|
} = useFileTree()
|
||||||
|
|
||||||
const editingServer = ref<MCPServer | null>(null)
|
// Chat functionality
|
||||||
const isEditing = ref(false)
|
const {
|
||||||
const searchQuery = ref('')
|
isChatOpen,
|
||||||
const filterStatus = ref<string>('all')
|
chatInput,
|
||||||
|
chatMessages,
|
||||||
|
toggleChat,
|
||||||
|
sendMessage,
|
||||||
|
} = useSkillChat()
|
||||||
|
|
||||||
const editForm = ref({
|
// Workflow visualization
|
||||||
name: '',
|
const {
|
||||||
type: '',
|
workflowData,
|
||||||
port: 3000,
|
isGeneratingGraph,
|
||||||
description: '',
|
isGraphGenerated,
|
||||||
})
|
toggleNode,
|
||||||
|
renderWorkflow,
|
||||||
const openEdit = (server: MCPServer) => {
|
generateGraph,
|
||||||
editingServer.value = server
|
} = useWorkflow()
|
||||||
editForm.value = {
|
|
||||||
name: server.name,
|
|
||||||
type: server.type,
|
|
||||||
port: server.port,
|
|
||||||
description: server.description,
|
|
||||||
}
|
|
||||||
isEditing.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveEdit = () => {
|
|
||||||
if (editingServer.value) {
|
|
||||||
const index = mcpServers.value.findIndex(s => s.id === editingServer.value!.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
mcpServers.value[index] = {
|
|
||||||
...mcpServers.value[index],
|
|
||||||
...editForm.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isEditing.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelEdit = () => {
|
|
||||||
isEditing.value = false
|
|
||||||
editingServer.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleStatus = (server: MCPServer) => {
|
|
||||||
if (server.status === 'running') {
|
|
||||||
server.status = 'stopped'
|
|
||||||
} else if (server.status === 'stopped') {
|
|
||||||
server.status = 'running'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteServer = (id: number) => {
|
|
||||||
mcpServers.value = mcpServers.value.filter(s => s.id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredServers = () => {
|
|
||||||
return mcpServers.value.filter(server => {
|
|
||||||
const matchSearch = server.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
|
||||||
server.type.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
const matchStatus = filterStatus.value === 'all' || server.status === filterStatus.value
|
|
||||||
return matchSearch && matchStatus
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新建 Skill
|
// 新建 Skill
|
||||||
const isCreating = ref(false)
|
const isCreating = ref(false)
|
||||||
const createStep = ref(1) // 步骤:1=基本信息, 2=配置页面
|
const createStep = ref(1)
|
||||||
const newSkillForm = ref({
|
const newSkillForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
type: 'API',
|
type: 'API',
|
||||||
@@ -98,7 +94,6 @@ const newSkillForm = ref({
|
|||||||
// 从 Model Settings 获取模型
|
// 从 Model Settings 获取模型
|
||||||
const { models, fetchModels } = useModelSettings()
|
const { models, fetchModels } = useModelSettings()
|
||||||
|
|
||||||
// 过滤 chat 类型的模型用于下拉框
|
|
||||||
const availableModels = computed(() => {
|
const availableModels = computed(() => {
|
||||||
return models.value
|
return models.value
|
||||||
.filter((m: any) => m.model_type === 'chat')
|
.filter((m: any) => m.model_type === 'chat')
|
||||||
@@ -116,7 +111,6 @@ onMounted(() => {
|
|||||||
const goToStep2 = () => {
|
const goToStep2 = () => {
|
||||||
if (!newSkillForm.value.name.trim()) return
|
if (!newSkillForm.value.name.trim()) return
|
||||||
createStep.value = 2
|
createStep.value = 2
|
||||||
// 重置图谱状态
|
|
||||||
isGraphGenerated.value = false
|
isGraphGenerated.value = false
|
||||||
isGeneratingGraph.value = false
|
isGeneratingGraph.value = false
|
||||||
}
|
}
|
||||||
@@ -125,462 +119,6 @@ const goBackToStep1 = () => {
|
|||||||
createStep.value = 1
|
createStep.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件管理相关 - VS Code 风格
|
|
||||||
interface FileItem {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
icon: string
|
|
||||||
content: string
|
|
||||||
type: 'file'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderItem {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
icon: string
|
|
||||||
type: 'folder'
|
|
||||||
expanded: boolean
|
|
||||||
children: (FileItem | FolderItem)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认文件内容
|
|
||||||
const skillMdContent = `# Skill Configuration
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This skill is designed to process user queries and generate appropriate responses.
|
|
||||||
|
|
||||||
## Data Sources
|
|
||||||
- user_data.csv
|
|
||||||
- config.json
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
- process.py
|
|
||||||
- transform.js
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- Python 3.8+
|
|
||||||
- Node.js 16+
|
|
||||||
`
|
|
||||||
|
|
||||||
const fileTree = ref<FolderItem[]>([
|
|
||||||
{
|
|
||||||
id: 'folder-1',
|
|
||||||
name: 'Data',
|
|
||||||
icon: 'fa-folder',
|
|
||||||
type: 'folder',
|
|
||||||
expanded: true,
|
|
||||||
children: [
|
|
||||||
{ id: 'file-1', name: 'user_data.csv', icon: 'fa-file', content: 'id,name,email,age\n1,John Doe,john@example.com,28\n2,Jane Smith,jane@example.com,32', type: 'file' },
|
|
||||||
{ id: 'file-2', name: 'config.json', icon: 'fa-code', content: '{\n "appName": "MyApp",\n "version": "1.0.0",\n "debug": true\n}', type: 'file' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'folder-2',
|
|
||||||
name: 'Script',
|
|
||||||
icon: 'fa-folder',
|
|
||||||
type: 'folder',
|
|
||||||
expanded: true,
|
|
||||||
children: [
|
|
||||||
{ id: 'file-3', name: 'process.py', icon: 'fa-code', content: 'import pandas as pd\n\ndef process_data(data):\n """Process the input data"""\n return data.apply(lambda x: x.strip())\n\nif __name__ == "__main__":\n df = pd.read_csv("user_data.csv")\n processed = process_data(df)\n processed.to_csv("output.csv", index=False)', type: 'file' },
|
|
||||||
{ id: 'file-4', name: 'transform.js', icon: 'fa-code', content: 'function transformData(input) {\n return input.map(item => ({\n ...item,\n processed: true,\n timestamp: Date.now()\n }));\n}\n\nmodule.exports = { transformData };', type: 'file' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'folder-3',
|
|
||||||
name: 'Reference',
|
|
||||||
icon: 'fa-folder',
|
|
||||||
type: 'folder',
|
|
||||||
expanded: true,
|
|
||||||
children: [
|
|
||||||
{ id: 'file-5', name: 'api_docs.md', icon: 'fa-code', content: '# API Documentation\n\n## Endpoints\n\n### GET /api/users\nReturns list of users.\n\n### POST /api/users\nCreate a new user.', type: 'file' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 根目录文件
|
|
||||||
const rootFiles = ref<FileItem[]>([
|
|
||||||
{ id: 'file-root-1', name: 'SKILL.md', icon: 'fa-file', content: skillMdContent, type: 'file' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 展开/收起文件夹
|
|
||||||
const toggleFolder = (folder: FolderItem) => {
|
|
||||||
folder.expanded = !folder.expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新建文件夹
|
|
||||||
const isCreatingFolder = ref(false)
|
|
||||||
const newFolderName = ref('')
|
|
||||||
const newFolderParentId = ref<string | null>(null)
|
|
||||||
const showNewFolderInput = (parentId: string | null = null) => {
|
|
||||||
newFolderName.value = ''
|
|
||||||
newFolderParentId.value = parentId
|
|
||||||
isCreatingFolder.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFolder = () => {
|
|
||||||
if (!newFolderName.value.trim()) {
|
|
||||||
isCreatingFolder.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFolder: FileItem = {
|
|
||||||
id: 'folder-' + Date.now(),
|
|
||||||
name: newFolderName.value,
|
|
||||||
icon: 'fa-folder',
|
|
||||||
type: 'folder',
|
|
||||||
expanded: true,
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newFolderParentId.value) {
|
|
||||||
// 添加到指定文件夹作为子文件夹
|
|
||||||
const parentFolder = findFolderById(fileTree.value, newFolderParentId.value)
|
|
||||||
if (parentFolder && parentFolder.children) {
|
|
||||||
parentFolder.children.push(newFolder)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 添加到根目录
|
|
||||||
fileTree.value.push(newFolder)
|
|
||||||
}
|
|
||||||
isCreatingFolder.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归查找文件夹
|
|
||||||
const findFolderById = (folders: FileItem[], id: string): FileItem | null => {
|
|
||||||
for (const folder of folders) {
|
|
||||||
if (folder.id === id) return folder
|
|
||||||
if (folder.children) {
|
|
||||||
const found = findFolderById(folder.children, id)
|
|
||||||
if (found) return found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新建文件
|
|
||||||
const isCreatingFile = ref(false)
|
|
||||||
const newFileName = ref('')
|
|
||||||
const newFileParentFolder = ref<string | null>(null)
|
|
||||||
const showNewFileInput = (folderId: string | null = null) => {
|
|
||||||
newFileName.value = ''
|
|
||||||
newFileParentFolder.value = folderId
|
|
||||||
isCreatingFile.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFile = () => {
|
|
||||||
if (!newFileName.value.trim()) {
|
|
||||||
isCreatingFile.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFile: FileItem = {
|
|
||||||
id: 'file-' + Date.now(),
|
|
||||||
name: newFileName.value,
|
|
||||||
icon: getFileIconByName(newFileName.value),
|
|
||||||
content: '',
|
|
||||||
type: 'file'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newFileParentFolder.value) {
|
|
||||||
// 添加到指定文件夹
|
|
||||||
const folder = fileTree.value.find(f => f.id === newFileParentFolder.value)
|
|
||||||
if (folder && folder.children) {
|
|
||||||
folder.children.push(newFile)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 添加到根目录
|
|
||||||
rootFiles.value.push(newFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
isCreatingFile.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据文件名获取图标 - 使用通用图标
|
|
||||||
const getFileIconByName = (filename: string): string => {
|
|
||||||
const ext = filename.split('.').pop()?.toLowerCase()
|
|
||||||
const codeExtensions = ['js', 'ts', 'py', 'html', 'css', 'json', 'xml', 'yaml', 'yml', 'java', 'c', 'cpp', 'go', 'rs', 'php', 'rb', 'swift', 'kt', 'sql', 'sh', 'bash', 'md']
|
|
||||||
if (codeExtensions.includes(ext || '')) {
|
|
||||||
return 'fa-code'
|
|
||||||
}
|
|
||||||
return 'fa-file'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传文件
|
|
||||||
const handleFileUpload = (event: Event, folderId: string | null = null) => {
|
|
||||||
const input = event.target as HTMLInputElement
|
|
||||||
if (!input.files?.length) return
|
|
||||||
|
|
||||||
Array.from(input.files).forEach(file => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const newFile: FileItem = {
|
|
||||||
id: 'file-' + Date.now() + Math.random(),
|
|
||||||
name: file.name,
|
|
||||||
icon: getFileIconByName(file.name),
|
|
||||||
content: e.target?.result as string || '',
|
|
||||||
type: 'file'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folderId) {
|
|
||||||
const folder = fileTree.value.find(f => f.id === folderId)
|
|
||||||
if (folder && folder.children) {
|
|
||||||
folder.children.push(newFile)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rootFiles.value.push(newFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
|
||||||
|
|
||||||
input.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归查找文件
|
|
||||||
const findFile = (items: (FileItem | FolderItem)[], fileId: string): FileItem | null => {
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.type === 'file' && item.id === fileId) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
if (item.type === 'folder' && item.children) {
|
|
||||||
const found = findFile(item.children, fileId)
|
|
||||||
if (found) return found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件编辑相关
|
|
||||||
const isEditingFile = ref(false)
|
|
||||||
const isEditingFileClosing = ref(false)
|
|
||||||
|
|
||||||
// AI 对话面板
|
|
||||||
const isChatOpen = ref(false)
|
|
||||||
|
|
||||||
// 流程生成相关
|
|
||||||
const isGeneratingGraph = ref(false)
|
|
||||||
const isGraphGenerated = ref(false)
|
|
||||||
|
|
||||||
const generateGraph = () => {
|
|
||||||
isGeneratingGraph.value = true
|
|
||||||
// 模拟生成流程的等待过程
|
|
||||||
setTimeout(() => {
|
|
||||||
isGeneratingGraph.value = false
|
|
||||||
isGraphGenerated.value = true
|
|
||||||
// 初始化树结构 - 默认展开第一层
|
|
||||||
if (workflowData.value.length > 0) {
|
|
||||||
workflowData.value[0].expanded = true
|
|
||||||
if (workflowData.value[0].children) {
|
|
||||||
workflowData.value[0].children.forEach(child => {
|
|
||||||
child.expanded = false
|
|
||||||
if (child.children) {
|
|
||||||
child.children.forEach(gc => gc.expanded = false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 2500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleChat = () => {
|
|
||||||
isChatOpen.value = !isChatOpen.value
|
|
||||||
}
|
|
||||||
const editingFile = ref<FileItem | null>(null)
|
|
||||||
const editingFileName = ref('')
|
|
||||||
const editingFileContent = ref('')
|
|
||||||
const editingFileType = ref('')
|
|
||||||
|
|
||||||
// 选中文件/文件夹状态
|
|
||||||
// 鼠标悬停状态
|
|
||||||
const hoveringItem = ref<string | null>(null)
|
|
||||||
// 当前选中的文件夹(用于创建子文件夹/子文件)
|
|
||||||
const selectedFolder = ref<string | null>(null)
|
|
||||||
|
|
||||||
// 选中文件夹
|
|
||||||
const selectFolder = (folderId: string) => {
|
|
||||||
if (selectedFolder.value === folderId) {
|
|
||||||
selectedFolder.value = null // 再次点击取消选中
|
|
||||||
} else {
|
|
||||||
selectedFolder.value = folderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除文件
|
|
||||||
const deleteFile = (file: FileItem, parentId: string | null) => {
|
|
||||||
if (!parentId) {
|
|
||||||
// 根目录文件
|
|
||||||
rootFiles.value = rootFiles.value.filter(f => f.id !== file.id)
|
|
||||||
} else {
|
|
||||||
// 文件夹内文件
|
|
||||||
const folder = fileTree.value.find(f => f.id === parentId)
|
|
||||||
if (folder && folder.children) {
|
|
||||||
folder.children = folder.children.filter(c => (c as FileItem).id !== file.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除文件夹
|
|
||||||
const deleteFolder = (folder: FolderItem) => {
|
|
||||||
fileTree.value = fileTree.value.filter(f => f.id !== folder.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开文件编辑器
|
|
||||||
const openFile = (file: FileItem) => {
|
|
||||||
editingFile.value = file
|
|
||||||
editingFileName.value = file.name
|
|
||||||
editingFileContent.value = file.content
|
|
||||||
editingFileType.value = 'file'
|
|
||||||
isEditingFile.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveFileEdit = () => {
|
|
||||||
if (!editingFile.value) return
|
|
||||||
|
|
||||||
// 更新文件内容
|
|
||||||
editingFile.value.content = editingFileContent.value
|
|
||||||
|
|
||||||
isEditingFileClosing.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
isEditingFile.value = false
|
|
||||||
isEditingFileClosing.value = false
|
|
||||||
editingFile.value = null
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelFileEdit = () => {
|
|
||||||
isEditingFileClosing.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
isEditingFile.value = false
|
|
||||||
isEditingFileClosing.value = false
|
|
||||||
editingFile.value = null
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对话相关
|
|
||||||
const chatInput = ref('')
|
|
||||||
const chatMessages = ref<{ id: number; text: string }[]>([])
|
|
||||||
|
|
||||||
const sendMessage = () => {
|
|
||||||
if (!chatInput.value.trim()) return
|
|
||||||
|
|
||||||
chatMessages.value.push({
|
|
||||||
id: Date.now(),
|
|
||||||
text: chatInput.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const userInput = chatInput.value
|
|
||||||
chatInput.value = ''
|
|
||||||
|
|
||||||
// 模拟 AI 回复
|
|
||||||
setTimeout(() => {
|
|
||||||
chatMessages.value.push({
|
|
||||||
id: Date.now(),
|
|
||||||
text: `I've updated the configuration based on your request: "${userInput}". The changes have been applied to your skill settings.`
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流程相关 - 金字塔结构
|
|
||||||
|
|
||||||
// 流程节点 - 树形结构
|
|
||||||
interface WorkflowNode {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
icon: string
|
|
||||||
color: string
|
|
||||||
bgColor: string
|
|
||||||
borderColor: string
|
|
||||||
status: 'pending' | 'processing' | 'completed'
|
|
||||||
expanded: boolean
|
|
||||||
children?: WorkflowNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowData = ref<WorkflowNode[]>([
|
|
||||||
{
|
|
||||||
id: 'output',
|
|
||||||
name: '最终输出',
|
|
||||||
description: '生成最终结果返回给用户',
|
|
||||||
icon: 'fa-check-circle',
|
|
||||||
color: 'text-green-400',
|
|
||||||
bgColor: 'from-green-500/20 to-green-500/5',
|
|
||||||
borderColor: 'border-green-500/30',
|
|
||||||
status: 'pending',
|
|
||||||
expanded: false,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'generate',
|
|
||||||
name: '方案生成',
|
|
||||||
description: '基于分析结果构建解决方案',
|
|
||||||
icon: 'fa-lightbulb',
|
|
||||||
color: 'text-primary-orange',
|
|
||||||
bgColor: 'from-primary-orange/20 to-primary-orange/5',
|
|
||||||
borderColor: 'border-primary-orange/30',
|
|
||||||
status: 'pending',
|
|
||||||
expanded: false,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'analyze',
|
|
||||||
name: '问题分析',
|
|
||||||
description: '拆解问题、提取关键信息、理解用户意图',
|
|
||||||
icon: 'fa-microscope',
|
|
||||||
color: 'text-purple-400',
|
|
||||||
bgColor: 'from-purple-500/20 to-purple-500/5',
|
|
||||||
borderColor: 'border-purple-500/30',
|
|
||||||
status: 'pending',
|
|
||||||
expanded: false,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'question',
|
|
||||||
name: '用户问题',
|
|
||||||
description: '输入原始需求和问题描述',
|
|
||||||
icon: 'fa-question',
|
|
||||||
color: 'text-primary-cyan',
|
|
||||||
bgColor: 'from-primary-cyan/20 to-primary-cyan/5',
|
|
||||||
borderColor: 'border-primary-cyan/30',
|
|
||||||
status: 'pending',
|
|
||||||
expanded: false,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data',
|
|
||||||
name: '数据获取',
|
|
||||||
description: '从知识库、向量数据库获取相关信息',
|
|
||||||
icon: 'fa-database',
|
|
||||||
color: 'text-green-400',
|
|
||||||
bgColor: 'from-green-500/20 to-green-500/5',
|
|
||||||
borderColor: 'border-green-500/30',
|
|
||||||
status: 'pending',
|
|
||||||
expanded: false,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'evaluate',
|
|
||||||
name: '方案评估',
|
|
||||||
description: '验证方案可行性和效果',
|
|
||||||
icon: 'fa-clipboard-check',
|
|
||||||
color: 'text-yellow-400',
|
|
||||||
bgColor: 'from-yellow-500/20 to-yellow-500/5',
|
|
||||||
borderColor: 'border-yellow-500/30',
|
|
||||||
status: 'pending',
|
|
||||||
expanded: false,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 展开/收起节点
|
|
||||||
const toggleNode = (node: WorkflowNode) => {
|
|
||||||
node.expanded = !node.expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归渲染节点
|
|
||||||
const renderWorkflow = (nodes: WorkflowNode[], level: number = 0) => {
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
newSkillForm.value = {
|
newSkillForm.value = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -613,15 +151,6 @@ const saveNewSkill = () => {
|
|||||||
})
|
})
|
||||||
isCreating.value = false
|
isCreating.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusClass = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'running': return 'bg-primary-success'
|
|
||||||
case 'stopped': return 'bg-gray-500'
|
|
||||||
case 'error': return 'bg-primary-danger'
|
|
||||||
default: return 'bg-gray-500'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -1053,10 +582,10 @@ const statusClass = (status: string) => {
|
|||||||
<span class="font-semibold text-white text-sm">Explorer</span>
|
<span class="font-semibold text-white text-sm">Explorer</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="showNewFolderInput" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New Folder">
|
<button @click="showInlineNewFolderInput(null)" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New Folder">
|
||||||
<i class="fa-solid fa-folder-plus text-sm"></i>
|
<i class="fa-solid fa-folder-plus text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="showNewFileInput(null)" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New File">
|
<button @click="showInlineNewFileInput(null)" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New File">
|
||||||
<i class="fa-solid fa-file-circle-plus text-sm"></i>
|
<i class="fa-solid fa-file-circle-plus text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
<label class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors cursor-pointer" title="Upload File">
|
<label class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors cursor-pointer" title="Upload File">
|
||||||
@@ -1090,6 +619,50 @@ const statusClass = (status: string) => {
|
|||||||
<i class="fa-solid fa-trash text-xs"></i>
|
<i class="fa-solid fa-trash text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 根目录新建文件夹/文件按钮 -->
|
||||||
|
<div v-if="inlineCreatingFolderId === 'root'" class="flex items-center gap-2 px-2 py-1">
|
||||||
|
<i class="fa-solid fa-folder text-yellow-400 text-sm"></i>
|
||||||
|
<input
|
||||||
|
v-model="inlineNewFolderName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Folder name"
|
||||||
|
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||||||
|
@keyup.enter="confirmInlineCreateFolder"
|
||||||
|
@blur="confirmInlineCreateFolder"
|
||||||
|
@keyup.escape="cancelInlineCreateFolder"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="inlineCreatingFileId === 'root'" class="flex items-center gap-2 px-2 py-1">
|
||||||
|
<i class="fa-solid fa-file text-gray-400 text-sm"></i>
|
||||||
|
<input
|
||||||
|
v-model="inlineNewFileName"
|
||||||
|
type="text"
|
||||||
|
placeholder="File name"
|
||||||
|
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||||||
|
@keyup.enter="confirmInlineCreateFile"
|
||||||
|
@blur="confirmInlineCreateFile"
|
||||||
|
@keyup.escape="cancelInlineCreateFile"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2 px-2 py-1">
|
||||||
|
<button
|
||||||
|
@click="showInlineNewFolderInput(null)"
|
||||||
|
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-folder-plus"></i>
|
||||||
|
<span>New Folder</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="showInlineNewFileInput(null)"
|
||||||
|
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-file-circle-plus"></i>
|
||||||
|
<span>New File</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件夹列表 -->
|
<!-- 文件夹列表 -->
|
||||||
@@ -1130,7 +703,7 @@ const statusClass = (status: string) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件夹内的文件 -->
|
<!-- 文件夹内的文件 -->
|
||||||
<div v-if="folder.expanded" class="ml-4">
|
<div v-if="folder.expanded || selectedFolder === folder.id" class="ml-4">
|
||||||
<div
|
<div
|
||||||
v-for="child in folder.children"
|
v-for="child in folder.children"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
@@ -1150,25 +723,54 @@ const statusClass = (status: string) => {
|
|||||||
<i class="fa-solid fa-trash text-xs"></i>
|
<i class="fa-solid fa-trash text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文件夹内新建文件夹/文件/上传按钮 -->
|
<!-- 文件夹内新建文件夹/文件按钮或内联输入框 -->
|
||||||
<div class="flex items-center gap-2 px-2 py-1">
|
<div v-if="inlineCreatingFolderId === folder.id" class="flex items-center gap-2 px-2 py-1">
|
||||||
|
<i class="fa-solid fa-folder text-yellow-400 text-xs"></i>
|
||||||
|
<input
|
||||||
|
v-model="inlineNewFolderName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Folder name"
|
||||||
|
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||||||
|
@keyup.enter="confirmInlineCreateFolder"
|
||||||
|
@blur="confirmInlineCreateFolder"
|
||||||
|
@keyup.escape="cancelInlineCreateFolder"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="inlineCreatingFileId === folder.id" class="flex items-center gap-2 px-2 py-1">
|
||||||
|
<i class="fa-solid fa-file text-gray-400 text-xs"></i>
|
||||||
|
<input
|
||||||
|
v-model="inlineNewFileName"
|
||||||
|
type="text"
|
||||||
|
placeholder="File name"
|
||||||
|
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||||||
|
@keyup.enter="confirmInlineCreateFile"
|
||||||
|
@blur="confirmInlineCreateFile"
|
||||||
|
@keyup.escape="cancelInlineCreateFile"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2 px-2 py-1">
|
||||||
<button
|
<button
|
||||||
@click.stop="showNewFolderInput(folder.id)"
|
@click.stop="showInlineNewFolderInput(folder.id)"
|
||||||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors opacity-0 group-hover:opacity-100"
|
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||||||
|
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
|
||||||
title="New Folder"
|
title="New Folder"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-folder-plus"></i>
|
<i class="fa-solid fa-folder-plus"></i>
|
||||||
<span>New Folder</span>
|
<span>New Folder</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.stop="showNewFileInput(folder.id)"
|
@click.stop="showInlineNewFileInput(folder.id)"
|
||||||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors opacity-0 group-hover:opacity-100"
|
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||||||
|
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
<span>New File</span>
|
<span>New File</span>
|
||||||
</button>
|
</button>
|
||||||
<label
|
<label
|
||||||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors cursor-pointer opacity-0 group-hover:opacity-100"
|
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors cursor-pointer"
|
||||||
|
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
|
||||||
title="Upload File"
|
title="Upload File"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-upload"></i>
|
<i class="fa-solid fa-upload"></i>
|
||||||
@@ -1180,38 +782,6 @@ const statusClass = (status: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新建文件夹输入框 -->
|
|
||||||
<div v-if="isCreatingFolder" class="p-2 border-t border-dark-600 bg-dark-800">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-folder text-yellow-400 text-sm"></i>
|
|
||||||
<input
|
|
||||||
v-model="newFolderName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Folder name..."
|
|
||||||
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-1 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
|
||||||
@keyup.enter="createFolder"
|
|
||||||
@blur="createFolder"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 新建文件输入框 -->
|
|
||||||
<div v-if="isCreatingFile" class="p-2 border-t border-dark-600 bg-dark-800">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-file text-gray-400 text-sm"></i>
|
|
||||||
<input
|
|
||||||
v-model="newFileName"
|
|
||||||
type="text"
|
|
||||||
placeholder="File name..."
|
|
||||||
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-1 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
|
||||||
@keyup.enter="createFile"
|
|
||||||
@blur="createFile"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部操作 -->
|
<!-- 底部操作 -->
|
||||||
<div class="p-3 border-t border-dark-600">
|
<div class="p-3 border-t border-dark-600">
|
||||||
<button
|
<button
|
||||||
9
web/src/views/Team.vue
Normal file
9
web/src/views/Team.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-white mb-4">Team</h1>
|
||||||
|
<p class="text-gray-400">Team management page - Coming soon</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -95,6 +95,27 @@
|
|||||||
border: 1px solid #374151;
|
border: 1px solid #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文件上传弹窗特殊样式 - 覆盖 kb-dialog */
|
||||||
|
.file-upload-dialog.kb-dialog :deep(.el-dialog) {
|
||||||
|
background-color: #1f2937;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-dialog.kb-dialog :deep(.el-dialog__header) {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-dialog.kb-dialog :deep(.el-dialog__title) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-dialog.kb-dialog :deep(.el-dialog__body) {
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0f1419;
|
||||||
|
}
|
||||||
|
|
||||||
.kb-dialog :deep(.el-dialog__header) {
|
.kb-dialog :deep(.el-dialog__header) {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
border-bottom: 1px solid #374151;
|
border-bottom: 1px solid #374151;
|
||||||
@@ -575,23 +596,27 @@
|
|||||||
|
|
||||||
/* 文件上传弹窗布局 */
|
/* 文件上传弹窗布局 */
|
||||||
.file-upload-dialog .el-dialog {
|
.file-upload-dialog .el-dialog {
|
||||||
position: absolute !important;
|
margin: auto !important;
|
||||||
top: 8px !important;
|
top: 20px !important;
|
||||||
left: 50% !important;
|
bottom: 20px !important;
|
||||||
transform: translateX(-50%) !important;
|
height: auto !important;
|
||||||
margin-bottom: 8px !important;
|
max-height: calc(100vh - 40px);
|
||||||
max-height: calc(100vh - 16px);
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-upload-dialog .el-dialog__body {
|
.file-upload-dialog .el-dialog__body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-upload-layout {
|
.file-upload-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 85vh;
|
height: 100%;
|
||||||
background-color: #0f1419;
|
background-color: #0f1419;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部导航 */
|
/* 顶部导航 */
|
||||||
@@ -669,6 +694,26 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #6b7280;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state i,
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
.file-item {
|
.file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -744,10 +789,41 @@
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-item-status .status-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
.file-item-status.parsing i {
|
.file-item-status.parsing i {
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-item-delete {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover .file-item-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item-delete:hover {
|
||||||
|
background-color: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
/* 右侧预览面板 */
|
/* 右侧预览面板 */
|
||||||
.file-preview {
|
.file-preview {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -756,6 +832,47 @@
|
|||||||
background-color: #121218;
|
background-color: #121218;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #6b7280;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-empty i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-empty span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-loading,
|
||||||
|
.preview-no-file {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #6b7280;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-loading i,
|
||||||
|
.preview-no-file i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-loading span,
|
||||||
|
.preview-no-file span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-header {
|
.preview-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -817,7 +934,8 @@
|
|||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-iframe {
|
.pdf-iframe,
|
||||||
|
.pdf-embed {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
200
web/src/views/knowledge/useKnowledge.ts
Normal file
200
web/src/views/knowledge/useKnowledge.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// Knowledge Base API
|
||||||
|
|
||||||
|
const API_BASE = 'http://localhost:8082'
|
||||||
|
|
||||||
|
export interface KnowledgeBase {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
llm_model_id: string
|
||||||
|
embedding_model_id: string
|
||||||
|
parsing_config: {
|
||||||
|
engine: string
|
||||||
|
docling_url?: string
|
||||||
|
enable_pdf: boolean
|
||||||
|
pandoc: boolean
|
||||||
|
}
|
||||||
|
status: string
|
||||||
|
document_count: number
|
||||||
|
chunk_count: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeDocument {
|
||||||
|
id: string
|
||||||
|
knowledge_base_id: string
|
||||||
|
name: string
|
||||||
|
file_key?: string
|
||||||
|
file_url?: string
|
||||||
|
file_size: number
|
||||||
|
status: string
|
||||||
|
chunk_count: number
|
||||||
|
uploaded_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取知识库列表
|
||||||
|
export const fetchKnowledgeBases = async (): Promise<KnowledgeBase[]> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/list`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Server error:', response.status)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text()
|
||||||
|
if (!text) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
if (data.success) {
|
||||||
|
return data.data || []
|
||||||
|
}
|
||||||
|
console.error('API error:', data.message)
|
||||||
|
return []
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Failed to parse JSON:', text)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch knowledge bases:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建知识库
|
||||||
|
export const createKnowledgeBase = async (params: {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
llm_model_id: string
|
||||||
|
embedding_model_id: string
|
||||||
|
parsing_config: {
|
||||||
|
engine: string
|
||||||
|
docling_url?: string
|
||||||
|
enable_pdf?: boolean
|
||||||
|
pandoc?: boolean
|
||||||
|
}
|
||||||
|
}): Promise<{ success: boolean; id?: string; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if response is OK
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
return { success: false, message: `Server error: ${response.status} - ${errorText}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text()
|
||||||
|
if (!text) {
|
||||||
|
return { success: false, message: 'Empty response from server' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
return data
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Failed to parse JSON:', text)
|
||||||
|
return { success: false, message: `Invalid JSON response: ${text}` }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create knowledge base:', error)
|
||||||
|
return { success: false, message: `Network error: ${error}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除知识库
|
||||||
|
export const deleteKnowledgeBase = async (id: string): Promise<{ success: boolean; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete knowledge base:', error)
|
||||||
|
return { success: false, message: 'Failed to delete knowledge base' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取知识库下的文档列表
|
||||||
|
export const fetchKnowledgeDocuments = async (kbId: string, status?: string): Promise<KnowledgeDocument[]> => {
|
||||||
|
try {
|
||||||
|
const query = status && status !== 'all' ? `?status=${status}` : ''
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents${query}`)
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
throw new Error(data.message || 'Failed to fetch documents')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch documents:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文档到知识库
|
||||||
|
export const uploadDocument = async (kbId: string, file: File): Promise<{ success: boolean; id?: string; url?: string; document?: KnowledgeDocument; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to upload document:', error)
|
||||||
|
return { success: false, message: 'Failed to upload document' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除知识库文档
|
||||||
|
export const deleteDocument = async (kbId: string, docId: string): Promise<{ success: boolean; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete document:', error)
|
||||||
|
return { success: false, message: 'Failed to delete document' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新解析文档
|
||||||
|
export const reparseDocument = async (kbId: string, docId: string): Promise<{ success: boolean; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}/reparse`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reparse document:', error)
|
||||||
|
return { success: false, message: 'Failed to reparse document' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档预览内容
|
||||||
|
export const getDocumentPreview = async (kbId: string, docId: string, page: number = 1): Promise<{ success: boolean; data?: { total_pages: number; current_page: number; content: string }; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}/preview?page=${page}`)
|
||||||
|
const data = await response.json()
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get document preview:', error)
|
||||||
|
return { success: false, message: 'Failed to get document preview' }
|
||||||
|
}
|
||||||
|
}
|
||||||
46
web/src/views/skill/types.ts
Normal file
46
web/src/views/skill/types.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Skill page types
|
||||||
|
|
||||||
|
export interface MCPServer {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
status: 'running' | 'stopped' | 'error'
|
||||||
|
type: string
|
||||||
|
port: number
|
||||||
|
createdAt: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon?: string
|
||||||
|
content?: string
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FolderItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon?: string
|
||||||
|
type: 'folder'
|
||||||
|
expanded?: boolean
|
||||||
|
children: (FileItem | FolderItem)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowNode {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
color: string
|
||||||
|
bgColor: string
|
||||||
|
borderColor: string
|
||||||
|
status: 'pending' | 'processing' | 'completed'
|
||||||
|
expanded: boolean
|
||||||
|
children?: WorkflowNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
340
web/src/views/skill/useFileTree.ts
Normal file
340
web/src/views/skill/useFileTree.ts
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import type { FileItem, FolderItem } from './types'
|
||||||
|
|
||||||
|
// 默认的 SKILL.md 内容
|
||||||
|
const DEFAULT_SKILL_CONTENT = `# Skill Configuration
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This skill is configured to process data and generate outputs.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Python 3.8+
|
||||||
|
- Node.js 16+
|
||||||
|
`
|
||||||
|
|
||||||
|
export function useFileTree() {
|
||||||
|
// 文件树数据
|
||||||
|
const fileTree = ref<FolderItem[]>([
|
||||||
|
{
|
||||||
|
id: 'folder-1',
|
||||||
|
name: 'Data',
|
||||||
|
icon: 'fa-folder',
|
||||||
|
type: 'folder',
|
||||||
|
expanded: true,
|
||||||
|
children: [
|
||||||
|
{ id: 'file-1', name: 'user_data.csv', icon: 'fa-file', content: 'id,name,email,age\n1,John Doe,john@example.com,28\n2,Jane Smith,jane@example.com,32', type: 'file' },
|
||||||
|
{ id: 'file-2', name: 'config.json', icon: 'fa-code', content: '{\n "appName": "MyApp",\n "version": "1.0.0",\n "debug": true\n}', type: 'file' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'folder-2',
|
||||||
|
name: 'Script',
|
||||||
|
icon: 'fa-folder',
|
||||||
|
type: 'folder',
|
||||||
|
expanded: true,
|
||||||
|
children: [
|
||||||
|
{ id: 'file-3', name: 'process.py', icon: 'fa-code', content: 'import pandas as pd\n\ndef process_data(data):\n """Process the input data"""\n return data.apply(lambda x: x.strip())\n\nif __name__ == "__main__":\n df = pd.read_csv("user_data.csv")\n processed = process_data(df)\n processed.to_csv("output.csv", index=False)', type: 'file' },
|
||||||
|
{ id: 'file-4', name: 'transform.js', icon: 'fa-code', content: 'function transformData(input) {\n return input.map(item => ({\n ...item,\n processed: true,\n timestamp: Date.now()\n }));\n}\n\nmodule.exports = { transformData };', type: 'file' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'folder-3',
|
||||||
|
name: 'Reference',
|
||||||
|
icon: 'fa-folder',
|
||||||
|
type: 'folder',
|
||||||
|
expanded: true,
|
||||||
|
children: [
|
||||||
|
{ id: 'file-5', name: 'api_docs.md', icon: 'fa-code', content: '# API Documentation\n\n## Endpoints\n\n### GET /api/users\nReturns list of users.\n\n### POST /api/users\nCreate a new user.', type: 'file' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 根目录文件
|
||||||
|
const rootFiles = ref<FileItem[]>([
|
||||||
|
{ id: 'file-root-1', name: 'SKILL.md', icon: 'fa-file', content: DEFAULT_SKILL_CONTENT, type: 'file' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 展开/收起文件夹
|
||||||
|
const toggleFolder = (folder: FolderItem) => {
|
||||||
|
folder.expanded = !folder.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件夹 - 内联输入模式
|
||||||
|
const inlineNewFolderName = ref('')
|
||||||
|
const inlineCreatingFolderId = ref<string | null>(null)
|
||||||
|
|
||||||
|
const showInlineNewFolderInput = (parentId: string | null = null) => {
|
||||||
|
inlineNewFolderName.value = ''
|
||||||
|
inlineCreatingFolderId.value = parentId === null ? 'root' : parentId
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmInlineCreateFolder = () => {
|
||||||
|
if (!inlineNewFolderName.value.trim()) {
|
||||||
|
inlineCreatingFolderId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFolder: FolderItem = {
|
||||||
|
id: 'folder-' + Date.now(),
|
||||||
|
name: inlineNewFolderName.value,
|
||||||
|
icon: 'fa-folder',
|
||||||
|
type: 'folder',
|
||||||
|
expanded: true,
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inlineCreatingFolderId.value && inlineCreatingFolderId.value !== 'root') {
|
||||||
|
const parentFolder = findFolderById(fileTree.value, inlineCreatingFolderId.value)
|
||||||
|
if (parentFolder && parentFolder.children) {
|
||||||
|
parentFolder.children.push(newFolder)
|
||||||
|
// 选中新创建的文件夹
|
||||||
|
selectedFolder.value = newFolder.id
|
||||||
|
parentFolder.expanded = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileTree.value.push(newFolder)
|
||||||
|
// 选中新创建的文件夹
|
||||||
|
selectedFolder.value = newFolder.id
|
||||||
|
}
|
||||||
|
inlineCreatingFolderId.value = null
|
||||||
|
inlineNewFolderName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelInlineCreateFolder = () => {
|
||||||
|
inlineCreatingFolderId.value = null
|
||||||
|
inlineNewFolderName.value = ''
|
||||||
|
// 恢复选中之前选中的文件夹
|
||||||
|
if (selectedFolder.value) {
|
||||||
|
const folder = findFolderById(fileTree.value, selectedFolder.value)
|
||||||
|
if (folder) {
|
||||||
|
folder.expanded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找文件夹
|
||||||
|
const findFolderById = (folders: (FileItem | FolderItem)[], id: string): FolderItem | null => {
|
||||||
|
for (const folder of folders) {
|
||||||
|
if ('children' in folder && folder.id === id) return folder as FolderItem
|
||||||
|
if ('children' in folder && folder.children) {
|
||||||
|
const found = findFolderById(folder.children, id)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件 - 内联输入模式
|
||||||
|
const inlineNewFileName = ref('')
|
||||||
|
const inlineCreatingFileId = ref<string | null>(null)
|
||||||
|
|
||||||
|
const showInlineNewFileInput = (folderId: string | null = null) => {
|
||||||
|
inlineNewFileName.value = ''
|
||||||
|
inlineCreatingFileId.value = folderId === null ? 'root' : folderId
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmInlineCreateFile = () => {
|
||||||
|
if (!inlineNewFileName.value.trim()) {
|
||||||
|
inlineCreatingFileId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFile: FileItem = {
|
||||||
|
id: 'file-' + Date.now(),
|
||||||
|
name: inlineNewFileName.value,
|
||||||
|
icon: getFileIconByName(inlineNewFileName.value),
|
||||||
|
content: '',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inlineCreatingFileId.value && inlineCreatingFileId.value !== 'root') {
|
||||||
|
const folder = findFolderById(fileTree.value, inlineCreatingFileId.value)
|
||||||
|
if (folder && folder.children) {
|
||||||
|
folder.children.push(newFile)
|
||||||
|
// 选中父文件夹
|
||||||
|
selectedFolder.value = folder.id
|
||||||
|
folder.expanded = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootFiles.value.push(newFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineCreatingFileId.value = null
|
||||||
|
inlineNewFileName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelInlineCreateFile = () => {
|
||||||
|
inlineCreatingFileId.value = null
|
||||||
|
inlineNewFileName.value = ''
|
||||||
|
// 恢复选中之前选中的文件夹
|
||||||
|
if (selectedFolder.value) {
|
||||||
|
const folder = findFolderById(fileTree.value, selectedFolder.value)
|
||||||
|
if (folder) {
|
||||||
|
folder.expanded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据文件名获取图标
|
||||||
|
const getFileIconByName = (filename: string): string => {
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase()
|
||||||
|
const codeExtensions = ['js', 'ts', 'py', 'html', 'css', 'json', 'xml', 'yaml', 'yml', 'java', 'c', 'cpp', 'go', 'rs', 'php', 'rb', 'swift', 'kt', 'sql', 'sh', 'bash', 'md']
|
||||||
|
if (codeExtensions.includes(ext || '')) {
|
||||||
|
return 'fa-code'
|
||||||
|
}
|
||||||
|
return 'fa-file'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
const handleFileUpload = (event: Event, folderId: string | null = null) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
if (!input.files?.length) return
|
||||||
|
|
||||||
|
Array.from(input.files).forEach(file => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const newFile: FileItem = {
|
||||||
|
id: 'file-' + Date.now() + Math.random(),
|
||||||
|
name: file.name,
|
||||||
|
icon: getFileIconByName(file.name),
|
||||||
|
content: e.target?.result as string || '',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folderId) {
|
||||||
|
const folder = findFolderById(fileTree.value, folderId)
|
||||||
|
if (folder && folder.children) {
|
||||||
|
folder.children.push(newFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootFiles.value.push(newFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找文件
|
||||||
|
const findFile = (items: (FileItem | FolderItem)[], fileId: string): FileItem | null => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type === 'file' && item.id === fileId) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if (item.type === 'folder' && item.children) {
|
||||||
|
const found = findFile(item.children, fileId)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件编辑相关状态
|
||||||
|
const isEditingFile = ref(false)
|
||||||
|
const isEditingFileClosing = ref(false)
|
||||||
|
const editingFile = ref<FileItem | null>(null)
|
||||||
|
const editingFileName = ref('')
|
||||||
|
const editingFileContent = ref('')
|
||||||
|
const editingFileType = ref('')
|
||||||
|
|
||||||
|
// 选中文件/文件夹状态
|
||||||
|
const hoveringItem = ref<string | null>(null)
|
||||||
|
const selectedFolder = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 选中文件夹
|
||||||
|
const selectFolder = (folderId: string) => {
|
||||||
|
if (selectedFolder.value === folderId) {
|
||||||
|
selectedFolder.value = null
|
||||||
|
} else {
|
||||||
|
selectedFolder.value = folderId
|
||||||
|
const folder = findFolderById(fileTree.value, folderId)
|
||||||
|
if (folder) {
|
||||||
|
folder.expanded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
const deleteFile = (file: FileItem, parentId: string | null) => {
|
||||||
|
if (!parentId) {
|
||||||
|
rootFiles.value = rootFiles.value.filter(f => f.id !== file.id)
|
||||||
|
} else {
|
||||||
|
const folder = fileTree.value.find(f => f.id === parentId)
|
||||||
|
if (folder && folder.children) {
|
||||||
|
folder.children = folder.children.filter(c => (c as FileItem).id !== file.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件夹
|
||||||
|
const deleteFolder = (folder: FolderItem) => {
|
||||||
|
fileTree.value = fileTree.value.filter(f => f.id !== folder.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开文件编辑器
|
||||||
|
const openFile = (file: FileItem) => {
|
||||||
|
editingFile.value = file
|
||||||
|
editingFileName.value = file.name
|
||||||
|
editingFileContent.value = file.content || ''
|
||||||
|
editingFileType.value = 'file'
|
||||||
|
isEditingFile.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFileEdit = () => {
|
||||||
|
if (!editingFile.value) return
|
||||||
|
editingFile.value.content = editingFileContent.value
|
||||||
|
isEditingFileClosing.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
isEditingFile.value = false
|
||||||
|
isEditingFileClosing.value = false
|
||||||
|
editingFile.value = null
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelFileEdit = () => {
|
||||||
|
isEditingFileClosing.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
isEditingFile.value = false
|
||||||
|
isEditingFileClosing.value = false
|
||||||
|
editingFile.value = null
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
fileTree,
|
||||||
|
rootFiles,
|
||||||
|
inlineNewFolderName,
|
||||||
|
inlineCreatingFolderId,
|
||||||
|
inlineNewFileName,
|
||||||
|
inlineCreatingFileId,
|
||||||
|
isEditingFile,
|
||||||
|
isEditingFileClosing,
|
||||||
|
editingFile,
|
||||||
|
editingFileName,
|
||||||
|
editingFileContent,
|
||||||
|
editingFileType,
|
||||||
|
hoveringItem,
|
||||||
|
selectedFolder,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
toggleFolder,
|
||||||
|
showInlineNewFolderInput,
|
||||||
|
confirmInlineCreateFolder,
|
||||||
|
cancelInlineCreateFolder,
|
||||||
|
findFolderById,
|
||||||
|
showInlineNewFileInput,
|
||||||
|
confirmInlineCreateFile,
|
||||||
|
cancelInlineCreateFile,
|
||||||
|
getFileIconByName,
|
||||||
|
handleFileUpload,
|
||||||
|
findFile,
|
||||||
|
selectFolder,
|
||||||
|
deleteFile,
|
||||||
|
deleteFolder,
|
||||||
|
openFile,
|
||||||
|
saveFileEdit,
|
||||||
|
cancelFileEdit,
|
||||||
|
}
|
||||||
|
}
|
||||||
46
web/src/views/skill/useSkillChat.ts
Normal file
46
web/src/views/skill/useSkillChat.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import type { ChatMessage } from './types'
|
||||||
|
|
||||||
|
export function useSkillChat() {
|
||||||
|
// AI 对话面板
|
||||||
|
const isChatOpen = ref(false)
|
||||||
|
|
||||||
|
// 对话相关
|
||||||
|
const chatInput = ref('')
|
||||||
|
const chatMessages = ref<ChatMessage[]>([])
|
||||||
|
|
||||||
|
const toggleChat = () => {
|
||||||
|
isChatOpen.value = !isChatOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (!chatInput.value.trim()) return
|
||||||
|
|
||||||
|
chatMessages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
text: chatInput.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const userInput = chatInput.value
|
||||||
|
chatInput.value = ''
|
||||||
|
|
||||||
|
// 模拟 AI 回复
|
||||||
|
setTimeout(() => {
|
||||||
|
chatMessages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
text: `I've updated the configuration based on your request: "${userInput}". The changes have been applied to your skill settings.`
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
isChatOpen,
|
||||||
|
chatInput,
|
||||||
|
chatMessages,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
toggleChat,
|
||||||
|
sendMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
114
web/src/views/skill/useSkillServers.ts
Normal file
114
web/src/views/skill/useSkillServers.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import type { MCPServer } from './types'
|
||||||
|
|
||||||
|
export function useSkillServers() {
|
||||||
|
// MCP Server 列表
|
||||||
|
const mcpServers = ref<MCPServer[]>([
|
||||||
|
{ id: 1, name: 'linear-demo', status: 'running', type: 'Linear', port: 3001, createdAt: '2025-04-10', description: 'Linear API integration for project management' },
|
||||||
|
{ id: 2, name: 'google-maps', status: 'running', type: 'Google Maps', port: 3002, createdAt: '2025-04-08', description: 'Google Maps API for location services' },
|
||||||
|
{ id: 3, name: 'explorer-mcp', status: 'error', type: 'File System', port: 3003, createdAt: '2025-04-05', description: 'File system explorer and editor' },
|
||||||
|
{ id: 4, name: 'postgres-mcp', status: 'running', type: 'PostgreSQL', port: 3004, createdAt: '2025-04-12', description: 'PostgreSQL database operations' },
|
||||||
|
{ id: 5, name: 'github-mcp', status: 'stopped', type: 'GitHub', port: 3005, createdAt: '2025-04-11', description: 'GitHub API integration' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 编辑状态
|
||||||
|
const editingServer = ref<MCPServer | null>(null)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
|
||||||
|
// 搜索和筛选
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const filterStatus = ref<string>('all')
|
||||||
|
|
||||||
|
// 编辑表单
|
||||||
|
const editForm = ref({
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
port: 3000,
|
||||||
|
description: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开编辑弹窗
|
||||||
|
const openEdit = (server: MCPServer) => {
|
||||||
|
editingServer.value = server
|
||||||
|
editForm.value = {
|
||||||
|
name: server.name,
|
||||||
|
type: server.type,
|
||||||
|
port: server.port,
|
||||||
|
description: server.description,
|
||||||
|
}
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存编辑
|
||||||
|
const saveEdit = () => {
|
||||||
|
if (editingServer.value) {
|
||||||
|
const index = mcpServers.value.findIndex(s => s.id === editingServer.value!.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
mcpServers.value[index] = {
|
||||||
|
...mcpServers.value[index],
|
||||||
|
...editForm.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
const cancelEdit = () => {
|
||||||
|
isEditing.value = false
|
||||||
|
editingServer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换状态
|
||||||
|
const toggleStatus = (server: MCPServer) => {
|
||||||
|
if (server.status === 'running') {
|
||||||
|
server.status = 'stopped'
|
||||||
|
} else if (server.status === 'stopped') {
|
||||||
|
server.status = 'running'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除服务器
|
||||||
|
const deleteServer = (id: number) => {
|
||||||
|
mcpServers.value = mcpServers.value.filter(s => s.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选服务器
|
||||||
|
const filteredServers = () => {
|
||||||
|
return mcpServers.value.filter(server => {
|
||||||
|
const matchSearch = server.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||||
|
server.type.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
const matchStatus = filterStatus.value === 'all' || server.status === filterStatus.value
|
||||||
|
return matchSearch && matchStatus
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态样式
|
||||||
|
const statusClass = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'running': return 'bg-primary-success'
|
||||||
|
case 'stopped': return 'bg-gray-500'
|
||||||
|
case 'error': return 'bg-primary-danger'
|
||||||
|
default: return 'bg-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
mcpServers,
|
||||||
|
editingServer,
|
||||||
|
isEditing,
|
||||||
|
searchQuery,
|
||||||
|
filterStatus,
|
||||||
|
editForm,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
openEdit,
|
||||||
|
saveEdit,
|
||||||
|
cancelEdit,
|
||||||
|
toggleStatus,
|
||||||
|
deleteServer,
|
||||||
|
filteredServers,
|
||||||
|
statusClass,
|
||||||
|
}
|
||||||
|
}
|
||||||
136
web/src/views/skill/useWorkflow.ts
Normal file
136
web/src/views/skill/useWorkflow.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface WorkflowNode {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
color: string
|
||||||
|
bgColor: string
|
||||||
|
borderColor: string
|
||||||
|
status: 'pending' | 'processing' | 'completed'
|
||||||
|
expanded: boolean
|
||||||
|
children?: WorkflowNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkflow() {
|
||||||
|
// 流程生成相关
|
||||||
|
const isGeneratingGraph = ref(false)
|
||||||
|
const isGraphGenerated = ref(false)
|
||||||
|
|
||||||
|
const workflowData = ref<WorkflowNode[]>([
|
||||||
|
{
|
||||||
|
id: 'output',
|
||||||
|
name: '最终输出',
|
||||||
|
description: '生成最终结果返回给用户',
|
||||||
|
icon: 'fa-check-circle',
|
||||||
|
color: 'text-green-400',
|
||||||
|
bgColor: 'from-green-500/20 to-green-500/5',
|
||||||
|
borderColor: 'border-green-500/30',
|
||||||
|
status: 'pending',
|
||||||
|
expanded: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'generate',
|
||||||
|
name: '方案生成',
|
||||||
|
description: '基于分析结果构建解决方案',
|
||||||
|
icon: 'fa-lightbulb',
|
||||||
|
color: 'text-primary-orange',
|
||||||
|
bgColor: 'from-primary-orange/20 to-primary-orange/5',
|
||||||
|
borderColor: 'border-primary-orange/30',
|
||||||
|
status: 'pending',
|
||||||
|
expanded: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'analyze',
|
||||||
|
name: '问题分析',
|
||||||
|
description: '拆解问题、提取关键信息、理解用户意图',
|
||||||
|
icon: 'fa-microscope',
|
||||||
|
color: 'text-purple-400',
|
||||||
|
bgColor: 'from-purple-500/20 to-purple-500/5',
|
||||||
|
borderColor: 'border-purple-500/30',
|
||||||
|
status: 'pending',
|
||||||
|
expanded: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'question',
|
||||||
|
name: '用户问题',
|
||||||
|
description: '输入原始需求和问题描述',
|
||||||
|
icon: 'fa-question',
|
||||||
|
color: 'text-primary-cyan',
|
||||||
|
bgColor: 'from-primary-cyan/20 to-primary-cyan/5',
|
||||||
|
borderColor: 'border-primary-cyan/30',
|
||||||
|
status: 'pending',
|
||||||
|
expanded: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data',
|
||||||
|
name: '数据获取',
|
||||||
|
description: '从知识库、向量数据库获取相关信息',
|
||||||
|
icon: 'fa-database',
|
||||||
|
color: 'text-green-400',
|
||||||
|
bgColor: 'from-green-500/20 to-green-500/5',
|
||||||
|
borderColor: 'border-green-500/30',
|
||||||
|
status: 'pending',
|
||||||
|
expanded: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evaluate',
|
||||||
|
name: '方案评估',
|
||||||
|
description: '验证方案可行性和效果',
|
||||||
|
icon: 'fa-clipboard-check',
|
||||||
|
color: 'text-yellow-400',
|
||||||
|
bgColor: 'from-yellow-500/20 to-yellow-500/5',
|
||||||
|
borderColor: 'border-yellow-500/30',
|
||||||
|
status: 'pending',
|
||||||
|
expanded: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 展开/收起节点
|
||||||
|
const toggleNode = (node: WorkflowNode) => {
|
||||||
|
node.expanded = !node.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归渲染节点
|
||||||
|
const renderWorkflow = (nodes: WorkflowNode[], level: number = 0) => {
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateGraph = () => {
|
||||||
|
isGeneratingGraph.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
isGeneratingGraph.value = false
|
||||||
|
isGraphGenerated.value = true
|
||||||
|
if (workflowData.value.length > 0) {
|
||||||
|
workflowData.value[0].expanded = true
|
||||||
|
if (workflowData.value[0].children) {
|
||||||
|
workflowData.value[0].children.forEach(child => {
|
||||||
|
child.expanded = false
|
||||||
|
if (child.children) {
|
||||||
|
child.children.forEach(gc => gc.expanded = false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
workflowData,
|
||||||
|
isGeneratingGraph,
|
||||||
|
isGraphGenerated,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
toggleNode,
|
||||||
|
renderWorkflow,
|
||||||
|
generateGraph,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
Reference in New Issue
Block a user