fix: 知识库文档上传(尾斜杠+FormData+真文件接收); AI回复强制Markdown; 平台全功能知识库注入系统提示
This commit is contained in:
1 parent
62631a3b95
commit
463824480b
5 files changed
+177
-18
No files matched your search
@@ -26,6 +26,7 @@ from ..models import (
|
|||||||
gen_uuid
|
gen_uuid
|
||||||
)
|
)
|
||||||
from ..auth import get_current_user
|
from ..auth import get_current_user
|
||||||
|
from ..platform_knowledge import get_platform_knowledge
|
||||||
from ..middleware.log_service import LogService
|
from ..middleware.log_service import LogService
|
||||||
from ..capability_registry import resolve_auto_model
|
from ..capability_registry import resolve_auto_model
|
||||||
|
|
||||||
@@ -657,6 +658,17 @@ def _build_system_prompt(
|
|||||||
"""
|
"""
|
||||||
system_parts = ["你是AIFlow平台的AI助手,具备专业的知识储备和工具调用能力。"]
|
system_parts = ["你是AIFlow平台的AI助手,具备专业的知识储备和工具调用能力。"]
|
||||||
|
|
||||||
|
# 平台知识库注入(始终开启):确保AI优先使用平台已有能力
|
||||||
|
try:
|
||||||
|
pk = get_platform_knowledge()
|
||||||
|
if pk:
|
||||||
|
system_parts.append(f"【平台知识库——优先使用以下知识回答用户问题】\n{pk}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 回复格式要求:强制使用Markdown
|
||||||
|
system_parts.append("回复格式要求:你必须使用Markdown格式回复,确保层次分明、条理清晰。使用##和###做标题、**加粗**强调重点、-或1.做列表、```代码块包裹代码或配置、表格展示对比数据。禁止回复纯文本长段落。")
|
||||||
|
|
||||||
function_definitions = []
|
function_definitions = []
|
||||||
|
|
||||||
# Stage B.1: Agent角色注入
|
# Stage B.1: Agent角色注入
|
||||||
|
|||||||
@@ -851,9 +851,12 @@ def get_documents(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/bases/{base_id}/documents")
|
@router.post("/bases/{base_id}/documents")
|
||||||
def upload_document(
|
async def upload_document(
|
||||||
base_id: str,
|
base_id: str,
|
||||||
req: CreateDocumentRequest,
|
file: Optional[UploadFile] = File(None),
|
||||||
|
filename: str = Form(...),
|
||||||
|
file_type: Optional[str] = Form(None),
|
||||||
|
file_size: Optional[int] = Form(None),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
@@ -864,7 +867,7 @@ def upload_document(
|
|||||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||||
|
|
||||||
# 确定文件扩展名和存储路径
|
# 确定文件扩展名和存储路径
|
||||||
file_ext = (req.file_type or "txt").lower()
|
file_ext = (file_type or "txt").lower()
|
||||||
if file_ext.startswith("."):
|
if file_ext.startswith("."):
|
||||||
file_ext = file_ext[1:]
|
file_ext = file_ext[1:]
|
||||||
|
|
||||||
@@ -877,28 +880,69 @@ def upload_document(
|
|||||||
stored_filename = f"{doc_id}.{file_ext}"
|
stored_filename = f"{doc_id}.{file_ext}"
|
||||||
stored_path = os.path.join(storage_dir, stored_filename)
|
stored_path = os.path.join(storage_dir, stored_filename)
|
||||||
|
|
||||||
# 文本解析:根据文件类型生成模拟文档内容
|
# 文件内容解析
|
||||||
# 实际生产环境中应使用 PyPDF2/pdfplumber(PDF)、python-docx(Word)、openpyxl(Excel)等库
|
actual_size = file_size or 0
|
||||||
file_size = req.file_size or len(req.filename.encode('utf-8')) * 100
|
content = ""
|
||||||
content = _generate_document_content(req.filename, file_ext, file_size)
|
uploaded_content = b""
|
||||||
|
|
||||||
# 将内容写入存储文件
|
# 如果有真实文件上传,先保存并读取内容
|
||||||
|
if file and file.filename:
|
||||||
|
try:
|
||||||
|
uploaded_content = await file.read()
|
||||||
|
actual_size = len(uploaded_content)
|
||||||
|
# 先写入原始文件
|
||||||
|
with open(stored_path, "wb") as f:
|
||||||
|
f.write(uploaded_content)
|
||||||
|
# 对于文本类格式,尝试直接解析
|
||||||
|
text_exts = {"txt", "md", "csv", "json", "yaml", "yml", "xml", "py", "js", "ts", "html", "css", "log"}
|
||||||
|
if file_ext in text_exts:
|
||||||
|
try:
|
||||||
|
content = uploaded_content.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
try:
|
||||||
|
content = uploaded_content.decode("gbk")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
content = f"[二进制文件: {filename}, 大小: {actual_size} bytes]"
|
||||||
|
else:
|
||||||
|
# 非文本格式:记录文件但内容用摘要代替
|
||||||
|
content = _generate_document_content(filename, file_ext, actual_size)
|
||||||
|
logger.info(f"非文本文件 {filename}({file_ext}) 已保存到 {stored_path}, 使用模拟内容用于检索")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"文件读取失败: {e}")
|
||||||
|
content = _generate_document_content(filename, file_ext, actual_size)
|
||||||
|
else:
|
||||||
|
# 无文件上传(兼容旧 JSON 模式):生成模拟内容
|
||||||
|
actual_size = file_size or len(filename.encode('utf-8')) * 100
|
||||||
|
content = _generate_document_content(filename, file_ext, actual_size)
|
||||||
|
# 写入模拟内容
|
||||||
try:
|
try:
|
||||||
with open(stored_path, "w", encoding="utf-8") as f:
|
with open(stored_path, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
logger.info(f"知识库文档已写入: {stored_path}, size={len(content)} chars")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"知识库文档写入失败: {stored_path}, error={e}")
|
logger.error(f"知识库文档写入失败: {stored_path}, error={e}")
|
||||||
raise HTTPException(status_code=500, detail=f"文件存储失败: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"文件存储失败: {str(e)}")
|
||||||
|
|
||||||
# 文本分块:将内容按段落分割为chunks
|
# 确保文件已写入(防止重复写入)
|
||||||
|
if uploaded_content and file_ext not in {"txt", "md", "csv", "json", "yaml", "yml", "xml", "py", "js", "ts", "html", "css", "log"}:
|
||||||
|
pass # 已在上面 wb 写入
|
||||||
|
elif not os.path.exists(stored_path):
|
||||||
|
try:
|
||||||
|
with open(stored_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"知识库文档写入失败: {stored_path}, error={e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"文件存储失败: {str(e)}")
|
||||||
|
|
||||||
|
logger.info(f"知识库文档已处理: {stored_path}, ext={file_ext}, size={actual_size}, content_len={len(content)}")
|
||||||
|
|
||||||
|
# 文本分块
|
||||||
chunks = _split_content_into_chunks(content, chunk_size=500)
|
chunks = _split_content_into_chunks(content, chunk_size=500)
|
||||||
|
|
||||||
doc = KnowledgeDocument(
|
doc = KnowledgeDocument(
|
||||||
id=doc_id,
|
id=doc_id,
|
||||||
knowledge_base_id=base_id,
|
knowledge_base_id=base_id,
|
||||||
filename=req.filename,
|
filename=filename,
|
||||||
file_size=file_size,
|
file_size=actual_size,
|
||||||
file_type=file_ext,
|
file_type=file_ext,
|
||||||
status="completed",
|
status="completed",
|
||||||
chunk_count=len(chunks),
|
chunk_count=len(chunks),
|
||||||
@@ -911,8 +955,8 @@ def upload_document(
|
|||||||
kb.document_count = (kb.document_count or 0) + 1
|
kb.document_count = (kb.document_count or 0) + 1
|
||||||
kb.updated_at = datetime.now()
|
kb.updated_at = datetime.now()
|
||||||
|
|
||||||
# 同步创建Asset记录(问题9:资产关联知识库文档用量)
|
# 同步创建Asset记录
|
||||||
_sync_asset_for_knowledge_doc(db, current_user.id, doc_id, req.filename, file_ext, file_size)
|
_sync_asset_for_knowledge_doc(db, current_user.id, doc_id, filename, file_ext, actual_size)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(doc)
|
db.refresh(doc)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
七虎AI平台 — 全功能知识库与能力导航
|
||||||
|
注入到每轮AI对话中,确保AI回复优先使用平台已有知识和能力。
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("aiflow")
|
||||||
|
|
||||||
|
PLATFORM_KNOWLEDGE = """# 七虎AI平台能力全览
|
||||||
|
|
||||||
|
你是七虎AI(qihuyun.com)平台的AI助手,你必须充分了解并使用以下平台能力来回答用户问题,**优先引用平台内部知识而非互联网搜索**。
|
||||||
|
|
||||||
|
## 1. AI聊天(核心功能)
|
||||||
|
- **多模型支持**: DeepSeek-V4-Flash(默认)、DeepSeek-V3、通义千问、OpenAI等,可按能力自动匹配(Auto模式)
|
||||||
|
- **Auto智能匹配**: 根据用户意图自动选择最佳模型——文本对话用DeepSeek-V4-Flash、图像生成用agnes-image-2.1-flash、视频生成用agnes-video-v2.0、语音识别用MiMo-ASR
|
||||||
|
- **强大提示**: 图片理解(OCR用DeepSeek-OCR)、思维链推理、函数调用(Function Calling)
|
||||||
|
- **Markdown格式化**: 回复使用Markdown格式(标题/加粗/列表/代码块/表格),确保清晰、有层次、有逻辑
|
||||||
|
- **多模态生成**: AI绘图(文生图)、AI视频(文生视频)
|
||||||
|
- **对话管理**: 新建会话、历史管理、消息分页
|
||||||
|
|
||||||
|
## 2. 知识库系统
|
||||||
|
- **文档管理**: 上传txt/md/csv/pdf/docx/xlsx/pptx等文件,自动分块索引
|
||||||
|
- **智能检索**: BM25+语义检索,RAG增强回复
|
||||||
|
- **Dify集成**: 接入Dify外部知识库,深度RAG检索,AI聊天产出+用户文档自动入库量化
|
||||||
|
- **知识图谱**: 文档关系可视化、节点关联
|
||||||
|
|
||||||
|
## 3. 技能市场
|
||||||
|
- **Skills工具**: 从skills.sh、ClawHub、GitHub、ModelScope、SkillsMP等平台导入技能,支持SKILL.md格式验证
|
||||||
|
- **智能Agent**: 角色定制、行为指引、自动执行任务
|
||||||
|
- **MCP协议应用**: 从ModelScope MCP、MCP.so、MCPMarket导入MCP服务,支持Streamable HTTP/SSE传输协议,AI代码验证
|
||||||
|
- **导入Hub**: 一键从公共平台导入Skills/MCP应用,AI自动验证代码完整性
|
||||||
|
- **自定义创建**: 手动创建Skills/MCP/Agent应用,配置价格和发布
|
||||||
|
|
||||||
|
## 4. 进化看板
|
||||||
|
- **自进化体系**: AI对话轨迹自动分析、技能权重排序、原子化学习
|
||||||
|
- **进化仪表盘**: 技能贡献度、轨迹图谱、进化历史可视化
|
||||||
|
- **Pipeline调度**: 定期扫描对话、提取高价值轨迹、更新技能链接
|
||||||
|
|
||||||
|
## 5. IM Bot(微信集成)
|
||||||
|
- **个人微信Bot**: 通过iLink官方接口接入,扫码绑定,微信内直接AI对话
|
||||||
|
- **企业微信/飞书/QQ/悟空IM**: Webhook框架已就绪,支持自定义机器人
|
||||||
|
- **统一会话管理**: 微信消息并入平台AI聊天,统一历史管理
|
||||||
|
|
||||||
|
## 6. 资源中心
|
||||||
|
- **Token驾驶舱**: 用量统计(按日/模型)、剩余额度、消耗趋势图表
|
||||||
|
- **会员订阅**: 免费版/专业版/企业版,享受不同Token配额和功能权限
|
||||||
|
- **资产文件**: 上传管理图片/视频/文档等文件,关联知识库和AI聊天
|
||||||
|
|
||||||
|
## 7. 商业服务
|
||||||
|
- **计费系统**: Token计费、订单管理、支付渠道(支持多种支付)
|
||||||
|
- **广告管理**: 首页广告位、轮播图、付费推广
|
||||||
|
- **推广系统**: 邀请码、分销返佣、等级激励机制
|
||||||
|
- **成就系统**: 用户行为奖励、里程碑徽章、积分兑换
|
||||||
|
|
||||||
|
## 8. 管理后台(Admin)
|
||||||
|
- **数据驾驶舱**: 平台概览(用户数/模型数/对话数/Token消耗)、30天趋势图、操作日志/审计日志(分页+CSV导出)
|
||||||
|
- **大模型库**: 模型供应商管理(DeepSeek/OpenAI/SiliconFlow等)、模型配置(能力标签/超时/定价)、能力默认模型设置(11项能力)
|
||||||
|
- **用户管理**: 用户列表、会员等级、角色权限(RBAC)
|
||||||
|
- **系统设置**: 网站基础配置、能力默认值、MCP全局代理
|
||||||
|
- **短信配置**: SMSBao短信服务,测试发送/余额查询
|
||||||
|
- **隐私与安全**: GDPR设置、MCP隐私策略、数据加密
|
||||||
|
|
||||||
|
## 9. 开发者工具
|
||||||
|
- **API密钥管理**: 创建/撤销API密钥,调用平台API
|
||||||
|
- **MCP Client管理**: MCP服务器配置(stdio/sse/http)、环境变量、连接测试
|
||||||
|
- **自动化**: 定时任务调度
|
||||||
|
- **代码沙箱**: 安全代码执行环境
|
||||||
|
|
||||||
|
## 10. 基础设施
|
||||||
|
- **数据存储**: MySQL主库+Redis缓存+PG向量库+Elasticsearch(可选)
|
||||||
|
- **智能路由代理**: 国内/香港双通道智能分流,容器透明代理
|
||||||
|
- **部署**: Docker容器化,Nginx反向代理,FastAPI+React前后端分离
|
||||||
|
- **安全**: JWT鉴权、HTTPS加密、操作审计日志
|
||||||
|
|
||||||
|
## AI回复规范
|
||||||
|
1. **回复格式**: 使用Markdown(标题 `##`/`###`、加粗 `**`、列表 `-`/`1.`、代码块等)
|
||||||
|
2. **层次分明**: 使用标题分隔段落,确保信息有层次
|
||||||
|
3. **平台优先**: 回答问题时优先引用本平台功能,而非泛泛建议
|
||||||
|
4. **准确具体**: 基于平台文档给出具体操作路径、配置方法、API示例
|
||||||
|
5. **结构清晰**: 复杂问题先概括再分点,使用编号列表或表格
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform_knowledge() -> str:
|
||||||
|
"""获取平台全功能知识库文本,注入到AI对话系统提示中"""
|
||||||
|
return PLATFORM_KNOWLEDGE
|
||||||
@@ -261,7 +261,7 @@ export const knowledgeAPI = {
|
|||||||
|
|
||||||
getDocument: (baseId: string, docId: string) => get<any>(`/knowledge/bases/${baseId}/documents/${docId}`),
|
getDocument: (baseId: string, docId: string) => get<any>(`/knowledge/bases/${baseId}/documents/${docId}`),
|
||||||
|
|
||||||
uploadDocument: (baseId: string, data: any) => post<any>(`/knowledge/bases/${baseId}/documents/`, data),
|
uploadDocument: (baseId: string, data: any) => post<any>(`/knowledge/bases/${baseId}/documents`, data),
|
||||||
|
|
||||||
deleteDocument: (baseId: string, docId: string) => del<any>(`/knowledge/bases/${baseId}/documents/${docId}`),
|
deleteDocument: (baseId: string, docId: string) => del<any>(`/knowledge/bases/${baseId}/documents/${docId}`),
|
||||||
|
|
||||||
|
|||||||
@@ -617,10 +617,12 @@ const UploadDocModal: React.FC<{
|
|||||||
const [fileSize, setFileSize] = useState(102400);
|
const [fileSize, setFileSize] = useState(102400);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
setSelectedFile(file);
|
||||||
setFilename(file.name);
|
setFilename(file.name);
|
||||||
const ext = file.name.split('.').pop()?.toLowerCase() || 'txt';
|
const ext = file.name.split('.').pop()?.toLowerCase() || 'txt';
|
||||||
setFileType(ext);
|
setFileType(ext);
|
||||||
@@ -633,7 +635,6 @@ const UploadDocModal: React.FC<{
|
|||||||
setUploading(true);
|
setUploading(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
|
|
||||||
// 模拟上传进度: 0 -> 90% in ~2 seconds (保留10%给成功动画)
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setProgress(prev => {
|
setProgress(prev => {
|
||||||
if (prev >= 90) return prev;
|
if (prev >= 90) return prev;
|
||||||
@@ -642,9 +643,25 @@ const UploadDocModal: React.FC<{
|
|||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onUpload(kbId, filename.trim(), fileType, fileSize);
|
const formData = new FormData();
|
||||||
|
if (selectedFile) {
|
||||||
|
formData.append('file', selectedFile);
|
||||||
|
}
|
||||||
|
formData.append('filename', filename.trim());
|
||||||
|
formData.append('file_type', fileType);
|
||||||
|
formData.append('file_size', String(fileSize));
|
||||||
|
|
||||||
|
const token = localStorage.getItem('aiflow_token');
|
||||||
|
const res = await fetch(`/app/api/knowledge/bases/${kbId}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || data.code !== 0) {
|
||||||
|
throw new Error(data.message || data.detail || '上传失败');
|
||||||
|
}
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
// 上传成功后先显示100% 800ms,让进度条动画完整可见,再关闭弹窗
|
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
Reference in new issue
Block a user