提交qiming-mcp-proxy
This commit is contained in:
58
qiming-mcp-proxy/mcp-common/Cargo.toml
Normal file
58
qiming-mcp-proxy/mcp-common/Cargo.toml
Normal file
@@ -0,0 +1,58 @@
|
||||
[package]
|
||||
name = "mcp-common"
|
||||
version = "0.1.28"
|
||||
edition = "2024"
|
||||
authors = ["nuwax-ai"]
|
||||
description = "Common types and utilities shared across MCP proxy components"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/nuwax-ai/mcp-proxy"
|
||||
keywords = ["mcp", "proxy", "protocol"]
|
||||
categories = ["web-programming", "network-programming"]
|
||||
|
||||
[dependencies]
|
||||
# 基础依赖
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# 日志
|
||||
tracing = { workspace = true }
|
||||
|
||||
# 异步
|
||||
async-trait = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
# 并发
|
||||
arc-swap = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
|
||||
# 错误处理
|
||||
anyhow = { workspace = true }
|
||||
|
||||
# 国际化支持
|
||||
rust-i18n = { workspace = true }
|
||||
|
||||
# OpenTelemetry 核心(条件依赖)
|
||||
opentelemetry = { workspace = true, optional = true }
|
||||
opentelemetry_sdk = { workspace = true, optional = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
|
||||
# OTLP exporter(条件依赖)
|
||||
opentelemetry-otlp = { workspace = true, optional = true }
|
||||
tonic = { workspace = true, optional = true }
|
||||
|
||||
# 进程组管理(跨平台子进程清理)
|
||||
# 版本需要与 mcp-streamable-proxy 保持一致
|
||||
process-wrap = { version = "9.0.3", features = ["tokio1", "process-group", "job-object"] }
|
||||
# 查找可执行文件
|
||||
which = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_System_Threading"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# 基础 OpenTelemetry 支持
|
||||
telemetry = ["opentelemetry", "opentelemetry_sdk", "tracing-opentelemetry", "tracing-subscriber"]
|
||||
# OTLP exporter(需要 telemetry)
|
||||
otlp = ["telemetry", "opentelemetry-otlp", "tonic"]
|
||||
67
qiming-mcp-proxy/mcp-common/README.md
Normal file
67
qiming-mcp-proxy/mcp-common/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# MCP Common
|
||||
|
||||
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
|
||||
|
||||
---
|
||||
|
||||
# MCP Common
|
||||
|
||||
Shared types and utilities for MCP proxy components.
|
||||
|
||||
## Overview
|
||||
|
||||
`mcp-common` provides common functionality shared across `mcp-sse-proxy` and `mcp-streamable-proxy` to avoid code duplication.
|
||||
|
||||
## Features
|
||||
|
||||
- **Configuration Types**: `McpServiceConfig`, `McpClientConfig` for unified configuration management
|
||||
- **Tool Filtering**: `ToolFilter` for filtering MCP tools by name or pattern
|
||||
- **OpenTelemetry Support**: Optional telemetry features for distributed tracing
|
||||
|
||||
## Feature Flags
|
||||
|
||||
- `telemetry`: Basic OpenTelemetry support
|
||||
- `otlp`: OTLP exporter support (for Jaeger, etc.)
|
||||
|
||||
## Installation
|
||||
|
||||
Add to `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
mcp-common = { version = "0.1.5", path = "../mcp-common" }
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use mcp_common::{McpServiceConfig, McpClientConfig, ToolFilter};
|
||||
|
||||
// Create client configuration
|
||||
let client_config = McpClientConfig::new("http://localhost:8080/mcp")
|
||||
.with_headers(vec![("Authorization".to_string(), "Bearer token".to_string())]);
|
||||
|
||||
// Create service configuration
|
||||
let service_config = McpServiceConfig::new("my-service".to_string())
|
||||
.with_persistent_type();
|
||||
|
||||
// Use tool filter
|
||||
let filter = ToolFilter::new(vec!["tool1".to_string(), "tool2".to_string()]);
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build -p mcp-common
|
||||
|
||||
# Test
|
||||
cargo test -p mcp-common
|
||||
|
||||
# With features
|
||||
cargo build -p mcp-common --features telemetry,otlp
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
67
qiming-mcp-proxy/mcp-common/README_zh-CN.md
Normal file
67
qiming-mcp-proxy/mcp-common/README_zh-CN.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# MCP Common
|
||||
|
||||
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
|
||||
|
||||
---
|
||||
|
||||
# MCP Common
|
||||
|
||||
MCP 代理组件的共享类型和工具。
|
||||
|
||||
## 概述
|
||||
|
||||
`mcp-common` 为 `mcp-sse-proxy` 和 `mcp-streamable-proxy` 提供共享功能,避免代码重复。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **配置类型**: `McpServiceConfig`、`McpClientConfig` 用于统一配置管理
|
||||
- **工具过滤**: `ToolFilter` 用于按名称或模式过滤 MCP 工具
|
||||
- **OpenTelemetry 支持**: 可选的遥测功能用于分布式追踪
|
||||
|
||||
## 功能标志
|
||||
|
||||
- `telemetry`: 基础 OpenTelemetry 支持
|
||||
- `otlp`: OTLP 导出器支持(用于 Jaeger 等)
|
||||
|
||||
## 安装
|
||||
|
||||
添加到 `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
mcp-common = { version = "0.1.5", path = "../mcp-common" }
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```rust
|
||||
use mcp_common::{McpServiceConfig, McpClientConfig, ToolFilter};
|
||||
|
||||
// 创建客户端配置
|
||||
let client_config = McpClientConfig::new("http://localhost:8080/mcp")
|
||||
.with_headers(vec![("Authorization".to_string(), "Bearer token".to_string())]);
|
||||
|
||||
// 创建服务配置
|
||||
let service_config = McpServiceConfig::new("my-service".to_string())
|
||||
.with_persistent_type();
|
||||
|
||||
// 使用工具过滤器
|
||||
let filter = ToolFilter::new(vec!["tool1".to_string(), "tool2".to_string()]);
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
cargo build -p mcp-common
|
||||
|
||||
# 测试
|
||||
cargo test -p mcp-common
|
||||
|
||||
# 启用功能
|
||||
cargo build -p mcp-common --features telemetry,otlp
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT OR Apache-2.0
|
||||
5
qiming-mcp-proxy/mcp-common/build.rs
Normal file
5
qiming-mcp-proxy/mcp-common/build.rs
Normal 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");
|
||||
}
|
||||
468
qiming-mcp-proxy/mcp-common/locales/en.yml
Normal file
468
qiming-mcp-proxy/mcp-common/locales/en.yml
Normal 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"
|
||||
468
qiming-mcp-proxy/mcp-common/locales/zh-CN.yml
Normal file
468
qiming-mcp-proxy/mcp-common/locales/zh-CN.yml
Normal 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: "跳过"
|
||||
468
qiming-mcp-proxy/mcp-common/locales/zh-TW.yml
Normal file
468
qiming-mcp-proxy/mcp-common/locales/zh-TW.yml
Normal 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: "跳過"
|
||||
60
qiming-mcp-proxy/mcp-common/src/backend_bridge.rs
Normal file
60
qiming-mcp-proxy/mcp-common/src/backend_bridge.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Backend Bridge trait for cross-protocol MCP proxy
|
||||
//!
|
||||
//! This trait provides a protocol-agnostic interface for backend MCP connections,
|
||||
//! enabling `mcp-sse-proxy` to communicate with any backend implementation
|
||||
//! (e.g., `mcp-streamable-proxy`) without direct dependencies.
|
||||
//!
|
||||
//! All method parameters and return values use `serde_json::Value` to avoid
|
||||
//! coupling to specific rmcp version types.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Protocol-agnostic backend bridge for MCP proxy
|
||||
///
|
||||
/// Implementations of this trait wrap a concrete MCP client (e.g., rmcp 1.4.0's
|
||||
/// `ProxyHandler`) and expose its functionality through a version-independent
|
||||
/// JSON-based interface.
|
||||
///
|
||||
/// # Design
|
||||
///
|
||||
/// This trait uses `Pin<Box<dyn Future>>` for async methods instead of `async_trait`
|
||||
/// to maintain object safety (`dyn BackendBridge`) without additional macro dependencies.
|
||||
pub trait BackendBridge: Send + Sync {
|
||||
/// Get the MCP service identifier (used for logging and tracking)
|
||||
fn mcp_id(&self) -> &str;
|
||||
|
||||
/// Get the backend's ServerInfo as JSON
|
||||
///
|
||||
/// Used for cross-rmcp-version bridging: the implementation serializes its
|
||||
/// native `ServerInfo` to JSON, and the consumer deserializes it into its
|
||||
/// own rmcp version's `ServerInfo` type.
|
||||
fn get_server_info_json(&self) -> Value;
|
||||
|
||||
/// Check if the backend connection is available (fast, synchronous check)
|
||||
fn is_backend_available(&self) -> bool;
|
||||
|
||||
/// Check if the MCP server is ready (async, sends a validation request)
|
||||
fn is_mcp_server_ready(&self) -> Pin<Box<dyn Future<Output = bool> + Send + '_>>;
|
||||
|
||||
/// Check if the backend connection is terminated (async)
|
||||
fn is_terminated_async(&self) -> Pin<Box<dyn Future<Output = bool> + Send + '_>>;
|
||||
|
||||
/// Call an MCP method on the backend via JSON bridge
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `method` - MCP method name (e.g., "tools/list", "tools/call",
|
||||
/// "resources/list", "prompts/get", "completion/complete")
|
||||
/// * `params` - JSON-serialized method parameters
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Value)` - JSON-serialized result from the backend
|
||||
/// * `Err(String)` - Error description
|
||||
fn call_peer_method(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send + '_>>;
|
||||
}
|
||||
131
qiming-mcp-proxy/mcp-common/src/client_config.rs
Normal file
131
qiming-mcp-proxy/mcp-common/src/client_config.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! Client connection configuration for MCP services
|
||||
//!
|
||||
//! This module provides a unified configuration structure for connecting
|
||||
//! to MCP servers via SSE or Streamable HTTP protocols.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration for MCP client connections
|
||||
///
|
||||
/// This struct provides a protocol-agnostic way to configure connections
|
||||
/// to MCP servers. It can be used with both SSE and Streamable HTTP transports.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use mcp_common::McpClientConfig;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let config = McpClientConfig::new("http://localhost:8080/mcp")
|
||||
/// .with_header("Authorization", "Bearer token123")
|
||||
/// .with_connect_timeout(Duration::from_secs(30));
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct McpClientConfig {
|
||||
/// Target URL for the MCP server
|
||||
pub url: String,
|
||||
/// HTTP headers to include in requests
|
||||
pub headers: HashMap<String, String>,
|
||||
/// Connection timeout duration
|
||||
pub connect_timeout: Option<Duration>,
|
||||
/// Read timeout duration
|
||||
pub read_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl McpClientConfig {
|
||||
/// Create a new configuration with the given URL
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - The MCP server URL (e.g., "http://localhost:8080/mcp")
|
||||
pub fn new(url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
url: url.into(),
|
||||
headers: HashMap::new(),
|
||||
connect_timeout: None,
|
||||
read_timeout: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a header to the configuration
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - Header name
|
||||
/// * `value` - Header value
|
||||
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.headers.insert(key.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple headers from a HashMap
|
||||
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
|
||||
self.headers.extend(headers);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Authorization header with a Bearer token
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `token` - The bearer token (without "Bearer " prefix)
|
||||
pub fn with_bearer_auth(self, token: impl Into<String>) -> Self {
|
||||
self.with_header("Authorization", format!("Bearer {}", token.into()))
|
||||
}
|
||||
|
||||
/// Set the connection timeout
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `timeout` - Maximum time to wait for connection establishment
|
||||
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.connect_timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the read timeout
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `timeout` - Maximum time to wait for response data
|
||||
pub fn with_read_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.read_timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_config() {
|
||||
let config = McpClientConfig::new("http://localhost:8080");
|
||||
assert_eq!(config.url, "http://localhost:8080");
|
||||
assert!(config.headers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_header() {
|
||||
let config = McpClientConfig::new("http://localhost:8080").with_header("X-Custom", "value");
|
||||
assert_eq!(config.headers.get("X-Custom"), Some(&"value".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_bearer_auth() {
|
||||
let config = McpClientConfig::new("http://localhost:8080").with_bearer_auth("mytoken");
|
||||
assert_eq!(
|
||||
config.headers.get("Authorization"),
|
||||
Some(&"Bearer mytoken".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_chain() {
|
||||
let config = McpClientConfig::new("http://localhost:8080")
|
||||
.with_header("X-Api-Key", "key123")
|
||||
.with_connect_timeout(Duration::from_secs(30))
|
||||
.with_read_timeout(Duration::from_secs(60));
|
||||
|
||||
assert_eq!(config.url, "http://localhost:8080");
|
||||
assert_eq!(config.headers.get("X-Api-Key"), Some(&"key123".to_string()));
|
||||
assert_eq!(config.connect_timeout, Some(Duration::from_secs(30)));
|
||||
assert_eq!(config.read_timeout, Some(Duration::from_secs(60)));
|
||||
}
|
||||
}
|
||||
51
qiming-mcp-proxy/mcp-common/src/config.rs
Normal file
51
qiming-mcp-proxy/mcp-common/src/config.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! MCP 服务配置
|
||||
|
||||
use crate::ToolFilter;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// MCP 服务配置
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct McpServiceConfig {
|
||||
/// 服务名称
|
||||
pub name: String,
|
||||
/// 启动命令
|
||||
pub command: String,
|
||||
/// 命令参数
|
||||
pub args: Option<Vec<String>>,
|
||||
/// 环境变量(来自 MCP JSON 配置)
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// 工具过滤配置
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_filter: Option<ToolFilter>,
|
||||
}
|
||||
|
||||
impl McpServiceConfig {
|
||||
/// 创建新配置
|
||||
pub fn new(name: String, command: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
command,
|
||||
args: None,
|
||||
env: None,
|
||||
tool_filter: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置参数
|
||||
pub fn with_args(mut self, args: Vec<String>) -> Self {
|
||||
self.args = Some(args);
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置环境变量
|
||||
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
|
||||
self.env = Some(env);
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置工具过滤器
|
||||
pub fn with_tool_filter(mut self, filter: ToolFilter) -> Self {
|
||||
self.tool_filter = Some(filter);
|
||||
self
|
||||
}
|
||||
}
|
||||
143
qiming-mcp-proxy/mcp-common/src/diagnostic.rs
Normal file
143
qiming-mcp-proxy/mcp-common/src/diagnostic.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! 子进程启动诊断工具
|
||||
//!
|
||||
//! 提供 stdio 子进程启动时的环境诊断日志,供 mcp-proxy / mcp-sse-proxy /
|
||||
//! mcp-streamable-proxy 共用,避免诊断代码散落在业务逻辑中。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// PATH 分隔符
|
||||
const PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
|
||||
|
||||
/// 诊断时检查的镜像相关环境变量
|
||||
const MIRROR_ENV_KEYS: &[&str] = &[
|
||||
"npm_config_registry",
|
||||
"UV_INDEX_URL",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_INSECURE_HOST",
|
||||
"PIP_INDEX_URL",
|
||||
];
|
||||
|
||||
// ─── 纯数据格式化(不依赖日志框架) ───
|
||||
|
||||
/// 返回 PATH 摘要字符串,只展示前 `max_segments` 段
|
||||
///
|
||||
/// 示例: `/usr/bin:/usr/local/bin ... (12 entries total)`
|
||||
pub fn format_path_summary(max_segments: usize) -> String {
|
||||
match std::env::var("PATH") {
|
||||
Ok(path) => {
|
||||
let segments: Vec<&str> = path.split(PATH_SEP).collect();
|
||||
let preview: String = segments
|
||||
.iter()
|
||||
.take(max_segments)
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
.join(&PATH_SEP.to_string());
|
||||
if segments.len() > max_segments {
|
||||
format!("{} ... ({} entries total)", preview, segments.len())
|
||||
} else {
|
||||
preview
|
||||
}
|
||||
}
|
||||
Err(_) => "(unset)".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 收集当前进程中已设置的镜像相关环境变量
|
||||
///
|
||||
/// 返回 `Vec<(key, value)>`,仅包含已设置的条目。
|
||||
pub fn collect_mirror_env_vars() -> Vec<(&'static str, String)> {
|
||||
MIRROR_ENV_KEYS
|
||||
.iter()
|
||||
.filter_map(|&key| std::env::var(key).ok().map(|val| (key, val)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 构造 spawn 失败时的完整错误信息
|
||||
pub fn format_spawn_error(
|
||||
mcp_id: &str,
|
||||
command: &str,
|
||||
args: &Option<Vec<String>>,
|
||||
inner: impl std::fmt::Display,
|
||||
) -> String {
|
||||
let path_val = std::env::var("PATH").unwrap_or_else(|_| "(unset)".to_string());
|
||||
format!(
|
||||
"Failed to spawn child process - MCP ID: {}, command: {}, \
|
||||
args: {:?}, PATH: {}, error: {}",
|
||||
mcp_id,
|
||||
command,
|
||||
args.as_ref().unwrap_or(&Vec::new()),
|
||||
path_val,
|
||||
inner
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 带 tracing 的便捷函数 ───
|
||||
|
||||
/// 输出 stdio 子进程启动前的诊断日志(debug 级别)
|
||||
///
|
||||
/// 包含:PATH 摘要、镜像变量、config env keys 列表。
|
||||
/// 业务代码只需在 spawn 前调用此函数。
|
||||
pub fn log_stdio_spawn_context(tag: &str, mcp_id: &str, env: &Option<HashMap<String, String>>) {
|
||||
tracing::debug!(
|
||||
"[{}] MCP ID: {}, PATH: {}",
|
||||
tag,
|
||||
mcp_id,
|
||||
format_path_summary(3),
|
||||
);
|
||||
|
||||
for (key, val) in collect_mirror_env_vars() {
|
||||
tracing::debug!("[{}] MCP ID: {}, {}={}", tag, mcp_id, key, val);
|
||||
}
|
||||
|
||||
if let Some(env_vars) = env {
|
||||
let keys: Vec<&String> = env_vars.keys().collect();
|
||||
tracing::debug!("[{}] MCP ID: {}, config env keys: {:?}", tag, mcp_id, keys);
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动阶段环境变量汇总(eprintln 输出,日志框架尚未初始化时使用)
|
||||
pub fn eprint_env_summary() {
|
||||
eprintln!(" - PATH: {}", format_path_summary(3));
|
||||
|
||||
for (key, val) in collect_mirror_env_vars() {
|
||||
eprintln!(" - {}={}", key, val);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_path_summary_not_empty() {
|
||||
let summary = format_path_summary(3);
|
||||
assert!(!summary.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_mirror_env_vars() {
|
||||
// 不应 panic
|
||||
let _ = collect_mirror_env_vars();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_spawn_error() {
|
||||
let msg = format_spawn_error(
|
||||
"test-id",
|
||||
"npx",
|
||||
&Some(vec!["-y".into(), "server".into()]),
|
||||
"file not found",
|
||||
);
|
||||
assert!(msg.contains("test-id"));
|
||||
assert!(msg.contains("npx"));
|
||||
assert!(msg.contains("file not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_stdio_spawn_context_no_panic() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("FOO".to_string(), "bar".to_string());
|
||||
log_stdio_spawn_context("Test", "test-id", &Some(env));
|
||||
log_stdio_spawn_context("Test", "test-id", &None);
|
||||
}
|
||||
}
|
||||
391
qiming-mcp-proxy/mcp-common/src/i18n.rs
Normal file
391
qiming-mcp-proxy/mcp-common/src/i18n.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
//! 国际化模块
|
||||
//!
|
||||
//! 使用 rust-i18n 提供多语言支持,支持中文简体、中文繁体、英文三种语言。
|
||||
//!
|
||||
//! # 使用方法
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use mcp_common::{t, set_locale, init_locale_from_env};
|
||||
//!
|
||||
//! // 初始化语言设置(通常在程序启动时调用)
|
||||
//! init_locale_from_env();
|
||||
//!
|
||||
//! // 获取翻译
|
||||
//! let msg = t!("errors.mcp_proxy.service_not_found", service = "my-service");
|
||||
//!
|
||||
//! // 手动设置语言
|
||||
//! set_locale("zh-CN");
|
||||
//! ```
|
||||
//!
|
||||
//! # 支持的语言
|
||||
//!
|
||||
//! - `en` - English
|
||||
//! - `zh-CN` - 中文简体
|
||||
//! - `zh-TW` - 中文繁体
|
||||
//!
|
||||
//! # 配置优先级
|
||||
//!
|
||||
//! 1. `DEFAULT_LOCALE` 环境变量(最高优先级)
|
||||
//! 2. `LANG` 系统环境变量
|
||||
//! 3. 默认使用英文
|
||||
//!
|
||||
//! # 线程安全
|
||||
//!
|
||||
//! `set_locale()` 和 `init_locale_from_env()` 应在程序启动时调用。
|
||||
//! 语言设置是全局状态,不建议在运行时多线程环境中修改。
|
||||
|
||||
// 注意: rust-i18n 的 i18n! 宏需要在 lib.rs 中调用
|
||||
|
||||
/// 导出 t! 宏,用于获取翻译
|
||||
pub use rust_i18n::t;
|
||||
|
||||
/// 设置当前语言
|
||||
///
|
||||
/// # 线程安全
|
||||
///
|
||||
/// 此函数应在程序启动时调用,不建议在运行时多线程环境中调用。
|
||||
/// 语言设置是全局状态,并发调用可能导致不一致的翻译结果。
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```rust
|
||||
/// use mcp_common::set_locale;
|
||||
///
|
||||
/// set_locale("zh-CN");
|
||||
/// set_locale("en");
|
||||
/// set_locale("zh-TW");
|
||||
/// ```
|
||||
pub fn set_locale(locale: &str) {
|
||||
rust_i18n::set_locale(locale);
|
||||
}
|
||||
|
||||
/// 获取当前语言设置
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```rust
|
||||
/// use mcp_common::current_locale;
|
||||
///
|
||||
/// let locale = current_locale();
|
||||
/// println!("Current locale: {}", locale);
|
||||
/// ```
|
||||
pub fn current_locale() -> String {
|
||||
rust_i18n::locale().to_string()
|
||||
}
|
||||
|
||||
/// 支持的语言列表
|
||||
pub const AVAILABLE_LOCALES: &[&str] = &["en", "zh-CN", "zh-TW"];
|
||||
|
||||
/// 默认语言
|
||||
pub const DEFAULT_LOCALE: &str = "en";
|
||||
|
||||
/// 从环境变量初始化语言设置
|
||||
///
|
||||
/// 按照以下优先级设置语言:
|
||||
/// 1. `DEFAULT_LOCALE` 环境变量(最高优先级)
|
||||
/// 2. `LANG` 系统环境变量(自动解析语言代码)
|
||||
/// 3. 默认使用英文
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```rust
|
||||
/// use mcp_common::init_locale_from_env;
|
||||
///
|
||||
/// // 在程序启动时调用
|
||||
/// init_locale_from_env();
|
||||
/// ```
|
||||
pub fn init_locale_from_env() {
|
||||
// 优先使用 DEFAULT_LOCALE 环境变量(最高优先级)
|
||||
if let Ok(lang) = std::env::var("DEFAULT_LOCALE") {
|
||||
let locale = normalize_locale(&lang);
|
||||
if AVAILABLE_LOCALES.contains(&locale.as_str()) {
|
||||
set_locale(&locale);
|
||||
return;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Invalid locale '{}' from DEFAULT_LOCALE, falling back. Supported: {:?}",
|
||||
locale,
|
||||
AVAILABLE_LOCALES
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 其次尝试从 LANG 环境变量解析
|
||||
if let Ok(lang) = std::env::var("LANG") {
|
||||
let locale = parse_lang_env(&lang);
|
||||
if AVAILABLE_LOCALES.contains(&locale.as_str()) {
|
||||
set_locale(&locale);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用默认语言
|
||||
set_locale(DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
/// 标准化语言代码
|
||||
///
|
||||
/// 支持的输入格式:
|
||||
/// - `en`, `EN`, `En`, `en_US`, `en_US.UTF-8` -> `en`
|
||||
/// - `zh-CN`, `zh-cn`, `ZH-CN` -> `zh-CN`
|
||||
/// - `zh_TW`, `zh-TW` -> `zh-TW`
|
||||
/// - `zh`, `ZH` -> `zh-CN` (默认简体中文)
|
||||
fn normalize_locale(input: &str) -> String {
|
||||
let input = input.trim();
|
||||
// 支持解析带编码/修饰符的值(例如 en_US.UTF-8、zh_CN@cjk)
|
||||
let input = input.split('.').next().unwrap_or(input);
|
||||
let input = input.split('@').next().unwrap_or(input);
|
||||
|
||||
// 直接匹配
|
||||
match input.to_lowercase().as_str() {
|
||||
"en" | "en_us" | "en-us" | "en_gb" | "en-gb" => return "en".to_string(),
|
||||
"zh-cn" | "zh_cn" | "zh-hans" => return "zh-CN".to_string(),
|
||||
"zh-tw" | "zh_tw" | "zh-hant" => return "zh-TW".to_string(),
|
||||
"zh" => return "zh-CN".to_string(), // 默认简体中文
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 尝试解析语言-地区格式
|
||||
let parts: Vec<&str> = input.split(|c| c == '-' || c == '_').collect();
|
||||
if parts.len() >= 2 {
|
||||
let lang = parts[0].to_lowercase();
|
||||
let region = parts[1].to_uppercase();
|
||||
// 英文变体统一映射到 en
|
||||
if lang == "en" {
|
||||
return "en".to_string();
|
||||
}
|
||||
return format!("{}-{}", lang, region);
|
||||
}
|
||||
|
||||
input.to_string()
|
||||
}
|
||||
|
||||
/// 解析 LANG 环境变量
|
||||
///
|
||||
/// 支持的格式:
|
||||
/// - `en_US.UTF-8` -> `en`
|
||||
/// - `zh_CN.UTF-8` -> `zh-CN`
|
||||
/// - `zh_TW.UTF-8` -> `zh-TW`
|
||||
/// - `zh_CN` -> `zh-CN`
|
||||
fn parse_lang_env(lang: &str) -> String {
|
||||
// 移除编码部分 (如 .UTF-8)
|
||||
let lang = lang.split('.').next().unwrap_or(lang);
|
||||
|
||||
// 移除修饰部分 (如 @cjk)
|
||||
let lang = lang.split('@').next().unwrap_or(lang);
|
||||
|
||||
// 标准化格式
|
||||
normalize_locale(lang)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
fn test_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
struct EnvRestore {
|
||||
saved: Vec<(&'static str, Option<String>)>,
|
||||
}
|
||||
|
||||
impl Drop for EnvRestore {
|
||||
fn drop(&mut self) {
|
||||
for (key, value) in &self.saved {
|
||||
match value {
|
||||
Some(v) => unsafe { std::env::set_var(key, v) },
|
||||
None => unsafe { std::env::remove_var(key) },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_env(overrides: &[(&'static str, Option<&str>)]) -> EnvRestore {
|
||||
let tracked_keys = ["DEFAULT_LOCALE", "LANG", "MCP_PROXY_LANG", "APP_LANG"];
|
||||
let mut saved = Vec::with_capacity(tracked_keys.len());
|
||||
|
||||
for key in tracked_keys {
|
||||
saved.push((key, std::env::var(key).ok()));
|
||||
unsafe { std::env::remove_var(key) };
|
||||
}
|
||||
|
||||
for (key, value) in overrides {
|
||||
match value {
|
||||
Some(v) => unsafe { std::env::set_var(key, v) },
|
||||
None => unsafe { std::env::remove_var(key) },
|
||||
}
|
||||
}
|
||||
|
||||
EnvRestore { saved }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_locale() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
assert_eq!(normalize_locale("en"), "en");
|
||||
assert_eq!(normalize_locale("EN"), "en");
|
||||
assert_eq!(normalize_locale("zh-CN"), "zh-CN");
|
||||
assert_eq!(normalize_locale("zh-cn"), "zh-CN");
|
||||
assert_eq!(normalize_locale("zh_CN"), "zh-CN");
|
||||
assert_eq!(normalize_locale("zh-TW"), "zh-TW");
|
||||
assert_eq!(normalize_locale("zh_tw"), "zh-TW");
|
||||
assert_eq!(normalize_locale("zh"), "zh-CN");
|
||||
assert_eq!(normalize_locale("en_US.UTF-8"), "en");
|
||||
assert_eq!(normalize_locale("zh_CN@cjk"), "zh-CN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_lang_env() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
assert_eq!(parse_lang_env("en_US.UTF-8"), "en");
|
||||
assert_eq!(parse_lang_env("zh_CN.UTF-8"), "zh-CN");
|
||||
assert_eq!(parse_lang_env("zh_TW.UTF-8"), "zh-TW");
|
||||
assert_eq!(parse_lang_env("zh_CN"), "zh-CN");
|
||||
assert_eq!(parse_lang_env("en_US@cjk"), "en");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_and_get_locale() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
set_locale("zh-CN");
|
||||
assert_eq!(current_locale(), "zh-CN");
|
||||
|
||||
set_locale("en");
|
||||
assert_eq!(current_locale(), "en");
|
||||
|
||||
set_locale("zh-TW");
|
||||
assert_eq!(current_locale(), "zh-TW");
|
||||
}
|
||||
|
||||
/// 关键翻译键在所有语言中都存在的测试
|
||||
#[test]
|
||||
fn test_translation_completeness() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
set_locale("en");
|
||||
let test_msg = t!("common.success").to_string();
|
||||
assert_ne!(
|
||||
test_msg, "common.success",
|
||||
"Translations are not loaded; expected crate-local locales to be available"
|
||||
);
|
||||
|
||||
// 测试关键错误消息键
|
||||
let critical_keys = [
|
||||
"errors.mcp_proxy.service_not_found",
|
||||
"errors.mcp_proxy.service_startup_failed",
|
||||
"errors.document_parser.config",
|
||||
"errors.document_parser.parse",
|
||||
"errors.oss.config",
|
||||
"errors.oss.network",
|
||||
"errors.voice.config",
|
||||
"errors.voice.transcription",
|
||||
"cli.startup.service_starting",
|
||||
"cli.startup.success",
|
||||
"common.error",
|
||||
"common.success",
|
||||
];
|
||||
|
||||
for locale in AVAILABLE_LOCALES {
|
||||
set_locale(locale);
|
||||
for key in &critical_keys {
|
||||
let msg = match *key {
|
||||
"errors.mcp_proxy.service_not_found" => {
|
||||
t!("errors.mcp_proxy.service_not_found", service = "test").to_string()
|
||||
}
|
||||
"errors.mcp_proxy.service_startup_failed" => t!(
|
||||
"errors.mcp_proxy.service_startup_failed",
|
||||
mcp_id = "test",
|
||||
reason = "test"
|
||||
)
|
||||
.to_string(),
|
||||
_ => t!(*key).to_string(),
|
||||
};
|
||||
// 翻译不应该返回 key 本身(表示翻译缺失)
|
||||
assert_ne!(
|
||||
msg, *key,
|
||||
"Missing translation for '{}' in locale '{}'",
|
||||
key, locale
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试所有支持的语言都能正确切换
|
||||
///
|
||||
/// 注意:此测试依赖于 rust-i18n 的全局状态,在测试环境中可能不稳定。
|
||||
/// 但在实际运行时,语言切换功能是正常的。
|
||||
#[test]
|
||||
fn test_all_locales_available() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
// 测试每个语言代码都是有效的
|
||||
for locale in AVAILABLE_LOCALES {
|
||||
// 验证语言代码格式正确
|
||||
assert!(
|
||||
locale.contains('-') || *locale == "en",
|
||||
"Locale '{}' should follow language-region format",
|
||||
locale
|
||||
);
|
||||
// 尝试设置(在翻译文件不可用时可能不生效,但不应该崩溃)
|
||||
set_locale(locale);
|
||||
}
|
||||
|
||||
// 重置为默认语言
|
||||
set_locale(DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_locale_from_env_prefers_default_locale() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
let _env = prepare_env(&[
|
||||
("DEFAULT_LOCALE", Some("zh-TW")),
|
||||
("LANG", Some("en_US.UTF-8")),
|
||||
("MCP_PROXY_LANG", Some("zh-CN")),
|
||||
("APP_LANG", Some("zh-CN")),
|
||||
]);
|
||||
|
||||
init_locale_from_env();
|
||||
assert_eq!(current_locale(), "zh-TW");
|
||||
set_locale(DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_locale_from_env_falls_back_to_lang() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
let _env = prepare_env(&[
|
||||
("DEFAULT_LOCALE", Some("unsupported")),
|
||||
("LANG", Some("zh_CN.UTF-8")),
|
||||
]);
|
||||
|
||||
init_locale_from_env();
|
||||
assert_eq!(current_locale(), "zh-CN");
|
||||
set_locale(DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_locale_from_env_falls_back_to_english() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
let _env = prepare_env(&[
|
||||
("DEFAULT_LOCALE", Some("unsupported")),
|
||||
("LANG", Some("ja_JP.UTF-8")),
|
||||
]);
|
||||
|
||||
init_locale_from_env();
|
||||
assert_eq!(current_locale(), "en");
|
||||
set_locale(DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_locale_from_env_ignores_removed_env_vars() {
|
||||
let _guard = test_lock().lock().expect("locale test lock poisoned");
|
||||
let _env = prepare_env(&[
|
||||
("MCP_PROXY_LANG", Some("zh-TW")),
|
||||
("APP_LANG", Some("zh-CN")),
|
||||
]);
|
||||
|
||||
init_locale_from_env();
|
||||
assert_eq!(current_locale(), "en");
|
||||
set_locale(DEFAULT_LOCALE);
|
||||
}
|
||||
}
|
||||
63
qiming-mcp-proxy/mcp-common/src/lib.rs
Normal file
63
qiming-mcp-proxy/mcp-common/src/lib.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! MCP Common - Shared types and utilities for MCP proxy modules
|
||||
//!
|
||||
//! This crate provides common functionality shared across mcp-sse-proxy
|
||||
//! and mcp-streamable-proxy to avoid code duplication.
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! - `telemetry`: 基础 OpenTelemetry 支持
|
||||
//! - `otlp`: OTLP exporter 支持(用于 Jaeger 等)
|
||||
//!
|
||||
//! # 国际化 (i18n)
|
||||
//!
|
||||
//! 本 crate 提供多语言支持,使用 rust-i18n 实现。
|
||||
//!
|
||||
//! ## 使用方法
|
||||
//!
|
||||
//! ```rust
|
||||
//! use mcp_common::{t, set_locale, init_locale_from_env};
|
||||
//!
|
||||
//! // 初始化语言设置(程序启动时调用)
|
||||
//! init_locale_from_env();
|
||||
//!
|
||||
//! // 获取翻译
|
||||
//! let msg = t!("errors.mcp_proxy.service_not_found", service = "my-service");
|
||||
//! ```
|
||||
|
||||
// 初始化 i18n,必须在 crate root 调用
|
||||
#[macro_use]
|
||||
extern crate rust_i18n;
|
||||
|
||||
// 初始化翻译文件,使用 crate 内置 locales(支持独立发布)
|
||||
i18n!("locales", fallback = "en");
|
||||
|
||||
pub mod backend_bridge;
|
||||
pub mod client_config;
|
||||
pub mod config;
|
||||
pub mod diagnostic;
|
||||
pub mod i18n;
|
||||
pub mod mirror;
|
||||
pub mod process_compat;
|
||||
pub mod tool_filter;
|
||||
|
||||
#[cfg(feature = "telemetry")]
|
||||
pub mod telemetry;
|
||||
|
||||
// Re-export main types
|
||||
pub use backend_bridge::BackendBridge;
|
||||
pub use client_config::McpClientConfig;
|
||||
pub use config::McpServiceConfig;
|
||||
pub use process_compat::check_windows_command;
|
||||
pub use process_compat::ensure_runtime_path;
|
||||
pub use process_compat::resolve_windows_command;
|
||||
pub use process_compat::spawn_stderr_reader;
|
||||
pub use tool_filter::ToolFilter;
|
||||
|
||||
// Re-export i18n types
|
||||
pub use i18n::{
|
||||
AVAILABLE_LOCALES, DEFAULT_LOCALE, current_locale, init_locale_from_env, set_locale, t,
|
||||
};
|
||||
|
||||
// Re-export telemetry types when feature is enabled
|
||||
#[cfg(feature = "telemetry")]
|
||||
pub use telemetry::{TracingConfig, TracingGuard, create_otel_layer, init_tracing};
|
||||
93
qiming-mcp-proxy/mcp-common/src/mirror.rs
Normal file
93
qiming-mcp-proxy/mcp-common/src/mirror.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
//! 镜像源配置:通过进程级环境变量为 npx/uvx 子进程设置国内镜像源
|
||||
|
||||
/// 镜像源配置
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MirrorConfig {
|
||||
pub npm_registry: Option<String>,
|
||||
pub pypi_index_url: Option<String>,
|
||||
}
|
||||
|
||||
impl MirrorConfig {
|
||||
/// 从环境变量 `MCP_PROXY_NPM_REGISTRY` / `MCP_PROXY_PYPI_INDEX_URL` 加载
|
||||
pub fn from_env() -> Self {
|
||||
Self {
|
||||
npm_registry: std::env::var("MCP_PROXY_NPM_REGISTRY").ok(),
|
||||
pypi_index_url: std::env::var("MCP_PROXY_PYPI_INDEX_URL").ok(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.npm_registry.is_none() && self.pypi_index_url.is_none()
|
||||
}
|
||||
|
||||
/// 设为进程级环境变量,所有子进程自动继承。
|
||||
///
|
||||
/// # Safety
|
||||
/// 应在 main() 启动早期、单线程阶段调用。
|
||||
pub fn apply_to_process_env(&self) {
|
||||
unsafe {
|
||||
if let Some(ref registry) = self.npm_registry
|
||||
&& std::env::var("npm_config_registry").is_err()
|
||||
{
|
||||
std::env::set_var("npm_config_registry", registry);
|
||||
}
|
||||
if let Some(ref index_url) = self.pypi_index_url {
|
||||
if std::env::var("UV_INDEX_URL").is_err() {
|
||||
std::env::set_var("UV_INDEX_URL", index_url);
|
||||
}
|
||||
if std::env::var("PIP_INDEX_URL").is_err() {
|
||||
std::env::set_var("PIP_INDEX_URL", index_url);
|
||||
}
|
||||
if std::env::var("UV_INSECURE_HOST").is_err()
|
||||
&& index_url.starts_with("http://")
|
||||
&& let Some(host) = extract_host(index_url)
|
||||
{
|
||||
std::env::set_var("UV_INSECURE_HOST", &host);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 URL 中提取 host
|
||||
fn extract_host(url: &str) -> Option<String> {
|
||||
let without_scheme = url
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| url.strip_prefix("http://"))?;
|
||||
let host = without_scheme.split('/').next()?.split(':').next()?;
|
||||
if host.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(host.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mirror_config_is_empty() {
|
||||
assert!(MirrorConfig::default().is_empty());
|
||||
assert!(
|
||||
!MirrorConfig {
|
||||
npm_registry: Some("test".to_string()),
|
||||
pypi_index_url: None,
|
||||
}
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_host() {
|
||||
assert_eq!(
|
||||
extract_host("https://mirrors.aliyun.com/pypi/simple/"),
|
||||
Some("mirrors.aliyun.com".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
extract_host("https://example.com:8080/path"),
|
||||
Some("example.com".to_string())
|
||||
);
|
||||
assert_eq!(extract_host("not-a-url"), None);
|
||||
}
|
||||
}
|
||||
433
qiming-mcp-proxy/mcp-common/src/process_compat.rs
Normal file
433
qiming-mcp-proxy/mcp-common/src/process_compat.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
//! 跨平台进程管理兼容层
|
||||
//!
|
||||
//! 提供统一的进程管理抽象,减少平台特定代码的侵入性。
|
||||
//!
|
||||
//! # 使用方法
|
||||
//!
|
||||
//! ## 命令检测
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use mcp_common::process_compat::check_windows_command;
|
||||
//!
|
||||
//! check_windows_command(&config.command);
|
||||
//! ```
|
||||
//!
|
||||
//! ## 进程包装宏
|
||||
//!
|
||||
//! process-wrap 8.x (TokioCommandWrap):
|
||||
//! ```ignore
|
||||
//! use mcp_common::process_compat::wrap_process_v8;
|
||||
//!
|
||||
//! let mut wrapped_cmd = TokioCommandWrap::with_new(...);
|
||||
//! wrap_process_v8!(wrapped_cmd);
|
||||
//! wrapped_cmd.wrap(KillOnDrop);
|
||||
//! ```
|
||||
//!
|
||||
//! process-wrap 9.x (CommandWrap):
|
||||
//! ```ignore
|
||||
//! use mcp_common::process_compat::wrap_process_v9;
|
||||
//!
|
||||
//! let mut wrapped_cmd = CommandWrap::with_new(...);
|
||||
//! wrap_process_v9!(wrapped_cmd);
|
||||
//! wrapped_cmd.wrap(KillOnDrop);
|
||||
//! ```
|
||||
|
||||
#[cfg(windows)]
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// 检测 Windows 平台上可能导致弹窗的命令格式
|
||||
///
|
||||
/// 在 Windows 上,运行 `.cmd`、`.bat` 文件或 `npx` 命令可能会弹出 CMD 窗口。
|
||||
/// 此函数会检测这些情况并输出警告,建议用户使用替代方案。
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `command` - 要执行的命令字符串
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use mcp_common::process_compat::check_windows_command;
|
||||
///
|
||||
/// check_windows_command("npx some-server");
|
||||
/// check_windows_command("mcp-server.cmd");
|
||||
/// ```
|
||||
#[cfg(windows)]
|
||||
pub fn check_windows_command(command: &str) {
|
||||
use std::path::Path;
|
||||
|
||||
let cmd_ext = Path::new(command)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|s| s.to_ascii_lowercase());
|
||||
|
||||
match cmd_ext.as_deref() {
|
||||
Some("cmd" | "bat") => {
|
||||
warn!(
|
||||
"[MCP] Windows detected .cmd/.bat command: {} - CMD window may pop up!",
|
||||
command
|
||||
);
|
||||
warn!(
|
||||
"[MCP] It is recommended to use node.exe to run the JS file directly, or use the full path in the configuration"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
// 无扩展名,检查是否是 npx 命令
|
||||
if command.contains("npx") {
|
||||
warn!(
|
||||
"[MCP] Windows detects npx command: {} - CMD window may pop up!",
|
||||
command
|
||||
);
|
||||
warn!("[MCP] It is recommended to use node.exe to run JS files directly");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
info!("[MCP] Windows detected command format: {}", command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix/macOS 平台的空实现
|
||||
#[cfg(not(windows))]
|
||||
pub fn check_windows_command(_command: &str) {
|
||||
// 非 Windows 平台无需检测
|
||||
}
|
||||
|
||||
/// Windows 上解析命令路径,自动添加扩展名
|
||||
///
|
||||
/// 在 Windows 上,命令如 `npx` 实际上是 `npx.cmd` 批处理文件。
|
||||
/// `std::process::Command` 不会自动查找 `.cmd` 扩展名,需要手动指定。
|
||||
/// 此函数尝试在 PATH 中查找命令,并返回带扩展名的完整路径或原始命令。
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `command` - 要解析的命令字符串
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 如果找到,返回带扩展名的命令;否则返回原始命令
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use mcp_common::process_compat::resolve_windows_command;
|
||||
///
|
||||
/// let resolved = resolve_windows_command("npx");
|
||||
/// // 返回 "npx.cmd" 或 "C:\Program Files\nodejs\npx.cmd"
|
||||
/// ```
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn resolve_windows_command(command: &str) -> String {
|
||||
use std::path::Path;
|
||||
|
||||
// 如果已经有扩展名,直接返回
|
||||
if Path::new(command).extension().is_some() {
|
||||
return command.to_string();
|
||||
}
|
||||
|
||||
// 如果是绝对路径,直接返回
|
||||
if Path::new(command).is_absolute() {
|
||||
return command.to_string();
|
||||
}
|
||||
|
||||
// 获取 PATH 环境变量
|
||||
let path_env = match std::env::var("PATH") {
|
||||
Ok(p) => p,
|
||||
Err(_) => return command.to_string(),
|
||||
};
|
||||
|
||||
// Windows 可执行文件扩展名(按优先级)
|
||||
let extensions = [".cmd", ".exe", ".bat", ".ps1"];
|
||||
|
||||
// 遍历 PATH 中的每个目录
|
||||
for dir in path_env.split(';') {
|
||||
let dir = dir.trim();
|
||||
if dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 尝试每个扩展名
|
||||
for ext in &extensions {
|
||||
let full_path = Path::new(dir).join(format!("{}{}", command, ext));
|
||||
if full_path.exists() {
|
||||
tracing::debug!(
|
||||
"[MCP] Windows command analysis: {} -> {}",
|
||||
command,
|
||||
full_path.display()
|
||||
);
|
||||
// 返回带扩展名的命令(不是完整路径,保持简洁)
|
||||
return format!("{}{}", command, ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 未找到,返回原始命令
|
||||
command.to_string()
|
||||
}
|
||||
|
||||
/// 非 Windows 平台的空实现
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn resolve_windows_command(command: &str) -> String {
|
||||
command.to_string()
|
||||
}
|
||||
|
||||
/// 确保应用内置运行时路径(NUWAX_APP_RUNTIME_PATH)在 PATH 最前面。
|
||||
///
|
||||
/// 当应用捆绑了 node/uv 等运行时时,通过 `NUWAX_APP_RUNTIME_PATH` 传递其路径。
|
||||
/// 此函数将这些路径插入到给定 PATH 的最前面,确保优先使用应用内置版本,
|
||||
/// 即使用户在 MCP 配置的 `env` 中指定了自定义 PATH。
|
||||
///
|
||||
/// **按段去重**:将 runtime_path 和现有 PATH 拆分为独立条目,
|
||||
/// 先放 runtime 段,再追加 PATH 中不在 runtime 里的段,彻底避免重复。
|
||||
///
|
||||
/// 如果 `NUWAX_APP_RUNTIME_PATH` 未设置或为空,直接返回原始 PATH。
|
||||
pub fn ensure_runtime_path(path: &str) -> String {
|
||||
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
|
||||
let runtime_path = runtime_path.trim();
|
||||
if !runtime_path.is_empty() {
|
||||
let sep = if cfg!(windows) { ";" } else { ":" };
|
||||
|
||||
// 将 runtime_path 拆成各段
|
||||
let runtime_segments: Vec<&str> =
|
||||
runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
|
||||
|
||||
// 将现有 PATH 拆成各段,去掉已在 runtime 中的
|
||||
let existing_segments: Vec<&str> = path
|
||||
.split(sep)
|
||||
.filter(|s| !s.is_empty() && !runtime_segments.contains(s))
|
||||
.collect();
|
||||
|
||||
let merged: Vec<&str> = runtime_segments
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(existing_segments)
|
||||
.collect();
|
||||
|
||||
let result = merged.join(sep);
|
||||
if result != path {
|
||||
tracing::info!(
|
||||
"[ProcessCompat] Front-end application built-in runtime to PATH: {}",
|
||||
runtime_path
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
/// 为 process-wrap 8.x 的 TokioCommandWrap 应用平台特定的包装
|
||||
///
|
||||
/// 此宏会根据目标平台自动应用正确的进程包装:
|
||||
/// - Windows: `CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)` + `JobObject`
|
||||
/// - Unix: `ProcessGroup::leader()`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `$cmd` - 可变的 TokioCommandWrap 实例
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use process_wrap::tokio::{TokioCommandWrap, KillOnDrop};
|
||||
/// use mcp_common::process_compat::wrap_process_v8;
|
||||
///
|
||||
/// let mut wrapped_cmd = TokioCommandWrap::with_new("node", |cmd| {
|
||||
/// cmd.arg("server.js");
|
||||
/// });
|
||||
/// wrap_process_v8!(wrapped_cmd);
|
||||
/// wrapped_cmd.wrap(KillOnDrop);
|
||||
/// ```
|
||||
#[cfg(unix)]
|
||||
#[macro_export]
|
||||
macro_rules! wrap_process_v8 {
|
||||
($cmd:expr) => {{
|
||||
use process_wrap::tokio::ProcessGroup;
|
||||
$cmd.wrap(ProcessGroup::leader());
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[macro_export]
|
||||
macro_rules! wrap_process_v8 {
|
||||
($cmd:expr) => {{
|
||||
use process_wrap::tokio::{CreationFlags, JobObject};
|
||||
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
|
||||
$cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
|
||||
$cmd.wrap(JobObject);
|
||||
}};
|
||||
}
|
||||
|
||||
/// 为 process-wrap 9.x 的 CommandWrap 应用平台特定的包装
|
||||
///
|
||||
/// 此宏会根据目标平台自动应用正确的进程包装:
|
||||
/// - Windows: `CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)` + `JobObject`
|
||||
/// - Unix: `ProcessGroup::leader()`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `$cmd` - 可变的 CommandWrap 实例
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use process_wrap::tokio::{CommandWrap, KillOnDrop};
|
||||
/// use mcp_common::process_compat::wrap_process_v9;
|
||||
///
|
||||
/// let mut wrapped_cmd = CommandWrap::with_new("node", |cmd| {
|
||||
/// cmd.arg("server.js");
|
||||
/// });
|
||||
/// wrap_process_v9!(wrapped_cmd);
|
||||
/// wrapped_cmd.wrap(KillOnDrop);
|
||||
/// ```
|
||||
#[cfg(unix)]
|
||||
#[macro_export]
|
||||
macro_rules! wrap_process_v9 {
|
||||
($cmd:expr) => {{
|
||||
use process_wrap::tokio::ProcessGroup;
|
||||
$cmd.wrap(ProcessGroup::leader());
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[macro_export]
|
||||
macro_rules! wrap_process_v9 {
|
||||
($cmd:expr) => {{
|
||||
use process_wrap::tokio::{CreationFlags, JobObject};
|
||||
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
|
||||
$cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
|
||||
$cmd.wrap(JobObject);
|
||||
}};
|
||||
}
|
||||
|
||||
/// 启动 stderr 日志读取任务
|
||||
///
|
||||
/// 创建一个异步任务来读取子进程的 stderr 输出并记录到日志。
|
||||
/// 这个函数封装了通用的 stderr 读取逻辑。
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `stderr` - stderr 管道(实现 AsyncRead + Unpin + Send)
|
||||
/// * `service_name` - MCP 服务名称(用于日志标识)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 返回 `JoinHandle<()>`,任务会在 stderr 关闭时自动结束
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use mcp_common::process_compat::spawn_stderr_reader;
|
||||
///
|
||||
/// let (tokio_process, child_stderr) = TokioChildProcess::builder(wrapped_cmd)
|
||||
/// .stderr(Stdio::piped())
|
||||
/// .spawn()?;
|
||||
///
|
||||
/// if let Some(stderr) = child_stderr {
|
||||
/// spawn_stderr_reader(stderr, "my-mcp-service".to_string());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn spawn_stderr_reader<T>(stderr: T, service_name: String) -> tokio::task::JoinHandle<()>
|
||||
where
|
||||
T: tokio::io::AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
let mut reader = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
match reader.read_line(&mut line).await {
|
||||
Ok(0) => {
|
||||
// EOF - stderr 已关闭
|
||||
tracing::debug!("[Subprocess stderr][{}] End of read (EOF)", service_name);
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
tracing::warn!("[child process stderr][{}] {}", service_name, trimmed);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("[Subprocess stderr][{}] Read error: {}", service_name, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_check_windows_command_non_windows() {
|
||||
// 在非 Windows 平台上,此函数应该不执行任何操作
|
||||
check_windows_command("npx some-server");
|
||||
check_windows_command("test.cmd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_runtime_path_no_env() {
|
||||
// NUWAX_APP_RUNTIME_PATH 未设置时,返回原始 PATH
|
||||
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
|
||||
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
|
||||
assert_eq!(result, "/usr/bin:/usr/local/bin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_runtime_path_prepend() {
|
||||
unsafe {
|
||||
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
|
||||
}
|
||||
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
|
||||
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
|
||||
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_runtime_path_dedup() {
|
||||
// 模拟:PATH 中已有 runtime 的部分段 → 不应重复
|
||||
unsafe {
|
||||
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
|
||||
}
|
||||
let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
|
||||
assert_eq!(
|
||||
result,
|
||||
"/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
|
||||
);
|
||||
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_runtime_path_all_present() {
|
||||
// PATH 已含全部 runtime 段 → 仅调整顺序确保 runtime 在前
|
||||
unsafe {
|
||||
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
|
||||
}
|
||||
let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
|
||||
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
|
||||
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_runtime_path_double_node() {
|
||||
// 模拟日志中的问题:node/bin 出现两次
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
"NUWAX_APP_RUNTIME_PATH",
|
||||
"/app/node/bin:/app/uv/bin:/app/debug",
|
||||
);
|
||||
}
|
||||
let result = ensure_runtime_path(
|
||||
"/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
"/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
|
||||
);
|
||||
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
|
||||
}
|
||||
}
|
||||
189
qiming-mcp-proxy/mcp-common/src/telemetry.rs
Normal file
189
qiming-mcp-proxy/mcp-common/src/telemetry.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
//! OpenTelemetry 追踪模块
|
||||
//!
|
||||
//! 提供统一的分布式追踪初始化接口,支持 OTLP (Jaeger) exporter。
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//! - `telemetry`: 基础 OpenTelemetry 支持
|
||||
//! - `otlp`: OTLP exporter 支持(用于 Jaeger)
|
||||
//!
|
||||
//! # 使用示例
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use mcp_common::{TracingConfig, init_tracing};
|
||||
//!
|
||||
//! let config = TracingConfig::new("my-service")
|
||||
//! .with_otlp("http://localhost:4317")
|
||||
//! .with_version("1.0.0");
|
||||
//!
|
||||
//! let _guard = init_tracing(&config)?;
|
||||
//! // guard 保持存活期间,追踪数据会被发送到 OTLP endpoint
|
||||
//! ```
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// 追踪配置
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TracingConfig {
|
||||
/// 服务名称
|
||||
pub service_name: String,
|
||||
/// 服务版本
|
||||
pub service_version: Option<String>,
|
||||
/// OTLP 端点 (如 http://localhost:4317)
|
||||
pub otlp_endpoint: Option<String>,
|
||||
/// 采样率 (0.0 - 1.0),默认为 1.0(全部采样)
|
||||
pub sample_ratio: Option<f64>,
|
||||
}
|
||||
|
||||
impl TracingConfig {
|
||||
/// 创建新的追踪配置
|
||||
pub fn new(service_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
service_name: service_name.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 OTLP 端点
|
||||
pub fn with_otlp(mut self, endpoint: impl Into<String>) -> Self {
|
||||
self.otlp_endpoint = Some(endpoint.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置服务版本
|
||||
pub fn with_version(mut self, version: impl Into<String>) -> Self {
|
||||
self.service_version = Some(version.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置采样率
|
||||
pub fn with_sample_ratio(mut self, ratio: f64) -> Self {
|
||||
self.sample_ratio = Some(ratio.clamp(0.0, 1.0));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化追踪系统(启用 OTLP feature 时)
|
||||
#[cfg(feature = "otlp")]
|
||||
pub fn init_tracing(config: &TracingConfig) -> Result<TracingGuard> {
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry::global;
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_sdk::Resource;
|
||||
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
|
||||
|
||||
let mut provider_builder = SdkTracerProvider::builder();
|
||||
|
||||
// 配置采样率
|
||||
if let Some(ratio) = config.sample_ratio {
|
||||
provider_builder = provider_builder.with_sampler(Sampler::TraceIdRatioBased(ratio));
|
||||
}
|
||||
|
||||
// 配置 OTLP exporter
|
||||
if let Some(endpoint) = &config.otlp_endpoint {
|
||||
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.build()?;
|
||||
|
||||
provider_builder = provider_builder.with_batch_exporter(exporter);
|
||||
tracing::info!(endpoint = %endpoint, "OTLP exporter configured");
|
||||
}
|
||||
|
||||
// 配置资源属性
|
||||
let mut attributes = vec![KeyValue::new("service.name", config.service_name.clone())];
|
||||
|
||||
if let Some(version) = &config.service_version {
|
||||
attributes.push(KeyValue::new("service.version", version.clone()));
|
||||
}
|
||||
|
||||
let resource = Resource::builder().with_attributes(attributes).build();
|
||||
provider_builder = provider_builder.with_resource(resource);
|
||||
|
||||
let provider = provider_builder.build();
|
||||
global::set_tracer_provider(provider.clone());
|
||||
|
||||
tracing::info!(
|
||||
service_name = %config.service_name,
|
||||
"OpenTelemetry tracer provider initialized"
|
||||
);
|
||||
|
||||
Ok(TracingGuard {
|
||||
provider: Some(provider),
|
||||
})
|
||||
}
|
||||
|
||||
/// 无 OTLP 时的空实现
|
||||
#[cfg(not(feature = "otlp"))]
|
||||
pub fn init_tracing(_config: &TracingConfig) -> Result<TracingGuard> {
|
||||
tracing::debug!("OTLP feature not enabled, skipping tracer initialization");
|
||||
Ok(TracingGuard { provider: None })
|
||||
}
|
||||
|
||||
/// 创建 tracing-opentelemetry layer
|
||||
///
|
||||
/// 此 layer 可以添加到 tracing_subscriber 中,将 tracing 的 span 和事件
|
||||
/// 转发到 OpenTelemetry。
|
||||
#[cfg(feature = "telemetry")]
|
||||
pub fn create_otel_layer() -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
|
||||
tracing_opentelemetry::layer()
|
||||
}
|
||||
|
||||
/// 追踪守卫 - Drop 时自动关闭 tracer provider
|
||||
///
|
||||
/// 必须保持此守卫存活,否则追踪数据可能不会被正确发送。
|
||||
pub struct TracingGuard {
|
||||
#[cfg(feature = "otlp")]
|
||||
provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
|
||||
#[cfg(not(feature = "otlp"))]
|
||||
#[allow(dead_code)]
|
||||
provider: Option<()>,
|
||||
}
|
||||
|
||||
impl TracingGuard {
|
||||
/// 检查追踪是否已初始化
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.provider.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TracingGuard {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "otlp")]
|
||||
if let Some(provider) = self.provider.take() {
|
||||
tracing::info!("Shutting down OpenTelemetry tracer provider");
|
||||
if let Err(e) = provider.shutdown() {
|
||||
tracing::warn!("Failed to shutdown tracer provider: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tracing_config_builder() {
|
||||
let config = TracingConfig::new("test-service")
|
||||
.with_otlp("http://localhost:4317")
|
||||
.with_version("1.0.0")
|
||||
.with_sample_ratio(0.5);
|
||||
|
||||
assert_eq!(config.service_name, "test-service");
|
||||
assert_eq!(
|
||||
config.otlp_endpoint,
|
||||
Some("http://localhost:4317".to_string())
|
||||
);
|
||||
assert_eq!(config.service_version, Some("1.0.0".to_string()));
|
||||
assert_eq!(config.sample_ratio, Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_ratio_clamping() {
|
||||
let config = TracingConfig::new("test").with_sample_ratio(1.5);
|
||||
assert_eq!(config.sample_ratio, Some(1.0));
|
||||
|
||||
let config = TracingConfig::new("test").with_sample_ratio(-0.5);
|
||||
assert_eq!(config.sample_ratio, Some(0.0));
|
||||
}
|
||||
}
|
||||
79
qiming-mcp-proxy/mcp-common/src/tool_filter.rs
Normal file
79
qiming-mcp-proxy/mcp-common/src/tool_filter.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! 工具过滤器
|
||||
//!
|
||||
//! 提供白名单和黑名单两种过滤模式
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// 工具过滤配置
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ToolFilter {
|
||||
/// 白名单(只允许这些工具)
|
||||
pub allow_tools: Option<HashSet<String>>,
|
||||
/// 黑名单(排除这些工具)
|
||||
pub deny_tools: Option<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl ToolFilter {
|
||||
/// 创建白名单过滤器
|
||||
pub fn allow(tools: Vec<String>) -> Self {
|
||||
Self {
|
||||
allow_tools: Some(tools.into_iter().collect()),
|
||||
deny_tools: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建黑名单过滤器
|
||||
pub fn deny(tools: Vec<String>) -> Self {
|
||||
Self {
|
||||
allow_tools: None,
|
||||
deny_tools: Some(tools.into_iter().collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查工具是否被允许
|
||||
pub fn is_allowed(&self, tool_name: &str) -> bool {
|
||||
// 白名单模式:只有在白名单中的工具才被允许
|
||||
if let Some(ref allow_list) = self.allow_tools {
|
||||
return allow_list.contains(tool_name);
|
||||
}
|
||||
// 黑名单模式:不在黑名单中的工具都被允许
|
||||
if let Some(ref deny_list) = self.deny_tools {
|
||||
return !deny_list.contains(tool_name);
|
||||
}
|
||||
// 无过滤:全部允许
|
||||
true
|
||||
}
|
||||
|
||||
/// 检查是否启用了过滤
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.allow_tools.is_some() || self.deny_tools.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_allow_filter() {
|
||||
let filter = ToolFilter::allow(vec!["tool1".to_string(), "tool2".to_string()]);
|
||||
assert!(filter.is_allowed("tool1"));
|
||||
assert!(filter.is_allowed("tool2"));
|
||||
assert!(!filter.is_allowed("tool3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deny_filter() {
|
||||
let filter = ToolFilter::deny(vec!["tool1".to_string()]);
|
||||
assert!(!filter.is_allowed("tool1"));
|
||||
assert!(filter.is_allowed("tool2"));
|
||||
assert!(filter.is_allowed("tool3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_filter() {
|
||||
let filter = ToolFilter::default();
|
||||
assert!(filter.is_allowed("any_tool"));
|
||||
assert!(!filter.is_enabled());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user