diff --git a/.gitignore b/.gitignore index 6a0cdb0..8a8120e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ voice-cli/server-config.yml voice-cli/server_debug.log voice-cli/test_audio.txt voice-cli/checkpoints/* +mcp-proxy/tmp/* diff --git a/CLAUDE.md b/CLAUDE.md index abc7493..9b2579d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,19 @@ This is a Rust workspace implementing an MCP (Model Context Protocol) proxy syst **Logging**: Structured logging with `tracing` and `tracing-subscriber`. Daily log rotation with `tracing-appender`. -**FFmpeg Integration**: Lightweight FFmpeg command execution via `ffmpeg-sidecar` for media metadata extraction. +**FFmpeg Integration**: Lightweight FFmpeg command execution via `ffmpeg-sidecar` for media metadata extraction. System FFmpeg installation required but provides graceful fallback. + +**Python Integration**: Both `document-parser` and `voice-cli` use Python services with `uv` for dependency management and virtual environment handling: +- Automatic virtual environment creation in `./venv/` +- uv package manager for fast Python dependency installation +- CUDA GPU acceleration support (optional) +- Graceful degradation if Python/uv unavailable + +**Task Queue & Persistence**: Voice services use `apalis` for background task processing: +- SQLite-based persistence for task state tracking +- Task retry mechanisms with exponential backoff +- Support for task prioritization and status monitoring +- Worker management with resource limits ### Configuration System @@ -148,13 +160,40 @@ All services use hierarchical configuration: **Code Organization**: Strict workspace structure - no code in root directory. All implementation in sub-crates with clear module boundaries. -**Error Handling**: Prefer `anyhow` for application code, `thiserror` for libraries. Avoid `unwrap()` except in tests. Use `?` operator for error propagation. - -**Concurrency**: Use `tokio` for async, `Arc>` or `Arc>` for shared state. Avoid blocking operations in async contexts. - -**Memory Management**: Prefer borrowing over ownership. Use `dashmap` for concurrent hashmaps instead of `Arc>>`. - -**Testing**: Unit tests alongside implementation code. Integration tests where appropriate. Use `assert_eq!`, `assert_ne!` for assertions. +**Formatting & Linting**: +- Line length: 100 characters +- 4-space indentation (no tabs) +- Always run `cargo fmt` and `cargo clippy` before commits +- Use `cargo audit` to check for security vulnerabilities +- Use `typos-cli` to check spelling + +**Error Handling**: +- Prefer `anyhow` for application code, `thiserror` for libraries +- Avoid `unwrap()` except in tests +- Use `?` operator for error propagation +- Add contextual error messages with `anyhow::Context` +- Never include sensitive data in error messages + +**Concurrency**: +- Use `tokio` for async, `Arc>` or `Arc>` for shared state +- Avoid blocking operations in async contexts +- Use `tokio::spawn` for creating concurrent tasks + +**Memory Management**: +- Prefer borrowing over ownership +- **Use `dashmap` for concurrent hashmaps** instead of `Arc>>` (dashmap provides atomic operations and is more efficient) +- Avoid unnecessary `clone()`, consider `Cow` or reference counting + +**Testing**: +- Unit tests alongside implementation code +- Integration tests where appropriate +- Use `assert_eq!`, `assert_ne!` for assertions +- Run specific tests: `cargo test -p ` + +**Documentation**: +- All public APIs must have documentation comments (`///`) +- Include usage examples in complex API documentation +- Keep README.md and other docs updated ## Cursor Rules Summary @@ -183,10 +222,67 @@ All services use hierarchical configuration: The project uses a sophisticated Makefile with Docker buildx for cross-platform compilation: +### Docker Build Commands +```bash +# Check Docker buildx availability +make check-buildx + +# Setup buildx builder (if needed) +make setup-buildx + +# Build document-parser for specific platforms +make build-document-parser-x86_64 +make build-document-parser-arm64 +make build-document-parser-multi + +# Build voice-cli for specific platforms +make build-voice-cli-x86_64 +make build-voice-cli-arm64 +make build-voice-cli-multi + +# Build all components +make build-all-x86_64 +make build-all-arm64 +make build-all-multi + +# Build and run Docker runtime image +make build-image +make run +``` + +**Build System Features**: - **Docker-based builds**: All compilation happens in containers for consistency - **Multi-platform support**: Linux x86_64 and ARM64 targets - **Export targets**: Separate build and runtime stages - **Automated dependency installation**: Python and Rust dependencies managed in containers +- **Output directory**: `./dist/` contains all built binaries organized by platform + +## Service-Specific Architecture Details + +### Document Parser (`document-parser/`) +- **Core Structure**: `app_state.rs`, `config.rs`, `main.rs`, `lib.rs` +- **Submodules**: `handlers/`, `middleware/`, `models/`, `parsers/`, `processors/`, `services/`, `tests/`, `utils/` +- **Python Integration**: MinerU for PDF parsing, MarkItDown for other formats +- **Virtual Environment**: Auto-managed in `./venv/`, activated via `source ./venv/bin/activate` +- **Server**: Axum-based HTTP server with multipart file upload support +- **Configuration**: YAML/JSON/TOML support with environment variable overrides + +### Voice CLI (`voice-cli/`) +- **Core Components**: + - `services/`: Model management, transcription engine, TTS service, task queue + - `server/`: HTTP handlers, routes, middleware configuration + - `models/`: Request/response data structures +- **Whisper Integration**: Model download and management via `voice-toolkit` +- **TTS Service**: Python-based with `uv` dependency management +- **FFmpeg**: Metadata extraction via `ffmpeg-sidecar` +- **Apalis**: Async task processing with SQLite persistence + +### MCP Proxy (`mcp-proxy/`) +- **Core Structure**: `config.rs`, `lib.rs`, `main.rs`, `mcp_error.rs` +- **Submodules**: `client/`, `model/`, `proxy/`, `server/`, `tests/` +- **SSE Protocol**: Real-time communication via Server-Sent Events +- **Plugin System**: Dynamic MCP service loading and management +- **HTTP API**: REST endpoints for service management and status checks ## Common Patterns @@ -198,4 +294,72 @@ The project uses a sophisticated Makefile with Docker buildx for cross-platform **Python Integration**: Both document-parser and voice-cli use Python services with uv for dependency management and virtual environment handling. -**Configuration Management**: Hierarchical configuration with environment variable overrides and command-line argument integration. \ No newline at end of file +**Configuration Management**: Hierarchical configuration with environment variable overrides and command-line argument integration. + +## Single Test Execution Examples +```bash +# Run tests for specific crate +cargo test -p mcp-proxy +cargo test -p voice-cli +cargo test -p document-parser + +# Run specific test +cargo test test_extract_basic_metadata -p voice-cli +cargo test -p mcp-proxy + +# Run tests in release mode +cargo test --release -p mcp-proxy + +# Run library tests only (excluding integration tests) +cargo test --lib -p voice-cli + +# Run tests with output +cargo test -p mcp-proxy -- --nocapture +``` + +## Python/uv Environment Management + +### For Document Parser: +```bash +cd document-parser +# Initialize Python environment (creates ./venv/) +cargo run --bin document-parser -- uv-init + +# Check environment status +cargo run --bin document-parser -- check + +# Start server +cargo run --bin document-parser -- server + +# Troubleshoot issues +cargo run --bin document-parser -- troubleshoot +``` + +### For Voice CLI TTS: +```bash +cd voice-cli +# Install uv package manager +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install Python dependencies +uv sync + +# Run TTS service directly +python3 tts_service.py --help +``` + +## Dependencies Management + +All dependencies are managed centrally in the workspace `Cargo.toml`: +- Sub-crates use `{ workspace = true }` for dependency references +- Specific versions (no `*` wildcards) +- Centralized feature flags +- Regular security audits with `cargo audit` + +Key workspace dependencies: +- `rmcp`: MCP protocol implementation with SSE support +- `tokio`: Async runtime +- `axum`: Web framework with tower middleware +- `tracing`: Structured logging +- `apalis`: Async task queue +- `dashmap`: Concurrent hashmap (preferred over `Arc>`) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 51551fa..a3afa3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -19,27 +19,28 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "aliyun-oss-rust-sdk" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b29348ca333cc5075adea75a3a0aea3df9d876ca0a00a46e7e1171351ea6466e" +checksum = "56ff96bb02a5b0eb9bd47e0686126e1983afd0851f913500a39c861a1510d120" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "hmac", - "reqwest 0.11.27", + "maybe-async", + "reqwest", "serde", "serde_json", "sha1", - "thiserror 1.0.69", + "thiserror 2.0.17", "urlencoding", ] @@ -64,12 +65,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -87,9 +82,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -102,9 +97,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -137,9 +132,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "apalis" @@ -151,7 +146,7 @@ dependencies = [ "futures", "pin-project-lite", "serde", - "thiserror 2.0.14", + "thiserror 2.0.17", "tower", "tracing", "tracing-futures", @@ -168,7 +163,7 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.17", "tower", "ulid", ] @@ -188,7 +183,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -230,18 +225,15 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.27" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" dependencies = [ - "brotli", - "flate2", + "compression-codecs", + "compression-core", "futures-core", - "memchr", "pin-project-lite", "tokio", - "zstd", - "zstd-safe", ] [[package]] @@ -290,9 +282,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -337,9 +329,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", "axum-macros", @@ -347,10 +339,10 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "itoa", "matchit", @@ -359,13 +351,12 @@ dependencies = [ "multer", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-tungstenite", "tower", @@ -376,19 +367,18 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -418,9 +408,9 @@ dependencies = [ "bytes", "bytesize", "cookie", - "http 1.3.1", + "http", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "mime", "pretty_assertions", @@ -437,9 +427,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -447,7 +437,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -494,7 +484,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -531,11 +521,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -562,9 +552,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -589,9 +579,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -607,9 +597,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytesize" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" +checksum = "c99fa31e08a43eaa5913ef68d7e01c37a2bdce6ed648168239ad33b7d30a9cd8" [[package]] name = "cast" @@ -619,10 +609,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.32" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -650,9 +641,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -662,17 +653,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -715,9 +705,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.45" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -725,9 +715,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -737,9 +727,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -749,9 +739,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -768,6 +758,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression-codecs" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -779,9 +789,9 @@ dependencies = [ [[package]] name = "config" -version = "0.15.16" +version = "0.15.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef036f0ecf99baef11555578630e2cca559909b4c50822dbba828c252d21c49" +checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" dependencies = [ "async-trait", "convert_case", @@ -1046,12 +1056,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b136475da5ef7b6ac596c0e956e37bad51b85b987ff3d5e230e964936736b2" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.21.1", - "darling_macro 0.21.1", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1070,9 +1080,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44ad32f92b75fb438b04b68547e521a548be8acc339a6dacc4a7121488f53e6" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", @@ -1095,11 +1105,11 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5be8a7a562d315a5b92a630c30cec6bcf663e6673f00fbb69cca66a6f521b9" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.1", + "darling_core 0.21.3", "quote", "syn", ] @@ -1115,7 +1125,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -1126,12 +1136,12 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deadpool" -version = "0.10.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ - "async-trait", "deadpool-runtime", + "lazy_static", "num_cpus", "tokio", ] @@ -1155,9 +1165,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1246,7 +1256,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1290,7 +1300,7 @@ dependencies = [ "flate2", "futures", "futures-util", - "http 1.3.1", + "http", "insta", "libc", "log", @@ -1300,7 +1310,7 @@ dependencies = [ "num_cpus", "once_cell", "oss-client", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "proptest", "pulldown-cmark", "pulldown-cmark-to-cmark", @@ -1309,7 +1319,7 @@ dependencies = [ "quickcheck_macros", "rand 0.9.2", "regex", - "reqwest 0.12.23", + "reqwest", "rstest", "serde", "serde_json", @@ -1320,7 +1330,7 @@ dependencies = [ "tar", "tempfile", "test-case", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-test", @@ -1394,9 +1404,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -1433,9 +1443,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -1444,12 +1454,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1512,9 +1522,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ffmpeg-sidecar" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869812b38b887a5fb7291e5551677476c4696a395ae6676955f42f01e6a5d1c1" +checksum = "704e93af9cb0b04d3861acd71cede382575a275ee4a0a3c49074ae681d54fbd1" dependencies = [ "anyhow", "tar", @@ -1525,21 +1535,27 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -1586,9 +1602,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1665,7 +1681,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.4", + "parking_lot 0.12.5", ] [[package]] @@ -1743,25 +1759,11 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generator" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.3", -] - [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -1769,9 +1771,9 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] @@ -1785,29 +1787,29 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" @@ -1815,25 +1817,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.12" @@ -1845,7 +1828,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http", "indexmap", "slab", "tokio", @@ -1855,12 +1838,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1880,6 +1864,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hashlink" version = "0.10.0" @@ -1927,11 +1917,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1942,7 +1932,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1951,17 +1941,6 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -1973,17 +1952,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1991,7 +1959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -2002,8 +1970,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -2027,44 +1995,22 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2076,28 +2022,15 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.6.0", + "http", + "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.2", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", + "webpki-roots 1.0.4", ] [[package]] @@ -2108,7 +2041,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "native-tls", "tokio", @@ -2118,24 +2051,24 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", - "system-configuration 0.6.1", + "socket2", + "system-configuration", "tokio", "tower-service", "tracing", @@ -2144,9 +2077,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2154,7 +2087,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -2168,9 +2101,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2181,9 +2114,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -2194,11 +2127,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2209,42 +2141,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2260,9 +2188,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2281,13 +2209,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -2301,9 +2230,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.1" +version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "once_cell", @@ -2325,17 +2254,6 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2344,9 +2262,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2354,9 +2272,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2384,22 +2302,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -2408,19 +2326,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -2448,18 +2366,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link 0.2.1", ] [[package]] @@ -2470,13 +2388,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -2492,53 +2410,39 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "loom" -version = "0.7.2" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru-slab" @@ -2559,11 +2463,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -2572,6 +2476,17 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mcp-proxy" version = "0.1.0" @@ -2588,21 +2503,21 @@ dependencies = [ "futures", "futures-util", "hostname", - "http 1.3.1", + "http", "log", "once_cell", - "opentelemetry 0.30.0", + "opentelemetry 0.31.0", "opentelemetry-jaeger", - "opentelemetry-semantic-conventions 0.30.0", - "opentelemetry_sdk 0.30.0", - "rand 0.8.5", - "reqwest 0.12.23", - "rmcp 0.8.2", + "opentelemetry-semantic-conventions 0.31.0", + "opentelemetry_sdk 0.31.0", + "rand 0.9.2", + "reqwest", + "rmcp 0.8.5", "run_code_rmcp", "serde", "serde_json", "serde_yaml", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-util", @@ -2629,9 +2544,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -2662,17 +2577,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -2703,23 +2619,22 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", + "equivalent", "event-listener", "futures-util", - "loom", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "portable-atomic", "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -2732,7 +2647,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.3.1", + "http", "httparse", "memchr", "mime", @@ -2763,7 +2678,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -2790,21 +2705,19 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -2872,9 +2785,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -2887,9 +2800,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -2899,11 +2812,11 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -2931,9 +2844,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -2957,15 +2870,15 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.14", + "thiserror 2.0.17", "tracing", ] @@ -2993,9 +2906,9 @@ checksum = "1869fb4bb9b35c5ba8a1e40c9b128a7b4c010d07091e864a29da19e4fe2ca4d7" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" [[package]] name = "opentelemetry_sdk" @@ -3020,18 +2933,17 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.30.0", + "opentelemetry 0.31.0", "percent-encoding", "rand 0.9.2", - "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -3075,21 +2987,15 @@ dependencies = [ "aliyun-oss-rust-sdk", "async-trait", "chrono", - "reqwest 0.12.23", + "reqwest", "serde", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tracing", "uuid", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking" version = "2.2.1" @@ -3109,12 +3015,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -3133,15 +3039,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -3167,28 +3073,27 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", "serde", "serde_json", - "thiserror 2.0.14", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -3196,9 +3101,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -3209,9 +3114,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -3321,9 +3226,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -3400,18 +3305,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3432,19 +3337,18 @@ dependencies = [ [[package]] name = "proptest" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.1", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -3456,7 +3360,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -3471,9 +3375,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pulldown-cmark-to-cmark" -version = "21.0.0" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" dependencies = [ "pulldown-cmark", ] @@ -3518,9 +3422,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -3529,8 +3433,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", - "thiserror 2.0.14", + "socket2", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3538,12 +3442,12 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -3551,7 +3455,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.14", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3559,23 +3463,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3642,7 +3546,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -3694,11 +3598,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] @@ -3709,23 +3613,23 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -3734,47 +3638,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" @@ -3784,62 +3673,22 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-rustls", - "hyper-tls 0.6.0", + "hyper-tls", "hyper-util", "js-sys", "log", @@ -3853,7 +3702,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls", @@ -3866,7 +3715,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.4", ] [[package]] @@ -3875,7 +3724,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" dependencies = [ - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -3903,20 +3752,20 @@ dependencies = [ "bytes", "chrono", "futures", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "paste", "pin-project-lite", "process-wrap", "rand 0.9.2", - "reqwest 0.12.23", + "reqwest", "rmcp-macros 0.6.4", "schemars", "serde", "serde_json", "sse-stream", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-util", @@ -3927,29 +3776,29 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e35d31f89beb59c83bc31363426da25b323ce0c2e5b53c7bf29867d16ee7898" +checksum = "e5947688160b56fb6c827e3c20a72c90392a1d7e9dec74749197aa1780ac42ca" dependencies = [ "axum", "base64 0.22.1", "bytes", "chrono", "futures", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "paste", "pin-project-lite", "process-wrap", "rand 0.9.2", - "reqwest 0.12.23", - "rmcp-macros 0.8.2", + "reqwest", + "rmcp-macros 0.8.5", "schemars", "serde", "serde_json", "sse-stream", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-util", @@ -3964,7 +3813,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1827cd98dab34cade0513243c6fe0351f0f0b2c9d6825460bcf45b42804bdda0" dependencies = [ - "darling 0.21.1", + "darling 0.21.3", "proc-macro2", "quote", "serde_json", @@ -3973,11 +3822,11 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d88518b38110c439a03f0f4eee40e5105d648a530711cb87f98991e3f324a664" +checksum = "01263441d3f8635c628e33856c468b96ebbce1af2d3699ea712ca71432d4ee7a" dependencies = [ - "darling 0.21.1", + "darling 0.21.3", "proc-macro2", "quote", "serde_json", @@ -3991,7 +3840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.1", + "bitflags 2.10.0", "serde", "serde_derive", ] @@ -4007,7 +3856,7 @@ dependencies = [ "log", "rubato", "serde", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] @@ -4025,7 +3874,7 @@ dependencies = [ "rubato", "serde", "sysinfo", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tracing", "whisper-rs", @@ -4115,15 +3964,15 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4132,9 +3981,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", @@ -4145,9 +3994,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "sha2", "walkdir", @@ -4172,10 +4021,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.3.1", + "http", "mime", "rand 0.9.2", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -4201,9 +4050,9 @@ dependencies = [ [[package]] name = "rustfft" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" dependencies = [ "num-complex", "num-integer", @@ -4215,22 +4064,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -4241,29 +4090,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -4271,9 +4102,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -4288,9 +4119,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -4315,27 +4146,27 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "chrono", "dyn-clone", @@ -4347,9 +4178,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633" dependencies = [ "proc-macro2", "quote", @@ -4357,12 +4188,6 @@ dependencies = [ "syn", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -4381,7 +4206,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -4390,9 +4215,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -4400,15 +4225,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -4428,18 +4253,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4459,31 +4284,33 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -4522,7 +4349,7 @@ dependencies = [ "futures", "log", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "scc", "serial_test_derive", ] @@ -4645,22 +4472,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4724,7 +4541,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -4779,7 +4596,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -4809,7 +4626,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.14", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -4823,7 +4640,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -4848,7 +4665,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.14", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -4874,7 +4691,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.14", + "thiserror 2.0.17", "tracing", "url", "uuid", @@ -4888,16 +4705,16 @@ checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" dependencies = [ "bytes", "futures-util", - "http-body 1.0.1", + "http-body", "http-body-util", "pin-project-lite", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strength_reduce" @@ -4930,9 +4747,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symphonia" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", "symphonia-bundle-flac", @@ -4953,9 +4770,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-flac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" dependencies = [ "log", "symphonia-core", @@ -4965,9 +4782,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" dependencies = [ "lazy_static", "log", @@ -4977,9 +4794,9 @@ dependencies = [ [[package]] name = "symphonia-codec-aac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" dependencies = [ "lazy_static", "log", @@ -4988,9 +4805,9 @@ dependencies = [ [[package]] name = "symphonia-codec-adpcm" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" dependencies = [ "log", "symphonia-core", @@ -4998,9 +4815,9 @@ dependencies = [ [[package]] name = "symphonia-codec-alac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" dependencies = [ "log", "symphonia-core", @@ -5008,9 +4825,9 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" dependencies = [ "log", "symphonia-core", @@ -5018,9 +4835,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" dependencies = [ "log", "symphonia-core", @@ -5029,9 +4846,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" dependencies = [ "arrayvec", "bitflags 1.3.2", @@ -5042,9 +4859,9 @@ dependencies = [ [[package]] name = "symphonia-format-caf" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" +checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096" dependencies = [ "log", "symphonia-core", @@ -5053,9 +4870,9 @@ dependencies = [ [[package]] name = "symphonia-format-isomp4" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" dependencies = [ "encoding_rs", "log", @@ -5066,9 +4883,9 @@ dependencies = [ [[package]] name = "symphonia-format-mkv" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" dependencies = [ "lazy_static", "log", @@ -5079,9 +4896,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" dependencies = [ "log", "symphonia-core", @@ -5091,9 +4908,9 @@ dependencies = [ [[package]] name = "symphonia-format-riff" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" dependencies = [ "extended", "log", @@ -5103,9 +4920,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" dependencies = [ "encoding_rs", "lazy_static", @@ -5115,9 +4932,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -5125,21 +4942,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.105" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -5175,36 +4986,15 @@ dependencies = [ "windows 0.52.0", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "core-foundation", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -5236,15 +5026,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5297,11 +5087,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.17", ] [[package]] @@ -5317,9 +5107,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -5359,9 +5149,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -5374,15 +5164,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -5399,9 +5189,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -5419,9 +5209,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -5434,29 +5224,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -5475,9 +5262,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -5509,9 +5296,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -5521,9 +5308,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -5534,48 +5321,43 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "serde_core", "serde_spanned", - "toml_datetime 0.7.1", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_datetime" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime 0.6.11", + "toml_datetime", + "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -5589,7 +5371,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-util", "tower-layer", @@ -5604,12 +5386,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "async-compression", - "bitflags 2.9.1", + "bitflags 2.10.0", "bytes", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "http-range-header", "httpdate", @@ -5705,15 +5487,16 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" dependencies = [ "js-sys", - "once_cell", - "opentelemetry 0.30.0", - "opentelemetry_sdk 0.30.0", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", + "rustversion", "smallvec", + "thiserror 2.0.17", "tracing", "tracing-core", "tracing-log", @@ -5723,14 +5506,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -5757,18 +5540,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.3.1", + "http", "httparse", "log", "rand 0.9.2", "sha1", - "thiserror 2.0.14", + "thiserror 2.0.17", "utf-8", ] @@ -5780,9 +5563,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -5820,24 +5603,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -5847,9 +5630,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unsafe-libyaml" @@ -5871,20 +5654,19 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "ureq" -version = "3.1.2" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" dependencies = [ "base64 0.22.1", "flate2", "log", "percent-encoding", "rustls", - "rustls-pemfile 2.2.0", "rustls-pki-types", "ureq-proto", "utf-8", - "webpki-roots 1.0.2", + "webpki-roots 1.0.4", ] [[package]] @@ -5894,20 +5676,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ "base64 0.22.1", - "http 1.3.1", + "http", "httparse", "log", ] [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -5979,12 +5762,12 @@ dependencies = [ [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "atomic", - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "md-5", "serde", @@ -6035,17 +5818,17 @@ dependencies = [ "ffmpeg-sidecar", "futures", "infer", - "opentelemetry 0.30.0", + "opentelemetry 0.31.0", "opentelemetry-jaeger", - "opentelemetry-semantic-conventions 0.30.0", - "reqwest 0.12.23", + "opentelemetry-semantic-conventions 0.31.0", + "reqwest", "serde", "serde_json", "serde_yaml", "sqlx", "symphonia", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-util", "tower", @@ -6070,7 +5853,7 @@ checksum = "2a1e0dd17b6cbf95f04cbd10e97158c08888b973634de9b03b46ffa9c0bfc7cd" dependencies = [ "rs-voice-toolkit-audio", "rs-voice-toolkit-stt", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -6108,12 +5891,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -6124,35 +5907,22 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -6163,9 +5933,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6173,22 +5943,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -6208,9 +5978,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -6232,14 +6002,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.4", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -6294,11 +6064,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6326,7 +6096,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -6356,9 +6126,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -6368,15 +6151,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -6385,9 +6168,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -6400,6 +6183,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -6407,7 +6196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -6416,9 +6205,9 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -6427,7 +6216,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -6436,7 +6234,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -6472,7 +6279,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -6508,19 +6324,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -6529,7 +6345,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -6546,9 +6362,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -6564,9 +6380,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -6582,9 +6398,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -6594,9 +6410,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -6612,9 +6428,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -6630,9 +6446,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -6648,9 +6464,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -6666,43 +6482,32 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wiremock" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "async-trait", "base64 0.22.1", "deadpool", "futures", - "http 1.3.1", + "http", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "log", "once_cell", @@ -6714,25 +6519,22 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", @@ -6766,11 +6568,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -6778,9 +6579,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -6790,18 +6591,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -6831,15 +6632,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -6848,9 +6649,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -6859,9 +6660,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -6898,15 +6699,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", @@ -6934,9 +6735,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 1e13920..32dd9ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,24 +14,25 @@ rmcp = { version = "0.8", features = [ "transport-sse-server", "transport-sse-client", "transport-streamable-http-client", + "transport-streamable-http-client-reqwest", "transport-streamable-http-server", "transport-child-process", "transport-io", "reqwest", "transport-sse-client-reqwest" ] } -tokio = { version = "1", features = ["macros", "net", "rt", "rt-multi-thread"] } +tokio = { version = "1.48", features = ["macros", "net", "rt", "rt-multi-thread"] } tokio-util = "0.7" # Logging and tracing tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "=0.2.2" -tracing-opentelemetry = "0.31" -opentelemetry = { version = "0.30", features = ["trace"] } +tracing-opentelemetry = "0.32" +opentelemetry = { version = "0.31", features = ["trace"] } opentelemetry-jaeger = { version = "0.22", features = ["rt-tokio"] } -opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"] } -opentelemetry_sdk = "0.30" +opentelemetry-semantic-conventions = { version = "0.31", features = ["semconv_experimental"] } +opentelemetry_sdk = "0.31" hostname = "0.4" uuid = { version = "1.18", features = ["v4", "v7"] } rand = "0.9" @@ -70,17 +71,19 @@ serde_yaml = "=0.9.33" serde_with = "3.12" reqwest = { version = "0.12", features = ["json"] } http = "1.3" -aliyun-oss-rust-sdk = "0.2" +aliyun-oss-rust-sdk = { version = "0.2", default-features = false, features = ["async"] } clap = { version = "4.5", features = ["derive", "env"] } futures = "0.3" run_code_rmcp = "0.0.33" derive_builder = "0.20" bytes = "1.0" base64 = "0.22" -tempfile = "3.22" +tempfile = "3.23" dirs = "6.0" scopeguard = "1.2" +async-trait = "0.1" + # 自己开发的语音相关工具 voice-toolkit = { version = "0.16", features = ["stt", "audio"] } @@ -93,3 +96,7 @@ sled = "0.34" # Audio format detection and processing symphonia = { version = "0.5", features = ["all"] } bincode = "2.0" +tokio-stream = "0.1.17" +backtrace = "0.3" +tracing-futures = "0.2.5" +urlencoding = "2.1.3" diff --git a/Makefile b/Makefile index 67490f7..22dc40f 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,12 @@ run: @echo "🚀 运行 document-parser..." docker run --rm -p 8080:8080 $(IMAGE_NAME):latest +# 启动 mcp-proxy +.PHONY: run-mcp-proxy +run-mcp-proxy: + @echo "🚀 启动 mcp-proxy..." + cargo run --bin mcp-proxy 2>&1 | tee /Volumes/soddygo/git_work/mcp-proxy/mcp-proxy/tmp/test.log + # 检查 Docker buildx 是否可用 .PHONY: check-buildx check-buildx: @@ -150,6 +156,7 @@ help: @echo "" @echo " 🚀 运行命令:" @echo " make run - 运行 document-parser Docker 镜像" + @echo " make run-mcp-proxy - 启动 mcp-proxy 并输出日志到 tmp/test.log" @echo "" @echo " 🛠️ 工具命令:" @echo " make check-buildx - 检查 Docker buildx 状态" diff --git a/document-parser/examples/markdown_image_processing.rs b/document-parser/examples/markdown_image_processing.rs index ccd5103..1af2071 100644 --- a/document-parser/examples/markdown_image_processing.rs +++ b/document-parser/examples/markdown_image_processing.rs @@ -3,9 +3,7 @@ use document_parser::models::ImageInfo; use document_parser::parsers::DualEngineParser; use document_parser::processors::MarkdownProcessor; use document_parser::processors::markdown_processor::MarkdownProcessorConfig; -use document_parser::services::{ - DocumentService, ImageProcessor, TaskService, -}; +use document_parser::services::{DocumentService, ImageProcessor, TaskService}; use std::sync::Arc; use tokio::fs; diff --git a/document-parser/src/app_state.rs b/document-parser/src/app_state.rs index bd40534..34d4173 100644 --- a/document-parser/src/app_state.rs +++ b/document-parser/src/app_state.rs @@ -154,8 +154,8 @@ impl AppState { } // 打开数据库 - let db = sled::open(db_path) - .map_err(|e| AppError::Database(format!("无法打开数据库: {e}")))?; + let db = + sled::open(db_path).map_err(|e| AppError::Database(format!("无法打开数据库: {e}")))?; // 设置缓存容量(Sled 0.34版本不支持set_cache_capacity方法) // db.set_cache_capacity(config.storage.sled.cache_capacity); diff --git a/document-parser/src/config.rs b/document-parser/src/config.rs index 795c222..b16e22a 100644 --- a/document-parser/src/config.rs +++ b/document-parser/src/config.rs @@ -295,8 +295,7 @@ impl ConfigBuilder { } /// CUDA环境状态 -#[derive(Debug, Clone)] -#[derive(Default)] +#[derive(Debug, Clone, Default)] pub struct CudaStatus { pub available: bool, pub version: Option, @@ -304,7 +303,6 @@ pub struct CudaStatus { pub recommended_device: Option, } - /// 应用配置 #[derive(Debug, Clone, Deserialize)] pub struct AppConfig { @@ -477,8 +475,7 @@ pub struct MinerUConfig { } /// 质量级别 -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, PartialEq, Deserialize, Default)] pub enum QualityLevel { Fast, #[default] @@ -486,7 +483,6 @@ pub enum QualityLevel { HighQuality, } - fn default_batch_size() -> usize { 1 } @@ -1246,7 +1242,7 @@ pub fn get_global_cuda_status_clone() -> CudaStatus { #[cfg(test)] mod tests { use super::*; - + use std::env; use tempfile::TempDir; diff --git a/document-parser/src/handlers/markdown_handler.rs b/document-parser/src/handlers/markdown_handler.rs index f49cee9..71b6629 100644 --- a/document-parser/src/handlers/markdown_handler.rs +++ b/document-parser/src/handlers/markdown_handler.rs @@ -17,81 +17,81 @@ use tracing::{error, info, warn}; use utoipa::ToSchema; /// Markdown处理请求参数 -/// +/// /// 用于配置Markdown文档处理的各项参数,支持自定义TOC生成、锚点设置、缓存等选项。 #[derive(Debug, Deserialize, ToSchema)] pub struct MarkdownProcessRequest { /// 是否启用目录(Table of Contents)生成 /// 当设置为true时,会自动解析文档标题并生成层级目录结构 pub enable_toc: Option, - + /// 目录的最大深度限制 /// 控制生成的目录层级数量,避免过深的嵌套结构 pub max_toc_depth: Option, - + /// 是否启用锚点(Anchor)功能 /// 为每个标题生成锚点链接,便于文档内部导航 pub enable_anchors: Option, - + /// 是否启用缓存功能 /// 缓存处理结果以提高重复请求的响应速度 pub enable_cache: Option, } /// Markdown URL响应 -/// +/// /// 表示Markdown文档处理完成后的访问链接信息,包含临时URL、文件元数据等。 #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct MarkdownUrlResponse { /// 文档的访问URL,可以是临时链接或永久链接 pub url: String, - + /// 文档处理任务的唯一标识符 pub task_id: String, - + /// 标记URL是否为临时链接 /// true表示临时链接,false表示永久链接 pub temporary: bool, - + /// 临时URL的过期时间(小时) /// 仅当temporary为true时有效,None表示永不过期 pub expires_in_hours: Option, - + /// 文档文件的大小(字节) pub file_size: Option, - + /// 文档的MIME类型,如 "text/markdown"、"application/pdf" 等 pub content_type: String, - + /// 存储在OSS中的文件名 /// 用于OSS存储系统的文件标识 pub oss_file_name: Option, - + /// OSS存储桶名称 /// 指定文档存储的OSS存储桶 pub oss_bucket: Option, } /// 同步处理响应 -/// +/// /// 表示Markdown文档同步处理完成后的结果,包含结构化文档和性能指标。 #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct SectionsSyncResponse { /// 处理完成的结构化文档对象 /// 包含完整的文档结构、目录和内容信息 pub document: StructuredDocument, - + /// 文档处理耗时(毫秒) /// 用于性能监控和优化参考 pub processing_time_ms: u64, - + /// 文档的总字数统计 /// 可选字段,用于内容分析和统计 pub word_count: Option, } /// 下载参数 -/// +/// /// 用于配置文档下载行为的参数,支持临时URL生成、格式选择等选项。 #[derive(Debug, Deserialize, ToSchema)] pub struct DownloadParams { @@ -99,17 +99,17 @@ pub struct DownloadParams { /// 当设置为true时,生成有时效性的临时下载链接 #[serde(default, skip_serializing_if = "Option::is_none")] pub temp: Option, - + /// 临时URL过期时间(小时) /// 控制临时下载链接的有效期,仅在temp为true时生效 #[serde(default, skip_serializing_if = "Option::is_none")] pub expires_hours: Option, - + /// 是否强制重新生成URL /// 忽略缓存,强制生成新的下载链接 #[serde(default, skip_serializing_if = "Option::is_none")] pub force_regenerate: Option, - + /// 下载格式 /// 指定文档的下载格式,如 "pdf"、"docx"、"html" 等 #[serde(default, skip_serializing_if = "Option::is_none")] @@ -117,22 +117,22 @@ pub struct DownloadParams { } /// 流式下载配置 -/// +/// /// 用于配置大文件流式下载的参数,优化内存使用和传输性能。 #[derive(Debug, Clone)] pub struct StreamingConfig { /// 每个数据块的大小(字节) /// 控制流式传输时每个数据块的大小,影响内存使用和网络效率 pub chunk_size: usize, - + /// 缓冲区大小(字节) /// 用于临时存储传输数据的缓冲区容量 pub buffer_size: usize, - + /// 是否启用压缩 /// 在传输过程中启用数据压缩,减少网络带宽使用 pub enable_compression: bool, - + /// 支持的最大文件大小(字节) /// 超过此大小的文件将使用流式下载,避免内存溢出 pub max_file_size: u64, @@ -233,8 +233,6 @@ async fn process_markdown_content( /// 处理Markdown multipart上传 async fn process_markdown_multipart(multipart: &mut Multipart) -> Result { - - let max_markdown_size = get_file_size_limit(&FileSizePurpose::ContentValidation).bytes() as usize; let mut content: Option = None; @@ -771,10 +769,8 @@ pub async fn get_markdown_url( } Err(e) => { error!("生成临时URL失败: task_id={}, error={}", task_id, e); - ApiResponse::internal_error::(&format!( - "生成下载URL失败: {e}" - )) - .into_response() + ApiResponse::internal_error::(&format!("生成下载URL失败: {e}")) + .into_response() } } } else { @@ -945,10 +941,8 @@ fn build_range_response( let mut range_headers = headers; range_headers.insert( header::CONTENT_RANGE, - header::HeaderValue::from_str(&format!( - "bytes {start}-{end}/{total_len}" - )) - .unwrap_or(header::HeaderValue::from_static("bytes */*")), + header::HeaderValue::from_str(&format!("bytes {start}-{end}/{total_len}")) + .unwrap_or(header::HeaderValue::from_static("bytes */*")), ); range_headers.insert( header::CONTENT_LENGTH, diff --git a/document-parser/src/handlers/task_handler.rs b/document-parser/src/handlers/task_handler.rs index fdbaf85..cd451da 100644 --- a/document-parser/src/handlers/task_handler.rs +++ b/document-parser/src/handlers/task_handler.rs @@ -1,7 +1,5 @@ use crate::error::AppError; -use crate::handlers::response::{ - ApiResponse, BatchOperationResponse, TaskOperationResponse, -}; +use crate::handlers::response::{ApiResponse, BatchOperationResponse, TaskOperationResponse}; use crate::handlers::validation::RequestValidator; use crate::models::{ DocumentFormat, DocumentTask, HttpResult, ParserEngine, SourceType, TaskStatus, @@ -741,8 +739,7 @@ pub async fn cleanup_expired_tasks( match state.task_service.cleanup_expired_tasks().await { Ok(count) => { info!("清理过期任务完成,删除了 {} 个任务", count); - ApiResponse::message(format!("清理过期任务完成,删除了 {count} 个任务")) - .into_response() + ApiResponse::message(format!("清理过期任务完成,删除了 {count} 个任务")).into_response() } Err(e) => { error!("清理过期任务失败: {}", e); diff --git a/document-parser/src/handlers/toc_handler.rs b/document-parser/src/handlers/toc_handler.rs index 07734d1..7c46333 100644 --- a/document-parser/src/handlers/toc_handler.rs +++ b/document-parser/src/handlers/toc_handler.rs @@ -8,66 +8,66 @@ use serde::Serialize; use utoipa::ToSchema; /// 目录响应结构 -/// +/// /// 表示文档目录处理完成后的结果,包含任务ID、目录结构和统计信息。 #[derive(Debug, Serialize, ToSchema)] pub struct TocResponse { /// 文档处理任务的唯一标识符 /// 用于关联请求和响应,支持异步处理和状态查询 pub task_id: String, - + /// 文档的目录结构 /// 包含所有章节和子章节的层级结构,支持无限嵌套 pub toc: Vec, - + /// 目录中章节的总数量 /// 用于统计分析和分页显示 pub total_sections: usize, } /// 章节响应结构 -/// +/// /// 表示单个章节的详细信息,用于章节内容的展示和编辑。 #[derive(Debug, Serialize, ToSchema)] pub struct SectionResponse { /// 章节的唯一标识符 /// 用于章节的定位、引用和更新操作 pub section_id: String, - + /// 章节的标题或名称 /// 显示在目录和导航中的章节标题 pub title: String, - + /// 章节的正文内容 /// 包含章节的完整文本内容,支持Markdown格式 pub content: String, - + /// 章节的层级深度 /// 1表示顶级章节,2表示二级章节,以此类推 pub level: u8, - + /// 是否包含子章节 /// 用于判断章节是否可以展开显示子章节 pub has_children: bool, } /// 章节列表响应结构 -/// +/// /// 表示文档所有章节的完整信息,包含文档元数据和章节结构。 #[derive(Debug, Serialize, ToSchema)] pub struct SectionsResponse { /// 文档处理任务的唯一标识符 /// 用于关联请求和响应,支持异步处理和状态查询 pub task_id: String, - + /// 文档的标题或名称 /// 显示在界面中的文档标题 pub document_title: String, - + /// 文档的完整目录结构 /// 包含所有章节和子章节的层级结构,支持无限嵌套 pub toc: Vec, - + /// 文档中章节的总数量 /// 用于统计分析和分页显示 pub total_sections: usize, diff --git a/document-parser/src/handlers/validation.rs b/document-parser/src/handlers/validation.rs index 1b5c59a..482f7f8 100644 --- a/document-parser/src/handlers/validation.rs +++ b/document-parser/src/handlers/validation.rs @@ -42,8 +42,8 @@ impl RequestValidator { /// 验证URL格式 pub fn validate_url(url_str: &str) -> Result { - let url = Url::parse(url_str) - .map_err(|e| AppError::Validation(format!("无效的URL格式: {e}")))?; + let url = + Url::parse(url_str).map_err(|e| AppError::Validation(format!("无效的URL格式: {e}")))?; // 检查协议 if !matches!(url.scheme(), "http" | "https") { @@ -65,8 +65,8 @@ impl RequestValidator { /// 验证URL格式(仅验证,不返回解析后的URL) pub fn validate_url_format(url_str: &str) -> Result<(), AppError> { - let _url = Url::parse(url_str) - .map_err(|e| AppError::Validation(format!("无效的URL格式: {e}")))?; + let _url = + Url::parse(url_str).map_err(|e| AppError::Validation(format!("无效的URL格式: {e}")))?; // 检查协议 if !url_str.starts_with("http://") && !url_str.starts_with("https://") { @@ -143,9 +143,7 @@ impl RequestValidator { let sort_by = sort_by.unwrap_or("created_at"); if !allowed_sort_fields.contains(sort_by) { - return Err(AppError::Validation(format!( - "不支持的排序字段: {sort_by}" - ))); + return Err(AppError::Validation(format!("不支持的排序字段: {sort_by}"))); } let sort_order = sort_order.unwrap_or("desc"); diff --git a/document-parser/src/main.rs b/document-parser/src/main.rs index ff2c19e..b316c32 100644 --- a/document-parser/src/main.rs +++ b/document-parser/src/main.rs @@ -180,7 +180,7 @@ async fn main() -> Result<()> { Ok(cuda_info) => { let recommended_device = if cuda_info.available && !cuda_info.devices.is_empty() { // 选择显存最大的设备作为推荐设备 - + cuda_info .devices .iter() diff --git a/document-parser/src/middleware/error_handler.rs b/document-parser/src/middleware/error_handler.rs index 89c818d..4a952d0 100644 --- a/document-parser/src/middleware/error_handler.rs +++ b/document-parser/src/middleware/error_handler.rs @@ -102,9 +102,7 @@ impl RateLimiter { let now = SystemTime::now(); let mut requests = self.requests.lock().unwrap(); - let client_requests = requests - .entry(client_ip.to_string()) - .or_default(); + let client_requests = requests.entry(client_ip.to_string()).or_default(); // 清理过期的请求记录 client_requests.retain(|&time| { diff --git a/document-parser/src/models/document_task.rs b/document-parser/src/models/document_task.rs index 324db17..e1595a9 100644 --- a/document-parser/src/models/document_task.rs +++ b/document-parser/src/models/document_task.rs @@ -1,8 +1,7 @@ use crate::config::{FileSizePurpose, get_global_file_size_config}; use crate::error::AppError; use crate::models::{ - DocumentFormat, OssData, ParserEngine, StructuredDocument, TaskError, - TaskStatus, + DocumentFormat, OssData, ParserEngine, StructuredDocument, TaskError, TaskStatus, }; use chrono::{DateTime, Duration, Utc}; use derive_builder::Builder; diff --git a/document-parser/src/models/structured_document.rs b/document-parser/src/models/structured_document.rs index b071386..bfb804f 100644 --- a/document-parser/src/models/structured_document.rs +++ b/document-parser/src/models/structured_document.rs @@ -6,29 +6,29 @@ use std::collections::HashMap; use utoipa::ToSchema; /// 统一结构化文档 -/// +/// /// 表示一个完整的结构化文档,包含文档的基本信息、目录结构、性能指标等。 /// 支持快速查找和索引功能,适用于大型文档的高效处理。 #[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)] pub struct StructuredDocument { /// 文档处理任务的唯一标识符 pub task_id: String, - + /// 文档的标题或名称 pub document_title: String, - + /// 文档的目录结构,包含所有章节和子章节 pub toc: Vec, - + /// 文档中章节的总数量 pub total_sections: usize, - + /// 文档最后更新的时间戳(UTC时区) pub last_updated: DateTime, - + /// 文档的总字数统计(可选) pub word_count: Option, - + /// 文档处理所需的时间(可选,格式如 "2.5s") pub processing_time: Option, @@ -37,12 +37,12 @@ pub struct StructuredDocument { /// 序列化时跳过此字段 #[serde(skip)] section_index: HashMap, // ID -> index mapping for O(1) lookup - + /// 章节层级到索引列表的映射,用于按层级快速检索 /// 序列化时跳过此字段 #[serde(skip)] level_index: HashMap>, // Level -> indices mapping - + /// 标记索引是否已构建,避免重复构建索引 /// 序列化时跳过此字段 #[serde(skip)] @@ -50,38 +50,38 @@ pub struct StructuredDocument { } /// 结构化章节 -/// +/// /// 表示文档中的一个章节或段落,支持嵌套的层级结构。 /// 包含内容、元数据和性能优化字段,适用于大型内容的处理。 #[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)] pub struct StructuredSection { /// 章节的唯一标识符,通常基于标题生成 pub id: String, - + /// 章节的标题或名称 pub title: String, - + /// 章节的层级深度,1表示顶级章节,2表示二级章节,以此类推 pub level: u8, - + /// 章节的正文内容 pub content: String, - + /// 子章节列表,支持无限层级的嵌套结构 /// 当为空时序列化时会被跳过,避免空数组的序列化 #[serde(skip_serializing_if = "Vec::is_empty", default)] #[schema(no_recursion)] pub children: Vec>, - + /// 标记章节是否已被编辑过(可选) pub is_edited: Option, - + /// 章节的字数统计(可选) pub word_count: Option, - + /// 章节在原文中的起始位置(可选,用于定位和引用) pub start_pos: Option, - + /// 章节在原文中的结束位置(可选,用于定位和引用) pub end_pos: Option, @@ -90,7 +90,7 @@ pub struct StructuredSection { /// 序列化时跳过此字段 #[serde(skip)] content_hash: Option, // For change detection - + /// 标记内容是否超过阈值,用于性能优化策略 /// 序列化时跳过此字段 #[serde(skip)] @@ -730,9 +730,7 @@ impl StructuredDocument { } Ok(changed) } else { - Err(AppError::Validation(format!( - "未找到章节ID: {section_id}" - ))) + Err(AppError::Validation(format!("未找到章节ID: {section_id}"))) } } diff --git a/document-parser/src/models/task_status.rs b/document-parser/src/models/task_status.rs index 25a2481..84edb53 100644 --- a/document-parser/src/models/task_status.rs +++ b/document-parser/src/models/task_status.rs @@ -370,7 +370,7 @@ impl TaskStatus { desc } TaskStatus::Completed { - completed_at:_, + completed_at: _, processing_time, result_summary, } => { @@ -382,7 +382,7 @@ impl TaskStatus { } TaskStatus::Failed { error, - failed_at:_, + failed_at: _, retry_count, is_recoverable, } => { @@ -396,7 +396,7 @@ impl TaskStatus { desc } TaskStatus::Cancelled { - cancelled_at:_, + cancelled_at: _, reason, } => { let mut desc = "任务已取消".to_string(); diff --git a/document-parser/src/parsers/format_detector.rs b/document-parser/src/parsers/format_detector.rs index cc84858..b659d4f 100644 --- a/document-parser/src/parsers/format_detector.rs +++ b/document-parser/src/parsers/format_detector.rs @@ -108,7 +108,8 @@ impl FormatDetector { // 安全地获取文件大小限制,如果全局配置未初始化则使用默认值 let max_file_size = std::panic::catch_unwind(|| { get_file_size_limit(&FileSizePurpose::FormatDetector).bytes() - }).unwrap_or(100 * 1024 * 1024); + }) + .unwrap_or(100 * 1024 * 1024); Self { custom_mappings: HashMap::new(), @@ -904,7 +905,8 @@ impl SecurityConfig { // 安全地获取文件大小限制,如果全局配置未初始化则使用默认值 let max_allowed_size = std::panic::catch_unwind(|| { get_file_size_limit(&FileSizePurpose::FormatDetector).bytes() - }).unwrap_or(100 * 1024 * 1024); + }) + .unwrap_or(100 * 1024 * 1024); Self { enable_size_check: true, diff --git a/document-parser/src/parsers/markitdown_parser.rs b/document-parser/src/parsers/markitdown_parser.rs index 183c01c..cd1c4b4 100644 --- a/document-parser/src/parsers/markitdown_parser.rs +++ b/document-parser/src/parsers/markitdown_parser.rs @@ -471,9 +471,7 @@ impl MarkItDownParser { progress_callback(MarkItDownProgress { stage: ProcessingStage::Completed, progress: 100.0, - message: format!( - "解析完成,耗时: {processing_time:?},字数: {word_count}" - ), + message: format!("解析完成,耗时: {processing_time:?},字数: {word_count}"), elapsed_time: processing_time, current_file: Some(file_path.to_string()), }); @@ -660,7 +658,8 @@ impl MarkItDownParser { } } } - }).await; + }) + .await; // 清理任务 stdout_task.abort(); diff --git a/document-parser/src/parsers/mineru_parser.rs b/document-parser/src/parsers/mineru_parser.rs index 448875f..ec5ffee 100644 --- a/document-parser/src/parsers/mineru_parser.rs +++ b/document-parser/src/parsers/mineru_parser.rs @@ -131,14 +131,13 @@ impl MinerUParser { }; // 尝试从全局配置获取MinerU配置,如果失败则使用默认值 - let (backend, device) = - match std::panic::catch_unwind(crate::config::get_global_config) { - Ok(global_config) => ( - global_config.mineru.backend.clone(), - global_config.mineru.device.clone(), - ), - Err(_) => ("pipeline".to_string(), "cpu".to_string()), - }; + let (backend, device) = match std::panic::catch_unwind(crate::config::get_global_config) { + Ok(global_config) => ( + global_config.mineru.backend.clone(), + global_config.mineru.device.clone(), + ), + Err(_) => ("pipeline".to_string(), "cpu".to_string()), + }; let config = MinerUConfig { python_path: python_path.to_string_lossy().to_string(), @@ -393,9 +392,7 @@ impl MinerUParser { progress_callback(ParseProgress { stage: ParseStage::Completed, progress: 100.0, - message: format!( - "解析完成,耗时: {processing_time:?},字数: {word_count}" - ), + message: format!("解析完成,耗时: {processing_time:?},字数: {word_count}"), elapsed_time: processing_time, }); diff --git a/document-parser/src/performance/mod.rs b/document-parser/src/performance/mod.rs index 982f2aa..c12cded 100644 --- a/document-parser/src/performance/mod.rs +++ b/document-parser/src/performance/mod.rs @@ -420,7 +420,6 @@ pub trait PerformanceOptimizable { #[cfg(test)] mod tests { use super::*; - #[tokio::test] async fn test_performance_optimizer_creation() { diff --git a/document-parser/src/processors/markdown_processor.rs b/document-parser/src/processors/markdown_processor.rs index dd51c2b..d176ee4 100644 --- a/document-parser/src/processors/markdown_processor.rs +++ b/document-parser/src/processors/markdown_processor.rs @@ -44,11 +44,11 @@ impl MarkdownProcessorConfig { /// 使用全局配置创建Markdown处理器配置 pub fn with_global_config() -> Self { // 安全地获取大文档阈值,如果全局配置未初始化则使用默认值 - let large_document_threshold = - match std::panic::catch_unwind(get_large_document_threshold) { - Ok(threshold) => threshold as usize, - Err(_) => 10 * 1024 * 1024, // 默认10MB - }; + let large_document_threshold = match std::panic::catch_unwind(get_large_document_threshold) + { + Ok(threshold) => threshold as usize, + Err(_) => 10 * 1024 * 1024, // 默认10MB + }; Self { enable_toc: true, @@ -1120,8 +1120,6 @@ impl Default for MarkdownProcessor { mod tests { use super::*; use crate::services::{ImageProcessor, ImageProcessorConfig}; - - #[tokio::test] async fn test_markdown_processor_basic() { diff --git a/document-parser/src/production/production_logging.rs b/document-parser/src/production/production_logging.rs index 3b7d2cc..64babdf 100644 --- a/document-parser/src/production/production_logging.rs +++ b/document-parser/src/production/production_logging.rs @@ -558,9 +558,9 @@ impl LogFilter for ModuleFilter { .denied_modules .iter() .any(|m| entry.module.starts_with(m)) - { - return false; - } + { + return false; + } if !self.allowed_modules.is_empty() { return self diff --git a/document-parser/src/routes.rs b/document-parser/src/routes.rs index 5f98e30..9c44642 100644 --- a/document-parser/src/routes.rs +++ b/document-parser/src/routes.rs @@ -123,4 +123,4 @@ fn oss_routes() -> Router { ) // 删除OSS文件 .route("/delete", get(private_oss_handler::delete_file_from_oss)) -} \ No newline at end of file +} diff --git a/document-parser/src/services/document_service.rs b/document-parser/src/services/document_service.rs index 905b9f8..056c878 100644 --- a/document-parser/src/services/document_service.rs +++ b/document-parser/src/services/document_service.rs @@ -798,7 +798,6 @@ impl DocumentService { // Parse document debug!("开始解析文档: {}", file_path); - self.parse_document(task_id, &file_path).await } diff --git a/document-parser/src/services/storage_service.rs b/document-parser/src/services/storage_service.rs index efeb129..e74285f 100644 --- a/document-parser/src/services/storage_service.rs +++ b/document-parser/src/services/storage_service.rs @@ -272,12 +272,8 @@ impl StorageService { .fetch_add(1, std::sync::atomic::Ordering::Relaxed); Ok(()) } - Err(TransactionError::Abort(e)) => { - Err(AppError::Database(format!("事务中止: {e:?}"))) - } - Err(TransactionError::Storage(e)) => { - Err(AppError::Database(format!("存储错误: {e}"))) - } + Err(TransactionError::Abort(e)) => Err(AppError::Database(format!("事务中止: {e:?}"))), + Err(TransactionError::Storage(e)) => Err(AppError::Database(format!("存储错误: {e}"))), } } @@ -529,8 +525,7 @@ impl StorageService { // 遍历所有任务 for result in self.tasks_tree.scan_prefix(TASK_PREFIX.as_bytes()) { - let (_, data) = - result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; + let (_, data) = result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; let task: DocumentTask = serde_json::from_slice(&data) .map_err(|e| AppError::Database(format!("反序列化任务失败: {e}")))?; @@ -572,8 +567,7 @@ impl StorageService { // 统计任务数量和大小 for result in self.tasks_tree.scan_prefix(TASK_PREFIX.as_bytes()) { - let (_, data) = - result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; + let (_, data) = result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; total_tasks += 1; total_size_bytes += data.len() as u64; } @@ -694,8 +688,7 @@ impl StorageService { let mut expired_tasks = Vec::new(); for result in self.tasks_tree.scan_prefix(TASK_PREFIX.as_bytes()) { - let (_, data) = - result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; + let (_, data) = result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; let task: DocumentTask = serde_json::from_slice(&data) .map_err(|e| AppError::Database(format!("反序列化任务失败: {e}")))?; @@ -840,8 +833,7 @@ impl StorageService { // 导出所有任务 let mut tasks = Vec::new(); for result in self.tasks_tree.scan_prefix(TASK_PREFIX.as_bytes()) { - let (_, data) = - result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; + let (_, data) = result.map_err(|e| AppError::Database(format!("扫描任务失败: {e}")))?; let task: DocumentTask = serde_json::from_slice(&data) .map_err(|e| AppError::Database(format!("反序列化任务失败: {e}")))?; tasks.push(task); diff --git a/document-parser/src/services/task_queue_service.rs b/document-parser/src/services/task_queue_service.rs index 198ddbd..0b65ecc 100644 --- a/document-parser/src/services/task_queue_service.rs +++ b/document-parser/src/services/task_queue_service.rs @@ -677,7 +677,7 @@ impl TaskQueueService { #[cfg(test)] mod tests { use super::*; - + use std::sync::atomic::{AtomicUsize, Ordering}; use tempfile::TempDir; diff --git a/document-parser/src/tests/current_directory_workflow_tests.rs b/document-parser/src/tests/current_directory_workflow_tests.rs index 728f683..c11f664 100644 --- a/document-parser/src/tests/current_directory_workflow_tests.rs +++ b/document-parser/src/tests/current_directory_workflow_tests.rs @@ -11,9 +11,9 @@ #[cfg(test)] mod tests { + use crate::AppState; use crate::models::{DocumentFormat, DocumentTask, SourceType, TaskStatus}; use crate::utils::environment_manager::EnvironmentManager; - use crate::AppState; use std::path::{Path, PathBuf}; use tempfile::TempDir; use tokio::fs; @@ -120,7 +120,8 @@ mod tests { // 创建pyvenv.cfg文件 let pyvenv_cfg = - "home = /usr/bin\ninclude-system-site-packages = false\nversion = 3.9.0\n".to_string(); + "home = /usr/bin\ninclude-system-site-packages = false\nversion = 3.9.0\n" + .to_string(); fs::write(venv_path.join("pyvenv.cfg"), pyvenv_cfg).await?; Ok(()) @@ -646,9 +647,7 @@ mod tests { } Err(e) => { // 清理可能因权限问题失败,这在某些测试环境中是预期的 - println!( - "Cleanup failed (may be expected in test environment): {e}" - ); + println!("Cleanup failed (may be expected in test environment): {e}"); } } diff --git a/document-parser/src/tests/environment_manager_enhanced_tests.rs b/document-parser/src/tests/environment_manager_enhanced_tests.rs index 1ba7383..fe48ffc 100644 --- a/document-parser/src/tests/environment_manager_enhanced_tests.rs +++ b/document-parser/src/tests/environment_manager_enhanced_tests.rs @@ -1,8 +1,6 @@ #[cfg(test)] mod tests { - use crate::utils::environment_manager::{ - EnvironmentManager, EnvironmentStatus, - }; + use crate::utils::environment_manager::{EnvironmentManager, EnvironmentStatus}; #[tokio::test] async fn test_virtual_env_status_reporting() { diff --git a/document-parser/src/tests/handlers.rs b/document-parser/src/tests/handlers.rs index 84ae9ac..b135772 100644 --- a/document-parser/src/tests/handlers.rs +++ b/document-parser/src/tests/handlers.rs @@ -12,7 +12,6 @@ use crate::models::*; #[cfg(test)] mod document_handler_tests { use super::*; - #[tokio::test] async fn test_upload_document_success() { @@ -589,12 +588,7 @@ mod response_format_tests { #[cfg(test)] mod comprehensive_handler_tests { use super::*; - use axum::{ - body::Body, - http::Request, - }; - - + use axum::{body::Body, http::Request}; #[tokio::test] async fn test_document_upload_validation() { @@ -933,7 +927,6 @@ mod comprehensive_handler_tests { #[cfg(test)] mod handler_integration_tests { use super::*; - #[tokio::test] async fn test_complete_document_processing_workflow() { diff --git a/document-parser/src/tests/mod.rs b/document-parser/src/tests/mod.rs index 7a4dcf6..632fe0c 100644 --- a/document-parser/src/tests/mod.rs +++ b/document-parser/src/tests/mod.rs @@ -47,8 +47,6 @@ pub mod test_helpers { .expect("Failed to create test app state") } - - /// 创建用于文件大小测试的应用状态 pub async fn create_test_app_state_for_file_size_test( max_mb: u64, @@ -263,7 +261,6 @@ pub mod test_helpers { .to_string() } - /// 创建测试用的任务ID pub fn create_test_task_id() -> String { uuid::Uuid::new_v4().to_string() diff --git a/document-parser/src/tests/parsers.rs b/document-parser/src/tests/parsers.rs index 4bb610a..db18f56 100644 --- a/document-parser/src/tests/parsers.rs +++ b/document-parser/src/tests/parsers.rs @@ -277,7 +277,6 @@ mod mineru_parser_tests { assert_eq!(parser.config().python_path, config.mineru.python_path); } - #[tokio::test] async fn test_mineru_parse_invalid_file() { let config = create_real_environment_test_config(); @@ -350,7 +349,6 @@ mod mineru_parser_tests { mod markitdown_parser_tests { use super::*; - #[tokio::test] async fn test_markitdown_parse_invalid_file() { let config = create_real_environment_test_config(); diff --git a/document-parser/src/tests/processors.rs b/document-parser/src/tests/processors.rs index 8be6a92..e9671eb 100644 --- a/document-parser/src/tests/processors.rs +++ b/document-parser/src/tests/processors.rs @@ -5,16 +5,12 @@ use uuid::Uuid; use crate::{ error::AppError, - models::{ - DocumentFormat, DocumentTask, ParserEngine, SourceType, - }, + models::{DocumentFormat, DocumentTask, ParserEngine, SourceType}, parsers::DualEngineParser, parsers::parser_trait::DocumentParser, processors::MarkdownProcessor, services::ImageProcessorConfig, - tests::test_helpers::{ - create_test_config, safe_init_global_config, - }, + tests::test_helpers::{create_test_config, safe_init_global_config}, }; use tempfile; @@ -834,7 +830,6 @@ mod integration_processor_tests { #[cfg(test)] mod comprehensive_processor_tests { use super::*; - #[tokio::test] async fn test_markdown_processor_comprehensive() { @@ -1060,7 +1055,6 @@ Text content here. safe_init_global_config(); use crate::services::ImageProcessor; - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let processor = ImageProcessor::new(ImageProcessorConfig::default(), None); @@ -1082,7 +1076,6 @@ Text content here. safe_init_global_config(); use crate::services::ImageProcessor; - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let processor = ImageProcessor::new(ImageProcessorConfig::default(), None); @@ -1110,7 +1103,6 @@ Text content here. safe_init_global_config(); use crate::services::ImageProcessor; - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let processor = ImageProcessor::new(ImageProcessorConfig::default(), None); @@ -1213,9 +1205,8 @@ mod processor_performance_tests { let processor_clone: std::sync::Arc = std::sync::Arc::clone(&processor); let handle = tokio::spawn(async move { - let markdown = format!( - "# Document {i}\nContent for document {i}.\n## Section\nMore content." - ); + let markdown = + format!("# Document {i}\nContent for document {i}.\n## Section\nMore content."); processor_clone.process_markdown(&markdown).await }); @@ -1282,7 +1273,6 @@ mod processor_error_handling_tests { safe_init_global_config(); use crate::services::ImageProcessor; - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let processor = ImageProcessor::new(ImageProcessorConfig::default(), None); @@ -1338,7 +1328,6 @@ mod processor_error_handling_tests { safe_init_global_config(); use crate::services::ImageProcessor; - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let processor = ImageProcessor::new(ImageProcessorConfig::default(), None); diff --git a/document-parser/src/tests/property_tests.rs b/document-parser/src/tests/property_tests.rs index b1161fe..2ef59a2 100644 --- a/document-parser/src/tests/property_tests.rs +++ b/document-parser/src/tests/property_tests.rs @@ -191,8 +191,6 @@ mod property_tests { /// Test utilities for generating test data pub mod generators { use super::*; - - /// Generate a valid test DocumentTask pub fn generate_test_document_task() -> DocumentTask { diff --git a/document-parser/src/tests/section_id_duplicate_tests.rs b/document-parser/src/tests/section_id_duplicate_tests.rs index ea792f0..2554f11 100644 --- a/document-parser/src/tests/section_id_duplicate_tests.rs +++ b/document-parser/src/tests/section_id_duplicate_tests.rs @@ -8,9 +8,7 @@ use crate::{ error::AppError, - models::{ - StructuredDocument, StructuredSection, - }, + models::{StructuredDocument, StructuredSection}, processors::MarkdownProcessor, tests::test_helpers::{create_test_app_state, safe_init_global_config}, }; diff --git a/document-parser/src/tests/services.rs b/document-parser/src/tests/services.rs index 3f5dc1e..6ea2b3e 100644 --- a/document-parser/src/tests/services.rs +++ b/document-parser/src/tests/services.rs @@ -1,14 +1,11 @@ use crate::{ AppState, models::{ - DocumentFormat, DocumentTask, ImageInfo, ParserEngine, ProcessingStage, - SourceType, TaskStatus, + DocumentFormat, DocumentTask, ImageInfo, ParserEngine, ProcessingStage, SourceType, + TaskStatus, }, services::{StorageService, TaskQueueService, TaskService}, - tests::test_helpers::{ - create_test_app_state, create_test_config, - safe_init_global_config, - }, + tests::test_helpers::{create_test_app_state, create_test_config, safe_init_global_config}, }; use chrono::Utc; use std::sync::Arc; diff --git a/document-parser/src/tests/utils.rs b/document-parser/src/tests/utils.rs index 8847143..836b508 100644 --- a/document-parser/src/tests/utils.rs +++ b/document-parser/src/tests/utils.rs @@ -1,9 +1,8 @@ //! 工具层单元测试 - #[cfg(test)] mod file_utils_tests { - + use crate::utils::*; use tempfile::TempDir; @@ -378,8 +377,6 @@ mod time_utils_tests { #[cfg(test)] mod config_utils_tests { - - #[tokio::test] async fn test_env_var_operations() { @@ -405,8 +402,6 @@ mod config_utils_tests { #[cfg(test)] mod error_utils_tests { - - #[tokio::test] async fn test_error_handling() { diff --git a/document-parser/src/utils/environment_manager.rs b/document-parser/src/utils/environment_manager.rs index de7b31d..ed4779a 100644 --- a/document-parser/src/utils/environment_manager.rs +++ b/document-parser/src/utils/environment_manager.rs @@ -1520,7 +1520,7 @@ impl EnvironmentManager { )); } - issues + issues } /// 生成虚拟环境问题的恢复建议 @@ -1714,9 +1714,7 @@ impl EnvironmentManager { } else if error_message.contains("超时") { "Python命令执行超时。检查系统负载或Python安装是否正常".to_string() } else { - format!( - "Python环境问题: {error_message}。请检查Python安装并确保可以正常执行" - ) + format!("Python环境问题: {error_message}。请检查Python安装并确保可以正常执行") } } @@ -2229,7 +2227,8 @@ impl EnvironmentManager { let venv_info = String::from_utf8_lossy(&venv_output.stdout); let lines: Vec<&str> = venv_info.trim().split('\n').collect(); - let virtual_env_active = lines.first() + let virtual_env_active = lines + .first() .and_then(|line| line.parse::().ok()) .unwrap_or(false); @@ -4737,8 +4736,6 @@ mod tests { #[tokio::test] async fn test_cross_platform_environment_variables() { - - let temp_dir = tempfile::TempDir::new().unwrap(); let manager = EnvironmentManager::new( "python3".to_string(), diff --git a/document-parser/src/utils/logging.rs b/document-parser/src/utils/logging.rs index 557aafe..0091c75 100644 --- a/document-parser/src/utils/logging.rs +++ b/document-parser/src/utils/logging.rs @@ -4,10 +4,7 @@ use std::sync::Arc; use std::time::SystemTime; use tokio::sync::RwLock; use tracing::{info, instrument, warn}; -use tracing_subscriber::{ - EnvFilter, Registry, - util::SubscriberInitExt, -}; +use tracing_subscriber::{EnvFilter, Registry, util::SubscriberInitExt}; use uuid::Uuid; /// 日志级别 @@ -46,8 +43,7 @@ impl From<&str> for LogLevel { } /// 关联ID管理器 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CorrelationContext { pub request_id: Option, pub task_id: Option, @@ -57,7 +53,6 @@ pub struct CorrelationContext { pub span_id: Option, } - impl CorrelationContext { pub fn new() -> Self { Self::default() @@ -462,8 +457,7 @@ impl EnhancedLoggingSystem { #[instrument(skip(config))] pub fn init(config: LoggingConfig) -> Result> { let guards = Vec::new(); - let layers: Vec + Send + Sync>> = - Vec::new(); + let layers: Vec + Send + Sync>> = Vec::new(); // 设置环境过滤器 let env_filter = EnvFilter::try_from_default_env() diff --git a/mcp-proxy/Cargo.toml b/mcp-proxy/Cargo.toml index 7615631..1e49fc8 100644 --- a/mcp-proxy/Cargo.toml +++ b/mcp-proxy/Cargo.toml @@ -8,7 +8,7 @@ rmcp = { workspace = true } axum = { workspace = true } tower = { workspace = true } tower-http = { workspace = true } -tokio = { workspace = true ,features = ["macros", "net", "rt", "rt-multi-thread","signal","io-util"]} +tokio = { workspace = true, features = ["macros", "net", "rt", "rt-multi-thread", "signal", "io-util"] } tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } @@ -33,14 +33,14 @@ uuid = { workspace = true } dashmap = { workspace = true } futures = { workspace = true } chrono = { workspace = true } -tokio-stream = "0.1.17" -backtrace = "0.3" -tracing-futures = "0.2.5" -rand = "0.8" +tokio-stream = { workspace = true } +backtrace = { workspace = true } +tracing-futures = { workspace = true } +rand = { workspace = true } # 执行js/ts/python代码,通过 uv/deno 命令方式执行 run_code_rmcp = { workspace = true } -urlencoding = "2.1.3" -base64 = "0.22" +urlencoding = { workspace = true } +base64 = { workspace = true } [dev-dependencies] diff --git a/mcp-proxy/QUICK_START.md b/mcp-proxy/QUICK_START.md new file mode 100644 index 0000000..4f2d83e --- /dev/null +++ b/mcp-proxy/QUICK_START.md @@ -0,0 +1,79 @@ +# 快速测试指南 + +## 前提 +- mcp-proxy 运行在: http://localhost:8080 +- 您的 Streamable 服务运行在: http://0.0.0.0:8000/mcp + +## 一键测试 (使用 curl) + +### 1. 检查服务 +```bash +curl -X POST http://localhost:8080/mcp/sse/check_status \ + -H "Content-Type: application/json" \ + -d '{ + "mcpId": "test-streamable-service", + "mcpJsonConfig": "{\"mcpServers\": {\"test-service\": {\"url\": \"http://0.0.0.0:8000/mcp\"}}}", + "mcpType": "Persistent", + "mcpProtocol": "Stream" + }' +``` + +### 2. 在新终端中建立 SSE 连接 +```bash +curl -N http://localhost:8080/mcp/sse/proxy/test-streamable-service/sse +``` + +### 3. 发送初始化消息 (在第三个终端) +```bash +curl -X POST http://localhost:8080/mcp/sse/proxy/test-streamable-service/message \ + -H "Content-Type: application/json" \ + -d '{ + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }' +``` + +### 4. 列出工具 +```bash +curl -X POST http://localhost:8080/mcp/sse/proxy/test-streamable-service/message \ + -H "Content-Type: application/json" \ + -d '{ + "id": "msg-2", + "method": "tools/list", + "params": {} + }' +``` + +## 透明代理工作流程 + +``` +[用户] SSE 接口 (端口 8080) + ↓ +[mcp-proxy] Streamable HTTP 客户端 + ↓ +[远程服务] Streamable HTTP (端口 8000) +``` + +- 用户通过 **SSE 协议** 访问 mcp-proxy +- mcp-proxy 使用 **Streamable HTTP 协议** 连接远程服务 +- 实现协议透明转换 + +## 查看日志 +在 mcp-proxy 启动终端中查看: +``` +[INFO] 创建Streamable HTTP客户端连接到: http://0.0.0.0:8000/mcp +[INFO] Streamable HTTP客户端已启动,MCP ID: test-streamable-service, 类型: Persistent +``` + +## 使用 VSCode REST Client +1. 打开 `test_mcp_streamable.rest` 文件 +2. 按 `Ctrl+Alt+R` (Mac: `Cmd+Alt+R`) 发送请求 +3. 按顺序执行请求 diff --git a/mcp-proxy/TEST_STREAMABLE.md b/mcp-proxy/TEST_STREAMABLE.md new file mode 100644 index 0000000..a49b3cc --- /dev/null +++ b/mcp-proxy/TEST_STREAMABLE.md @@ -0,0 +1,152 @@ +# 测试 Streamable HTTP MCP 服务 + +## 前提条件 + +1. **启动 mcp-proxy 服务** + ```bash + cd /Volumes/soddygo/git_work/mcp-proxy + cargo run -p mcp-proxy --bin mcp-proxy + ``` + 默认端口: 8080 + +2. **启动您的 Streamable HTTP MCP 服务** + ```bash + # 您的服务地址 + http://0.0.0.0:8000/mcp + ``` + +## 测试步骤 + +### 步骤 1: 检查服务状态 +```bash +curl -X POST http://localhost:8080/mcp/sse/check_status \ + -H "Content-Type: application/json" \ + -d '{ + "mcpId": "test-streamable-service", + "mcpJsonConfig": "{\"mcpServers\": {\"test-service\": {\"url\": \"http://0.0.0.0:8000/mcp\"}}}", + "mcpType": "Persistent", + "mcpProtocol": "Stream" + }' +``` + +期望响应: +```json +{ + "ready": true, + "status": "Ready", + "message": null +} +``` + +### 步骤 2: 建立 SSE 连接 +```bash +curl -N http://localhost:8080/mcp/sse/proxy/test-streamable-service/sse \ + -H "Accept: text/event-stream" +``` + +这个连接会保持打开,接收来自远程 Streamable 服务的实时更新。 + +### 步骤 3: 发送初始化消息 +在新终端中执行: +```bash +curl -X POST http://localhost:8080/mcp/sse/proxy/test-streamable-service/message \ + -H "Content-Type: application/json" \ + -d '{ + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }' +``` + +### 步骤 4: 列出工具 +```bash +curl -X POST http://localhost:8080/mcp/sse/proxy/test-streamable-service/message \ + -H "Content-Type: application/json" \ + -d '{ + "id": "msg-2", + "method": "tools/list", + "params": {} + }' +``` + +### 步骤 5: 调用工具 +```bash +curl -X POST http://localhost:8080/mcp/sse/proxy/test-streamable-service/message \ + -H "Content-Type: application/json" \ + -d '{ + "id": "msg-3", + "method": "tools/call", + "params": { + "name": "your-tool-name", + "arguments": {} + } + }' +``` + +## 使用 REST Client + +如果使用 VSCode REST Client 扩展: +1. 打开 `test_mcp_streamable.rest` 文件 +2. 按 `Ctrl+Alt+R` (Mac: `Cmd+Alt+R`) 发送请求 +3. 按顺序执行请求 + +## 透明代理说明 + +``` +用户 (SSE 接口) + ↓ +mcp-proxy (端口 8080) + ↓ +Streamable HTTP 客户端 + ↓ +远程服务 (端口 8000) +``` + +- 用户通过 SSE 协议访问 mcp-proxy +- mcp-proxy 内部使用 Streamable HTTP 协议连接远程服务 +- 实现协议透明转换 + +## 故障排除 + +### 1. 连接失败 +```bash +# 检查服务是否启动 +curl http://0.0.0.0:8000/mcp/health +``` + +### 2. 协议不匹配 +- 确保 mcpProtocol 字段设置为 "Stream" +- 远程服务必须支持 Streamable HTTP 协议 + +### 3. 认证问题 +如果远程服务需要认证: +```json +{ + "mcpId": "test-streamable-service", + "mcpJsonConfig": "{ + \"mcpServers\": { + \"test-service\": { + \"url\": \"http://0.0.0.0:8000/mcp\", + \"authToken\": \"your-token\" + } + } + }", + "mcpType": "Persistent", + "mcpProtocol": "Stream" +} +``` + +## 查看日志 + +在 mcp-proxy 启动的终端中查看日志: +``` +[INFO] 创建Streamable HTTP客户端连接到: http://0.0.0.0:8000/mcp +[INFO] Streamable HTTP客户端已启动,MCP ID: test-streamable-service, 类型: Persistent +``` diff --git a/mcp-proxy/config.yml b/mcp-proxy/config.yml index 097ccd5..7aaf698 100644 --- a/mcp-proxy/config.yml +++ b/mcp-proxy/config.yml @@ -1,6 +1,6 @@ server: # The port to listen on for incoming connections - port: 8087 + port: 8085 # The log level to use log: level: debug diff --git a/mcp-proxy/debug_routes.rest b/mcp-proxy/debug_routes.rest new file mode 100644 index 0000000..db4f5b7 --- /dev/null +++ b/mcp-proxy/debug_routes.rest @@ -0,0 +1,37 @@ +### 调试路由问题 + +### 1. 检查服务状态 +# @name checkStatus +POST http://localhost:8086/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "debug-test-id", + "mcpJsonConfig": "{\"mcpServers\":{\"test\":{\"url\":\"https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06\"}}}", + "mcpType": "OneShot" +} + +### 2. 测试 SSE 连接 +GET http://localhost:8086/mcp/sse/proxy/debug-test-id/sse?sessionId=debug-session +Accept: text/event-stream + +### 3. 测试 Message 路径 +POST http://localhost:8086/mcp/sse/proxy/debug-test-id/message?sessionId=debug-session +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "debug_001", + "method": "ping", + "params": {} +} + +### 4. 测试命令行配置对比 +POST http://localhost:8086/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "command-test-id", + "mcpJsonConfig": "{\"mcpServers\":{\"test\":{\"command\":\"echo\",\"args\":[\"hello\"],\"env\":{}}}}", + "mcpType": "OneShot" +} \ No newline at end of file diff --git a/mcp-proxy/docs/protocol-auto-detection.md b/mcp-proxy/docs/protocol-auto-detection.md new file mode 100644 index 0000000..c6ffcd3 --- /dev/null +++ b/mcp-proxy/docs/protocol-auto-detection.md @@ -0,0 +1,174 @@ +# MCP 协议自动检测功能 + +## 概述 + +MCP 代理服务现在支持自动检测后端 MCP 服务的协议类型,无需手动指定 `backendProtocol` 参数。 + +## 功能特性 + +### 1. 自动协议检测 + +当你不指定 `backendProtocol` 参数时,系统会自动检测远程 MCP 服务使用的协议: + +- **Streamable HTTP 协议检测**: + - 发送带有 `Accept: application/json, text/event-stream` 头的 POST 请求 + - 检查响应头中的 `mcp-session-id`(Streamable HTTP 的特征) + - 检查 `Content-Type` 是否为 `text/event-stream` 或 `application/json` + - 检查是否返回 `406 Not Acceptable`(说明需要特定的 Accept 头) + +- **SSE 协议检测**: + - 发送 GET 请求到服务端点 + - 检查响应头中的 `Content-Type: text/event-stream` + +### 2. 协议转换 + +支持客户端协议和后端协议的独立配置: + +- **客户端协议**:由请求路径决定(`/mcp/sse/` 或 `/mcp/stream/`) +- **后端协议**:自动检测或手动指定 + +这样可以实现: +- SSE 客户端 ↔ Streamable HTTP 后端 +- Streamable HTTP 客户端 ↔ SSE 后端 +- 同协议透明代理 + +## 使用方法 + +### 方式一:自动检测(推荐) + +```http +POST http://localhost:8085/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "my-service", + "mcpJsonConfig": "{\"mcpServers\": {\"service\": {\"url\": \"http://127.0.0.1:8000/mcp\"}}}", + "mcpType": "Persistent" +} +``` + +系统会自动: +1. 解析配置中的 URL +2. 向 URL 发送探测请求 +3. 根据响应判断协议类型 +4. 使用检测到的协议连接后端服务 + +### 方式二:手动指定 + +如果你确定后端协议类型,可以手动指定以跳过检测: + +```http +POST http://localhost:8085/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "my-service", + "mcpJsonConfig": "{\"mcpServers\": {\"service\": {\"url\": \"http://127.0.0.1:8000/mcp\"}}}", + "mcpType": "Persistent", + "backendProtocol": "Stream" +} +``` + +## 检测逻辑 + +### Streamable HTTP 检测 + +```rust +// 发送探测请求 +POST {url} +Accept: application/json, text/event-stream +Content-Type: application/json +Body: {"jsonrpc":"2.0","id":"probe","method":"ping","params":{}} + +// 检查响应 +if response.headers.contains("mcp-session-id") { + return Streamable HTTP +} +if response.status == 406 Not Acceptable { + return Streamable HTTP +} +if response.content_type.contains("text/event-stream") { + return Streamable HTTP +} +``` + +### SSE 检测 + +```rust +// 发送探测请求 +GET {url} +Accept: text/event-stream + +// 检查响应 +if response.content_type.contains("text/event-stream") { + return SSE +} +``` + +## 日志输出 + +启用自动检测后,你会在日志中看到: + +``` +INFO 开始自动检测 MCP 服务协议: http://127.0.0.1:8000/mcp +DEBUG 尝试检测 Streamable HTTP 协议: http://127.0.0.1:8000/mcp +DEBUG 发现 mcp-session-id 头,确认为 Streamable HTTP 协议 +INFO 检测到 Streamable HTTP 协议: http://127.0.0.1:8000/mcp +INFO 自动检测到后端协议: Stream for MCP ID: my-service +``` + +## 性能考虑 + +- 协议检测会增加首次连接的延迟(约 1-5 秒) +- 检测结果不会被缓存,每次启动服务都会重新检测 +- 如果你的服务协议固定,建议手动指定 `backendProtocol` 以提高性能 + +## 错误处理 + +如果协议检测失败: +1. 系统会记录警告日志 +2. 默认使用客户端协议作为后端协议 +3. 服务仍会尝试启动,但可能连接失败 + +## 示例场景 + +### 场景 1:透明代理 Streamable HTTP 服务 + +```http +# 客户端使用 SSE,后端自动检测为 Streamable HTTP +POST http://localhost:8085/mcp/sse/check_status +{ + "mcpId": "streamable-service", + "mcpJsonConfig": "{\"mcpServers\": {\"s\": {\"url\": \"http://remote:8000/mcp\"}}}", + "mcpType": "Persistent" +} + +# 客户端连接 +GET http://localhost:8085/mcp/sse/proxy/streamable-service/sse +``` + +### 场景 2:命令行启动的服务 + +```http +# 命令行服务默认使用 SSE 协议(stdio 传输) +POST http://localhost:8085/mcp/sse/check_status +{ + "mcpId": "local-service", + "mcpJsonConfig": "{\"mcpServers\": {\"s\": {\"command\": \"npx\", \"args\": [\"@modelcontextprotocol/server-filesystem\"]}}}", + "mcpType": "Persistent" +} +``` + +## 限制 + +1. 只支持 URL 配置的自动检测 +2. 命令行启动的服务默认使用 SSE 协议 +3. 检测超时时间为 5 秒 +4. 不支持需要认证的服务检测(会在实际连接时处理认证) + +## 未来改进 + +- [ ] 缓存检测结果 +- [ ] 支持自定义检测超时时间 +- [ ] 支持更多协议类型 +- [ ] 支持认证服务的检测 diff --git a/mcp-proxy/examples/axum_router.rs b/mcp-proxy/examples/axum_router.rs index 17fcff3..6ecea2b 100644 --- a/mcp-proxy/examples/axum_router.rs +++ b/mcp-proxy/examples/axum_router.rs @@ -1,4 +1,4 @@ -use rmcp::transport::sse_server::{SseServer, SseServerConfig}; +use rmcp::transport::sse_server::SseServer; use tracing_subscriber::{ layer::SubscriberExt, util::SubscriberInitExt, @@ -17,36 +17,12 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); - let config = SseServerConfig { - bind: BIND_ADDRESS.parse()?, - sse_path: "/sse".to_string(), - post_path: "/message".to_string(), - ct: tokio_util::sync::CancellationToken::new(), - sse_keep_alive: None, - }; - - let (sse_server, router) = SseServer::new(config); - - // Do something with the router, e.g., add routes or middleware - - let listener = tokio::net::TcpListener::bind(sse_server.config.bind).await?; - - let ct = sse_server.config.ct.child_token(); - - let server = axum::serve(listener, router).with_graceful_shutdown(async move { - ct.cancelled().await; - tracing::info!("sse server cancelled"); - }); - - tokio::spawn(async move { - if let Err(e) = server.await { - tracing::error!(error = %e, "sse server shutdown with error"); - } - }); - - // let ct = sse_server.with_service(Counter::new); + // 使用简化方法启动 SSE 服务器 + let ct = SseServer::serve(BIND_ADDRESS.parse()?) + .await? + .with_service_directly(()); tokio::signal::ctrl_c().await?; - // ct.cancel(); + ct.cancel(); Ok(()) } diff --git a/mcp-proxy/fixtures/streamable_mcp/streamable_hello.py b/mcp-proxy/fixtures/streamable_mcp/streamable_hello.py new file mode 100644 index 0000000..47c7bdf --- /dev/null +++ b/mcp-proxy/fixtures/streamable_mcp/streamable_hello.py @@ -0,0 +1,10 @@ +from fastmcp import FastMCP + +mcp = FastMCP("MCPServer") + +@mcp.tool +def hello(name: str) -> str: + return f"Hello, {name}!" + +if __name__ == "__main__": + mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, path="/mcp") \ No newline at end of file diff --git a/mcp-proxy/src/client/sse_client.rs b/mcp-proxy/src/client/sse_client.rs index 0eff1dc..d15f148 100644 --- a/mcp-proxy/src/client/sse_client.rs +++ b/mcp-proxy/src/client/sse_client.rs @@ -1,5 +1,5 @@ use http::HeaderName; -use rmcp::transport::{stdio, SseClientTransport}; +use rmcp::transport::{SseClientTransport, stdio}; /** * Create a local server that proxies requests to a remote server over SSE. */ diff --git a/mcp-proxy/src/lib.rs b/mcp-proxy/src/lib.rs index 8f8bca9..48d0b28 100644 --- a/mcp-proxy/src/lib.rs +++ b/mcp-proxy/src/lib.rs @@ -13,8 +13,9 @@ pub use mcp_error::AppError; pub use model::{AppState, DynamicRouterService, ProxyHandlerManager, get_proxy_manager}; pub use proxy::ProxyHandler; pub use server::{ - get_health, get_ready, get_router, mcp_start_task, schedule_check_mcp_live, set_layer, - start_schedule_task, log_service_info, shutdown_telemetry, init_tracer_provider, create_telemetry_layer, + create_telemetry_layer, get_health, get_ready, get_router, init_tracer_provider, + log_service_info, mcp_start_task, schedule_check_mcp_live, set_layer, shutdown_telemetry, + start_schedule_task, }; // 导出用于基准测试的组件 pub use server::handlers::run_code_handler::{RunCodeMessageRequest, run_code_handler}; diff --git a/mcp-proxy/src/main.rs b/mcp-proxy/src/main.rs index f9f533d..ee8c37d 100644 --- a/mcp-proxy/src/main.rs +++ b/mcp-proxy/src/main.rs @@ -2,7 +2,10 @@ mod config; use anyhow::Result; use backtrace::Backtrace; use log::{error, info, warn}; -use mcp_proxy::{AppConfig, AppState, get_proxy_manager, get_router, start_schedule_task, log_service_info, init_tracer_provider}; +use mcp_proxy::{ + AppConfig, AppState, get_proxy_manager, get_router, init_tracer_provider, log_service_info, + start_schedule_task, +}; use run_code_rmcp::warm_up_all_envs; use tokio::net::TcpListener; use tokio::signal; @@ -44,7 +47,7 @@ async fn main() -> Result<()> { // 初始化 OpenTelemetry tracer provider init_tracer_provider("mcp-proxy", "0.1.0")?; - + // 配置 OpenTelemetry let telemetry_layer = tracing_opentelemetry::layer(); @@ -170,29 +173,34 @@ async fn clean_old_logs(log_path: &str, retain_days: u32) -> Result<()> { } let entries = fs::read_dir(log_dir)?; - + for entry in entries { let entry = entry?; let path = entry.path(); - + // 只处理日志文件(文件名包含日期 log.YYYY-MM-DD) if path.is_file() { if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { // 尝试从文件名中提取日期(格式: log.YYYY-MM-DD) - if let Some(date_str) = file_name.strip_prefix("log.") { - if let Ok(file_date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { - // 基于文件名中的日期判断是否过期 - let today = chrono::Local::now().date_naive(); - let age_days = (today - file_date).num_days(); - if age_days > retain_days as i64 { - if let Err(e) = fs::remove_file(&path) { - warn!("删除旧日志文件失败: {:?}, 错误: {}", path, e); - } else { - log::debug!("已删除旧日志文件: {:?} (文件日期: {}, 超过{}天)", path, file_date, retain_days); - } + if let Some(date_str) = file_name.strip_prefix("log.") { + if let Ok(file_date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { + // 基于文件名中的日期判断是否过期 + let today = chrono::Local::now().date_naive(); + let age_days = (today - file_date).num_days(); + if age_days > retain_days as i64 { + if let Err(e) = fs::remove_file(&path) { + warn!("删除旧日志文件失败: {:?}, 错误: {}", path, e); + } else { + log::debug!( + "已删除旧日志文件: {:?} (文件日期: {}, 超过{}天)", + path, + file_date, + retain_days + ); } } } + } } } } diff --git a/mcp-proxy/src/model/global.rs b/mcp-proxy/src/model/global.rs index f960698..530f204 100644 --- a/mcp-proxy/src/model/global.rs +++ b/mcp-proxy/src/model/global.rs @@ -1,6 +1,6 @@ use axum::Router; use dashmap::DashMap; -use log::info; +use log::{debug, info}; use once_cell::sync::Lazy; use std::sync::Arc; use tokio::time::Instant; @@ -25,17 +25,37 @@ pub struct DynamicRouterService(pub McpProtocol); impl DynamicRouterService { // 注册动态 handler pub fn register_route(path: &str, handler: Router) { + debug!("=== 注册路由 ==="); + debug!("注册路径: {}", path); GLOBAL_ROUTES.insert(path.to_string(), handler); + debug!("=== 注册路由完成 ==="); } // 删除动态 handler pub fn delete_route(path: &str) { + debug!("=== 删除路由 ==="); + debug!("删除路径: {}", path); GLOBAL_ROUTES.remove(path); + debug!("=== 删除路由完成 ==="); } // 获取动态 handler pub fn get_route(path: &str) -> Option { - GLOBAL_ROUTES.get(path).map(|entry| entry.value().clone()) + let result = GLOBAL_ROUTES.get(path).map(|entry| entry.value().clone()); + if result.is_some() { + debug!("get_route('{}') = Some(Router)", path); + } else { + debug!("get_route('{}') = None", path); + } + result + } + + // 获取所有已注册的路由(debug用) + pub fn get_all_routes() -> Vec { + GLOBAL_ROUTES + .iter() + .map(|entry| entry.key().clone()) + .collect() } } diff --git a/mcp-proxy/src/model/mcp_check_status_model.rs b/mcp-proxy/src/model/mcp_check_status_model.rs index 61afb0f..d67826a 100644 --- a/mcp-proxy/src/model/mcp_check_status_model.rs +++ b/mcp-proxy/src/model/mcp_check_status_model.rs @@ -2,7 +2,7 @@ use axum::response::{IntoResponse, Response}; use http::StatusCode; use serde::{Deserialize, Serialize}; -use super::McpType; +use super::{McpProtocol, McpType}; //check mcp服务状态的请求参数 #[derive(Deserialize, Debug, Clone)] @@ -16,6 +16,10 @@ pub struct CheckMcpStatusRequestParams { //mcp类型,必须有,默认:OneShot #[serde(rename = "mcpType", default = "default_mcp_type")] pub mcp_type: McpType, + //后端MCP服务的协议类型(可选),用于指定连接到后端服务时使用的协议 + //如果不指定,则使用客户端协议(由路由路径决定) + #[serde(rename = "backendProtocol")] + pub backend_protocol: Option, } //默认的mcp类型 diff --git a/mcp-proxy/src/model/mcp_config.rs b/mcp-proxy/src/model/mcp_config.rs index 668b258..b7f71e2 100644 --- a/mcp-proxy/src/model/mcp_config.rs +++ b/mcp-proxy/src/model/mcp_config.rs @@ -1,9 +1,12 @@ use std::str::FromStr; -use anyhow::Result; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use super::McpProtocol; +use super::{ + McpProtocol, + mcp_router_model::{McpJsonServerParameters, McpServerConfig}, +}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpConfig { @@ -16,9 +19,16 @@ pub struct McpConfig { //mcp类型,默认为持续运行 #[serde(default = "default_mcp_type", rename = "mcpType")] pub mcp_type: McpType, - //mcp协议 + //mcp协议(后端协议,用于连接到远程MCP服务) #[serde(default = "default_mcp_protocol", rename = "mcpProtocol")] pub mcp_protocol: McpProtocol, + //客户端协议(用于暴露给客户端的API接口类型) + //如果不指定,则与 mcp_protocol 相同 + #[serde(default = "default_mcp_protocol", rename = "clientProtocol")] + pub client_protocol: McpProtocol, + // 解析后的服务器配置(可选) + #[serde(skip_serializing, skip_deserializing)] + pub server_config: Option, } fn default_mcp_protocol() -> McpProtocol { @@ -61,7 +71,26 @@ impl McpConfig { mcp_id, mcp_json_config, mcp_type, + client_protocol: mcp_protocol.clone(), mcp_protocol, + server_config: None, + } + } + + pub fn new_with_protocols( + mcp_id: String, + mcp_json_config: Option, + mcp_type: McpType, + client_protocol: McpProtocol, + backend_protocol: McpProtocol, + ) -> Self { + Self { + mcp_id, + mcp_json_config, + mcp_type, + client_protocol, + mcp_protocol: backend_protocol, + server_config: None, } } @@ -69,4 +98,27 @@ impl McpConfig { let config: McpConfig = serde_json::from_str(json)?; Ok(config) } + + /// 从 JSON 字符串创建并解析服务器配置 + pub fn from_json_with_server( + mcp_id: String, + mcp_json_config: String, + mcp_type: McpType, + mcp_protocol: McpProtocol, + ) -> Result { + let mcp_json_server_parameters = + crate::model::McpJsonServerParameters::from(mcp_json_config.clone()); + let server_config = mcp_json_server_parameters + .try_get_first_mcp_server() + .context("Failed to parse MCP server config")?; + + Ok(Self { + mcp_id, + mcp_json_config: Some(mcp_json_config), + mcp_type, + client_protocol: mcp_protocol.clone(), + mcp_protocol, + server_config: Some(server_config), + }) + } } diff --git a/mcp-proxy/src/model/mcp_router_model.rs b/mcp-proxy/src/model/mcp_router_model.rs index 3845a67..15883e5 100644 --- a/mcp-proxy/src/model/mcp_router_model.rs +++ b/mcp-proxy/src/model/mcp_router_model.rs @@ -28,6 +28,14 @@ pub struct SseServerSettings { pub bind_addr: SocketAddr, pub keep_alive: Option, } +//mcp的配置,支持命令行和URL两种方式 +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +pub enum McpServerConfig { + Command(McpServerCommandConfig), + Url(McpServerUrlConfig), +} + //mcp的命令行配置 #[derive(Debug, Deserialize, Clone)] pub struct McpServerCommandConfig { @@ -36,34 +44,75 @@ pub struct McpServerCommandConfig { pub env: Option>, } -impl TryFrom for McpServerCommandConfig { +//mcp的URL配置(用于Streamable/SSE协议) +#[derive(Debug, Deserialize, Clone)] +pub struct McpServerUrlConfig { + pub url: String, + + // 认证配置 + pub auth_token: Option, + pub headers: Option>, + + // 连接配置 + pub timeout_secs: Option, + pub connect_timeout_secs: Option, + + // 重试配置 + pub max_retries: Option, + pub retry_min_backoff_ms: Option, + pub retry_max_backoff_ms: Option, +} + +impl Default for McpServerUrlConfig { + fn default() -> Self { + Self { + url: String::new(), + auth_token: None, + headers: None, + timeout_secs: Some(30), + connect_timeout_secs: Some(5), + max_retries: Some(3), + retry_min_backoff_ms: Some(100), + retry_max_backoff_ms: Some(5000), + } + } +} + +impl TryFrom for McpServerConfig { type Error = anyhow::Error; fn try_from(s: String) -> Result { - info!("mcp_server_command_config: {s:?}"); + info!("mcp_server_config: {s:?}"); let mcp_json_server_parameters = McpJsonServerParameters::from(s); mcp_json_server_parameters.try_get_first_mcp_server() } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +pub enum McpServerInnerConfig { + Command(McpServerCommandConfig), + Url(McpServerUrlConfig), +} + +#[derive(Debug, Deserialize, Clone)] pub struct McpJsonServerParameters { #[serde(rename = "mcpServers")] - pub mcp_servers: HashMap, + pub mcp_servers: HashMap, } impl McpJsonServerParameters { //check里面的hashmap是否只有一个,如果没问题,尝试返回第一个 - pub fn try_get_first_mcp_server(&self) -> Result { + pub fn try_get_first_mcp_server(&self) -> Result { debug!("mcp_servers: {:?}", &self.mcp_servers); if self.mcp_servers.len() == 1 { let vals = self.mcp_servers.values().next(); if let Some(val) = vals { - Ok(val.clone()) + match val { + McpServerInnerConfig::Command(cmd) => Ok(McpServerConfig::Command(cmd.clone())), + McpServerInnerConfig::Url(url) => Ok(McpServerConfig::Url(url.clone())), + } } else { - error!( - "mcp_server_command_config: {:?}", - "没有找到对应的mcp_server_command_config" - ); + error!("mcp_server_config: {:?}", "没有找到对应的mcp_server_config"); Err(anyhow::anyhow!("没有找到对应的mcp配置")) } } else { @@ -191,13 +240,11 @@ impl McpRouterPath { McpRouterPath::extract_mcp_id(path_without_prefix, "/proxy/", &["/stream"])?; // 创建流路径 - let stream_path = format!( - "{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}/stream" - ); + let stream_path = format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"); return Some(Self { mcp_id: mcp_id.clone(), - base_path: GLOBAL_STREAM_MCP_ROUTES_PREFIX.to_string(), + base_path: format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"), mcp_protocol_path: McpProtocolPath::StreamPath(StreamMcpRouterPath { stream_path }), mcp_protocol: McpProtocol::Stream, last_accessed: Instant::now(), @@ -224,9 +271,8 @@ impl McpRouterPath { } } McpProtocol::Stream => { - let stream_path: String = format!( - "{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}/stream" - ); + let stream_path: String = + format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"); Self { mcp_id: mcp_id.clone(), base_path: format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"), @@ -306,23 +352,31 @@ mod tests { .mcp_servers .get("baidu-map") .expect("baidu-map should exist"); - assert_eq!(baidu.command, "npx"); - assert_eq!( - baidu.args, - Some(vec![ - "-y".to_string(), - "@baidumap/mcp-server-baidu-map".to_string() - ]) - ); - assert_eq!( - baidu - .env - .as_ref() - .unwrap() - .get("BAIDU_MAP_API_KEY") - .unwrap(), - "xxx" - ); + + match baidu { + McpServerInnerConfig::Command(cmd_config) => { + assert_eq!(cmd_config.command, "npx"); + assert_eq!( + cmd_config.args, + Some(vec![ + "-y".to_string(), + "@baidumap/mcp-server-baidu-map".to_string() + ]) + ); + assert_eq!( + cmd_config + .env + .as_ref() + .unwrap() + .get("BAIDU_MAP_API_KEY") + .unwrap(), + "xxx" + ); + } + McpServerInnerConfig::Url(_) => { + panic!("Expected command config, got URL config"); + } + } } #[test] @@ -332,27 +386,32 @@ mod tests { "#; let params = McpJsonServerParameters::from(json.to_string()); println!("params.len: {:?}", params.mcp_servers.len()); - let mcp_server_parameters = params.try_get_first_mcp_server()?; - assert_eq!( - mcp_server_parameters.command, - "/Users/soddy/go/bin/go-mcp-mysql" - ); - assert_eq!( - mcp_server_parameters.args, - Some(vec![ - "--host".to_string(), - "192.168.1.12".to_string(), - "--user".to_string(), - "agent_platform_test".to_string(), - "--pass".to_string(), - "SRJG7NdiwKGDkmPs".to_string(), - "--port".to_string(), - "3306".to_string(), - "--db".to_string(), - "agent_platform_test".to_string() - ]) - ); - assert_eq!(mcp_server_parameters.env, Some(HashMap::new())); + let mcp_server_config = params.try_get_first_mcp_server()?; + + match mcp_server_config { + McpServerConfig::Command(cmd_config) => { + assert_eq!(cmd_config.command, "/Users/soddy/go/bin/go-mcp-mysql"); + assert_eq!( + cmd_config.args, + Some(vec![ + "--host".to_string(), + "192.168.1.12".to_string(), + "--user".to_string(), + "agent_platform_test".to_string(), + "--pass".to_string(), + "SRJG7NdiwKGDkmPs".to_string(), + "--port".to_string(), + "3306".to_string(), + "--db".to_string(), + "agent_platform_test".to_string() + ]) + ); + assert_eq!(cmd_config.env, Some(HashMap::new())); + } + McpServerConfig::Url(_) => { + panic!("Expected command config, got URL config"); + } + } Ok(()) } @@ -372,17 +431,52 @@ mod tests { }"#; let params = McpJsonServerParameters::from(json.to_string()); - let mcp_server_parameters = params.try_get_first_mcp_server()?; - - assert_eq!(mcp_server_parameters.command, "npx"); - assert_eq!( - mcp_server_parameters.args, - Some(vec![ - "@playwright/mcp@latest".to_string(), - "--headless".to_string() - ]) - ); - assert_eq!(mcp_server_parameters.env, None); + let mcp_server_config = params.try_get_first_mcp_server()?; + + match mcp_server_config { + McpServerConfig::Command(cmd_config) => { + assert_eq!(cmd_config.command, "npx"); + assert_eq!( + cmd_config.args, + Some(vec![ + "@playwright/mcp@latest".to_string(), + "--headless".to_string() + ]) + ); + assert_eq!(cmd_config.env, None); + } + McpServerConfig::Url(_) => { + panic!("Expected command config, got URL config"); + } + } + + Ok(()) + } + + #[test] + fn test_stdio_server_parameters_from_url_json() -> Result<()> { + let json = r#"{ + "mcpServers": { + "ocr_edu": { + "url": "https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06" + } + } + }"#; + + let params = McpJsonServerParameters::from(json.to_string()); + let mcp_server_config = params.try_get_first_mcp_server()?; + + match mcp_server_config { + McpServerConfig::Url(url_config) => { + assert_eq!( + url_config.url, + "https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06" + ); + } + McpServerConfig::Command(_) => { + panic!("Expected URL config, got command config"); + } + } Ok(()) } diff --git a/mcp-proxy/src/model/mod.rs b/mcp-proxy/src/model/mod.rs index 67d4c18..0908dfc 100644 --- a/mcp-proxy/src/model/mod.rs +++ b/mcp-proxy/src/model/mod.rs @@ -13,6 +13,7 @@ pub use mcp_check_status_model::{ }; pub use mcp_config::{McpConfig, McpType}; pub use mcp_router_model::{ - AddRouteParams, GLOBAL_SSE_MCP_ROUTES_PREFIX, GLOBAL_STREAM_MCP_ROUTES_PREFIX, McpProtocol, - McpProtocolPath, McpRouterPath, McpServerCommandConfig, SseServerSettings, + AddRouteParams, GLOBAL_SSE_MCP_ROUTES_PREFIX, GLOBAL_STREAM_MCP_ROUTES_PREFIX, + McpJsonServerParameters, McpProtocol, McpProtocolPath, McpRouterPath, McpServerCommandConfig, + McpServerConfig, McpServerUrlConfig, SseServerSettings, }; diff --git a/mcp-proxy/src/proxy/proxy_handler.rs b/mcp-proxy/src/proxy/proxy_handler.rs index 2f4414f..26b9520 100644 --- a/mcp-proxy/src/proxy/proxy_handler.rs +++ b/mcp-proxy/src/proxy/proxy_handler.rs @@ -33,6 +33,8 @@ impl ServerHandler for ProxyHandler { } // 如果缓存为空,尝试动态获取 + // 使用 try_lock 而不是 lock,避免阻塞 + // peer_info() 是同步方法,可以安全调用 let client = self.client.clone(); if let Ok(guard) = client.try_lock() { if let Some(peer_info) = guard.peer_info() { @@ -141,8 +143,7 @@ impl ServerHandler for ProxyHandler { // 记录工具调用结果,这些结果会通过 SSE 推送给客户端 info!( "[call_tool] 工具调用结果 - MCP ID: {}, 工具: {}", - self.mcp_id, - request.name + self.mcp_id, request.name ); debug!("Tool call succeeded"); @@ -229,8 +230,7 @@ impl ServerHandler for ProxyHandler { // 记录资源读取结果,这些结果会通过 SSE 推送给客户端 info!( "[read_resource] 资源读取结果 - MCP ID: {}, URI: {}", - self.mcp_id, - request.uri + self.mcp_id, request.uri ); debug!("Proxying read_resource response for {}", request.uri); @@ -431,17 +431,17 @@ impl ProxyHandler { // Create a ServerInfo object that forwards the server's capabilities let cached_info = peer_info.map(|peer_info| ServerInfo { - protocol_version: peer_info.protocol_version.clone(), - server_info: Implementation { - name: peer_info.server_info.name.clone(), - version: peer_info.server_info.version.clone(), - title: None, - website_url: None, - icons: None, - }, - instructions: peer_info.instructions.clone(), - capabilities: peer_info.capabilities.clone(), - }); + protocol_version: peer_info.protocol_version.clone(), + server_info: Implementation { + name: peer_info.server_info.name.clone(), + version: peer_info.server_info.version.clone(), + title: None, + website_url: None, + icons: None, + }, + instructions: peer_info.instructions.clone(), + capabilities: peer_info.capabilities.clone(), + }); Self { client: Arc::new(Mutex::new(client)), @@ -452,9 +452,15 @@ impl ProxyHandler { //检查 mcp服务是否正常,尝试调用 list_tools 方法,如果成功返回结果,则认为成功 pub async fn is_mcp_server_ready(&self) -> bool { - let client = self.client.clone(); - let guard = client.lock().await; - (guard.list_tools(None).await).is_ok() + // 使用 try_lock 避免在定时检查时阻塞正常的业务请求 + // 如果无法获取锁,说明正在处理其他请求,假设服务正常 + match self.client.try_lock() { + Ok(guard) => (guard.list_tools(None).await).is_ok(), + Err(_) => { + debug!("is_mcp_server_ready: 无法获取锁,假设服务正常"); + true + } + } } /// 检查子进程是否已经终止 diff --git a/mcp-proxy/src/server/handlers/check_mcp_is_status.rs b/mcp-proxy/src/server/handlers/check_mcp_is_status.rs index 5d60cf1..8a11710 100644 --- a/mcp-proxy/src/server/handlers/check_mcp_is_status.rs +++ b/mcp-proxy/src/server/handlers/check_mcp_is_status.rs @@ -19,24 +19,18 @@ pub async fn check_mcp_is_status_handler( if let Some(status) = status { match status.clone() { - CheckMcpStatusResponseStatus::Ready => { - Ok(HttpResult::success( - CheckMcpStatusResponseParams::new(true, status, None), - None, - )) - } - CheckMcpStatusResponseStatus::Pending => { - Ok(HttpResult::success( - CheckMcpStatusResponseParams::new(false, status, None), - None, - )) - } - CheckMcpStatusResponseStatus::Error(err) => { - Ok(HttpResult::success( - CheckMcpStatusResponseParams::new(false, status, Some(err)), - None, - )) - } + CheckMcpStatusResponseStatus::Ready => Ok(HttpResult::success( + CheckMcpStatusResponseParams::new(true, status, None), + None, + )), + CheckMcpStatusResponseStatus::Pending => Ok(HttpResult::success( + CheckMcpStatusResponseParams::new(false, status, None), + None, + )), + CheckMcpStatusResponseStatus::Error(err) => Ok(HttpResult::success( + CheckMcpStatusResponseParams::new(false, status, Some(err)), + None, + )), } } else { warn!("mcp_id: {mcp_id} 不存在"); diff --git a/mcp-proxy/src/server/handlers/mcp_add_handler.rs b/mcp-proxy/src/server/handlers/mcp_add_handler.rs index 90f27f6..306256f 100644 --- a/mcp-proxy/src/server/handlers/mcp_add_handler.rs +++ b/mcp-proxy/src/server/handlers/mcp_add_handler.rs @@ -4,7 +4,7 @@ use log::{debug, error}; use tracing::instrument; use uuid::Uuid; -use crate::model::{AddRouteParams, HttpResult, McpProtocolPath, McpServerCommandConfig, McpType}; +use crate::model::{AddRouteParams, HttpResult, McpProtocolPath, McpServerConfig, McpType}; use crate::model::{AppState, McpRouterPath}; use crate::server::task::integrate_sse_server_with_axum; use serde_json::json; @@ -35,15 +35,18 @@ pub async fn add_route_handler( let mcp_plugin_json = params.mcp_json_config; // 将mcp_plugin_json转换为 McpJsonServerParameters 结构体 let mcp_server_config = - McpServerCommandConfig::try_from(mcp_plugin_json).expect("解析 MCP 配置失败"); + McpServerConfig::try_from(mcp_plugin_json).expect("解析 MCP 配置失败"); let mcp_type = params.mcp_type.unwrap_or(McpType::default()); // 使用新的集成方法,而不是单独启动 SSE 服务 + // 在 add_route_handler 中,客户端协议和后端协议相同 + let backend_protocol = mcp_router_path.mcp_protocol.clone(); match integrate_sse_server_with_axum( mcp_server_config.clone(), mcp_router_path.clone(), mcp_type.clone(), + backend_protocol, ) .await { diff --git a/mcp-proxy/src/server/handlers/mcp_check_status_handler.rs b/mcp-proxy/src/server/handlers/mcp_check_status_handler.rs index eb50269..d9542d5 100644 --- a/mcp-proxy/src/server/handlers/mcp_check_status_handler.rs +++ b/mcp-proxy/src/server/handlers/mcp_check_status_handler.rs @@ -1,5 +1,6 @@ +use anyhow::Result; use axum::{Json, extract::State, http::uri::Uri}; -use log::error; +use log::{error, info}; use tokio::time::Instant; use tokio_util::sync::CancellationToken; use tracing::instrument; @@ -9,9 +10,9 @@ use crate::{ model::{ AppState, CheckMcpStatusRequestParams, CheckMcpStatusResponseParams, CheckMcpStatusResponseStatus, HttpResult, McpConfig, McpProtocol, McpRouterPath, - McpServiceStatus, McpType, + McpServerConfig, McpServiceStatus, McpType, }, - server::mcp_start_task, + server::{detect_mcp_protocol, mcp_start_task}, }; /// 创建响应结果的辅助函数 @@ -94,11 +95,35 @@ pub async fn check_mcp_status_handler( return create_response(ready_status, status, None); } else { // 如果服务不存在,则取 mcp_json_config 中的配置,生成mcp透明代理服务 + // 使用 backend_protocol(如果指定)或自动检测 + let backend_protocol = if let Some(protocol) = params.backend_protocol.clone() { + protocol + } else { + // 尝试从配置中提取 URL 并自动检测协议 + match try_detect_backend_protocol(¶ms.mcp_json_config).await { + Ok(detected) => { + info!( + "自动检测到后端协议: {:?} for MCP ID: {}", + detected, params.mcp_id + ); + detected + } + Err(e) => { + info!( + "无法自动检测后端协议,使用客户端协议: {:?}, 错误: {}", + mcp_protocol, e + ); + mcp_protocol.clone() + } + } + }; + spawn_mcp_service( ¶ms.mcp_id, params.mcp_json_config, params.mcp_type, mcp_protocol.clone(), + backend_protocol, )?; // 返回 PENDING 状态,表示服务正在启动 @@ -133,22 +158,30 @@ pub async fn check_mcp_status_handler_stream( } /// 异步启动MCP服务 +/// +/// # 参数 +/// - `mcp_id`: MCP服务的唯一标识 +/// - `mcp_json_config`: MCP服务的JSON配置 +/// - `mcp_type`: MCP服务类型(OneShot或Persistent) +/// - `client_protocol`: 客户端使用的协议(决定暴露的API接口类型) +/// - `backend_protocol`: 连接后端服务使用的协议(决定如何连接到远程MCP服务) fn spawn_mcp_service( mcp_id: &str, mcp_json_config: String, mcp_type: McpType, - mcp_protocol: McpProtocol, + client_protocol: McpProtocol, + backend_protocol: McpProtocol, ) -> Result<(), AppError> { let mcp_id = mcp_id.to_string(); // 使用全局 ProxyHandlerManager let proxy_manager = get_proxy_manager(); - // 设置初始化状态 + // 设置初始化状态 - 使用客户端协议创建路由路径 let mcp_service_status = McpServiceStatus::new( mcp_id.clone(), mcp_type.clone(), - McpRouterPath::new(mcp_id.clone(), mcp_protocol.clone()), + McpRouterPath::new(mcp_id.clone(), client_protocol.clone()), CancellationToken::new(), // This will be the single cancellation_token CheckMcpStatusResponseStatus::Pending, ); @@ -157,11 +190,13 @@ fn spawn_mcp_service( //异步添加 mcp 透明代理服务 let mcp_id_clone = mcp_id.clone(); - let mcp_config: McpConfig = McpConfig::new( + // 使用客户端协议和后端协议创建配置 + let mcp_config: McpConfig = McpConfig::new_with_protocols( mcp_id_clone.clone(), Some(mcp_json_config), mcp_type, - mcp_protocol, + client_protocol, + backend_protocol, ); tokio::spawn(async move { match mcp_start_task(mcp_config).await { @@ -184,3 +219,24 @@ fn spawn_mcp_service( Ok(()) } + +/// 尝试从 MCP JSON 配置中提取 URL 并自动检测后端协议 +async fn try_detect_backend_protocol(mcp_json_config: &str) -> Result { + // 解析配置 + let mcp_server_config = McpServerConfig::try_from(mcp_json_config.to_string()) + .map_err(|e| AppError::McpServerError(anyhow::anyhow!("解析 MCP 配置失败: {}", e)))?; + + // 只有 URL 配置才需要检测协议 + match mcp_server_config { + McpServerConfig::Url(url_config) => { + // 调用协议检测函数 + detect_mcp_protocol(&url_config.url) + .await + .map_err(|e| AppError::McpServerError(anyhow::anyhow!("协议检测失败: {}", e))) + } + McpServerConfig::Command(_) => { + // 命令行方式启动的服务,默认使用 SSE 协议(stdio 传输) + Ok(McpProtocol::Sse) + } + } +} diff --git a/mcp-proxy/src/server/handlers/run_code_handler.rs b/mcp-proxy/src/server/handlers/run_code_handler.rs index 0cd1292..6f2f532 100644 --- a/mcp-proxy/src/server/handlers/run_code_handler.rs +++ b/mcp-proxy/src/server/handlers/run_code_handler.rs @@ -63,6 +63,14 @@ pub async fn run_code_handler( } }; + if !result.success { + let error_message = result + .error + .as_deref() + .unwrap_or("run_code_handler执行失败但未提供错误信息"); + error!("run_code_handler执行返回失败: {error_message}"); + } + let data = match serde_json::to_value(&result) { Ok(data) => data, Err(e) => { diff --git a/mcp-proxy/src/server/mcp_dynamic_router_service.rs b/mcp-proxy/src/server/mcp_dynamic_router_service.rs index aeba32c..97fcbb8 100644 --- a/mcp-proxy/src/server/mcp_dynamic_router_service.rs +++ b/mcp-proxy/src/server/mcp_dynamic_router_service.rs @@ -32,6 +32,11 @@ impl Service> for DynamicRouterService { let method = req.method().clone(); let headers = req.headers().clone(); + // DEBUG: 详细路径解析日志 + debug!("=== 路径解析开始 ==="); + debug!("原始请求路径: {}", path); + debug!("路径包含的通配符参数: {:?}", req.extensions()); + // 提取 trace_id let trace_id = extract_trace_id(); @@ -70,22 +75,56 @@ impl Service> for DynamicRouterService { span.record("mcp.id", &mcp_id); span.record("mcp.base_path", &base_path); - debug!("请求访问MCP ID: {mcp_id}"); + debug!("=== 路径解析结果 ==="); + debug!("解析出的mcp_id: {}", mcp_id); + debug!("解析出的base_path: {}", base_path); + debug!("请求路径: {} vs base_path: {}", path, base_path); + debug!("=== 路径解析结束 ==="); Box::pin(async move { let _guard = span.enter(); // 先尝试查找已注册的路由 + debug!("=== 路由查找过程 ==="); + debug!("查找base_path: '{}'", base_path); + if let Some(router_entry) = DynamicRouterService::get_route(&base_path) { + debug!( + "✅ 找到已注册的路由: base_path={}, path={}", + base_path, path + ); + debug!("=== 路由查找结束(成功) ==="); return handle_request_with_router(req, router_entry).await; + } else { + debug!( + "❌ 未找到已注册的路由: base_path='{}', path='{}'", + base_path, path + ); + + // 显示所有已注册的路由 + let all_routes = DynamicRouterService::get_all_routes(); + debug!("当前已注册的路由: {:?}", all_routes); + debug!("=== 路由查找结束(失败) ==="); } // 未找到路由,尝试启动服务 - warn!( - "未找到匹配的路径,尝试启动服务:base_path={base_path},path={path}" - ); + warn!("未找到匹配的路径,尝试启动服务:base_path={base_path},path={path}"); span.record("error.route_not_found", true); + // 先检查MCP服务是否存在 + let proxy_manager = crate::model::get_proxy_manager(); + if proxy_manager.get_mcp_service_status(&mcp_id).is_none() { + // MCP服务不存在 + warn!("MCP服务不存在: {}", mcp_id); + span.record("error.mcp_service_not_found", true); + return Ok(( + axum::http::StatusCode::NOT_FOUND, + [("Content-Type", "text/plain")], + format!("MCP service '{}' not found", mcp_id), + ) + .into_response()); + } + // 从请求扩展中获取MCP配置 if let Some(mcp_config) = req.extensions().get::().cloned() { //mcp_config.mcp_json_config 非空判断 @@ -142,10 +181,7 @@ async fn handle_request_with_router( let uri = req.uri().clone(); let path = uri.path(); - info!( - "[handle_request_with_router]处理请求: {} {}", - method, path - ); + info!("[handle_request_with_router]处理请求: {} {}", method, path); // 记录请求头中的关键信息 if let Some(content_type) = req.headers().get("content-type") { @@ -178,10 +214,7 @@ async fn handle_request_with_router( // 记录查询参数 if let Some(query) = uri.query() { - info!( - "[handle_request_with_router] Query: {}", - query - ); + info!("[handle_request_with_router] Query: {}", query); } let span = tracing::info_span!( diff --git a/mcp-proxy/src/server/middlewares/opentelemetry_middleware.rs b/mcp-proxy/src/server/middlewares/opentelemetry_middleware.rs index 6027051..300272a 100644 --- a/mcp-proxy/src/server/middlewares/opentelemetry_middleware.rs +++ b/mcp-proxy/src/server/middlewares/opentelemetry_middleware.rs @@ -1,35 +1,34 @@ use axum::{ - extract::Request, + extract::{MatchedPath, Request}, http::{HeaderMap, HeaderValue}, middleware::Next, response::Response, }; -use opentelemetry::{ - trace::TraceContextExt, - Context, -}; +use opentelemetry::{Context, trace::TraceContextExt}; use std::time::Instant; -use tracing::{Instrument, debug_span, info_span}; +use tracing::{Instrument, debug_span}; use tracing_opentelemetry::OpenTelemetrySpanExt; use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER}; /// OpenTelemetry 追踪中间件 -/// +/// /// 功能: /// 1. 自动创建 OpenTelemetry span 和 trace /// 2. 在响应头中添加 x-request-id (trace_id) /// 3. 在响应头中添加 x-server-time (请求处理时间) /// 4. 记录 HTTP 请求的语义化属性 -pub async fn opentelemetry_tracing_middleware( - request: Request, - next: Next, -) -> Response { +pub async fn opentelemetry_tracing_middleware(request: Request, next: Next) -> Response { let start_time = Instant::now(); - + // 提取请求信息 let method = request.method().to_string(); let uri = request.uri().to_string(); + let route = request + .extensions() + .get::() + .map(|matched_path| matched_path.as_str().to_owned()) + .unwrap_or_else(|| request.uri().path().to_owned()); let version = format!("{:?}", request.version()); let user_agent = request .headers() @@ -40,10 +39,11 @@ pub async fn opentelemetry_tracing_middleware( // 创建 OpenTelemetry span let span = debug_span!( "http_request", - otel.name = format!("{} {}", method, uri).as_str(), + otel.name = format!("{} {}", method, route).as_str(), otel.kind = "server", http.method = method.as_str(), http.url = uri.as_str(), + http.route = route.as_str(), http.scheme = "http", http.version = version.as_str(), http.user_agent = user_agent, @@ -51,16 +51,13 @@ pub async fn opentelemetry_tracing_middleware( // 设置 OpenTelemetry 属性 let otel_cx = Context::current(); - span.set_parent(otel_cx); + if let Err(error) = span.set_parent(otel_cx) { + tracing::warn!(target: "telemetry", %method, %uri, ?error, "failed to attach OpenTelemetry parent context"); + } // 获取 trace_id - let trace_id = span - .context() - .span() - .span_context() - .trace_id() - .to_string(); - + let trace_id = span.context().span().span_context().trace_id().to_string(); + // 如果 trace_id 全为0,生成一个随机的 trace_id let trace_id = if trace_id == "00000000000000000000000000000000" { use uuid::Uuid; @@ -72,27 +69,27 @@ pub async fn opentelemetry_tracing_middleware( async move { // 执行请求处理 let mut response = next.run(request).await; - + // 计算处理时间 let duration = start_time.elapsed(); let duration_micros = duration.as_micros(); - + // 记录响应状态码 let status_code = response.status().as_u16(); - + // 添加响应头 let headers = response.headers_mut(); - + // 添加 trace_id 到响应头 if let Ok(trace_header) = HeaderValue::from_str(&trace_id) { headers.insert(REQUEST_ID_HEADER, trace_header); } - + // 添加服务器处理时间到响应头 (微秒) if let Ok(time_header) = HeaderValue::from_str(&duration_micros.to_string()) { headers.insert(SERVER_TIME_HEADER, time_header); } - + // 记录请求完成日志(改为 debug 级别,减少日志量) tracing::debug!( method = %method, @@ -102,7 +99,7 @@ pub async fn opentelemetry_tracing_middleware( trace_id = %trace_id, "HTTP request completed" ); - + response } .instrument(span) @@ -110,20 +107,20 @@ pub async fn opentelemetry_tracing_middleware( } /// 从当前 OpenTelemetry 上下文中提取 trace_id -/// +/// /// 这个函数可以在任何地方调用来获取当前请求的 trace_id pub fn extract_trace_id() -> String { let current_span = tracing::Span::current(); let context = current_span.context(); let span_ref = context.span(); let span_context = span_ref.span_context(); - + let trace_id = if span_context.is_valid() { span_context.trace_id().to_string() } else { "00000000000000000000000000000000".to_string() }; - + // 如果 trace_id 全为0,生成一个随机的 trace_id if trace_id == "00000000000000000000000000000000" { use uuid::Uuid; @@ -134,7 +131,7 @@ pub fn extract_trace_id() -> String { } /// 从请求头中提取现有的 trace_id(如果有的话) -/// +/// /// 支持标准的 OpenTelemetry 传播头: /// - traceparent (W3C Trace Context) /// - x-trace-id (自定义) @@ -150,14 +147,14 @@ pub fn extract_trace_from_headers(headers: &HeaderMap) -> Option { } } } - + // 尝试从自定义头中提取 if let Some(trace_id) = headers.get("x-trace-id") { if let Ok(trace_id_str) = trace_id.to_str() { return Some(trace_id_str.to_string()); } } - + None } @@ -173,9 +170,12 @@ mod tests { "traceparent", HeaderValue::from_static("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"), ); - + let trace_id = extract_trace_from_headers(&headers); - assert_eq!(trace_id, Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())); + assert_eq!( + trace_id, + Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string()) + ); } #[test] @@ -185,7 +185,7 @@ mod tests { "x-trace-id", HeaderValue::from_static("custom-trace-id-123"), ); - + let trace_id = extract_trace_from_headers(&headers); assert_eq!(trace_id, Some("custom-trace-id-123".to_string())); } @@ -196,4 +196,4 @@ mod tests { let trace_id = extract_trace_from_headers(&headers); assert_eq!(trace_id, None); } -} \ No newline at end of file +} diff --git a/mcp-proxy/src/server/mod.rs b/mcp-proxy/src/server/mod.rs index 1d8f9f3..c1d8779 100644 --- a/mcp-proxy/src/server/mod.rs +++ b/mcp-proxy/src/server/mod.rs @@ -1,6 +1,7 @@ pub mod handlers; mod mcp_dynamic_router_service; mod middlewares; +mod protocol_detector; mod router_layer; mod task; pub mod telemetry; @@ -9,6 +10,9 @@ pub use handlers::{get_health, get_ready}; pub use middlewares::set_layer; +pub use protocol_detector::detect_mcp_protocol; pub use router_layer::get_router; pub use task::{mcp_start_task, schedule_check_mcp_live, start_schedule_task}; -pub use telemetry::{init_tracer_provider, create_telemetry_layer, log_service_info, shutdown_telemetry}; +pub use telemetry::{ + create_telemetry_layer, init_tracer_provider, log_service_info, shutdown_telemetry, +}; diff --git a/mcp-proxy/src/server/protocol_detector.rs b/mcp-proxy/src/server/protocol_detector.rs new file mode 100644 index 0000000..7f27443 --- /dev/null +++ b/mcp-proxy/src/server/protocol_detector.rs @@ -0,0 +1,183 @@ +use anyhow::Result; +use log::{debug, info}; +use reqwest::header::{ACCEPT, CONTENT_TYPE, HeaderMap, HeaderValue}; + +use crate::model::McpProtocol; + +/// 自动检测 MCP 服务的协议类型 +/// +/// 通过发送探测请求来判断服务支持的协议: +/// 1. 先尝试 Streamable HTTP 协议(发送带有特定 Accept 头的请求) +/// 2. 如果失败,尝试 SSE 协议 +pub async fn detect_mcp_protocol(url: &str) -> Result { + info!("开始自动检测 MCP 服务协议: {}", url); + + // 首先尝试 Streamable HTTP 协议 + if is_streamable_http(url).await { + info!("检测到 Streamable HTTP 协议: {}", url); + return Ok(McpProtocol::Stream); + } + + // 然后尝试 SSE 协议 + if is_sse_protocol(url).await { + info!("检测到 SSE 协议: {}", url); + return Ok(McpProtocol::Sse); + } + + // 如果都不支持,默认返回 SSE(向后兼容) + info!("无法确定协议类型,默认使用 SSE 协议: {}", url); + Ok(McpProtocol::Sse) +} + +/// 检测是否为 Streamable HTTP 协议 +/// +/// Streamable HTTP 协议的特征: +/// - 需要 Accept: application/json, text/event-stream 头 +/// - 返回 200 OK 或 406 Not Acceptable(如果缺少正确的 Accept 头) +/// - 响应头包含 content-type: text/event-stream 或 application/json +async fn is_streamable_http(url: &str) -> bool { + debug!("尝试检测 Streamable HTTP 协议: {}", url); + + let client = match reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + { + Ok(c) => c, + Err(e) => { + debug!("创建 HTTP 客户端失败: {}", e); + return false; + } + }; + + // 构造一个简单的探测请求 + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT, + HeaderValue::from_static("application/json, text/event-stream"), + ); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + // 发送一个简单的 ping 或 initialize 请求 + let body = serde_json::json!({ + "jsonrpc": "2.0", + "id": "probe", + "method": "ping", + "params": {} + }); + + match client.post(url).headers(headers).json(&body).send().await { + Ok(response) => { + let status = response.status(); + let headers = response.headers(); + + debug!("Streamable HTTP 探测响应状态: {}", status); + debug!("响应头: {:?}", headers); + + // 检查响应头中是否包含 mcp-session-id(Streamable HTTP 的特征) + if headers.contains_key("mcp-session-id") { + debug!("发现 mcp-session-id 头,确认为 Streamable HTTP 协议"); + return true; + } + + // 检查 content-type + if let Some(content_type) = headers.get(CONTENT_TYPE) { + if let Ok(ct) = content_type.to_str() { + debug!("Content-Type: {}", ct); + // Streamable HTTP 可能返回 text/event-stream 或 application/json + if ct.contains("text/event-stream") || ct.contains("application/json") { + // 进一步检查是否为 Streamable HTTP(而不是普通的 JSON API) + // 如果状态码是 200 且有正确的 content-type,很可能是 Streamable HTTP + if status.is_success() { + debug!("响应成功且 Content-Type 匹配,可能是 Streamable HTTP"); + return true; + } + } + } + } + + // 如果返回 406 Not Acceptable,说明服务器期望特定的 Accept 头 + // 这也是 Streamable HTTP 的一个特征 + if status == reqwest::StatusCode::NOT_ACCEPTABLE { + debug!("收到 406 Not Acceptable,可能是 Streamable HTTP 协议"); + return true; + } + + false + } + Err(e) => { + debug!("Streamable HTTP 探测失败: {}", e); + false + } + } +} + +/// 检测是否为 SSE 协议 +/// +/// SSE 协议的特征: +/// - 通常是 GET 请求到特定的 SSE 端点 +/// - 响应头包含 content-type: text/event-stream +/// - 连接保持打开状态 +async fn is_sse_protocol(url: &str) -> bool { + debug!("尝试检测 SSE 协议: {}", url); + + let client = match reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + { + Ok(c) => c, + Err(e) => { + debug!("创建 HTTP 客户端失败: {}", e); + return false; + } + }; + + // SSE 通常使用 GET 请求 + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream")); + + match client.get(url).headers(headers).send().await { + Ok(response) => { + let status = response.status(); + let headers = response.headers(); + + debug!("SSE 探测响应状态: {}", status); + + // 检查 content-type 是否为 text/event-stream + if let Some(content_type) = headers.get(CONTENT_TYPE) { + if let Ok(ct) = content_type.to_str() { + debug!("Content-Type: {}", ct); + if ct.contains("text/event-stream") && status.is_success() { + debug!("确认为 SSE 协议"); + return true; + } + } + } + + false + } + Err(e) => { + debug!("SSE 探测失败: {}", e); + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_detect_protocol() { + // 这个测试需要实际的服务运行 + // 这里只是示例 + let url = "http://127.0.0.1:8000/mcp"; + match detect_mcp_protocol(url).await { + Ok(protocol) => { + println!("检测到的协议: {:?}", protocol); + } + Err(e) => { + println!("检测失败: {}", e); + } + } + } +} diff --git a/mcp-proxy/src/server/router_layer.rs b/mcp-proxy/src/server/router_layer.rs index 466137d..c12b2e0 100644 --- a/mcp-proxy/src/server/router_layer.rs +++ b/mcp-proxy/src/server/router_layer.rs @@ -1,5 +1,6 @@ use axum::{ Router, + extract::DefaultBodyLimit, routing::{delete, get, post}, }; use http::Method; @@ -68,6 +69,7 @@ pub async fn get_router(state: AppState) -> Result { post(check_mcp_status_handler_stream), ) .route("/api/run_code_with_log", post(run_code_handler)) + .layer(DefaultBodyLimit::max(20 * 1024 * 1024)) .layer(cors); // 创建基本路由 diff --git a/mcp-proxy/src/server/task/mcp_start_task.rs b/mcp-proxy/src/server/task/mcp_start_task.rs index 7f48dce..5c3013e 100644 --- a/mcp-proxy/src/server/task/mcp_start_task.rs +++ b/mcp-proxy/src/server/task/mcp_start_task.rs @@ -1,8 +1,8 @@ use crate::{ DynamicRouterService, ProxyHandler, get_proxy_manager, model::{ - CheckMcpStatusResponseStatus, McpConfig, McpProtocolPath, McpRouterPath, - McpServerCommandConfig, McpServiceStatus, McpType, + CheckMcpStatusResponseStatus, McpConfig, McpProtocol, McpProtocolPath, McpRouterPath, + McpServerCommandConfig, McpServerConfig, McpServiceStatus, McpType, }, }; @@ -14,7 +14,10 @@ use rmcp::{ transport::streamable_http_server::{ StreamableHttpService, session::local::LocalSessionManager, }, - transport::{SseServer, TokioChildProcess, sse_server::SseServerConfig}, + transport::{ + SseClientTransport, SseServer, TokioChildProcess, sse_server::SseServerConfig, + streamable_http_client::StreamableHttpClientTransport, + }, }; use tokio::process::Command; @@ -23,63 +26,39 @@ pub async fn mcp_start_task( mcp_config: McpConfig, ) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> { let mcp_id = mcp_config.mcp_id.clone(); - let mcp_protocol = mcp_config.mcp_protocol.clone(); + let backend_protocol = mcp_config.mcp_protocol.clone(); + let client_protocol = mcp_config.client_protocol.clone(); - let mcp_router_path: McpRouterPath = McpRouterPath::new(mcp_id, mcp_protocol); + // 使用客户端协议创建路由路径(决定暴露的API接口) + let mcp_router_path: McpRouterPath = McpRouterPath::new(mcp_id, client_protocol); let mcp_json_config = mcp_config .mcp_json_config .clone() .expect("mcp_json_config is required"); - let mcp_server_config = McpServerCommandConfig::try_from(mcp_json_config)?; + let mcp_server_config = McpServerConfig::try_from(mcp_json_config)?; - // 使用新的集成方法,而不是单独启动 SSE 服务 + // 使用新的集成方法,传递后端协议用于连接远程服务 integrate_sse_server_with_axum( mcp_server_config.clone(), mcp_router_path.clone(), mcp_config.mcp_type, + backend_protocol, ) .await } // 创建一个新函数,将 SseServer 与 axum 路由集成 pub async fn integrate_sse_server_with_axum( - mcp_config: McpServerCommandConfig, + mcp_config: McpServerConfig, mcp_router_path: McpRouterPath, mcp_type: McpType, + backend_protocol: McpProtocol, ) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> { let base_path = mcp_router_path.base_path.clone(); let mcp_id = mcp_router_path.mcp_id.clone(); - // 创建子进程命令 - let mut command = Command::new(&mcp_config.command); - - // 正确处理Option> - if let Some(args) = &mcp_config.args { - command.args(args); - } - - // 正确处理Option> - if let Some(env_vars) = &mcp_config.env { - for (key, value) in env_vars { - command.env(key, value); - } - } - - // 记录命令执行信息,方便调试 - log_command_details(&mcp_config, &mcp_router_path); - - // 创建子进程 - let tokio_process = TokioChildProcess::new(command)?; - - // 记录子进程已启动的信息 - info!( - "子进程已启动,MCP ID: {}, 类型: {:?}", - mcp_router_path.mcp_id, - mcp_type.clone() - ); - // 创建客户端信息 let client_info = ClientInfo { protocol_version: Default::default(), @@ -92,8 +71,78 @@ pub async fn integrate_sse_server_with_axum( ..Default::default() }; - // 创建客户端服务 - let client = client_info.serve(tokio_process).await?; + // 根据配置类型创建不同的客户端服务 + let client = match &mcp_config { + McpServerConfig::Command(cmd_config) => { + // 创建子进程命令 + let mut command = Command::new(&cmd_config.command); + + // 正确处理Option> + if let Some(args) = &cmd_config.args { + command.args(args); + } + + // 正确处理Option> + if let Some(env_vars) = &cmd_config.env { + for (key, value) in env_vars { + command.env(key, value); + } + } + + // 记录命令执行信息,方便调试 + log_command_details(cmd_config, &mcp_router_path); + + info!( + "子进程已启动,MCP ID: {}, 类型: {:?}", + mcp_router_path.mcp_id, + mcp_type.clone() + ); + + // 创建子进程传输并创建客户端服务 + let tokio_process = TokioChildProcess::new(command)?; + client_info.serve(tokio_process).await? + } + McpServerConfig::Url(url_config) => { + // 根据后端协议类型创建不同的客户端传输 + info!( + "连接到远程MCP服务: {}, 后端协议: {:?}, 客户端协议: {:?}", + url_config.url, backend_protocol, mcp_router_path.mcp_protocol + ); + + match backend_protocol { + McpProtocol::Sse => { + // SSE 协议 - 创建 SSE 客户端传输 + info!("使用SSE协议连接到: {}", url_config.url); + + let sse_transport = SseClientTransport::start(url_config.url.clone()).await?; + client_info.serve(sse_transport).await? + } + McpProtocol::Stream => { + // Streamable 协议 - 创建 Streamable HTTP 客户端传输 + info!("使用Streamable HTTP协议连接到: {}", url_config.url); + + // 使用默认方式创建传输,rmcp 库会自动处理 Accept 头和会话管理 + let transport = StreamableHttpClientTransport::from_uri(url_config.url.clone()); + + info!( + "Streamable HTTP传输已创建,开始建立连接,MCP ID: {}, 类型: {:?}", + mcp_router_path.mcp_id, + mcp_type.clone() + ); + + // serve 会建立连接并完成初始化握手 + let client = client_info.serve(transport).await?; + + info!( + "Streamable HTTP客户端连接成功,MCP ID: {}", + mcp_router_path.mcp_id + ); + + client + } + } + } + }; // 创建代理处理器 let proxy_handler = ProxyHandler::with_mcp_id(client, mcp_id.clone()); @@ -106,15 +155,20 @@ pub async fn integrate_sse_server_with_axum( let proxy_handler_for_sse = proxy_handler_clone.clone(); let proxy_handler_for_stream = proxy_handler_clone.clone(); - //区分协议,如果是sse 协议,使用: SseServer - //如果是stream 协议,使用: StreamableHttpServer - let (router, ct) = match &mcp_router_path.mcp_protocol_path { - McpProtocolPath::SsePath(sse_path) => { - // 创建 SseServer - // 使用随机端口,让 axum 来管理; 这里不使用这个地址绑定,只需要对应的router + // 根据客户端协议和后端协议创建服务器(支持协议转换) + // 支持三种模式: + // 1. client=SSE, backend=SSE (透明代理) + // 2. client=Stream, backend=Stream (透明代理) + // 3. client=SSE, backend=Stream (协议转换) - 关键功能 + let (router, ct) = match (mcp_router_path.mcp_protocol.clone(), backend_protocol) { + // SSE 客户端协议 + (McpProtocol::Sse, McpProtocol::Sse) => { + // 模式1: SSE -> SSE (透明代理) let addr: String = "0.0.0.0:0".to_string(); - - // 创建SSE配置 + let sse_path = match &mcp_router_path.mcp_protocol_path { + McpProtocolPath::SsePath(sse_path) => sse_path, + _ => unreachable!(), + }; let config = SseServerConfig { bind: addr.parse()?, sse_path: sse_path.sse_path.clone(), @@ -122,18 +176,61 @@ pub async fn integrate_sse_server_with_axum( ct: tokio_util::sync::CancellationToken::new(), sse_keep_alive: None, }; + + debug!( + "创建SSE服务器,配置: bind={}, sse_path={}, post_path={}", + config.bind, config.sse_path, config.post_path + ); + let (sse_server, router) = SseServer::new(config); let ct = sse_server.with_service(move || proxy_handler_for_sse.clone()); + (router, ct) + } + (McpProtocol::Sse, McpProtocol::Stream) => { + // 模式3: SSE -> Streamable HTTP (协议转换) + // 对外提供SSE接口,内部转换为Streamable HTTP + let addr: String = "0.0.0.0:0".to_string(); + let sse_path = match &mcp_router_path.mcp_protocol_path { + McpProtocolPath::SsePath(sse_path) => sse_path, + _ => unreachable!(), + }; + let config = SseServerConfig { + bind: addr.parse()?, + sse_path: sse_path.sse_path.clone(), + post_path: sse_path.message_path.clone(), + ct: tokio_util::sync::CancellationToken::new(), + sse_keep_alive: None, + }; + + debug!( + "创建SSE服务器(协议转换),配置: bind={}, sse_path={}, post_path={}", + config.bind, config.sse_path, config.post_path + ); + let (sse_server, router) = SseServer::new(config); + let ct = sse_server.with_service(move || proxy_handler_for_stream.clone()); (router, ct) } - McpProtocolPath::StreamPath(_stream_path) => { + (McpProtocol::Stream, McpProtocol::Stream) => { + // 模式2: Stream -> Stream (透明代理) let service = StreamableHttpService::new( move || Ok(proxy_handler_for_stream.clone()), LocalSessionManager::default().into(), Default::default(), ); - let router = axum::Router::new().nest_service("/mcp", service); + let router = axum::Router::new().fallback_service(service); + let ct = tokio_util::sync::CancellationToken::new(); + (router, ct) + } + (McpProtocol::Stream, McpProtocol::Sse) => { + // 模式4: Streamable HTTP -> SSE (协议转换) + // 对外提供Streamable HTTP接口,内部连接到SSE后端 + let service = StreamableHttpService::new( + move || Ok(proxy_handler_for_sse.clone()), + LocalSessionManager::default().into(), + Default::default(), + ); + let router = axum::Router::new().fallback_service(service); let ct = tokio_util::sync::CancellationToken::new(); (router, ct) } @@ -154,13 +251,163 @@ pub async fn integrate_sse_server_with_axum( // 添加 MCP 服务状态到全局管理器,以及 proxy_handler 的透明代理 proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(proxy_handler)); + // 为SSE和Stream协议添加基础路径处理 + // 支持直接访问基础路径,自动重定向到正确的子路径 + let router = if matches!(mcp_router_path.mcp_protocol, McpProtocol::Sse) { + // 使用fallback处理器来匹配基础路径 + let modified_router = router.fallback(base_path_fallback_handler); + info!("SSE基础路径处理器已添加, 基础路径: {}", base_path); + modified_router + } else { + router + }; + // 注册路由到全局路由表 + info!("注册路由: base_path={}, mcp_id={}", base_path, mcp_id); + info!( + "SSE路径配置: sse_path={}, post_path={}", + match &mcp_router_path.mcp_protocol_path { + McpProtocolPath::SsePath(sse_path) => &sse_path.sse_path, + _ => "N/A", + }, + match &mcp_router_path.mcp_protocol_path { + McpProtocolPath::SsePath(sse_path) => &sse_path.message_path, + _ => "N/A", + } + ); DynamicRouterService::register_route(&base_path, router.clone()); + info!("路由注册完成: base_path={}", base_path); // 返回路由和取消令牌 Ok((router, ct)) } +// 基础路径处理器 - 支持直接访问基础路径,自动重定向到正确的子路径 +#[axum::debug_handler] +async fn base_path_fallback_handler( + method: axum::http::Method, + uri: axum::http::Uri, + headers: axum::http::HeaderMap, +) -> impl axum::response::IntoResponse { + let path = uri.path(); + info!("基础路径处理器: {} {}", method, path); + + // 判断是SSE还是Stream协议 + if path.contains("/sse/proxy/") { + // SSE协议处理 + match method { + axum::http::Method::GET => { + // 从路径中提取 MCP ID + let mcp_id = path.split("/sse/proxy/").nth(1); + + if let Some(mcp_id) = mcp_id { + // 检查MCP服务是否存在 + let proxy_manager = get_proxy_manager(); + if proxy_manager.get_mcp_service_status(mcp_id).is_none() { + // MCP服务不存在 + ( + axum::http::StatusCode::NOT_FOUND, + [("Content-Type", "text/plain".to_string())], + format!("MCP service '{}' not found", mcp_id).to_string(), + ) + } else { + // MCP服务存在,检查Accept头 + let accept_header = headers.get("accept"); + if let Some(accept) = accept_header { + let accept_str = accept.to_str().unwrap_or(""); + if accept_str.contains("text/event-stream") { + // 正确的Accept头,重定向到 /sse + let redirect_uri = format!("{}/sse", path); + info!("SSE重定向到: {}", redirect_uri); + ( + axum::http::StatusCode::FOUND, + [("Location", redirect_uri.to_string())], + "Redirecting to SSE endpoint".to_string(), + ) + } else { + // Accept头不正确 + ( + axum::http::StatusCode::BAD_REQUEST, + [("Content-Type", "text/plain".to_string())], + "SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(), + ) + } + } else { + // 没有Accept头 + ( + axum::http::StatusCode::BAD_REQUEST, + [("Content-Type", "text/plain".to_string())], + "SSE error: Missing Accept header, expected 'text/event-stream'" + .to_string(), + ) + } + } + } else { + // 无法从路径中提取MCP ID + ( + axum::http::StatusCode::BAD_REQUEST, + [("Content-Type", "text/plain".to_string())], + "SSE error: Invalid SSE path".to_string(), + ) + } + } + axum::http::Method::POST => { + // POST请求重定向到 /message + let redirect_uri = format!("{}/message", path); + info!("SSE重定向到: {}", redirect_uri); + ( + axum::http::StatusCode::FOUND, + [("Location", redirect_uri.to_string())], + "Redirecting to message endpoint".to_string(), + ) + } + _ => { + // 其他方法返回405 Method Not Allowed + ( + axum::http::StatusCode::METHOD_NOT_ALLOWED, + [("Allow", "GET, POST".to_string())], + "Only GET and POST methods are allowed".to_string(), + ) + } + } + } else if path.contains("/stream/proxy/") { + // Stream协议处理 - 直接返回成功,不重定向 + match method { + axum::http::Method::GET => { + // GET请求返回服务器信息 + ( + axum::http::StatusCode::OK, + [("Content-Type", "application/json".to_string())], + r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(), + ) + } + axum::http::Method::POST => { + // POST请求返回成功,让StreamableHttpService处理 + ( + axum::http::StatusCode::OK, + [("Content-Type", "application/json".to_string())], + r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(), + ) + } + _ => { + // 其他方法返回405 Method Not Allowed + ( + axum::http::StatusCode::METHOD_NOT_ALLOWED, + [("Allow", "GET, POST".to_string())], + "Only GET and POST methods are allowed".to_string(), + ) + } + } + } else { + // 未知协议 + ( + axum::http::StatusCode::BAD_REQUEST, + [("Content-Type", "text/plain".to_string())], + "Unknown protocol or path".to_string(), + ) + } +} + // 提取记录命令详情的函数 fn log_command_details(mcp_config: &McpServerCommandConfig, mcp_router_path: &McpRouterPath) { // 打印命令行参数 @@ -173,10 +420,7 @@ fn log_command_details(mcp_config: &McpServerCommandConfig, mcp_router_path: &Mc // 打印环境变量 if let Some(env_vars) = &mcp_config.env { - let env_vars: Vec = env_vars - .iter() - .map(|(k, v)| format!("{k}={v}")) - .collect(); + let env_vars: Vec = env_vars.iter().map(|(k, v)| format!("{k}={v}")).collect(); if !env_vars.is_empty() { debug!("环境变量: {}", env_vars.join(", ")); } diff --git a/mcp-proxy/src/server/telemetry.rs b/mcp-proxy/src/server/telemetry.rs index 3b9ff4c..1a0ad96 100644 --- a/mcp-proxy/src/server/telemetry.rs +++ b/mcp-proxy/src/server/telemetry.rs @@ -1,11 +1,9 @@ use anyhow::Result; use opentelemetry::global; -use opentelemetry_sdk::{ - trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, -}; +use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler, SdkTracerProvider}; /// 初始化 OpenTelemetry tracer provider -/// +/// /// 这个函数必须在创建 telemetry layer 之前调用 pub fn init_tracer_provider(_service_name: &str, _service_version: &str) -> Result<()> { // 创建 tracer provider @@ -21,15 +19,15 @@ pub fn init_tracer_provider(_service_name: &str, _service_version: &str) -> Resu } /// 创建增强的 OpenTelemetry layer -/// +/// /// 这个函数创建一个配置好的 OpenTelemetry layer,可以与现有的 tracing 配置集成 -/// 注意:必须先调用 init_tracer_provider() +/// 注意:必须先调用 init_tracer_provider() pub fn create_telemetry_layer() -> impl tracing_subscriber::Layer { tracing_opentelemetry::layer() } /// 记录服务启动信息 -/// +/// /// 在 telemetry 系统初始化后调用,记录服务的基本信息 pub fn log_service_info(service_name: &str, service_version: &str) -> Result<()> { tracing::info!( @@ -55,8 +53,8 @@ mod tests { fn test_log_service_info() { let result = log_service_info("test-service", "0.1.0"); assert!(result.is_ok()); - + // 清理 shutdown_telemetry(); } -} \ No newline at end of file +} diff --git a/mcp-proxy/src/tests/mcp_sse_test.rs b/mcp-proxy/src/tests/mcp_sse_test.rs index 8a906c7..643c04d 100644 --- a/mcp-proxy/src/tests/mcp_sse_test.rs +++ b/mcp-proxy/src/tests/mcp_sse_test.rs @@ -6,9 +6,7 @@ mod sse_test { use rmcp::{ ServiceExt, model::{CallToolRequestParam, ClientCapabilities, ClientInfo, Implementation}, - transport::{ - SseClientTransport, sse_client::SseClientConfig, - }, + transport::{SseClientTransport, sse_client::SseClientConfig}, }; use std::process::Command; use std::time::Duration; @@ -185,6 +183,9 @@ mod sse_test { client_info: Implementation { name: "test sse client".to_string(), version: "0.0.1".to_string(), + icons: None, + title: None, + website_url: None, }, }; diff --git a/mcp-proxy/test_mcp_sse_streamable.rest b/mcp-proxy/test_mcp_sse_streamable.rest new file mode 100644 index 0000000..c5df837 --- /dev/null +++ b/mcp-proxy/test_mcp_sse_streamable.rest @@ -0,0 +1,142 @@ +### 测试说明 +# 本地启动了一个 Streamable HTTP MCP 服务: http://0.0.0.0:8000/mcp +# +# 重要:请求参数说明 +# - mcpProtocol: 此参数不存在!会被忽略 +# - backendProtocol: 指定后端服务的协议(可选,支持自动检测) +# - 客户端协议由请求路径决定: +# - /mcp/sse/check_status → SSE 客户端协议 +# - /mcp/stream/check_status → Stream 客户端协议 + +### ======================================== +### 方案 A:SSE 客户端 + Streamable HTTP 后端(协议转换) +### ======================================== + +### 1a. 检查服务状态 - 自动检测后端协议(推荐) +# 客户端使用 SSE 协议,后端自动检测为 Streamable HTTP +POST http://localhost:8085/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "test-sse-streamable-service", + "mcpJsonConfig": "{\"mcpServers\": {\"test-service\": {\"url\": \"http://127.0.0.1:8000/mcp\"}}}", + "mcpType": "Persistent" +} + +### 1b. 检查服务状态 - 手动指定后端协议 +# 客户端使用 SSE 协议,手动指定后端为 Streamable HTTP +POST http://localhost:8085/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "test-sse-to-stream-manual", + "mcpJsonConfig": "{\"mcpServers\": {\"test-service\": {\"url\": \"http://127.0.0.1:8000/mcp\"}}}", + "mcpType": "Persistent", + "backendProtocol": "Stream" +} + +### 2a. 获取 SSE 连接(自动检测版本) +GET http://localhost:8085/mcp/sse/proxy/test-streamable-service/sse +Accept: text/event-stream + +### 2b. 获取 SSE 连接(手动指定版本) +GET http://localhost:8085/mcp/sse/proxy/test-sse-to-stream-manual/sse +Accept: text/event-stream + +### 3. 发送 initialize 消息(需要先从 SSE 响应中获取 sessionId) +# 替换 {sessionId} 为实际的 session ID +POST http://localhost:8085/mcp/sse/proxy/test-streamable-service/message?sessionId={sessionId} +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} + +### 4. 列出可用工具 +POST http://localhost:8085/mcp/sse/proxy/test-streamable-service/message?sessionId={sessionId} +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} +} + +### ======================================== +### 方案 B:Streamable HTTP 客户端 + Streamable HTTP 后端(透明代理) +### ======================================== + +### 5. 检查服务状态 - Stream 到 Stream(自动检测) +# 客户端和后端都使用 Streamable HTTP 协议 +POST http://localhost:8085/mcp/stream/check_status +Content-Type: application/json + +{ + "mcpId": "test-stream-to-stream", + "mcpJsonConfig": "{\"mcpServers\": {\"test-service\": {\"url\": \"http://127.0.0.1:8000/mcp\"}}}", + "mcpType": "Persistent" +} + +### 6. 发送 Streamable HTTP 请求 - initialize +POST http://localhost:8085/mcp/stream/proxy/test-stream-to-stream +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} + +### 7. 列出可用工具 +POST http://localhost:8085/mcp/stream/proxy/test-stream-to-stream +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} +} + +### ======================================== +### 清理服务 +### ======================================== + +### 8a. 清理 SSE 服务(自动检测) +DELETE http://localhost:8085/mcp/config/delete/test-streamable-service +Content-Type: application/json + +{} + +### 8b. 清理 SSE 服务(手动指定) +DELETE http://localhost:8085/mcp/config/delete/test-sse-to-stream-manual +Content-Type: application/json + +{} + +### 8c. 清理 Stream 服务 +DELETE http://localhost:8085/mcp/config/delete/test-stream-to-stream +Content-Type: application/json + +{} diff --git a/mcp-proxy/test_mcp_status.rest b/mcp-proxy/test_mcp_status.rest index e41275c..53959b0 100644 --- a/mcp-proxy/test_mcp_status.rest +++ b/mcp-proxy/test_mcp_status.rest @@ -19,6 +19,17 @@ Content-Type: application/json } +### 测试检查百度OCR MCP服务状态 +POST http://localhost:8086/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "baidu-ocr-test-id2", + "mcpJsonConfig": "{\"mcpServers\":{\"ocr_edu\":{\"url\":\"https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06\"}}}", + "mcpType": "OneShot" +} + + ### 测试检查context7 POST http://localhost:8087/mcp/sse/check_status Content-Type: application/json @@ -100,3 +111,339 @@ Content-Type: application/json "mcpJsonConfig": "{\"mcpServers\": {\"error\": {\"command\": \"/not/exist/command\", \"args\": [], \"env\": {}}}}", "mcpType": "OneShot" } + +### ========== 百度OCR MCP服务测试 ========== + +### 1. 首先检查百度OCR MCP服务状态(确保服务已启动) +# @name checkBaiduOcrStatus +POST http://localhost:8086/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "baidu-ocr-test-id2", + "mcpJsonConfig": "{\"mcpServers\":{\"ocr_edu\":{\"url\":\"https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06\"}}}", + "mcpType": "OneShot" +} + +### 提取百度OCR服务的信息 +@baidu_mcp_id = baidu-ocr-test-id2 +@baidu_ready = {{checkBaiduOcrStatus.response.body.data.ready}} +@baidu_status = {{checkBaiduOcrStatus.response.body.data.status}} + +### 2. 测试获取百度OCR MCP服务的工具列表 +# @name getBaiduOcrTools +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "tools_001", + "method": "tools/list", + "params": {} +} + +### 3. 测试获取百度OCR MCP服务的资源列表 +# @name getBaiduOcrResources +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "resources_001", + "method": "resources/list", + "params": {} +} + +### 4. 测试获取百度OCR MCP服务的提示列表 +# @name getBaiduOcrPrompts +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "prompts_001", + "method": "prompts/list", + "params": {} +} + +### 5. 测试调用百度OCR工具 - 通用文字识别 +# @name callBaiduOcrGeneral +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_general_001", + "method": "tools/call", + "params": { + "name": "general_basic", + "arguments": { + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=" + } + } +} + +### 6. 测试调用百度OCR工具 - 身份证识别 +# @name callBaiduOcrIdCard +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_idcard_001", + "method": "tools/call", + "params": { + "name": "idcard", + "arguments": { + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=", + "id_card_side": "front" + } + } +} + +### 7. 测试调用百度OCR工具 - 银行卡识别 +# @name callBaiduOcrBankCard +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_bankcard_001", + "method": "tools/call", + "params": { + "name": "bankcard", + "arguments": { + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=" + } + } +} + +### 8. 测试调用百度OCR工具 - 驾驶证识别 +# @name callBaiduOcrDriverLicense +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_driver_001", + "method": "tools/call", + "params": { + "name": "driving_license", + "arguments": { + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=" + } + } +} + +### 9. 测试调用百度OCR工具 - 营业执照识别 +# @name callBaiduOcrBusinessLicense +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_business_001", + "method": "tools/call", + "params": { + "name": "business_license", + "arguments": { + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=" + } + } +} + +### 10. 测试调用百度OCR工具 - 表格识别 +# @name callBaiduOcrTable +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_table_001", + "method": "tools/call", + "params": { + "name": "table", + "arguments": { + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=" + } + } +} + +### 11. 测试获取百度OCR服务信息 +# @name getBaiduOcrServerInfo +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "server_info_001", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": { + "roots": { + "listChanged": true + }, + "sampling": {} + }, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} + +### 12. 测试ping百度OCR服务 +# @name pingBaiduOcrService +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ping_001", + "method": "ping", + "params": {} +} + +### ========== 百度OCR SSE连接测试 ========== + +### 13. SSE连接测试(在浏览器中打开以下URL来测试实时连接) +# 打开浏览器访问: http://localhost:8086/mcp/sse/proxy/baidu-ocr-test-id2/sse +# 这将建立SSE连接,你可以看到实时的消息流 + +### ========== 使用真实图片的测试示例 ========== + +### 14. 使用真实图片进行通用文字识别测试 +# 注意:请将下面的base64字符串替换为你的真实图片的base64编码 +# @name callBaiduOcrRealImage +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ocr_real_001", + "method": "tools/call", + "params": { + "name": "general_basic", + "arguments": { + "image": "请将此处替换为真实图片的base64编码", + "language_type": "CHN_ENG", + "detect_direction": "true", + "detect_language": "true", + "probability": "true" + } + } +} + +### 15. 测试错误处理 - 无效的工具名称 +# @name testInvalidTool +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "error_test_001", + "method": "tools/call", + "params": { + "name": "invalid_tool_name", + "arguments": {} + } +} + +### 16. 测试错误处理 - 无效的图片数据 +# @name testInvalidImage +POST http://localhost:8086/mcp/sse/proxy/{{baidu_mcp_id}}/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "error_test_002", + "method": "tools/call", + "params": { + "name": "general_basic", + "arguments": { + "image": "invalid_base64_data" + } + } +} +### === +======= 调试百度OCR服务问题 ========== + +### 调试1: 重新检查百度OCR服务状态并获取正确路径 +# @name debugBaiduOcrStatus +POST http://localhost:8086/mcp/sse/check_status +Content-Type: application/json + +{ + "mcpId": "baidu-ocr-test-id2", + "mcpJsonConfig": "{\"mcpServers\":{\"ocr_edu\":{\"url\":\"https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06\"}}}", + "mcpType": "OneShot" +} + +### 提取调试信息 +@debug_ready = {{debugBaiduOcrStatus.response.body.data.ready}} +@debug_status = {{debugBaiduOcrStatus.response.body.data.status}} +@debug_message = {{debugBaiduOcrStatus.response.body.data.message}} + +### 调试2: 测试简单的初始化请求(不需要sessionId) +# @name debugInitialize +POST http://localhost:8086/mcp/sse/proxy/baidu-ocr-test-id2/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "init_001", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": { + "roots": { + "listChanged": true + }, + "sampling": {} + }, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} + +### 调试3: 测试带sessionId的请求 +# @name debugWithSessionId +POST http://localhost:8086/mcp/sse/proxy/baidu-ocr-test-id2/message?sessionId=test-session-001 +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "tools_with_session_001", + "method": "tools/list", + "params": {} +} + +### 调试4: 测试不同的路径格式 +# @name debugAlternatePath +POST http://localhost:8086/mcp/sse/proxy/baidu-ocr-test-id2/message +Content-Type: application/json +x-session-id: test-session-001 + +{ + "jsonrpc": "2.0", + "id": "tools_header_session_001", + "method": "tools/list", + "params": { + "sessionId": "test-session-001" + } +} + +### 调试5: 检查服务器是否正确处理URL配置 +# @name debugUrlConfig +POST http://localhost:8086/mcp/sse/proxy/baidu-ocr-test-id2/message +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": "ping_debug_001", + "method": "ping", + "params": {} +} \ No newline at end of file diff --git a/mcp-proxy/test_mcp_streamable_streamable.rest b/mcp-proxy/test_mcp_streamable_streamable.rest new file mode 100644 index 0000000..0a195cf --- /dev/null +++ b/mcp-proxy/test_mcp_streamable_streamable.rest @@ -0,0 +1,95 @@ +### 测试说明 +# 本地启动了一个 Streamable HTTP MCP 服务: http://0.0.0.0:8000/mcp +# +# 重要:请求参数说明 +# - mcpProtocol: 此参数不存在!会被忽略 +# - backendProtocol: 指定后端服务的协议(可选,支持自动检测) +# - 客户端协议由请求路径决定: +# - /mcp/sse/check_status → SSE 客户端协议 +# - /mcp/stream/check_status → Stream 客户端协议 + +### ======================================== +### 测试 Streamable HTTP 客户端 + Streamable HTTP 后端(透明代理) +### ======================================== + +### 1. 检查服务状态 - Stream 到 Stream(自动检测) +# 客户端和后端都使用 Streamable HTTP 协议 +POST http://localhost:8085/mcp/stream/check_status +Content-Type: application/json + +{ + "mcpId": "test-streamable-streamable-service", + "mcpJsonConfig": "{\"mcpServers\": {\"test-service\": {\"url\": \"http://127.0.0.1:8000/mcp\"}}}", + "mcpType": "Persistent" +} + +### 2. 发送 Streamable HTTP 请求 - initialize +# 直接向基础路径发送 POST 请求,系统会自动重定向到正确的端点 +POST http://localhost:8085/mcp/stream/proxy/test-streamable-service +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} + +### 3. 列出可用工具 +# 继续使用同一个连接发送后续请求 +POST http://localhost:8085/mcp/stream/proxy/test-streamable-service +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} +} + +### 4. 调用工具(示例) +# 假设有工具可用,这里是调用工具的示例 +POST http://localhost:8085/mcp/stream/proxy/test-streamable-service +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": "msg-3", + "method": "tools/call", + "params": { + "name": "example-tool", + "arguments": {} + } +} + +### 5. 获取服务器信息 +POST http://localhost:8085/mcp/stream/proxy/test-streamable-service +Content-Type: application/json +Accept: application/json, text/event-stream + +{ + "jsonrpc": "2.0", + "id": "msg-4", + "method": "server/info", + "params": {} +} + +### ======================================== +### 清理服务 +### ======================================== + +### 6. 清理 Stream 服务 +DELETE http://localhost:8085/mcp/config/delete/test-streamable-service +Content-Type: application/json + +{} diff --git a/mcp-proxy/test_run_code.rest b/mcp-proxy/test_run_code.rest new file mode 100644 index 0000000..d201588 --- /dev/null +++ b/mcp-proxy/test_run_code.rest @@ -0,0 +1,12 @@ +### 测试运行脚本接口 (默认超时 180 秒) +# @name runCodeWithLog +POST http://localhost:8085/api/run_code_with_log +Content-Type: application/json + +{ + "engine_type": "js", + "uid": "run_code_timeout_test", + "code": "export default async function main() { console.log('start timeout test'); await new Promise(r => setTimeout(r, 200000)); return { status: 'should timeout' }; }", + "json_param": {} +} + diff --git a/mcp-proxy/test_sse_client.py b/mcp-proxy/test_sse_client.py new file mode 100644 index 0000000..a0662c2 --- /dev/null +++ b/mcp-proxy/test_sse_client.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +测试 SSE MCP 客户端 +""" +import json +import requests +import sseclient +import threading +import time + +MCP_ID = "test-sse-stream" +BASE_URL = "http://localhost:8085" +SSE_URL = f"{BASE_URL}/mcp/sse/proxy/{MCP_ID}/sse" +MESSAGE_URL_TEMPLATE = f"{BASE_URL}/mcp/sse/proxy/{MCP_ID}/message" +MESSAGE_URL = None # 将在获取 sessionId 后设置 + +def listen_sse(): + """监听 SSE 事件""" + global MESSAGE_URL + print("=== 开始监听 SSE 连接 ===") + try: + response = requests.get(SSE_URL, headers={'Accept': 'text/event-stream'}, stream=True) + client = sseclient.SSEClient(response) + + for event in client.events(): + print(f"\n收到 SSE 事件:") + print(f" Event: {event.event}") + print(f" Data: {event.data}") + + # 如果是 endpoint 事件,提取 sessionId + if event.event == "endpoint": + MESSAGE_URL = f"{BASE_URL}{event.data}" + print(f" ✅ 获取到 MESSAGE_URL: {MESSAGE_URL}") + + # 尝试解析 JSON + try: + data = json.loads(event.data) + print(f" 解析后: {json.dumps(data, indent=2, ensure_ascii=False)}") + except: + pass + + except Exception as e: + print(f"SSE 连接错误: {e}") + +def send_message(msg_id, method, params=None): + """发送消息到 MCP 服务""" + message = { + "jsonrpc": "2.0", + "id": msg_id, + "method": method, + "params": params or {} + } + + print(f"\n=== 发送消息: {method} ===") + print(json.dumps(message, indent=2, ensure_ascii=False)) + + try: + response = requests.post( + MESSAGE_URL, + json=message, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) + print(f"响应状态码: {response.status_code}") + if response.text: + print(f"响应内容: {response.text}") + except requests.exceptions.Timeout: + print("请求超时(这是正常的,响应会通过 SSE 返回)") + except Exception as e: + print(f"发送消息错误: {e}") + +def main(): + global MESSAGE_URL + # 启动 SSE 监听线程 + sse_thread = threading.Thread(target=listen_sse, daemon=True) + sse_thread.start() + + # 等待 SSE 连接建立并获取 sessionId + print("等待获取 sessionId...") + timeout = time.time() + 10 + while MESSAGE_URL is None and time.time() < timeout: + time.sleep(0.5) + + if MESSAGE_URL is None: + print("❌ 未能获取 sessionId,退出") + return + + print(f"✅ 已获取 MESSAGE_URL: {MESSAGE_URL}") + time.sleep(1) + + # 发送 initialize 消息 + send_message("msg-1", "initialize", { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + }) + + time.sleep(2) + + # 发送 tools/list 消息 + send_message("msg-2", "tools/list", {}) + + time.sleep(2) + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + main() diff --git a/mcp-proxy/test_sse_complete.sh b/mcp-proxy/test_sse_complete.sh new file mode 100755 index 0000000..a590c9b --- /dev/null +++ b/mcp-proxy/test_sse_complete.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +set -e + +MCP_ID="test-sse-stream" +BASE_URL="http://localhost:8085" + +echo "=== 1. 创建服务 ===" +curl -s -X POST "${BASE_URL}/mcp/sse/check_status" \ + -H "Content-Type: application/json" \ + -d "{ + \"mcpId\": \"${MCP_ID}\", + \"mcpJsonConfig\": \"{\\\"mcpServers\\\": {\\\"test\\\": {\\\"url\\\": \\\"http://127.0.0.1:8000/mcp\\\"}}}\", + \"mcpType\": \"Persistent\", + \"backendProtocol\": \"Stream\" + }" | jq . + +echo "" +echo "=== 2. 等待服务就绪 ===" +sleep 5 + +STATUS=$(curl -s "${BASE_URL}/mcp/check/status/${MCP_ID}" | jq -r '.data.status') +echo "服务状态: ${STATUS}" + +if [ "${STATUS}" != "Ready" ]; then + echo "服务未就绪,退出" + exit 1 +fi + +echo "" +echo "=== 3. 建立 SSE 连接并保存到文件 ===" +curl -N "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/sse" \ + -H "Accept: text/event-stream" > /tmp/sse_output.txt & +SSE_PID=$! +echo "SSE 连接已启动,PID: ${SSE_PID}" + +sleep 3 + +echo "" +echo "=== 4. 从 SSE 输出中提取 sessionId ===" +SESSION_ID=$(grep "sessionId=" /tmp/sse_output.txt | head -1 | sed 's/.*sessionId=\([^"]*\).*/\1/') +echo "Session ID: ${SESSION_ID}" + +if [ -z "${SESSION_ID}" ]; then + echo "未能获取 session ID" + kill ${SSE_PID} 2>/dev/null + exit 1 +fi + +echo "" +echo "=== 5. 发送 initialize 消息 ===" +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-init", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }' + +sleep 2 + +echo "" +echo "=== 6. 发送 tools/list 消息 ===" +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-tools", + "method": "tools/list", + "params": {} + }' + +sleep 3 + +echo "" +echo "=== 7. 查看 SSE 接收到的消息 ===" +cat /tmp/sse_output.txt + +echo "" +echo "=== 8. 清理 ===" +kill ${SSE_PID} 2>/dev/null || true +rm -f /tmp/sse_output.txt + +echo "" +echo "=== 测试完成 ===" diff --git a/mcp-proxy/test_sse_connection.sh b/mcp-proxy/test_sse_connection.sh new file mode 100755 index 0000000..7645814 --- /dev/null +++ b/mcp-proxy/test_sse_connection.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# 测试 SSE 连接和消息发送 + +MCP_ID="test-streamable-new" +BASE_URL="http://localhost:8085" + +echo "=== 测试 SSE 连接 ===" +echo "启动 SSE 连接(后台运行)..." + +# 启动 SSE 连接并保存到文件 +curl -N -s "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/sse" \ + -H "Accept: text/event-stream" > /tmp/sse_output.txt & +SSE_PID=$! + +echo "SSE 连接已启动,PID: ${SSE_PID}" +sleep 2 + +echo "" +echo "=== 发送 initialize 消息 ===" +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }' + +echo "" +sleep 2 + +echo "" +echo "=== 发送 tools/list 消息 ===" +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} + }' + +echo "" +sleep 2 + +echo "" +echo "=== SSE 接收到的消息 ===" +cat /tmp/sse_output.txt + +# 清理 +kill ${SSE_PID} 2>/dev/null +rm -f /tmp/sse_output.txt + +echo "" +echo "=== 测试完成 ===" diff --git a/mcp-proxy/test_sse_paths.md b/mcp-proxy/test_sse_paths.md new file mode 100644 index 0000000..fdc3cd9 --- /dev/null +++ b/mcp-proxy/test_sse_paths.md @@ -0,0 +1,40 @@ +# SSE 路径配置测试 + +## 当前问题 +- 路由注册:`/mcp/sse/proxy/baidu-ocr-test-id2` +- SSE 路径:`/mcp/sse/proxy/baidu-ocr-test-id2/sse` +- Message 路径:`/mcp/sse/proxy/baidu-ocr-test-id2/message` +- 结果:404 错误 + +## 可能的解决方案 + +### 方案1:相对路径配置 +```rust +let config = SseServerConfig { + bind: addr.parse()?, + sse_path: "/sse".to_string(), + post_path: "/message".to_string(), + // ... +}; +``` + +### 方案2:绝对路径配置 +```rust +let config = SseServerConfig { + bind: addr.parse()?, + sse_path: sse_path.sse_path.clone(), + post_path: sse_path.message_path.clone(), + // ... +}; +``` + +### 方案3:路由嵌套 +```rust +let nested_router = axum::Router::new() + .nest(&format!("/{}", mcp_id), sse_router); +``` + +## 需要从官方 SDK 了解的信息 +1. SSE 服务器的路径配置是相对路径还是绝对路径? +2. 路由注册时应该注册到什么级别的路径? +3. 是否需要特殊的路径重写逻辑? \ No newline at end of file diff --git a/mcp-proxy/test_sse_simple.sh b/mcp-proxy/test_sse_simple.sh new file mode 100644 index 0000000..a1ef61b --- /dev/null +++ b/mcp-proxy/test_sse_simple.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +MCP_ID="test-streamable-new" +BASE_URL="http://localhost:8085" + +echo "=== 测试 1: 检查服务状态 ===" +curl -s "${BASE_URL}/mcp/check/status/${MCP_ID}" | jq . + +echo "" +echo "=== 测试 2: 尝试直接调用 list_tools(通过透明代理) ===" +echo "注意:这个测试是为了验证 ProxyHandler 是否工作" + +# 直接测试后端服务 +echo "" +echo "=== 测试 3: 直接测试后端 Streamable HTTP 服务 ===" +curl -s -X POST http://127.0.0.1:8000/mcp \ + -H "Accept: application/json, text/event-stream" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":"test","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \ + | head -5 + +echo "" +echo "" +echo "=== 测试 4: SSE 连接测试 ===" +echo "提示:SSE 连接会保持打开状态,按 Ctrl+C 停止" +echo "在另一个终端运行以下命令发送消息:" +echo "" +echo "curl -X POST ${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message \\" +echo " -H 'Content-Type: application/json' \\" +echo " -d '{\"jsonrpc\":\"2.0\",\"id\":\"msg-1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0\"}}}'" +echo "" +echo "开始 SSE 连接..." +curl -N "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/sse" \ + -H "Accept: text/event-stream" diff --git a/oss-client/Cargo.toml b/oss-client/Cargo.toml index fea768b..c9b8584 100644 --- a/oss-client/Cargo.toml +++ b/oss-client/Cargo.toml @@ -13,9 +13,9 @@ serde = { workspace = true } tokio = { workspace = true, features = ["fs"] } tracing = { workspace = true } uuid = { workspace = true, features = ["v4"] } -async-trait = "0.1" -tempfile = "3.0" -thiserror = "2.0" +async-trait = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] tokio = { workspace = true } \ No newline at end of file diff --git a/oss-client/src/public_client.rs b/oss-client/src/public_client.rs index fb9b39e..d98e4bf 100644 --- a/oss-client/src/public_client.rs +++ b/oss-client/src/public_client.rs @@ -508,7 +508,6 @@ mod tests { ); let client = PublicOssClient::new(config).unwrap(); - let url = client .generate_public_download_url("test/file.txt") .unwrap(); @@ -532,7 +531,6 @@ mod tests { ); let client = PublicOssClient::new(config).unwrap(); - let url = client.generate_public_access_url("test/image.jpg").unwrap(); // 验证URL包含正确的路径,但域名可能被替换为自定义域名 assert!(url.contains("edu/test/image.jpg")); @@ -554,7 +552,6 @@ mod tests { ); let client = PublicOssClient::new(config).unwrap(); - let keys = vec!["doc1.pdf", "doc2.pdf", "image.jpg"]; let urls = client.generate_public_urls_batch(&keys).unwrap(); diff --git a/test_complete_sse_streamable.sh b/test_complete_sse_streamable.sh new file mode 100755 index 0000000..f20ecc0 --- /dev/null +++ b/test_complete_sse_streamable.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +set -e + +MCP_ID="test-streamable-service" +BASE_URL="http://localhost:8085" + +echo "=========================================" +echo "测试 SSE 客户端 → Streamable HTTP 后端" +echo "=========================================" + +# 1. 创建服务 +echo "" +echo "1. 创建服务..." +RESPONSE=$(curl -s -X POST "${BASE_URL}/mcp/sse/check_status" \ + -H "Content-Type: application/json" \ + -d "{ + \"mcpId\": \"${MCP_ID}\", + \"mcpJsonConfig\": \"{\\\"mcpServers\\\": {\\\"test\\\": {\\\"url\\\": \\\"http://127.0.0.1:8000/mcp\\\"}}}\", + \"mcpType\": \"Persistent\" + }") + +echo "$RESPONSE" | jq . + +# 2. 等待服务就绪 +echo "" +echo "2. 等待服务就绪..." +sleep 5 + +STATUS=$(curl -s "${BASE_URL}/mcp/check/status/${MCP_ID}" | jq -r '.data.status') +echo " 服务状态: ${STATUS}" + +if [ "${STATUS}" != "Ready" ]; then + echo " ❌ 服务未就绪" + exit 1 +fi + +echo " ✅ 服务已就绪" + +# 3. 建立 SSE 连接 +echo "" +echo "3. 建立 SSE 连接..." +curl -N "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/sse" \ + -H "Accept: text/event-stream" > /tmp/sse_test_output.txt 2>&1 & +SSE_PID=$! +echo " SSE PID: ${SSE_PID}" + +sleep 3 + +# 4. 提取 sessionId +echo "" +echo "4. 提取 sessionId..." +SESSION_ID=$(grep "sessionId=" /tmp/sse_test_output.txt | head -1 | sed 's/.*sessionId=\([^ ]*\).*/\1/') +echo " Session ID: ${SESSION_ID}" + +if [ -z "${SESSION_ID}" ]; then + echo " ❌ 未能获取 sessionId" + kill ${SSE_PID} 2>/dev/null + exit 1 +fi + +echo " ✅ 成功获取 sessionId" + +# 5. 发送 initialize +echo "" +echo "5. 发送 initialize 消息..." +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "test-client", "version": "1.0"} + } + }' > /dev/null 2>&1 & + +sleep 3 + +# 6. 发送 tools/list +echo "" +echo "6. 发送 tools/list 消息..." +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} + }' > /dev/null 2>&1 & + +sleep 3 + +# 7. 发送 tools/call +echo "" +echo "7. 发送 tools/call 消息..." +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-3", + "method": "tools/call", + "params": { + "name": "hello", + "arguments": {"name": "World"} + } + }' > /dev/null 2>&1 & + +sleep 3 + +# 8. 显示结果 +echo "" +echo "=========================================" +echo "SSE 接收到的所有消息" +echo "=========================================" +cat /tmp/sse_test_output.txt +echo "" + +# 9. 验证结果 +echo "" +echo "=========================================" +echo "测试结果验证" +echo "=========================================" + +if grep -q "msg-1" /tmp/sse_test_output.txt; then + echo "✅ Initialize 成功" + echo " 服务器信息:" + grep "msg-1" /tmp/sse_test_output.txt | grep -o '"serverInfo":{[^}]*}' | head -1 | jq . +else + echo "❌ Initialize 失败" +fi + +if grep -q "msg-2" /tmp/sse_test_output.txt; then + echo "✅ Tools/list 成功" + TOOLS=$(grep "msg-2" /tmp/sse_test_output.txt | grep -o '"tools":\[[^]]*\]' | head -1) + echo " 工具列表: $TOOLS" +else + echo "❌ Tools/list 失败" +fi + +if grep -q "msg-3" /tmp/sse_test_output.txt; then + echo "✅ Tools/call 成功" + RESULT=$(grep "msg-3" /tmp/sse_test_output.txt | grep -o '"content":\[[^]]*\]' | head -1) + echo " 调用结果: $RESULT" +else + echo "❌ Tools/call 失败" +fi + +# 清理 +kill ${SSE_PID} 2>/dev/null || true +rm -f /tmp/sse_test_output.txt + +echo "" +echo "=========================================" +echo "测试完成!" +echo "=========================================" diff --git a/test_sse_stream.sh b/test_sse_stream.sh new file mode 100644 index 0000000..79d00d9 --- /dev/null +++ b/test_sse_stream.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +MCP_ID="test-sse-stream" +BASE_URL="http://localhost:8085" + +echo "=== 建立 SSE 连接 ===" +curl -N "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/sse" \ + -H "Accept: text/event-stream" > /tmp/sse_test.txt & +SSE_PID=$! +echo "SSE PID: $SSE_PID" + +sleep 3 + +echo "" +echo "=== 提取 sessionId ===" +SESSION_ID=$(grep "sessionId=" /tmp/sse_test.txt | head -1 | sed 's/.*sessionId=\([^ ]*\).*/\1/') +echo "Session ID: $SESSION_ID" + +echo "" +echo "=== 发送 initialize ===" +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "test", "version": "1.0"} + } + }' & + +sleep 3 + +echo "" +echo "=== 发送 tools/list ===" +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} + }' & + +sleep 5 + +echo "" +echo "=== SSE 接收到的消息 ===" +cat /tmp/sse_test.txt + +kill $SSE_PID 2>/dev/null || true +rm -f /tmp/sse_test.txt diff --git a/test_sse_streamable.sh b/test_sse_streamable.sh new file mode 100755 index 0000000..866ec1d --- /dev/null +++ b/test_sse_streamable.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +MCP_ID="sse-to-stream-test" +BASE_URL="http://localhost:8085" + +echo "=== 测试 SSE 客户端 → Streamable HTTP 后端 ===" +echo "" + +# 1. 建立 SSE 连接 +echo "1. 建立 SSE 连接..." +curl -N "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/sse" \ + -H "Accept: text/event-stream" > /tmp/sse_output.txt 2>&1 & +SSE_PID=$! +echo " SSE PID: $SSE_PID" + +sleep 3 + +# 2. 提取 sessionId +echo "" +echo "2. 提取 sessionId..." +SESSION_ID=$(grep "sessionId=" /tmp/sse_output.txt | head -1 | sed 's/.*sessionId=\([^ ]*\).*/\1/') +echo " Session ID: $SESSION_ID" + +if [ -z "$SESSION_ID" ]; then + echo " ❌ 未能获取 sessionId" + kill $SSE_PID 2>/dev/null + exit 1 +fi + +# 3. 发送 initialize +echo "" +echo "3. 发送 initialize 消息..." +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "test-client", "version": "1.0"} + } + }' > /dev/null 2>&1 & + +sleep 3 + +# 4. 发送 tools/list +echo "" +echo "4. 发送 tools/list 消息..." +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} + }' > /dev/null 2>&1 & + +sleep 3 + +# 5. 发送 tools/call +echo "" +echo "5. 发送 tools/call 消息..." +curl -s -X POST "${BASE_URL}/mcp/sse/proxy/${MCP_ID}/message?sessionId=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "msg-3", + "method": "tools/call", + "params": { + "name": "hello", + "arguments": {"name": "World"} + } + }' > /dev/null 2>&1 & + +sleep 3 + +# 6. 显示结果 +echo "" +echo "=== SSE 接收到的所有消息 ===" +cat /tmp/sse_output.txt +echo "" + +# 7. 解析并显示关键信息 +echo "" +echo "=== 测试结果摘要 ===" +if grep -q "msg-1" /tmp/sse_output.txt; then + echo "✅ Initialize 成功" + grep "msg-1" /tmp/sse_output.txt | grep -o '"serverInfo":{[^}]*}' | head -1 +fi + +if grep -q "msg-2" /tmp/sse_output.txt; then + echo "✅ Tools/list 成功" + grep "msg-2" /tmp/sse_output.txt | grep -o '"tools":\[[^]]*\]' | head -1 +fi + +if grep -q "msg-3" /tmp/sse_output.txt; then + echo "✅ Tools/call 成功" + grep "msg-3" /tmp/sse_output.txt | grep -o '"content":\[[^]]*\]' | head -1 +fi + +# 清理 +kill $SSE_PID 2>/dev/null +rm -f /tmp/sse_output.txt + +echo "" +echo "=== 测试完成 ===" diff --git a/test_sse_to_stream.py b/test_sse_to_stream.py new file mode 100644 index 0000000..1e81209 --- /dev/null +++ b/test_sse_to_stream.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""测试 SSE 客户端连接到 Streamable HTTP 后端""" + +import requests +import json +import sseclient +import time + +MCP_ID = "test-sse-stream" +BASE_URL = "http://localhost:8085" + +def test_sse_to_stream(): + print("=" * 60) + print("测试 SSE 客户端 → Streamable HTTP 后端") + print("=" * 60) + + # 1. 建立 SSE 连接 + print("\n1. 建立 SSE 连接...") + sse_url = f"{BASE_URL}/mcp/sse/proxy/{MCP_ID}/sse" + print(f" URL: {sse_url}") + + response = requests.get(sse_url, stream=True, headers={"Accept": "text/event-stream"}) + client = sseclient.SSEClient(response) + + # 获取 endpoint 事件 + endpoint_url = None + for event in client.events(): + print(f" 收到事件: {event.event}") + print(f" 数据: {event.data}") + if event.event == "endpoint": + endpoint_url = event.data + break + + if not endpoint_url: + print(" ❌ 未能获取 endpoint") + return + + print(f" ✅ 获取到 endpoint: {endpoint_url}") + + # 2. 发送 initialize 消息 + print("\n2. 发送 initialize 消息...") + message_url = f"{BASE_URL}{endpoint_url}" + print(f" URL: {message_url}") + + initialize_msg = { + "jsonrpc": "2.0", + "id": "msg-1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + } + + print(f" 发送: {json.dumps(initialize_msg, indent=2)}") + resp = requests.post(message_url, json=initialize_msg) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text}") + + # 3. 从 SSE 流中读取响应 + print("\n3. 从 SSE 流中读取响应...") + timeout = time.time() + 5 # 5秒超时 + for event in client.events(): + if time.time() > timeout: + print(" ⏱️ 超时") + break + + print(f" 事件类型: {event.event}") + if event.event == "message": + print(f" ✅ 收到消息: {event.data}") + msg = json.loads(event.data) + if msg.get("id") == "msg-1": + print(f" ✅ Initialize 成功!") + print(f" 服务器信息: {json.dumps(msg.get('result', {}), indent=2)}") + break + + # 4. 发送 tools/list 消息 + print("\n4. 发送 tools/list 消息...") + tools_msg = { + "jsonrpc": "2.0", + "id": "msg-2", + "method": "tools/list", + "params": {} + } + + print(f" 发送: {json.dumps(tools_msg, indent=2)}") + resp = requests.post(message_url, json=tools_msg) + print(f" 状态码: {resp.status_code}") + + # 5. 从 SSE 流中读取 tools/list 响应 + print("\n5. 从 SSE 流中读取 tools/list 响应...") + timeout = time.time() + 5 + for event in client.events(): + if time.time() > timeout: + print(" ⏱️ 超时") + break + + print(f" 事件类型: {event.event}") + if event.event == "message": + print(f" 收到消息: {event.data}") + msg = json.loads(event.data) + if msg.get("id") == "msg-2": + print(f" ✅ Tools/list 成功!") + tools = msg.get("result", {}).get("tools", []) + print(f" 工具数量: {len(tools)}") + for tool in tools: + print(f" - {tool.get('name')}: {tool.get('description')}") + break + + print("\n" + "=" * 60) + print("测试完成!") + print("=" * 60) + +if __name__ == "__main__": + test_sse_to_stream() diff --git a/voice-cli/src/cli/mod.rs b/voice-cli/src/cli/mod.rs index ef0dce8..69fc600 100644 --- a/voice-cli/src/cli/mod.rs +++ b/voice-cli/src/cli/mod.rs @@ -3,8 +3,6 @@ pub mod tts; pub use tts::TtsAction; - - use clap::{Parser, Subcommand}; #[derive(Parser)] @@ -86,6 +84,5 @@ pub enum ModelAction { }, } - // Daemon mode is no longer supported // Use foreground mode with shell scripts for background operation diff --git a/voice-cli/src/cli/model.rs b/voice-cli/src/cli/model.rs index 011d130..2a7d320 100644 --- a/voice-cli/src/cli/model.rs +++ b/voice-cli/src/cli/model.rs @@ -1,6 +1,6 @@ +use crate::VoiceCliError; use crate::models::Config; use crate::services::ModelService; -use crate::VoiceCliError; use tracing::{error, info, warn}; pub async fn handle_model_download(config: &Config, model_name: &str) -> crate::Result<()> { @@ -181,10 +181,10 @@ pub async fn ensure_default_model(config: &Config) -> crate::Result<()> { ); handle_model_download(config, &config.whisper.default_model).await?; } else { - return Err(VoiceCliError::ModelNotFound( - format!("No models found and auto_download is disabled. Please run: voice-cli model download {}", - config.whisper.default_model) - )); + return Err(VoiceCliError::ModelNotFound(format!( + "No models found and auto_download is disabled. Please run: voice-cli model download {}", + config.whisper.default_model + ))); } Ok(()) diff --git a/voice-cli/src/cli/tts.rs b/voice-cli/src/cli/tts.rs index 5a422a2..c3b5313 100644 --- a/voice-cli/src/cli/tts.rs +++ b/voice-cli/src/cli/tts.rs @@ -14,27 +14,27 @@ pub enum TtsAction { /// Text to synthesize #[arg(short, long, default_value = "Hello, world!")] text: String, - + /// Output file path #[arg(short, long)] output: Option, - + /// Model to use #[arg(short, long)] model: Option, - + /// Speech speed (0.5-2.0) #[arg(short, long, default_value = "1.0")] speed: f32, - + /// Pitch adjustment (-20 to 20) #[arg(short, long, default_value = "0")] pitch: i32, - + /// Volume adjustment (0.5-2.0) #[arg(short, long, default_value = "1.0")] volume: f32, - + /// Output format #[arg(short, long, default_value = "mp3")] format: String, @@ -44,11 +44,12 @@ pub enum TtsAction { /// Initialize TTS environment pub async fn handle_tts_init(force: bool) -> anyhow::Result<()> { println!("🎤 Initializing TTS environment..."); - + // Reuse the server init logic for Python environment - crate::server::init_python_tts_environment().await + crate::server::init_python_tts_environment() + .await .map_err(|e| anyhow::anyhow!("Failed to initialize TTS environment: {}", e))?; - + println!("✅ TTS environment initialized successfully"); Ok(()) } @@ -65,13 +66,14 @@ pub async fn handle_tts_test( format: String, ) -> anyhow::Result<()> { println!("🎤 Testing TTS functionality..."); - + // Create TTS service let tts_service = crate::services::TtsService::new( config.tts.python_path.clone(), config.tts.model_path.clone(), - ).map_err(|e| anyhow::anyhow!("Failed to create TTS service: {}", e))?; - + ) + .map_err(|e| anyhow::anyhow!("Failed to create TTS service: {}", e))?; + // Create request let request = crate::models::TtsSyncRequest { text, @@ -81,44 +83,40 @@ pub async fn handle_tts_test( volume: Some(volume), format: Some(format), }; - + // Test synthesis - let result_path = tts_service.synthesize_sync(request).await + let result_path = tts_service + .synthesize_sync(request) + .await .map_err(|e| anyhow::anyhow!("TTS synthesis failed: {}", e))?; - + println!("✅ TTS test successful!"); println!("📁 Output file: {}", result_path.display()); - + // Copy to specified output path if provided if let Some(output_path) = output { - tokio::fs::copy(&result_path, &output_path).await + tokio::fs::copy(&result_path, &output_path) + .await .map_err(|e| anyhow::anyhow!("Failed to copy output file: {}", e))?; println!("📁 Copied to: {}", output_path.display()); } - + Ok(()) } /// Handle TTS-related commands -pub async fn handle_tts_command( - action: TtsAction, - config: &crate::Config, -) -> anyhow::Result<()> { +pub async fn handle_tts_command(action: TtsAction, config: &crate::Config) -> anyhow::Result<()> { match action { - TtsAction::Init { force } => { - handle_tts_init(force).await - } - - TtsAction::Test { - text, - output, - model, - speed, - pitch, - volume, - format - } => { - handle_tts_test(config, text, output, model, speed, pitch, volume, format).await - } + TtsAction::Init { force } => handle_tts_init(force).await, + + TtsAction::Test { + text, + output, + model, + speed, + pitch, + volume, + format, + } => handle_tts_test(config, text, output, model, speed, pitch, volume, format).await, } -} \ No newline at end of file +} diff --git a/voice-cli/src/config.rs b/voice-cli/src/config.rs index 05e9432..a1cc6cd 100644 --- a/voice-cli/src/config.rs +++ b/voice-cli/src/config.rs @@ -28,9 +28,7 @@ impl ServiceType { /// 获取所有支持的服务类型 pub fn all() -> &'static [ServiceType] { - &[ - ServiceType::Server, - ] + &[ServiceType::Server] } } @@ -92,4 +90,3 @@ pub struct ConfigChangeNotification { pub new_config: Config, pub changed_at: SystemTime, } - diff --git a/voice-cli/src/config_rs_integration.rs b/voice-cli/src/config_rs_integration.rs index cceb778..91d42b5 100644 --- a/voice-cli/src/config_rs_integration.rs +++ b/voice-cli/src/config_rs_integration.rs @@ -1,5 +1,5 @@ -use crate::models::Config; use crate::VoiceCliError; +use crate::models::Config; use config::{Config as ConfigRs, Environment, File}; use serde::Deserialize; use std::path::PathBuf; @@ -8,15 +8,15 @@ use std::path::PathBuf; fn create_default_config_source() -> Result { // Create a temporary config with defaults let default_config = Config::default(); - + // Serialize to YAML and then parse back as config source let yaml_content = serde_yaml::to_string(&default_config)?; - + // Create config from YAML content let config_rs = ConfigRs::builder() .add_source(File::from_str(&yaml_content, config::FileFormat::Yaml)) .build()?; - + Ok(config_rs) } @@ -73,8 +73,8 @@ impl ConfigRsLoader { } } else if let Some(service_type) = service_type { // Try to load service-specific default config - let default_config_path = std::env::current_dir()? - .join(service_type.default_config_filename()); + let default_config_path = + std::env::current_dir()?.join(service_type.default_config_filename()); if default_config_path.exists() { config_rs = config_rs.add_source(File::from(default_config_path)); } @@ -91,11 +91,10 @@ impl ConfigRsLoader { // 4. Build the config and debug what's being loaded let built_config = config_rs.build()?; - - + // 5. Deserialize the built config let mut config: Config = built_config.try_deserialize()?; - + // 6. Apply CLI overrides (highest priority) Self::apply_cli_overrides(&mut config, cli_overrides); @@ -153,14 +152,14 @@ impl ConfigRsLoader { /// Manually merge environment variable overrides (config-rs adds underscore prefix) fn merge_environment_overrides(_config: &mut Config, _built_config: &ConfigRs) { use config::ValueKind; - + // Check if there are any underscore-prefixed values from environment variables if let ValueKind::Table(cache) = &_built_config.cache.kind { for (key, value) in cache { if key.starts_with('_') { // This is an environment variable override let clean_key = &key[1..]; // Remove the underscore prefix - + // Handle specific environment variable overrides // Add environment variable overrides here as needed let _ = clean_key; // Avoid unused variable warning @@ -224,4 +223,4 @@ mod tests { assert_eq!(config.server.port, 9090); assert_eq!(config.logging.level, "debug"); } -} \ No newline at end of file +} diff --git a/voice-cli/src/error.rs b/voice-cli/src/error.rs index 114e94b..5a1ca82 100644 --- a/voice-cli/src/error.rs +++ b/voice-cli/src/error.rs @@ -1,6 +1,6 @@ +use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use axum::Json; use config::ConfigError; use serde_json::json; use thiserror::Error; @@ -146,7 +146,9 @@ impl IntoResponse for VoiceCliError { VoiceCliError::ConfigRs(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), VoiceCliError::TaskManagementDisabled => (StatusCode::BAD_REQUEST, self.to_string()), VoiceCliError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), - VoiceCliError::Initialization(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + VoiceCliError::Initialization(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) + } VoiceCliError::TtsError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), VoiceCliError::InvalidInput(_) => (StatusCode::BAD_REQUEST, self.to_string()), VoiceCliError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), @@ -161,6 +163,4 @@ impl IntoResponse for VoiceCliError { } } - pub type Result = std::result::Result; - diff --git a/voice-cli/src/main.rs b/voice-cli/src/main.rs index 3c8c7df..052e865 100644 --- a/voice-cli/src/main.rs +++ b/voice-cli/src/main.rs @@ -169,7 +169,15 @@ async fn handle_tts_command(action: TtsAction, config: &voice_cli::Config) -> Re .await .context("Failed to initialize TTS environment") } - TtsAction::Test { text, output, model, speed, pitch, volume, format } => { + TtsAction::Test { + text, + output, + model, + speed, + pitch, + volume, + format, + } => { info!("Testing TTS functionality"); tts::handle_tts_test(config, text, output, model, speed, pitch, volume, format) .await diff --git a/voice-cli/src/models/config.rs b/voice-cli/src/models/config.rs index 542023d..c12eb9c 100644 --- a/voice-cli/src/models/config.rs +++ b/voice-cli/src/models/config.rs @@ -135,7 +135,6 @@ pub struct TtsConfig { pub timeout_seconds: u64, } - impl Default for Config { fn default() -> Self { Self { @@ -268,8 +267,6 @@ impl Default for TtsConfig { } } - - impl Config { pub fn load(config_path: &PathBuf) -> crate::Result { let config_content = std::fs::read_to_string(config_path).map_err(|e| { @@ -352,8 +349,6 @@ impl Config { tracing::info!("Applied environment override: VOICE_CLI_PORT = {}", port); } - - // Max file size override if let Ok(size_str) = std::env::var("VOICE_CLI_MAX_FILE_SIZE") { let size = size_str.parse::().map_err(|_| { @@ -389,9 +384,6 @@ impl Config { ); } - - - // Logging configuration overrides if let Ok(level) = std::env::var("VOICE_CLI_LOG_LEVEL") { let level = level.to_lowercase(); @@ -501,7 +493,6 @@ impl Config { ); } - // Daemon configuration overrides if let Ok(work_dir) = std::env::var("VOICE_CLI_WORK_DIR") { if work_dir.trim().is_empty() { @@ -529,7 +520,6 @@ impl Config { ); } - if let Ok(max_tasks_str) = std::env::var("VOICE_CLI_MAX_CONCURRENT_TASKS") { let max_tasks = max_tasks_str.parse::().map_err(|_| { crate::VoiceCliError::Config(format!( @@ -594,7 +584,6 @@ impl Config { ); } - Ok(()) } @@ -712,8 +701,6 @@ impl Config { )); } - Ok(()) } - } diff --git a/voice-cli/src/models/http_result.rs b/voice-cli/src/models/http_result.rs index 45fc9b0..e1c7f03 100644 --- a/voice-cli/src/models/http_result.rs +++ b/voice-cli/src/models/http_result.rs @@ -1,7 +1,7 @@ use crate::VoiceCliError; use axum::{ - response::{IntoResponse, Response}, Json, + response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -146,14 +146,22 @@ impl From for HttpResult { } VoiceCliError::Model(msg) => Self::system_error(format!("Model error: {}", msg)), VoiceCliError::Daemon(msg) => Self::system_error(format!("Daemon error: {}", msg)), - VoiceCliError::ConfigRs(err) => Self::system_error(format!("Configuration error: {}", err)), + VoiceCliError::ConfigRs(err) => { + Self::system_error(format!("Configuration error: {}", err)) + } VoiceCliError::Storage(msg) => Self::system_error(format!("Storage error: {}", msg)), - VoiceCliError::TaskManagementDisabled => Self::system_error("Task management is disabled".to_string()), + VoiceCliError::TaskManagementDisabled => { + Self::system_error("Task management is disabled".to_string()) + } VoiceCliError::NotFound(msg) => Self::task_not_found(msg), VoiceCliError::Network(msg) => Self::system_error(format!("Network error: {}", msg)), - VoiceCliError::Initialization(msg) => Self::system_error(format!("Initialization error: {}", msg)), + VoiceCliError::Initialization(msg) => { + Self::system_error(format!("Initialization error: {}", msg)) + } VoiceCliError::TtsError(msg) => Self::processing_failed(format!("TTS error: {}", msg)), - VoiceCliError::InvalidInput(msg) => Self::unsupported_format(format!("Invalid input: {}", msg)), + VoiceCliError::InvalidInput(msg) => { + Self::unsupported_format(format!("Invalid input: {}", msg)) + } VoiceCliError::Io(msg) => Self::system_error(format!("I/O error: {}", msg)), } } diff --git a/voice-cli/src/models/mod.rs b/voice-cli/src/models/mod.rs index cbb59b8..91deee1 100644 --- a/voice-cli/src/models/mod.rs +++ b/voice-cli/src/models/mod.rs @@ -12,22 +12,22 @@ pub use http_result::*; // Request module exports (for HTTP API) pub use request::{ - AudioFormat, AudioFormatResult, DaemonStatus, DetectionMethod, DownloadStatus, HealthResponse, - ModelDownloadStatus, ModelInfo, ModelsResponse, ProcessedAudio, Segment, TranscriptionResponse, - AudioMetadata, TranscriptionRequest, + AudioFormat, AudioFormatResult, AudioMetadata, DaemonStatus, DetectionMethod, DownloadStatus, + HealthResponse, ModelDownloadStatus, ModelInfo, ModelsResponse, ProcessedAudio, Segment, + TranscriptionRequest, TranscriptionResponse, }; // Stepped task module exports pub use stepped_task::{ AsyncTranscriptionTask, AudioProcessedTask, ProcessingStage, ProcessingStageInfo, - ProgressDetails, SerializableSegment, SerializableTranscriptionResult, TaskError, - TaskPriority, TaskStatus, TranscriptionCompletedTask, + ProgressDetails, SerializableSegment, SerializableTranscriptionResult, TaskError, TaskPriority, + TaskStatus, TranscriptionCompletedTask, }; // TTS module exports pub use tts::{ - TtsSyncRequest, TtsAsyncRequest, TtsTaskResponse, TtsProcessingStage, TtsTaskStatus, - TtsProgressDetails, TtsTaskError, TaskPriority as TtsTaskPriority, + TaskPriority as TtsTaskPriority, TtsAsyncRequest, TtsProcessingStage, TtsProgressDetails, + TtsSyncRequest, TtsTaskError, TtsTaskResponse, TtsTaskStatus, }; // 简化的任务响应类型 diff --git a/voice-cli/src/models/request.rs b/voice-cli/src/models/request.rs index 97af0aa..eaabeec 100644 --- a/voice-cli/src/models/request.rs +++ b/voice-cli/src/models/request.rs @@ -18,7 +18,7 @@ pub struct AudioVideoMetadata { /// 文件大小(字节) #[schema(example = 3640010)] pub file_size_bytes: u64, - + // 音频信息 /// 音频编码器 #[schema(example = "mp3")] @@ -32,7 +32,7 @@ pub struct AudioVideoMetadata { /// 音频码率 (kbps) #[schema(example = 128)] pub audio_bitrate: u32, - + // 视频信息(如果是视频文件) /// 是否包含视频 #[schema(example = false)] @@ -52,7 +52,7 @@ pub struct AudioVideoMetadata { /// 帧率 #[serde(skip_serializing_if = "Option::is_none")] pub frame_rate: Option, - + // 其他元数据 /// 总码率 (kbps) #[schema(example = 160)] @@ -379,7 +379,7 @@ impl AudioFormat { pub fn from_symphonia_codec(codec_type: symphonia::core::codecs::CodecType) -> Self { // Convert codec type to string for matching since Symphonia 0.5 uses different constants let codec_str = format!("{:?}", codec_type).to_lowercase(); - + if codec_str.contains("pcm") || codec_str.contains("wav") { AudioFormat::Wav } else if codec_str.contains("mp3") || codec_str.contains("mpeg") { diff --git a/voice-cli/src/models/stepped_task.rs b/voice-cli/src/models/stepped_task.rs index d8f4159..f2603c3 100644 --- a/voice-cli/src/models/stepped_task.rs +++ b/voice-cli/src/models/stepped_task.rs @@ -10,7 +10,7 @@ use crate::models::AudioFormat; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AsyncTranscriptionTask { pub task_id: String, - pub audio_file_path: PathBuf, // Path to the audio file on disk + pub audio_file_path: PathBuf, // Path to the audio file on disk pub original_filename: String, // Original filename from upload pub model: Option, pub response_format: Option, @@ -194,8 +194,16 @@ impl std::fmt::Display for TaskError { TaskError::StorageError { operation, message } => { write!(f, "存储错误 ({}): {}", operation, message) } - TaskError::TimeoutError { stage, timeout_duration } => { - write!(f, "超时错误 ({}): {} 秒", stage.step_name(), timeout_duration.as_secs()) + TaskError::TimeoutError { + stage, + timeout_duration, + } => { + write!( + f, + "超时错误 ({}): {} 秒", + stage.step_name(), + timeout_duration.as_secs() + ) } TaskError::CancellationRequested => { write!(f, "任务已被取消") @@ -259,7 +267,11 @@ impl From for SerializableTranscription fn from(result: voice_toolkit::stt::TranscriptionResult) -> Self { Self { text: result.text, - segments: result.segments.into_iter().map(|s| SerializableSegment::from_voice_toolkit_segment(s)).collect(), + segments: result + .segments + .into_iter() + .map(|s| SerializableSegment::from_voice_toolkit_segment(s)) + .collect(), language: result.language, audio_duration: result.audio_duration, } @@ -281,7 +293,7 @@ impl SerializableSegment { pub fn from_voice_toolkit_segment(segment: voice_toolkit::stt::TranscriptionSegment) -> Self { Self { start_time: segment.start_time * 1000, // Convert seconds to milliseconds (assuming start_time is in seconds as u64) - end_time: segment.end_time * 1000, // Convert seconds to milliseconds (assuming end_time is in seconds as u64) + end_time: segment.end_time * 1000, // Convert seconds to milliseconds (assuming end_time is in seconds as u64) text: segment.text, confidence: segment.confidence, } @@ -304,7 +316,7 @@ impl From for crate::models::TranscriptionRespo .collect(), language: result.language, duration: Some(result.audio_duration as f32 / 1000.0), // Convert from ms to seconds - processing_time: 0.0, // Will be set by the handler + processing_time: 0.0, // Will be set by the handler metadata: None, } } @@ -414,4 +426,4 @@ mod tests { assert_eq!(response.language, Some("en".to_string())); assert_eq!(response.duration, Some(2.0)); // Converted to seconds } -} \ No newline at end of file +} diff --git a/voice-cli/src/models/tts.rs b/voice-cli/src/models/tts.rs index 1516511..965c776 100644 --- a/voice-cli/src/models/tts.rs +++ b/voice-cli/src/models/tts.rs @@ -1,7 +1,7 @@ -use std::path::PathBuf; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use utoipa::ToSchema; -use chrono::{DateTime, Utc}; /// TTS同步请求 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -163,8 +163,16 @@ impl std::fmt::Display for TtsTaskError { TtsTaskError::StorageError { operation, message } => { write!(f, "存储错误 ({}): {}", operation, message) } - TtsTaskError::TimeoutError { stage, timeout_duration } => { - write!(f, "超时错误 ({}): {} 秒", stage.step_name(), timeout_duration.num_seconds()) + TtsTaskError::TimeoutError { + stage, + timeout_duration, + } => { + write!( + f, + "超时错误 ({}): {} 秒", + stage.step_name(), + timeout_duration.num_seconds() + ) } TtsTaskError::CancellationRequested => { write!(f, "任务已被取消") @@ -185,4 +193,4 @@ impl Default for TaskPriority { fn default() -> Self { TaskPriority::Normal } -} \ No newline at end of file +} diff --git a/voice-cli/src/openapi.rs b/voice-cli/src/openapi.rs index 6a49b80..2924f27 100644 --- a/voice-cli/src/openapi.rs +++ b/voice-cli/src/openapi.rs @@ -1,6 +1,6 @@ use crate::models::{ AsyncTaskResponse, CancelResponse, DeleteResponse, HealthResponse, ModelInfo, ModelsResponse, - RetryResponse, Segment, TaskPriority, TaskStatsResponse, TaskStatus, TaskStatusResponse, + RetryResponse, Segment, TaskPriority, TaskStatsResponse, TaskStatus, TaskStatusResponse, TranscriptionResponse, }; use crate::server::handlers; diff --git a/voice-cli/src/server/app_state.rs b/voice-cli/src/server/app_state.rs index 58d328e..87277c0 100644 --- a/voice-cli/src/server/app_state.rs +++ b/voice-cli/src/server/app_state.rs @@ -1,8 +1,8 @@ +use crate::VoiceCliError; use crate::models::Config; +use crate::services::TranscriptionTask; use crate::services::{LockFreeApalisManager, ModelService}; -use crate::VoiceCliError; use apalis_sql::sqlite::SqliteStorage; -use crate::services::TranscriptionTask; use std::sync::Arc; use std::time::SystemTime; use tracing::info; @@ -23,13 +23,14 @@ impl AppState { // 初始化无锁 Apalis 管理器 info!("初始化无锁 Apalis 任务管理器"); - let (manager, storage) = LockFreeApalisManager::new( - config.task_management.clone(), - model_service.clone(), - ).await?; + let (manager, storage) = + LockFreeApalisManager::new(config.task_management.clone(), model_service.clone()) + .await?; // 启动 worker - manager.start_worker(storage.clone(), model_service.clone()).await?; + manager + .start_worker(storage.clone(), model_service.clone()) + .await?; let apalis_storage = Some(storage); @@ -47,4 +48,4 @@ impl AppState { // Apalis 管理器会在 Drop 时自动清理 info!("应用状态关闭完成"); } -} \ No newline at end of file +} diff --git a/voice-cli/src/server/handlers.rs b/voice-cli/src/server/handlers.rs index 574e1de..1f0ba3e 100644 --- a/voice-cli/src/server/handlers.rs +++ b/voice-cli/src/server/handlers.rs @@ -2,20 +2,21 @@ use crate::VoiceCliError; use crate::models::{ AsyncTaskResponse, CancelResponse, Config, DeleteResponse, HealthResponse, HttpResult, ModelsResponse, RetryResponse, SimpleTaskStatus, TaskStatsResponse, TaskStatus, - TaskStatusResponse, TranscriptionResponse, TtsSyncRequest, TtsAsyncRequest, TtsTaskResponse, + TaskStatusResponse, TranscriptionResponse, TtsAsyncRequest, TtsSyncRequest, TtsTaskResponse, }; use crate::services::{ - AudioFileManager, AudioFormatDetector, LockFreeApalisManager, MetadataExtractor, ModelService, TranscriptionTask, TtsService, + AudioFileManager, AudioFormatDetector, LockFreeApalisManager, MetadataExtractor, ModelService, + TranscriptionTask, TtsService, }; use apalis_sql::sqlite::SqliteStorage; -use axum::extract::{Multipart, State, Json}; +use axum::extract::{Json, Multipart, State}; +use axum::response::IntoResponse; use futures::TryStreamExt; use std::path::{Path, PathBuf}; -use axum::response::IntoResponse; -use tower_http::body::Full; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::io::AsyncWriteExt; +use tower_http::body::Full; use tracing::{error, info, warn}; use url::Url; use utoipa; @@ -61,7 +62,8 @@ impl AppState { TtsService::new( config.tts.python_path.clone(), config.tts.model_path.clone(), - ).map_err(|e| VoiceCliError::Config(format!("创建TTS服务失败: {}", e)))? + ) + .map_err(|e| VoiceCliError::Config(format!("创建TTS服务失败: {}", e)))?, ); Ok(Self { @@ -175,7 +177,10 @@ pub async fn transcribe_handler( // 提取音视频元数据 let metadata = match MetadataExtractor::extract_metadata(&temp_file).await { Ok(meta) => { - info!("成功提取音视频元数据: {}", crate::services::MetadataExtractor::get_format_description(&meta)); + info!( + "成功提取音视频元数据: {}", + crate::services::MetadataExtractor::get_format_description(&meta) + ); // 转换为models::request::AudioVideoMetadata Some(crate::models::request::AudioVideoMetadata { format: meta.format, @@ -239,7 +244,7 @@ pub async fn transcribe_handler( } info!("同步转录完成: {} 字符", response.text.len()); - + // 清理临时文件 - 使用异步任务确保即使出错也不影响响应 let cleanup_file = temp_file.clone(); info!("临时文件: {}", temp_file.display()); @@ -249,7 +254,7 @@ pub async fn transcribe_handler( Err(e) => warn!("清理临时文件失败 {}: {}", cleanup_file.display(), e), } }); - + Ok(HttpResult::success(response)) } @@ -350,10 +355,14 @@ pub async fn transcribe_from_url_handler( Json(request): Json, ) -> Result, VoiceCliError> { let task_id = generate_task_id(); - info!("开始处理URL异步转录请求: {} - URL: {}", task_id, request.url); + info!( + "开始处理URL异步转录请求: {} - URL: {}", + task_id, request.url + ); // 从URL中提取文件名 - let filename = extract_filename_from_url(&request.url).unwrap_or_else(|| "audio_from_url".to_string()); + let filename = + extract_filename_from_url(&request.url).unwrap_or_else(|| "audio_from_url".to_string()); // 提交URL任务到队列 - 使用无锁管理器 info!("开始提交URL任务到队列..."); @@ -778,14 +787,17 @@ async fn extract_transcription_request_streaming( let extension = match AudioFormatDetector::detect_format_from_path(&temp_file_path) { Ok(Some(file_type)) => file_type.extension().to_lowercase(), Ok(None) => { - warn!("[Task {}] 无法检测音频文件格式,尝试使用文件扩展名", task_id); + warn!( + "[Task {}] 无法检测音频文件格式,尝试使用文件扩展名", + task_id + ); // 尝试使用文件扩展名作为后备 if let Some(ext) = temp_file_path.extension().and_then(|e| e.to_str()) { ext.to_lowercase() } else { "bin".to_string() } - }, + } Err(_) => { warn!("[Task {}] 检测文件格式时出错,使用默认扩展名", task_id); "bin".to_string() @@ -834,7 +846,8 @@ fn extract_filename_from_url(url: &str) -> Option { Url::parse(url) .ok() .and_then(|parsed_url| { - parsed_url.path_segments() + parsed_url + .path_segments() .and_then(|segments| segments.last()) .map(|last_segment| last_segment.to_string()) }) @@ -866,22 +879,33 @@ pub async fn tts_sync_handler( Json(request): Json, ) -> Result> { let start_time = std::time::Instant::now(); - + info!("收到TTS同步请求 - 文本长度: {}", request.text.len()); // 验证文本长度 if request.text.len() > state.config.tts.max_text_length { - let error_msg = format!("文本长度超过限制 ({} > {})", - request.text.len(), state.config.tts.max_text_length); + let error_msg = format!( + "文本长度超过限制 ({} > {})", + request.text.len(), + state.config.tts.max_text_length + ); error!("{}", error_msg); - return Ok(HttpResult::::from(VoiceCliError::InvalidInput(error_msg)).into_response()); + return Ok( + HttpResult::::from(VoiceCliError::InvalidInput(error_msg)).into_response(), + ); } // 应用默认参数 let mut processed_request = request.clone(); - processed_request.speed.get_or_insert(state.config.tts.default_speed); - processed_request.pitch.get_or_insert(state.config.tts.default_pitch); - processed_request.volume.get_or_insert(state.config.tts.default_volume); + processed_request + .speed + .get_or_insert(state.config.tts.default_speed); + processed_request + .pitch + .get_or_insert(state.config.tts.default_pitch); + processed_request + .volume + .get_or_insert(state.config.tts.default_volume); processed_request.format.get_or_insert("mp3".to_string()); // 执行TTS合成 @@ -893,9 +917,11 @@ pub async fn tts_sync_handler( // 读取音频文件并返回 match tokio::fs::read(&audio_file_path).await { Ok(audio_data) => { - let content_type = match audio_file_path.extension() + let content_type = match audio_file_path + .extension() .and_then(|ext| ext.to_str()) - .unwrap_or("mp3") { + .unwrap_or("mp3") + { "wav" => "audio/wav", "mp3" => "audio/mpeg", _ => "audio/octet-stream", @@ -914,7 +940,10 @@ pub async fn tts_sync_handler( Err(e) => { let error_msg = format!("读取音频文件失败: {}", e); error!("{}", error_msg); - Ok(HttpResult::::from(VoiceCliError::TtsError(error_msg)).into_response()) + Ok( + HttpResult::::from(VoiceCliError::TtsError(error_msg)) + .into_response(), + ) } } } @@ -949,17 +978,26 @@ pub async fn tts_async_handler( // 验证文本长度 if request.text.len() > state.config.tts.max_text_length { - let error_msg = format!("文本长度超过限制 ({} > {})", - request.text.len(), state.config.tts.max_text_length); + let error_msg = format!( + "文本长度超过限制 ({} > {})", + request.text.len(), + state.config.tts.max_text_length + ); error!("{}", error_msg); return HttpResult::::error("400".to_string(), error_msg); } // 应用默认参数 let mut processed_request = request.clone(); - processed_request.speed.get_or_insert(state.config.tts.default_speed); - processed_request.pitch.get_or_insert(state.config.tts.default_pitch); - processed_request.volume.get_or_insert(state.config.tts.default_volume); + processed_request + .speed + .get_or_insert(state.config.tts.default_speed); + processed_request + .pitch + .get_or_insert(state.config.tts.default_pitch); + processed_request + .volume + .get_or_insert(state.config.tts.default_volume); processed_request.format.get_or_insert("mp3".to_string()); // 创建异步任务 diff --git a/voice-cli/src/server/http_tracing.rs b/voice-cli/src/server/http_tracing.rs index 20c0e32..48a1d29 100644 --- a/voice-cli/src/server/http_tracing.rs +++ b/voice-cli/src/server/http_tracing.rs @@ -1,6 +1,6 @@ -use axum::{extract::Request, middleware::Next, response::Response}; use axum::body::Body; -use tracing::{info_span, Instrument}; +use axum::{extract::Request, middleware::Next, response::Response}; +use tracing::{Instrument, info_span}; use uuid::Uuid; /// 基本追踪中间件 @@ -8,10 +8,10 @@ use uuid::Uuid; pub async fn basic_tracing_middleware(request: Request, next: Next) -> Response { // 获取或生成追踪ID let tid = get_or_generate_trace_id(&request); - + // 在移动 request 之前先获取 URI 信息 let is_health_check = request.uri().path() == "/health"; - + // 创建请求span let span = info_span!( "http_request", @@ -23,14 +23,18 @@ pub async fn basic_tracing_middleware(request: Request, next: Next) -> Response // 在span中执行请求处理 let response = next.run(request).instrument(span).await; - + // 健康检查不处理 if is_health_check { return response; } // 仅当响应是 HttpResult(通过扩展标记判断)才注入 tid - if response.extensions().get::().is_some() { + if response + .extensions() + .get::() + .is_some() + { return add_tid_to_response(response, tid).await; } diff --git a/voice-cli/src/server/middleware.rs b/voice-cli/src/server/middleware.rs index 9d26a7f..3e89b92 100644 --- a/voice-cli/src/server/middleware.rs +++ b/voice-cli/src/server/middleware.rs @@ -1,13 +1,13 @@ use axum::{ + body::Body, extract::Request, - http::{HeaderMap, Method, Uri, HeaderValue, header::CONNECTION}, + http::{HeaderMap, HeaderValue, Method, Uri, header::CONNECTION}, middleware::Next, response::Response, - body::Body, }; +use serde_json::Value; use std::time::Instant; use tracing::{error, info, warn}; -use serde_json::Value; /// Connection: close 中间件 /// 为所有HTTP响应添加 Connection: close 头,禁用长连接 @@ -15,10 +15,9 @@ pub async fn connection_close_middleware(request: Request, next: Next) -> Respon let mut response = next.run(request).await; // 设置 Connection: close 响应头(使用框架常量) - response.headers_mut().insert( - CONNECTION, - HeaderValue::from_static("close") - ); + response + .headers_mut() + .insert(CONNECTION, HeaderValue::from_static("close")); response } @@ -29,31 +28,31 @@ pub async fn connection_close_middleware(request: Request, next: Next) -> Respon /// 对于其他请求,记录请求参数(body和query params) pub async fn request_logging_middleware(request: Request, next: Next) -> Response { let start_time = Instant::now(); - + // 提取请求信息 let method = request.method().clone(); let uri = request.uri().clone(); let headers = request.headers().clone(); let version = request.version(); - + // 检查是否为Multipart请求 let is_multipart = is_multipart_request(&method, &uri, &headers); - + // 获取用户IP (从headers中提取) let client_ip = extract_client_ip(&headers); - + // 获取用户代理 let user_agent = headers .get("user-agent") .and_then(|v| v.to_str().ok()) .unwrap_or("unknown"); - + // 获取内容类型 let content_type = headers .get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or("unknown"); - + // 获取内容长度 let content_length = headers .get("content-length") @@ -82,7 +81,7 @@ pub async fn request_logging_middleware(request: Request, next: Next) -> Respons } else { // 对于非Multipart请求,提取请求体参数 let (body_params, rebuilt_request) = extract_body_params(request).await; - + info!( method = %method, uri = %uri, @@ -101,15 +100,15 @@ pub async fn request_logging_middleware(request: Request, next: Next) -> Respons // 处理请求 let response = next.run(request).await; - + // 计算处理时间 let duration = start_time.elapsed(); let duration_ms = duration.as_millis() as u64; - + // 获取响应信息 let status = response.status(); let response_headers = response.headers(); - + // 获取响应内容长度 let response_content_length = response_headers .get("content-length") @@ -183,21 +182,22 @@ fn is_multipart_request(method: &Method, uri: &Uri, headers: &HeaderMap) -> bool /// 注意:此函数会消费请求,调用者需要重新构建请求 async fn extract_body_params(request: Request) -> (Value, Request) { // 只处理JSON请求体 - let content_type = request.headers() + let content_type = request + .headers() .get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or(""); - + if content_type.contains("application/json") { // 提取请求体 let (parts, body) = request.into_parts(); - + // 尝试读取请求体 match axum::body::to_bytes(body, usize::MAX).await { Ok(bytes) => { // 克隆字节数据以便重新构建请求 let bytes_clone = bytes.clone(); - + // 尝试解析JSON match serde_json::from_slice::(&bytes) { Ok(json_value) => { @@ -208,7 +208,8 @@ async fn extract_body_params(request: Request) -> (Value, Request) { } Err(_) => { // JSON解析失败,重新构建请求并返回原始内容 - let raw_content = Value::String(String::from_utf8_lossy(&bytes).to_string()); + let raw_content = + Value::String(String::from_utf8_lossy(&bytes).to_string()); let new_body = Body::from(bytes); let new_request = Request::from_parts(parts, new_body); (raw_content, new_request) @@ -225,17 +226,17 @@ async fn extract_body_params(request: Request) -> (Value, Request) { } else if content_type.contains("application/x-www-form-urlencoded") { // 处理表单数据 let (parts, body) = request.into_parts(); - + match axum::body::to_bytes(body, usize::MAX).await { Ok(bytes) => { let bytes_clone = bytes.clone(); let form_data = String::from_utf8_lossy(&bytes); let mut params = serde_json::Map::new(); - + for (key, value) in url::form_urlencoded::parse(form_data.as_bytes()) { params.insert(key.to_string(), Value::String(value.to_string())); } - + // 重新构建请求体 let new_body = Body::from(bytes_clone); let new_request = Request::from_parts(parts, new_body); @@ -260,21 +261,21 @@ fn is_file_upload_request(method: &Method, uri: &Uri, headers: &HeaderMap) -> bo if method != Method::POST { return false; } - + // 检查路径是否为转录端点 if uri.path() == "/transcribe" { return true; } - + // 检查Content-Type是否包含multipart或媒体文件 if let Some(content_type) = headers.get("content-type") { if let Ok(content_type_str) = content_type.to_str() { - return content_type_str.contains("multipart/form-data") + return content_type_str.contains("multipart/form-data") || content_type_str.contains("audio/") || content_type_str.contains("video/"); } } - + false } @@ -282,29 +283,28 @@ fn is_file_upload_request(method: &Method, uri: &Uri, headers: &HeaderMap) -> bo fn extract_query_params(uri: &Uri) -> Value { if let Some(query) = uri.query() { let mut params = serde_json::Map::new(); - + for (key, value) in url::form_urlencoded::parse(query.as_bytes()) { params.insert(key.to_string(), Value::String(value.to_string())); } - + Value::Object(params) } else { Value::Null } } - /// 从请求头中提取客户端IP地址 fn extract_client_ip(headers: &HeaderMap) -> String { // 按优先级检查不同的IP头 let ip_headers = [ "x-forwarded-for", - "x-real-ip", + "x-real-ip", "x-client-ip", "cf-connecting-ip", "true-client-ip", ]; - + for header_name in &ip_headers { if let Some(header_value) = headers.get(*header_name) { if let Ok(ip_str) = header_value.to_str() { @@ -316,7 +316,7 @@ fn extract_client_ip(headers: &HeaderMap) -> String { } } } - + "unknown".to_string() } @@ -324,51 +324,58 @@ fn extract_client_ip(headers: &HeaderMap) -> String { mod tests { use super::*; use axum::http::{HeaderName, HeaderValue}; - + #[test] fn test_middleware_module_exists() { // Simple test to verify the module compiles // Actual middleware testing would require more complex setup assert!(true); } - + #[test] fn test_is_multipart_request() { let mut headers = HeaderMap::new(); headers.insert( HeaderName::from_static("content-type"), - HeaderValue::from_static("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"), + HeaderValue::from_static( + "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW", + ), ); - + let method = Method::POST; let uri: Uri = "/api/v1/tasks/transcribe".parse().unwrap(); - + assert!(is_multipart_request(&method, &uri, &headers)); - + // Test with non-multipart content type let mut headers = HeaderMap::new(); headers.insert( HeaderName::from_static("content-type"), HeaderValue::from_static("application/json"), ); - + assert!(!is_multipart_request(&method, &uri, &headers)); - + // Test with GET request let method = Method::GET; let uri: Uri = "/health".parse().unwrap(); let headers = HeaderMap::new(); - + assert!(!is_multipart_request(&method, &uri, &headers)); } #[test] fn test_extract_query_params() { - let uri: Uri = "http://localhost:8080/api/v1/tasks?status=completed&limit=10".parse().unwrap(); + let uri: Uri = "http://localhost:8080/api/v1/tasks?status=completed&limit=10" + .parse() + .unwrap(); let params = extract_query_params(&uri); - + if let Value::Object(map) = params { - assert_eq!(map.get("status"), Some(&Value::String("completed".to_string()))); + assert_eq!( + map.get("status"), + Some(&Value::String("completed".to_string())) + ); assert_eq!(map.get("limit"), Some(&Value::String("10".to_string()))); } else { panic!("Expected object but got: {:?}", params); @@ -379,10 +386,10 @@ mod tests { fn test_extract_query_params_empty() { let uri: Uri = "http://localhost:8080/health".parse().unwrap(); let params = extract_query_params(&uri); - + assert_eq!(params, Value::Null); } - + #[test] fn test_extract_client_ip() { let mut headers = HeaderMap::new(); @@ -406,5 +413,4 @@ mod tests { let headers = HeaderMap::new(); assert_eq!(extract_client_ip(&headers), "unknown"); } - - } +} diff --git a/voice-cli/src/server/middleware_config.rs b/voice-cli/src/server/middleware_config.rs index fbefb57..95a9ad2 100644 --- a/voice-cli/src/server/middleware_config.rs +++ b/voice-cli/src/server/middleware_config.rs @@ -7,7 +7,7 @@ use tower_http::trace::TraceLayer; use tracing::info; use crate::server::http_tracing::basic_tracing_middleware; -use crate::server::middleware::{request_logging_middleware, connection_close_middleware}; +use crate::server::middleware::{connection_close_middleware, request_logging_middleware}; /// 与 mcp-proxy 风格一致的统一挂载接口 /// 建议路由构建完成后统一调用该函数挂载层 diff --git a/voice-cli/src/server/mod.rs b/voice-cli/src/server/mod.rs index f7e2130..e6c2990 100644 --- a/voice-cli/src/server/mod.rs +++ b/voice-cli/src/server/mod.rs @@ -1,9 +1,9 @@ +pub mod app_state; pub mod handlers; -pub mod middleware; -pub mod routes; pub mod http_tracing; +pub mod middleware; pub mod middleware_config; -pub mod app_state; +pub mod routes; use crate::models::Config; use std::net::SocketAddr; @@ -12,10 +12,6 @@ use std::sync::Arc; use tokio::sync::broadcast; use tracing::{info, warn}; - - - - async fn shutdown_signal_with_broadcast(shutdown_tx: broadcast::Sender<()>) { let ctrl_c = async { tokio::signal::ctrl_c() @@ -55,10 +51,13 @@ pub async fn handle_server_init(config_path: Option, force: bool) -> cr } // 生成配置文件 - crate::config::ConfigTemplateGenerator::generate_config_file(crate::config::ServiceType::Server, &output_path)?; + crate::config::ConfigTemplateGenerator::generate_config_file( + crate::config::ServiceType::Server, + &output_path, + )?; println!("✅ Server configuration initialized: {:?}", output_path); - + // 检查并创建 tts_service.py 文件 if let Err(e) = create_tts_service_file().await { warn!("Failed to create tts_service.py: {}", e); @@ -66,12 +65,14 @@ pub async fn handle_server_init(config_path: Option, force: bool) -> cr } else { println!("✅ TTS service file created successfully"); } - + // 初始化 Python 虚拟环境和 TTS 依赖 if let Err(e) = init_python_tts_environment().await { warn!("Failed to initialize Python TTS environment: {}", e); println!("⚠️ Python TTS environment initialization failed: {}", e); - println!("💡 You can manually initialize it later with: uv venv && uv add index-tts torch torchaudio numpy soundfile"); + println!( + "💡 You can manually initialize it later with: uv venv && uv add index-tts torch torchaudio numpy soundfile" + ); } else { println!("✅ Python TTS environment initialized successfully"); } @@ -85,38 +86,47 @@ pub async fn handle_server_init(config_path: Option, force: bool) -> cr /// Create tts_service.py file if it doesn't exist pub async fn create_tts_service_file() -> crate::Result<()> { use std::fs; - - let current_dir = std::env::current_dir() - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to get current directory: {}", e)))?; - + + let current_dir = std::env::current_dir().map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to get current directory: {}", e)) + })?; + let tts_service_path = current_dir.join("tts_service.py"); - + // 如果文件已存在,跳过创建 if tts_service_path.exists() { info!("tts_service.py already exists, skipping creation"); return Ok(()); } - + // 从模板文件加载 tts_service.py 内容 let tts_service_content = include_str!("../../templates/tts_service.py.template"); - + // 写入文件 - fs::write(&tts_service_path, tts_service_content) - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to create tts_service.py: {}", e)))?; - + fs::write(&tts_service_path, tts_service_content).map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to create tts_service.py: {}", e)) + })?; + // 设置文件权限为可执行 #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let mut perms = tts_service_path.metadata() - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to get file permissions: {}", e)))? + let mut perms = tts_service_path + .metadata() + .map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to get file permissions: {}", e)) + })? .permissions(); perms.set_mode(0o755); - fs::set_permissions(&tts_service_path, perms) - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to set file permissions: {}", e)))?; + fs::set_permissions(&tts_service_path, perms).map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to set file permissions: {}", e)) + })?; } - - info!("tts_service.py created successfully: {:?}", tts_service_path); + + info!( + "tts_service.py created successfully: {:?}", + tts_service_path + ); Ok(()) } @@ -147,9 +157,10 @@ pub async fn init_python_tts_environment() -> crate::Result<()> { } // Get the path to the pyproject.toml file (in the voice-cli crate directory) - let project_dir = std::env::current_dir() - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to get current directory: {}", e)))?; - + let project_dir = std::env::current_dir().map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to get current directory: {}", e)) + })?; + // Check if pyproject.toml exists in current directory let pyproject_path = project_dir.join("pyproject.toml"); let work_dir = if pyproject_path.exists() { @@ -162,11 +173,11 @@ pub async fn init_python_tts_environment() -> crate::Result<()> { crate_path } else { return Err(crate::VoiceCliError::Config( - "pyproject.toml not found in current directory or crate directory".to_string() + "pyproject.toml not found in current directory or crate directory".to_string(), )); } }; - + println!(" Using project directory: {:?}", work_dir); // Create virtual environment if it doesn't exist @@ -174,11 +185,11 @@ pub async fn init_python_tts_environment() -> crate::Result<()> { if !venv_path.exists() { println!("📦 Creating Python virtual environment..."); let mut cmd = Command::new("uv"); - cmd.arg("venv") - .current_dir(&work_dir); - - let output = cmd.output() - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to create virtual environment: {}", e)))?; + cmd.arg("venv").current_dir(&work_dir); + + let output = cmd.output().map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to create virtual environment: {}", e)) + })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -194,18 +205,17 @@ pub async fn init_python_tts_environment() -> crate::Result<()> { // Install TTS dependencies println!("📚 Installing TTS dependencies..."); - + // Install audio processing dependencies let dependencies = ["torch", "torchaudio", "numpy", "soundfile"]; for dep in &dependencies { println!(" Installing {}...", dep); let mut cmd = Command::new("uv"); - cmd.arg("add") - .arg(dep) - .current_dir(&work_dir); - - let output = cmd.output() - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to install {}: {}", dep, e)))?; + cmd.arg("add").arg(dep).current_dir(&work_dir); + + let output = cmd.output().map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to install {}: {}", dep, e)) + })?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -249,13 +259,14 @@ except Exception as e: let mut cmd = Command::new("uv"); cmd.arg("run") - .arg("python") - .arg("-c") - .arg(test_script) - .current_dir(&work_dir); - - let output = cmd.output() - .map_err(|e| crate::VoiceCliError::Config(format!("Failed to test TTS installation: {}", e)))?; + .arg("python") + .arg("-c") + .arg(test_script) + .current_dir(&work_dir); + + let output = cmd.output().map_err(|e| { + crate::VoiceCliError::Config(format!("Failed to test TTS installation: {}", e)) + })?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); @@ -290,14 +301,14 @@ pub async fn handle_server_run(config: &Config) -> crate::Result<()> { let config_arc = Arc::new(config.clone()); let app_state = handlers::AppState::new(config_arc.clone()).await?; let mut app = routes::create_routes_with_state(app_state.clone()).await?; - + // Clone app_state for use in monitor let app_state_for_monitor = app_state.clone(); - + // Create shutdown channel for monitor task let (shutdown_tx, _) = broadcast::channel(1); let mut shutdown_rx = shutdown_tx.subscribe(); - + // 添加 storage 作为 Extension app = app.layer(axum::Extension(app_state.apalis_storage.clone())); @@ -307,8 +318,11 @@ pub async fn handle_server_run(config: &Config) -> crate::Result<()> { let listener = tokio::net::TcpListener::bind(&addr) .await .map_err(|e| crate::VoiceCliError::Config(format!("Failed to bind to address: {}", e)))?; - - info!("TCP listener created successfully: {:?}", listener.local_addr()); + + info!( + "TCP listener created successfully: {:?}", + listener.local_addr() + ); info!("Starting axum server..."); let http = async { @@ -316,15 +330,15 @@ pub async fn handle_server_run(config: &Config) -> crate::Result<()> { .with_graceful_shutdown(shutdown_signal_with_broadcast(shutdown_tx)) .await .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)); - + info!("Axum server completed, performing graceful shutdown..."); - + // Perform graceful shutdown of application state app_state.shutdown().await; - + // Perform global cleanup operations crate::utils::perform_shutdown_cleanup().await; - + info!("Graceful shutdown completed with result: {:?}", result); result }; @@ -333,12 +347,16 @@ pub async fn handle_server_run(config: &Config) -> crate::Result<()> { // Wait for shutdown signal let _ = shutdown_rx.recv().await; info!("Monitor task received shutdown signal, stopping Apalis manager..."); - + // Gracefully shutdown the Apalis manager - if let Err(e) = app_state_for_monitor.lock_free_apalis_manager.shutdown().await { + if let Err(e) = app_state_for_monitor + .lock_free_apalis_manager + .shutdown() + .await + { warn!("Failed to shutdown Apalis manager gracefully: {}", e); } - + Ok::<(), std::io::Error>(()) }; diff --git a/voice-cli/src/server/routes.rs b/voice-cli/src/server/routes.rs index f579856..be9e979 100644 --- a/voice-cli/src/server/routes.rs +++ b/voice-cli/src/server/routes.rs @@ -1,12 +1,12 @@ +use crate::models::Config; +use crate::openapi; +use crate::server::handlers; +use crate::server::middleware_config::set_layer; use axum::{ - routing::{delete, get, post}, Router, + routing::{delete, get, post}, }; use std::sync::Arc; -use crate::models::Config; -use crate::server::handlers; -use crate::openapi; -use crate::server::middleware_config::set_layer; /// Create routes for the server pub async fn create_routes(config: Arc) -> crate::Result { @@ -17,7 +17,7 @@ pub async fn create_routes(config: Arc) -> crate::Result { /// Create routes with pre-created AppState pub async fn create_routes_with_state(shared_state: handlers::AppState) -> crate::Result { let config = shared_state.config.clone(); - + let app = Router::new() // Health check endpoint .route("/health", get(handlers::health_handler)) @@ -51,7 +51,10 @@ fn task_routes() -> Router { // Task submission .route("/transcribe", post(handlers::async_transcribe_handler)) // URL-based task submission - .route("/transcribeFromUrl", post(handlers::transcribe_from_url_handler)) + .route( + "/transcribeFromUrl", + post(handlers::transcribe_from_url_handler), + ) // TTS task submission .route("/tts", post(handlers::tts_async_handler)) // Task status and management diff --git a/voice-cli/src/services/apalis_manager.rs b/voice-cli/src/services/apalis_manager.rs index 8486cb8..462eed1 100644 --- a/voice-cli/src/services/apalis_manager.rs +++ b/voice-cli/src/services/apalis_manager.rs @@ -3,8 +3,10 @@ use crate::models::{ AsyncTranscriptionTask, ProcessingStage, TaskError, TaskManagementConfig, TaskStatsResponse, TaskStatus, TranscriptionResponse, }; +use crate::services::{ + AudioFileManager, AudioFormatDetector, MetadataExtractor, ModelService, TranscriptionEngine, +}; use crate::utils::{get_file_extension, is_supported_media_format}; -use crate::services::{AudioFileManager, AudioFormatDetector, MetadataExtractor, ModelService, TranscriptionEngine}; use apalis::layers::WorkerBuilderExt; use apalis::layers::retry::RetryPolicy; use apalis::prelude::*; @@ -122,11 +124,14 @@ impl StepContext { ) -> Result<(), Error> { let result_json = serde_json::to_string(result) .map_err(|e| Error::from(Box::new(e) as Box))?; - + let metadata_json = metadata .as_ref() - .map(|m| serde_json::to_string(m) - .map_err(|e| Error::from(Box::new(e) as Box))) + .map(|m| { + serde_json::to_string(m).map_err(|e| { + Error::from(Box::new(e) as Box) + }) + }) .transpose()?; sqlx::query( @@ -636,21 +641,21 @@ impl LockFreeApalisManager { let result_json: String = row .try_get("result") .map_err(|e| VoiceCliError::Storage(format!("获取结果字段失败: {}", e)))?; - + let mut result: TranscriptionResponse = serde_json::from_str(&result_json) .map_err(|e| VoiceCliError::Storage(format!("解析任务结果失败: {}", e)))?; - + // 尝试获取元数据 - let metadata_json: Option = row - .try_get("metadata") - .unwrap_or(None); - + let metadata_json: Option = row.try_get("metadata").unwrap_or(None); + if let Some(meta_json) = metadata_json { - if let Ok(metadata) = serde_json::from_str::(&meta_json) { + if let Ok(metadata) = + serde_json::from_str::(&meta_json) + { result.metadata = Some(metadata); } } - + Ok(Some(result)) } else { Ok(None) @@ -665,11 +670,14 @@ impl LockFreeApalisManager { ) -> Result<(), VoiceCliError> { let result_json = serde_json::to_string(result) .map_err(|e| VoiceCliError::Storage(format!("序列化任务结果失败: {}", e)))?; - - let metadata_json = result.metadata + + let metadata_json = result + .metadata .as_ref() - .map(|m| serde_json::to_string(m) - .map_err(|e| VoiceCliError::Storage(format!("序列化元数据失败: {}", e)))) + .map(|m| { + serde_json::to_string(m) + .map_err(|e| VoiceCliError::Storage(format!("序列化元数据失败: {}", e))) + }) .transpose()?; sqlx::query( @@ -1340,10 +1348,7 @@ async fn transcription_step( }) } Err(e) => { - warn!( - "[Task {}] 提取元数据失败: {}", - task.task_id, e - ); + warn!("[Task {}] 提取元数据失败: {}", task.task_id, e); None } }; @@ -1351,23 +1356,26 @@ async fn transcription_step( // 执行转录,使用配置中的默认模型 let default_model = ctx.transcription_engine.default_model(); let model = task.model.as_deref().unwrap_or(default_model); - + // 首先检查文件是否有音频流 - let has_audio = check_file_has_audio_stream(&task.processed_audio_path).await + let has_audio = check_file_has_audio_stream(&task.processed_audio_path) + .await .map_err(|e| { Error::Abort(std::sync::Arc::new(Box::new(std::io::Error::new( std::io::ErrorKind::Other, format!("检查音频流失败: {}", e), )))) })?; - + if !has_audio { - return Err(Error::Abort(std::sync::Arc::new(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - "文件不包含音频流,无法进行转录".to_string(), - ))))); + return Err(Error::Abort(std::sync::Arc::new(Box::new( + std::io::Error::new( + std::io::ErrorKind::Other, + "文件不包含音频流,无法进行转录".to_string(), + ), + )))); } - + let transcription_result = ctx .transcription_engine .transcribe_with_conversion( @@ -1427,19 +1435,22 @@ async fn transcription_step( /// 检查文件是否包含音频流 async fn check_file_has_audio_stream(file_path: &Path) -> Result { use std::process::Command; - + // 使用 ffprobe 检查文件是否有音频流 let output = Command::new("ffprobe") .args([ - "-v", "quiet", + "-v", + "quiet", "-show_streams", - "-select_streams", "a", - "-of", "csv=p=0", + "-select_streams", + "a", + "-of", + "csv=p=0", file_path.to_str().unwrap_or("invalid_path"), ]) .output() .map_err(|e| VoiceCliError::AudioConversionFailed(format!("执行 ffprobe 失败: {}", e)))?; - + // 如果输出为空,则没有音频流 Ok(!output.stdout.is_empty()) } @@ -1469,10 +1480,7 @@ async fn result_formatting_step( metadata.duration_seconds ) } else { - format!( - "转录了 {} 个字符", - task.transcription_result.text.len() - ) + format!("转录了 {} 个字符", task.transcription_result.text.len()) }; ctx.save_task_status( @@ -1625,7 +1633,7 @@ async fn download_audio_from_url( .unwrap_or("application/octet-stream"); let extension = get_file_extension(content_type, url); - + // 检查是否为支持的媒体格式 if !is_supported_media_format(content_type) { warn!( @@ -1767,7 +1775,6 @@ async fn update_task_file_path_in_db( Ok(()) } - #[cfg(test)] mod tests { use super::*; diff --git a/voice-cli/src/services/audio_file_manager.rs b/voice-cli/src/services/audio_file_manager.rs index 7b067ed..96aed1e 100644 --- a/voice-cli/src/services/audio_file_manager.rs +++ b/voice-cli/src/services/audio_file_manager.rs @@ -1,11 +1,11 @@ -use std::path::{Path, PathBuf}; -use std::fs; -use bytes::Bytes; -use tracing::{info, warn, error}; use crate::VoiceCliError; use axum::extract::multipart::Field; -use futures::{TryStreamExt}; // StreamExt 未使用,移除 +use bytes::Bytes; +use futures::TryStreamExt; // StreamExt 未使用,移除 +use std::fs; +use std::path::{Path, PathBuf}; use tokio::io::AsyncWriteExt; +use tracing::{error, info, warn}; /// Service for managing audio files on disk #[derive(Debug, Clone)] @@ -17,7 +17,7 @@ impl AudioFileManager { /// Create a new AudioFileManager pub fn new>(storage_dir: P) -> Result { let storage_dir = storage_dir.as_ref().to_path_buf(); - + // Create storage directory if it doesn't exist if !storage_dir.exists() { fs::create_dir_all(&storage_dir).map_err(|e| { @@ -28,12 +28,15 @@ impl AudioFileManager { )) })?; } - - info!("AudioFileManager initialized with storage directory: {}", storage_dir.display()); - + + info!( + "AudioFileManager initialized with storage directory: {}", + storage_dir.display() + ); + Ok(Self { storage_dir }) } - + /// Save audio data to disk and return the file path pub async fn save_audio_file( &self, @@ -46,30 +49,32 @@ impl AudioFileManager { .extension() .and_then(|ext| ext.to_str()) .unwrap_or("bin"); - + // Create a unique filename using task_id let filename = format!("{}_{}.{}", task_id, uuid::Uuid::new_v4(), extension); let file_path = self.storage_dir.join(&filename); - + // Write audio data to file - tokio::fs::write(&file_path, audio_data).await.map_err(|e| { - VoiceCliError::Storage(format!( - "Failed to write audio file '{}': {}", - file_path.display(), - e - )) - })?; - + tokio::fs::write(&file_path, audio_data) + .await + .map_err(|e| { + VoiceCliError::Storage(format!( + "Failed to write audio file '{}': {}", + file_path.display(), + e + )) + })?; + info!( "Saved audio file: {} ({} bytes) -> {}", original_filename, audio_data.len(), file_path.display() ); - + Ok(file_path) } - + /// Save audio data from multipart field stream directly to disk pub async fn save_audio_file_streaming( &self, @@ -78,21 +83,27 @@ impl AudioFileManager { temp_file_name: &str, ) -> Result { // 获取原始文件名(如果有)用于日志记录 - let original_filename = field.file_name().map(|s| s.to_string()).unwrap_or_else(|| "unknown".to_string()); + let original_filename = field + .file_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| "unknown".to_string()); info!( "[Task {}] 开始接收音频文件流: {}, 目标临时文件名: {}", - task_id, - original_filename, - temp_file_name + task_id, original_filename, temp_file_name ); - + let file_path = self.storage_dir.join(&temp_file_name); - + // 确保存储目录存在 if let Some(parent) = file_path.parent() { if !parent.exists() { tokio::fs::create_dir_all(parent).await.map_err(|e| { - error!("[Task {}] 无法创建存储目录 '{}': {}", task_id, parent.display(), e); + error!( + "[Task {}] 无法创建存储目录 '{}': {}", + task_id, + parent.display(), + e + ); VoiceCliError::Storage(format!( "无法创建存储目录 '{}': {}", parent.display(), @@ -101,52 +112,60 @@ impl AudioFileManager { })?; } } - + // 创建文件 let file = tokio::fs::File::create(&file_path).await.map_err(|e| { - error!("[Task {}] 无法创建音频文件 '{}': {}", task_id, file_path.display(), e); - VoiceCliError::Storage(format!( - "无法创建音频文件 '{}': {}", + error!( + "[Task {}] 无法创建音频文件 '{}': {}", + task_id, file_path.display(), e - )) + ); + VoiceCliError::Storage(format!("无法创建音频文件 '{}': {}", file_path.display(), e)) })?; - + // 创建缓冲写入器以提高性能 let mut writer = tokio::io::BufWriter::new(file); - + // 将 field 转换为 StreamReader (实现 AsyncRead trait) let mut reader = tokio_util::io::StreamReader::new( - field.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + field.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), ); - + // 使用 tokio::io::copy 进行高效的流式复制 - let total_bytes = tokio::io::copy(&mut reader, &mut writer).await.map_err(|e| { + let total_bytes = tokio::io::copy(&mut reader, &mut writer) + .await + .map_err(|e| { + error!( + "[Task {}] 流式复制音频文件数据失败 ({} -> {}): {}", + task_id, + original_filename, + file_path.display(), + e + ); + VoiceCliError::Storage(format!( + "流式复制音频文件数据失败 ({} -> {}): {}", + original_filename, + file_path.display(), + e + )) + })?; + + // 确保所有数据都写入磁盘 + writer.flush().await.map_err(|e| { error!( - "[Task {}] 流式复制音频文件数据失败 ({} -> {}): {}", + "[Task {}] 无法刷新数据到文件 '{}': {}", task_id, - original_filename, file_path.display(), e ); - VoiceCliError::Storage(format!( - "流式复制音频文件数据失败 ({} -> {}): {}", - original_filename, - file_path.display(), - e - )) - })?; - - // 确保所有数据都写入磁盘 - writer.flush().await.map_err(|e| { - error!("[Task {}] 无法刷新数据到文件 '{}': {}", task_id, file_path.display(), e); VoiceCliError::Storage(format!( "无法刷新数据到文件 '{}': {}", file_path.display(), e )) })?; - + info!( "[Task {}] 成功接收并保存音频文件: {} ({} 字节) -> {}", task_id, @@ -154,14 +173,17 @@ impl AudioFileManager { total_bytes, file_path.display() ); - + Ok(file_path.to_string_lossy().into_owned()) } - + /// Delete an audio file from disk - pub async fn delete_audio_file>(&self, file_path: P) -> Result<(), VoiceCliError> { + pub async fn delete_audio_file>( + &self, + file_path: P, + ) -> Result<(), VoiceCliError> { let file_path = file_path.as_ref(); - + if file_path.exists() { tokio::fs::remove_file(file_path).await.map_err(|e| { VoiceCliError::Storage(format!( @@ -170,17 +192,20 @@ impl AudioFileManager { e )) })?; - + info!("Deleted audio file: {}", file_path.display()); } else { warn!("Audio file not found for deletion: {}", file_path.display()); } - + Ok(()) } - + /// Delete multiple audio files - pub async fn delete_audio_files>(&self, file_paths: &[P]) -> Result<(), VoiceCliError> { + pub async fn delete_audio_files>( + &self, + file_paths: &[P], + ) -> Result<(), VoiceCliError> { for file_path in file_paths { if let Err(e) = self.delete_audio_file(file_path).await { // Log error but continue with other files @@ -189,12 +214,13 @@ impl AudioFileManager { } Ok(()) } - + /// Clean up old audio files based on age pub async fn cleanup_old_files(&self, max_age_hours: u64) -> Result { let mut cleaned_count = 0u32; - let cutoff_time = std::time::SystemTime::now() - std::time::Duration::from_secs(max_age_hours * 3600); - + let cutoff_time = + std::time::SystemTime::now() - std::time::Duration::from_secs(max_age_hours * 3600); + let mut entries = tokio::fs::read_dir(&self.storage_dir).await.map_err(|e| { VoiceCliError::Storage(format!( "Failed to read storage directory '{}': {}", @@ -202,12 +228,14 @@ impl AudioFileManager { e )) })?; - - while let Some(entry) = entries.next_entry().await.map_err(|e| { - VoiceCliError::Storage(format!("Failed to read directory entry: {}", e)) - })? { + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|e| VoiceCliError::Storage(format!("Failed to read directory entry: {}", e)))? + { let path = entry.path(); - + if path.is_file() { if let Ok(metadata) = entry.metadata().await { if let Ok(modified) = metadata.modified() { @@ -222,14 +250,14 @@ impl AudioFileManager { } } } - + if cleaned_count > 0 { info!("Cleaned up {} old audio files", cleaned_count); } - + Ok(cleaned_count) } - + /// Get the size of a file pub async fn get_file_size>(&self, file_path: P) -> Result { let metadata = tokio::fs::metadata(file_path.as_ref()).await.map_err(|e| { @@ -239,14 +267,14 @@ impl AudioFileManager { e )) })?; - + Ok(metadata.len()) } - + /// Get total storage usage pub async fn get_storage_usage(&self) -> Result { let mut total_size = 0u64; - + let mut entries = tokio::fs::read_dir(&self.storage_dir).await.map_err(|e| { VoiceCliError::Storage(format!( "Failed to read storage directory '{}': {}", @@ -254,20 +282,22 @@ impl AudioFileManager { e )) })?; - - while let Some(entry) = entries.next_entry().await.map_err(|e| { - VoiceCliError::Storage(format!("Failed to read directory entry: {}", e)) - })? { + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|e| VoiceCliError::Storage(format!("Failed to read directory entry: {}", e)))? + { if entry.path().is_file() { if let Ok(metadata) = entry.metadata().await { total_size += metadata.len(); } } } - + Ok(total_size) } - + /// Check if a file exists pub async fn file_exists>(&self, file_path: P) -> bool { tokio::fs::metadata(file_path.as_ref()).await.is_ok() @@ -278,76 +308,85 @@ impl AudioFileManager { mod tests { use super::*; use tempfile::TempDir; - + #[tokio::test] async fn test_audio_file_manager_creation() { let temp_dir = TempDir::new().unwrap(); let manager = AudioFileManager::new(temp_dir.path()).unwrap(); - + assert_eq!(manager.storage_dir, temp_dir.path()); } - + #[tokio::test] async fn test_save_and_delete_audio_file() { let temp_dir = TempDir::new().unwrap(); let manager = AudioFileManager::new(temp_dir.path()).unwrap(); - + let audio_data = Bytes::from(vec![1, 2, 3, 4, 5]); let task_id = "test-task-123"; let original_filename = "test.mp3"; - + // Save file - let file_path = manager.save_audio_file(task_id, &audio_data, original_filename).await.unwrap(); - + let file_path = manager + .save_audio_file(task_id, &audio_data, original_filename) + .await + .unwrap(); + // Check file exists assert!(manager.file_exists(&file_path).await); - + // Check file size let size = manager.get_file_size(&file_path).await.unwrap(); assert_eq!(size, 5); - + // Delete file manager.delete_audio_file(&file_path).await.unwrap(); - + // Check file no longer exists assert!(!manager.file_exists(&file_path).await); } - + #[tokio::test] async fn test_cleanup_old_files() { let temp_dir = TempDir::new().unwrap(); let manager = AudioFileManager::new(temp_dir.path()).unwrap(); - + let audio_data = Bytes::from(vec![1, 2, 3, 4, 5]); - + // Save a file - let file_path = manager.save_audio_file("test-task", &audio_data, "test.mp3").await.unwrap(); - + let file_path = manager + .save_audio_file("test-task", &audio_data, "test.mp3") + .await + .unwrap(); + // File should exist assert!(manager.file_exists(&file_path).await); - + // Cleanup files older than 0 hours (should clean everything) let cleaned = manager.cleanup_old_files(0).await.unwrap(); - + // Should have cleaned at least 1 file assert!(cleaned >= 1); } - + #[tokio::test] async fn test_storage_usage() { let temp_dir = TempDir::new().unwrap(); let manager = AudioFileManager::new(temp_dir.path()).unwrap(); - + // Initially should be 0 let initial_usage = manager.get_storage_usage().await.unwrap(); assert_eq!(initial_usage, 0); - + // Save a file let audio_data = Bytes::from(vec![1, 2, 3, 4, 5]); - let _file_path = manager.save_audio_file("test-task", &audio_data, "test.mp3").await.unwrap(); - + let _file_path = manager + .save_audio_file("test-task", &audio_data, "test.mp3") + .await + .unwrap(); + // Usage should increase let usage_after = manager.get_storage_usage().await.unwrap(); assert_eq!(usage_after, 5); } -} \ No newline at end of file +} diff --git a/voice-cli/src/services/audio_format_detector.rs b/voice-cli/src/services/audio_format_detector.rs index a613b52..26e02a2 100644 --- a/voice-cli/src/services/audio_format_detector.rs +++ b/voice-cli/src/services/audio_format_detector.rs @@ -17,7 +17,6 @@ use crate::models::request::{AudioFormat, AudioFormatResult, AudioMetadata, Dete pub struct AudioFormatDetector; impl AudioFormatDetector { - /// Detect audio format using infer library (magic number detection) pub fn detect_format_from_path(path: &Path) -> anyhow::Result> { let kind = infer::get_from_path(path) diff --git a/voice-cli/src/services/audio_processor.rs b/voice-cli/src/services/audio_processor.rs index a881aa7..69f4b33 100644 --- a/voice-cli/src/services/audio_processor.rs +++ b/voice-cli/src/services/audio_processor.rs @@ -1,6 +1,6 @@ -use crate::models::request::ProcessedAudio; -use crate::models::AudioFormat; use crate::VoiceCliError; +use crate::models::AudioFormat; +use crate::models::request::ProcessedAudio; use bytes::Bytes; use std::io::Write; use std::path::PathBuf; diff --git a/voice-cli/src/services/metadata_extractor.rs b/voice-cli/src/services/metadata_extractor.rs index 0192487..c26e711 100644 --- a/voice-cli/src/services/metadata_extractor.rs +++ b/voice-cli/src/services/metadata_extractor.rs @@ -13,23 +13,23 @@ pub struct AudioVideoMetadata { pub container_format: String, // 容器格式 pub duration_seconds: f64, // 时长(秒) pub file_size_bytes: u64, // 文件大小 - + // 音频信息 - pub audio_codec: String, // 音频编码器 - pub sample_rate: u32, // 采样率 (Hz) - pub channels: u8, // 声道数 - pub audio_bitrate: u32, // 音频码率 (kbps) - + pub audio_codec: String, // 音频编码器 + pub sample_rate: u32, // 采样率 (Hz) + pub channels: u8, // 声道数 + pub audio_bitrate: u32, // 音频码率 (kbps) + // 视频信息(如果是视频文件) - pub has_video: bool, // 是否包含视频 + pub has_video: bool, // 是否包含视频 pub video_codec: Option, // 视频编码器 - pub width: Option, // 视频宽度 - pub height: Option, // 视频高度 - pub video_bitrate: Option, // 视频码率 (kbps) - pub frame_rate: Option, // 帧率 - + pub width: Option, // 视频宽度 + pub height: Option, // 视频高度 + pub video_bitrate: Option, // 视频码率 (kbps) + pub frame_rate: Option, // 帧率 + // 其他元数据 - pub bitrate: u32, // 总码率 (kbps) + pub bitrate: u32, // 总码率 (kbps) pub creation_time: Option, // 创建时间 } @@ -63,30 +63,30 @@ impl MetadataExtractor { /// 从文件路径提取音视频元数据 pub async fn extract_metadata(file_path: &Path) -> Result { info!("开始提取音视频元数据: {:?}", file_path); - + // 首先获取文件基本信息 let file_metadata = fs::metadata(file_path) .await .map_err(|e| VoiceCliError::Storage(format!("无法访问文件: {}", e)))?; - + let _file_size = file_metadata.len(); let file_extension = file_path .extension() .and_then(|ext| ext.to_str()) .unwrap_or("unknown") .to_lowercase(); - + // 尝试使用 FFmpeg 提取详细元数据 if let Ok(ffmpeg_metadata) = Self::extract_with_ffmpeg(file_path).await { info!("FFmpeg 元数据提取成功"); return Ok(ffmpeg_metadata); } - + // 如果 FFmpeg 不可用或失败,使用基础方法 warn!("FFmpeg 不可用或失败,使用基础元数据提取方法"); Self::extract_basic_metadata(file_path, &file_metadata, &file_extension).await } - + /// 使用 FFmpeg 提取详细元数据 async fn extract_with_ffmpeg(file_path: &Path) -> Result { use ffmpeg_sidecar::command::FfmpegCommand; @@ -95,148 +95,143 @@ impl MetadataExtractor { let file_path_buf = file_path.to_path_buf(); - let metadata = task::spawn_blocking(move || -> Result { - let mut metadata = AudioVideoMetadata::default(); - let file_path_str = file_path_buf.to_string_lossy().to_string(); - - // 使用 FfmpegCommand 获取文件信息 - let mut child = FfmpegCommand::new() - .arg("-i") - .arg(&file_path_str) - .arg("-hide_banner") - .spawn() - .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 执行失败: {}", e)))?; - - // 等待命令完成(在阻塞线程中执行) - let _exit_status = child - .wait() - .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 执行失败: {}", e)))?; - - // 使用传统方法获取输出(因为 ffmpeg-sidecar 主要用于处理媒体流,不是元数据提取) - let output = std::process::Command::new("ffmpeg") - .args([ - "-i", - &file_path_str, - "-hide_banner", - "-f", - "null", - "-", - ]) - .output() - .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 执行失败: {}", e)))?; - - // 解析 stderr 输出中的元数据信息 - let stderr_output = String::from_utf8_lossy(&output.stderr); - - // 解析输出中的元数据信息 - for line in stderr_output.lines() { - if line.contains("Duration:") { - // 解析时长: Duration: 00:00:01.60, start: 0.000000, bitrate: 705 kb/s - if let Some(duration_part) = line.split("Duration: ").nth(1) { - if let Some(duration_str) = duration_part.split(',').next() { - let parts: Vec<&str> = duration_str.split(':').collect(); - if parts.len() == 3 { - let hours: f64 = parts[0].parse().unwrap_or(0.0); - let minutes: f64 = parts[1].parse().unwrap_or(0.0); - let seconds: f64 = parts[2].parse().unwrap_or(0.0); - metadata.duration_seconds = - hours * 3600.0 + minutes * 60.0 + seconds; + let metadata = + task::spawn_blocking(move || -> Result { + let mut metadata = AudioVideoMetadata::default(); + let file_path_str = file_path_buf.to_string_lossy().to_string(); + + // 使用 FfmpegCommand 获取文件信息 + let mut child = FfmpegCommand::new() + .arg("-i") + .arg(&file_path_str) + .arg("-hide_banner") + .spawn() + .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 执行失败: {}", e)))?; + + // 等待命令完成(在阻塞线程中执行) + let _exit_status = child + .wait() + .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 执行失败: {}", e)))?; + + // 使用传统方法获取输出(因为 ffmpeg-sidecar 主要用于处理媒体流,不是元数据提取) + let output = std::process::Command::new("ffmpeg") + .args(["-i", &file_path_str, "-hide_banner", "-f", "null", "-"]) + .output() + .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 执行失败: {}", e)))?; + + // 解析 stderr 输出中的元数据信息 + let stderr_output = String::from_utf8_lossy(&output.stderr); + + // 解析输出中的元数据信息 + for line in stderr_output.lines() { + if line.contains("Duration:") { + // 解析时长: Duration: 00:00:01.60, start: 0.000000, bitrate: 705 kb/s + if let Some(duration_part) = line.split("Duration: ").nth(1) { + if let Some(duration_str) = duration_part.split(',').next() { + let parts: Vec<&str> = duration_str.split(':').collect(); + if parts.len() == 3 { + let hours: f64 = parts[0].parse().unwrap_or(0.0); + let minutes: f64 = parts[1].parse().unwrap_or(0.0); + let seconds: f64 = parts[2].parse().unwrap_or(0.0); + metadata.duration_seconds = + hours * 3600.0 + minutes * 60.0 + seconds; + } } } } - } - if line.contains("Audio:") { - // 解析音频信息: Stream #0:0: Audio: pcm_f32le, 22050 Hz, mono, fltp, 705 kb/s - let audio_info = line.split("Audio: ").nth(1).unwrap_or(""); - let parts: Vec<&str> = audio_info.split(',').collect(); + if line.contains("Audio:") { + // 解析音频信息: Stream #0:0: Audio: pcm_f32le, 22050 Hz, mono, fltp, 705 kb/s + let audio_info = line.split("Audio: ").nth(1).unwrap_or(""); + let parts: Vec<&str> = audio_info.split(',').collect(); - if let Some(codec) = parts.first() { - metadata.audio_codec = codec.trim().to_string(); - } + if let Some(codec) = parts.first() { + metadata.audio_codec = codec.trim().to_string(); + } - for part in parts { - if part.contains("Hz") { - if let Some(rate_str) = part.split("Hz").next() { - metadata.sample_rate = rate_str.trim().parse().unwrap_or(0); + for part in parts { + if part.contains("Hz") { + if let Some(rate_str) = part.split("Hz").next() { + metadata.sample_rate = rate_str.trim().parse().unwrap_or(0); + } } - } - if part.contains("mono") { - metadata.channels = 1; - } - if part.contains("stereo") { - metadata.channels = 2; - } - if part.contains("kb/s") { - if let Some(bitrate_str) = part.split("kb/s").next() { - metadata.audio_bitrate = bitrate_str.trim().parse().unwrap_or(0); + if part.contains("mono") { + metadata.channels = 1; + } + if part.contains("stereo") { + metadata.channels = 2; + } + if part.contains("kb/s") { + if let Some(bitrate_str) = part.split("kb/s").next() { + metadata.audio_bitrate = + bitrate_str.trim().parse().unwrap_or(0); + } } } } - } - if line.contains("Video:") { - // 解析视频信息: Stream #0:1: Video: h264, yuv420p, 1280x720, 24 fps, 1992 kb/s - metadata.has_video = true; - let video_info = line.split("Video: ").nth(1).unwrap_or(""); - let parts: Vec<&str> = video_info.split(',').collect(); + if line.contains("Video:") { + // 解析视频信息: Stream #0:1: Video: h264, yuv420p, 1280x720, 24 fps, 1992 kb/s + metadata.has_video = true; + let video_info = line.split("Video: ").nth(1).unwrap_or(""); + let parts: Vec<&str> = video_info.split(',').collect(); - if let Some(codec) = parts.first() { - metadata.video_codec = Some(codec.trim().to_string()); - } + if let Some(codec) = parts.first() { + metadata.video_codec = Some(codec.trim().to_string()); + } - for part in parts { - if part.contains('x') { - let resolution_parts: Vec<&str> = part.trim().split('x').collect(); - if resolution_parts.len() == 2 { - metadata.width = resolution_parts[0].trim().parse().ok(); - metadata.height = resolution_parts[1].trim().parse().ok(); + for part in parts { + if part.contains('x') { + let resolution_parts: Vec<&str> = part.trim().split('x').collect(); + if resolution_parts.len() == 2 { + metadata.width = resolution_parts[0].trim().parse().ok(); + metadata.height = resolution_parts[1].trim().parse().ok(); + } } - } - if part.contains("fps") { - if let Some(fps_str) = part.split("fps").next() { - metadata.frame_rate = fps_str.trim().parse().ok(); + if part.contains("fps") { + if let Some(fps_str) = part.split("fps").next() { + metadata.frame_rate = fps_str.trim().parse().ok(); + } } - } - if part.contains("kb/s") { - if let Some(bitrate_str) = part.split("kb/s").next() { - metadata.video_bitrate = - Some(bitrate_str.trim().parse().unwrap_or(0)); + if part.contains("kb/s") { + if let Some(bitrate_str) = part.split("kb/s").next() { + metadata.video_bitrate = + Some(bitrate_str.trim().parse().unwrap_or(0)); + } } } } } - } - // 获取文件大小 - if let Ok(file_meta) = std::fs::metadata(&file_path_buf) { - metadata.file_size_bytes = file_meta.len(); - } + // 获取文件大小 + if let Ok(file_meta) = std::fs::metadata(&file_path_buf) { + metadata.file_size_bytes = file_meta.len(); + } - // 如果没有从输出中获取到码率,计算总码率 - if metadata.bitrate == 0 - && metadata.duration_seconds > 0.0 - && metadata.file_size_bytes > 0 - { - let total_bits = metadata.file_size_bytes as f64 * 8.0; - metadata.bitrate = (total_bits / metadata.duration_seconds / 1000.0) as u32; - } + // 如果没有从输出中获取到码率,计算总码率 + if metadata.bitrate == 0 + && metadata.duration_seconds > 0.0 + && metadata.file_size_bytes > 0 + { + let total_bits = metadata.file_size_bytes as f64 * 8.0; + metadata.bitrate = (total_bits / metadata.duration_seconds / 1000.0) as u32; + } - // 根据文件扩展名设置格式 - if let Some(extension) = file_path_buf.extension().and_then(|ext| ext.to_str()) { - metadata.format = extension.to_lowercase(); - metadata.container_format = extension.to_lowercase(); - } + // 根据文件扩展名设置格式 + if let Some(extension) = file_path_buf.extension().and_then(|ext| ext.to_str()) { + metadata.format = extension.to_lowercase(); + metadata.container_format = extension.to_lowercase(); + } - Ok(metadata) - }) - .await - .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 阻塞任务失败: {}", e)))??; + Ok(metadata) + }) + .await + .map_err(|e| VoiceCliError::Storage(format!("FFmpeg 阻塞任务失败: {}", e)))??; info!("FFmpeg 元数据提取完成: {:?}", metadata); Ok(metadata) } - + /// 基础元数据提取(不依赖 FFmpeg) async fn extract_basic_metadata( file_path: &Path, @@ -244,39 +239,40 @@ impl MetadataExtractor { file_extension: &str, ) -> Result { debug!("使用基础方法提取元数据: {:?}", file_path); - + let mut metadata = AudioVideoMetadata { file_size_bytes: file_metadata.len(), format: file_extension.to_string(), container_format: file_extension.to_string(), ..Default::default() }; - + // 尝试使用现有的 AudioFormatDetector 获取音频信息 - if let Ok(Some(format_type)) = crate::services::AudioFormatDetector::detect_format_from_path(file_path) { + if let Ok(Some(format_type)) = + crate::services::AudioFormatDetector::detect_format_from_path(file_path) + { metadata.format = format_type.extension().to_string(); metadata.audio_codec = format_type.mime_type().to_string(); } - + // 判断是否为视频文件 metadata.has_video = Self::is_video_format(file_extension); - + // 如果是视频文件,设置默认值 if metadata.has_video { metadata.video_codec = Some("unknown".to_string()); } - + // 计算码率 if metadata.duration_seconds > 0.0 && metadata.file_size_bytes > 0 { let total_bits = metadata.file_size_bytes as f64 * 8.0; metadata.bitrate = (total_bits / metadata.duration_seconds / 1000.0) as u32; } - + info!("基础元数据提取完成: {:?}", metadata); Ok(metadata) } - - + /// 判断是否为视频格式 fn is_video_format(extension: &str) -> bool { matches!( @@ -284,7 +280,7 @@ impl MetadataExtractor { "mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" | "3gp" | "mpg" | "mpeg" ) } - + /// 获取文件格式描述 pub fn get_format_description(metadata: &AudioVideoMetadata) -> String { if metadata.has_video { @@ -307,18 +303,18 @@ impl MetadataExtractor { #[cfg(test)] mod tests { use super::*; - use tempfile::NamedTempFile; use std::io::Write; - + use tempfile::NamedTempFile; + #[tokio::test] async fn test_extract_basic_metadata() { // 创建一个临时文件进行测试 let mut temp_file = NamedTempFile::new().unwrap(); temp_file.write_all(b"dummy audio data").unwrap(); temp_file.flush().unwrap(); - + let metadata = MetadataExtractor::extract_metadata(temp_file.path()).await; - + // 验证基本结构 match metadata { Ok(meta) => { @@ -331,7 +327,7 @@ mod tests { } } } - + #[test] fn test_is_video_format() { assert!(MetadataExtractor::is_video_format("mp4")); @@ -339,7 +335,7 @@ mod tests { assert!(!MetadataExtractor::is_video_format("mp3")); assert!(!MetadataExtractor::is_video_format("wav")); } - + #[test] fn test_get_format_description() { let audio_meta = AudioVideoMetadata { @@ -350,7 +346,7 @@ mod tests { has_video: false, ..Default::default() }; - + let video_meta = AudioVideoMetadata { format: "mp4".to_string(), width: Some(1920), @@ -359,13 +355,13 @@ mod tests { has_video: true, ..Default::default() }; - + let audio_desc = MetadataExtractor::get_format_description(&audio_meta); let video_desc = MetadataExtractor::get_format_description(&video_meta); - + assert!(audio_desc.contains("音频文件")); assert!(audio_desc.contains("44100Hz")); assert!(video_desc.contains("视频文件")); assert!(video_desc.contains("1920x1080")); } -} \ No newline at end of file +} diff --git a/voice-cli/src/services/mod.rs b/voice-cli/src/services/mod.rs index 2e7b352..976c69e 100644 --- a/voice-cli/src/services/mod.rs +++ b/voice-cli/src/services/mod.rs @@ -9,12 +9,16 @@ pub mod tts_service; pub mod tts_task_manager; // 重新导出核心服务 -pub use apalis_manager::{ApalisManager, LockFreeApalisManager, TranscriptionTask, StepContext, TaskStatusUpdate, init_global_apalis_manager, init_global_lock_free_apalis_manager, transcription_pipeline_worker}; +pub use apalis_manager::{ + ApalisManager, LockFreeApalisManager, StepContext, TaskStatusUpdate, TranscriptionTask, + init_global_apalis_manager, init_global_lock_free_apalis_manager, + transcription_pipeline_worker, +}; pub use audio_file_manager::AudioFileManager; pub use audio_format_detector::AudioFormatDetector; pub use audio_processor::AudioProcessor; -pub use metadata_extractor::{MetadataExtractor, AudioVideoMetadata}; +pub use metadata_extractor::{AudioVideoMetadata, MetadataExtractor}; pub use model_service::ModelService; pub use transcription_engine::TranscriptionEngine; pub use tts_service::TtsService; -pub use tts_task_manager::{TtsTaskManager, TtsTaskStats}; \ No newline at end of file +pub use tts_task_manager::{TtsTaskManager, TtsTaskStats}; diff --git a/voice-cli/src/services/model_service.rs b/voice-cli/src/services/model_service.rs index 64790f8..dd483a3 100644 --- a/voice-cli/src/services/model_service.rs +++ b/voice-cli/src/services/model_service.rs @@ -1,5 +1,5 @@ -use crate::models::{Config, DownloadStatus, ModelDownloadStatus, ModelInfo}; use crate::VoiceCliError; +use crate::models::{Config, DownloadStatus, ModelDownloadStatus, ModelInfo}; use reqwest::Client; use std::path::{Path, PathBuf}; use tokio::fs; @@ -311,8 +311,10 @@ impl ModelService { // Allow 20% size difference to accommodate different versions if size_diff_percent > 20.0 { - warn!("Model file size differs significantly from expected: actual={} bytes, expected={} bytes, diff={:.1}%", - actual_size, expected_size, size_diff_percent); + warn!( + "Model file size differs significantly from expected: actual={} bytes, expected={} bytes, diff={:.1}%", + actual_size, expected_size, size_diff_percent + ); // Don't fail validation, just warn - the file might still be valid } } diff --git a/voice-cli/src/services/transcription_engine.rs b/voice-cli/src/services/transcription_engine.rs index 6445fab..43b23d8 100644 --- a/voice-cli/src/services/transcription_engine.rs +++ b/voice-cli/src/services/transcription_engine.rs @@ -1,8 +1,8 @@ -use crate::services::ModelService; use crate::VoiceCliError; +use crate::services::ModelService; +use dashmap::DashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; -use dashmap::DashMap; // Reuse an already-loaded WhisperTranscriber to avoid reloading the model use voice_toolkit::stt::{self, TranscriptionResult, WhisperConfig, WhisperTranscriber}; @@ -94,10 +94,13 @@ impl TranscriptionEngine { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .map_err(|e| format!("Failed to create runtime for Whisper transcription: {}", e))?; + .map_err(|e| { + format!("Failed to create runtime for Whisper transcription: {}", e) + })?; rt.block_on(async { - stt::transcribe_file_with_transcriber(&transcriber, &audio_path).await + stt::transcribe_file_with_transcriber(&transcriber, &audio_path) + .await .map_err(|e| e.to_string()) }) }), @@ -108,9 +111,14 @@ impl TranscriptionEngine { if e.is_panic() { VoiceCliError::TranscriptionFailed("Whisper transcription panicked".to_string()) } else if e.is_cancelled() { - VoiceCliError::TranscriptionFailed("Whisper transcription was cancelled".to_string()) + VoiceCliError::TranscriptionFailed( + "Whisper transcription was cancelled".to_string(), + ) } else { - VoiceCliError::TranscriptionFailed(format!("Whisper transcription join error: {}", e)) + VoiceCliError::TranscriptionFailed(format!( + "Whisper transcription join error: {}", + e + )) } })?; @@ -143,10 +151,7 @@ impl TranscriptionEngine { .map_err(|e| VoiceCliError::AudioConversionFailed(format!("Task join error: {}", e)))? .map_err(|e| VoiceCliError::AudioConversionFailed(e.to_string()))?; - self - .transcribe_compatible_audio(model_name, &compatible.path, timeout_secs) + self.transcribe_compatible_audio(model_name, &compatible.path, timeout_secs) .await } } - - diff --git a/voice-cli/src/services/tts_service.rs b/voice-cli/src/services/tts_service.rs index e2c5488..2ba426c 100644 --- a/voice-cli/src/services/tts_service.rs +++ b/voice-cli/src/services/tts_service.rs @@ -1,5 +1,5 @@ use crate::VoiceCliError; -use crate::models::{TtsSyncRequest, TtsTaskResponse, TtsAsyncRequest}; +use crate::models::{TtsAsyncRequest, TtsSyncRequest, TtsTaskResponse}; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::NamedTempFile; @@ -16,7 +16,10 @@ pub struct TtsService { impl TtsService { /// 创建新的TTS服务实例 - pub fn new(python_path: Option, model_path: Option) -> Result { + pub fn new( + python_path: Option, + model_path: Option, + ) -> Result { let python_path = python_path.unwrap_or_else(|| { // 尝试在多个位置查找虚拟环境中的 Python let possible_venv_paths = vec![ @@ -33,14 +36,14 @@ impl TtsService { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".venv/bin/python") }, ]; - + // 查找第一个存在的 Python 解释器 for venv_python in possible_venv_paths { if venv_python.exists() { return venv_python; } } - + // 回退到系统 Python if let Ok(_output) = Command::new("python3").arg("--version").output() { PathBuf::from("python3") @@ -54,9 +57,9 @@ impl TtsService { // 获取脚本路径(首先尝试当前目录,然后尝试 crate 目录) let current_dir = std::env::current_dir() .map_err(|e| VoiceCliError::Config(format!("获取当前目录失败: {}", e)))?; - + let script_path = current_dir.join("tts_service.py"); - + let final_script_path = if script_path.exists() { script_path } else { @@ -66,15 +69,19 @@ impl TtsService { if crate_script_path.exists() { crate_script_path } else { - return Err(VoiceCliError::Config( - format!("TTS脚本不存在: 在 {:?} 或 {:?} 中都未找到", script_path, crate_script_path) - )); + return Err(VoiceCliError::Config(format!( + "TTS脚本不存在: 在 {:?} 或 {:?} 中都未找到", + script_path, crate_script_path + ))); } }; info!("使用 TTS 脚本: {:?}", final_script_path); - info!("初始化TTS服务 - Python: {:?}, 脚本: {:?}", python_path, final_script_path); + info!( + "初始化TTS服务 - Python: {:?}, 脚本: {:?}", + python_path, final_script_path + ); Ok(Self { python_path, @@ -86,7 +93,7 @@ impl TtsService { /// 同步TTS合成 pub async fn synthesize_sync(&self, request: TtsSyncRequest) -> Result { let start_time = std::time::Instant::now(); - + // 验证输入 if request.text.trim().is_empty() { return Err(VoiceCliError::InvalidInput("文本不能为空".to_string())); @@ -94,19 +101,25 @@ impl TtsService { if let Some(speed) = request.speed { if !(0.5..=2.0).contains(&speed) { - return Err(VoiceCliError::InvalidInput("语速必须在0.5-2.0之间".to_string())); + return Err(VoiceCliError::InvalidInput( + "语速必须在0.5-2.0之间".to_string(), + )); } } if let Some(pitch) = request.pitch { if !(-20..=20).contains(&pitch) { - return Err(VoiceCliError::InvalidInput("音调必须在-20到20之间".to_string())); + return Err(VoiceCliError::InvalidInput( + "音调必须在-20到20之间".to_string(), + )); } } if let Some(volume) = request.volume { if !(0.5..=2.0).contains(&volume) { - return Err(VoiceCliError::InvalidInput("音量必须在0.5-2.0之间".to_string())); + return Err(VoiceCliError::InvalidInput( + "音量必须在0.5-2.0之间".to_string(), + )); } } @@ -114,25 +127,39 @@ impl TtsService { let output_format = request.format.as_deref().unwrap_or("mp3"); let temp_file = NamedTempFile::new() .map_err(|e| VoiceCliError::Io(format!("创建临时文件失败: {}", e)))?; - + let output_path = temp_file.into_temp_path(); - let output_path_str = output_path.to_str() + let output_path_str = output_path + .to_str() .ok_or_else(|| VoiceCliError::Io("临时文件路径无效".to_string()))?; - info!("开始TTS合成 - 文本长度: {}, 格式: {}", request.text.len(), output_format); + info!( + "开始TTS合成 - 文本长度: {}, 格式: {}", + request.text.len(), + output_format + ); // 使用 uv run 来确保在正确的虚拟环境中运行 let mut cmd = Command::new("uv"); cmd.arg("run") - .arg(&self.script_path) - .arg(&request.text) - .arg("--output").arg(output_path_str) - .arg("--speed").arg(request.speed.unwrap_or(1.0).to_string()) - .arg("--pitch").arg(request.pitch.unwrap_or(0).to_string()) - .arg("--volume").arg(request.volume.unwrap_or(1.0).to_string()) - .arg("--format").arg(output_format) - // 设置工作目录为脚本所在的目录 - .current_dir(self.script_path.parent().unwrap_or(&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))); + .arg(&self.script_path) + .arg(&request.text) + .arg("--output") + .arg(output_path_str) + .arg("--speed") + .arg(request.speed.unwrap_or(1.0).to_string()) + .arg("--pitch") + .arg(request.pitch.unwrap_or(0).to_string()) + .arg("--volume") + .arg(request.volume.unwrap_or(1.0).to_string()) + .arg("--format") + .arg(output_format) + // 设置工作目录为脚本所在的目录 + .current_dir( + self.script_path + .parent() + .unwrap_or(&std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))), + ); // 添加模型参数 if let Some(model) = &request.model { @@ -146,43 +173,51 @@ impl TtsService { debug!("执行TTS命令: {:?}", cmd); // 执行命令 - let output = cmd.output() + let output = cmd + .output() .map_err(|e| VoiceCliError::TtsError(format!("执行TTS命令失败: {}", e)))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); error!("TTS合成失败 - stderr: {}, stdout: {}", stderr, stdout); - return Err(VoiceCliError::TtsError(format!( - "TTS合成失败: {}", - stderr - ))); + return Err(VoiceCliError::TtsError(format!("TTS合成失败: {}", stderr))); } // 验证输出文件 if !output_path.exists() { - return Err(VoiceCliError::TtsError("TTS合成失败:输出文件未创建".to_string())); + return Err(VoiceCliError::TtsError( + "TTS合成失败:输出文件未创建".to_string(), + )); } - let file_size = output_path.metadata() - .map(|m| m.len()) - .unwrap_or(0); + let file_size = output_path.metadata().map(|m| m.len()).unwrap_or(0); if file_size == 0 { - return Err(VoiceCliError::TtsError("TTS合成失败:输出文件为空".to_string())); + return Err(VoiceCliError::TtsError( + "TTS合成失败:输出文件为空".to_string(), + )); } let processing_time = start_time.elapsed(); - info!("TTS合成完成 - 文件大小: {} bytes, 耗时: {:?}", file_size, processing_time); + info!( + "TTS合成完成 - 文件大小: {} bytes, 耗时: {:?}", + file_size, processing_time + ); // 将临时文件持久化到正式位置 - let final_output_path = self.persist_output_file(&output_path, output_format).await?; + let final_output_path = self + .persist_output_file(&output_path, output_format) + .await?; Ok(final_output_path) } /// 创建异步TTS任务 - pub async fn create_async_task(&self, request: TtsAsyncRequest) -> Result { + pub async fn create_async_task( + &self, + request: TtsAsyncRequest, + ) -> Result { // 验证输入 if request.text.trim().is_empty() { return Err(VoiceCliError::InvalidInput("文本不能为空".to_string())); @@ -191,13 +226,16 @@ impl TtsService { // 预估处理时间(基于文本长度) let estimated_duration = self.estimate_processing_time(&request.text); - info!("创建TTS异步任务 - 文本长度: {}, 预估时长: {}s", - request.text.len(), estimated_duration); + info!( + "创建TTS异步任务 - 文本长度: {}, 预估时长: {}s", + request.text.len(), + estimated_duration + ); // TODO: 将任务提交到TTS任务管理器 // 这里暂时返回模拟的任务ID,实际实现需要集成TtsTaskManager let task_id = Uuid::new_v4().to_string(); - + Ok(TtsTaskResponse { task_id: task_id.clone(), message: "TTS任务已提交".to_string(), @@ -211,16 +249,21 @@ impl TtsService { // 假设每秒处理10个字符 let chars_per_second = 10; let estimated_seconds = (text.len() as f32 / chars_per_second as f32).ceil() as u32; - + // 最少3秒,最多300秒(5分钟) estimated_seconds.max(3).min(300) } /// 持久化输出文件 - async fn persist_output_file(&self, temp_path: &Path, format: &str) -> Result { + async fn persist_output_file( + &self, + temp_path: &Path, + format: &str, + ) -> Result { // 创建输出目录 let output_dir = PathBuf::from("./data/tts"); - tokio::fs::create_dir_all(&output_dir).await + tokio::fs::create_dir_all(&output_dir) + .await .map_err(|e| VoiceCliError::Io(format!("创建输出目录失败: {}", e)))?; // 生成唯一文件名 @@ -228,7 +271,8 @@ impl TtsService { let final_path = output_dir.join(filename); // 复制文件 - tokio::fs::copy(temp_path, &final_path).await + tokio::fs::copy(temp_path, &final_path) + .await .map_err(|e| VoiceCliError::Io(format!("复制文件失败: {}", e)))?; Ok(final_path) @@ -249,16 +293,16 @@ mod tests { #[test] fn test_estimate_processing_time() { let service = TtsService::new(None, None).unwrap(); - + // 测试短文本 let short_time = service.estimate_processing_time("Hello"); assert!(short_time >= 3); - + // 测试长文本 let long_text = "A".repeat(1000); let long_time = service.estimate_processing_time(&long_text); assert!(long_time > 50); - + // 测试最大限制 let very_long_text = "A".repeat(10000); let max_time = service.estimate_processing_time(&very_long_text); @@ -268,7 +312,7 @@ mod tests { #[tokio::test] async fn test_create_async_task() { let service = TtsService::new(None, None).unwrap(); - + let request = TtsAsyncRequest { text: "Hello, world!".to_string(), model: None, @@ -278,11 +322,11 @@ mod tests { format: Some("mp3".to_string()), priority: None, }; - + let response = service.create_async_task(request).await.unwrap(); - + assert!(!response.task_id.is_empty()); assert_eq!(response.message, "TTS任务已提交"); assert!(response.estimated_duration.unwrap() >= 3); } -} \ No newline at end of file +} diff --git a/voice-cli/src/services/tts_task_manager.rs b/voice-cli/src/services/tts_task_manager.rs index dec7868..c14745c 100644 --- a/voice-cli/src/services/tts_task_manager.rs +++ b/voice-cli/src/services/tts_task_manager.rs @@ -1,5 +1,7 @@ use crate::VoiceCliError; -use crate::models::{TtsAsyncRequest, TtsTaskStatus, TtsProcessingStage, TtsTaskError, TtsProgressDetails}; +use crate::models::{ + TtsAsyncRequest, TtsProcessingStage, TtsProgressDetails, TtsTaskError, TtsTaskStatus, +}; use apalis::prelude::*; use apalis_sql::sqlite::SqliteStorage; use chrono::{DateTime, Utc}; @@ -42,7 +44,10 @@ pub struct TtsTaskManager { impl TtsTaskManager { /// 创建新的TTS任务管理器 - pub async fn new(database_url: &str, max_concurrent_tasks: usize) -> Result { + pub async fn new( + database_url: &str, + max_concurrent_tasks: usize, + ) -> Result { info!("初始化TTS任务管理器 - 数据库: {}", database_url); // 创建SQLite存储 @@ -51,10 +56,8 @@ impl TtsTaskManager { .connect(database_url) .await .map_err(|e| VoiceCliError::Storage(format!("连接SQLite失败: {}", e)))?; - - let storage = Arc::new(RwLock::new( - SqliteStorage::new(pool) - )); + + let storage = Arc::new(RwLock::new(SqliteStorage::new(pool))); // 创建任务表 Self::create_tables_if_not_exists(&storage).await?; @@ -66,10 +69,12 @@ impl TtsTaskManager { } /// 创建必要的表 - async fn create_tables_if_not_exists(storage: &Arc>>) -> Result<(), VoiceCliError> { + async fn create_tables_if_not_exists( + storage: &Arc>>, + ) -> Result<(), VoiceCliError> { let guard = storage.read().await; let pool = guard.pool(); - + sqlx::query( r#" CREATE TABLE IF NOT EXISTS tts_tasks ( @@ -125,7 +130,7 @@ impl TtsTaskManager { // 保存任务到数据库 let guard = self.storage.read().await; let pool = guard.pool(); - + sqlx::query( r#" INSERT INTO tts_tasks ( @@ -154,10 +159,13 @@ impl TtsTaskManager { } /// 获取任务状态 - pub async fn get_task_status(&self, task_id: &str) -> Result, VoiceCliError> { + pub async fn get_task_status( + &self, + task_id: &str, + ) -> Result, VoiceCliError> { let guard = self.storage.read().await; let pool = guard.pool(); - + let row = sqlx::query( "SELECT status, updated_at, result_path, file_size, duration_seconds, error_message, retry_count FROM tts_tasks WHERE task_id = ?" ) @@ -177,7 +185,9 @@ impl TtsTaskManager { let retry_count: i32 = row.get("retry_count"); let status = match status_str.as_str() { - "pending" => TtsTaskStatus::Pending { queued_at: updated_at }, + "pending" => TtsTaskStatus::Pending { + queued_at: updated_at, + }, "processing" => TtsTaskStatus::Processing { stage: TtsProcessingStage::VoiceSynthesis, started_at: updated_at, @@ -190,7 +200,9 @@ impl TtsTaskManager { }), }, "completed" => { - if let (Some(path), Some(size), Some(duration)) = (result_path, file_size, duration_seconds) { + if let (Some(path), Some(size), Some(duration)) = + (result_path, file_size, duration_seconds) + { TtsTaskStatus::Completed { completed_at: updated_at, processing_time: updated_at.signed_duration_since(updated_at), // 这里应该用创建时间 @@ -199,7 +211,9 @@ impl TtsTaskManager { duration_seconds: duration as f32, } } else { - return Err(VoiceCliError::Storage("完成的任务缺少结果信息".to_string())); + return Err(VoiceCliError::Storage( + "完成的任务缺少结果信息".to_string(), + )); } } "failed" => { @@ -219,7 +233,12 @@ impl TtsTaskManager { cancelled_at: updated_at, reason: None, }, - _ => return Err(VoiceCliError::Storage(format!("未知的任务状态: {}", status_str))), + _ => { + return Err(VoiceCliError::Storage(format!( + "未知的任务状态: {}", + status_str + ))); + } }; Ok(Some(status)) @@ -229,7 +248,11 @@ impl TtsTaskManager { } /// 更新任务状态 - pub async fn update_task_status(&self, task_id: &str, status: TtsTaskStatus) -> Result<(), VoiceCliError> { + pub async fn update_task_status( + &self, + task_id: &str, + status: TtsTaskStatus, + ) -> Result<(), VoiceCliError> { let guard = self.storage.read().await; let pool = guard.pool(); let updated_at = Utc::now(); @@ -237,9 +260,18 @@ impl TtsTaskManager { let (status_str, result_path, file_size, duration_seconds, error_message) = match status { TtsTaskStatus::Pending { .. } => ("pending", None, None, None, None), TtsTaskStatus::Processing { .. } => ("processing", None, None, None, None), - TtsTaskStatus::Completed { audio_file_path, file_size, duration_seconds, .. } => { - ("completed", Some(audio_file_path), Some(file_size as i64), Some(duration_seconds as f64), None) - } + TtsTaskStatus::Completed { + audio_file_path, + file_size, + duration_seconds, + .. + } => ( + "completed", + Some(audio_file_path), + Some(file_size as i64), + Some(duration_seconds as f64), + None, + ), TtsTaskStatus::Failed { error, .. } => { ("failed", None, None, None, Some(error.to_string())) } @@ -269,7 +301,7 @@ impl TtsTaskManager { // TODO: 实现实际的任务处理逻辑 // 这里应该启动一个后台worker来处理TTS任务队列 - + Ok(()) } @@ -277,7 +309,7 @@ impl TtsTaskManager { pub async fn get_stats(&self) -> Result { let guard = self.storage.read().await; let pool = guard.pool(); - + let row = sqlx::query( r#" SELECT @@ -352,4 +384,4 @@ mod tests { let task_id = manager.submit_task(request).await.unwrap(); assert!(!task_id.is_empty()); } -} \ No newline at end of file +} diff --git a/voice-cli/src/tests/config_env_tests.rs b/voice-cli/src/tests/config_env_tests.rs index 8584663..ab0c915 100644 --- a/voice-cli/src/tests/config_env_tests.rs +++ b/voice-cli/src/tests/config_env_tests.rs @@ -1,10 +1,10 @@ #[cfg(test)] mod config_env_tests { use crate::models::Config; + use std::collections::HashMap; use std::env; - use tempfile::TempDir; use std::sync::{Mutex, OnceLock}; - use std::collections::HashMap; + use tempfile::TempDir; // Safe environment variable testing using static state static TEST_ENV_LOCK: OnceLock>>> = OnceLock::new(); @@ -17,12 +17,12 @@ mod config_env_tests { fn safe_set_env_var(key: &str, value: &str) { let lock = get_test_env_lock(); let mut env_state = lock.lock().unwrap(); - + // Store the original value if this is the first time setting this var if !env_state.contains_key(key) { env_state.insert(key.to_string(), env::var(key).ok()); } - + // This is still technically unsafe, but we'll wrap it in unsafe block // and document that tests should run serially to avoid race conditions unsafe { @@ -34,12 +34,12 @@ mod config_env_tests { fn safe_remove_env_var(key: &str) { let lock = get_test_env_lock(); let mut env_state = lock.lock().unwrap(); - + // Store the original value if this is the first time touching this var if !env_state.contains_key(key) { env_state.insert(key.to_string(), env::var(key).ok()); } - + unsafe { env::remove_var(key); } @@ -75,11 +75,15 @@ mod config_env_tests { fn restore_original_env_vars() { let lock = get_test_env_lock(); let env_state = lock.lock().unwrap(); - + for (key, original_value) in env_state.iter() { match original_value { - Some(value) => unsafe { env::set_var(key, value); }, - None => unsafe { env::remove_var(key); }, + Some(value) => unsafe { + env::set_var(key, value); + }, + None => unsafe { + env::remove_var(key); + }, } } } @@ -213,4 +217,4 @@ mod config_env_tests { // Clean up safe_remove_env_var("VOICE_CLI_HOST"); } -} \ No newline at end of file +} diff --git a/voice-cli/src/tests/config_validation_tests.rs b/voice-cli/src/tests/config_validation_tests.rs index 0f0d794..8e3edfc 100644 --- a/voice-cli/src/tests/config_validation_tests.rs +++ b/voice-cli/src/tests/config_validation_tests.rs @@ -1,8 +1,8 @@ #[cfg(test)] mod config_validation_tests { use crate::models::{ - AudioProcessingConfig, Config, DaemonConfig, - LoggingConfig, ServerConfig, TaskManagementConfig, WhisperConfig, WorkersConfig, + AudioProcessingConfig, Config, DaemonConfig, LoggingConfig, ServerConfig, + TaskManagementConfig, WhisperConfig, WorkersConfig, }; use std::path::PathBuf; use tempfile::TempDir; @@ -70,9 +70,11 @@ mod config_validation_tests { assert!(result.is_err()); let error = result.unwrap_err(); - assert!(error - .to_string() - .contains("Server port must be between 1 and 65535")); + assert!( + error + .to_string() + .contains("Server port must be between 1 and 65535") + ); } #[test] @@ -96,9 +98,11 @@ mod config_validation_tests { assert!(result.is_err()); let error = result.unwrap_err(); - assert!(error - .to_string() - .contains("Max file size must be greater than 0")); + assert!( + error + .to_string() + .contains("Max file size must be greater than 0") + ); } #[test] @@ -122,9 +126,11 @@ mod config_validation_tests { assert!(result.is_err()); let error = result.unwrap_err(); - assert!(error - .to_string() - .contains("Models directory cannot be empty")); + assert!( + error + .to_string() + .contains("Models directory cannot be empty") + ); } #[test] @@ -136,9 +142,11 @@ mod config_validation_tests { assert!(result.is_err()); let error = result.unwrap_err(); - assert!(error - .to_string() - .contains("Transcription workers must be greater than 0")); + assert!( + error + .to_string() + .contains("Transcription workers must be greater than 0") + ); } #[test] @@ -204,9 +212,11 @@ mod config_validation_tests { assert!(result.is_err()); let error = result.unwrap_err(); - assert!(error - .to_string() - .contains("Max log files must be greater than 0")); + assert!( + error + .to_string() + .contains("Max log files must be greater than 0") + ); } #[test] @@ -322,4 +332,4 @@ whisper: config.logging.max_files = 1000; assert!(config.validate().is_ok()); } -} \ No newline at end of file +} diff --git a/voice-cli/src/tests/graceful_shutdown_test.rs b/voice-cli/src/tests/graceful_shutdown_test.rs index 4af70a5..f9d0a97 100644 --- a/voice-cli/src/tests/graceful_shutdown_test.rs +++ b/voice-cli/src/tests/graceful_shutdown_test.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod graceful_shutdown_tests { - use crate::server; use crate::models::Config; + use crate::server; use std::sync::Arc; use tokio::sync::broadcast; use tracing::info; @@ -11,26 +11,34 @@ mod graceful_shutdown_tests { // Test that the broadcast channel shutdown mechanism works let (shutdown_tx, _) = broadcast::channel(1); let mut shutdown_rx = shutdown_tx.subscribe(); - + // Test sending and receiving shutdown signals let send_handle = tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(100)).await; info!("Sending shutdown signal..."); - shutdown_tx.send(()).expect("Failed to send shutdown signal"); + shutdown_tx + .send(()) + .expect("Failed to send shutdown signal"); }); - + let receive_handle = tokio::spawn(async move { info!("Waiting for shutdown signal..."); let result = shutdown_rx.recv().await; assert!(result.is_ok(), "Should receive shutdown signal"); info!("Received shutdown signal successfully"); }); - + // Wait for both tasks to complete let (send_result, receive_result) = tokio::join!(send_handle, receive_handle); - - assert!(send_result.is_ok(), "Send task should complete successfully"); - assert!(receive_result.is_ok(), "Receive task should complete successfully"); + + assert!( + send_result.is_ok(), + "Send task should complete successfully" + ); + assert!( + receive_result.is_ok(), + "Receive task should complete successfully" + ); } #[tokio::test] @@ -38,14 +46,14 @@ mod graceful_shutdown_tests { // Test that AppState can be created and shut down gracefully let config = Config::default(); let config_arc = Arc::new(config); - + let app_state = server::handlers::AppState::new(config_arc.clone()) .await .expect("Failed to create app state"); - + // Test that shutdown works app_state.shutdown().await; - + info!("AppState shutdown completed successfully"); } -} \ No newline at end of file +} diff --git a/voice-cli/src/tests/task_management_integration_tests.rs b/voice-cli/src/tests/task_management_integration_tests.rs index ed3e71d..5a8bbc1 100644 --- a/voice-cli/src/tests/task_management_integration_tests.rs +++ b/voice-cli/src/tests/task_management_integration_tests.rs @@ -1,8 +1,6 @@ #[cfg(test)] mod task_management_integration_tests { - use crate::models::{ - AsyncTranscriptionTask, Config, TaskManagementConfig, TaskStatus, - }; + use crate::models::{AsyncTranscriptionTask, Config, TaskManagementConfig, TaskStatus}; use std::path::PathBuf; use std::sync::Arc; use tempfile::TempDir; @@ -54,4 +52,4 @@ mod task_management_integration_tests { // assert!(status.is_some()); // assert!(matches!(status.unwrap(), TaskStatus::Pending { .. })); // } -} \ No newline at end of file +} diff --git a/voice-cli/src/utils/cleanup.rs b/voice-cli/src/utils/cleanup.rs index d49a34d..075aeaf 100644 --- a/voice-cli/src/utils/cleanup.rs +++ b/voice-cli/src/utils/cleanup.rs @@ -4,13 +4,13 @@ use tracing::{info, warn}; /// Perform global cleanup operations during shutdown pub async fn perform_shutdown_cleanup() { info!("Starting shutdown cleanup operations"); - + // Clean up any remaining temporary files cleanup_temp_directories().await; - + // Flush any remaining logs flush_logs().await; - + info!("Shutdown cleanup completed"); } @@ -18,10 +18,10 @@ pub async fn perform_shutdown_cleanup() { async fn cleanup_temp_directories() { let temp_patterns = [ "/tmp/voice-cli-*", - "./temp/voice-cli-*", + "./temp/voice-cli-*", "./tmp/voice-cli-*", ]; - + for pattern in &temp_patterns { if let Some(parent) = Path::new(pattern).parent() { if parent.exists() { @@ -39,7 +39,10 @@ async fn cleanup_temp_directories() { } } else if path.is_dir() { if let Err(e) = std::fs::remove_dir_all(&path) { - warn!("Failed to cleanup temp directory {:?}: {}", path, e); + warn!( + "Failed to cleanup temp directory {:?}: {}", + path, e + ); } else { info!("Cleaned up temp directory: {:?}", path); } @@ -61,7 +64,7 @@ async fn cleanup_temp_directories() { async fn flush_logs() { // Give the logging system a moment to flush any remaining logs tokio::time::sleep(std::time::Duration::from_millis(100)).await; - + // Force flush tracing subscriber if possible // Note: This is a best-effort operation info!("Log flush completed"); @@ -93,52 +96,52 @@ pub fn cleanup_directory>(dir: P) -> Result<(), std::io::Error> { #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::fs::File; - + use tempfile::TempDir; + #[tokio::test] async fn test_cleanup_files() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("test1.txt"); let file2 = temp_dir.path().join("test2.txt"); - + // Create test files File::create(&file1).unwrap(); File::create(&file2).unwrap(); - + assert!(file1.exists()); assert!(file2.exists()); - + // Cleanup files cleanup_files(&[file1.clone(), file2.clone()]); - + assert!(!file1.exists()); assert!(!file2.exists()); } - + #[test] fn test_cleanup_directory() { let temp_dir = TempDir::new().unwrap(); let test_dir = temp_dir.path().join("test_cleanup"); std::fs::create_dir(&test_dir).unwrap(); - + // Create a file in the directory let test_file = test_dir.join("test.txt"); File::create(&test_file).unwrap(); - + assert!(test_dir.exists()); assert!(test_file.exists()); - + // Cleanup directory cleanup_directory(&test_dir).unwrap(); - + assert!(!test_dir.exists()); assert!(!test_file.exists()); } - + #[tokio::test] async fn test_perform_shutdown_cleanup() { // This test just ensures the function runs without panicking perform_shutdown_cleanup().await; } -} \ No newline at end of file +} diff --git a/voice-cli/src/utils/mime_types.rs b/voice-cli/src/utils/mime_types.rs index df00bdd..bb60ba5 100644 --- a/voice-cli/src/utils/mime_types.rs +++ b/voice-cli/src/utils/mime_types.rs @@ -25,7 +25,7 @@ pub fn mime_type_to_extension(content_type: &str) -> &'static str { "audio/x-m4a" => "m4a", "audio/x-mpeg" => "mp3", "audio/x-ogg" => "ogg", - + // 视频类型 "video/mp4" => "mp4", "video/webm" => "webm", @@ -38,7 +38,7 @@ pub fn mime_type_to_extension(content_type: &str) -> &'static str { "video/3gpp2" => "3g2", "video/mpeg" => "mpeg", "video/x-mpeg" => "mpeg", - + // 其他所有类型统一使用 bin 扩展名 _ => { // 对于非音视频类型,直接使用默认扩展名,不输出警告日志 @@ -123,20 +123,21 @@ pub fn extension_to_mime_type(extension: &str) -> &'static str { /// 智能获取文件扩展名(优先使用 MIME 类型,回退到 URL 扩展名) pub fn get_file_extension(content_type: &str, url: &str) -> &'static str { let extension = mime_type_to_extension(content_type); - + // 如果得到的是默认扩展名,尝试从 URL 中获取更精确的扩展名 if extension == "bin" { if let Some(url_extension) = extract_extension_from_url(url) { return url_extension; } } - + extension } /// 检查是否为支持的音频格式 pub fn is_supported_audio_format(mime_type: &str) -> bool { - matches!(mime_type, + matches!( + mime_type, "audio/mpeg" | // MP3 "audio/wav" | // WAV "audio/flac" | // FLAC @@ -150,13 +151,14 @@ pub fn is_supported_audio_format(mime_type: &str) -> bool { "audio/vnd.wave" | // WAV (alternative) "audio/x-aiff" | // AIFF "audio/aiff" | // AIFF (alternative) - "audio/x-caf" // CAF + "audio/x-caf" // CAF ) } /// 检查是否为支持的视频格式 pub fn is_supported_video_format(mime_type: &str) -> bool { - matches!(mime_type, + matches!( + mime_type, "video/mp4" | // MP4 "video/webm" | // WebM "video/x-matroska" | // MKV @@ -167,7 +169,7 @@ pub fn is_supported_video_format(mime_type: &str) -> bool { "video/3gpp" | // 3GP "video/3gpp2" | // 3G2 "video/mpeg" | // MPEG - "video/x-mpeg" // MPEG (alternative) + "video/x-mpeg" // MPEG (alternative) ) } @@ -193,9 +195,18 @@ mod tests { #[test] fn test_extract_extension_from_url() { - assert_eq!(extract_extension_from_url("https://example.com/test.mp3"), Some("mp3")); - assert_eq!(extract_extension_from_url("https://example.com/test.wav"), Some("wav")); - assert_eq!(extract_extension_from_url("https://example.com/test?format=mp3"), None); + assert_eq!( + extract_extension_from_url("https://example.com/test.mp3"), + Some("mp3") + ); + assert_eq!( + extract_extension_from_url("https://example.com/test.wav"), + Some("wav") + ); + assert_eq!( + extract_extension_from_url("https://example.com/test?format=mp3"), + None + ); assert_eq!(extract_extension_from_url("invalid-url"), None); } @@ -204,14 +215,26 @@ mod tests { assert_eq!(extension_to_mime_type("mp3"), "audio/mpeg"); assert_eq!(extension_to_mime_type("wav"), "audio/wav"); assert_eq!(extension_to_mime_type("mp4"), "video/mp4"); - assert_eq!(extension_to_mime_type("unknown"), "application/octet-stream"); + assert_eq!( + extension_to_mime_type("unknown"), + "application/octet-stream" + ); } #[test] fn test_get_file_extension() { - assert_eq!(get_file_extension("audio/mpeg", "https://example.com/test.mp3"), "mp3"); - assert_eq!(get_file_extension("unknown/type", "https://example.com/test.wav"), "wav"); - assert_eq!(get_file_extension("unknown/type", "https://example.com/test"), "bin"); + assert_eq!( + get_file_extension("audio/mpeg", "https://example.com/test.mp3"), + "mp3" + ); + assert_eq!( + get_file_extension("unknown/type", "https://example.com/test.wav"), + "wav" + ); + assert_eq!( + get_file_extension("unknown/type", "https://example.com/test"), + "bin" + ); } #[test] @@ -219,13 +242,13 @@ mod tests { assert!(is_supported_audio_format("audio/mpeg")); assert!(is_supported_audio_format("audio/wav")); assert!(!is_supported_audio_format("video/mp4")); - + assert!(is_supported_video_format("video/mp4")); assert!(is_supported_video_format("video/webm")); assert!(!is_supported_video_format("audio/mpeg")); - + assert!(is_supported_media_format("audio/mpeg")); assert!(is_supported_media_format("video/mp4")); assert!(!is_supported_media_format("unknown/type")); } -} \ No newline at end of file +} diff --git a/voice-cli/src/utils/mod.rs b/voice-cli/src/utils/mod.rs index 595da73..0071f7d 100644 --- a/voice-cli/src/utils/mod.rs +++ b/voice-cli/src/utils/mod.rs @@ -1,7 +1,7 @@ -pub mod signal_handling; pub mod cleanup; -pub mod task_id; pub mod mime_types; +pub mod signal_handling; +pub mod task_id; use crate::VoiceCliError; use crate::models::Config; @@ -11,23 +11,17 @@ use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{EnvFilter, prelude::*}; // Re-export signal handling components +pub use cleanup::perform_shutdown_cleanup; +pub use mime_types::{ + extension_to_mime_type, extract_extension_from_url, get_file_extension, + infer_mime_type_from_path, is_supported_audio_format, is_supported_media_format, + is_supported_video_format, mime_type_to_extension, +}; pub use signal_handling::{ create_combined_shutdown_signal, create_service_shutdown_signal, create_shutdown_signal, handle_system_signals, }; -pub use cleanup::perform_shutdown_cleanup; -pub use task_id::{generate_task_id}; -pub use mime_types::{ - mime_type_to_extension, - extract_extension_from_url, - infer_mime_type_from_path, - extension_to_mime_type, - get_file_extension, - is_supported_audio_format, - is_supported_video_format, - is_supported_media_format, -}; - +pub use task_id::generate_task_id; /// Initialize logging based on configuration /// The guard is stored globally to ensure logging stays active diff --git a/voice-cli/src/utils/signal_handling.rs b/voice-cli/src/utils/signal_handling.rs index edd6930..09e1c6f 100644 --- a/voice-cli/src/utils/signal_handling.rs +++ b/voice-cli/src/utils/signal_handling.rs @@ -1,22 +1,22 @@ //! Shared signal handling utilities -//! +//! //! This module provides unified signal handling for background services, //! eliminating duplicate signal handling code across different services. use tokio::signal; -use tracing::{info, error, debug}; +use tracing::{debug, error, info}; /// Create a unified shutdown signal handler that listens to multiple sources -/// +/// /// This function provides consistent signal handling across all services: -/// - Ctrl+C (SIGINT) +/// - Ctrl+C (SIGINT) /// - SIGTERM (on Unix platforms) /// - Manual shutdown signals -/// +/// /// # Example /// ```rust /// use voice_cli::utils::signal_handling::create_shutdown_signal; -/// +/// /// let shutdown_signal = create_shutdown_signal(); /// tokio::select! { /// _ = service_work => {}, @@ -30,7 +30,7 @@ pub async fn create_shutdown_signal() { } /// Handle system signals for graceful shutdown -/// +/// /// This provides the core signal handling logic used by all services. /// Separated into its own function for reusability and testing. pub async fn handle_system_signals() { @@ -44,7 +44,7 @@ pub async fn handle_system_signals() { #[cfg(unix)] let terminate = async { - use tokio::signal::unix::{signal, SignalKind}; + use tokio::signal::unix::{SignalKind, signal}; match signal(SignalKind::terminate()) { Ok(mut stream) => { stream.recv().await; @@ -66,22 +66,22 @@ pub async fn handle_system_signals() { } /// Create a combined shutdown signal that listens to both system signals and manual channels -/// +/// /// This is useful for services that need to respond to both system signals (Ctrl+C, SIGTERM) /// and programmatic shutdown requests via channels. -/// +/// /// # Arguments /// * `manual_shutdown` - A future that completes when manual shutdown is requested -/// +/// /// # Example /// ```rust /// use voice_cli::utils::signal_handling::create_combined_shutdown_signal; -/// +/// /// let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); /// let shutdown_signal = create_combined_shutdown_signal(async { /// shutdown_rx.await.ok(); /// }); -/// +/// /// tokio::select! { /// _ = service_work => {}, /// _ = shutdown_signal => { @@ -89,7 +89,7 @@ pub async fn handle_system_signals() { /// } /// } /// ``` -pub async fn create_combined_shutdown_signal(manual_shutdown: F) +pub async fn create_combined_shutdown_signal(manual_shutdown: F) where F: std::future::Future, { @@ -104,16 +104,16 @@ where } /// Create a shutdown signal with service-specific logging -/// +/// /// This provides service-specific logging messages for better debugging. -/// +/// /// # Arguments /// * `service_name` - Name of the service for logging context -/// +/// /// # Example /// ```rust /// use voice_cli::utils::signal_handling::create_service_shutdown_signal; -/// +/// /// let shutdown_signal = create_service_shutdown_signal("http-server"); /// tokio::select! { /// _ = service_work => {}, @@ -131,7 +131,7 @@ pub async fn create_service_shutdown_signal(service_name: &str) { #[cfg(unix)] let terminate = async { - use tokio::signal::unix::{signal, SignalKind}; + use tokio::signal::unix::{SignalKind, signal}; match signal(SignalKind::terminate()) { Ok(mut stream) => { stream.recv().await; @@ -155,46 +155,44 @@ pub async fn create_service_shutdown_signal(service_name: &str) { #[cfg(test)] mod tests { use super::*; - use tokio::time::{timeout, Duration}; - + use tokio::time::{Duration, timeout}; + #[tokio::test] async fn test_signal_handling_timeout() { // Test that the signal handlers don't hang indefinitely - let result = timeout( - Duration::from_millis(100), - create_shutdown_signal() - ).await; - + let result = timeout(Duration::from_millis(100), create_shutdown_signal()).await; + // Should timeout since no signals are sent assert!(result.is_err()); } - + #[tokio::test] async fn test_combined_shutdown_manual() { let (tx, rx) = tokio::sync::oneshot::channel::<()>(); - + // Start the combined signal handler let signal_future = create_combined_shutdown_signal(async move { rx.await.ok(); }); - + // Send manual shutdown signal let _ = tx.send(()); - + // Should complete quickly due to manual signal let result = timeout(Duration::from_millis(100), signal_future).await; assert!(result.is_ok()); } - + #[tokio::test] async fn test_service_shutdown_signal_timeout() { // Test service-specific signal handling let result = timeout( Duration::from_millis(100), - create_service_shutdown_signal("test-service") - ).await; - + create_service_shutdown_signal("test-service"), + ) + .await; + // Should timeout since no signals are sent assert!(result.is_err()); } -} \ No newline at end of file +} diff --git a/voice-cli/src/utils/task_id.rs b/voice-cli/src/utils/task_id.rs index 05d6ee7..e6d16c9 100644 --- a/voice-cli/src/utils/task_id.rs +++ b/voice-cli/src/utils/task_id.rs @@ -3,9 +3,9 @@ use uuid::Uuid; /// Generate a clean task ID with only alphanumeric characters /// Format: "task" + 32-character hexadecimal string (from UUID v7) -/// +/// /// # Examples -/// +/// /// ``` /// let task_id = generate_task_id(); /// assert!(task_id.starts_with("task")); @@ -20,7 +20,6 @@ pub fn generate_task_id() -> String { format!("task{}", cleaned_uuid) } - #[cfg(test)] mod tests { use super::*; @@ -28,15 +27,15 @@ mod tests { #[test] fn test_generate_task_id_format() { let task_id = generate_task_id(); - + // Check format assert!(task_id.starts_with("task")); assert!(!task_id.contains('-')); assert!(!task_id.contains('_')); - + // Check length (task + 32 hex chars) assert_eq!(task_id.len(), 36); - + // Check that it's all alphanumeric after "task" prefix let suffix = &task_id[4..]; assert!(suffix.chars().all(|c| c.is_ascii_hexdigit())); @@ -46,8 +45,8 @@ mod tests { fn test_generate_task_id_uniqueness() { let id1 = generate_task_id(); let id2 = generate_task_id(); - + // Should be different (due to timestamp in UUID v7) assert_ne!(id1, id2); } -} \ No newline at end of file +} diff --git a/voice-cli/tests/config_template_tests.rs b/voice-cli/tests/config_template_tests.rs index b90161b..6b33eb0 100644 --- a/voice-cli/tests/config_template_tests.rs +++ b/voice-cli/tests/config_template_tests.rs @@ -16,8 +16,14 @@ mod config_template_tests { assert!(server_template.contains("daemon:"), "应该包含daemon配置"); // Server模板不应该包含cluster配置 - assert!(!server_template.contains("cluster:"), "server模板不应该包含cluster配置"); - assert!(!server_template.contains("load_balancer:"), "server模板不应该包含load_balancer配置"); + assert!( + !server_template.contains("cluster:"), + "server模板不应该包含cluster配置" + ); + assert!( + !server_template.contains("load_balancer:"), + "server模板不应该包含load_balancer配置" + ); println!( "✅ Server配置模板包含所有必要的配置项,长度: {} 字节", @@ -38,14 +44,26 @@ mod config_template_tests { println!("✅ Server配置模板YAML格式有效"); // 验证关键配置节点存在 - assert!(yaml_value.get("server").is_some(), "server模板应该有server配置节点"); - assert!(yaml_value.get("whisper").is_some(), "server模板应该有whisper配置节点"); - assert!(yaml_value.get("logging").is_some(), "server模板应该有logging配置节点"); - assert!(yaml_value.get("daemon").is_some(), "server模板应该有daemon配置节点"); + assert!( + yaml_value.get("server").is_some(), + "server模板应该有server配置节点" + ); + assert!( + yaml_value.get("whisper").is_some(), + "server模板应该有whisper配置节点" + ); + assert!( + yaml_value.get("logging").is_some(), + "server模板应该有logging配置节点" + ); + assert!( + yaml_value.get("daemon").is_some(), + "server模板应该有daemon配置节点" + ); } Err(e) => { panic!("Server配置模板YAML格式无效: {}", e); } } } -} \ No newline at end of file +} diff --git a/voice-cli/tests/integration_tests.rs b/voice-cli/tests/integration_tests.rs index 2e7619b..2d739dc 100644 --- a/voice-cli/tests/integration_tests.rs +++ b/voice-cli/tests/integration_tests.rs @@ -4,7 +4,7 @@ use bytes::Bytes; use serde_json::Value; use std::sync::Arc; use tempfile::TempDir; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; use tower::ServiceExt; use voice_cli::models::{Config, TaskManagementConfig}; use voice_cli::server::routes; @@ -48,25 +48,25 @@ async fn create_test_config() -> (Arc, TempDir) { fn create_test_audio_data() -> Bytes { // Create a minimal WAV file header + some dummy audio data let mut wav_data = Vec::new(); - + // WAV header (44 bytes) wav_data.extend_from_slice(b"RIFF"); wav_data.extend_from_slice(&(36u32).to_le_bytes()); // File size - 8 wav_data.extend_from_slice(b"WAVE"); wav_data.extend_from_slice(b"fmt "); wav_data.extend_from_slice(&(16u32).to_le_bytes()); // Subchunk1 size - wav_data.extend_from_slice(&(1u16).to_le_bytes()); // Audio format (PCM) - wav_data.extend_from_slice(&(1u16).to_le_bytes()); // Num channels + wav_data.extend_from_slice(&(1u16).to_le_bytes()); // Audio format (PCM) + wav_data.extend_from_slice(&(1u16).to_le_bytes()); // Num channels wav_data.extend_from_slice(&(44100u32).to_le_bytes()); // Sample rate wav_data.extend_from_slice(&(88200u32).to_le_bytes()); // Byte rate - wav_data.extend_from_slice(&(2u16).to_le_bytes()); // Block align + wav_data.extend_from_slice(&(2u16).to_le_bytes()); // Block align wav_data.extend_from_slice(&(16u16).to_le_bytes()); // Bits per sample wav_data.extend_from_slice(b"data"); wav_data.extend_from_slice(&(0u32).to_le_bytes()); // Data size - + // Add some dummy audio data (silence) wav_data.extend_from_slice(&[0u8; 1000]); - + Bytes::from(wav_data) } @@ -74,14 +74,16 @@ fn create_test_audio_data() -> Bytes { fn create_multipart_body(audio_data: &Bytes, model: Option<&str>) -> (String, Bytes) { let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; let mut body = Vec::new(); - + // Audio field body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - body.extend_from_slice(b"Content-Disposition: form-data; name=\"audio\"; filename=\"test.wav\"\r\n"); + body.extend_from_slice( + b"Content-Disposition: form-data; name=\"audio\"; filename=\"test.wav\"\r\n", + ); body.extend_from_slice(b"Content-Type: audio/wav\r\n\r\n"); body.extend_from_slice(audio_data); body.extend_from_slice(b"\r\n"); - + // Model field (if provided) if let Some(model) = model { body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); @@ -89,10 +91,10 @@ fn create_multipart_body(audio_data: &Bytes, model: Option<&str>) -> (String, By body.extend_from_slice(model.as_bytes()); body.extend_from_slice(b"\r\n"); } - + // End boundary body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); - + let content_type = format!("multipart/form-data; boundary={}", boundary); (content_type, Bytes::from(body)) } @@ -101,19 +103,21 @@ fn create_multipart_body(audio_data: &Bytes, model: Option<&str>) -> (String, By async fn test_health_endpoint() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + let request = Request::builder() .method(Method::GET) .uri("/health") .body(Body::empty()) .unwrap(); - + let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); - - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); let health_response: Value = serde_json::from_slice(&body).unwrap(); - + assert_eq!(health_response["status"], "healthy"); } @@ -121,19 +125,21 @@ async fn test_health_endpoint() { async fn test_models_endpoint() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + let request = Request::builder() .method(Method::GET) .uri("/models") .body(Body::empty()) .unwrap(); - + let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); - - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); let models_response: Value = serde_json::from_slice(&body).unwrap(); - + assert_eq!(models_response["success"], true); assert!(models_response["data"]["available_models"].is_array()); } @@ -142,10 +148,10 @@ async fn test_models_endpoint() { async fn test_async_transcription_workflow() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + let audio_data = create_test_audio_data(); let (content_type, body) = create_multipart_body(&audio_data, Some("base")); - + // Submit async transcription task let request = Request::builder() .method(Method::POST) @@ -153,36 +159,40 @@ async fn test_async_transcription_workflow() { .header("content-type", content_type) .body(Body::from(body)) .unwrap(); - + let response = app.clone().oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); - - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); let task_response: Value = serde_json::from_slice(&body).unwrap(); - + assert_eq!(task_response["success"], true); let task_id = task_response["data"]["task_id"].as_str().unwrap(); assert!(!task_id.is_empty()); - + // Wait a bit for task processing sleep(Duration::from_millis(100)).await; - + // Check task status let status_request = Request::builder() .method(Method::GET) .uri(&format!("/tasks/{}", task_id)) .body(Body::empty()) .unwrap(); - + let status_response = app.clone().oneshot(status_request).await.unwrap(); assert_eq!(status_response.status(), StatusCode::OK); - - let status_body = axum::body::to_bytes(status_response.into_body(), usize::MAX).await.unwrap(); + + let status_body = axum::body::to_bytes(status_response.into_body(), usize::MAX) + .await + .unwrap(); let status_data: Value = serde_json::from_slice(&status_body).unwrap(); - + assert_eq!(status_data["success"], true); assert_eq!(status_data["data"]["task_id"], task_id); - + // The task should be in pending or processing state let status = status_data["data"]["status"].as_object().unwrap(); assert!(status.contains_key("Pending") || status.contains_key("Processing")); @@ -192,10 +202,10 @@ async fn test_async_transcription_workflow() { async fn test_task_cancellation() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + let audio_data = create_test_audio_data(); let (content_type, body) = create_multipart_body(&audio_data, None); - + // Submit task let request = Request::builder() .method(Method::POST) @@ -203,62 +213,69 @@ async fn test_task_cancellation() { .header("content-type", content_type) .body(Body::from(body)) .unwrap(); - + let response = app.clone().oneshot(request).await.unwrap(); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); let task_response: Value = serde_json::from_slice(&body).unwrap(); let task_id = task_response["data"]["task_id"].as_str().unwrap(); - + // Cancel task let cancel_request = Request::builder() .method(Method::DELETE) .uri(&format!("/tasks/{}", task_id)) .body(Body::empty()) .unwrap(); - + let cancel_response = app.clone().oneshot(cancel_request).await.unwrap(); - + // Should succeed or return 400 if task already started processing - assert!(cancel_response.status() == StatusCode::OK || cancel_response.status() == StatusCode::BAD_REQUEST); + assert!( + cancel_response.status() == StatusCode::OK + || cancel_response.status() == StatusCode::BAD_REQUEST + ); } #[tokio::test] async fn test_task_listing() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + // Submit multiple tasks for _i in 0..3 { let audio_data = create_test_audio_data(); let (content_type, body) = create_multipart_body(&audio_data, None); - + let request = Request::builder() .method(Method::POST) .uri("/tasks/transcribe") .header("content-type", content_type) .body(Body::from(body)) .unwrap(); - + let response = app.clone().oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } - + // Wait a bit sleep(Duration::from_millis(100)).await; - + // List tasks let list_request = Request::builder() .method(Method::GET) .uri("/tasks?limit=10") .body(Body::empty()) .unwrap(); - + let list_response = app.clone().oneshot(list_request).await.unwrap(); assert_eq!(list_response.status(), StatusCode::OK); - - let list_body = axum::body::to_bytes(list_response.into_body(), usize::MAX).await.unwrap(); + + let list_body = axum::body::to_bytes(list_response.into_body(), usize::MAX) + .await + .unwrap(); let list_data: Value = serde_json::from_slice(&list_body).unwrap(); - + assert_eq!(list_data["success"], true); let tasks = list_data["data"]["tasks"].as_array().unwrap(); assert!(tasks.len() >= 3); @@ -268,20 +285,22 @@ async fn test_task_listing() { async fn test_task_statistics() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + // Get initial stats let stats_request = Request::builder() .method(Method::GET) .uri("/tasks/stats") .body(Body::empty()) .unwrap(); - + let stats_response = app.clone().oneshot(stats_request).await.unwrap(); assert_eq!(stats_response.status(), StatusCode::OK); - - let stats_body = axum::body::to_bytes(stats_response.into_body(), usize::MAX).await.unwrap(); + + let stats_body = axum::body::to_bytes(stats_response.into_body(), usize::MAX) + .await + .unwrap(); let stats_data: Value = serde_json::from_slice(&stats_body).unwrap(); - + assert_eq!(stats_data["success"], true); assert!(stats_data["data"]["total_tasks"].is_number()); assert!(stats_data["data"]["pending_tasks"].is_number()); @@ -294,20 +313,22 @@ async fn test_task_statistics() { async fn test_cleanup_endpoint() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + // Trigger cleanup let cleanup_request = Request::builder() .method(Method::POST) .uri("/tasks/cleanup") .body(Body::empty()) .unwrap(); - + let cleanup_response = app.oneshot(cleanup_request).await.unwrap(); assert_eq!(cleanup_response.status(), StatusCode::OK); - - let cleanup_body = axum::body::to_bytes(cleanup_response.into_body(), usize::MAX).await.unwrap(); + + let cleanup_body = axum::body::to_bytes(cleanup_response.into_body(), usize::MAX) + .await + .unwrap(); let cleanup_data: Value = serde_json::from_slice(&cleanup_body).unwrap(); - + assert_eq!(cleanup_data["success"], true); assert!(cleanup_data["data"]["cleaned_tasks"].is_number()); assert!(cleanup_data["data"]["message"].is_string()); @@ -317,10 +338,10 @@ async fn test_cleanup_endpoint() { async fn test_sync_transcription_still_works() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + let audio_data = create_test_audio_data(); let (content_type, body) = create_multipart_body(&audio_data, Some("base")); - + // Test synchronous transcription endpoint let request = Request::builder() .method(Method::POST) @@ -328,29 +349,33 @@ async fn test_sync_transcription_still_works() { .header("content-type", content_type) .body(Body::from(body)) .unwrap(); - + let response = app.oneshot(request).await.unwrap(); - + // Should return OK or an error (depending on whether models are available) // The important thing is that the endpoint is accessible - assert!(response.status() == StatusCode::OK || response.status().is_client_error() || response.status().is_server_error()); + assert!( + response.status() == StatusCode::OK + || response.status().is_client_error() + || response.status().is_server_error() + ); } #[tokio::test] async fn test_error_handling() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + // Test invalid task ID let invalid_request = Request::builder() .method(Method::GET) .uri("/tasks/invalid-task-id") .body(Body::empty()) .unwrap(); - + let invalid_response = app.clone().oneshot(invalid_request).await.unwrap(); assert_eq!(invalid_response.status(), StatusCode::NOT_FOUND); - + // Test malformed multipart data let malformed_request = Request::builder() .method(Method::POST) @@ -358,21 +383,27 @@ async fn test_error_handling() { .header("content-type", "multipart/form-data; boundary=invalid") .body(Body::from("invalid data")) .unwrap(); - + let malformed_response = app.clone().oneshot(malformed_request).await.unwrap(); assert!(malformed_response.status().is_client_error()); - + // Test missing audio field let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; - let empty_body = format!("--{}\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\nbase\r\n--{}--\r\n", boundary, boundary); - + let empty_body = format!( + "--{}\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\nbase\r\n--{}--\r\n", + boundary, boundary + ); + let empty_request = Request::builder() .method(Method::POST) .uri("/tasks/transcribe") - .header("content-type", format!("multipart/form-data; boundary={}", boundary)) + .header( + "content-type", + format!("multipart/form-data; boundary={}", boundary), + ) .body(Body::from(empty_body)) .unwrap(); - + let empty_response = app.oneshot(empty_request).await.unwrap(); assert!(empty_response.status().is_client_error()); } @@ -381,30 +412,30 @@ async fn test_error_handling() { async fn test_concurrent_requests() { let (config, _temp_dir) = create_test_config().await; let app = routes::create_routes(config).await.unwrap(); - + let mut handles = vec![]; - + // Submit multiple concurrent requests for i in 0..5 { let app = app.clone(); let handle = tokio::spawn(async move { let audio_data = create_test_audio_data(); let (content_type, body) = create_multipart_body(&audio_data, None); - + let request = Request::builder() .method(Method::POST) .uri("/tasks/transcribe") .header("content-type", content_type) .body(Body::from(body)) .unwrap(); - + let response = app.oneshot(request).await.unwrap(); (i, response.status()) }); - + handles.push(handle); } - + // Wait for all requests to complete let mut success_count = 0; for handle in handles { @@ -414,7 +445,7 @@ async fn test_concurrent_requests() { } println!("Request {}: {:?}", i, status); } - + // At least some requests should succeed assert!(success_count > 0); } @@ -426,12 +457,12 @@ async fn test_task_management_disabled() { let mut config_clone = (*config).clone(); config_clone.task_management.enabled = false; let config = Arc::new(config_clone); - + let app = routes::create_routes(config).await.unwrap(); - + let audio_data = create_test_audio_data(); let (content_type, body) = create_multipart_body(&audio_data, None); - + // Try to submit async task when task management is disabled let request = Request::builder() .method(Method::POST) @@ -439,13 +470,20 @@ async fn test_task_management_disabled() { .header("content-type", content_type) .body(Body::from(body)) .unwrap(); - + let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); - - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); let error_response: Value = serde_json::from_slice(&body).unwrap(); - + assert_eq!(error_response["success"], false); - assert!(error_response["error"].as_str().unwrap().contains("not enabled")); -} \ No newline at end of file + assert!( + error_response["error"] + .as_str() + .unwrap() + .contains("not enabled") + ); +}