Compare commits

...

5 Commits

Author SHA1 Message Date
e633462ace style: 优化 Settings 页面样式
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:43:24 +08:00
9ca267244d chore: 更新前端依赖和侧边栏导航
- 添加必要的前端依赖
- 优化侧边栏交互
- 更新 AI-Core API 文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:43:07 +08:00
49433f1681 feat: 增强 Knowledge 页面功能
- 优化文档预览功能
- 添加 CSV 文件解析支持
- 增强知识库详情展示
- 优化样式和交互

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:42:55 +08:00
4a7199de93 feat: 完善后端知识库服务和配置
- 优化 AI-Core 客户端调用
- 添加更多知识库配置选项
- 完善文档解析逻辑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:42:42 +08:00
5012a25f99 feat: 增强 AI-Core 文档解析器
- 添加 VLM 客户端支持
- 优化解析器配置
- 添加配置示例文件
- 生成新的 gRPC protobuf 文件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:42:35 +08:00
41 changed files with 5627 additions and 235 deletions

View File

@@ -0,0 +1,18 @@
# AI-Core 配置文件示例
# 复制此文件为 config.yaml 并填入实际配置
# VLM 配置(可选)
# 如果配置了 VLM图片文件会自动使用 VLM 解析
vlm:
enabled: false # 是否启用 VLM
provider: "openai" # openai / anthropic / qwen
model: "gpt-4o" # 模型名称
api_key: "" # API Key
base_url: "" # 自定义 API 地址(可选)
prompt: "" # 自定义提示词(可选)
# 服务配置
server:
port: 50051
max_workers: 10
log_level: INFO

84
ai-core/parser/config.py Normal file
View File

@@ -0,0 +1,84 @@
"""
配置管理模块
"""
import os
import yaml
import logging
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
# 默认配置
DEFAULT_CONFIG = {
"vlm": {
"enabled": False,
"provider": "openai",
"model": "gpt-4o",
"api_key": "",
"base_url": "",
"prompt": ""
},
"server": {
"port": 50051,
"max_workers": 10,
"log_level": "INFO"
}
}
def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
"""加载配置文件"""
if config_path is None:
# 默认查找 config.yaml
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_path = os.path.join(base_dir, "config.yaml")
# 环境变量覆盖
vlm_api_key = os.environ.get("VLM_API_KEY", "")
if vlm_api_key:
DEFAULT_CONFIG["vlm"]["api_key"] = vlm_api_key
DEFAULT_CONFIG["vlm"]["enabled"] = True
logger.info("VLM enabled via environment variable")
vlm_provider = os.environ.get("VLM_PROVIDER", "")
if vlm_provider:
DEFAULT_CONFIG["vlm"]["provider"] = vlm_provider
vlm_model = os.environ.get("VLM_MODEL", "")
if vlm_model:
DEFAULT_CONFIG["vlm"]["model"] = vlm_model
# 尝试加载配置文件
if os.path.exists(config_path):
try:
with open(config_path, 'r', encoding='utf-8') as f:
file_config = yaml.safe_load(f)
if file_config:
# 合并配置
for key in file_config:
if key in DEFAULT_CONFIG:
DEFAULT_CONFIG[key].update(file_config[key])
logger.info(f"Loaded config from {config_path}")
except Exception as e:
logger.warning(f"Failed to load config: {e}")
# 检查 VLM 是否有效
if DEFAULT_CONFIG["vlm"]["enabled"] and not DEFAULT_CONFIG["vlm"]["api_key"]:
logger.warning("VLM enabled but API key is empty, disabling VLM")
DEFAULT_CONFIG["vlm"]["enabled"] = False
return DEFAULT_CONFIG
def get_vlm_config() -> Optional[Dict[str, Any]]:
"""获取 VLM 配置"""
config = load_config()
if config.get("vlm", {}).get("enabled") and config["vlm"].get("api_key"):
return config["vlm"]
return None
def get_server_config() -> Dict[str, Any]:
"""获取服务器配置"""
config = load_config()
return config.get("server", DEFAULT_CONFIG["server"])

View File

@@ -1,32 +1,61 @@
import logging import logging
import os import os
import tempfile import tempfile
from typing import Optional from typing import Optional, Dict, Any
from markitdown import MarkItDown from markitdown import MarkItDown
from .vlm_client import VLMClient
from .config import get_vlm_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Parser: class Parser:
"""基于 MarkItDown 的统一文档解析器 """基于 MarkItDown + VLM 的统一文档解析器
支持格式PDF、DOCX、DOC、PPTX、PPT、XLSX、XLS、CSV、图片、网页、Markdown 等 支持格式PDF、DOCX、DOC、PPTX、PPT、XLSX、XLS、CSV、图片、网页、Markdown 等
VLM 解析:
- 方式一启动时配置config.yaml 或环境变量)
- 方式二gRPC 请求时传入 VLM 配置(优先级更高)
""" """
def __init__(self): def __init__(self):
self.markitdown = MarkItDown() self.markitdown = MarkItDown()
logger.info("Parser initialized with MarkItDown") self.vlm_client: Optional[VLMClient] = None
def parse(self, file_path: str, file_type: Optional[str] = None) -> dict: # 尝试加载配置的 VLM
vlm_config = get_vlm_config()
if vlm_config:
self.vlm_client = VLMClient(vlm_config)
logger.info(f"VLM enabled: provider={vlm_config.get('provider')}, model={vlm_config.get('model')}")
else:
logger.info("VLM not configured, using MarkItDown only")
def set_vlm_config(self, config: Dict[str, Any]) -> None:
"""手动设置 VLM 配置(优先级高于全局配置)"""
if config and config.get("enabled") and config.get("api_key"):
self.vlm_client = VLMClient(config)
logger.info(f"VLM enabled: provider={config.get('provider')}, model={config.get('model')}")
else:
self.vlm_client = None
logger.info("VLM disabled")
def parse(self, file_path: str, file_type: Optional[str] = None, vlm_config: Optional[Dict[str, Any]] = None) -> dict:
"""解析文档为 Markdown """解析文档为 Markdown
Args: Args:
file_path: 文件路径或 URL file_path: 文件路径或 URL
file_type: 文件类型可选MarkItDown 会自动检测) file_type: 文件类型可选MarkItDown 会自动检测)
vlm_config: VLM 配置(可选,优先级高于全局配置)
Returns: Returns:
dict: 包含 markdown 内容和元数据 dict: 包含 markdown 内容和元数据
""" """
# 如果有 VLM 配置,覆盖全局配置
if vlm_config:
self.set_vlm_config(vlm_config)
try: try:
logger.info(f"Parsing file: {file_path}") logger.info(f"Parsing file: {file_path}")
@@ -49,20 +78,31 @@ class Parser:
"error": str(e) "error": str(e)
} }
def parse_bytes(self, content: bytes, file_name: str, file_type: Optional[str] = None) -> dict: def parse_bytes(self, content: bytes, file_name: str, file_type: Optional[str] = None, vlm_config: Optional[Dict[str, Any]] = None) -> dict:
"""解析字节内容为 Markdown """解析字节内容为 Markdown
Args: Args:
content: 文件字节内容 content: 文件字节内容
file_name: 文件名 file_name: 文件名
file_type: 文件类型(可选) file_type: 文件类型(可选)
vlm_config: VLM 配置(可选,优先级高于全局配置)
Returns: Returns:
dict: 包含 markdown 内容和元数据 dict: 包含 markdown 内容和元数据
""" """
# 如果有 VLM 配置,覆盖全局配置
if vlm_config:
self.set_vlm_config(vlm_config)
try: try:
logger.info(f"Parsing bytes: {file_name}, size: {len(content)} bytes") logger.info(f"Parsing bytes: {file_name}, size: {len(content)} bytes")
# 检查是否应该使用 VLM根据文件名自动判断
if self._should_use_vlm(file_name):
logger.info("Using VLM for parsing")
return self._parse_with_vlm(content, file_name)
# 否则使用 MarkItDown
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file_name)[1] or '') as temp_file: with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file_name)[1] or '') as temp_file:
temp_file.write(content) temp_file.write(content)
temp_path = temp_file.name temp_path = temp_file.name
@@ -89,6 +129,65 @@ class Parser:
"error": str(e) "error": str(e)
} }
def _should_use_vlm(self, file_name: str) -> bool:
"""判断是否应该使用 VLM"""
if not self.vlm_client:
return False
# 图片文件使用 VLM
image_exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff']
ext = os.path.splitext(file_name)[1].lower()
return ext in image_exts
def _parse_with_vlm(self, content: bytes, file_name: str) -> dict:
"""使用 VLM 解析"""
if not self.vlm_client:
return {
"success": False,
"content": "",
"content_length": 0,
"error": "VLM not configured"
}
# 确定 MIME 类型
ext = os.path.splitext(file_name)[1].lower()
mime_types = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.webp': 'image/webp',
'.tiff': 'image/tiff',
}
mime_type = mime_types.get(ext, 'image/png')
try:
result = self.vlm_client.analyze_image(content, mime_type)
if result.get("success"):
return {
"success": True,
"content": result["content"],
"content_length": len(result["content"]),
"metadata": {"vlm_used": True}
}
else:
return {
"success": False,
"content": "",
"content_length": 0,
"error": result.get("error", "VLM parsing failed")
}
except Exception as e:
logger.error(f"VLM parsing error: {e}")
return {
"success": False,
"content": "",
"content_length": 0,
"error": str(e)
}
if __name__ == "__main__": if __name__ == "__main__":
parser = Parser() parser = Parser()

View File

@@ -0,0 +1,209 @@
"""
VLM 客户端 - 用于调用 VLM 模型进行文档理解
"""
import logging
import base64
import requests
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
class VLMClient:
"""VLM 客户端,支持多种提供商"""
def __init__(self, config: Dict[str, Any]):
"""
初始化 VLM 客户端
Args:
config: VLM 配置,包含 provider, model, api_key, base_url, prompt 等
"""
self.config = config
self.provider = config.get("provider", "openai")
self.model = config.get("model", "gpt-4o")
self.api_key = config.get("api_key", "")
self.base_url = config.get("base_url", "")
self.prompt = config.get("prompt", "") or self._default_prompt()
logger.info(f"VLMClient initialized: provider={self.provider}, model={self.model}")
def _default_prompt(self) -> str:
"""默认提示词"""
return """请分析这张图片中的文档内容,并将其转换为 Markdown 格式。
要求:
1. 保持原文的格式和结构
2. 表格用 Markdown 表格格式
3. 标题用 # ## ### 标记
4. 代码块用 ``` 标记
5. 尽量保留原文的所有信息"""
def analyze_image(self, image_data: bytes, mime_type: str = "image/png") -> Dict[str, Any]:
"""
使用 VLM 分析图片
Args:
image_data: 图片二进制数据
mime_type: 图片 MIME 类型
Returns:
包含分析结果的字典
"""
if self.provider == "openai":
return self._call_openai(image_data, mime_type)
elif self.provider == "anthropic":
return self._call_anthropic(image_data, mime_type)
elif self.provider == "qwen":
return self._call_qwen(image_data, mime_type)
else:
return {
"success": False,
"content": "",
"error": f"Unsupported provider: {self.provider}"
}
def _call_openai(self, image_data: bytes, mime_type: str) -> Dict[str, Any]:
"""调用 OpenAI GPT-4o API"""
try:
url = (self.base_url or "https://api.openai.com/v1") + "/chat/completions"
# Base64 编码图片
image_base64 = base64.b64encode(image_data).decode("utf-8")
data_url = f"data:{mime_type};base64,{image_base64}"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": self.model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": self.prompt},
{"type": "image_url", "image_url": {"url": data_url}}
]
}
],
"max_tokens": 4096
}
response = requests.post(url, headers=headers, json=payload, timeout=120)
response.raise_for_status()
result = response.json()
content = result["choices"][0]["message"]["content"]
return {
"success": True,
"content": content,
"usage": result.get("usage", {})
}
except Exception as e:
logger.error(f"OpenAI API error: {e}")
return {
"success": False,
"content": "",
"error": str(e)
}
def _call_anthropic(self, image_data: bytes, mime_type: str) -> Dict[str, Any]:
"""调用 Anthropic Claude API"""
try:
url = (self.base_url or "https://api.anthropic.com/v1") + "/messages"
image_base64 = base64.b64encode(image_data).decode("utf-8")
headers = {
"x-api-key": self.api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json"
}
# Anthropic 支持 image 类型
payload = {
"model": self.model,
"max_tokens": 4096,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": self.prompt},
{
"type": "image",
"source": {
"type": "base64",
"media_type": mime_type,
"data": image_base64
}
}
]
}
]
}
response = requests.post(url, headers=headers, json=payload, timeout=120)
response.raise_for_status()
result = response.json()
content = result["content"][0]["text"]
return {
"success": True,
"content": content,
"usage": result.get("usage", {})
}
except Exception as e:
logger.error(f"Anthropic API error: {e}")
return {
"success": False,
"content": "",
"error": str(e)
}
def _call_qwen(self, image_data: bytes, mime_type: str) -> Dict[str, Any]:
"""调用阿里 Qwen VL API"""
try:
url = (self.base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1") + "/chat/completions"
image_base64 = base64.b64encode(image_data).decode("utf-8")
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# Qwen 格式
payload = {
"model": self.model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": self.prompt},
{"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{image_base64}"}}
]
}
]
}
response = requests.post(url, headers=headers, json=payload, timeout=120)
response.raise_for_status()
result = response.json()
content = result["choices"][0]["message"]["content"]
return {
"success": True,
"content": content,
"usage": {}
}
except Exception as e:
logger.error(f"Qwen API error: {e}")
return {
"success": False,
"content": "",
"error": str(e)
}

View File

@@ -16,6 +16,18 @@ message ParseRequest {
string file_type = 3; string file_type = 3;
string parser_engine = 4; string parser_engine = 4;
map<string, string> engine_overrides = 5; map<string, string> engine_overrides = 5;
// VLM 配置(可选)
VLMConfig vlm_config = 6;
}
message VLMConfig {
bool enabled = 1; // 是否启用 VLM
string provider = 2; // VLM 提供商: openai, anthropic, local 等
string model = 3; // 模型名称
string api_key = 4; // API Key
string base_url = 5; // 自定义 API 地址
string prompt = 6; // 自定义提示词
} }
message ParseResponse { message ParseResponse {

View File

@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x64ocument_parser.proto\x12\tdocparser\"\xdd\x01\n\x0cParseRequest\x12\x10\n\x08\x66ile_url\x18\x01 \x01(\t\x12\x11\n\tfile_name\x18\x02 \x01(\t\x12\x11\n\tfile_type\x18\x03 \x01(\t\x12\x15\n\rparser_engine\x18\x04 \x01(\t\x12\x46\n\x10\x65ngine_overrides\x18\x05 \x03(\x0b\x32,.docparser.ParseRequest.EngineOverridesEntry\x1a\x36\n\x14\x45ngineOverridesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x84\x01\n\rParseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x16\n\x0e\x63ontent_length\x18\x04 \x01(\x05\x12\x11\n\tfile_type\x18\x05 \x01(\t\x12\x15\n\rparser_engine\x18\x06 \x01(\t\"\x07\n\x05\x45mpty\"\xca\x01\n\x18SupportedFormatsResponse\x12\x12\n\nfile_types\x18\x01 \x03(\t\x12]\n\x16\x66ile_type_descriptions\x18\x02 \x03(\x0b\x32=.docparser.SupportedFormatsResponse.FileTypeDescriptionsEntry\x1a;\n\x19\x46ileTypeDescriptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"9\n\x0f\x45nginesResponse\x12&\n\x07\x65ngines\x18\x01 \x03(\x0b\x32\x15.docparser.EngineInfo\"|\n\nEngineInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x1c\n\x14supported_file_types\x18\x03 \x03(\t\x12\x11\n\tavailable\x18\x04 \x01(\x08\x12\x1a\n\x12unavailable_reason\x18\x05 \x01(\t2\xde\x01\n\x0e\x44ocumentParser\x12\x42\n\rParseDocument\x12\x17.docparser.ParseRequest\x1a\x18.docparser.ParseResponse\x12L\n\x13GetSupportedFormats\x12\x10.docparser.Empty\x1a#.docparser.SupportedFormatsResponse\x12:\n\nGetEngines\x12\x10.docparser.Empty\x1a\x1a.docparser.EnginesResponseB\x1aZ\x18x-agents/proto/docparserb\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x64ocument_parser.proto\x12\tdocparser\"\x87\x02\n\x0cParseRequest\x12\x10\n\x08\x66ile_url\x18\x01 \x01(\t\x12\x11\n\tfile_name\x18\x02 \x01(\t\x12\x11\n\tfile_type\x18\x03 \x01(\t\x12\x15\n\rparser_engine\x18\x04 \x01(\t\x12\x46\n\x10\x65ngine_overrides\x18\x05 \x03(\x0b\x32,.docparser.ParseRequest.EngineOverridesEntry\x12(\n\nvlm_config\x18\x06 \x01(\x0b\x32\x14.docparser.VLMConfig\x1a\x36\n\x14\x45ngineOverridesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"p\n\tVLMConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x10\n\x08provider\x18\x02 \x01(\t\x12\r\n\x05model\x18\x03 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x04 \x01(\t\x12\x10\n\x08\x62\x61se_url\x18\x05 \x01(\t\x12\x0e\n\x06prompt\x18\x06 \x01(\t\"\x84\x01\n\rParseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x16\n\x0e\x63ontent_length\x18\x04 \x01(\x05\x12\x11\n\tfile_type\x18\x05 \x01(\t\x12\x15\n\rparser_engine\x18\x06 \x01(\t\"\x07\n\x05\x45mpty\"\xca\x01\n\x18SupportedFormatsResponse\x12\x12\n\nfile_types\x18\x01 \x03(\t\x12]\n\x16\x66ile_type_descriptions\x18\x02 \x03(\x0b\x32=.docparser.SupportedFormatsResponse.FileTypeDescriptionsEntry\x1a;\n\x19\x46ileTypeDescriptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"9\n\x0f\x45nginesResponse\x12&\n\x07\x65ngines\x18\x01 \x03(\x0b\x32\x15.docparser.EngineInfo\"|\n\nEngineInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x1c\n\x14supported_file_types\x18\x03 \x03(\t\x12\x11\n\tavailable\x18\x04 \x01(\x08\x12\x1a\n\x12unavailable_reason\x18\x05 \x01(\t2\xde\x01\n\x0e\x44ocumentParser\x12\x42\n\rParseDocument\x12\x17.docparser.ParseRequest\x1a\x18.docparser.ParseResponse\x12L\n\x13GetSupportedFormats\x12\x10.docparser.Empty\x1a#.docparser.SupportedFormatsResponse\x12:\n\nGetEngines\x12\x10.docparser.Empty\x1a\x1a.docparser.EnginesResponseB\x1aZ\x18x-agents/proto/docparserb\x06proto3')
_globals = globals() _globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -37,21 +37,23 @@ if not _descriptor._USE_C_DESCRIPTORS:
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._loaded_options = None _globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._loaded_options = None
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_options = b'8\001' _globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_options = b'8\001'
_globals['_PARSEREQUEST']._serialized_start=37 _globals['_PARSEREQUEST']._serialized_start=37
_globals['_PARSEREQUEST']._serialized_end=258 _globals['_PARSEREQUEST']._serialized_end=300
_globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_start=204 _globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_start=246
_globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_end=258 _globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_end=300
_globals['_PARSERESPONSE']._serialized_start=261 _globals['_VLMCONFIG']._serialized_start=302
_globals['_PARSERESPONSE']._serialized_end=393 _globals['_VLMCONFIG']._serialized_end=414
_globals['_EMPTY']._serialized_start=395 _globals['_PARSERESPONSE']._serialized_start=417
_globals['_EMPTY']._serialized_end=402 _globals['_PARSERESPONSE']._serialized_end=549
_globals['_SUPPORTEDFORMATSRESPONSE']._serialized_start=405 _globals['_EMPTY']._serialized_start=551
_globals['_SUPPORTEDFORMATSRESPONSE']._serialized_end=607 _globals['_EMPTY']._serialized_end=558
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_start=548 _globals['_SUPPORTEDFORMATSRESPONSE']._serialized_start=561
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_end=607 _globals['_SUPPORTEDFORMATSRESPONSE']._serialized_end=763
_globals['_ENGINESRESPONSE']._serialized_start=609 _globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_start=704
_globals['_ENGINESRESPONSE']._serialized_end=666 _globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_end=763
_globals['_ENGINEINFO']._serialized_start=668 _globals['_ENGINESRESPONSE']._serialized_start=765
_globals['_ENGINEINFO']._serialized_end=792 _globals['_ENGINESRESPONSE']._serialized_end=822
_globals['_DOCUMENTPARSER']._serialized_start=795 _globals['_ENGINEINFO']._serialized_start=824
_globals['_DOCUMENTPARSER']._serialized_end=1017 _globals['_ENGINEINFO']._serialized_end=948
_globals['_DOCUMENTPARSER']._serialized_start=951
_globals['_DOCUMENTPARSER']._serialized_end=1173
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -6,8 +6,9 @@ grpcio-tools>=1.60.0
grpcio-reflection>=1.60.0 grpcio-reflection>=1.60.0
protobuf>=4.25.0 protobuf>=4.25.0
# HTTP 请求 # 配置文件解析
pyyaml>=6.0
requests>=2.31.0 requests>=2.31.0
# 文档解析 # 文档解析 - markitdown 及其所有依赖
markitdown>=0.0.1 markitdown[pdf,docx,pptx,xlsx,all]>=0.0.1

View File

@@ -75,6 +75,21 @@ class DocumentParserServicer:
content_length=0, content_length=0,
) )
# 提取 VLM 配置
vlm_config = None
if hasattr(request, 'vlm_config') and request.vlm_config:
vlm_cfg = request.vlm_config
if vlm_cfg.enabled:
vlm_config = {
"enabled": vlm_cfg.enabled,
"provider": vlm_cfg.provider,
"model": vlm_cfg.model,
"api_key": vlm_cfg.api_key,
"base_url": vlm_cfg.base_url,
"prompt": vlm_cfg.prompt,
}
logger.info(f"VLM config: provider={vlm_cfg.provider}, model={vlm_cfg.model}")
logger.info("Downloading file from URL: %s", file_url) logger.info("Downloading file from URL: %s", file_url)
try: try:
@@ -95,9 +110,9 @@ class DocumentParserServicer:
content_length=0, content_length=0,
) )
logger.info("Parsing file with MarkItDown") logger.info("Parsing file with MarkItDown + VLM")
result = self.parser.parse_bytes(content, file_name) result = self.parser.parse_bytes(content, file_name, vlm_config=vlm_config)
if not result.get("success", False): if not result.get("success", False):
logger.warning("Parser returned failure: %s", result.get("error", "Unknown error")) logger.warning("Parser returned failure: %s", result.get("error", "Unknown error"))

View File

@@ -87,7 +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, cfg.AICoreServiceAddr) knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr, cfg.MarkdownLocalPath)
// 6. 初始化 Handler // 6. 初始化 Handler
dbHandler := handler.NewDatabaseHandler(dbService) dbHandler := handler.NewDatabaseHandler(dbService)

View File

@@ -0,0 +1,498 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v5.29.3
// source: document_parser.proto
package docparser
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ParseRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
FileUrl string `protobuf:"bytes,1,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"`
FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"`
FileType string `protobuf:"bytes,3,opt,name=file_type,json=fileType,proto3" json:"file_type,omitempty"`
ParserEngine string `protobuf:"bytes,4,opt,name=parser_engine,json=parserEngine,proto3" json:"parser_engine,omitempty"`
EngineOverrides map[string]string `protobuf:"bytes,5,rep,name=engine_overrides,json=engineOverrides,proto3" json:"engine_overrides,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ParseRequest) Reset() {
*x = ParseRequest{}
mi := &file_document_parser_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ParseRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ParseRequest) ProtoMessage() {}
func (x *ParseRequest) ProtoReflect() protoreflect.Message {
mi := &file_document_parser_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ParseRequest.ProtoReflect.Descriptor instead.
func (*ParseRequest) Descriptor() ([]byte, []int) {
return file_document_parser_proto_rawDescGZIP(), []int{0}
}
func (x *ParseRequest) GetFileUrl() string {
if x != nil {
return x.FileUrl
}
return ""
}
func (x *ParseRequest) GetFileName() string {
if x != nil {
return x.FileName
}
return ""
}
func (x *ParseRequest) GetFileType() string {
if x != nil {
return x.FileType
}
return ""
}
func (x *ParseRequest) GetParserEngine() string {
if x != nil {
return x.ParserEngine
}
return ""
}
func (x *ParseRequest) GetEngineOverrides() map[string]string {
if x != nil {
return x.EngineOverrides
}
return nil
}
type ParseResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
ContentLength int32 `protobuf:"varint,4,opt,name=content_length,json=contentLength,proto3" json:"content_length,omitempty"`
FileType string `protobuf:"bytes,5,opt,name=file_type,json=fileType,proto3" json:"file_type,omitempty"`
ParserEngine string `protobuf:"bytes,6,opt,name=parser_engine,json=parserEngine,proto3" json:"parser_engine,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ParseResponse) Reset() {
*x = ParseResponse{}
mi := &file_document_parser_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ParseResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ParseResponse) ProtoMessage() {}
func (x *ParseResponse) ProtoReflect() protoreflect.Message {
mi := &file_document_parser_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ParseResponse.ProtoReflect.Descriptor instead.
func (*ParseResponse) Descriptor() ([]byte, []int) {
return file_document_parser_proto_rawDescGZIP(), []int{1}
}
func (x *ParseResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *ParseResponse) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
func (x *ParseResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ParseResponse) GetContentLength() int32 {
if x != nil {
return x.ContentLength
}
return 0
}
func (x *ParseResponse) GetFileType() string {
if x != nil {
return x.FileType
}
return ""
}
func (x *ParseResponse) GetParserEngine() string {
if x != nil {
return x.ParserEngine
}
return ""
}
type Empty struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Empty) Reset() {
*x = Empty{}
mi := &file_document_parser_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Empty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_document_parser_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) {
return file_document_parser_proto_rawDescGZIP(), []int{2}
}
type SupportedFormatsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
FileTypes []string `protobuf:"bytes,1,rep,name=file_types,json=fileTypes,proto3" json:"file_types,omitempty"`
FileTypeDescriptions map[string]string `protobuf:"bytes,2,rep,name=file_type_descriptions,json=fileTypeDescriptions,proto3" json:"file_type_descriptions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SupportedFormatsResponse) Reset() {
*x = SupportedFormatsResponse{}
mi := &file_document_parser_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SupportedFormatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SupportedFormatsResponse) ProtoMessage() {}
func (x *SupportedFormatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_document_parser_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SupportedFormatsResponse.ProtoReflect.Descriptor instead.
func (*SupportedFormatsResponse) Descriptor() ([]byte, []int) {
return file_document_parser_proto_rawDescGZIP(), []int{3}
}
func (x *SupportedFormatsResponse) GetFileTypes() []string {
if x != nil {
return x.FileTypes
}
return nil
}
func (x *SupportedFormatsResponse) GetFileTypeDescriptions() map[string]string {
if x != nil {
return x.FileTypeDescriptions
}
return nil
}
type EnginesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Engines []*EngineInfo `protobuf:"bytes,1,rep,name=engines,proto3" json:"engines,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EnginesResponse) Reset() {
*x = EnginesResponse{}
mi := &file_document_parser_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EnginesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnginesResponse) ProtoMessage() {}
func (x *EnginesResponse) ProtoReflect() protoreflect.Message {
mi := &file_document_parser_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EnginesResponse.ProtoReflect.Descriptor instead.
func (*EnginesResponse) Descriptor() ([]byte, []int) {
return file_document_parser_proto_rawDescGZIP(), []int{4}
}
func (x *EnginesResponse) GetEngines() []*EngineInfo {
if x != nil {
return x.Engines
}
return nil
}
type EngineInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
SupportedFileTypes []string `protobuf:"bytes,3,rep,name=supported_file_types,json=supportedFileTypes,proto3" json:"supported_file_types,omitempty"`
Available bool `protobuf:"varint,4,opt,name=available,proto3" json:"available,omitempty"`
UnavailableReason string `protobuf:"bytes,5,opt,name=unavailable_reason,json=unavailableReason,proto3" json:"unavailable_reason,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EngineInfo) Reset() {
*x = EngineInfo{}
mi := &file_document_parser_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EngineInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EngineInfo) ProtoMessage() {}
func (x *EngineInfo) ProtoReflect() protoreflect.Message {
mi := &file_document_parser_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EngineInfo.ProtoReflect.Descriptor instead.
func (*EngineInfo) Descriptor() ([]byte, []int) {
return file_document_parser_proto_rawDescGZIP(), []int{5}
}
func (x *EngineInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *EngineInfo) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *EngineInfo) GetSupportedFileTypes() []string {
if x != nil {
return x.SupportedFileTypes
}
return nil
}
func (x *EngineInfo) GetAvailable() bool {
if x != nil {
return x.Available
}
return false
}
func (x *EngineInfo) GetUnavailableReason() string {
if x != nil {
return x.UnavailableReason
}
return ""
}
var File_document_parser_proto protoreflect.FileDescriptor
const file_document_parser_proto_rawDesc = "" +
"\n" +
"\x15document_parser.proto\x12\tdocparser\"\xa5\x02\n" +
"\fParseRequest\x12\x19\n" +
"\bfile_url\x18\x01 \x01(\tR\afileUrl\x12\x1b\n" +
"\tfile_name\x18\x02 \x01(\tR\bfileName\x12\x1b\n" +
"\tfile_type\x18\x03 \x01(\tR\bfileType\x12#\n" +
"\rparser_engine\x18\x04 \x01(\tR\fparserEngine\x12W\n" +
"\x10engine_overrides\x18\x05 \x03(\v2,.docparser.ParseRequest.EngineOverridesEntryR\x0fengineOverrides\x1aB\n" +
"\x14EngineOverridesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc6\x01\n" +
"\rParseResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
"\acontent\x18\x02 \x01(\tR\acontent\x12\x18\n" +
"\amessage\x18\x03 \x01(\tR\amessage\x12%\n" +
"\x0econtent_length\x18\x04 \x01(\x05R\rcontentLength\x12\x1b\n" +
"\tfile_type\x18\x05 \x01(\tR\bfileType\x12#\n" +
"\rparser_engine\x18\x06 \x01(\tR\fparserEngine\"\a\n" +
"\x05Empty\"\xf7\x01\n" +
"\x18SupportedFormatsResponse\x12\x1d\n" +
"\n" +
"file_types\x18\x01 \x03(\tR\tfileTypes\x12s\n" +
"\x16file_type_descriptions\x18\x02 \x03(\v2=.docparser.SupportedFormatsResponse.FileTypeDescriptionsEntryR\x14fileTypeDescriptions\x1aG\n" +
"\x19FileTypeDescriptionsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
"\x0fEnginesResponse\x12/\n" +
"\aengines\x18\x01 \x03(\v2\x15.docparser.EngineInfoR\aengines\"\xc1\x01\n" +
"\n" +
"EngineInfo\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x120\n" +
"\x14supported_file_types\x18\x03 \x03(\tR\x12supportedFileTypes\x12\x1c\n" +
"\tavailable\x18\x04 \x01(\bR\tavailable\x12-\n" +
"\x12unavailable_reason\x18\x05 \x01(\tR\x11unavailableReason2\xde\x01\n" +
"\x0eDocumentParser\x12B\n" +
"\rParseDocument\x12\x17.docparser.ParseRequest\x1a\x18.docparser.ParseResponse\x12L\n" +
"\x13GetSupportedFormats\x12\x10.docparser.Empty\x1a#.docparser.SupportedFormatsResponse\x12:\n" +
"\n" +
"GetEngines\x12\x10.docparser.Empty\x1a\x1a.docparser.EnginesResponseB\x1aZ\x18x-agents/proto/docparserb\x06proto3"
var (
file_document_parser_proto_rawDescOnce sync.Once
file_document_parser_proto_rawDescData []byte
)
func file_document_parser_proto_rawDescGZIP() []byte {
file_document_parser_proto_rawDescOnce.Do(func() {
file_document_parser_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_document_parser_proto_rawDesc), len(file_document_parser_proto_rawDesc)))
})
return file_document_parser_proto_rawDescData
}
var file_document_parser_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_document_parser_proto_goTypes = []any{
(*ParseRequest)(nil), // 0: docparser.ParseRequest
(*ParseResponse)(nil), // 1: docparser.ParseResponse
(*Empty)(nil), // 2: docparser.Empty
(*SupportedFormatsResponse)(nil), // 3: docparser.SupportedFormatsResponse
(*EnginesResponse)(nil), // 4: docparser.EnginesResponse
(*EngineInfo)(nil), // 5: docparser.EngineInfo
nil, // 6: docparser.ParseRequest.EngineOverridesEntry
nil, // 7: docparser.SupportedFormatsResponse.FileTypeDescriptionsEntry
}
var file_document_parser_proto_depIdxs = []int32{
6, // 0: docparser.ParseRequest.engine_overrides:type_name -> docparser.ParseRequest.EngineOverridesEntry
7, // 1: docparser.SupportedFormatsResponse.file_type_descriptions:type_name -> docparser.SupportedFormatsResponse.FileTypeDescriptionsEntry
5, // 2: docparser.EnginesResponse.engines:type_name -> docparser.EngineInfo
0, // 3: docparser.DocumentParser.ParseDocument:input_type -> docparser.ParseRequest
2, // 4: docparser.DocumentParser.GetSupportedFormats:input_type -> docparser.Empty
2, // 5: docparser.DocumentParser.GetEngines:input_type -> docparser.Empty
1, // 6: docparser.DocumentParser.ParseDocument:output_type -> docparser.ParseResponse
3, // 7: docparser.DocumentParser.GetSupportedFormats:output_type -> docparser.SupportedFormatsResponse
4, // 8: docparser.DocumentParser.GetEngines:output_type -> docparser.EnginesResponse
6, // [6:9] is the sub-list for method output_type
3, // [3:6] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_document_parser_proto_init() }
func file_document_parser_proto_init() {
if File_document_parser_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_document_parser_proto_rawDesc), len(file_document_parser_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_document_parser_proto_goTypes,
DependencyIndexes: file_document_parser_proto_depIdxs,
MessageInfos: file_document_parser_proto_msgTypes,
}.Build()
File_document_parser_proto = out.File
file_document_parser_proto_goTypes = nil
file_document_parser_proto_depIdxs = nil
}

View File

@@ -0,0 +1,197 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v5.29.3
// source: document_parser.proto
package docparser
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
DocumentParser_ParseDocument_FullMethodName = "/docparser.DocumentParser/ParseDocument"
DocumentParser_GetSupportedFormats_FullMethodName = "/docparser.DocumentParser/GetSupportedFormats"
DocumentParser_GetEngines_FullMethodName = "/docparser.DocumentParser/GetEngines"
)
// DocumentParserClient is the client API for DocumentParser service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DocumentParserClient interface {
ParseDocument(ctx context.Context, in *ParseRequest, opts ...grpc.CallOption) (*ParseResponse, error)
GetSupportedFormats(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*SupportedFormatsResponse, error)
GetEngines(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*EnginesResponse, error)
}
type documentParserClient struct {
cc grpc.ClientConnInterface
}
func NewDocumentParserClient(cc grpc.ClientConnInterface) DocumentParserClient {
return &documentParserClient{cc}
}
func (c *documentParserClient) ParseDocument(ctx context.Context, in *ParseRequest, opts ...grpc.CallOption) (*ParseResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ParseResponse)
err := c.cc.Invoke(ctx, DocumentParser_ParseDocument_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *documentParserClient) GetSupportedFormats(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*SupportedFormatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SupportedFormatsResponse)
err := c.cc.Invoke(ctx, DocumentParser_GetSupportedFormats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *documentParserClient) GetEngines(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*EnginesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(EnginesResponse)
err := c.cc.Invoke(ctx, DocumentParser_GetEngines_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DocumentParserServer is the server API for DocumentParser service.
// All implementations must embed UnimplementedDocumentParserServer
// for forward compatibility.
type DocumentParserServer interface {
ParseDocument(context.Context, *ParseRequest) (*ParseResponse, error)
GetSupportedFormats(context.Context, *Empty) (*SupportedFormatsResponse, error)
GetEngines(context.Context, *Empty) (*EnginesResponse, error)
mustEmbedUnimplementedDocumentParserServer()
}
// UnimplementedDocumentParserServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedDocumentParserServer struct{}
func (UnimplementedDocumentParserServer) ParseDocument(context.Context, *ParseRequest) (*ParseResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ParseDocument not implemented")
}
func (UnimplementedDocumentParserServer) GetSupportedFormats(context.Context, *Empty) (*SupportedFormatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetSupportedFormats not implemented")
}
func (UnimplementedDocumentParserServer) GetEngines(context.Context, *Empty) (*EnginesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetEngines not implemented")
}
func (UnimplementedDocumentParserServer) mustEmbedUnimplementedDocumentParserServer() {}
func (UnimplementedDocumentParserServer) testEmbeddedByValue() {}
// UnsafeDocumentParserServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DocumentParserServer will
// result in compilation errors.
type UnsafeDocumentParserServer interface {
mustEmbedUnimplementedDocumentParserServer()
}
func RegisterDocumentParserServer(s grpc.ServiceRegistrar, srv DocumentParserServer) {
// If the following call panics, it indicates UnimplementedDocumentParserServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&DocumentParser_ServiceDesc, srv)
}
func _DocumentParser_ParseDocument_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ParseRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DocumentParserServer).ParseDocument(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DocumentParser_ParseDocument_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DocumentParserServer).ParseDocument(ctx, req.(*ParseRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DocumentParser_GetSupportedFormats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DocumentParserServer).GetSupportedFormats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DocumentParser_GetSupportedFormats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DocumentParserServer).GetSupportedFormats(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _DocumentParser_GetEngines_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DocumentParserServer).GetEngines(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DocumentParser_GetEngines_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DocumentParserServer).GetEngines(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
// DocumentParser_ServiceDesc is the grpc.ServiceDesc for DocumentParser service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var DocumentParser_ServiceDesc = grpc.ServiceDesc{
ServiceName: "docparser.DocumentParser",
HandlerType: (*DocumentParserServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ParseDocument",
Handler: _DocumentParser_ParseDocument_Handler,
},
{
MethodName: "GetSupportedFormats",
Handler: _DocumentParser_GetSupportedFormats_Handler,
},
{
MethodName: "GetEngines",
Handler: _DocumentParser_GetEngines_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "document_parser.proto",
}

View File

@@ -26,6 +26,7 @@ type Config struct {
UploadMode string // "local" 或 "minio" UploadMode string // "local" 或 "minio"
UploadLocalPath string // 本地存储路径,如 "resource/files" UploadLocalPath string // 本地存储路径,如 "resource/files"
ServerBaseURL string // 服务器基础 URL用于生成本地文件 URL ServerBaseURL string // 服务器基础 URL用于生成本地文件 URL
MarkdownLocalPath string // Markdown 文件存储路径,如 "resource/markdown"
// MinIO 配置 // MinIO 配置
MinIOEndpoint string MinIOEndpoint string
MinIOAccessKey string MinIOAccessKey string
@@ -56,6 +57,7 @@ func Load() *Config {
viper.SetDefault("upload_mode", "local") viper.SetDefault("upload_mode", "local")
viper.SetDefault("upload_local_path", "resource/files") viper.SetDefault("upload_local_path", "resource/files")
viper.SetDefault("server_base_url", "http://localhost:8080") viper.SetDefault("server_base_url", "http://localhost:8080")
viper.SetDefault("markdown_local_path", "resource/markdown")
viper.SetDefault("minio_endpoint", "localhost:9000") viper.SetDefault("minio_endpoint", "localhost:9000")
viper.SetDefault("minio_access_key", "") viper.SetDefault("minio_access_key", "")
viper.SetDefault("minio_secret_key", "") viper.SetDefault("minio_secret_key", "")
@@ -90,6 +92,7 @@ func Load() *Config {
UploadMode: viper.GetString("upload_mode"), UploadMode: viper.GetString("upload_mode"),
UploadLocalPath: viper.GetString("upload_local_path"), UploadLocalPath: viper.GetString("upload_local_path"),
ServerBaseURL: viper.GetString("server_base_url"), ServerBaseURL: viper.GetString("server_base_url"),
MarkdownLocalPath: viper.GetString("markdown_local_path"),
// MinIO 配置 // MinIO 配置
MinIOEndpoint: viper.GetString("minio_endpoint"), MinIOEndpoint: viper.GetString("minio_endpoint"),
MinIOAccessKey: viper.GetString("minio_access_key"), MinIOAccessKey: viper.GetString("minio_access_key"),

View File

@@ -148,4 +148,5 @@ type DocumentPreviewResponse struct {
TotalPages int `json:"total_pages"` TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"` CurrentPage int `json:"current_page"`
Content string `json:"content"` Content string `json:"content"`
ContentType string `json:"content_type"` // url: 文件URL, html: HTML内容
} }

View File

@@ -7,6 +7,8 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
docparser "x-agents/server"
) )
// AICoreClient AI-Core 文档解析服务客户端 // AICoreClient AI-Core 文档解析服务客户端
@@ -53,7 +55,7 @@ func (c *AICoreClient) Close() {
} }
} }
// ParseDocument 解析文档 // ParseDocument 解析文档 - 使用生成的 protobuf 代码
func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*ParseResult, error) { func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*ParseResult, error) {
if c.conn == nil { if c.conn == nil {
if err := c.Connect(); err != nil { if err := c.Connect(); err != nil {
@@ -61,17 +63,16 @@ func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*Parse
} }
} }
// 使用 gRPC raw bytes 调用 // 使用生成的 protobuf 客户端
// 由于没有生成 protobuf 代码,使用 raw bytes 方式调用 client := docparser.NewDocumentParserClient(c.conn)
client := NewDocumentParserClient(c.conn)
req := &ParseRequest{ req := &docparser.ParseRequest{
FileUrl: fileURL, FileUrl: fileURL,
FileName: fileName, FileName: fileName,
FileType: fileType, FileType: fileType,
} }
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
resp, err := client.ParseDocument(ctx, req) resp, err := client.ParseDocument(ctx, req)
@@ -80,53 +81,11 @@ func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*Parse
} }
return &ParseResult{ return &ParseResult{
Success: resp.Success, Success: resp.GetSuccess(),
Content: resp.Content, Content: resp.GetContent(),
Message: resp.Message, Message: resp.GetMessage(),
ContentLength: resp.ContentLength, ContentLength: resp.GetContentLength(),
FileType: resp.FileType, FileType: resp.GetFileType(),
ParserEngine: resp.ParserEngine, ParserEngine: resp.GetParserEngine(),
}, nil }, nil
} }
// 以下是手动定义的 protobuf messages与 proto 文件一致)
// 不需要生成 .pb.go 文件,直接手动定义
type ParseRequest struct {
FileUrl string
FileName string
FileType string
ParserEngine string
}
type ParseResponse struct {
Success bool
Content string
Message string
ContentLength int32
FileType string
ParserEngine string
}
// DocumentParserClient gRPC 客户端接口(手动实现)
type DocumentParserClient interface {
ParseDocument(ctx context.Context, in *ParseRequest, opts ...grpc.CallOption) (*ParseResponse, error)
}
type documentParserClient struct {
cc grpc.ClientConnInterface
}
// NewDocumentParserClient 创建 DocumentParser 客户端
func NewDocumentParserClient(cc grpc.ClientConnInterface) DocumentParserClient {
return &documentParserClient{cc: cc}
}
func (c *documentParserClient) ParseDocument(ctx context.Context, in *ParseRequest, opts ...grpc.CallOption) (*ParseResponse, error) {
out := new(ParseResponse)
err := c.cc.Invoke(ctx, "/docparser.DocumentParser/ParseDocument", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}

View File

@@ -29,9 +29,10 @@ type KnowledgeService struct {
uploadService *UploadService uploadService *UploadService
pythonServiceURL string pythonServiceURL string
aiCoreClient *AICoreClient aiCoreClient *AICoreClient
markdownLocalPath string // Markdown 本地存储路径
} }
func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *repository.ModelRepository, uploadService *UploadService, pythonServiceURL, aiCoreServiceAddr string) *KnowledgeService { func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *repository.ModelRepository, uploadService *UploadService, pythonServiceURL, aiCoreServiceAddr, markdownLocalPath string) *KnowledgeService {
aiCoreClient, _ := NewAICoreClient(aiCoreServiceAddr) aiCoreClient, _ := NewAICoreClient(aiCoreServiceAddr)
return &KnowledgeService{ return &KnowledgeService{
repo: repo, repo: repo,
@@ -39,6 +40,7 @@ func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *reposi
uploadService: uploadService, uploadService: uploadService,
pythonServiceURL: pythonServiceURL, pythonServiceURL: pythonServiceURL,
aiCoreClient: aiCoreClient, aiCoreClient: aiCoreClient,
markdownLocalPath: markdownLocalPath,
} }
} }
@@ -307,6 +309,13 @@ func (s *KnowledgeService) parseDocumentWithAICore(docID, fileURL, fileName stri
if result.Success && result.Content != "" { if result.Success && result.Content != "" {
knowledgeDebugLog.Printf("[AICore] 解析成功: docID=%s, contentLength=%d", docID, len(result.Content)) knowledgeDebugLog.Printf("[AICore] 解析成功: docID=%s, contentLength=%d", docID, len(result.Content))
// 保存到本地文件
markdownPath := s.saveMarkdownToFile(docID, fileName, result.Content)
if markdownPath != "" {
knowledgeDebugLog.Printf("[AICore] Markdown 保存到本地: docID=%s, path=%s", docID, markdownPath)
}
// 更新文档的 Content 字段 // 更新文档的 Content 字段
s.repo.UpdateDocument(docID, map[string]interface{}{ s.repo.UpdateDocument(docID, map[string]interface{}{
"content": result.Content, "content": result.Content,
@@ -316,6 +325,31 @@ func (s *KnowledgeService) parseDocumentWithAICore(docID, fileURL, fileName stri
} }
} }
// saveMarkdownToFile 保存 Markdown 内容到本地文件
func (s *KnowledgeService) saveMarkdownToFile(docID, fileName, content string) string {
if s.markdownLocalPath == "" {
s.markdownLocalPath = "resource/markdown"
}
// 创建目录
if err := os.MkdirAll(s.markdownLocalPath, 0755); err != nil {
knowledgeDebugLog.Printf("[AICore] 创建目录失败: path=%s, err=%v", s.markdownLocalPath, err)
return ""
}
// 生成文件名(用 docID + .md
markdownFileName := docID + ".md"
markdownPath := s.markdownLocalPath + "/" + markdownFileName
// 写入文件
if err := os.WriteFile(markdownPath, []byte(content), 0644); err != nil {
knowledgeDebugLog.Printf("[AICore] 保存 Markdown 失败: path=%s, err=%v", markdownPath, err)
return ""
}
return markdownPath
}
// DeleteDocument 删除文档 // DeleteDocument 删除文档
func (s *KnowledgeService) DeleteDocument(kbID, docID string) error { func (s *KnowledgeService) DeleteDocument(kbID, docID string) error {
// 验证文档存在 // 验证文档存在
@@ -400,24 +434,66 @@ func (s *KnowledgeService) GetDocumentPreview(kbID, docID string, page int) (*mo
return nil, nil return nil, nil
} }
// 如果已解析,返回解析内容;否则返回文件 URL // 获取文件URL
if doc.Status == "parsed" {
// TODO: 从存储中读取解析内容(可以存到数据库或文件)
// 暂时返回文件 URL
fileURL, _ := s.uploadService.GetFileURL(doc.FileKey) fileURL, _ := s.uploadService.GetFileURL(doc.FileKey)
// 根据文件类型决定预览方式
fileName := doc.Name
isPDF := strings.HasSuffix(strings.ToLower(fileName), ".pdf")
isOffice := false
officeExts := []string{".csv", ".xlsx", ".xls", ".docx", ".doc", ".pptx", ".ppt", ".txt", ".md"}
for _, ext := range officeExts {
if strings.HasSuffix(strings.ToLower(fileName), ext) {
isOffice = true
break
}
}
// PDF文件返回文件URL
if isPDF {
return &model.DocumentPreviewResponse{ return &model.DocumentPreviewResponse{
TotalPages: 1, TotalPages: 1,
CurrentPage: page, CurrentPage: page,
Content: fileURL, Content: fileURL,
ContentType: "url",
}, nil }, nil
} }
// 未解析,返回文件 URL // Office文件调用解析服务转换为HTML
fileURL, _ := s.uploadService.GetFileURL(doc.FileKey) if isOffice && s.aiCoreClient != nil {
knowledgeDebugLog.Printf("[Preview] Parsing office file: %s, URL: %s", fileName, fileURL)
result, err := s.aiCoreClient.ParseDocument(fileURL, fileName, "")
if err != nil {
// 解析失败返回文件URL
knowledgeDebugLog.Printf("[Preview] Parse document failed: %v", err)
return &model.DocumentPreviewResponse{ return &model.DocumentPreviewResponse{
TotalPages: 1, TotalPages: 1,
CurrentPage: page, CurrentPage: page,
Content: fileURL, Content: fileURL,
ContentType: "url",
}, nil
}
knowledgeDebugLog.Printf("[Preview] Parse result: success=%v, content_length=%d", result.Success, len(result.Content))
// 返回HTML内容
if result.Success && result.Content != "" {
knowledgeDebugLog.Printf("[Preview] Returning HTML content, length: %d", len(result.Content))
return &model.DocumentPreviewResponse{
TotalPages: 1,
CurrentPage: page,
Content: result.Content,
ContentType: "html",
}, nil
}
}
// 其他情况返回文件URL
return &model.DocumentPreviewResponse{
TotalPages: 1,
CurrentPage: page,
Content: fileURL,
ContentType: "url",
}, nil }, nil
} }

View File

@@ -0,0 +1,5 @@
sk-5706307e3e3a4eb09452dbf0bb87fe31
https://dashscope.aliyuncs.com/compatible-mode/v1
qwen3.5-flash

View File

@@ -0,0 +1,5 @@
sk-5706307e3e3a4eb09452dbf0bb87fe31
https://dashscope.aliyuncs.com/compatible-mode/v1
qwen3.5-flash

View File

@@ -0,0 +1,4 @@
## students
| 班级 | 姓名 | 年龄 | 性别 |
| --- | --- | --- | --- |
| 1 | 曹 | 123 | 男 |

View File

@@ -0,0 +1,13 @@
# 1 . 你好么?
### 表哥啊啊
大叔大婶打扫的暗示打扫暗示
> 太好了
```python
print("hello world")
```

View File

@@ -0,0 +1,13 @@
# 1 . 你好么?
### 表哥啊啊
大叔大婶打扫的暗示打扫暗示
> 太好了
```python
print("hello world")
```

View File

@@ -0,0 +1,13 @@
# 1 . 你好么?
### 表哥啊啊
大叔大婶打扫的暗示打扫暗示
> 太好了
```python
print("hello world")
```

View File

@@ -0,0 +1,13 @@
# 1 . 你好么?
### 表哥啊啊
大叔大婶打扫的暗示打扫暗示
> 太好了
```python
print("hello world")
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
# 1 . 你好么?
### 表哥啊啊
大叔大婶打扫的暗示打扫暗示
> 太好了
```python
print("hello world")
```

View File

@@ -0,0 +1,5 @@
sk-5706307e3e3a4eb09452dbf0bb87fe31
https://dashscope.aliyuncs.com/compatible-mode/v1
qwen3.5-flash

View File

@@ -0,0 +1,3 @@
| 班级 | 姓名 | 年龄 | 性别 |
| --- | --- | --- | --- |
| 1 | 曹 | 123 | 男 |

View File

@@ -0,0 +1,5 @@
sk-5706307e3e3a4eb09452dbf0bb87fe31
https://dashscope.aliyuncs.com/compatible-mode/v1
qwen3.5-flash

View File

@@ -0,0 +1,13 @@
# 1 . 你好么?
### 表哥啊啊
大叔大婶打扫的暗示打扫暗示
> 太好了
```python
print("hello world")
```

View File

@@ -0,0 +1,3 @@
| 겯섬 | 檎츰 | 쾨쥑 | 昑깎 |
| --- | --- | --- | --- |
| 1 | 꿀 | 123 | 켕 |

View File

@@ -0,0 +1,5 @@
sk-5706307e3e3a4eb09452dbf0bb87fe31
https://dashscope.aliyuncs.com/compatible-mode/v1
qwen3.5-flash

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,43 @@
localhost:50051 localhost:50051
``` ```
## VLM 配置(可选)
VLM 用于提升图片文件的解析效果。如果不配置 VLM则使用默认的 MarkItDown 解析。
### 方式一:环境变量
```bash
# 设置环境变量
export VLM_API_KEY="your-api-key"
export VLM_PROVIDER="openai" # openai / anthropic / qwen
export VLM_MODEL="gpt-4o"
```
### 方式二:配置文件
`ai-core/config.yaml` 中配置:
```yaml
vlm:
enabled: true
provider: "openai" # openai / anthropic / qwen
model: "gpt-4o" # 模型名称
api_key: "sk-xxx" # API Key
base_url: "" # 自定义 API 地址(可选)
prompt: "" # 自定义提示词(可选)
```
### 支持的 VLM 提供商
| 提供商 | 示例模型 |
|--------|----------|
| openai | gpt-4o, gpt-4o-mini |
| anthropic | claude-3-opus, claude-3-sonnet |
| qwen | qwen-vl-max, qwen2-vl-72b |
---
## gRPC API 定义 ## gRPC API 定义
### 1. ParseDocument - 解析文档 ### 1. ParseDocument - 解析文档
@@ -16,128 +53,85 @@ message ParseRequest {
string file_url = 1; // 文件 URL必填 string file_url = 1; // 文件 URL必填
string file_name = 2; // 文件名,带扩展名(必填) string file_name = 2; // 文件名,带扩展名(必填)
string file_type = 3; // 文件类型(可选,自动检测) string file_type = 3; // 文件类型(可选,自动检测)
map<string, string> engine_overrides = 4; // 引擎配置 string parser_engine = 4; // 解析引擎(可选)
map<string, string> engine_overrides = 5; // 引擎配置
// VLM 配置(可选,优先级高于全局配置)
VLMConfig vlm_config = 6;
}
message VLMConfig {
bool enabled = 1;
string provider = 2;
string model = 3;
string api_key = 4;
string base_url = 5;
string prompt = 6;
} }
``` ```
**响应 (ParseResponse)** **响应 (ParseResponse)**
```protobuf ```protobuf
message ParseResponse { message ParseResponse {
bool success = 1; // 是否成功 bool success = 1;
string content = 2; // Markdown 内容 string content = 2; // Markdown 内容
string message = 3; // 状态消息 string message = 3;
int32 content_length = 4; // 内容长度 int32 content_length = 4;
string file_type = 5; // 文件类型 string file_type = 5;
string parser_engine = 6; // 解析引擎 (markitdown) string parser_engine = 6;
} }
``` ```
### 2. GetSupportedFormats - 获取支持的格式
**请求**: 空消息
**响应**
- `file_types`: string[] - 支持的扩展名列表
- `file_type_descriptions`: map<string, string> - 格式描述
--- ---
## Golang 对接示例 ## Golang 对接示例
### 1. 安装依赖 ### 基础调用(无 VLM 配置时使用 MarkItDown
```bash
go get google.golang.org/grpc
go get google.golang.org/grpc/credentials/insecure
```
### 2. 生成 Go Proto 代码
需要先将 `proto/document_parser.proto` 生成 Go 代码:
```bash
# 方法一:使用 grpc_tools
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/document_parser.proto
```
### 3. 完整调用代码
```go ```go
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "your-project/proto" // 替换为你的 proto 包路径
)
func main() {
// 连接 gRPC 服务
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
client := pb.NewDocumentParserClient(conn)
ctx := context.Background()
// 调用 ParseDocument
req := &pb.ParseRequest{ req := &pb.ParseRequest{
FileUrl: "https://example.com/document.pdf", FileUrl: "https://example.com/document.pdf",
FileName: "document.pdf", FileName: "document.pdf",
} }
resp, err := client.ParseDocument(ctx, req) resp, client.ParseDocument(ctx, req)
if err != nil {
log.Fatalf("解析失败: %v", err)
}
// 处理响应
if resp.Success {
fmt.Printf("解析成功!\n")
fmt.Printf("内容长度: %d 字符\n", resp.ContentLength)
fmt.Printf("Markdown 内容:\n%s\n", resp.Content)
} else {
fmt.Printf("解析失败: %s\n", resp.Message)
}
}
``` ```
### 4. 获取支持的格式 ### 带 VLM 配置调用
```go ```go
// 获取支持的文件格式 req := &pb.ParseRequest{
formatsReq := &pb.Empty{} FileUrl: "https://example.com/image.png",
formatsResp, err := client.GetSupportedFormats(ctx, formatsReq) FileName: "image.png",
if err != nil { VlmConfig: &pb.VLMConfig{
log.Fatal(err) Enabled: Provider: "open true,
ai",
Model: "gpt-4o",
ApiKey: "sk-xxx",
},
} }
fmt.Println("支持的格式:") resp, err := client.ParseDocument(ctx, req)
for _, ft := range formatsResp.FileTypes {
desc := formatsResp.FileTypeDescriptions[ft]
fmt.Printf(" - %s: %s\n", ft, desc)
}
``` ```
--- ---
## 解析逻辑
1. **图片文件** (jpg, png, webp 等)
- 如果配置了 VLM → 使用 VLM 解析
- 如果没有配置 VLM → 使用 MarkItDown 解析
2. **PDF/DOCX/PPTX 等文档**
- 使用 MarkItDown 解析
3. **VLM 优先级**
- gRPC 请求中的 vlm_config > 全局配置config.yaml/环境变量)
---
## 注意事项 ## 注意事项
1. **文件 URL**: 必须是可直接访问的 URL,服务会下载文件到内存解析 1. **文件 URL**: 必须是可直接访问的 URL
2. **文件名**: 必须带扩展名(如 `.pdf`, `.docx`),用于自动识别文件类型 2. **文件名**: 必须带扩展名(如 `.pdf`, `.png`
3. **返回内容**: 直接返回 Markdown 格式文本,可用于向量检索或 LLM 处理 3. **返回内容**: Markdown 格式文本

View File

@@ -6,10 +6,15 @@
- [ ] **AI-Core 文档解析服务对接** - [ ] **AI-Core 文档解析服务对接**
- 服务ai-core (gRPC, 端口 50051) - 服务ai-core (gRPC, 端口 50051)
- 功能将文档PDF/DOCX/PPTX 等)转换为 Markdown - 功能将文档PDF/DOCX/PPTX/图片等)转换为 Markdown
- 对接方式gRPC 调用 - 对接方式gRPC 调用
- 详细需求:[ai-core-api.md](./ai-core-api.md) - 详细需求:[ai-core-api.md](./ai-core-api.md)
- [ ] **VLM 调用支持**
- 支持 OpenAI GPT-4o、Anthropic Claude、阿里 Qwen VL
- 通过 vlm_config 配置启用
- 适用场景图片文件jpg, png 等)自动使用 VLM 解析
--- ---
> 需求完成后请完成者打 ✔ > 需求完成后请完成者打 ✔

58
web/package-lock.json generated
View File

@@ -8,10 +8,15 @@
"name": "x-agent-dashboard", "name": "x-agent-dashboard",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@vue-office/docx": "^1.6.3",
"@vue-office/excel": "^1.7.14",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.13.3", "element-plus": "^2.13.3",
"marked": "^17.0.4",
"papaparse": "^5.5.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-demi": "^0.14.10",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
@@ -1026,6 +1031,40 @@
"vscode-uri": "^3.0.8" "vscode-uri": "^3.0.8"
} }
}, },
"node_modules/@vue-office/docx": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@vue-office/docx/-/docx-1.6.3.tgz",
"integrity": "sha512-Cs+3CAaRBOWOiW4XAhTwwxJ0dy8cPIf6DqfNvYcD3YACiLwO4kuawLF2IAXxyijhbuOeoFsfvoVbOc16A/4bZA==",
"hasInstallScript": true,
"license": "MIT",
"peerDependencies": {
"@vue/composition-api": "^1.7.1",
"vue": "^2.0.0 || >=3.0.0",
"vue-demi": "^0.14.6"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vue-office/excel": {
"version": "1.7.14",
"resolved": "https://registry.npmmirror.com/@vue-office/excel/-/excel-1.7.14.tgz",
"integrity": "sha512-pVUgt+emDQUnW7q22CfnQ+jl43mM/7IFwYzOg7lwOwPEbiVB4K4qEQf+y/bc4xGXz75w1/e3Kz3G6wAafmFBFg==",
"hasInstallScript": true,
"license": "MIT",
"peerDependencies": {
"@vue/composition-api": "^1.7.1",
"vue": "^2.0.0 || >=3.0.0",
"vue-demi": "^0.14.6"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.29", "version": "3.5.29",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.29.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
@@ -1862,6 +1901,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/marked": {
"version": "17.0.4",
"resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.4.tgz",
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -1988,6 +2039,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
"license": "MIT"
},
"node_modules/path-browserify": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -2701,6 +2758,7 @@
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js" "vue-demi-switch": "bin/vue-demi-switch.js"

View File

@@ -9,10 +9,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vue-office/docx": "^1.6.3",
"@vue-office/excel": "^1.7.14",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.13.3", "element-plus": "^2.13.3",
"marked": "^17.0.4",
"papaparse": "^5.5.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-demi": "^0.14.10",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -5,6 +5,7 @@ defineProps<{
description?: string description?: string
icon?: string icon?: string
iconClass?: string iconClass?: string
class?: string
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -18,7 +19,7 @@ const close = () => {
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="modelValue" class="dialog-overlay" @click.self="close"> <div v-if="modelValue" class="dialog-overlay" :class="$props.class" @click.self="close">
<div class="dialog-container"> <div class="dialog-container">
<!-- 头部 --> <!-- 头部 -->
<div class="dialog-header"> <div class="dialog-header">
@@ -61,7 +62,7 @@ const close = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 9999; z-index: 2000;
} }
.dialog-container { .dialog-container {

View File

@@ -21,12 +21,15 @@ 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: 'Team', icon: 'fa-users', path: '/team' },
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
{ name: 'Database', icon: 'fa-database', path: '/database' }, { name: 'Database', icon: 'fa-database', path: '/database' },
{ name: 'Knowledge', icon: 'fa-brain', path: '/knowledge' }, { name: 'Knowledge', icon: 'fa-brain', path: '/knowledge' },
] ]
const middleMenu: MenuItem[] = [
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
]
const bottomMenu: MenuItem[] = [ const bottomMenu: MenuItem[] = [
{ name: 'Settings', icon: 'fa-gear', path: '/settings' }, { name: 'Settings', icon: 'fa-gear', path: '/settings' },
] ]
@@ -117,6 +120,25 @@ const handleUserCommand = (command: string) => {
<!-- 分隔线 --> <!-- 分隔线 -->
<li class="my-4 border-t border-dark-500"></li> <li class="my-4 border-t border-dark-500"></li>
<!-- Skills & Tools -->
<li v-for="item in middleMenu" :key="item.name">
<a
href="#"
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
@click="navigateTo(item)"
>
<div class="flex items-center gap-3">
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
<span>{{ item.name }}</span>
</div>
<span v-if="item.badge" class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">{{ item.badge }}</span>
</a>
</li>
<!-- 分隔线 -->
<li class="my-4 border-t border-dark-500"></li>
<!-- Settings --> <!-- Settings -->
<li v-for="item in bottomMenu" :key="item.name"> <li v-for="item in bottomMenu" :key="item.name">
<a <a

View File

@@ -3,6 +3,10 @@ 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 { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge'
import VueOfficeDocx from '@vue-office/docx'
import VueOfficeExcel from '@vue-office/excel'
import Papa from 'papaparse'
import { marked } from 'marked'
import './knowledge/knowledge.css' import './knowledge/knowledge.css'
// 获取已配置的模型列表 // 获取已配置的模型列表
@@ -135,7 +139,12 @@ const loadingDocuments = ref(false)
const fileInput = ref<HTMLInputElement | null>(null) const fileInput = ref<HTMLInputElement | null>(null)
const uploading = ref(false) const uploading = ref(false)
const previewUrl = ref('') // 文档预览URL (blob URL) const previewUrl = ref('') // 文档预览URL (blob URL)
const previewHtml = ref('') // HTML内容预览
const previewContentType = ref('') // content_type: url 或 html
const previewDownloadUrl = ref('') // 原始下载链接 const previewDownloadUrl = ref('') // 原始下载链接
const previewFileType = ref('') // 文件类型: pdf, docx, xlsx, csv
const previewCsvData = ref<any[]>([]) // CSV数据
const previewCsvHeaders = ref<string[]>([]) // CSV表头
const loadingPreview = ref(false) const loadingPreview = ref(false)
const previewPage = ref(1) // 当前页码 const previewPage = ref(1) // 当前页码
const previewTotalPages = ref(1) // 总页数 const previewTotalPages = ref(1) // 总页数
@@ -157,6 +166,20 @@ const loadPdfWithProxy = async (doc: any): Promise<string> => {
} }
} }
// 将URL转换为blob URL (用于Excel组件)
const loadFileAsBlob = async (url: string): Promise<Blob> => {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status}`)
}
return await response.blob()
} catch (error) {
console.error('Failed to load file as blob:', error)
throw error
}
}
const newKbForm = ref({ const newKbForm = ref({
name: '', name: '',
description: '', description: '',
@@ -337,6 +360,11 @@ const enterKnowledge = async (kb: any) => {
selectedKnowledge.value = kb selectedKnowledge.value = kb
selectedFile.value = null selectedFile.value = null
previewUrl.value = '' previewUrl.value = ''
previewHtml.value = ''
previewContentType.value = ''
previewFileType.value = ''
previewCsvData.value = []
previewCsvHeaders.value = []
previewDownloadUrl.value = '' previewDownloadUrl.value = ''
// 获取文档列表 // 获取文档列表
@@ -375,13 +403,126 @@ const selectDocument = async (doc: any) => {
selectedFile.value = doc.id selectedFile.value = doc.id
selectedDocument.value = doc selectedDocument.value = doc
previewUrl.value = '' previewUrl.value = ''
previewHtml.value = ''
previewContentType.value = ''
previewDownloadUrl.value = '' previewDownloadUrl.value = ''
previewFileType.value = ''
previewCsvData.value = []
previewCsvHeaders.value = []
previewPage.value = 1 previewPage.value = 1
previewTotalPages.value = 1 previewTotalPages.value = 1
// 优先使用代理接口加载PDF // 检测文件类型
const fileName = doc.name || ''
const ext = fileName.split('.').pop()?.toLowerCase() || ''
console.log('File name:', fileName, 'Ext:', ext)
if (ext === 'pdf') {
previewFileType.value = 'pdf'
} else if (ext === 'docx' || ext === 'doc') {
previewFileType.value = 'docx'
} else if (ext === 'xlsx' || ext === 'xls') {
previewFileType.value = 'xlsx'
} else if (ext === 'csv') {
previewFileType.value = 'csv'
} else if (ext === 'txt' || ext === 'md') {
previewFileType.value = 'text'
}
console.log('Preview file type:', previewFileType.value)
// 纯文本文件 (txt, md) 直接读取内容显示
if (previewFileType.value === 'text') {
// 优先使用 file_url上传时后端返回的
let url = doc.file_url || doc.fileUrl || doc.url || doc.FileURL || ''
// 如果没有 file_url尝试用 file_key 通过代理
if (!url && doc.file_key && selectedKnowledge.value) {
url = await loadPdfWithProxy(doc)
}
console.log('Text file URL:', url)
if (url) {
try {
const response = await fetch(url)
console.log('Response status:', response.status)
const arrayBuffer = await response.arrayBuffer()
const decoder = new TextDecoder('utf-8')
const content = decoder.decode(arrayBuffer)
console.log('Content length:', content.length, 'ext:', ext)
// .md 文件用 marked 渲染
if (ext === 'md') {
// marked.parse 返回 Promise需要 await
const html = await marked.parse(content)
previewHtml.value = html as string
} else {
// .txt 文件直接显示
previewHtml.value = '<pre style="white-space: pre-wrap; word-wrap: break-word; color: #e8eaed;">' + content + '</pre>'
}
console.log('PreviewHtml set, length:', previewHtml.value.length)
return
} catch (error) {
console.error('Failed to load text file:', error)
}
} else {
console.log('No URL for text file, file_key:', doc.file_key, 'file_url:', doc.file_url)
}
}
// CSV需要特殊处理用PapaParse解析
if (previewFileType.value === 'csv' && (doc.file_key || doc.file_url)) {
const url = doc.file_key && selectedKnowledge.value
? await loadPdfWithProxy(doc)
: doc.file_url || doc.fileUrl || doc.url || ''
if (url) {
try {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
// 尝试多种编码解码
let csvText = ''
const encodings = ['utf-8', 'gbk', 'gb2312', 'big5']
for (const encoding of encodings) {
try {
const decoder = new TextDecoder(encoding)
csvText = decoder.decode(arrayBuffer)
// 检查是否成功解码(如果没有乱码字符)
if (!csvText.includes('\uFFFD')) {
console.log(`CSV decoded with: ${encoding}`)
break
}
} catch (e) {
continue
}
}
const result = Papa.parse(csvText, { header: false, skipEmptyLines: true })
if (result.data && result.data.length > 0) {
previewCsvHeaders.value = result.data[0] as string[]
previewCsvData.value = result.data.slice(1) as any[][]
}
return
} catch (error) {
console.error('Failed to parse CSV:', error)
}
}
}
// 使用代理接口加载文件
if (doc.file_key && selectedKnowledge.value) { if (doc.file_key && selectedKnowledge.value) {
previewUrl.value = await loadPdfWithProxy(doc) const proxyUrl = await loadPdfWithProxy(doc)
// Excel/Word需要blob URL
if ((previewFileType.value === 'xlsx' || previewFileType.value === 'docx') && proxyUrl) {
try {
const blob = await loadFileAsBlob(proxyUrl)
const blobUrl = URL.createObjectURL(blob)
previewUrl.value = blobUrl
return
} catch (error) {
console.error('Failed to convert to blob:', error)
}
}
previewUrl.value = proxyUrl
if (previewUrl.value) { if (previewUrl.value) {
return return
} }
@@ -397,11 +538,19 @@ const selectDocument = async (doc: any) => {
// 如果没有file_url调用预览API获取 // 如果没有file_url调用预览API获取
if (selectedKnowledge.value) { if (selectedKnowledge.value) {
loadingPreview.value = true loadingPreview.value = true
previewUrl.value = ''
previewHtml.value = ''
previewContentType.value = ''
try { try {
const { getDocumentPreview } = await import('./knowledge/useKnowledge') const { getDocumentPreview } = await import('./knowledge/useKnowledge')
const result = await getDocumentPreview(selectedKnowledge.value.id, doc.id) const result = await getDocumentPreview(selectedKnowledge.value.id, doc.id)
if (result.success && result.data?.content) { if (result.success && result.data?.content) {
previewContentType.value = result.data.content_type || 'url'
if (previewContentType.value === 'html') {
previewHtml.value = result.data.content
} else {
previewUrl.value = result.data.content previewUrl.value = result.data.content
}
previewTotalPages.value = result.data.total_pages || 1 previewTotalPages.value = result.data.total_pages || 1
previewPage.value = result.data.current_page || 1 previewPage.value = result.data.current_page || 1
} }
@@ -416,6 +565,8 @@ const selectDocument = async (doc: any) => {
// 翻页 // 翻页
const changePreviewPage = async (page: number) => { const changePreviewPage = async (page: number) => {
if (!selectedKnowledge.value || !selectedFile.value || page < 1 || page > previewTotalPages.value) return if (!selectedKnowledge.value || !selectedFile.value || page < 1 || page > previewTotalPages.value) return
// HTML内容不支持翻页
if (previewContentType.value === 'html') return
loadingPreview.value = true loadingPreview.value = true
previewPage.value = page previewPage.value = page
@@ -423,7 +574,12 @@ const changePreviewPage = async (page: number) => {
const { getDocumentPreview } = await import('./knowledge/useKnowledge') const { getDocumentPreview } = await import('./knowledge/useKnowledge')
const result = await getDocumentPreview(selectedKnowledge.value.id, selectedFile.value, page) const result = await getDocumentPreview(selectedKnowledge.value.id, selectedFile.value, page)
if (result.success && result.data?.content) { if (result.success && result.data?.content) {
previewContentType.value = result.data.content_type || 'url'
if (previewContentType.value === 'html') {
previewHtml.value = result.data.content
} else {
previewUrl.value = result.data.content previewUrl.value = result.data.content
}
previewTotalPages.value = result.data.total_pages || 1 previewTotalPages.value = result.data.total_pages || 1
previewPage.value = result.data.current_page || page previewPage.value = result.data.current_page || page
} }
@@ -459,16 +615,8 @@ const handleFileSelect = async (event: Event) => {
// 获取刚上传的文档 // 获取刚上传的文档
const uploadedDoc = knowledgeDocuments.value.find(d => d.id === result.id) const uploadedDoc = knowledgeDocuments.value.find(d => d.id === result.id)
if (uploadedDoc) { if (uploadedDoc) {
// 选中新上传的文档 // 选中新上传的文档,调用 selectDocument 处理预览
selectedFile.value = result.id await selectDocument(uploadedDoc)
selectedDocument.value = uploadedDoc
// 使用代理接口加载PDF
if (uploadedDoc.file_key) {
previewUrl.value = await loadPdfWithProxy(uploadedDoc)
} else if (uploadedDoc.file_url) {
previewUrl.value = uploadedDoc.file_url
}
} }
} else { } else {
ElMessage.error(result.message || 'Failed to upload file') ElMessage.error(result.message || 'Failed to upload file')
@@ -497,6 +645,11 @@ const deleteDocument = async (docId: string) => {
selectedFile.value = null selectedFile.value = null
selectedDocument.value = null selectedDocument.value = null
previewUrl.value = '' previewUrl.value = ''
previewHtml.value = ''
previewContentType.value = ''
previewFileType.value = ''
previewCsvData.value = []
previewCsvHeaders.value = []
previewDownloadUrl.value = '' previewDownloadUrl.value = ''
} }
// 刷新文档列表 // 刷新文档列表
@@ -873,7 +1026,7 @@ const deleteDocument = async (docId: string) => {
type="file" type="file"
ref="fileInput" ref="fileInput"
style="display: none" style="display: none"
accept=".pdf,.doc,.docx,.txt,.md" accept=".pdf,.doc,.docx,.docx,.txt,.md,.csv,.xlsx,.xls,.pptx,.ppt"
@change="handleFileSelect" @change="handleFileSelect"
/> />
<button class="btn-primary" @click="triggerFileUpload"> <button class="btn-primary" @click="triggerFileUpload">
@@ -987,7 +1140,40 @@ const deleteDocument = async (docId: string) => {
<i class="fa-solid fa-spinner fa-spin"></i> <i class="fa-solid fa-spinner fa-spin"></i>
<span>Loading preview...</span> <span>Loading preview...</span>
</div> </div>
<!-- 有blob预览URL时显示PDF (使用iframe) --> <!-- Word文件预览 -->
<VueOfficeDocx
v-else-if="previewUrl && previewFileType === 'docx'"
:src="previewUrl"
class="office-embed"
@rendered="console.log('Docx rendered')"
@error="console.error('Docx error', $event)"
/>
<!-- Excel文件预览 -->
<VueOfficeExcel
v-else-if="previewUrl && previewFileType === 'xlsx'"
:src="previewUrl"
class="office-embed"
@rendered="console.log('Excel rendered')"
@error="console.error('Excel error', $event)"
/>
<!-- CSV文件预览 -->
<div v-else-if="previewFileType === 'csv' && previewCsvData.length > 0" class="csv-preview">
<table>
<thead>
<tr>
<th v-for="(header, index) in previewCsvHeaders" :key="index">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in previewCsvData" :key="rowIndex">
<td v-for="(cell, cellIndex) in row" :key="cellIndex">{{ cell }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 文本文件预览 (txt, md) -->
<div v-else-if="previewHtml" class="text-preview" v-html="previewHtml"></div>
<!-- PDF/其他文件预览 (使用iframe) -->
<iframe <iframe
v-else-if="previewUrl" v-else-if="previewUrl"
:src="previewUrl" :src="previewUrl"
@@ -996,9 +1182,9 @@ const deleteDocument = async (docId: string) => {
<!-- 无预览但有下载链接时显示下载按钮 --> <!-- 无预览但有下载链接时显示下载按钮 -->
<div v-else-if="previewDownloadUrl" class="preview-no-file"> <div v-else-if="previewDownloadUrl" class="preview-no-file">
<i class="fa-solid fa-file-pdf"></i> <i class="fa-solid fa-file-pdf"></i>
<span>Cannot preview PDF directly</span> <span>Cannot preview file directly</span>
<a :href="previewDownloadUrl" target="_blank" class="download-link"> <a :href="previewDownloadUrl" target="_blank" class="download-link">
<i class="fa-solid fa-download"></i> Download PDF <i class="fa-solid fa-download"></i> Download File
</a> </a>
</div> </div>
<!-- 无预览也无下载链接时显示提示 --> <!-- 无预览也无下载链接时显示提示 -->

View File

@@ -241,6 +241,7 @@ const showChangePassword = () => {
description="Configure your model settings" description="Configure your model settings"
icon="fa-solid fa-brain" icon="fa-solid fa-brain"
icon-class="bg-gradient-to-br from-primary-orange to-red-500" icon-class="bg-gradient-to-br from-primary-orange to-red-500"
class="add-model-dialog"
> >
<div class="space-y-4"> <div class="space-y-4">
<div> <div>

View File

@@ -943,6 +943,166 @@
border: none; border: none;
} }
/* HTML内容预览 */
.html-preview {
width: 100%;
height: 100%;
overflow: auto;
padding: 20px;
background-color: #1e1e24;
color: #e8eaed;
line-height: 1.6;
}
.html-preview table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.html-preview table th,
.html-preview table td {
border: 1px solid #3a3a4a;
padding: 8px 12px;
text-align: left;
}
.html-preview table th {
background-color: #2a2a3a;
}
.html-preview h1, .html-preview h2, .html-preview h3 {
margin-top: 16px;
margin-bottom: 8px;
color: #ffffff;
}
.html-preview pre {
background-color: #2a2a3a;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
}
.html-preview code {
background-color: #2a2a3a;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
/* Office 组件预览 */
.office-embed {
width: 100%;
height: 100%;
overflow: auto;
background-color: #ffffff;
}
/* 修复 vue-office 表格黑色背景问题 */
.office-embed :deep(table) {
background-color: #ffffff !important;
color: #333333 !important;
}
.office-embed :deep(td),
.office-embed :deep(th) {
background-color: #ffffff !important;
color: #333333 !important;
border: 1px solid #dddddd !important;
}
/* CSV 表格预览 */
.csv-preview {
width: 100%;
height: 100%;
overflow: auto;
background-color: #ffffff;
padding: 16px;
}
.csv-preview table {
width: 100%;
border-collapse: collapse;
}
.csv-preview th,
.csv-preview td {
border: 1px solid #dddddd;
padding: 8px 12px;
text-align: left;
color: #333333;
}
.csv-preview th {
background-color: #f5f5f5;
font-weight: 600;
}
.csv-preview tr:nth-child(even) {
background-color: #fafafa;
}
/* 文本文件预览 */
.text-preview {
width: 100%;
height: 100%;
overflow: auto;
padding: 16px;
background-color: #1e1e24;
color: #e8eaed;
}
.text-preview pre {
white-space: pre-wrap;
word-wrap: break-word;
color: #e8eaed;
font-family: monospace;
font-size: 14px;
line-height: 1.6;
}
/* Markdown 渲染样式 */
.text-preview h1, .text-preview h2, .text-preview h3,
.text-preview h4, .text-preview h5, .text-preview h6 {
color: #ffffff;
margin-top: 16px;
margin-bottom: 8px;
}
.text-preview code {
background-color: #2a2a3a;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
.text-preview pre {
background-color: #2a2a3a;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
}
.text-preview ul, .text-preview ol {
padding-left: 24px;
}
.text-preview li {
margin-bottom: 4px;
}
.text-preview a {
color: #f97316;
}
.text-preview blockquote {
border-left: 4px solid #f97316;
padding-left: 16px;
margin-left: 0;
color: #9ca3af;
}
.preview-info { .preview-info {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);

View File

@@ -256,10 +256,14 @@
color: #6b7280; color: #6b7280;
} }
.add-model-dialog :deep(.el-select) { .add-model-dialog .el-select {
width: 100%; width: 100%;
} }
.add-model-dialog .el-select .el-input {
pointer-events: auto;
}
.add-model-dialog :deep(.el-select .el-input__wrapper) { .add-model-dialog :deep(.el-select .el-input__wrapper) {
background-color: #171922; background-color: #171922;
border: 1px solid #4b5563; border: 1px solid #4b5563;
@@ -276,6 +280,7 @@
.add-model-dialog :deep(.el-select-dropdown) { .add-model-dialog :deep(.el-select-dropdown) {
background-color: #1a1a24; background-color: #1a1a24;
border: 1px solid #2a2a3a; border: 1px solid #2a2a3a;
z-index: 10000;
} }
.add-model-dialog :deep(.el-select-dropdown__item) { .add-model-dialog :deep(.el-select-dropdown__item) {