Compare commits
5 Commits
ab7131eb05
...
e633462ace
| Author | SHA1 | Date | |
|---|---|---|---|
| e633462ace | |||
| 9ca267244d | |||
| 49433f1681 | |||
| 4a7199de93 | |||
| 5012a25f99 |
18
ai-core/config.example.yaml
Normal file
18
ai-core/config.example.yaml
Normal 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
84
ai-core/parser/config.py
Normal 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"])
|
||||
@@ -1,39 +1,68 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, Any
|
||||
from markitdown import MarkItDown
|
||||
|
||||
from .vlm_client import VLMClient
|
||||
from .config import get_vlm_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Parser:
|
||||
"""基于 MarkItDown 的统一文档解析器
|
||||
|
||||
"""基于 MarkItDown + VLM 的统一文档解析器
|
||||
|
||||
支持格式:PDF、DOCX、DOC、PPTX、PPT、XLSX、XLS、CSV、图片、网页、Markdown 等
|
||||
|
||||
VLM 解析:
|
||||
- 方式一:启动时配置(config.yaml 或环境变量)
|
||||
- 方式二:gRPC 请求时传入 VLM 配置(优先级更高)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
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
|
||||
|
||||
|
||||
Args:
|
||||
file_path: 文件路径或 URL
|
||||
file_type: 文件类型(可选,MarkItDown 会自动检测)
|
||||
|
||||
vlm_config: VLM 配置(可选,优先级高于全局配置)
|
||||
|
||||
Returns:
|
||||
dict: 包含 markdown 内容和元数据
|
||||
"""
|
||||
# 如果有 VLM 配置,覆盖全局配置
|
||||
if vlm_config:
|
||||
self.set_vlm_config(vlm_config)
|
||||
|
||||
try:
|
||||
logger.info(f"Parsing file: {file_path}")
|
||||
|
||||
|
||||
result = self.markitdown.convert(file_path)
|
||||
|
||||
|
||||
logger.info(f"Parse successful: {len(result.text_content)} characters")
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": result.text_content,
|
||||
@@ -49,29 +78,40 @@ class Parser:
|
||||
"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
|
||||
|
||||
|
||||
Args:
|
||||
content: 文件字节内容
|
||||
file_name: 文件名
|
||||
file_type: 文件类型(可选)
|
||||
|
||||
vlm_config: VLM 配置(可选,优先级高于全局配置)
|
||||
|
||||
Returns:
|
||||
dict: 包含 markdown 内容和元数据
|
||||
"""
|
||||
# 如果有 VLM 配置,覆盖全局配置
|
||||
if vlm_config:
|
||||
self.set_vlm_config(vlm_config)
|
||||
|
||||
try:
|
||||
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:
|
||||
temp_file.write(content)
|
||||
temp_path = temp_file.name
|
||||
|
||||
|
||||
try:
|
||||
result = self.markitdown.convert(temp_path)
|
||||
|
||||
|
||||
logger.info(f"Parse successful: {len(result.text_content)} characters")
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": result.text_content,
|
||||
@@ -89,10 +129,69 @@ class Parser:
|
||||
"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__":
|
||||
parser = Parser()
|
||||
|
||||
|
||||
# 测试
|
||||
test_url = "https://example.com"
|
||||
result = parser.parse(test_url)
|
||||
|
||||
209
ai-core/parser/vlm_client.py
Normal file
209
ai-core/parser/vlm_client.py
Normal 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)
|
||||
}
|
||||
@@ -16,6 +16,18 @@ message ParseRequest {
|
||||
string file_type = 3;
|
||||
string parser_engine = 4;
|
||||
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 {
|
||||
@@ -44,4 +56,4 @@ message EngineInfo {
|
||||
repeated string supported_file_types = 3;
|
||||
bool available = 4;
|
||||
string unavailable_reason = 5;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
@@ -37,21 +37,23 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._loaded_options = None
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_options = b'8\001'
|
||||
_globals['_PARSEREQUEST']._serialized_start=37
|
||||
_globals['_PARSEREQUEST']._serialized_end=258
|
||||
_globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_start=204
|
||||
_globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_end=258
|
||||
_globals['_PARSERESPONSE']._serialized_start=261
|
||||
_globals['_PARSERESPONSE']._serialized_end=393
|
||||
_globals['_EMPTY']._serialized_start=395
|
||||
_globals['_EMPTY']._serialized_end=402
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE']._serialized_start=405
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE']._serialized_end=607
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_start=548
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_end=607
|
||||
_globals['_ENGINESRESPONSE']._serialized_start=609
|
||||
_globals['_ENGINESRESPONSE']._serialized_end=666
|
||||
_globals['_ENGINEINFO']._serialized_start=668
|
||||
_globals['_ENGINEINFO']._serialized_end=792
|
||||
_globals['_DOCUMENTPARSER']._serialized_start=795
|
||||
_globals['_DOCUMENTPARSER']._serialized_end=1017
|
||||
_globals['_PARSEREQUEST']._serialized_end=300
|
||||
_globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_start=246
|
||||
_globals['_PARSEREQUEST_ENGINEOVERRIDESENTRY']._serialized_end=300
|
||||
_globals['_VLMCONFIG']._serialized_start=302
|
||||
_globals['_VLMCONFIG']._serialized_end=414
|
||||
_globals['_PARSERESPONSE']._serialized_start=417
|
||||
_globals['_PARSERESPONSE']._serialized_end=549
|
||||
_globals['_EMPTY']._serialized_start=551
|
||||
_globals['_EMPTY']._serialized_end=558
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE']._serialized_start=561
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE']._serialized_end=763
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_start=704
|
||||
_globals['_SUPPORTEDFORMATSRESPONSE_FILETYPEDESCRIPTIONSENTRY']._serialized_end=763
|
||||
_globals['_ENGINESRESPONSE']._serialized_start=765
|
||||
_globals['_ENGINESRESPONSE']._serialized_end=822
|
||||
_globals['_ENGINEINFO']._serialized_start=824
|
||||
_globals['_ENGINEINFO']._serialized_end=948
|
||||
_globals['_DOCUMENTPARSER']._serialized_start=951
|
||||
_globals['_DOCUMENTPARSER']._serialized_end=1173
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -6,8 +6,9 @@ grpcio-tools>=1.60.0
|
||||
grpcio-reflection>=1.60.0
|
||||
protobuf>=4.25.0
|
||||
|
||||
# HTTP 请求
|
||||
# 配置文件解析
|
||||
pyyaml>=6.0
|
||||
requests>=2.31.0
|
||||
|
||||
# 文档解析
|
||||
markitdown>=0.0.1
|
||||
# 文档解析 - markitdown 及其所有依赖
|
||||
markitdown[pdf,docx,pptx,xlsx,all]>=0.0.1
|
||||
@@ -75,6 +75,21 @@ class DocumentParserServicer:
|
||||
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)
|
||||
|
||||
try:
|
||||
@@ -95,9 +110,9 @@ class DocumentParserServicer:
|
||||
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):
|
||||
logger.warning("Parser returned failure: %s", result.get("error", "Unknown error"))
|
||||
|
||||
@@ -87,7 +87,7 @@ func main() {
|
||||
if err != nil {
|
||||
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
|
||||
dbHandler := handler.NewDatabaseHandler(dbService)
|
||||
|
||||
498
server/document_parser.pb.go
Normal file
498
server/document_parser.pb.go
Normal 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
|
||||
}
|
||||
197
server/document_parser_grpc.pb.go
Normal file
197
server/document_parser_grpc.pb.go
Normal 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",
|
||||
}
|
||||
@@ -23,9 +23,10 @@ type Config struct {
|
||||
PythonServiceURL string
|
||||
AICoreServiceAddr string // AI-Core gRPC 服务地址,如 "localhost:50051"
|
||||
// 文件上传配置
|
||||
UploadMode string // "local" 或 "minio"
|
||||
UploadLocalPath string // 本地存储路径,如 "resource/files"
|
||||
ServerBaseURL string // 服务器基础 URL,用于生成本地文件 URL
|
||||
UploadMode string // "local" 或 "minio"
|
||||
UploadLocalPath string // 本地存储路径,如 "resource/files"
|
||||
ServerBaseURL string // 服务器基础 URL,用于生成本地文件 URL
|
||||
MarkdownLocalPath string // Markdown 文件存储路径,如 "resource/markdown"
|
||||
// MinIO 配置
|
||||
MinIOEndpoint string
|
||||
MinIOAccessKey string
|
||||
@@ -56,6 +57,7 @@ func Load() *Config {
|
||||
viper.SetDefault("upload_mode", "local")
|
||||
viper.SetDefault("upload_local_path", "resource/files")
|
||||
viper.SetDefault("server_base_url", "http://localhost:8080")
|
||||
viper.SetDefault("markdown_local_path", "resource/markdown")
|
||||
viper.SetDefault("minio_endpoint", "localhost:9000")
|
||||
viper.SetDefault("minio_access_key", "")
|
||||
viper.SetDefault("minio_secret_key", "")
|
||||
@@ -87,9 +89,10 @@ func Load() *Config {
|
||||
PythonServiceURL: viper.GetString("python_service_url"),
|
||||
AICoreServiceAddr: viper.GetString("ai_core_service_addr"),
|
||||
// 文件上传配置
|
||||
UploadMode: viper.GetString("upload_mode"),
|
||||
UploadLocalPath: viper.GetString("upload_local_path"),
|
||||
ServerBaseURL: viper.GetString("server_base_url"),
|
||||
UploadMode: viper.GetString("upload_mode"),
|
||||
UploadLocalPath: viper.GetString("upload_local_path"),
|
||||
ServerBaseURL: viper.GetString("server_base_url"),
|
||||
MarkdownLocalPath: viper.GetString("markdown_local_path"),
|
||||
// MinIO 配置
|
||||
MinIOEndpoint: viper.GetString("minio_endpoint"),
|
||||
MinIOAccessKey: viper.GetString("minio_access_key"),
|
||||
|
||||
@@ -145,7 +145,8 @@ type UploadDocumentResponse struct {
|
||||
|
||||
// DocumentPreviewResponse 文档预览响应
|
||||
type DocumentPreviewResponse struct {
|
||||
TotalPages int `json:"total_pages"`
|
||||
CurrentPage int `json:"current_page"`
|
||||
Content string `json:"content"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
CurrentPage int `json:"current_page"`
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"` // url: 文件URL, html: HTML内容
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
docparser "x-agents/server"
|
||||
)
|
||||
|
||||
// AICoreClient AI-Core 文档解析服务客户端
|
||||
@@ -53,7 +55,7 @@ func (c *AICoreClient) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// ParseDocument 解析文档
|
||||
// ParseDocument 解析文档 - 使用生成的 protobuf 代码
|
||||
func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*ParseResult, error) {
|
||||
if c.conn == nil {
|
||||
if err := c.Connect(); err != nil {
|
||||
@@ -61,17 +63,16 @@ func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*Parse
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 gRPC raw bytes 调用
|
||||
// 由于没有生成 protobuf 代码,使用 raw bytes 方式调用
|
||||
client := NewDocumentParserClient(c.conn)
|
||||
// 使用生成的 protobuf 客户端
|
||||
client := docparser.NewDocumentParserClient(c.conn)
|
||||
|
||||
req := &ParseRequest{
|
||||
req := &docparser.ParseRequest{
|
||||
FileUrl: fileURL,
|
||||
FileName: fileName,
|
||||
FileType: fileType,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.ParseDocument(ctx, req)
|
||||
@@ -80,53 +81,11 @@ func (c *AICoreClient) ParseDocument(fileURL, fileName, fileType string) (*Parse
|
||||
}
|
||||
|
||||
return &ParseResult{
|
||||
Success: resp.Success,
|
||||
Content: resp.Content,
|
||||
Message: resp.Message,
|
||||
ContentLength: resp.ContentLength,
|
||||
FileType: resp.FileType,
|
||||
ParserEngine: resp.ParserEngine,
|
||||
Success: resp.GetSuccess(),
|
||||
Content: resp.GetContent(),
|
||||
Message: resp.GetMessage(),
|
||||
ContentLength: resp.GetContentLength(),
|
||||
FileType: resp.GetFileType(),
|
||||
ParserEngine: resp.GetParserEngine(),
|
||||
}, 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
|
||||
}
|
||||
|
||||
@@ -29,9 +29,10 @@ type KnowledgeService struct {
|
||||
uploadService *UploadService
|
||||
pythonServiceURL string
|
||||
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)
|
||||
return &KnowledgeService{
|
||||
repo: repo,
|
||||
@@ -39,6 +40,7 @@ func NewKnowledgeService(repo *repository.KnowledgeRepository, modelRepo *reposi
|
||||
uploadService: uploadService,
|
||||
pythonServiceURL: pythonServiceURL,
|
||||
aiCoreClient: aiCoreClient,
|
||||
markdownLocalPath: markdownLocalPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +309,13 @@ func (s *KnowledgeService) parseDocumentWithAICore(docID, fileURL, fileName stri
|
||||
|
||||
if result.Success && 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 字段
|
||||
s.repo.UpdateDocument(docID, map[string]interface{}{
|
||||
"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 删除文档
|
||||
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
|
||||
}
|
||||
|
||||
// 如果已解析,返回解析内容;否则返回文件 URL
|
||||
if doc.Status == "parsed" {
|
||||
// TODO: 从存储中读取解析内容(可以存到数据库或文件)
|
||||
// 暂时返回文件 URL
|
||||
fileURL, _ := s.uploadService.GetFileURL(doc.FileKey)
|
||||
// 获取文件URL
|
||||
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{
|
||||
TotalPages: 1,
|
||||
CurrentPage: page,
|
||||
Content: fileURL,
|
||||
ContentType: "url",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 未解析,返回文件 URL
|
||||
fileURL, _ := s.uploadService.GetFileURL(doc.FileKey)
|
||||
// Office文件调用解析服务转换为HTML
|
||||
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{
|
||||
TotalPages: 1,
|
||||
CurrentPage: page,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
sk-5706307e3e3a4eb09452dbf0bb87fe31
|
||||
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
qwen3.5-flash
|
||||
@@ -0,0 +1,5 @@
|
||||
sk-5706307e3e3a4eb09452dbf0bb87fe31
|
||||
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
qwen3.5-flash
|
||||
@@ -0,0 +1,4 @@
|
||||
## students
|
||||
| 班级 | 姓名 | 年龄 | 性别 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 曹 | 123 | 男 |
|
||||
@@ -0,0 +1,13 @@
|
||||
# 1 . 你好么?
|
||||
|
||||
### 表哥啊啊
|
||||
|
||||
大叔大婶打扫的暗示打扫暗示
|
||||
|
||||
> 太好了
|
||||
|
||||
```python
|
||||
|
||||
print("hello world")
|
||||
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# 1 . 你好么?
|
||||
|
||||
### 表哥啊啊
|
||||
|
||||
大叔大婶打扫的暗示打扫暗示
|
||||
|
||||
> 太好了
|
||||
|
||||
```python
|
||||
|
||||
print("hello world")
|
||||
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# 1 . 你好么?
|
||||
|
||||
### 表哥啊啊
|
||||
|
||||
大叔大婶打扫的暗示打扫暗示
|
||||
|
||||
> 太好了
|
||||
|
||||
```python
|
||||
|
||||
print("hello world")
|
||||
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# 1 . 你好么?
|
||||
|
||||
### 表哥啊啊
|
||||
|
||||
大叔大婶打扫的暗示打扫暗示
|
||||
|
||||
> 太好了
|
||||
|
||||
```python
|
||||
|
||||
print("hello world")
|
||||
|
||||
```
|
||||
1834
server/resource/markdown/698e892b-cf0c-4fa8-998a-d46c5ec5dc5d.md
Normal file
1834
server/resource/markdown/698e892b-cf0c-4fa8-998a-d46c5ec5dc5d.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
# 1 . 你好么?
|
||||
|
||||
### 表哥啊啊
|
||||
|
||||
大叔大婶打扫的暗示打扫暗示
|
||||
|
||||
> 太好了
|
||||
|
||||
```python
|
||||
|
||||
print("hello world")
|
||||
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
sk-5706307e3e3a4eb09452dbf0bb87fe31
|
||||
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
qwen3.5-flash
|
||||
@@ -0,0 +1,3 @@
|
||||
| 班级 | 姓名 | 年龄 | 性别 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 曹 | 123 | 男 |
|
||||
@@ -0,0 +1,5 @@
|
||||
sk-5706307e3e3a4eb09452dbf0bb87fe31
|
||||
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
qwen3.5-flash
|
||||
@@ -0,0 +1,13 @@
|
||||
# 1 . 你好么?
|
||||
|
||||
### 表哥啊啊
|
||||
|
||||
大叔大婶打扫的暗示打扫暗示
|
||||
|
||||
> 太好了
|
||||
|
||||
```python
|
||||
|
||||
print("hello world")
|
||||
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
| 겯섬 | 檎츰 | 쾨쥑 | 昑깎 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 꿀 | 123 | 켕 |
|
||||
@@ -0,0 +1,5 @@
|
||||
sk-5706307e3e3a4eb09452dbf0bb87fe31
|
||||
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
qwen3.5-flash
|
||||
1834
server/resource/markdown/eea7fcdc-503d-47a8-af04-13e2adc0ca4a.md
Normal file
1834
server/resource/markdown/eea7fcdc-503d-47a8-af04-13e2adc0ca4a.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,43 @@
|
||||
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 定义
|
||||
|
||||
### 1. ParseDocument - 解析文档
|
||||
@@ -16,128 +53,85 @@ message ParseRequest {
|
||||
string file_url = 1; // 文件 URL(必填)
|
||||
string file_name = 2; // 文件名,带扩展名(必填)
|
||||
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)**
|
||||
```protobuf
|
||||
message ParseResponse {
|
||||
bool success = 1; // 是否成功
|
||||
bool success = 1;
|
||||
string content = 2; // Markdown 内容
|
||||
string message = 3; // 状态消息
|
||||
int32 content_length = 4; // 内容长度
|
||||
string file_type = 5; // 文件类型
|
||||
string parser_engine = 6; // 解析引擎 (markitdown)
|
||||
string message = 3;
|
||||
int32 content_length = 4;
|
||||
string file_type = 5;
|
||||
string parser_engine = 6;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. GetSupportedFormats - 获取支持的格式
|
||||
|
||||
**请求**: 空消息
|
||||
|
||||
**响应**
|
||||
- `file_types`: string[] - 支持的扩展名列表
|
||||
- `file_type_descriptions`: map<string, string> - 格式描述
|
||||
|
||||
---
|
||||
|
||||
## Golang 对接示例
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```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. 完整调用代码
|
||||
### 基础调用(无 VLM 配置时使用 MarkItDown)
|
||||
|
||||
```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{
|
||||
FileUrl: "https://example.com/document.pdf",
|
||||
FileName: "document.pdf",
|
||||
}
|
||||
|
||||
resp, err := 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)
|
||||
}
|
||||
req := &pb.ParseRequest{
|
||||
FileUrl: "https://example.com/document.pdf",
|
||||
FileName: "document.pdf",
|
||||
}
|
||||
|
||||
resp, client.ParseDocument(ctx, req)
|
||||
```
|
||||
|
||||
### 4. 获取支持的格式
|
||||
### 带 VLM 配置调用
|
||||
|
||||
```go
|
||||
// 获取支持的文件格式
|
||||
formatsReq := &pb.Empty{}
|
||||
formatsResp, err := client.GetSupportedFormats(ctx, formatsReq)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
req := &pb.ParseRequest{
|
||||
FileUrl: "https://example.com/image.png",
|
||||
FileName: "image.png",
|
||||
VlmConfig: &pb.VLMConfig{
|
||||
Enabled: Provider: "open true,
|
||||
ai",
|
||||
Model: "gpt-4o",
|
||||
ApiKey: "sk-xxx",
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("支持的格式:")
|
||||
for _, ft := range formatsResp.FileTypes {
|
||||
desc := formatsResp.FileTypeDescriptions[ft]
|
||||
fmt.Printf(" - %s: %s\n", ft, desc)
|
||||
}
|
||||
resp, err := client.ParseDocument(ctx, req)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 解析逻辑
|
||||
|
||||
1. **图片文件** (jpg, png, webp 等)
|
||||
- 如果配置了 VLM → 使用 VLM 解析
|
||||
- 如果没有配置 VLM → 使用 MarkItDown 解析
|
||||
|
||||
2. **PDF/DOCX/PPTX 等文档**
|
||||
- 使用 MarkItDown 解析
|
||||
|
||||
3. **VLM 优先级**
|
||||
- gRPC 请求中的 vlm_config > 全局配置(config.yaml/环境变量)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **文件 URL**: 必须是可直接访问的 URL,服务会下载文件到内存解析
|
||||
2. **文件名**: 必须带扩展名(如 `.pdf`, `.docx`),用于自动识别文件类型
|
||||
3. **返回内容**: 直接返回 Markdown 格式文本,可用于向量检索或 LLM 处理
|
||||
1. **文件 URL**: 必须是可直接访问的 URL
|
||||
2. **文件名**: 必须带扩展名(如 `.pdf`, `.png`)
|
||||
3. **返回内容**: Markdown 格式文本
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
|
||||
- [ ] **AI-Core 文档解析服务对接**
|
||||
- 服务:ai-core (gRPC, 端口 50051)
|
||||
- 功能:将文档(PDF/DOCX/PPTX 等)转换为 Markdown
|
||||
- 功能:将文档(PDF/DOCX/PPTX/图片等)转换为 Markdown
|
||||
- 对接方式:gRPC 调用
|
||||
- 详细需求:[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
58
web/package-lock.json
generated
@@ -8,10 +8,15 @@
|
||||
"name": "x-agent-dashboard",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@vue-office/docx": "^1.6.3",
|
||||
"@vue-office/excel": "^1.7.14",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.13.3",
|
||||
"marked": "^17.0.4",
|
||||
"papaparse": "^5.5.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1026,6 +1031,40 @@
|
||||
"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": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
|
||||
@@ -1862,6 +1901,18 @@
|
||||
"@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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
@@ -1988,6 +2039,12 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
@@ -2701,6 +2758,7 @@
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
|
||||
@@ -9,10 +9,15 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue-office/docx": "^1.6.3",
|
||||
"@vue-office/excel": "^1.7.14",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.13.3",
|
||||
"marked": "^17.0.4",
|
||||
"papaparse": "^5.5.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,6 +5,7 @@ defineProps<{
|
||||
description?: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -18,7 +19,7 @@ const close = () => {
|
||||
|
||||
<template>
|
||||
<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-header">
|
||||
@@ -61,7 +62,7 @@ const close = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
|
||||
@@ -21,12 +21,15 @@ const mainMenu: MenuItem[] = [
|
||||
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
||||
{ name: 'Agents', icon: 'fa-robot', badge: 3, path: '/agents' },
|
||||
{ name: 'Team', icon: 'fa-users', path: '/team' },
|
||||
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
||||
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
||||
{ name: 'Database', icon: 'fa-database', path: '/database' },
|
||||
{ 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[] = [
|
||||
{ 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>
|
||||
|
||||
<!-- 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 -->
|
||||
<li v-for="item in bottomMenu" :key="item.name">
|
||||
<a
|
||||
|
||||
@@ -3,6 +3,10 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useModelSettings } from './settings/useModelSettings'
|
||||
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'
|
||||
|
||||
// 获取已配置的模型列表
|
||||
@@ -135,7 +139,12 @@ const loadingDocuments = ref(false)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const uploading = ref(false)
|
||||
const previewUrl = ref('') // 文档预览URL (blob URL)
|
||||
const previewHtml = ref('') // HTML内容预览
|
||||
const previewContentType = ref('') // content_type: url 或 html
|
||||
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 previewPage = 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({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -337,6 +360,11 @@ const enterKnowledge = async (kb: any) => {
|
||||
selectedKnowledge.value = kb
|
||||
selectedFile.value = null
|
||||
previewUrl.value = ''
|
||||
previewHtml.value = ''
|
||||
previewContentType.value = ''
|
||||
previewFileType.value = ''
|
||||
previewCsvData.value = []
|
||||
previewCsvHeaders.value = []
|
||||
previewDownloadUrl.value = ''
|
||||
|
||||
// 获取文档列表
|
||||
@@ -375,13 +403,126 @@ const selectDocument = async (doc: any) => {
|
||||
selectedFile.value = doc.id
|
||||
selectedDocument.value = doc
|
||||
previewUrl.value = ''
|
||||
previewHtml.value = ''
|
||||
previewContentType.value = ''
|
||||
previewDownloadUrl.value = ''
|
||||
previewFileType.value = ''
|
||||
previewCsvData.value = []
|
||||
previewCsvHeaders.value = []
|
||||
previewPage.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) {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
@@ -397,11 +538,19 @@ const selectDocument = async (doc: any) => {
|
||||
// 如果没有file_url,调用预览API获取
|
||||
if (selectedKnowledge.value) {
|
||||
loadingPreview.value = true
|
||||
previewUrl.value = ''
|
||||
previewHtml.value = ''
|
||||
previewContentType.value = ''
|
||||
try {
|
||||
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||
const result = await getDocumentPreview(selectedKnowledge.value.id, doc.id)
|
||||
if (result.success && result.data?.content) {
|
||||
previewUrl.value = result.data.content
|
||||
previewContentType.value = result.data.content_type || 'url'
|
||||
if (previewContentType.value === 'html') {
|
||||
previewHtml.value = result.data.content
|
||||
} else {
|
||||
previewUrl.value = result.data.content
|
||||
}
|
||||
previewTotalPages.value = result.data.total_pages || 1
|
||||
previewPage.value = result.data.current_page || 1
|
||||
}
|
||||
@@ -416,6 +565,8 @@ const selectDocument = async (doc: any) => {
|
||||
// 翻页
|
||||
const changePreviewPage = async (page: number) => {
|
||||
if (!selectedKnowledge.value || !selectedFile.value || page < 1 || page > previewTotalPages.value) return
|
||||
// HTML内容不支持翻页
|
||||
if (previewContentType.value === 'html') return
|
||||
|
||||
loadingPreview.value = true
|
||||
previewPage.value = page
|
||||
@@ -423,7 +574,12 @@ const changePreviewPage = async (page: number) => {
|
||||
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||
const result = await getDocumentPreview(selectedKnowledge.value.id, selectedFile.value, page)
|
||||
if (result.success && result.data?.content) {
|
||||
previewUrl.value = result.data.content
|
||||
previewContentType.value = result.data.content_type || 'url'
|
||||
if (previewContentType.value === 'html') {
|
||||
previewHtml.value = result.data.content
|
||||
} else {
|
||||
previewUrl.value = result.data.content
|
||||
}
|
||||
previewTotalPages.value = result.data.total_pages || 1
|
||||
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)
|
||||
if (uploadedDoc) {
|
||||
// 选中新上传的文档
|
||||
selectedFile.value = result.id
|
||||
selectedDocument.value = uploadedDoc
|
||||
|
||||
// 使用代理接口加载PDF
|
||||
if (uploadedDoc.file_key) {
|
||||
previewUrl.value = await loadPdfWithProxy(uploadedDoc)
|
||||
} else if (uploadedDoc.file_url) {
|
||||
previewUrl.value = uploadedDoc.file_url
|
||||
}
|
||||
// 选中新上传的文档,调用 selectDocument 处理预览
|
||||
await selectDocument(uploadedDoc)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Failed to upload file')
|
||||
@@ -497,7 +645,12 @@ const deleteDocument = async (docId: string) => {
|
||||
selectedFile.value = null
|
||||
selectedDocument.value = null
|
||||
previewUrl.value = ''
|
||||
previewDownloadUrl.value = ''
|
||||
previewHtml.value = ''
|
||||
previewContentType.value = ''
|
||||
previewFileType.value = ''
|
||||
previewCsvData.value = []
|
||||
previewCsvHeaders.value = []
|
||||
previewDownloadUrl.value = ''
|
||||
}
|
||||
// 刷新文档列表
|
||||
await changeFileFilter(fileFilter.value)
|
||||
@@ -873,7 +1026,7 @@ const deleteDocument = async (docId: string) => {
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
style="display: none"
|
||||
accept=".pdf,.doc,.docx,.txt,.md"
|
||||
accept=".pdf,.doc,.docx,.docx,.txt,.md,.csv,.xlsx,.xls,.pptx,.ppt"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<button class="btn-primary" @click="triggerFileUpload">
|
||||
@@ -987,7 +1140,40 @@ const deleteDocument = async (docId: string) => {
|
||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||
<span>Loading preview...</span>
|
||||
</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
|
||||
v-else-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
@@ -996,9 +1182,9 @@ const deleteDocument = async (docId: string) => {
|
||||
<!-- 无预览但有下载链接时显示下载按钮 -->
|
||||
<div v-else-if="previewDownloadUrl" class="preview-no-file">
|
||||
<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">
|
||||
<i class="fa-solid fa-download"></i> Download PDF
|
||||
<i class="fa-solid fa-download"></i> Download File
|
||||
</a>
|
||||
</div>
|
||||
<!-- 无预览也无下载链接时显示提示 -->
|
||||
|
||||
@@ -241,6 +241,7 @@ const showChangePassword = () => {
|
||||
description="Configure your model settings"
|
||||
icon="fa-solid fa-brain"
|
||||
icon-class="bg-gradient-to-br from-primary-orange to-red-500"
|
||||
class="add-model-dialog"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
|
||||
@@ -943,6 +943,166 @@
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@@ -256,10 +256,14 @@
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select) {
|
||||
.add-model-dialog .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-model-dialog .el-select .el-input {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select .el-input__wrapper) {
|
||||
background-color: #171922;
|
||||
border: 1px solid #4b5563;
|
||||
@@ -276,6 +280,7 @@
|
||||
.add-model-dialog :deep(.el-select-dropdown) {
|
||||
background-color: #1a1a24;
|
||||
border: 1px solid #2a2a3a;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.add-model-dialog :deep(.el-select-dropdown__item) {
|
||||
|
||||
Reference in New Issue
Block a user