fix: 知识库文档上传(尾斜杠+FormData+真文件接收); AI回复强制Markdown; 平台全功能知识库注入系统提示

This commit is contained in:
QihuAI Design committed 2026-07-14 23:04:30 +08:00
1 parent 62631a3b95
commit 463824480b
5 files changed
+183 -24

No files matched your search

+12
View File
@@ -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
@@ -656,6 +657,17 @@ def _build_system_prompt(
返回: (system_prompt_str, function_definitions_list) 返回: (system_prompt_str, function_definitions_list)
""" """
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 = []
+64 -20
View File
@@ -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""
# 将内容写入存储文件 # 如果有真实文件上传,先保存并读取内容
try: if file and file.filename:
with open(stored_path, "w", encoding="utf-8") as f: try:
f.write(content) uploaded_content = await file.read()
logger.info(f"知识库文档已写入: {stored_path}, size={len(content)} chars") actual_size = len(uploaded_content)
except Exception as e: # 先写入原始文件
logger.error(f"知识库文档写入失败: {stored_path}, error={e}") with open(stored_path, "wb") as f:
raise HTTPException(status_code=500, detail=f"文件存储失败: {str(e)}") 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:
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)}")
# 文本分块:将内容按段落分割为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)
+86
View File
@@ -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
+1 -1
View File
@@ -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}`),
+20 -3
View File
@@ -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();