Commit 1217d4e3 by 彭顺

feat: new version of llm2docx

parent 2aac4fcd
No preview for this file type
No preview for this file type
import json
from pathlib import Path
import requests
import copy
import subprocess
from typing import Optional, Dict, Any
from fastapi import HTTPException
import os
async def convert_doc_to_docx(input_path: Path, output_dir: Path) -> Optional[Path]:
"""
使用LibreOffice将DOC文件转换为DOCX
:param input_path: 输入文件路径
:param output_dir: 输出目录
:return: 转换后的文件路径,失败返回None
"""
try:
# 获取LibreOffice路径,从配置中读取或使用默认值
libreoffice_path = os.getenv("LIBREOFFICE_PATH", "/usr/bin/soffice")
command = [
libreoffice_path,
"--headless",
"--convert-to",
"docx",
"--outdir",
str(output_dir),
str(input_path)
]
result = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=10000
)
if result.returncode != 0:
error_msg = result.stderr.decode('utf-8')
raise RuntimeError(f"转换失败: {error_msg}")
# 获取转换后的文件名
output_filename = input_path.stem + ".docx"
output_path = output_dir / output_filename
if not output_path.exists():
raise FileNotFoundError("转换后的文件未找到")
return output_path
except subprocess.TimeoutExpired:
raise HTTPException(status_code=500, detail="doc文件转换超时")
except Exception as e:
raise HTTPException(status_code=500, detail=f"doc文件转换错误: {str(e)}")
import uvicorn
from fastapi import FastAPI, HTTPException, Form
from pathlib import Path
import shutil
import tempfile
import os
import httpx
from urllib.parse import urlparse
from markitdown import MarkItDown
from core import convert_doc_to_docx
from typing import List
import ast
app = FastAPI()
async def download_file_from_url(url: str, save_path: Path) -> bool:
"""从URL下载文件到本地"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=30)
if response.status_code == 200:
with open(save_path, "wb") as f:
f.write(response.content)
return True
return False
except Exception as e:
raise HTTPException(status_code=400, detail=f"文件下载失败: {str(e)}")
@app.post("/file2text/")
async def file_to_text(file_urls: List[str] = Form(...)):
"""
从多个URL下载文件并使用MarkItDown解析为Markdown文本
:param file_urls: 文件URL列表
:return: 拼接后的文件内容,格式为:
File: 文件名1
<Content>
文件内容1
</Content>
File: 文件名2
<Content>
文件内容2
</Content>
"""
temp_dir = Path(tempfile.mkdtemp())
combined_content = []
success_count = 0
if isinstance(file_urls[0], str):
try:
file_urls = ast.literal_eval(file_urls[0])
except (ValueError, SyntaxError):
# 如果转换失败,假设它是一个单独的URL字符串
file_urls = [file_urls]
try:
for file_url in file_urls:
try:
# 从URL下载文件
parsed_url = urlparse(file_url)
filename = os.path.basename(parsed_url.path)
file_path = temp_dir / filename
if not await download_file_from_url(file_url, file_path):
combined_content.append(f"File: {filename}\n<Error>文件下载失败</Error>\n")
continue
# 检查文件后缀是否为.doc
if filename.lower().endswith('.doc'):
try:
# 转换为.docx
converted_path = await convert_doc_to_docx(file_path, temp_dir)
if not converted_path:
combined_content.append(f"File: {filename}\n<Error>DOC文件转换失败</Error>\n")
continue
file_path = converted_path # 使用转换后的文件路径
filename = file_path.name # 更新为转换后的文件名
except Exception as e:
combined_content.append(f"File: {filename}\n<Error>DOC文件转换错误: {str(e)}</Error>\n")
continue
# 初始化MarkItDown
md_parser = MarkItDown(
enable_plugins=False
)
# 转换文件
result = md_parser.convert(str(file_path))
if not result:
combined_content.append(f"File: {filename}\n<Error>文件解析失败</Error>\n")
continue
# 添加成功处理的内容
combined_content.append(
f"File: {filename}\n"
f"<Content>\n"
f"{result.text_content}\n"
f"</Content>\n\n"
)
success_count += 1
except Exception as e:
combined_content.append(f"File: {filename}\n<Error>处理文件时发生错误: {str(e)}</Error>\n")
continue
# 检查是否有成功处理的文件
if success_count == 0:
print("INFO: ", combined_content)
raise HTTPException(status_code=400, detail="所有文件处理失败")
return {
"msg": f"成功处理 {success_count}/{len(file_urls)} 个文件",
"data": "".join(combined_content).strip()
}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"文件处理失败: {str(e)}")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=2386, reload=True)
\ No newline at end of file
jpai_llm2docx_v2 @ 5b982295
Subproject commit 5b982295d3a2e5e688559a1934a16b9a4881fc92
import httpx
from fastapi import HTTPException, Query, UploadFile, File, Form
from typing import Dict, Any, Optional, List
from pathlib import Path
import shutil
import tempfile
from urllib.parse import urlparse
import os
async def download_file_from_url(url: str, save_path: Path) -> bool:
"""从URL下载文件到本地"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=30)
if response.status_code == 200:
with open(save_path, "wb") as f:
f.write(response.content)
return True
return False
except Exception as e:
raise HTTPException(status_code=400, detail=f"文件下载失败: {str(e)}")
async def getline(
topic: Optional[str] = None,
file: Optional[List] = None,
stream: Optional[str] = None
) -> Dict[str, Any]:
"""
转发PPT大纲生成请求到指定API
:param topic: PPT主题内容
:param file: 上传文件(可选)
:param stream: 流式响应标志(可选)
:return: API响应结果
"""
url = "https://openai-api.islide.cc/api/presentation/generate/outline/ai/chat"
headers = {
"Authorization": "Bearer e792de4d-8278-47a8-a88f-b863717e9211"
}
# 准备表单数据 - 使用字典存储所有字段
form_data = {}
# 添加主题参数(可选)
if topic:
form_data["topic"] = (None, topic)
# 添加流式参数(可选)
if stream:
form_data["stream"] = (None, stream)
# 处理文件上传
temp_dir = Path(tempfile.mkdtemp())
file_handles = [] # 用于保存所有打开的文件句柄
try:
if file and len(file) > 0:
file_url = file[0]
# 保存临时文件
parsed_url = urlparse(file_url)
filename = os.path.basename(parsed_url.path)
file_path = temp_dir / filename
if not await download_file_from_url(file_url, file_path):
raise HTTPException(
status_code=500,
detail=f"文件下载失败: {file_url}"
)
f = open(file_path, "rb")
file_handles.append(f) # 保存文件句柄以便后续关闭
# 检查文件大小(20MB限制)
file_size = os.path.getsize(file_path)
if file_size > 20 * 1024 * 1024:
raise HTTPException(status_code=400, detail=f"文件大小超过20MB限制: {filename}")
form_data["file"] = (filename, f)
async with httpx.AsyncClient(timeout=300.0) as client:
response = await client.post(url, headers=headers, files=form_data)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"API请求失败,状态码: {response.status_code}"
)
return response.json()
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="请求超时")
except Exception as e:
raise HTTPException(status_code=500, detail=f"处理请求时发生错误: {str(e)}")
finally:
# 关闭所有打开的文件句柄
for f in file_handles:
try:
f.close()
except:
pass
# 清理临时文件
shutil.rmtree(temp_dir, ignore_errors=True)
async def get_themes(
start: Optional[int] = None,
size: Optional[int] = None,
keywords: Optional[str] = None,
ids: Optional[str] = None,
tags: Optional[str] = None
) -> Dict[str, Any]:
"""
转发获取主题资源请求到指定API
:param start: 从第几个资源开始
:param size: 获取主题资源的个数
:param keywords: 搜索关键字
:param ids: 需要查询的id(逗号分隔)
:param tags: 主题标签(逗号分隔)
:return: API响应结果
"""
url = "https://openai-api.islide.cc/api/presentation/theme"
headers = {
"Authorization": "Bearer e792de4d-8278-47a8-a88f-b863717e9211"
}
# 验证参数
if start is not None and start < 0:
raise HTTPException(status_code=400, detail="start参数必须大于等于0")
if size is not None and size <= 0:
raise HTTPException(status_code=400, detail="size参数必须大于0")
if keywords and len(keywords) > 100:
raise HTTPException(status_code=400, detail="keywords参数长度不能超过100字符")
if ids and len(ids.split(',')) > 100:
raise HTTPException(status_code=400, detail="ids参数最多只能包含100个ID")
# 准备查询参数
params = {}
if start is not None:
params["start"] = start
if size is not None:
params["size"] = size
if keywords:
params["keywords"] = keywords
if ids:
params["ids"] = ids
if tags:
params["tags"] = tags
try:
async with httpx.AsyncClient(timeout=300.0) as client:
response = await client.get(url, headers=headers, params=params)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"API请求失败,状态码: {response.status_code}"
)
return response.json()
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="请求超时")
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"请求发送失败: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"处理请求时发生错误: {str(e)}")
import uvicorn
import httpx
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Query
from typing import Dict, Any, Optional, Union, List
from core import get_themes, getline
from pydantic import BaseModel
import json
import re
from fastapi.responses import JSONResponse
app = FastAPI()
# 定义 JSON 请求体的模型
class GenerateOutlineRequest(BaseModel):
topic: Optional[str] = None # PPT主题(必填)
file: Optional[List[str]] = None # 上传文件URL列表(可选)
stream: Optional[str] = None # 流式响应标志(设置为"1"启用流式响应)
@app.post("/generate-outline/")
async def generate_outline(request: GenerateOutlineRequest):
"""
生成PPT大纲的接口
:param topic: PPT主题(必填)
:param file: 上传文件(可选)
:param stream: 流式响应标志(设置为"1"启用流式响应)
:return: 大纲数据
"""
try:
result = await getline(request.topic, request.file, request.stream)
return result
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/str2json/")
async def str2json(keyData: str = Form(...)):
# 尝试直接解析整个文本
try:
return JSONResponse(content=json.loads(keyData))
except json.JSONDecodeError:
pass
# 使用正则表达式提取JSON代码块
json_pattern = r'```json\n(.*?)\n```'
matches = re.findall(json_pattern, keyData, re.DOTALL)
if not matches:
# 如果没有找到json标记,尝试提取最外层的大括号内容
json_pattern = r'\{.*\}'
matches = re.findall(json_pattern, keyData, re.DOTALL)
if not matches:
return JSONResponse(content={"error": "No valid JSON found"}, status_code=400)
# 尝试解析每个匹配项, 直到成功为止
for match in matches:
try:
# 清理可能的缩进和换行
cleaned = re.sub(r'^\s+', '', match, flags=re.MULTILINE)
return JSONResponse(content=json.loads(cleaned))
except json.JSONDecodeError as e:
continue
return JSONResponse(content={"error": "No valid JSON found"}, status_code=400)
@app.get("/get-themes/")
async def get_presentation_themes(
start: Optional[int] = Query(None, ge=0, description="从第几个资源开始"),
size: Optional[int] = Query(10, gt=0, description="获取主题资源的个数"),
keywords: Optional[str] = Query(None, max_length=100, description="搜索关键字"),
ids: Optional[str] = Query(None, description="需要查询的id(逗号分隔)"),
tags: Optional[str] = Query(None, description="主题标签(逗号分隔)")
):
"""
获取主题资源接口
:param start: 从第几个资源开始(可选)
:param size: 获取主题资源的个数(可选,默认10)
:param keywords: 搜索关键字(可选)
:param ids: 需要查询的id(可选,传了ids后为id精确查询,其余字段不生效)
:param tags: 主题标签(可选)
:return: 主题资源数据
"""
try:
result = await get_themes(start, size, keywords, ids, tags)
return result
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=2486, reload=True)
\ No newline at end of file
server {
listen 2586;
server_name 192.168.252.71 218.77.58.8; ## 重要!!!修改成你的外网 IP/域名218.77.58.8
location /api/ { ## 后端项目 - 管理后台
proxy_pass https://openai-api.islide.cc/api/; ## 重要!!!proxy_pass 需要设置为后端项目所在服务器的 IP
proxy_set_header Host openai-api.islide.cc;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
proxy_send_timeout 60s;
proxy_connect_timeout 60s;
}
# 其他请求(如根路径)可返回404或自定义响应
location / {
return 404 "Not Found";
}
}
2.0.1.161 - - [02/Sep/2024:11:48:59 +0800] "GET / HTTP/1.1" 200 1173 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0
2.0.2.31 - - [26/May/2025:15:06:13 +0800] "POST /api/presentation/history HTTP/1.1" 403 238 "-" "Apifox/1.0.0 (https://apifox.com)"
2.0.2.31 - - [26/May/2025:15:15:48 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "Apifox/1.0.0 (https://apifox.com)"
172.17.0.4 - - [26/May/2025:17:28:01 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:19:43:00 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:19:46:38 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:19:50:30 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:19:57:15 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:19:58:08 +0800] "POST /api/presentation HTTP/1.1" 200 256 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:20:42:46 +0800] "GET /api/presentation/theme?start=0&size=10&keywords=%E8%93%9D%E8%89%B2%E7%A7%91%E6%8A%80&ids=&tags= HTTP/1.1" 401 44 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:20:43:46 +0800] "GET /api/presentation/theme?start=0&size=10&keywords=%E8%93%9D%E8%89%B2%E7%A7%91%E6%8A%80&ids=&tags= HTTP/1.1" 200 1738 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:20:55:12 +0800] "GET /api/presentation/theme?start=0&size=10&keywords=%E8%93%9D%E8%89%B2,%E7%A7%91%E6%8A%80%E6%84%9F&ids=&tags= HTTP/1.1" 200 2110 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:20:58:29 +0800] "GET /api/presentation/theme?start=0&size=10&keywords=%E8%93%9D%E8%89%B2%E7%A7%91%E6%8A%80%E6%84%9F&ids=&tags= HTTP/1.1" 200 2180 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:21:01:15 +0800] "GET /api/presentation/theme?start=&size=10&keywords=%E8%93%9D%E8%89%B2%E7%A7%91%E6%8A%80%E6%84%9F&ids=&tags= HTTP/1.1" 200 1924 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:21:03:18 +0800] "GET /api/presentation/theme?start=0&size=10&keywords=%E8%93%9D%E8%89%B2%E7%A7%91%E6%8A%80%E6%84%9F&ids=&tags= HTTP/1.1" 200 2108 "-" "axios/1.8.3"
172.17.0.4 - - [26/May/2025:21:05:57 +0800] "GET /api/presentation/theme?start=0&size=10&keywords=%E8%93%9D%E8%89%B2%E7%A7%91%E6%8A%80%E6%84%9F&ids=&tags= HTTP/1.1" 200 1841 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:09:44:25 +0800] "GET /api/presentation/themeTags HTTP/1.1" 200 799 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:09:44:25 +0800] "GET /api/presentation/diagramTags HTTP/1.1" 200 562 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:09:55:27 +0800] "GET /api/presentation/themeTags HTTP/1.1" 200 799 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:09:55:27 +0800] "GET /api/presentation/diagramTags HTTP/1.1" 200 562 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:03:36 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:04:43 +0800] "POST /api/presentation HTTP/1.1" 200 253 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:12:23 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:21:17 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:22:19 +0800] "POST /api/presentation HTTP/1.1" 200 255 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:36:19 +0800] "POST /api/presentation HTTP/1.1" 200 69 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:44:08 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:46:20 +0800] "GET /api/presentation/diagramTags HTTP/1.1" 200 562 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:46:20 +0800] "GET /api/presentation/themeTags HTTP/1.1" 200 799 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:15:56:16 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:16:12:31 +0800] "GET /api/presentation/theme?start=10&size=5&keywords=%E4%B8%BB%E9%A2%98%E6%9C%89%E5%93%AA%E4%BA%9B&ids=&tags= HTTP/1.1" 200 953 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:16:15:29 +0800] "GET /api/presentation/theme?start=5&size=5&keywords=%E5%85%AC%E5%8F%B8%E4%BB%8B%E7%BB%8DPPT%E4%B8%BB%E9%A2%98%E6%9C%89%E5%93%AA%E4%BA%9B&ids=&tags= HTTP/1.1" 200 1132 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:11:22 +0800] "POST /api/presentation HTTP/1.1" 200 79 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:15:40 +0800] "POST /api/presentation HTTP/1.1" 200 256 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:33:19 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:35:40 +0800] "GET /api/presentation/theme?start=10&size=5&keywords=%E5%88%97%E5%87%BA%E5%85%AC%E5%8F%B8%E4%BB%8B%E7%BB%8DPPT%E4%B8%BB%E9%A2%98%E9%A3%8E%E6%A0%BC&ids=&tags= HTTP/1.1" 200 1355 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:36:33 +0800] "POST /api/presentation HTTP/1.1" 200 253 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:43:01 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [27/May/2025:17:44:31 +0800] "POST /api/presentation HTTP/1.1" 200 255 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:09:19:00 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:09:20:37 +0800] "POST /api/presentation HTTP/1.1" 200 259 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:09:31:15 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:09:32:58 +0800] "POST /api/presentation HTTP/1.1" 200 101 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:09:40:34 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:09:41:59 +0800] "POST /api/presentation HTTP/1.1" 200 85 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:10:44:56 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:10:46:35 +0800] "POST /api/presentation HTTP/1.1" 200 85 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:10:51:45 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:10:53:20 +0800] "POST /api/presentation HTTP/1.1" 200 253 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:11:09:17 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:11:10:50 +0800] "POST /api/presentation HTTP/1.1" 200 85 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:11:14:35 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:11:22:52 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:11:24:27 +0800] "POST /api/presentation HTTP/1.1" 200 259 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:15:27:30 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:15:28:57 +0800] "POST /api/presentation HTTP/1.1" 200 257 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:15:49:19 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:15:52:04 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [28/May/2025:15:53:33 +0800] "POST /api/presentation HTTP/1.1" 200 253 "-" "axios/1.8.3"
172.17.0.4 - - [30/May/2025:15:05:04 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [30/May/2025:15:06:24 +0800] "POST /api/presentation HTTP/1.1" 200 251 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:10:32 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:12:17 +0800] "POST /api/presentation HTTP/1.1" 200 254 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:19:29 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:21:05 +0800] "POST /api/presentation HTTP/1.1" 200 253 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:22:59 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:24:33 +0800] "POST /api/presentation HTTP/1.1" 200 253 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:29:25 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:31:06 +0800] "POST /api/presentation HTTP/1.1" 200 254 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:34:57 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [04/Jun/2025:21:36:50 +0800] "POST /api/presentation HTTP/1.1" 200 251 "-" "axios/1.8.3"
172.17.0.4 - - [10/Jun/2025:09:56:48 +0800] "POST /api/presentation/history HTTP/1.1" 200 87 "-" "axios/1.8.3"
172.17.0.4 - - [10/Jun/2025:09:58:08 +0800] "POST /api/presentation HTTP/1.1" 200 255 "-" "axios/1.8.3"
2025/05/27 15:04:35 [warn] 20#20: *39 a client request body is buffered to a temporary file /var/cache/nginx/client_temp/0000000001, client: 172.17.0.4, server: 192.168.252.71, request: "POST /api/presentation HTTP/1.1", host: "192.168.252.71:2586"
2025/05/27 17:44:18 [warn] 20#20: *73 a client request body is buffered to a temporary file /var/cache/nginx/client_temp/0000000002, client: 172.17.0.4, server: 192.168.252.71, request: "POST /api/presentation HTTP/1.1", host: "192.168.252.71:2586"
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# access_log /var/log/nginx/access.log main;
gzip on;
gzip_min_length 1k; # 设置允许压缩的页面最小字节数
gzip_buffers 4 16k; # 用来存储 gzip 的压缩结果
gzip_http_version 1.1; # 识别 HTTP 协议版本
gzip_comp_level 2; # 设置 gzip 的压缩比 1-9。1 压缩比最小但最快,而 9 相反
gzip_types text/plain application/x-javascript text/css application/xml application/javascript; # 指定压缩类型
gzip_proxied any; # 无论后端服务器的 headers 头返回什么信息,都无条件启用压缩
include /etc/nginx/conf.d/*.conf; ## 加载该目录下的其它 Nginx 配置文件
client_max_body_size 20m; ## 上传文件大小的最大限制
}
docker run -d \
--name jp_ai_nginx_ppt \
--network host \
-e TZ=Asia/Shanghai \
-v ./nginx/nginx.conf:/etc/nginx/nginx.conf \
-v ./nginx/conf.d:/etc/nginx/conf.d \
-v ./nginx/logs:/var/log/nginx \
-v ./nginx/cert:/etc/nginx/cert \
--restart unless-stopped \
nginx:alpine
\ No newline at end of file
import pytest
from fastapi.testclient import TestClient
from main import app
import os
import tempfile
from pathlib import Path
client = TestClient(app)
# 测试数据
TEST_TOPIC = "公司PPT介绍"
TEST_FILE_URL = "http://218.77.58.8:48080/admin-api/infra/file/24/get/17f1bc82a952e171d2dfd0307fc3fff80dd3222fa00d6769fc3060b4efdc984a.docx" # 替换为实际测试文件URL
TEST_IDS = "1,2,3"
TEST_TAGS = "category.conference,color.blue"
def test_generate_outline_without_file():
"""测试不带文件的生成大纲请求"""
response = client.post(
"/generate-outline/",
data={"topic": TEST_TOPIC},
)
assert response.status_code == 200
assert "data" in response.json()
def test_generate_outline_with_file():
"""测试带文件的生成大纲请求"""
# 创建临时测试文件
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_file:
tmp_file.write(b"Test file content")
tmp_file_path = tmp_file.name
try:
with open(tmp_file_path, "rb") as f:
response = client.post(
"/generate-outline/",
data={"topic": TEST_TOPIC},
files={"file": (os.path.basename(tmp_file_path), f)}
)
assert response.status_code == 200
assert "data" in response.json()
finally:
Path(tmp_file_path).unlink(missing_ok=True)
def test_generate_outline_with_stream():
"""测试流式响应的生成大纲请求"""
response = client.post(
"/generate-outline/",
data={"topic": TEST_TOPIC, "stream": "1"},
)
assert response.status_code == 200
assert "data" in response.json()
def test_get_themes_basic():
"""测试基本获取主题请求"""
response = client.get("/get-themes/")
assert response.status_code == 200
assert "data" in response.json()
def test_get_themes_with_pagination():
"""测试带分页的获取主题请求"""
response = client.get("/get-themes/?start=5&size=10")
assert response.status_code == 200
assert "data" in response.json()
def test_get_themes_with_keywords():
"""测试带关键字的获取主题请求"""
response = client.get("/get-themes/?keywords=商务")
assert response.status_code == 200
assert "data" in response.json()
def test_get_themes_with_ids():
"""测试带ID的获取主题请求"""
response = client.get(f"/get-themes/?ids={TEST_IDS}")
assert response.status_code == 200
assert "data" in response.json()
def test_get_themes_with_tags():
"""测试带标签的获取主题请求"""
response = client.get(f"/get-themes/?tags={TEST_TAGS}")
assert response.status_code == 200
assert "data" in response.json()
def test_get_themes_invalid_params():
"""测试无效参数"""
# start 小于0
response = client.get("/get-themes/?start=-1")
assert response.status_code == 400
# size 等于0
response = client.get("/get-themes/?size=0")
assert response.status_code == 400
# keywords 过长
long_keywords = "a" * 101
response = client.get(f"/get-themes/?keywords={long_keywords}")
assert response.status_code == 400
@pytest.mark.asyncio
async def test_download_file_from_url_success():
"""测试文件下载成功"""
# 需要一个真实的测试URL,这里使用示例URL
test_url = "https://example.com/testfile.txt"
temp_file = Path(tempfile.mktemp())
try:
# 注意:这个测试会失败,因为example.com没有实际文件
# 实际使用时应该替换为有效的测试文件URL
result = await download_file_from_url(test_url, temp_file)
assert result is False # 因为example.com没有实际文件
finally:
temp_file.unlink(missing_ok=True)
@pytest.mark.asyncio
async def test_download_file_from_url_failure():
"""测试文件下载失败"""
test_url = "https://invalid.url/notexist.file"
temp_file = Path(tempfile.mktemp())
try:
with pytest.raises(HTTPException):
await download_file_from_url(test_url, temp_file)
finally:
temp_file.unlink(missing_ok=True)
\ No newline at end of file
......@@ -13,21 +13,21 @@
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.6.2/mapstruct-processor-1.6.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct/1.6.2/mapstruct-1.6.2.jar" />
</processorPath>
<module name="yudao-spring-boot-starter-job" />
<module name="yudao-server" />
<module name="yudao-module-ai-api" />
<module name="yudao-spring-boot-starter-job" />
<module name="yudao-module-system-biz" />
<module name="yudao-spring-boot-starter-test" />
<module name="yudao-module-ai-api" />
<module name="yudao-module-ai-biz" />
<module name="yudao-spring-boot-starter-biz-data-permission" />
<module name="yudao-spring-boot-starter-test" />
<module name="yudao-spring-boot-starter-security" />
<module name="yudao-spring-boot-starter-biz-data-permission" />
<module name="yudao-spring-boot-starter-redis" />
<module name="yudao-spring-boot-starter-mybatis" />
<module name="yudao-spring-boot-starter-protection" />
<module name="yudao-spring-boot-starter-web" />
<module name="yudao-spring-boot-starter-protection" />
<module name="yudao-spring-boot-starter-biz-ip" />
<module name="yudao-spring-boot-starter-biz-tenant" />
<module name="yudao-module-infra-api" />
<module name="yudao-spring-boot-starter-biz-tenant" />
<module name="yudao-spring-boot-starter-monitor" />
<module name="yudao-spring-boot-starter-websocket" />
<module name="yudao-module-infra-biz" />
......
......@@ -8,5 +8,5 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="corretto-21" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="ms-17" project-jdk-type="JavaSDK" />
</project>
\ No newline at end of file
......@@ -66,4 +66,6 @@ public interface ErrorCodeConstants {
ErrorCode TOGNZHIDAN_DEMO_NOT_EXISTS = new ErrorCode(1_022_009_002, "通知单智能体业务不存在");
ErrorCode APP_API_KEY_NOT_EXISTS = new ErrorCode(1_022_009_003, "AI密钥无效");
ErrorCode FASTGPT_SERVER_ERROR = new ErrorCode(1_022_009_004, "大模型管理系统服务错误");
}
......@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
......@@ -35,12 +36,12 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_AGENT_NOT_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
@Tag(name = "管理后台 - 智能体")
......@@ -153,7 +154,7 @@ public class AgentChatController {
return ResponseEntity.ok(data.toString());
}
@PostMapping("/get-messages")
@PostMapping("/get-messages-v2")
@Operation(summary = "获得智能体对话消息")
public ResponseEntity<?> getAgentMessagesv2(@RequestBody @Valid AgentMsgReqVO reqVO) {
......@@ -206,77 +207,77 @@ public class AgentChatController {
return response;
}
// @PostMapping("/get-messages")
// @Operation(summary = "获得智能体对话消息")
// public ResponseEntity<?> getAgentMessagesv2(@RequestBody @Valid AgentMsgReqVO reqVO) {
// if(chatMessageService.checkMsgExist(reqVO.getConversationId())) {
// // 直接构建 JSON 字符串
// String jsonResponse = "{ \"code\": 200, \"data\": {}, \"msg\": \"\" }";
// // 创建 ResponseEntity
// return ResponseEntity.ok(jsonResponse);
// }
//
// AiChatRoleDO aiRole = chatRoleService.getChatRole(reqVO.getRoleId());
// AiChatConversationDO conversation = chatConversationService.getChatConversation(reqVO.getConversationId());
// AiChatModelDO model = chatModelService.getChatModel(conversation.getModelId());
// AiApiKeyDO apiKeyDO = apiKeyService.getApiKey(model.getKeyId());
//
// // 目标服务的URL
// String baseUrl = apiKeyDO.getUrl();
//
// // 构建请求参数
// String appId = aiRole.getAppId();
// String chatId = String.valueOf(reqVO.getConversationId());
// boolean loadCustomFeedbacks = true;
//
// // 1. 首先获取init数据
// String initUrl = baseUrl + "/core/chat/init?appId=" + appId + "&chatId=" + chatId + "&loadCustomFeedbacks=" + loadCustomFeedbacks;
// HttpHeaders headers = new HttpHeaders();
// headers.setContentType(MediaType.APPLICATION_JSON);
// headers.set("Authorization", "Bearer " + apiKeyDO.getApiKey());
// HttpEntity<String> initEntity = new HttpEntity<>(headers);
// ResponseEntity<String> initResponse = restTemplate.exchange(initUrl, HttpMethod.GET, initEntity, String.class);
//
// // 解析init响应获取variables和app.chatConfig.variables
// JSONObject initJson = new JSONObject(initResponse.getBody());
// JSONObject initData = initJson.getJSONObject("data");
//// JSONObject variables = initData.optJSONObject("variables");
// JSONArray variablesConfig = initData.getJSONObject("app")
// .getJSONObject("chatConfig")
// .optJSONArray("variables");
//
// // 2. 获取分页记录
// Map<String, Object> requestBody = new HashMap<>();
// requestBody.put("offset", reqVO.getOffset());
// requestBody.put("pageSize", reqVO.getPageSize());
// requestBody.put("chatId", chatId);
// requestBody.put("appId", appId);
// requestBody.put("loadCustomFeedbacks", loadCustomFeedbacks);
// requestBody.put("type", "normal");
//
// String recordsUrl = baseUrl + "/core/chat/getPaginationRecords";
// HttpEntity<Map<String, Object>> recordsEntity = new HttpEntity<>(requestBody, headers);
// ResponseEntity<String> recordsResponse = restTemplate.exchange(recordsUrl, HttpMethod.POST, recordsEntity, String.class);
//
// // 3. 合并数据到分页记录响应中
// JSONObject recordsJson = new JSONObject(recordsResponse.getBody());
// JSONObject data = recordsJson.optJSONObject("data");
// if (data == null) {
// data = new JSONObject();
// recordsJson.put("data", data);
// }
//
// // 合并variables和variablesConfig
//// if (variables != null) {
//// data.put("variables", variables);
//// }
// if (variablesConfig != null) {
// data.put("variables", variablesConfig);
// }
//
// // 4. 返回合并后的响应
// return ResponseEntity.ok(recordsJson.toString());
// }
@PostMapping("/get-messages")
@Operation(summary = "获得智能体对话消息")
public ResponseEntity<?> getAgentMessagesv3(@RequestBody @Valid AgentMsgReqVO reqVO) throws JsonProcessingException {
// 1. 查询本地未删除的消息数量
Long localMessageCount = chatMessageService.getActiveMessageCount(reqVO.getConversationId());
// 2. 从远程获取消息
AiChatRoleDO aiRole = chatRoleService.getChatRole(reqVO.getRoleId());
AiChatConversationDO conversation = chatConversationService.getChatConversation(reqVO.getConversationId());
AiChatModelDO model = chatModelService.getChatModel(conversation.getModelId());
AiApiKeyDO apiKeyDO = apiKeyService.getApiKey(model.getKeyId());
// 构建远程请求
String requestUrl = apiKeyDO.getUrl() + "/core/chat/getPaginationRecords";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("offset", reqVO.getOffset());
requestBody.put("pageSize", reqVO.getPageSize()); // 获取足够多的消息用于同步
requestBody.put("chatId", String.valueOf(reqVO.getConversationId()));
requestBody.put("appId", aiRole.getAppId());
requestBody.put("loadCustomFeedbacks", true);
requestBody.put("type", "normal");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKeyDO.getApiKey());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
JsonNode remoteMessages;
// 发送请求
try {
ResponseEntity<String> remoteResponse = restTemplate.exchange(
requestUrl, HttpMethod.POST, entity, String.class);
// 3. 处理远程响应
ObjectMapper mapper = new ObjectMapper();
JsonNode remoteData = mapper.readTree(remoteResponse.getBody()).path("data");
remoteMessages = remoteData.path("list");
} catch (Exception e) {
throw exception(FASTGPT_SERVER_ERROR);
}
// 按时间排序并截取最新消息
List<JsonNode> sortedMessages = new ArrayList<>();
remoteMessages.forEach(sortedMessages::add);
sortedMessages.sort((a, b) -> {
String timeA = a.path("time").asText();
String timeB = b.path("time").asText();
return timeA.compareTo(timeB); // 正序排序(最早的消息在前)
});
// 4. 根据本地消息数量从列表末尾开始截取消息
Long messagesToKeep = Math.min(localMessageCount, sortedMessages.size());
List<JsonNode> filteredMessages = sortedMessages.subList(
(int) (sortedMessages.size() - messagesToKeep), // 从末尾开始
sortedMessages.size()
);
// 5. 构建响应
Map<String, Object> responseData = new HashMap<>();
responseData.put("list", filteredMessages);
responseData.put("total", filteredMessages.size()); // 保持原始总数
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("code", 200);
responseBody.put("data", responseData);
responseBody.put("msg", "");
return ResponseEntity.ok(responseBody);
}
@Operation(summary = "删除指定对话的上下文")
@DeleteMapping("/delete-context")
......
......@@ -29,6 +29,7 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
......@@ -41,9 +42,11 @@ import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_LOGIN;
@Tag(name = "管理后台 - 聊天消息")
@RestController
......@@ -67,28 +70,28 @@ public class AiChatMessageController {
@Resource
private AiModelFactory modelFactory;
@Operation(summary = "test", description = "test")
@PermitAll
@PostMapping("/test")
public String test() {
ChatModel model = modelFactory.getOrCreateChatModel(AiPlatformEnum.OPENAI,
"fastgpt-x2RF9hkc90AUk3iFSsNaWphpA1jV78i9kUTNsmogjqpKQJcEamEvIrpq3T", "http://218.77.58.8:8088/api");
return model.call(new UserMessage("你是谁"), new AssistantMessage("我是孙悟空"), new UserMessage("用python写helloworld代码"));
}
@Value("${yudao.outside-net-ip}")
private String outSideNetIp;
@Value("${yudao.inside-net-ip}")
private String inSideNetIp;
@Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
@PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) throws MalformedURLException {
if (getLoginUserId() == null){
throw exception(USER_NOT_LOGIN);
}
// 禁用ruoyi的上下文能力
sendReqVO.setUseContext(false);
List<String> originalUrls = sendReqVO.getFileUrl();
if (originalUrls != null) {
for (int i = 0; i < originalUrls.size(); i++) {
String originalUrl = originalUrls.get(i);
if (originalUrl != null && originalUrl.contains("218.77.58.8")) {
if (originalUrl != null && originalUrl.contains(outSideNetIp)) {
// 替换IP地址
String replacedUrl = originalUrl.replace("218.77.58.8", "192.168.252.71");
String replacedUrl = originalUrl.replace(outSideNetIp, inSideNetIp);
originalUrls.set(i, replacedUrl); // 更新列表中的URL
}
}
......
......@@ -26,8 +26,10 @@ import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_MODEL_ERROR;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_LOGIN;
/**
* AI 聊天对话 Service 实现类
......@@ -91,6 +93,13 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
if (model != null) {
updateObj.setModel(model.getModel());
}
// 3.截断多长title
String content = updateObj.getTitle();
String title = content != null ?
(content.length() > 252 ? content.substring(0, 252) : content)
: "";
updateObj.setTitle(title);
chatConversationMapper.updateById(updateObj);
}
......@@ -157,6 +166,9 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
@Override
public List<AiChatConversationDO> getChatAgentListByUserId(Long userId){
if (userId == null){
throw exception(USER_NOT_LOGIN);
}
return chatConversationMapper.selectListWithCon(userId);
}
......@@ -179,7 +191,11 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
.setModelId(model.getId()).setModel(model.getModel())
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
if (role != null) {
conversation.setTitle(createReqVO.getFirstMsg()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
String content = createReqVO.getFirstMsg();
String title = content != null ?
(content.length() > 252 ? content.substring(0, 252) : content)
: AiChatConversationDO.TITLE_DEFAULT;
conversation.setTitle(title).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
} else {
conversation.setTitle(AiChatConversationDO.TITLE_DEFAULT);
}
......
......@@ -89,4 +89,6 @@ public interface AiChatMessageService {
PageResult<AiChatMessageDO> getChatMessagePage(AiChatMessagePageReqVO pageReqVO);
void updateFeedback(AiChatMessageFeedBackReqVO sendReqVO);
Long getActiveMessageCount(Long conversationId);
}
......@@ -45,8 +45,8 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_LOGIN;
/**
* AI 聊天消息 Service 实现类
......@@ -113,9 +113,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 2. 插入 user 发送消息
Long count = chatMessageMapper.selectCounts(conversation.getId());
if(count == 0){
// if(count == 0 && Objects.equals(conversation.getTitle(), AiChatConversationDO.TITLE_DEFAULT)){
// 获取内容并确保不超过数据库限制
String content = sendReqVO.getContent();
String title = content != null ?
(content.length() > 252 ? content.substring(0, 252) : content)
: "新对话";
AiChatConversationUpdateMyReqVO updateMyReq = BeanUtils.toBean(conversation, AiChatConversationUpdateMyReqVO.class);
updateMyReq.setTitle(sendReqVO.getContent());
updateMyReq.setTitle(title);
chatConversationService.updateChatConversationMy(updateMyReq, userId);
}
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
......@@ -268,6 +273,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
@Override
public void deleteChatMessageByConversationId(Long conversationId, Long userId) {
if (userId == null){
throw exception(USER_NOT_LOGIN);
}
// 1. 校验消息存在
List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
......@@ -324,4 +332,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
chatMessageMapper.updateById(message);
}
@Override
public Long getActiveMessageCount(Long conversationId) {
Long count = chatMessageMapper.selectCounts(conversationId);
return count;
}
}
......@@ -153,10 +153,10 @@ public class AiChatRoleServiceImpl implements AiChatRoleService {
@Override
public PageResult<AiChatRoleDO> getChatRolePageByDept(AiChatRolePageReqVO pageReqVO, Long userId) {
AdminUserDO loginUser = null;
if (userId != null) {
loginUser = adminUserMapper.selectOne(AdminUserDO::getId, userId);
if (userId == null){
throw exception(USER_NOT_LOGIN);
}
AdminUserDO loginUser = adminUserMapper.selectOne(AdminUserDO::getId, userId);
List<Long> userIds = new ArrayList<>();
List<Long> deptIds = new ArrayList<>();
if (loginUser != null) {
......
......@@ -80,6 +80,7 @@ public class FileController {
}
// 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
path = URLUtil.decode(path);
String fileName = fileService.getFileNameByPath(path);
// 读取内容
byte[] content = fileService.getFileContent(configId, path);
......@@ -88,7 +89,7 @@ public class FileController {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
writeAttachment(response, path, content);
writeAttachment(response, fileName, content);
}
@GetMapping("/page")
......
......@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
/**
* 文件操作 Mapper
......@@ -22,5 +23,6 @@ public interface FileMapper extends BaseMapperX<FileDO> {
.betweenIfPresent(FileDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(FileDO::getId));
}
@Select("SELECT name FROM infra_file WHERE path = #{path} LIMIT 1")
String selectNameByPath(String path);
}
......@@ -21,6 +21,8 @@ public interface FileService {
*/
PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO);
String getFileNameByPath(String path);
/**
* 保存文件,并返回文件的访问路径
*
......
......@@ -40,6 +40,11 @@ public class FileServiceImpl implements FileService {
}
@Override
public String getFileNameByPath(String path) {
return fileMapper.selectNameByPath(path);
}
@Override
@SneakyThrows
public String createFile(String name, String path, byte[] content) {
// 计算默认的 path 名
......
package cn.iocoder.yudao.module.system.controller.admin.dict;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeRespVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.DictTypeSimpleRespVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.type.*;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import cn.iocoder.yudao.module.system.service.dict.DictTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
......@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
......@@ -37,6 +38,9 @@ public class DictTypeController {
@Resource
private DictTypeService dictTypeService;
@Resource
private DictDataService dictDataService;
@PostMapping("/create")
@Operation(summary = "创建字典类型")
@PreAuthorize("@ss.hasPermission('system:dict:create')")
......@@ -87,6 +91,47 @@ public class DictTypeController {
return success(BeanUtils.toBean(list, DictTypeSimpleRespVO.class));
}
@GetMapping(value = "/list-agent")
@Parameter(name = "id", description = "智能体编号", required = true, example = "48")
@Operation(summary = "获得智能体字典列表", description = "通过智能体ID获取其相应的ID")
// 无需添加权限认证,因为前端全局都需要
public CommonResult<List<DictAgentRespVO>> getAgentList(@RequestParam("id") Long id) {
// 1. 获取与agent相关的字典类型列表
List<DictTypeDO> dictTypeList = dictTypeService.getDictListByAgentId(id);
// 2. 转换结果为DictAgentRespVO列表
List<DictAgentRespVO> result = dictTypeList.stream().map(dictType -> {
// 2.1 创建响应VO
DictAgentRespVO vo = new DictAgentRespVO();
vo.setId(dictType.getId());
vo.setLabel(dictType.getName());
vo.setName(dictType.getType());
// 2.2 获取该字典类型下的所有启用的字典数据
List<DictDataDO> dictDataList = dictDataService.getDictDataList(
CommonStatusEnum.ENABLE.getStatus(), dictType.getType());
// 2.3 提取字典数据的label列表
List<String> labels = dictDataList.stream()
.map(DictDataDO::getLabel)
.collect(Collectors.toList());
// 2.3 构建LabelVO列表
List<DictAgentDataRespVO> enmus = dictDataList.stream()
.map(dictData -> {
DictAgentDataRespVO label = new DictAgentDataRespVO();
label.setValue(dictData.getLabel()); // 使用字典数据的label作为name
label.setType(dictData.getColorType()); // 使用字典类型作为type
return label;
})
.collect(Collectors.toList());
vo.setEnums(enmus);
return vo;
}).collect(Collectors.toList());
return success(result);
}
@Operation(summary = "导出数据类型")
@GetMapping("/export")
@PreAuthorize("@ss.hasPermission('system:dict:query')")
......
package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - agent选项数据信息 Response VO")
@Data
public class DictAgentDataRespVO {
@Schema(description = "选项名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "合同封面")
private String value;
@Schema(description = "是否默认", requiredMode = Schema.RequiredMode.REQUIRED, example = "default")
private String type;
}
package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - agent选项信息 Response VO")
@Data
public class DictAgentRespVO {
@Schema(description = "类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "规则")
private String label;
@Schema(description = "变量名", requiredMode = Schema.RequiredMode.REQUIRED, example = "rule")
private String name;
@Schema(description = "选项标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "[d,x,z]")
private List<DictAgentDataRespVO> enums;
}
......@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface DictTypeMapper extends BaseMapperX<DictTypeDO> {
......@@ -34,4 +35,8 @@ public interface DictTypeMapper extends BaseMapperX<DictTypeDO> {
@Update("UPDATE system_dict_type SET deleted = 1, deleted_time = #{deletedTime} WHERE id = #{id}")
void updateToDelete(@Param("id") Long id, @Param("deletedTime") LocalDateTime deletedTime);
default List<DictTypeDO> selectListByAgentId(Long id) {
return selectList(new LambdaQueryWrapperX<DictTypeDO>()
.like(DictTypeDO::getRemark, "agent_" + id)); // 确保以"agent_id"结尾
}
}
......@@ -67,4 +67,6 @@ public interface DictTypeService {
*/
List<DictTypeDO> getDictTypeList();
List<DictTypeDO> getDictListByAgentId(Long id);
}
......@@ -92,6 +92,11 @@ public class DictTypeServiceImpl implements DictTypeService {
return dictTypeMapper.selectList();
}
@Override
public List<DictTypeDO> getDictListByAgentId(Long id) {
return dictTypeMapper.selectListByAgentId(id);
}
@VisibleForTesting
void validateDictTypeNameUnique(Long id, String name) {
DictTypeDO dictType = dictTypeMapper.selectByName(name);
......
......@@ -169,6 +169,8 @@ yudao:
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
demo: false # 开启演示模式
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
outside-net-ip: 218.77.58.8
inside-net-ip: 192.168.252.71
justauth:
enabled: true
......
......@@ -227,6 +227,8 @@ yudao:
wxa-subscribe-message:
miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”;体验版为 “trial”为;正式版为 “formal”
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
outside-net-ip: 218.77.58.8
inside-net-ip: 192.168.252.71
justauth:
enabled: true
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment