提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
[package]
name = "oss-client"
version = "0.1.0"
edition = "2024"
description = "简洁的阿里云OSS操作库"
license = "MIT"
[dependencies]
aliyun-oss-rust-sdk = { workspace = true }
chrono = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
async-trait = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
# 国际化支持
rust-i18n = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }

View File

@@ -0,0 +1,86 @@
# OSS Client
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
---
# OSS Client
A lightweight and easy-to-use Alibaba Cloud OSS (Object Storage Service) client library, providing basic file operations and signed URL functionality.
## Features
### OssClientTrait (Unified Interface)
- **Unified Interface**: Defines basic operation interfaces for OSS clients
- **Polymorphic Support**: Supports unified usage of private bucket and public bucket clients
- **Code Reuse**: Reduces duplicate code, improves maintainability
### OssClient (Private Bucket Client)
- **File Operations**: Upload, download, delete files
- **Signed URLs**: Generate upload/download signed URLs with expiration time
- **File Management**: Check file existence, generate unique object keys
- **Connection Test**: Test OSS connection status
### PublicOssClient (Public Bucket Client)
- **Public Access**: Generate unsigned public download/access URLs
- **Batch Operations**: Batch generate public URLs
- **File Operations**: Upload, download, delete files (using public bucket)
- **Signed URLs**: Generate upload signed URLs (using public bucket)
- **File Management**: Check file existence, generate unique object keys
- **Connection Test**: Test public bucket connection status
- **Metadata Retrieval**: Get file metadata (via HTTP HEAD requests)
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
oss-client = { path = "../oss-client" } # If in the same workspace
# or
oss-client = "0.1.0" # If published to crates.io
```
## Quick Start
### Environment Variable Configuration
```bash
export OSS_ACCESS_KEY_ID="your_access_key_id"
export OSS_ACCESS_KEY_SECRET="your_access_key_secret"
```
### Basic Usage
```rust
use oss_client::{OssClient, OssConfig};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client from environment variables (recommended)
let client = oss_client::create_client_from_env()?;
// Upload file
let upload_url = client.upload_file("local/document.pdf", "uploads/document.pdf")?;
println!("File uploaded successfully: {}", upload_url);
// Check if file exists
let exists = client.file_exists("uploads/document.pdf")?;
println!("File exists: {}", exists);
// Download file
client.download_file("uploads/document.pdf", "downloaded/document.pdf")?;
println!("File downloaded successfully");
Ok(())
}
```
## License
MIT OR Apache-2.0
## Contributing
Issues and Pull Requests are welcome!

View File

@@ -0,0 +1,86 @@
# OSS Client
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
---
# OSS Client
简洁易用的阿里云 OSS (Object Storage Service) 操作库,提供基本的文件操作和签名 URL 功能。
## 功能特性
### OssClientTrait (统一接口)
- **统一接口**: 定义了OSS客户端的基本操作接口
- **多态支持**: 支持私有bucket和公有bucket客户端的统一使用
- **代码复用**: 减少重复代码,提高维护性
### OssClient (私有Bucket客户端)
- **文件操作**: 上传、下载、删除文件
- **签名URL**: 生成带过期时间的上传/下载签名URL
- **文件管理**: 检查文件存在性、生成唯一对象键
- **连接测试**: 测试OSS连接状态
### PublicOssClient (公有Bucket客户端)
- **公开访问**: 生成无需签名的公开下载/访问URL
- **批量操作**: 批量生成公开URL
- **文件操作**: 上传、下载、删除文件使用公有bucket
- **签名URL**: 生成上传签名URL使用公有bucket
- **文件管理**: 检查文件存在性、生成唯一对象键
- **连接测试**: 测试公有bucket连接状态
- **元信息获取**: 获取文件元信息通过HTTP HEAD请求
## 安装
在你的 `Cargo.toml` 中添加依赖:
```toml
[dependencies]
oss-client = { path = "../oss-client" } # 如果在同一个 workspace 中
# 或者
oss-client = "0.1.0" # 如果发布到 crates.io
```
## 快速开始
### 环境变量配置
```bash
export OSS_ACCESS_KEY_ID="your_access_key_id"
export OSS_ACCESS_KEY_SECRET="your_access_key_secret"
```
### 基本使用
```rust
use oss_client::{OssClient, OssConfig};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 从环境变量创建客户端(推荐方式)
let client = oss_client::create_client_from_env()?;
// 上传文件
let upload_url = client.upload_file("local/document.pdf", "uploads/document.pdf")?;
println!("文件上传成功: {}", upload_url);
// 检查文件是否存在
let exists = client.file_exists("uploads/document.pdf")?;
println!("文件存在: {}", exists);
// 下载文件
client.download_file("uploads/document.pdf", "downloaded/document.pdf")?;
println!("文件下载成功");
Ok(())
}
```
## 许可证
MIT OR Apache-2.0
## 贡献
欢迎提交 Issue 和 Pull Request

View File

@@ -0,0 +1,5 @@
fn main() {
println!("cargo:rerun-if-changed=locales/en.yml");
println!("cargo:rerun-if-changed=locales/zh-CN.yml");
println!("cargo:rerun-if-changed=locales/zh-TW.yml");
}

View File

@@ -0,0 +1,105 @@
//! 基本使用示例
use oss_client::{OssConfig, PrivateOssClient, format_file_size, generate_random_filename};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Basic usage examples of OSS client ===\\n");
// 显示库信息
println!("Library name: {}", oss_client::name());
println!("Version: {}", oss_client::version());
println!("Description: {}\\n", oss_client::description());
// 方式1从环境变量创建客户端推荐
println!("1. Try creating a client from environment variables...");
let oss_config = oss_client::OssConfig::new(
oss_client::defaults::ENDPOINT.to_string(),
oss_client::defaults::PUBLIC_BUCKET.to_string(),
"demo_access_key_id".to_string(),
"demo_access_key_secret".to_string(),
oss_client::defaults::REGION.to_string(),
oss_client::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PrivateOssClient::new(oss_config);
match client {
Ok(client) => {
println!("✓ Successful creation of client from environment variables");
println!("Base URL: {}", client.get_base_url());
// 演示工具函数
demonstrate_utilities(&client)?;
// 注意实际的文件操作需要有效的OSS凭证
println!(
"\\nNote: To perform actual file operations, set the following environment variables:"
);
println!(" export OSS_ACCESS_KEY_ID=\"your_access_key_id\"");
println!(" export OSS_ACCESS_KEY_SECRET=\"your_access_key_secret\"");
}
Err(e) => {
println!("✗ Failed to create client from environment variable: {e}");
println!(
"Please set the OSS_ACCESS_KEY_ID and OSS_ACCESS_KEY_SECRET environment variables"
);
// 方式2手动创建配置演示用
println!("\\n2. Create a client using the sample configuration...");
let config = OssConfig::new(
oss_client::defaults::ENDPOINT.to_string(),
oss_client::defaults::PUBLIC_BUCKET.to_string(),
"demo_access_key_id".to_string(),
"demo_access_key_secret".to_string(),
oss_client::defaults::REGION.to_string(),
oss_client::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PrivateOssClient::new(config)?;
println!("✓ Successful creation of client using sample configuration");
println!("Base URL: {}", client.get_base_url());
// 演示工具函数
demonstrate_utilities(&client)?;
}
}
Ok(())
}
fn demonstrate_utilities(client: &PrivateOssClient) -> Result<(), Box<dyn std::error::Error>> {
println!("\\n=== Tool function demonstration ===");
// 文件名处理
let random_filename = generate_random_filename(Some("txt"));
println!("Random file name: {random_filename}");
// 文件大小格式化
let sizes = vec![1024, 1024 * 1024, 1024 * 1024 * 1024];
for size in sizes {
println!("File size {} bytes = {}", size, format_file_size(size));
}
// MIME类型检测
let files = vec!["test.jpg", "document.pdf", "data.xlsx", "video.mp4"];
for file in files {
println!(
"MIME type of file {}: {}",
file,
oss_client::detect_mime_type(file)
);
}
// 签名URL生成演示不会实际调用OSS
println!("\\n=== Signed URL function demonstration ===");
println!("Note: The following features require valid OSS credentials to work properly");
// 演示API调用但不实际执行因为可能没有有效凭证
println!("Available API methods:");
println!(" - client.upload_file(local_path, object_key)");
println!(" - client.upload_content(content, object_key, content_type)");
println!(" - client.download_file(object_key, local_path)");
println!(" - client.delete_file(object_key)");
println!(" - client.file_exists(object_key)");
println!(" - client.generate_upload_url(object_key, expires_in, content_type)");
println!(" - client.generate_download_url(object_key, expires_in)");
Ok(())
}

View File

@@ -0,0 +1,69 @@
//! OSS域名替换示例
//!
//! 展示如何使用域名替换功能来解决跨域问题
use oss_client::{replace_oss_domain, replace_oss_domains_batch};
fn main() {
println!("=== OSS domain name replacement example ===\\n");
// 示例1: 单个URL替换
let original_url = "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image.jpg";
let replaced_url = replace_oss_domain(original_url);
println!("Original URL: {original_url}");
println!("After replacement: {replaced_url}\\n");
// 示例2: 带路径的URL替换
let original_url_with_path =
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/folder/subfolder/image.png";
let replaced_url_with_path = replace_oss_domain(original_url_with_path);
println!("Original URL with path: {original_url_with_path}");
println!("After replacement: {replaced_url_with_path}\\n");
// 示例3: 带查询参数的URL替换
let original_url_with_query =
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image.jpg?version=1.0&size=large";
let replaced_url_with_query = replace_oss_domain(original_url_with_query);
println!("Original URL with query parameters: {original_url_with_query}");
println!("After replacement: {replaced_url_with_query}\\n");
// 示例4: 不匹配的域名保持不变
let other_url = "https://other-domain.com/image.jpg";
let unchanged_url = replace_oss_domain(other_url);
println!("Other domain name URL: {other_url}");
println!("Remain unchanged: {unchanged_url}\\n");
// 示例5: 批量替换
let urls = vec![
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image1.jpg".to_string(),
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image2.jpg".to_string(),
"https://other-domain.com/image3.jpg".to_string(),
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/folder/image4.png".to_string(),
];
println!("URLs before batch replacement:");
for (i, url) in urls.iter().enumerate() {
println!(" {}: {}", i + 1, url);
}
let replaced_urls = replace_oss_domains_batch(&urls);
println!("\\nURLs after batch replacement:");
for (i, url) in replaced_urls.iter().enumerate() {
println!(" {}: {}", i + 1, url);
}
println!("\\n=== Usage scenario description ===");
println!(
"1. Solve the cross-domain problem: Replace the Alibaba Cloud OSS domain name with a custom domain name"
);
println!("2. Image preview: Normally display images stored in OSS on the web page");
println!(
"3. Batch processing: Process the domain name replacement of multiple URLs at one time"
);
println!("4. Maintain compatibility: Unmatched domain names remain unchanged");
}

View File

@@ -0,0 +1,143 @@
//! 签名URL使用示例
use oss_client::{OssClientTrait, PrivateOssClient};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Example of using OSS signed URL ===\\n");
// 从环境变量创建客户端
let oss_config = oss_client::OssConfig::new(
oss_client::defaults::ENDPOINT.to_string(),
oss_client::defaults::PUBLIC_BUCKET.to_string(),
"demo_access_key_id".to_string(),
"demo_access_key_secret".to_string(),
oss_client::defaults::REGION.to_string(),
oss_client::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PrivateOssClient::new(oss_config);
match client {
Ok(client) => {
println!("✓ OSS client created successfully");
println!("Base URL: {}\\n", client.get_base_url());
// 演示签名URL生成
demonstrate_signed_urls(&client)?;
}
Err(e) => {
println!("✗ Failed to create OSS client: {e}");
println!("Please set the following environment variables:");
println!(" export OSS_ACCESS_KEY_ID=\"your_access_key_id\"");
println!(" export OSS_ACCESS_KEY_SECRET=\"your_access_key_secret\"");
println!("\\nContinue with demo configuration...\\n");
// 使用演示配置(显式提供全部参数)
let config = oss_client::OssConfig::new(
oss_client::defaults::ENDPOINT.to_string(),
oss_client::defaults::PUBLIC_BUCKET.to_string(),
"demo_access_key_id".to_string(),
"demo_access_key_secret".to_string(),
oss_client::defaults::REGION.to_string(),
oss_client::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PrivateOssClient::new(config)?;
demonstrate_signed_urls(&client)?;
}
}
Ok(())
}
fn demonstrate_signed_urls(client: &dyn OssClientTrait) -> Result<(), Box<dyn std::error::Error>> {
println!("=== Signed URL generation demo ===");
// 生成上传签名URL
println!("1. Generate upload signature URL");
let object_key = "uploads/document.pdf";
let expires_in = Duration::from_secs(4 * 3600); // 4小时有效
println!("Object key: {object_key}");
println!("Validity period: {} hours", expires_in.as_secs() / 3600);
println!("Content type: application/pdf");
match client.generate_upload_url(object_key, expires_in, Some("application/pdf")) {
Ok(upload_url) => {
println!("✓ Upload URL generated successfully:");
println!(" {upload_url}");
println!("How to use:");
println!(" curl -X PUT \"{upload_url}\" \\");
println!(" -H \"Content-Type: application/pdf\" \\");
println!(" --data-binary @local_file.pdf");
}
Err(e) => {
println!("✗ Failed to generate upload URL: {e}");
println!("NOTE: Valid OSS credentials are required to generate signed URLs");
}
}
println!();
// 生成下载签名URL4小时有效
println!("2. Generate download signature URL (valid for 4 hours)");
match client.generate_download_url(object_key, Some(expires_in)) {
Ok(download_url) => {
println!("✓ Download URL generated successfully:");
println!(" {download_url}");
println!("How to use:");
println!(" curl \"{download_url}\" -o downloaded_file.pdf");
}
Err(e) => {
println!("✗ Download URL generation failed: {e}");
}
}
println!();
// 生成永久下载URL实际上是4小时有效因为我们的实现中没有真正的永久URL
println!("3. Generate download signature URL (valid for 1 hour by default)");
match client.generate_download_url(object_key, None) {
Ok(download_url) => {
println!("✓ Download URL generated successfully:");
println!(" {download_url}");
}
Err(e) => {
println!("✗ Download URL generation failed: {e}");
}
}
println!();
// 演示不同文件类型的上传URL
println!("4. Upload URLs for different file types");
let file_examples = vec![
("uploads/image.jpg", "image/jpeg"),
(
"uploads/document.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
),
("uploads/data.json", "application/json"),
("uploads/video.mp4", "video/mp4"),
];
for (key, content_type) in file_examples {
println!("File: {key} ({content_type})");
match client.generate_upload_url(key, Duration::from_secs(3600), Some(content_type)) {
Ok(url) => println!(" ✓ URL: {url}"),
Err(e) => println!("✗ Failure: {e}"),
}
}
println!("\\n=== Usage Tips ===");
println!(
"1. Uploading a signed URL allows the client to directly upload files to OSS without exposing the access key."
);
println!("2. Downloading signed URLs allows temporary access to private files");
println!("3. The signed URL is time-sensitive and needs to be regenerated after expiration.");
println!(
"4. In a production environment, it is recommended to set an appropriate expiration time based on actual needs."
);
Ok(())
}

View File

@@ -0,0 +1,122 @@
//! 展示如何使用统一的 OssClientTrait 接口
//!
//! 这个示例展示了如何通过 trait 接口统一使用私有bucket和公有bucket客户端
use oss_client::{OssClientTrait, OssConfig, PrivateOssClient, PublicOssClient};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Example of using the OSS client unified interface ===\\n");
// 创建配置
let config = OssConfig::new(
oss_client::defaults::ENDPOINT.to_string(),
oss_client::defaults::PUBLIC_BUCKET.to_string(),
"your_access_key_id".to_string(),
"your_access_key_secret".to_string(),
oss_client::defaults::REGION.to_string(),
oss_client::defaults::UPLOAD_DIRECTORY.to_string(),
);
// 创建两个客户端
let private_client = PrivateOssClient::new(config.clone())?;
let public_client = PublicOssClient::new(config.clone())?;
println!("1. Use private bucket client through unified interface:");
demonstrate_client_usage(&private_client, "私有bucket").await?;
println!("\\n2. Use the public bucket client through the unified interface:");
demonstrate_client_usage(&public_client, "公有bucket").await?;
println!("\\n3. Examples of polymorphic usage:");
demonstrate_polymorphic_usage(&private_client, &public_client).await?;
Ok(())
}
/// 演示客户端的基本使用
async fn demonstrate_client_usage(
client: &dyn OssClientTrait,
client_type: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Client type: {client_type}");
println!("Configuration information: {:?}", client.get_config());
println!("Base URL: {}", client.get_base_url());
// 生成上传签名URL
let expires_in = Duration::from_secs(3600); // 1小时
match client.generate_upload_url("documents/test.pdf", expires_in, Some("application/pdf")) {
Ok(url) => println!("Upload signature URL: {url}"),
Err(e) => println!("Failed to generate upload signature URL: {e}"),
}
// 生成下载签名URL
match client.generate_download_url("documents/test.pdf", Some(expires_in)) {
Ok(url) => println!("Download signature URL: {url}"),
Err(e) => println!("Failed to generate download signature URL: {e}"),
}
// 生成唯一对象键
let object_key = client.generate_object_key("documents", Some("manual.pdf"));
println!("Generated object key: {object_key}");
Ok(())
}
/// 演示多态使用
async fn demonstrate_polymorphic_usage(
private_client: &dyn OssClientTrait,
public_client: &dyn OssClientTrait,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Polymorphic use - can handle different types of clients uniformly");
// 创建一个客户端列表
let clients: Vec<&dyn OssClientTrait> = vec![private_client, public_client];
for (i, client) in clients.iter().enumerate() {
println!("Client {}: {}", i + 1, client.get_base_url());
// 所有客户端都实现了相同的接口
let object_key = client.generate_object_key("test", Some("file.txt"));
println!("Generated object key: {object_key}");
// 测试连接需要实际的OSS权限
println!("Test connection...");
match client.test_connection().await {
Ok(_) => println!("✅Connected successfully"),
Err(e) => println!("❌ Connection failed: {e}"),
}
}
Ok(())
}
/// 通用的文件操作函数,接受任何实现了 OssClientTrait 的客户端
async fn upload_file_generic(
client: &dyn OssClientTrait,
local_path: &str,
object_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
println!("Use the common interface to upload files: {local_path} -> {object_key}");
// 通过 trait 接口调用上传方法
let url = client.upload_file(local_path, object_key).await?;
println!("Upload successful, file URL: {url}");
Ok(url)
}
/// 通用的文件删除函数
async fn delete_file_generic(
client: &dyn OssClientTrait,
object_key: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Delete files using common interface: {object_key}");
// 通过 trait 接口调用删除方法
client.delete_file(object_key).await?;
println!("Delete successfully");
Ok(())
}

View File

@@ -0,0 +1,468 @@
# ===========================================
# Error Messages - mcp-proxy
# ===========================================
errors.mcp_proxy.service_not_found: "Service %{service} not found"
errors.mcp_proxy.service_restart_cooldown: "Service %{service} is in restart cooldown, please try again later"
errors.mcp_proxy.service_startup_in_progress: "Service %{service} is starting, please wait"
errors.mcp_proxy.service_startup_failed: "Service startup failed: %{mcp_id}: %{reason}"
errors.mcp_proxy.backend_connection: "Backend connection error: %{detail}"
errors.mcp_proxy.config_parse: "Configuration parse error: %{detail}"
errors.mcp_proxy.mcp_server_error: "MCP server error: %{detail}"
errors.mcp_proxy.json_serialization: "JSON serialization error: %{detail}"
errors.mcp_proxy.io_error: "IO error: %{detail}"
errors.mcp_proxy.route_not_found: "Route not found: %{path}"
errors.mcp_proxy.invalid_parameter: "Invalid request parameter: %{detail}"
# ===========================================
# Error Messages - document-parser
# ===========================================
errors.document_parser.config: "Configuration error: %{detail}"
errors.document_parser.file: "File operation error: %{detail}"
errors.document_parser.unsupported_format: "Unsupported file format: %{format}"
errors.document_parser.parse: "Parse error: %{detail}"
errors.document_parser.mineru: "MinerU error: %{detail}"
errors.document_parser.markitdown: "MarkItDown error: %{detail}"
errors.document_parser.oss: "OSS operation error: %{detail}"
errors.document_parser.database: "Database error: %{detail}"
errors.document_parser.network: "Network error: %{detail}"
errors.document_parser.task: "Task error: %{detail}"
errors.document_parser.internal: "Internal error: %{detail}"
errors.document_parser.timeout: "Operation timeout: %{detail}"
errors.document_parser.validation: "Validation error: %{detail}"
errors.document_parser.environment: "Environment error: %{detail}"
errors.document_parser.virtual_environment_path: "Virtual environment path error: %{detail}"
errors.document_parser.permission: "Permission error: %{detail}"
errors.document_parser.path: "Path error: %{detail}"
errors.document_parser.queue: "Queue error: %{detail}"
errors.document_parser.processing: "Processing error: %{detail}"
# Error Suggestions - document-parser
errors.document_parser.suggestions.config: "Check configuration file and environment variables"
errors.document_parser.suggestions.file: "Check file path and permissions"
errors.document_parser.suggestions.unsupported_format: "Check if file format is supported"
errors.document_parser.suggestions.parse: "Check if file content is complete"
errors.document_parser.suggestions.mineru: "Check MinerU environment configuration"
errors.document_parser.suggestions.markitdown: "Check MarkItDown environment configuration"
errors.document_parser.suggestions.oss: "Check OSS configuration and network connection"
errors.document_parser.suggestions.database: "Check database connection and permissions"
errors.document_parser.suggestions.network: "Check network connection and firewall settings"
errors.document_parser.suggestions.task: "Check task parameters and status"
errors.document_parser.suggestions.internal: "Contact technical support"
errors.document_parser.suggestions.timeout: "Check network latency or increase timeout"
errors.document_parser.suggestions.validation: "Check input parameter format"
errors.document_parser.suggestions.environment: "Check system environment and dependency installation"
errors.document_parser.suggestions.queue: "Check queue service status and configuration"
errors.document_parser.suggestions.processing: "Check processing flow and data format"
errors.document_parser.suggestions.virtual_environment_path: "Check virtual environment path and directory permissions"
errors.document_parser.suggestions.permission: "Check file and directory permission settings"
errors.document_parser.suggestions.path: "Check if path exists and is accessible"
# ===========================================
# Error Messages - oss-client
# ===========================================
errors.oss.config: "Configuration error: %{detail}"
errors.oss.network: "Network error: %{detail}"
errors.oss.file_not_found: "File not found: %{path}"
errors.oss.permission: "Permission denied: %{detail}"
errors.oss.io: "IO error: %{detail}"
errors.oss.sdk: "OSS SDK error: %{detail}"
errors.oss.file_size_exceeded: "File size exceeded: %{detail}"
errors.oss.unsupported_file_type: "Unsupported file type: %{detail}"
errors.oss.timeout: "Operation timeout: %{detail}"
errors.oss.invalid_parameter: "Invalid parameter: %{detail}"
# ===========================================
# Error Messages - voice-cli
# ===========================================
errors.voice.config: "Configuration error: %{detail}"
errors.voice.audio_processing: "Audio processing error: %{detail}"
errors.voice.transcription: "Transcription error: %{detail}"
errors.voice.model: "Model error: %{detail}"
errors.voice.file_io: "File I/O error: %{detail}"
errors.voice.http: "HTTP request error: %{detail}"
errors.voice.serialization: "Serialization error: %{detail}"
errors.voice.json: "JSON error: %{detail}"
errors.voice.config_rs: "Config-rs error: %{detail}"
errors.voice.daemon: "Daemon error: %{detail}"
errors.voice.unsupported_format: "Audio format not supported: %{detail}"
errors.voice.file_too_large: "File too large: %{size} bytes (max: %{max} bytes)"
errors.voice.model_not_found: "Model not found: %{model}"
errors.voice.invalid_model_name: "Invalid model name: %{model}"
errors.voice.worker_pool: "Worker pool error: %{detail}"
errors.voice.transcription_timeout: "Transcription timeout after %{seconds} seconds"
errors.voice.transcription_failed: "Transcription failed: %{detail}"
errors.voice.audio_conversion_failed: "Audio conversion failed: %{detail}"
errors.voice.audio_probe_error: "Audio probe error: %{detail}"
errors.voice.temp_file_error: "Temporary file error: %{detail}"
errors.voice.multipart_error: "Multipart form error: %{detail}"
errors.voice.missing_field: "Missing required field: %{field}"
errors.voice.network: "Network error: %{detail}"
errors.voice.storage: "Storage error: %{detail}"
errors.voice.task_management_disabled: "Task management is disabled"
errors.voice.not_found: "Resource not found: %{resource}"
errors.voice.initialization: "Initialization error: %{detail}"
errors.voice.tts: "TTS error: %{detail}"
errors.voice.invalid_input: "Invalid input: %{detail}"
errors.voice.io: "IO error: %{detail}"
# ===========================================
# CLI Messages - mcp-proxy startup
# ===========================================
cli.mirror.not_configured: "No mirror configured (npm/PyPI), using default sources"
cli.mirror.npm: "npm mirror: %{url}"
cli.mirror.pypi: "PyPI mirror: %{url}"
cli.startup.service_starting: "MCP-Proxy starting..."
cli.startup.version: "Version: %{version}"
cli.startup.config_loaded: "Configuration loaded"
cli.startup.port: "Port: %{port}"
cli.startup.log_dir: "Log directory: %{path}"
cli.startup.log_level: "Log level: %{level}"
cli.startup.log_retain_days: "Log retention days: %{days}"
cli.startup.success: "✅ Service started successfully, listening on: %{addr}"
cli.startup.health_endpoint: "✅ Health check endpoint: http://%{addr}/health"
cli.startup.mcp_list: "✅ MCP service list: http://%{addr}/mcp"
cli.startup.schedule_task_started: "✅ MCP service status check scheduled task started"
cli.startup.log_rotation_configured: "✅ Log rotation configured (keeping last %{count} log files)"
cli.startup.system_info: "System information:"
cli.startup.os: "Operating system: %{os}"
cli.startup.arch: "Architecture: %{arch}"
cli.startup.work_dir: "Working directory: %{path}"
cli.startup.env_override: "Environment variable overrides:"
cli.startup.env_port_override: "MCP_PROXY_PORT: %{port}"
cli.startup.env_log_dir_override: "MCP_PROXY_LOG_DIR: %{dir}"
cli.startup.env_log_level_override: "MCP_PROXY_LOG_LEVEL: %{level}"
cli.startup.trying_bind: "Attempting to bind to address: %{addr}"
cli.startup.bind_success: "Successfully bound to address: %{addr}"
cli.startup.bind_failed: "Failed to bind address %{addr}: %{error}"
cli.startup.init_state: "Initializing application state..."
cli.startup.state_done: "Application state initialized"
cli.startup.init_router: "Initializing router..."
cli.startup.router_done: "Router initialized"
cli.startup.warming_up: "\U0001F504 Warming up uv/deno environment dependencies..."
cli.startup.warmup_success: "✅ uv/deno environment warmup complete"
cli.startup.warmup_failed: "❌ uv/deno environment warmup failed: %{error}"
cli.startup.http_server_starting: "\U0001F680 HTTP server starting, waiting for connections..."
cli.startup.proxy_mode: "Command: proxy (HTTP server mode)"
cli.startup.config_info: "Configuration info:"
cli.startup.listen_port: "Listen port: %{port}"
cli.startup.log_retention: "Log retention: %{days} days"
# CLI Messages - mcp-proxy shutdown
cli.shutdown.signal_received: "⚠️ Server received shutdown signal, cleaning up resources..."
cli.shutdown.cleanup_success: "✅ Resource cleanup successful"
cli.shutdown.cleanup_failed: "❌ Error during resource cleanup: %{error}"
cli.shutdown.complete: "✅ Resource cleanup complete, service fully shut down"
cli.shutdown.service_error: "❌ Service runtime error: %{error}"
# CLI Messages - panic handling
cli.panic.handler_started: "Program panic occurred, executing cleanup..."
cli.panic.reason: "Panic reason: %{reason}"
cli.panic.reason_unknown: "Panic reason: unknown"
cli.panic.location: "Panic location: %{file}:%{line}"
cli.panic.stack_trace: "Stack trace:"
# ===========================================
# CLI Messages - convert mode
# ===========================================
cli.convert.starting: "Starting URL mode processing"
cli.convert.target_url: "Target URL: %{url}"
cli.convert.protocol_specified: "Using specified protocol: %{protocol}"
cli.convert.protocol_config: "Using configured protocol: %{protocol}"
cli.convert.detecting_protocol: "Starting protocol auto-detection..."
cli.convert.detect_failed: "Protocol detection failed: %{error}"
cli.convert.using_protocol: "Using %{protocol} protocol mode"
cli.convert.stdio_url_not_supported: "Stdio protocol does not support URL conversion"
cli.convert.tool_whitelist: "Tool whitelist: %{tools}"
cli.convert.tool_blacklist: "Tool blacklist: %{tools}"
cli.convert.config_parsed: "Configuration parsed successfully"
cli.convert.service_name: "MCP service name: %{name}"
cli.convert.service_name_not_specified: "MCP service name: not specified (using direct URL)"
cli.convert.cli_starting: "MCP-Proxy CLI starting"
cli.convert.command: "Command: convert (stdio bridge mode)"
cli.convert.version: "Version: %{version}"
cli.convert.diagnostic_mode: "Diagnostic mode: %{enabled}"
cli.convert.mode_direct_url: "Mode: direct URL mode"
cli.convert.mode_remote_service: "Mode: remote service configuration mode"
cli.convert.service_url: "Service URL: %{url}"
cli.convert.config_protocol: "Configured protocol: %{protocol}"
cli.convert.ping_config: "Ping interval: %{interval}s, Ping timeout: %{timeout}s"
cli.convert.connecting_backend: "Connecting to backend service (timeout: %{timeout}s)..."
cli.convert.detect_complete: "Protocol detection complete: %{protocol} (duration: %{duration})"
# ===========================================
# CLI Messages - SSE mode
# ===========================================
cli.sse.mode_starting: "SSE mode starting"
cli.sse.connect_timeout: "Backend connection timeout (%{seconds}s)"
cli.sse.connect_failed: "Backend connection failed: %{error}"
cli.sse.connect_success: "Backend connected (duration: %{duration})"
cli.sse.stdio_starting: "Starting stdio server..."
cli.sse.stdio_started: "Stdio server started"
cli.sse.waiting_events: "Waiting for stdio server events..."
cli.sse.stdio_exit_eof: "Stdio server exited - reason: MCP client disconnected (stdin EOF)"
cli.sse.watchdog_exit: "Watchdog task exited"
cli.sse.normal_exit: "mcp-proxy convert (SSE mode) exited normally"
cli.sse.watchdog_starting: "SSE Watchdog starting"
cli.sse.max_retries: "Max retries: %{count} (0=unlimited)"
cli.sse.monitoring_connection: "Monitoring initial connection..."
cli.sse.initial_disconnect: "Initial connection disconnected: %{reason}"
cli.sse.connection_alive: "Initial connection alive for: %{seconds}s"
cli.sse.reconnect_attempt: "Reconnect attempt #%{attempt}/%{max}"
cli.sse.backoff_time: "Backoff time: %{seconds}s"
cli.sse.reconnect_success: "Reconnect successful (duration: %{duration})"
cli.sse.monitoring_reconnect: "Monitoring reconnected connection..."
cli.sse.reconnect_disconnect: "Disconnected after reconnect: %{reason}"
cli.sse.reconnect_alive: "Reconnected connection alive for: %{seconds}s"
cli.sse.max_retries_reached: "Max retries reached (%{count}), stopping reconnect"
cli.sse.watchdog_exit_msg: "SSE Watchdog exited"
# ===========================================
# CLI Messages - Stream mode
# ===========================================
cli.stream.mode_starting: "Stream mode starting"
cli.stream.connect_timeout: "Backend connection timeout (%{seconds}s)"
cli.stream.connect_failed: "Backend connection failed: %{error}"
cli.stream.connect_success: "Backend connected (duration: %{duration})"
cli.stream.stdio_starting: "Starting stdio server..."
cli.stream.stdio_started: "Stdio server started"
cli.stream.waiting_events: "Waiting for stdio server events..."
cli.stream.stdio_exit_eof: "Stdio server exited - reason: MCP client disconnected (stdin EOF)"
cli.stream.watchdog_exit: "Watchdog task exited"
cli.stream.normal_exit: "mcp-proxy convert (Stream mode) exited normally"
# ===========================================
# CLI Messages - Command mode
# ===========================================
cli.command.local_mode: "Mode: local command mode"
cli.command.command: "Command: %{cmd} %{args}"
cli.command.ctrl_c: "Received Ctrl+C signal, shutting down..."
# ===========================================
# CLI Messages - monitoring
# ===========================================
cli.monitoring.health: "Monitoring %{protocol} connection health"
cli.monitoring.health_check_status: "\U0001F493 [%{protocol}][Health Check] Connection status: %{status} (connection check only, no list_tools), check #%{count}, alive: %{seconds}s"
cli.monitoring.health_check_normal: "\U0001F493 [%{protocol}][Health Check] Backend service healthy (list_tools verified), Ping #%{count}, alive: %{seconds}s"
# ===========================================
# Diagnostic Messages
# ===========================================
diagnostic.report_header: "========== Diagnostic Report =========="
diagnostic.connection_protocol: "Connection protocol: %{protocol}"
diagnostic.service_url: "Service URL: %{url}"
diagnostic.connection_duration: "Connection duration: %{seconds} seconds"
diagnostic.disconnect_reason: "Disconnect reason: %{reason}"
diagnostic.error_type: "Error type: %{error_type}"
diagnostic.possible_causes: "Possible causes:"
diagnostic.suggestions: "Suggestions:"
# Diagnostic - 30s timeout analysis
diagnostic.analysis.timeout_30s: "⚠️ Connection dropped at ~30 seconds, likely causes:"
diagnostic.analysis.timeout_30s_cause1: "Server-side 30-second timeout limit"
diagnostic.analysis.timeout_30s_cause2: "Load balancer (e.g., Nginx/ALB) default timeout"
diagnostic.analysis.timeout_30s_cause3: "Cloud provider gateway timeout limit"
# Diagnostic - Quick disconnect analysis
diagnostic.analysis.quick_disconnect: "⚠️ Connection dropped quickly (%{seconds}s), possible causes:"
diagnostic.analysis.quick_disconnect_cause1: "Authentication failed or invalid token"
diagnostic.analysis.quick_disconnect_cause2: "Server rejected connection"
diagnostic.analysis.quick_disconnect_cause3: "Network instability"
# Diagnostic - Long connection analysis
diagnostic.analysis.long_connection: "✅ Connection maintained for a long time (%{seconds}s), possible causes:"
diagnostic.analysis.long_connection_cause1: "Tool call execution took too long"
diagnostic.analysis.long_connection_cause2: "Network fluctuation caused disconnect"
# Diagnostic suggestions
diagnostic.suggestion.timeout_30s: "Contact service provider to increase timeout limit"
diagnostic.suggestion.timeout_client: "Use --request-timeout parameter to set client timeout"
diagnostic.suggestion.async_mode: "Consider using async processing mode (webhook callback)"
diagnostic.suggestion.ping_interval: "Try increasing ping interval: --ping-interval %{seconds}"
diagnostic.suggestion.ping_timeout: "Increase ping timeout: --ping-timeout %{seconds}"
diagnostic.suggestion.ping_disable: "Or disable ping: --ping-interval 0"
# ===========================================
# Error Classification
# ===========================================
error_classify.timeout_30s: "30s timeout (possibly server limit)"
error_classify.service_unavailable_503: "Service unavailable (503)"
error_classify.internal_server_error_500: "Internal server error (500)"
error_classify.bad_gateway_502: "Bad gateway (502)"
error_classify.gateway_timeout_504: "Gateway timeout (504)"
error_classify.unauthorized_401: "Unauthorized (401)"
error_classify.forbidden_403: "Forbidden (403)"
error_classify.not_found_404: "Not found (404)"
error_classify.request_timeout_408: "Request timeout (408)"
error_classify.timeout: "Timeout"
error_classify.connection_refused: "Connection refused"
error_classify.connection_reset: "Connection reset"
error_classify.connection_closed: "Connection closed"
error_classify.dns_failed: "DNS resolution failed"
error_classify.ssl_tls_error: "SSL/TLS error"
error_classify.network_error: "Network error"
error_classify.session_error: "Session error"
error_classify.unknown_error: "Unknown error"
# ===========================================
# CLI Messages - health command
# ===========================================
cli.health.checking: "\U0001F50D Health checking service: %{url}"
cli.health.using_protocol: "\U0001F50D Using specified protocol: %{protocol}"
cli.health.detecting_protocol: "\U0001F50D Detecting protocol..."
cli.health.detected_protocol: "\U0001F50D Detected %{protocol} protocol"
cli.health.healthy: "✅ Service healthy"
cli.health.unhealthy: "❌ Service unhealthy"
cli.health.stdio_not_supported: "health command does not support stdio protocol"
# ===========================================
# CLI Messages - check command
# ===========================================
cli.check.checking: "\U0001F50D Checking service: %{url}"
cli.check.healthy: "✅ Service healthy, detected %{protocol} protocol"
cli.check.failed: "❌ Service check failed: %{error}"
# ===========================================
# CLI Messages - document-parser
# ===========================================
doc_parser.startup.app_info: "=== %{name} v%{version} starting ==="
doc_parser.startup.config_summary: "Configuration summary: %{summary}"
doc_parser.startup.listening: "Service listening on: %{host}:%{port}"
doc_parser.startup.checking_python: "Starting Python environment check and initialization..."
doc_parser.startup.checking_venv: "Checking and activating virtual environment..."
doc_parser.startup.venv_activate_failed: "Virtual environment auto-activation failed: %{error}"
doc_parser.startup.venv_activate_manual: "Please manually activate virtual environment: source ./venv/bin/activate"
doc_parser.startup.venv_activated: "Virtual environment auto-activated"
doc_parser.startup.env_check_failed: "Environment check failed, will auto-install in background: %{error}"
doc_parser.startup.mineru_not_installed: "MinerU dependencies not installed, starting background auto-install..."
doc_parser.startup.markitdown_not_installed: "MarkItDown dependencies not installed, starting background auto-install..."
doc_parser.startup.install_complete: "Background Python environment installation complete"
doc_parser.startup.install_failed: "Background Python environment installation failed: %{error}"
doc_parser.startup.install_task_started: "Python dependency installation task started (background), service will start normally"
doc_parser.startup.mineru_ready: "MinerU dependencies installed, version: %{version}"
doc_parser.startup.markitdown_ready: "MarkItDown dependencies installed"
doc_parser.startup.python_ready: "Python environment check complete, all dependencies ready"
doc_parser.startup.state_failed: "Failed to create application state: %{error}"
doc_parser.startup.health_check_failed: "Application health check failed: %{error}"
doc_parser.startup.state_ready: "Application state initialized successfully"
doc_parser.startup.http_routes_ready: "HTTP routes initialized successfully"
doc_parser.startup.background_tasks_started: "Background tasks started"
doc_parser.startup.service_ready: "Service started successfully, waiting for connections..."
doc_parser.shutdown.service_stopped: "Service stopped"
doc_parser.shutdown.ctrl_c_received: "Received Ctrl+C signal, starting graceful shutdown..."
doc_parser.shutdown.terminate_received: "Received terminate signal, starting graceful shutdown..."
doc_parser.shutdown.closing: "Shutting down service..."
# uv-init command
doc_parser.uv_init.starting: "\U0001F680 Starting uv virtual environment and dependency initialization in current directory..."
doc_parser.uv_init.current_dir: "\U0001F4C1 Current working directory: %{path}"
doc_parser.uv_init.venv_path: "\U0001F4C1 Virtual environment will be created at: ./venv/"
doc_parser.uv_init.validating_dir: "\U0001F50D Validating current directory settings..."
doc_parser.uv_init.dir_valid: " ✅ Directory validation passed"
doc_parser.uv_init.warnings_found: " ⚠️ Found %{count} warnings"
doc_parser.uv_init.dir_invalid: " ❌ Directory validation failed, found %{count} issues"
doc_parser.uv_init.auto_fixing: " \U0001F527 Attempting to auto-fix %{count} issues..."
doc_parser.uv_init.fix_success: " ✅ %{message}"
doc_parser.uv_init.fix_failed: " ❌ Cleanup failed: %{error}"
doc_parser.uv_init.manual_fix_hint: " \U0001F4A1 Please manually resolve the following issues:"
doc_parser.uv_init.dir_validation_failed: "Directory validation failed, please resolve the above issues and try again"
doc_parser.uv_init.dir_validation_error: " ⚠️ Directory validation failed: %{error}"
doc_parser.uv_init.continue_install: " Continuing installation, but may encounter issues..."
# Environment check
doc_parser.check.checking_env: "\U0001F50D Checking current environment status..."
doc_parser.check.env_check_complete: " Environment check complete:"
doc_parser.check.python_available: "✅ Available"
doc_parser.check.python_unavailable: "❌ Unavailable"
doc_parser.check.uv_available: "✅ Available"
doc_parser.check.uv_unavailable: "❌ Unavailable"
doc_parser.check.venv_active: "✅ Active"
doc_parser.check.venv_inactive: "❌ Inactive"
doc_parser.check.mineru_available: "✅ Available"
doc_parser.check.mineru_unavailable: "❌ Unavailable"
doc_parser.check.markitdown_available: "✅ Available"
doc_parser.check.markitdown_unavailable: "❌ Unavailable"
doc_parser.check.env_check_failed: " ⚠️ Environment check failed: %{error}"
doc_parser.check.continue_install: " Continuing installation..."
# Installation process
doc_parser.install.all_ready: "✨ All dependencies are ready, no installation needed!"
doc_parser.install.plan: "\U0001F4CB Installation plan:"
doc_parser.install.install_uv: "Install uv tool"
doc_parser.install.create_venv: "Create virtual environment (./venv/)"
doc_parser.install.install_mineru: "Install MinerU dependencies"
doc_parser.install.install_markitdown: "Install MarkItDown dependencies"
doc_parser.install.starting: "⚙️ Starting Python environment and dependency setup..."
doc_parser.install.wait_hint: "This may take a few minutes, please wait..."
doc_parser.install.path_issues: "⚠️ Detected potential path issues:"
doc_parser.install.auto_fixing: "\U0001F527 Attempting to auto-fix issues..."
doc_parser.install.fixed: "✅ Fixed the following issues:"
doc_parser.install.cannot_auto_fix: "Cannot auto-fix, please manually resolve the above issues"
doc_parser.install.fix_failed: "❌ Auto-fix failed: %{error}"
doc_parser.install.manual_fix_hint: "\U0001F4A1 Manual fix suggestions:"
doc_parser.install.python_setup_complete: "✅ Python environment setup complete!"
doc_parser.install.python_setup_failed: "❌ Python environment setup failed: %{error}"
doc_parser.install.detailed_hints: "\U0001F4A1 Detailed troubleshooting suggestions:"
doc_parser.install.diagnostic_commands: "\U0001F50D Diagnostic commands:"
doc_parser.install.check_env: "Check environment status: document-parser check"
doc_parser.install.view_logs: "View detailed logs: check logs/ directory"
# Verification
doc_parser.verify.starting: "\U0001F50D Verifying installation results..."
doc_parser.verify.complete: "Verification complete:"
doc_parser.verify.version: "Version: %{version}"
doc_parser.verify.partial_issues: "⚠️ Some dependencies may have installation issues"
doc_parser.verify.need_fix: "\U0001F527 Issues to resolve:"
doc_parser.verify.suggestion: "Suggestion: %{suggestion}"
doc_parser.verify.init_incomplete: "Environment initialization incomplete"
doc_parser.verify.failed: "❌ Verification failed: %{error}"
# Success messages
doc_parser.success.init_complete: "\U0001F389 uv environment initialization complete!"
doc_parser.success.all_ready: "✨ All dependencies are ready, you can now start the server"
doc_parser.success.venv_activate: "\U0001F4CB Virtual environment activation commands:"
doc_parser.success.start_server: "\U0001F680 Start server:"
doc_parser.success.use_uv: "\U0001F527 Or use uv to run commands directly:"
doc_parser.success.more_help: "\U0001F4DA More help:"
doc_parser.success.tips: "\U0001F4A1 Tips:"
doc_parser.success.venv_location: "Virtual environment location: ./venv/"
doc_parser.success.python_path: "Python executable: ./venv/bin/python (Linux/macOS) or .\\venv\\Scripts\\python.exe (Windows)"
doc_parser.success.troubleshoot_hint: "If issues occur, run 'document-parser troubleshoot' for detailed guide"
# troubleshoot command
doc_parser.troubleshoot.title: "\U0001F527 Document Parser Troubleshooting Guide"
doc_parser.troubleshoot.env_overview: "\U0001F4CA Current Environment Overview:"
doc_parser.troubleshoot.work_dir: "Working directory: %{path}"
doc_parser.troubleshoot.venv_path: "Virtual environment: ./venv/"
doc_parser.troubleshoot.os: "Operating system: %{os}"
# Virtual environment issues
doc_parser.troubleshoot.venv_problems: "\U0001F3E0 1. Virtual Environment Issues"
doc_parser.troubleshoot.venv_create_failed: "❓ Issue: Virtual environment creation failed"
doc_parser.troubleshoot.diagnostic_steps: "\U0001F50D Diagnostic steps:"
doc_parser.troubleshoot.solutions: "\U0001F4A1 Solutions:"
doc_parser.troubleshoot.venv_activate_failed: "❓ Issue: Virtual environment activation failed"
# Dependency installation issues
doc_parser.troubleshoot.deps_problems: "\U0001F4E6 2. Dependency Installation Issues"
doc_parser.troubleshoot.uv_not_installed: "❓ Issue: UV tool not installed or unavailable"
doc_parser.troubleshoot.mineru_markitdown_failed: "❓ Issue: MinerU or MarkItDown installation failed"
# Network issues
doc_parser.troubleshoot.network_problems: "\U0001F310 3. Network and Download Issues"
doc_parser.troubleshoot.network_timeout: "❓ Issue: Network connection timeout or download failed"
# System environment issues
doc_parser.troubleshoot.system_problems: "⚙️ 4. System Environment Issues"
doc_parser.troubleshoot.python_incompatible: "❓ Issue: Python version incompatible"
doc_parser.troubleshoot.cuda_config: "❓ Issue: CUDA environment configuration (optional, for GPU acceleration)"
# Diagnostic commands
doc_parser.troubleshoot.diagnostic_commands: "\U0001F50D 5. Common Diagnostic Commands"
doc_parser.troubleshoot.env_check: "Environment check:"
doc_parser.troubleshoot.manual_verify: "Manual verification:"
doc_parser.troubleshoot.view_logs: "Log viewing:"
# Get help
doc_parser.troubleshoot.more_help: "\U0001F198 6. Get More Help"
doc_parser.troubleshoot.if_unsolved: "If the above methods don't resolve the issue:"
# Real-time diagnosis
doc_parser.troubleshoot.realtime_diagnosis: "\U0001F52C Real-time Environment Diagnosis"
doc_parser.troubleshoot.env_good: "✅ Environment status good, all dependencies ready"
doc_parser.troubleshoot.issues_found: "⚠️ Found the following issues:"
doc_parser.troubleshoot.env_check_failed: "❌ Environment check failed: %{error}"
doc_parser.troubleshoot.follow_guide: "Please follow the above guide for troubleshooting"
# Tip
doc_parser.troubleshoot.tip: "\U0001F4A1 Tip: Most issues can be resolved by re-running 'document-parser uv-init'"
# Background cleanup
doc_parser.background.cleanup_failed: "Failed to cleanup expired data: %{error}"
doc_parser.background.cleanup_complete: "Background cleanup task completed"
# Signal handling
doc_parser.signal.ctrl_c_failed: "Failed to listen for Ctrl+C signal"
doc_parser.signal.terminate_failed: "Failed to listen for terminate signal"
# ===========================================
# Common Messages
# ===========================================
common.yes: "Yes"
common.no: "No"
common.success: "Success"
common.failed: "Failed"
common.error: "Error"
common.warning: "Warning"
common.info: "Info"
common.loading: "Loading..."
common.please_wait: "Please wait..."
common.retry: "Retry"
common.cancel: "Cancel"
common.confirm: "Confirm"
common.back: "Back"
common.next: "Next"
common.previous: "Previous"
common.done: "Done"
common.skip: "Skip"

View File

@@ -0,0 +1,468 @@
# ===========================================
# 错误消息 - mcp-proxy
# ===========================================
errors.mcp_proxy.service_not_found: "服务 %{service} 未找到"
errors.mcp_proxy.service_restart_cooldown: "服务 %{service} 在重启冷却期内,请稍后再试"
errors.mcp_proxy.service_startup_in_progress: "服务 %{service} 正在启动中,请稍后再试"
errors.mcp_proxy.service_startup_failed: "服务启动失败: %{mcp_id}: %{reason}"
errors.mcp_proxy.backend_connection: "后端连接错误: %{detail}"
errors.mcp_proxy.config_parse: "配置解析错误: %{detail}"
errors.mcp_proxy.mcp_server_error: "MCP 服务器错误: %{detail}"
errors.mcp_proxy.json_serialization: "JSON 序列化错误: %{detail}"
errors.mcp_proxy.io_error: "IO 错误: %{detail}"
errors.mcp_proxy.route_not_found: "路由未找到: %{path}"
errors.mcp_proxy.invalid_parameter: "无效的请求参数: %{detail}"
# ===========================================
# 错误消息 - document-parser
# ===========================================
errors.document_parser.config: "配置错误: %{detail}"
errors.document_parser.file: "文件操作错误: %{detail}"
errors.document_parser.unsupported_format: "不支持的文件格式: %{format}"
errors.document_parser.parse: "解析错误: %{detail}"
errors.document_parser.mineru: "MinerU错误: %{detail}"
errors.document_parser.markitdown: "MarkItDown错误: %{detail}"
errors.document_parser.oss: "OSS操作错误: %{detail}"
errors.document_parser.database: "数据库错误: %{detail}"
errors.document_parser.network: "网络错误: %{detail}"
errors.document_parser.task: "任务错误: %{detail}"
errors.document_parser.internal: "内部错误: %{detail}"
errors.document_parser.timeout: "操作超时: %{detail}"
errors.document_parser.validation: "验证错误: %{detail}"
errors.document_parser.environment: "环境错误: %{detail}"
errors.document_parser.virtual_environment_path: "虚拟环境路径错误: %{detail}"
errors.document_parser.permission: "权限错误: %{detail}"
errors.document_parser.path: "路径错误: %{detail}"
errors.document_parser.queue: "队列错误: %{detail}"
errors.document_parser.processing: "处理错误: %{detail}"
# 错误建议 - document-parser
errors.document_parser.suggestions.config: "检查配置文件和环境变量"
errors.document_parser.suggestions.file: "检查文件路径和权限"
errors.document_parser.suggestions.unsupported_format: "检查文件格式是否支持"
errors.document_parser.suggestions.parse: "检查文件内容是否完整"
errors.document_parser.suggestions.mineru: "检查MinerU环境配置"
errors.document_parser.suggestions.markitdown: "检查MarkItDown环境配置"
errors.document_parser.suggestions.oss: "检查OSS配置和网络连接"
errors.document_parser.suggestions.database: "检查数据库连接和权限"
errors.document_parser.suggestions.network: "检查网络连接和防火墙设置"
errors.document_parser.suggestions.task: "检查任务参数和状态"
errors.document_parser.suggestions.internal: "联系技术支持"
errors.document_parser.suggestions.timeout: "检查网络延迟或增加超时时间"
errors.document_parser.suggestions.validation: "检查输入参数格式"
errors.document_parser.suggestions.environment: "检查系统环境和依赖安装"
errors.document_parser.suggestions.queue: "检查队列服务状态和配置"
errors.document_parser.suggestions.processing: "检查处理流程和数据格式"
errors.document_parser.suggestions.virtual_environment_path: "检查虚拟环境路径和目录权限"
errors.document_parser.suggestions.permission: "检查文件和目录权限设置"
errors.document_parser.suggestions.path: "检查路径是否存在和可访问"
# ===========================================
# 错误消息 - oss-client
# ===========================================
errors.oss.config: "配置错误: %{detail}"
errors.oss.network: "网络错误: %{detail}"
errors.oss.file_not_found: "文件不存在: %{path}"
errors.oss.permission: "权限不足: %{detail}"
errors.oss.io: "IO错误: %{detail}"
errors.oss.sdk: "OSS SDK错误: %{detail}"
errors.oss.file_size_exceeded: "文件大小超出限制: %{detail}"
errors.oss.unsupported_file_type: "不支持的文件类型: %{detail}"
errors.oss.timeout: "操作超时: %{detail}"
errors.oss.invalid_parameter: "无效的参数: %{detail}"
# ===========================================
# 错误消息 - voice-cli
# ===========================================
errors.voice.config: "配置错误: %{detail}"
errors.voice.audio_processing: "音频处理错误: %{detail}"
errors.voice.transcription: "转录错误: %{detail}"
errors.voice.model: "模型错误: %{detail}"
errors.voice.file_io: "文件I/O错误: %{detail}"
errors.voice.http: "HTTP请求错误: %{detail}"
errors.voice.serialization: "序列化错误: %{detail}"
errors.voice.json: "JSON错误: %{detail}"
errors.voice.config_rs: "配置读取错误: %{detail}"
errors.voice.daemon: "守护进程错误: %{detail}"
errors.voice.unsupported_format: "不支持的音频格式: %{detail}"
errors.voice.file_too_large: "文件过大: %{size} 字节 (最大: %{max} 字节)"
errors.voice.model_not_found: "模型未找到: %{model}"
errors.voice.invalid_model_name: "无效的模型名称: %{model}"
errors.voice.worker_pool: "工作池错误: %{detail}"
errors.voice.transcription_timeout: "转录超时 (%{seconds} 秒后)"
errors.voice.transcription_failed: "转录失败: %{detail}"
errors.voice.audio_conversion_failed: "音频转换失败: %{detail}"
errors.voice.audio_probe_error: "音频探测错误: %{detail}"
errors.voice.temp_file_error: "临时文件错误: %{detail}"
errors.voice.multipart_error: "Multipart表单错误: %{detail}"
errors.voice.missing_field: "缺少必填字段: %{field}"
errors.voice.network: "网络错误: %{detail}"
errors.voice.storage: "存储错误: %{detail}"
errors.voice.task_management_disabled: "任务管理已禁用"
errors.voice.not_found: "资源未找到: %{resource}"
errors.voice.initialization: "初始化错误: %{detail}"
errors.voice.tts: "TTS错误: %{detail}"
errors.voice.invalid_input: "无效输入: %{detail}"
errors.voice.io: "IO错误: %{detail}"
# ===========================================
# CLI 消息 - mcp-proxy 启动
# ===========================================
cli.mirror.not_configured: "未配置镜像源npm/PyPI将使用默认源"
cli.mirror.npm: "npm 镜像: %{url}"
cli.mirror.pypi: "PyPI 镜像: %{url}"
cli.startup.service_starting: "MCP-Proxy 启动中..."
cli.startup.version: "版本: %{version}"
cli.startup.config_loaded: "配置加载完成"
cli.startup.port: "端口: %{port}"
cli.startup.log_dir: "日志目录: %{path}"
cli.startup.log_level: "日志级别: %{level}"
cli.startup.log_retain_days: "日志保留天数: %{days}"
cli.startup.success: "✅ 服务启动成功,监听地址: %{addr}"
cli.startup.health_endpoint: "✅ 健康检查端点: http://%{addr}/health"
cli.startup.mcp_list: "✅ MCP 服务列表: http://%{addr}/mcp"
cli.startup.schedule_task_started: "✅ MCP服务状态检查定时任务已启动"
cli.startup.log_rotation_configured: "✅ 日志自动轮转已配置(保留最近 %{count} 个日志文件)"
cli.startup.system_info: "系统信息:"
cli.startup.os: "操作系统: %{os}"
cli.startup.arch: "架构: %{arch}"
cli.startup.work_dir: "工作目录: %{path}"
cli.startup.env_override: "环境变量覆盖:"
cli.startup.env_port_override: "MCP_PROXY_PORT: %{port}"
cli.startup.env_log_dir_override: "MCP_PROXY_LOG_DIR: %{dir}"
cli.startup.env_log_level_override: "MCP_PROXY_LOG_LEVEL: %{level}"
cli.startup.trying_bind: "尝试绑定到地址: %{addr}"
cli.startup.bind_success: "成功绑定到地址: %{addr}"
cli.startup.bind_failed: "绑定地址 %{addr} 失败: %{error}"
cli.startup.init_state: "初始化应用状态..."
cli.startup.state_done: "应用状态初始化完成"
cli.startup.init_router: "初始化路由..."
cli.startup.router_done: "路由初始化完成"
cli.startup.warming_up: "\U0001F504 开始预热 uv/deno 环境依赖..."
cli.startup.warmup_success: "✅ 预热 uv/deno 环境依赖完成"
cli.startup.warmup_failed: "❌ 预热 uv/deno 环境依赖失败: %{error}"
cli.startup.http_server_starting: "\U0001F680 HTTP 服务器启动,等待连接..."
cli.startup.proxy_mode: "命令: proxy (HTTP 服务器模式)"
cli.startup.config_info: "配置信息:"
cli.startup.listen_port: "监听端口: %{port}"
cli.startup.log_retention: "日志保留: %{days} 天"
# CLI 消息 - mcp-proxy 关闭
cli.shutdown.signal_received: "⚠️ 服务器收到关闭信号,开始清理资源..."
cli.shutdown.cleanup_success: "✅ 资源清理成功"
cli.shutdown.cleanup_failed: "❌ 清理资源时出错: %{error}"
cli.shutdown.complete: "✅ 资源清理完成,服务已完全关闭"
cli.shutdown.service_error: "❌ 服务运行错误: %{error}"
# CLI 消息 - panic 处理
cli.panic.handler_started: "程序发生panic执行清理..."
cli.panic.reason: "Panic 原因: %{reason}"
cli.panic.reason_unknown: "Panic 原因: 未知"
cli.panic.location: "Panic 位置: %{file}:%{line}"
cli.panic.stack_trace: "堆栈跟踪:"
# ===========================================
# CLI 消息 - convert 模式
# ===========================================
cli.convert.starting: "开始 URL 模式处理"
cli.convert.target_url: "目标 URL: %{url}"
cli.convert.protocol_specified: "使用命令行指定协议: %{protocol}"
cli.convert.protocol_config: "使用配置文件协议: %{protocol}"
cli.convert.detecting_protocol: "开始自动检测协议..."
cli.convert.detect_failed: "协议检测失败: %{error}"
cli.convert.using_protocol: "使用 %{protocol} 协议模式"
cli.convert.stdio_url_not_supported: "Stdio 协议不支持通过 URL 转换"
cli.convert.tool_whitelist: "工具白名单: %{tools}"
cli.convert.tool_blacklist: "工具黑名单: %{tools}"
cli.convert.config_parsed: "配置解析成功"
cli.convert.service_name: "MCP 服务名称: %{name}"
cli.convert.service_name_not_specified: "MCP 服务名称: 未指定(使用 direct URL"
cli.convert.cli_starting: "MCP-Proxy CLI 启动"
cli.convert.command: "命令: convert (stdio 桥接模式)"
cli.convert.version: "版本: %{version}"
cli.convert.diagnostic_mode: "诊断模式: %{enabled}"
cli.convert.mode_direct_url: "模式: 直接 URL 模式"
cli.convert.mode_remote_service: "模式: 远程服务配置模式"
cli.convert.service_url: "服务 URL: %{url}"
cli.convert.config_protocol: "配置协议: %{protocol}"
cli.convert.ping_config: "Ping 间隔: %{interval}s, Ping 超时: %{timeout}s"
cli.convert.connecting_backend: "开始连接到后端服务 (超时: %{timeout}s)..."
cli.convert.detect_complete: "协议检测完成: %{protocol} (耗时: %{duration})"
# ===========================================
# CLI 消息 - SSE 模式
# ===========================================
cli.sse.mode_starting: "SSE 模式启动"
cli.sse.connect_timeout: "连接后端超时 (%{seconds}s)"
cli.sse.connect_failed: "连接后端失败: %{error}"
cli.sse.connect_success: "后端连接成功 (耗时: %{duration})"
cli.sse.stdio_starting: "启动 stdio server..."
cli.sse.stdio_started: "stdio server 已启动"
cli.sse.waiting_events: "开始等待 stdio server 事件..."
cli.sse.stdio_exit_eof: "stdio server 退出 - 原因: MCP 客户端断开连接 (stdin EOF)"
cli.sse.watchdog_exit: "Watchdog 任务退出"
cli.sse.normal_exit: "mcp-proxy convert (SSE 模式) 正常退出"
cli.sse.watchdog_starting: "SSE Watchdog 启动"
cli.sse.max_retries: "最大重试次数: %{count} (0=无限)"
cli.sse.monitoring_connection: "开始监控初始连接..."
cli.sse.initial_disconnect: "初始连接断开: %{reason}"
cli.sse.connection_alive: "初始连接存活时长: %{seconds}s"
cli.sse.reconnect_attempt: "重连尝试 #%{attempt}/%{max}"
cli.sse.backoff_time: "退避时间: %{seconds}s"
cli.sse.reconnect_success: "重连成功 (耗时: %{duration})"
cli.sse.monitoring_reconnect: "开始监控重连后的连接..."
cli.sse.reconnect_disconnect: "重连后断开: %{reason}"
cli.sse.reconnect_alive: "重连后存活时长: %{seconds}s"
cli.sse.max_retries_reached: "达到最大重试次数 (%{count}), 停止重连"
cli.sse.watchdog_exit_msg: "SSE Watchdog 退出"
# ===========================================
# CLI 消息 - Stream 模式
# ===========================================
cli.stream.mode_starting: "Stream 模式启动"
cli.stream.connect_timeout: "连接后端超时 (%{seconds}s)"
cli.stream.connect_failed: "连接后端失败: %{error}"
cli.stream.connect_success: "后端连接成功 (耗时: %{duration})"
cli.stream.stdio_starting: "启动 stdio server..."
cli.stream.stdio_started: "stdio server 已启动"
cli.stream.waiting_events: "开始等待 stdio server 事件..."
cli.stream.stdio_exit_eof: "stdio server 退出 - 原因: MCP 客户端断开连接 (stdin EOF)"
cli.stream.watchdog_exit: "Watchdog 任务退出"
cli.stream.normal_exit: "mcp-proxy convert (Stream 模式) 正常退出"
# ===========================================
# CLI 消息 - Command 模式
# ===========================================
cli.command.local_mode: "模式: 本地命令模式"
cli.command.command: "命令: %{cmd} %{args}"
cli.command.ctrl_c: "收到 Ctrl+C 信号,正在关闭..."
# ===========================================
# CLI 消息 - 监控
# ===========================================
cli.monitoring.health: "开始监控 %{protocol} 连接健康状态"
cli.monitoring.health_check_status: "\U0001F493 [%{protocol}][健康检查] 连接状态: %{status} (仅检查连接通道, 未调用 list_tools), 检查 #%{count}, 已存活: %{seconds}s"
cli.monitoring.health_check_normal: "\U0001F493 [%{protocol}][健康检查] 后端服务正常 (list_tools 验证通过), Ping #%{count}, 已存活: %{seconds}s"
# ===========================================
# 诊断消息
# ===========================================
diagnostic.report_header: "========== 诊断报告 =========="
diagnostic.connection_protocol: "连接协议: %{protocol}"
diagnostic.service_url: "服务 URL: %{url}"
diagnostic.connection_duration: "连接存活时长: %{seconds} 秒"
diagnostic.disconnect_reason: "断开原因: %{reason}"
diagnostic.error_type: "错误类型: %{error_type}"
diagnostic.possible_causes: "可能原因分析:"
diagnostic.suggestions: "建议:"
# 诊断 - 30秒超时分析
diagnostic.analysis.timeout_30s: "⚠️ 连接在约 30 秒时断开,极有可能是:"
diagnostic.analysis.timeout_30s_cause1: "服务器端设置了 30 秒超时限制"
diagnostic.analysis.timeout_30s_cause2: "负载均衡器(如 Nginx/ALB的默认超时"
diagnostic.analysis.timeout_30s_cause3: "云服务商的网关超时限制"
# 诊断 - 快速断开分析
diagnostic.analysis.quick_disconnect: "⚠️ 连接很快断开(%{seconds}秒),可能是:"
diagnostic.analysis.quick_disconnect_cause1: "认证失败或 token 无效"
diagnostic.analysis.quick_disconnect_cause2: "服务器拒绝连接"
diagnostic.analysis.quick_disconnect_cause3: "网络不稳定"
# 诊断 - 长时间连接分析
diagnostic.analysis.long_connection: "✅ 连接保持了较长时间(%{seconds}秒),可能是:"
diagnostic.analysis.long_connection_cause1: "工具调用执行时间过长"
diagnostic.analysis.long_connection_cause2: "网络波动导致断开"
# 诊断建议
diagnostic.suggestion.timeout_30s: "联系服务提供商增加超时限制"
diagnostic.suggestion.timeout_client: "使用 --request-timeout 参数设置客户端超时"
diagnostic.suggestion.async_mode: "考虑使用异步处理模式webhook 回调)"
diagnostic.suggestion.ping_interval: "尝试增加 ping 间隔: --ping-interval %{seconds}"
diagnostic.suggestion.ping_timeout: "增加 ping 超时时间: --ping-timeout %{seconds}"
diagnostic.suggestion.ping_disable: "或禁用 ping: --ping-interval 0"
# ===========================================
# 错误分类
# ===========================================
error_classify.timeout_30s: "30秒超时可能是服务器限制"
error_classify.service_unavailable_503: "服务不可用(503)"
error_classify.internal_server_error_500: "服务器内部错误(500)"
error_classify.bad_gateway_502: "网关错误(502)"
error_classify.gateway_timeout_504: "网关超时(504)"
error_classify.unauthorized_401: "未授权(401)"
error_classify.forbidden_403: "禁止访问(403)"
error_classify.not_found_404: "资源未找到(404)"
error_classify.request_timeout_408: "请求超时(408)"
error_classify.timeout: "超时"
error_classify.connection_refused: "连接被拒绝"
error_classify.connection_reset: "连接被重置"
error_classify.connection_closed: "连接关闭"
error_classify.dns_failed: "DNS解析失败"
error_classify.ssl_tls_error: "SSL/TLS错误"
error_classify.network_error: "网络错误"
error_classify.session_error: "会话错误"
error_classify.unknown_error: "未知错误"
# ===========================================
# CLI 消息 - health 命令
# ===========================================
cli.health.checking: "\U0001F50D 健康检查服务: %{url}"
cli.health.using_protocol: "\U0001F50D 使用指定协议: %{protocol}"
cli.health.detecting_protocol: "\U0001F50D 正在检测协议..."
cli.health.detected_protocol: "\U0001F50D 检测到 %{protocol} 协议"
cli.health.healthy: "✅ 服务健康"
cli.health.unhealthy: "❌ 服务不健康"
cli.health.stdio_not_supported: "health 命令不支持 stdio 协议"
# ===========================================
# CLI 消息 - check 命令
# ===========================================
cli.check.checking: "\U0001F50D 检查服务: %{url}"
cli.check.healthy: "✅ 服务正常,检测到 %{protocol} 协议"
cli.check.failed: "❌ 服务检查失败: %{error}"
# ===========================================
# CLI 消息 - document-parser
# ===========================================
doc_parser.startup.app_info: "=== %{name} v%{version} 启动 ==="
doc_parser.startup.config_summary: "配置摘要: %{summary}"
doc_parser.startup.listening: "服务监听地址: %{host}:%{port}"
doc_parser.startup.checking_python: "开始检查和初始化Python环境..."
doc_parser.startup.checking_venv: "检查并激活虚拟环境..."
doc_parser.startup.venv_activate_failed: "虚拟环境自动激活失败: %{error}"
doc_parser.startup.venv_activate_manual: "请手动激活虚拟环境: source ./venv/bin/activate"
doc_parser.startup.venv_activated: "虚拟环境已自动激活"
doc_parser.startup.env_check_failed: "环境检查失败,将在后台自动安装: %{error}"
doc_parser.startup.mineru_not_installed: "MinerU依赖未安装开始后台自动安装..."
doc_parser.startup.markitdown_not_installed: "MarkItDown依赖未安装开始后台自动安装..."
doc_parser.startup.install_complete: "后台Python环境安装完成"
doc_parser.startup.install_failed: "后台Python环境安装失败: %{error}"
doc_parser.startup.install_task_started: "Python依赖安装任务已启动后台进行服务将正常启动"
doc_parser.startup.mineru_ready: "MinerU依赖已安装版本: %{version}"
doc_parser.startup.markitdown_ready: "MarkItDown依赖已安装"
doc_parser.startup.python_ready: "Python环境检查完成所有依赖已就绪"
doc_parser.startup.state_failed: "无法创建应用状态: %{error}"
doc_parser.startup.health_check_failed: "应用健康检查失败: %{error}"
doc_parser.startup.state_ready: "应用状态初始化成功"
doc_parser.startup.http_routes_ready: "HTTP路由初始化成功"
doc_parser.startup.background_tasks_started: "后台任务已启动"
doc_parser.startup.service_ready: "服务启动成功,开始监听连接..."
doc_parser.shutdown.service_stopped: "服务已关闭"
doc_parser.shutdown.ctrl_c_received: "收到 Ctrl+C 信号,开始优雅关闭..."
doc_parser.shutdown.terminate_received: "收到 terminate 信号,开始优雅关闭..."
doc_parser.shutdown.closing: "正在关闭服务..."
# uv-init 命令
doc_parser.uv_init.starting: "\U0001F680 开始在当前目录初始化uv虚拟环境和依赖..."
doc_parser.uv_init.current_dir: "\U0001F4C1 当前工作目录: %{path}"
doc_parser.uv_init.venv_path: "\U0001F4C1 虚拟环境将创建在: ./venv/"
doc_parser.uv_init.validating_dir: "\U0001F50D 验证当前目录设置..."
doc_parser.uv_init.dir_valid: " ✅ 目录验证通过"
doc_parser.uv_init.warnings_found: " ⚠️ 发现 %{count} 个警告"
doc_parser.uv_init.dir_invalid: " ❌ 目录验证失败,发现 %{count} 个问题"
doc_parser.uv_init.auto_fixing: " \U0001F527 尝试自动修复 %{count} 个问题..."
doc_parser.uv_init.fix_success: " ✅ %{message}"
doc_parser.uv_init.fix_failed: " ❌ 清理失败: %{error}"
doc_parser.uv_init.manual_fix_hint: " \U0001F4A1 请手动解决以下问题:"
doc_parser.uv_init.dir_validation_failed: "目录验证失败,请解决上述问题后重试"
doc_parser.uv_init.dir_validation_error: " ⚠️ 目录验证失败: %{error}"
doc_parser.uv_init.continue_install: " 继续进行安装,但可能遇到问题..."
# 环境检查
doc_parser.check.checking_env: "\U0001F50D 检查当前环境状态..."
doc_parser.check.env_check_complete: " 环境检查完成:"
doc_parser.check.python_available: "✅ 可用"
doc_parser.check.python_unavailable: "❌ 不可用"
doc_parser.check.uv_available: "✅ 可用"
doc_parser.check.uv_unavailable: "❌ 不可用"
doc_parser.check.venv_active: "✅ 激活"
doc_parser.check.venv_inactive: "❌ 未激活"
doc_parser.check.mineru_available: "✅ 可用"
doc_parser.check.mineru_unavailable: "❌ 不可用"
doc_parser.check.markitdown_available: "✅ 可用"
doc_parser.check.markitdown_unavailable: "❌ 不可用"
doc_parser.check.env_check_failed: " ⚠️ 环境检查失败: %{error}"
doc_parser.check.continue_install: " 继续进行安装..."
# 安装过程
doc_parser.install.all_ready: "✨ 所有依赖都已就绪,无需安装!"
doc_parser.install.plan: "\U0001F4CB 安装计划:"
doc_parser.install.install_uv: "安装 uv 工具"
doc_parser.install.create_venv: "创建虚拟环境 (./venv/)"
doc_parser.install.install_mineru: "安装 MinerU 依赖"
doc_parser.install.install_markitdown: "安装 MarkItDown 依赖"
doc_parser.install.starting: "⚙️ 开始设置Python环境和依赖..."
doc_parser.install.wait_hint: "这可能需要几分钟时间,请耐心等待..."
doc_parser.install.path_issues: "⚠️ 检测到潜在的路径问题:"
doc_parser.install.auto_fixing: "\U0001F527 尝试自动修复问题..."
doc_parser.install.fixed: "✅ 已修复以下问题:"
doc_parser.install.cannot_auto_fix: "无法自动修复,请手动解决上述问题"
doc_parser.install.fix_failed: "❌ 自动修复失败: %{error}"
doc_parser.install.manual_fix_hint: "\U0001F4A1 手动修复建议:"
doc_parser.install.python_setup_complete: "✅ Python环境设置完成"
doc_parser.install.python_setup_failed: "❌ Python环境设置失败: %{error}"
doc_parser.install.detailed_hints: "\U0001F4A1 详细故障排除建议:"
doc_parser.install.diagnostic_commands: "\U0001F50D 诊断命令:"
doc_parser.install.check_env: "检查环境状态: document-parser check"
doc_parser.install.view_logs: "查看详细日志: 检查 logs/ 目录"
# 验证
doc_parser.verify.starting: "\U0001F50D 验证安装结果..."
doc_parser.verify.complete: "验证完成:"
doc_parser.verify.version: "版本: %{version}"
doc_parser.verify.partial_issues: "⚠️ 部分依赖安装可能存在问题"
doc_parser.verify.need_fix: "\U0001F527 需要解决的问题:"
doc_parser.verify.suggestion: "建议: %{suggestion}"
doc_parser.verify.init_incomplete: "环境初始化未完全成功"
doc_parser.verify.failed: "❌ 验证失败: %{error}"
# 成功消息
doc_parser.success.init_complete: "\U0001F389 uv环境初始化完成"
doc_parser.success.all_ready: "✨ 所有依赖都已就绪,现在可以启动服务器了"
doc_parser.success.venv_activate: "\U0001F4CB 虚拟环境激活指令:"
doc_parser.success.start_server: "\U0001F680 启动服务器:"
doc_parser.success.use_uv: "\U0001F527 或者使用 uv 直接运行命令:"
doc_parser.success.more_help: "\U0001F4DA 更多帮助:"
doc_parser.success.tips: "\U0001F4A1 提示:"
doc_parser.success.venv_location: "虚拟环境位置: ./venv/"
doc_parser.success.python_path: "Python可执行文件: ./venv/bin/python (Linux/macOS) 或 .\\venv\\Scripts\\python.exe (Windows)"
doc_parser.success.troubleshoot_hint: "如遇问题,请运行 'document-parser troubleshoot' 查看详细指南"
# troubleshoot 命令
doc_parser.troubleshoot.title: "\U0001F527 Document Parser 故障排除指南"
doc_parser.troubleshoot.env_overview: "\U0001F4CA 当前环境概览:"
doc_parser.troubleshoot.work_dir: "工作目录: %{path}"
doc_parser.troubleshoot.venv_path: "虚拟环境: ./venv/"
doc_parser.troubleshoot.os: "操作系统: %{os}"
# 虚拟环境问题
doc_parser.troubleshoot.venv_problems: "\U0001F3E0 1. 虚拟环境问题"
doc_parser.troubleshoot.venv_create_failed: "❓ 问题: 虚拟环境创建失败"
doc_parser.troubleshoot.diagnostic_steps: "\U0001F50D 诊断步骤:"
doc_parser.troubleshoot.solutions: "\U0001F4A1 解决方案:"
doc_parser.troubleshoot.venv_activate_failed: "❓ 问题: 虚拟环境激活失败"
# 依赖安装问题
doc_parser.troubleshoot.deps_problems: "\U0001F4E6 2. 依赖安装问题"
doc_parser.troubleshoot.uv_not_installed: "❓ 问题: UV工具未安装或不可用"
doc_parser.troubleshoot.mineru_markitdown_failed: "❓ 问题: MinerU或MarkItDown安装失败"
# 网络问题
doc_parser.troubleshoot.network_problems: "\U0001F310 3. 网络和下载问题"
doc_parser.troubleshoot.network_timeout: "❓ 问题: 网络连接超时或下载失败"
# 系统环境问题
doc_parser.troubleshoot.system_problems: "⚙️ 4. 系统环境问题"
doc_parser.troubleshoot.python_incompatible: "❓ 问题: Python版本不兼容"
doc_parser.troubleshoot.cuda_config: "❓ 问题: CUDA环境配置 (可选用于GPU加速)"
# 诊断命令
doc_parser.troubleshoot.diagnostic_commands: "\U0001F50D 5. 常用诊断命令"
doc_parser.troubleshoot.env_check: "环境检查:"
doc_parser.troubleshoot.manual_verify: "手动验证:"
doc_parser.troubleshoot.view_logs: "日志查看:"
# 获取帮助
doc_parser.troubleshoot.more_help: "\U0001F198 6. 获取更多帮助"
doc_parser.troubleshoot.if_unsolved: "如果上述方法都无法解决问题,请:"
# 实时诊断
doc_parser.troubleshoot.realtime_diagnosis: "\U0001F52C 实时环境诊断"
doc_parser.troubleshoot.env_good: "✅ 环境状态良好,所有依赖都已就绪"
doc_parser.troubleshoot.issues_found: "⚠️ 发现以下问题:"
doc_parser.troubleshoot.env_check_failed: "❌ 环境检查失败: %{error}"
doc_parser.troubleshoot.follow_guide: "请按照上述指南进行故障排除"
# 提示
doc_parser.troubleshoot.tip: "\U0001F4A1 提示: 大多数问题可以通过重新运行 'document-parser uv-init' 解决"
# 后台清理
doc_parser.background.cleanup_failed: "清理过期数据失败: %{error}"
doc_parser.background.cleanup_complete: "后台清理任务执行完成"
# 信号处理
doc_parser.signal.ctrl_c_failed: "无法监听 Ctrl+C 信号"
doc_parser.signal.terminate_failed: "无法监听 terminate 信号"
# ===========================================
# 通用消息
# ===========================================
common.yes: "是"
common.no: "否"
common.success: "成功"
common.failed: "失败"
common.error: "错误"
common.warning: "警告"
common.info: "信息"
common.loading: "加载中..."
common.please_wait: "请稍候..."
common.retry: "重试"
common.cancel: "取消"
common.confirm: "确认"
common.back: "返回"
common.next: "下一步"
common.previous: "上一步"
common.done: "完成"
common.skip: "跳过"

View File

@@ -0,0 +1,468 @@
# ===========================================
# 錯誤訊息 - mcp-proxy
# ===========================================
errors.mcp_proxy.service_not_found: "服務 %{service} 未找到"
errors.mcp_proxy.service_restart_cooldown: "服務 %{service} 在重啟冷卻期內,請稍後再試"
errors.mcp_proxy.service_startup_in_progress: "服務 %{service} 正在啟動中,請稍後再試"
errors.mcp_proxy.service_startup_failed: "服務啟動失敗: %{mcp_id}: %{reason}"
errors.mcp_proxy.backend_connection: "後端連線錯誤: %{detail}"
errors.mcp_proxy.config_parse: "設定解析錯誤: %{detail}"
errors.mcp_proxy.mcp_server_error: "MCP 伺服器錯誤: %{detail}"
errors.mcp_proxy.json_serialization: "JSON 序列化錯誤: %{detail}"
errors.mcp_proxy.io_error: "IO 錯誤: %{detail}"
errors.mcp_proxy.route_not_found: "路由未找到: %{path}"
errors.mcp_proxy.invalid_parameter: "無效的請求參數: %{detail}"
# ===========================================
# 錯誤訊息 - document-parser
# ===========================================
errors.document_parser.config: "設定錯誤: %{detail}"
errors.document_parser.file: "檔案操作錯誤: %{detail}"
errors.document_parser.unsupported_format: "不支援的檔案格式: %{format}"
errors.document_parser.parse: "解析錯誤: %{detail}"
errors.document_parser.mineru: "MinerU錯誤: %{detail}"
errors.document_parser.markitdown: "MarkItDown錯誤: %{detail}"
errors.document_parser.oss: "OSS操作錯誤: %{detail}"
errors.document_parser.database: "資料庫錯誤: %{detail}"
errors.document_parser.network: "網路錯誤: %{detail}"
errors.document_parser.task: "任務錯誤: %{detail}"
errors.document_parser.internal: "內部錯誤: %{detail}"
errors.document_parser.timeout: "操作逾時: %{detail}"
errors.document_parser.validation: "驗證錯誤: %{detail}"
errors.document_parser.environment: "環境錯誤: %{detail}"
errors.document_parser.virtual_environment_path: "虛擬環境路徑錯誤: %{detail}"
errors.document_parser.permission: "權限錯誤: %{detail}"
errors.document_parser.path: "路徑錯誤: %{detail}"
errors.document_parser.queue: "佇列錯誤: %{detail}"
errors.document_parser.processing: "處理錯誤: %{detail}"
# 錯誤建議 - document-parser
errors.document_parser.suggestions.config: "檢查設定檔案和環境變數"
errors.document_parser.suggestions.file: "檢查檔案路徑和權限"
errors.document_parser.suggestions.unsupported_format: "檢查檔案格式是否支援"
errors.document_parser.suggestions.parse: "檢查檔案內容是否完整"
errors.document_parser.suggestions.mineru: "檢查MinerU環境設定"
errors.document_parser.suggestions.markitdown: "檢查MarkItDown環境設定"
errors.document_parser.suggestions.oss: "檢查OSS設定和網路連線"
errors.document_parser.suggestions.database: "檢查資料庫連線和權限"
errors.document_parser.suggestions.network: "檢查網路連線和防火牆設定"
errors.document_parser.suggestions.task: "檢查任務參數和狀態"
errors.document_parser.suggestions.internal: "聯絡技術支援"
errors.document_parser.suggestions.timeout: "檢查網路延遲或增加逾時時間"
errors.document_parser.suggestions.validation: "檢查輸入參數格式"
errors.document_parser.suggestions.environment: "檢查系統環境和相依套件安裝"
errors.document_parser.suggestions.queue: "檢查佇列服務狀態和設定"
errors.document_parser.suggestions.processing: "檢查處理流程和資料格式"
errors.document_parser.suggestions.virtual_environment_path: "檢查虛擬環境路徑和目錄權限"
errors.document_parser.suggestions.permission: "檢查檔案和目錄權限設定"
errors.document_parser.suggestions.path: "檢查路徑是否存在且可存取"
# ===========================================
# 錯誤訊息 - oss-client
# ===========================================
errors.oss.config: "設定錯誤: %{detail}"
errors.oss.network: "網路錯誤: %{detail}"
errors.oss.file_not_found: "檔案不存在: %{path}"
errors.oss.permission: "權限不足: %{detail}"
errors.oss.io: "IO錯誤: %{detail}"
errors.oss.sdk: "OSS SDK錯誤: %{detail}"
errors.oss.file_size_exceeded: "檔案大小超出限制: %{detail}"
errors.oss.unsupported_file_type: "不支援的檔案類型: %{detail}"
errors.oss.timeout: "操作逾時: %{detail}"
errors.oss.invalid_parameter: "無效的參數: %{detail}"
# ===========================================
# 錯誤訊息 - voice-cli
# ===========================================
errors.voice.config: "設定錯誤: %{detail}"
errors.voice.audio_processing: "音訊處理錯誤: %{detail}"
errors.voice.transcription: "轉錄錯誤: %{detail}"
errors.voice.model: "模型錯誤: %{detail}"
errors.voice.file_io: "檔案I/O錯誤: %{detail}"
errors.voice.http: "HTTP請求錯誤: %{detail}"
errors.voice.serialization: "序列化錯誤: %{detail}"
errors.voice.json: "JSON錯誤: %{detail}"
errors.voice.config_rs: "設定讀取錯誤: %{detail}"
errors.voice.daemon: "常駐程式錯誤: %{detail}"
errors.voice.unsupported_format: "不支援的音訊格式: %{detail}"
errors.voice.file_too_large: "檔案過大: %{size} 位元組 (最大: %{max} 位元組)"
errors.voice.model_not_found: "模型未找到: %{model}"
errors.voice.invalid_model_name: "無效的模型名稱: %{model}"
errors.voice.worker_pool: "工作池錯誤: %{detail}"
errors.voice.transcription_timeout: "轉錄逾時 (%{seconds} 秒後)"
errors.voice.transcription_failed: "轉錄失敗: %{detail}"
errors.voice.audio_conversion_failed: "音訊轉換失敗: %{detail}"
errors.voice.audio_probe_error: "音訊探測錯誤: %{detail}"
errors.voice.temp_file_error: "暫存檔錯誤: %{detail}"
errors.voice.multipart_error: "Multipart表單錯誤: %{detail}"
errors.voice.missing_field: "缺少必填欄位: %{field}"
errors.voice.network: "網路錯誤: %{detail}"
errors.voice.storage: "儲存錯誤: %{detail}"
errors.voice.task_management_disabled: "任務管理已停用"
errors.voice.not_found: "資源未找到: %{resource}"
errors.voice.initialization: "初始化錯誤: %{detail}"
errors.voice.tts: "TTS錯誤: %{detail}"
errors.voice.invalid_input: "無效輸入: %{detail}"
errors.voice.io: "IO錯誤: %{detail}"
# ===========================================
# CLI 訊息 - mcp-proxy 啟動
# ===========================================
cli.mirror.not_configured: "未配置鏡像源npm/PyPI將使用預設來源"
cli.mirror.npm: "npm 鏡像: %{url}"
cli.mirror.pypi: "PyPI 鏡像: %{url}"
cli.startup.service_starting: "MCP-Proxy 啟動中..."
cli.startup.version: "版本: %{version}"
cli.startup.config_loaded: "設定載入完成"
cli.startup.port: "連接埠: %{port}"
cli.startup.log_dir: "日誌目錄: %{path}"
cli.startup.log_level: "日誌級別: %{level}"
cli.startup.log_retain_days: "日誌保留天數: %{days}"
cli.startup.success: "✅ 服務啟動成功,監聽位址: %{addr}"
cli.startup.health_endpoint: "✅ 健康檢查端點: http://%{addr}/health"
cli.startup.mcp_list: "✅ MCP 服務列表: http://%{addr}/mcp"
cli.startup.schedule_task_started: "✅ MCP服務狀態檢查定時任務已啟動"
cli.startup.log_rotation_configured: "✅ 日誌自動輪轉已設定(保留最近 %{count} 個日誌檔案)"
cli.startup.system_info: "系統資訊:"
cli.startup.os: "作業系統: %{os}"
cli.startup.arch: "架構: %{arch}"
cli.startup.work_dir: "工作目錄: %{path}"
cli.startup.env_override: "環境變數覆蓋:"
cli.startup.env_port_override: "MCP_PROXY_PORT: %{port}"
cli.startup.env_log_dir_override: "MCP_PROXY_LOG_DIR: %{dir}"
cli.startup.env_log_level_override: "MCP_PROXY_LOG_LEVEL: %{level}"
cli.startup.trying_bind: "嘗試綁定到位址: %{addr}"
cli.startup.bind_success: "成功綁定到位址: %{addr}"
cli.startup.bind_failed: "綁定位址 %{addr} 失敗: %{error}"
cli.startup.init_state: "初始化應用狀態..."
cli.startup.state_done: "應用狀態初始化完成"
cli.startup.init_router: "初始化路由..."
cli.startup.router_done: "路由初始化完成"
cli.startup.warming_up: "\U0001F504 開始預熱 uv/deno 環境相依套件..."
cli.startup.warmup_success: "✅ 預熱 uv/deno 環境相依套件完成"
cli.startup.warmup_failed: "❌ 預熱 uv/deno 環境相依套件失敗: %{error}"
cli.startup.http_server_starting: "\U0001F680 HTTP 伺服器啟動,等待連線..."
cli.startup.proxy_mode: "命令: proxy (HTTP 伺服器模式)"
cli.startup.config_info: "設定資訊:"
cli.startup.listen_port: "監聽連接埠: %{port}"
cli.startup.log_retention: "日誌保留: %{days} 天"
# CLI 訊息 - mcp-proxy 關閉
cli.shutdown.signal_received: "⚠️ 伺服器收到關閉訊號,開始清理資源..."
cli.shutdown.cleanup_success: "✅ 資源清理成功"
cli.shutdown.cleanup_failed: "❌ 清理資源時出錯: %{error}"
cli.shutdown.complete: "✅ 資源清理完成,服務已完全關閉"
cli.shutdown.service_error: "❌ 服務執行錯誤: %{error}"
# CLI 訊息 - panic 處理
cli.panic.handler_started: "程式發生panic執行清理..."
cli.panic.reason: "Panic 原因: %{reason}"
cli.panic.reason_unknown: "Panic 原因: 未知"
cli.panic.location: "Panic 位置: %{file}:%{line}"
cli.panic.stack_trace: "堆疊追蹤:"
# ===========================================
# CLI 訊息 - convert 模式
# ===========================================
cli.convert.starting: "開始 URL 模式處理"
cli.convert.target_url: "目標 URL: %{url}"
cli.convert.protocol_specified: "使用命令列指定協定: %{protocol}"
cli.convert.protocol_config: "使用設定檔協定: %{protocol}"
cli.convert.detecting_protocol: "開始自動偵測協定..."
cli.convert.detect_failed: "協定偵測失敗: %{error}"
cli.convert.using_protocol: "使用 %{protocol} 協定模式"
cli.convert.stdio_url_not_supported: "Stdio 協定不支援透過 URL 轉換"
cli.convert.tool_whitelist: "工具白名單: %{tools}"
cli.convert.tool_blacklist: "工具黑名單: %{tools}"
cli.convert.config_parsed: "設定解析成功"
cli.convert.service_name: "MCP 服務名稱: %{name}"
cli.convert.service_name_not_specified: "MCP 服務名稱: 未指定(使用 direct URL"
cli.convert.cli_starting: "MCP-Proxy CLI 啟動"
cli.convert.command: "命令: convert (stdio 橋接模式)"
cli.convert.version: "版本: %{version}"
cli.convert.diagnostic_mode: "診斷模式: %{enabled}"
cli.convert.mode_direct_url: "模式: 直接 URL 模式"
cli.convert.mode_remote_service: "模式: 遠端服務設定模式"
cli.convert.service_url: "服務 URL: %{url}"
cli.convert.config_protocol: "設定協定: %{protocol}"
cli.convert.ping_config: "Ping 間隔: %{interval}s, Ping 逾時: %{timeout}s"
cli.convert.connecting_backend: "開始連線到後端服務 (逾時: %{timeout}s)..."
cli.convert.detect_complete: "協定偵測完成: %{protocol} (耗時: %{duration})"
# ===========================================
# CLI 訊息 - SSE 模式
# ===========================================
cli.sse.mode_starting: "SSE 模式啟動"
cli.sse.connect_timeout: "連線後端逾時 (%{seconds}s)"
cli.sse.connect_failed: "連線後端失敗: %{error}"
cli.sse.connect_success: "後端連線成功 (耗時: %{duration})"
cli.sse.stdio_starting: "啟動 stdio server..."
cli.sse.stdio_started: "stdio server 已啟動"
cli.sse.waiting_events: "開始等待 stdio server 事件..."
cli.sse.stdio_exit_eof: "stdio server 結束 - 原因: MCP 客戶端斷開連線 (stdin EOF)"
cli.sse.watchdog_exit: "Watchdog 任務結束"
cli.sse.normal_exit: "mcp-proxy convert (SSE 模式) 正常結束"
cli.sse.watchdog_starting: "SSE Watchdog 啟動"
cli.sse.max_retries: "最大重試次數: %{count} (0=無限)"
cli.sse.monitoring_connection: "開始監控初始連線..."
cli.sse.initial_disconnect: "初始連線斷開: %{reason}"
cli.sse.connection_alive: "初始連線存活時長: %{seconds}s"
cli.sse.reconnect_attempt: "重連嘗試 #%{attempt}/%{max}"
cli.sse.backoff_time: "退避時間: %{seconds}s"
cli.sse.reconnect_success: "重連成功 (耗時: %{duration})"
cli.sse.monitoring_reconnect: "開始監控重連後的連線..."
cli.sse.reconnect_disconnect: "重連後斷開: %{reason}"
cli.sse.reconnect_alive: "重連後存活時長: %{seconds}s"
cli.sse.max_retries_reached: "達到最大重試次數 (%{count}), 停止重連"
cli.sse.watchdog_exit_msg: "SSE Watchdog 結束"
# ===========================================
# CLI 訊息 - Stream 模式
# ===========================================
cli.stream.mode_starting: "Stream 模式啟動"
cli.stream.connect_timeout: "連線後端逾時 (%{seconds}s)"
cli.stream.connect_failed: "連線後端失敗: %{error}"
cli.stream.connect_success: "後端連線成功 (耗時: %{duration})"
cli.stream.stdio_starting: "啟動 stdio server..."
cli.stream.stdio_started: "stdio server 已啟動"
cli.stream.waiting_events: "開始等待 stdio server 事件..."
cli.stream.stdio_exit_eof: "stdio server 結束 - 原因: MCP 客戶端斷開連線 (stdin EOF)"
cli.stream.watchdog_exit: "Watchdog 任務結束"
cli.stream.normal_exit: "mcp-proxy convert (Stream 模式) 正常結束"
# ===========================================
# CLI 訊息 - Command 模式
# ===========================================
cli.command.local_mode: "模式: 本地命令模式"
cli.command.command: "命令: %{cmd} %{args}"
cli.command.ctrl_c: "收到 Ctrl+C 訊號,正在關閉..."
# ===========================================
# CLI 訊息 - 監控
# ===========================================
cli.monitoring.health: "開始監控 %{protocol} 連線健康狀態"
cli.monitoring.health_check_status: "\U0001F493 [%{protocol}][健康檢查] 連線狀態: %{status} (僅檢查連線通道, 未呼叫 list_tools), 檢查 #%{count}, 已存活: %{seconds}s"
cli.monitoring.health_check_normal: "\U0001F493 [%{protocol}][健康檢查] 後端服務正常 (list_tools 驗證通過), Ping #%{count}, 已存活: %{seconds}s"
# ===========================================
# 診斷訊息
# ===========================================
diagnostic.report_header: "========== 診斷報告 =========="
diagnostic.connection_protocol: "連線協定: %{protocol}"
diagnostic.service_url: "服務 URL: %{url}"
diagnostic.connection_duration: "連線存活時長: %{seconds} 秒"
diagnostic.disconnect_reason: "斷開原因: %{reason}"
diagnostic.error_type: "錯誤類型: %{error_type}"
diagnostic.possible_causes: "可能原因分析:"
diagnostic.suggestions: "建議:"
# 診斷 - 30秒逾時分析
diagnostic.analysis.timeout_30s: "⚠️ 連線在約 30 秒時斷開,極有可能是:"
diagnostic.analysis.timeout_30s_cause1: "伺服器端設定了 30 秒逾時限制"
diagnostic.analysis.timeout_30s_cause2: "負載平衡器(如 Nginx/ALB的預設逾時"
diagnostic.analysis.timeout_30s_cause3: "雲端服務商的閘道逾時限制"
# 診斷 - 快速斷開分析
diagnostic.analysis.quick_disconnect: "⚠️ 連線很快斷開(%{seconds}秒),可能是:"
diagnostic.analysis.quick_disconnect_cause1: "認證失敗或 token 無效"
diagnostic.analysis.quick_disconnect_cause2: "伺服器拒絕連線"
diagnostic.analysis.quick_disconnect_cause3: "網路不穩定"
# 診斷 - 長時間連線分析
diagnostic.analysis.long_connection: "✅ 連線保持了較長時間(%{seconds}秒),可能是:"
diagnostic.analysis.long_connection_cause1: "工具呼叫執行時間過長"
diagnostic.analysis.long_connection_cause2: "網路波動導致斷開"
# 診斷建議
diagnostic.suggestion.timeout_30s: "聯絡服務提供商增加逾時限制"
diagnostic.suggestion.timeout_client: "使用 --request-timeout 參數設定客戶端逾時"
diagnostic.suggestion.async_mode: "考慮使用非同步處理模式webhook 回呼)"
diagnostic.suggestion.ping_interval: "嘗試增加 ping 間隔: --ping-interval %{seconds}"
diagnostic.suggestion.ping_timeout: "增加 ping 逾時時間: --ping-timeout %{seconds}"
diagnostic.suggestion.ping_disable: "或停用 ping: --ping-interval 0"
# ===========================================
# 錯誤分類
# ===========================================
error_classify.timeout_30s: "30秒逾時可能是伺服器限制"
error_classify.service_unavailable_503: "服務不可用(503)"
error_classify.internal_server_error_500: "伺服器內部錯誤(500)"
error_classify.bad_gateway_502: "閘道錯誤(502)"
error_classify.gateway_timeout_504: "閘道逾時(504)"
error_classify.unauthorized_401: "未授權(401)"
error_classify.forbidden_403: "禁止存取(403)"
error_classify.not_found_404: "資源未找到(404)"
error_classify.request_timeout_408: "請求逾時(408)"
error_classify.timeout: "逾時"
error_classify.connection_refused: "連線被拒絕"
error_classify.connection_reset: "連線被重設"
error_classify.connection_closed: "連線關閉"
error_classify.dns_failed: "DNS解析失敗"
error_classify.ssl_tls_error: "SSL/TLS錯誤"
error_classify.network_error: "網路錯誤"
error_classify.session_error: "工作階段錯誤"
error_classify.unknown_error: "未知錯誤"
# ===========================================
# CLI 訊息 - health 命令
# ===========================================
cli.health.checking: "\U0001F50D 健康檢查服務: %{url}"
cli.health.using_protocol: "\U0001F50D 使用指定協定: %{protocol}"
cli.health.detecting_protocol: "\U0001F50D 正在偵測協定..."
cli.health.detected_protocol: "\U0001F50D 偵測到 %{protocol} 協定"
cli.health.healthy: "✅ 服務健康"
cli.health.unhealthy: "❌ 服務不健康"
cli.health.stdio_not_supported: "health 命令不支援 stdio 協定"
# ===========================================
# CLI 訊息 - check 命令
# ===========================================
cli.check.checking: "\U0001F50D 檢查服務: %{url}"
cli.check.healthy: "✅ 服務正常,偵測到 %{protocol} 協定"
cli.check.failed: "❌ 服務檢查失敗: %{error}"
# ===========================================
# CLI 訊息 - document-parser
# ===========================================
doc_parser.startup.app_info: "=== %{name} v%{version} 啟動 ==="
doc_parser.startup.config_summary: "設定摘要: %{summary}"
doc_parser.startup.listening: "服務監聽位址: %{host}:%{port}"
doc_parser.startup.checking_python: "開始檢查和初始化Python環境..."
doc_parser.startup.checking_venv: "檢查並啟用虛擬環境..."
doc_parser.startup.venv_activate_failed: "虛擬環境自動啟用失敗: %{error}"
doc_parser.startup.venv_activate_manual: "請手動啟用虛擬環境: source ./venv/bin/activate"
doc_parser.startup.venv_activated: "虛擬環境已自動啟用"
doc_parser.startup.env_check_failed: "環境檢查失敗,將在背景自動安裝: %{error}"
doc_parser.startup.mineru_not_installed: "MinerU相依套件未安裝開始背景自動安裝..."
doc_parser.startup.markitdown_not_installed: "MarkItDown相依套件未安裝開始背景自動安裝..."
doc_parser.startup.install_complete: "背景Python環境安裝完成"
doc_parser.startup.install_failed: "背景Python環境安裝失敗: %{error}"
doc_parser.startup.install_task_started: "Python相依套件安裝任務已啟動背景進行服務將正常啟動"
doc_parser.startup.mineru_ready: "MinerU相依套件已安裝版本: %{version}"
doc_parser.startup.markitdown_ready: "MarkItDown相依套件已安裝"
doc_parser.startup.python_ready: "Python環境檢查完成所有相依套件已就緒"
doc_parser.startup.state_failed: "無法建立應用狀態: %{error}"
doc_parser.startup.health_check_failed: "應用健康檢查失敗: %{error}"
doc_parser.startup.state_ready: "應用狀態初始化成功"
doc_parser.startup.http_routes_ready: "HTTP路由初始化成功"
doc_parser.startup.background_tasks_started: "背景任務已啟動"
doc_parser.startup.service_ready: "服務啟動成功,開始監聽連線..."
doc_parser.shutdown.service_stopped: "服務已關閉"
doc_parser.shutdown.ctrl_c_received: "收到 Ctrl+C 訊號,開始優雅關閉..."
doc_parser.shutdown.terminate_received: "收到 terminate 訊號,開始優雅關閉..."
doc_parser.shutdown.closing: "正在關閉服務..."
# uv-init 命令
doc_parser.uv_init.starting: "\U0001F680 開始在目前目錄初始化uv虛擬環境和相依套件..."
doc_parser.uv_init.current_dir: "\U0001F4C1 目前工作目錄: %{path}"
doc_parser.uv_init.venv_path: "\U0001F4C1 虛擬環境將建立在: ./venv/"
doc_parser.uv_init.validating_dir: "\U0001F50D 驗證目前目錄設定..."
doc_parser.uv_init.dir_valid: " ✅ 目錄驗證通過"
doc_parser.uv_init.warnings_found: " ⚠️ 發現 %{count} 個警告"
doc_parser.uv_init.dir_invalid: " ❌ 目錄驗證失敗,發現 %{count} 個問題"
doc_parser.uv_init.auto_fixing: " \U0001F527 嘗試自動修復 %{count} 個問題..."
doc_parser.uv_init.fix_success: " ✅ %{message}"
doc_parser.uv_init.fix_failed: " ❌ 清理失敗: %{error}"
doc_parser.uv_init.manual_fix_hint: " \U0001F4A1 請手動解決以下問題:"
doc_parser.uv_init.dir_validation_failed: "目錄驗證失敗,請解決上述問題後重試"
doc_parser.uv_init.dir_validation_error: " ⚠️ 目錄驗證失敗: %{error}"
doc_parser.uv_init.continue_install: " 繼續進行安裝,但可能遇到問題..."
# 環境檢查
doc_parser.check.checking_env: "\U0001F50D 檢查目前環境狀態..."
doc_parser.check.env_check_complete: " 環境檢查完成:"
doc_parser.check.python_available: "✅ 可用"
doc_parser.check.python_unavailable: "❌ 不可用"
doc_parser.check.uv_available: "✅ 可用"
doc_parser.check.uv_unavailable: "❌ 不可用"
doc_parser.check.venv_active: "✅ 啟用"
doc_parser.check.venv_inactive: "❌ 未啟用"
doc_parser.check.mineru_available: "✅ 可用"
doc_parser.check.mineru_unavailable: "❌ 不可用"
doc_parser.check.markitdown_available: "✅ 可用"
doc_parser.check.markitdown_unavailable: "❌ 不可用"
doc_parser.check.env_check_failed: " ⚠️ 環境檢查失敗: %{error}"
doc_parser.check.continue_install: " 繼續進行安裝..."
# 安裝過程
doc_parser.install.all_ready: "✨ 所有相依套件都已就緒,無需安裝!"
doc_parser.install.plan: "\U0001F4CB 安裝計畫:"
doc_parser.install.install_uv: "安裝 uv 工具"
doc_parser.install.create_venv: "建立虛擬環境 (./venv/)"
doc_parser.install.install_mineru: "安裝 MinerU 相依套件"
doc_parser.install.install_markitdown: "安裝 MarkItDown 相依套件"
doc_parser.install.starting: "⚙️ 開始設定Python環境和相依套件..."
doc_parser.install.wait_hint: "這可能需要幾分鐘時間,請耐心等待..."
doc_parser.install.path_issues: "⚠️ 偵測到潛在的路徑問題:"
doc_parser.install.auto_fixing: "\U0001F527 嘗試自動修復問題..."
doc_parser.install.fixed: "✅ 已修復以下問題:"
doc_parser.install.cannot_auto_fix: "無法自動修復,請手動解決上述問題"
doc_parser.install.fix_failed: "❌ 自動修復失敗: %{error}"
doc_parser.install.manual_fix_hint: "\U0001F4A1 手動修復建議:"
doc_parser.install.python_setup_complete: "✅ Python環境設定完成"
doc_parser.install.python_setup_failed: "❌ Python環境設定失敗: %{error}"
doc_parser.install.detailed_hints: "\U0001F4A1 詳細故障排除建議:"
doc_parser.install.diagnostic_commands: "\U0001F50D 診斷命令:"
doc_parser.install.check_env: "檢查環境狀態: document-parser check"
doc_parser.install.view_logs: "查看詳細日誌: 檢查 logs/ 目錄"
# 驗證
doc_parser.verify.starting: "\U0001F50D 驗證安裝結果..."
doc_parser.verify.complete: "驗證完成:"
doc_parser.verify.version: "版本: %{version}"
doc_parser.verify.partial_issues: "⚠️ 部分相依套件安裝可能有問題"
doc_parser.verify.need_fix: "\U0001F527 需要解決的問題:"
doc_parser.verify.suggestion: "建議: %{suggestion}"
doc_parser.verify.init_incomplete: "環境初始化未完全成功"
doc_parser.verify.failed: "❌ 驗證失敗: %{error}"
# 成功訊息
doc_parser.success.init_complete: "\U0001F389 uv環境初始化完成"
doc_parser.success.all_ready: "✨ 所有相依套件都已就緒,現在可以啟動伺服器了"
doc_parser.success.venv_activate: "\U0001F4CB 虛擬環境啟用指令:"
doc_parser.success.start_server: "\U0001F680 啟動伺服器:"
doc_parser.success.use_uv: "\U0001F527 或者使用 uv 直接執行命令:"
doc_parser.success.more_help: "\U0001F4DA 更多說明:"
doc_parser.success.tips: "\U0001F4A1 提示:"
doc_parser.success.venv_location: "虛擬環境位置: ./venv/"
doc_parser.success.python_path: "Python可執行檔: ./venv/bin/python (Linux/macOS) 或 .\\venv\\Scripts\\python.exe (Windows)"
doc_parser.success.troubleshoot_hint: "如遇問題,請執行 'document-parser troubleshoot' 查看詳細指南"
# troubleshoot 命令
doc_parser.troubleshoot.title: "\U0001F527 Document Parser 故障排除指南"
doc_parser.troubleshoot.env_overview: "\U0001F4CA 目前環境概覽:"
doc_parser.troubleshoot.work_dir: "工作目錄: %{path}"
doc_parser.troubleshoot.venv_path: "虛擬環境: ./venv/"
doc_parser.troubleshoot.os: "作業系統: %{os}"
# 虛擬環境問題
doc_parser.troubleshoot.venv_problems: "\U0001F3E0 1. 虛擬環境問題"
doc_parser.troubleshoot.venv_create_failed: "❓ 問題: 虛擬環境建立失敗"
doc_parser.troubleshoot.diagnostic_steps: "\U0001F50D 診斷步驟:"
doc_parser.troubleshoot.solutions: "\U0001F4A1 解決方案:"
doc_parser.troubleshoot.venv_activate_failed: "❓ 問題: 虛擬環境啟用失敗"
# 相依套件安裝問題
doc_parser.troubleshoot.deps_problems: "\U0001F4E6 2. 相依套件安裝問題"
doc_parser.troubleshoot.uv_not_installed: "❓ 問題: UV工具未安裝或不可用"
doc_parser.troubleshoot.mineru_markitdown_failed: "❓ 問題: MinerU或MarkItDown安裝失敗"
# 網路問題
doc_parser.troubleshoot.network_problems: "\U0001F310 3. 網路和下載問題"
doc_parser.troubleshoot.network_timeout: "❓ 問題: 網路連線逾時或下載失敗"
# 系統環境問題
doc_parser.troubleshoot.system_problems: "⚙️ 4. 系統環境問題"
doc_parser.troubleshoot.python_incompatible: "❓ 問題: Python版本不相容"
doc_parser.troubleshoot.cuda_config: "❓ 問題: CUDA環境設定 (可選用於GPU加速)"
# 診斷命令
doc_parser.troubleshoot.diagnostic_commands: "\U0001F50D 5. 常用診斷命令"
doc_parser.troubleshoot.env_check: "環境檢查:"
doc_parser.troubleshoot.manual_verify: "手動驗證:"
doc_parser.troubleshoot.view_logs: "日誌查看:"
# 取得幫助
doc_parser.troubleshoot.more_help: "\U0001F198 6. 取得更多幫助"
doc_parser.troubleshoot.if_unsolved: "如果上述方法都無法解決問題,請:"
# 即時診斷
doc_parser.troubleshoot.realtime_diagnosis: "\U0001F52C 即時環境診斷"
doc_parser.troubleshoot.env_good: "✅ 環境狀態良好,所有相依套件都已就緒"
doc_parser.troubleshoot.issues_found: "⚠️ 發現以下問題:"
doc_parser.troubleshoot.env_check_failed: "❌ 環境檢查失敗: %{error}"
doc_parser.troubleshoot.follow_guide: "請按照上述指南進行故障排除"
# 提示
doc_parser.troubleshoot.tip: "\U0001F4A1 提示: 大多數問題可以透過重新執行 'document-parser uv-init' 解決"
# 背景清理
doc_parser.background.cleanup_failed: "清理過期資料失敗: %{error}"
doc_parser.background.cleanup_complete: "背景清理任務執行完成"
# 訊號處理
doc_parser.signal.ctrl_c_failed: "無法監聽 Ctrl+C 訊號"
doc_parser.signal.terminate_failed: "無法監聽 terminate 訊號"
# ===========================================
# 通用訊息
# ===========================================
common.yes: "是"
common.no: "否"
common.success: "成功"
common.failed: "失敗"
common.error: "錯誤"
common.warning: "警告"
common.info: "資訊"
common.loading: "載入中..."
common.please_wait: "請稍候..."
common.retry: "重試"
common.cancel: "取消"
common.confirm: "確認"
common.back: "返回"
common.next: "下一步"
common.previous: "上一步"
common.done: "完成"
common.skip: "跳過"

View File

@@ -0,0 +1,85 @@
//! OSS配置管理模块
use crate::error::{OssError, Result};
use serde::{Deserialize, Serialize};
/// 默认配置常量
pub mod defaults {
pub const ENDPOINT: &str = "oss-rg-china-mainland.aliyuncs.com";
pub const PUBLIC_BUCKET: &str = "nuwa-packages";
pub const PRIVATE_BUCKET: &str = "edu-nuwa-packages";
pub const REGION: &str = "oss-rg-china-mainland";
pub const UPLOAD_DIRECTORY: &str = "edu";
}
/// OSS配置结构体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OssConfig {
/// OSS endpoint (默认: oss-rg-china-mainland.aliyuncs.com)
pub endpoint: String,
/// 存储桶名称 (默认: nuwa-packages)
pub bucket: String,
/// 访问密钥ID (必须通过环境变量设置)
pub access_key_id: String,
/// 访问密钥Secret (必须通过环境变量设置)
pub access_key_secret: String,
/// 区域 (默认: oss-rg-china-mainland)
pub region: String,
/// 上传目录前缀 (默认: edu)
pub upload_directory: String,
}
impl OssConfig {
/// 创建自定义配置(所有字段需显式提供)
pub fn new(
endpoint: String,
bucket: String,
access_key_id: String,
access_key_secret: String,
region: String,
upload_directory: String,
) -> Self {
Self {
endpoint,
bucket,
access_key_id,
access_key_secret,
region,
upload_directory,
}
}
/// 验证配置有效性
pub fn validate(&self) -> Result<()> {
if self.access_key_id.is_empty() {
return Err(OssError::Config("access_key_id 不能为空".to_string()));
}
if self.access_key_secret.is_empty() {
return Err(OssError::Config("access_key_secret 不能为空".to_string()));
}
if self.endpoint.is_empty() {
return Err(OssError::Config("endpoint 不能为空".to_string()));
}
if self.bucket.is_empty() {
return Err(OssError::Config("bucket 不能为空".to_string()));
}
if self.region.is_empty() {
return Err(OssError::Config("region 不能为空".to_string()));
}
Ok(())
}
/// 获取完整的OSS URL
pub fn get_base_url(&self) -> String {
format!("https://{}.{}", self.bucket, self.endpoint)
}
/// 获取带前缀的object key
pub fn get_prefixed_key(&self, key: &str) -> String {
if key.starts_with(&self.upload_directory) {
key.to_string()
} else {
format!("{}/{}", self.upload_directory, key.trim_start_matches('/'))
}
}
}

View File

@@ -0,0 +1,187 @@
//! 错误处理模块
use thiserror::Error;
/// OSS操作错误类型
#[derive(Error, Debug)]
pub enum OssError {
#[error("{0}")]
Config(String),
#[error("{0}")]
Network(String),
#[error("{0}")]
FileNotFound(String),
#[error("{0}")]
Permission(String),
#[error("{0}")]
Io(String),
#[error("{0}")]
Sdk(String),
#[error("{0}")]
FileSizeExceeded(String),
#[error("{0}")]
UnsupportedFileType(String),
#[error("{0}")]
Timeout(String),
#[error("{0}")]
InvalidParameter(String),
}
impl OssError {
/// 创建配置错误
pub fn config<T: Into<String>>(msg: T) -> Self {
Self::Config(t!("errors.oss.config", detail = msg.into()).to_string())
}
/// 创建网络错误
pub fn network<T: Into<String>>(msg: T) -> Self {
Self::Network(t!("errors.oss.network", detail = msg.into()).to_string())
}
/// 创建文件不存在错误
pub fn file_not_found<T: Into<String>>(msg: T) -> Self {
Self::FileNotFound(t!("errors.oss.file_not_found", path = msg.into()).to_string())
}
/// 创建权限错误
pub fn permission<T: Into<String>>(msg: T) -> Self {
Self::Permission(t!("errors.oss.permission", detail = msg.into()).to_string())
}
/// 创建SDK错误
pub fn sdk<T: Into<String>>(msg: T) -> Self {
Self::Sdk(t!("errors.oss.sdk", detail = msg.into()).to_string())
}
/// 创建文件大小超出限制错误
pub fn file_size_exceeded<T: Into<String>>(msg: T) -> Self {
Self::FileSizeExceeded(t!("errors.oss.file_size_exceeded", detail = msg.into()).to_string())
}
/// 创建不支持的文件类型错误
pub fn unsupported_file_type<T: Into<String>>(msg: T) -> Self {
Self::UnsupportedFileType(
t!("errors.oss.unsupported_file_type", detail = msg.into()).to_string(),
)
}
/// 创建超时错误
pub fn timeout<T: Into<String>>(msg: T) -> Self {
Self::Timeout(t!("errors.oss.timeout", detail = msg.into()).to_string())
}
/// 创建无效参数错误
pub fn invalid_parameter<T: Into<String>>(msg: T) -> Self {
Self::InvalidParameter(t!("errors.oss.invalid_parameter", detail = msg.into()).to_string())
}
/// 创建IO错误
pub fn io_error<T: Into<String>>(msg: T) -> Self {
Self::Io(t!("errors.oss.io", detail = msg.into()).to_string())
}
/// 判断是否为配置错误
pub fn is_config_error(&self) -> bool {
matches!(self, Self::Config(_))
}
/// 判断是否为网络错误
pub fn is_network_error(&self) -> bool {
matches!(self, Self::Network(_))
}
/// 判断是否为文件不存在错误
pub fn is_file_not_found(&self) -> bool {
matches!(self, Self::FileNotFound(_))
}
/// 判断是否为权限错误
pub fn is_permission_error(&self) -> bool {
matches!(self, Self::Permission(_))
}
}
/// 从标准库错误转换
impl From<std::io::Error> for OssError {
fn from(err: std::io::Error) -> Self {
Self::io_error(err.to_string())
}
}
/// Result类型别名
pub type Result<T> = std::result::Result<T, OssError>;
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_error_creation() {
let config_err = OssError::config("test config error");
assert!(config_err.is_config_error());
assert_eq!(config_err.to_string(), "配置错误: test config error");
let network_err = OssError::network("test network error");
assert!(network_err.is_network_error());
assert_eq!(network_err.to_string(), "网络错误: test network error");
let file_err = OssError::file_not_found("test.txt");
assert!(file_err.is_file_not_found());
assert_eq!(file_err.to_string(), "文件不存在: test.txt");
let permission_err = OssError::permission("access denied");
assert!(permission_err.is_permission_error());
assert_eq!(permission_err.to_string(), "权限不足: access denied");
}
#[test]
fn test_io_error_conversion() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let oss_err: OssError = io_err.into();
match oss_err {
OssError::Io(_) => {} // 正确
_ => panic!("IO错误转换失败"),
}
}
#[test]
fn test_error_display() {
let err = OssError::FileSizeExceeded("文件大小超过100MB".to_string());
assert_eq!(err.to_string(), "文件大小超出限制: 文件大小超过100MB");
let err = OssError::UnsupportedFileType("不支持.xyz格式".to_string());
assert_eq!(err.to_string(), "不支持的文件类型: 不支持.xyz格式");
let err = OssError::Timeout("操作超时30秒".to_string());
assert_eq!(err.to_string(), "操作超时: 操作超时30秒");
let err = OssError::InvalidParameter("object_key不能为空".to_string());
assert_eq!(err.to_string(), "无效的参数: object_key不能为空");
}
#[test]
fn test_error_type_checking() {
let config_err = OssError::Config("test".to_string());
assert!(config_err.is_config_error());
assert!(!config_err.is_network_error());
assert!(!config_err.is_file_not_found());
assert!(!config_err.is_permission_error());
let network_err = OssError::Network("test".to_string());
assert!(!network_err.is_config_error());
assert!(network_err.is_network_error());
assert!(!network_err.is_file_not_found());
assert!(!network_err.is_permission_error());
}
}

View File

@@ -0,0 +1,175 @@
//! 简洁的阿里云OSS操作库
//!
//! 提供基本的文件上传、下载、删除功能以及预签名URL生成。
//!
//! # 快速开始
//!
//! ```rust,no_run
//! use oss_client::{PrivateOssClient, OssConfig, OssClientTrait};
//!
//! #[tokio::main]
//! async fn main() -> oss_client::Result<()> {
//! // 创建配置
//! let config = OssConfig::new(
//! "oss-rg-china-mainland.aliyuncs.com".to_string(),
//! "bucket_name".to_string(),
//! "access_key_id".to_string(),
//! "access_key_secret".to_string(),
//! "oss-rg-china-mainland".to_string(),
//! "upload_directory".to_string(),
//! );
//!
//! // 创建客户端
//! let client = PrivateOssClient::new(config)?;
//!
//! // 上传文件
//! let url = client.upload_file("local/file.txt", "remote/file.txt").await?;
//! println!("File uploaded successfully: {}", url);
//! Ok(())
//! }
//! ```
// 初始化 i18n使用 crate 内置翻译文件
#[macro_use]
extern crate rust_i18n;
// 初始化翻译文件,使用 crate 内置 locales支持独立发布
i18n!("locales", fallback = "en");
pub mod config;
pub mod error;
pub mod private_client;
pub mod public_client;
pub mod utils;
// 重新导出主要类型
pub use config::{OssConfig, defaults};
pub use error::{OssError, Result};
pub use private_client::PrivateOssClient;
pub use public_client::PublicOssClient;
// 重新导出常用工具函数
pub use utils::{
detect_mime_type, detect_mime_type_by_extension, format_file_size, generate_random_filename,
get_file_extension, get_filename, get_filename_without_extension, is_audio_file,
is_document_file, is_image_file, is_video_file, parse_file_size, replace_oss_domain,
replace_oss_domains_batch, sanitize_filename,
};
/// OSS客户端公共接口trait
///
/// 定义了OSS客户端的基本操作接口包括文件操作、签名URL生成等
/// 私有bucket和公有bucket客户端都实现这个trait
#[async_trait::async_trait]
pub trait OssClientTrait: Send + Sync {
/// 获取配置信息
fn get_config(&self) -> &OssConfig;
/// 获取基础URL
fn get_base_url(&self) -> String;
/// 生成上传签名URL
///
/// # 参数
/// * `object_key` - 对象键
/// * `expires_in` - 过期时间
/// * `content_type` - 内容类型(可选)
///
/// # 返回
/// * 带签名的上传URL
fn generate_upload_url(
&self,
object_key: &str,
expires_in: std::time::Duration,
content_type: Option<&str>,
) -> Result<String>;
/// 生成下载签名URL
///
/// # 参数
/// * `object_key` - 对象键
/// * `expires_in` - 过期时间(可选)
///
/// # 返回
/// * 带签名的下载URL
fn generate_download_url(
&self,
object_key: &str,
expires_in: Option<std::time::Duration>,
) -> Result<String>;
/// 上传文件
///
/// # 参数
/// * `local_path` - 本地文件路径
/// * `object_key` - 对象键
///
/// # 返回
/// * 上传后的文件URL
async fn upload_file(&self, local_path: &str, object_key: &str) -> Result<String>;
/// 上传内容
///
/// # 参数
/// * `content` - 要上传的内容
/// * `object_key` - 对象键
/// * `content_type` - 内容类型(可选)
///
/// # 返回
/// * 上传后的文件URL
async fn upload_content(
&self,
content: &[u8],
object_key: &str,
content_type: Option<&str>,
) -> Result<String>;
/// 删除文件
///
/// # 参数
/// * `object_key` - 对象键
///
/// # 返回
/// * 删除操作结果
async fn delete_file(&self, object_key: &str) -> Result<()>;
/// 检查文件是否存在
///
/// # 参数
/// * `object_key` - 对象键
///
/// # 返回
/// * 文件是否存在
async fn file_exists(&self, object_key: &str) -> Result<bool>;
/// 测试连接
///
/// # 返回
/// * 连接测试结果
async fn test_connection(&self) -> Result<()>;
/// 生成唯一的object key
///
/// # 参数
/// * `prefix` - 前缀
/// * `filename` - 原始文件名(可选)
///
/// # 返回
/// * 唯一的对象键
fn generate_object_key(&self, prefix: &str, filename: Option<&str>) -> String;
}
/// 获取库版本信息
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
/// 获取库名称
pub fn name() -> &'static str {
env!("CARGO_PKG_NAME")
}
/// 获取库描述
pub fn description() -> &'static str {
env!("CARGO_PKG_DESCRIPTION")
}

View File

@@ -0,0 +1,219 @@
//! 私有Bucket客户端实现
use aliyun_oss_rust_sdk::oss::OSS;
use aliyun_oss_rust_sdk::request::RequestBuilder;
use aliyun_oss_rust_sdk::url::UrlApi;
use chrono::Utc;
use std::path::Path;
use std::time::Duration;
use tokio::fs;
use tracing::warn;
use crate::OssClientTrait;
use crate::config::OssConfig;
use crate::error::{OssError, Result};
use crate::utils::{self, detect_mime_type, sanitize_filename};
/// 私有OSS客户端签名访问
#[derive(Debug)]
pub struct PrivateOssClient {
client: OSS,
config: OssConfig,
}
impl PrivateOssClient {
/// 创建新的私有OSS客户端
pub fn new(config: OssConfig) -> Result<Self> {
config.validate()?;
let client = OSS::new(
&config.access_key_id,
&config.access_key_secret,
&config.endpoint,
&config.bucket,
);
Ok(Self { client, config })
}
/// 获取配置信息
pub fn get_config(&self) -> &OssConfig {
&self.config
}
/// 获取基础URL
pub fn get_base_url(&self) -> String {
self.config.get_base_url()
}
}
#[async_trait::async_trait]
impl OssClientTrait for PrivateOssClient {
fn get_config(&self) -> &OssConfig {
&self.config
}
fn get_base_url(&self) -> String {
self.config.get_base_url()
}
fn generate_upload_url(
&self,
object_key: &str,
expires_in: std::time::Duration,
content_type: Option<&str>,
) -> Result<String> {
let prefixed_key = self.config.get_prefixed_key(object_key);
let mut builder = RequestBuilder::new().with_expire(expires_in.as_secs() as i64);
if let Some(ct) = content_type {
builder = builder.with_content_type(ct);
} else {
builder = builder.with_content_type("application/octet-stream");
}
let url = self.client.sign_upload_url(&prefixed_key, &builder);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
fn generate_download_url(
&self,
object_key: &str,
expires_in: Option<std::time::Duration>,
) -> Result<String> {
let prefixed_key = self.config.get_prefixed_key(object_key);
let duration = expires_in.unwrap_or_else(|| Duration::from_secs(7 * 24 * 3600));
let builder = RequestBuilder::new().with_expire(duration.as_secs() as i64);
let url = self.client.sign_download_url(&prefixed_key, &builder);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
async fn upload_file(&self, local_path: &str, object_key: &str) -> Result<String> {
if !Path::new(local_path).exists() {
return Err(OssError::file_not_found(format!(
"本地文件不存在: {local_path}"
)));
}
let content_type = detect_mime_type(local_path);
let prefixed_key = self.config.get_prefixed_key(object_key);
let builder = RequestBuilder::new().with_content_type(&content_type);
let local_path_string = local_path.to_string();
match self
.client
.put_object_from_file(&prefixed_key, &local_path_string, builder)
.await
{
Ok(_) => {
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
Err(e) => Err(OssError::sdk(format!("上传文件失败: {e}"))),
}
}
async fn upload_content(
&self,
content: &[u8],
object_key: &str,
content_type: Option<&str>,
) -> Result<String> {
let prefixed_key = self.config.get_prefixed_key(object_key);
let temp_file =
tempfile::NamedTempFile::new().map_err(|e| OssError::io_error(e.to_string()))?;
fs::write(temp_file.path(), content)
.await
.map_err(|e| OssError::io_error(e.to_string()))?;
let mut builder = RequestBuilder::new();
if let Some(ct) = content_type {
builder = builder.with_content_type(ct);
} else {
builder = builder.with_content_type("application/octet-stream");
}
let temp_path_string = temp_file.path().to_str().unwrap().to_string();
match self
.client
.put_object_from_file(&prefixed_key, &temp_path_string, builder)
.await
{
Ok(_) => {
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
Err(e) => Err(OssError::sdk(format!("上传内容失败: {e}"))),
}
}
async fn delete_file(&self, object_key: &str) -> Result<()> {
let prefixed_key = self.config.get_prefixed_key(object_key);
let builder = RequestBuilder::new();
match self.client.delete_object(&prefixed_key, builder).await {
Ok(_) => Ok(()),
Err(e) => Err(OssError::sdk(format!("删除文件失败: {e}"))),
}
}
async fn file_exists(&self, object_key: &str) -> Result<bool> {
let prefixed_key = self.config.get_prefixed_key(object_key);
let builder = RequestBuilder::new();
// 使用 HEAD 请求检查对象是否存在,避免下载主体
match self
.client
.get_object_metadata(&prefixed_key, builder)
.await
{
Ok(_) => Ok(true),
Err(e) => {
warn!("File existence check failed: {}", e);
Ok(false)
}
}
}
async fn test_connection(&self) -> Result<()> {
let test_key = format!("health-check-{}", chrono::Utc::now().timestamp_millis());
let test_content = b"OSS connection test";
match <PrivateOssClient as OssClientTrait>::upload_content(
self,
test_content,
&test_key,
Some("text/plain"),
)
.await
{
Ok(_) => {
let _ = <PrivateOssClient as OssClientTrait>::delete_file(self, &test_key).await;
Ok(())
}
Err(e) => Err(OssError::network(format!("无法连接到OSS: {e}"))),
}
}
fn generate_object_key(&self, prefix: &str, filename: Option<&str>) -> String {
let timestamp = Utc::now().format("%Y%m%d_%H%M%S").to_string();
let uid = uuid::Uuid::new_v4().to_string()[..8].to_string();
let filename = if let Some(original) = filename {
let clean_name = sanitize_filename(original);
if let Some(dot_pos) = clean_name.rfind('.') {
let name_part = &clean_name[..dot_pos];
let ext_part = &clean_name[dot_pos..];
format!("{name_part}_{timestamp}_{uid}{ext_part}")
} else {
format!("{clean_name}_{timestamp}_{uid}.")
}
} else {
format!("file_{timestamp}_{uid}")
};
format!("{prefix}/{filename}")
}
}

View File

@@ -0,0 +1,642 @@
//! 公有Bucket客户端实现
//!
//! 专门用于处理公有bucket的公开访问服务无需签名验证
use crate::OssClientTrait;
use crate::config::OssConfig;
use crate::error::{OssError, Result};
use crate::utils::{self, detect_mime_type, sanitize_filename};
use aliyun_oss_rust_sdk::oss::OSS;
use aliyun_oss_rust_sdk::request::RequestBuilder;
use aliyun_oss_rust_sdk::url::UrlApi;
use chrono::Utc;
use tracing::{info, warn};
/// 公有Bucket客户端
///
/// 专门用于处理公有bucket的公开访问服务
/// 所有操作都使用公有bucket无需签名验证
#[derive(Debug)]
pub struct PublicOssClient {
config: OssConfig,
}
impl PublicOssClient {
/// 创建新的公有Bucket客户端
pub fn new(config: OssConfig) -> Result<Self> {
config.validate()?;
Ok(Self { config })
}
/// 获取配置信息
pub fn get_config(&self) -> &OssConfig {
&self.config
}
/// 获取公有bucket的基础URL
pub fn get_base_url(&self) -> String {
self.config.get_base_url()
}
/// 生成公有bucket的公开下载URL无需签名永久有效
///
/// # 参数
/// * `object_key` - 对象键,如 "documents/manual.pdf"
///
/// # 返回
/// * 公开访问的下载URL任何人都可以访问
///
/// # 示例
/// ```rust,no_run
/// use oss_client::{PublicOssClient, OssConfig};
///
/// let config = OssConfig::new(
/// "oss-rg-china-mainland.aliyuncs.com".to_string(),
/// "bucket".to_string(),
/// "".to_string(),
/// "".to_string(),
/// "oss-rg-china-mainland".to_string(),
/// "upload_directory".to_string(),
/// );
/// let client = PublicOssClient::new(config)?;
/// let url = client.generate_public_download_url("documents/manual.pdf")?;
/// println!("Public download URL: {}", url);
/// # Ok::<(), oss_client::OssError>(())
/// ```
pub fn generate_public_download_url(&self, object_key: &str) -> Result<String> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 使用公有bucket生成公开URL
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
info!("Generate public bucket public download URL: {}", url);
Ok(url)
}
/// 生成公有bucket的公开访问URL无需签名永久有效
///
/// # 参数
/// * `object_key` - 对象键,如 "images/logo.png"
///
/// # 返回
/// * 公开访问的URL任何人都可以访问
///
/// # 示例
/// ```rust,no_run
/// use oss_client::{PublicOssClient, OssConfig};
///
/// let config = OssConfig::new(
/// "oss-rg-china-mainland.aliyuncs.com".to_string(),
/// "bucket".to_string(),
/// "".to_string(),
/// "".to_string(),
/// "oss-rg-china-mainland".to_string(),
/// "upload_directory".to_string(),
/// );
/// let client = PublicOssClient::new(config)?;
/// let url = client.generate_public_access_url("images/logo.png")?;
/// println!("Public access URL: {}", url);
/// # Ok::<(), oss_client::OssError>(())
/// ```
pub fn generate_public_access_url(&self, object_key: &str) -> Result<String> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 使用公有bucket生成公开URL
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
info!("Generate public bucket public access URL: {}", url);
Ok(url)
}
/// 批量生成公有bucket的公开访问URL
///
/// # 参数
/// * `object_keys` - 对象键列表
///
/// # 返回
/// * 对象键到公开URL的映射
///
/// # 示例
/// ```rust,no_run
/// use oss_client::{PublicOssClient, OssConfig};
///
/// let config = OssConfig::new(
/// "oss-rg-china-mainland.aliyuncs.com".to_string(),
/// "bucket".to_string(),
/// "".to_string(),
/// "".to_string(),
/// "oss-rg-china-mainland".to_string(),
/// "upload_directory".to_string(),
/// );
/// let client = PublicOssClient::new(config)?;
/// let keys = vec!["doc1.pdf", "doc2.pdf", "image.jpg"];
/// let urls = client.generate_public_urls_batch(&keys)?;
///
/// for (key, url) in urls {
/// println!("{}: {}", key, url);
/// }
/// # Ok::<(), oss_client::OssError>(())
/// ```
pub fn generate_public_urls_batch(
&self,
object_keys: &[&str],
) -> Result<std::collections::HashMap<String, String>> {
let mut url_map = std::collections::HashMap::new();
for &key in object_keys {
let url = self.generate_public_access_url(key)?;
url_map.insert(key.to_string(), url);
}
info!(
"Generate public bucket public URLs in batches, totaling {}",
object_keys.len()
);
Ok(url_map)
}
/// 获取公有bucket的基础信息
///
/// # 返回
/// * 包含bucket名称、endpoint等信息的字符串
pub fn get_bucket_info(&self) -> String {
format!(
"Bucket: {} (Endpoint: {}, Region: {})",
self.config.bucket, self.config.endpoint, self.config.region
)
}
// 以下通用接口建议通过 OssClientTrait 使用
/// 获取公有bucket中文件的元信息
///
/// # 参数
/// * `object_key` - 对象键
///
/// # 返回
/// * 文件元信息(如果存在)
///
/// # 注意
/// 这个方法通过HTTP HEAD请求获取文件元信息
/// 由于是公有bucket任何人都可以执行此操作
pub async fn get_file_metadata(
&self,
object_key: &str,
) -> Result<Option<std::collections::HashMap<String, String>>> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 构建完整的URL
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
// 使用HTTP HEAD请求获取文件元信息
let client = reqwest::Client::new();
let response = client
.head(&url)
.send()
.await
.map_err(|e| OssError::network(format!("获取文件元信息失败: {e}")))?;
if response.status().is_success() {
let headers = response.headers();
let mut metadata = std::collections::HashMap::new();
// 提取常用的元信息
if let Some(content_length) = headers.get("content-length") {
metadata.insert(
"content-length".to_string(),
content_length.to_str().unwrap_or("").to_string(),
);
}
if let Some(content_type) = headers.get("content-type") {
metadata.insert(
"content-type".to_string(),
content_type.to_str().unwrap_or("").to_string(),
);
}
if let Some(last_modified) = headers.get("last-modified") {
metadata.insert(
"last-modified".to_string(),
last_modified.to_str().unwrap_or("").to_string(),
);
}
if let Some(etag) = headers.get("etag") {
metadata.insert("etag".to_string(), etag.to_str().unwrap_or("").to_string());
}
info!(
"Get public bucket file meta information: {} -> {} fields",
prefixed_key,
metadata.len()
);
Ok(Some(metadata))
} else {
info!("The public bucket file does not exist: {}", prefixed_key);
Ok(None)
}
}
/// 生成唯一的object key
///
/// # 参数
/// * `prefix` - 前缀
/// * `filename` - 原始文件名(可选)
///
/// # 返回
/// * 唯一的对象键
pub fn generate_object_key(&self, prefix: &str, filename: Option<&str>) -> String {
let timestamp = Utc::now().format("%Y%m%d_%H%M%S").to_string();
let uid = uuid::Uuid::new_v4().to_string()[..8].to_string(); // 取前8位作为短UID
// 如果有原始文件名,使用原始文件名和后缀
let filename = if let Some(original) = filename {
let clean_name = sanitize_filename(original);
// 分离文件名和扩展名
if let Some(dot_pos) = clean_name.rfind('.') {
let name_part = &clean_name[..dot_pos];
let ext_part = &clean_name[dot_pos..]; // 包含点号
format!("{name_part}_{timestamp}_{uid}{ext_part}")
} else {
// 没有扩展名,保持原名
format!("{clean_name}_{timestamp}_{uid}.")
}
} else {
// 没有原始文件名,生成默认名称
format!("file_{timestamp}_{uid}")
};
format!("{prefix}/{filename}")
}
}
#[async_trait::async_trait]
impl OssClientTrait for PublicOssClient {
fn get_config(&self) -> &OssConfig {
&self.config
}
fn get_base_url(&self) -> String {
self.config.get_base_url()
}
fn generate_upload_url(
&self,
object_key: &str,
expires_in: std::time::Duration,
content_type: Option<&str>,
) -> Result<String> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 创建请求构建器
let mut builder = aliyun_oss_rust_sdk::request::RequestBuilder::new()
.with_expire(expires_in.as_secs() as i64);
// 设置Content-Type
if let Some(ct) = content_type {
builder = builder.with_content_type(ct);
} else {
builder = builder.with_content_type("application/octet-stream");
}
// 创建OSS客户端使用公有bucket
let oss_client = aliyun_oss_rust_sdk::oss::OSS::new(
&self.config.access_key_id,
&self.config.access_key_secret,
&self.config.endpoint,
&self.config.bucket,
);
// 生成签名URL
let url = oss_client.sign_upload_url(&prefixed_key, &builder);
// 替换域名
let url = utils::replace_oss_domain(&url);
Ok(url)
}
fn generate_download_url(
&self,
object_key: &str,
_expires_in: Option<std::time::Duration>,
) -> Result<String> {
// 公有bucket的下载URL不需要签名直接返回公开URL
let prefixed_key = self.config.get_prefixed_key(object_key);
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
async fn upload_file(&self, local_path: &str, object_key: &str) -> Result<String> {
// 检查文件是否存在
if !std::path::Path::new(local_path).exists() {
return Err(OssError::file_not_found(format!(
"本地文件不存在: {local_path}"
)));
}
// 检测MIME类型
let content_type = detect_mime_type(local_path);
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 创建OSS客户端使用公有bucket
let oss_client = aliyun_oss_rust_sdk::oss::OSS::new(
&self.config.access_key_id,
&self.config.access_key_secret,
&self.config.endpoint,
&self.config.bucket,
);
// 创建请求构建器
let builder = RequestBuilder::new().with_content_type(&content_type);
// 执行上传
let local_path_string = local_path.to_string();
match oss_client
.put_object_from_file(&prefixed_key, &local_path_string, builder)
.await
{
Ok(_) => {
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
Err(e) => Err(OssError::sdk(format!("上传文件到公有bucket失败: {e}"))),
}
}
async fn upload_content(
&self,
content: &[u8],
object_key: &str,
content_type: Option<&str>,
) -> Result<String> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 创建临时文件
let temp_file =
tempfile::NamedTempFile::new().map_err(|e| OssError::io_error(e.to_string()))?;
// 写入内容到临时文件
tokio::fs::write(temp_file.path(), content)
.await
.map_err(|e| OssError::io_error(e.to_string()))?;
// 创建OSS客户端使用公有bucket
let oss_client = OSS::new(
&self.config.access_key_id,
&self.config.access_key_secret,
&self.config.endpoint,
&self.config.bucket,
);
// 创建请求构建器
let mut builder = RequestBuilder::new();
if let Some(ct) = content_type {
builder = builder.with_content_type(ct);
} else {
builder = builder.with_content_type("application/octet-stream");
}
// 执行上传
let temp_path_string = temp_file.path().to_str().unwrap().to_string();
match oss_client
.put_object_from_file(&prefixed_key, &temp_path_string, builder)
.await
{
Ok(_) => {
let url = format!("{}/{}", self.get_base_url(), prefixed_key);
let url = utils::replace_oss_domain(&url);
Ok(url)
}
Err(e) => Err(OssError::sdk(format!("上传内容到公有bucket失败: {e}"))),
}
}
async fn delete_file(&self, object_key: &str) -> Result<()> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 创建OSS客户端使用公有bucket
let oss_client = OSS::new(
&self.config.access_key_id,
&self.config.access_key_secret,
&self.config.endpoint,
&self.config.bucket,
);
// 创建请求构建器
let builder = RequestBuilder::new();
// 执行删除
match oss_client.delete_object(&prefixed_key, builder).await {
Ok(_) => Ok(()),
Err(e) => Err(OssError::sdk(format!("删除公有bucket文件失败: {e}"))),
}
}
async fn file_exists(&self, object_key: &str) -> Result<bool> {
// 获取带前缀的object key
let prefixed_key = self.config.get_prefixed_key(object_key);
// 创建OSS客户端使用公有bucket
let oss_client = OSS::new(
&self.config.access_key_id,
&self.config.access_key_secret,
&self.config.endpoint,
&self.config.bucket,
);
// 创建请求构建器
let builder = RequestBuilder::new();
// 使用 HEAD 获取元信息来检查是否存在(避免下载主体)
match oss_client.get_object_metadata(&prefixed_key, builder).await {
Ok(_) => Ok(true),
Err(e) => {
warn!("File existence check failed: {}", e);
Ok(false)
}
}
}
async fn test_connection(&self) -> Result<()> {
// 通过尝试上传一个小的测试文件来验证连接
let test_key = format!("health-check-{}", chrono::Utc::now().timestamp_millis());
let test_content = b"OSS connection test";
match <PublicOssClient as OssClientTrait>::upload_content(
self,
test_content,
&test_key,
Some("text/plain"),
)
.await
{
Ok(_) => {
// 尝试删除测试文件(忽略删除失败)
let _ = <PublicOssClient as OssClientTrait>::delete_file(self, &test_key).await;
Ok(())
}
Err(e) => Err(OssError::network(format!("无法连接到公有bucket: {e}"))),
}
}
fn generate_object_key(&self, prefix: &str, filename: Option<&str>) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
let uid = uuid::Uuid::new_v4().to_string()[..8].to_string();
let filename = if let Some(original) = filename {
let clean_name = sanitize_filename(original);
if let Some(dot_pos) = clean_name.rfind('.') {
let name_part = &clean_name[..dot_pos];
let ext_part = &clean_name[dot_pos..];
format!("{name_part}_{timestamp}_{uid}{ext_part}")
} else {
format!("{clean_name}_{timestamp}_{uid}.")
}
} else {
format!("file_{timestamp}_{uid}")
};
format!("{prefix}/{filename}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_public_client_creation() {
let config = OssConfig::new(
crate::config::defaults::ENDPOINT.to_string(),
crate::config::defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
crate::config::defaults::REGION.to_string(),
crate::config::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
assert_eq!(client.get_config().bucket, "nuwa-packages");
}
#[test]
fn test_generate_public_download_url() {
let config = OssConfig::new(
crate::config::defaults::ENDPOINT.to_string(),
crate::config::defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
crate::config::defaults::REGION.to_string(),
crate::config::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
let url = client
.generate_public_download_url("test/file.txt")
.unwrap();
// 验证URL包含正确的路径但域名可能被替换为自定义域名
assert!(url.contains("edu/test/file.txt"));
// 由于 replace_oss_domain 可能替换域名,我们只验证路径部分
// 公开URL不应该包含签名参数
assert!(!url.contains("Expires="));
assert!(!url.contains("Signature="));
}
#[test]
fn test_generate_public_access_url() {
let config = OssConfig::new(
crate::config::defaults::ENDPOINT.to_string(),
crate::config::defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
crate::config::defaults::REGION.to_string(),
crate::config::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
let url = client.generate_public_access_url("test/image.jpg").unwrap();
// 验证URL包含正确的路径但域名可能被替换为自定义域名
assert!(url.contains("edu/test/image.jpg"));
// 由于 replace_oss_domain 可能替换域名,我们只验证路径部分
// 公开URL不应该包含签名参数
assert!(!url.contains("Expires="));
assert!(!url.contains("Signature="));
}
#[test]
fn test_generate_public_urls_batch() {
let config = OssConfig::new(
crate::config::defaults::ENDPOINT.to_string(),
crate::config::defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
crate::config::defaults::REGION.to_string(),
crate::config::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
let keys = vec!["doc1.pdf", "doc2.pdf", "image.jpg"];
let urls = client.generate_public_urls_batch(&keys).unwrap();
assert_eq!(urls.len(), 3);
for (key, url) in urls {
// 验证URL包含正确的路径但域名可能被替换为自定义域名
assert!(url.contains(&format!("edu/{key}")));
// 由于 replace_oss_domain 可能替换域名,我们只验证路径部分
}
}
#[test]
fn test_get_bucket_info() {
let config = OssConfig::new(
crate::config::defaults::ENDPOINT.to_string(),
crate::config::defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
crate::config::defaults::REGION.to_string(),
crate::config::defaults::UPLOAD_DIRECTORY.to_string(),
);
let bucket = config.bucket.clone();
let endpoint = config.endpoint.clone();
let region = config.region.clone();
let client = PublicOssClient::new(config).unwrap();
let info = client.get_bucket_info();
// 验证信息包含配置的bucket和endpoint但不硬编码具体的值
assert!(info.contains(&bucket));
assert!(info.contains(&endpoint));
assert!(info.contains(&region));
}
#[test]
fn test_generate_object_key() {
let config = OssConfig::new(
crate::config::defaults::ENDPOINT.to_string(),
crate::config::defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
crate::config::defaults::REGION.to_string(),
crate::config::defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
// 测试生成带文件名的对象键
let key1 = client.generate_object_key("documents", Some("manual.pdf"));
assert!(key1.starts_with("documents/"));
assert!(key1.contains("manual"));
assert!(key1.ends_with(".pdf"));
// 测试生成不带文件名的对象键
let key2 = client.generate_object_key("images", None);
assert!(key2.starts_with("images/"));
assert!(key2.contains("file_"));
}
}

View File

@@ -0,0 +1,501 @@
//! 工具函数模块
use std::collections::HashMap;
use std::path::Path;
use tracing::debug;
/// MIME类型映射表
fn get_mime_type_map() -> HashMap<&'static str, &'static str> {
let mut map = HashMap::new();
// 图片类型
map.insert("jpg", "image/jpeg");
map.insert("jpeg", "image/jpeg");
map.insert("png", "image/png");
map.insert("gif", "image/gif");
map.insert("webp", "image/webp");
map.insert("svg", "image/svg+xml");
map.insert("bmp", "image/bmp");
map.insert("tiff", "image/tiff");
map.insert("ico", "image/x-icon");
// 文档类型
map.insert("pdf", "application/pdf");
map.insert("doc", "application/msword");
map.insert(
"docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
);
map.insert("xls", "application/vnd.ms-excel");
map.insert(
"xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
);
map.insert("ppt", "application/vnd.ms-powerpoint");
map.insert(
"pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
);
// 文本类型
map.insert("txt", "text/plain");
map.insert("html", "text/html");
map.insert("htm", "text/html");
map.insert("css", "text/css");
map.insert("js", "application/javascript");
map.insert("json", "application/json");
map.insert("xml", "application/xml");
map.insert("csv", "text/csv");
map.insert("md", "text/markdown");
// 压缩文件
map.insert("zip", "application/zip");
map.insert("rar", "application/x-rar-compressed");
map.insert("7z", "application/x-7z-compressed");
map.insert("tar", "application/x-tar");
map.insert("gz", "application/gzip");
// 音频类型
map.insert("mp3", "audio/mpeg");
map.insert("wav", "audio/wav");
map.insert("ogg", "audio/ogg");
map.insert("m4a", "audio/mp4");
map.insert("aac", "audio/aac");
map.insert("flac", "audio/flac");
// 视频类型
map.insert("mp4", "video/mp4");
map.insert("avi", "video/x-msvideo");
map.insert("mov", "video/quicktime");
map.insert("wmv", "video/x-ms-wmv");
map.insert("flv", "video/x-flv");
map.insert("webm", "video/webm");
map
}
/// 检测文件MIME类型
pub fn detect_mime_type(file_path: &str) -> String {
let path = Path::new(file_path);
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
let mime_map = get_mime_type_map();
mime_map
.get(extension.as_str())
.unwrap_or(&"application/octet-stream")
.to_string()
}
/// 根据扩展名检测MIME类型
pub fn detect_mime_type_by_extension(extension: &str) -> String {
let ext = extension.trim_start_matches('.').to_lowercase();
let mime_map = get_mime_type_map();
mime_map
.get(ext.as_str())
.unwrap_or(&"application/octet-stream")
.to_string()
}
/// 判断是否为图片文件
pub fn is_image_file(file_path: &str) -> bool {
let mime_type = detect_mime_type(file_path);
mime_type.starts_with("image/")
}
/// 判断是否为文档文件
pub fn is_document_file(file_path: &str) -> bool {
let mime_type = detect_mime_type(file_path);
mime_type.starts_with("application/")
&& (mime_type.contains("pdf")
|| mime_type.contains("word")
|| mime_type.contains("excel")
|| mime_type.contains("powerpoint")
|| mime_type.contains("document"))
}
/// 判断是否为音频文件
pub fn is_audio_file(file_path: &str) -> bool {
let mime_type = detect_mime_type(file_path);
mime_type.starts_with("audio/")
}
/// 判断是否为视频文件
pub fn is_video_file(file_path: &str) -> bool {
let mime_type = detect_mime_type(file_path);
mime_type.starts_with("video/")
}
/// 清理文件名,移除特殊字符
pub fn sanitize_filename(filename: &str) -> String {
filename
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '.' || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect()
}
/// 格式化文件大小
pub fn format_file_size(size: u64) -> String {
let size = size as f64;
if size < 1024.0 {
format!("{size} B")
} else if size < 1024.0 * 1024.0 {
format!("{:.2} KB", size / 1024.0)
} else if size < 1024.0 * 1024.0 * 1024.0 {
format!("{:.2} MB", size / (1024.0 * 1024.0))
} else if size < 1024.0 * 1024.0 * 1024.0 * 1024.0 {
format!("{:.2} GB", size / (1024.0 * 1024.0 * 1024.0))
} else {
format!("{:.2} TB", size / (1024.0 * 1024.0 * 1024.0 * 1024.0))
}
}
/// 解析文件大小字符串(如"100MB")为字节数
pub fn parse_file_size(size_str: &str) -> Result<u64, String> {
let size_str = size_str.trim().to_uppercase();
if size_str.is_empty() {
return Err("文件大小字符串不能为空".to_string());
}
// 提取数字部分和单位部分
let (number_part, unit_part) = if size_str.ends_with("TB") {
(&size_str[..size_str.len() - 2], "TB")
} else if size_str.ends_with("GB") {
(&size_str[..size_str.len() - 2], "GB")
} else if size_str.ends_with("MB") {
(&size_str[..size_str.len() - 2], "MB")
} else if size_str.ends_with("KB") {
(&size_str[..size_str.len() - 2], "KB")
} else if size_str.ends_with("B") {
(&size_str[..size_str.len() - 1], "B")
} else {
// 没有单位,默认为字节
(size_str.as_str(), "B")
};
let number: f64 = number_part
.parse()
.map_err(|_| format!("无效的数字: {number_part}"))?;
if number < 0.0 {
return Err("文件大小不能为负数".to_string());
}
let bytes = match unit_part {
"B" => number,
"KB" => number * 1024.0,
"MB" => number * 1024.0 * 1024.0,
"GB" => number * 1024.0 * 1024.0 * 1024.0,
"TB" => number * 1024.0 * 1024.0 * 1024.0 * 1024.0,
_ => return Err(format!("不支持的单位: {unit_part}")),
};
Ok(bytes as u64)
}
/// 生成随机文件名
pub fn generate_random_filename(extension: Option<&str>) -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
let uid = uuid::Uuid::new_v4().to_string()[..8].to_string();
match extension {
Some(ext) => {
let clean_ext = ext.trim_start_matches('.');
format!("{timestamp}_{uid}.{clean_ext}")
}
None => format!("{timestamp}_{uid}"),
}
}
/// 提取文件扩展名
pub fn get_file_extension(file_path: &str) -> Option<String> {
Path::new(file_path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
}
/// 获取文件名(不包含路径)
pub fn get_filename(file_path: &str) -> Option<String> {
Path::new(file_path)
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
}
/// 获取文件名(不包含扩展名)
pub fn get_filename_without_extension(file_path: &str) -> Option<String> {
Path::new(file_path)
.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| stem.to_string())
}
/// 替换OSS域名前缀解决跨域问题
///
/// 将阿里云OSS的域名替换为自定义域名避免跨域问题
///
/// # 参数
/// * `url` - 原始OSS URL
///
/// # 返回值
/// * 替换后的URL如果没有匹配的域名则返回原URL
///
/// # 示例
/// ```
/// use oss_client::utils::replace_oss_domain;
///
/// let original_url = "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image.jpg";
/// let replaced_url = replace_oss_domain(original_url);
/// assert_eq!(replaced_url, "https://statics-ali.nuwax.com/image.jpg");
/// ```
pub fn replace_oss_domain(url: &str) -> String {
//把固定的公网 bucket 替换为自定义域名
const OLD_DOMAIN: &str = "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com";
const NEW_DOMAIN: &str = "https://statics-ali.nuwax.com";
debug!("Replace OSS domain name: {}", url);
let new_url = if url.starts_with(OLD_DOMAIN) {
url.replacen(OLD_DOMAIN, NEW_DOMAIN, 1)
} else {
url.to_string()
};
debug!("Replaced OSS domain name: {}", new_url);
new_url
}
/// 批量替换OSS域名前缀
///
/// 对多个URL进行域名替换
///
/// # 参数
/// * `urls` - URL列表的引用
///
/// # 返回值
/// * 替换后的URL列表
///
/// # 示例
/// ```
/// use oss_client::utils::replace_oss_domains_batch;
///
/// let urls = vec![
/// "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image1.jpg".to_string(),
/// "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image2.jpg".to_string(),
/// ];
/// let replaced_urls = replace_oss_domains_batch(&urls);
/// assert_eq!(replaced_urls[0], "https://statics-ali.nuwax.com/image1.jpg");
/// assert_eq!(replaced_urls[1], "https://statics-ali.nuwax.com/image2.jpg");
/// ```
pub fn replace_oss_domains_batch(urls: &[String]) -> Vec<String> {
urls.iter().map(|url| replace_oss_domain(url)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_mime_type() {
assert_eq!(detect_mime_type("test.jpg"), "image/jpeg");
assert_eq!(detect_mime_type("test.png"), "image/png");
assert_eq!(detect_mime_type("test.pdf"), "application/pdf");
assert_eq!(
detect_mime_type("test.docx"),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
assert_eq!(detect_mime_type("test.mp3"), "audio/mpeg");
assert_eq!(detect_mime_type("test.mp4"), "video/mp4");
assert_eq!(detect_mime_type("test.unknown"), "application/octet-stream");
assert_eq!(detect_mime_type("test"), "application/octet-stream");
}
#[test]
fn test_detect_mime_type_by_extension() {
assert_eq!(detect_mime_type_by_extension("jpg"), "image/jpeg");
assert_eq!(detect_mime_type_by_extension(".png"), "image/png");
assert_eq!(detect_mime_type_by_extension("PDF"), "application/pdf");
assert_eq!(
detect_mime_type_by_extension("unknown"),
"application/octet-stream"
);
}
#[test]
fn test_file_type_detection() {
assert!(is_image_file("test.jpg"));
assert!(is_image_file("test.png"));
assert!(!is_image_file("test.pdf"));
assert!(is_document_file("test.pdf"));
assert!(is_document_file("test.docx"));
assert!(!is_document_file("test.jpg"));
assert!(is_audio_file("test.mp3"));
assert!(is_audio_file("test.wav"));
assert!(!is_audio_file("test.jpg"));
assert!(is_video_file("test.mp4"));
assert!(is_video_file("test.avi"));
assert!(!is_video_file("test.jpg"));
}
#[test]
fn test_sanitize_filename() {
assert_eq!(sanitize_filename("test file.txt"), "test_file.txt");
assert_eq!(sanitize_filename("test@#$%file.txt"), "test____file.txt");
assert_eq!(
sanitize_filename("normal-file_name.txt"),
"normal-file_name.txt"
);
// 中文字符实际上会被保留因为它们通过了is_alphanumeric()检查
let result = sanitize_filename("中文文件名.txt");
assert!(result.contains(".txt"));
assert!(result.contains("中文文件名"));
// 特殊字符会被替换为下划线
assert_eq!(sanitize_filename("file@name.txt"), "file_name.txt");
assert_eq!(
sanitize_filename("file name with spaces.txt"),
"file_name_with_spaces.txt"
);
}
#[test]
fn test_format_file_size() {
assert_eq!(format_file_size(0), "0 B");
assert_eq!(format_file_size(512), "512 B");
assert_eq!(format_file_size(1024), "1.00 KB");
assert_eq!(format_file_size(1536), "1.50 KB");
assert_eq!(format_file_size(1024 * 1024), "1.00 MB");
assert_eq!(format_file_size(1024 * 1024 * 1024), "1.00 GB");
assert_eq!(format_file_size(1024_u64.pow(4)), "1.00 TB");
}
#[test]
fn test_parse_file_size() {
assert_eq!(parse_file_size("100").unwrap(), 100);
assert_eq!(parse_file_size("100B").unwrap(), 100);
assert_eq!(parse_file_size("1KB").unwrap(), 1024);
assert_eq!(parse_file_size("1MB").unwrap(), 1024 * 1024);
assert_eq!(parse_file_size("1GB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_file_size("1TB").unwrap(), 1024_u64.pow(4));
assert_eq!(
parse_file_size("1.5MB").unwrap(),
(1.5 * 1024.0 * 1024.0) as u64
);
assert_eq!(parse_file_size("100mb").unwrap(), 100 * 1024 * 1024);
assert!(parse_file_size("").is_err());
assert!(parse_file_size("abc").is_err());
assert!(parse_file_size("-100MB").is_err());
}
#[test]
fn test_generate_random_filename() {
let filename1 = generate_random_filename(Some("txt"));
let filename2 = generate_random_filename(Some(".jpg"));
let filename3 = generate_random_filename(None);
assert!(filename1.ends_with(".txt"));
assert!(filename2.ends_with(".jpg"));
assert!(!filename3.contains("."));
// 确保生成的文件名不同
assert_ne!(filename1, filename2);
}
#[test]
fn test_file_path_utilities() {
assert_eq!(get_file_extension("test.txt"), Some("txt".to_string()));
assert_eq!(get_file_extension("test.TAR.GZ"), Some("gz".to_string()));
assert_eq!(get_file_extension("test"), None);
assert_eq!(
get_filename("/path/to/test.txt"),
Some("test.txt".to_string())
);
assert_eq!(get_filename("test.txt"), Some("test.txt".to_string()));
assert_eq!(
get_filename_without_extension("/path/to/test.txt"),
Some("test".to_string())
);
assert_eq!(
get_filename_without_extension("test"),
Some("test".to_string())
);
}
#[test]
fn test_replace_oss_domain() {
// 测试正常替换
let original_url = "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image.jpg";
let replaced_url = replace_oss_domain(original_url);
assert_eq!(replaced_url, "https://statics-ali.nuwax.com/image.jpg");
// 测试带路径的URL
let original_url_with_path =
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/folder/subfolder/image.png";
let replaced_url_with_path = replace_oss_domain(original_url_with_path);
assert_eq!(
replaced_url_with_path,
"https://statics-ali.nuwax.com/folder/subfolder/image.png"
);
// 测试带查询参数的URL
let original_url_with_query = "https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image.jpg?version=1.0&size=large";
let replaced_url_with_query = replace_oss_domain(original_url_with_query);
assert_eq!(
replaced_url_with_query,
"https://statics-ali.nuwax.com/image.jpg?version=1.0&size=large"
);
// 测试不匹配的域名
let other_url = "https://other-domain.com/image.jpg";
let unchanged_url = replace_oss_domain(other_url);
assert_eq!(unchanged_url, other_url);
// 测试空字符串
let empty_url = "";
let unchanged_empty = replace_oss_domain(empty_url);
assert_eq!(unchanged_empty, "");
}
#[test]
fn test_replace_oss_domains_batch() {
let urls = vec![
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image1.jpg".to_string(),
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/image2.jpg".to_string(),
"https://other-domain.com/image3.jpg".to_string(),
"https://nuwa-packages.oss-rg-china-mainland.aliyuncs.com/folder/image4.png"
.to_string(),
];
let replaced_urls = replace_oss_domains_batch(&urls);
assert_eq!(replaced_urls[0], "https://statics-ali.nuwax.com/image1.jpg");
assert_eq!(replaced_urls[1], "https://statics-ali.nuwax.com/image2.jpg");
assert_eq!(replaced_urls[2], "https://other-domain.com/image3.jpg"); // 不匹配的域名保持不变
assert_eq!(
replaced_urls[3],
"https://statics-ali.nuwax.com/folder/image4.png"
);
// 测试空列表
let empty_urls: Vec<String> = vec![];
let replaced_empty = replace_oss_domains_batch(&empty_urls);
assert_eq!(replaced_empty.len(), 0);
}
}

View File

@@ -0,0 +1,302 @@
//! 集成测试
//!
//! 注意这些测试需要有效的OSS凭证才能运行
//! 设置环境变量 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET 来运行实际的OSS操作测试
use oss_client::{
OssClientTrait, OssConfig, OssError, PrivateOssClient, PublicOssClient, defaults,
};
use std::time::Duration;
fn create_private_client_from_env() -> Result<PrivateOssClient, OssError> {
let access_key_id = std::env::var("OSS_ACCESS_KEY_ID").unwrap_or_default();
let access_key_secret = std::env::var("OSS_ACCESS_KEY_SECRET").unwrap_or_default();
if access_key_id.is_empty() || access_key_secret.is_empty() {
return Err(OssError::config("缺少OSS凭证环境变量"));
}
let bucket =
std::env::var("OSS_BUCKET").unwrap_or_else(|_| defaults::PRIVATE_BUCKET.to_string());
let endpoint = std::env::var("OSS_ENDPOINT").unwrap_or_else(|_| defaults::ENDPOINT.to_string());
let region = std::env::var("OSS_REGION").unwrap_or_else(|_| defaults::REGION.to_string());
let upload_directory =
std::env::var("OSS_UPLOAD_DIR").unwrap_or_else(|_| defaults::UPLOAD_DIRECTORY.to_string());
let config = OssConfig::new(
endpoint,
bucket,
access_key_id,
access_key_secret,
region,
upload_directory,
);
PrivateOssClient::new(config)
}
fn create_public_client_from_env() -> Result<PublicOssClient, OssError> {
let access_key_id = std::env::var("OSS_ACCESS_KEY_ID").unwrap_or_default();
let access_key_secret = std::env::var("OSS_ACCESS_KEY_SECRET").unwrap_or_default();
if access_key_id.is_empty() || access_key_secret.is_empty() {
return Err(OssError::config("缺少OSS凭证环境变量"));
}
let bucket =
std::env::var("OSS_PUBLIC_BUCKET").unwrap_or_else(|_| defaults::PUBLIC_BUCKET.to_string());
let endpoint = std::env::var("OSS_ENDPOINT").unwrap_or_else(|_| defaults::ENDPOINT.to_string());
let region = std::env::var("OSS_REGION").unwrap_or_else(|_| defaults::REGION.to_string());
let upload_directory =
std::env::var("OSS_UPLOAD_DIR").unwrap_or_else(|_| defaults::UPLOAD_DIRECTORY.to_string());
let config = OssConfig::new(
endpoint,
bucket,
access_key_id,
access_key_secret,
region,
upload_directory,
);
PublicOssClient::new(config)
}
/// 测试客户端创建
#[test]
fn test_client_creation() {
// 测试从环境变量创建私有客户端
match create_private_client_from_env() {
Ok(client) => {
let config = client.get_config();
assert!(!config.access_key_id.is_empty());
assert!(!config.access_key_secret.is_empty());
assert!(!config.endpoint.is_empty());
assert!(!config.bucket.is_empty());
}
Err(e) => {
assert!(e.is_config_error());
}
}
// 测试从环境变量创建公有客户端
match create_public_client_from_env() {
Ok(client) => {
let config = client.get_config();
assert!(!config.access_key_id.is_empty());
assert!(!config.access_key_secret.is_empty());
assert!(!config.endpoint.is_empty());
assert!(!config.bucket.is_empty());
}
Err(e) => {
assert!(e.is_config_error());
}
}
// 测试使用自定义配置创建公有客户端
let config = OssConfig::new(
defaults::ENDPOINT.to_string(),
defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
defaults::REGION.to_string(),
defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
assert_eq!(client.get_config().access_key_id, "test_key_id");
assert_eq!(client.get_config().access_key_secret, "test_key_secret");
}
/// 测试配置验证
#[test]
fn test_config_validation() {
// 测试空的access_key_id
let config = OssConfig::new(
defaults::ENDPOINT.to_string(),
defaults::PUBLIC_BUCKET.to_string(),
"".to_string(),
"secret".to_string(),
defaults::REGION.to_string(),
defaults::UPLOAD_DIRECTORY.to_string(),
);
let result = PublicOssClient::new(config);
assert!(result.is_err());
assert!(result.unwrap_err().is_config_error());
// 测试空的access_key_secret
let config = OssConfig::new(
defaults::ENDPOINT.to_string(),
defaults::PUBLIC_BUCKET.to_string(),
"key_id".to_string(),
"".to_string(),
defaults::REGION.to_string(),
defaults::UPLOAD_DIRECTORY.to_string(),
);
let result = PublicOssClient::new(config);
assert!(result.is_err());
assert!(result.unwrap_err().is_config_error());
}
/// 测试签名URL生成不需要实际OSS连接
#[test]
fn test_signed_url_generation() {
// 私有客户端:签名上传/下载链接
let private_config = OssConfig::new(
defaults::ENDPOINT.to_string(),
defaults::PRIVATE_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
defaults::REGION.to_string(),
defaults::UPLOAD_DIRECTORY.to_string(),
);
let private_client = PrivateOssClient::new(private_config).unwrap();
let upload_url = private_client.generate_upload_url(
"test/file.txt",
Duration::from_secs(3600),
Some("text/plain"),
);
assert!(upload_url.is_ok());
let url = upload_url.unwrap();
assert!(
url.contains("nuwa-packages.oss-rg-china-mainland.aliyuncs.com")
|| url.contains("edu-nuwa-packages.oss-rg-china-mainland.aliyuncs.com")
);
assert!(url.contains("edu/test/file.txt"));
assert!(url.contains("Expires=") || url.contains("x-oss-expires"));
assert!(url.contains("Signature=") || url.contains("x-oss-signature"));
let download_url =
private_client.generate_download_url("test/file.txt", Some(Duration::from_secs(3600)));
assert!(download_url.is_ok());
let url = download_url.unwrap();
assert!(url.contains("edu/test/file.txt"));
assert!(url.contains("Expires=") || url.contains("x-oss-expires"));
assert!(url.contains("Signature=") || url.contains("x-oss-signature"));
// 公有客户端:下载链接不应包含签名
let public_config = OssConfig::new(
defaults::ENDPOINT.to_string(),
defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
defaults::REGION.to_string(),
defaults::UPLOAD_DIRECTORY.to_string(),
);
let public_client = PublicOssClient::new(public_config.clone()).unwrap();
let public_download_url = public_client.generate_download_url("test/file.txt", None);
let url = public_download_url.unwrap();
// 验证URL包含正确的路径但域名可能被替换为自定义域名
assert!(url.contains("edu/test/file.txt"));
// 由于 replace_oss_domain 可能替换域名,我们只验证路径部分
assert!(!url.contains("Expires="));
assert!(!url.contains("Signature="));
}
/// 测试object key生成
#[test]
fn test_object_key_generation() {
let config = OssConfig::new(
defaults::ENDPOINT.to_string(),
defaults::PUBLIC_BUCKET.to_string(),
"test_key_id".to_string(),
"test_key_secret".to_string(),
defaults::REGION.to_string(),
defaults::UPLOAD_DIRECTORY.to_string(),
);
let client = PublicOssClient::new(config).unwrap();
// 测试带文件名的object key生成
let key1 = client.generate_object_key("uploads", Some("document.pdf"));
assert!(key1.starts_with("uploads/"));
assert!(key1.contains("document"));
assert!(key1.ends_with(".pdf"));
// 测试不带文件名的object key生成
let key2 = client.generate_object_key("temp", None);
assert!(key2.starts_with("temp/"));
assert!(key2.contains("file_"));
// 确保生成的key是唯一的
let key3 = client.generate_object_key("uploads", Some("document.pdf"));
assert_ne!(key1, key3);
}
/// 测试错误处理
#[test]
fn test_error_handling() {
// 测试配置错误
let config_err = OssError::config("test config error");
assert!(config_err.is_config_error());
assert!(!config_err.is_network_error());
// 测试网络错误
let network_err = OssError::network("test network error");
assert!(network_err.is_network_error());
assert!(!network_err.is_config_error());
// 测试文件不存在错误
let file_err = OssError::file_not_found("test.txt");
assert!(file_err.is_file_not_found());
// 测试权限错误
let perm_err = OssError::permission("access denied");
assert!(perm_err.is_permission_error());
}
// 以下测试需要有效的OSS凭证只有在设置了环境变量时才会运行
#[tokio::test]
async fn test_actual_oss_operations() -> oss_client::Result<()> {
// 只有在设置了环境变量时才运行
let client = match create_private_client_from_env() {
Ok(client) => client,
Err(_) => {
println!("Skip actual OSS operation test: environment variables not set");
return Ok(());
}
};
println!("Run actual OSS operation test...");
// 测试文件存在性检查(对一个不存在的文件)
let test_key = format!("test/non-existent-{}.txt", chrono::Utc::now().timestamp());
match client.file_exists(&test_key).await {
Ok(exists) => {
assert!(!exists, "不存在的文件应该返回false");
println!("✓ File existence check test passed");
}
Err(e) => {
println!("File existence check failed: {e}");
}
}
// 测试上传小文件
let test_content = b"Hello, OSS!";
let test_key = format!(
"test/integration-test-{}.txt",
chrono::Utc::now().timestamp()
);
match client
.upload_content(test_content, &test_key, Some("text/plain"))
.await
{
Ok(url) => {
println!("✓ File uploaded successfully: {url}");
// 测试文件存在性
match client.file_exists(&test_key).await {
Ok(exists) => {
assert!(exists, "上传的文件应该存在");
println!("✓ The file existence check passes after uploading");
}
Err(e) => println!("File existence check failed: {e}"),
}
// 清理测试文件
match client.delete_file(&test_key).await {
Ok(_) => println!("✓ Test file cleanup successful"),
Err(e) => println!("Test file cleanup failed: {e}"),
}
}
Err(e) => {
println!("File upload failed: {e}");
println!("This may be caused by invalid OSS credentials or insufficient permissions");
}
}
Ok(())
}