From f037daf97f6184f6e7c5f61737f4b88080f0e957 Mon Sep 17 00:00:00 2001 From: zhangyangrui <740884666@qq.com> Date: Wed, 8 Jul 2026 09:18:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20model-based=20routing=20=E2=80=94=20wil?= =?UTF-8?q?dcard=20model-to-provider=20mapping=20in=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现基于模型名称的路由功能:通过通配符模式将不同的模型请求路由到指定的 provider。 核心功能: - model_routes 表 + 完整 CRUD DAO(含外键级联删除) - ModelRouter:通配符 * 转正则匹配,大小写不敏感,按优先级选择 - CLI:proxy model-route list|add|remove|toggle|update - TUI:Settings → Model Routes 页面 + Dashboard 多色路由命中图例 Bug 修复: - 模型路由命中不静默切换当前 provider(加 is_model_routed 标记) - 通配符模式加 ^ 锚定,防止前缀越界匹配 - TUI 编辑保留 disabled 状态 --- .gitignore | 2 +- .planning/PROJECT.md | 61 + .planning/codebase/ARCHITECTURE.md | 268 +++ .planning/codebase/CONCERNS.md | 166 ++ .planning/codebase/CONVENTIONS.md | 112 ++ .planning/codebase/INTEGRATIONS.md | 147 ++ .planning/codebase/STACK.md | 98 + .planning/codebase/STRUCTURE.md | 305 +++ .planning/codebase/TESTING.md | 210 +++ .planning/phase-1/RESEARCH.md | 243 +++ .planning/phase-2/RESEARCH.md | 193 ++ .planning/phase-3/RESEARCH.md | 29 + .planning/phase-4/RESEARCH.md | 42 + docs/tui-usability-roadmap.md | 91 +- src-tauri/.gitignore | 1 + src-tauri/src/cli/commands/proxy.rs | 634 ++++++- src-tauri/src/cli/commands/sessions.rs | 1 + src-tauri/src/cli/i18n.rs | 262 ++- .../src/cli/i18n/texts/config_actions.rs | 1141 ++++++++++++ src-tauri/src/cli/i18n/texts/core.rs | 1630 +++++++++++++++++ src-tauri/src/cli/i18n/texts/menu_skills.rs | 360 ++++ src-tauri/src/cli/i18n/texts/mod.rs | 21 + .../src/cli/i18n/texts/provider_editor.rs | 605 ++++++ .../src/cli/i18n/texts/provider_management.rs | 1058 +++++++++++ src-tauri/src/cli/i18n/texts/providers.rs | 1489 +++++++++++++++ src-tauri/src/cli/i18n/texts/settings_misc.rs | 912 +++++++++ src-tauri/src/cli/i18n/texts/toasts.rs | 1111 +++++++++++ src-tauri/src/cli/i18n/texts/update.rs | 164 ++ src-tauri/src/cli/tui/app/app_state.rs | 31 +- src-tauri/src/cli/tui/app/content_config.rs | 82 +- src-tauri/src/cli/tui/app/content_entities.rs | 170 +- src-tauri/src/cli/tui/app/helpers.rs | 17 +- src-tauri/src/cli/tui/app/menu.rs | 82 +- .../cli/tui/app/overlay_handlers/dialogs.rs | 98 + .../src/cli/tui/app/overlay_handlers/views.rs | 97 +- src-tauri/src/cli/tui/app/tests.rs | 98 +- src-tauri/src/cli/tui/app/types.rs | 32 + src-tauri/src/cli/tui/data.rs | 21 + src-tauri/src/cli/tui/help.rs | 72 +- src-tauri/src/cli/tui/icons.rs | 258 --- src-tauri/src/cli/tui/keymap.rs | 124 +- src-tauri/src/cli/tui/mod.rs | 5 +- src-tauri/src/cli/tui/route.rs | 1 + src-tauri/src/cli/tui/runtime_actions/mod.rs | 14 + .../cli/tui/runtime_actions/model_routes.rs | 133 ++ .../src/cli/tui/runtime_systems/types.rs | 1 + src-tauri/src/cli/tui/ui.rs | 6 +- src-tauri/src/cli/tui/ui/chrome.rs | 46 +- src-tauri/src/cli/tui/ui/config.rs | 272 +-- src-tauri/src/cli/tui/ui/main_page.rs | 611 +++++- src-tauri/src/cli/tui/ui/model_routes.rs | 91 + src-tauri/src/cli/tui/ui/overlay/basic.rs | 67 + src-tauri/src/cli/tui/ui/overlay/frame.rs | 2 +- src-tauri/src/cli/tui/ui/overlay/render.rs | 9 + src-tauri/src/cli/tui/ui/proxy_wave.rs | 4 +- src-tauri/src/cli/tui/ui/sessions.rs | 25 +- src-tauri/src/cli/tui/ui/shared.rs | 10 +- src-tauri/src/cli/tui/ui/tests.rs | 102 +- src-tauri/src/database/dao/mod.rs | 1 + src-tauri/src/database/dao/model_routes.rs | 473 +++++ src-tauri/src/database/mod.rs | 2 +- src-tauri/src/database/schema.rs | 127 ++ src-tauri/src/database/tests.rs | 514 +++++- src-tauri/src/deeplink/provider.rs | 61 +- src-tauri/src/lib.rs | 2 + src-tauri/src/model_route.rs | 53 + src-tauri/src/proxy/handler_context.rs | 399 +++- src-tauri/src/proxy/handlers.rs | 215 ++- src-tauri/src/proxy/mod.rs | 1 + src-tauri/src/proxy/model_mapper.rs | 96 +- src-tauri/src/proxy/model_router.rs | 636 +++++++ .../src/proxy/providers/transform_gemini.rs | 1 + src-tauri/src/proxy/response_handler.rs | 49 +- src-tauri/src/proxy/response_handler/tests.rs | 65 +- src-tauri/src/proxy/server.rs | 468 ++--- src-tauri/src/proxy/types.rs | 5 +- src-tauri/src/services/proxy.rs | 34 +- src-tauri/src/services/skill.rs | 2 + src-tauri/src/settings.rs | 17 - src-tauri/tests/deeplink_import.rs | 67 - target/rust-analyzer/flycheck0/stderr | 90 + target/rust-analyzer/flycheck0/stdout | 438 +++++ 82 files changed, 16311 insertions(+), 1442 deletions(-) create mode 100644 .planning/PROJECT.md create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md create mode 100644 .planning/phase-1/RESEARCH.md create mode 100644 .planning/phase-2/RESEARCH.md create mode 100644 .planning/phase-3/RESEARCH.md create mode 100644 .planning/phase-4/RESEARCH.md create mode 100644 src-tauri/src/cli/i18n/texts/config_actions.rs create mode 100644 src-tauri/src/cli/i18n/texts/core.rs create mode 100644 src-tauri/src/cli/i18n/texts/menu_skills.rs create mode 100644 src-tauri/src/cli/i18n/texts/mod.rs create mode 100644 src-tauri/src/cli/i18n/texts/provider_editor.rs create mode 100644 src-tauri/src/cli/i18n/texts/provider_management.rs create mode 100644 src-tauri/src/cli/i18n/texts/providers.rs create mode 100644 src-tauri/src/cli/i18n/texts/settings_misc.rs create mode 100644 src-tauri/src/cli/i18n/texts/toasts.rs create mode 100644 src-tauri/src/cli/i18n/texts/update.rs delete mode 100644 src-tauri/src/cli/tui/icons.rs create mode 100644 src-tauri/src/cli/tui/runtime_actions/model_routes.rs create mode 100644 src-tauri/src/cli/tui/ui/model_routes.rs create mode 100644 src-tauri/src/database/dao/model_routes.rs create mode 100644 src-tauri/src/model_route.rs create mode 100644 src-tauri/src/proxy/model_router.rs create mode 100644 target/rust-analyzer/flycheck0/stderr create mode 100644 target/rust-analyzer/flycheck0/stdout diff --git a/.gitignore b/.gitignore index 0b750440..789c18f2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ docs/superpowers/ .omx/ .superpowers/ skills-lock.json -/skills/ +skills/ diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 00000000..8f1f8fb7 --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,61 @@ +# CC-Switch CLI + +## What This Is + +CC-Switch CLI 是一个 Rust 命令行管理工具,面向使用多个 AI 编程助手(Claude Code、Codex、Gemini、OpenCode、Hermes、OpenClaw)的开发者。它统一管理 provider 配置、MCP 服务器、prompts、skills、WebDAV 同步、本地代理路由、故障转移和守护进程,让开发者在不同 AI 工具之间自由切换 provider 而无需手动修改各工具的配置文件。 + +## Core Value + +**一键切换 AI 编程工具的底层 provider,零配置摩擦。** 如果这做不到,其他功能都无意义。 + +## Requirements + +### Validated + +- ✓ Multi-app provider 切换(Claude/Codex/Gemini/OpenCode/Hermes/OpenClaw)— v5.x +- ✓ 本地 HTTP 代理(Axum),支持请求转发和 provider 路由 — v5.x +- ✓ SQLite 持久化存储,DAO 模式 — v5.x +- ✓ TUI 交互界面(ratatui)— v5.x +- ✓ 故障转移队列(failover)— v5.x +- ✓ WebDAV/S3 配置同步 — v5.x +- ✓ Unix 守护进程模式 — v5.x +- ✓ MCP 服务器管理 — v5.x +- ✓ Prompts/Skills 管理 — v5.x + +### Active + +- [ ] Per-model provider routing:根据请求的 model 名称(如 `*sonnet*`)将代理请求路由到不同的 provider + +### Out of Scope + +- 新增 AI 编程工具支持(目前 6 个已覆盖) +- 多用户/多租户支持 — 当前为单用户本地工具 +- Windows 服务集成 — 当前仅 Unix daemon + +## Context + +- **代码库规模**: Rust ~80K+ 行,SQLite 单文件数据库,ratatui TUI +- **Schema 版本**: 当前 v10(已支持 Hermes Agent) +- **上游参考**: [cc-switch PR #4081](https://github.com/farion1231/cc-switch/pull/4081) 已实现 per-model routing 的后端(Rust)+ 前端(React) +- **关键差异**: cc-switch-cli 无 React 前端,需用 ratatui TUI 重新实现管理界面;代理架构可能有细节差异需要适配 +- **CI**: GitHub Actions 运行 `cargo fmt --check`、单元测试、集成测试 +- **已知技术债务**: `services/proxy.rs` 7085 行单体文件、513 处 `unwrap()` 调用、DB 单 Mutex 瓶颈 + +## Constraints + +- **Tech stack**: Rust 2021 Edition,MSRV 1.91.1,tokio + axum + ratatui + rusqlite +- **Compatibility**: Schema v10→v11 升级路径必须平滑,向后兼容空路由表 +- **No frontend**: 无 React/Web 前端,所有管理 UI 通过 CLI 子命令或 TUI 实现 +- **Testing**: 需覆盖数据库迁移、路由匹配逻辑、代理集成 +- **PR quality**: 最终产出应是可直接合并的纯净 Git 分支(仅包含功能代码,无 .planning/) + +## Key Decisions + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| 参考 cc-switch PR #4081 作为上游设计 | PR 已经过 review,后端设计可复用 | — Pending | +| 使用 TUI 替代 React 前端 | cc-switch-cli 无前端,需用 ratatui 构建管理界面 | — Pending | +| Schema 升级到 v11 | 需要新 `model_routes` 表存储路由规则 | — Pending | + +--- +*Last updated: 2026-06-11 after milestone init* diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 00000000..8edf893b --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,268 @@ +# Architecture + +**Analysis Date:** 2026-06-12 + +## Pattern Overview + +**Overall:** Layered Rust CLI with SQLite-backed single source of truth, live app-config adapters, an async HTTP proxy subsystem, and a Unix supervisor daemon for managed proxy workers. + +**Key Characteristics:** +- `src-tauri/src/main.rs` is a thin binary entry point: parse `Cli`, initialize logging, run startup state recovery for most commands, then dispatch into `src-tauri/src/cli/commands/`. +- `src-tauri/src/store.rs` centralizes durable runtime state in `AppState`, combining `Database`, an in-memory `MultiAppConfig` snapshot, and `ProxyService`. +- `src-tauri/src/database/mod.rs` owns the SQLite connection and schema lifecycle; DAO methods under `src-tauri/src/database/dao/` are the persistence boundary. +- `src-tauri/src/services/` holds durable business logic shared by direct CLI commands and the ratatui TUI. +- `src-tauri/src/proxy/` runs an Axum-based multi-app proxy with request routing, provider failover, model-based routing, response transforms, usage logging, and provider-specific adapters. +- `src-tauri/src/daemon/` supervises proxy worker processes on Unix and exposes JSON IPC over a Unix domain socket. + +## Layers + +**Binary Entry Layer:** +- Purpose: Parse process arguments, set logging behavior, decide whether startup state is required, and dispatch commands. +- Location: `src-tauri/src/main.rs` +- Contains: `main()`, `run()`, `command_requires_startup_state()`, `initialize_startup_state_if_needed()` +- Depends on: `src-tauri/src/cli/mod.rs`, `src-tauri/src/store.rs`, `src-tauri/src/error.rs` +- Used by: The `cc-switch` binary target declared in `src-tauri/Cargo.toml` + +**CLI Command Layer:** +- Purpose: Define the Clap command surface and perform terminal command I/O. +- Location: `src-tauri/src/cli/` +- Contains: Top-level `Cli` and `Commands` in `src-tauri/src/cli/mod.rs`; command implementations in `src-tauri/src/cli/commands/` +- Depends on: `src-tauri/src/services/`, `src-tauri/src/store.rs`, `src-tauri/src/app_config.rs`, `src-tauri/src/provider.rs` +- Used by: `src-tauri/src/main.rs` and integration tests under `src-tauri/tests/` +- Pattern: Add a user-facing command by defining Clap shape in `src-tauri/src/cli/mod.rs` or a command module, implementing command I/O in `src-tauri/src/cli/commands/`, and moving reusable behavior into `src-tauri/src/services/`. + +**Interactive TUI Layer:** +- Purpose: Provide interactive provider, MCP, prompt, skill, proxy, usage, session, pricing, and config workflows. +- Location: `src-tauri/src/cli/tui/` +- Contains: Event loop in `src-tauri/src/cli/tui/mod.rs`, state in `src-tauri/src/cli/tui/app/`, rendering in `src-tauri/src/cli/tui/ui/`, forms in `src-tauri/src/cli/tui/form/`, runtime actions in `src-tauri/src/cli/tui/runtime_actions/`, background systems in `src-tauri/src/cli/tui/runtime_systems/` +- Depends on: `src-tauri/src/services/`, `src-tauri/src/database/`, `src-tauri/src/proxy/`, `src-tauri/src/cli/ui/` +- Used by: `cc-switch` with no subcommand or `cc-switch interactive` + +**Application State Layer:** +- Purpose: Coordinate database state, legacy migration, in-memory config snapshots, startup recovery, and proxy service construction. +- Location: `src-tauri/src/store.rs` +- Contains: `AppState`, `try_new()`, `try_new_with_startup_recovery()`, `try_open_snapshot()`, `save()`, live-provider import and recovery helpers +- Depends on: `src-tauri/src/database/`, `src-tauri/src/app_config.rs`, `src-tauri/src/services/proxy.rs`, live config adapters such as `src-tauri/src/codex_config.rs` +- Used by: Most CLI commands, TUI actions, startup initialization, integration tests + +**Service Layer:** +- Purpose: Hold business logic that should not be tied to terminal prompts or rendering. +- Location: `src-tauri/src/services/` +- Contains: `ProviderService`, `McpService`, `PromptService`, `SkillService`, `ProxyService`, `ConfigService`, auth services, usage services, WebDAV sync, environment checks, speed tests, stream checks +- Depends on: `src-tauri/src/database/`, live config adapter modules, external HTTP/auth libraries where needed +- Used by: `src-tauri/src/cli/commands/`, `src-tauri/src/cli/tui/runtime_actions/`, startup recovery in `src-tauri/src/store.rs` +- Pattern: Put shared command/TUI behavior here; keep direct user prompts and formatted output in `src-tauri/src/cli/`. + +**Persistence Layer:** +- Purpose: Persist providers, MCP servers, prompts, skills, settings, proxy state, failover queues, usage rollups, stream checks, model pricing, universal providers, and model routes. +- Location: `src-tauri/src/database/` +- Contains: `Database` in `src-tauri/src/database/mod.rs`, schema/migration in `src-tauri/src/database/schema.rs` and `src-tauri/src/database/migration.rs`, DAOs in `src-tauri/src/database/dao/` +- Depends on: `rusqlite`, `serde_json`, domain models such as `src-tauri/src/provider.rs` and `src-tauri/src/model_route.rs` +- Used by: `AppState`, services, proxy runtime, daemon, integration tests +- Pattern: Add new persisted entities as domain model plus DAO methods under `src-tauri/src/database/dao/`; update schema creation/migration in `src-tauri/src/database/schema.rs`. + +**Live Config Adapter Layer:** +- Purpose: Translate CC-Switch database/provider state to and from supported assistant app config files. +- Location: `src-tauri/src/*_config.rs`, `src-tauri/src/*_mcp.rs`, `src-tauri/src/prompt_files.rs` +- Contains: App-specific adapters including `src-tauri/src/codex_config.rs`, `src-tauri/src/gemini_config.rs`, `src-tauri/src/opencode_config.rs`, `src-tauri/src/openclaw_config.rs`, `src-tauri/src/hermes_config.rs`, `src-tauri/src/claude_mcp.rs`, `src-tauri/src/gemini_mcp.rs` +- Depends on: `src-tauri/src/config.rs`, `src-tauri/src/app_config.rs`, `src-tauri/src/provider.rs` +- Used by: Provider, MCP, prompt, proxy takeover, and startup recovery workflows + +**Proxy Runtime Layer:** +- Purpose: Accept local HTTP traffic, select providers, transform requests/responses, forward upstream calls, track usage, and maintain live proxy status. +- Location: `src-tauri/src/proxy/` +- Contains: Axum server in `src-tauri/src/proxy/server.rs`, route handlers in `src-tauri/src/proxy/handlers.rs`, per-request context in `src-tauri/src/proxy/handler_context.rs`, failover routing in `src-tauri/src/proxy/provider_router.rs`, model routing in `src-tauri/src/proxy/model_router.rs`, forwarding in `src-tauri/src/proxy/forwarder.rs`, adapters in `src-tauri/src/proxy/providers/` +- Depends on: `src-tauri/src/database/`, `src-tauri/src/provider.rs`, `src-tauri/src/model_route.rs`, `reqwest`, `axum`, `tokio` +- Used by: `ProxyService`, proxy CLI commands, daemon worker processes, proxy tests + +**Daemon Layer:** +- Purpose: Own and supervise proxy worker processes, align worker state with persisted proxy state, and expose foreground control over IPC. +- Location: `src-tauri/src/daemon/` +- Contains: `run()` and `notify_global_switch()` in `src-tauri/src/daemon/mod.rs`, supervisor in `src-tauri/src/daemon/supervisor.rs`, IPC protocol/client/server in `src-tauri/src/daemon/ipc/`, pidfile and path helpers +- Depends on: `src-tauri/src/database/`, `src-tauri/src/services/proxy.rs`, Unix sockets and Tokio +- Used by: Unix `daemon` and proxy-management commands + +**Library Command Layer:** +- Purpose: Expose non-Clap command helpers for embedded or app-like callers. +- Location: `src-tauri/src/commands/` +- Contains: OpenClaw workspace file and daily memory operations in `src-tauri/src/commands/workspace.rs` +- Depends on: `src-tauri/src/openclaw_config.rs`, `src-tauri/src/config.rs` +- Used by: Library callers and tests; not wired as normal Clap subcommands + +**Deep Link Layer:** +- Purpose: Parse and import `ccswitch://v1/import?...` resources. +- Location: `src-tauri/src/deeplink/` +- Contains: Request model in `src-tauri/src/deeplink/mod.rs`, URL parsing in `src-tauri/src/deeplink/parser.rs`, provider import in `src-tauri/src/deeplink/provider.rs` +- Depends on: `src-tauri/src/provider.rs`, `src-tauri/src/app_config.rs`, `serde_json` +- Used by: Public library exports from `src-tauri/src/lib.rs` and deep-link integration tests + +## Data Flow + +**Normal CLI Command Startup:** + +1. `src-tauri/src/main.rs` parses arguments into `Cli` from `src-tauri/src/cli/mod.rs`. +2. `command_requires_startup_state()` skips startup state for commands such as `update`, `auth`, `sessions`, `completions`, `internal`, and Unix `daemon`. +3. Most commands call `AppState::try_new_with_startup_recovery()` in `src-tauri/src/store.rs`. +4. `AppState` initializes `Database`, migrates legacy `config.json`/`skills.json` if needed, exports DB rows into `MultiAppConfig`, imports live provider configs where appropriate, recovers proxy takeovers, migrates Codex history buckets, and syncs session usage. +5. `run()` dispatches to a command module under `src-tauri/src/cli/commands/`. +6. Command modules call service-layer methods and print command-specific output. + +**Provider Switch:** + +1. Provider commands in `src-tauri/src/cli/commands/provider.rs` or TUI actions in `src-tauri/src/cli/tui/runtime_actions/providers.rs` collect a target provider and app. +2. `ProviderService` in `src-tauri/src/services/provider/` validates and persists current-provider state through `Database`. +3. Live config adapters such as `src-tauri/src/codex_config.rs`, `src-tauri/src/gemini_config.rs`, and `src-tauri/src/opencode_config.rs` write app-specific config files when that app is initialized and the workflow requires live sync. +4. Proxy/takeover paths coordinate with `ProxyService` in `src-tauri/src/services/proxy.rs` to update backup or takeover state. +5. `AppState::refresh_config_from_db()` keeps the in-memory `MultiAppConfig` snapshot aligned after DB changes. + +**Proxy Request Routing:** + +1. `ProxyServer` in `src-tauri/src/proxy/server.rs` routes HTTP endpoints to handlers in `src-tauri/src/proxy/handlers.rs`. +2. `HandlerContext::load()` in `src-tauri/src/proxy/handler_context.rs` records request start, extracts the requested `model`, reads app proxy and optimizer settings, and builds a provider candidate list. +3. `ModelRouter` in `src-tauri/src/proxy/model_router.rs` first checks enabled `ModelRoute` records from `src-tauri/src/model_route.rs`; a match selects a single route-targeted provider and records route hit count. +4. If no model route matches, `ProviderRouter` in `src-tauri/src/proxy/provider_router.rs` selects the current provider or the failover queue, respecting circuit-breaker state. +5. `RequestForwarder` in `src-tauri/src/proxy/forwarder.rs` sends the request to upstream providers through provider adapters under `src-tauri/src/proxy/providers/`. +6. Response helpers in `src-tauri/src/proxy/response.rs` and `src-tauri/src/proxy/response_handler.rs` build buffered, passthrough, transformed, SSE, or error responses. +7. Usage logging under `src-tauri/src/proxy/usage/` records request metadata and costs, and successful failover updates current provider state. + +**Daemon-Managed Proxy:** + +1. CLI daemon commands in `src-tauri/src/cli/commands/daemon.rs` call daemon entry points in `src-tauri/src/daemon/mod.rs`. +2. `daemon::run()` opens `Database`, starts periodic usage maintenance, creates `Supervisor`, runs startup recovery, binds the Unix IPC socket, and handles shutdown signals. +3. Foreground commands use `src-tauri/src/daemon/ipc/client.rs` to send requests defined in `src-tauri/src/daemon/ipc/protocol.rs`. +4. `src-tauri/src/daemon/supervisor.rs` starts, stops, restarts, and monitors proxy worker processes according to persisted desired state. +5. `ProxyService` stores runtime-session metadata so foreground/TUI processes can probe or recover managed proxy workers. + +**OpenClaw Workspace File Flow:** + +1. Callers use helper functions in `src-tauri/src/commands/workspace.rs`. +2. Filenames are restricted to `ALLOWED_FILES` or daily memory filename patterns. +3. The module validates workspace roots, rejects symlinks/path traversal, and reads/writes through helpers from `src-tauri/src/config.rs`. +4. Tests for this behavior live in `src-tauri/tests/workspace_commands.rs`. + +**State Management:** +- SQLite at `cc-switch.db` is the durable source of truth, accessed through `Database` in `src-tauri/src/database/mod.rs`. +- `MultiAppConfig` in `src-tauri/src/app_config.rs` is an in-memory snapshot used by command and service code; refresh it from DB after persistence changes. +- `settings.json` values accessed through `src-tauri/src/settings.rs` hold app settings and some current-provider/UI integration settings. +- Proxy runtime state uses async `RwLock` fields in `ProxyServerState` (`src-tauri/src/proxy/server.rs`) and per-database shared runtime state in `ProxyService` (`src-tauri/src/services/proxy.rs`). + +## Key Abstractions + +**`AppState`:** +- Purpose: Process-local coordination object for DB, in-memory config, and proxy service. +- Examples: `src-tauri/src/store.rs`, public export in `src-tauri/src/lib.rs` +- Pattern: Construct with `try_new_with_startup_recovery()` on normal process startup; use `try_open_snapshot()` for read-only TUI refresh paths. + +**`Database`:** +- Purpose: SQLite connection wrapper, schema migrator, backup manager, and DAO host. +- Examples: `src-tauri/src/database/mod.rs`, `src-tauri/src/database/schema.rs`, `src-tauri/src/database/dao/providers.rs`, `src-tauri/src/database/dao/model_routes.rs` +- Pattern: Keep direct SQL in DAO/schema modules; call `Database` methods from services and proxy code. + +**`MultiAppConfig` and `AppType`:** +- Purpose: Shared in-memory representation of supported apps and their provider/MCP/prompt/skill state. +- Examples: `src-tauri/src/app_config.rs`, `src-tauri/src/store.rs` +- Pattern: Use `AppType` labels (`claude`, `codex`, `gemini`, `opencode`, `hermes`, `openclaw`) at command boundaries and database rows. + +**`Provider` and Provider Services:** +- Purpose: Represent provider config, metadata, usage scripts, current-provider selection, import/export, and live sync. +- Examples: `src-tauri/src/provider.rs`, `src-tauri/src/services/provider/mod.rs`, `src-tauri/src/services/provider/live.rs`, `src-tauri/src/cli/commands/provider.rs` +- Pattern: Put provider CRUD/switch semantics in `ProviderService`; keep formatting and prompts in command/TUI modules. + +**`ModelRoute` and `ModelRouter`:** +- Purpose: Route proxy requests to provider IDs based on model-name patterns before normal provider/failover routing. +- Examples: `src-tauri/src/model_route.rs`, `src-tauri/src/proxy/model_router.rs`, `src-tauri/src/database/dao/model_routes.rs`, `src-tauri/src/proxy/handler_context.rs` +- Pattern: Routes are app-scoped, enabled/disabled, priority ordered, wildcard-enabled, and record hit counts asynchronously. + +**`ProxyService`:** +- Purpose: Manage proxy lifecycle, takeover mode, live config rewrites, runtime session persistence, hot switching, and daemon/foreground coordination. +- Examples: `src-tauri/src/services/proxy.rs`, command surface in `src-tauri/src/cli/commands/proxy.rs` +- Pattern: Use service methods for lifecycle and live-config mutation; `src-tauri/src/proxy/` owns HTTP request processing. + +**`ProxyServerState` and `HandlerContext`:** +- Purpose: Carry async proxy runtime state and per-request routing/timeout/config decisions. +- Examples: `src-tauri/src/proxy/server.rs`, `src-tauri/src/proxy/handler_context.rs` +- Pattern: `HandlerContext::load()` is the request setup boundary before forwarding logic. + +**Provider Adapters:** +- Purpose: Encapsulate upstream-specific auth, endpoint, schema, streaming, and transform behavior. +- Examples: `src-tauri/src/proxy/providers/adapter.rs`, `src-tauri/src/proxy/providers/claude.rs`, `src-tauri/src/proxy/providers/codex.rs`, `src-tauri/src/proxy/providers/gemini.rs` +- Pattern: Add provider-specific proxy behavior under `src-tauri/src/proxy/providers/`, not in generic handlers. + +**TUI Runtime Split:** +- Purpose: Keep interactive state, rendering, forms, actions, and background workers separate. +- Examples: `src-tauri/src/cli/tui/app/`, `src-tauri/src/cli/tui/ui/`, `src-tauri/src/cli/tui/form/`, `src-tauri/src/cli/tui/runtime_actions/`, `src-tauri/src/cli/tui/runtime_systems/` +- Pattern: UI modules render state; runtime action modules mutate state or call services; background systems handle async refresh/workers. + +## Entry Points + +**Binary Process:** +- Location: `src-tauri/src/main.rs` +- Triggers: Running `cc-switch` or `cargo run` from `src-tauri/` +- Responsibilities: Parse CLI, configure logging, initialize startup state when required, dispatch commands + +**Library Root:** +- Location: `src-tauri/src/lib.rs` +- Triggers: Integration tests and external Rust callers importing `cc_switch_lib` +- Responsibilities: Declare internal modules and re-export public types/services such as `AppState`, `Database`, `ProviderService`, `ProxyService`, `ModelRoute`, and deep-link helpers + +**CLI Shape:** +- Location: `src-tauri/src/cli/mod.rs` +- Triggers: Clap parsing from `src-tauri/src/main.rs` +- Responsibilities: Define global `--app`, verbose flag, subcommands, shell completion generation + +**Command Implementations:** +- Location: `src-tauri/src/cli/commands/` +- Triggers: Dispatch from `src-tauri/src/main.rs` +- Responsibilities: Implement terminal-facing command flows for providers, MCP, prompts, skills, config, proxy, settings, failover, sessions, Hermes, daemon, env, auth, update, completions, and internal commands + +**Interactive Mode:** +- Location: `src-tauri/src/cli/interactive/mod.rs`, `src-tauri/src/cli/tui/mod.rs` +- Triggers: No subcommand, `interactive`, or `ui` +- Responsibilities: Start the ratatui event loop and coordinate TUI state/actions/rendering + +**Proxy HTTP Server:** +- Location: `src-tauri/src/proxy/server.rs`, `src-tauri/src/proxy/handlers.rs` +- Triggers: Proxy service start or daemon-managed proxy worker +- Responsibilities: Bind Axum routes, serve health/status, handle Claude messages, Codex chat/responses, Gemini passthrough, forwarding, response transformation, and status tracking + +**Unix Daemon:** +- Location: `src-tauri/src/daemon/mod.rs` +- Triggers: `cc-switch daemon start` on Unix +- Responsibilities: Own pidfile/socket/logging, supervise worker processes, handle IPC requests, recover desired proxy state + +**Deep Link Import:** +- Location: `src-tauri/src/deeplink/` +- Triggers: Library callers/tests parsing `ccswitch://v1/import?...` +- Responsibilities: Parse URL query fields and import provider resources + +**OpenClaw Workspace Helpers:** +- Location: `src-tauri/src/commands/workspace.rs` +- Triggers: Library command callers/tests +- Responsibilities: Read/write allowlisted workspace files and daily memory files safely + +## Error Handling + +**Strategy:** Typed errors at subsystem boundaries, command-level propagation, and explicit proxy error-to-response conversion. + +**Patterns:** +- Use `AppError` from `src-tauri/src/error.rs` for application, database, config, and I/O errors outside the proxy HTTP boundary. +- Use `ProxyError` from `src-tauri/src/proxy/error.rs` inside proxy routing/forwarding code and convert to HTTP responses via `src-tauri/src/proxy/response_handler.rs`. +- Command handlers return `Result<(), AppError>` or command-specific `Result` values; `src-tauri/src/main.rs` prints `Error: ...` and exits with status 1. +- Startup recovery logs best-effort failures for non-fatal import/migration/sync work in `src-tauri/src/store.rs`. +- Daemon entry points return `Result<(), String>` in `src-tauri/src/daemon/mod.rs` because IPC/process supervision errors need user-readable messages. + +## Cross-Cutting Concerns + +**Logging:** `src-tauri/src/main.rs` initializes `env_logger` for normal commands; Unix daemon start uses its own file logger in `src-tauri/src/daemon/logging.rs`; proxy and startup paths use `log` macros. + +**Validation:** Clap validates command shapes in `src-tauri/src/cli/mod.rs`; workspace filenames and symlinks are validated in `src-tauri/src/commands/workspace.rs`; live config adapters validate app-specific file formats; database schema version gates are enforced in `src-tauri/src/database/mod.rs`. + +**Authentication:** Provider credentials live inside provider settings or managed account flows; auth services are in `src-tauri/src/services/auth.rs`, `src-tauri/src/services/codex_oauth.rs`, and `src-tauri/src/services/copilot_auth.rs`; proxy provider auth strategies are in `src-tauri/src/proxy/providers/auth.rs`. + +**Configuration Safety:** Tests and commands that touch live app config paths should use explicit env overrides (`CC_SWITCH_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `HOME`, `XDG_CONFIG_HOME`, `XDG_RUNTIME_DIR`, `XDG_STATE_HOME`) and helpers from `src-tauri/tests/support.rs` or `src-tauri/src/test_support.rs`. + +**Concurrency:** SQLite access is guarded by `Mutex` in `src-tauri/src/database/mod.rs`; proxy runtime state uses Tokio `RwLock` in `src-tauri/src/proxy/server.rs` and `src-tauri/src/services/proxy.rs`; model-route hit recording uses `tokio::task::spawn_blocking()` in `src-tauri/src/proxy/model_router.rs`. + +--- + +*Architecture analysis: 2026-06-12* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 00000000..0f29062a --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,166 @@ +# Codebase Concerns + +**Analysis Date:** 2026-06-12 + +## Tech Debt + +**Model route test migration is incomplete:** +- Issue: The model-route CLI tests still mix the old integer route ID API with the current UUID/string route ID API. `ModelRouteCommand::{Remove,Toggle,Update}` expects `String`, but several tests pass integers, and several tests use `route_id` without assigning it from `create_model_route()`. +- Files: `src-tauri/src/cli/commands/proxy.rs` +- Impact: The crate does not compile in the current worktree. `rtk cargo test --manifest-path src-tauri/Cargo.toml model_route_remove_deletes_by_id --no-run` fails with 18 errors, including undefined `route_id` at `src-tauri/src/cli/commands/proxy.rs:1104` and integer/string mismatches at `src-tauri/src/cli/commands/proxy.rs:1121`, `src-tauri/src/cli/commands/proxy.rs:1180`, `src-tauri/src/cli/commands/proxy.rs:1213`, `src-tauri/src/cli/commands/proxy.rs:1260`, `src-tauri/src/cli/commands/proxy.rs:1305`, and `src-tauri/src/cli/commands/proxy.rs:1334`. +- Fix approach: Capture the returned `ModelRoute` from `db.create_model_route()`, use `created.id.clone()` as the route ID, and keep update `provider_id` fields as `Option`. Re-run the focused test with `--no-run` before broader test execution. + +**Monolithic proxy service:** +- Issue: `src-tauri/src/services/proxy.rs` is 7,085 lines and combines proxy lifecycle management, app takeover, live config restore, runtime status assembly, process cleanup, startup recovery, and many unit tests. +- Files: `src-tauri/src/services/proxy.rs` +- Impact: Proxy changes have a high merge-conflict and regression surface. Related responsibilities are difficult to test independently, and behavior changes can accidentally cross live-config, daemon, and failover boundaries. +- Fix approach: Split durable domains into modules such as lifecycle, takeover, live config sync/restore, runtime status, and process supervision. Keep shared orchestration in `ProxyService`. + +**Large TUI and CLI modules:** +- Issue: Several single files carry too much UI state, rendering, or form logic: `src-tauri/src/cli/tui/data.rs` is 4,603 lines, `src-tauri/src/cli/commands/provider_input.rs` is 4,339 lines, and `src-tauri/src/cli/i18n.rs` is 11,384 lines. +- Files: `src-tauri/src/cli/tui/data.rs`, `src-tauri/src/cli/commands/provider_input.rs`, `src-tauri/src/cli/i18n.rs` +- Impact: Small feature work requires navigating large files with mixed concerns. Compile times and review load increase, and narrow UI behavior changes can be hard to isolate. +- Fix approach: Extract TUI data builders by route/domain, split provider input flows by provider/app family, and move i18n test blocks into dedicated test modules while preserving public text helper names. + +**Schema version compatibility is encoded as a special case:** +- Issue: `SCHEMA_VERSION` is `11`, while the initialization path explicitly allows database user version `12` as a compatible future/upstream version. +- Files: `src-tauri/src/database/mod.rs`, `src-tauri/src/database/schema.rs` +- Impact: The application can run with a DB version higher than its declared schema version, but only for the hardcoded `12` case. Future upstream schema drift can fail abruptly, and contributors may be unsure whether model-route changes belong to v11 or v12. +- Fix approach: Document the compatibility contract near `SCHEMA_VERSION`, add explicit migration tests for v11 and v12 compatibility, and avoid adding new schema behavior without a corresponding version strategy. + +## Known Bugs + +**Current worktree fails to compile:** +- Symptoms: `cargo test --no-run` fails before running tests because model-route test code has stale types and missing route ID variables. +- Files: `src-tauri/src/cli/commands/proxy.rs` +- Trigger: Run `rtk cargo test --manifest-path src-tauri/Cargo.toml model_route_remove_deletes_by_id --no-run` from the repository root. +- Workaround: None for the current worktree. Fix the test migration before treating the branch or PR as reviewable. + +**PR lookup is not directly discoverable from the proxied remote:** +- Symptoms: `rtk gh pr status` fails because the remote URL uses `https://gh-proxy.com/https://github.com/SaladDay/cc-switch-cli.git`; `rtk gh pr list --repo SaladDay/cc-switch-cli --head feat/model-based-routing --state all --json ...` returned `[]` during this audit. +- Files: `.git/config` +- Trigger: Use GitHub CLI repo inference on this checkout. +- Workaround: Pass an explicit `--repo` for GitHub operations and verify the actual upstream/fork target before making PR-state claims. + +## Security Considerations + +**Release profile aborts on panic:** +- Risk: Release builds use `panic = "abort"`, so any unrecovered panic terminates the process instead of unwinding a task. +- Files: `src-tauri/Cargo.toml` +- Current mitigation: Many database paths use `Result` and the `lock_conn!` macro instead of direct `unwrap()`, and request handlers generally propagate `ProxyError`. +- Recommendations: Keep panic-prone code out of proxy hot paths. Convert production `unwrap()`/`expect()` sites to structured errors before enabling new proxy features. + +**Environment variable mutation uses unsafe blocks:** +- Risk: `std::env::set_var()` and `std::env::remove_var()` are unsafe in current Rust because global process environment mutation can race with concurrent readers. +- Files: `src-tauri/src/main.rs`, `src-tauri/src/config.rs`, `src-tauri/src/cli/commands/completions.rs` +- Current mitigation: Most mutations are scoped to startup or temporary guard patterns. +- Recommendations: Keep env mutation outside concurrent runtime paths. Use explicit config structs for new behavior rather than adding more process-wide environment overrides. + +**Dynamic SQL construction requires continued discipline:** +- Risk: SQL fragments are built with `format!()` in usage statistics, backup export, and helper modules. Current usage appears to use hardcoded columns/table names and parameterized user values, but the pattern can become injectable if user-controlled identifiers are added later. +- Files: `src-tauri/src/services/usage_stats.rs`, `src-tauri/src/services/sql_helpers.rs`, `src-tauri/src/database/backup.rs`, `src-tauri/src/database/dao/model_routes.rs` +- Current mitigation: User-facing values are generally bound with `?` parameters or `rusqlite::params![]`. +- Recommendations: Keep dynamic SQL helper inputs as enums/constants, not raw strings. Prefer query builders or explicit match arms when adding filter/sort fields. + +## Performance Bottlenecks + +**Model route matching does database reads and regex compilation per request:** +- Problem: `ModelRouter::match_route()` calls `db.list_model_routes(app_type)` for every request, then compiles each route pattern into a `Regex` inside the loop. +- Files: `src-tauri/src/proxy/model_router.rs`, `src-tauri/src/proxy/handler_context.rs` +- Cause: Routes are stored as raw patterns and there is no cache for enabled route lists or compiled regexes. +- Improvement path: Cache enabled routes per app with compiled regexes and invalidate on model-route CRUD operations. Keep the database as source of truth but avoid per-request full-list loads. + +**Route hit tracking can amplify database contention:** +- Problem: A matched route spawns a blocking task that calls `record_model_route_hit()` for each hit, incrementing `hit_count` and updating `last_hit_at`. +- Files: `src-tauri/src/proxy/model_router.rs`, `src-tauri/src/database/dao/model_routes.rs`, `src-tauri/src/database/mod.rs` +- Cause: `Database` wraps a single `rusqlite::Connection` in `Mutex`, so request logging, route hit writes, provider state reads, and config operations contend on one connection. +- Improvement path: Batch hit counters in memory and flush periodically, or use a separate write queue for telemetry updates. Avoid spawning one blocking task per hot-path match under high traffic. + +**Large inline test files increase compile cost:** +- Problem: TUI tests are concentrated in very large files: `src-tauri/src/cli/tui/app/tests.rs` is 15,016 lines and `src-tauri/src/cli/tui/ui/tests.rs` is 9,331 lines. +- Files: `src-tauri/src/cli/tui/app/tests.rs`, `src-tauri/src/cli/tui/ui/tests.rs` +- Cause: Route, form, overlay, rendering, and settings tests are grouped by broad module rather than feature area. +- Improvement path: Split tests by domain and keep shared fixtures in support modules. Preserve existing serial filesystem isolation patterns when moving tests. + +## Fragile Areas + +**Model-routed providers interact with current-provider sync:** +- Files: `src-tauri/src/proxy/handler_context.rs`, `src-tauri/src/proxy/server.rs`, `src-tauri/src/proxy/response_handler.rs`, `src-tauri/src/proxy/model_router.rs` +- Why fragile: `HandlerContext::load()` can select a single provider from a model route, while successful proxy responses later call provider-selection sync paths. If model-routed providers are treated like failover-selected providers, a request can accidentally change the global current provider. +- Safe modification: Preserve tests around `route_source`, `current_provider_id_at_start`, and model-route bypass behavior. Add a regression test that a model-route hit does not mutate DB/settings current provider unless that is explicitly intended. +- Test coverage: Current coverage exists in `src-tauri/src/proxy/handler_context.rs`, but the active worktree does not compile because of `src-tauri/src/cli/commands/proxy.rs` tests. + +**Live config and host config safety:** +- Files: `src-tauri/src/store.rs`, `src-tauri/src/services/proxy.rs`, `src-tauri/src/config.rs`, `src-tauri/tests/support.rs`, `src-tauri/src/test_support.rs` +- Why fragile: Startup recovery, proxy takeover, and live config sync can read or write real app configuration unless tests isolate `HOME`, `CC_SWITCH_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`, and `CODEX_HOME`. +- Safe modification: Always use `src-tauri/tests/support.rs` or `src-tauri/src/test_support.rs` helpers for tests touching app config directories. Never run exploratory product commands without explicit temporary config overrides. +- Test coverage: Many integration tests use temp homes, but new CLI or proxy tests need to follow the same pattern. + +**SQLite migration and compatibility paths:** +- Files: `src-tauri/src/database/mod.rs`, `src-tauri/src/database/schema.rs`, `src-tauri/src/database/tests.rs` +- Why fragile: `create_tables_on_conn()` creates current tables, migrations mutate old schemas in place, and a special compatible future-version path skips migrations for user version `12`. +- Safe modification: Add targeted migration tests for every schema change. Keep `SCHEMA_VERSION`, migration function names, `PRAGMA user_version`, and compatibility exceptions aligned. +- Test coverage: Existing database tests cover model-route CRUD and migration cases, but current compile failure prevents validating them. + +## Scaling Limits + +**Single SQLite connection limits concurrent proxy throughput:** +- Current capacity: One `rusqlite::Connection` protected by `Mutex` with WAL and a 5-second busy timeout. +- Limit: Concurrent proxy requests serialize whenever they touch the database, including provider lookup, proxy config reads, usage logging, route-hit updates, and health/circuit state changes. +- Scaling path: Introduce a read connection pool or dedicated read-only snapshot path for hot reads, and push telemetry writes through a bounded async queue. + +**Per-request route list scans scale with number of routes:** +- Current capacity: Every model-routed request scans all routes for the app in priority order. +- Limit: Matching cost grows linearly with configured model routes and repeats regex compilation work. +- Scaling path: Precompile enabled route patterns and update the cache from model-route CRUD operations. + +## Dependencies at Risk + +**No dependency vulnerability scan is detected in CI:** +- Risk: `.github/workflows/` contains `benchmark.yml`, `release.yml`, and `rust-ci.yml`, but no detected `cargo audit`, `cargo deny`, advisory, or RustSec check. +- Impact: Vulnerable transitive dependencies can enter releases unnoticed. +- Migration plan: Add `cargo audit` or `cargo deny check advisories` to `rust-ci.yml` and make release workflow depend on it. + +**Bundled SQLite requires application releases for SQLite fixes:** +- Risk: `rusqlite` uses the `bundled` feature. +- Impact: SQLite security or correctness fixes require dependency updates and a new cc-switch release rather than relying on system SQLite. +- Migration plan: Keep `rusqlite` current and include it in dependency audit/release checks. + +**Embedded JavaScript engine increases review surface:** +- Risk: `rquickjs` brings a JavaScript runtime into the binary for usage-script behavior. +- Impact: Runtime script execution features have a larger security and maintenance surface than pure Rust parsing/configuration. +- Migration plan: Keep script inputs constrained and documented. Prefer declarative config for new usage features unless JS execution is required. + +## Missing Critical Features + +**No enforced PR-ready verification gate in this checkout:** +- Problem: Current branch has compile errors, and GitHub CLI cannot infer PR state from the proxied remote. No current PR was found for `feat/model-based-routing` under `SaladDay/cc-switch-cli`. +- Blocks: Treating the pushed PR as ready for review or merge. + +**No model-route hot-path performance guardrails:** +- Problem: There are functional tests for matching patterns, but no benchmark or stress test for many routes or high request volume. +- Blocks: Confidently scaling model-route routing beyond small user-configured rule sets. + +## Test Coverage Gaps + +**Model-route CLI tests are stale and currently block compilation:** +- What's not tested: Remove, toggle, and update behavior with UUID/string route IDs. +- Files: `src-tauri/src/cli/commands/proxy.rs` +- Risk: The CLI surface can regress independently of DAO and proxy matching tests. +- Priority: High + +**Model-route current-provider sync regression needs explicit coverage:** +- What's not tested: A full handler/response path proving that a successful model-route request does not mutate global current provider when the routed provider differs from the request-start current provider. +- Files: `src-tauri/src/proxy/handler_context.rs`, `src-tauri/src/proxy/server.rs`, `src-tauri/src/proxy/response_handler.rs` +- Risk: Route-specific providers can leak into global app state. +- Priority: High + +**Remote PR state is not verified by local automation:** +- What's not tested: Whether the local branch has a matching upstream PR, and whether pushed PR checks pass. +- Files: `.git/config`, `.github/workflows/rust-ci.yml` +- Risk: Local work can be mistaken for a pushed/reviewable PR. +- Priority: Medium + +--- + +*Concerns audit: 2026-06-12* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 00000000..932208fe --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,112 @@ +# Coding Conventions + +**Analysis Date:** 2026-06-12 + +## Naming Patterns + +**Files:** +- Use Rust snake_case module filenames under `src-tauri/src/`, for example `src-tauri/src/model_route.rs`, `src-tauri/src/usage_events.rs`, and `src-tauri/src/proxy/model_router.rs`. +- Use `mod.rs` to anchor larger module directories such as `src-tauri/src/cli/mod.rs`, `src-tauri/src/services/provider/mod.rs`, `src-tauri/src/proxy/mod.rs`, and `src-tauri/src/database/mod.rs`. +- Split large subsystems into named submodules under domain directories: CLI command modules in `src-tauri/src/cli/commands/*.rs`, TUI runtime handlers in `src-tauri/src/cli/tui/runtime_actions/*.rs`, proxy adapters in `src-tauri/src/proxy/providers/*.rs`, and DAOs in `src-tauri/src/database/dao/*.rs`. +- Integration test filenames describe the command or subsystem under test: `src-tauri/tests/provider_commands.rs`, `src-tauri/tests/mcp_commands.rs`, `src-tauri/tests/proxy_daemon.rs`, and nested proxy suites such as `src-tauri/tests/proxy_claude_openai_chat/transform_cases.rs`. + +**Functions:** +- Use snake_case with behavior-first names: `command_requires_startup_state` in `src-tauri/src/main.rs`, `generate_provider_key` in `src-tauri/src/services/provider/mod.rs`, and `set_claude_proxy_port_to_ephemeral` in `src-tauri/tests/proxy_claude_streaming/helpers.rs`. +- Prefix small test fixture builders with the object they create, for example `usage_script_fixture` and `saved_provider` in `src-tauri/tests/provider_commands.rs`, and `claude_gemini_native_provider` in `src-tauri/src/services/stream_check/tests.rs`. +- Use `execute` as the standard top-level command entry function in command modules called from `src-tauri/src/main.rs`, for example `cc_switch_lib::cli::commands::provider::execute` and `cc_switch_lib::cli::commands::proxy::execute`. + +**Variables:** +- Use snake_case locals and explicit domain names: `listen_port`, `takeovers`, `provider_id`, `app_type`, `request_timeout`, and `bypass_circuit_breaker` appear across `src-tauri/src/cli/mod.rs`, `src-tauri/src/services/provider/mod.rs`, and `src-tauri/src/proxy/forwarder/tests/request_building.rs`. +- Prefer meaningful temporary names in tests: `state`, `provider`, `meta`, `script`, `response`, `sent`, and `err` in `src-tauri/tests/provider_commands.rs` and `src-tauri/src/proxy/forwarder/tests/request_building.rs`. +- Use guard variables prefixed with `_guard` or `_lock` when the value is held for RAII side effects, as in `src-tauri/tests/provider_commands.rs`, `src-tauri/src/main.rs`, and `src-tauri/src/database/tests.rs`. + +**Types:** +- Use PascalCase for structs, enums, and guards: `Cli`, `Commands`, `AppError`, `ProviderService`, `PostCommitAction`, `ConfigDirEnvGuard`, and `TestEnvGuard`. +- Use service structs as stateless namespaces for durable business behavior, for example `ProviderService` in `src-tauri/src/services/provider/mod.rs` and `StreamCheckService` in `src-tauri/src/services/stream_check/service.rs`. +- Use enum variants for command trees and structured errors: `Commands::Provider`, `Commands::Proxy`, and `AppError::Localized` in `src-tauri/src/cli/mod.rs` and `src-tauri/src/error.rs`. + +## Code Style + +**Formatting:** +- Use `rustfmt` from the pinned toolchain in `src-tauri/rust-toolchain.toml`. +- Run formatting from `src-tauri/` with: +```bash +cargo fmt +cargo fmt --check +``` +- CI enforces `cargo fmt --check` for `src-tauri/**` and workflow changes in `.github/workflows/rust-ci.yml`. +- No repository-level `rustfmt.toml` or `.rustfmt.toml` is detected, so use default Rust formatting. + +**Linting:** +- `clippy` is part of the pinned toolchain in `src-tauri/rust-toolchain.toml`, and documented local usage is: +```bash +cargo clippy +``` +- No `clippy.toml` is detected; follow standard Clippy guidance and local patterns. +- CI currently runs format and selected tests in `.github/workflows/rust-ci.yml`; `cargo clippy` is documented in `AGENTS.md`, `CLAUDE.md`, and `README.md` as a local quality gate. + +## Import Organization + +**Order:** +1. Standard library imports first, usually grouped by module path, as in `src-tauri/src/main.rs` and `src-tauri/tests/provider_commands.rs`. +2. External crates next, such as `clap`, `serde_json`, `serial_test`, `tokio`, `axum`, `rusqlite`, and `tempfile`. +3. Crate-local imports after external crates, using `crate::...` inside the library and `cc_switch_lib::...` in integration tests. +4. Local test support imports after `#[path = "support.rs"] mod support;`, as in `src-tauri/tests/provider_commands.rs`. + +**Path Aliases:** +- Library code uses `crate::...` for internal modules, for example `crate::app_config::AppType` and `crate::error::AppError` in `src-tauri/src/services/provider/mod.rs`. +- Integration tests import public API through `cc_switch_lib::...`, for example `cc_switch_lib::{AppState, Database, ProviderService}` in `src-tauri/tests/support.rs`. +- Tests use `#[path = "support.rs"] mod support;` for shared integration-test helpers in `src-tauri/tests/provider_commands.rs`. + +## Error Handling + +**Patterns:** +- Use the central `AppError` enum in `src-tauri/src/error.rs` for product errors that cross CLI, service, database, and config boundaries. +- Return `Result<(), AppError>` or `Result` from command and service code; `src-tauri/src/main.rs` prints `Error: {}` and exits with status 1 at the binary boundary. +- Prefer typed error variants with context over raw strings: `AppError::Io`, `AppError::Json`, `AppError::Toml`, `AppError::Database`, and `AppError::Localized` in `src-tauri/src/error.rs`. +- Use `AppError::localized(key, zh, en)` for user-visible validation or policy errors that need stable localization keys, for example `provider.key.invalid` in `src-tauri/src/services/provider/mod.rs`. +- Use `expect` freely in tests for setup failures with clear messages, for example `expect("create temp dir")`, `expect("seed database from config")`, and `expect("provider command should succeed")`. +- Use `matches!` for structured error assertions, as in `src-tauri/tests/proxy_database.rs` when checking `AppError::Localized` keys. + +## Logging + +**Framework:** `env_logger` plus `log` + +**Patterns:** +- Initialize logging in `src-tauri/src/main.rs` with `env_logger::Builder::from_env`; default CLI logging is `error`, and `--verbose` switches to `debug`. +- Commands with their own logger opt out through `command_uses_own_logger` in `src-tauri/src/main.rs`; Unix daemon start is the visible special case. +- Tests typically assert behavior directly instead of checking logs. Proxy helpers capture request bodies, headers, and database log rows through helpers such as `request_log_insert_lines` in `src-tauri/tests/proxy_claude_openai_chat/helpers.rs`. +- Use `eprintln!` sparingly in test cleanup paths where cleanup failure should be visible but should not mask the test result, as in `src-tauri/tests/support.rs`. + +## Comments + +**When to Comment:** +- Keep comments short and explanatory for operational constraints, startup behavior, or migration snapshots. +- Chinese comments are present and acceptable in existing source, especially around test isolation and user-facing configuration behavior, for example `src-tauri/tests/support.rs`, `src-tauri/src/main.rs`, and `src-tauri/src/database/tests.rs`. +- Use module-level comments for test modules where they describe the suite purpose, as in `src-tauri/src/database/tests.rs`. + +**JSDoc/TSDoc:** +- Not applicable; this is a Rust crate. +- Use Rust doc comments only where a public API or module needs documentation. Most internal helpers use ordinary comments or self-descriptive names. + +## Function Design + +**Size:** Keep command dispatch and command-shape code thin; move reusable behavior into services or helpers. `src-tauri/src/main.rs` dispatches to command modules, while durable behavior lives in `src-tauri/src/services/`, `src-tauri/src/database/`, and `src-tauri/src/proxy/`. + +**Parameters:** Prefer explicit typed parameters over loosely structured maps for command/service boundaries. Examples include `ProviderCommand` in `src-tauri/src/cli/commands/provider.rs`, `AppType` arguments in `src-tauri/src/main.rs`, and `ForwardOptions` in `src-tauri/src/proxy/forwarder/tests/request_building.rs`. + +**Return Values:** Use `Result` for fallible product paths and direct values for pure helpers. Tests can return `Result<(), AppError>` when `?` improves readability, as in `src-tauri/tests/proxy_database.rs`. + +## Module Design + +**Exports:** Re-export public API from `src-tauri/src/lib.rs` for integration tests and command code. Keep most subsystem internals private or `pub(crate)` unless they are intentionally used across modules. + +**Barrel Files:** Use Rust module barrels through `mod.rs` files. Examples include `src-tauri/src/cli/mod.rs`, `src-tauri/src/services/mod.rs`, `src-tauri/src/services/provider/mod.rs`, `src-tauri/src/proxy/mod.rs`, and `src-tauri/src/database/dao/mod.rs`. + +**Command Modules:** Add user-facing CLI shape in `src-tauri/src/cli/mod.rs` or a relevant file under `src-tauri/src/cli/commands/`, implement command I/O in that command module, and put shared behavior in `src-tauri/src/services/`. + +**Test-Only Modules:** Gate module-local test support with `#[cfg(test)]`, as in `src-tauri/src/services/provider/mod.rs`, `src-tauri/src/database/tests.rs`, and `src-tauri/src/proxy/forwarder/tests/mod.rs`. + +--- + +*Convention analysis: 2026-06-12* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 00000000..aa93d241 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,147 @@ +# External Integrations + +**Analysis Date:** 2026-06-12 + +## APIs & External Services + +**AI Provider APIs:** +- Anthropic/Claude API - Claude provider routing, stream checks, proxy forwarding, and quota checks. + - SDK/Client: `reqwest` through `src-tauri/src/proxy/http_client.rs`, adapters in `src-tauri/src/proxy/providers/claude.rs`, and quota code in `src-tauri/src/services/subscription.rs`. + - Auth: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, Claude OAuth credential files/keychain. +- OpenAI-compatible/Codex APIs - Codex, OpenCode, Hermes, OpenClaw, OpenAI Responses, and OpenAI Chat Completions compatible routing. + - SDK/Client: `reqwest`; adapter code in `src-tauri/src/proxy/providers/codex.rs`, transform code in `src-tauri/src/proxy/providers/transform_responses.rs`, and live Codex config code in `src-tauri/src/codex_config.rs`. + - Auth: `OPENAI_API_KEY`, Codex `auth.json`, provider TOML config, or managed Codex OAuth. +- Google Gemini API - Gemini provider routing and Claude-to-Gemini transformation. + - SDK/Client: `reqwest`; adapter and transform code in `src-tauri/src/proxy/providers/gemini.rs`, `src-tauri/src/proxy/providers/transform_gemini.rs`, and `src-tauri/src/gemini_config.rs`. + - Auth: `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, or OAuth token (`ya29.*`) written through Gemini settings. +- OpenRouter and OpenAI-compatible relay providers - user-configured provider endpoints and shipped templates. + - SDK/Client: generic `reqwest` forwarding in `src-tauri/src/proxy/forwarder.rs` and provider templates in `src-tauri/src/cli/tui/form/provider_templates.rs`. + - Auth: provider API keys stored in SQLite-backed provider settings and live app configs. + +**Managed OAuth Services:** +- OpenAI/Codex OAuth - managed ChatGPT/Codex login for Codex OAuth providers. + - SDK/Client: `reqwest` in `src-tauri/src/proxy/providers/codex_oauth_auth.rs` and wrapper service `src-tauri/src/services/codex_oauth.rs`. + - Auth: OpenAI device flow endpoints `https://auth.openai.com/api/accounts/deviceauth/usercode`, `https://auth.openai.com/api/accounts/deviceauth/token`, `https://auth.openai.com/oauth/token`; credentials persist to `codex_oauth_auth.json` under the CC-Switch config dir. +- GitHub Copilot OAuth - GitHub device flow plus Copilot token exchange. + - SDK/Client: `reqwest` in `src-tauri/src/proxy/providers/copilot_auth.rs` and wrapper service `src-tauri/src/services/copilot_auth.rs`. + - Auth: GitHub device flow at `https://github.com/login/device/code`, token exchange at `https://github.com/login/oauth/access_token`, Copilot token at `https://api.github.com/copilot_internal/v2/token`, Copilot API at `https://api.githubcopilot.com`; GHES domains are also supported. + +**Sync & Distribution:** +- WebDAV - cross-device sync for SQLite state and skills. + - SDK/Client: `reqwest` in `src-tauri/src/services/webdav.rs`; archive protocol in `src-tauri/src/services/webdav_sync/mod.rs`. + - Auth: basic auth username/password stored in `WebDavSyncSettings` in `src-tauri/src/settings.rs`; preset URL for Jianguoyun is `https://dav.jianguoyun.com/dav`. +- GitHub Releases - installer, self-update, release assets, checksums, and `latest.json` update manifest. + - SDK/Client: `reqwest` updater in `src-tauri/src/cli/commands/update.rs`; release publishing in `.github/workflows/release.yml`. + - Auth: GitHub Actions `GITHUB_TOKEN`; updater asset signing uses `CC_SWITCH_MINISIGN_SECRET_KEY` in `.github/workflows/release.yml`. + +**Local Assistant Apps:** +- Claude Code - live config sync, prompts, MCP, OAuth quota reading, proxy takeover. + - SDK/Client: filesystem adapters in `src-tauri/src/config.rs`, `src-tauri/src/claude_*`, `src-tauri/src/services/provider/claude.rs`, and `src-tauri/src/services/proxy.rs`. + - Auth: `~/.claude/settings.json`, `~/.claude.json`, Claude credentials file/keychain. +- Codex CLI - live config sync, MCP, auth, model catalog, prompt file, proxy takeover. + - SDK/Client: `src-tauri/src/codex_config.rs`, `src-tauri/src/services/provider/codex.rs`, and `src-tauri/src/services/proxy/codex_toml.rs`. + - Auth: `~/.codex/auth.json`, `~/.codex/config.toml`, or managed Codex OAuth. +- Gemini CLI, OpenCode, Hermes, OpenClaw - app-specific live config adapters. + - SDK/Client: `src-tauri/src/gemini_config.rs`, `src-tauri/src/opencode_config.rs`, `src-tauri/src/hermes_config.rs`, and `src-tauri/src/openclaw_config.rs`. + - Auth: app-specific live config files and provider settings persisted by CC-Switch. + +## Data Storage + +**Databases:** +- SQLite via `rusqlite`. + - Connection: `CC_SWITCH_CONFIG_DIR/cc-switch.db`, defaulting to `$HOME/.cc-switch/cc-switch.db`; configured in `src-tauri/src/config.rs` and opened in `src-tauri/src/database/mod.rs`. + - Client: `Database` wrapper in `src-tauri/src/database/mod.rs` with DAO modules under `src-tauri/src/database/dao/`. + - Tables include providers, provider endpoints, MCP servers, prompts, skills, settings, proxy config, provider health, proxy request logs, model pricing, stream checks, proxy live backup, usage rollups, session log sync, and model routes in `src-tauri/src/database/schema.rs`. + +**File Storage:** +- CC-Switch config root: `$HOME/.cc-switch` or `CC_SWITCH_CONFIG_DIR`, containing `cc-switch.db`, `settings.json`, `skills/`, backups, `codex_oauth_auth.json`, and `copilot_auth.json`. +- Live config files: + - Claude: `~/.claude/settings.json`, `~/.claude.json`, `~/.claude/CLAUDE.md`. + - Codex: `~/.codex/auth.json`, `~/.codex/config.toml`, `~/.codex/AGENTS.md`, `~/.codex/models_cache.json`. + - Gemini: `~/.gemini/.env`, `~/.gemini/settings.json`, `~/.gemini/GEMINI.md`. + - OpenCode: `~/.config/opencode/opencode.json`, `~/.config/opencode/AGENTS.md`. + - Hermes: config path resolved by `src-tauri/src/hermes_config.rs`. + - OpenClaw: `~/.openclaw/openclaw.json`, `~/.openclaw/AGENTS.md`. +- WebDAV sync artifacts: SQL dump, skills zip, and manifest generated by `src-tauri/src/services/webdav_sync/mod.rs`. + +**Caching:** +- In-process caches for Codex OAuth access tokens in `src-tauri/src/proxy/providers/codex_oauth_auth.rs`. +- In-process caches for GitHub Copilot tokens, models, and API endpoints in `src-tauri/src/proxy/providers/copilot_auth.rs`. +- Codex model catalog cache at `~/.codex/models_cache.json`, read by `src-tauri/src/codex_config.rs`. +- Proxy runtime state and request counters in `ProxyServerState` in `src-tauri/src/proxy/server.rs`. + +## Authentication & Identity + +**Auth Provider:** +- Custom per-provider auth is the default model; provider credentials live in SQLite-backed `Provider.settings_config` and are exported to live assistant config files by service code under `src-tauri/src/services/provider/`. + - Implementation: provider adapters choose auth strategies in `src-tauri/src/proxy/providers/auth.rs` and adapter files under `src-tauri/src/proxy/providers/`. +- OpenAI/Codex OAuth is managed in `src-tauri/src/proxy/providers/codex_oauth_auth.rs`. + - Implementation: device code flow, refresh token storage, access-token cache, multi-account selection. +- GitHub Copilot OAuth is managed in `src-tauri/src/proxy/providers/copilot_auth.rs`. + - Implementation: GitHub device code flow, GitHub token storage, Copilot token exchange, GHES domain support. +- Claude and Codex quota checks can read macOS Keychain when available and fallback to local credential files; see `src-tauri/src/services/subscription.rs`. + +## Monitoring & Observability + +**Error Tracking:** +- None external. Errors are represented through local `AppError` in `src-tauri/src/error.rs` and proxy-specific errors in `src-tauri/src/proxy/error.rs`. + +**Logs:** +- Rust `log` facade with `env_logger` declared in `src-tauri/Cargo.toml`. +- Proxy request and usage logs persist to SQLite tables through `src-tauri/src/proxy/usage/logger.rs` and DAO code under `src-tauri/src/database/dao/`. +- Daemon logging lives in `src-tauri/src/daemon/logging.rs`. +- Stream check results persist through `src-tauri/src/services/stream_check/` and `src-tauri/src/database/dao/stream_check.rs`. +- CI uploads benchmark artifacts from `.github/workflows/benchmark.yml` and `.github/workflows/release.yml`. + +## CI/CD & Deployment + +**Hosting:** +- Source and releases target GitHub. `src-tauri/Cargo.toml` declares repository `https://github.com/saladday/cc-switch-cli`; `README.md` and `install.sh` point to GitHub release downloads. +- Current checkout remote is proxied through `https://gh-proxy.com/https://github.com/SaladDay/cc-switch-cli.git`; `gh pr status` cannot infer a known GitHub host from that remote, so PR status lookup requires an explicit GitHub repo target. + +**CI Pipeline:** +- GitHub Actions: + - `.github/workflows/rust-ci.yml` runs `cargo fmt --check`, library unit tests, binary unit tests, and focused integration tests in sandboxed config directories. + - `.github/workflows/benchmark.yml` builds the release binary and runs blocking operation benchmarks. + - `.github/workflows/release.yml` builds multi-platform artifacts, creates macOS universal binary, signs update assets with Minisign, generates `latest.json`, computes `checksums.txt`, and publishes a GitHub Release with `softprops/action-gh-release@v2`. +- Nix package pipeline is defined in `flake.nix`; tests are disabled there because packaging should not depend on host assistant CLIs or live config fixtures. + +## Environment Configuration + +**Required env vars:** +- `CC_SWITCH_CONFIG_DIR` - optional override for CC-Switch state; required in tests/CI to avoid host config writes. +- `CLAUDE_CONFIG_DIR` - optional override for Claude config; required in tests/CI to avoid host config writes. +- `CODEX_HOME` - optional override for Codex config; required in tests/CI to avoid host config writes. +- `HOME` and `USERPROFILE` - used for default config roots and sandboxed in CI/tests. +- `XDG_CONFIG_HOME`, `XDG_RUNTIME_DIR`, `XDG_STATE_HOME` - used in tests and app config discovery. +- `CC_SWITCH_MINISIGN_SECRET_KEY` - GitHub Actions release signing secret. +- `GITHUB_TOKEN` - GitHub Actions release publishing token. + +**Secrets location:** +- User/provider secrets may be stored in `cc-switch.db`, `settings.json`, app live config files, `codex_oauth_auth.json`, and `copilot_auth.json` under the configured CC-Switch/app config roots. These files must not be read or quoted in codebase maps. +- WebDAV credentials are stored in `WebDavSyncSettings` through `src-tauri/src/settings.rs`. +- Release signing secret is stored only in GitHub Actions secrets and referenced as `CC_SWITCH_MINISIGN_SECRET_KEY` in `.github/workflows/release.yml`. + +## Webhooks & Callbacks + +**Incoming:** +- Local proxy routes in `src-tauri/src/proxy/handlers.rs`: + - `GET /health` and `GET /status`. + - `POST /v1/messages` for Anthropic/Claude messages. + - `POST /v1/messages/count_tokens` for Claude token counting. + - `POST /chat/completions` for OpenAI-compatible chat completions. + - `POST /responses` and `POST /responses/compact` for OpenAI Responses API. + - Gemini passthrough routes under `/gemini`. +- Daemon IPC is local Unix supervisor/socket logic under `src-tauri/src/daemon/ipc/`. +- Deep-link import protocol `ccswitch://v1/import?...` is parsed under `src-tauri/src/deeplink/`. + +**Outgoing:** +- Provider API calls to Anthropic, OpenAI-compatible providers, Google Gemini, OpenRouter/relay endpoints, ChatGPT Codex backend, and GitHub Copilot through `src-tauri/src/proxy/forwarder.rs` and provider adapters. +- OAuth device-code and token polling to OpenAI auth and GitHub/GHES endpoints from `src-tauri/src/proxy/providers/codex_oauth_auth.rs` and `src-tauri/src/proxy/providers/copilot_auth.rs`. +- WebDAV `PROPFIND`, `MKCOL`, `HEAD`, `GET`, and `PUT` requests from `src-tauri/src/services/webdav.rs`. +- GitHub release/update manifest and asset downloads from `src-tauri/src/cli/commands/update.rs`. +- Optional usage/quota probes through `src-tauri/src/services/subscription.rs`, `src-tauri/src/services/coding_plan.rs`, and `src-tauri/src/usage_script.rs`. + +--- + +*Integration audit: 2026-06-12* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 00000000..b9a31f20 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,98 @@ +# Technology Stack + +**Analysis Date:** 2026-06-12 + +## Languages + +**Primary:** +- Rust 1.91.1, edition 2021 - native CLI, TUI, daemon, proxy, database, and service logic in `src-tauri/src/`; pinned by `src-tauri/rust-toolchain.toml` and `src-tauri/Cargo.toml`. + +**Secondary:** +- Python 3 - benchmark and release-helper scripts in `scripts/benchmark_cc_switch.py`, `scripts/check_benchmark_thresholds.py`, and `scripts/generate_latest_json.py`. +- Shell - installer and release/download workflow glue in `install.sh` and `.github/workflows/release.yml`. +- Nix - package definition in `flake.nix`. +- Markdown - user docs and prompt/live-memory files such as `README.md`, `README_ZH.md`, `AGENTS.md`, and generated planning docs under `.planning/`. + +## Runtime + +**Environment:** +- Native Rust binary named `cc-switch`, with async work on Tokio 1.x. The binary entry point is `src-tauri/src/main.rs`; the reusable library crate is exported from `src-tauri/src/lib.rs`. +- The repository directory is named `src-tauri/`, but the current manifest defines a Rust `rlib` and `cc-switch` binary only; no Tauri GUI crate metadata is present in `src-tauri/Cargo.toml`. +- Local HTTP proxy runtime uses Axum/Tokio in `src-tauri/src/proxy/server.rs` and `src-tauri/src/proxy/handlers.rs`. +- Unix daemon/supervisor runtime lives in `src-tauri/src/daemon/`. + +**Package Manager:** +- Cargo 1.91.1. +- Lockfile: present at `src-tauri/Cargo.lock`. + +## Frameworks + +**Core:** +- Clap 4.5 - top-level CLI parsing in `src-tauri/src/cli/mod.rs` and command modules under `src-tauri/src/cli/commands/`. +- Ratatui 0.30 + Crossterm 0.29 - interactive terminal UI under `src-tauri/src/cli/tui/` and `src-tauri/src/cli/interactive/`. +- Axum 0.7 + Tower 0.5 + Tower HTTP 0.5 - local proxy server, route handlers, and CORS in `src-tauri/src/proxy/server.rs`. +- Tokio 1.x - async runtime, networking, process, signal, sync, and timers across proxy, WebDAV, OAuth, daemon, and service code. +- rusqlite 0.31 with `bundled`, `backup`, and `hooks` - SQLite persistence in `src-tauri/src/database/`. + +**Testing:** +- Cargo test - unit and integration tests under module-local `#[cfg(test)]` blocks and `src-tauri/tests/`. +- serial_test 3 - serializes tests that mutate process environment or filesystem state. +- tempfile 3 - isolated temporary homes/config roots in `src-tauri/tests/support.rs` and module tests. + +**Build/Dev:** +- rustfmt and clippy - pinned as toolchain components in `src-tauri/rust-toolchain.toml`; CI enforces `cargo fmt --check`. +- GitHub Actions - Rust CI, benchmark CI, and release workflows in `.github/workflows/rust-ci.yml`, `.github/workflows/benchmark.yml`, and `.github/workflows/release.yml`. +- Nix flakes - `flake.nix` builds the Cargo crate from `src-tauri/` and disables test execution for packaging. +- cross-rs/cross - release workflow uses cross compilation for Linux MUSL and ARM targets in `.github/workflows/release.yml`. + +## Key Dependencies + +**Critical:** +- `serde`, `serde_json`, `toml`, `toml_edit`, `serde_yaml`, `json5`, `json-five` - parse and write live config formats for Claude, Codex, Gemini, OpenCode, Hermes, and OpenClaw across `src-tauri/src/*config*.rs`. +- `reqwest` 0.12 with `rustls-tls`, `json`, `stream`, and `socks` - external API calls, WebDAV transport, proxy forwarding, model fetching, OAuth, update downloads, and usage checks. +- `rusqlite` 0.31 - durable state store for providers, MCP, prompts, skills, proxy state, usage logs, model pricing, model routes, failover queues, and settings. +- `rquickjs` 0.8 - executes JavaScript usage scripts for provider quota/coding-plan integrations in `src-tauri/src/usage_script.rs`. +- `minisign-verify`, `sha2`, `semver`, `flate2`, `tar`, and `zip` - signed self-update, checksum, archive, and release asset handling in `src-tauri/src/cli/commands/update.rs`. + +**Infrastructure:** +- `dirs` - resolves user directories in `src-tauri/src/config.rs` and app-specific config adapters. +- `which` - detects local assistant CLIs and tools in environment-check code under `src-tauri/src/services/env_checker.rs` and `src-tauri/src/services/local_env_check.rs`. +- `chrono` and `rust_decimal` - timestamps, usage windows, pricing, and usage rollups in `src-tauri/src/database/dao/` and `src-tauri/src/services/`. +- `uuid`, `indexmap`, `regex`, `base64`, `url`, and `bytes` - identifiers, ordered maps, matching, OAuth token parsing, URL handling, and proxy body handling. +- Windows-only `winreg` and `self-replace` - registry and binary replacement support declared in `src-tauri/Cargo.toml`. + +## Configuration + +**Environment:** +- `CC_SWITCH_CONFIG_DIR` controls CC-Switch storage root; default is `$HOME/.cc-switch` from `src-tauri/src/config.rs`. +- `CLAUDE_CONFIG_DIR` overrides Claude config directory; default is `$HOME/.claude` from `src-tauri/src/config.rs`. +- `CODEX_HOME` controls Codex config directory when it exists; fallback is `$HOME/.codex` from `src-tauri/src/codex_config.rs`. +- Tests and CI set sandboxed `HOME`, `USERPROFILE`, `XDG_CONFIG_HOME`, `XDG_RUNTIME_DIR`, `XDG_STATE_HOME`, `CC_SWITCH_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`, and `CODEX_HOME`; see `src-tauri/tests/support.rs` and `.github/workflows/rust-ci.yml`. +- Never run commands that write live app configuration without temporary environment overrides; this is required by `AGENTS.md`. + +**Build:** +- `src-tauri/Cargo.toml` is the crate manifest; run Cargo commands from `src-tauri/`. +- `src-tauri/rust-toolchain.toml` pins Rust 1.91.1 with `rustfmt` and `clippy`. +- `src-tauri/Cargo.lock` locks Rust dependencies. +- `flake.nix` packages the crate for x86_64/aarch64 Linux and Darwin. +- `.github/workflows/rust-ci.yml` runs format and selected test targets in sandboxed homes. +- `.github/workflows/benchmark.yml` builds release binary and runs benchmark thresholds through `scripts/benchmark_cc_switch.py` and `scripts/check_benchmark_thresholds.py`. +- `.github/workflows/release.yml` builds multi-platform artifacts, signs updater assets, generates `latest.json`, and creates GitHub releases. + +## Platform Requirements + +**Development:** +- Rust 1.91.1 via rustup/toolchain file. +- Cargo commands should be run from `src-tauri/`, for example `cargo fmt --check`, `cargo test`, and `cargo build --release`. +- Local repository shell commands should use the `rtk` prefix per `AGENTS.md`. +- Current checkout uses a proxied remote (`https://gh-proxy.com/https://github.com/SaladDay/cc-switch-cli.git`); `gh pr status` cannot infer a GitHub repo from this remote, so PR inspection needs an explicit `--repo` target or remote adjustment outside this mapping task. + +**Production:** +- Distributed as a native `cc-switch` binary for macOS, Linux, and Windows via GitHub Releases. +- Release targets in `.github/workflows/release.yml`: macOS x64/ARM64/universal, Windows x64, Linux x64/ARM64 MUSL, and Linux x64/ARM64 GLIBC. +- Install/update surface includes `install.sh`, release archives, `checksums.txt`, `latest.json`, Minisign public key `src-tauri/updater/minisign.pub`, and updater code in `src-tauri/src/cli/commands/update.rs`. +- Nix package output is defined in `flake.nix` as `cc-switch`, `cc-switch-cli`, and `default`. + +--- + +*Stack analysis: 2026-06-12* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 00000000..1934e8bc --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,305 @@ +# Codebase Structure + +**Analysis Date:** 2026-06-12 + +## Directory Layout + +``` +cc-switch-cli/ +├── AGENTS.md # Codex/agent guidance; mirrors CLAUDE.md +├── CLAUDE.md # Claude Code project guidance +├── README.md # English product documentation +├── README_ZH.md # Chinese product documentation +├── CHANGELOG.md # Release notes +├── LICENSE # MIT license +├── install.sh # Install/update shell script +├── flake.nix # Nix flake definition +├── flake.lock # Nix flake lockfile +├── .github/workflows/ # GitHub Actions workflows +├── .planning/codebase/ # Generated GSD codebase map documents +├── assets/ # Product images, partner banners, screenshots +├── docs/ # Additional project documentation +├── scripts/ # Utility/build/release scripts +└── src-tauri/ # Main Rust crate + ├── Cargo.toml # Crate manifest for library and binary targets + ├── Cargo.lock # Locked Rust dependencies + ├── rust-toolchain.toml # Pinned Rust toolchain + ├── build.rs # Rust build script + ├── icons/ # App/tray/platform icons + ├── updater/ # Updater metadata/configuration + ├── wix/ # Windows installer files + ├── src/ # Rust source code + └── tests/ # Integration test targets +``` + +## Directory Purposes + +**Repository Root:** +- Purpose: Documentation, release/install metadata, GitHub workflows, planning artifacts, and the Rust crate wrapper. +- Contains: `README.md`, `README_ZH.md`, `CHANGELOG.md`, `install.sh`, `flake.nix`, `.github/workflows/`, `.planning/codebase/`, `assets/`, `docs/`, `scripts/` +- Key files: `AGENTS.md`, `CLAUDE.md`, `src-tauri/Cargo.toml` + +**`src-tauri/`:** +- Purpose: The only Rust crate in the repository; contains both `cc_switch_lib` library and `cc-switch` binary. +- Contains: `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, `build.rs`, `src/`, `tests/`, packaging assets +- Key files: `src-tauri/Cargo.toml`, `src-tauri/rust-toolchain.toml`, `src-tauri/src/main.rs`, `src-tauri/src/lib.rs` + +**`src-tauri/src/`:** +- Purpose: All production Rust source for CLI, TUI, services, persistence, proxy, daemon, live config adapters, and library helpers. +- Contains: Top-level domain modules plus feature directories such as `cli/`, `services/`, `database/`, `proxy/`, `daemon/`, `deeplink/`, `commands/`, `session_manager/` +- Key files: `src-tauri/src/main.rs`, `src-tauri/src/lib.rs`, `src-tauri/src/store.rs`, `src-tauri/src/app_config.rs` + +**`src-tauri/src/cli/`:** +- Purpose: Command-line interface and interactive terminal UI. +- Contains: Clap definitions, command modules, ratatui TUI modules, shared terminal UI helpers, i18n text, editor helpers +- Key files: `src-tauri/src/cli/mod.rs`, `src-tauri/src/cli/interactive/mod.rs`, `src-tauri/src/cli/tui/mod.rs`, `src-tauri/src/cli/ui/table.rs` + +**`src-tauri/src/cli/commands/`:** +- Purpose: Direct Clap subcommand implementations. +- Contains: One module per command family plus helper modules. +- Key files: `src-tauri/src/cli/commands/provider.rs`, `src-tauri/src/cli/commands/proxy.rs`, `src-tauri/src/cli/commands/mcp.rs`, `src-tauri/src/cli/commands/prompts.rs`, `src-tauri/src/cli/commands/skills.rs`, `src-tauri/src/cli/commands/config.rs`, `src-tauri/src/cli/commands/daemon.rs` + +**`src-tauri/src/cli/tui/`:** +- Purpose: Ratatui interactive UI. +- Contains: Event loop, route/data/theme/help, app state, form definitions, UI renderers, runtime actions, background systems, terminal setup, text editing helpers +- Key files: `src-tauri/src/cli/tui/mod.rs`, `src-tauri/src/cli/tui/app/app_state.rs`, `src-tauri/src/cli/tui/ui/mod.rs`, `src-tauri/src/cli/tui/runtime_actions/mod.rs`, `src-tauri/src/cli/tui/runtime_systems/mod.rs` + +**`src-tauri/src/services/`:** +- Purpose: Reusable business logic shared by CLI commands, TUI actions, startup recovery, proxy lifecycle, and environment checks. +- Contains: Provider, MCP, prompt, skill, proxy, auth, WebDAV, usage, speed test, stream check, environment, subscription, and config services. +- Key files: `src-tauri/src/services/mod.rs`, `src-tauri/src/services/provider/mod.rs`, `src-tauri/src/services/proxy.rs`, `src-tauri/src/services/mcp.rs`, `src-tauri/src/services/skill.rs`, `src-tauri/src/services/prompt.rs` + +**`src-tauri/src/database/`:** +- Purpose: SQLite persistence layer. +- Contains: `Database` connection wrapper, schema creation/migration, legacy JSON migration, backups, DAO modules, database tests +- Key files: `src-tauri/src/database/mod.rs`, `src-tauri/src/database/schema.rs`, `src-tauri/src/database/migration.rs`, `src-tauri/src/database/backup.rs`, `src-tauri/src/database/dao/mod.rs` + +**`src-tauri/src/database/dao/`:** +- Purpose: Data access modules grouped by persisted entity. +- Contains: Provider, MCP, prompt, skill, settings, proxy, failover, stream check, model pricing, universal provider, usage rollup, and model route methods. +- Key files: `src-tauri/src/database/dao/providers.rs`, `src-tauri/src/database/dao/proxy.rs`, `src-tauri/src/database/dao/model_routes.rs`, `src-tauri/src/database/dao/failover.rs`, `src-tauri/src/database/dao/settings.rs` + +**`src-tauri/src/proxy/`:** +- Purpose: Local multi-app HTTP proxy, routing/failover, transforms, response handling, usage logging, and provider adapters. +- Contains: Axum server, handlers, request context, model router, provider router, forwarder, adapters, response builders, circuit breaker, metrics, SSE utilities, usage modules +- Key files: `src-tauri/src/proxy/server.rs`, `src-tauri/src/proxy/handlers.rs`, `src-tauri/src/proxy/handler_context.rs`, `src-tauri/src/proxy/model_router.rs`, `src-tauri/src/proxy/provider_router.rs`, `src-tauri/src/proxy/forwarder.rs` + +**`src-tauri/src/proxy/providers/`:** +- Purpose: Provider-specific proxy auth, endpoint, schema, streaming, and transform behavior. +- Contains: Generic adapter trait plus Claude, Codex/OpenAI, Gemini, Copilot, streaming, transform, history, OAuth, and shadow-store modules. +- Key files: `src-tauri/src/proxy/providers/adapter.rs`, `src-tauri/src/proxy/providers/claude.rs`, `src-tauri/src/proxy/providers/codex.rs`, `src-tauri/src/proxy/providers/gemini.rs`, `src-tauri/src/proxy/providers/auth.rs` + +**`src-tauri/src/daemon/`:** +- Purpose: Unix supervisor daemon for proxy worker lifecycle and foreground IPC. +- Contains: Daemon entry point, supervisor, IPC client/server/protocol, pidfile, logging, paths, restart policy +- Key files: `src-tauri/src/daemon/mod.rs`, `src-tauri/src/daemon/supervisor.rs`, `src-tauri/src/daemon/ipc/protocol.rs`, `src-tauri/src/daemon/ipc/client.rs`, `src-tauri/src/daemon/ipc/server.rs` + +**`src-tauri/src/deeplink/`:** +- Purpose: `ccswitch://v1/import?...` parsing and provider-resource import. +- Contains: Parser, provider importer, URL utility functions, request model +- Key files: `src-tauri/src/deeplink/mod.rs`, `src-tauri/src/deeplink/parser.rs`, `src-tauri/src/deeplink/provider.rs` + +**`src-tauri/src/commands/`:** +- Purpose: Library-level command helpers that are not normal Clap subcommands. +- Contains: OpenClaw workspace file and daily memory operations. +- Key files: `src-tauri/src/commands/mod.rs`, `src-tauri/src/commands/workspace.rs` + +**`src-tauri/src/session_manager/`:** +- Purpose: Multi-app session listing and terminal/session provider helpers. +- Contains: Provider-specific session readers and terminal helpers. +- Key files: `src-tauri/src/session_manager/mod.rs`, `src-tauri/src/session_manager/providers/claude.rs`, `src-tauri/src/session_manager/providers/codex.rs`, `src-tauri/src/session_manager/terminal/mod.rs` + +**`src-tauri/tests/`:** +- Purpose: Integration test targets for CLI commands, config isolation, proxy behavior, database behavior, providers, WebDAV, sessions, skills, deep links, and install script checks. +- Contains: Flat integration tests and grouped proxy test directories. +- Key files: `src-tauri/tests/support.rs`, `src-tauri/tests/provider_commands.rs`, `src-tauri/tests/proxy_service.rs`, `src-tauri/tests/proxy_claude_streaming.rs`, `src-tauri/tests/proxy_claude_openai_chat.rs` + +## Key File Locations + +**Entry Points:** +- `src-tauri/src/main.rs`: Binary entry point, startup-state gate, command dispatcher. +- `src-tauri/src/lib.rs`: Library root, module declarations, public re-exports for tests and callers. +- `src-tauri/src/cli/mod.rs`: Clap root command, global `--app`, top-level subcommands. +- `src-tauri/src/cli/interactive/mod.rs`: Interactive-mode dispatch. +- `src-tauri/src/proxy/server.rs`: Axum proxy server construction and runtime state. +- `src-tauri/src/daemon/mod.rs`: Unix daemon entry point and global proxy-switch notification. + +**Configuration:** +- `src-tauri/Cargo.toml`: Crate metadata, library/binary targets, dependencies, features. +- `src-tauri/rust-toolchain.toml`: Pinned Rust toolchain. +- `src-tauri/src/config.rs`: CC-Switch config directory and JSON/text file helpers. +- `src-tauri/src/settings.rs`: App settings and `settings.json` accessors. +- `src-tauri/src/codex_config.rs`: Codex live config paths and TOML/auth writes. +- `src-tauri/src/gemini_config.rs`: Gemini `.env` and settings conversion. +- `src-tauri/src/opencode_config.rs`: OpenCode live config adapter. +- `src-tauri/src/openclaw_config.rs`: OpenClaw live config and workspace paths. +- `src-tauri/src/hermes_config.rs`: Hermes app config adapter. + +**Core Logic:** +- `src-tauri/src/store.rs`: `AppState`, database/config snapshot coordination, startup recovery. +- `src-tauri/src/app_config.rs`: `AppType`, `MultiAppConfig`, MCP/prompt/skill app models. +- `src-tauri/src/provider.rs`: `Provider`, provider metadata, usage script model. +- `src-tauri/src/model_route.rs`: Model-based proxy routing record type. +- `src-tauri/src/services/provider/mod.rs`: Provider business logic facade. +- `src-tauri/src/services/proxy.rs`: Proxy lifecycle, takeover, hot-switch, runtime session coordination. +- `src-tauri/src/database/mod.rs`: `Database` connection, schema initialization, migration gate. +- `src-tauri/src/proxy/handler_context.rs`: Per-request proxy setup, model-route fallback to provider routing. +- `src-tauri/src/proxy/model_router.rs`: Model pattern matching and route hit recording. +- `src-tauri/src/proxy/provider_router.rs`: Current-provider/failover queue selection and circuit breaker coordination. + +**Testing:** +- `src-tauri/tests/support.rs`: Shared integration-test filesystem/home isolation helpers. +- `src-tauri/src/test_support.rs`: Crate-local test helpers. +- `src-tauri/tests/provider_commands.rs`: Provider CLI command integration tests. +- `src-tauri/tests/mcp_commands.rs`: MCP command integration tests. +- `src-tauri/tests/prompt_commands.rs`: Prompt command integration tests. +- `src-tauri/tests/proxy_service.rs`: Proxy service integration tests. +- `src-tauri/tests/proxy_claude_streaming.rs`: Claude streaming proxy tests. +- `src-tauri/tests/proxy_claude_openai_chat.rs`: Claude-to-OpenAI chat transform tests. +- `src-tauri/tests/deeplink_import.rs`: Deep-link import tests. +- `src-tauri/tests/workspace_commands.rs`: OpenClaw workspace command tests. + +**CI and Release:** +- `.github/workflows/rust-ci.yml`: Rust CI workflow. +- `.github/workflows/release.yml`: Release workflow. +- `.github/workflows/benchmark.yml`: Benchmark workflow. +- `scripts/`: Repository utility scripts. +- `src-tauri/updater/`: Updater artifacts/configuration. +- `src-tauri/wix/`: Windows installer configuration. + +## Naming Conventions + +**Files:** +- Use Rust `snake_case.rs` module filenames: `src-tauri/src/model_route.rs`, `src-tauri/src/proxy/model_router.rs`, `src-tauri/src/cli/proxy_settings.rs`. +- Use one command family per file under `src-tauri/src/cli/commands/`: `provider.rs`, `proxy.rs`, `mcp.rs`, `prompts.rs`, `skills.rs`. +- Use directory `mod.rs` as the public module root: `src-tauri/src/services/mod.rs`, `src-tauri/src/proxy/providers/mod.rs`, `src-tauri/src/database/dao/mod.rs`. +- Use grouped test directories for large proxy suites: `src-tauri/tests/proxy_claude_streaming/`, `src-tauri/tests/proxy_claude_openai_chat/`. + +**Directories:** +- Group by architectural layer first: `cli/`, `services/`, `database/`, `proxy/`, `daemon/`. +- Group TUI by responsibility: `src-tauri/src/cli/tui/app/`, `src-tauri/src/cli/tui/ui/`, `src-tauri/src/cli/tui/form/`, `src-tauri/src/cli/tui/runtime_actions/`, `src-tauri/src/cli/tui/runtime_systems/`. +- Group proxy provider specifics under `src-tauri/src/proxy/providers/` and keep generic routing/forwarding at `src-tauri/src/proxy/`. +- Group database persistence by entity under `src-tauri/src/database/dao/`. + +**Rust Types and Functions:** +- Public service facades use `PascalCase` with `Service` suffix: `ProviderService`, `ProxyService`, `McpService`, `SkillService`. +- Domain records use `PascalCase`: `Provider`, `ProviderMeta`, `ModelRoute`, `MultiAppConfig`, `AppSettings`. +- Functions and methods use `snake_case`: `try_new_with_startup_recovery()`, `match_route()`, `select_providers()`, `record_model_route_hit()`. +- App labels and DB string values are lowercase identifiers such as `claude`, `codex`, `gemini`, `opencode`, `hermes`, `openclaw`. + +## Where to Add New Code + +**New User-Facing CLI Command:** +- Clap shape: `src-tauri/src/cli/mod.rs` or the relevant module under `src-tauri/src/cli/commands/` +- Command implementation: `src-tauri/src/cli/commands/.rs` +- Shared logic: `src-tauri/src/services/.rs` or `src-tauri/src/services//` +- Tests: `src-tauri/tests/_commands.rs` for integration behavior or module-local `#[cfg(test)]` tests for pure parsing/formatting + +**New Provider Workflow:** +- Domain model changes: `src-tauri/src/provider.rs` or `src-tauri/src/app_config.rs` +- Service logic: `src-tauri/src/services/provider/` +- Persistence: `src-tauri/src/database/dao/providers.rs` and `src-tauri/src/database/schema.rs` +- CLI command surface: `src-tauri/src/cli/commands/provider.rs` +- TUI actions/rendering: `src-tauri/src/cli/tui/runtime_actions/providers.rs`, `src-tauri/src/cli/tui/ui/providers.rs`, and related form files under `src-tauri/src/cli/tui/form/` +- Tests: `src-tauri/tests/provider_commands.rs`, `src-tauri/tests/provider_service.rs`, or service module tests + +**New Proxy Routing Feature:** +- Request setup: `src-tauri/src/proxy/handler_context.rs` +- Routing engine: `src-tauri/src/proxy/provider_router.rs` or a focused module like `src-tauri/src/proxy/model_router.rs` +- Persistence: `src-tauri/src/database/dao/` and `src-tauri/src/database/schema.rs` +- CLI/TUI management: `src-tauri/src/cli/commands/proxy.rs` and `src-tauri/src/cli/tui/` +- Tests: focused integration tests under `src-tauri/tests/proxy_*.rs` and unit tests near the routing module + +**New Proxy Provider Adapter:** +- Adapter implementation: `src-tauri/src/proxy/providers/.rs` +- Adapter trait updates: `src-tauri/src/proxy/providers/adapter.rs` +- Streaming/transform helpers: `src-tauri/src/proxy/providers/streaming*.rs` or `src-tauri/src/proxy/providers/transform*.rs` +- Endpoint mapping: `src-tauri/src/proxy/provider_router/upstream_endpoint.rs` +- Tests: `src-tauri/tests/proxy_*.rs` or module tests under `src-tauri/src/proxy/providers/` + +**New Persisted Entity:** +- Domain type: top-level source file such as `src-tauri/src/model_route.rs`, or an existing domain module if scoped. +- Schema: `src-tauri/src/database/schema.rs` +- DAO: `src-tauri/src/database/dao/.rs` +- DAO registration: `src-tauri/src/database/dao/mod.rs` +- Public export if needed: `src-tauri/src/lib.rs` +- Tests: `src-tauri/src/database/tests.rs` and feature-specific tests + +**New TUI View or Overlay:** +- Route/menu state: `src-tauri/src/cli/tui/route.rs` and `src-tauri/src/cli/tui/app/menu.rs` +- App state/types: `src-tauri/src/cli/tui/app/types.rs` or `src-tauri/src/cli/tui/app/app_state.rs` +- Rendering: `src-tauri/src/cli/tui/ui/` or `src-tauri/src/cli/tui/ui/overlay/` +- Actions: `src-tauri/src/cli/tui/runtime_actions/` +- Forms: `src-tauri/src/cli/tui/form/` and `src-tauri/src/cli/tui/ui/forms/` +- Tests: module-local tests in the affected TUI files + +**New Live App Config Support:** +- Adapter module: `src-tauri/src/_config.rs` +- MCP adapter if applicable: `src-tauri/src/_mcp.rs` +- App enum/config model: `src-tauri/src/app_config.rs` +- Provider integration: `src-tauri/src/services/provider/` +- Proxy takeover integration: `src-tauri/src/services/proxy.rs` +- Tests: app-specific integration tests under `src-tauri/tests/` + +**New OpenClaw Workspace Operation:** +- Implementation: `src-tauri/src/commands/workspace.rs` +- Config path helpers: `src-tauri/src/openclaw_config.rs` +- Tests: `src-tauri/tests/workspace_commands.rs` +- Rule: Preserve allowlist and symlink/path-traversal rejection patterns. + +**New Utility Shared Across CLI and TUI:** +- Business logic: `src-tauri/src/services/` +- Terminal-only formatting: `src-tauri/src/cli/ui/` +- Low-level path or file helpers: `src-tauri/src/config.rs` +- Test helpers: `src-tauri/tests/support.rs` for integration tests or `src-tauri/src/test_support.rs` for unit tests + +## Special Directories + +**`.planning/codebase/`:** +- Purpose: GSD-generated codebase map consumed by planning/execution commands. +- Generated: Yes +- Committed: Project-dependent; this mapping task writes `ARCHITECTURE.md` and `STRUCTURE.md` here. + +**`.github/workflows/`:** +- Purpose: CI, release, and benchmark workflows. +- Generated: No +- Committed: Yes + +**`assets/`:** +- Purpose: Product images, screenshots, and partner assets. +- Generated: No +- Committed: Yes + +**`docs/`:** +- Purpose: Human documentation beyond root README files. +- Generated: No +- Committed: Yes + +**`scripts/`:** +- Purpose: Repository utility scripts for install/build/release/support tasks. +- Generated: No +- Committed: Yes + +**`src-tauri/icons/`:** +- Purpose: Platform and app icon assets. +- Generated: Usually yes from design/icon sources, but stored as committed assets. +- Committed: Yes + +**`src-tauri/updater/`:** +- Purpose: Update metadata/configuration used by updater/release flows. +- Generated: Partially +- Committed: Yes + +**`src-tauri/wix/`:** +- Purpose: Windows installer configuration. +- Generated: Partially +- Committed: Yes + +**`target/`:** +- Purpose: Cargo build output and rust-analyzer/flycheck artifacts. +- Generated: Yes +- Committed: No + +--- + +*Structure analysis: 2026-06-12* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 00000000..63e76835 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,210 @@ +# Testing Patterns + +**Analysis Date:** 2026-06-12 + +## Test Framework + +**Runner:** +- Rust built-in test harness through Cargo. +- Toolchain: Rust 1.91.1 from `src-tauri/rust-toolchain.toml`. +- Config: `src-tauri/Cargo.toml` declares the crate, binary, library, `test-hooks` feature, and dev dependencies. + +**Assertion Library:** +- Standard Rust assertions: `assert!`, `assert_eq!`, `assert_ne!`, `matches!`, `expect_err`, and `unwrap_err`. +- Structured fixtures use `serde_json::json!` from `src-tauri/Cargo.toml`. + +**Run Commands:** +```bash +cargo test # Run all tests from src-tauri/ +cargo test --lib # Run library unit tests +cargo test --bin cc-switch # Run binary unit tests +cargo test provider_switch # Run tests whose names contain provider_switch +cargo test --test provider_commands # Run one integration test target +cargo test --features test-hooks # Run tests with the test-hooks feature enabled +cargo fmt --check # Required format check +``` + +## Test File Organization + +**Location:** +- Module-local unit tests live beside implementation under `src-tauri/src/**` behind `#[cfg(test)]`, for example `src-tauri/src/main.rs`, `src-tauri/src/database/tests.rs`, `src-tauri/src/services/stream_check/tests.rs`, and `src-tauri/src/proxy/forwarder/tests/*.rs`. +- Integration tests live under `src-tauri/tests/`, for example `src-tauri/tests/provider_commands.rs`, `src-tauri/tests/mcp_commands.rs`, `src-tauri/tests/proxy_service.rs`, and `src-tauri/tests/proxy_database.rs`. +- Large integration suites can be split into a directory plus helper module, for example `src-tauri/tests/proxy_claude_streaming/*.rs` and `src-tauri/tests/proxy_claude_openai_chat/*.rs`. + +**Naming:** +- Name test files by subsystem or command surface: `provider_commands.rs`, `prompt_commands.rs`, `proxy_daemon.rs`, `settings_visible_apps.rs`, and `workspace_commands.rs`. +- Name test functions as behavior statements in snake_case: `provider_usage_query_set_writes_upstream_defaults_and_preserves_meta`, `schema_migration_rejects_future_version`, and `default_cost_multiplier_rejects_non_numeric_values`. + +**Structure:** +```text +src-tauri/ +├── src/ +│ ├── main.rs # binary helpers plus unit tests +│ ├── test_support.rs # crate-local test environment guards +│ ├── database/tests.rs # module test suite +│ └── proxy/forwarder/tests/*.rs # focused module test suite +└── tests/ + ├── support.rs # integration-test support + ├── provider_commands.rs # command integration tests + ├── proxy_database.rs # database/proxy integration tests + └── proxy_claude_openai_chat/*.rs # split proxy integration suite +``` + +## Test Structure + +**Suite Organization:** +```rust +#[path = "support.rs"] +mod support; + +use support::{ensure_test_home, lock_test_mutex, reset_test_fs}; + +#[test] +#[serial] +fn provider_usage_query_set_writes_upstream_defaults_and_preserves_meta() { + let _guard = lock_test_mutex(); + reset_test_fs(); + ensure_test_home(); + + // arrange state + // execute command/service + // assert persisted state and user-visible behavior +} +``` + +**Patterns:** +- Use arrange/act/assert inside each test, even when not explicitly commented. `src-tauri/tests/provider_commands.rs` builds config state, executes a command, reloads state, and asserts persisted fields. +- Use `#[tokio::test]` for async database, proxy, HTTP, and stream behavior, as in `src-tauri/tests/proxy_database.rs`, `src-tauri/tests/proxy_service.rs`, and `src-tauri/src/proxy/forwarder/tests/request_building.rs`. +- Use `#[serial]` from `serial_test` for tests that mutate process-global environment, current directory, app settings, daemon state, or singleton config paths. +- Use RAII guards for environment and current-directory restoration: `ConfigDirEnvGuard` in `src-tauri/src/main.rs` and `src-tauri/src/database/tests.rs`, `CurrentDirGuard` in `src-tauri/tests/support.rs`, and `TestEnvGuard` in `src-tauri/src/test_support.rs`. + +## Mocking + +**Framework:** No dedicated mocking framework detected. + +**Patterns:** +```rust +let db = Database::memory()?; +let provider = Provider::with_id( + "claude-p1".to_string(), + "claude-p1".to_string(), + json!({"env": {"BASE_URL": "https://example.com"}}), + None, +); +db.save_provider("claude", &provider)?; +``` + +```rust +let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; +let app = axum::Router::new().route("/chat/completions", post(handle_chat_completions)); +``` + +**What to Mock:** +- Use in-memory SQLite through `Database::memory()` for DAO and proxy/database behavior where disk persistence is not under test, as in `src-tauri/tests/proxy_database.rs`. +- Use temporary HOME/config directories for stateful command and live-config paths, via `src-tauri/tests/support.rs` and `src-tauri/src/test_support.rs`. +- Use local Axum upstream servers for proxy request/response tests, as in `src-tauri/tests/proxy_claude_openai_chat/helpers.rs` and `src-tauri/tests/proxy_claude_streaming/helpers.rs`. +- Use scripted upstream helpers for lower-level forwarder tests, as in `src-tauri/src/proxy/forwarder/tests/request_building.rs`. + +**What NOT to Mock:** +- Do not touch real `HOME`, `CC_SWITCH_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`, or `CODEX_HOME`. Use the helper isolation layer instead. +- Do not replace SQLite with ad hoc structs when testing persistence semantics; use `Database::memory()` or isolated real config directories. +- Do not call external provider APIs for normal tests. Use local listeners and synthetic JSON/SSE fixtures. + +## Fixtures and Factories + +**Test Data:** +```rust +fn usage_script_fixture() -> UsageScript { + UsageScript { + enabled: true, + language: "javascript".to_string(), + code: "return { remaining: 1, unit: 'USD' };".to_string(), + timeout: Some(9), + api_key: Some("sk-old".to_string()), + base_url: Some("https://old.example.com".to_string()), + access_token: None, + user_id: None, + template_type: Some("general".to_string()), + auto_query_interval: Some(30), + coding_plan_provider: None, + } +} +``` + +**Location:** +- Shared integration helpers live in `src-tauri/tests/support.rs`. +- Crate-local test helpers live in `src-tauri/src/test_support.rs`. +- Suite-specific proxy helpers live in `src-tauri/tests/proxy_claude_openai_chat/helpers.rs` and `src-tauri/tests/proxy_claude_streaming/helpers.rs`. +- Module-specific fixtures stay near tests, for example `usage_script_fixture` in `src-tauri/tests/provider_commands.rs` and `claude_gemini_native_provider` in `src-tauri/src/services/stream_check/tests.rs`. + +## Coverage + +**Requirements:** No enforced coverage threshold or coverage configuration detected. + +**View Coverage:** +```bash +# Not configured in the repository. Add cargo-llvm-cov or tarpaulin only if a phase explicitly requires coverage reporting. +``` + +## Test Types + +**Unit Tests:** +- Scope pure helpers, parsers, command parsing, schema migration functions, stream-check configuration, and transformation logic. +- Examples: `src-tauri/src/cli/mod.rs`, `src-tauri/src/main.rs`, `src-tauri/src/services/stream_check/tests.rs`, `src-tauri/src/proxy/providers/transform.rs`, and `src-tauri/src/proxy/providers/transform_codex_chat.rs`. + +**Integration Tests:** +- Scope command execution, persisted state, live-config import/export, proxy HTTP behavior, daemon lifecycle, settings, WebDAV sync, and multi-app behavior. +- Examples: `src-tauri/tests/provider_commands.rs`, `src-tauri/tests/import_export_sync.rs`, `src-tauri/tests/proxy_service.rs`, `src-tauri/tests/proxy_daemon.rs`, and `src-tauri/tests/webdav_sync_service.rs`. + +**E2E Tests:** +- No browser or full packaged-app E2E framework detected. +- The closest E2E coverage is command-level and proxy-level integration testing through Cargo integration tests under `src-tauri/tests/`. + +## Common Patterns + +**Async Testing:** +```rust +#[tokio::test] +async fn default_cost_multiplier_round_trips() -> Result<(), AppError> { + let db = Database::memory()?; + db.set_default_cost_multiplier("claude", "1.5").await?; + let updated = db.get_default_cost_multiplier("claude").await?; + assert_eq!(updated, "1.5"); + Ok(()) +} +``` + +**Error Testing:** +```rust +let err = db + .set_default_cost_multiplier("claude", "not-a-number") + .await + .unwrap_err(); +assert!(matches!( + err, + AppError::Localized { + key: "error.invalidMultiplier", + .. + } +)); +``` + +**Environment Isolation:** +```rust +let _guard = lock_test_mutex(); +reset_test_fs(); +ensure_test_home(); +``` + +**CI Test Isolation:** +- `.github/workflows/rust-ci.yml` creates a `mktemp -d` sandbox for `HOME`, `USERPROFILE`, `CC_SWITCH_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `XDG_CONFIG_HOME`, `XDG_RUNTIME_DIR`, and `XDG_STATE_HOME`. +- CI sets `RUST_TEST_THREADS=1` for unit and selected integration jobs because many tests manipulate process-global config state. + +**Targeted CI Jobs:** +- Format job: `cargo fmt --check` from `src-tauri/`. +- Unit jobs: `cargo test --lib -- --nocapture` and `cargo test --bin cc-switch -- --nocapture`. +- Integration jobs: selected proxy/failover/database tests in `.github/workflows/rust-ci.yml`. + +--- + +*Testing analysis: 2026-06-12* diff --git a/.planning/phase-1/RESEARCH.md b/.planning/phase-1/RESEARCH.md new file mode 100644 index 00000000..c518f1d2 --- /dev/null +++ b/.planning/phase-1/RESEARCH.md @@ -0,0 +1,243 @@ +# Phase 1 Research: Database Layer (Schema v11 + model_routes DAO) + +**Date:** 2026-06-11 +**Status:** Complete + +## 1. 当前 Schema 架构 + +### 版本管理 + +- **当前版本**: v10 +- **常量**: `SCHEMA_VERSION: i32 = 10`(`database/mod.rs:56`) +- **版本存储**: SQLite `PRAGMA user_version` +- **读写方法**: `get_user_version(conn)` / `set_user_version(conn, version)`(`schema.rs:2011-2023`) + +### 迁移基础设施 + +迁移入口 `apply_schema_migrations_on_conn()`(`schema.rs:337-430`): + +``` +SAVEPOINT schema_migration + → while version < SCHEMA_VERSION: + match version: + 0 → migrate_v0_to_v1 → set_user_version(1) + 1 → migrate_v1_to_v2 → set_user_version(2) + ... + 9 → migrate_v9_to_v10 → set_user_version(10) + _ → error "未知的数据库版本" + → repair_proxy_request_logs_columns + → create_request_logs_indexes_if_supported + → normalize_auto_failover_requires_takeover + → RELEASE schema_migration (or ROLLBACK on error) +``` + +关键特性: +- 使用 SAVEPOINT 包裹整个迁移链,失败自动回滚 +- 逐版本递增迁移,不跳版本 +- 迁移前在 `Database::init()` 中自动创建备份(`schema.rs:127-134`) +- 检测 future schema 版本时拒绝启动并提示升级 + +### v9→v10 迁移参考(`schema.rs:1193-1213`) + +```rust +fn migrate_v9_to_v10(conn: &Connection) -> Result<(), AppError> { + Self::add_column_if_missing(conn, "mcp_servers", "enabled_hermes", "BOOLEAN NOT NULL DEFAULT 0")?; + if Self::table_exists(conn, "skills")? { + Self::add_column_if_missing(conn, "skills", "enabled_hermes", "BOOLEAN NOT NULL DEFAULT 0")?; + } + log::info!("v9 -> v10 迁移完成:已添加 Hermes Agent 支持"); + Ok(()) +} +``` + +模式:使用 `add_column_if_missing` 安全添加列(幂等),使用 `table_exists` 处理可选表。 + +### 表创建 + +`create_tables_on_conn()`(`schema.rs:16-328`)定义所有表: +1. providers (复合主键: id + app_type) +2. provider_endpoints (FOREIGN KEY → providers) +3. mcp_servers (per-app enablement columns) +4. prompts (复合主键: id + app_type) +5. skills (id PRIMARY KEY + per-app enablement) +6. skill_repos +7. settings (key-value) +8. proxy_config (三行结构: claude/codex/gemini) +9. provider_health (FOREIGN KEY → providers) +10. proxy_request_logs +11. model_pricing +12. stream_check_logs +13. proxy_live_backup +14. usage_daily_rollups +15. session_log_sync + +每个表使用 `CREATE TABLE IF NOT EXISTS` 保证幂等。 + +## 2. 上游 PR (#4081) 的 Schema 变更 + +PR 新增 v11 迁移,创建 `model_routes` 表: + +```sql +CREATE TABLE IF NOT EXISTS model_routes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + app_type TEXT NOT NULL, + pattern TEXT NOT NULL, + provider_id TEXT NOT NULL, + priority INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE +); +``` + +### ⚠️ 关键对齐约束 + +cc-switch-cli 的 `dao/mod.rs:16-17` 明确声明: +```rust +// NOTE(cc-switch-cli): keep schema aligned with upstream, but only compile the DAOs +// that are currently supported by the CLI build. +``` + +**这意味着**: +1. `model_routes` 表结构必须与上游完全一致 +2. 迁移 v10→v11 必须与上游完全相同 +3. 用户可能在 cc-switch 和 cc-switch-cli 之间共用同一个 `cc-switch.db` 文件 +4. 任何 schema 差异都会导致数据损坏或启动失败 + +## 3. DAO 模式分析 + +### 现有 DAO 结构(`dao/mod.rs`) + +```rust +pub mod failover; +pub mod mcp; +pub mod model_pricing; +pub mod prompts; +pub mod providers; +pub mod providers_seed; +pub mod proxy; +pub mod settings; +pub mod skills; +pub mod stream_check; +pub mod usage_rollup; +pub mod universal_providers; // 注意:这个也在目录中 +``` + +### DAO 方法模式(参考 `dao/failover.rs`) + +- DAO 方法直接 `impl Database { ... }` +- 使用 `lock_conn!(self.conn)` 获取连接 +- 使用参数化查询(`?1`, `params![]`) +- 错误类型:`AppError::Database(...)` +- 返回类型:`Result` + +### 新增 DAO 需要 + +1. 新建 `dao/model_routes.rs` +2. 在 `dao/mod.rs` 添加 `pub mod model_routes;` +3. 可选:在 `database/mod.rs` 中添加类型导出 + +## 4. 类型定义模式 + +### Provider 类型(`provider.rs`) + +`Provider` struct 使用 `#[derive(Debug, Clone, Serialize, Deserialize)]`。 + +### ModelRoute 类型设计 + +参考上游 PR 和现有 `FailoverQueueItem` 模式: + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelRoute { + pub id: Option, // None for new, Some for persisted + pub app_type: String, + pub pattern: String, + pub provider_id: String, + pub priority: i32, + pub enabled: bool, + pub created_at: Option, + pub updated_at: Option, +} +``` + +### 类型放置位置 + +有两个选择: +- **选项 A**: 新建 `src-tauri/src/model_route.rs`(独立模块) +- **选项 B**: 放在 `provider.rs` 中(与 Provider 放在一起) + +上游 PR 倾向于独立模块。cc-switch-cli 的 `lib.rs` 模式支持独立模块。 + +## 5. 测试模式 + +### 迁移测试模式(`database/tests.rs:1225-1272`) + +```rust +#[test] +fn schema_migration_v9_adds_hermes_columns() { + let conn = Connection::open_in_memory().expect("open memory db"); + conn.execute_batch("CREATE TABLE mcp_servers (...); CREATE TABLE skills (...);") + .expect("seed v9 schema"); + Database::set_user_version(&conn, 9).expect("set user_version=9"); + Database::apply_schema_migrations_on_conn(&conn).expect("apply migrations"); + assert_eq!(Database::get_user_version(&conn).unwrap(), SCHEMA_VERSION); + assert!(Database::has_column(&conn, "mcp_servers", "enabled_hermes").unwrap()); +} +``` + +### DAO 测试模式 + +- 使用 `Database::memory()` 创建内存数据库 +- 直接在测试中构造 SQL INSERT 准备数据 +- 调用 DAO 方法验证 CRUD 正确性 +- 测试边界条件(重复插入、缺失外键等) +- 使用 `#[test]` 标记,非 `#[tokio::test]`(数据库操作是同步的) + +## 6. 迁移安全性分析 + +### 用户数据保护 + +1. **自动备份**: `Database::init()` 在迁移前自动备份数据库文件(`schema.rs:127-134`) +2. **SAVEPOINT**: 迁移包裹在 SAVEPOINT 中,失败自动回滚 +3. **幂等建表**: 使用 `CREATE TABLE IF NOT EXISTS` +4. **Future schema 检测**: 新版本数据库不会在旧版本代码中打开 + +### cc-switch 兼容性 + +- cc-switch-cli 和 cc-switch 共用同一个 `~/.cc-switch/cc-switch.db` +- **v10→v11 迁移必须完全一致** +- 如果 cc-switch 先升级到 v11,cc-switch-cli 必须能正确打开 v11 数据库 +- 如果 cc-switch-cli 先升级,cc-switch 也必须能正确打开 + +## 7. 决策点 + +| 决策 | 选项 | 推荐 | +|------|------|------| +| model_routes 表结构 | 与上游 PR 完全一致 | ✅ 必须一致 | +| ModelRoute 类型位置 | `model_route.rs` 独立模块 | ✅ 与上游保持一致 | +| DAO 方法放置 | `impl Database` in `dao/model_routes.rs` | ✅ 与现有模式一致 | +| 迁移版本号 | v11 (SCHEMA_VERSION=11) | ✅ 与上游一致 | +| 外键约束 | `FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE` | ✅ provider 删除时级联删除路由 | + +## 8. 实现清单 + +### 必须修改的文件 + +| 文件 | 变更 | +|------|------| +| `database/mod.rs` | `SCHEMA_VERSION` 10→11 | +| `database/schema.rs` | ① `create_tables_on_conn` 添加 model_routes 表 ② 添加 `migrate_v10_to_v11` ③ 在 `apply_schema_migrations_on_conn` 添加 version 10 分支 | +| `database/dao/mod.rs` | 添加 `pub mod model_routes;` | +| `database/dao/model_routes.rs` | **新建** — CRUD DAO 实现 | +| `model_route.rs` (src-tauri/src/) | **新建** — ModelRoute 类型定义 | +| `lib.rs` | 添加 `mod model_route;` + 公开导出 | +| `database/tests.rs` | 添加 v10→v11 迁移测试 + DAO 测试 | + +### 不需要在此阶段修改的文件 + +- `provider.rs` — 不需要修改(ModelRoute 独立于 Provider) +- 代理层文件 — Phase 2 处理 +- CLI 命令文件 — Phase 3 处理 diff --git a/.planning/phase-2/RESEARCH.md b/.planning/phase-2/RESEARCH.md new file mode 100644 index 00000000..d9be078f --- /dev/null +++ b/.planning/phase-2/RESEARCH.md @@ -0,0 +1,193 @@ +# Phase 2 Research: Router Engine + Proxy Integration + +**Date:** 2026-06-12 +**Status:** Complete + +## 1. 请求处理流程分析 + +### 当前流程 + +``` +Handler (handlers.rs) + → HandlerContext::load(state, app_type, headers, body) // handler_context.rs:35 + → provider_router.select_providers(app_type) // handler_context.rs:51 + → 如果 auto_failover: 返回 failover 队列中的 provider 列表 + → 否则: 返回当前 provider(单个) + → 提取 request_model from body["model"] // handler_context.rs:66-70 + → RequestForwarder::new(context.provider_router) // handlers.rs:137 + → forwarder.forward_response_detailed(..., context.providers().to_vec(), ...) // handlers.rs:166 + → 遍历 providers 列表,逐个尝试 +``` + +### 目标流程(集成 ModelRouter 后) + +``` +Handler + → HandlerContext::load(state, app_type, headers, body) + → 提取 request_model from body["model"] + → CHECK model_router.match_route(app_type, request_model) // 新增 + → 匹配成功: providers = [matched_provider](单 provider,无 failover) + → 匹配失败: providers = provider_router.select_providers(app_type)(回退现有逻辑) + → RequestForwarder::new(context.provider_router) // 不变 + → forwarder.forward_response_detailed(..., context.providers().to_vec(), ...) // 不变 + → 单 provider 模式: 只尝试一次(成功/失败即返回) + → 多 provider 模式: failover 逐个尝试(现有行为) +``` + +## 2. 关键集成点 + +### 2.1 ProxyServerState (server.rs:31-40) + +当前字段: +```rust +pub struct ProxyServerState { + pub db: Arc, + pub config: Arc>, + pub status: Arc>, + pub start_time: Arc>>, + pub current_providers: Arc>>, + pub provider_router: Arc, + pub codex_chat_history: Arc, + pub gemini_shadow: Arc, +} +``` + +需要新增:`pub model_router: Arc` + +ModelRouter 创建时机:在 ProxyService 启动时(services/proxy.rs 中的 start 方法),创建 ProxyServerState 时同时创建 ModelRouter。 + +### 2.2 HandlerContext (handler_context.rs:18-32) + +当前字段需要新增: +```rust +pub model_router: Arc, // 新增 +pub route_source: Option, // 新增,用于日志/调试 +``` + +`load()` 方法的修改(handler_context.rs:35-88): +- 在提取 `request_model` 之后(line 70) +- 调用 `state.model_router.match_route(app_type.as_str(), &request_model)` +- 匹配成功:绕过 `provider_router.select_providers()`,使用匹配的 provider +- 匹配失败:回退到现有 `select_providers()` 逻辑 +- 设置 `route_source` 字段(匹配成功时记录 pattern) +- **重要**: `current_provider_id_at_start` 仍需在 route 匹配之前获取(与现有行为一致) + +### 2.3 RequestForwarder (forwarder.rs) + +**无需修改**。Forwarder 已经接受 `providers: Vec` 参数,单 provider 或多 provider 都由 HandlerContext 传入。 + +### 2.4 代理模块注册 (proxy/mod.rs) + +新增:`pub mod model_router;` + +### 2.5 ProxyService 启动 (services/proxy.rs) + +在 ProxyService 启动函数中创建 `ModelRouter` 实例并注入到 `ProxyServerState`。 + +## 3. ModelRouter 引擎设计 + +### 通配符匹配逻辑 + +``` +pattern "*sonnet*" → regex "(?i).*sonnet.*" +pattern "claude-*" → regex "(?i)claude-.*" +pattern "*-4-5" → regex "(?i).*-4-5" +pattern "exact" → 精确匹配(不转 regex) +``` + +- 仅 `*` 是通配符,其他字符按字面匹配 +- 大小写不敏感 +- 多个匹配规则时,取 priority 最小的 + +### ModelRouter 结构 + +```rust +pub struct ModelRouter { + db: Arc, +} + +impl ModelRouter { + pub fn new(db: Arc) -> Self { Self { db } } + + pub async fn match_route(&self, app_type: &str, model: &str) + -> Result, ProxyError> +} +``` + +## 4. 上游 PR (#4081) 对比 + +上游 PR 的文件结构(供参考): +- `proxy/model_router.rs` — 新建 202 行 +- `proxy/handler_context.rs` — +150/-12 行 +- `proxy/forwarder.rs` — +38/-10 行 +- `proxy/server.rs` — +5 行 + +cc-switch-cli 的差异: +- handler_context 结构类似但字段名可能不同 +- forwarder 签名可能不同(需仔细对比) +- server.rs 中 ProxyServerState 字段不同 + +## 5. 服务层集成 + +### proxy_service.rs 中的启动流程 + +需要在 ProxyService::start() 方法中(约 line 463)创建 ModelRouter 并注入到 ProxyServerState。 + +查看现有模式: +```rust +let provider_router = Arc::new(ProviderRouter::new(db.clone())); +let state = ProxyServerState { + db: db.clone(), + provider_router, + // ... other fields +}; +``` + +需要类似地创建: +```rust +let model_router = Arc::new(ModelRouter::new(db.clone())); +let state = ProxyServerState { + db: db.clone(), + provider_router, + model_router, // 新增 + // ... other fields +}; +``` + +## 6. 测试策略 + +### ModelRouter 单元测试 +- 精确匹配测试 +- 通配符 `*sonnet*` 匹配 +- 通配符 `claude-*` 匹配 +- 通配符 `*-4-5` 匹配 +- 多规则优先级测试(priority 小的优先) +- enabled=false 规则被跳过 +- 无匹配返回 None +- 大小写不敏感匹配 +- 特殊字符(regex 元字符在 pattern 中视为字面量) + +### 代理集成测试 +- 匹配规则 → router-selected provider 被使用 +- 无匹配规则 → fallback 到 ProviderRouter +- 空路由表 → 行为不变 + +## 7. 需要处理的边界情况 + +1. **model 字段缺失**: body 中没有 "model" 字段 → `request_model = "unknown"` → 回退到 ProviderRouter +2. **路由指向的 provider 不存在**: ModelRoute 指向的 provider_id 已被删除 → 记录 warning 日志,回退到 ProviderRouter(外键级联删除已处理大部分情况) +3. **并发路由更新**: 请求处理过程中路由表被修改 → 每次请求都从 DB 重新读取(不使用缓存) +4. **app_type 过滤**: 只匹配与请求 app_type 相同的路由规则 + +## 8. 文件变更清单 + +| 文件 | 变更类型 | 估计行数 | +|------|----------|----------| +| `proxy/model_router.rs` | **新建** | ~180 | +| `proxy/mod.rs` | 修改 | +1 | +| `proxy/handler_context.rs` | 修改 | ~+60/-5 | +| `proxy/server.rs` | 修改 | +2 | +| `services/proxy.rs` | 修改 | +3 | +| `lib.rs` | 修改 | +1 | +| `proxy/handler_context.rs` (tests) | 修改 | ~+80 | +| `proxy/forwarder.rs` | **无需修改** | 0 | diff --git a/.planning/phase-3/RESEARCH.md b/.planning/phase-3/RESEARCH.md new file mode 100644 index 00000000..c64d0d9b --- /dev/null +++ b/.planning/phase-3/RESEARCH.md @@ -0,0 +1,29 @@ +# Phase 3 Research: CLI Commands + +**Date:** 2026-06-12 +**Status:** Complete + +## Key Findings + +### Dispatch Chain +``` +main.rs:63 → commands::proxy::execute(cmd, cli.app) + → match cmd → ProxyCommand::ModelRoute(subcmd) → handle_model_route() +``` + +### ProxyCommand pattern (cli/commands/proxy.rs) +- `#[derive(Subcommand, Debug, Clone)]` enum +- Handled in `execute(cmd, app)` function +- Uses `AppState::try_new()` to get DB access + +### New ModelRouteCommand +- Add as subcommand variant to ProxyCommand +- Five sub-variants: List, Add, Remove, Toggle, Update +- Call Phase 1 DAO methods directly (no service layer needed) + +### No main.rs changes needed +- `Commands::Proxy(cmd)` already dispatches to `proxy::execute(cmd, app)` + +### Files +- `cli/commands/proxy.rs` — ~+100 lines (enum variants + handlers) +- No other files changed diff --git a/.planning/phase-4/RESEARCH.md b/.planning/phase-4/RESEARCH.md new file mode 100644 index 00000000..aaeb35c3 --- /dev/null +++ b/.planning/phase-4/RESEARCH.md @@ -0,0 +1,42 @@ +# Phase 4 Research: TUI Interface + +**Date:** 2026-06-12 +**Status:** Complete + +## Key Findings + +### TUI Architecture +- **Table rendering**: ratatui `Table::new(rows, constraints)` with `Row::new(cells)` — see `ui/providers.rs:290-339` +- **Actions**: `Action` enum variants in `runtime_actions/mod.rs` dispatch to handler functions +- **Config page**: `app/content_config.rs` — Proxy settings area with `LocalProxySettingsItem` +- **Overlays**: `ui/overlay/` — forms (`basic.rs`, `pickers.rs`) and modals +- **Data**: `UiData` struct in `data.rs:832` — TUI data snapshot +- **No React/svelte**: everything is ratatui + crossterm + +### Integration Points +1. **New file**: `ui/model_routes.rs` — table rendering + key handling +2. **Runtime actions**: `runtime_actions/model_routes.rs` (NEW) — CRUD handlers +3. **Config page**: Add model routes section to `content_config.rs` +4. **App state**: Add model routes state to `app/app_state.rs` +5. **Data**: Add model routes data to `data.rs` `UiData` + +### Patterns to Follow +- Table: `ui/providers.rs` (266 lines) — header, rows, highlight, key bar +- Actions: `runtime_actions/providers.rs` — CRUD via DB calls +- Form: `ui/overlay/basic.rs` — text input overlays + +### Simpler Approach +Given the TUI complexity (4545-line data.rs), **embed model routes table directly into the existing proxy settings section** rather than creating a new top-level tab. This minimizes changes: +- Add a "Model Routes" section below proxy settings in `content_config.rs` +- Render a compact 4-column table (Pattern | Provider | Priority | Enabled) +- Add/Edit via simple text input overlays (reuse existing overlay system) +- Delete via confirmation dialog +- Toggle via keyboard shortcut + +### Files (estimated) +- `ui/model_routes.rs` — NEW, ~250 lines (table render + key handler) +- `runtime_actions/model_routes.rs` — NEW, ~150 lines (5 handlers) +- `app/content_config.rs` — modify, ~+50 lines (integration) +- `app/app_state.rs` — modify, ~+30 lines (state fields) +- `data.rs` — modify, ~+50 lines (data loading) +- `runtime_actions/mod.rs` — modify, ~+10 lines (action enum + dispatch) diff --git a/docs/tui-usability-roadmap.md b/docs/tui-usability-roadmap.md index 6568ff1c..344c2bf8 100644 --- a/docs/tui-usability-roadmap.md +++ b/docs/tui-usability-roadmap.md @@ -10,9 +10,7 @@ For context — the foundations the remaining items build on: - **Keymap registry** (`src/cli/tui/keymap.rs`): one binding table per page drives both key dispatch and the page key bar, so hints can never drift from handlers. Migrated: Providers, MCP, Prompts, Skills (installed), - Usage, Sessions. Sessions binds only its action keys (Enter/R/d/r/a); - pane/list navigation stays explicit in the handler (pane-dependent and - reused by the filter path), with a static nav-hint prefix on the bar. + Usage. - **Overlay frame** (`src/cli/tui/ui/overlay/frame.rs`): all ~24 dialogs render through `overlay_frame`/`overlay_frame_at` with unified body padding; fixed-count pickers size to their options (`FitRows`). @@ -23,51 +21,45 @@ For context — the foundations the remaining items build on: semantic colors (`fg_strong`, `on_accent`, `on_comment`), Settings › Theme (Auto/Dark/Light, persisted), COLORFGBG auto-detection, curated ansi256 pins for both palettes. -- **Help sheet generation** (`src/cli/tui/help.rs::global_help_lines`): the - MCP/Prompts/Sessions/Skills/Usage page lines are generated from - `keymap::::help_items` (a `never` sentinel + `fn_addr_eq` skips - hidden aliases like Usage's reverse-Tab), so those hints track dispatch. - Providers/Config/Settings and the Hermes-only Memory line stay - hand-written (`texts::tui_help_line_*`) for their app-scope prose; the - static prelude is `texts::tui_help_prelude`. `context_help_for_app` now - takes `&UiData` to evaluate the keymap labels. -- **Icon fallback** (`src/cli/tui/icons.rs`): `CC_SWITCH_ICONS=auto|emoji| - ascii` env override + a persisted Settings › Icons row, mirroring the - color-mode philosophy. `Auto` keeps emoji unless the locale is clearly - not UTF-8 (never flips the default when locale info is absent). The nav - sidebar collapses its emoji column to zero width in ASCII mode and - page/overlay titles strip a leading emoji via `icons::strip_icon`, so - wide glyphs can no longer break border alignment on legacy terminals. - Also accepts the full-width `?` as the help hotkey. - Word-wrapped, message-adaptive dialogs; breadcrumb titles on sub-pages; empty-state guidance on empty lists; `? more` degradation for - overflowing key bars. + overflowing key bars; help sheet synced with actual bindings. ## Remaining work ### 1. Finish the mechanical migrations (low risk, mostly delegatable) -- **config.rs sub-pages → `render_page_frame`**: **done** for Config, - WebDAV, Settings, and Managed Accounts (the clean 1:1 fits — the last - uses the frame's `Some(summary)` path then splits the body into two - columns). Added `shared::breadcrumb_path` (unpadded) for frame callers, - since the frame wraps the title itself. Still hand-rolled — and - deliberately skipped because their layouts don't match the frame: - - **Settings › Proxy** (`render_settings_proxy`): trailing 2-line - footer (`[1, Min, 2]`) and a *conditional* key bar. - - **Hermes Memory** (`render_hermes_memory`): a custom info-row - paragraph (`[1, 2, Min]`), not a summary bar. - - **OpenClaw** Env/Tools/Agents routes and Workspace/Daily Memory: a - section-scroll layout, not a table body. - These need `render_page_frame` variants (footer slot / info-row slot) - before they can migrate; not maintenance-only. - -Help-sheet generation (was #2) and the icon fallback (was #3, issue #314 -class) are both done — see the Landed section. A possible follow-up on the -icon work: per-item ASCII nav markers instead of the current text-only -collapse, if a visible glyph in ASCII mode is wanted. - -### 2. Provider form decomposition (largest remaining UX item) +- **config.rs sub-pages → `render_page_frame`**: Config, WebDAV, OpenClaw + Workspace/Daily Memory/Env/Tools/Agents, Hermes Memory, Settings, + Settings › Proxy, Managed Accounts still hand-roll the page shell. + Visual output is already consistent (padded titles, persistent key + bars); this is maintenance-only deduplication. +- **Sessions page → keymap registry**: the last main page still + dispatching raw key codes. Its Enter/R/d/r/a actions are + pane-dependent (`SessionsPane`), so the migration needs the intent + handler to keep the pane checks — follow the Providers pattern where + guards stay in the handler body. + +### 2. Help sheet generated from the keymap registry + +`texts::tui_help_text*` page-key lines are still hand-written prose. Once +Sessions is migrated, generate the per-page lines from +`keymap::::BINDINGS` (display + label, skipping `shown == never` +aliases) so dispatch, key bars, and help share one source of truth. The +static text should keep the global-keys and text-editing sections. + +### 3. Terminal compatibility: icon fallback (issue #314 class) + +Nav/emoji glyphs (🏠 🔑 …) render double-width on some SSH/legacy +terminals and break border alignment. Plan: + +- `CC_SWITCH_ICONS=ascii|emoji|auto` env override plus a Settings row; +- `auto`: fall back to ASCII markers when the locale is not UTF-8 + (`LC_ALL`/`LC_CTYPE`/`LANG` without `utf-8`), mirroring the + color-mode philosophy — add per-terminal cases, never flip defaults + (see the pinned tests in `theme.rs`). + +### 4. Provider form decomposition (largest remaining UX item) The add/edit form spans ~60 fields across six apps in one scrolling table. Plan: @@ -80,13 +72,13 @@ table. Plan: - Sub-pages already show breadcrumbs; also surface a toast when `Ctrl+S` is ignored on a sub-page (`form_handlers/mod.rs` refuses silently). -### 3. Command palette (optional, largest discoverability win) +### 5. Command palette (optional, largest discoverability win) `Ctrl+P` (or `:`) fuzzy palette over the 24 routes plus per-page intents. The `Route` enum and keymap intent tables make the candidate list nearly free; the work is the overlay UX and dispatch plumbing. -### 4. Key vocabulary leftovers +### 6. Key vocabulary leftovers - Case-pair traps kept for now: Sessions `R` (restore) vs `r` (refresh), Skill detail `s` (sync) vs `S` (sync all). If they cause real @@ -94,12 +86,15 @@ free; the work is the overlay UX and dispatch plumbing. - Usage `P` (pricing) vs Main `p` (proxy) cross-screen overload: tolerated because both are chip-labeled. -### 5. Upstream housekeeping (not TUI, found along the way) — done +### 7. Upstream housekeeping (not TUI, found along the way) -- **done** — `.gitignore` `skills/` anchored to `/skills/` so it no - longer swallows `src/cli/tui/ui/skills/`. -- **done** — deleted `src/cli/i18n/texts/` (the uncompiled divergent copy - of the inline `texts` module in `i18n.rs`). +- `.gitignore` line `skills/` is unanchored and swallows + `src/cli/tui/ui/skills/` — new files there are silently ignored + (`git add` needs `-f`). Should be `/skills/`. +- `src/cli/i18n/texts/` is an uncompiled copy of the inline `texts` + module (nothing declares `mod texts;` against the directory) and has + already diverged from `i18n.rs`. Either finish that split or delete + the directory. ## Conventions for new work diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index 502406b4..7d30e7df 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -2,3 +2,4 @@ # will have compiled files and executables /target/ /gen/schemas +.planning/ diff --git a/src-tauri/src/cli/commands/proxy.rs b/src-tauri/src/cli/commands/proxy.rs index fac12653..86c171e5 100644 --- a/src-tauri/src/cli/commands/proxy.rs +++ b/src-tauri/src/cli/commands/proxy.rs @@ -4,6 +4,7 @@ use crate::app_config::AppType; use crate::cli::proxy_settings::{validate_proxy_listen_address, validate_proxy_listen_port}; use crate::cli::ui::{highlight, info, success}; use crate::error::AppError; +use crate::model_route::ModelRoute; use crate::{AppState, ProxyConfig}; #[cfg(unix)] @@ -13,11 +14,45 @@ use crate::daemon::ipc::protocol::{Request as DaemonRequest, Response as DaemonR #[cfg(unix)] use crate::daemon::supervisor::{DAEMON_SOCKET_ENV, SESSION_TOKEN_ENV}; +#[derive(Subcommand, Debug, Clone)] +pub enum ModelRouteCommand { + /// List model routing rules + List, + /// Add a model routing rule + Add { + /// Wildcard pattern (e.g., *sonnet*, claude-*) + pattern: String, + /// Provider ID to route matching models to + provider_id: String, + /// Priority (lower = higher priority) + #[arg(long, default_value = "0")] + priority: i32, + }, + /// Remove a model routing rule + Remove { id: String }, + /// Toggle a model routing rule on/off + Toggle { id: String }, + /// Update a model routing rule + Update { + id: String, + #[arg(long)] + pattern: Option, + #[arg(long = "provider")] + provider_id: Option, + #[arg(long)] + priority: Option, + }, +} + #[derive(Subcommand, Debug, Clone)] pub enum ProxyCommand { /// Show current proxy configuration and routes Show, + /// Manage model-based routing rules + #[command(subcommand)] + ModelRoute(ModelRouteCommand), + /// Enable the persisted proxy switch Enable, @@ -54,6 +89,10 @@ pub enum ProxyCommand { pub fn execute(cmd: ProxyCommand, app: Option) -> Result<(), AppError> { let app_type = app.unwrap_or(AppType::Claude); match cmd { + ProxyCommand::ModelRoute(subcmd) => { + let state = get_state()?; + handle_model_route(&state, &app_type, subcmd) + } ProxyCommand::Show => show_proxy(), ProxyCommand::Enable => set_proxy_enabled(app_type, true), ProxyCommand::Disable => set_proxy_enabled(app_type, false), @@ -69,6 +108,134 @@ pub fn execute(cmd: ProxyCommand, app: Option) -> Result<(), AppError> } } +fn print_model_routes(routes: &[ModelRoute]) { + if routes.is_empty() { + println!("{}", info("No model routing rules found.")); + return; + } + let mut table = comfy_table::Table::new(); + table.load_preset(comfy_table::presets::UTF8_FULL); + table.set_header(vec!["ID", "Pattern", "Provider", "Priority", "Enabled"]); + for r in routes { + table.add_row(vec![ + r.id.clone(), + r.pattern.clone(), + r.provider_id.clone(), + r.priority.to_string(), + if r.enabled { "yes" } else { "no" }.to_string(), + ]); + } + println!("{table}"); +} + +fn handle_model_route( + state: &AppState, + app: &AppType, + cmd: ModelRouteCommand, +) -> Result<(), AppError> { + match cmd { + ModelRouteCommand::List => { + let routes = state.db.list_model_routes(app.as_str())?; + print_model_routes(&routes); + } + ModelRouteCommand::Add { + pattern, + provider_id, + priority, + } => { + let route = ModelRoute { + id: String::new(), + app_type: app.as_str().to_string(), + pattern: pattern.clone(), + provider_id: provider_id.clone(), + priority, + enabled: true, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }; + let created = state.db.create_model_route(&route)?; + println!( + "{}", + success(&format!( + "Model route created: id={}, pattern=\"{}\" → provider={}, priority={}", + created.id, created.pattern, created.provider_id, created.priority + )) + ); + } + ModelRouteCommand::Remove { id } => { + require_route_for_app(state, &id, app)?; + state.db.delete_model_route(&id)?; + println!("{}", success(&format!("Model route {id} removed."))); + } + ModelRouteCommand::Toggle { id } => { + require_route_for_app(state, &id, app)?; + let toggled = state.db.toggle_model_route(&id)?; + let status = if toggled.enabled { + "enabled" + } else { + "disabled" + }; + println!( + "{}", + success(&format!( + "Model route {id} toggled: pattern=\"{}\" now {status}.", + toggled.pattern + )) + ); + } + ModelRouteCommand::Update { + id, + pattern, + provider_id, + priority, + } => { + let existing = require_route_for_app(state, &id, app)?; + let updated = ModelRoute { + id: existing.id.clone(), + app_type: existing.app_type.clone(), + pattern: pattern.unwrap_or(existing.pattern), + provider_id: provider_id.unwrap_or(existing.provider_id), + priority: priority.unwrap_or(existing.priority), + enabled: existing.enabled, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }; + let result = state.db.update_model_route(&id, &updated)?; + println!( + "{}", + success(&format!( + "Model route {id} updated: pattern=\"{}\" → provider={}, priority={}.", + result.pattern, result.provider_id, result.priority + )) + ); + } + } + Ok(()) +} + +/// 取出路由并校验它属于当前 app,避免 `--app claude` 误删/误改其他 app 的路由。 +fn require_route_for_app( + state: &AppState, + id: &str, + app: &AppType, +) -> Result { + let existing = state + .db + .get_model_route(id)? + .ok_or_else(|| AppError::Database(format!("model route {id} not found")))?; + if existing.app_type != app.as_str() { + return Err(AppError::Database(format!( + "model route {id} belongs to app '{}', not the current app '{}'", + existing.app_type, app + ))); + } + Ok(existing) +} + fn get_state() -> Result { AppState::try_new() } @@ -647,8 +814,15 @@ mod tests { Database, MultiAppConfig, ProxyService, }; - use super::{apply_overrides, build_proxy_overview_lines, load_proxy_app_ports}; + use super::{ + apply_overrides, build_proxy_overview_lines, handle_model_route, load_proxy_app_ports, + ModelRouteCommand, + }; + use crate::app_config::AppType; use crate::cli::proxy_settings::validate_proxy_listen_port; + use crate::database::lock_conn; + use crate::error::AppError; + use crate::model_route::ModelRoute; #[test] fn cli_proxy_listen_port_validation_rejects_reserved_ports() { @@ -805,4 +979,462 @@ mod tests { "proxy show output should not hard-code automatic failover as disabled" ); } + + // --------------------------------------------------------------------------- + // Model-route command tests + // --------------------------------------------------------------------------- + + fn seed_provider(db: &Database, app_type: &str, id: &str) -> Result<(), AppError> { + let conn = lock_conn!(db.conn); + conn.execute( + "INSERT INTO providers (id, app_type, name, settings_config, meta) + VALUES (?1, ?2, ?3, '{}', '{}')", + rusqlite::params![id, app_type, id], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) + } + + #[test] + fn model_route_list_empty_shows_no_routes_message() { + let db = Arc::new(Database::memory().expect("create database")); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let result = handle_model_route(&state, &app, ModelRouteCommand::List); + assert!(result.is_ok(), "list should succeed"); + } + + #[test] + fn model_route_add_and_list_roundtrip() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + // Add a route + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Add { + pattern: "*-4-5".to_string(), + provider_id: "test-prov".to_string(), + priority: 0, + }, + ); + assert!(result.is_ok(), "add should succeed"); + + // Verify via list + let routes = db.list_model_routes("claude").expect("list routes"); + assert_eq!(routes.len(), 1); + let route = &routes[0]; + assert_eq!(route.pattern, "*-4-5"); + assert_eq!(route.provider_id, "test-prov"); + assert!(route.enabled); + } + + #[test] + fn model_route_add_rejects_nonexistent_provider() { + let db = Arc::new(Database::memory().expect("create database")); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Add { + pattern: "*-4-5".to_string(), + provider_id: "nonexistent".to_string(), + priority: 0, + }, + ); + assert!(result.is_err(), "add with nonexistent provider should fail"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("provider") && err.contains("not found"), + "expected provider not found error, got: {err}" + ); + } + + #[test] + fn model_route_add_with_explicit_priority() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Add { + pattern: "*-sonnet".to_string(), + provider_id: "test-prov".to_string(), + priority: 7, + }, + ); + assert!(result.is_ok(), "add with priority should succeed"); + + let routes = db.list_model_routes("claude").expect("list routes"); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].priority, 7); + } + + #[test] + fn model_route_remove_deletes_by_id() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + // Add then remove + let route_id = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".to_string(), + pattern: "*-sonnet".to_string(), + provider_id: "test-prov".to_string(), + priority: 0, + enabled: true, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }) + .expect("create route") + .id; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Remove { + id: route_id.clone(), + }, + ); + assert!(result.is_ok(), "remove should succeed"); + + let routes = db.list_model_routes("claude").expect("list routes"); + assert!(routes.is_empty(), "route should be deleted"); + } + + #[test] + fn model_route_remove_nonexistent_id_errors() { + let db = Arc::new(Database::memory().expect("create database")); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Remove { + id: "missing-route".to_string(), + }, + ); + assert!(result.is_err(), "remove nonexistent should fail"); + } + + #[test] + fn model_route_toggle_flips_enabled() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + // Create an enabled route + let route_id = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".to_string(), + pattern: "*-sonnet".to_string(), + provider_id: "test-prov".to_string(), + priority: 0, + enabled: true, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }) + .expect("create route") + .id; + + // Toggle off + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Toggle { + id: route_id.clone(), + }, + ); + assert!(result.is_ok(), "toggle should succeed"); + + let route = db + .get_model_route(&route_id) + .expect("get route") + .expect("route exists"); + assert!(!route.enabled, "should be disabled after toggle"); + + // Toggle on + handle_model_route( + &state, + &app, + ModelRouteCommand::Toggle { + id: route_id.clone(), + }, + ) + .expect("toggle back"); + let route = db + .get_model_route(&route_id) + .expect("get route") + .expect("route exists"); + assert!(route.enabled, "should be enabled after second toggle"); + } + + #[test] + fn model_route_toggle_nonexistent_id_errors() { + let db = Arc::new(Database::memory().expect("create database")); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Toggle { + id: "missing-route".to_string(), + }, + ); + assert!(result.is_err(), "toggle nonexistent should fail"); + } + + #[test] + fn model_route_update_changes_pattern_only() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let route_id = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".to_string(), + pattern: "original-*".to_string(), + provider_id: "test-prov".to_string(), + priority: 5, + enabled: true, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }) + .expect("create route") + .id; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Update { + id: route_id.clone(), + pattern: Some("new-pattern-*".to_string()), + provider_id: None, + priority: None, + }, + ); + assert!(result.is_ok(), "update pattern should succeed"); + + let route = db + .get_model_route(&route_id) + .expect("get route") + .expect("route exists"); + assert_eq!(route.pattern, "new-pattern-*"); + assert_eq!(route.provider_id, "test-prov"); // unchanged + assert_eq!(route.priority, 5); // unchanged + } + + #[test] + fn model_route_update_changes_provider_only() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + seed_provider(&db, "claude", "other-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let route_id = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".to_string(), + pattern: "*-sonnet".to_string(), + provider_id: "test-prov".to_string(), + priority: 5, + enabled: true, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }) + .expect("create route") + .id; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Update { + id: route_id.clone(), + pattern: None, + provider_id: Some("other-prov".to_string()), + priority: None, + }, + ); + assert!(result.is_ok(), "update provider should succeed"); + + let route = db + .get_model_route(&route_id) + .expect("get route") + .expect("route exists"); + assert_eq!(route.provider_id, "other-prov"); + assert_eq!(route.pattern, "*-sonnet"); // unchanged + } + + #[test] + fn model_route_update_changes_priority_only() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "claude", "test-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let route_id = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".to_string(), + pattern: "*-sonnet".to_string(), + provider_id: "test-prov".to_string(), + priority: 5, + enabled: true, + created_at: None, + hit_count: 0, + last_hit_at: None, + updated_at: None, + }) + .expect("create route") + .id; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Update { + id: route_id.clone(), + pattern: None, + provider_id: None, + priority: Some(99), + }, + ); + assert!(result.is_ok(), "update priority should succeed"); + + let route = db + .get_model_route(&route_id) + .expect("get route") + .expect("route exists"); + assert_eq!(route.priority, 99); + } + + #[test] + fn model_route_update_nonexistent_id_errors() { + let db = Arc::new(Database::memory().expect("create database")); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Claude; + + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Update { + id: "missing-route".to_string(), + pattern: Some("new-*".to_string()), + provider_id: None, + priority: None, + }, + ); + assert!(result.is_err(), "update nonexistent should fail"); + } + + #[test] + fn model_route_with_codex_app_type() { + let db = Arc::new(Database::memory().expect("create database")); + seed_provider(&db, "codex", "codex-prov").expect("seed provider"); + let state = crate::AppState { + db: db.clone(), + config: RwLock::new(MultiAppConfig::default()), + proxy_service: ProxyService::new(db.clone()), + }; + let app = AppType::Codex; + + // Add a codex route + let result = handle_model_route( + &state, + &app, + ModelRouteCommand::Add { + pattern: "gpt-*".to_string(), + provider_id: "codex-prov".to_string(), + priority: 0, + }, + ); + assert!(result.is_ok(), "add codex route should succeed"); + + // Verify stored under codex + let routes = db.list_model_routes("codex").expect("list codex routes"); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].app_type, "codex"); + assert_eq!(routes[0].pattern, "gpt-*"); + + // Codex routes should NOT appear in claude listing + let claude_routes = db.list_model_routes("claude").expect("list claude routes"); + assert!( + claude_routes.is_empty(), + "codex routes should not leak to claude" + ); + } } diff --git a/src-tauri/src/cli/commands/sessions.rs b/src-tauri/src/cli/commands/sessions.rs index e0a183eb..16a96e00 100644 --- a/src-tauri/src/cli/commands/sessions.rs +++ b/src-tauri/src/cli/commands/sessions.rs @@ -9,6 +9,7 @@ use crate::cli::ui::{create_table, info, success, to_json, warning}; use crate::database::Database; use crate::error::AppError; use crate::services::session_usage::SessionSyncResult; +#[allow(unused_imports)] use crate::session_manager::{self, SessionMessage, SessionMeta, SessionSearchHit}; #[derive(Subcommand, Debug, Clone)] diff --git a/src-tauri/src/cli/i18n.rs b/src-tauri/src/cli/i18n.rs index 356aa384..a6491f4d 100644 --- a/src-tauri/src/cli/i18n.rs +++ b/src-tauri/src/cli/i18n.rs @@ -265,9 +265,9 @@ pub mod texts { // Welcome & Headers pub fn welcome_title() -> &'static str { if is_chinese() { - "🎯 CC-Switch 交互模式" + " 🎯 CC-Switch 交互模式" } else { - "🎯 CC-Switch Interactive Mode" + " 🎯 CC-Switch Interactive Mode" } } @@ -571,60 +571,23 @@ pub mod texts { } } - /// The static top of the help sheet: global keys, the text-input line, - /// and the "Page keys" header. The per-page key lines below it are - /// generated from the keymap registry (see `cli::tui::help`), except the - /// static bullets returned by the `tui_help_line_*` functions. - pub fn tui_help_prelude() -> &'static str { + pub fn tui_help_text() -> &'static str { if is_chinese() { - "[ ] 切换应用\n←→ 切换菜单/内容焦点\n↑↓ 或 h/j/k/l 移动\n/ 过滤\nEsc 返回\n? 显示/关闭帮助\n\n文本输入:Ctrl+A/E 行首/行尾,Ctrl+U/K 删除行片段,Ctrl+W 删除前词,Alt+B/F 按词移动\n\n页面快捷键(在页面内容区顶部显示):" + "[ ] 切换应用\n←→ 切换菜单/内容焦点\n↑↓ 或 h/j/k/l 移动\n/ 过滤\nEsc 返回\n? 显示/关闭帮助\n\n文本输入:Ctrl+A/E 行首/行尾,Ctrl+U/K 删除行片段,Ctrl+W 删除前词,Alt+B/F 按词移动\n\n页面快捷键(在页面内容区顶部显示):\n- 供应商:Space 切换,Enter/e 编辑,a 新增,c 复制,d 删除,t 测试,r 刷新,o 临时启动(Claude/Codex),f 管理故障转移(Claude/Codex/Gemini),x 设为默认(OpenClaw)\n- MCP:Space 启用/禁用(当前应用),m 选择应用,a 添加,e 编辑,i 导入已有,d 删除\n- 提示词:Space 启用/禁用,a 新增,Enter 查看,e 编辑,d 删除\n- 会话:Enter 查看,R 恢复,d 删除,r 刷新,a 全部供应商\n- 技能:Enter 详情,Space 启用/禁用(当前应用),m 选择应用,f 发现,i 导入已有,d 卸载\n- 使用统计:1/2/3 时间范围,c 自定义范围,Tab 切换指标,L 明细,P 价格,r 刷新\n- 配置:Enter 打开/执行,e 编辑片段\n- 设置:Enter 应用" } else { - "[ ] switch app\n←→ focus menu/content\n↑↓ or h/j/k/l move\n/ filter\nEsc back\n? toggle help\n\nText input: Ctrl+A/E move line, Ctrl+U/K delete line parts, Ctrl+W delete word, Alt+B/F move word\n\nPage keys (shown at the top of each page):" + "[ ] switch app\n←→ focus menu/content\n↑↓ or h/j/k/l move\n/ filter\nEsc back\n? toggle help\n\nText input: Ctrl+A/E move line, Ctrl+U/K delete line parts, Ctrl+W delete word, Alt+B/F move word\n\nPage keys (shown at the top of each page):\n- Providers: Space switch, Enter/e edit, a add, c copy, d delete, t test, r refresh, o launch temp (Claude/Codex), f manage failover (Claude/Codex/Gemini), x set default (OpenClaw)\n- MCP: Space toggle current, m select apps, a add, e edit, i import existing, d delete\n- Prompts: Space toggle, a add, Enter view, e edit, d delete\n- Sessions: Enter view, R restore, d delete, r refresh, a all providers\n- Skills: Enter details, Space toggle current, m select apps, f discover, i import existing, d uninstall\n- Usage: 1/2/3 range, c custom range, Tab switch metric, L details, P pricing, r reload\n- Config: Enter open/run, e edit snippet\n- Settings: Enter apply" } } - /// The Providers help line (without the leading "- "). Kept hand-written - /// because it carries app-scope annotations ("(OpenClaw)" etc.) that the - /// keymap labels do not, and its keys are app-conditional. - pub fn tui_help_line_providers(app_type: &crate::app_config::AppType) -> &'static str { + pub fn tui_help_text_for_app(app_type: &crate::app_config::AppType) -> &'static str { if matches!(app_type, crate::app_config::AppType::Hermes) { if is_chinese() { - "供应商:Space 添加/移除,Enter/e 编辑,a 新增,c 复制,d 删除,t 测试,r 刷新,x 启用" + "[ ] 切换应用\n←→ 切换菜单/内容焦点\n↑↓ 或 h/j/k/l 移动\n/ 过滤\nEsc 返回\n? 显示/关闭帮助\n\n文本输入:Ctrl+A/E 行首/行尾,Ctrl+U/K 删除行片段,Ctrl+W 删除前词,Alt+B/F 按词移动\n\n页面快捷键(在页面内容区顶部显示):\n- 供应商:Space 添加/移除,Enter/e 编辑,a 新增,c 复制,d 删除,t 测试,r 刷新,x 启用\n- MCP:Space 启用/禁用(当前应用),m 选择应用,a 添加,e 编辑,i 导入已有,d 删除\n- 记忆管理:Enter 编辑,Space/x 启用/禁用,o 打开目录\n- 会话:Enter 查看,R 恢复,d 删除,r 刷新,a 全部供应商\n- 技能:Enter 详情,Space 启用/禁用(当前应用),m 选择应用,f 发现,i 导入已有,d 卸载\n- 使用统计:1/2/3 时间范围,c 自定义范围,Tab 切换指标,L 明细,P 价格,r 刷新\n- 设置:Enter 应用" } else { - "Providers: Space add/remove, Enter/e edit, a add, c copy, d delete, t test, r refresh, x enable" + "[ ] switch app\n←→ focus menu/content\n↑↓ or h/j/k/l move\n/ filter\nEsc back\n? toggle help\n\nText input: Ctrl+A/E move line, Ctrl+U/K delete line parts, Ctrl+W delete word, Alt+B/F move word\n\nPage keys (shown at the top of each page):\n- Providers: Space add/remove, Enter/e edit, a add, c copy, d delete, t test, r refresh, x enable\n- MCP: Space toggle current, m select apps, a add, e edit, i import existing, d delete\n- Memory: Enter edit, Space/x toggle, o open directory\n- Sessions: Enter view, R restore, d delete, r refresh, a all providers\n- Skills: Enter details, Space toggle current, m select apps, f discover, i import existing, d uninstall\n- Usage: 1/2/3 range, c custom range, Tab switch metric, L details, P pricing, r reload\n- Settings: Enter apply" } - } else if is_chinese() { - "供应商:Space 切换,Enter/e 编辑,a 新增,c 复制,d 删除,t 测试,r 刷新,o 临时启动(Claude/Codex),f 管理故障转移(Claude/Codex/Gemini),x 设为默认(OpenClaw)" - } else { - "Providers: Space switch, Enter/e edit, a add, c copy, d delete, t test, r refresh, o launch temp (Claude/Codex), f manage failover (Claude/Codex/Gemini), x set default (OpenClaw)" - } - } - - /// The Hermes-only Memory help line (without the leading "- "). - pub fn tui_help_line_memory() -> &'static str { - if is_chinese() { - "记忆管理:Enter 编辑,Space/x 启用/禁用,o 打开目录" - } else { - "Memory: Enter edit, Space/x toggle, o open directory" - } - } - - /// The Config help line (without the leading "- "). Config has no keymap - /// module yet, so it stays static. - pub fn tui_help_line_config() -> &'static str { - if is_chinese() { - "配置:Enter 打开/执行,e 编辑片段" - } else { - "Config: Enter open/run, e edit snippet" - } - } - - /// The Settings help line (without the leading "- "). - pub fn tui_help_line_settings() -> &'static str { - if is_chinese() { - "设置:Enter 应用" } else { - "Settings: Enter apply" + tui_help_text() } } @@ -4160,39 +4123,6 @@ pub mod texts { } } - pub fn tui_settings_icons_label() -> &'static str { - if is_chinese() { - "图标" - } else { - "Icons" - } - } - - pub fn tui_settings_icon_mode_name(mode: crate::cli::tui::icons::IconMode) -> &'static str { - use crate::cli::tui::icons::IconMode; - if is_chinese() { - match mode { - IconMode::Auto => "自动", - IconMode::Emoji => "表情", - IconMode::Ascii => "ASCII", - } - } else { - match mode { - IconMode::Auto => "Auto", - IconMode::Emoji => "Emoji", - IconMode::Ascii => "ASCII", - } - } - } - - pub fn tui_toast_icons_changed(mode_name: &str) -> String { - if is_chinese() { - format!("图标已切换为{mode_name}") - } else { - format!("Icons set to {mode_name}") - } - } - pub fn tui_settings_header_setting() -> &'static str { if is_chinese() { "设置项" @@ -4233,6 +4163,150 @@ pub mod texts { } } + pub fn tui_settings_model_routes_title() -> &'static str { + if is_chinese() { + "模型路由" + } else { + "Model Routes" + } + } + + pub fn tui_toast_model_route_added() -> &'static str { + if is_chinese() { + "已添加模型路由" + } else { + "Model route added" + } + } + + pub fn tui_toast_model_route_updated() -> &'static str { + if is_chinese() { + "已更新模型路由" + } else { + "Model route updated" + } + } + + pub fn tui_toast_model_route_deleted() -> &'static str { + if is_chinese() { + "已删除模型路由" + } else { + "Model route deleted" + } + } + + pub fn tui_model_route_add_pattern_title() -> &'static str { + if is_chinese() { + "添加模型路由 — 模型模式" + } else { + "Add Model Route — Pattern" + } + } + + pub fn tui_model_route_add_pattern_prompt() -> &'static str { + if is_chinese() { + "输入模型名称模式(如 *-sonnet, gpt-4*)" + } else { + "Enter model name pattern (e.g. *-sonnet, gpt-4*)" + } + } + + pub fn tui_model_route_add_provider_title() -> &'static str { + if is_chinese() { + "添加模型路由 — 供应商" + } else { + "Add Model Route — Provider" + } + } + + pub fn tui_model_route_add_provider_prompt() -> &'static str { + if is_chinese() { + "输入供应商 ID" + } else { + "Enter provider ID" + } + } + + pub fn tui_model_route_add_priority_title() -> &'static str { + if is_chinese() { + "添加模型路由 — 优先级" + } else { + "Add Model Route — Priority" + } + } + + pub fn tui_model_route_add_priority_prompt() -> &'static str { + if is_chinese() { + "输入优先级(数值越小越优先,默认 0)" + } else { + "Enter priority (lower = higher priority, default 0)" + } + } + + pub fn tui_model_route_edit_pattern_title() -> &'static str { + if is_chinese() { + "编辑模型路由 — 模型模式" + } else { + "Edit Model Route — Pattern" + } + } + + pub fn tui_model_route_edit_pattern_prompt() -> &'static str { + if is_chinese() { + "输入模型名称模式" + } else { + "Enter model name pattern" + } + } + + pub fn tui_model_route_edit_provider_title() -> &'static str { + if is_chinese() { + "编辑模型路由 — 供应商" + } else { + "Edit Model Route — Provider" + } + } + + pub fn tui_model_route_edit_provider_prompt() -> &'static str { + if is_chinese() { + "输入供应商 ID" + } else { + "Enter provider ID" + } + } + + pub fn tui_model_route_edit_priority_title() -> &'static str { + if is_chinese() { + "编辑模型路由 — 优先级" + } else { + "Edit Model Route — Priority" + } + } + + pub fn tui_model_route_edit_priority_prompt() -> &'static str { + if is_chinese() { + "输入优先级" + } else { + "Enter priority" + } + } + + pub fn tui_model_route_confirm_delete_message(pattern: &str) -> String { + if is_chinese() { + format!("确认删除模型路由 \"{pattern}\"?此操作不可撤销。") + } else { + format!("Delete model route \"{pattern}\"? This cannot be undone.") + } + } + + pub fn tui_model_route_confirm_delete_title() -> &'static str { + if is_chinese() { + "删除模型路由" + } else { + "Delete Model Route" + } + } + pub fn tui_managed_accounts_not_loaded() -> &'static str { if is_chinese() { "未加载" @@ -11653,22 +11727,20 @@ mod tests { assert_eq!(texts::skills_management(), "技能管理"); assert_eq!(texts::menu_manage_mcp(), "🔌 MCP 服务器"); - // The per-page bullets for MCP/Prompts/Sessions/Skills/Usage are now - // generated from the keymap registry (covered in cli::tui::help - // tests); here we pin the static Chinese pieces the help sheet still - // owns: the prelude and the hand-written Providers/Config/Settings - // lines. - let prelude = texts::tui_help_prelude(); - assert!(prelude.contains("文本输入:Ctrl+A/E 行首/行尾")); - assert!(!prelude.contains("Text input:")); - let providers = texts::tui_help_line_providers(&crate::app_config::AppType::Claude); - assert!(providers.contains("供应商:Space 切换")); - assert!(!providers.contains("供应商详情:")); - assert!(!providers.contains("Providers:")); - assert!(texts::tui_help_line_config().contains("配置:Enter 打开/执行")); - assert!(!texts::tui_help_line_config().contains("Config:")); - assert!(texts::tui_help_line_settings().contains("设置:Enter 应用")); - assert!(!texts::tui_help_line_settings().contains("Settings:")); + let help = texts::tui_help_text(); + assert!(help.contains("文本输入:Ctrl+A/E 行首/行尾")); + assert!(help.contains("供应商:Space 切换")); + assert!(!help.contains("供应商详情:")); + assert!(help.contains("提示词:Space 启用/禁用")); + assert!(help.contains("技能:Enter 详情")); + assert!(help.contains("配置:Enter 打开/执行")); + assert!(help.contains("设置:Enter 应用")); + assert!(!help.contains("Text input:")); + assert!(!help.contains("Providers:")); + assert!(!help.contains("Provider Detail:")); + assert!(!help.contains("Skills:")); + assert!(!help.contains("Config:")); + assert!(!help.contains("Settings:")); } #[test] diff --git a/src-tauri/src/cli/i18n/texts/config_actions.rs b/src-tauri/src/cli/i18n/texts/config_actions.rs new file mode 100644 index 00000000..32bdceca --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/config_actions.rs @@ -0,0 +1,1141 @@ +use super::is_chinese; +pub fn tui_settings_proxy_restart_hint() -> &'static str { + if is_chinese() { + "修改监听地址或端口后,需先停止并重新开启本地代理才能生效" + } else { + "Changes to listen address or port require stopping and restarting the local proxy" + } +} + +pub fn tui_settings_proxy_stop_before_edit_hint(current_app_has_active_worker: bool) -> &'static str { + if is_chinese() { + if current_app_has_active_worker { + "修改监听地址:需先停止本地代理。修改监听端口:需先停止当前应用的代理路由。改完后重新启动路由生效。" + } else { + "修改监听地址:需先停止本地代理。监听端口可以修改。改完后重新启动路由生效。" + } + } else { + if current_app_has_active_worker { + "Listen address: stop the proxy to edit. Listen port: stop this app's route to edit. Restart routing after changes." + } else { + "Listen address: stop the proxy to edit. Listen port can be edited. Restart routing after changes." + } + } +} + +pub fn tui_toast_proxy_listen_address_invalid() -> &'static str { + if is_chinese() { + "地址无效,请输入有效的 IPv4 地址、localhost 或 0.0.0.0" + } else { + "Invalid address. Enter a valid IPv4 address, localhost, or 0.0.0.0" + } +} + +pub fn tui_toast_proxy_listen_port_invalid() -> &'static str { + if is_chinese() { + "端口无效,请输入 1024-65535 之间的数字" + } else { + "Invalid port. Enter a number between 1024 and 65535" + } +} + +pub fn tui_toast_proxy_settings_saved() -> &'static str { + if is_chinese() { + "本地代理配置已保存。" + } else { + "Local proxy settings saved." + } +} + +pub fn tui_toast_proxy_settings_restart_required() -> &'static str { + if is_chinese() { + "本地代理正在运行;新监听地址/端口会在重启代理后生效。" + } else { + "The local proxy is running; the new listen address/port will apply after restart." + } +} + +pub fn tui_toast_proxy_settings_stop_proxy_before_edit_address() -> &'static str { + if is_chinese() { + "本地代理正在运行。请先停止代理,再修改监听地址。" + } else { + "The local proxy is running. Stop it before editing listen address." + } +} + +pub fn tui_toast_proxy_settings_stop_app_route_before_edit_port() -> &'static str { + if is_chinese() { + "当前应用正在使用代理。请先停止当前应用的代理路由,再修改监听端口。" + } else { + "This app is using the proxy. Stop this app's proxy route before editing listen port." + } +} + +pub fn tui_toast_openclaw_config_dir_saved() -> &'static str { + if is_chinese() { + "OpenClaw 配置目录已保存。" + } else { + "OpenClaw config directory saved." + } +} + +pub fn tui_toast_openclaw_config_dir_sync_skipped() -> &'static str { + if is_chinese() { + "目标 OpenClaw 目录尚未初始化;已保存设置,但暂未同步 live 配置。" + } else { + "The target OpenClaw directory is not initialized yet; the setting was saved but live sync was skipped." + } +} + +pub fn tui_toast_openclaw_config_dir_sync_failed(err: &str) -> String { + if is_chinese() { + format!("OpenClaw 配置目录已保存,但同步 live 配置失败: {err}") + } else { + format!("OpenClaw config directory saved, but live sync failed: {err}") + } +} + +pub fn tui_config_title() -> &'static str { + if is_chinese() { + "配置" + } else { + "Configuration" + } +} + +// --------------------------------------------------------------------- +// Ratatui TUI - Skills +// --------------------------------------------------------------------- + +pub fn tui_skills_install_title() -> &'static str { + if is_chinese() { + "安装 Skill" + } else { + "Install Skill" + } +} + +pub fn tui_skills_install_prompt() -> &'static str { + if is_chinese() { + "输入技能目录,或完整标识(owner/name:directory):" + } else { + "Enter a skill directory, or a full key (owner/name:directory):" + } +} + +pub fn tui_skills_uninstall_title() -> &'static str { + if is_chinese() { + "卸载 Skill" + } else { + "Uninstall Skill" + } +} + +pub fn tui_confirm_uninstall_skill_message(name: &str, directory: &str) -> String { + if is_chinese() { + format!("确认卸载 '{name}'({directory})?") + } else { + format!("Uninstall '{name}' ({directory})?") + } +} + +pub fn tui_skills_discover_title() -> &'static str { + if is_chinese() { + "发现 Skills" + } else { + "Discover Skills" + } +} + +pub fn tui_skills_discover_prompt() -> &'static str { + if is_chinese() { + "输入关键词(留空显示全部):" + } else { + "Enter a keyword (leave empty to show all):" + } +} + +pub fn tui_skills_discover_query_empty() -> &'static str { + if is_chinese() { + "全部" + } else { + "all" + } +} + +pub fn tui_skills_discover_hint() -> &'static str { + if is_chinese() { + "按 Tab 切换仓库/skills.sh,按 f 搜索,按 r 管理技能仓库。" + } else { + "Press Tab to switch repositories/skills.sh, f to search, or r to manage repositories." + } +} + +pub fn tui_skills_discover_empty() -> &'static str { + if is_chinese() { + "暂无结果" + } else { + "No results" + } +} + +pub fn tui_skills_skillssh_search_prompt() -> &'static str { + if is_chinese() { + "搜索 skills.sh(至少 2 个字符)..." + } else { + "Search skills.sh (at least 2 characters)..." + } +} + +pub fn tui_skills_source_repos() -> &'static str { + if is_chinese() { + "仓库" + } else { + "Repos" + } +} + +pub fn tui_skills_source_marketplace() -> &'static str { + if is_chinese() { + "skills.sh" + } else { + "skills.sh" + } +} + +pub fn tui_skills_source_switch_hint() -> &'static str { + if is_chinese() { + "Tab 切换来源" + } else { + "Tab to switch source" + } +} + +pub fn tui_key_source() -> &'static str { + if is_chinese() { + "来源" + } else { + "Source" + } +} + +pub fn tui_key_repo_manager() -> &'static str { + if is_chinese() { + "仓库管理" + } else { + "Manage repos" + } +} + +pub fn tui_skills_repos_title() -> &'static str { + if is_chinese() { + "Skill 仓库" + } else { + "Skill Repositories" + } +} + +pub fn tui_skills_repos_hint() -> &'static str { + if is_chinese() { + "技能发现会从这里已启用的仓库加载列表。" + } else { + "Skill discovery loads results from the repositories enabled here." + } +} + +pub fn tui_skills_repos_empty() -> &'static str { + if is_chinese() { + "未配置任何 Skill 仓库。按 a 添加。" + } else { + "No skill repositories configured. Press a to add." + } +} + +pub fn tui_skills_repos_add_title() -> &'static str { + if is_chinese() { + "添加仓库" + } else { + "Add Repository" + } +} + +pub fn tui_skills_repos_add_prompt() -> &'static str { + if is_chinese() { + "输入 GitHub 仓库(owner/name,可选 @branch)或完整 URL:" + } else { + "Enter a GitHub repository (owner/name, optional @branch) or a full URL:" + } +} + +pub fn tui_skills_repos_remove_title() -> &'static str { + if is_chinese() { + "移除仓库" + } else { + "Remove Repository" + } +} + +pub fn tui_confirm_remove_repo_message(owner: &str, name: &str) -> String { + let repo = format!("{owner}/{name}"); + if is_chinese() { + format!("确认移除仓库 '{repo}'?") + } else { + format!("Remove repository '{repo}'?") + } +} + +pub fn tui_skills_unmanaged_title() -> &'static str { + tui_skills_import_title() +} + +pub fn tui_skills_import_title() -> &'static str { + if is_chinese() { + "导入已有技能" + } else { + "Import Existing Skills" + } +} + +pub fn tui_skills_unmanaged_hint() -> &'static str { + tui_skills_import_description() +} + +pub fn tui_skills_import_description() -> &'static str { + if is_chinese() { + "选择要导入到 CC Switch 统一管理的技能。" + } else { + "Select skills to import into CC Switch unified management." + } +} + +pub fn tui_skills_unmanaged_empty() -> &'static str { + if is_chinese() { + "未发现可导入的技能。" + } else { + "No skills to import found." + } +} + +pub fn tui_skills_detail_title() -> &'static str { + if is_chinese() { + "Skill 详情" + } else { + "Skill Detail" + } +} + +pub fn tui_skill_not_found() -> &'static str { + if is_chinese() { + "未找到该 Skill。" + } else { + "Skill not found." + } +} + +pub fn tui_skills_sync_method_label() -> &'static str { + if is_chinese() { + "同步方式" + } else { + "Sync" + } +} + +pub fn tui_skills_sync_method_title() -> &'static str { + if is_chinese() { + "选择同步方式" + } else { + "Select Sync Method" + } +} + +pub fn tui_skills_sync_method_name(method: crate::services::skill::SyncMethod) -> &'static str { + match method { + crate::services::skill::SyncMethod::Auto => { + if is_chinese() { + "自动(优先使用链接,失败时复制)" + } else { + "Automatic (prefer links, fall back to copy)" + } + } + crate::services::skill::SyncMethod::Symlink => { + if is_chinese() { + "仅链接" + } else { + "Links only" + } + } + crate::services::skill::SyncMethod::Copy => { + if is_chinese() { + "仅复制" + } else { + "Copy only" + } + } + } +} + +pub fn tui_skills_installed_summary(installed: usize, enabled: usize, app: &str) -> String { + if is_chinese() { + format!("已安装: {installed} 当前应用({app})已启用: {enabled}") + } else { + format!("Installed: {installed} Enabled for {app}: {enabled}") + } +} + +pub fn tui_skills_installed_counts( + claude: usize, + codex: usize, + gemini: usize, + opencode: usize, + hermes: usize, +) -> String { + if is_chinese() { + format!( + "已安装 · Claude: {claude} · Codex: {codex} · Gemini: {gemini} · OpenCode: {opencode} · Hermes: {hermes}" + ) + } else { + format!( + "Installed · Claude: {claude} · Codex: {codex} · Gemini: {gemini} · OpenCode: {opencode} · Hermes: {hermes}" + ) + } +} + +pub fn tui_mcp_server_counts( + claude: usize, + codex: usize, + gemini: usize, + opencode: usize, + hermes: usize, +) -> String { + if is_chinese() { + format!( + "已安装 · Claude: {claude} · Codex: {codex} · Gemini: {gemini} · OpenCode: {opencode} · Hermes: {hermes}" + ) + } else { + format!( + "Installed · Claude: {claude} · Codex: {codex} · Gemini: {gemini} · OpenCode: {opencode} · Hermes: {hermes}" + ) + } +} + +pub fn tui_mcp_action_import_existing() -> &'static str { + if is_chinese() { + "导入已有" + } else { + "Import Existing" + } +} + +pub fn tui_skills_action_import_existing() -> &'static str { + if is_chinese() { + "导入已有" + } else { + "Import Existing" + } +} + +pub fn tui_skills_empty_title() -> &'static str { + if is_chinese() { + "暂无已安装的技能" + } else { + "No installed skills" + } +} + +pub fn tui_skills_empty_subtitle() -> &'static str { + if is_chinese() { + "从仓库发现并安装技能,或导入已有技能。" + } else { + "Discover and install skills from repositories, or import existing skills." + } +} + +pub fn tui_skills_empty_hint() -> &'static str { + if is_chinese() { + "暂无已安装技能。按 f 发现新技能,或按 i 导入已有技能。" + } else { + "No installed skills. Press f to discover skills, or i to import existing skills." + } +} + +pub fn tui_prompt_no_active_summary() -> &'static str { + if is_chinese() { + "未激活" + } else { + "no active prompt" + } +} + +pub fn tui_prompts_summary(count: usize, active: &str) -> String { + if is_chinese() { + format!("{count} 个提示词 · 当前: {active}") + } else { + format!("{count} prompts · active: {active}") + } +} + +pub fn tui_config_item_export() -> &'static str { + if is_chinese() { + "导出配置" + } else { + "Export Config" + } +} + +pub fn tui_config_item_import() -> &'static str { + if is_chinese() { + "导入配置" + } else { + "Import Config" + } +} + +pub fn tui_config_item_backup() -> &'static str { + if is_chinese() { + "备份配置" + } else { + "Backup Config" + } +} + +pub fn tui_config_item_restore() -> &'static str { + if is_chinese() { + "恢复配置" + } else { + "Restore Config" + } +} + +pub fn tui_config_item_validate() -> &'static str { + if is_chinese() { + "验证配置" + } else { + "Validate Config" + } +} + +pub fn tui_config_item_common_snippet() -> &'static str { + if is_chinese() { + "通用配置片段" + } else { + "Common Config Snippet" + } +} + +pub fn tui_config_item_usage_query() -> &'static str { + if is_chinese() { + "用量查询" + } else { + "Usage Query" + } +} + +pub fn tui_config_item_proxy() -> &'static str { + if is_chinese() { + "本地代理" + } else { + "Local Proxy" + } +} + +pub fn tui_config_item_webdav_sync() -> &'static str { + if is_chinese() { + "WebDAV 同步" + } else { + "WebDAV Sync" + } +} + +pub fn tui_config_item_webdav_settings() -> &'static str { + if is_chinese() { + "WebDAV 同步设置(JSON)" + } else { + "WebDAV Sync Settings (JSON)" + } +} + +pub fn tui_config_item_webdav_check_connection() -> &'static str { + if is_chinese() { + "WebDAV 检查连接" + } else { + "WebDAV Check Connection" + } +} + +pub fn tui_config_item_webdav_upload() -> &'static str { + if is_chinese() { + "WebDAV 上传到远端" + } else { + "WebDAV Upload to Remote" + } +} + +pub fn tui_config_item_webdav_download() -> &'static str { + if is_chinese() { + "WebDAV 下载到本地" + } else { + "WebDAV Download to Local" + } +} + +pub fn tui_config_item_webdav_reset() -> &'static str { + if is_chinese() { + "重置 WebDAV 配置" + } else { + "Reset WebDAV Settings" + } +} + +pub fn tui_config_item_webdav_jianguoyun_quick_setup() -> &'static str { + if is_chinese() { + "坚果云一键配置" + } else { + "Jianguoyun Quick Setup" + } +} + +pub fn tui_webdav_settings_editor_title() -> &'static str { + if is_chinese() { + "编辑 WebDAV 同步设置(JSON)" + } else { + "Edit WebDAV Sync Settings (JSON)" + } +} + +pub fn tui_config_webdav_title() -> &'static str { + if is_chinese() { + "WebDAV 同步" + } else { + "WebDAV Sync" + } +} + +pub fn tui_webdav_jianguoyun_setup_title() -> &'static str { + if is_chinese() { + "坚果云一键配置" + } else { + "Jianguoyun Quick Setup" + } +} + +pub fn tui_webdav_jianguoyun_username_prompt() -> &'static str { + if is_chinese() { + "请输入坚果云账号(通常是邮箱):" + } else { + "Enter your Jianguoyun account (usually email):" + } +} + +pub fn tui_webdav_jianguoyun_app_password_prompt() -> &'static str { + if is_chinese() { + "请输入坚果云第三方应用密码:" + } else { + "Enter your Jianguoyun app password:" + } +} + +pub fn tui_webdav_loading_title_check_connection() -> &'static str { + if is_chinese() { + "WebDAV 检查连接" + } else { + "WebDAV Check Connection" + } +} + +pub fn tui_webdav_loading_title_upload() -> &'static str { + if is_chinese() { + "WebDAV 上传" + } else { + "WebDAV Upload" + } +} + +pub fn tui_webdav_loading_title_download() -> &'static str { + if is_chinese() { + "WebDAV 下载" + } else { + "WebDAV Download" + } +} + +pub fn tui_webdav_loading_title_quick_setup() -> &'static str { + if is_chinese() { + "坚果云一键配置" + } else { + "Jianguoyun Quick Setup" + } +} + +pub fn tui_webdav_loading_message() -> &'static str { + if is_chinese() { + "正在处理 WebDAV 请求,请稍候…" + } else { + "Processing WebDAV request, please wait..." + } +} + +pub fn tui_config_item_reset() -> &'static str { + if is_chinese() { + "重置配置" + } else { + "Reset Config" + } +} + +pub fn tui_config_item_show_full() -> &'static str { + if is_chinese() { + "查看完整配置" + } else { + "Show Full Config" + } +} + +pub fn tui_config_item_show_path() -> &'static str { + if is_chinese() { + "显示配置路径" + } else { + "Show Config Path" + } +} + +pub fn tui_hint_esc_close() -> &'static str { + if is_chinese() { + "Esc = 关闭" + } else { + "Esc = Close" + } +} + +pub fn tui_hint_enter_submit_esc_cancel() -> &'static str { + if is_chinese() { + "Enter = 提交, Esc = 取消" + } else { + "Enter = Submit, Esc = Cancel" + } +} + +pub fn tui_hint_enter_restore_esc_cancel() -> &'static str { + if is_chinese() { + "Enter = 恢复, Esc = 取消" + } else { + "Enter = restore, Esc = cancel" + } +} + +pub fn tui_backup_picker_title() -> &'static str { + if is_chinese() { + "选择备份(Enter 恢复)" + } else { + "Select Backup (Enter to restore)" + } +} + +pub fn tui_speedtest_running(url: &str) -> String { + if is_chinese() { + format!("正在测速: {}", url) + } else { + format!("Running: {}", url) + } +} + +pub fn tui_speedtest_title_with_url(url: &str) -> String { + if is_chinese() { + format!("测速: {}", url) + } else { + format!("Speedtest: {}", url) + } +} + +pub fn tui_stream_check_running(provider_name: &str) -> String { + if is_chinese() { + format!("正在检查: {}", provider_name) + } else { + format!("Checking: {}", provider_name) + } +} + +pub fn tui_stream_check_title_with_provider(provider_name: &str) -> String { + if is_chinese() { + format!("健康检查: {}", provider_name) + } else { + format!("Stream Check: {}", provider_name) + } +} + +pub fn tui_toast_provider_already_in_use() -> &'static str { + if is_chinese() { + "已在使用该供应商。" + } else { + "Already using this provider." + } +} + +pub fn tui_toast_provider_cannot_delete_current() -> &'static str { + if is_chinese() { + "不能删除当前供应商。" + } else { + "Cannot delete current provider." + } +} + +pub fn tui_toast_provider_managed_by_hermes() -> &'static str { + if is_chinese() { + "该供应商由 Hermes 管理,请在 Hermes Web UI 中编辑。" + } else { + "This provider is managed by Hermes. Edit it in the Hermes Web UI." + } +} + +pub fn tui_confirm_delete_provider_title() -> &'static str { + if is_chinese() { + "删除供应商" + } else { + "Delete Provider" + } +} + +pub fn tui_confirm_delete_provider_message(name: &str, id: &str) -> String { + if is_chinese() { + format!("确定删除供应商 '{}' ({})?", name, id) + } else { + format!("Delete provider '{}' ({})?", name, id) + } +} + +pub fn tui_mcp_add_title() -> &'static str { + if is_chinese() { + "新增 MCP 服务器" + } else { + "Add MCP Server" + } +} + +pub fn tui_mcp_edit_title(name: &str) -> String { + if is_chinese() { + format!("编辑 MCP 服务器: {}", name) + } else { + format!("Edit MCP Server: {}", name) + } +} + +pub fn tui_mcp_apps_title(name: &str) -> String { + if is_chinese() { + format!("选择 MCP 应用: {}", name) + } else { + format!("Select MCP Apps: {}", name) + } +} + +pub fn tui_mcp_env_title() -> &'static str { + if is_chinese() { + "MCP 环境变量" + } else { + "MCP Env" + } +} + +pub fn tui_mcp_env_add_entry_title() -> &'static str { + if is_chinese() { + "新增环境变量" + } else { + "Add Env Entry" + } +} + +pub fn tui_mcp_env_edit_entry_title() -> &'static str { + if is_chinese() { + "编辑环境变量" + } else { + "Edit Env Entry" + } +} + +pub fn tui_mcp_env_empty_state() -> &'static str { + if is_chinese() { + "暂无环境变量,按 a 新增。" + } else { + "No env entries yet. Press a to add one." + } +} + +pub fn tui_skill_apps_title(name: &str) -> String { + if is_chinese() { + format!("选择 Skill 应用: {}", name) + } else { + format!("Select Skill Apps: {}", name) + } +} + +pub fn tui_toast_provider_no_api_url() -> &'static str { + if is_chinese() { + "该供应商未配置 API URL。" + } else { + "No API URL configured for this provider." + } +} + +pub fn tui_confirm_delete_mcp_title() -> &'static str { + if is_chinese() { + "删除 MCP 服务器" + } else { + "Delete MCP Server" + } +} + +pub fn tui_confirm_delete_mcp_message(name: &str, id: &str) -> String { + if is_chinese() { + format!("确定删除 MCP 服务器 '{}' ({})?", name, id) + } else { + format!("Delete MCP server '{}' ({})?", name, id) + } +} + +pub fn tui_prompt_title(name: &str) -> String { + if is_chinese() { + format!("提示词: {}", name) + } else { + format!("Prompt: {}", name) + } +} + +pub fn tui_prompt_rename_title() -> &'static str { + if is_chinese() { + "编辑提示词" + } else { + "Edit Prompt" + } +} + +pub fn tui_prompt_create_title() -> &'static str { + if is_chinese() { + "创建提示词" + } else { + "Create Prompt" + } +} + +pub fn tui_prompt_create_prompt() -> &'static str { + if is_chinese() { + "输入提示词名称:" + } else { + "Enter a prompt name:" + } +} + +pub fn tui_prompt_rename_prompt() -> &'static str { + if is_chinese() { + "输入新的提示词名称:" + } else { + "Enter a new prompt name:" + } +} + +pub fn tui_label_prompt_metadata() -> &'static str { + if is_chinese() { + "提示词元信息" + } else { + "Prompt Metadata" + } +} + +pub fn tui_toast_prompt_no_active_to_deactivate() -> &'static str { + if is_chinese() { + "没有可停用的活动提示词。" + } else { + "No active prompt to deactivate." + } +} + +pub fn tui_toast_prompt_cannot_delete_active() -> &'static str { + if is_chinese() { + "不能删除正在启用的提示词。" + } else { + "Cannot delete the active prompt." + } +} + +pub fn tui_confirm_delete_prompt_title() -> &'static str { + if is_chinese() { + "删除提示词" + } else { + "Delete Prompt" + } +} + +pub fn tui_confirm_delete_prompt_message(name: &str, id: &str) -> String { + if is_chinese() { + format!("确定删除提示词 '{}' ({})?", name, id) + } else { + format!("Delete prompt '{}' ({})?", name, id) + } +} + +pub fn tui_confirm_import_prompt_title() -> &'static str { + if is_chinese() { + "导入现有提示词" + } else { + "Import Existing Prompt" + } +} + +pub fn tui_confirm_import_prompt_message(filename: &str) -> String { + if is_chinese() { + format!("当前提示词列表为空,检测到已有 {filename}。是否把它作为新提示词打开编辑?") + } else { + format!( + "The prompt list is empty and {filename} already exists. Open it as a new editable prompt?" + ) + } +} + +pub fn tui_prompt_default_name() -> &'static str { + if is_chinese() { + "默认提示词" + } else { + "Default Prompt" + } +} + +pub fn tui_prompt_imported_description(filename: &str) -> String { + if is_chinese() { + format!("从现有 {filename} 预填") + } else { + format!("Prefilled from existing {filename}") + } +} + +pub fn tui_toast_prompt_import_candidate_missing() -> &'static str { + if is_chinese() { + "没有可导入的现有提示词文件。" + } else { + "No existing prompt file is available to import." + } +} + +pub fn tui_toast_prompt_edit_not_implemented() -> &'static str { + if is_chinese() { + "提示词编辑尚未实现。" + } else { + "Prompt editing not implemented yet." + } +} + +pub fn tui_toast_prompt_edit_finished() -> &'static str { + if is_chinese() { + "提示词编辑完成" + } else { + "Prompt edit finished" + } +} + +pub fn tui_toast_prompt_name_empty() -> &'static str { + if is_chinese() { + "提示词名称不能为空。" + } else { + "Prompt name cannot be empty." + } +} + +pub fn tui_toast_prompt_not_found(id: &str) -> String { + if is_chinese() { + format!("未找到提示词:{}", id) + } else { + format!("Prompt not found: {}", id) + } +} + +pub fn tui_config_paths_title() -> &'static str { + if is_chinese() { + "配置路径" + } else { + "Configuration Paths" + } +} + +pub fn tui_config_paths_config_file(path: &str) -> String { + if is_chinese() { + format!("配置文件: {}", path) + } else { + format!("Config file: {}", path) + } +} + +pub fn tui_config_paths_config_dir(path: &str) -> String { + if is_chinese() { + format!("配置目录: {}", path) + } else { + format!("Config dir: {}", path) + } +} + +pub fn tui_error_failed_to_read_config(e: &str) -> String { + if is_chinese() { + format!("读取配置失败: {e}") + } else { + format!("Failed to read config: {e}") + } +} + +pub fn tui_config_export_title() -> &'static str { + if is_chinese() { + "导出配置" + } else { + "Export Configuration" + } +} + +pub fn tui_config_export_prompt() -> &'static str { + if is_chinese() { + "导出路径:" + } else { + "Export path:" + } +} + +pub fn tui_config_import_title() -> &'static str { + if is_chinese() { + "导入配置" + } else { + "Import Configuration" + } +} + +pub fn tui_config_import_prompt() -> &'static str { + if is_chinese() { + "从路径导入:" + } else { + "Import from path:" + } +} + +pub fn tui_config_backup_title() -> &'static str { + if is_chinese() { + "备份配置" + } else { + "Backup Configuration" + } +} + +pub fn tui_config_backup_prompt() -> &'static str { + if is_chinese() { + "可选名称(留空使用默认值):" + } else { + "Optional name (empty for default):" + } +} + +pub fn tui_toast_no_backups_found() -> &'static str { + if is_chinese() { + "未找到备份。" + } else { + "No backups found." + } +} + +pub fn tui_error_failed_to_read(e: &str) -> String { + if is_chinese() { + format!("读取失败: {e}") + } else { + format!("Failed to read: {e}") + } +} diff --git a/src-tauri/src/cli/i18n/texts/core.rs b/src-tauri/src/cli/i18n/texts/core.rs new file mode 100644 index 00000000..2b484a00 --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/core.rs @@ -0,0 +1,1630 @@ +use super::is_chinese; + +// ============================================ +// ENTITY TYPE CONSTANTS (实体类型常量) +// ============================================ + +pub fn entity_provider() -> &'static str { + if is_chinese() { + "供应商" + } else { + "provider" + } +} + +pub fn entity_server() -> &'static str { + if is_chinese() { + "服务器" + } else { + "server" + } +} + +pub fn entity_prompt() -> &'static str { + if is_chinese() { + "提示词" + } else { + "prompt" + } +} + +// ============================================ +// GENERIC ENTITY OPERATIONS (通用实体操作) +// ============================================ + +pub fn entity_added_success(entity_type: &str, name: &str) -> String { + if is_chinese() { + format!("✓ 成功添加{} '{}'", entity_type, name) + } else { + format!("✓ Successfully added {} '{}'", entity_type, name) + } +} + +pub fn entity_updated_success(entity_type: &str, name: &str) -> String { + if is_chinese() { + format!("✓ 成功更新{} '{}'", entity_type, name) + } else { + format!("✓ Successfully updated {} '{}'", entity_type, name) + } +} + +pub fn entity_deleted_success(entity_type: &str, name: &str) -> String { + if is_chinese() { + format!("✓ 成功删除{} '{}'", entity_type, name) + } else { + format!("✓ Successfully deleted {} '{}'", entity_type, name) + } +} + +pub fn provider_duplicated_success(source_id: &str, duplicate_id: &str) -> String { + if is_chinese() { + format!("✓ 已复制供应商 '{}' 为 '{}'", source_id, duplicate_id) + } else { + format!( + "✓ Duplicated provider '{}' as '{}'", + source_id, duplicate_id + ) + } +} + +pub fn entity_not_found(entity_type: &str, id: &str) -> String { + if is_chinese() { + format!("{}不存在: {}", entity_type, id) + } else { + format!("{} not found: {}", entity_type, id) + } +} + +pub fn confirm_create_entity(entity_type: &str) -> String { + if is_chinese() { + format!("\n确认创建此{}?", entity_type) + } else { + format!("\nConfirm create this {}?", entity_type) + } +} + +pub fn confirm_update_entity(entity_type: &str) -> String { + if is_chinese() { + format!("\n确认更新此{}?", entity_type) + } else { + format!("\nConfirm update this {}?", entity_type) + } +} + +pub fn confirm_delete_entity(entity_type: &str, name: &str) -> String { + if is_chinese() { + format!("\n确认删除{} '{}'?", entity_type, name) + } else { + format!("\nConfirm delete {} '{}'?", entity_type, name) + } +} + +pub fn select_to_delete_entity(entity_type: &str) -> String { + if is_chinese() { + format!("选择要删除的{}:", entity_type) + } else { + format!("Select {} to delete:", entity_type) + } +} + +pub fn no_entities_to_delete(entity_type: &str) -> String { + if is_chinese() { + format!("没有可删除的{}", entity_type) + } else { + format!("No {} available for deletion", entity_type) + } +} + +// ============================================ +// COMMON UI ELEMENTS (通用界面元素) +// ============================================ + +// Welcome & Headers +pub fn welcome_title() -> &'static str { + if is_chinese() { + " 🎯 CC-Switch 交互模式" + } else { + " 🎯 CC-Switch Interactive Mode" + } +} + +pub fn application() -> &'static str { + if is_chinese() { + "应用程序" + } else { + "Application" + } +} + +pub fn goodbye() -> &'static str { + if is_chinese() { + "👋 再见!" + } else { + "👋 Goodbye!" + } +} + +// Main Menu +pub fn main_menu_prompt(app: &str) -> String { + if is_chinese() { + format!("请选择操作 (当前: {})", app) + } else { + format!("What would you like to do? (Current: {})", app) + } +} + +pub fn interactive_requires_tty() -> &'static str { + if is_chinese() { + "交互模式需要在 TTY 终端中运行(请不要通过管道/重定向调用)。" + } else { + "Interactive mode requires a TTY (do not run with pipes/redirection)." + } +} + +pub fn interactive_legacy_tui_removed() -> &'static str { + if is_chinese() { + "旧版 legacy TUI 已移除,请直接使用当前默认的交互 TUI。" + } else { + "The legacy TUI has been removed. Please use the default interactive TUI instead." + } +} + +// Ratatui TUI (new interactive UI) +pub fn tui_app_title() -> &'static str { + "cc-switch" +} + +pub fn tui_tabs_title() -> &'static str { + if is_chinese() { + "App" + } else { + "App" + } +} + +pub fn tui_hint_app_switch() -> &'static str { + if is_chinese() { + "切换 App:" + } else { + "Switch App:" + } +} + +pub fn tui_filter_icon() -> &'static str { + "🔎 " +} + +pub fn tui_marker_active() -> &'static str { + "✓" +} + +pub fn tui_marker_inactive() -> &'static str { + " " +} + +pub fn tui_highlight_symbol() -> &'static str { + "➤ " +} + +pub fn tui_toast_prefix_info() -> &'static str { + " ℹ " +} + +pub fn tui_toast_prefix_success() -> &'static str { + " ✓ " +} + +pub fn tui_toast_prefix_warning() -> &'static str { + " ! " +} + +pub fn tui_toast_prefix_error() -> &'static str { + " ✗ " +} + +pub fn tui_toast_invalid_json(details: &str) -> String { + if is_chinese() { + format!("JSON 无效:{details}") + } else { + format!("Invalid JSON: {details}") + } +} + +pub fn tui_toast_json_must_be_object() -> &'static str { + if is_chinese() { + "JSON 必须是对象(例如:{\"env\":{...}})" + } else { + "JSON must be an object (e.g. {\"env\":{...}})" + } +} + +pub fn tui_error_invalid_config_structure(e: &str) -> String { + if is_chinese() { + format!("配置结构无效:{e}") + } else { + format!("Invalid config structure: {e}") + } +} + +pub fn tui_rule(width: usize) -> String { + if is_chinese() { + "─".repeat(width) + } else { + "─".repeat(width) + } +} + +pub fn tui_rule_heavy(width: usize) -> String { + if is_chinese() { + "═".repeat(width) + } else { + "═".repeat(width) + } +} + +pub fn tui_icon_app() -> &'static str { + "📱" +} + +pub fn tui_default_config_filename() -> &'static str { + "config.json" +} + +pub fn tui_default_config_export_path() -> &'static str { + "./config-export.sql" +} + +pub fn tui_default_common_snippet() -> &'static str { + "{}\n" +} + +pub fn tui_default_common_snippet_for_app(app: &str) -> &'static str { + match app { + "codex" => "", + _ => "{}\n", + } +} + +pub fn tui_latency_ms(ms: u128) -> String { + if is_chinese() { + format!("{ms} ms") + } else { + format!("{ms} ms") + } +} +pub fn tui_nav_title() -> &'static str { + if is_chinese() { + "菜单" + } else { + "Menu" + } +} + +pub fn tui_filter_title() -> &'static str { + if is_chinese() { + "过滤" + } else { + "Filter" + } +} + +pub fn tui_footer_global() -> &'static str { + if is_chinese() { + "[ ] 切换应用 ←→ 切换菜单/内容 ↑↓ 移动 Enter 详情 Space 切换 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app ←→ focus menu/content ↑↓ move Enter details Space switch / filter Esc back ? help" + } +} + +pub fn tui_footer_group_nav() -> &'static str { + if is_chinese() { + "导航" + } else { + "NAV" + } +} + +pub fn tui_footer_group_actions() -> &'static str { + if is_chinese() { + "功能" + } else { + "ACT" + } +} + +pub fn tui_footer_nav_keys() -> &'static str { + if is_chinese() { + "←→ 菜单/内容 ↑↓ 移动" + } else { + "←→ menu/content ↑↓ move" + } +} + +pub fn tui_footer_action_keys() -> &'static str { + if is_chinese() { + "[ ] 切换应用 Enter 详情 Space 切换 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app Enter details Space switch / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_main() -> &'static str { + if is_chinese() { + "[ ] 切换应用 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_providers() -> &'static str { + if is_chinese() { + "[ ] 切换应用 Space 切换 a 新增 e 编辑 d 删除 t 测试 r 刷新 o 临时启动 f 管理故障转移 x 设为默认 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app Space switch a add e edit d delete t test r refresh o launch temp f manage failover x set default / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_mcp() -> &'static str { + if is_chinese() { + "[ ] 切换应用 x 启用/禁用 m 应用 a 添加 e 编辑 i 导入 d 删除 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app x toggle m apps a add e edit i import d delete / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_prompts() -> &'static str { + if is_chinese() { + "[ ] 切换应用 Space 启用/禁用 a 新增 Enter 查看 e 编辑 d 删除 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app Space toggle a add Enter view e edit d delete / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_config() -> &'static str { + if is_chinese() { + "[ ] 切换应用 Enter 打开 e 编辑片段 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app Enter open e edit snippet / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_common_snippet_view() -> &'static str { + if is_chinese() { + "a 应用 c 清空 e 编辑 ↑↓ 滚动 Esc 返回" + } else { + "a apply c clear e edit ↑↓ scroll Esc back" + } +} + +pub fn tui_footer_action_keys_settings() -> &'static str { + if is_chinese() { + "[ ] 切换应用 Enter 应用 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app Enter apply / filter Esc back ? help" + } +} + +pub fn tui_footer_action_keys_global() -> &'static str { + if is_chinese() { + "[ ] 切换应用 / 过滤 Esc 返回 ? 帮助" + } else { + "[ ] switch app / filter Esc back ? help" + } +} + +pub fn tui_footer_filter_mode() -> &'static str { + if is_chinese() { + "输入关键字过滤,Enter 应用,Esc 清空并退出" + } else { + "Type to filter, Enter apply, Esc clear & exit" + } +} + +pub fn tui_help_title() -> &'static str { + if is_chinese() { + "帮助" + } else { + "Help" + } +} + +pub fn tui_help_text() -> &'static str { + if is_chinese() { + "[ ] 切换应用\n←→ 切换菜单/内容焦点\n↑↓ 移动\n/ 过滤\nEsc 返回\n? 显示/关闭帮助\n\n文本输入:Ctrl+A/E 行首/行尾,Ctrl+U/K 删除行片段,Ctrl+W 删除前词,Alt+B/F 按词移动\n\n页面快捷键(在页面内容区顶部显示):\n- 供应商:Space 切换,a 新增,e 编辑,d 删除,t 测试,r 刷新,o 临时启动,f 管理故障转移,x 设为默认\n- MCP:x 启用/禁用(当前应用),m 选择应用,a 添加,e 编辑,i 导入已有,d 删除\n- 提示词:Space 启用/禁用,a 新增,Enter 查看,e 编辑,d 删除\n- 技能:Enter 详情,x 启用/禁用(当前应用),m 选择应用,d 卸载,i 导入已有\n- 配置:Enter 打开/执行,e 编辑片段\n- 设置:Enter 应用" + } else { + "[ ] switch app\n←→ focus menu/content\n↑↓ move\n/ filter\nEsc back\n? toggle help\n\nText input: Ctrl+A/E move line, Ctrl+U/K delete line parts, Ctrl+W delete word, Alt+B/F move word\n\nPage keys (shown at the top of each page):\n- Providers: Space switch, a add, e edit, d delete, t test, r refresh, o launch temp, f manage failover, x set default\n- MCP: x toggle current, m select apps, a add, e edit, i import existing, d delete\n- Prompts: Space toggle, a add, Enter view, e edit, d delete\n- Skills: Enter details, x toggle current, m select apps, d uninstall, i import existing\n- Config: Enter open/run, e edit snippet\n- Settings: Enter apply" + } +} + +pub fn tui_confirm_title() -> &'static str { + if is_chinese() { + "确认" + } else { + "Confirm" + } +} + +pub fn tui_confirm_exit_title() -> &'static str { + if is_chinese() { + "退出" + } else { + "Exit" + } +} + +pub fn tui_confirm_exit_message() -> &'static str { + if is_chinese() { + "确定退出 cc-switch?" + } else { + "Exit cc-switch?" + } +} + +pub fn tui_confirm_yes_hint() -> &'static str { + if is_chinese() { + "y/Enter = 是" + } else { + "y/Enter = Yes" + } +} + +pub fn tui_confirm_no_hint() -> &'static str { + if is_chinese() { + "n/Esc = 否" + } else { + "n/Esc = No" + } +} + +pub fn tui_input_title() -> &'static str { + if is_chinese() { + "输入" + } else { + "Input" + } +} + +pub fn tui_editor_text_field_title() -> &'static str { + if is_chinese() { + "文本" + } else { + "Text" + } +} + +pub fn tui_editor_json_field_title() -> &'static str { + "JSON" +} + +pub fn tui_editor_toml_field_title() -> &'static str { + "TOML" +} + +pub fn tui_editor_hint_view() -> &'static str { + if is_chinese() { + "Enter 编辑 ↑↓ 滚动 Ctrl+S 保存 Esc 返回" + } else { + "Enter edit ↑↓ scroll Ctrl+S save Esc back" + } +} + +pub fn tui_editor_hint_edit() -> &'static str { + if is_chinese() { + "编辑中:Esc 退出编辑 Ctrl+S 保存" + } else { + "Editing: Esc stop editing Ctrl+S save" + } +} + +pub fn tui_editor_discard_title() -> &'static str { + if is_chinese() { + "放弃修改" + } else { + "Discard Changes" + } +} + +pub fn tui_editor_discard_message() -> &'static str { + if is_chinese() { + "有未保存的修改,确定放弃?" + } else { + "You have unsaved changes. Discard them?" + } +} + +pub fn tui_editor_save_before_close_title() -> &'static str { + if is_chinese() { + "当前未保存" + } else { + "Unsaved Changes" + } +} + +pub fn tui_editor_save_before_close_message() -> &'static str { + if is_chinese() { + "当前有未保存的修改。" + } else { + "You have unsaved changes." + } +} + +pub fn tui_speedtest_title() -> &'static str { + if is_chinese() { + "测速" + } else { + "Speedtest" + } +} + +pub fn tui_stream_check_title() -> &'static str { + if is_chinese() { + "健康检查" + } else { + "Stream Check" + } +} + +pub fn tui_provider_test_menu_title() -> &'static str { + if is_chinese() { + "测试" + } else { + "Test" + } +} + +pub fn tui_main_hint() -> &'static str { + if is_chinese() { + "使用左侧菜单(↑↓ + Enter)。←→ 在菜单与内容间切换焦点。" + } else { + "Use the left menu (↑↓ + Enter). ←→ switches focus between menu and content." + } +} + +pub fn tui_header_proxy_status(enabled: bool) -> String { + if is_chinese() { + format!("代理: {}", if enabled { "开" } else { "关" }) + } else { + format!("Proxy: {}", if enabled { "On" } else { "Off" }) + } +} + +pub fn tui_home_section_connection() -> &'static str { + if is_chinese() { + "连接信息" + } else { + "Connection Details" + } +} + +pub fn tui_home_section_proxy() -> &'static str { + if is_chinese() { + "代理仪表盘" + } else { + "Proxy Dashboard" + } +} + +pub fn tui_home_section_context() -> &'static str { + if is_chinese() { + "Session Context" + } else { + "Session Context" + } +} + +pub fn tui_home_section_local_env_check() -> &'static str { + if is_chinese() { + "本地环境检查" + } else { + "Local environment check" + } +} + +pub fn tui_home_section_webdav() -> &'static str { + if is_chinese() { + "WebDAV 同步" + } else { + "WebDAV Sync" + } +} + +pub fn tui_label_webdav_status() -> &'static str { + if is_chinese() { + "状态" + } else { + "Status" + } +} + +pub fn tui_label_webdav_last_sync() -> &'static str { + if is_chinese() { + "最近同步" + } else { + "Last sync" + } +} + +pub fn tui_webdav_status_not_configured() -> &'static str { + if is_chinese() { + "未配置" + } else { + "Not configured" + } +} + +pub fn tui_webdav_status_configured() -> &'static str { + if is_chinese() { + "已配置" + } else { + "Configured" + } +} + +pub fn tui_webdav_status_never_synced() -> &'static str { + if is_chinese() { + "从未同步" + } else { + "Never synced" + } +} + +pub fn tui_webdav_status_ok() -> &'static str { + if is_chinese() { + "正常" + } else { + "OK" + } +} + +pub fn tui_webdav_status_error() -> &'static str { + if is_chinese() { + "失败" + } else { + "Error" + } +} + +pub fn tui_webdav_status_error_with_detail(detail: &str) -> String { + if is_chinese() { + format!("失败({detail})") + } else { + format!("Error ({detail})") + } +} + +pub fn tui_local_env_not_installed() -> &'static str { + if is_chinese() { + "未安装或不可执行" + } else { + "not installed or not executable" + } +} + +pub fn tui_home_status_online() -> &'static str { + if is_chinese() { + "在线" + } else { + "Online" + } +} + +pub fn tui_home_status_offline() -> &'static str { + if is_chinese() { + "离线" + } else { + "Offline" + } +} + +pub fn tui_proxy_dashboard_status_running() -> &'static str { + if is_chinese() { + "已启用" + } else { + "ACTIVE" + } +} + +pub fn tui_proxy_dashboard_status_stopped() -> &'static str { + if is_chinese() { + "本地" + } else { + "LOCAL" + } +} + +pub fn tui_proxy_dashboard_status_local_only() -> &'static str { + if is_chinese() { + "仅本地" + } else { + "LOCAL ONLY" + } +} + +pub fn tui_proxy_dashboard_status_unsupported() -> &'static str { + if is_chinese() { + "不支持" + } else { + "UNSUPPORTED" + } +} + +pub fn tui_proxy_dashboard_manual_routing_copy(app: &str) -> String { + if is_chinese() { + format!("手动路由:{app} 的流量会通过 cc-switch。") + } else { + format!("Manual routing only: traffic goes through cc-switch for {app}.") + } +} + +pub fn tui_proxy_dashboard_failover_copy() -> &'static str { + if is_chinese() { + "仅做手动路由,不会自动切换供应商。" + } else { + "automatic failover stays off; provider changes stay manual." + } +} + +pub fn tui_proxy_dashboard_cta_start(app: &str) -> String { + if is_chinese() { + format!("按 P 启动托管代理,并让 {app} 走 cc-switch。") + } else { + format!("Press P to start the managed proxy and route {app} through cc-switch.") + } +} + +pub fn tui_proxy_dashboard_cta_stop(app: &str) -> String { + if is_chinese() { + format!("按 P 恢复 {app} 的 live 配置,并停止托管代理。") + } else { + format!("Press P to restore {app} to its live config and stop the managed proxy.") + } +} + +pub fn tui_proxy_loading_title_start() -> &'static str { + if is_chinese() { + "启动代理中" + } else { + "Starting proxy" + } +} + +pub fn tui_proxy_loading_title_stop() -> &'static str { + if is_chinese() { + "停止代理中" + } else { + "Stopping proxy" + } +} + +pub fn tui_proxy_dashboard_running_elsewhere() -> &'static str { + if is_chinese() { + "代理已在运行。请先停止当前路由,再从这里启动。" + } else { + "Proxy is already running. Stop the current route before starting it here." + } +} + +pub fn tui_proxy_dashboard_current_app_on(app: &str) -> String { + if is_chinese() { + format!("{app} 已接入代理") + } else { + format!("{app} active") + } +} + +pub fn tui_proxy_dashboard_current_app_off(app: &str) -> String { + if is_chinese() { + format!("{app} 本地直连") + } else { + format!("{app} local") + } +} + +pub fn tui_proxy_dashboard_unsupported_app(app: &str) -> String { + if is_chinese() { + format!("{app} 仅本地") + } else { + format!("{app} local only") + } +} + +pub fn tui_proxy_dashboard_shared_runtime_ready() -> &'static str { + if is_chinese() { + "共享 runtime 就绪" + } else { + "Shared runtime ready" + } +} + +pub fn tui_proxy_dashboard_no_route_for_app(app: &str) -> String { + if is_chinese() { + format!("{app} 暂无路由") + } else { + format!("No route for {app} yet") + } +} + +pub fn tui_proxy_dashboard_takeover_active() -> &'static str { + if is_chinese() { + "已接管" + } else { + "active" + } +} + +pub fn tui_proxy_dashboard_takeover_inactive() -> &'static str { + if is_chinese() { + "未接管" + } else { + "inactive" + } +} + +pub fn tui_proxy_dashboard_takeover_unsupported() -> &'static str { + if is_chinese() { + "不支持" + } else { + "not supported" + } +} + +pub fn tui_proxy_dashboard_uptime_stopped() -> &'static str { + if is_chinese() { + "未运行" + } else { + "--" + } +} + +pub fn tui_proxy_dashboard_requests_idle() -> &'static str { + if is_chinese() { + "暂无流量" + } else { + "No traffic yet" + } +} + +pub fn tui_proxy_dashboard_tokens_idle() -> &'static str { + if is_chinese() { + "暂无 token 流量" + } else { + "No token traffic yet" + } +} + +pub fn tui_proxy_dashboard_target_waiting() -> &'static str { + if is_chinese() { + "等待首个请求" + } else { + "Waiting for first request" + } +} + +pub fn tui_proxy_dashboard_request_summary(total: u64, success_rate: f32) -> String { + if is_chinese() { + format!("{total} 总计 / {success_rate:.1}% 成功") + } else { + format!("{total} total / {success_rate:.1}% success") + } +} + +pub fn tui_proxy_dashboard_token_summary(output: &str, input: &str) -> String { + if is_chinese() { + format!("{output} 下行 / {input} 上行") + } else { + format!("{output} out / {input} in") + } +} + +pub fn tui_label_current_app_takeover() -> &'static str { + if is_chinese() { + "当前应用接管" + } else { + "Current app takeover" + } +} + +pub fn tui_label_current_app_route() -> &'static str { + if is_chinese() { + "当前应用路由" + } else { + "Current app route" + } +} + +pub fn tui_label_latest_proxy_route() -> &'static str { + if is_chinese() { + "最近代理路由" + } else { + "Latest proxy route" + } +} + +pub fn tui_label_shared_runtime() -> &'static str { + if is_chinese() { + "共享 runtime" + } else { + "Shared runtime" + } +} + +pub fn tui_label_listen() -> &'static str { + if is_chinese() { + "监听" + } else { + "Listen" + } +} + +pub fn tui_label_uptime() -> &'static str { + if is_chinese() { + "运行时长" + } else { + "Uptime" + } +} + +pub fn tui_label_requests() -> &'static str { + if is_chinese() { + "请求" + } else { + "Requests" + } +} + +pub fn tui_label_traffic() -> &'static str { + if is_chinese() { + "流量" + } else { + "Traffic" + } +} + +pub fn tui_label_proxy_requests() -> &'static str { + if is_chinese() { + "代理总请求" + } else { + "Proxy requests" + } +} + +pub fn tui_label_active_target() -> &'static str { + if is_chinese() { + "当前路由目标" + } else { + "Active target" + } +} + +pub fn tui_label_last_error() -> &'static str { + if is_chinese() { + "最近错误" + } else { + "Last error" + } +} + +pub fn tui_label_last_proxy_error() -> &'static str { + if is_chinese() { + "最近一次代理错误" + } else { + "Last proxy error" + } +} + +pub fn tui_label_mcp_servers_active() -> &'static str { + if is_chinese() { + "已启用" + } else { + "Active" + } +} + +pub fn tui_na() -> &'static str { + "N/A" +} + +pub fn tui_loading() -> &'static str { + if is_chinese() { + "处理中…" + } else { + "Working…" + } +} + +pub fn tui_header_quota() -> &'static str { + if is_chinese() { + "额度" + } else { + "Quota" + } +} + +pub fn tui_label_quota() -> &'static str { + if is_chinese() { + "额度" + } else { + "Quota" + } +} + +pub fn tui_label_provider_proxy() -> &'static str { + if is_chinese() { + "代理" + } else { + "Proxy" + } +} + +pub fn tui_provider_needs_proxy_label() -> &'static str { + if is_chinese() { + "需要代理" + } else { + "Needs Proxy" + } +} + +pub fn tui_provider_no_proxy_support_label() -> &'static str { + if is_chinese() { + "不支持代理" + } else { + "No Proxy Support" + } +} + +pub fn tui_quota_loading() -> &'static str { + if is_chinese() { + "查询中…" + } else { + "checking…" + } +} + +pub fn tui_quota_not_available() -> &'static str { + if is_chinese() { + "不可用" + } else { + "not available" + } +} + +pub fn tui_quota_parse_error() -> &'static str { + if is_chinese() { + "凭据解析失败" + } else { + "credential parse failed" + } +} + +pub fn tui_quota_expired() -> &'static str { + if is_chinese() { + "登录过期" + } else { + "login expired" + } +} + +pub fn tui_quota_query_failed() -> &'static str { + if is_chinese() { + "查询失败" + } else { + "query failed" + } +} + +pub fn tui_quota_not_queried() -> &'static str { + if is_chinese() { + "未查询" + } else { + "not queried" + } +} + +pub fn tui_quota_refresh_hint() -> &'static str { + if is_chinese() { + "按 r 刷新" + } else { + "press r to refresh" + } +} + +pub fn tui_quota_ok() -> &'static str { + if is_chinese() { + "已获取" + } else { + "ok" + } +} + +pub fn tui_quota_last_checked() -> &'static str { + if is_chinese() { + "更新于" + } else { + "checked" + } +} + +pub fn tui_quota_resets_in(time: &str) -> String { + if is_chinese() { + format!("{time} 后重置") + } else { + format!("resets in {time}") + } +} + +pub fn tui_quota_just_now() -> &'static str { + if is_chinese() { + "刚刚" + } else { + "just now" + } +} + +pub fn tui_quota_seconds_ago(count: i64) -> String { + if is_chinese() { + format!("{count} 秒前") + } else if count == 1 { + "1 second ago".to_string() + } else { + format!("{count} seconds ago") + } +} + +pub fn tui_quota_minutes_ago(count: i64) -> String { + if is_chinese() { + format!("{count} 分钟前") + } else if count == 1 { + "1 minute ago".to_string() + } else { + format!("{count} minutes ago") + } +} + +pub fn tui_quota_hours_ago(count: i64) -> String { + if is_chinese() { + format!("{count} 小时前") + } else if count == 1 { + "1 hour ago".to_string() + } else { + format!("{count} hours ago") + } +} + +pub fn tui_quota_days_ago(count: i64) -> String { + if is_chinese() { + format!("{count} 天前") + } else if count == 1 { + "1 day ago".to_string() + } else { + format!("{count} days ago") + } +} + +pub fn tui_quota_extra_usage() -> &'static str { + if is_chinese() { + "额外用量" + } else { + "Extra usage" + } +} + +pub fn tui_quota_tier_five_hour() -> &'static str { + if is_chinese() { + "5小时" + } else { + "5h" + } +} + +pub fn tui_quota_tier_seven_day() -> &'static str { + if is_chinese() { + "7天" + } else { + "7d" + } +} + +pub fn tui_quota_tier_seven_day_opus() -> &'static str { + if is_chinese() { + "7天 Opus" + } else { + "7d Opus" + } +} + +pub fn tui_quota_tier_seven_day_sonnet() -> &'static str { + if is_chinese() { + "7天 Sonnet" + } else { + "7d Sonnet" + } +} + +pub fn tui_quota_tier_weekly_limit() -> &'static str { + if is_chinese() { + "周额度" + } else { + "weekly" + } +} + +pub fn tui_quota_tier_premium() -> &'static str { + "premium" +} + +pub fn tui_quota_tier_gemini_pro() -> &'static str { + "Gemini Pro" +} + +pub fn tui_quota_tier_gemini_flash() -> &'static str { + "Gemini Flash" +} + +pub fn tui_quota_tier_gemini_flash_lite() -> &'static str { + "Gemini Flash Lite" +} + +pub fn tui_header_id() -> &'static str { + "ID" +} + +pub fn tui_header_api_url() -> &'static str { + "API URL" +} + +pub fn tui_header_directory() -> &'static str { + if is_chinese() { + "目录" + } else { + "Directory" + } +} + +pub fn tui_header_repo() -> &'static str { + if is_chinese() { + "仓库" + } else { + "Repo" + } +} + +pub fn tui_header_branch() -> &'static str { + if is_chinese() { + "分支" + } else { + "Branch" + } +} + +pub fn tui_header_path() -> &'static str { + if is_chinese() { + "路径" + } else { + "Path" + } +} + +pub fn tui_header_found_in() -> &'static str { + if is_chinese() { + "发现于" + } else { + "Found In" + } +} + +pub fn tui_header_field() -> &'static str { + if is_chinese() { + "字段" + } else { + "Field" + } +} + +pub fn tui_header_value() -> &'static str { + if is_chinese() { + "值" + } else { + "Value" + } +} + +pub fn tui_header_claude_short() -> &'static str { + "C" +} + +pub fn tui_header_codex_short() -> &'static str { + "X" +} + +pub fn tui_header_gemini_short() -> &'static str { + "G" +} + +pub fn tui_header_opencode_short() -> &'static str { + "O" +} + +pub fn tui_label_id() -> &'static str { + "ID" +} + +pub fn tui_label_api_url() -> &'static str { + "API URL" +} + +pub fn tui_label_directory() -> &'static str { + if is_chinese() { + "目录" + } else { + "Directory" + } +} + +pub fn tui_label_enabled_for() -> &'static str { + if is_chinese() { + "已启用" + } else { + "Enabled" + } +} + +pub fn tui_label_repo() -> &'static str { + if is_chinese() { + "仓库" + } else { + "Repo" + } +} + +pub fn tui_label_readme() -> &'static str { + if is_chinese() { + "README" + } else { + "README" + } +} + +pub fn tui_label_base_url() -> &'static str { + if is_chinese() { + "API 请求地址" + } else { + "Base URL" + } +} + +pub fn tui_label_api_key() -> &'static str { + if is_chinese() { + "API Key" + } else { + "API Key" + } +} + +pub fn tui_label_claude_api_format() -> &'static str { + if is_chinese() { + "API 格式" + } else { + "API Format" + } +} + +pub fn tui_claude_api_format_value(api_format: &str) -> &'static str { + match api_format { + "openai_chat" => { + if is_chinese() { + "OpenAI Chat Completions (需开启代理)" + } else { + "OpenAI Chat Completions (Requires proxy)" + } + } + "openai_responses" => { + if is_chinese() { + "OpenAI Responses API (需开启代理)" + } else { + "OpenAI Responses API (Requires proxy)" + } + } + "gemini_native" => { + if is_chinese() { + "Gemini Native generateContent (需开启代理)" + } else { + "Gemini Native generateContent (Requires proxy)" + } + } + _ => { + if is_chinese() { + "Anthropic Messages (原生)" + } else { + "Anthropic Messages (Native)" + } + } + } +} + +pub fn tui_codex_api_format_value(api_format: &str) -> &'static str { + match api_format { + "openai_chat" => { + if is_chinese() { + "OpenAI Chat Completions (需本地路由)" + } else { + "OpenAI Chat Completions (Local routing)" + } + } + _ => { + if is_chinese() { + "OpenAI Responses API (原生)" + } else { + "OpenAI Responses API (Native)" + } + } + } +} + +pub fn tui_claude_api_format_requires_proxy_title() -> &'static str { + if is_chinese() { + "需开启代理" + } else { + "Proxy Required" + } +} + +pub fn tui_claude_api_format_requires_proxy_message(api_format: &str) -> String { + let label = tui_claude_api_format_value(api_format); + if is_chinese() { + format!("已切换为 {label}。\n该格式需开启本地代理使用。\n请在主页按 P 打开本地代理。") + } else { + format!("Switched to {label}.\nThis format requires the local proxy.\nPress P on the home page to open local proxy.") + } +} + +pub fn tui_codex_api_format_requires_proxy_message(api_format: &str) -> String { + let label = tui_codex_api_format_value(api_format); + if is_chinese() { + format!("已切换为 {label}。\n该格式需要本地路由映射。\n使用此供应商时请保持本地代理开启。") + } else { + format!("Switched to {label}.\nThis format requires local route mapping.\nKeep the local proxy enabled while using this provider.") + } +} + +pub fn tui_label_codex_local_routing() -> &'static str { + if is_chinese() { + "本地路由" + } else { + "Local Routing" + } +} + +pub fn tui_label_codex_upstream_format() -> &'static str { + if is_chinese() { + "上游格式" + } else { + "Upstream format" + } +} + +pub fn tui_label_codex_model_mapping() -> &'static str { + if is_chinese() { + "模型映射" + } else { + "Model mapping" + } +} + +pub fn tui_codex_local_routing_title(provider: &str) -> String { + let title = tui_label_codex_local_routing(); + if provider.trim().is_empty() { + title.to_string() + } else { + format!("{title} - {provider}") + } +} + +pub fn tui_codex_local_routing_enable() -> &'static str { + if is_chinese() { + "启用本地路由" + } else { + "Enable Local Routing" + } +} + +pub fn tui_codex_reasoning_supports_thinking() -> &'static str { + if is_chinese() { + "支持思考模式" + } else { + "Supports Thinking" + } +} + +pub fn tui_codex_reasoning_supports_effort() -> &'static str { + if is_chinese() { + "支持思考等级" + } else { + "Supports Reasoning Effort" + } +} + +pub fn tui_codex_model_catalog() -> &'static str { + if is_chinese() { + "模型映射" + } else { + "Model Mapping" + } +} + +pub fn tui_codex_model_catalog_title(provider: &str) -> String { + if is_chinese() { + if provider.trim().is_empty() { + "模型映射".to_string() + } else { + format!("模型映射 - {provider}") + } + } else if provider.trim().is_empty() { + "Model Mapping".to_string() + } else { + format!("Model Mapping - {provider}") + } +} + +pub fn tui_codex_model_catalog_model_header() -> &'static str { + if is_chinese() { + "模型" + } else { + "Model" + } +} + +pub fn tui_codex_model_catalog_display_header() -> &'static str { + if is_chinese() { + "显示名称" + } else { + "Display" + } +} + +pub fn tui_codex_model_catalog_context_header() -> &'static str { + if is_chinese() { + "上下文" + } else { + "Context" + } +} + +pub fn tui_codex_model_catalog_empty() -> &'static str { + if is_chinese() { + "暂无模型映射" + } else { + "No model mappings" + } +} + +pub fn tui_codex_model_catalog_model_prompt() -> &'static str { + if is_chinese() { + "模型 ID" + } else { + "Model ID" + } +} + +pub fn tui_codex_model_catalog_display_prompt() -> &'static str { + if is_chinese() { + "显示名称" + } else { + "Display Name" + } +} + +pub fn tui_codex_model_catalog_context_prompt() -> &'static str { + if is_chinese() { + "上下文窗口" + } else { + "Context Window" + } +} + +pub fn tui_codex_model_catalog_preview_title() -> &'static str { + if is_chinese() { + "模型映射" + } else { + "Model Mapping" + } +} diff --git a/src-tauri/src/cli/i18n/texts/menu_skills.rs b/src-tauri/src/cli/i18n/texts/menu_skills.rs new file mode 100644 index 00000000..6b05e0b7 --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/menu_skills.rs @@ -0,0 +1,360 @@ +use super::is_chinese; +pub fn menu_home() -> &'static str { + let (en, zh) = menu_home_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_home_variants() -> (&'static str, &'static str) { + ("🏠 Home", "🏠 首页") +} + +pub fn menu_manage_providers() -> &'static str { + let (en, zh) = menu_manage_providers_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_manage_providers_variants() -> (&'static str, &'static str) { + ("🔑 Providers", "🔑 供应商") +} + +pub fn menu_usage() -> &'static str { + let (en, zh) = menu_usage_variants(); + if is_chinese() { zh } else { en } +} + +pub fn menu_usage_variants() -> (&'static str, &'static str) { + ("📊 Usage", "📊 使用统计") +} + +pub fn menu_pricing() -> &'static str { + let (en, zh) = menu_pricing_variants(); + if is_chinese() { zh } else { en } +} + +pub fn menu_pricing_variants() -> (&'static str, &'static str) { + ("💵 Pricing", "💵 模型定价") +} + +pub fn menu_manage_mcp() -> &'static str { + let (en, zh) = menu_manage_mcp_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_manage_mcp_variants() -> (&'static str, &'static str) { + ("🔌 MCP Servers", "🔌 MCP 服务器") +} + +pub fn menu_manage_prompts() -> &'static str { + let (en, zh) = menu_manage_prompts_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_manage_prompts_variants() -> (&'static str, &'static str) { + ("💬 Prompts", "💬 提示词") +} + +pub fn menu_manage_config() -> &'static str { + let (en, zh) = menu_manage_config_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_manage_config_variants() -> (&'static str, &'static str) { + ("📋 Configuration", "📋 配置") +} + +pub fn menu_manage_skills() -> &'static str { + let (en, zh) = menu_manage_skills_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_manage_skills_variants() -> (&'static str, &'static str) { + ("🧩 Skills", "🧩 技能") +} + +pub fn menu_settings() -> &'static str { + let (en, zh) = menu_settings_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_settings_variants() -> (&'static str, &'static str) { + ("🔧 Settings", "🔧 设置") +} + +pub fn menu_exit() -> &'static str { + let (en, zh) = menu_exit_variants(); + if is_chinese() { + zh + } else { + en + } +} + +pub fn menu_exit_variants() -> (&'static str, &'static str) { + ("🚪 Exit", "🚪 退出") +} + +// ============================================ +// SKILLS (Skills) +// ============================================ + +pub fn skills_management() -> &'static str { + if is_chinese() { + "技能管理" + } else { + "Skills Management" + } +} + +pub fn no_skills_installed() -> &'static str { + if is_chinese() { + "未安装任何 Skills。" + } else { + "No skills installed." + } +} + +pub fn skills_discover() -> &'static str { + if is_chinese() { + "🔎 发现/搜索 Skills" + } else { + "🔎 Discover/Search Skills" + } +} + +pub fn skills_install() -> &'static str { + if is_chinese() { + "⬇️ 安装 Skill" + } else { + "⬇️ Install Skill" + } +} + +pub fn skills_uninstall() -> &'static str { + if is_chinese() { + "🗑️ 卸载 Skill" + } else { + "🗑️ Uninstall Skill" + } +} + +pub fn skills_toggle_for_app() -> &'static str { + if is_chinese() { + "✅ 启用/禁用(当前应用)" + } else { + "✅ Enable/Disable (Current App)" + } +} + +pub fn skills_show_info() -> &'static str { + if is_chinese() { + "ℹ️ 查看 Skill 信息" + } else { + "ℹ️ Skill Info" + } +} + +pub fn skills_sync_now() -> &'static str { + if is_chinese() { + "🔄 同步 Skills 到本地" + } else { + "🔄 Sync Skills to Live" + } +} + +pub fn skills_sync_method() -> &'static str { + if is_chinese() { + "🔗 同步方式(auto/symlink/copy)" + } else { + "🔗 Sync Method (auto/symlink/copy)" + } +} + +pub fn skills_select_sync_method() -> &'static str { + if is_chinese() { + "选择同步方式:" + } else { + "Select sync method:" + } +} + +pub fn skills_current_sync_method(method: &str) -> String { + if is_chinese() { + format!("当前同步方式:{method}") + } else { + format!("Current sync method: {method}") + } +} + +pub fn skills_current_app_note(app: &str) -> String { + if is_chinese() { + format!("提示:启用/禁用将作用于当前应用({app})。") + } else { + format!("Note: Enable/Disable applies to the current app ({app}).") + } +} + +pub fn skills_scan_unmanaged() -> &'static str { + if is_chinese() { + "🕵️ 查找已有技能" + } else { + "🕵️ Find Existing Skills" + } +} + +pub fn skills_import_from_apps() -> &'static str { + if is_chinese() { + "📥 导入已有技能" + } else { + "📥 Import Existing Skills" + } +} + +pub fn skills_manage_repos() -> &'static str { + if is_chinese() { + "📦 管理技能仓库" + } else { + "📦 Manage Skill Repos" + } +} + +pub fn skills_enter_query() -> &'static str { + if is_chinese() { + "输入搜索关键词(可选):" + } else { + "Enter search query (optional):" + } +} + +pub fn skills_enter_install_spec() -> &'static str { + if is_chinese() { + "输入技能目录,或完整标识(owner/name:directory):" + } else { + "Enter a skill directory, or a full key (owner/name:directory):" + } +} + +pub fn skills_select_skill() -> &'static str { + if is_chinese() { + "选择一个 Skill:" + } else { + "Select a skill:" + } +} + +pub fn skills_confirm_install(name: &str, app: &str) -> String { + if is_chinese() { + format!("确认安装 '{name}' 并启用到 {app}?") + } else { + format!("Install '{name}' and enable for {app}?") + } +} + +pub fn skills_confirm_uninstall(name: &str) -> String { + if is_chinese() { + format!("确认卸载 '{name}'?") + } else { + format!("Uninstall '{name}'?") + } +} + +pub fn skills_confirm_toggle(name: &str, app: &str, enabled: bool) -> String { + if is_chinese() { + if enabled { + format!("确认启用 '{name}' 到 {app}?") + } else { + format!("确认在 {app} 禁用 '{name}'?") + } + } else if enabled { + format!("Enable '{name}' for {app}?") + } else { + format!("Disable '{name}' for {app}?") + } +} + +pub fn skills_no_unmanaged_found() -> &'static str { + if is_chinese() { + "未发现可导入的技能。所有技能已在 CC Switch 中统一管理。" + } else { + "No skills to import found. All skills are already managed by CC Switch." + } +} + +pub fn skills_select_unmanaged_to_import() -> &'static str { + if is_chinese() { + "选择要导入的技能:" + } else { + "Select skills to import:" + } +} + +pub fn skills_repos_management() -> &'static str { + if is_chinese() { + "技能仓库管理" + } else { + "Skill Repos" + } +} + +pub fn skills_repo_list() -> &'static str { + if is_chinese() { + "📋 查看仓库列表" + } else { + "📋 List Repos" + } +} + +pub fn skills_repo_add() -> &'static str { + if is_chinese() { + "➕ 添加仓库" + } else { + "➕ Add Repo" + } +} + +pub fn skills_repo_remove() -> &'static str { + if is_chinese() { + "➖ 移除仓库" + } else { + "➖ Remove Repo" + } +} + +pub fn skills_repo_enter_spec() -> &'static str { + if is_chinese() { + "输入 GitHub 仓库(owner/name,可选 @branch)或完整 URL:" + } else { + "Enter a GitHub repository (owner/name, optional @branch) or a full URL:" + } +} + +// ============================================ +// PROVIDER MANAGEMENT (供应商管理) +// ============================================ diff --git a/src-tauri/src/cli/i18n/texts/mod.rs b/src-tauri/src/cli/i18n/texts/mod.rs new file mode 100644 index 00000000..5926c1c2 --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/mod.rs @@ -0,0 +1,21 @@ +pub(super) use super::is_chinese; + +mod config_actions; +mod core; +mod menu_skills; +mod provider_editor; +mod provider_management; +mod providers; +mod settings_misc; +mod toasts; +mod update; + +pub use config_actions::*; +pub use core::*; +pub use menu_skills::*; +pub use provider_editor::*; +pub use provider_management::*; +pub use providers::*; +pub use settings_misc::*; +pub use toasts::*; +pub use update::*; diff --git a/src-tauri/src/cli/i18n/texts/provider_editor.rs b/src-tauri/src/cli/i18n/texts/provider_editor.rs new file mode 100644 index 00000000..50e55bfb --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/provider_editor.rs @@ -0,0 +1,605 @@ +use super::is_chinese; +pub fn id_label() -> &'static str { + if is_chinese() { + "ID" + } else { + "ID" + } +} + +pub fn website_label() -> &'static str { + if is_chinese() { + "官网" + } else { + "Website" + } +} + +pub fn core_config_label() -> &'static str { + if is_chinese() { + "核心配置:" + } else { + "Core Configuration:" + } +} + +pub fn model_label() -> &'static str { + if is_chinese() { + "模型" + } else { + "Model" + } +} + +pub fn config_toml_lines(count: usize) -> String { + if is_chinese() { + format!("Config (TOML): {} 行", count) + } else { + format!("Config (TOML): {} lines", count) + } +} + +pub fn optional_fields_label() -> &'static str { + if is_chinese() { + "可选字段:" + } else { + "Optional Fields:" + } +} + +pub fn notes_label_colon() -> &'static str { + if is_chinese() { + "备注" + } else { + "Notes" + } +} + +pub fn sort_index_label_colon() -> &'static str { + if is_chinese() { + "排序索引" + } else { + "Sort Index" + } +} + +pub fn id_label_colon() -> &'static str { + if is_chinese() { + "ID" + } else { + "ID" + } +} + +pub fn url_label_colon() -> &'static str { + if is_chinese() { + "网址" + } else { + "URL" + } +} + +pub fn api_url_label_colon() -> &'static str { + if is_chinese() { + "API 地址" + } else { + "API URL" + } +} + +pub fn summary_divider() -> &'static str { + "======================" +} + +// Provider Input - Summary Display +pub fn basic_info_header() -> &'static str { + if is_chinese() { + "基本信息" + } else { + "Basic Info" + } +} + +pub fn name_display_label() -> &'static str { + if is_chinese() { + "名称" + } else { + "Name" + } +} + +pub fn app_display_label() -> &'static str { + if is_chinese() { + "应用" + } else { + "App" + } +} + +pub fn notes_display_label() -> &'static str { + if is_chinese() { + "备注" + } else { + "Notes" + } +} + +pub fn sort_index_display_label() -> &'static str { + if is_chinese() { + "排序" + } else { + "Sort Index" + } +} + +pub fn config_info_header() -> &'static str { + if is_chinese() { + "配置信息" + } else { + "Configuration" + } +} + +pub fn api_key_display_label() -> &'static str { + if is_chinese() { + "API Key" + } else { + "API Key" + } +} + +pub fn base_url_display_label() -> &'static str { + if is_chinese() { + "Base URL" + } else { + "Base URL" + } +} + +pub fn model_config_header() -> &'static str { + if is_chinese() { + "模型配置" + } else { + "Model Configuration" + } +} + +pub fn default_model_display() -> &'static str { + if is_chinese() { + "默认" + } else { + "Default" + } +} + +pub fn haiku_model_display() -> &'static str { + if is_chinese() { + "Haiku" + } else { + "Haiku" + } +} + +pub fn sonnet_model_display() -> &'static str { + if is_chinese() { + "Sonnet" + } else { + "Sonnet" + } +} + +pub fn opus_model_display() -> &'static str { + if is_chinese() { + "Opus" + } else { + "Opus" + } +} + +pub fn auth_type_display_label() -> &'static str { + if is_chinese() { + "认证" + } else { + "Auth Type" + } +} + +pub fn project_id_display_label() -> &'static str { + if is_chinese() { + "项目 ID" + } else { + "Project ID" + } +} + +pub fn location_display_label() -> &'static str { + if is_chinese() { + "位置" + } else { + "Location" + } +} + +// Interactive Provider - Menu Options +pub fn edit_provider_menu() -> &'static str { + if is_chinese() { + "➕ 编辑供应商" + } else { + "➕ Edit Provider" + } +} + +pub fn no_editable_providers() -> &'static str { + if is_chinese() { + "没有可编辑的供应商" + } else { + "No providers available for editing" + } +} + +pub fn select_provider_to_edit() -> &'static str { + if is_chinese() { + "选择要编辑的供应商:" + } else { + "Select provider to edit:" + } +} + +pub fn choose_edit_mode() -> &'static str { + if is_chinese() { + "选择编辑模式:" + } else { + "Choose edit mode:" + } +} + +pub fn select_config_file_to_edit() -> &'static str { + if is_chinese() { + "选择要编辑的配置文件:" + } else { + "Select config file to edit:" + } +} + +pub fn provider_missing_auth_field() -> &'static str { + if is_chinese() { + "settings_config 中缺少 'auth' 字段" + } else { + "Missing 'auth' field in settings_config" + } +} + +pub fn provider_missing_or_invalid_config_field() -> &'static str { + if is_chinese() { + "settings_config 中缺少或无效的 'config' 字段" + } else { + "Missing or invalid 'config' field in settings_config" + } +} + +pub fn edit_mode_interactive() -> &'static str { + if is_chinese() { + "📝 交互式编辑 (分步提示)" + } else { + "📝 Interactive editing (step-by-step prompts)" + } +} + +pub fn edit_mode_json_editor() -> &'static str { + if is_chinese() { + "✏️ JSON 编辑 (使用外部编辑器)" + } else { + "✏️ JSON editing (use external editor)" + } +} + +pub fn cancel() -> &'static str { + if is_chinese() { + "❌ 取消" + } else { + "❌ Cancel" + } +} + +pub fn opening_external_editor() -> &'static str { + if is_chinese() { + "正在打开外部编辑器..." + } else { + "Opening external editor..." + } +} + +pub fn invalid_json_syntax() -> &'static str { + if is_chinese() { + "无效的 JSON 语法" + } else { + "Invalid JSON syntax" + } +} + +pub fn invalid_provider_structure() -> &'static str { + if is_chinese() { + "无效的供应商结构" + } else { + "Invalid provider structure" + } +} + +pub fn provider_id_cannot_be_changed() -> &'static str { + if is_chinese() { + "供应商 ID 不能被修改" + } else { + "Provider ID cannot be changed" + } +} + +pub fn provider_id_empty_error() -> &'static str { + if is_chinese() { + "供应商 ID 不能为空" + } else { + "Provider ID cannot be empty" + } +} + +pub fn retry_editing() -> &'static str { + if is_chinese() { + "是否重新编辑?" + } else { + "Retry editing?" + } +} + +pub fn no_changes_detected() -> &'static str { + if is_chinese() { + "未检测到任何更改" + } else { + "No changes detected" + } +} + +pub fn provider_summary() -> &'static str { + if is_chinese() { + "供应商信息摘要" + } else { + "Provider Summary" + } +} + +pub fn confirm_save_changes() -> &'static str { + if is_chinese() { + "确认保存更改?" + } else { + "Save changes?" + } +} + +pub fn editor_failed() -> &'static str { + if is_chinese() { + "编辑器失败" + } else { + "Editor failed" + } +} + +pub fn invalid_selection_format() -> &'static str { + if is_chinese() { + "无效的选择格式" + } else { + "Invalid selection format" + } +} + +// Provider Display Labels (for show_current and view_provider_detail) +pub fn basic_info_section_header() -> &'static str { + if is_chinese() { + "基本信息 / Basic Info" + } else { + "Basic Info" + } +} + +pub fn name_label_with_colon() -> &'static str { + if is_chinese() { + "名称" + } else { + "Name" + } +} + +pub fn app_label_with_colon() -> &'static str { + if is_chinese() { + "应用" + } else { + "App" + } +} + +pub fn api_config_section_header() -> &'static str { + if is_chinese() { + "API 配置 / API Configuration" + } else { + "API Configuration" + } +} + +pub fn model_config_section_header() -> &'static str { + if is_chinese() { + "模型配置 / Model Configuration" + } else { + "Model Configuration" + } +} + +pub fn main_model_label_with_colon() -> &'static str { + if is_chinese() { + "主模型" + } else { + "Main Model" + } +} + +pub fn updated_config_header() -> &'static str { + if is_chinese() { + "修改后配置:" + } else { + "Updated Configuration:" + } +} + +// Provider Add/Edit Messages +pub fn generated_id_message(id: &str) -> String { + if is_chinese() { + format!("生成的 ID: {}", id) + } else { + format!("Generated ID: {}", id) + } +} + +pub fn edit_fields_instruction() -> &'static str { + if is_chinese() { + "逐个编辑字段(直接回车保留当前值):\n" + } else { + "Edit fields one by one (press Enter to keep current value):\n" + } +} + +// ============================================ +// MCP SERVER MANAGEMENT (MCP 服务器管理) +// ============================================ + +pub fn mcp_management() -> &'static str { + if is_chinese() { + "🛠️ MCP 服务器管理" + } else { + "🛠️ MCP Server Management" + } +} + +pub fn no_mcp_servers() -> &'static str { + if is_chinese() { + "未找到 MCP 服务器。" + } else { + "No MCP servers found." + } +} + +pub fn sync_all_servers() -> &'static str { + if is_chinese() { + "🔄 同步所有服务器" + } else { + "🔄 Sync All Servers" + } +} + +pub fn synced_successfully() -> &'static str { + if is_chinese() { + "✓ 所有 MCP 服务器同步成功" + } else { + "✓ All MCP servers synced successfully" + } +} + +// ============================================ +// PROMPT MANAGEMENT (提示词管理) +// ============================================ + +pub fn prompts_management() -> &'static str { + if is_chinese() { + "💬 提示词管理" + } else { + "💬 Prompt Management" + } +} + +pub fn no_prompts() -> &'static str { + if is_chinese() { + "未找到提示词预设。" + } else { + "No prompt presets found." + } +} + +pub fn switch_active_prompt() -> &'static str { + if is_chinese() { + "🔄 切换活动提示词" + } else { + "🔄 Switch Active Prompt" + } +} + +pub fn no_prompts_available() -> &'static str { + if is_chinese() { + "没有可用的提示词。" + } else { + "No prompts available." + } +} + +pub fn select_prompt_to_activate() -> &'static str { + if is_chinese() { + "选择要激活的提示词:" + } else { + "Select prompt to activate:" + } +} + +pub fn activated_prompt(id: &str) -> String { + if is_chinese() { + format!("✓ 已激活提示词 '{}'", id) + } else { + format!("✓ Activated prompt '{}'", id) + } +} + +pub fn deactivated_prompt(id: &str) -> String { + if is_chinese() { + format!("✓ 已取消激活提示词 '{}'", id) + } else { + format!("✓ Deactivated prompt '{}'", id) + } +} + +pub fn prompt_cleared_note() -> &'static str { + if is_chinese() { + "实时文件已清空" + } else { + "Live prompt file has been cleared" + } +} + +pub fn prompt_synced_note() -> &'static str { + if is_chinese() { + "注意:提示词已同步到实时配置文件。" + } else { + "Note: The prompt has been synced to the live configuration file." + } +} + +// Configuration View +pub fn current_configuration() -> &'static str { + if is_chinese() { + "👁️ 当前配置" + } else { + "👁️ Current Configuration" + } +} + +pub fn provider_label() -> &'static str { + if is_chinese() { + "供应商:" + } else { + "Provider:" + } +} + +pub fn mcp_servers_label() -> &'static str { + if is_chinese() { + "MCP 服务器:" + } else { + "MCP Servers:" + } +} + +pub fn tui_label_mcp_short() -> &'static str { + "MCP:" +} diff --git a/src-tauri/src/cli/i18n/texts/provider_management.rs b/src-tauri/src/cli/i18n/texts/provider_management.rs new file mode 100644 index 00000000..7daf9a24 --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/provider_management.rs @@ -0,0 +1,1058 @@ +use super::is_chinese; +pub fn provider_management() -> &'static str { + if is_chinese() { + "🔌 供应商管理" + } else { + "🔌 Provider Management" + } +} + +pub fn no_providers() -> &'static str { + if is_chinese() { + "未找到供应商。" + } else { + "No providers found." + } +} + +pub fn view_current_provider() -> &'static str { + if is_chinese() { + "📋 查看当前供应商详情" + } else { + "📋 View Current Provider Details" + } +} + +pub fn switch_provider() -> &'static str { + if is_chinese() { + "🔄 切换供应商" + } else { + "🔄 Switch Provider" + } +} + +pub fn add_provider() -> &'static str { + if is_chinese() { + "➕ 新增供应商" + } else { + "➕ Add Provider" + } +} + +pub fn add_official_provider() -> &'static str { + if is_chinese() { + "添加官方供应商" + } else { + "Add Official Provider" + } +} + +pub fn add_third_party_provider() -> &'static str { + if is_chinese() { + "添加第三方供应商" + } else { + "Add Third-Party Provider" + } +} + +pub fn select_provider_add_mode() -> &'static str { + if is_chinese() { + "请选择供应商类型:" + } else { + "Select provider type:" + } +} + +pub fn delete_provider() -> &'static str { + if is_chinese() { + "🗑️ 删除供应商" + } else { + "🗑️ Delete Provider" + } +} + +pub fn back_to_main() -> &'static str { + if is_chinese() { + "⬅️ 返回主菜单" + } else { + "⬅️ Back to Main Menu" + } +} + +pub fn choose_action() -> &'static str { + if is_chinese() { + "选择操作:" + } else { + "Choose an action:" + } +} + +pub fn esc_to_go_back_help() -> &'static str { + if is_chinese() { + "Esc 返回上一步" + } else { + "Esc to go back" + } +} + +pub fn select_filter_help() -> &'static str { + if is_chinese() { + "Esc 返回;输入可过滤" + } else { + "Esc to go back; type to filter" + } +} + +pub fn current_provider_details() -> &'static str { + if is_chinese() { + "当前供应商详情" + } else { + "Current Provider Details" + } +} + +pub fn only_one_provider() -> &'static str { + if is_chinese() { + "只有一个供应商,无法切换。" + } else { + "Only one provider available. Cannot switch." + } +} + +pub fn no_other_providers() -> &'static str { + if is_chinese() { + "没有其他供应商可切换。" + } else { + "No other providers to switch to." + } +} + +pub fn select_provider_to_switch() -> &'static str { + if is_chinese() { + "选择要切换到的供应商:" + } else { + "Select provider to switch to:" + } +} + +pub fn switched_to_provider(id: &str) -> String { + if is_chinese() { + format!("✓ 已切换到供应商 '{}'", id) + } else { + format!("✓ Switched to provider '{}'", id) + } +} + +pub fn restart_note() -> &'static str { + if is_chinese() { + "注意:请重启 CLI 客户端以应用更改。" + } else { + "Note: Restart your CLI client to apply the changes." + } +} + +pub fn live_sync_skipped_uninitialized_warning(app: &str) -> String { + if is_chinese() { + format!("⚠ 未检测到 {app} 客户端本地配置,已跳过写入 live 文件;先运行一次 {app} 初始化后再试。") + } else { + format!("⚠ Live sync skipped: {app} client not initialized; run it once to initialize, then retry.") + } +} + +pub fn no_deletable_providers() -> &'static str { + if is_chinese() { + "没有可删除的供应商(无法删除当前供应商)。" + } else { + "No providers available for deletion (cannot delete current provider)." + } +} + +pub fn select_provider_to_delete() -> &'static str { + if is_chinese() { + "选择要删除的供应商:" + } else { + "Select provider to delete:" + } +} + +pub fn confirm_delete(id: &str) -> String { + if is_chinese() { + format!("确定要删除供应商 '{}' 吗?", id) + } else { + format!("Are you sure you want to delete provider '{}'?", id) + } +} + +pub fn cancelled() -> &'static str { + if is_chinese() { + "已取消。" + } else { + "Cancelled." + } +} + +pub fn selection_cancelled() -> &'static str { + if is_chinese() { + "已取消选择" + } else { + "Selection cancelled" + } +} + +pub fn invalid_selection() -> &'static str { + if is_chinese() { + "选择无效" + } else { + "Invalid selection" + } +} + +pub fn available_backups() -> &'static str { + if is_chinese() { + "可用备份" + } else { + "Available Backups" + } +} + +pub fn no_backups_found() -> &'static str { + if is_chinese() { + "未找到备份。" + } else { + "No backups found." + } +} + +pub fn create_backup_first_hint() -> &'static str { + if is_chinese() { + "请先创建备份:cc-switch config backup" + } else { + "Create a backup first: cc-switch config backup" + } +} + +pub fn found_backups(count: usize) -> String { + if is_chinese() { + format!("找到 {} 个备份:", count) + } else { + format!("Found {} backup(s):", count) + } +} + +pub fn select_backup_to_restore() -> &'static str { + if is_chinese() { + "选择要恢复的备份:" + } else { + "Select backup to restore:" + } +} + +pub fn warning_title() -> &'static str { + if is_chinese() { + "警告:" + } else { + "Warning:" + } +} + +pub fn config_restore_warning_replace() -> &'static str { + if is_chinese() { + "这将用所选备份替换你当前的配置。" + } else { + "This will replace your current configuration with the selected backup." + } +} + +pub fn config_restore_warning_pre_backup() -> &'static str { + if is_chinese() { + "系统会先创建一次当前状态的备份。" + } else { + "A backup of the current state will be created first." + } +} + +pub fn config_restore_confirm_prompt() -> &'static str { + if is_chinese() { + "确认继续恢复?" + } else { + "Continue with restore?" + } +} + +pub fn deleted_provider(id: &str) -> String { + if is_chinese() { + format!("✓ 已删除供应商 '{}'", id) + } else { + format!("✓ Deleted provider '{}'", id) + } +} + +// Provider Input - Basic Fields +pub fn provider_name_label() -> &'static str { + if is_chinese() { + "供应商名称:" + } else { + "Provider Name:" + } +} + +pub fn provider_name_help() -> &'static str { + if is_chinese() { + "必填,用于显示的友好名称" + } else { + "Required, friendly display name" + } +} + +pub fn provider_name_help_edit() -> &'static str { + if is_chinese() { + "必填,直接回车保持原值" + } else { + "Required, press Enter to keep" + } +} + +pub fn provider_name_placeholder() -> &'static str { + "OpenAI" +} + +pub fn provider_name_empty_error() -> &'static str { + if is_chinese() { + "供应商名称不能为空" + } else { + "Provider name cannot be empty" + } +} + +pub fn website_url_label() -> &'static str { + if is_chinese() { + "官网 URL(可选):" + } else { + "Website URL (opt.):" + } +} + +pub fn website_url_help() -> &'static str { + if is_chinese() { + "供应商的网站地址,直接回车跳过" + } else { + "Provider's website, press Enter to skip" + } +} + +pub fn website_url_help_edit() -> &'static str { + if is_chinese() { + "留空则不修改,直接回车跳过" + } else { + "Leave blank to keep, Enter to skip" + } +} + +pub fn website_url_placeholder() -> &'static str { + "https://openai.com" +} + +// Provider Commands +pub fn no_providers_hint() -> &'static str { + "Use 'cc-switch provider add' to create a new provider." +} + +pub fn app_config_not_found(app: &str) -> String { + if is_chinese() { + format!("应用 {} 配置不存在", app) + } else { + format!("Application {} configuration not found", app) + } +} + +pub fn provider_not_found(id: &str) -> String { + if is_chinese() { + format!("供应商不存在: {}", id) + } else { + format!("Provider not found: {}", id) + } +} + +pub fn generated_id(id: &str) -> String { + if is_chinese() { + format!("生成的 ID: {}", id) + } else { + format!("Generated ID: {}", id) + } +} + +pub fn configure_optional_fields_prompt() -> &'static str { + if is_chinese() { + "配置可选字段(备注、排序索引)?" + } else { + "Configure optional fields (notes, sort index)?" + } +} + +pub fn current_config_header() -> &'static str { + if is_chinese() { + "当前配置:" + } else { + "Current Configuration:" + } +} + +pub fn modify_provider_config_prompt() -> &'static str { + if is_chinese() { + "修改供应商配置(API Key, Base URL 等)?" + } else { + "Modify provider configuration (API Key, Base URL, etc.)?" + } +} + +pub fn modify_optional_fields_prompt() -> &'static str { + if is_chinese() { + "修改可选字段(备注、排序索引)?" + } else { + "Modify optional fields (notes, sort index)?" + } +} + +pub fn current_provider_synced_warning() -> &'static str { + if is_chinese() { + "⚠ 此供应商当前已激活,修改已同步到 live 配置" + } else { + "⚠ This provider is currently active, changes synced to live config" + } +} + +pub fn input_failed_error(err: &str) -> String { + if is_chinese() { + format!("输入失败: {}", err) + } else { + format!("Input failed: {}", err) + } +} + +pub fn cannot_delete_current_provider() -> &'static str { + "Cannot delete the current active provider. Please switch to another provider first." +} + +// Provider Input - Basic Fields +pub fn provider_name_prompt() -> &'static str { + if is_chinese() { + "供应商名称:" + } else { + "Provider Name:" + } +} + +// Provider Input - Claude Configuration +pub fn config_claude_header() -> &'static str { + if is_chinese() { + "配置 Claude 供应商:" + } else { + "Configure Claude Provider:" + } +} + +pub fn api_key_label() -> &'static str { + if is_chinese() { + "API Key:" + } else { + "API Key:" + } +} + +pub fn api_key_help() -> &'static str { + if is_chinese() { + "留空使用默认值" + } else { + "Leave empty to use default" + } +} + +pub fn base_url_label() -> &'static str { + if is_chinese() { + "Base URL:" + } else { + "Base URL:" + } +} + +pub fn base_url_empty_error() -> &'static str { + if is_chinese() { + "API 请求地址不能为空" + } else { + "API URL cannot be empty" + } +} + +pub fn base_url_placeholder() -> &'static str { + if is_chinese() { + "如 https://api.anthropic.com" + } else { + "e.g., https://api.anthropic.com" + } +} + +pub fn configure_model_names_prompt() -> &'static str { + if is_chinese() { + "配置模型名称?" + } else { + "Configure model names?" + } +} + +pub fn model_default_label() -> &'static str { + if is_chinese() { + "默认模型:" + } else { + "Default Model:" + } +} + +pub fn model_default_help() -> &'static str { + if is_chinese() { + "留空使用 Claude Code 默认模型" + } else { + "Leave empty to use Claude Code default" + } +} + +pub fn model_haiku_label() -> &'static str { + if is_chinese() { + "Haiku 模型:" + } else { + "Haiku Model:" + } +} + +pub fn model_haiku_placeholder() -> &'static str { + if is_chinese() { + "如 claude-3-5-haiku-20241022" + } else { + "e.g., claude-3-5-haiku-20241022" + } +} + +pub fn model_sonnet_label() -> &'static str { + if is_chinese() { + "Sonnet 模型:" + } else { + "Sonnet Model:" + } +} + +pub fn model_sonnet_placeholder() -> &'static str { + if is_chinese() { + "如 claude-3-5-sonnet-20241022" + } else { + "e.g., claude-3-5-sonnet-20241022" + } +} + +pub fn model_opus_label() -> &'static str { + if is_chinese() { + "Opus 模型:" + } else { + "Opus Model:" + } +} + +pub fn model_opus_placeholder() -> &'static str { + if is_chinese() { + "如 claude-3-opus-20240229" + } else { + "e.g., claude-3-opus-20240229" + } +} + +// Provider Input - Codex Configuration +pub fn config_codex_header() -> &'static str { + if is_chinese() { + "配置 Codex 供应商:" + } else { + "Configure Codex Provider:" + } +} + +pub fn openai_api_key_label() -> &'static str { + if is_chinese() { + "OpenAI API Key:" + } else { + "OpenAI API Key:" + } +} + +pub fn anthropic_api_key_label() -> &'static str { + if is_chinese() { + "Anthropic API Key:" + } else { + "Anthropic API Key:" + } +} + +pub fn config_toml_label() -> &'static str { + if is_chinese() { + "配置内容 (TOML):" + } else { + "Config Content (TOML):" + } +} + +pub fn config_toml_help() -> &'static str { + if is_chinese() { + "按 Esc 后 Enter 提交" + } else { + "Press Esc then Enter to submit" + } +} + +pub fn config_toml_placeholder() -> &'static str { + if is_chinese() { + "留空使用默认配置" + } else { + "Leave empty to use default config" + } +} + +// Codex 0.64+ Configuration +pub fn codex_auth_mode_info() -> &'static str { + if is_chinese() { + "⚠ 请选择 Codex 的鉴权方式(决定 API Key 从哪里读取)" + } else { + "⚠ Choose how Codex authenticates (where the API key is read from)" + } +} + +pub fn codex_auth_mode_label() -> &'static str { + if is_chinese() { + "认证方式:" + } else { + "Auth Mode:" + } +} + +pub fn codex_auth_mode_help() -> &'static str { + if is_chinese() { + "OpenAI 认证:使用 auth.json/凭据存储;环境变量:使用 env_key 指定的变量(未设置会报错)" + } else { + "OpenAI auth uses auth.json/credential store; env var mode uses env_key (missing env var will error)" + } +} + +pub fn codex_auth_mode_openai() -> &'static str { + if is_chinese() { + "OpenAI 认证(推荐,无需环境变量)" + } else { + "OpenAI auth (recommended, no env var)" + } +} + +pub fn codex_auth_mode_env_var() -> &'static str { + if is_chinese() { + "环境变量(env_key,需要手动 export)" + } else { + "Environment variable (env_key, requires export)" + } +} + +pub fn codex_official_provider_tip() -> &'static str { + if is_chinese() { + "提示:官方供应商将使用 Codex 官方登录保存的凭证(codex login 可能会打开浏览器),无需填写 API Key" + } else { + "Tip: Official provider uses Codex login credentials (`codex login` may open a browser); no API key required" + } +} + +pub fn codex_env_key_info() -> &'static str { + if is_chinese() { + "⚠ 环境变量模式:Codex 将从指定的环境变量读取 API Key" + } else { + "⚠ Env var mode: Codex will read the API key from the specified environment variable" + } +} + +pub fn codex_env_key_label() -> &'static str { + if is_chinese() { + "环境变量名称:" + } else { + "Environment Variable Name:" + } +} + +pub fn codex_env_key_help() -> &'static str { + if is_chinese() { + "Codex 将从此环境变量读取 API 密钥(默认: OPENAI_API_KEY)" + } else { + "Codex will read API key from this env var (default: OPENAI_API_KEY)" + } +} + +pub fn codex_wire_api_label() -> &'static str { + if is_chinese() { + "API 格式:" + } else { + "API Format:" + } +} + +pub fn codex_wire_api_help() -> &'static str { + if is_chinese() { + "chat = Chat Completions API (大多数第三方), responses = OpenAI Responses API" + } else { + "chat = Chat Completions API (most providers), responses = OpenAI Responses API" + } +} + +pub fn codex_env_reminder(env_key: &str) -> String { + if is_chinese() { + format!( + "⚠ 请确保已设置环境变量 {} 并包含您的 API 密钥\n 例如: export {}=\"your-api-key\"", + env_key, env_key + ) + } else { + format!( + "⚠ Make sure to set the {} environment variable with your API key\n Example: export {}=\"your-api-key\"", + env_key, env_key + ) + } +} + +pub fn codex_openai_auth_info() -> &'static str { + if is_chinese() { + "✓ OpenAI 认证模式:Codex 将使用 auth.json/系统凭据存储,无需设置 OPENAI_API_KEY 环境变量" + } else { + "✓ OpenAI auth mode: Codex will use auth.json/credential store; no OPENAI_API_KEY env var required" + } +} + +pub fn codex_dual_write_info(env_key: &str, _api_key: &str) -> String { + if is_chinese() { + format!( + "✓ 双写模式已启用(兼容所有 Codex 版本)\n\ + • 旧版本 Codex: 将使用 auth.json 中的 API Key\n\ + • Codex 0.64+: 可使用环境变量 {} (更安全)\n\ + 例如: export {}=\"your-api-key\"", + env_key, env_key + ) + } else { + format!( + "✓ Dual-write mode enabled (compatible with all Codex versions)\n\ + • Legacy Codex: Will use API Key from auth.json\n\ + • Codex 0.64+: Can use env variable {} (more secure)\n\ + Example: export {}=\"your-api-key\"", + env_key, env_key + ) + } +} + +pub fn use_current_config_prompt() -> &'static str { + if is_chinese() { + "使用当前配置?" + } else { + "Use current configuration?" + } +} + +pub fn use_current_config_help() -> &'static str { + if is_chinese() { + "选择 No 将进入自定义输入模式" + } else { + "Select No to enter custom input mode" + } +} + +pub fn input_toml_config() -> &'static str { + if is_chinese() { + "输入 TOML 配置(多行,输入空行结束):" + } else { + "Enter TOML config (multiple lines, empty line to finish):" + } +} + +pub fn direct_enter_to_finish() -> &'static str { + if is_chinese() { + "直接回车结束输入" + } else { + "Press Enter to finish" + } +} + +pub fn current_config_label() -> &'static str { + if is_chinese() { + "当前配置:" + } else { + "Current Config:" + } +} + +pub fn config_toml_header() -> &'static str { + if is_chinese() { + "Config.toml 配置:" + } else { + "Config.toml Configuration:" + } +} + +// Provider Input - Gemini Configuration +pub fn config_gemini_header() -> &'static str { + if is_chinese() { + "配置 Gemini 供应商:" + } else { + "Configure Gemini Provider:" + } +} + +pub fn auth_type_label() -> &'static str { + if is_chinese() { + "认证类型:" + } else { + "Auth Type:" + } +} + +pub fn auth_type_api_key() -> &'static str { + if is_chinese() { + "API Key" + } else { + "API Key" + } +} + +pub fn auth_type_service_account() -> &'static str { + if is_chinese() { + "Service Account (ADC)" + } else { + "Service Account (ADC)" + } +} + +pub fn gemini_api_key_label() -> &'static str { + if is_chinese() { + "Gemini API Key:" + } else { + "Gemini API Key:" + } +} + +pub fn gemini_base_url_label() -> &'static str { + if is_chinese() { + "Base URL:" + } else { + "Base URL:" + } +} + +pub fn gemini_base_url_help() -> &'static str { + if is_chinese() { + "留空使用官方 API" + } else { + "Leave empty to use official API" + } +} + +pub fn gemini_base_url_placeholder() -> &'static str { + if is_chinese() { + "如 https://generativelanguage.googleapis.com" + } else { + "e.g., https://generativelanguage.googleapis.com" + } +} + +pub fn adc_project_id_label() -> &'static str { + if is_chinese() { + "GCP Project ID:" + } else { + "GCP Project ID:" + } +} + +pub fn adc_location_label() -> &'static str { + if is_chinese() { + "GCP Location:" + } else { + "GCP Location:" + } +} + +pub fn adc_location_placeholder() -> &'static str { + if is_chinese() { + "如 us-central1" + } else { + "e.g., us-central1" + } +} + +pub fn google_oauth_official() -> &'static str { + if is_chinese() { + "Google OAuth(官方)" + } else { + "Google OAuth (Official)" + } +} + +pub fn packycode_api_key() -> &'static str { + if is_chinese() { + "PackyCode API Key" + } else { + "PackyCode API Key" + } +} + +pub fn generic_api_key() -> &'static str { + if is_chinese() { + "通用 API Key" + } else { + "Generic API Key" + } +} + +pub fn select_auth_method_help() -> &'static str { + if is_chinese() { + "选择 Gemini 的认证方式" + } else { + "Select authentication method for Gemini" + } +} + +pub fn use_google_oauth_warning() -> &'static str { + if is_chinese() { + "使用 Google OAuth,将清空 API Key 配置" + } else { + "Using Google OAuth, API Key config will be cleared" + } +} + +pub fn packycode_api_key_help() -> &'static str { + if is_chinese() { + "从 PackyCode 获取的 API Key" + } else { + "API Key obtained from PackyCode" + } +} + +pub fn packycode_endpoint_help() -> &'static str { + if is_chinese() { + "PackyCode API 端点" + } else { + "PackyCode API endpoint" + } +} + +pub fn generic_api_key_help() -> &'static str { + if is_chinese() { + "通用的 Gemini API Key" + } else { + "Generic Gemini API Key" + } +} + +// Provider Input - Optional Fields +pub fn notes_label() -> &'static str { + if is_chinese() { + "备注:" + } else { + "Notes:" + } +} + +pub fn notes_placeholder() -> &'static str { + if is_chinese() { + "可选的备注信息" + } else { + "Optional notes" + } +} + +pub fn sort_index_label() -> &'static str { + if is_chinese() { + "排序索引:" + } else { + "Sort Index:" + } +} + +pub fn sort_index_help() -> &'static str { + if is_chinese() { + "数字越小越靠前,留空使用创建时间排序" + } else { + "Lower numbers appear first, leave empty to sort by creation time" + } +} + +pub fn sort_index_placeholder() -> &'static str { + if is_chinese() { + "如 1, 2, 3..." + } else { + "e.g., 1, 2, 3..." + } +} + +pub fn invalid_sort_index() -> &'static str { + if is_chinese() { + "排序索引必须是有效的数字" + } else { + "Sort index must be a valid number" + } +} + +pub fn optional_fields_config() -> &'static str { + if is_chinese() { + "可选字段配置:" + } else { + "Optional Fields Configuration:" + } +} + +pub fn notes_example_placeholder() -> &'static str { + if is_chinese() { + "自定义供应商,用于测试" + } else { + "Custom provider for testing" + } +} + +pub fn notes_help_edit() -> &'static str { + if is_chinese() { + "关于此供应商的额外说明,直接回车保持原值" + } else { + "Additional notes about this provider, press Enter to keep current value" + } +} + +pub fn notes_help_new() -> &'static str { + if is_chinese() { + "关于此供应商的额外说明,直接回车跳过" + } else { + "Additional notes about this provider, press Enter to skip" + } +} + +pub fn sort_index_help_edit() -> &'static str { + if is_chinese() { + "数字,用于控制显示顺序,直接回车保持原值" + } else { + "Number for display order, press Enter to keep current value" + } +} + +pub fn sort_index_help_new() -> &'static str { + if is_chinese() { + "数字,用于控制显示顺序,直接回车跳过" + } else { + "Number for display order, press Enter to skip" + } +} + +pub fn invalid_sort_index_number() -> &'static str { + if is_chinese() { + "排序索引必须是数字" + } else { + "Sort index must be a number" + } +} + +pub fn provider_config_summary() -> &'static str { + if is_chinese() { + "=== 供应商配置摘要 ===" + } else { + "=== Provider Configuration Summary ===" + } +} diff --git a/src-tauri/src/cli/i18n/texts/providers.rs b/src-tauri/src/cli/i18n/texts/providers.rs new file mode 100644 index 00000000..b38acb68 --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/providers.rs @@ -0,0 +1,1489 @@ +use super::is_chinese; + +pub fn tui_claude_api_format_popup_title() -> &'static str { + if is_chinese() { + "API 格式" + } else { + "API Format" + } +} + +pub fn tui_label_claude_model_config() -> &'static str { + if is_chinese() { + "模型映射" + } else { + "Model Mapping" + } +} + +pub fn tui_label_claude_fallback_model() -> &'static str { + if is_chinese() { + "默认兜底模型" + } else { + "Default fallback model" + } +} + +pub fn tui_label_provider_package() -> &'static str { + if is_chinese() { + "Provider / npm 包" + } else { + "Provider / npm" + } +} + +pub fn tui_label_opencode_model_id() -> &'static str { + if is_chinese() { + "主模型 ID" + } else { + "Main Model ID" + } +} + +pub fn tui_label_opencode_model_name() -> &'static str { + if is_chinese() { + "主模型名称" + } else { + "Main Model Name" + } +} + +pub fn tui_label_context_limit() -> &'static str { + if is_chinese() { + "上下文限制" + } else { + "Context Limit" + } +} + +pub fn tui_label_output_limit() -> &'static str { + if is_chinese() { + "输出限制" + } else { + "Output Limit" + } +} + +pub fn tui_label_command() -> &'static str { + if is_chinese() { + "命令" + } else { + "Command" + } +} + +pub fn tui_label_mcp_type() -> &'static str { + if is_chinese() { + "连接类型" + } else { + "Transport" + } +} + +pub fn tui_label_url() -> &'static str { + if is_chinese() { + "URL" + } else { + "URL" + } +} + +pub fn tui_label_args() -> &'static str { + if is_chinese() { + "参数" + } else { + "Args" + } +} + +pub fn tui_label_env() -> &'static str { + if is_chinese() { + "环境变量" + } else { + "Env" + } +} + +pub fn tui_mcp_env_entry_count(count: usize) -> String { + if is_chinese() { + format!("{count} 项") + } else if count == 1 { + "1 entry".to_string() + } else { + format!("{count} entries") + } +} + +pub fn tui_mcp_env_editor_hint() -> &'static str { + if is_chinese() { + "按 Enter 管理环境变量" + } else { + "Press Enter to manage env entries" + } +} + +pub fn tui_mcp_type_editor_hint() -> &'static str { + if is_chinese() { + "按 Enter 选择连接类型" + } else { + "Press Enter to choose transport" + } +} + +pub fn tui_mcp_type_title() -> &'static str { + if is_chinese() { + "选择 MCP 连接类型" + } else { + "Select MCP Transport" + } +} + +pub fn tui_mcp_env_key_label() -> &'static str { + if is_chinese() { + "键" + } else { + "Key" + } +} + +pub fn tui_mcp_env_value_label() -> &'static str { + if is_chinese() { + "值" + } else { + "Value" + } +} + +pub fn tui_label_app_claude() -> &'static str { + if is_chinese() { + "应用: Claude" + } else { + "App: Claude" + } +} + +pub fn tui_label_app_codex() -> &'static str { + if is_chinese() { + "应用: Codex" + } else { + "App: Codex" + } +} + +pub fn tui_label_app_gemini() -> &'static str { + if is_chinese() { + "应用: Gemini" + } else { + "App: Gemini" + } +} + +pub fn tui_label_app_opencode() -> &'static str { + if is_chinese() { + "应用: OpenCode" + } else { + "App: OpenCode" + } +} + +pub fn tui_label_app_hermes() -> &'static str { + if is_chinese() { + "应用: Hermes" + } else { + "App: Hermes" + } +} + +pub fn tui_form_templates_title() -> &'static str { + if is_chinese() { + "模板" + } else { + "Templates" + } +} + +pub fn tui_form_common_config_button() -> &'static str { + if is_chinese() { + "通用配置" + } else { + "Common Config" + } +} + +pub fn tui_form_attach_common_config() -> &'static str { + if is_chinese() { + "添加通用配置" + } else { + "Attach Common Config" + } +} + +pub fn tui_form_fields_title() -> &'static str { + if is_chinese() { + "字段" + } else { + "Fields" + } +} + +pub fn tui_usage_query_title(provider: &str) -> String { + if provider.trim().is_empty() { + tui_usage_query_configure_title().to_string() + } else { + format!("{} - {provider}", tui_usage_query_configure_title()) + } +} + +pub fn tui_usage_query_configure_title() -> &'static str { + if is_chinese() { + "配置用量查询" + } else { + "Configure Usage Query" + } +} + +pub fn tui_usage_query_notice_title() -> &'static str { + tui_usage_query_configure_title() +} + +pub fn tui_usage_query_notice_message() -> &'static str { + if is_chinese() { + "用量查询需要配置专用的查询脚本或 API 参数,请确保您已从供应商处获取相关信息。\n\n如不确定如何配置,请先查阅供应商文档。" + } else { + "Usage query requires a custom script or API parameters. Please make sure you have obtained the necessary information from your provider.\n\nIf unsure how to configure, please consult your provider's documentation first." + } +} + +pub fn tui_usage_query_enable() -> &'static str { + if is_chinese() { + "启用用量查询" + } else { + "Enable usage query" + } +} + +pub fn tui_usage_query_template() -> &'static str { + if is_chinese() { + "预设模板" + } else { + "Preset template" + } +} + +pub fn tui_usage_query_access_token() -> &'static str { + if is_chinese() { + "访问令牌(在个人安全设置里获取)" + } else { + "Access Token" + } +} + +pub fn tui_usage_query_user_id() -> &'static str { + if is_chinese() { + "用户 ID" + } else { + "User ID" + } +} + +pub fn tui_usage_query_timeout_seconds() -> &'static str { + if is_chinese() { + "超时时间(秒)" + } else { + "Timeout (seconds)" + } +} + +pub fn tui_usage_query_auto_interval() -> &'static str { + if is_chinese() { + "自动查询间隔(分钟,0 表示不自动查询)" + } else { + "Auto query interval (minutes, 0 to disable)" + } +} + +pub fn tui_usage_query_script() -> &'static str { + if is_chinese() { + "提取器代码" + } else { + "Extractor Code" + } +} + +pub fn tui_usage_query_script_preview_title() -> &'static str { + if is_chinese() { + "提取器代码 | 返回对象需包含剩余额度等字段" + } else { + "Extractor code | Return object should include remaining quota fields" + } +} + +pub fn tui_usage_query_script_help_title() -> &'static str { + if is_chinese() { + "脚本编写说明:" + } else { + "Script writing instructions:" + } +} + +pub fn tui_usage_query_copilot_auto_auth() -> &'static str { + if is_chinese() { + "自动使用 OAuth 认证,无需手动配置凭证" + } else { + "Auto OAuth authentication, no manual credentials needed" + } +} + +pub fn tui_usage_query_token_plan_hint() -> &'static str { + if is_chinese() { + "自动使用供应商的 API Key 和 Base URL 查询 Token Plan 额度" + } else { + "Automatically uses the provider's API Key and Base URL to query Token Plan quota" + } +} + +pub fn tui_usage_query_balance_hint() -> &'static str { + if is_chinese() { + "自动使用供应商的 API Key 查询账户余额" + } else { + "Automatically uses the provider's API Key to query account balance" + } +} + +pub fn tui_usage_query_script_empty() -> &'static str { + if is_chinese() { + "脚本配置不能为空" + } else { + "Script configuration cannot be empty" + } +} + +pub fn tui_usage_query_must_have_return() -> &'static str { + if is_chinese() { + "脚本必须包含 return 语句" + } else { + "Script must contain return statement" + } +} + +pub fn tui_usage_query_coding_plan_provider() -> &'static str { + if is_chinese() { + "Coding Plan 供应商" + } else { + "Coding Plan Provider" + } +} + +pub fn tui_usage_query_info() -> &'static str { + if is_chinese() { + "说明" + } else { + "Info" + } +} + +pub fn tui_usage_query_custom_hint() -> &'static str { + if is_chinese() { + "支持变量: {{apiKey}}, {{baseUrl}} | extractor 函数接收 API 响应的 JSON 对象" + } else { + "Supported variables: {{apiKey}}, {{baseUrl}} | extractor function receives API response JSON object" + } +} + +pub fn tui_usage_query_credentials_config() -> &'static str { + if is_chinese() { + "凭证配置" + } else { + "Credentials" + } +} + +pub fn tui_usage_query_credentials_hint() -> &'static str { + if is_chinese() { + "留空则自动使用供应商配置" + } else { + "Leave empty to use provider config" + } +} + +pub fn tui_usage_query_optional() -> &'static str { + if is_chinese() { + "可选" + } else { + "optional" + } +} + +pub fn tui_usage_query_base_url() -> &'static str { + if is_chinese() { + "请求地址" + } else { + "Base URL" + } +} + +pub fn tui_usage_query_api_key_placeholder() -> &'static str { + if is_chinese() { + "留空则使用供应商的 API Key" + } else { + "Leave empty to use provider's API Key" + } +} + +pub fn tui_usage_query_base_url_placeholder() -> &'static str { + if is_chinese() { + "留空则使用供应商的请求地址" + } else { + "Leave empty to use provider's base URL" + } +} + +pub fn tui_usage_query_access_token_placeholder() -> &'static str { + if is_chinese() { + "在'安全设置'里生成" + } else { + "Generate in 'Security Settings'" + } +} + +pub fn tui_usage_query_user_id_placeholder() -> &'static str { + if is_chinese() { + "例如:114514" + } else { + "e.g., 114514" + } +} + +pub fn tui_usage_query_config_format() -> &'static str { + if is_chinese() { + "配置格式:" + } else { + "Configuration format:" + } +} + +pub fn tui_usage_query_extractor_format() -> &'static str { + if is_chinese() { + "extractor 返回格式(所有字段均为可选):" + } else { + "Extractor return format (all fields optional):" + } +} + +pub fn tui_usage_query_tips() -> &'static str { + if is_chinese() { + "💡 提示:" + } else { + "💡 Tips:" + } +} + +pub fn tui_usage_query_field_is_valid() -> &'static str { + if is_chinese() { + "• isValid: 布尔值,套餐是否有效" + } else { + "• isValid: Boolean, whether plan is valid" + } +} + +pub fn tui_usage_query_field_invalid_message() -> &'static str { + if is_chinese() { + "• invalidMessage: 字符串,失效原因说明(当 isValid 为 false 时显示)" + } else { + "• invalidMessage: String, reason for expiration (shown when isValid is false)" + } +} + +pub fn tui_usage_query_field_remaining() -> &'static str { + if is_chinese() { + "• remaining: 数字,剩余额度" + } else { + "• remaining: Number, remaining quota" + } +} + +pub fn tui_usage_query_field_unit() -> &'static str { + if is_chinese() { + "• unit: 字符串,单位(如 \"USD\")" + } else { + "• unit: String, unit (e.g., \"USD\")" + } +} + +pub fn tui_usage_query_field_plan_name() -> &'static str { + if is_chinese() { + "• planName: 字符串,套餐名称" + } else { + "• planName: String, plan name" + } +} + +pub fn tui_usage_query_field_total() -> &'static str { + if is_chinese() { + "• total: 数字,总额度" + } else { + "• total: Number, total quota" + } +} + +pub fn tui_usage_query_field_used() -> &'static str { + if is_chinese() { + "• used: 数字,已用额度" + } else { + "• used: Number, used quota" + } +} + +pub fn tui_usage_query_field_extra() -> &'static str { + if is_chinese() { + "• extra: 字符串,扩展字段,可自由补充需要展示的文本" + } else { + "• extra: String, custom display text" + } +} + +pub fn tui_usage_query_tip1() -> &'static str { + if is_chinese() { + "• 变量 {{apiKey}} 和 {{baseUrl}} 会自动替换" + } else { + "• Variables {{apiKey}} and {{baseUrl}} are automatically replaced" + } +} + +pub fn tui_usage_query_tip2() -> &'static str { + if is_chinese() { + "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法" + } else { + "• Extractor function runs in sandbox environment, supports ES2020+ syntax" + } +} + +pub fn tui_usage_query_tip3() -> &'static str { + if is_chinese() { + "• 整个配置必须用 () 包裹,形成对象字面量表达式" + } else { + "• Entire config must be wrapped in () to form object literal expression" + } +} + +pub fn tui_form_json_title() -> &'static str { + "JSON" +} + +pub fn tui_codex_auth_json_title() -> &'static str { + if is_chinese() { + "auth.json (JSON) *" + } else { + "auth.json (JSON) *" + } +} + +pub fn tui_codex_config_toml_title() -> &'static str { + if is_chinese() { + "config.toml (TOML)" + } else { + "config.toml (TOML)" + } +} + +pub fn tui_form_input_title() -> &'static str { + if is_chinese() { + "输入" + } else { + "Input" + } +} + +pub fn tui_form_editing_title() -> &'static str { + if is_chinese() { + "编辑中" + } else { + "Editing" + } +} + +pub fn tui_claude_model_config_popup_title() -> &'static str { + if is_chinese() { + "Claude 模型配置" + } else { + "Claude Model Configuration" + } +} + +pub fn tui_claude_model_main_label() -> &'static str { + if is_chinese() { + "主模型" + } else { + "Main Model" + } +} + +pub fn tui_claude_reasoning_model_label() -> &'static str { + if is_chinese() { + "推理模型 (Thinking)" + } else { + "Reasoning Model (Thinking)" + } +} + +pub fn tui_claude_default_haiku_model_label() -> &'static str { + if is_chinese() { + "默认 Haiku 模型" + } else { + "Default Haiku Model" + } +} + +pub fn tui_claude_default_sonnet_model_label() -> &'static str { + if is_chinese() { + "默认 Sonnet 模型" + } else { + "Default Sonnet Model" + } +} + +pub fn tui_claude_default_opus_model_label() -> &'static str { + if is_chinese() { + "默认 Opus 模型" + } else { + "Default Opus Model" + } +} + +pub fn tui_claude_model_config_summary(configured_count: usize) -> String { + if is_chinese() { + format!("已配置 {configured_count}/4") + } else { + format!("Configured {configured_count}/4") + } +} + +pub fn tui_claude_model_config_open_hint() -> &'static str { + if is_chinese() { + "按 Enter 配置 Claude 模型" + } else { + "Press Enter to configure Claude models" + } +} + +pub fn tui_form_open_editor_hint() -> &'static str { + if is_chinese() { + "按 Enter 打开编辑器" + } else { + "Press Enter to open editor" + } +} + +pub fn tui_form_open_page_hint() -> &'static str { + if is_chinese() { + "按 Enter 打开" + } else { + "Press Enter to open" + } +} + +pub fn tui_hint_press() -> &'static str { + if is_chinese() { + "按 " + } else { + "Press " + } +} + +pub fn tui_hint_auto_fetch_models_from_api() -> &'static str { + if is_chinese() { + " 从 API 自动获取模型。" + } else { + " to auto-fetch models from API." + } +} + +pub fn tui_model_fetch_popup_title(fetching: bool) -> String { + if is_chinese() { + if fetching { + "选择模型 (获取中...)".to_string() + } else { + "选择模型".to_string() + } + } else { + if fetching { + "Select Model (Fetching...)".to_string() + } else { + "Select Model".to_string() + } + } +} + +pub fn tui_model_fetch_search_placeholder() -> &'static str { + if is_chinese() { + "输入过滤 或 直接回车使用输入值..." + } else { + "Type to filter, or press Enter to use input..." + } +} + +pub fn tui_model_fetch_search_title() -> &'static str { + if is_chinese() { + "模型搜索" + } else { + "Model Search" + } +} + +pub fn tui_model_fetch_no_models() -> &'static str { + if is_chinese() { + "没有获取到模型 (可直接输入并在此回车)" + } else { + "No models found (type custom and press Enter)" + } +} + +pub fn tui_model_fetch_no_matches() -> &'static str { + if is_chinese() { + "没有匹配结果 (可直接输入并在此回车)" + } else { + "No matching models (press Enter to use input)" + } +} + +pub fn tui_model_fetch_error_hint(err: &str) -> String { + if is_chinese() { + format!("获取失败: {}", err) + } else { + format!("Fetch failed: {}", err) + } +} + +pub fn tui_provider_not_found() -> &'static str { + if is_chinese() { + "未找到该供应商。" + } else { + "Provider not found." + } +} + +pub fn tui_provider_title() -> &'static str { + if is_chinese() { + "供应商" + } else { + "Provider" + } +} + +pub fn tui_provider_add_title() -> &'static str { + if is_chinese() { + "新增供应商" + } else { + "Add Provider" + } +} + +pub fn tui_provider_empty_title() -> &'static str { + if is_chinese() { + "还没有添加任何供应商" + } else { + "No providers have been added yet" + } +} + +pub fn tui_provider_empty_subtitle() -> &'static str { + if is_chinese() { + "如果你已有配置,请点击\"导入当前配置\",所有数据将安全保存在 default 供应商中" + } else { + "If you already have a config, use \"Import Current Config\". Everything will be safely stored in the default provider." + } +} + +pub fn tui_key_import_current_config() -> &'static str { + if is_chinese() { + "导入当前配置" + } else { + "import current config" + } +} + +pub fn tui_key_add_provider() -> &'static str { + if is_chinese() { + "添加供应商" + } else { + "add provider" + } +} + +pub fn tui_codex_official_no_api_key_tip() -> &'static str { + if is_chinese() { + "官方无需填写 API Key,直接保存即可。" + } else { + "Official provider doesn't require an API key. Just save." + } +} + +pub fn tui_toast_codex_official_auth_json_disabled() -> &'static str { + if is_chinese() { + "官方模式下不支持编辑 auth.json(切换时会移除)。" + } else { + "auth.json editing is disabled for the official provider (it will be removed on switch)." + } +} + +pub fn tui_provider_edit_title(name: &str) -> String { + if is_chinese() { + format!("编辑供应商: {name}") + } else { + format!("Edit Provider: {name}") + } +} + +pub fn tui_key_switch() -> &'static str { + if is_chinese() { + "切换" + } else { + "switch" + } +} + +pub fn tui_key_edit() -> &'static str { + if is_chinese() { + "编辑" + } else { + "edit" + } +} + +pub fn tui_key_speedtest() -> &'static str { + if is_chinese() { + "测速" + } else { + "speedtest" + } +} + +pub fn tui_key_test() -> &'static str { + if is_chinese() { + "测试" + } else { + "test" + } +} + +pub fn tui_key_stream_check() -> &'static str { + if is_chinese() { + "健康检查" + } else { + "stream check" + } +} + +pub fn tui_key_launch_temp() -> &'static str { + if is_chinese() { + "临时启动" + } else { + "launch temp" + } +} + +pub fn tui_temp_launch_failed(message: &str) -> String { + if is_chinese() { + format!("临时启动失败: {message}") + } else { + format!("Temporary launch failed: {message}") + } +} + +pub fn tui_stream_check_status_operational() -> &'static str { + if is_chinese() { + "正常" + } else { + "operational" + } +} + +pub fn tui_stream_check_status_degraded() -> &'static str { + if is_chinese() { + "降级" + } else { + "degraded" + } +} + +pub fn tui_stream_check_status_failed() -> &'static str { + if is_chinese() { + "失败" + } else { + "failed" + } +} + +pub fn tui_key_details() -> &'static str { + if is_chinese() { + "详情" + } else { + "details" + } +} + +pub fn tui_key_view() -> &'static str { + if is_chinese() { + "查看" + } else { + "view" + } +} + +pub fn tui_key_add() -> &'static str { + if is_chinese() { + "新增" + } else { + "add" + } +} + +pub fn tui_key_delete() -> &'static str { + if is_chinese() { + "删除" + } else { + "delete" + } +} + +pub fn tui_key_import() -> &'static str { + if is_chinese() { + "导入" + } else { + "import" + } +} + +pub fn tui_key_failover() -> &'static str { + if is_chinese() { + "管理故障转移" + } else { + "manage failover" + } +} + +pub fn tui_key_install() -> &'static str { + if is_chinese() { + "安装" + } else { + "install" + } +} + +pub fn tui_key_uninstall() -> &'static str { + if is_chinese() { + "卸载" + } else { + "uninstall" + } +} + +pub fn tui_key_discover() -> &'static str { + if is_chinese() { + "发现" + } else { + "discover" + } +} + +pub fn tui_key_unmanaged() -> &'static str { + if is_chinese() { + "已有" + } else { + "existing" + } +} + +pub fn tui_key_repos() -> &'static str { + if is_chinese() { + "仓库" + } else { + "repos" + } +} + +pub fn tui_key_sync() -> &'static str { + if is_chinese() { + "同步" + } else { + "sync" + } +} + +pub fn tui_key_sync_method() -> &'static str { + if is_chinese() { + "同步方式" + } else { + "sync method" + } +} + +pub fn tui_key_search() -> &'static str { + if is_chinese() { + "搜索" + } else { + "search" + } +} + +pub fn tui_key_refresh() -> &'static str { + if is_chinese() { + "刷新" + } else { + "refresh" + } +} + +pub fn tui_key_rename() -> &'static str { + if is_chinese() { + "重命名" + } else { + "rename" + } +} + +pub fn tui_key_start_proxy() -> &'static str { + if is_chinese() { + "启动代理" + } else { + "start proxy" + } +} + +pub fn tui_key_stop_proxy() -> &'static str { + if is_chinese() { + "停止代理" + } else { + "stop proxy" + } +} + +pub fn tui_key_proxy_on() -> &'static str { + if is_chinese() { + "代理开" + } else { + "proxy on" + } +} + +pub fn tui_key_proxy_off() -> &'static str { + if is_chinese() { + "代理关" + } else { + "proxy off" + } +} + +pub fn tui_key_focus() -> &'static str { + if is_chinese() { + "切换窗口" + } else { + "next pane" + } +} + +pub fn tui_key_pane() -> &'static str { + if is_chinese() { + "切换面板" + } else { + "switch panel" + } +} + +pub fn tui_key_toggle() -> &'static str { + if is_chinese() { + "启用/禁用" + } else { + "toggle" + } +} + +pub fn tui_key_apps() -> &'static str { + if is_chinese() { + "应用" + } else { + "apps" + } +} + +pub fn tui_key_activate() -> &'static str { + if is_chinese() { + "激活" + } else { + "activate" + } +} + +pub fn tui_key_deactivate() -> &'static str { + if is_chinese() { + "取消激活" + } else { + "deactivate" + } +} + +pub fn tui_key_open() -> &'static str { + if is_chinese() { + "打开" + } else { + "open" + } +} + +pub fn tui_key_apply() -> &'static str { + if is_chinese() { + "应用" + } else { + "apply" + } +} + +pub fn tui_key_extract() -> &'static str { + if is_chinese() { + "提取" + } else { + "extract" + } +} + +pub fn tui_key_format() -> &'static str { + if is_chinese() { + "格式化" + } else { + "format" + } +} + +pub fn tui_key_edit_snippet() -> &'static str { + if is_chinese() { + "编辑片段" + } else { + "edit snippet" + } +} + +pub fn tui_key_close() -> &'static str { + if is_chinese() { + "关闭" + } else { + "close" + } +} + +pub fn tui_key_exit() -> &'static str { + if is_chinese() { + "退出" + } else { + "exit" + } +} + +pub fn tui_key_cancel() -> &'static str { + if is_chinese() { + "取消" + } else { + "cancel" + } +} + +pub fn tui_key_submit() -> &'static str { + if is_chinese() { + "提交" + } else { + "submit" + } +} + +pub fn tui_key_yes() -> &'static str { + if is_chinese() { + "确认" + } else { + "confirm" + } +} + +pub fn tui_key_no() -> &'static str { + if is_chinese() { + "返回" + } else { + "back" + } +} + +pub fn tui_key_scroll() -> &'static str { + if is_chinese() { + "滚动" + } else { + "scroll" + } +} + +pub fn tui_key_restore() -> &'static str { + if is_chinese() { + "恢复" + } else { + "restore" + } +} + +pub fn tui_key_takeover() -> &'static str { + if is_chinese() { + "接管" + } else { + "take over" + } +} + +pub fn tui_key_save() -> &'static str { + if is_chinese() { + "保存" + } else { + "save" + } +} + +pub fn tui_key_external_editor() -> &'static str { + if is_chinese() { + "外部编辑器" + } else { + "external editor" + } +} + +pub fn tui_key_save_and_exit() -> &'static str { + if is_chinese() { + "保存并退出" + } else { + "save & exit" + } +} + +pub fn tui_key_exit_without_save() -> &'static str { + if is_chinese() { + "不保存退出" + } else { + "exit w/o save" + } +} + +pub fn tui_key_edit_mode() -> &'static str { + if is_chinese() { + "编辑" + } else { + "edit" + } +} + +pub fn tui_key_clear() -> &'static str { + if is_chinese() { + "清除" + } else { + "clear" + } +} + +pub fn tui_key_move() -> &'static str { + if is_chinese() { + "移动" + } else { + "move" + } +} + +pub fn tui_key_exit_edit() -> &'static str { + if is_chinese() { + "退出编辑" + } else { + "exit edit" + } +} + +pub fn tui_key_select() -> &'static str { + if is_chinese() { + "选择" + } else { + "select" + } +} + +pub fn tui_key_fetch_model() -> &'static str { + if is_chinese() { + "获取模型" + } else { + "fetch model" + } +} + +pub fn tui_key_deactivate_active() -> &'static str { + if is_chinese() { + "取消激活(当前)" + } else { + "deactivate active" + } +} + +pub fn tui_provider_list_keys() -> &'static str { + if is_chinese() { + "按键:a=新增 e=编辑 Space=切换 /=搜索" + } else { + "Keys: a=add e=edit Space=switch /=filter" + } +} + +pub fn tui_home_ascii_logo() -> &'static str { + // Same ASCII art across languages. + r#" _ _ _ + ___ ___ ___ __ __(_)| |_ ___ | |__ + / __|/ __|_____ / __|\ \ /\ / /| || __|/ __|| '_ \ + | (__| (__|_____|\__ \ \ V V / | || |_| (__ | | | | + \___|\___| |___/ \_/\_/ |_| \__|\___||_| |_| + "# +} + +pub fn tui_common_snippet_keys() -> &'static str { + if is_chinese() { + "按键:e=编辑 c=清除 a=应用 Esc=返回" + } else { + "Keys: e=edit c=clear a=apply Esc=back" + } +} + +pub fn tui_view_config_app(app: &str) -> String { + if is_chinese() { + format!("应用: {}", app) + } else { + format!("App: {}", app) + } +} + +pub fn tui_view_config_provider(provider: &str) -> String { + if is_chinese() { + format!("供应商: {}", provider) + } else { + format!("Provider: {}", provider) + } +} + +pub fn tui_view_config_api_url(url: &str) -> String { + if is_chinese() { + format!("API URL: {}", url) + } else { + format!("API URL: {}", url) + } +} + +pub fn tui_view_config_mcp_servers(enabled: usize, total: usize) -> String { + if is_chinese() { + format!("MCP 服务器: {} 启用 / {} 总数", enabled, total) + } else { + format!("MCP servers: {} enabled / {} total", enabled, total) + } +} + +pub fn tui_view_config_prompts(active: &str) -> String { + if is_chinese() { + format!("提示词: {}", active) + } else { + format!("Prompts: {}", active) + } +} + +pub fn tui_view_config_config_file(path: &str) -> String { + if is_chinese() { + format!("配置文件: {}", path) + } else { + format!("Config file: {}", path) + } +} + +pub fn tui_settings_header_language() -> &'static str { + if is_chinese() { + "语言" + } else { + "Language" + } +} + +pub fn tui_settings_header_setting() -> &'static str { + if is_chinese() { + "设置项" + } else { + "Setting" + } +} + +pub fn tui_settings_header_value() -> &'static str { + if is_chinese() { + "值" + } else { + "Value" + } +} + +pub fn tui_settings_title() -> &'static str { + if is_chinese() { + "设置" + } else { + "Settings" + } +} + +pub fn tui_settings_proxy_title() -> &'static str { + if is_chinese() { + "本地代理" + } else { + "Local Proxy" + } +} + +pub fn tui_settings_proxy_listen_address_label() -> &'static str { + if is_chinese() { + "监听地址" + } else { + "Listen Address" + } +} + +pub fn tui_settings_proxy_listen_port_label() -> &'static str { + if is_chinese() { + "监听端口" + } else { + "Listen Port" + } +} + +pub fn tui_settings_proxy_listen_address_prompt() -> &'static str { + if is_chinese() { + "输入监听地址(如 127.0.0.1 / localhost / 0.0.0.0)" + } else { + "Enter listen address (for example 127.0.0.1 / localhost / 0.0.0.0)" + } +} + +pub fn tui_settings_proxy_listen_port_prompt() -> &'static str { + if is_chinese() { + "输入监听端口(1024-65535)" + } else { + "Enter listen port (1024-65535)" + } +} + +pub fn tui_settings_openclaw_config_dir_label() -> &'static str { + if is_chinese() { + "OpenClaw 配置目录" + } else { + "OpenClaw Config Directory" + } +} + +pub fn tui_settings_openclaw_config_dir_prompt() -> &'static str { + if is_chinese() { + "输入 OpenClaw 配置目录;留空恢复默认 ~/.openclaw" + } else { + "Enter the OpenClaw config directory; leave empty to use ~/.openclaw" + } +} + +pub fn tui_settings_openclaw_config_dir_default_value() -> &'static str { + "Default (~/.openclaw)" +} diff --git a/src-tauri/src/cli/i18n/texts/settings_misc.rs b/src-tauri/src/cli/i18n/texts/settings_misc.rs new file mode 100644 index 00000000..32878d7b --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/settings_misc.rs @@ -0,0 +1,912 @@ +use super::is_chinese; +pub fn tui_label_skills() -> &'static str { + if is_chinese() { + "技能:" + } else { + "Skills:" + } +} + +pub fn prompts_label() -> &'static str { + if is_chinese() { + "提示词:" + } else { + "Prompts:" + } +} + +pub fn total() -> &'static str { + if is_chinese() { + "总计" + } else { + "Total" + } +} + +pub fn enabled() -> &'static str { + if is_chinese() { + "启用" + } else { + "Enabled" + } +} + +pub fn disabled() -> &'static str { + if is_chinese() { + "禁用" + } else { + "Disabled" + } +} + +pub fn active() -> &'static str { + if is_chinese() { + "活动" + } else { + "Active" + } +} + +pub fn none() -> &'static str { + if is_chinese() { + "无" + } else { + "None" + } +} + +// Settings +pub fn settings_title() -> &'static str { + if is_chinese() { + "⚙️ 设置" + } else { + "⚙️ Settings" + } +} + +pub fn change_language() -> &'static str { + if is_chinese() { + "🌐 切换语言" + } else { + "🌐 Change Language" + } +} + +pub fn current_language_label() -> &'static str { + if is_chinese() { + "当前语言" + } else { + "Current Language" + } +} + +pub fn select_language() -> &'static str { + if is_chinese() { + "选择语言:" + } else { + "Select language:" + } +} + +pub fn language_changed() -> &'static str { + if is_chinese() { + "✓ 语言已更改" + } else { + "✓ Language changed" + } +} + +pub fn skip_claude_onboarding() -> &'static str { + if is_chinese() { + "🚫 跳过 Claude Code 初次安装确认" + } else { + "🚫 Skip Claude Code onboarding confirmation" + } +} + +pub fn skip_claude_onboarding_label() -> &'static str { + if is_chinese() { + "跳过 Claude Code 初次安装确认" + } else { + "Skip Claude Code onboarding confirmation" + } +} + +pub fn skip_claude_onboarding_confirm(enable: bool, path: &str) -> String { + if is_chinese() { + if enable { + format!( + "确认启用跳过 Claude Code 初次安装确认?\n将写入 {path}: hasCompletedOnboarding=true" + ) + } else { + format!("确认恢复 Claude Code 初次安装确认?\n将从 {path} 删除 hasCompletedOnboarding") + } + } else { + if enable { + format!( + "Enable skipping Claude Code onboarding confirmation?\nWrites hasCompletedOnboarding=true to {path}" + ) + } else { + format!( + "Disable skipping Claude Code onboarding confirmation?\nRemoves hasCompletedOnboarding from {path}" + ) + } + } +} + +pub fn skip_claude_onboarding_changed(enable: bool) -> String { + if is_chinese() { + if enable { + "✓ 已启用:跳过 Claude Code 初次安装确认".to_string() + } else { + "✓ 已恢复 Claude Code 初次安装确认".to_string() + } + } else { + if enable { + "✓ Skip Claude Code onboarding confirmation enabled".to_string() + } else { + "✓ Claude Code onboarding confirmation restored".to_string() + } + } +} + +pub fn enable_claude_plugin_integration() -> &'static str { + if is_chinese() { + "🔌 接管 Claude Code for VSCode 插件" + } else { + "🔌 Apply to Claude Code for VSCode" + } +} + +pub fn enable_claude_plugin_integration_label() -> &'static str { + if is_chinese() { + "接管 Claude Code for VSCode 插件" + } else { + "Apply to Claude Code for VSCode" + } +} + +pub fn enable_claude_plugin_integration_confirm(enable: bool, path: &str) -> String { + if is_chinese() { + if enable { + format!( + "确认启用 Claude Code for VSCode 插件联动?\n将写入 {path}: primaryApiKey=\"any\"" + ) + } else { + "确认关闭 Claude Code for VSCode 插件联动?".to_string() + } + } else { + if enable { + format!( + "Enable Claude Code for VSCode integration?\nWrites primaryApiKey=\"any\" to {path}" + ) + } else { + format!( + "Disable Claude Code for VSCode integration?\nRemoves primaryApiKey from {path}" + ) + } + } +} + +pub fn enable_claude_plugin_integration_changed(enable: bool) -> String { + if is_chinese() { + if enable { + "✓ 已启用 Claude Code for VSCode 插件联动".to_string() + } else { + "✓ 已关闭 Claude Code for VSCode 插件联动".to_string() + } + } else { + if enable { + "✓ Claude Code for VSCode integration enabled".to_string() + } else { + "✓ Claude Code for VSCode integration disabled".to_string() + } + } +} + +pub fn claude_plugin_sync_failed_warning(err: &str) -> String { + if is_chinese() { + format!("⚠ Claude Code for VSCode 插件联动失败: {err}") + } else { + format!("⚠ Claude Code for VSCode integration failed: {err}") + } +} + +pub fn codex_unified_session_history_label() -> &'static str { + if is_chinese() { + "统一 Codex 会话历史" + } else { + "Unified Codex session history" + } +} + +pub fn codex_unified_session_history_confirm(enable: bool) -> String { + if is_chinese() { + if enable { + "确认开启统一 Codex 会话历史?\n官方订阅将使用共享 custom 供应商标识运行;已有官方会话不会自动迁移,可用 CLI 命令 settings codex-history migrate-existing 单独迁移。".to_string() + } else { + "确认关闭统一 Codex 会话历史?\n不会自动恢复已迁移的会话;如需恢复,请使用 CLI 命令 settings codex-history restore。".to_string() + } + } else { + if enable { + "Enable unified Codex session history?\nOfficial subscriptions will use the shared custom provider id. Existing official sessions are not migrated automatically; use settings codex-history migrate-existing from the CLI if needed.".to_string() + } else { + "Disable unified Codex session history?\nMigrated sessions are not restored automatically; use settings codex-history restore from the CLI if needed.".to_string() + } + } +} + +// App Selection +pub fn select_application() -> &'static str { + if is_chinese() { + "选择应用程序:" + } else { + "Select application:" + } +} + +pub fn switched_to_app(app: &str) -> String { + if is_chinese() { + format!("✓ 已切换到 {}", app) + } else { + format!("✓ Switched to {}", app) + } +} + +// Common +pub fn press_enter() -> &'static str { + if is_chinese() { + "按 Enter 继续..." + } else { + "Press Enter to continue..." + } +} + +pub fn error_prefix() -> &'static str { + if is_chinese() { + "错误" + } else { + "Error" + } +} + +// Table Headers +pub fn header_name() -> &'static str { + if is_chinese() { + "名称" + } else { + "Name" + } +} + +pub fn header_category() -> &'static str { + if is_chinese() { + "类别" + } else { + "Category" + } +} + +pub fn header_description() -> &'static str { + if is_chinese() { + "描述" + } else { + "Description" + } +} + +// Config Management +pub fn config_management() -> &'static str { + if is_chinese() { + "⚙️ 配置文件管理" + } else { + "⚙️ Configuration Management" + } +} + +pub fn config_export() -> &'static str { + if is_chinese() { + "📤 导出配置" + } else { + "📤 Export Config" + } +} + +pub fn config_import() -> &'static str { + if is_chinese() { + "📥 导入配置" + } else { + "📥 Import Config" + } +} + +pub fn config_backup() -> &'static str { + if is_chinese() { + "💾 备份配置" + } else { + "💾 Backup Config" + } +} + +pub fn config_restore() -> &'static str { + if is_chinese() { + "♻️ 恢复配置" + } else { + "♻️ Restore Config" + } +} + +pub fn config_validate() -> &'static str { + if is_chinese() { + "✓ 验证配置" + } else { + "✓ Validate Config" + } +} + +pub fn config_common_snippet() -> &'static str { + if is_chinese() { + "🧩 通用配置片段" + } else { + "🧩 Common Config Snippet" + } +} + +pub fn config_common_snippet_title() -> &'static str { + if is_chinese() { + "通用配置片段" + } else { + "Common Config Snippet" + } +} + +pub fn config_common_snippet_none_set() -> &'static str { + if is_chinese() { + "未设置通用配置片段。" + } else { + "No common config snippet is set." + } +} + +pub fn config_common_snippet_set_for_app(app: &str) -> String { + if is_chinese() { + format!("✓ 已为应用 '{}' 设置通用配置片段", app) + } else { + format!("✓ Common config snippet set for app '{}'", app) + } +} + +pub fn config_common_snippet_require_json_or_file() -> &'static str { + if is_chinese() { + "请提供 --snippet(或兼容别名 --json)或 --file" + } else { + "Please provide --snippet (or the compatibility alias --json) or --file" + } +} + +pub fn config_reset() -> &'static str { + if is_chinese() { + "🔄 重置配置" + } else { + "🔄 Reset Config" + } +} + +pub fn config_show_full() -> &'static str { + if is_chinese() { + "👁️ 查看完整配置" + } else { + "👁️ Show Full Config" + } +} + +pub fn config_show_path() -> &'static str { + if is_chinese() { + "📍 显示配置路径" + } else { + "📍 Show Config Path" + } +} + +pub fn enter_export_path() -> &'static str { + if is_chinese() { + "输入导出文件路径:" + } else { + "Enter export file path:" + } +} + +pub fn enter_import_path() -> &'static str { + if is_chinese() { + "输入导入文件路径:" + } else { + "Enter import file path:" + } +} + +pub fn enter_restore_path() -> &'static str { + if is_chinese() { + "输入备份文件路径:" + } else { + "Enter backup file path:" + } +} + +pub fn confirm_import() -> &'static str { + if is_chinese() { + "确定要导入配置吗?这将覆盖当前配置。" + } else { + "Are you sure you want to import? This will overwrite current configuration." + } +} + +pub fn confirm_reset() -> &'static str { + if is_chinese() { + "确定要重置配置吗?这将删除所有自定义设置。" + } else { + "Are you sure you want to reset? This will delete all custom settings." + } +} + +pub fn common_config_snippet_editor_prompt(app: &str) -> String { + let is_codex = app == "codex"; + if is_chinese() { + if is_codex { + format!("编辑 {app} 的通用配置片段(TOML,留空则清除):") + } else { + format!("编辑 {app} 的通用配置片段(JSON 对象,留空则清除):") + } + } else { + if is_codex { + format!("Edit common config snippet for {app} (TOML; empty to clear):") + } else { + format!("Edit common config snippet for {app} (JSON object; empty to clear):") + } + } +} + +pub fn common_config_snippet_invalid_json(err: &str) -> String { + if is_chinese() { + format!("JSON 无效:{err}") + } else { + format!("Invalid JSON: {err}") + } +} + +pub fn common_config_snippet_invalid_toml(err: &str) -> String { + if is_chinese() { + format!("TOML 无效:{err}") + } else { + format!("Invalid TOML: {err}") + } +} + +pub fn failed_to_serialize_json(err: &str) -> String { + if is_chinese() { + format!("序列化 JSON 失败:{err}") + } else { + format!("Failed to serialize JSON: {err}") + } +} + +pub fn common_config_snippet_not_object() -> &'static str { + if is_chinese() { + "通用配置必须是 JSON 对象(例如:{\"env\":{...}})" + } else { + "Common config must be a JSON object (e.g. {\"env\":{...}})" + } +} + +pub fn common_config_snippet_saved() -> &'static str { + if is_chinese() { + "✓ 已保存通用配置片段" + } else { + "✓ Common config snippet saved" + } +} + +pub fn common_config_snippet_cleared() -> &'static str { + if is_chinese() { + "✓ 已清除通用配置片段" + } else { + "✓ Common config snippet cleared" + } +} + +pub fn common_config_snippet_apply_now() -> &'static str { + if is_chinese() { + "现在在适用时刷新 live 配置?" + } else { + "Refresh live config now when applicable?" + } +} + +pub fn common_config_snippet_no_current_provider() -> &'static str { + if is_chinese() { + "当前未选择供应商,已保存通用配置片段。" + } else { + "No current provider selected; common config snippet saved." + } +} + +pub fn common_config_snippet_no_current_provider_after_clear() -> &'static str { + if is_chinese() { + "当前未选择供应商,已清除通用配置片段。" + } else { + "No current provider selected; common config snippet cleared." + } +} + +pub fn common_config_snippet_applied() -> &'static str { + if is_chinese() { + "✓ 已在适用时刷新 live 配置(请重启对应客户端)" + } else { + "✓ Refreshed live config when applicable (restart the client)" + } +} + +pub fn common_config_snippet_apply_not_needed() -> &'static str { + if is_chinese() { + "当前应用使用 additive 模式,无需执行当前 provider 刷新。" + } else { + "This app uses additive mode; no current-provider refresh is needed." + } +} + +pub fn common_config_snippet_apply_hint() -> &'static str { + if is_chinese() { + "提示:切换一次供应商即可重新写入 live 配置。" + } else { + "Tip: switch provider once to re-write the live config." + } +} + +pub fn common_config_snippet_extracted() -> &'static str { + if is_chinese() { + "已从当前编辑内容提取通用配置片段" + } else { + "Extracted common config snippet from current edits" + } +} + +pub fn common_config_snippet_formatted() -> &'static str { + if is_chinese() { + "已格式化通用配置片段" + } else { + "Formatted common config snippet" + } +} + +pub fn common_config_snippet_extract_empty() -> &'static str { + if is_chinese() { + "当前编辑内容没有可提取的通用配置" + } else { + "No common config found in the current edits" + } +} + +pub fn tui_common_config_notice_title() -> &'static str { + if is_chinese() { + "关于通用配置" + } else { + "About Common Config" + } +} + +pub fn tui_common_config_notice_message(app: &str) -> String { + if is_chinese() { + format!( + "通用配置适合保存多个 {app} 供应商共享的插件、环境变量和工具配置。\ + \n\n有可用片段时,新建供应商会默认勾选“添加通用配置”。\ + \n\n如果在当前表单里新增了插件、hooks 或环境变量,可以在“通用配置”编辑器里按 F4 从当前编辑内容提取,再按 Ctrl+S 保存片段。" + ) + } else { + format!( + "Common Config is for plugin, environment, and tool settings shared by multiple {app} providers.\ + \n\nWhen a usable snippet exists, new providers will default to attaching it.\ + \n\nAfter adding plugins, hooks, or environment variables in this form, open Common Config, press F4 to extract from the current edits, then press Ctrl+S to save the snippet." + ) + } +} + +pub fn confirm_restore() -> &'static str { + if is_chinese() { + "确定要从备份恢复配置吗?" + } else { + "Are you sure you want to restore from backup?" + } +} + +pub fn exported_to(path: &str) -> String { + if is_chinese() { + format!("✓ 已导出到 '{}'", path) + } else { + format!("✓ Exported to '{}'", path) + } +} + +pub fn imported_from(path: &str) -> String { + if is_chinese() { + format!("✓ 已从 '{}' 导入", path) + } else { + format!("✓ Imported from '{}'", path) + } +} + +pub fn backup_created(id: &str) -> String { + if is_chinese() { + format!("✓ 已创建备份,ID: {}", id) + } else { + format!("✓ Backup created, ID: {}", id) + } +} + +pub fn restored_from(path: &str) -> String { + if is_chinese() { + format!("✓ 已从 '{}' 恢复", path) + } else { + format!("✓ Restored from '{}'", path) + } +} + +pub fn config_valid() -> &'static str { + if is_chinese() { + "✓ 配置文件有效" + } else { + "✓ Configuration is valid" + } +} + +pub fn config_reset_done() -> &'static str { + if is_chinese() { + "✓ 配置已重置为默认值" + } else { + "✓ Configuration reset to defaults" + } +} + +pub fn file_overwrite_confirm(path: &str) -> String { + if is_chinese() { + format!("文件 '{}' 已存在,是否覆盖?", path) + } else { + format!("File '{}' exists. Overwrite?", path) + } +} + +// MCP Management Additional +pub fn mcp_delete_server() -> &'static str { + if is_chinese() { + "🗑️ 删除服务器" + } else { + "🗑️ Delete Server" + } +} + +pub fn mcp_enable_server() -> &'static str { + if is_chinese() { + "✅ 启用服务器" + } else { + "✅ Enable Server" + } +} + +pub fn mcp_disable_server() -> &'static str { + if is_chinese() { + "❌ 禁用服务器" + } else { + "❌ Disable Server" + } +} + +pub fn mcp_import_servers() -> &'static str { + if is_chinese() { + "📥 导入已有 MCP 服务器" + } else { + "📥 Import Existing MCP Servers" + } +} + +pub fn mcp_validate_command() -> &'static str { + if is_chinese() { + "✓ 验证命令" + } else { + "✓ Validate Command" + } +} + +pub fn select_server_to_delete() -> &'static str { + if is_chinese() { + "选择要删除的服务器:" + } else { + "Select server to delete:" + } +} + +pub fn select_server_to_enable() -> &'static str { + if is_chinese() { + "选择要启用的服务器:" + } else { + "Select server to enable:" + } +} + +pub fn select_server_to_disable() -> &'static str { + if is_chinese() { + "选择要禁用的服务器:" + } else { + "Select server to disable:" + } +} + +pub fn select_apps_to_enable() -> &'static str { + if is_chinese() { + "选择要启用的应用:" + } else { + "Select apps to enable for:" + } +} + +pub fn select_apps_to_disable() -> &'static str { + if is_chinese() { + "选择要禁用的应用:" + } else { + "Select apps to disable for:" + } +} + +pub fn enter_command_to_validate() -> &'static str { + if is_chinese() { + "输入要验证的命令:" + } else { + "Enter command to validate:" + } +} + +pub fn server_deleted(id: &str) -> String { + if is_chinese() { + format!("✓ 已删除服务器 '{}'", id) + } else { + format!("✓ Deleted server '{}'", id) + } +} + +pub fn server_enabled(id: &str) -> String { + if is_chinese() { + format!("✓ 已启用服务器 '{}'", id) + } else { + format!("✓ Enabled server '{}'", id) + } +} + +pub fn server_disabled(id: &str) -> String { + if is_chinese() { + format!("✓ 已禁用服务器 '{}'", id) + } else { + format!("✓ Disabled server '{}'", id) + } +} + +pub fn servers_imported(count: usize) -> String { + if is_chinese() { + format!("✓ 已导入 {count} 个 MCP 服务器") + } else { + format!("✓ Imported {count} MCP server(s)") + } +} + +pub fn command_valid(cmd: &str) -> String { + if is_chinese() { + format!("✓ 命令 '{}' 有效", cmd) + } else { + format!("✓ Command '{}' is valid", cmd) + } +} + +pub fn command_invalid(cmd: &str) -> String { + if is_chinese() { + format!("✗ 命令 '{}' 未找到", cmd) + } else { + format!("✗ Command '{}' not found", cmd) + } +} + +// Prompts Management Additional +pub fn prompts_show_content() -> &'static str { + if is_chinese() { + "👁️ 查看完整内容" + } else { + "👁️ View Full Content" + } +} + +pub fn prompts_delete() -> &'static str { + if is_chinese() { + "🗑️ 删除提示词" + } else { + "🗑️ Delete Prompt" + } +} + +pub fn prompts_view_current() -> &'static str { + if is_chinese() { + "📋 查看当前提示词" + } else { + "📋 View Current Prompt" + } +} + +pub fn select_prompt_to_view() -> &'static str { + if is_chinese() { + "选择要查看的提示词:" + } else { + "Select prompt to view:" + } +} + +pub fn select_prompt_to_delete() -> &'static str { + if is_chinese() { + "选择要删除的提示词:" + } else { + "Select prompt to delete:" + } +} + +pub fn prompt_deleted(id: &str) -> String { + if is_chinese() { + format!("✓ 已删除提示词 '{}'", id) + } else { + format!("✓ Deleted prompt '{}'", id) + } +} + +pub fn no_active_prompt() -> &'static str { + if is_chinese() { + "当前没有激活的提示词。" + } else { + "No active prompt." + } +} + +pub fn cannot_delete_active() -> &'static str { + if is_chinese() { + "无法删除当前激活的提示词。" + } else { + "Cannot delete the active prompt." + } +} + +pub fn no_servers_to_delete() -> &'static str { + if is_chinese() { + "没有可删除的服务器。" + } else { + "No servers to delete." + } +} + +pub fn no_prompts_to_delete() -> &'static str { + if is_chinese() { + "没有可删除的提示词。" + } else { + "No prompts to delete." + } +} + +// Provider Speedtest +pub fn speedtest_endpoint() -> &'static str { + if is_chinese() { + "🚀 测试端点速度" + } else { + "🚀 Speedtest endpoint" + } +} + +pub fn back() -> &'static str { + if is_chinese() { + "← 返回" + } else { + "← Back" + } +} + +// ============================================ +// TUI UPDATE (TUI 自更新) diff --git a/src-tauri/src/cli/i18n/texts/toasts.rs b/src-tauri/src/cli/i18n/texts/toasts.rs new file mode 100644 index 00000000..30dfa31b --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/toasts.rs @@ -0,0 +1,1111 @@ +use super::is_chinese; +pub fn tui_common_snippet_title(app: &str) -> String { + if is_chinese() { + format!("通用片段 ({})", app) + } else { + format!("Common Snippet ({})", app) + } +} + +pub fn tui_config_reset_title() -> &'static str { + if is_chinese() { + "重置配置" + } else { + "Reset Configuration" + } +} + +pub fn tui_config_reset_message() -> &'static str { + if is_chinese() { + "重置为默认配置?(这将覆盖当前配置)" + } else { + "Reset to default configuration? (This will overwrite your current config)" + } +} + +pub fn tui_toast_export_path_empty() -> &'static str { + if is_chinese() { + "导出路径为空。" + } else { + "Export path is empty." + } +} + +pub fn tui_toast_import_path_empty() -> &'static str { + if is_chinese() { + "导入路径为空。" + } else { + "Import path is empty." + } +} + +pub fn tui_confirm_import_message(path: &str) -> String { + if is_chinese() { + format!("确认从 '{}' 导入?", path) + } else { + format!("Import from '{}'?", path) + } +} + +pub fn tui_toast_command_empty() -> &'static str { + if is_chinese() { + "命令为空。" + } else { + "Command is empty." + } +} + +pub fn tui_toast_url_empty() -> &'static str { + if is_chinese() { + "URL 为空。" + } else { + "URL is empty." + } +} + +pub fn tui_toast_mcp_env_key_empty() -> &'static str { + if is_chinese() { + "环境变量 Key 不能为空。" + } else { + "Env key cannot be empty." + } +} + +pub fn tui_toast_mcp_env_duplicate_key(key: &str) -> String { + if is_chinese() { + format!("环境变量 Key '{}' 已存在。", key) + } else { + format!("Env key '{key}' already exists.") + } +} + +pub fn tui_confirm_restore_backup_title() -> &'static str { + if is_chinese() { + "恢复备份" + } else { + "Restore Backup" + } +} + +pub fn tui_confirm_restore_backup_message(name: &str) -> String { + if is_chinese() { + format!("确认从备份 '{}' 恢复?", name) + } else { + format!("Restore from backup '{}'?", name) + } +} + +pub fn tui_speedtest_line_url(url: &str) -> String { + format!("URL: {}", url) +} + +pub fn tui_stream_check_line_provider(provider_name: &str) -> String { + if is_chinese() { + format!("供应商: {provider_name}") + } else { + format!("Provider: {provider_name}") + } +} + +pub fn tui_stream_check_line_status(status: &str) -> String { + if is_chinese() { + format!("状态: {status}") + } else { + format!("Status: {status}") + } +} + +pub fn tui_stream_check_line_response_time(response_time: &str) -> String { + if is_chinese() { + format!("耗时: {response_time}") + } else { + format!("Time: {response_time}") + } +} + +pub fn tui_stream_check_line_http_status(status: &str) -> String { + if is_chinese() { + format!("HTTP: {status}") + } else { + format!("HTTP: {status}") + } +} + +pub fn tui_stream_check_line_model(model: &str) -> String { + if is_chinese() { + format!("模型: {model}") + } else { + format!("Model: {model}") + } +} + +pub fn tui_stream_check_line_retries(retries: &str) -> String { + if is_chinese() { + format!("重试: {retries}") + } else { + format!("Retries: {retries}") + } +} + +pub fn tui_stream_check_line_message(message: &str) -> String { + if is_chinese() { + format!("信息: {message}") + } else { + format!("Message: {message}") + } +} + +pub fn tui_speedtest_line_latency(latency: &str) -> String { + if is_chinese() { + format!("延迟: {latency}") + } else { + format!("Latency: {latency}") + } +} + +pub fn tui_speedtest_line_status(status: &str) -> String { + if is_chinese() { + format!("状态: {status}") + } else { + format!("Status: {status}") + } +} + +pub fn tui_speedtest_line_error(err: &str) -> String { + if is_chinese() { + format!("错误: {err}") + } else { + format!("Error: {err}") + } +} + +pub fn tui_toast_speedtest_finished() -> &'static str { + if is_chinese() { + "测速完成。" + } else { + "Speedtest finished." + } +} + +pub fn tui_toast_speedtest_failed(err: &str) -> String { + if is_chinese() { + format!("测速失败: {err}") + } else { + format!("Speedtest failed: {err}") + } +} + +pub fn tui_toast_speedtest_unavailable(err: &str) -> String { + if is_chinese() { + format!("测速不可用: {err}") + } else { + format!("Speedtest unavailable: {err}") + } +} + +pub fn tui_toast_speedtest_disabled() -> &'static str { + if is_chinese() { + "本次会话测速不可用。" + } else { + "Speedtest is disabled for this session." + } +} + +pub fn tui_toast_local_env_check_unavailable(err: &str) -> String { + if is_chinese() { + format!("本地环境检查不可用: {err}") + } else { + format!("Local environment check unavailable: {err}") + } +} + +pub fn tui_toast_local_env_check_disabled() -> &'static str { + if is_chinese() { + "本次会话本地环境检查不可用。" + } else { + "Local environment check is disabled for this session." + } +} + +pub fn tui_toast_local_env_check_request_failed(err: &str) -> String { + if is_chinese() { + format!("本地环境检查刷新请求失败: {err}") + } else { + format!("Failed to enqueue local environment check: {err}") + } +} + +pub fn tui_toast_speedtest_request_failed(err: &str) -> String { + if is_chinese() { + format!("测速请求失败: {err}") + } else { + format!("Failed to enqueue speedtest: {err}") + } +} + +pub fn tui_toast_stream_check_finished() -> &'static str { + if is_chinese() { + "健康检查完成。" + } else { + "Stream check finished." + } +} + +pub fn tui_toast_stream_check_failed(err: &str) -> String { + if is_chinese() { + format!("健康检查失败: {err}") + } else { + format!("Stream check failed: {err}") + } +} + +pub fn tui_toast_stream_check_unavailable(err: &str) -> String { + if is_chinese() { + format!("健康检查不可用: {err}") + } else { + format!("Stream check unavailable: {err}") + } +} + +pub fn tui_toast_stream_check_disabled() -> &'static str { + if is_chinese() { + "本次会话健康检查不可用。" + } else { + "Stream check is disabled for this session." + } +} + +pub fn tui_toast_stream_check_request_failed(err: &str) -> String { + if is_chinese() { + format!("健康检查请求失败: {err}") + } else { + format!("Failed to enqueue stream check: {err}") + } +} + +pub fn tui_toast_quota_not_available() -> &'static str { + if is_chinese() { + "当前供应商没有官方额度查询。" + } else { + "This provider has no official quota query." + } +} + +pub fn tui_toast_quota_worker_unavailable(err: &str) -> String { + if is_chinese() { + format!("额度查询后台任务不可用: {err}") + } else { + format!("Quota worker unavailable: {err}") + } +} + +pub fn tui_toast_quota_refresh_started(provider: &str) -> String { + if is_chinese() { + format!("正在刷新额度: {provider}") + } else { + format!("Refreshing quota: {provider}") + } +} + +pub fn tui_toast_quota_refresh_finished(provider: &str) -> String { + if is_chinese() { + format!("额度已刷新: {provider}") + } else { + format!("Quota refreshed: {provider}") + } +} + +pub fn tui_toast_quota_refresh_failed(err: &str) -> String { + if is_chinese() { + format!("额度刷新失败: {err}") + } else { + format!("Quota refresh failed: {err}") + } +} + +pub fn tui_toast_skills_worker_unavailable(err: &str) -> String { + if is_chinese() { + format!("Skills 后台任务不可用: {err}") + } else { + format!("Skills worker unavailable: {err}") + } +} + +pub fn tui_toast_webdav_worker_unavailable(err: &str) -> String { + if is_chinese() { + format!("WebDAV 后台任务不可用: {err}") + } else { + format!("WebDAV worker unavailable: {err}") + } +} + +pub fn tui_toast_model_fetch_worker_unavailable(err: &str) -> String { + if is_chinese() { + format!("模型获取后台任务不可用: {err}") + } else { + format!("Model fetch worker unavailable: {err}") + } +} + +pub fn tui_toast_model_fetch_worker_disabled() -> &'static str { + if is_chinese() { + "本次会话模型获取后台任务不可用。" + } else { + "Model fetch worker is disabled for this session." + } +} + +pub fn tui_toast_webdav_worker_disabled() -> &'static str { + if is_chinese() { + "本次会话 WebDAV 后台任务不可用。" + } else { + "WebDAV worker is disabled for this session." + } +} + +pub fn tui_error_skills_worker_unavailable() -> &'static str { + if is_chinese() { + "Skills 后台任务不可用。" + } else { + "Skills worker unavailable." + } +} + +pub fn tui_toast_skills_discover_finished(count: usize) -> String { + if is_chinese() { + format!("发现完成:{count} 个结果。") + } else { + format!("Discover finished: {count} result(s).") + } +} + +pub fn tui_toast_skills_discover_failed(err: &str) -> String { + if is_chinese() { + format!("发现失败: {err}") + } else { + format!("Discover failed: {err}") + } +} + +pub fn tui_toast_skill_installed(directory: &str) -> String { + if is_chinese() { + format!("已安装: {directory}") + } else { + format!("Installed: {directory}") + } +} + +pub fn tui_toast_skill_install_failed(spec: &str, err: &str) -> String { + if is_chinese() { + format!("安装失败({spec}): {err}") + } else { + format!("Install failed ({spec}): {err}") + } +} + +pub fn tui_toast_skill_already_installed() -> &'static str { + if is_chinese() { + "该 Skill 已安装。" + } else { + "Skill already installed." + } +} + +pub fn tui_toast_skill_spec_empty() -> &'static str { + if is_chinese() { + "Skill 不能为空。" + } else { + "Skill spec is empty." + } +} + +pub fn tui_toast_skill_toggled(directory: &str, enabled: bool) -> String { + if is_chinese() { + format!("{} {directory}", if enabled { "已启用" } else { "已禁用" }) + } else { + format!( + "{} {directory}", + if enabled { "Enabled" } else { "Disabled" } + ) + } +} + +pub fn tui_toast_skill_uninstalled(directory: &str) -> String { + if is_chinese() { + format!("已卸载: {directory}") + } else { + format!("Uninstalled: {directory}") + } +} + +pub fn tui_toast_skill_apps_updated() -> &'static str { + if is_chinese() { + "Skill 应用已更新。" + } else { + "Skill apps updated." + } +} + +pub fn tui_toast_skills_synced() -> &'static str { + if is_chinese() { + "Skills 同步完成。" + } else { + "Skills synced." + } +} + +pub fn tui_toast_skills_sync_method_set(method: &str) -> String { + if is_chinese() { + format!("同步方式已设置为: {method}") + } else { + format!("Sync method set to: {method}") + } +} + +pub fn tui_toast_repo_spec_empty() -> &'static str { + if is_chinese() { + "仓库不能为空。" + } else { + "Repository is empty." + } +} + +pub fn tui_error_repo_spec_empty() -> &'static str { + if is_chinese() { + "仓库不能为空。" + } else { + "Repository cannot be empty." + } +} + +pub fn tui_error_repo_spec_invalid() -> &'static str { + if is_chinese() { + "仓库格式无效。请使用 owner/name 或 https://github.com/owner/name" + } else { + "Invalid repo format. Use owner/name or https://github.com/owner/name" + } +} + +pub fn tui_toast_repo_added() -> &'static str { + if is_chinese() { + "仓库已添加。" + } else { + "Repository added." + } +} + +pub fn tui_toast_repo_removed() -> &'static str { + if is_chinese() { + "仓库已移除。" + } else { + "Repository removed." + } +} + +pub fn tui_toast_repo_toggled(enabled: bool) -> String { + if is_chinese() { + if enabled { + "仓库已启用。".to_string() + } else { + "仓库已禁用。".to_string() + } + } else { + if enabled { + "Repository enabled.".to_string() + } else { + "Repository disabled.".to_string() + } + } +} + +pub fn tui_toast_skip_claude_onboarding_toggled(enabled: bool) -> String { + if is_chinese() { + if enabled { + "已跳过 Claude Code 初次安装确认。".to_string() + } else { + "已恢复 Claude Code 初次安装确认。".to_string() + } + } else { + if enabled { + "Claude Code onboarding confirmation will be skipped.".to_string() + } else { + "Claude Code onboarding confirmation restored.".to_string() + } + } +} + +pub fn tui_toast_claude_plugin_integration_toggled(enabled: bool) -> String { + if is_chinese() { + if enabled { + "已启用 Claude Code for VSCode 插件联动。".to_string() + } else { + "已关闭 Claude Code for VSCode 插件联动。".to_string() + } + } else { + if enabled { + "Claude Code for VSCode integration enabled.".to_string() + } else { + "Claude Code for VSCode integration disabled.".to_string() + } + } +} + +pub fn tui_toast_claude_plugin_sync_failed(err: &str) -> String { + if is_chinese() { + format!("同步 Claude Code for VSCode 插件失败: {err}") + } else { + format!("Failed to sync Claude Code for VSCode integration: {err}") + } +} + +pub fn tui_toast_codex_unified_session_history_toggled(enabled: bool) -> String { + if is_chinese() { + if enabled { + "已启用统一 Codex 会话历史。".to_string() + } else { + "已关闭统一 Codex 会话历史。".to_string() + } + } else { + if enabled { + "Unified Codex session history enabled.".to_string() + } else { + "Unified Codex session history disabled.".to_string() + } + } +} + +pub fn tui_toast_codex_unified_session_history_already(enabled: bool) -> String { + if is_chinese() { + if enabled { + "统一 Codex 会话历史已经开启。".to_string() + } else { + "统一 Codex 会话历史已经关闭。".to_string() + } + } else { + if enabled { + "Unified Codex session history is already enabled.".to_string() + } else { + "Unified Codex session history is already disabled.".to_string() + } + } +} + +pub fn tui_toast_unmanaged_scanned(count: usize) -> String { + if is_chinese() { + format!("扫描完成:发现 {count} 个可导入技能。") + } else { + format!("Scan finished: found {count} skill(s) available to import.") + } +} + +pub fn tui_toast_no_unmanaged_selected() -> &'static str { + if is_chinese() { + "请至少选择一个要导入的技能。" + } else { + "Select at least one skill to import." + } +} + +pub fn tui_toast_unmanaged_imported(count: usize) -> String { + if is_chinese() { + format!("已导入 {count} 个技能。") + } else { + format!("Imported {count} skill(s).") + } +} + +pub fn tui_toast_provider_deleted() -> &'static str { + if is_chinese() { + "供应商已删除。" + } else { + "Provider deleted." + } +} + +pub fn tui_toast_provider_live_config_imported() -> &'static str { + if is_chinese() { + "已将当前 Claude 配置导入为供应商。" + } else { + "Imported the current Claude config as a provider." + } +} + +pub fn tui_toast_codex_live_config_imported() -> &'static str { + if is_chinese() { + "已将当前 Codex 配置导入为供应商。" + } else { + "Imported the current Codex config as a provider." + } +} + +pub fn tui_toast_provider_add_finished() -> &'static str { + if is_chinese() { + "供应商新增流程已完成。" + } else { + "Provider add flow finished." + } +} + +pub fn tui_toast_provider_add_missing_fields() -> &'static str { + if is_chinese() { + "请填写 name,id 会自动生成。" + } else { + "Please fill in name. id will be generated automatically." + } +} + +pub fn tui_toast_provider_missing_name() -> &'static str { + if is_chinese() { + "请在 JSON 中填写 name。" + } else { + "Please fill in name in JSON." + } +} + +pub fn tui_toast_provider_add_failed() -> &'static str { + if is_chinese() { + "新增供应商失败。" + } else { + "Failed to add provider." + } +} + +pub fn tui_toast_provider_edit_finished() -> &'static str { + if is_chinese() { + "供应商编辑流程已完成。" + } else { + "Provider edit flow finished." + } +} + +pub fn tui_toast_mcp_updated() -> &'static str { + if is_chinese() { + "MCP 已更新。" + } else { + "MCP updated." + } +} + +pub fn tui_toast_mcp_upserted() -> &'static str { + if is_chinese() { + "MCP 服务器已保存。" + } else { + "MCP server saved." + } +} + +pub fn tui_toast_mcp_missing_fields() -> &'static str { + if is_chinese() { + "请在 JSON 中填写 id 和 name。" + } else { + "Please fill in id and name in JSON." + } +} + +pub fn tui_toast_mcp_server_deleted() -> &'static str { + if is_chinese() { + "MCP 服务器已删除。" + } else { + "MCP server deleted." + } +} + +pub fn tui_toast_mcp_server_not_found() -> &'static str { + if is_chinese() { + "未找到 MCP 服务器。" + } else { + "MCP server not found." + } +} + +pub fn tui_toast_mcp_imported(count: usize) -> String { + if is_chinese() { + format!("已导入 {count} 个 MCP 服务器。") + } else { + format!("Imported {count} MCP server(s).") + } +} + +pub fn tui_toast_live_sync_skipped_uninitialized(app: &str) -> String { + if is_chinese() { + format!( + "未检测到 {app} 客户端本地配置,已跳过写入 live 文件;先运行一次 {app} 初始化后再试。" + ) + } else { + format!("Live sync skipped: {app} client not initialized; run it once to initialize, then retry.") + } +} + +pub fn tui_toast_mcp_updated_live_sync_skipped(apps: &[&str]) -> String { + let list = if is_chinese() { + apps.join("、") + } else { + apps.join(", ") + }; + + if is_chinese() { + format!( + "MCP 已更新,但以下客户端未初始化,已跳过写入 live 文件:{list};先运行一次对应客户端初始化后再试。" + ) + } else { + format!( + "MCP updated, but live sync skipped for uninitialized client(s): {list}; run them once to initialize, then retry." + ) + } +} + +pub fn tui_toast_prompt_activated() -> &'static str { + if is_chinese() { + "提示词已启用。" + } else { + "Prompt activated." + } +} + +pub fn tui_toast_prompt_deactivated() -> &'static str { + if is_chinese() { + "提示词已停用。" + } else { + "Prompt deactivated." + } +} + +pub fn tui_toast_prompt_deleted() -> &'static str { + if is_chinese() { + "提示词已删除。" + } else { + "Prompt deleted." + } +} + +pub fn tui_toast_prompt_created() -> &'static str { + if is_chinese() { + "提示词已创建。" + } else { + "Prompt created." + } +} + +pub fn tui_toast_prompt_renamed() -> &'static str { + if is_chinese() { + "提示词已重命名。" + } else { + "Prompt renamed." + } +} + +pub fn tui_toast_exported_to(path: &str) -> String { + if is_chinese() { + format!("已导出到 {}", path) + } else { + format!("Exported to {}", path) + } +} + +pub fn tui_error_import_file_not_found(path: &str) -> String { + if is_chinese() { + format!("导入文件不存在: {}", path) + } else { + format!("Import file not found: {}", path) + } +} + +pub fn tui_toast_imported_config() -> &'static str { + if is_chinese() { + "配置已导入。" + } else { + "Imported config." + } +} + +pub fn tui_toast_imported_with_backup(backup_id: &str) -> String { + if is_chinese() { + format!("已导入(备份: {backup_id})") + } else { + format!("Imported (backup: {backup_id})") + } +} + +pub fn tui_toast_no_config_file_to_backup() -> &'static str { + if is_chinese() { + "没有可备份的配置文件。" + } else { + "No config file to backup." + } +} + +pub fn tui_toast_backup_created(id: &str) -> String { + if is_chinese() { + format!("备份已创建: {id}") + } else { + format!("Backup created: {id}") + } +} + +pub fn tui_toast_restored_from_backup() -> &'static str { + if is_chinese() { + "已从备份恢复。" + } else { + "Restored from backup." + } +} + +pub fn tui_toast_restored_with_pre_backup(pre_backup: &str) -> String { + if is_chinese() { + format!("已恢复(恢复前备份: {pre_backup})") + } else { + format!("Restored (pre-backup: {pre_backup})") + } +} + +pub fn tui_toast_webdav_settings_saved() -> &'static str { + if is_chinese() { + "WebDAV 同步设置已保存。" + } else { + "WebDAV sync settings saved." + } +} + +pub fn tui_toast_proxy_takeover_requires_running() -> &'static str { + if is_chinese() { + "前台代理未运行,请先启动 `cc-switch proxy serve`。" + } else { + "Foreground proxy is not running. Start `cc-switch proxy serve` first." + } +} + +pub fn tui_toast_proxy_takeover_updated(app: &str, enabled: bool) -> String { + if is_chinese() { + if enabled { + format!("已将 {app} 接管到前台代理。") + } else { + format!("已将 {app} 恢复到 live 配置。") + } + } else if enabled { + format!("{app} now uses the foreground proxy.") + } else { + format!("{app} restored to its live config.") + } +} + +pub fn tui_toast_proxy_managed_current_app_updated(app: &str, enabled: bool) -> String { + if is_chinese() { + if enabled { + format!("{app} 已走 cc-switch 代理。") + } else { + format!("{app} 已恢复 live 配置。") + } + } else if enabled { + format!("{app} now routes through cc-switch.") + } else { + format!("{app} restored to its live config.") + } +} + +pub fn tui_toast_proxy_worker_unavailable(err: &str) -> String { + if is_chinese() { + format!("代理任务不可用:{err}") + } else { + format!("Proxy worker unavailable: {err}") + } +} + +pub fn tui_toast_proxy_request_failed(err: &str) -> String { + if is_chinese() { + format!("代理请求发送失败:{err}") + } else { + format!("Proxy request failed: {err}") + } +} + +pub fn tui_error_proxy_worker_unavailable() -> &'static str { + if is_chinese() { + "代理任务不可用。" + } else { + "Proxy worker unavailable." + } +} + +pub fn tui_toast_webdav_settings_cleared() -> &'static str { + if is_chinese() { + "WebDAV 同步设置已清空。" + } else { + "WebDAV sync settings cleared." + } +} + +pub fn tui_toast_webdav_connection_ok() -> &'static str { + if is_chinese() { + "WebDAV 连接检查通过。" + } else { + "WebDAV connection check passed." + } +} + +pub fn tui_toast_webdav_upload_ok() -> &'static str { + if is_chinese() { + "WebDAV 上传完成。" + } else { + "WebDAV upload completed." + } +} + +pub fn tui_toast_webdav_download_ok() -> &'static str { + if is_chinese() { + "WebDAV 下载完成。" + } else { + "WebDAV download completed." + } +} + +pub fn tui_webdav_v1_migration_title() -> &'static str { + if is_chinese() { + "发现旧版同步数据" + } else { + "Legacy sync data detected" + } +} + +pub fn tui_webdav_v1_migration_message() -> &'static str { + if is_chinese() { + "远端存在 V1 格式的同步数据,是否迁移到 V2?\n迁移将下载旧数据、应用到本地、重新上传为新格式,并清理旧数据。" + } else { + "V1 sync data found on remote. Migrate to V2?\nThis will download old data, apply locally, re-upload as V2, and clean up V1 data." + } +} + +pub fn tui_webdav_loading_title_v1_migration() -> &'static str { + if is_chinese() { + "V1 → V2 迁移" + } else { + "V1 → V2 Migration" + } +} + +pub fn tui_toast_webdav_v1_migration_ok() -> &'static str { + if is_chinese() { + "V1 → V2 迁移完成,旧数据已清理。" + } else { + "V1 → V2 migration completed, old data cleaned up." + } +} + +pub fn tui_toast_webdav_jianguoyun_configured() -> &'static str { + if is_chinese() { + "坚果云一键配置完成,连接检查通过。" + } else { + "Jianguoyun quick setup completed and connection verified." + } +} + +pub fn tui_toast_webdav_username_empty() -> &'static str { + if is_chinese() { + "请输入 WebDAV 用户名。" + } else { + "Please enter a WebDAV username." + } +} + +pub fn tui_toast_webdav_password_empty() -> &'static str { + if is_chinese() { + "请输入 WebDAV 第三方应用密码。" + } else { + "Please enter a WebDAV app password." + } +} + +pub fn tui_toast_webdav_request_failed(err: &str) -> String { + if is_chinese() { + format!("WebDAV 请求提交失败: {err}") + } else { + format!("Failed to enqueue WebDAV request: {err}") + } +} + +pub fn tui_toast_webdav_action_failed(action: &str, err: &str) -> String { + if is_chinese() { + format!("{action} 失败: {err}") + } else { + format!("{action} failed: {err}") + } +} + +pub fn tui_toast_webdav_quick_setup_failed(err: &str) -> String { + if is_chinese() { + format!("坚果云一键配置已保存,但连接检查失败: {err}") + } else { + format!("Jianguoyun quick setup was saved, but connection check failed: {err}") + } +} + +pub fn tui_toast_config_file_does_not_exist() -> &'static str { + if is_chinese() { + "配置文件不存在。" + } else { + "Config file does not exist." + } +} + +pub fn tui_config_validation_title() -> &'static str { + if is_chinese() { + "配置校验" + } else { + "Config Validation" + } +} + +pub fn tui_config_validation_failed_title() -> &'static str { + if is_chinese() { + "配置校验失败" + } else { + "Config Validation Failed" + } +} + +pub fn tui_config_validation_ok() -> &'static str { + if is_chinese() { + "✓ 配置是有效的 JSON" + } else { + "✓ Configuration is valid JSON" + } +} + +pub fn tui_config_validation_provider_count(app: &str, count: usize) -> String { + if is_chinese() { + format!("{app} 供应商: {count}") + } else { + format!("{app} providers: {count}") + } +} + +pub fn tui_config_validation_mcp_servers(count: usize) -> String { + if is_chinese() { + format!("MCP 服务器: {count}") + } else { + format!("MCP servers: {count}") + } +} + +pub fn tui_toast_validation_passed() -> &'static str { + if is_chinese() { + "校验通过。" + } else { + "Validation passed." + } +} + +pub fn tui_toast_config_reset_to_defaults() -> &'static str { + if is_chinese() { + "配置已重置为默认值。" + } else { + "Config reset to defaults." + } +} + +pub fn tui_toast_config_reset_with_backup(backup_id: &str) -> String { + if is_chinese() { + format!("配置已重置(备份: {backup_id})") + } else { + format!("Config reset (backup: {backup_id})") + } +} diff --git a/src-tauri/src/cli/i18n/texts/update.rs b/src-tauri/src/cli/i18n/texts/update.rs new file mode 100644 index 00000000..a47de10a --- /dev/null +++ b/src-tauri/src/cli/i18n/texts/update.rs @@ -0,0 +1,164 @@ +use super::is_chinese; +// ============================================ + +pub fn tui_settings_check_for_updates() -> &'static str { + if is_chinese() { + "检查更新" + } else { + "Check for Updates" + } +} + +pub fn tui_update_checking_title() -> &'static str { + if is_chinese() { + "检查更新中" + } else { + "Checking for Updates" + } +} + +pub fn tui_update_available_title() -> &'static str { + if is_chinese() { + "发现新版本" + } else { + "Update Available" + } +} + +pub fn tui_update_downloading_title() -> &'static str { + if is_chinese() { + "正在更新" + } else { + "Updating" + } +} + +pub fn tui_update_result_title() -> &'static str { + if is_chinese() { + "更新结果" + } else { + "Update Result" + } +} + +pub fn tui_update_version_info(current: &str, new: &str) -> String { + if is_chinese() { + format!("当前: v{current} → 最新: {new}") + } else { + format!("Current: v{current} → Latest: {new}") + } +} + +pub fn tui_update_btn_update() -> &'static str { + if is_chinese() { + "更新" + } else { + "Update" + } +} + +pub fn tui_update_btn_cancel() -> &'static str { + if is_chinese() { + "取消" + } else { + "Cancel" + } +} + +pub fn tui_update_downloading_kb(kb: u64) -> String { + if is_chinese() { + format!("已下载 {kb} KB") + } else { + format!("Downloaded {kb} KB") + } +} + +pub fn tui_update_downloading_progress(pct: u64, downloaded_kb: u64, total_kb: u64) -> String { + if is_chinese() { + format!("{pct}% ({downloaded_kb} / {total_kb} KB)") + } else { + format!("{pct}% ({downloaded_kb} / {total_kb} KB)") + } +} + +pub fn tui_update_success(tag: &str) -> String { + if is_chinese() { + format!("已更新到 {tag},按 Enter 退出") + } else { + format!("Updated to {tag}. Press Enter to exit.") + } +} + +pub fn tui_update_err_worker_unavailable() -> &'static str { + if is_chinese() { + "更新服务不可用" + } else { + "Update worker unavailable" + } +} + +pub fn tui_update_err_check_first() -> &'static str { + if is_chinese() { + "请先检查更新" + } else { + "Please check for updates first" + } +} + +pub fn tui_toast_already_latest(v: &str) -> String { + if is_chinese() { + format!("已是最新版本 v{v}") + } else { + format!("Already on latest v{v}") + } +} + +pub fn tui_toast_update_downgrade(current: &str, target: &str) -> String { + if is_chinese() { + format!("当前 v{current} 比 {target} 更新") + } else { + format!("Current v{current} is newer than {target}") + } +} + +pub fn tui_toast_update_homebrew_required(current: &str, target: &str) -> String { + if is_chinese() { + format!("发现新版本 {target}(当前 v{current})\n请使用 brew upgrade cc-switch 更新") + } else { + format!( + "Update {target} is available (current v{current}).\nPlease update with: brew upgrade cc-switch" + ) + } +} + +pub fn tui_toast_update_check_failed(err: &str) -> String { + if is_chinese() { + format!("检查更新失败: {err}") + } else { + format!("Update check failed: {err}") + } +} + +pub fn tui_key_hide() -> &'static str { + if is_chinese() { + "隐藏" + } else { + "hide" + } +} + +pub fn tui_toast_update_bg_success(tag: &str) -> String { + if is_chinese() { + format!("后台更新到 {tag} 完成") + } else { + format!("Background update to {tag} complete") + } +} + +pub fn tui_toast_update_bg_failed(err: &str) -> String { + if is_chinese() { + format!("后台更新失败: {err}") + } else { + format!("Background update failed: {err}") + } +} diff --git a/src-tauri/src/cli/tui/app/app_state.rs b/src-tauri/src/cli/tui/app/app_state.rs index ef62aee9..e4da26e6 100644 --- a/src-tauri/src/cli/tui/app/app_state.rs +++ b/src-tauri/src/cli/tui/app/app_state.rs @@ -1,4 +1,5 @@ use super::*; +use std::collections::HashMap; #[derive(Debug, Clone)] pub enum Action { @@ -116,6 +117,23 @@ pub enum Action { field: ProviderAddField, claude_idx: Option, }, + ModelRouteAdd { + pattern: String, + provider_id: String, + priority: i32, + }, + ModelRouteEdit { + id: String, + pattern: String, + provider_id: String, + priority: i32, + }, + ModelRouteDelete { + id: String, + }, + ModelRouteToggle { + id: String, + }, UsageCustomRange { range: data::UsageCustomRange, }, @@ -443,7 +461,6 @@ impl ConfigItem { pub enum SettingsItem { Language, Theme, - Icons, VisibleAppsMode, VisibleApps, OpenClawConfigDir, @@ -452,6 +469,7 @@ pub enum SettingsItem { ClaudePluginIntegration, CodexUnifiedSessionHistory, Proxy, + ModelRoutes, CheckForUpdates, } @@ -460,7 +478,6 @@ impl SettingsItem { SettingsItem::ManagedAccounts, SettingsItem::Language, SettingsItem::Theme, - SettingsItem::Icons, SettingsItem::VisibleAppsMode, SettingsItem::VisibleApps, SettingsItem::OpenClawConfigDir, @@ -468,19 +485,22 @@ impl SettingsItem { SettingsItem::ClaudePluginIntegration, SettingsItem::CodexUnifiedSessionHistory, SettingsItem::Proxy, + SettingsItem::ModelRoutes, SettingsItem::CheckForUpdates, ]; } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LocalProxySettingsItem { + ProxySwitch, ListenAddress, ListenPort, AutoFailover, } impl LocalProxySettingsItem { - pub const ALL: [LocalProxySettingsItem; 3] = [ + pub const ALL: [LocalProxySettingsItem; 4] = [ + LocalProxySettingsItem::ProxySwitch, LocalProxySettingsItem::ListenAddress, LocalProxySettingsItem::ListenPort, LocalProxySettingsItem::AutoFailover, @@ -559,6 +579,9 @@ pub struct App { pub proxy_output_activity_samples: Vec, pub proxy_activity_last_input_tokens: Option, pub proxy_activity_last_output_tokens: Option, + /// 按 provider 聚合的 activity 样本(provider_id → samples),用于仪表盘点阵图多色展示 + pub proxy_provider_activity_samples: HashMap>, + pub proxy_activity_last_provider_tokens: Option>, pub proxy_visual_state: Option, pub proxy_visual_transition: Option, pub quota_auto_target_key: Option, @@ -606,6 +629,8 @@ pub struct App { pub settings_idx: usize, pub settings_proxy_idx: usize, pub settings_managed_accounts_idx: usize, + /// Selected index in the model routes table. + pub model_routes_idx: usize, pub managed_auth_status: Option, pub managed_auth_loading: bool, pub managed_auth_login: Option, diff --git a/src-tauri/src/cli/tui/app/content_config.rs b/src-tauri/src/cli/tui/app/content_config.rs index d1f26d51..417368ef 100644 --- a/src-tauri/src/cli/tui/app/content_config.rs +++ b/src-tauri/src/cli/tui/app/content_config.rs @@ -742,23 +742,6 @@ impl App { } Action::None } - Some(SettingsItem::Icons) => { - let next = crate::cli::tui::icons::configured_icon_mode().next(); - match crate::settings::set_icon_mode(next.code()) { - Ok(()) => { - self.push_toast( - texts::tui_toast_icons_changed(texts::tui_settings_icon_mode_name( - next, - )), - ToastKind::Success, - ); - } - Err(err) => { - self.push_toast(err.to_string(), ToastKind::Error); - } - } - Action::None - } Some(SettingsItem::VisibleAppsMode) => { let current = crate::settings::get_visible_apps_settings().mode; let next = match current { @@ -847,6 +830,9 @@ impl App { Action::None } Some(SettingsItem::Proxy) => self.push_route_and_switch(Route::SettingsProxy), + Some(SettingsItem::ModelRoutes) => { + self.push_route_and_switch(Route::SettingsModelRoutes) + } Some(SettingsItem::CheckForUpdates) => Action::CheckUpdate, None => Action::None, }, @@ -917,6 +903,13 @@ impl App { Action::None } KeyCode::Enter => match LocalProxySettingsItem::ALL.get(self.settings_proxy_idx) { + Some(LocalProxySettingsItem::ProxySwitch) => + { + #[allow(clippy::needless_return)] + return Action::SetProxyEnabled { + enabled: !data.proxy.enabled, + } + } Some(LocalProxySettingsItem::AutoFailover) => { self.request_auto_failover_toggle(data) } @@ -987,6 +980,61 @@ impl App { } } + pub(crate) fn on_settings_model_routes_key(&mut self, key: KeyEvent, data: &UiData) -> Action { + let routes_len = data.model_routes.rows.len(); + match key.code { + KeyCode::Up => { + self.model_routes_idx = self.model_routes_idx.saturating_sub(1); + Action::None + } + KeyCode::Down => { + if routes_len > 0 { + self.model_routes_idx = (self.model_routes_idx + 1).min(routes_len - 1); + } + Action::None + } + KeyCode::Char('a') => { + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_model_route_add_pattern_title().to_string(), + prompt: texts::tui_model_route_add_pattern_prompt().to_string(), + input: TextInput::new(String::new()), + submit: TextSubmit::ModelRouteAddPattern, + secret: false, + }); + Action::None + } + KeyCode::Char('e') => { + if let Some(row) = data.model_routes.rows.get(self.model_routes_idx) { + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_model_route_edit_pattern_title().to_string(), + prompt: texts::tui_model_route_edit_pattern_prompt().to_string(), + input: TextInput::new(row.pattern.clone()), + submit: TextSubmit::ModelRouteEditPattern { id: row.id.clone() }, + secret: false, + }); + } + Action::None + } + KeyCode::Char('d') => { + if let Some(row) = data.model_routes.rows.get(self.model_routes_idx) { + self.overlay = Overlay::Confirm(ConfirmOverlay { + title: texts::tui_model_route_confirm_delete_title().to_string(), + message: texts::tui_model_route_confirm_delete_message(&row.pattern), + action: ConfirmAction::ModelRouteDelete { id: row.id.clone() }, + }); + } + Action::None + } + KeyCode::Char(' ') => { + if let Some(row) = data.model_routes.rows.get(self.model_routes_idx) { + return Action::ModelRouteToggle { id: row.id.clone() }; + } + Action::None + } + _ => Action::None, + } + } + fn managed_auth_account_count(&self) -> usize { self.managed_auth_status .as_ref() diff --git a/src-tauri/src/cli/tui/app/content_entities.rs b/src-tauri/src/cli/tui/app/content_entities.rs index c8f5c9fd..2adceecc 100644 --- a/src-tauri/src/cli/tui/app/content_entities.rs +++ b/src-tauri/src/cli/tui/app/content_entities.rs @@ -520,8 +520,6 @@ impl App { } pub(crate) fn on_sessions_key(&mut self, key: KeyEvent, data: &UiData) -> Action { - use crate::cli::tui::keymap::sessions::Intent; - let visible = visible_sessions_for_state( &self.filter, &self.app_type, @@ -622,99 +620,91 @@ impl App { } Action::None } - _ => { - let Some(intent) = crate::cli::tui::keymap::sessions::intent_for(key.code) else { + KeyCode::Enter => match self.sessions.pane { + SessionsPane::List => self.open_selected_session_detail(data), + SessionsPane::Detail => { + let messages = visible_session_messages(&self.sessions); + let message = messages + .iter() + .find(|(index, _)| *index == self.sessions.message_idx) + .or_else(|| messages.first()) + .map(|(_, message)| *message); + let Some(message) = message else { + return Action::None; + }; + self.overlay = Overlay::TextView(TextViewState { + title: texts::tui_sessions_message_detail_title(&message.role), + lines: message + .content + .lines() + .map(|line| line.to_string()) + .collect(), + scroll: 0, + action: None, + }); + Action::None + } + }, + KeyCode::Char('R') => { + let Some(session) = self.selected_session_from_visible(&visible) else { return Action::None; }; - match intent { - Intent::View => match self.sessions.pane { - SessionsPane::List => self.open_selected_session_detail(data), - SessionsPane::Detail => { - let messages = visible_session_messages(&self.sessions); - let message = messages - .iter() - .find(|(index, _)| *index == self.sessions.message_idx) - .or_else(|| messages.first()) - .map(|(_, message)| *message); - let Some(message) = message else { - return Action::None; - }; - self.overlay = Overlay::TextView(TextViewState { - title: texts::tui_sessions_message_detail_title(&message.role), - lines: message - .content - .lines() - .map(|line| line.to_string()) - .collect(), - scroll: 0, - action: None, - }); - Action::None - } + let Some(command) = session + .resume_command + .clone() + .filter(|value| !value.trim().is_empty()) + else { + self.push_toast( + texts::tui_sessions_toast_action_unavailable(), + ToastKind::Info, + ); + return Action::None; + }; + Action::SessionResume { + command, + cwd: session.project_dir.clone(), + } + } + KeyCode::Char('d') => { + let Some(session) = self.selected_session_from_visible(&visible) else { + return Action::None; + }; + let Some(source_path) = session + .source_path + .clone() + .filter(|value| !value.trim().is_empty()) + else { + self.push_toast( + texts::tui_sessions_toast_source_missing(), + ToastKind::Warning, + ); + return Action::None; + }; + let key = session_key(session); + self.overlay = Overlay::Confirm(ConfirmOverlay { + title: texts::tui_sessions_delete_confirm_title().to_string(), + message: texts::tui_sessions_delete_confirm_message(&session_title(session)), + action: ConfirmAction::SessionDelete { + key, + provider_id: session.provider_id.clone(), + session_id: session.session_id.clone(), + source_path, }, - Intent::Restore => { - let Some(session) = self.selected_session_from_visible(&visible) else { - return Action::None; - }; - let Some(command) = session - .resume_command - .clone() - .filter(|value| !value.trim().is_empty()) - else { - self.push_toast( - texts::tui_sessions_toast_action_unavailable(), - ToastKind::Info, - ); - return Action::None; - }; - Action::SessionResume { - command, - cwd: session.project_dir.clone(), - } - } - Intent::Delete => { - let Some(session) = self.selected_session_from_visible(&visible) else { - return Action::None; - }; - let Some(source_path) = session - .source_path - .clone() - .filter(|value| !value.trim().is_empty()) - else { - self.push_toast( - texts::tui_sessions_toast_source_missing(), - ToastKind::Warning, - ); - return Action::None; - }; - let key = session_key(session); - self.overlay = Overlay::Confirm(ConfirmOverlay { - title: texts::tui_sessions_delete_confirm_title().to_string(), - message: texts::tui_sessions_delete_confirm_message(&session_title( - session, - )), - action: ConfirmAction::SessionDelete { - key, - provider_id: session.provider_id.clone(), - session_id: session.session_id.clone(), - source_path, - }, - }); - Action::None - } - Intent::Refresh => Action::SessionsRefresh, - Intent::ShowAll => { - // Enter "show all providers" mode (the "全部" tab) - if !self.sessions.show_all_providers { - self.sessions.show_all_providers = true; - self.sessions.loaded_once = false; - self.sessions.selected_idx = 0; - self.sessions.clear_detail(); - } - Action::None - } + }); + Action::None + } + KeyCode::Char('r') => Action::SessionsRefresh, + KeyCode::Char('a') => { + // Enter "show all providers" mode (the "全部" tab) + if !self.sessions.show_all_providers { + self.sessions.show_all_providers = true; + self.sessions.loaded_once = false; + self.sessions.selected_idx = 0; + self.sessions.clear_detail(); } + Action::None } + _ => Action::None, } } diff --git a/src-tauri/src/cli/tui/app/helpers.rs b/src-tauri/src/cli/tui/app/helpers.rs index 924eef3d..ebb4b1c9 100644 --- a/src-tauri/src/cli/tui/app/helpers.rs +++ b/src-tauri/src/cli/tui/app/helpers.rs @@ -788,6 +788,7 @@ pub(crate) fn visible_sessions<'a>( .collect() } +#[allow(clippy::too_many_arguments)] pub(crate) fn visible_sessions_for_state<'a>( filter: &FilterState, app_type: &AppType, @@ -808,16 +809,12 @@ pub(crate) fn visible_sessions_for_state<'a>( // If deep search is active, only show sessions that appear in search hits // (merged with metadata matches). let deep_search_source_paths: Option> = - if let Some(_q) = deep_search_query { - Some( - deep_search_results - .iter() - .map(|h| h.source_path.as_str()) - .collect(), - ) - } else { - None - }; + deep_search_query.map(|_q| { + deep_search_results + .iter() + .map(|h| h.source_path.as_str()) + .collect() + }); rows.iter() .filter(|row| show_all || row.provider_id == provider_id) diff --git a/src-tauri/src/cli/tui/app/menu.rs b/src-tauri/src/cli/tui/app/menu.rs index 8d75b3e6..12394688 100644 --- a/src-tauri/src/cli/tui/app/menu.rs +++ b/src-tauri/src/cli/tui/app/menu.rs @@ -66,6 +66,8 @@ impl App { proxy_output_activity_samples: Vec::new(), proxy_activity_last_input_tokens: None, proxy_activity_last_output_tokens: None, + proxy_provider_activity_samples: HashMap::new(), + proxy_activity_last_provider_tokens: None, proxy_visual_state: None, proxy_visual_transition: None, quota_auto_target_key: None, @@ -108,6 +110,7 @@ impl App { settings_idx: 0, settings_proxy_idx: 0, settings_managed_accounts_idx: 0, + model_routes_idx: 0, managed_auth_status: None, managed_auth_loading: false, managed_auth_login: None, @@ -170,9 +173,10 @@ impl App { | Route::SkillsDiscover | Route::SkillsRepos | Route::SkillDetail { .. } => NavItem::Skills, - Route::Settings | Route::SettingsProxy | Route::SettingsManagedAccounts => { - NavItem::Settings - } + Route::Settings + | Route::SettingsProxy + | Route::SettingsManagedAccounts + | Route::SettingsModelRoutes => NavItem::Settings, } } @@ -343,8 +347,10 @@ impl App { pub(crate) fn reset_proxy_activity(&mut self, input_tokens: u64, output_tokens: u64) { self.proxy_input_activity_samples.clear(); self.proxy_output_activity_samples.clear(); + self.proxy_provider_activity_samples.clear(); self.proxy_activity_last_input_tokens = Some(input_tokens); self.proxy_activity_last_output_tokens = Some(output_tokens); + self.proxy_activity_last_provider_tokens = None; } pub(crate) fn observe_proxy_token_activity(&mut self, input_tokens: u64, output_tokens: u64) { @@ -384,6 +390,62 @@ impl App { } } + /// 按 provider 记录 token activity 样本,用于仪表盘点阵图多色展示 + #[allow(dead_code)] + pub(crate) fn observe_proxy_provider_activity( + &mut self, + provider_token_map: &HashMap, + ) { + // proxy 重启会令主 token 计数回退,触发 observe_proxy_token_activity 清空主样本。 + // 这里同步清空 provider 样本,保持列对齐,避免颜色栈错位退化为单色。 + let main_len = self.proxy_input_activity_samples.len(); + let prev_len = self + .proxy_provider_activity_samples + .values() + .map(|s| s.len()) + .max() + .unwrap_or(0); + if prev_len > main_len { + for samples in self.proxy_provider_activity_samples.values_mut() { + samples.clear(); + } + } + + let first_tick = self.proxy_activity_last_provider_tokens.is_none(); + let prev_map = self + .proxy_activity_last_provider_tokens + .clone() + .unwrap_or_default(); + + // Compute per-provider deltas(首 tick 全为 0,与主样本首列对齐) + for (provider_id, current_tokens) in provider_token_map { + let prev = prev_map.get(provider_id).copied().unwrap_or(0); + let delta = if first_tick || *current_tokens < prev { + 0 + } else { + current_tokens.saturating_sub(prev) + }; + let samples = self + .proxy_provider_activity_samples + .entry(provider_id.clone()) + .or_default(); + samples.push(delta); + while samples.len() > PROXY_ACTIVITY_WINDOW { + samples.remove(0); + } + } + + // Pad all provider samples to match input/output sample length + let target_len = main_len; + for samples in self.proxy_provider_activity_samples.values_mut() { + while samples.len() < target_len { + samples.insert(0, 0); + } + } + + self.proxy_activity_last_provider_tokens = Some(provider_token_map.clone()); + } + pub fn push_toast(&mut self, message: impl Into, kind: ToastKind) { self.toast = Some(Toast::new(message, kind)); } @@ -452,7 +514,7 @@ impl App { } let help = Overlay::Help(crate::cli::tui::help::HelpState::new( - crate::cli::tui::help::context_help_for_app(self, data), + crate::cli::tui::help::context_help_for_app(self), )); if self.overlay.can_be_covered_by_help() { let previous = std::mem::replace(&mut self.overlay, help); @@ -565,9 +627,7 @@ impl App { let key = self.normalize_vim_navigation_key(key); - if matches!(key.code, KeyCode::Char('?') | KeyCode::Char('?')) - && self.help_shortcut_is_available() - { + if matches!(key.code, KeyCode::Char('?')) && self.help_shortcut_is_available() { self.open_help(data); return Action::None; } @@ -872,6 +932,7 @@ impl App { Route::Settings => self.on_settings_key(key, data), Route::SettingsProxy => self.on_settings_proxy_key(key, data), Route::SettingsManagedAccounts => self.on_settings_managed_accounts_key(key, data), + Route::SettingsModelRoutes => self.on_settings_model_routes_key(key, data), Route::Main => match key.code { KeyCode::Char('r') => Action::LocalEnvRefresh, KeyCode::Char('p') | KeyCode::Char('P') => self.main_proxy_action(data), @@ -1044,5 +1105,12 @@ impl App { } else { self.config_webdav_idx = self.config_webdav_idx.min(config_webdav_len - 1); } + + let routes_len = data.model_routes.rows.len(); + if routes_len == 0 { + self.model_routes_idx = 0; + } else { + self.model_routes_idx = self.model_routes_idx.min(routes_len - 1); + } } } diff --git a/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs b/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs index d8d0baaf..b97591a9 100644 --- a/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs +++ b/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs @@ -146,6 +146,9 @@ impl App { }; return Some(Action::None); } + ConfirmAction::ModelRouteDelete { id } => { + Action::ModelRouteDelete { id: id.clone() } + } }; self.close_overlay(); action @@ -370,6 +373,101 @@ impl App { } TextSubmit::WebDavJianguoyunUsername => self.handle_webdav_username_submit(raw), TextSubmit::WebDavJianguoyunPassword => self.handle_webdav_password_submit(raw), + TextSubmit::ModelRouteAddPattern => { + if raw.is_empty() { + self.push_toast( + texts::tui_toast_provider_add_missing_fields(), + ToastKind::Warning, + ); + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_model_route_add_pattern_title().to_string(), + prompt: texts::tui_model_route_add_pattern_prompt().to_string(), + input: TextInput::new(raw), + submit: TextSubmit::ModelRouteAddPattern, + secret: false, + }); + return Action::None; + } + // 打开 provider 选择器而非文本输入 + self.overlay = Overlay::ModelRouteProviderPicker { + pattern: raw, + selected: 0, + editing: false, + existing_id: None, + }; + Action::None + } + TextSubmit::ModelRouteAddProvider { .. } => { + // 不再使用 — provider 选择器直接跳到优先级步骤 + Action::None + } + TextSubmit::ModelRouteAddPriority { + pattern, + provider_id, + } => { + let priority: i32 = raw.trim().parse().unwrap_or(0); + Action::ModelRouteAdd { + pattern, + provider_id, + priority, + } + } + TextSubmit::ModelRouteEditPattern { id } => { + if raw.is_empty() { + self.push_toast( + texts::tui_toast_provider_add_missing_fields(), + ToastKind::Warning, + ); + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_model_route_edit_pattern_title().to_string(), + prompt: texts::tui_model_route_edit_pattern_prompt().to_string(), + input: TextInput::new(raw), + submit: TextSubmit::ModelRouteEditPattern { id }, + secret: false, + }); + return Action::None; + } + // 编辑时预选当前 provider,避免回车静默改成首个 provider (Codex P2) + let selected = data + .model_routes + .rows + .iter() + .find(|row| row.id == id) + .and_then(|route| { + data.providers + .rows + .iter() + .position(|p| p.id == route.provider_id) + }) + .unwrap_or(0); + self.overlay = Overlay::ModelRouteProviderPicker { + pattern: raw, + + selected, + + editing: true, + + existing_id: Some(id), + }; + + Action::None + } + + TextSubmit::ModelRouteEditProvider { .. } => Action::None, + + TextSubmit::ModelRouteEditPriority { + id, + pattern, + provider_id, + } => { + let priority: i32 = raw.trim().parse().unwrap_or(0); + Action::ModelRouteEdit { + id, + pattern, + provider_id, + priority, + } + } } } diff --git a/src-tauri/src/cli/tui/app/overlay_handlers/views.rs b/src-tauri/src/cli/tui/app/overlay_handlers/views.rs index 82f02b33..5342479c 100644 --- a/src-tauri/src/cli/tui/app/overlay_handlers/views.rs +++ b/src-tauri/src/cli/tui/app/overlay_handlers/views.rs @@ -36,6 +36,9 @@ impl App { if let Some(action) = self.handle_backup_picker_key(key, data) { return Some(action); } + if let Some(action) = self.handle_model_route_provider_picker_key(key, data) { + return Some(action); + } if let Some(action) = self.handle_text_view_overlay_key(key, data) { return Some(action); } @@ -62,7 +65,7 @@ impl App { return None; } Some(match key.code { - KeyCode::Esc | KeyCode::Char('?') | KeyCode::Char('?') => { + KeyCode::Esc | KeyCode::Char('?') => { self.close_overlay(); Action::None } @@ -332,4 +335,96 @@ impl App { _ => Action::None, }) } + + fn handle_model_route_provider_picker_key( + &mut self, + key: KeyEvent, + data: &UiData, + ) -> Option { + let Overlay::ModelRouteProviderPicker { + pattern, + selected, + editing, + existing_id, + } = &mut self.overlay + else { + return None; + }; + + let providers = &data.providers.rows; + + Some(match key.code { + KeyCode::Esc => { + self.overlay = Overlay::TextInput(TextInputState { + title: if *editing { + texts::tui_model_route_edit_pattern_title().to_string() + } else { + texts::tui_model_route_add_pattern_title().to_string() + }, + prompt: if *editing { + texts::tui_model_route_edit_pattern_prompt().to_string() + } else { + texts::tui_model_route_add_pattern_prompt().to_string() + }, + input: TextInput::new(pattern.clone()), + submit: if *editing { + TextSubmit::ModelRouteEditPattern { + id: existing_id.clone().unwrap_or_default(), + } + } else { + TextSubmit::ModelRouteAddPattern + }, + secret: false, + }); + Action::None + } + KeyCode::Up => { + *selected = selected.saturating_sub(1); + Action::None + } + KeyCode::Down => { + if !providers.is_empty() { + *selected = (*selected + 1).min(providers.len() - 1); + } + Action::None + } + KeyCode::Enter => { + if let Some(provider_row) = providers.get(*selected) { + let provider_id = provider_row.id.clone(); + let pattern = std::mem::take(pattern); + let is_editing = *editing; + let eid = existing_id.clone(); + // 编辑时预填原有 priority,避免误改顺序;新增时默认 0 + let priority_input = if is_editing { + eid.as_ref() + .and_then(|id| data.model_routes.rows.iter().find(|row| &row.id == id)) + .map(|row| row.priority.to_string()) + .unwrap_or_else(|| "0".to_string()) + } else { + "0".to_string() + }; + self.overlay = Overlay::TextInput(TextInputState { + title: texts::tui_model_route_add_priority_title().to_string(), + prompt: texts::tui_model_route_add_priority_prompt().to_string(), + input: TextInput::new(priority_input), + submit: if is_editing { + TextSubmit::ModelRouteEditPriority { + id: eid.unwrap_or_default(), + pattern, + provider_id, + } + } else { + TextSubmit::ModelRouteAddPriority { + pattern, + provider_id, + } + }, + secret: false, + }); + } + Action::None + } + _ => Action::None, + }) + } } diff --git a/src-tauri/src/cli/tui/app/tests.rs b/src-tauri/src/cli/tui/app/tests.rs index edf5ee1b..bf36e641 100644 --- a/src-tauri/src/cli/tui/app/tests.rs +++ b/src-tauri/src/cli/tui/app/tests.rs @@ -1219,6 +1219,56 @@ mod tests { assert_eq!(app.proxy_activity_last_output_tokens, Some(8)); } + #[test] + fn proxy_provider_activity_aligns_with_main_samples_on_first_tick() { + let mut app = App::new(Some(AppType::Claude)); + + // 首 tick:主样本 push 一个 0,provider 样本必须同长(修复前会因 + // 静默 return 而落后一列,导致点阵图颜色栈错位退化为单色)。 + app.reset_proxy_activity(10, 20); + app.observe_proxy_token_activity(10, 20); + let mut map = HashMap::new(); + map.insert("p1".to_string(), 5); + app.observe_proxy_provider_activity(&map); + + assert_eq!(app.proxy_input_activity_samples.len(), 1); + assert_eq!( + app.proxy_provider_activity_samples + .get("p1") + .map(|s| s.len()), + Some(1), + "provider samples must align with main samples from the first tick" + ); + } + + #[test] + fn proxy_provider_activity_resyncs_after_proxy_restart() { + let mut app = App::new(Some(AppType::Claude)); + + // 正常积累几个 tick + app.reset_proxy_activity(0, 0); + for i in 1..=3 { + app.observe_proxy_token_activity(i * 10, i * 20); + let mut map = HashMap::new(); + map.insert("p1".to_string(), i * 5); + app.observe_proxy_provider_activity(&map); + } + assert_eq!(app.proxy_input_activity_samples.len(), 3); + assert_eq!(app.proxy_provider_activity_samples["p1"].len(), 3); + + // proxy 重启:主计数回退触发主样本清空,provider 样本必须同步清空 + app.observe_proxy_token_activity(1, 2); + assert_eq!(app.proxy_input_activity_samples, vec![0]); + let mut map = HashMap::new(); + map.insert("p1".to_string(), 1); + app.observe_proxy_provider_activity(&map); + assert_eq!( + app.proxy_provider_activity_samples["p1"].len(), + 1, + "provider samples must resync after proxy restart realigns main samples" + ); + } + #[test] fn proxy_transition_starts_when_proxy_route_state_changes() { let mut app = App::new(Some(AppType::Claude)); @@ -9169,37 +9219,6 @@ mod tests { )); } - #[test] - fn settings_icons_row_toggles_and_persists_mode() { - let temp_home = TempDir::new().expect("create temp home"); - let _env = TestEnvGuard::isolated(temp_home.path()); - // The CC_SWITCH_ICONS override would short-circuit the persisted - // value, so clear it to exercise the Settings path deterministically. - let saved_icons = std::env::var_os("CC_SWITCH_ICONS"); - std::env::remove_var("CC_SWITCH_ICONS"); - - let mut app = App::new(Some(AppType::Claude)); - app.route = Route::Settings; - app.focus = Focus::Content; - app.settings_idx = SettingsItem::ALL - .iter() - .position(|item| matches!(item, SettingsItem::Icons)) - .expect("Icons missing from SettingsItem::ALL"); - - // No persisted value is Auto; Enter cycles Auto -> Emoji -> Ascii -> Auto. - assert!(matches!( - app.on_key(key(KeyCode::Enter), &UiData::default()), - Action::None - )); - assert_eq!(crate::settings::get_icon_mode().as_deref(), Some("emoji")); - app.on_key(key(KeyCode::Enter), &UiData::default()); - assert_eq!(crate::settings::get_icon_mode().as_deref(), Some("ascii")); - app.on_key(key(KeyCode::Enter), &UiData::default()); - assert_eq!(crate::settings::get_icon_mode().as_deref(), Some("auto")); - - crate::test_support::restore_env("CC_SWITCH_ICONS", &saved_icons); - } - #[test] fn settings_openclaw_config_dir_text_submit_emits_action() { let mut app = App::new(Some(AppType::Claude)); @@ -13108,23 +13127,6 @@ mod tests { assert!(!text.contains("显示/关闭帮助"), "{text}"); } - #[test] - fn fullwidth_question_mark_toggles_the_help_overlay() { - let _lang = use_test_language(Language::English); - let mut app = App::new(Some(AppType::Claude)); - - // The full-width Chinese '?' opens help, same as ASCII '?'. - let action = app.on_key(key(KeyCode::Char('?')), &UiData::default()); - assert!(matches!(action, Action::None)); - assert!(matches!(app.overlay, Overlay::Help(_))); - assert!(help_text(&app).contains("? toggle help")); - - // Pressing it again toggles the overlay closed. - let action = app.on_key(key(KeyCode::Char('?')), &UiData::default()); - assert!(matches!(action, Action::None)); - assert!(!app.overlay.is_active(), "help overlay should close"); - } - #[test] fn context_help_provider_field_tracks_focused_codex_base_url() { let _lang = use_test_language(Language::English); diff --git a/src-tauri/src/cli/tui/app/types.rs b/src-tauri/src/cli/tui/app/types.rs index a5697e27..c62ce0de 100644 --- a/src-tauri/src/cli/tui/app/types.rs +++ b/src-tauri/src/cli/tui/app/types.rs @@ -557,6 +557,9 @@ pub enum ConfirmAction { ClaudeModelFillAll { source_idx: usize, }, + ModelRouteDelete { + id: String, + }, } #[derive(Debug, Clone)] @@ -567,6 +570,7 @@ pub struct ConfirmOverlay { } #[derive(Debug, Clone, PartialEq, Eq)] +#[allow(dead_code)] pub enum TextSubmit { ConfigExport, ConfigImport, @@ -593,6 +597,26 @@ pub enum TextSubmit { }, WebDavJianguoyunUsername, WebDavJianguoyunPassword, + ModelRouteAddPattern, + ModelRouteAddProvider { + pattern: String, + }, + ModelRouteAddPriority { + pattern: String, + provider_id: String, + }, + ModelRouteEditPattern { + id: String, + }, + ModelRouteEditProvider { + id: String, + pattern: String, + }, + ModelRouteEditPriority { + id: String, + pattern: String, + provider_id: String, + }, } #[derive(Debug, Clone)] @@ -706,6 +730,12 @@ pub enum Overlay { UsageQueryTemplatePicker { selected: usize, }, + ModelRouteProviderPicker { + pattern: String, + selected: usize, + editing: bool, // true=edit mode (has existing id), false=add mode + existing_id: Option, // for edit mode + }, ManagedAccountPicker { auth_provider: String, selected: usize, @@ -817,6 +847,7 @@ impl Overlay { matches!( self, Overlay::BackupPicker { .. } + | Overlay::ModelRouteProviderPicker { .. } | Overlay::TextView(_) | Overlay::CommonSnippetPicker { .. } | Overlay::ProviderTestMenu { .. } @@ -856,6 +887,7 @@ impl Overlay { | Overlay::Help(_) | Overlay::Confirm(_) | Overlay::BackupPicker { .. } + | Overlay::ModelRouteProviderPicker { .. } | Overlay::TextView(_) | Overlay::CommonSnippetPicker { .. } | Overlay::ProviderTestMenu { .. } diff --git a/src-tauri/src/cli/tui/data.rs b/src-tauri/src/cli/tui/data.rs index 2dd37c66..436c20ae 100644 --- a/src-tauri/src/cli/tui/data.rs +++ b/src-tauri/src/cli/tui/data.rs @@ -845,10 +845,29 @@ pub struct UiData { pub proxy: ProxySnapshot, pub usage: UsageSnapshot, pub pricing: ModelPricingSnapshot, + pub model_routes: ModelRouteSnapshot, pub(crate) quota: QuotaSnapshot, pub(crate) reload_token: UiDataReloadToken, } +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct ModelRouteRow { + pub id: String, + pub pattern: String, + pub provider_id: String, + pub provider_name: String, + pub priority: i32, + pub enabled: bool, + pub hit_count: i64, + pub last_hit_at: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct ModelRouteSnapshot { + pub rows: Vec, +} + pub(crate) fn load_state() -> Result { AppState::try_new() } @@ -939,6 +958,7 @@ impl UiData { proxy, usage: UsageSnapshot::default(), pricing: ModelPricingSnapshot::default(), + model_routes: ModelRouteSnapshot::default(), quota: QuotaSnapshot::default(), reload_token: next_reload_token(), }) @@ -962,6 +982,7 @@ impl UiData { proxy, usage: UsageSnapshot::default(), pricing: ModelPricingSnapshot::default(), + model_routes: ModelRouteSnapshot::default(), quota: QuotaSnapshot::default(), reload_token: next_reload_token(), } diff --git a/src-tauri/src/cli/tui/help.rs b/src-tauri/src/cli/tui/help.rs index d455080e..2d477d08 100644 --- a/src-tauri/src/cli/tui/help.rs +++ b/src-tauri/src/cli/tui/help.rs @@ -3,7 +3,6 @@ use crate::cli::i18n; use crate::cli::i18n::texts; use super::app::{App, Overlay}; -use super::data::UiData; use super::form::{ CodexLocalRoutingField, CodexModelCatalogField, CodexPreviewSection, FormFocus, FormMode, FormState, ProviderAddField, ProviderFormPage, UsageQueryField, UsageQueryTemplate, @@ -74,8 +73,8 @@ enum HelpTarget { Empty, } -pub fn context_help_for_app(app: &App, data: &UiData) -> HelpContent { - help_for_target(current_help_target(app), app, data) +pub fn context_help_for_app(app: &App) -> HelpContent { + help_for_target(current_help_target(app), app.app_type.clone()) } fn current_help_target(app: &App) -> HelpTarget { @@ -203,11 +202,9 @@ fn current_help_target(app: &App) -> HelpTarget { } } -fn help_for_target(target: HelpTarget, app: &App, data: &UiData) -> HelpContent { +fn help_for_target(target: HelpTarget, app_type: AppType) -> HelpContent { match target { - HelpTarget::Global => { - HelpContent::new(texts::tui_help_title(), global_help_lines(app, data)) - } + HelpTarget::Global => HelpContent::new(texts::tui_help_title(), global_help_lines(&app_type)), HelpTarget::ProviderTemplate => HelpContent::new( tr("供应商模板", "Provider templates"), help_lines( @@ -229,66 +226,11 @@ fn help_for_target(target: HelpTarget, app: &App, data: &UiData) -> HelpContent } } -/// The global help sheet: the static prelude, then one key line per page. -/// The generated pages (MCP/Prompts/Sessions/Skills/Usage) come from the -/// keymap registry so their hints track dispatch; Providers/Config/Settings -/// (and the Hermes-only Memory line) stay hand-written for their app-scope -/// prose. Order matches the previous static sheet. -fn global_help_lines(app: &App, data: &UiData) -> Vec { - use super::keymap; - - let hermes = matches!(app.app_type, AppType::Hermes); - let mut lines: Vec = texts::tui_help_prelude() +fn global_help_lines(app_type: &AppType) -> Vec { + texts::tui_help_text_for_app(app_type) .lines() .map(str::to_string) - .collect(); - - lines.push(format!( - "- {}", - texts::tui_help_line_providers(&app.app_type) - )); - lines.push(keymap_bullet("MCP", keymap::mcp::help_items(app, data))); - if hermes { - lines.push(format!("- {}", texts::tui_help_line_memory())); - } else { - lines.push(keymap_bullet( - crate::t!("Prompts", "提示词"), - keymap::prompts::help_items(app, data), - )); - } - lines.push(keymap_bullet( - crate::t!("Sessions", "会话"), - keymap::sessions::help_items(app, data), - )); - lines.push(keymap_bullet( - crate::t!("Skills", "技能"), - keymap::skills_installed::help_items(app, data), - )); - lines.push(keymap_bullet( - crate::t!("Usage", "使用统计"), - keymap::usage::help_items(app, data), - )); - if !hermes { - lines.push(format!("- {}", texts::tui_help_line_config())); - } - lines.push(format!("- {}", texts::tui_help_line_settings())); - lines -} - -/// Render one generated page-key bullet: `- : , ...`, -/// with the locale's list punctuation. -fn keymap_bullet(name: &str, items: Vec<(&'static str, &'static str)>) -> String { - let (colon, sep) = if i18n::is_chinese() { - (":", ",") - } else { - (": ", ", ") - }; - let keys = items - .iter() - .map(|(display, label)| format!("{display} {label}")) - .collect::>() - .join(sep); - format!("- {name}{colon}{keys}") + .collect() } fn provider_field_help(app_type: AppType, field: ProviderAddField) -> HelpContent { diff --git a/src-tauri/src/cli/tui/icons.rs b/src-tauri/src/cli/tui/icons.rs deleted file mode 100644 index e14596eb..00000000 --- a/src-tauri/src/cli/tui/icons.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Terminal-icon fallback, mirroring the color-mode philosophy in `theme.rs`. -//! -//! Decorative emoji glyphs (🏠 🔑 …) render double-width on some SSH and -//! legacy terminals and break border alignment. `CC_SWITCH_ICONS` and the -//! Settings › Icons row select the mode; `Auto` keeps emoji unless the locale -//! is clearly not UTF-8 (mirroring how COLORFGBG drives the theme). As with -//! color mode, `Auto` never flips the default blindly — it only downgrades for -//! a locale that cannot render wide glyphs; absent locale info stays emoji. - -const ICON_MODE_ENV: &str = "CC_SWITCH_ICONS"; - -/// User-selectable icon rendering. `Auto` keeps emoji unless the locale is -/// not UTF-8. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum IconMode { - #[default] - Auto, - Emoji, - Ascii, -} - -impl IconMode { - pub fn code(&self) -> &'static str { - match self { - IconMode::Auto => "auto", - IconMode::Emoji => "emoji", - IconMode::Ascii => "ascii", - } - } - - pub fn parse(value: &str) -> Option { - match value.trim().to_ascii_lowercase().as_str() { - "auto" => Some(IconMode::Auto), - "emoji" => Some(IconMode::Emoji), - "ascii" | "text" => Some(IconMode::Ascii), - _ => None, - } - } - - pub fn next(&self) -> Self { - match self { - IconMode::Auto => IconMode::Emoji, - IconMode::Emoji => IconMode::Ascii, - IconMode::Ascii => IconMode::Auto, - } - } -} - -fn icon_mode_override() -> Option { - IconMode::parse(&std::env::var(ICON_MODE_ENV).ok()?) -} - -/// The configured icon mode: the `CC_SWITCH_ICONS` override wins, then the -/// persisted Settings value, else `Auto`. -pub fn configured_icon_mode() -> IconMode { - if let Some(mode) = icon_mode_override() { - return mode; - } - crate::settings::get_icon_mode() - .as_deref() - .and_then(IconMode::parse) - .unwrap_or_default() -} - -/// Whether emoji glyphs should be emitted. `Auto` keeps them only when the -/// locale advertises UTF-8, mirroring the color-mode auto-detection. -pub fn use_emoji() -> bool { - match configured_icon_mode() { - IconMode::Emoji => true, - IconMode::Ascii => false, - IconMode::Auto => locale_is_utf8(), - } -} - -/// True when the first non-empty locale variable advertises UTF-8. Absent -/// locale info is treated as UTF-8 (the historical default) so `Auto` never -/// flips the default blindly — only a locale that is clearly not UTF-8 -/// downgrades to ASCII. -fn locale_is_utf8() -> bool { - for key in ["LC_ALL", "LC_CTYPE", "LANG"] { - match std::env::var(key) { - Ok(value) if !value.is_empty() => { - let lower = value.to_ascii_lowercase(); - return lower.contains("utf-8") || lower.contains("utf8"); - } - _ => continue, - } - } - true -} - -/// True for the decorative emoji glyphs used as label icons — the pictograph -/// planes plus the dingbats/symbols blocks and variation selectors, but NOT -/// CJK text (which terminals size consistently and must not be stripped). -pub fn is_emoji(c: char) -> bool { - matches!( - c as u32, - 0x2600..=0x27BF // Misc symbols + Dingbats (⚙ ⚡ …) - | 0x2B00..=0x2BFF // Misc symbols and arrows - | 0xFE00..=0xFE0F // Variation selectors - | 0x1F000..=0x1FAFF // Emoji / pictograph planes - ) -} - -/// Strip a leading emoji marker (`"🔑 Providers"` → `"Providers"`) when icons -/// are disabled. Labels without an emoji prefix are returned unchanged, so -/// this is safe to apply to any title or menu label. -pub fn strip_icon(label: &str) -> &str { - if use_emoji() { - return label; - } - strip_leading_emoji(label) -} - -/// Icon-mode-agnostic version of [`strip_icon`]: removes a leading emoji and -/// its trailing space, tolerating any leading indent spaces (e.g. the centred -/// home title `" 🎯 CC-Switch …"`). Kept separate so callers that already -/// resolved the mode (and the tests) don't re-read the environment. -pub fn strip_leading_emoji(label: &str) -> &str { - let trimmed = label.trim_start_matches(' '); - match trimmed.chars().next() { - Some(c) if is_emoji(c) => trimmed - .split_once(' ') - .map(|(_, rest)| rest) - .unwrap_or(label), - _ => label, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::ffi::OsString; - use std::sync::{Mutex, OnceLock}; - - fn env_lock() -> &'static Mutex<()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - } - - struct EnvGuard { - key: &'static str, - previous: Option, - } - - impl EnvGuard { - fn set(key: &'static str, value: &str) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::set_var(key, value) }; - Self { key, previous } - } - - fn remove(key: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::remove_var(key) }; - Self { key, previous } - } - } - - impl Drop for EnvGuard { - fn drop(&mut self) { - match self.previous.take() { - Some(value) => unsafe { std::env::set_var(self.key, value) }, - None => unsafe { std::env::remove_var(self.key) }, - } - } - } - - #[test] - fn icon_mode_codes_round_trip() { - for mode in [IconMode::Auto, IconMode::Emoji, IconMode::Ascii] { - assert_eq!(IconMode::parse(mode.code()), Some(mode)); - } - assert_eq!(IconMode::parse(" Ascii "), Some(IconMode::Ascii)); - assert_eq!(IconMode::parse("text"), Some(IconMode::Ascii)); - assert_eq!(IconMode::parse("nerdfont"), None); - } - - #[test] - fn icon_mode_cycles_through_all_variants() { - let start = IconMode::Auto; - assert_eq!(start.next(), IconMode::Emoji); - assert_eq!(start.next().next(), IconMode::Ascii); - assert_eq!(start.next().next().next(), IconMode::Auto); - } - - #[test] - fn env_override_forces_mode_regardless_of_locale() { - let _lock = env_lock().lock().expect("env lock poisoned"); - let _lang = EnvGuard::set("LANG", "en_US.UTF-8"); - let _lc_all = EnvGuard::remove("LC_ALL"); - let _lc_ctype = EnvGuard::remove("LC_CTYPE"); - - let _icons = EnvGuard::set(ICON_MODE_ENV, "ascii"); - assert!(!use_emoji(), "explicit ascii override should disable emoji"); - - let _icons = EnvGuard::set(ICON_MODE_ENV, "emoji"); - assert!(use_emoji(), "explicit emoji override should force emoji"); - } - - #[test] - fn auto_downgrades_only_for_non_utf8_locales() { - let _lock = env_lock().lock().expect("env lock poisoned"); - let _icons = EnvGuard::set(ICON_MODE_ENV, "auto"); - let _lc_all = EnvGuard::remove("LC_ALL"); - let _lc_ctype = EnvGuard::remove("LC_CTYPE"); - - let _lang = EnvGuard::set("LANG", "en_US.UTF-8"); - assert!(use_emoji(), "utf-8 locale keeps emoji under auto"); - - let _lang = EnvGuard::set("LANG", "C"); - assert!(!use_emoji(), "non-utf-8 locale downgrades under auto"); - - let _lang = EnvGuard::set("LANG", "POSIX"); - assert!(!use_emoji(), "POSIX locale downgrades under auto"); - } - - #[test] - fn auto_keeps_emoji_when_locale_is_unset() { - let _lock = env_lock().lock().expect("env lock poisoned"); - let _icons = EnvGuard::set(ICON_MODE_ENV, "auto"); - let _lang = EnvGuard::remove("LANG"); - let _lc_all = EnvGuard::remove("LC_ALL"); - let _lc_ctype = EnvGuard::remove("LC_CTYPE"); - // Absent locale info must not flip the default. - assert!(use_emoji()); - } - - #[test] - fn strip_leading_emoji_handles_menu_labels() { - // The real menu labels: emoji + space + text (some with a variation - // selector). Only the leading emoji is removed; CJK text stays. - assert_eq!(strip_leading_emoji("🏠 Home"), "Home"); - assert_eq!(strip_leading_emoji("🔑 供应商"), "供应商"); - assert_eq!( - strip_leading_emoji("🛠️ MCP Server Management"), - "MCP Server Management" - ); - // Leading indent spaces before the emoji (the centred home title). - assert_eq!( - strip_leading_emoji(" 🎯 CC-Switch 交互模式"), - "CC-Switch 交互模式" - ); - // No emoji prefix → unchanged, even with a leading indent or CJK text. - assert_eq!(strip_leading_emoji("供应商管理"), "供应商管理"); - assert_eq!(strip_leading_emoji("Custom Provider"), "Custom Provider"); - assert_eq!(strip_leading_emoji(" Plain Title"), " Plain Title"); - } - - #[test] - fn cjk_is_not_treated_as_an_emoji() { - assert!(!is_emoji('供')); - assert!(!is_emoji('设')); - assert!(!is_emoji('A')); - assert!(is_emoji('🔑')); - assert!(is_emoji('⚙')); - } -} diff --git a/src-tauri/src/cli/tui/keymap.rs b/src-tauri/src/cli/tui/keymap.rs index 1d34d613..6795c398 100644 --- a/src-tauri/src/cli/tui/keymap.rs +++ b/src-tauri/src/cli/tui/keymap.rs @@ -51,31 +51,6 @@ pub(crate) fn key_bar_items( .collect() } -/// Sentinel `shown` predicate for bindings that never appear in the key bar -/// (hidden aliases, e.g. a reverse-direction key already covered by another -/// chip). The help-sheet generator skips these by function identity — so use -/// this named function rather than an inline `|_, _| false` closure. -pub(crate) fn never(_: &super::app::App, _: &super::data::UiData) -> bool { - false -} - -/// (display, label) pairs for the help sheet: every binding except the -/// `never`-shown aliases, labeled for the given app/data. Unlike -/// `key_bar_items` this does not evaluate each binding's `shown` state, so -/// the catalog lists a page's full key vocabulary regardless of the current -/// selection. -pub(crate) fn help_items( - bindings: &[Binding], - app: &super::app::App, - data: &super::data::UiData, -) -> Vec<(&'static str, &'static str)> { - bindings - .iter() - .filter(|binding| !std::ptr::fn_addr_eq(binding.shown, never as PredicateFn)) - .map(|binding| (binding.display, (binding.label)(app, data))) - .collect() -} - pub(crate) mod providers { use crossterm::event::KeyCode; @@ -338,10 +313,6 @@ pub(crate) mod mcp { super::key_bar_items(BINDINGS, app, data) } - pub(crate) fn help_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { - super::help_items(BINDINGS, app, data) - } - fn any_visible(app: &App, data: &UiData) -> bool { visible_mcp(&app.filter, data) .get(app.mcp_idx) @@ -413,10 +384,6 @@ pub(crate) mod prompts { super::key_bar_items(BINDINGS, app, data) } - pub(crate) fn help_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { - super::help_items(BINDINGS, app, data) - } - fn any_visible(app: &App, data: &UiData) -> bool { visible_prompts(&app.filter, data) .get(app.prompt_idx) @@ -496,10 +463,6 @@ pub(crate) mod skills_installed { super::key_bar_items(BINDINGS, app, data) } - pub(crate) fn help_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { - super::help_items(BINDINGS, app, data) - } - fn any_visible(app: &App, data: &UiData) -> bool { visible_skills_installed(&app.filter, data) .get(app.skills_idx) @@ -573,7 +536,7 @@ pub(crate) mod usage { keys: &[KeyCode::BackTab], intent: Intent::PrevMetric, label: |_, _| crate::t!("switch metric", "切换指标"), - shown: super::never, + shown: |_, _| false, }, Binding { display: "L", @@ -605,91 +568,6 @@ pub(crate) mod usage { pub(crate) fn key_bar_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { super::key_bar_items(BINDINGS, app, data) } - - pub(crate) fn help_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { - super::help_items(BINDINGS, app, data) - } -} - -pub(crate) mod sessions { - use crossterm::event::KeyCode; - - use super::Binding; - use crate::cli::i18n::texts; - use crate::cli::tui::app::App; - use crate::cli::tui::data::UiData; - - /// Action keys only. Pane/list navigation (arrows, h/l, PageUp/Down, - /// Home/End) is handled globally and in the handler's explicit arms - /// because it is pane-dependent and reused while the filter is active; - /// only these actions resolve through the table. The pane checks for - /// `View` stay in the handler body (the Providers pattern). - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub(crate) enum Intent { - View, - Restore, - Delete, - Refresh, - ShowAll, - } - - pub(crate) const BINDINGS: &[Binding] = &[ - Binding { - display: "Enter", - keys: &[KeyCode::Enter], - intent: Intent::View, - label: |_, _| texts::tui_key_view(), - shown: |_, _| true, - }, - Binding { - display: "R", - keys: &[KeyCode::Char('R')], - intent: Intent::Restore, - label: |_, _| texts::tui_key_restore(), - shown: |_, _| true, - }, - Binding { - display: "d", - keys: &[KeyCode::Char('d')], - intent: Intent::Delete, - label: |_, _| texts::tui_key_delete(), - shown: |_, _| true, - }, - Binding { - display: "r", - keys: &[KeyCode::Char('r')], - intent: Intent::Refresh, - label: |_, _| texts::tui_key_refresh(), - shown: |_, _| true, - }, - Binding { - display: "a", - keys: &[KeyCode::Char('a')], - intent: Intent::ShowAll, - label: show_all_label, - shown: |_, _| true, - }, - ]; - - pub(crate) fn intent_for(key: KeyCode) -> Option { - super::intent_for(BINDINGS, key) - } - - pub(crate) fn key_bar_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { - super::key_bar_items(BINDINGS, app, data) - } - - pub(crate) fn help_items(app: &App, data: &UiData) -> Vec<(&'static str, &'static str)> { - super::help_items(BINDINGS, app, data) - } - - fn show_all_label(app: &App, _: &UiData) -> &'static str { - if app.sessions.show_all_providers { - texts::tui_key_sessions_all_active() - } else { - texts::tui_key_sessions_all() - } - } } #[cfg(test)] diff --git a/src-tauri/src/cli/tui/mod.rs b/src-tauri/src/cli/tui/mod.rs index 0c53c71e..6dd13599 100644 --- a/src-tauri/src/cli/tui/mod.rs +++ b/src-tauri/src/cli/tui/mod.rs @@ -2,7 +2,6 @@ mod app; mod data; mod form; pub(crate) mod help; -pub(crate) mod icons; mod keymap; mod route; mod runtime_actions; @@ -1248,6 +1247,10 @@ fn cache_invalidation_for_action(action: &Action) -> CacheInvalidation { | Action::ProviderImportLiveConfig | Action::ProviderDelete { .. } | Action::ProviderSetFailoverQueue { .. } + | Action::ModelRouteAdd { .. } + | Action::ModelRouteEdit { .. } + | Action::ModelRouteDelete { .. } + | Action::ModelRouteToggle { .. } | Action::ProviderMoveFailoverQueue { .. } | Action::EditorSubmit { submit: EditorSubmit::ProviderAdd | EditorSubmit::ProviderEdit { .. }, diff --git a/src-tauri/src/cli/tui/route.rs b/src-tauri/src/cli/tui/route.rs index 3031d60f..c3483908 100644 --- a/src-tauri/src/cli/tui/route.rs +++ b/src-tauri/src/cli/tui/route.rs @@ -26,6 +26,7 @@ pub enum Route { Settings, SettingsProxy, SettingsManagedAccounts, + SettingsModelRoutes, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src-tauri/src/cli/tui/runtime_actions/mod.rs b/src-tauri/src/cli/tui/runtime_actions/mod.rs index f6542f60..61337c9c 100644 --- a/src-tauri/src/cli/tui/runtime_actions/mod.rs +++ b/src-tauri/src/cli/tui/runtime_actions/mod.rs @@ -18,6 +18,7 @@ mod config; mod editor; mod helpers; mod mcp; +mod model_routes; mod pricing; mod prompts; mod providers; @@ -473,6 +474,19 @@ pub(crate) fn handle_action( Action::PromptOpenImportCandidate { filename, content } => { prompts::open_import_candidate(&mut ctx, filename, content) } + Action::ModelRouteAdd { + pattern, + provider_id, + priority, + } => model_routes::handle_add(&mut ctx, pattern, provider_id, priority), + Action::ModelRouteEdit { + id, + pattern, + provider_id, + priority, + } => model_routes::handle_edit(&mut ctx, id, pattern, provider_id, priority), + Action::ModelRouteDelete { id } => model_routes::handle_delete(&mut ctx, id), + Action::ModelRouteToggle { id } => model_routes::handle_toggle(&mut ctx, id), Action::ConfigExport { path } => config::export(&mut ctx, path), Action::ConfigShowFull => config::show_full(&mut ctx), Action::ConfigImport { path } => config::import(&mut ctx, path), diff --git a/src-tauri/src/cli/tui/runtime_actions/model_routes.rs b/src-tauri/src/cli/tui/runtime_actions/model_routes.rs new file mode 100644 index 00000000..60ec8b4d --- /dev/null +++ b/src-tauri/src/cli/tui/runtime_actions/model_routes.rs @@ -0,0 +1,133 @@ +use crate::cli::i18n::texts; +use crate::error::AppError; +use crate::model_route::ModelRoute; + +use super::super::app::ToastKind; +use super::super::data::{load_state, ModelRouteRow, ModelRouteSnapshot}; +use super::RuntimeActionContext; + +fn refresh_model_routes_data(ctx: &mut RuntimeActionContext<'_>) -> Result<(), AppError> { + let state = load_state()?; + let routes = state.db.list_model_routes(ctx.app.app_type.as_str())?; + + let rows: Vec = routes + .into_iter() + .map(|route| { + let provider_name = ctx + .data + .providers + .rows + .iter() + .find(|p| p.id == route.provider_id) + .map(|p| super::super::data::provider_display_name(&ctx.app.app_type, p)) + .unwrap_or_else(|| route.provider_id.clone()); + + ModelRouteRow { + id: route.id, + pattern: route.pattern, + provider_id: route.provider_id, + provider_name, + priority: route.priority, + enabled: route.enabled, + hit_count: route.hit_count, + last_hit_at: route.last_hit_at, + } + }) + .collect(); + + ctx.data.model_routes = ModelRouteSnapshot { rows }; + ctx.app.clamp_selections(ctx.data); + ctx.data.mark_current_app_data_changed(); + Ok(()) +} + +pub(super) fn handle_add( + ctx: &mut RuntimeActionContext<'_>, + pattern: String, + provider_id: String, + priority: i32, +) -> Result<(), AppError> { + let state = load_state()?; + let route = ModelRoute { + id: String::new(), + app_type: ctx.app.app_type.as_str().to_string(), + pattern, + provider_id, + priority, + enabled: true, + created_at: None, + + hit_count: 0, + + last_hit_at: None, + updated_at: None, + }; + + state.db.create_model_route(&route)?; + refresh_model_routes_data(ctx)?; + ctx.app + .push_toast(texts::tui_toast_model_route_added(), ToastKind::Success); + ctx.app.overlay = super::super::app::Overlay::None; + Ok(()) +} + +pub(super) fn handle_edit( + ctx: &mut RuntimeActionContext<'_>, + id: String, + pattern: String, + provider_id: String, + priority: i32, +) -> Result<(), AppError> { + let state = load_state()?; + // 保留已有的 enabled 状态,不因编辑而静默恢复已禁用的路由 + let enabled = state + .db + .get_model_route(&id) + .ok() + .flatten() + .map(|existing| existing.enabled) + .unwrap_or(true); + let route = ModelRoute { + id: String::new(), + app_type: ctx.app.app_type.as_str().to_string(), + pattern, + provider_id, + priority, + enabled, + created_at: None, + + hit_count: 0, + + last_hit_at: None, + updated_at: None, + }; + + state.db.update_model_route(&id, &route)?; + refresh_model_routes_data(ctx)?; + ctx.app + .push_toast(texts::tui_toast_model_route_updated(), ToastKind::Success); + ctx.app.overlay = super::super::app::Overlay::None; + Ok(()) +} + +pub(super) fn handle_delete( + ctx: &mut RuntimeActionContext<'_>, + id: String, +) -> Result<(), AppError> { + let state = load_state()?; + state.db.delete_model_route(&id)?; + refresh_model_routes_data(ctx)?; + ctx.app + .push_toast(texts::tui_toast_model_route_deleted(), ToastKind::Success); + Ok(()) +} + +pub(super) fn handle_toggle( + ctx: &mut RuntimeActionContext<'_>, + id: String, +) -> Result<(), AppError> { + let state = load_state()?; + state.db.toggle_model_route(&id)?; + refresh_model_routes_data(ctx)?; + Ok(()) +} diff --git a/src-tauri/src/cli/tui/runtime_systems/types.rs b/src-tauri/src/cli/tui/runtime_systems/types.rs index f7c06be4..242c4d62 100644 --- a/src-tauri/src/cli/tui/runtime_systems/types.rs +++ b/src-tauri/src/cli/tui/runtime_systems/types.rs @@ -390,6 +390,7 @@ pub(crate) enum ProxyReq { }, } +#[allow(clippy::large_enum_variant)] pub(crate) enum ProxyMsg { ManagedSessionFinished { request_id: u64, diff --git a/src-tauri/src/cli/tui/ui.rs b/src-tauri/src/cli/tui/ui.rs index 600bc4ec..a3980428 100644 --- a/src-tauri/src/cli/tui/ui.rs +++ b/src-tauri/src/cli/tui/ui.rs @@ -28,7 +28,6 @@ use super::{ CodexPreviewSection, FormFocus, FormState, GeminiAuthType, McpAddField, PromptMetaField, ProviderAddField, }, - icons, route::{NavItem, Route}, theme, theme::theme_for, @@ -40,6 +39,7 @@ mod editor; mod forms; mod main_page; mod mcp; +mod model_routes; mod overlay; mod pricing; mod prompts; @@ -62,6 +62,7 @@ use editor::*; use forms::*; use main_page::*; use mcp::*; +use model_routes::*; use overlay::*; use pricing::*; use prompts::*; @@ -195,6 +196,9 @@ fn render_content( Route::SettingsManagedAccounts => { render_settings_managed_accounts(frame, app, data, content_area, theme) } + Route::SettingsModelRoutes => { + render_settings_model_routes(frame, app, data, content_area, theme) + } } } diff --git a/src-tauri/src/cli/tui/ui/chrome.rs b/src-tauri/src/cli/tui/ui/chrome.rs index 7f7f5482..1bfe5f0e 100644 --- a/src-tauri/src/cli/tui/ui/chrome.rs +++ b/src-tauri/src/cli/tui/ui/chrome.rs @@ -279,16 +279,9 @@ pub(super) fn nav_pane_width(theme: &super::theme::Theme) -> u16 { .saturating_add(NAV_TEXT_EXTRA_WIDTH) .max(NAV_TEXT_MIN_WIDTH); - // In ASCII mode the emoji column is collapsed, so it reserves no width. - let icon_col_width = if icons::use_emoji() { - NAV_ICON_COL_WIDTH - } else { - 0 - }; - NAV_BORDER_WIDTH .saturating_add(highlight_width) - .saturating_add(icon_col_width) + .saturating_add(NAV_ICON_COL_WIDTH) .saturating_add(NAV_COL_SPACING) .saturating_add(text_col_width) } @@ -298,34 +291,23 @@ pub(super) fn render_nav( area: Rect, theme: &super::theme::Theme, ) { - let emoji = icons::use_emoji(); let rows = app.nav_items().iter().map(|item| { let (icon, text) = split_nav_label(nav_label(*item)); - // ASCII mode drops the emoji entirely (an empty, zero-width column) - // so wide-rendered glyphs can never push the text past the border. - let icon_cell = if emoji { - cell_pad(icon).replace('\u{FE0F}', "") - } else { - String::new() - }; - Row::new(vec![Cell::from(icon_cell), Cell::from(text)]) + let icon_clean = cell_pad(icon).replace('\u{FE0F}', ""); + Row::new(vec![Cell::from(icon_clean), Cell::from(text)]) }); - let icon_col_width = if emoji { 3 } else { 0 }; - let table = Table::new( - rows, - [Constraint::Length(icon_col_width), Constraint::Min(10)], - ) - .column_spacing(1) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(pane_border_style(app, Focus::Nav, theme)) - .title(format!(" {} ", texts::tui_nav_title())), - ) - .row_highlight_style(selection_style(theme)) - .highlight_symbol(highlight_symbol(theme)); + let table = Table::new(rows, [Constraint::Length(3), Constraint::Min(10)]) + .column_spacing(1) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Nav, theme)) + .title(format!(" {} ", texts::tui_nav_title())), + ) + .row_highlight_style(selection_style(theme)) + .highlight_symbol(highlight_symbol(theme)); let mut state = TableState::default(); state.select(Some(app.nav_idx)); diff --git a/src-tauri/src/cli/tui/ui/config.rs b/src-tauri/src/cli/tui/ui/config.rs index 6462db78..3e6880f2 100644 --- a/src-tauri/src/cli/tui/ui/config.rs +++ b/src-tauri/src/cli/tui/ui/config.rs @@ -21,6 +21,7 @@ pub(super) fn webdav_config_item_label(item: &WebDavConfigItem) -> &'static str pub(super) fn local_proxy_settings_item_label(item: &LocalProxySettingsItem) -> &'static str { match item { + LocalProxySettingsItem::ProxySwitch => crate::t!("Proxy enabled", "代理开关"), LocalProxySettingsItem::ListenAddress => texts::tui_settings_proxy_listen_address_label(), LocalProxySettingsItem::ListenPort => texts::tui_settings_proxy_listen_port_label(), LocalProxySettingsItem::AutoFailover => crate::t!("Automatic failover", "自动故障转移"), @@ -56,19 +57,24 @@ pub(super) fn render_config( .iter() .map(|item| Row::new(vec![Cell::from(config_item_label(item))])); + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(format!(" {} ", texts::tui_config_title())); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(inner); + let mut keys = vec![("Enter", texts::tui_key_select())]; if matches!(items.get(app.config_idx), Some(ConfigItem::CommonSnippet)) { keys.push(("e", texts::tui_key_edit_snippet())); } - let body = render_page_frame( - frame, - area, - theme, - app, - texts::tui_config_title(), - &keys, - None, - ); + render_page_key_bar(frame, chunks[0], theme, &keys, app.focus == Focus::Content); let table = Table::new(rows, [Constraint::Min(10)]) .block(Block::default().borders(Borders::NONE)) @@ -77,7 +83,7 @@ pub(super) fn render_config( let mut state = TableState::default(); state.select(Some(app.config_idx)); - frame.render_stateful_widget(table, inset_left(body, CONTENT_INSET_LEFT), &mut state); + frame.render_stateful_widget(table, inset_left(chunks[1], CONTENT_INSET_LEFT), &mut state); } pub(super) fn render_config_webdav( @@ -92,6 +98,22 @@ pub(super) fn render_config_webdav( .iter() .map(|item| Row::new(vec![Cell::from(webdav_config_item_label(item))])); + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(breadcrumb_title(&[ + texts::tui_config_title(), + texts::tui_config_webdav_title(), + ])); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(inner); + let mut keys = vec![("Enter", texts::tui_key_select())]; if matches!( items.get(app.config_webdav_idx), @@ -99,15 +121,7 @@ pub(super) fn render_config_webdav( ) { keys.push(("e", texts::tui_key_edit())); } - let body = render_page_frame( - frame, - area, - theme, - app, - &breadcrumb_path(&[texts::tui_config_title(), texts::tui_config_webdav_title()]), - &keys, - None, - ); + render_page_key_bar(frame, chunks[0], theme, &keys, app.focus == Focus::Content); let table = Table::new(rows, [Constraint::Min(10)]) .block(Block::default().borders(Borders::NONE)) @@ -116,7 +130,7 @@ pub(super) fn render_config_webdav( let mut state = TableState::default(); state.select(Some(app.config_webdav_idx)); - frame.render_stateful_widget(table, inset_left(body, CONTENT_INSET_LEFT), &mut state); + frame.render_stateful_widget(table, inset_left(chunks[1], CONTENT_INSET_LEFT), &mut state); } pub(super) fn render_config_openclaw_route( @@ -2516,89 +2530,87 @@ pub(super) fn render_settings( let claude_plugin_integration = crate::settings::get_enable_claude_plugin_integration(); let codex_unified_session_history = crate::settings::unify_codex_session_history(); - let rows_data = super::app::SettingsItem::ALL - .iter() - .map(|item| match item { - super::app::SettingsItem::Language => ( - texts::tui_settings_header_language().to_string(), - language.display_name().to_string(), - ), - super::app::SettingsItem::Theme => { - ( + let rows_data = + super::app::SettingsItem::ALL + .iter() + .map(|item| match item { + super::app::SettingsItem::Language => ( + texts::tui_settings_header_language().to_string(), + language.display_name().to_string(), + ), + super::app::SettingsItem::Theme => ( texts::tui_settings_theme_label().to_string(), texts::tui_settings_theme_mode_name( crate::cli::tui::theme::configured_theme_mode(), ) .to_string(), - ) - } - super::app::SettingsItem::Icons => ( - texts::tui_settings_icons_label().to_string(), - texts::tui_settings_icon_mode_name(crate::cli::tui::icons::configured_icon_mode()) - .to_string(), - ), - super::app::SettingsItem::VisibleAppsMode => ( - texts::tui_settings_visible_apps_mode_label().to_string(), - match visible_apps_mode { - crate::settings::VisibleAppsMode::Auto => { - texts::tui_settings_visible_apps_mode_auto().to_string() - } - crate::settings::VisibleAppsMode::Manual => { - texts::tui_settings_visible_apps_mode_manual().to_string() - } - }, - ), - super::app::SettingsItem::VisibleApps => ( - texts::tui_settings_visible_apps_label().to_string(), - visible_apps_summary(&visible_apps), - ), - super::app::SettingsItem::OpenClawConfigDir => ( - texts::tui_settings_openclaw_config_dir_label().to_string(), - openclaw_config_dir.clone().unwrap_or_else(|| { - texts::tui_settings_openclaw_config_dir_default_value().to_string() - }), - ), - super::app::SettingsItem::ManagedAccounts => ( - texts::tui_settings_managed_accounts_title().to_string(), - managed_accounts_summary(app), - ), - super::app::SettingsItem::SkipClaudeOnboarding => ( - texts::skip_claude_onboarding_label().to_string(), - if skip_claude_onboarding { - texts::enabled().to_string() - } else { - texts::disabled().to_string() - }, - ), - super::app::SettingsItem::ClaudePluginIntegration => ( - texts::enable_claude_plugin_integration_label().to_string(), - if claude_plugin_integration { - texts::enabled().to_string() - } else { - texts::disabled().to_string() - }, - ), - super::app::SettingsItem::CodexUnifiedSessionHistory => ( - texts::codex_unified_session_history_label().to_string(), - if codex_unified_session_history { - texts::enabled().to_string() - } else { - texts::disabled().to_string() - }, - ), - super::app::SettingsItem::Proxy => ( - texts::tui_config_item_proxy().to_string(), - format!( - "{}:{}", - data.proxy.configured_listen_address, data.proxy.configured_listen_port, ), - ), - super::app::SettingsItem::CheckForUpdates => ( - texts::tui_settings_check_for_updates().to_string(), - format!("v{}", env!("CARGO_PKG_VERSION")), - ), - }) - .collect::>(); + super::app::SettingsItem::VisibleAppsMode => ( + texts::tui_settings_visible_apps_mode_label().to_string(), + match visible_apps_mode { + crate::settings::VisibleAppsMode::Auto => { + texts::tui_settings_visible_apps_mode_auto().to_string() + } + crate::settings::VisibleAppsMode::Manual => { + texts::tui_settings_visible_apps_mode_manual().to_string() + } + }, + ), + super::app::SettingsItem::VisibleApps => ( + texts::tui_settings_visible_apps_label().to_string(), + visible_apps_summary(&visible_apps), + ), + super::app::SettingsItem::OpenClawConfigDir => ( + texts::tui_settings_openclaw_config_dir_label().to_string(), + openclaw_config_dir.clone().unwrap_or_else(|| { + texts::tui_settings_openclaw_config_dir_default_value().to_string() + }), + ), + super::app::SettingsItem::ManagedAccounts => ( + texts::tui_settings_managed_accounts_title().to_string(), + managed_accounts_summary(app), + ), + super::app::SettingsItem::SkipClaudeOnboarding => ( + texts::skip_claude_onboarding_label().to_string(), + if skip_claude_onboarding { + texts::enabled().to_string() + } else { + texts::disabled().to_string() + }, + ), + super::app::SettingsItem::ClaudePluginIntegration => ( + texts::enable_claude_plugin_integration_label().to_string(), + if claude_plugin_integration { + texts::enabled().to_string() + } else { + texts::disabled().to_string() + }, + ), + super::app::SettingsItem::CodexUnifiedSessionHistory => ( + texts::codex_unified_session_history_label().to_string(), + if codex_unified_session_history { + texts::enabled().to_string() + } else { + texts::disabled().to_string() + }, + ), + super::app::SettingsItem::Proxy => ( + texts::tui_config_item_proxy().to_string(), + format!( + "{}:{}", + data.proxy.configured_listen_address, data.proxy.configured_listen_port, + ), + ), + super::app::SettingsItem::ModelRoutes => ( + "模型路由".to_string(), + format!("{} 条规则", data.model_routes.rows.len()), + ), + super::app::SettingsItem::CheckForUpdates => ( + texts::tui_settings_check_for_updates().to_string(), + format!("v{}", env!("CARGO_PKG_VERSION")), + ), + }) + .collect::>(); let label_col_width = field_label_column_width( rows_data @@ -2618,14 +2630,25 @@ pub(super) fn render_settings( .iter() .map(|(label, value)| Row::new(vec![Cell::from(label.clone()), Cell::from(value.clone())])); - let body = render_page_frame( + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(format!(" {} ", texts::menu_settings())); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(inner); + + render_page_key_bar( frame, - area, + chunks[0], theme, - app, - texts::menu_settings(), &[("Enter", texts::tui_key_apply())], - None, + app.focus == Focus::Content, ); let table = Table::new( @@ -2639,7 +2662,7 @@ pub(super) fn render_settings( let mut state = TableState::default(); state.select(Some(app.settings_idx)); - frame.render_stateful_widget(table, inset_left(body, CONTENT_INSET_LEFT), &mut state); + frame.render_stateful_widget(table, inset_left(chunks[1], CONTENT_INSET_LEFT), &mut state); } fn managed_accounts_summary(app: &App) -> String { @@ -2663,24 +2686,35 @@ pub(super) fn render_settings_managed_accounts( area: Rect, theme: &super::theme::Theme, ) { - let keys = managed_account_key_items(app); - let body = render_page_frame( - frame, - area, - theme, - app, - &breadcrumb_path(&[ + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(breadcrumb_title(&[ texts::menu_settings(), texts::tui_settings_managed_accounts_title(), - ]), - &keys, - Some(managed_accounts_page_summary(app)), - ); + ])); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(0), + ]) + .split(inner); + + let keys = managed_account_key_items(app); + render_page_key_bar(frame, chunks[0], theme, &keys, app.focus == Focus::Content); + + render_summary_bar(frame, chunks[1], theme, managed_accounts_page_summary(app)); let columns = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(44), Constraint::Percentage(56)]) - .split(body); + .split(chunks[2]); render_managed_account_list(frame, app, columns[0], theme); render_managed_account_details(frame, app, columns[1], theme); @@ -3112,6 +3146,14 @@ pub(super) fn render_settings_proxy( let rows_data = LocalProxySettingsItem::ALL .iter() .map(|item| match item { + LocalProxySettingsItem::ProxySwitch => ( + local_proxy_settings_item_label(item).to_string(), + if data.proxy.enabled { + texts::enabled().to_string() + } else { + texts::disabled().to_string() + }, + ), LocalProxySettingsItem::ListenAddress => ( local_proxy_settings_item_label(item).to_string(), data.proxy.configured_listen_address.clone(), diff --git a/src-tauri/src/cli/tui/ui/main_page.rs b/src-tauri/src/cli/tui/ui/main_page.rs index f5c3380e..d0bbccb7 100644 --- a/src-tauri/src/cli/tui/ui/main_page.rs +++ b/src-tauri/src/cli/tui/ui/main_page.rs @@ -1,7 +1,14 @@ use crate::cli::tui::data; +use std::collections::{HashMap, HashSet}; use super::*; +/// Dracula purple — used for input (downstream) graph to contrast with accent-colored output. +const DRACULA_PURPLE: (u8, u8, u8) = (189, 147, 249); + +/// 图例中最低显示 token 数(近期窗口增量);低于此值的 provider 会从图例中隐藏,避免 0% 干扰主图例 +const LEGEND_MIN_RECENT_TOKENS: u64 = 1_000; + fn opencode_configured_provider_count(data: &UiData) -> usize { data.providers .rows @@ -254,7 +261,7 @@ pub(super) fn render_main( .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(pane_border_style(app, Focus::Content, theme)) - .title(format!(" {} ", icons::strip_icon(texts::welcome_title()))); + .title(format!(" {} ", texts::welcome_title())); frame.render_widget(block.clone(), area); let inner = block.inner(area); @@ -291,12 +298,16 @@ pub(super) fn render_main( .split(chunks[1]); if current_app_routed { + // 收集近期 token 活动按 provider 聚合(用于多色图例,与点阵图同口径) + let route_hits = + collect_route_hits_for_dashboard(data, &app.proxy_provider_activity_samples); render_proxy_activity_dashboard( frame, hero_chunks[0], theme, &app.proxy_input_activity_samples, &app.proxy_output_activity_samples, + &app.proxy_provider_activity_samples, &uptime_text, &proxy_last_error_text, data.proxy.last_error.is_some(), @@ -305,6 +316,7 @@ pub(super) fn render_main( auto_failover_queue_len, data.proxy.estimated_input_tokens_total, data.proxy.estimated_output_tokens_total, + &route_hits, ); } else { render_logo_hero(frame, hero_chunks[0], theme); @@ -330,6 +342,7 @@ fn render_proxy_activity_dashboard( theme: &super::theme::Theme, input_activity_samples: &[u64], output_activity_samples: &[u64], + provider_activity_samples: &HashMap>, uptime_text: &str, proxy_last_error_text: &str, has_proxy_error: bool, @@ -338,6 +351,7 @@ fn render_proxy_activity_dashboard( auto_failover_queue_len: usize, input_tokens_total: u64, output_tokens_total: u64, + route_hits: &[ProviderHitInfo], ) -> Rect { let has_token_traffic = input_tokens_total > 0 || output_tokens_total > 0; let title_output_style = if has_token_traffic { @@ -348,7 +362,13 @@ fn render_proxy_activity_dashboard( Style::default().fg(theme.surface) }; let title_input_style = if has_token_traffic { - Style::default().fg(theme.cyan).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Rgb( + DRACULA_PURPLE.0, + DRACULA_PURPLE.1, + DRACULA_PURPLE.2, + )) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(theme.surface) }; @@ -424,6 +444,40 @@ fn render_proxy_activity_dashboard( ); } + // 多色 Provider 近期流量图例(与点阵图共用近期 token 口径) + // 过滤掉过小流量(< LEGEND_MIN_RECENT_TOKENS tok)的 provider + let display_hits: Vec<&ProviderHitInfo> = route_hits + .iter() + .filter(|h| h.recent_tokens >= LEGEND_MIN_RECENT_TOKENS) + .take(5) + .collect(); + if !display_hits.is_empty() { + // 总量基于所有 route_hits(含 < LEGEND_MIN_RECENT_TOKENS 的),让百分比统计更准 + let total_tokens: u64 = route_hits.iter().map(|h| h.recent_tokens).sum(); + if total_tokens > 0 { + let legend_label = crate::t!("Recent tokens", "近期流量"); + meta_spans.push(Span::raw(" ")); + meta_spans.push(Span::styled(format!("{legend_label}: "), label_style)); + meta_plain.push_str(" "); + meta_plain.push_str(legend_label); + meta_plain.push_str(": "); + for (i, hit) in display_hits.iter().enumerate() { + if i > 0 { + meta_spans.push(Span::raw(", ")); + meta_plain.push_str(", "); + } + let pct = (hit.recent_tokens as f64 / total_tokens as f64) * 100.0; + let tok_text = format_estimated_token_compact(hit.recent_tokens); + let text = format!("{} {}% ({})", hit.display_name, pct as i32, tok_text); + meta_spans.push(Span::styled( + text.clone(), + Style::default().fg(hit.color).add_modifier(Modifier::BOLD), + )); + meta_plain.push_str(&text); + } + } + } + let max_text_height = inner.height.saturating_sub(2).clamp(1, 4); let text_height = wrapped_display_line_count(&meta_plain, inner.width).min(max_text_height); let graph_height = inner.height.saturating_sub(text_height).max(2); @@ -445,37 +499,124 @@ fn render_proxy_activity_dashboard( let lower_height = graph_height.saturating_sub(upper_height).max(1); let wave_width = sections[1].width.saturating_sub(1); let mut graph_lines = Vec::new(); - let upper_style = Style::default().fg(theme.accent); - let lower_style = if theme.no_color { + + // 从图例数据构建 provider_id → 颜色映射(与 legend 颜色一致) + let mut provider_color_map: HashMap = route_hits + .iter() + .map(|h| (h.provider_id.clone(), h.color)) + .collect(); + + // 补全颜色:直接切换的 provider(不在 route_hits 中)但仍在活动 sample 里。 + // 复用图例同款调色板,按 i % 8 取色,确保点阵有颜色。 + let palette: [Color; 8] = PER_PROVIDER_PALETTE_RGBS.map(|rgb| Color::Rgb(rgb.0, rgb.1, rgb.2)); + let palette_len = palette.len(); + if palette_len > 0 { + // 先收齐所有缺失颜色的 provider_id,避免借用冲突 + let missing: Vec = provider_activity_samples + .keys() + .filter(|id| !provider_color_map.contains_key(*id)) + .cloned() + .collect(); + for (i, provider_id) in missing.iter().enumerate() { + provider_color_map.insert(provider_id.clone(), palette[i % palette_len]); + } + } + + let visible_provider_ids: HashSet = + route_hits.iter().map(|h| h.provider_id.clone()).collect(); + let visible_samples: Vec<(&String, &Vec)> = provider_activity_samples + .iter() + .filter(|(id, _)| visible_provider_ids.contains(*id)) + .collect(); + + // 点阵每列实际占据的行数(从底部算)。颜色只填点阵字符所在的区间,避免 minor + // provider 颜色被分配到点阵空白行而不可见(图例颜色与点阵颜色对不上的根因)。 + let upper_filled = + column_filled_rows(wave_width as usize, upper_height, output_activity_samples); + let lower_filled = + column_filled_rows(wave_width as usize, lower_height, input_activity_samples); + + let upper_color_stacks = compute_column_color_stacks( + visible_samples.iter().copied(), + wave_width as usize, + &provider_color_map, + upper_height as usize, + &upper_filled, + ); + let lower_color_stacks = compute_column_color_stacks( + visible_samples.iter().copied(), + wave_width as usize, + &provider_color_map, + lower_height as usize, + &lower_filled, + ); + + let upper_rows = proxy_wave_lines( + wave_width, + upper_height, + true, + output_activity_samples, + &DOTS, + false, + ); + let lower_rows = proxy_wave_lines( + wave_width, + lower_height, + true, + input_activity_samples, + &REV_DOTS, + true, + ); + + let default_upper = Style::default().fg(theme.accent); + let default_lower = if theme.no_color { Style::default() } else { - Style::default().fg(theme.cyan) + Style::default().fg(Color::Rgb( + DRACULA_PURPLE.0, + DRACULA_PURPLE.1, + DRACULA_PURPLE.2, + )) }; - graph_lines.extend( - proxy_wave_lines( - wave_width, - upper_height, - true, - output_activity_samples, - &DOTS, - false, - ) - .into_iter() - .map(|row| Line::from(vec![Span::raw(" "), Span::styled(row, upper_style)])), - ); - graph_lines.extend( - proxy_wave_lines( - wave_width, - lower_height, - true, - input_activity_samples, - &REV_DOTS, - true, - ) - .into_iter() - .map(|row| Line::from(vec![Span::raw(" "), Span::styled(row, lower_style)])), - ); + // 上半部分(output),每列按 provider 颜色 + for (row_idx, row) in upper_rows.iter().enumerate() { + let mut spans = vec![Span::raw(" ")]; + for (col_idx, ch) in row.chars().enumerate() { + let style = match stack_color_at(&upper_color_stacks, col_idx, row_idx) { + Some(provider_color) => { + if theme.no_color { + Style::default().add_modifier(Modifier::BOLD) + } else { + // 上半部使用 provider 颜色,稍微调亮 + Style::default().fg(provider_color) + } + } + None => default_upper, + }; + spans.push(Span::styled(ch.to_string(), style)); + } + graph_lines.push(Line::from(spans)); + } + + // 下半部分(input),使用与上半部相同的 per-provider 颜色 + for (row_idx, row) in lower_rows.iter().enumerate() { + let mut spans = vec![Span::raw(" ")]; + for (col_idx, ch) in row.chars().enumerate() { + let style = match stack_color_at(&lower_color_stacks, col_idx, row_idx) { + Some(provider_color) => { + if theme.no_color { + Style::default().add_modifier(Modifier::BOLD) + } else { + Style::default().fg(provider_color) + } + } + None => default_lower, + }; + spans.push(Span::styled(ch.to_string(), style)); + } + graph_lines.push(Line::from(spans)); + } frame.render_widget( Paragraph::new(graph_lines).wrap(Wrap { trim: false }), @@ -493,6 +634,243 @@ fn wrapped_display_line_count(text: &str, width: u16) -> u16 { UnicodeWidthStr::width(text).max(1).div_ceil(width as usize) as u16 } +/// 点阵图多色 palette(与 legend 共用同一组颜色) +const PER_PROVIDER_PALETTE_RGBS: [(u8, u8, u8); 8] = [ + (189, 147, 249), // 紫 + (135, 206, 250), // 天蓝 + (255, 160, 122), // 浅三文鱼 + (144, 238, 144), // 浅绿 + (221, 160, 221), // 李子紫 + (255, 215, 0), // 金 + (127, 255, 212), // 碧绿 + (176, 196, 222), // 淡钢蓝 +]; + +/// 根据 per-provider 活动样本,计算每列的垂直颜色栈。 +/// 颜色只填充该列点阵实际占据的行(`column_filled_rows`,从底部算), +/// 并在区间内按 token 占比分配行高:dominant 在底部,minor 紧贴其上。 +/// 这样每个 provider 的颜色都落在有点阵字符的行上,minor provider 也可见。 +#[allow(clippy::needless_range_loop)] +fn compute_column_color_stacks<'a>( + provider_activity_samples: impl IntoIterator)>, + num_columns: usize, + provider_color_map: &HashMap, + stack_height: usize, + column_filled_rows: &[usize], +) -> Vec>> { + if num_columns == 0 || stack_height == 0 { + return vec![vec![None; stack_height]; num_columns]; + } + + let provider_activity_samples = provider_activity_samples.into_iter().collect::>(); + if provider_activity_samples.is_empty() { + return vec![vec![None; stack_height]; num_columns]; + } + + let mut color_stacks = vec![vec![None; stack_height]; num_columns]; + for col in 0..num_columns { + // 该列点阵实际占据的行数(从底部算)。颜色只填这个区间,避免 minor + // provider 的颜色被分配到点阵空白行(图例与点阵颜色对不上的根因)。 + let filled = column_filled_rows + .get(col) + .copied() + .unwrap_or(0) + .min(stack_height); + if filled == 0 { + continue; + } + + let mut entries = Vec::new(); + for (provider_id, samples) in &provider_activity_samples { + let tokens = samples.get(col).copied().unwrap_or(0); + if tokens > 0 { + if let Some(color) = provider_color_map.get(*provider_id).copied() { + entries.push((provider_id.as_str(), tokens, color)); + } + } + } + if entries.is_empty() { + continue; + } + + entries.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0))); + let total_tokens = entries.iter().map(|(_, tokens, _)| *tokens).sum::(); + // 在 [0, filled) 内分配行数,dominant 占高 idx(点阵底部),minor 占低 idx(顶部字符行)。 + let mut rows = allocate_provider_rows(&entries, total_tokens, filled); + rows.reverse(); + + let base = stack_height - filled; + let mut idx = 0; + for (entry_idx, row_count) in rows { + let color = entries[entry_idx].2; + for _ in 0..row_count { + if idx >= filled { + break; + } + color_stacks[col][base + idx] = Some(color); + idx += 1; + } + } + } + color_stacks +} + +fn stack_color_at( + color_stacks: &[Vec>], + col_idx: usize, + row_idx: usize, +) -> Option { + color_stacks + .get(col_idx) + .and_then(|stack| stack.get(row_idx)) + .copied() + .flatten() +} + +/// 计算点阵每列实际占据的行数(从底部算),与 `proxy_wave_lines` 的渲染口径一致。 +/// 颜色栈据此只填充点阵有字符的区间,确保 provider 颜色落在可见的字符行上。 +fn column_filled_rows(width: usize, height: u16, samples: &[u64]) -> Vec { + if width == 0 || height == 0 { + return Vec::new(); + } + let recent = super::proxy_wave::recent_samples(width, true, samples); + let scaled = super::proxy_wave::scale_samples(height, &recent, true); + scaled + .iter() + .map(|v| (*v as usize).div_ceil(8)) + .map(|rows| rows.min(height as usize)) + .collect() +} + +fn allocate_provider_rows( + entries: &[(&str, u64, Color)], + total_tokens: u64, + stack_height: usize, +) -> Vec<(usize, usize)> { + if entries.is_empty() || total_tokens == 0 || stack_height == 0 { + return Vec::new(); + } + + let mut allocations = entries + .iter() + .enumerate() + .map(|(idx, (_, tokens, _))| { + let exact = (*tokens as f64 / total_tokens as f64) * stack_height as f64; + let mut rows = exact.floor() as usize; + if rows == 0 { + rows = 1; + } + (idx, rows, exact - exact.floor()) + }) + .collect::>(); + + let mut total_rows = allocations.iter().map(|(_, rows, _)| *rows).sum::(); + while total_rows > stack_height { + if let Some((_, rows, _)) = allocations + .iter_mut() + .filter(|(_, rows, _)| *rows > 1) + .min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)) + { + *rows -= 1; + total_rows -= 1; + } else { + break; + } + } + + while total_rows < stack_height { + if let Some((_, rows, _)) = allocations + .iter_mut() + .max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)) + { + *rows += 1; + total_rows += 1; + } else { + break; + } + } + + allocations + .into_iter() + .filter_map(|(idx, rows, _)| (rows > 0).then_some((idx, rows))) + .collect() +} + +/// Provider 命中信息(用于仪表盘多色图例和点阵图着色) +#[derive(Clone)] +struct ProviderHitInfo { + provider_id: String, + display_name: String, + /// 最近 PROXY_ACTIVITY_WINDOW 窗口的 token 增量总和(近期实际流量) + recent_tokens: u64, + color: Color, +} + +/// 从近期 token 活动样本按 provider 聚合(与点阵图同口径),分配不同颜色。 +/// 聚合源为 `samples`(按 provider 的窗口 token 增量),并补齐 model_routes 中 +/// enabled 但近期无流量的 provider(其 recent_tokens 为 0,会被图例阈值过滤)。 +fn collect_route_hits_for_dashboard( + data: &UiData, + samples: &HashMap>, +) -> Vec { + let mut agg: HashMap = HashMap::new(); + + // 1) 近期 token 增量是主信号:每个窗口 delta 之和 + for (provider_id, sample_vec) in samples { + let sum: u64 = sample_vec.iter().sum(); + agg.insert(provider_id.clone(), sum); + } + + // 2) 并集 model_routes enabled 的 provider(近期无流量的 recent_tokens 记 0, + // 下游由 LEGEND_MIN_RECENT_TOKENS 阈值过滤) + for row in &data.model_routes.rows { + if !row.enabled { + continue; + } + agg.entry(row.provider_id.clone()).or_insert(0); + } + + if agg.is_empty() { + return Vec::new(); + } + + let mut v: Vec<(String, u64)> = agg.into_iter().collect(); + // recent_tokens 降序;相同值按 provider_id 字典序,保证测试与显示稳定 + v.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); + // 使用与点阵图相同的 palette,确保颜色一致 + let palette: [Color; 8] = PER_PROVIDER_PALETTE_RGBS.map(|rgb| Color::Rgb(rgb.0, rgb.1, rgb.2)); + v.into_iter() + .enumerate() + .map(|(i, (provider_id, recent_tokens))| { + let display_name = data + .providers + .rows + .iter() + .find(|p| p.id == provider_id) + .map(|p| { + // 截断过长的 provider 名 + let s = p.provider.name.clone(); + if s.chars().count() > 8 { + let truncated: String = s.chars().take(6).collect(); + format!("{truncated}…") + } else { + s + } + }) + .unwrap_or_else(|| { + // provider 已被删除时使用 id 前 8 字符 + provider_id.chars().take(8).collect() + }); + ProviderHitInfo { + provider_id: provider_id.clone(), + display_name, + recent_tokens, + color: palette[i % palette.len()], + } + }) + .collect() +} + fn render_logo_hero(frame: &mut Frame<'_>, area: Rect, theme: &super::theme::Theme) { let logo_lines = logo_hero_lines(theme); let logo_height = (logo_lines.len() as u16).min(area.height); @@ -719,3 +1097,178 @@ pub(super) fn proxy_activity_wave(width: u16, current_app_routed: bool, samples: .next() .unwrap_or_default() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::tui::data::{ModelRouteRow, ProviderRow}; + use crate::provider::Provider; + use serde_json::Value; + + /// 构造一个最小可用的 Provider(仅 id/name 有意义,其余留空) + fn make_provider(id: &str, name: &str) -> Provider { + Provider { + id: id.to_string(), + name: name.to_string(), + settings_config: Value::Null, + website_url: None, + category: None, + created_at: None, + sort_index: None, + notes: None, + meta: None, + icon: None, + icon_color: None, + in_failover_queue: false, + } + } + + /// 构造一个最小 ProviderRow + fn make_provider_row(id: &str, name: &str) -> ProviderRow { + ProviderRow { + id: id.to_string(), + provider: make_provider(id, name), + api_url: None, + is_current: false, + is_in_config: false, + is_saved: false, + is_default_model: false, + primary_model_id: None, + default_model_id: None, + } + } + + /// 构造仅含给定 providers 的 UiData + fn make_ui_data_with_providers(providers: &[(&str, &str)]) -> UiData { + let mut data = UiData::default(); + data.providers.rows = providers + .iter() + .map(|(id, name)| make_provider_row(id, name)) + .collect(); + data + } + + #[test] + fn collect_aggregates_recent_tokens_from_samples() { + let data = make_ui_data_with_providers(&[("p1", "DeepSeek"), ("p2", "Minimax")]); + let mut samples = HashMap::new(); + samples.insert("p1".to_string(), vec![100, 200, 300]); // sum = 600 + samples.insert("p2".to_string(), vec![50, 50, 50]); // sum = 150 + + let result = collect_route_hits_for_dashboard(&data, &samples); + assert_eq!(result.len(), 2); + // recent_tokens 降序:p1 在前 + assert_eq!(result[0].provider_id, "p1"); + assert_eq!(result[0].recent_tokens, 600); + assert_eq!(result[1].provider_id, "p2"); + assert_eq!(result[1].recent_tokens, 150); + assert_eq!(result[0].display_name, "DeepSeek"); + assert_eq!(result[1].display_name, "Minimax"); + } + + #[test] + fn collect_returns_empty_when_no_samples_and_no_enabled_routes() { + let data = UiData::default(); + let samples = HashMap::new(); + let result = collect_route_hits_for_dashboard(&data, &samples); + assert!( + result.is_empty(), + "expected empty Vec, got {} entries", + result.len() + ); + } + + #[test] + fn collect_unions_samples_with_model_routes_enabled_providers() { + let mut data = + make_ui_data_with_providers(&[("p_routed", "Routed"), ("p_direct", "Direct")]); + // model_routes 含一个 enabled route 指向 p_routed(无 samples,近期无流量) + data.model_routes.rows.push(ModelRouteRow { + id: "r1".to_string(), + pattern: "*".to_string(), + provider_id: "p_routed".to_string(), + provider_name: "Routed".to_string(), + priority: 0, + enabled: true, + hit_count: 999, // 历史命中不应影响 recent_tokens 口径 + last_hit_at: None, + }); + // samples 含 p_direct(直接切换,无 route) + let mut samples = HashMap::new(); + samples.insert("p_direct".to_string(), vec![400, 400]); // sum = 800 + + let result = collect_route_hits_for_dashboard(&data, &samples); + let ids: Vec<&str> = result.iter().map(|h| h.provider_id.as_str()).collect(); + assert!(ids.contains(&"p_direct"), "p_direct should be in union"); + assert!( + ids.contains(&"p_routed"), + "p_routed should be in union via model_routes" + ); + // recent_tokens 降序:p_direct(800) 在前,p_routed(0) 在后 + assert_eq!(result[0].provider_id, "p_direct"); + assert_eq!(result[1].provider_id, "p_routed"); + assert_eq!(result[1].recent_tokens, 0); + } + + #[test] + fn color_stacks_keep_multiple_providers_in_same_column() { + let mut samples = HashMap::new(); + samples.insert("p1".to_string(), vec![90]); + samples.insert("p2".to_string(), vec![10]); + + let p1 = Color::Rgb(255, 0, 0); + let p2 = Color::Rgb(0, 255, 0); + let colors = HashMap::from([("p1".to_string(), p1), ("p2".to_string(), p2)]); + + // 点阵画满 4 行:dominant(p1) 占底部,minor(p2) 占顶部字符行。 + let stacks = compute_column_color_stacks(samples.iter(), 1, &colors, 4, &[4]); + + assert_eq!(stacks.len(), 1); + assert_eq!(stacks[0].len(), 4); + assert!( + stacks[0].contains(&Some(p1)), + "dominant provider should be present" + ); + assert!( + stacks[0].contains(&Some(p2)), + "smaller provider should still be visible in the same column" + ); + } + + #[test] + fn color_stacks_allow_single_provider_to_fill_column() { + let mut samples = HashMap::new(); + samples.insert("p1".to_string(), vec![100]); + samples.insert("p2".to_string(), vec![0]); + + let p1 = Color::Rgb(255, 0, 0); + let p2 = Color::Rgb(0, 255, 0); + let colors = HashMap::from([("p1".to_string(), p1), ("p2".to_string(), p2)]); + + let stacks = compute_column_color_stacks(samples.iter(), 1, &colors, 3, &[3]); + + assert_eq!(stacks[0], vec![Some(p1), Some(p1), Some(p1)]); + } + + #[test] + fn color_stacks_only_fill_rendered_rows() { + // Regression: 点阵只画 2 行(filled=2),stack_height=4。颜色必须只填 + // 点阵字符所在的 [2, 4) 区间,minor(p2) 在顶部字符行(base=2),dominant(p1) + // 在底部,[0, 2) 的空白行保持 None,避免图例颜色与点阵颜色对不上。 + let mut samples = HashMap::new(); + samples.insert("p1".to_string(), vec![90]); + samples.insert("p2".to_string(), vec![10]); + + let p1 = Color::Rgb(255, 0, 0); + let p2 = Color::Rgb(0, 255, 0); + let colors = HashMap::from([("p1".to_string(), p1), ("p2".to_string(), p2)]); + + let stacks = compute_column_color_stacks(samples.iter(), 1, &colors, 4, &[2]); + + assert_eq!( + stacks[0], + vec![None, None, Some(p2), Some(p1)], + "colors must occupy only the rendered [base, stack_height) rows" + ); + } +} diff --git a/src-tauri/src/cli/tui/ui/model_routes.rs b/src-tauri/src/cli/tui/ui/model_routes.rs new file mode 100644 index 00000000..b17b3202 --- /dev/null +++ b/src-tauri/src/cli/tui/ui/model_routes.rs @@ -0,0 +1,91 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Modifier, Style}, + widgets::{Block, BorderType, Borders, Cell, Row, Table, TableState}, + Frame, +}; + +use crate::cli::i18n::texts; + +use super::{ + app::{App, Focus}, + shared::{ + highlight_symbol, inset_left, pane_border_style, render_key_bar_center, selection_style, + CONTENT_INSET_LEFT, + }, + theme::Theme, +}; + +use crate::cli::tui::data::UiData; + +pub(super) fn render_settings_model_routes( + frame: &mut Frame<'_>, + app: &App, + data: &UiData, + area: Rect, + theme: &Theme, +) { + let title = texts::tui_settings_model_routes_title(); + + let header_cells = vec![ + Cell::from("Pattern"), + Cell::from("Provider"), + Cell::from("Priority"), + Cell::from("Enabled"), + ]; + let header = + Row::new(header_cells).style(Style::default().fg(theme.dim).add_modifier(Modifier::BOLD)); + + let rows = data.model_routes.rows.iter().map(|r| { + Row::new(vec![ + Cell::from(r.pattern.clone()), + Cell::from(r.provider_name.clone()), + Cell::from(r.priority.to_string()), + Cell::from(if r.enabled { "Yes" } else { "No" }), + ]) + }); + + let constraints = vec![ + Constraint::Percentage(30), + Constraint::Percentage(35), + Constraint::Length(10), + Constraint::Length(8), + ]; + + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(title); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(inner); + + if app.focus == Focus::Content { + let selected = data.model_routes.rows.get(app.model_routes_idx); + let mut key_items: Vec<(&str, &str)> = vec![ + ("a", texts::tui_key_add()), + ("Space", texts::tui_key_toggle()), + ]; + if selected.is_some() { + key_items.push(("e", texts::tui_key_edit())); + key_items.push(("d", texts::tui_key_delete())); + }; + key_items.push(("\u{2191}\u{2193}", texts::tui_key_move())); + render_key_bar_center(frame, chunks[0], theme, &key_items); + } + + let table = Table::new(rows, constraints) + .header(header) + .block(Block::default().borders(Borders::NONE)) + .row_highlight_style(selection_style(theme)) + .highlight_symbol(highlight_symbol(theme)); + + let mut state = TableState::default(); + state.select(Some(app.model_routes_idx)); + frame.render_stateful_widget(table, inset_left(chunks[1], CONTENT_INSET_LEFT), &mut state); +} diff --git a/src-tauri/src/cli/tui/ui/overlay/basic.rs b/src-tauri/src/cli/tui/ui/overlay/basic.rs index b1764733..abb12945 100644 --- a/src-tauri/src/cli/tui/ui/overlay/basic.rs +++ b/src-tauri/src/cli/tui/ui/overlay/basic.rs @@ -270,3 +270,70 @@ fn render_scrolling_lines(frame: &mut Frame<'_>, area: Rect, lines: &[String], s frame.render_widget(Paragraph::new(shown).wrap(Wrap { trim: false }), area); } + +pub(super) fn render_model_route_provider_picker( + frame: &mut Frame<'_>, + data: &UiData, + content_area: Rect, + theme: &theme::Theme, + selected: usize, +) { + use crate::app_config::AppType; + use crate::cli::tui::data::provider_display_name; + use ratatui::widgets::{Clear, List, ListItem, ListState}; + use unicode_width::UnicodeWidthStr; + + let area = centered_rect(OVERLAY_LG.0, OVERLAY_LG.1, content_area); + frame.render_widget(Clear, area); + + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(overlay_border_style(theme, false)) + .title(crate::t!("Select provider", "选择供应商")); + let inner = block.inner(area); + frame.render_widget(block, area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(inner); + + render_key_bar_center( + frame, + chunks[0], + theme, + &[ + ("Enter", crate::t!("Confirm", "确认")), + ("Esc", crate::t!("Back", "返回")), + ], + ); + + let body_area = inset_top(chunks[1], 1); + let items: Vec = data + .providers + .rows + .iter() + .map(|row| { + let display = provider_display_name(&AppType::Claude, row); + let extra = if row.is_current { " (*)" } else { "" }; + let mut text = format!("{display}{extra}"); + if UnicodeWidthStr::width(text.as_str()) as u16 > body_area.width.saturating_sub(2) { + text = text + .chars() + .take(body_area.width.saturating_sub(5) as usize) + .collect::() + + "…"; + } + ListItem::new(Line::from(Span::raw(text))) + }) + .collect(); + + let list = List::new(items) + .highlight_style(selection_style(theme)) + .highlight_symbol(highlight_symbol(theme)); + + let mut state = ListState::default(); + state.select(Some(selected)); + frame.render_stateful_widget(list, body_area, &mut state); +} diff --git a/src-tauri/src/cli/tui/ui/overlay/frame.rs b/src-tauri/src/cli/tui/ui/overlay/frame.rs index 0392c776..99377a32 100644 --- a/src-tauri/src/cli/tui/ui/overlay/frame.rs +++ b/src-tauri/src/cli/tui/ui/overlay/frame.rs @@ -82,7 +82,7 @@ pub(super) fn overlay_frame_at( .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(border) - .title(format!(" {} ", icons::strip_icon(title))); + .title(format!(" {} ", title)); frame.render_widget(block.clone(), area); let inner = block.inner(area); diff --git a/src-tauri/src/cli/tui/ui/overlay/render.rs b/src-tauri/src/cli/tui/ui/overlay/render.rs index 69dd6365..abadeb2a 100644 --- a/src-tauri/src/cli/tui/ui/overlay/render.rs +++ b/src-tauri/src/cli/tui/ui/overlay/render.rs @@ -21,6 +21,15 @@ pub(crate) fn render_overlay( Overlay::BackupPicker { selected } => { super::basic::render_backup_picker_overlay(frame, data, content_area, theme, *selected) } + Overlay::ModelRouteProviderPicker { selected, .. } => { + super::basic::render_model_route_provider_picker( + frame, + data, + content_area, + theme, + *selected, + ) + } Overlay::TextView(view) => super::basic::render_text_view_overlay( frame, content_area, diff --git a/src-tauri/src/cli/tui/ui/proxy_wave.rs b/src-tauri/src/cli/tui/ui/proxy_wave.rs index 8664c429..5fd4f4ac 100644 --- a/src-tauri/src/cli/tui/ui/proxy_wave.rs +++ b/src-tauri/src/cli/tui/ui/proxy_wave.rs @@ -57,7 +57,7 @@ pub(super) fn proxy_wave_lines( rows } -fn recent_samples(width: usize, current_app_routed: bool, samples: &[u64]) -> Vec { +pub(super) fn recent_samples(width: usize, current_app_routed: bool, samples: &[u64]) -> Vec { if !current_app_routed { return vec![0; width]; } @@ -73,7 +73,7 @@ fn recent_samples(width: usize, current_app_routed: bool, samples: &[u64]) -> Ve out } -fn scale_samples(height: u16, samples: &[u64], show_idle_baseline: bool) -> Vec { +pub(super) fn scale_samples(height: u16, samples: &[u64], show_idle_baseline: bool) -> Vec { let baseline = if show_idle_baseline { 1 } else { 0 }; let max = samples.iter().copied().max().unwrap_or(0); if max == 0 { diff --git a/src-tauri/src/cli/tui/ui/sessions.rs b/src-tauri/src/cli/tui/ui/sessions.rs index 64adc8d1..27f0b0b0 100644 --- a/src-tauri/src/cli/tui/ui/sessions.rs +++ b/src-tauri/src/cli/tui/ui/sessions.rs @@ -5,7 +5,7 @@ use super::*; pub(super) fn render_sessions( frame: &mut Frame<'_>, app: &App, - data: &UiData, + _data: &UiData, area: Rect, theme: &super::theme::Theme, ) { @@ -21,14 +21,22 @@ pub(super) fn render_sessions( &app.sessions.deep_search_results, ); - // Pane/list navigation is handled globally (arrows, h/l, Tab), so it is - // hinted here as a static prefix; the action chips are generated from the - // same table the handler dispatches through. - let mut keys = vec![ + let keys = [ ("↑↓", texts::tui_key_select()), ("←→/h/l", texts::tui_key_pane()), + ("Enter", texts::tui_key_view()), + ("R", texts::tui_key_restore()), + ("d", texts::tui_key_delete()), + ("r", texts::tui_key_refresh()), + ( + "a", + if app.sessions.show_all_providers { + texts::tui_key_sessions_all_active() + } else { + texts::tui_key_sessions_all() + }, + ), ]; - keys.extend(crate::cli::tui::keymap::sessions::key_bar_items(app, data)); let summary = if app.sessions.loading && !app.sessions.loaded_once { texts::tui_sessions_loading_summary().to_string() } else if app.sessions.deep_search_active.is_some() { @@ -74,10 +82,7 @@ fn render_session_list( .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(session_pane_border_style(app, SessionsPane::List, theme)) - .title(format!( - " {} ", - icons::strip_icon(texts::menu_manage_sessions()) - )); + .title(format!(" {} ", texts::menu_manage_sessions())); frame.render_widget(block.clone(), area); let inner = block.inner(area); diff --git a/src-tauri/src/cli/tui/ui/shared.rs b/src-tauri/src/cli/tui/ui/shared.rs index 68130280..16d04145 100644 --- a/src-tauri/src/cli/tui/ui/shared.rs +++ b/src-tauri/src/cli/tui/ui/shared.rs @@ -123,7 +123,7 @@ pub(super) fn render_page_frame( .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(pane_border_style(app, Focus::Content, theme)) - .title(format!(" {} ", icons::strip_icon(title))); + .title(format!(" {} ", title)); frame.render_widget(outer.clone(), area); let inner = outer.inner(area); @@ -152,13 +152,7 @@ pub(super) fn render_page_frame( /// Sub-page titles show their place in the hierarchy (" Usage › Details ") /// so nesting depth stays visible and Esc's destination is predictable. pub(super) fn breadcrumb_title(segments: &[&str]) -> String { - format!(" {} ", breadcrumb_path(segments)) -} - -/// Breadcrumb path without the surrounding padding that `breadcrumb_title` -/// adds. Use with `render_page_frame`, which wraps the title itself. -pub(super) fn breadcrumb_path(segments: &[&str]) -> String { - segments.join(" › ") + format!(" {} ", segments.join(" › ")) } /// Centered guidance for empty list screens: a bold title, a muted diff --git a/src-tauri/src/cli/tui/ui/tests.rs b/src-tauri/src/cli/tui/ui/tests.rs index a12f2373..5b36f7f2 100644 --- a/src-tauri/src/cli/tui/ui/tests.rs +++ b/src-tauri/src/cli/tui/ui/tests.rs @@ -23,7 +23,7 @@ use crate::{ Focus, Overlay, TextInputState, TextSubmit, UsagePane, }, data::{ - ConfigSnapshot, McpSnapshot, ModelPricingRow, ModelPricingSnapshot, + ConfigSnapshot, McpSnapshot, ModelPricingRow, ModelPricingSnapshot, ModelRouteSnapshot, OpenClawWorkspaceSnapshot, PromptsSnapshot, ProviderRow, ProvidersSnapshot, ProxySnapshot, SkillsSnapshot, UiData, UsageLogRow, UsageProviderStatsRow, UsageRangePreset, UsageSnapshot, UsageSummarySnapshot, UsageTrendBucket, @@ -1582,6 +1582,7 @@ pub(super) fn minimal_data(_app_type: &AppType) -> UiData { usage: UsageSnapshot::default(), pricing: Default::default(), quota: Default::default(), + model_routes: ModelRouteSnapshot::default(), reload_token: Default::default(), } } @@ -2651,9 +2652,6 @@ fn header_keeps_proxy_visible_and_truncates_long_provider_name() { fn nav_icons_have_left_padding_from_border() { let _lock = lock_env(); let _no_color = EnvGuard::remove("NO_COLOR"); - // This test is specifically about the emoji icon column, so pin emoji - // mode rather than depend on the ambient locale (auto-detection). - let _icons = EnvGuard::set("CC_SWITCH_ICONS", "emoji"); let app = App::new(Some(AppType::Claude)); let data = minimal_data(&app.app_type); @@ -4159,37 +4157,13 @@ fn mcp_page_uses_import_existing_label() { assert!(all.contains(texts::tui_mcp_action_import_existing())); } -fn global_help_text(app_type: AppType) -> String { - let app = App::new(Some(app_type)); - let data = minimal_data(&app.app_type); - crate::cli::tui::help::context_help_for_app(&app, &data) - .lines - .join("\n") -} - #[test] fn help_text_mentions_import_existing_for_mcp() { - let _lock = lock_env(); - let _lang = use_test_language(Language::English); - // MCP and Skills both generate their import line from the same label - // (tui_*_action_import_existing); assert the wording on BOTH lines so a - // regression on either page is caught, not just its presence somewhere. - let help = global_help_text(AppType::Claude); - let mcp_line = help - .lines() - .find(|line| line.starts_with("- MCP:")) - .unwrap_or_else(|| panic!("no MCP help line: {help}")); - let skills_line = help - .lines() - .find(|line| line.starts_with("- Skills:")) - .unwrap_or_else(|| panic!("no Skills help line: {help}")); - assert!( - mcp_line.contains("i Import Existing"), - "MCP help line missing shared import wording: {mcp_line}" - ); + let help = texts::tui_help_text(); + assert!( - skills_line.contains("i Import Existing"), - "Skills help line missing shared import wording: {skills_line}" + help.contains("i import existing") || help.contains("i 导入已有"), + "help text should use the same import wording for MCP and Skills" ); } @@ -4199,7 +4173,7 @@ fn help_text_shows_space_as_the_mcp_toggle_key() { for lang in [Language::English, Language::Chinese] { let _lang = use_test_language(lang); for app_type in [AppType::Claude, AppType::Hermes] { - let help = global_help_text(app_type.clone()); + let help = texts::tui_help_text_for_app(&app_type); // The MCP toggle handler listens for Space (content_entities.rs); // the help sheet used to claim `x` here. assert!( @@ -4214,34 +4188,6 @@ fn help_text_shows_space_as_the_mcp_toggle_key() { } } -#[test] -fn generated_help_swaps_prompts_for_memory_on_hermes() { - let _lock = lock_env(); - let _lang = use_test_language(Language::English); - - let claude = global_help_text(AppType::Claude); - assert!(claude.contains("- Prompts: "), "{claude}"); - assert!(!claude.contains("- Memory:"), "{claude}"); - assert!(claude.contains("- Config: "), "{claude}"); - - let hermes = global_help_text(AppType::Hermes); - assert!(hermes.contains("- Memory: "), "{hermes}"); - assert!(!hermes.contains("- Prompts:"), "{hermes}"); - // Hermes has no Config route, so no Config line in its help sheet. - assert!(!hermes.contains("- Config:"), "{hermes}"); -} - -#[test] -fn generated_usage_help_omits_the_hidden_reverse_tab_alias() { - let _lock = lock_env(); - let _lang = use_test_language(Language::English); - let help = global_help_text(AppType::Claude); - // Shift+Tab (reverse metric) is a never-shown alias, so it is skipped in - // the generated help even though Tab (forward) is listed. - assert!(help.contains("Tab switch metric"), "{help}"); - assert!(!help.contains("Shift+Tab"), "{help}"); -} - #[test] fn page_key_bar_stays_visible_while_nav_has_focus() { let _lock = lock_env(); @@ -4268,40 +4214,6 @@ fn page_key_bar_stays_visible_while_nav_has_focus() { ); } -#[test] -fn nav_drops_emoji_icons_in_ascii_mode() { - let _lock = lock_env(); - let _lang = use_test_language(Language::English); - let _no_color = EnvGuard::set("NO_COLOR", "1"); - let app = App::new(Some(AppType::Claude)); - let data = minimal_data(&app.app_type); - - let _emoji = EnvGuard::set("CC_SWITCH_ICONS", "emoji"); - let emoji_view = all_text(&render(&app, &data)); - assert!( - emoji_view.contains('🔑'), - "emoji mode should render the nav provider icon: {emoji_view}" - ); - assert!( - emoji_view.contains('🎯'), - "emoji mode should render the home title icon: {emoji_view}" - ); - - let _ascii = EnvGuard::set("CC_SWITCH_ICONS", "ascii"); - let ascii_view = all_text(&render(&app, &data)); - assert!( - !ascii_view.contains('🔑'), - "ascii mode should drop the nav emoji: {ascii_view}" - ); - assert!( - !ascii_view.contains('🎯'), - "ascii mode should drop the home title emoji: {ascii_view}" - ); - // The label text still renders — only the decorative glyph is gone. - assert!(ascii_view.contains("Providers"), "{ascii_view}"); - assert!(ascii_view.contains("CC-Switch"), "{ascii_view}"); -} - #[test] fn empty_lists_show_guidance_instead_of_blank_space() { let _lock = lock_env(); diff --git a/src-tauri/src/database/dao/mod.rs b/src-tauri/src/database/dao/mod.rs index ab7e2742..a076ed88 100644 --- a/src-tauri/src/database/dao/mod.rs +++ b/src-tauri/src/database/dao/mod.rs @@ -5,6 +5,7 @@ pub mod failover; pub mod mcp; pub mod model_pricing; +pub mod model_routes; pub mod prompts; pub mod providers; pub mod providers_seed; diff --git a/src-tauri/src/database/dao/model_routes.rs b/src-tauri/src/database/dao/model_routes.rs new file mode 100644 index 00000000..e6f04fc7 --- /dev/null +++ b/src-tauri/src/database/dao/model_routes.rs @@ -0,0 +1,473 @@ +//! 模型路由 DAO (Model Route Data Access Object) +//! +//! 管理 model_routes 表的 CRUD 操作,为 per-model provider routing 提供持久化层。 +//! 支持按 app_type 列出路由、创建/更新/删除路由、切换启用状态、记录命中统计。 +//! id 使用 UUID v4 (TEXT PRIMARY KEY),与上游 cc-switch 一致。 + +use crate::database::{lock_conn, Database}; +use crate::error::AppError; +use crate::model_route::ModelRoute; + +const SELECT_COLS: &str = "id, app_type, pattern, provider_id, priority, enabled, hit_count, last_hit_at, created_at, updated_at"; + +impl Database { + /// 列出指定 app_type 的所有模型路由,按 priority ASC, created_at ASC 排序 + pub fn list_model_routes(&self, app_type: &str) -> Result, AppError> { + let conn = lock_conn!(self.conn); + + let mut stmt = conn + .prepare(&format!( + "SELECT {SELECT_COLS} FROM model_routes WHERE app_type = ?1 ORDER BY priority ASC, created_at ASC" + )) + .map_err(|e| AppError::Database(e.to_string()))?; + + let items = stmt + .query_map([app_type], |row| Ok(row_to_route(row))) + .map_err(|e| AppError::Database(e.to_string()))? + .collect::, _>>() + .map_err(|e| AppError::Database(e.to_string()))?; + + Ok(items) + } + + /// 根据 ID 获取单个模型路由 + pub fn get_model_route(&self, id: &str) -> Result, AppError> { + let conn = lock_conn!(self.conn); + + let mut stmt = conn + .prepare(&format!( + "SELECT {SELECT_COLS} FROM model_routes WHERE id = ?1" + )) + .map_err(|e| AppError::Database(e.to_string()))?; + + let mut rows = stmt + .query_map([id], |row| Ok(row_to_route(row))) + .map_err(|e| AppError::Database(e.to_string()))?; + + rows.next() + .transpose() + .map_err(|e| AppError::Database(e.to_string())) + } + + /// 创建模型路由(生成 UUID id,验证 provider_id 存在) + pub fn create_model_route(&self, route: &ModelRoute) -> Result { + let conn = lock_conn!(self.conn); + + let provider_exists: bool = conn + .query_row( + "SELECT COUNT(*) > 0 FROM providers WHERE id = ?1 AND app_type = ?2", + rusqlite::params![&route.provider_id, &route.app_type], + |row| row.get(0), + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + if !provider_exists { + return Err(AppError::Database(format!( + "provider '{}' not found for app '{}'", + route.provider_id, route.app_type + ))); + } + + let id = if route.id.is_empty() { + uuid::Uuid::new_v4().to_string() + } else { + route.id.clone() + }; + + let mut stmt = conn + .prepare(&format!( + "INSERT INTO model_routes (id, app_type, pattern, provider_id, priority, enabled) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + RETURNING {SELECT_COLS}" + )) + .map_err(|e| AppError::Database(e.to_string()))?; + + stmt.query_row( + rusqlite::params![ + &id, + &route.app_type, + &route.pattern, + &route.provider_id, + route.priority, + route.enabled as i32, + ], + |row| Ok(row_to_route(row)), + ) + .map_err(|e| AppError::Database(e.to_string())) + } + + /// 更新模型路由 + pub fn update_model_route(&self, id: &str, route: &ModelRoute) -> Result { + let conn = lock_conn!(self.conn); + + let current_provider: String = conn + .query_row( + "SELECT provider_id FROM model_routes WHERE id = ?1", + [id], + |row| row.get(0), + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + if route.provider_id != current_provider { + let provider_exists: bool = conn + .query_row( + "SELECT COUNT(*) > 0 FROM providers WHERE id = ?1 AND app_type = ?2", + rusqlite::params![&route.provider_id, &route.app_type], + |row| row.get(0), + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + if !provider_exists { + return Err(AppError::Database(format!( + "provider '{}' not found for app '{}'", + route.provider_id, route.app_type + ))); + } + } + + let mut stmt = conn + .prepare(&format!( + "UPDATE model_routes SET + pattern = ?1, provider_id = ?2, priority = ?3, enabled = ?4, + updated_at = datetime('now') + WHERE id = ?5 + RETURNING {SELECT_COLS}" + )) + .map_err(|e| AppError::Database(e.to_string()))?; + + stmt.query_row( + rusqlite::params![ + &route.pattern, + &route.provider_id, + route.priority, + route.enabled as i32, + id, + ], + |row| Ok(row_to_route(row)), + ) + .map_err(|e| AppError::Database(e.to_string())) + } + + /// 删除模型路由 + pub fn delete_model_route(&self, id: &str) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + let changes = conn + .execute("DELETE FROM model_routes WHERE id = ?1", [id]) + .map_err(|e| AppError::Database(e.to_string()))?; + + if changes == 0 { + return Err(AppError::Database("model_route not found".to_string())); + } + + Ok(()) + } + + /// 切换模型路由的启用状态 + pub fn toggle_model_route(&self, id: &str) -> Result { + let conn = lock_conn!(self.conn); + + let mut stmt = conn + .prepare(&format!( + "UPDATE model_routes SET + enabled = NOT enabled, + updated_at = datetime('now') + WHERE id = ?1 + RETURNING {SELECT_COLS}" + )) + .map_err(|e| AppError::Database(e.to_string()))?; + + stmt.query_row([id], |row| Ok(row_to_route(row))) + .map_err(|e| AppError::Database(e.to_string())) + } + + /// 记录一次命中(增加 hit_count 并更新 last_hit_at) + /// 使用 UPDATE 而非事务,性能更好;last_hit_at 只在每次调用时更新(不频繁) + pub fn record_model_route_hit(&self, id: &str) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + let changes = conn + .execute( + "UPDATE model_routes SET + hit_count = hit_count + 1, + last_hit_at = datetime('now') + WHERE id = ?1", + [id], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + if changes == 0 { + return Err(AppError::Database("model_route not found".to_string())); + } + + Ok(()) + } + + /// 获取所有启用的 model_routes(按 app_type + provider_id 聚合用于仪表盘) + /// 返回 (app_type, provider_id, total_hits) 列表 + pub fn aggregate_route_hits_by_provider(&self) -> Result, AppError> { + let conn = lock_conn!(self.conn); + + let mut stmt = conn + .prepare( + "SELECT app_type, provider_id, SUM(hit_count) as total + FROM model_routes + WHERE enabled = 1 + GROUP BY app_type, provider_id + ORDER BY total DESC", + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + let rows = stmt + .query_map([], |row| { + Ok((row.get(0)?, row.get(1)?, row.get::<_, i64>(2)?)) + }) + .map_err(|e| AppError::Database(e.to_string()))? + .collect::, _>>() + .map_err(|e| AppError::Database(e.to_string()))?; + + Ok(rows) + } +} + +fn row_to_route(row: &rusqlite::Row) -> ModelRoute { + ModelRoute { + id: row.get(0).expect("id"), + app_type: row.get(1).expect("app_type"), + pattern: row.get(2).expect("pattern"), + provider_id: row.get(3).expect("provider_id"), + priority: row.get(4).expect("priority"), + enabled: row.get::<_, i32>(5).expect("enabled") != 0, + hit_count: row.get(6).expect("hit_count"), + last_hit_at: row.get(7).expect("last_hit_at"), + created_at: row.get(8).expect("created_at"), + updated_at: row.get(9).expect("updated_at"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn seed_provider(db: &Database, app_type: &str, id: &str) -> Result<(), AppError> { + let conn = lock_conn!(db.conn); + conn.execute( + "INSERT INTO providers (id, app_type, name, settings_config, meta) + VALUES (?1, ?2, ?3, '{}', '{}')", + rusqlite::params![id, app_type, id], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) + } + + fn test_route(pattern: &str, provider_id: &str, priority: i32) -> ModelRoute { + ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: pattern.into(), + provider_id: provider_id.into(), + priority, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + } + } + + #[test] + fn create_and_get_model_route_roundtrip() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "test-prov")?; + + let created = db.create_model_route(&test_route("*-sonnet", "test-prov", 10))?; + + assert_eq!(created.id.len(), 36); + assert_eq!(created.pattern, "*-sonnet"); + assert_eq!(created.provider_id, "test-prov"); + assert_eq!(created.priority, 10); + assert!(created.enabled); + assert_eq!(created.hit_count, 0); + assert!(created.created_at.is_some()); + + let got = db.get_model_route(&created.id)?; + assert!(got.is_some()); + assert_eq!(got.unwrap().pattern, "*-sonnet"); + + Ok(()) + } + + #[test] + fn create_model_route_rejects_invalid_provider() -> Result<(), AppError> { + let db = Database::memory()?; + + let result = db.create_model_route(&test_route("*-sonnet", "nonexistent", 10)); + assert!(result.is_err()); + + let err = result.unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("provider") && msg.contains("not found"), + "expected provider not found error, got: {msg}" + ); + + Ok(()) + } + + #[test] + fn list_model_routes_ordered_by_priority() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "p1")?; + + let r1 = db.create_model_route(&test_route("mid", "p1", 5))?; + let r2 = db.create_model_route(&test_route("low", "p1", 1))?; + let r3 = db.create_model_route(&test_route("high", "p1", 3))?; + + let routes = db.list_model_routes("claude")?; + assert_eq!(routes.len(), 3); + assert_eq!(routes[0].id, r2.id); + assert_eq!(routes[0].priority, 1); + assert_eq!(routes[1].id, r3.id); + assert_eq!(routes[1].priority, 3); + assert_eq!(routes[2].id, r1.id); + assert_eq!(routes[2].priority, 5); + + Ok(()) + } + + #[test] + fn update_model_route_modifies_fields() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "p1")?; + seed_provider(&db, "claude", "p2")?; + + let created = db.create_model_route(&test_route("*-sonnet", "p1", 10))?; + + let updated = db.update_model_route( + &created.id, + &ModelRoute { + id: created.id.clone(), + app_type: "claude".into(), + pattern: "claude-*".into(), + provider_id: "p2".into(), + priority: 5, + enabled: false, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }, + )?; + + assert_eq!(updated.pattern, "claude-*"); + assert_eq!(updated.provider_id, "p2"); + assert_eq!(updated.priority, 5); + assert!(!updated.enabled); + + let got = db.get_model_route(&created.id)?; + assert!(got.is_some()); + let got = got.unwrap(); + assert_eq!(got.pattern, "claude-*"); + assert!(!got.enabled); + + Ok(()) + } + + #[test] + fn toggle_model_route_flips_enabled() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "p1")?; + + let created = db.create_model_route(&test_route("*-sonnet", "p1", 10))?; + assert!(created.enabled); + + let toggled_off = db.toggle_model_route(&created.id)?; + assert!(!toggled_off.enabled); + + let toggled_on = db.toggle_model_route(&created.id)?; + assert!(toggled_on.enabled); + + Ok(()) + } + + #[test] + fn delete_model_route_removes_row() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "p1")?; + + let created = db.create_model_route(&test_route("*-sonnet", "p1", 10))?; + + db.delete_model_route(&created.id)?; + + let got = db.get_model_route(&created.id)?; + assert!(got.is_none()); + + let result = db.delete_model_route("nonexistent-id"); + assert!(result.is_err()); + + Ok(()) + } + + #[test] + fn record_model_route_hit_increments_count() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "p1")?; + + let created = db.create_model_route(&test_route("*-sonnet", "p1", 10))?; + assert_eq!(created.hit_count, 0); + + db.record_model_route_hit(&created.id)?; + db.record_model_route_hit(&created.id)?; + db.record_model_route_hit(&created.id)?; + + let got = db.get_model_route(&created.id)?.unwrap(); + assert_eq!(got.hit_count, 3); + assert!(got.last_hit_at.is_some()); + + Ok(()) + } + + #[test] + fn aggregate_route_hits_by_provider_groups_correctly() -> Result<(), AppError> { + let db = Database::memory()?; + seed_provider(&db, "claude", "p1")?; + seed_provider(&db, "claude", "p2")?; + seed_provider(&db, "codex", "cx1")?; + + let r1 = db.create_model_route(&test_route("*sonnet*", "p1", 1))?; + let r2 = db.create_model_route(&test_route("*opus*", "p2", 2))?; + let mut codex_route = test_route("*codex*", "cx1", 1); + codex_route.app_type = "codex".to_string(); + let r3 = db.create_model_route(&codex_route)?; + let _r4 = db.create_model_route(&test_route("disabled", "p1", 5))?; + + // r4 is disabled + db.toggle_model_route( + &db.list_model_routes("claude")? + .iter() + .find(|r| r.pattern == "disabled") + .unwrap() + .id, + )?; + + // 5 hits to claude/p1, 3 to claude/p2, 2 to codex/cx1 + for _ in 0..5 { + db.record_model_route_hit(&r1.id)?; + } + for _ in 0..3 { + db.record_model_route_hit(&r2.id)?; + } + for _ in 0..2 { + db.record_model_route_hit(&r3.id)?; + } + + let agg = db.aggregate_route_hits_by_provider()?; + // r4 was disabled but got 0 hits, so it should be filtered out + assert_eq!(agg.len(), 3); + assert_eq!(agg[0], ("claude".to_string(), "p1".to_string(), 5)); + assert_eq!(agg[1], ("claude".to_string(), "p2".to_string(), 3)); + assert_eq!(agg[2], ("codex".to_string(), "cx1".to_string(), 2)); + + Ok(()) + } +} diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 2bbae262..ab758e82 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -59,7 +59,7 @@ static DATABASE_PERMISSION_CHECK: Once = Once::new(); /// 当前 Schema 版本号 /// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑 -pub(crate) const SCHEMA_VERSION: i32 = 11; +pub(crate) const SCHEMA_VERSION: i32 = 12; fn database_open_flags() -> OpenFlags { OpenFlags::SQLITE_OPEN_READ_WRITE diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 62b8be29..5e9a2d98 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -278,6 +278,8 @@ impl Database { ) .map_err(|e| AppError::Database(e.to_string()))?; + Self::create_model_routes_table(conn)?; + // 尝试添加 live_takeover_active 列到 proxy_config 表 let _ = conn.execute( "ALTER TABLE proxy_config ADD COLUMN live_takeover_active INTEGER NOT NULL DEFAULT 0", @@ -427,6 +429,11 @@ impl Database { Self::migrate_v10_to_v11(conn)?; Self::set_user_version(conn, 11)?; } + 11 => { + log::info!("迁移数据库从 v11 到 v12(添加模型路由表和命中统计字段)"); + Self::migrate_v11_to_v12(conn)?; + Self::set_user_version(conn, 12)?; + } _ => { return Err(AppError::Database(format!( "未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}" @@ -436,6 +443,7 @@ impl Database { version = Self::get_user_version(conn)?; } Self::repair_proxy_request_logs_columns(conn)?; + Self::repair_usage_daily_rollups_columns(conn)?; Self::create_request_logs_indexes_if_supported(conn)?; Self::normalize_auto_failover_requires_takeover(conn)?; Ok(()) @@ -534,6 +542,17 @@ impl Database { "BOOLEAN NOT NULL DEFAULT 0", )?; + // model_routes 统计字段(cc-switch v12 未含,留作向后兼容 + 命中追踪) + if Self::table_exists(conn, "model_routes")? { + Self::add_column_if_missing( + conn, + "model_routes", + "hit_count", + "INTEGER NOT NULL DEFAULT 0", + )?; + Self::add_column_if_missing(conn, "model_routes", "last_hit_at", "TEXT")?; + } + // 添加代理超时配置字段 if Self::table_exists(conn, "proxy_config")? { // 兼容旧版本缺失的基础字段 @@ -1347,6 +1366,56 @@ impl Database { Ok(()) } + /// v11 -> v12 迁移:添加模型路由表和命中统计字段。 + fn migrate_v11_to_v12(conn: &Connection) -> Result<(), AppError> { + Self::create_model_routes_table(conn)?; + log::info!("v11 -> v12 迁移完成:已添加模型路由表和命中统计字段"); + Ok(()) + } + + fn create_model_routes_table(conn: &Connection) -> Result<(), AppError> { + conn.execute( + "CREATE TABLE IF NOT EXISTS model_routes ( + id TEXT PRIMARY KEY, + app_type TEXT NOT NULL, + pattern TEXT NOT NULL, + provider_id TEXT NOT NULL, + priority INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + hit_count INTEGER NOT NULL DEFAULT 0, + last_hit_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE + )", + [], + ) + .map_err(|e| AppError::Database(format!("创建 model_routes 表失败: {e}")))?; + + Self::add_column_if_missing( + conn, + "model_routes", + "hit_count", + "INTEGER NOT NULL DEFAULT 0", + )?; + Self::add_column_if_missing(conn, "model_routes", "last_hit_at", "TEXT")?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_routes_lookup + ON model_routes(app_type, enabled, priority DESC, created_at ASC, id ASC)", + [], + ) + .map_err(|e| AppError::Database(format!("创建 model_routes lookup 索引失败: {e}")))?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_routes_provider + ON model_routes(provider_id, app_type)", + [], + ) + .map_err(|e| AppError::Database(format!("创建 model_routes provider 索引失败: {e}")))?; + + Ok(()) + } + /// 插入默认模型定价数据 /// 格式: (model_id, display_name, input, output, cache_read, cache_creation) /// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致 @@ -2251,6 +2320,7 @@ impl Database { ("app_type", "TEXT NOT NULL DEFAULT 'claude'"), ("model", "TEXT NOT NULL DEFAULT ''"), ("request_model", "TEXT"), + ("pricing_model", "TEXT"), ("input_tokens", "INTEGER NOT NULL DEFAULT 0"), ("output_tokens", "INTEGER NOT NULL DEFAULT 0"), ("cache_read_tokens", "INTEGER NOT NULL DEFAULT 0"), @@ -2278,6 +2348,63 @@ impl Database { Ok(()) } + /// 幂等修复 `usage_daily_rollups`:若缺少 v11 的 `request_model`/`pricing_model` + /// 列(主键仍是旧的 4 列形式),则重建为 v11 schema 并迁移历史聚合数据。 + /// + /// 覆盖一条历史路径:某些 DB 的 `user_version` 已是 v11/v12,但 rollups 表却停留在 + /// 更早的 v6 schema(建库时 `create_tables` 使用旧 DDL 后直接 set_user_version), + /// 导致 v10→v11 迁移循环从未运行,`run_usage_maintenance` 持续报 + /// `no such column: request_model`。重建 SQL 与 `migrate_v10_to_v11` 保持一致。 + fn repair_usage_daily_rollups_columns(conn: &Connection) -> Result<(), AppError> { + if !Self::table_exists(conn, "usage_daily_rollups")? { + return Ok(()); + } + + let has_request_model = Self::has_column(conn, "usage_daily_rollups", "request_model")?; + let has_pricing_model = Self::has_column(conn, "usage_daily_rollups", "pricing_model")?; + if has_request_model && has_pricing_model { + return Ok(()); + } + + log::info!( + "修复 usage_daily_rollups:缺少 request_model/pricing_model 列,重建为 v11 schema" + ); + + conn.execute_batch( + "ALTER TABLE usage_daily_rollups RENAME TO usage_daily_rollups_repair_backup; + CREATE TABLE usage_daily_rollups ( + date TEXT NOT NULL, + app_type TEXT NOT NULL, + provider_id TEXT NOT NULL, + model TEXT NOT NULL, + request_model TEXT NOT NULL DEFAULT '', + pricing_model TEXT NOT NULL DEFAULT '', + request_count INTEGER NOT NULL DEFAULT 0, + success_count INTEGER NOT NULL DEFAULT 0, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cache_read_tokens INTEGER NOT NULL DEFAULT 0, + cache_creation_tokens INTEGER NOT NULL DEFAULT 0, + total_cost_usd TEXT NOT NULL DEFAULT '0', + avg_latency_ms INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (date, app_type, provider_id, model, request_model, pricing_model) + ); + INSERT INTO usage_daily_rollups + (date, app_type, provider_id, model, request_model, pricing_model, + request_count, success_count, input_tokens, output_tokens, + cache_read_tokens, cache_creation_tokens, total_cost_usd, avg_latency_ms) + SELECT date, app_type, provider_id, model, '', '', + request_count, success_count, input_tokens, output_tokens, + cache_read_tokens, cache_creation_tokens, total_cost_usd, avg_latency_ms + FROM usage_daily_rollups_repair_backup; + DROP TABLE usage_daily_rollups_repair_backup;", + ) + .map_err(|e| AppError::Database(format!("重建 usage_daily_rollups 失败: {e}")))?; + + log::info!("usage_daily_rollups 重建完成,已迁移历史聚合数据"); + Ok(()) + } + fn create_request_logs_indexes_if_supported(conn: &Connection) -> Result<(), AppError> { if !Self::table_exists(conn, "proxy_request_logs")? { return Ok(()); diff --git a/src-tauri/src/database/tests.rs b/src-tauri/src/database/tests.rs index 86c801f8..1edce9aa 100644 --- a/src-tauri/src/database/tests.rs +++ b/src-tauri/src/database/tests.rs @@ -4,6 +4,7 @@ use super::*; use crate::app_config::MultiAppConfig; +use crate::model_route::ModelRoute; use crate::prompt::Prompt; use crate::provider::{Provider, ProviderManager}; use indexmap::IndexMap; @@ -208,13 +209,13 @@ fn schema_migration_sets_user_version_when_missing() { fn schema_migration_rejects_future_version() { let conn = Connection::open_in_memory().expect("open memory db"); Database::create_tables_on_conn(&conn).expect("create tables"); - Database::set_user_version(&conn, SCHEMA_VERSION + 1).expect("set future version"); + Database::set_user_version(&conn, SCHEMA_VERSION + 2).expect("set future version"); let err = Database::apply_schema_migrations_on_conn(&conn).expect_err("should reject higher version"); let message = err.to_string(); assert!(message.contains("由较新版本的 CC Switch 创建")); - assert!(message.contains(&format!("数据库版本: {}", SCHEMA_VERSION + 1))); + assert!(message.contains(&format!("数据库版本: {}", SCHEMA_VERSION + 2))); assert!(message.contains(&format!("最高支持数据库版本: {SCHEMA_VERSION}"))); assert!(message.contains("cc-switch update")); } @@ -227,7 +228,7 @@ fn init_rejects_future_schema_before_creating_tables() { let _guard = ConfigDirEnvGuard::set(temp.path()); let db_path = temp.path().join("cc-switch.db"); let conn = Connection::open(&db_path).expect("open db"); - Database::set_user_version(&conn, SCHEMA_VERSION + 1).expect("set future version"); + Database::set_user_version(&conn, SCHEMA_VERSION + 2).expect("set future version"); drop(conn); let err = match Database::init() { @@ -2352,6 +2353,513 @@ fn model_pricing_upsert_rejects_invalid_values() { assert!(ModelPricingUpdate::new("", "Blank Model", "1", "1", "0", "0").is_err()); } +#[test] +fn schema_migration_v10_to_v12_adds_model_routes_table() { + let conn = Connection::open_in_memory().expect("open memory db"); + conn.execute_batch( + r#" + CREATE TABLE providers ( + id TEXT NOT NULL, + app_type TEXT NOT NULL, + name TEXT NOT NULL, + settings_config TEXT NOT NULL, + website_url TEXT, + category TEXT, + created_at INTEGER, + sort_index INTEGER, + notes TEXT, + icon TEXT, + icon_color TEXT, + meta TEXT NOT NULL DEFAULT '{}', + is_current BOOLEAN NOT NULL DEFAULT 0, + in_failover_queue BOOLEAN NOT NULL DEFAULT 0, + PRIMARY KEY (id, app_type) + ); + CREATE TABLE mcp_servers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + server_config TEXT NOT NULL, + description TEXT, + homepage TEXT, + docs TEXT, + tags TEXT NOT NULL DEFAULT '[]', + enabled_claude BOOLEAN NOT NULL DEFAULT 0, + enabled_codex BOOLEAN NOT NULL DEFAULT 0, + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, + enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + enabled_hermes BOOLEAN NOT NULL DEFAULT 0 + ); + CREATE TABLE skills ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + directory TEXT NOT NULL, + repo_owner TEXT, + repo_name TEXT, + repo_branch TEXT DEFAULT 'main', + readme_url TEXT, + enabled_claude BOOLEAN NOT NULL DEFAULT 0, + enabled_codex BOOLEAN NOT NULL DEFAULT 0, + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, + enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + enabled_hermes BOOLEAN NOT NULL DEFAULT 0, + installed_at INTEGER NOT NULL DEFAULT 0, + content_hash TEXT, + updated_at INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE prompts ( + id TEXT NOT NULL, + app_type TEXT NOT NULL, + name TEXT NOT NULL, + content TEXT NOT NULL, + description TEXT, + enabled BOOLEAN NOT NULL DEFAULT 1, + created_at INTEGER, + updated_at INTEGER, + PRIMARY KEY (id, app_type) + ); + CREATE TABLE skill_repos ( + owner TEXT NOT NULL, + name TEXT NOT NULL, + branch TEXT NOT NULL DEFAULT 'main', + enabled BOOLEAN NOT NULL DEFAULT 1, + PRIMARY KEY (owner, name) + ); + CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE proxy_config ( + app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')), + proxy_enabled INTEGER NOT NULL DEFAULT 0, + listen_address TEXT NOT NULL DEFAULT '127.0.0.1', + listen_port INTEGER NOT NULL DEFAULT 15721, + enable_logging INTEGER NOT NULL DEFAULT 1, + enabled INTEGER NOT NULL DEFAULT 0, + auto_failover_enabled INTEGER NOT NULL DEFAULT 0, + max_retries INTEGER NOT NULL DEFAULT 3, + streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60, + streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, + non_streaming_timeout INTEGER NOT NULL DEFAULT 600, + circuit_failure_threshold INTEGER NOT NULL DEFAULT 4, + circuit_success_threshold INTEGER NOT NULL DEFAULT 2, + circuit_timeout_seconds INTEGER NOT NULL DEFAULT 60, + circuit_error_rate_threshold REAL NOT NULL DEFAULT 0.6, + circuit_min_requests INTEGER NOT NULL DEFAULT 10, + default_cost_multiplier TEXT NOT NULL DEFAULT '1', + pricing_model_source TEXT NOT NULL DEFAULT 'response', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE TABLE proxy_request_logs ( + request_id TEXT PRIMARY KEY, + provider_id TEXT NOT NULL, + app_type TEXT NOT NULL, + model TEXT NOT NULL, + request_model TEXT, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cache_read_tokens INTEGER NOT NULL DEFAULT 0, + cache_creation_tokens INTEGER NOT NULL DEFAULT 0, + input_cost_usd TEXT NOT NULL DEFAULT '0', + output_cost_usd TEXT NOT NULL DEFAULT '0', + cache_read_cost_usd TEXT NOT NULL DEFAULT '0', + cache_creation_cost_usd TEXT NOT NULL DEFAULT '0', + total_cost_usd TEXT NOT NULL DEFAULT '0', + latency_ms INTEGER NOT NULL, + first_token_ms INTEGER, + duration_ms INTEGER, + status_code INTEGER NOT NULL, + error_message TEXT, + session_id TEXT, + provider_type TEXT, + is_streaming INTEGER NOT NULL DEFAULT 0, + cost_multiplier TEXT NOT NULL DEFAULT '1.0', + created_at INTEGER NOT NULL, + data_source TEXT NOT NULL DEFAULT 'proxy' + ); + CREATE TABLE stream_check_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider_id TEXT NOT NULL, + provider_name TEXT NOT NULL, + app_type TEXT NOT NULL, + status TEXT NOT NULL, + success INTEGER NOT NULL, + message TEXT NOT NULL, + response_time_ms INTEGER, + http_status INTEGER, + model_used TEXT, + retry_count INTEGER DEFAULT 0, + tested_at INTEGER NOT NULL + ); + CREATE TABLE model_pricing ( + model_id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + input_cost_per_million TEXT NOT NULL, + output_cost_per_million TEXT NOT NULL, + cache_read_cost_per_million TEXT NOT NULL DEFAULT '0', + cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0' + ); + INSERT INTO model_pricing ( + model_id, display_name, input_cost_per_million, output_cost_per_million, + cache_read_cost_per_million, cache_creation_cost_per_million + ) VALUES ('test-model', 'Test Model', '1.0', '2.0', '0.1', '0'); + CREATE TABLE proxy_live_backup ( + app_type TEXT PRIMARY KEY, + original_config TEXT NOT NULL, + backed_up_at TEXT NOT NULL + ); + CREATE TABLE usage_daily_rollups ( + date TEXT NOT NULL, + app_type TEXT NOT NULL, + provider_id TEXT NOT NULL, + model TEXT NOT NULL, + request_count INTEGER NOT NULL DEFAULT 0, + success_count INTEGER NOT NULL DEFAULT 0, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cache_read_tokens INTEGER NOT NULL DEFAULT 0, + cache_creation_tokens INTEGER NOT NULL DEFAULT 0, + total_cost_usd TEXT NOT NULL DEFAULT '0', + avg_latency_ms INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (date, app_type, provider_id, model) + ); + CREATE TABLE session_log_sync ( + file_path TEXT PRIMARY KEY, + last_modified INTEGER NOT NULL, + last_line_offset INTEGER NOT NULL DEFAULT 0, + last_synced_at INTEGER NOT NULL + ); + "#, + ) + .expect("seed v10 schema"); + + Database::set_user_version(&conn, 10).expect("set user_version=10"); + Database::apply_schema_migrations_on_conn(&conn).expect("apply migrations"); + + assert_eq!( + Database::get_user_version(&conn).expect("version after migration"), + SCHEMA_VERSION + ); + + assert!( + Database::table_exists(&conn, "model_routes").expect("check model_routes exists"), + "model_routes table should exist after v10 -> v12 migration" + ); + assert!( + Database::has_column(&conn, "model_routes", "pattern").expect("check pattern column"), + "model_routes.pattern column should exist" + ); + assert!( + Database::has_column(&conn, "model_routes", "priority").expect("check priority column"), + "model_routes.priority column should exist" + ); +} + +#[test] +fn repair_usage_daily_rollups_rebuilds_legacy_table_when_already_at_current_schema() { + // 模拟历史路径:user_version 已是 SCHEMA_VERSION,但 usage_daily_rollups 停留在 + // 更早的 v6 schema(缺 request_model/pricing_model,旧 4 列主键),迁移循环从未运行, + // 仅靠 apply_schema_migrations 的 repair 阶段重建。 + let db = Database::memory().expect("create memory db"); + let conn = db.conn.lock().expect("lock conn"); + + conn.execute_batch( + "DROP TABLE usage_daily_rollups; + CREATE TABLE usage_daily_rollups ( + date TEXT NOT NULL, + app_type TEXT NOT NULL, + provider_id TEXT NOT NULL, + model TEXT NOT NULL, + request_count INTEGER NOT NULL DEFAULT 0, + success_count INTEGER NOT NULL DEFAULT 0, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cache_read_tokens INTEGER NOT NULL DEFAULT 0, + cache_creation_tokens INTEGER NOT NULL DEFAULT 0, + total_cost_usd TEXT NOT NULL DEFAULT '0', + avg_latency_ms INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (date, app_type, provider_id, model) + ); + INSERT INTO usage_daily_rollups + (date, app_type, provider_id, model, request_count, input_tokens, output_tokens) + VALUES ('2026-06-22', 'claude', 'prov-1', 'glm-5.2', 5, 100, 200);", + ) + .expect("seed legacy rollups table"); + + // version 已是当前 schema,迁移循环不进入,仅 repair 阶段应重建 rollups + Database::apply_schema_migrations_on_conn(&conn).expect("repair via migration entrypoint"); + + assert!( + Database::has_column(&conn, "usage_daily_rollups", "request_model") + .expect("check request_model"), + "request_model column should exist after repair" + ); + assert!( + Database::has_column(&conn, "usage_daily_rollups", "pricing_model") + .expect("check pricing_model"), + "pricing_model column should exist after repair" + ); + + // 历史聚合数据应保留 + let (request_count, input_tokens): (i64, i64) = conn + .query_row( + "SELECT request_count, input_tokens FROM usage_daily_rollups + WHERE provider_id = 'prov-1'", + [], + |r| Ok((r.get(0)?, r.get(1)?)), + ) + .expect("query migrated row"); + assert_eq!(request_count, 5); + assert_eq!(input_tokens, 100); +} + +#[test] +fn model_route_dao_crud_roundtrip() { + let db = Database::memory().expect("create memory db"); + + // Seed a provider for FK validation + let conn = db.conn.lock().expect("lock conn"); + conn.execute( + "INSERT INTO providers (id, app_type, name, settings_config, meta) + VALUES ('test-prov', 'claude', 'Test Provider', '{}', '{}')", + [], + ) + .expect("seed provider"); + drop(conn); + + // Create + let created = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "*-sonnet".into(), + provider_id: "test-prov".into(), + priority: 10, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create model route"); + + assert_eq!(created.id.len(), 36); // UUID v4 + assert_eq!(created.pattern, "*-sonnet"); + assert_eq!(created.provider_id, "test-prov"); + assert_eq!(created.priority, 10); + assert!(created.enabled); + assert!(created.created_at.is_some()); + + // Get by id + let got = db.get_model_route(&created.id).expect("get model route"); + assert!(got.is_some()); + assert_eq!(got.unwrap().pattern, "*-sonnet"); + + // Create second route + let second = db + .create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "gpt-*".into(), + provider_id: "test-prov".into(), + priority: 20, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create second route"); + + // FK constraint: reject non-existent provider + let result = db.create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "bad-*".into(), + provider_id: "nonexistent".into(), + priority: 1, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("not found"), + "expected 'not found' error, got: {err_msg}" + ); + + // Update + let updated = db + .update_model_route( + &created.id, + &ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "claude-*".into(), + provider_id: "test-prov".into(), + priority: 5, + enabled: false, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }, + ) + .expect("update model route"); + + assert_eq!(updated.pattern, "claude-*"); + assert_eq!(updated.priority, 5); + assert!(!updated.enabled); + + // Toggle + let toggled_off = db.toggle_model_route(&created.id).expect("toggle off"); + assert!(toggled_off.enabled, "toggle off should re-enable"); + + let toggled_on = db.toggle_model_route(&created.id).expect("toggle on"); + assert!(!toggled_on.enabled, "toggle on should disable"); + + // Delete + db.delete_model_route(&created.id) + .expect("delete model route"); + let gone = db.get_model_route(&created.id).expect("get deleted route"); + assert!(gone.is_none()); + + // Clean up the second route + db.delete_model_route(&second.id) + .expect("delete second route"); + + // List ordering: create 3 routes with priorities 5, 1, 3 + db.create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "mid".into(), + provider_id: "test-prov".into(), + priority: 5, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create priority 5"); + db.create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "low".into(), + provider_id: "test-prov".into(), + priority: 1, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create priority 1"); + db.create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "high".into(), + provider_id: "test-prov".into(), + priority: 3, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create priority 3"); + + let routes = db.list_model_routes("claude").expect("list routes"); + assert_eq!(routes.len(), 3); + assert_eq!(routes[0].priority, 1); + assert_eq!(routes[1].priority, 3); + assert_eq!(routes[2].priority, 5); + + // List filtering: create a codex route + let conn2 = db.conn.lock().expect("lock conn"); + conn2 + .execute( + "INSERT INTO providers (id, app_type, name, settings_config, meta) + VALUES ('codex-prov', 'codex', 'Codex Provider', '{}', '{}')", + [], + ) + .expect("seed codex provider"); + drop(conn2); + + db.create_model_route(&ModelRoute { + id: String::new(), + app_type: "codex".into(), + pattern: "*-codex".into(), + provider_id: "codex-prov".into(), + priority: 1, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create codex route"); + + let claude_routes = db.list_model_routes("claude").expect("list claude routes"); + assert_eq!(claude_routes.len(), 3, "only claude routes listed"); + + let codex_routes = db.list_model_routes("codex").expect("list codex routes"); + assert_eq!(codex_routes.len(), 1); + assert_eq!(codex_routes[0].pattern, "*-codex"); +} + +#[test] +fn model_route_cascade_delete_on_provider_removal() { + let db = Database::memory().expect("create memory db"); + + // Seed provider + let conn = db.conn.lock().expect("lock conn"); + conn.execute( + "INSERT INTO providers (id, app_type, name, settings_config, meta) + VALUES ('cascade-prov', 'claude', 'Cascade Provider', '{}', '{}')", + [], + ) + .expect("seed provider"); + drop(conn); + + // Create a model_route pointing to this provider + db.create_model_route(&ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "*-test".into(), + provider_id: "cascade-prov".into(), + priority: 1, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }) + .expect("create model route"); + + assert_eq!( + db.list_model_routes("claude").expect("list routes").len(), + 1 + ); + + // Delete the provider — should cascade delete the model_route + let conn2 = db.conn.lock().expect("lock conn"); + conn2 + .execute( + "DELETE FROM providers WHERE id = 'cascade-prov' AND app_type = 'claude'", + [], + ) + .expect("delete provider"); + drop(conn2); + + let routes = db.list_model_routes("claude").expect("list after cascade"); + assert!( + routes.is_empty(), + "model_routes should be empty after provider cascade delete" + ); +} + #[test] fn should_auto_extract_config_snippet_respects_snippet_and_cleared_flag() { let db = Database::memory().expect("create memory db"); diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index ebe541f3..2f53392f 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -260,65 +260,38 @@ fn build_claude_settings(request: &DeepLinkImportRequest) -> serde_json::Value { } fn build_codex_settings(request: &DeepLinkImportRequest) -> serde_json::Value { - // The `model_provider` key is always the shared `custom` id; the human - // readable provider name is preserved in the `name` field of the - // `[model_providers.custom]` table. Codex rejects a provider table whose - // `name` is missing or empty ("provider name must not be empty", #333), so - // the display name is always written (falling back to `custom`). - let provider_display_name = request - .name - .as_deref() - .unwrap_or("custom") - .chars() - .filter(|c| !c.is_control()) - .collect::() - .trim() - .to_string(); - let provider_display_name = if provider_display_name.is_empty() { - "custom".to_string() - } else { - provider_display_name - }; - - // Model name: use deeplink model or default let model_name = request .model .as_deref() - .unwrap_or("gpt-5-codex") - .to_string(); + .filter(|s| !s.trim().is_empty()) + .unwrap_or("gpt-5.4"); - // Endpoint: normalize trailing slashes (use primary endpoint only) let endpoint = get_primary_endpoint(request) .trim() .trim_end_matches('/') .to_string(); - // Escape every user-controlled value through toml_edit so quotes/specials - // in the display name or endpoint cannot corrupt the generated TOML. - let provider_display_name = toml_edit::Value::from(provider_display_name.as_str()).to_string(); - let model_name = toml_edit::Value::from(model_name.as_str()).to_string(); - let endpoint = toml_edit::Value::from(endpoint.as_str()).to_string(); - - // Build config.toml content - let config_toml = format!( - r#"model_provider = "custom" -model = {model_name} -model_reasoning_effort = "high" -disable_response_storage = true - -[model_providers.custom] -name = {provider_display_name} -base_url = {endpoint} -wire_api = "responses" -requires_openai_auth = true -"# + // Generate a provider key from the name (same logic as clean_codex_provider_key) + let provider_key = + crate::codex_config::clean_codex_provider_key(request.name.as_deref().unwrap_or("custom")); + + // Use upstream model_provider + [model_providers.] format + let config_snippet = format!( + "model_provider = \"{provider_key}\"\n\ + model = \"{model_name}\"\n\ + \n\ + [model_providers.{provider_key}]\n\ + base_url = \"{endpoint}\"\n\ + wire_api = \"responses\"\n\ + requires_openai_auth = false\n\ + env_key = \"OPENAI_API_KEY\"" ); json!({ "auth": { "OPENAI_API_KEY": request.api_key, }, - "config": config_toml + "config": config_snippet }) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d6922c5f..b0427645 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,6 +18,7 @@ mod import_export; #[allow(dead_code)] mod init_status; mod mcp; +mod model_route; mod openclaw_config; mod opencode_config; mod prompt; @@ -62,6 +63,7 @@ pub use mcp::{ sync_enabled_to_codex, sync_enabled_to_gemini, sync_single_server_to_claude, sync_single_server_to_codex, sync_single_server_to_gemini, }; +pub use model_route::ModelRoute; pub use provider::{Provider, ProviderMeta, UsageScript}; pub use proxy::{ProxyConfig, ProxyServerInfo, ProxyStatus}; pub use services::{ diff --git a/src-tauri/src/model_route.rs b/src-tauri/src/model_route.rs new file mode 100644 index 00000000..35ed6982 --- /dev/null +++ b/src-tauri/src/model_route.rs @@ -0,0 +1,53 @@ +//! 模型路由类型定义 (Model Route type definition) +//! +//! 定义 per-model provider routing 的数据结构,用于根据模型名称模式 +//! 将代理请求路由到不同的 provider。 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelRoute { + pub id: String, + pub app_type: String, + pub pattern: String, + pub provider_id: String, + pub priority: i32, + pub enabled: bool, + pub hit_count: i64, + pub last_hit_at: Option, + pub created_at: Option, + pub updated_at: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn model_route_serialization_roundtrip_camelcase() { + let route = ModelRoute { + id: "test-id-001".into(), + app_type: "claude".into(), + pattern: "*-sonnet".into(), + provider_id: "test-prov".into(), + priority: 10, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: Some("2025-01-01 00:00:00".into()), + updated_at: Some("2025-01-01 00:00:00".into()), + }; + + let json = serde_json::to_string(&route).expect("serialize"); + assert!(json.contains("\"appType\""), "camelCase: {}", json); + assert!(json.contains("\"providerId\""), "camelCase: {}", json); + assert!(json.contains("\"createdAt\""), "camelCase: {}", json); + assert!(json.contains("\"updatedAt\""), "camelCase: {}", json); + + let deserialized: ModelRoute = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(deserialized.id, "test-id-001"); + assert_eq!(deserialized.created_at, Some("2025-01-01 00:00:00".into())); + assert_eq!(deserialized.updated_at, Some("2025-01-01 00:00:00".into())); + } +} diff --git a/src-tauri/src/proxy/handler_context.rs b/src-tauri/src/proxy/handler_context.rs index e5fa03e9..f6e0bdef 100644 --- a/src-tauri/src/proxy/handler_context.rs +++ b/src-tauri/src/proxy/handler_context.rs @@ -8,17 +8,36 @@ use crate::provider::Provider; use super::{ error::ProxyError, + model_mapper::provider_has_explicit_role_mapping, provider_router::ProviderRouter, server::ProxyServerState, session::extract_session_id, types::{AppProxyConfig, CopilotOptimizerConfig, OptimizerConfig, RectifierConfig}, }; +/// Extract the model identifier from a Gemini API path like +/// `/v1beta/models/gemini-2.5-pro:generateContent` or +/// `/v1/models/gemini-2.5-flash:streamGenerateContent`. Returns `None` if +/// the path does not match the expected `models/[:action]` shape. +fn extract_gemini_model_from_path(path: &str) -> Option { + // Find the "models/" segment and take what follows up to ":" or end. + let idx = path.find("/models/")?; + let after = &path[idx + "/models/".len()..]; + let end = after.find([':', '?', '/']).unwrap_or(after.len()); + let model = &after[..end]; + if model.is_empty() { + None + } else { + Some(model.to_string()) + } +} + pub struct HandlerContext { pub start_time: Instant, pub state: ProxyServerState, pub app_type: AppType, pub provider_router: Arc, + pub route_source: Option, providers: Vec, pub app_proxy: AppProxyConfig, pub rectifier_config: RectifierConfig, @@ -36,6 +55,7 @@ impl HandlerContext { app_type: AppType, headers: &HeaderMap, body: &Value, + path: &str, ) -> Result { let _ = crate::settings::reload_settings(); let current_provider_id_at_start = @@ -47,7 +67,72 @@ impl HandlerContext { let start_time = Instant::now(); let provider_router = state.provider_router.clone(); - let providers = provider_router.select_providers(app_type.as_str()).await?; + let model_router = state.model_router.clone(); + // Gemini 请求的 model 在 URI 路径中(如 /v1beta/models/gemini-2.5-pro:generateContent), + // 标准 Claude/Codex/OpenAI 请求的 model 在 JSON body 中。 + let request_model = body + .get("model") + .and_then(|value| value.as_str()) + .map(|s| s.to_string()) + .or_else(|| extract_gemini_model_from_path(path)) + .unwrap_or_default(); + + let manual_provider = if current_provider_id_at_start.is_empty() { + None + } else { + state + .db + .get_provider_by_id(¤t_provider_id_at_start, app_type.as_str()) + .ok() + .flatten() + }; + + // A manual Claude provider switch writes role-model mappings into live config + // (for example client-visible aliases mapped to provider-specific upstream + // models). Treat that selected provider as the user's active choice and let + // normal-priority automatic routes yield to it. + let manual_role_provider = if matches!(app_type, AppType::Claude) { + manual_provider + .clone() + .filter(|provider| provider_has_explicit_role_mapping(provider, &request_model)) + } else { + None + }; + + // Model route matching first. The router compares generic route priority + // against the active manual provider choice; it does not special-case model + // families or provider names. + let (providers, route_source) = match model_router + .match_route_respecting_manual_provider( + app_type.as_str(), + &request_model, + manual_role_provider.as_ref(), + ) + .await + { + Ok(Some((_route_id, provider))) => (vec![provider], Some("model_route".to_string())), + Ok(None) => { + if let Some(provider) = manual_role_provider { + // No model route matched — use manual role mapping as fallback + (vec![provider], Some("manual_provider_model".to_string())) + } else { + // RT-04: no match, fallback to existing ProviderRouter + let providers = provider_router.select_providers(app_type.as_str()).await?; + (providers, None) + } + } + Err(e) => { + if let Some(provider) = manual_role_provider { + log::warn!("model route lookup failed: {e}, using manual role mapping"); + (vec![provider], Some("manual_provider_model".to_string())) + } else { + // RT-05: match_route error (DB error), log warning and fallback + log::warn!("model route lookup failed: {e}, falling back to provider router"); + let providers = provider_router.select_providers(app_type.as_str()).await?; + (providers, None) + } + } + }; let app_proxy = state .db @@ -62,11 +147,6 @@ impl HandlerContext { let rectifier_config = state.db.get_rectifier_config().unwrap_or_default(); let optimizer_config = state.db.get_optimizer_config().unwrap_or_default(); let copilot_optimizer_config = state.db.get_copilot_optimizer_config().unwrap_or_default(); - let request_model = body - .get("model") - .and_then(|value| value.as_str()) - .unwrap_or("unknown") - .to_string(); let session_result = extract_session_id(headers, body, app_type.as_str()); Ok(Self { @@ -74,6 +154,7 @@ impl HandlerContext { state: state.clone(), app_type, provider_router, + route_source, providers, app_proxy, rectifier_config, @@ -133,12 +214,18 @@ mod tests { use serde_json::json; use serial_test::serial; + use std::collections::HashMap; use std::env; use tempfile::TempDir; use tokio::sync::RwLock; - use crate::proxy::providers::gemini_shadow::GeminiShadowStore; - use crate::{database::Database, proxy::types::ProxyConfig}; + use crate::{ + database::Database, + proxy::{ + model_router::ModelRouter, providers::gemini_shadow::GeminiShadowStore, + types::ProxyConfig, + }, + }; struct TempHome { #[allow(dead_code)] @@ -214,9 +301,11 @@ mod tests { status: Arc::new(RwLock::new(Default::default())), start_time: Arc::new(RwLock::new(None)), current_providers: Arc::new(RwLock::new(Default::default())), - provider_router: Arc::new(ProviderRouter::new(db)), + provider_router: Arc::new(ProviderRouter::new(db.clone())), + model_router: Arc::new(ModelRouter::new(db)), codex_chat_history: Arc::new(Default::default()), gemini_shadow: Arc::new(GeminiShadowStore::default()), + provider_token_map: Arc::new(RwLock::new(HashMap::new())), } } @@ -250,6 +339,7 @@ mod tests { AppType::Claude, &HeaderMap::new(), &json!({"model": "claude-3-7-sonnet-20250219"}), + "", ) .await .expect("load handler context"); @@ -290,6 +380,7 @@ mod tests { AppType::Claude, &HeaderMap::new(), &json!({"model": "claude-3-7-sonnet-20250219"}), + "", ) .await .expect("load handler context"); @@ -331,6 +422,7 @@ mod tests { AppType::Claude, &HeaderMap::new(), &json!({"model": "claude-3-7-sonnet-20250219"}), + "", ) .await }) @@ -349,4 +441,293 @@ mod tests { assert_eq!(context.providers()[0].id, "claude-failover"); assert_eq!(context.current_provider_id_at_start, "claude-current"); } + + #[tokio::test] + #[serial(home_settings)] + async fn model_route_match_bypasses_failover_queue() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().expect("create memory database")); + let current = test_provider("claude-current", 1); + let failover = test_provider("claude-failover", 0); + + db.save_provider("claude", ¤t) + .expect("save current provider"); + db.save_provider("claude", &failover) + .expect("save failover provider"); + db.set_current_provider("claude", ¤t.id) + .expect("set current provider"); + + // Enable auto failover so select_providers would normally return the queue + let mut config = db + .get_proxy_config_for_app("claude") + .await + .expect("read app proxy config"); + config.enabled = true; + config.auto_failover_enabled = true; + db.update_proxy_config_for_app(config) + .await + .expect("enable auto failover"); + + // Create model route: pattern "*sonnet*" → claude-current (priority 1) + use crate::model_route::ModelRoute; + let route = ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "*sonnet*".into(), + provider_id: "claude-current".into(), + priority: 1, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }; + db.create_model_route(&route).expect("create model route"); + + let state = test_state(db); + let context = HandlerContext::load( + &state, + AppType::Claude, + &HeaderMap::new(), + &json!({"model": "claude-sonnet-4-6"}), + "", + ) + .await + .expect("load handler context"); + + // Model route matched — single provider, not the failover queue + assert_eq!(context.providers().len(), 1); + assert_eq!(context.providers()[0].id, "claude-current"); + assert_eq!(context.route_source, Some("model_route".to_string())); + } + + #[tokio::test] + #[serial(home_settings)] + async fn manual_role_mapping_beats_normal_priority_model_route() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().expect("create memory database")); + let mut current = test_provider("deepseek-current", 1); + current.name = "DeepSeek".to_string(); + current.settings_config = json!({ + "env": { + "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]" + } + }); + let route_target = test_provider("pp-coder", 0); + + db.save_provider("claude", ¤t) + .expect("save current provider"); + db.save_provider("claude", &route_target) + .expect("save route target provider"); + db.set_current_provider("claude", ¤t.id) + .expect("set current provider"); + + let mut config = db + .get_proxy_config_for_app("claude") + .await + .expect("read app proxy config"); + config.enabled = true; + config.auto_failover_enabled = true; + db.update_proxy_config_for_app(config) + .await + .expect("enable auto failover"); + + use crate::model_route::ModelRoute; + let route = ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "*".into(), + provider_id: route_target.id.clone(), + priority: 0, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }; + db.create_model_route(&route).expect("create model route"); + + let state = test_state(db); + let context = HandlerContext::load( + &state, + AppType::Claude, + &HeaderMap::new(), + &json!({"model": "claude-opus-4-8[1M]"}), + "", + ) + .await + .expect("load handler context"); + + // Normal-priority automatic routes are fallbacks. A manual provider with an + // explicit mapping must keep the request on the selected provider. + assert_eq!(context.providers().len(), 1); + assert_eq!(context.providers()[0].id, "deepseek-current"); + assert_eq!( + context.route_source, + Some("manual_provider_model".to_string()) + ); + } + + #[tokio::test] + #[serial(home_settings)] + async fn higher_priority_model_route_beats_manual_role_mapping() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().expect("create memory database")); + + // Manual provider: deepseek-current, with explicit opus role mapping + let mut current = test_provider("deepseek-current", 1); + current.name = "DeepSeek".to_string(); + current.settings_config = json!({ + "env": { + "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]" + } + }); + + // Another provider that a *specific* model route should direct to + let specific_target = test_provider("specific-opus-prov", 0); + + db.save_provider("claude", ¤t) + .expect("save current provider"); + db.save_provider("claude", &specific_target) + .expect("save specific target provider"); + db.set_current_provider("claude", ¤t.id) + .expect("set current provider"); + + let mut config = db + .get_proxy_config_for_app("claude") + .await + .expect("read app proxy config"); + config.enabled = true; + config.auto_failover_enabled = true; + db.update_proxy_config_for_app(config) + .await + .expect("enable auto failover"); + + use crate::model_route::ModelRoute; + let specific_route = ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "*".into(), + provider_id: specific_target.id.clone(), + priority: -2, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }; + db.create_model_route(&specific_route) + .expect("create specific model route"); + + let state = test_state(db); + let context = HandlerContext::load( + &state, + AppType::Claude, + &HeaderMap::new(), + &json!({"model": "claude-opus-4-8[1M]"}), + "", + ) + .await + .expect("load handler context"); + + // Routes with explicit higher priority can still win over manual selection. + assert_eq!(context.providers().len(), 1); + assert_eq!(context.providers()[0].id, "specific-opus-prov"); + assert_eq!(context.route_source, Some("model_route".to_string())); + } + + #[tokio::test] + #[serial(home_settings)] + async fn role_specific_opus_route_beats_current_glm_role_mapping() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().expect("create memory database")); + + let mut current = test_provider("glm-current", 1); + current.name = "Zhipu GLM".to_string(); + current.settings_config = json!({ + "env": { + "ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-5.1" + } + }); + let route_target = test_provider("pp-coder", 0); + + db.save_provider("claude", ¤t) + .expect("save current provider"); + db.save_provider("claude", &route_target) + .expect("save route target provider"); + db.set_current_provider("claude", ¤t.id) + .expect("set current provider"); + + use crate::model_route::ModelRoute; + let route = ModelRoute { + id: String::new(), + app_type: "claude".into(), + pattern: "*opus*".into(), + provider_id: route_target.id.clone(), + priority: 0, + enabled: true, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + }; + db.create_model_route(&route).expect("create model route"); + + let state = test_state(db); + let context = HandlerContext::load( + &state, + AppType::Claude, + &HeaderMap::new(), + &json!({"model": "claude-opus-4-8"}), + "", + ) + .await + .expect("load handler context"); + + assert_eq!(context.providers().len(), 1); + assert_eq!(context.providers()[0].id, "pp-coder"); + assert_eq!(context.route_source, Some("model_route".to_string())); + } + + #[tokio::test] + #[serial(home_settings)] + async fn no_model_route_falls_back_to_provider_router() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().expect("create memory database")); + let current = test_provider("claude-current", 1); + let failover = test_provider("claude-failover", 0); + + db.save_provider("claude", ¤t) + .expect("save current provider"); + db.save_provider("claude", &failover) + .expect("save failover provider"); + db.set_current_provider("claude", ¤t.id) + .expect("set current provider"); + + let mut config = db + .get_proxy_config_for_app("claude") + .await + .expect("read app proxy config"); + config.enabled = true; + config.auto_failover_enabled = true; + db.update_proxy_config_for_app(config) + .await + .expect("enable auto failover"); + + // No model route matches "gemini-2.5-pro" + let state = test_state(db); + let context = HandlerContext::load( + &state, + AppType::Claude, + &HeaderMap::new(), + &json!({"model": "gemini-2.5-pro"}), + "", + ) + .await + .expect("load handler context"); + + // Falls back to normal ProviderRouter behavior (failover queue) + assert_eq!(context.providers()[0].id, "claude-failover"); + assert_eq!(context.route_source, None); + } } diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs index 9554e66d..8a41a8cc 100644 --- a/src-tauri/src/proxy/handlers.rs +++ b/src-tauri/src/proxy/handlers.rs @@ -10,6 +10,8 @@ use std::time::{Duration, Instant}; use crate::{app_config::AppType, provider::Provider}; +use super::model_mapper::strip_one_m_suffix_for_upstream; + use super::{ error::ProxyError, forwarder::{ForwardOptions, RequestForwarder}, @@ -38,6 +40,173 @@ pub async fn get_status(State(state): State) -> impl IntoRespo Json(state.snapshot_status().await) } +/// Handle `GET /v1/models` — return merged model list from model routes +/// and provider env configs. +/// +/// Emits a protocol superset (Anthropic + OpenAI) so that both +/// Anthropic clients (via `ANTHROPIC_BASE_URL`) and OpenAI-style +/// clients can consume the response. +pub async fn handle_models(State(state): State) -> impl IntoResponse { + let db = state.db; + let mut model_ids: Vec = Vec::new(); + + // /v1/models 是模型发现端点,客户端(Claude Code、Codex 等)不会携带 app 标识, + // 所以遍历所有 app,合并各自的供应商 env 模型和启用的 model route, + // 让每个客户端都能看到为自己 app 配置的模型。 + for app in AppType::all() { + let app_type = app.as_str(); + + // 1. Collect model names from this app's providers' env config + if let Ok(providers) = db.get_all_providers(app_type) { + for provider in providers.values() { + let env = provider.settings_config.get("env"); + if let Some(env) = env { + let keys = [ + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_MODEL", + ]; + for key in &keys { + if let Some(val) = env + .get(*key) + .and_then(|v| v.as_str()) + .filter(|v| !v.is_empty()) + { + let cleaned = strip_one_m_suffix_for_upstream(val).to_string(); + if !model_ids.contains(&cleaned) { + model_ids.push(cleaned); + } + } + } + } + } + } + + // 2. Add standard Claude role models from route patterns + if let Ok(routes) = db.list_model_routes(app_type) { + for route in &routes { + if !route.enabled { + continue; + } + let pattern_lower = route.pattern.trim().to_ascii_lowercase(); + let standard_models = match pattern_lower.as_str() { + "*haiku*" | "haiku" => vec!["claude-haiku-4-5-20251001"], + "*sonnet*" | "sonnet" => vec!["claude-sonnet-4-6"], + "*opus*" | "opus" => vec!["claude-opus-4-8"], + _ => Vec::new(), + }; + for m in standard_models { + if !model_ids.contains(&m.to_string()) { + model_ids.push(m.to_string()); + } + } + // 精确 pattern(无通配符)也作为 model id 暴露,否则路由专属 model + // (如 gpt-5、deepseek-v4-pro)不会出现在 /v1/models,客户端 picker 看不到。 + if !pattern_lower.contains('*') { + let exact = route.pattern.trim().to_string(); + if !exact.is_empty() && !model_ids.contains(&exact) { + model_ids.push(exact); + } + } + } + } + } + + // 3. Build protocol superset: Anthropic + OpenAI fields + let data: Vec = model_ids + .iter() + .map(|id| { + let display_name = model_display_name(id); + json!({ + // Anthropic fields + "type": "model", + "display_name": display_name, + "created_at": "2025-01-01T00:00:00Z", + // OpenAI fields + "id": id, + "object": "model", + "created": 1700000000, + "owned_by": "cc-switch" + }) + }) + .collect(); + + let first_id = model_ids.first().cloned(); + let last_id = model_ids.last().cloned(); + + Json(json!({ + // Anthropic pagination + "type": "page", + "has_more": false, + "first_id": first_id, + "last_id": last_id, + // OpenAI + "object": "list", + "data": data + })) +} + +/// Map a model id to a human-readable display name for Anthropic's +/// `display_name` field on GET /v1/models. +fn model_display_name(id: &str) -> String { + // Some common well-known model patterns + let mapping: &[(&str, &str)] = &[ + ("claude-opus-4-8-20250514", "Claude 4.8 Opus"), + ("claude-opus-4-8", "Claude 4.8 Opus"), + ("claude-sonnet-4-6-20250514", "Claude 4.6 Sonnet"), + ("claude-sonnet-4-6", "Claude 4.6 Sonnet"), + ("claude-haiku-4-5-20251001", "Claude 4.5 Haiku"), + ("claude-haiku-4-5", "Claude 4.5 Haiku"), + ("claude-opus-4-5-20251101", "Claude 4.5 Opus"), + ("claude-opus-4-5", "Claude 4.5 Opus"), + ("claude-sonnet-4-5-20250915", "Claude 4.5 Sonnet"), + ("claude-sonnet-4-5", "Claude 4.5 Sonnet"), + ("claude-haiku-3-5-20250112", "Claude 3.5 Haiku"), + ("claude-haiku-3-5", "Claude 3.5 Haiku"), + ("deepseek-v4-pro", "DeepSeek V4 Pro"), + ("deepseek-v4", "DeepSeek V4"), + ("deepseek-v3-1", "DeepSeek V3.1"), + ("deepseek-v3", "DeepSeek V3"), + ("deepseek-r1", "DeepSeek R1"), + ("gpt-5", "GPT-5"), + ("gpt-5-mini", "GPT-5 Mini"), + ("gpt-5-nano", "GPT-5 Nano"), + ("gpt-4.1", "GPT-4.1"), + ("gpt-4.1-mini", "GPT-4.1 Mini"), + ("gpt-4.1-nano", "GPT-4.1 Nano"), + ("gemini-3.0-pro", "Gemini 3.0 Pro"), + ("gemini-2.5-pro", "Gemini 2.5 Pro"), + ("gemini-2.5-flash", "Gemini 2.5 Flash"), + ("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"), + ("minimax-m2.5", "MiniMax M2.5"), + ("minimax-m1", "MiniMax M1"), + ("kimi-k2.5", "Kimi K2.5"), + ("kimi-k2", "Kimi K2"), + ("qwen3-coder", "Qwen3 Coder"), + ("qwen3-235b", "Qwen3 235B"), + ]; + + let id_lower = id.to_ascii_lowercase(); + for (pattern, name) in mapping { + if id_lower == *pattern { + return name.to_string(); + } + } + + // Fallback: title-case the segments + id.split('-') + .map(|seg| { + let mut chars = seg.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect::>() + .join(" ") +} + pub async fn handle_messages( State(state): State, headers: HeaderMap, @@ -123,10 +292,11 @@ async fn handle_claude_request( headers: HeaderMap, body: Value, ) -> Response { + let estimated_input_tokens = estimate_tokens_from_value(&body); state - .record_estimated_input_tokens(estimate_tokens_from_value(&body)) + .record_estimated_input_tokens(estimated_input_tokens) .await; - let context = match HandlerContext::load(&state, AppType::Claude, &headers, &body).await { + let context = match HandlerContext::load(&state, AppType::Claude, &headers, &body, "").await { Ok(context) => context, Err(error) => { state.record_request_error(&error).await; @@ -209,6 +379,8 @@ async fn handle_claude_request( app_type: context.app_type.clone(), provider: forward_result.provider.clone(), current_provider_id_at_start: context.current_provider_id_at_start.clone(), + is_model_routed: context.route_source.as_deref() == Some("model_route"), + estimated_input_tokens, }); let first_byte_timeout = remaining_timeout(first_byte_timeout, request_started_at); let idle_timeout = context.streaming_idle_timeout(); @@ -326,6 +498,8 @@ async fn handle_claude_request( app_type: context.app_type.clone(), provider: provider.clone(), current_provider_id_at_start: context.current_provider_id_at_start.clone(), + is_model_routed: context.route_source.as_deref() == Some("model_route"), + estimated_input_tokens, }); let api_format = super::providers::get_claude_api_format(provider); let response_result = if adapter.needs_transform(provider) { @@ -465,10 +639,11 @@ async fn handle_passthrough_request( app_type: AppType, endpoint: String, ) -> Response { + let estimated_input_tokens = estimate_tokens_from_value(&body); state - .record_estimated_input_tokens(estimate_tokens_from_value(&body)) + .record_estimated_input_tokens(estimated_input_tokens) .await; - let context = match HandlerContext::load(&state, app_type, &headers, &body).await { + let context = match HandlerContext::load(&state, app_type, &headers, &body, &endpoint).await { Ok(context) => context, Err(error) => { state.record_request_error(&error).await; @@ -558,6 +733,8 @@ async fn handle_passthrough_request( app_type: context.app_type.clone(), provider: forward_result.provider.clone(), current_provider_id_at_start: context.current_provider_id_at_start.clone(), + is_model_routed: context.route_source.as_deref() == Some("model_route"), + estimated_input_tokens, }); let response_result = match response { super::forwarder::StreamingResponse::Live(response) @@ -667,6 +844,7 @@ async fn handle_passthrough_request( streaming_first_byte_timeout, non_streaming_timeout, codex_tool_context.unwrap_or_default(), + estimated_input_tokens, ) .await; } @@ -709,6 +887,8 @@ async fn handle_passthrough_request( app_type: context.app_type.clone(), provider: forward_result.provider.clone(), current_provider_id_at_start: context.current_provider_id_at_start.clone(), + is_model_routed: context.route_source.as_deref() == Some("model_route"), + estimated_input_tokens, }); let status = response.status; let request_log = Some(RequestLogContext::from_handler( @@ -922,6 +1102,7 @@ fn compact_codex_proxy_error_message(message: &str, max_chars: usize) -> String format!("{truncated}...(truncated)") } +#[allow(clippy::too_many_arguments)] async fn finish_codex_live_aware_response( context: &HandlerContext, endpoint: &str, @@ -930,6 +1111,7 @@ async fn finish_codex_live_aware_response( streaming_first_byte_timeout: Option, non_streaming_timeout: Option, tool_context: super::providers::transform_codex_chat::CodexToolContext, + estimated_input_tokens: u64, ) -> Response { let provider = forward_result.provider; let response = forward_result.response; @@ -938,6 +1120,8 @@ async fn finish_codex_live_aware_response( app_type: context.app_type.clone(), provider: provider.clone(), current_provider_id_at_start: context.current_provider_id_at_start.clone(), + is_model_routed: context.route_source.as_deref() == Some("model_route"), + estimated_input_tokens, }); if super::providers::should_convert_codex_responses_to_chat(&provider, endpoint) { @@ -1131,7 +1315,7 @@ fn remaining_timeout(timeout: Option, started_at: Instant) -> Option Option { + let model_lower = original_model.to_lowercase(); + + if model_lower.contains("haiku") { + return self.haiku_model.clone(); + } + if model_lower.contains("opus") { + return self.opus_model.clone(); + } + if model_lower.contains("sonnet") { + return self.sonnet_model.clone(); + } + + None + } +} + +pub fn provider_has_explicit_role_mapping(provider: &Provider, original_model: &str) -> bool { + let Some(mapped) = + ModelMapping::from_provider(provider).map_explicit_role_model(original_model) + else { + return false; + }; + + maps_to_different_role_family(original_model, &mapped) +} + +fn maps_to_different_role_family(original_model: &str, mapped_model: &str) -> bool { + let Some(original_role) = claude_role_family(original_model) else { + return false; + }; + + let Some(mapped_role) = claude_role_family(mapped_model) else { + return true; + }; + + original_role != mapped_role +} + +fn claude_role_family(model: &str) -> Option<&'static str> { + let normalized = strip_one_m_suffix_for_upstream(model) + .trim() + .to_ascii_lowercase(); + + if normalized.contains("haiku") { + Some("haiku") + } else if normalized.contains("opus") { + Some("opus") + } else if normalized.contains("sonnet") { + Some("sonnet") + } else { + None + } } pub fn apply_model_mapping( @@ -127,12 +181,16 @@ mod tests { use serde_json::json; fn provider_with_mapping(mapped_model: &str) -> Provider { + provider_with_role_mapping("ANTHROPIC_DEFAULT_SONNET_MODEL", mapped_model) + } + + fn provider_with_role_mapping(key: &str, mapped_model: &str) -> Provider { Provider { id: "test".to_string(), name: "Test".to_string(), settings_config: json!({ "env": { - "ANTHROPIC_DEFAULT_SONNET_MODEL": mapped_model + key: mapped_model } }), website_url: None, @@ -186,4 +244,40 @@ mod tests { let result = strip_one_m_suffix_for_upstream_from_body(body); assert_eq!(result["model"], "deepseek-v4-pro"); } + + #[test] + fn detects_explicit_role_mapping_without_using_default_model() { + let mut provider = provider_with_mapping("deepseek-v4-pro [1M]"); + provider.settings_config["env"]["ANTHROPIC_MODEL"] = json!("default-model"); + + assert!(provider_has_explicit_role_mapping( + &provider, + "claude-sonnet-4-6[1M]" + )); + assert!(!provider_has_explicit_role_mapping( + &provider, + "some-custom-model" + )); + } + + #[test] + fn standard_claude_role_mapping_does_not_count_as_manual_override() { + let provider = + provider_with_role_mapping("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-8[1M]"); + + assert!(!provider_has_explicit_role_mapping( + &provider, + "claude-opus-4-8" + )); + } + + #[test] + fn non_claude_role_mapping_counts_as_manual_override() { + let provider = provider_with_role_mapping("ANTHROPIC_DEFAULT_OPUS_MODEL", "glm-5.1"); + + assert!(provider_has_explicit_role_mapping( + &provider, + "claude-opus-4-8" + )); + } } diff --git a/src-tauri/src/proxy/model_router.rs b/src-tauri/src/proxy/model_router.rs new file mode 100644 index 00000000..ed53e346 --- /dev/null +++ b/src-tauri/src/proxy/model_router.rs @@ -0,0 +1,636 @@ +//! Model Router — per-model provider routing engine +//! +//! When a model route matches, the request uses the route-targeted provider only (single +//! provider, no failover queue). When no model route matches, the request falls back to +//! existing ProviderRouter logic. +//! +//! Wildcard * in pattern matches zero or more characters in model name, case-insensitively. +//! Multiple matching rules resolve to the one with lowest priority number (highest priority). +//! Disabled rules (enabled=false) are never matched. + +use std::sync::Arc; + +use regex::Regex; + +use crate::database::Database; +use crate::provider::Provider; + +use super::error::ProxyError; + +// Route priority uses lower numbers as higher priority. Manual provider +// selection outranks normal automatic routes (default 0), while an explicitly +// higher-priority route (< -1) can still override it. +const MANUAL_PROVIDER_PRIORITY: i32 = -1; + +pub struct ModelRouter { + db: Arc, +} + +#[allow(dead_code)] +impl ModelRouter { + pub fn new(db: Arc) -> Self { + Self { db } + } + + /// Match a model name against stored model routes for the given app_type. + /// + /// Routes are ordered by priority ASC (lowest number = highest priority). + /// The first enabled route whose pattern matches `model` wins. + /// Returns the matched (route_id, Provider) if found, or None if no route matches. + pub async fn match_route( + &self, + app_type: &str, + model: &str, + ) -> Result, ProxyError> { + self.match_route_internal(app_type, model, None).await + } + + pub async fn match_route_respecting_manual_provider( + &self, + app_type: &str, + model: &str, + manual_provider: Option<&Provider>, + ) -> Result, ProxyError> { + self.match_route_internal(app_type, model, manual_provider) + .await + } + + async fn match_route_internal( + &self, + app_type: &str, + model: &str, + manual_provider: Option<&Provider>, + ) -> Result, ProxyError> { + if model.is_empty() { + return Ok(None); + } + + let routes = self + .db + .list_model_routes(app_type) + .map_err(|e| ProxyError::DatabaseError(format!("list_model_routes: {e}")))?; + + for route in routes { + if !route.enabled { + continue; + } + if should_skip_route_for_manual_provider( + &route.pattern, + route.priority, + model, + manual_provider, + ) { + continue; + } + + let regex = match compile_pattern(&route.pattern) { + Ok(re) => re, + Err(_) => { + log::warn!( + "model route pattern '{}' is not a valid pattern, skipping", + route.pattern + ); + continue; + } + }; + + if regex.is_match(model) { + let provider_opt = self + .db + .get_provider_by_id(&route.provider_id, app_type) + .map_err(|e| ProxyError::DatabaseError(format!("get_provider_by_id: {e}")))?; + let Some(provider) = provider_opt else { + log::warn!( + "model route matched but provider '{}' not found for app '{}' (route={}, pattern={})", + route.provider_id, app_type, route.id, route.pattern + ); + continue; + }; + // 记录命中(异步 + spawn_blocking 避免阻塞) + let db = self.db.clone(); + let route_id = route.id.clone(); + let model_str = model.to_string(); + let pattern = route.pattern.clone(); + let provider_name = provider.name.clone(); + let provider_id = provider.id.clone(); + let app_type_owned = app_type.to_string(); + tokio::task::spawn_blocking(move || { + if let Err(e) = db.record_model_route_hit(&route_id) { + log::warn!("failed to record model_route hit: {e}"); + } else { + log::info!( + "model route matched: app={app_type_owned}, model={model_str}, pattern={pattern} → provider={provider_name} (id={provider_id})" + ); + } + }); + return Ok(Some((route.id, provider))); + } + } + + Ok(None) + } +} + +fn should_skip_route_for_manual_provider( + route_pattern: &str, + route_priority: i32, + request_model: &str, + manual_provider: Option<&Provider>, +) -> bool { + manual_provider.is_some() + && route_priority >= MANUAL_PROVIDER_PRIORITY + && !route_pattern_mentions_request_role(route_pattern, request_model) +} + +fn route_pattern_mentions_request_role(route_pattern: &str, request_model: &str) -> bool { + let Some(role) = claude_role_family(request_model) else { + return false; + }; + + route_pattern.to_ascii_lowercase().contains(role) +} + +fn claude_role_family(model: &str) -> Option<&'static str> { + let model = model.to_ascii_lowercase(); + if model.contains("haiku") { + Some("haiku") + } else if model.contains("opus") { + Some("opus") + } else if model.contains("sonnet") { + Some("sonnet") + } else { + None + } +} + +/// Compile a model route pattern into a case-insensitive regex. +/// +/// The only special character is `*`, which becomes `.*`. +/// All other characters are treated as literals (regex meta-characters are escaped). +/// Exact patterns (no `*`) are anchored with `^...$`. +fn compile_pattern(pattern: &str) -> Result { + if !pattern.contains('*') { + // Exact match — anchor and escape + let escaped = regex::escape(pattern); + return Regex::new(&format!("(?i)^{escaped}$")); + } + + // Split on *, escape each segment, join with .* and anchor at the start. + // ^ prevents substring matches (e.g. "claude-*" matching "xclaude-opus"). + // Patterns that do NOT end with '*' are also anchored at the end ($): a + // suffix rule like "*-4-5" then matches only ids ending in "-4-5" and not + // "claude-haiku-4-55". Patterns ending in '*' (e.g. "claude-*", "sonnet*") + // stay open-ended prefix matches; use "*sonnet*" to match a substring. + let ends_with_wild = pattern.ends_with('*'); + let segments: Vec<&str> = pattern.split('*').collect(); + let mut regex_str = String::from("(?i)^"); + for (i, segment) in segments.iter().enumerate() { + if i > 0 { + regex_str.push_str(".*"); + } + regex_str.push_str(®ex::escape(segment)); + } + if !ends_with_wild { + regex_str.push('$'); + } + + Regex::new(®ex_str) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model_route::ModelRoute; + + fn seed_provider(db: &Database, app_type: &str, id: &str) { + // lock_conn! macro expands to a scope that uses AppError — we call the raw + // Mutex::lock to avoid requiring an AppError import here. + let guard = db.conn.lock().unwrap_or_else(|e| e.into_inner()); + guard + .execute( + "INSERT INTO providers (id, app_type, name, settings_config, meta) + VALUES (?1, ?2, ?3, '{}', '{}')", + rusqlite::params![id, app_type, id], + ) + .expect("seed provider"); + } + + fn test_route( + app_type: &str, + pattern: &str, + provider_id: &str, + priority: i32, + enabled: bool, + ) -> ModelRoute { + ModelRoute { + id: String::new(), + app_type: app_type.into(), + pattern: pattern.into(), + provider_id: provider_id.into(), + priority, + enabled, + hit_count: 0, + last_hit_at: None, + created_at: None, + updated_at: None, + } + } + + fn manual_provider(id: &str) -> Provider { + Provider { + id: id.to_string(), + name: id.to_string(), + settings_config: serde_json::json!({}), + website_url: None, + category: None, + created_at: None, + sort_index: None, + notes: None, + meta: None, + icon: None, + icon_color: None, + in_failover_queue: false, + } + } + + // --- Unit tests for compile_pattern --- + + #[test] + fn compile_pattern_exact_match() { + let re = compile_pattern("claude-sonnet-4-6").expect("compile exact pattern"); + assert!(re.is_match("claude-sonnet-4-6")); + assert!(!re.is_match("claude-sonnet-4-55")); + // Leading/trailing text should not match (anchored) + assert!(!re.is_match("prefix-claude-sonnet-4-6")); + } + + #[test] + fn compile_pattern_star_middle() { + let re = compile_pattern("*sonnet*").expect("compile *sonnet*"); + assert!(re.is_match("claude-sonnet-4-6")); + assert!(re.is_match("sonnet")); + assert!(!re.is_match("opus")); + } + + #[test] + fn compile_pattern_star_suffix() { + let re = compile_pattern("claude-*").expect("compile claude-*"); + assert!(re.is_match("claude-opus-4-8")); + assert!(!re.is_match("gemini-2.5-pro")); + // 锚定保证:前缀匹配,不可中间包含 + assert!(!re.is_match("xclaude-opus")); + } + + #[test] + fn compile_pattern_star_middle_anchored() { + // *sonnet* 加 ^ 锚定后,必须从开头匹配,但 .* 仍允许中间任意内容 + let re = compile_pattern("*sonnet*").expect("compile *sonnet*"); + assert!(re.is_match("sonnet")); + assert!(re.is_match("claude-sonnet-4-6")); + assert!(re.is_match("claude-sonnet")); + // 包含 "sonnet" 的都匹配(.*sonnet.* 语义) + assert!(re.is_match("claude- haikuxxsonnetyy")); + assert!(!re.is_match("claude-haiku-4-6")); + } + + #[test] + fn compile_pattern_prefix_anchor_prevents_substring() { + // claude-* 加 ^ 后,不再匹配 xclaude-opus + let re = compile_pattern("claude-*").expect("compile claude-*"); + assert!(re.is_match("claude-opus-4-8")); + assert!(re.is_match("claude-")); + assert!(!re.is_match("xclaude-opus")); + assert!(!re.is_match("gemini-2.5-pro")); + } + + #[test] + fn compile_pattern_star_prefix() { + let re = compile_pattern("*-4-5").expect("compile *-4-5"); + assert!(re.is_match("claude-haiku-4-5")); + assert!(re.is_match("deepseek-4-5")); + assert!(!re.is_match("claude-haiku-4-6")); + } + + #[test] + fn compile_pattern_regex_meta_chars_escaped() { + // + is a regex quantifier — should be treated as literal + let re = compile_pattern("gpt-4+").expect("compile gpt-4+"); + assert!(re.is_match("gpt-4+")); + assert!(!re.is_match("gpt-4")); + assert!(!re.is_match("gpt-4++")); + } + + // --- Integration tests for match_route (uses in-memory DB) --- + + #[tokio::test] + async fn test_match_route_exact_pattern() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-sonnet"); + + let route = test_route("claude", "claude-sonnet-4-6", "prov-sonnet", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + let result = router + .match_route("claude", "claude-sonnet-4-6") + .await + .expect("match_route"); + assert!(result.is_some()); + assert_eq!(result.unwrap().1.id, "prov-sonnet"); + } + + #[tokio::test] + async fn test_match_route_star_sonnet_star() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-sonnet"); + + let route = test_route("claude", "*sonnet*", "prov-sonnet", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "claude-sonnet-4-6") + .await + .expect("match_route") + .is_some()); + assert!(router + .match_route("claude", "sonnet") + .await + .expect("match_route") + .is_some()); + } + + #[tokio::test] + async fn test_match_route_claude_star() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-claude"); + + let route = test_route("claude", "claude-*", "prov-claude", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "claude-opus-4-8") + .await + .expect("match_route") + .is_some()); + assert!(router + .match_route("claude", "gemini-2.5-pro") + .await + .expect("match_route") + .is_none()); + } + + #[tokio::test] + async fn test_match_route_star_suffix() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-45"); + + let route = test_route("claude", "*-4-5", "prov-45", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "claude-haiku-4-5") + .await + .expect("match_route") + .is_some()); + assert!(router + .match_route("claude", "deepseek-4-5") + .await + .expect("match_route") + .is_some()); + } + + #[tokio::test] + async fn test_match_route_star_suffix_rejects_partial() { + // Regression (Codex P2): "*-4-5" must not match "claude-haiku-4-55". + // Non-trailing-* suffix rules are anchored at the end, so a longer id + // that merely contains "-4-5" as a substring is not matched. + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-45"); + + let route = test_route("claude", "*-4-5", "prov-45", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "claude-haiku-4-55") + .await + .expect("match_route") + .is_none()); + } + + #[tokio::test] + async fn test_match_route_priority() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-high"); + seed_provider(&db, "claude", "prov-low"); + + // Higher priority (lower number) should win + let route_high = test_route("claude", "*sonnet*", "prov-high", 1, true); + let route_low = test_route("claude", "*sonnet*", "prov-low", 10, true); + db.create_model_route(&route_high) + .expect("create high-priority route"); + db.create_model_route(&route_low) + .expect("create low-priority route"); + + let router = ModelRouter::new(db); + let result = router + .match_route("claude", "claude-sonnet-4-6") + .await + .expect("match_route"); + assert!(result.is_some()); + assert_eq!(result.unwrap().1.id, "prov-high"); + } + + #[tokio::test] + async fn test_match_route_disabled_skipped() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-disabled"); + + let route = test_route("claude", "*sonnet*", "prov-disabled", 1, false); + db.create_model_route(&route) + .expect("create disabled route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "claude-sonnet-4-6") + .await + .expect("match_route") + .is_none()); + } + + #[tokio::test] + async fn test_match_route_no_match() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-specific"); + + let route = test_route("claude", "claude-*", "prov-specific", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "gemini-2.5-pro") + .await + .expect("match_route") + .is_none()); + } + + #[tokio::test] + async fn test_match_route_empty_model() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-any"); + + let route = test_route("claude", "*", "prov-any", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + assert!(router + .match_route("claude", "") + .await + .expect("match_route") + .is_none()); + } + + #[tokio::test] + async fn test_match_route_case_insensitive() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-case"); + + let route = test_route("claude", "claude-sonnet-*", "prov-case", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + let result = router + .match_route("claude", "CLAUDE-SONNET-4-6") + .await + .expect("match_route"); + assert!(result.is_some()); + assert_eq!(result.unwrap().1.id, "prov-case"); + } + + #[tokio::test] + async fn test_match_route_regex_meta_chars() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "prov-meta"); + + // gpt-4+ has a literal + — the pattern's + is escaped, not a regex quantifier + let route = test_route("claude", "gpt-4+", "prov-meta", 1, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + let result = router + .match_route("claude", "gpt-4+") + .await + .expect("match_route"); + assert!(result.is_some()); + assert_eq!(result.unwrap().1.id, "prov-meta"); + } + + #[tokio::test] + async fn test_match_route_missing_provider() { + let db = Arc::new(Database::memory().expect("create memory database")); + + // FK constraint prevents create_model_route from referencing a non-existent + // provider. Disable foreign keys to insert a dangling route, then re-enable. + let guard = db.conn.lock().unwrap_or_else(|e| e.into_inner()); + guard + .execute_batch("PRAGMA foreign_keys = OFF") + .expect("disable foreign keys"); + guard + .execute( + "INSERT INTO model_routes (id, app_type, pattern, provider_id, priority, enabled) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + rusqlite::params![ + uuid::Uuid::new_v4().to_string(), + "claude", + "*-missing", + "prov-missing", + 1, + true + ], + ) + .expect("insert dangling model route"); + guard + .execute_batch("PRAGMA foreign_keys = ON") + .expect("re-enable foreign keys"); + drop(guard); + + let router = ModelRouter::new(db); + let result = router + .match_route("claude", "claude-missing") + .await + .expect("match_route"); + // Provider doesn't exist — get_provider_by_id returns None + assert!(result.is_none()); + } + + #[tokio::test] + async fn normal_route_priority_yields_to_manual_provider() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "automatic-provider"); + + let route = test_route("claude", "*", "automatic-provider", 0, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + let manual_provider = manual_provider("manually-selected"); + let result = router + .match_route_respecting_manual_provider( + "claude", + "any-request-model", + Some(&manual_provider), + ) + .await + .expect("match route"); + + assert!(result.is_none()); + } + + #[tokio::test] + async fn role_specific_route_priority_does_not_yield_to_manual_provider() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "opus-provider"); + + let route = test_route("claude", "*opus*", "opus-provider", 0, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + let manual_provider = manual_provider("manually-selected"); + let result = router + .match_route_respecting_manual_provider( + "claude", + "claude-opus-4-8", + Some(&manual_provider), + ) + .await + .expect("match route") + .expect("role-specific route should match"); + + assert_eq!(result.1.id, "opus-provider"); + } + + #[tokio::test] + async fn explicit_higher_priority_route_can_override_manual_provider() { + let db = Arc::new(Database::memory().expect("create memory database")); + seed_provider(&db, "claude", "explicit-route-provider"); + + let route = test_route("claude", "*", "explicit-route-provider", -2, true); + db.create_model_route(&route).expect("create route"); + + let router = ModelRouter::new(db); + let manual_provider = manual_provider("manually-selected"); + let result = router + .match_route_respecting_manual_provider( + "claude", + "any-request-model", + Some(&manual_provider), + ) + .await + .expect("match route") + .expect("higher-priority route should match"); + + assert_eq!(result.1.id, "explicit-route-provider"); + } +} diff --git a/src-tauri/src/proxy/providers/transform_gemini.rs b/src-tauri/src/proxy/providers/transform_gemini.rs index 44f3f47f..ebdb3f72 100644 --- a/src-tauri/src/proxy/providers/transform_gemini.rs +++ b/src-tauri/src/proxy/providers/transform_gemini.rs @@ -39,6 +39,7 @@ pub(crate) fn is_synthesized_tool_call_id(id: &str) -> bool { id.starts_with(SYNTHESIZED_ID_PREFIX) } +#[allow(dead_code)] pub fn anthropic_to_gemini(body: Value) -> Result { anthropic_to_gemini_with_shadow(body, None, None, None) } diff --git a/src-tauri/src/proxy/response_handler.rs b/src-tauri/src/proxy/response_handler.rs index 99d9f592..3811ebb4 100644 --- a/src-tauri/src/proxy/response_handler.rs +++ b/src-tauri/src/proxy/response_handler.rs @@ -28,6 +28,13 @@ pub struct SuccessSyncInfo { pub app_type: AppType, pub provider: Provider, pub current_provider_id_at_start: String, + /// 当为 true 时,跳过 set_current_provider / update_live_backup, + /// 因为 provider 是模型路由命中选中的,不是用户主动切换的。 + pub is_model_routed: bool, + /// 该请求估算的 input token(请求入口算出,随请求带到此处按 provider 归类)。 + /// 用于让 per-provider 活动统计同时覆盖 input 流量,使点阵图 input/output 波形 + /// 都能正确按 provider 着色。 + pub estimated_input_tokens: u64, } impl ResponseHandler { @@ -55,6 +62,15 @@ impl ResponseHandler { state .record_estimated_output_tokens(estimated_output_tokens) .await; + if let Some(ref sync) = success_sync { + state + .record_provider_activity( + &sync.provider.id, + sync.estimated_input_tokens + .saturating_add(estimated_output_tokens), + ) + .await; + } if status.is_success() { if let Some(success_sync) = success_sync { state @@ -62,6 +78,7 @@ impl ResponseHandler { &success_sync.app_type, &success_sync.provider, &success_sync.current_provider_id_at_start, + success_sync.is_model_routed, ) .await; } @@ -281,12 +298,20 @@ impl StreamingOutcomeRecorder { state .record_estimated_output_tokens(estimated_output_tokens) .await; - if let Some(success_sync) = success_sync { + if let Some(ref sync) = success_sync { + state + .record_provider_activity( + &sync.provider.id, + sync.estimated_input_tokens + .saturating_add(estimated_output_tokens), + ) + .await; state .sync_successful_provider_selection( - &success_sync.app_type, - &success_sync.provider, - &success_sync.current_provider_id_at_start, + &sync.app_type, + &sync.provider, + &sync.current_provider_id_at_start, + sync.is_model_routed, ) .await; } @@ -306,12 +331,20 @@ impl StreamingOutcomeRecorder { state .record_estimated_output_tokens(estimated_output_tokens) .await; - if let Some(success_sync) = success_sync { + if let Some(ref sync) = success_sync { + state + .record_provider_activity( + &sync.provider.id, + sync.estimated_input_tokens + .saturating_add(estimated_output_tokens), + ) + .await; state .sync_successful_provider_selection( - &success_sync.app_type, - &success_sync.provider, - &success_sync.current_provider_id_at_start, + &sync.app_type, + &sync.provider, + &sync.current_provider_id_at_start, + sync.is_model_routed, ) .await; } diff --git a/src-tauri/src/proxy/response_handler/tests.rs b/src-tauri/src/proxy/response_handler/tests.rs index d50aeea7..0183f34d 100644 --- a/src-tauri/src/proxy/response_handler/tests.rs +++ b/src-tauri/src/proxy/response_handler/tests.rs @@ -16,8 +16,8 @@ use crate::{ database::Database, provider::Provider, proxy::{ - provider_router::ProviderRouter, providers::gemini_shadow::GeminiShadowStore, - types::ProxyConfig, + model_router::ModelRouter, provider_router::ProviderRouter, + providers::gemini_shadow::GeminiShadowStore, types::ProxyConfig, }, test_support::TestEnvGuard, }; @@ -52,9 +52,11 @@ fn test_state_with_db(db: Arc) -> ProxyServerState { status: Arc::new(RwLock::new(crate::proxy::types::ProxyStatus::default())), start_time: Arc::new(RwLock::new(None)), current_providers: Arc::new(RwLock::new(HashMap::new())), - provider_router: Arc::new(ProviderRouter::new(db)), + provider_router: Arc::new(ProviderRouter::new(db.clone())), + model_router: Arc::new(ModelRouter::new(db)), codex_chat_history: Arc::new(Default::default()), gemini_shadow: Arc::new(GeminiShadowStore::default()), + provider_token_map: Arc::new(RwLock::new(HashMap::new())), } } @@ -107,6 +109,61 @@ async fn buffered_failures_still_accumulate_output_tokens() { assert_eq!(snapshot.estimated_output_tokens_total, 9); } +#[tokio::test] +async fn buffered_success_records_input_and_output_tokens_per_provider() { + // provider_token_map 应同时累积 input + output token(按服务 provider 归类), + // 让点阵图 input/output 波形都能正确按 provider 着色。 + let state = test_state(); + state.record_request_start().await; + + let provider = test_provider_with_settings( + "zhipu", + "Zhipu", + json!({"apiKey": "zhipu-key", "base_url": "https://zhipu.example"}), + ); + let estimated_input_tokens: u64 = 4_000u64; + let estimated_output_tokens: u64 = 600u64; + + let response = PreparedResponse { + response: Response::builder() + .status(StatusCode::OK) + .body(Body::from("ok response body")) + .expect("response"), + stream_completion: None, + estimated_output_tokens, + upstream_error_summary: None, + body_bytes: Some(Bytes::from_static(b"ok response body")), + }; + + let _ = ResponseHandler::finish_buffered( + &state, + Ok(response), + reqwest::StatusCode::OK, + Some(SuccessSyncInfo { + app_type: AppType::Claude, + provider: provider.clone(), + current_provider_id_at_start: provider.id.clone(), + is_model_routed: false, + estimated_input_tokens, + }), + None, + ) + .await; + settle_tasks().await; + + let snapshot = state.snapshot_status().await; + let recorded = snapshot + .provider_token_map + .get(&provider.id) + .copied() + .unwrap_or(0); + assert!( + recorded >= estimated_input_tokens.saturating_add(estimated_output_tokens), + "provider_token_map should record input+output tokens (>= {}), got {recorded}", + estimated_input_tokens.saturating_add(estimated_output_tokens) + ); +} + #[tokio::test] async fn interrupted_streams_keep_partial_output_estimate() { let state = test_state(); @@ -273,6 +330,8 @@ async fn streaming_success_syncs_failover_state_after_body_drains() { app_type: AppType::Claude, provider: failover.clone(), current_provider_id_at_start: current.id.clone(), + is_model_routed: false, + estimated_input_tokens: 0, }), None, ) diff --git a/src-tauri/src/proxy/server.rs b/src-tauri/src/proxy/server.rs index 745b07f5..6a9e932a 100644 --- a/src-tauri/src/proxy/server.rs +++ b/src-tauri/src/proxy/server.rs @@ -19,6 +19,7 @@ use super::{ circuit_breaker::CircuitBreakerConfig, error::ProxyError, handlers, + model_router::ModelRouter, provider_router::ProviderRouter, providers::codex_chat_history::CodexChatHistoryStore, providers::gemini_shadow::GeminiShadowStore, @@ -35,8 +36,10 @@ pub struct ProxyServerState { pub start_time: Arc>>, pub current_providers: Arc>>, pub provider_router: Arc, + pub model_router: Arc, pub codex_chat_history: Arc, pub gemini_shadow: Arc, + pub provider_token_map: Arc>>, } impl ProxyServerState { @@ -61,6 +64,8 @@ impl ProxyServerState { active_targets.sort_by(|left, right| left.app_type.cmp(&right.app_type)); status.active_targets = active_targets; + status.provider_token_map = self.provider_token_map.read().await.clone(); + status } @@ -91,6 +96,14 @@ impl ProxyServerState { status.estimated_output_tokens_total.saturating_add(tokens); } + /// 按 provider 记录预估 token 数,用于仪表盘点阵图多色展示。 + /// 即使 token 估算为 0(非流式响应无 char_count 估算),也至少记录一次 + /// 命中计数为 1,避免点阵图因 estimated_output_tokens == 0 而完全空。 + pub async fn record_provider_activity(&self, provider_id: &str, tokens: u64) { + let mut map = self.provider_token_map.write().await; + *map.entry(provider_id.to_string()).or_default() += tokens.max(1); + } + pub async fn record_active_target(&self, app_type: &AppType, provider: &Provider) { self.current_providers.write().await.insert( app_type.as_str().to_string(), @@ -107,6 +120,7 @@ impl ProxyServerState { app_type: &AppType, provider: &Provider, current_provider_id_at_start: &str, + is_model_routed: bool, ) { self.record_active_target(app_type, provider).await; @@ -114,6 +128,12 @@ impl ProxyServerState { return; } + // 模型路由选中的 provider 不应切换当前 provider / 更新 live backup。 + // 路由命中是瞬态行为,不应覆盖用户主动选择的 provider。 + if is_model_routed { + return; + } + let takeover_enabled = self .db .get_proxy_config_for_app(app_type.as_str()) @@ -175,215 +195,6 @@ impl ProxyServerState { } } -fn update_success_rate(status: &mut ProxyStatus) { - status.success_rate = if status.total_requests == 0 { - 0.0 - } else { - (status.success_requests as f32 / status.total_requests as f32) * 100.0 - }; -} - -pub struct ProxyServer { - state: ProxyServerState, - shutdown_tx: Arc>>>, - server_handle: Arc>>>, -} - -impl ProxyServer { - pub fn new(config: ProxyConfig, db: Arc) -> Self { - let provider_router = Arc::new(ProviderRouter::new(db.clone())); - let managed_session_token = std::env::var(PROXY_RUNTIME_SESSION_TOKEN_ENV_KEY) - .ok() - .filter(|value| !value.trim().is_empty()); - let status = ProxyStatus { - managed_session_token, - ..ProxyStatus::default() - }; - - Self { - state: ProxyServerState { - db, - config: Arc::new(RwLock::new(config)), - status: Arc::new(RwLock::new(status)), - start_time: Arc::new(RwLock::new(None)), - current_providers: Arc::new(RwLock::new(HashMap::new())), - provider_router, - codex_chat_history: Arc::new(CodexChatHistoryStore::default()), - gemini_shadow: Arc::new(GeminiShadowStore::default()), - }, - shutdown_tx: Arc::new(RwLock::new(None)), - server_handle: Arc::new(RwLock::new(None)), - } - } - - pub async fn start(&self) -> Result { - if self.shutdown_tx.read().await.is_some() { - let status = self.get_status().await; - return Ok(ProxyServerInfo { - address: status.address, - port: status.port, - started_at: chrono::Utc::now().to_rfc3339(), - }); - } - - let bind_config = self.state.config.read().await.clone(); - let addr: SocketAddr = - format!("{}:{}", bind_config.listen_address, bind_config.listen_port) - .parse() - .map_err(|e| format!("invalid bind address: {e}"))?; - - let listener = match tokio::net::TcpListener::bind(addr).await { - Ok(listener) => listener, - Err(error) if error.kind() == ErrorKind::AddrInUse && addr.port() != 0 => { - let fallback_addr = SocketAddr::new(addr.ip(), 0); - log::warn!( - "proxy listen address {} is already in use; retrying with ephemeral port", - addr - ); - tokio::net::TcpListener::bind(fallback_addr) - .await - .map_err(|fallback_error| { - format!( - "bind proxy listener failed after {} was in use: {}", - addr, fallback_error - ) - })? - } - Err(error) => return Err(format!("bind proxy listener failed: {error}")), - }; - let local_addr = listener - .local_addr() - .map_err(|e| format!("read proxy listener address failed: {e}"))?; - - super::http_client::set_proxy_port(local_addr.port()); - - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - *self.shutdown_tx.write().await = Some(shutdown_tx); - - { - let mut status = self.state.status.write().await; - status.running = true; - status.address = bind_config.listen_address.clone(); - status.port = local_addr.port(); - } - *self.state.start_time.write().await = Some(Instant::now()); - - let app = self.build_router(); - let state = self.state.clone(); - let handle = tokio::spawn(async move { - let _ = axum::serve(listener, app) - .with_graceful_shutdown(async { - let _ = shutdown_rx.await; - }) - .await; - - state.status.write().await.running = false; - *state.start_time.write().await = None; - }); - *self.server_handle.write().await = Some(handle); - - Ok(ProxyServerInfo { - address: bind_config.listen_address, - port: local_addr.port(), - started_at: chrono::Utc::now().to_rfc3339(), - }) - } - - pub async fn set_active_target(&self, app_type: &str, provider_id: &str, provider_name: &str) { - let mut current_providers = self.state.current_providers.write().await; - current_providers.insert( - app_type.to_string(), - (provider_id.to_string(), provider_name.to_string()), - ); - } - - pub async fn stop(&self) -> Result<(), String> { - if let Some(tx) = self.shutdown_tx.write().await.take() { - let _ = tx.send(()); - } else { - return Ok(()); - } - - if let Some(handle) = self.server_handle.write().await.take() { - handle - .await - .map_err(|e| format!("join proxy task failed: {e}"))?; - } - Ok(()) - } - - pub async fn get_status(&self) -> ProxyStatus { - self.state.snapshot_status().await - } - - pub async fn update_circuit_breaker_configs(&self, config: CircuitBreakerConfig) { - self.state.provider_router.update_all_configs(config).await; - } - - pub async fn reset_provider_circuit_breaker(&self, provider_id: &str, app_type: &str) { - self.state - .provider_router - .reset_provider_breaker(provider_id, app_type) - .await; - } - - #[cfg(test)] - pub(crate) fn provider_router(&self) -> Arc { - self.state.provider_router.clone() - } - - fn build_router(&self) -> Router { - let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any); - - Router::new() - .route("/health", get(handlers::health_check)) - .route("/status", get(handlers::get_status)) - .route("/v1/messages", post(handlers::handle_messages)) - .route("/claude/v1/messages", post(handlers::handle_messages)) - .route("/chat/completions", post(handlers::handle_chat_completions)) - .route( - "/v1/chat/completions", - post(handlers::handle_chat_completions), - ) - .route( - "/v1/v1/chat/completions", - post(handlers::handle_chat_completions), - ) - .route( - "/codex/v1/chat/completions", - post(handlers::handle_chat_completions), - ) - .route("/responses", post(handlers::handle_responses)) - .route("/v1/responses", post(handlers::handle_responses)) - .route("/v1/v1/responses", post(handlers::handle_responses)) - .route("/codex/v1/responses", post(handlers::handle_responses)) - .route( - "/responses/compact", - post(handlers::handle_responses_compact), - ) - .route( - "/v1/responses/compact", - post(handlers::handle_responses_compact), - ) - .route( - "/v1/v1/responses/compact", - post(handlers::handle_responses_compact), - ) - .route( - "/codex/v1/responses/compact", - post(handlers::handle_responses_compact), - ) - .route("/v1beta/*path", post(handlers::handle_gemini)) - .route("/gemini/v1beta/*path", post(handlers::handle_gemini)) - .layer(DefaultBodyLimit::max(200 * 1024 * 1024)) - .layer(cors) - .with_state(self.state.clone()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -488,9 +299,11 @@ mod tests { status: Arc::new(RwLock::new(ProxyStatus::default())), start_time: Arc::new(RwLock::new(None)), current_providers: Arc::new(RwLock::new(HashMap::new())), - provider_router: Arc::new(ProviderRouter::new(db)), + provider_router: Arc::new(ProviderRouter::new(db.clone())), + model_router: Arc::new(ModelRouter::new(db)), codex_chat_history: Arc::new(CodexChatHistoryStore::default()), gemini_shadow: Arc::new(GeminiShadowStore::default()), + provider_token_map: Arc::new(RwLock::new(HashMap::new())), } } @@ -539,7 +352,7 @@ mod tests { let state = test_state(db.clone()); state - .sync_successful_provider_selection(&AppType::Claude, &failover, ¤t.id) + .sync_successful_provider_selection(&AppType::Claude, &failover, ¤t.id, false) .await; assert_eq!( @@ -552,6 +365,9 @@ mod tests { crate::settings::get_current_provider(&AppType::Claude).as_deref(), Some("claude-failover") ); + // The original takeover live backup must be preserved verbatim: auto-failover + // writes a separate failover snapshot instead of mutating proxy_live_backup, so + // restore_live_config_for_app still restores the user's original provider config. let backup = db .get_live_backup("claude") .await @@ -566,15 +382,16 @@ mod tests { Some("https://current.example") ); - let snapshot = db - .get_failover_live_snapshot("claude", "claude-failover") + // The failover provider's config is captured in a dedicated failover snapshot. + let failover_snapshot = db + .get_failover_live_snapshot("claude", &failover.id) .await .expect("read failover snapshot after sync") - .expect("failover snapshot should exist"); - let snapshot_value: serde_json::Value = - serde_json::from_str(&snapshot.config_json).expect("parse failover snapshot"); + .expect("failover snapshot should be written"); + let failover_config: serde_json::Value = + serde_json::from_str(&failover_snapshot.config_json).expect("parse failover snapshot"); assert_eq!( - snapshot_value + failover_config .get("base_url") .and_then(serde_json::Value::as_str), Some("https://failover.example") @@ -604,7 +421,7 @@ mod tests { let state = test_state(db.clone()); state - .sync_successful_provider_selection(&AppType::Claude, ¤t, ¤t.id) + .sync_successful_provider_selection(&AppType::Claude, ¤t, ¤t.id, false) .await; assert_eq!( @@ -658,7 +475,7 @@ mod tests { let state = test_state(db.clone()); state - .sync_successful_provider_selection(&AppType::Claude, &failover, ¤t.id) + .sync_successful_provider_selection(&AppType::Claude, &failover, ¤t.id, false) .await; assert_eq!( @@ -696,3 +513,216 @@ mod tests { assert_eq!(status.active_targets[0].provider_id, "claude-failover"); } } + +fn update_success_rate(status: &mut ProxyStatus) { + status.success_rate = if status.total_requests == 0 { + 0.0 + } else { + (status.success_requests as f32 / status.total_requests as f32) * 100.0 + }; +} + +pub struct ProxyServer { + state: ProxyServerState, + shutdown_tx: Arc>>>, + server_handle: Arc>>>, +} + +impl ProxyServer { + pub fn new(config: ProxyConfig, db: Arc) -> Self { + let provider_router = Arc::new(ProviderRouter::new(db.clone())); + let model_router = Arc::new(ModelRouter::new(db.clone())); + let managed_session_token = std::env::var(PROXY_RUNTIME_SESSION_TOKEN_ENV_KEY) + .ok() + .filter(|value| !value.trim().is_empty()); + let status = ProxyStatus { + managed_session_token, + ..ProxyStatus::default() + }; + + Self { + state: ProxyServerState { + db, + config: Arc::new(RwLock::new(config)), + status: Arc::new(RwLock::new(status)), + start_time: Arc::new(RwLock::new(None)), + current_providers: Arc::new(RwLock::new(HashMap::new())), + provider_router, + model_router, + codex_chat_history: Arc::new(CodexChatHistoryStore::default()), + gemini_shadow: Arc::new(GeminiShadowStore::default()), + provider_token_map: Arc::new(RwLock::new(HashMap::new())), + }, + shutdown_tx: Arc::new(RwLock::new(None)), + server_handle: Arc::new(RwLock::new(None)), + } + } + + pub async fn start(&self) -> Result { + if self.shutdown_tx.read().await.is_some() { + let status = self.get_status().await; + return Ok(ProxyServerInfo { + address: status.address, + port: status.port, + started_at: chrono::Utc::now().to_rfc3339(), + }); + } + + let bind_config = self.state.config.read().await.clone(); + let addr: SocketAddr = + format!("{}:{}", bind_config.listen_address, bind_config.listen_port) + .parse() + .map_err(|e| format!("invalid bind address: {e}"))?; + + let listener = match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => listener, + Err(error) if error.kind() == ErrorKind::AddrInUse && addr.port() != 0 => { + let fallback_addr = SocketAddr::new(addr.ip(), 0); + log::warn!( + "proxy listen address {} is already in use; retrying with ephemeral port", + addr + ); + tokio::net::TcpListener::bind(fallback_addr) + .await + .map_err(|fallback_error| { + format!( + "bind proxy listener failed after {} was in use: {}", + addr, fallback_error + ) + })? + } + Err(error) => return Err(format!("bind proxy listener failed: {error}")), + }; + let local_addr = listener + .local_addr() + .map_err(|e| format!("read proxy listener address failed: {e}"))?; + + super::http_client::set_proxy_port(local_addr.port()); + + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + *self.shutdown_tx.write().await = Some(shutdown_tx); + + { + let mut status = self.state.status.write().await; + status.running = true; + status.address = bind_config.listen_address.clone(); + status.port = local_addr.port(); + } + *self.state.start_time.write().await = Some(Instant::now()); + + let app = self.build_router(); + let state = self.state.clone(); + let handle = tokio::spawn(async move { + let _ = axum::serve(listener, app) + .with_graceful_shutdown(async { + let _ = shutdown_rx.await; + }) + .await; + + state.status.write().await.running = false; + *state.start_time.write().await = None; + }); + *self.server_handle.write().await = Some(handle); + + Ok(ProxyServerInfo { + address: bind_config.listen_address, + port: local_addr.port(), + started_at: chrono::Utc::now().to_rfc3339(), + }) + } + + pub async fn set_active_target(&self, app_type: &str, provider_id: &str, provider_name: &str) { + let mut current_providers = self.state.current_providers.write().await; + current_providers.insert( + app_type.to_string(), + (provider_id.to_string(), provider_name.to_string()), + ); + } + + pub async fn stop(&self) -> Result<(), String> { + if let Some(tx) = self.shutdown_tx.write().await.take() { + let _ = tx.send(()); + } else { + return Ok(()); + } + + if let Some(handle) = self.server_handle.write().await.take() { + handle + .await + .map_err(|e| format!("join proxy task failed: {e}"))?; + } + Ok(()) + } + + pub async fn get_status(&self) -> ProxyStatus { + self.state.snapshot_status().await + } + + pub async fn update_circuit_breaker_configs(&self, config: CircuitBreakerConfig) { + self.state.provider_router.update_all_configs(config).await; + } + + pub async fn reset_provider_circuit_breaker(&self, provider_id: &str, app_type: &str) { + self.state + .provider_router + .reset_provider_breaker(provider_id, app_type) + .await; + } + + #[cfg(test)] + pub(crate) fn provider_router(&self) -> Arc { + self.state.provider_router.clone() + } + + fn build_router(&self) -> Router { + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + Router::new() + .route("/health", get(handlers::health_check)) + .route("/status", get(handlers::get_status)) + .route("/v1/models", get(handlers::handle_models)) + .route("/v1/messages", post(handlers::handle_messages)) + .route("/claude/v1/messages", post(handlers::handle_messages)) + .route("/chat/completions", post(handlers::handle_chat_completions)) + .route( + "/v1/chat/completions", + post(handlers::handle_chat_completions), + ) + .route( + "/v1/v1/chat/completions", + post(handlers::handle_chat_completions), + ) + .route( + "/codex/v1/chat/completions", + post(handlers::handle_chat_completions), + ) + .route("/responses", post(handlers::handle_responses)) + .route("/v1/responses", post(handlers::handle_responses)) + .route("/v1/v1/responses", post(handlers::handle_responses)) + .route("/codex/v1/responses", post(handlers::handle_responses)) + .route( + "/responses/compact", + post(handlers::handle_responses_compact), + ) + .route( + "/v1/responses/compact", + post(handlers::handle_responses_compact), + ) + .route( + "/v1/v1/responses/compact", + post(handlers::handle_responses_compact), + ) + .route( + "/codex/v1/responses/compact", + post(handlers::handle_responses_compact), + ) + .route("/v1beta/*path", post(handlers::handle_gemini)) + .route("/gemini/v1beta/*path", post(handlers::handle_gemini)) + .layer(DefaultBodyLimit::max(200 * 1024 * 1024)) + .layer(cors) + .with_state(self.state.clone()) + } +} diff --git a/src-tauri/src/proxy/types.rs b/src-tauri/src/proxy/types.rs index 19da6211..1a1577a2 100644 --- a/src-tauri/src/proxy/types.rs +++ b/src-tauri/src/proxy/types.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use serde::{Deserialize, Serialize}; @@ -103,6 +103,9 @@ pub struct ProxyStatus { /// 当前活跃的 daemon-managed worker 列表 #[serde(default)] pub active_workers: Vec, + /// 按 provider 聚合的预估 token 数(provider_id → token_count) + #[serde(default)] + pub provider_token_map: HashMap, } /// 活跃的 daemon-managed worker 信息 diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 9135c1ea..b00b10fc 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -2408,7 +2408,11 @@ impl ProxyService { let mut effective_provider = provider.clone(); effective_provider.settings_config = self.build_live_snapshot_from_provider(&AppType::Claude, provider)?; - let mut effective_settings = effective_provider.settings_config.clone(); + let live_settings = self.read_claude_live().ok(); + let mut effective_settings = Self::merge_claude_live_overlay( + live_settings.as_ref(), + &effective_provider.settings_config, + ); let (proxy_url, _) = self.build_proxy_urls_for_app(&AppType::Claude).await?; Self::apply_claude_takeover_fields_for_provider( @@ -2419,6 +2423,21 @@ impl ProxyService { self.write_claude_live(&effective_settings) } + fn merge_claude_live_overlay(live: Option<&Value>, provider_snapshot: &Value) -> Value { + let (Some(live_object), Some(provider_object)) = ( + live.and_then(Value::as_object), + provider_snapshot.as_object(), + ) else { + return provider_snapshot.clone(); + }; + + let mut merged = live_object.clone(); + for (key, value) in provider_object { + merged.insert(key.clone(), value.clone()); + } + Value::Object(merged) + } + pub async fn switch_proxy_target( &self, app_type: &str, @@ -7959,7 +7978,12 @@ requires_openai_auth = true "ANTHROPIC_MODEL": "stale-model", "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet" }, - "permissions": { "allow": ["Bash"] } + "permissions": { "allow": ["Bash"] }, + "statusLine": { + "type": "command", + "command": "~/.claude/statusline.sh", + "padding": 0 + } })) .expect("seed taken-over live file"); @@ -7974,6 +7998,12 @@ requires_openai_auth = true provider_b.settings_config.get("permissions"), "provider-derived live settings should be refreshed" ); + assert_eq!( + live.get("statusLine") + .and_then(|value| value.get("command")), + Some(&json!("~/.claude/statusline.sh")), + "Claude live-only statusLine should survive proxy target refresh" + ); let env = env_object(&live); assert_env_str( env, diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index 004557cf..6b24984a 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -1514,6 +1514,7 @@ impl SkillService { Ok(skills) } + #[allow(clippy::redundant_closure)] pub async fn search_skills_sh( &self, query: &str, @@ -1602,6 +1603,7 @@ impl SkillService { }) .collect(); + #[allow(clippy::needless_borrow)] // Add local SSOT-only skills not in repos. Self::merge_local_ssot_skills(&index, &mut out)?; diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 6e6f5e96..573de540 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -443,9 +443,6 @@ pub struct AppSettings { /// TUI appearance: "auto" | "dark" | "light" (absent = auto). #[serde(default, skip_serializing_if = "Option::is_none")] pub theme: Option, - /// TUI icon rendering: "auto" | "emoji" | "ascii" (absent = auto). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub icons: Option, /// 是否开机自启 #[serde(default)] pub launch_on_startup: bool, @@ -517,7 +514,6 @@ impl Default for AppSettings { visible_apps_settings: VisibleAppsSettings::default(), language: None, theme: None, - icons: None, launch_on_startup: false, preserve_codex_official_auth_on_switch: false, unify_codex_session_history: false, @@ -971,19 +967,6 @@ pub fn set_theme_mode(mode: &str) -> Result<(), AppError> { update_settings(settings) } -pub fn get_icon_mode() -> Option { - settings_store() - .read() - .ok() - .and_then(|settings| settings.icons.clone()) -} - -pub fn set_icon_mode(mode: &str) -> Result<(), AppError> { - let mut settings = get_settings(); - settings.icons = Some(mode.to_string()); - update_settings(settings) -} - pub fn get_visible_apps() -> VisibleApps { settings_store() .read() diff --git a/src-tauri/tests/deeplink_import.rs b/src-tauri/tests/deeplink_import.rs index caad7f78..fe634858 100644 --- a/src-tauri/tests/deeplink_import.rs +++ b/src-tauri/tests/deeplink_import.rs @@ -129,73 +129,6 @@ fn deeplink_import_codex_provider_builds_auth_and_config() { assert!(persisted.is_some(), "provider should be persisted to db"); } -/// Regression for issue #333: a deeplink-imported Codex provider must carry a -/// non-empty `name` in its `[model_providers.custom]` table, otherwise Codex -/// refuses to load config.toml ("provider name must not be empty"). Mirrors the -/// upstream `build_codex_settings_uses_custom_key_and_preserves_display_name`. -#[test] -fn deeplink_import_codex_provider_preserves_display_name_in_config() { - let _guard = lock_test_mutex(); - reset_test_fs(); - let _home = ensure_test_home(); - - // Name carries a quote to exercise TOML escaping; model is omitted to hit - // the default. `%22` decodes to `"`. - let url = "ccswitch://v1/import?resource=provider&app=codex&name=My%20%22Relay%22&endpoint=https%3A%2F%2Fapi.example.com%2Fv1%2F&apiKey=sk-test"; - let request = parse_deeplink_url(url).expect("parse deeplink url"); - - let mut config = MultiAppConfig::default(); - config.ensure_app(&AppType::Codex); - let state = state_from_config(config); - - let provider_id = - import_provider_from_deeplink(&state, request).expect("import provider from deeplink"); - - let guard = state.config.read().expect("read config"); - let provider = guard - .get_manager(&AppType::Codex) - .expect("codex manager should exist") - .providers - .get(&provider_id) - .expect("provider created via deeplink"); - let config_text = provider - .settings_config - .get("config") - .and_then(|v| v.as_str()) - .expect("config text"); - - // Must parse as valid Codex config (this is what Codex itself does). - let parsed: toml::Value = toml::from_str(config_text).expect("valid Codex config.toml"); - assert_eq!( - parsed.get("model_provider").and_then(|v| v.as_str()), - Some("custom"), - "deeplink Codex import should use the shared `custom` model_provider id" - ); - let custom = parsed - .get("model_providers") - .and_then(|v| v.get("custom")) - .expect("[model_providers.custom] table"); - assert_eq!( - custom.get("name").and_then(|v| v.as_str()), - Some("My \"Relay\""), - "provider display name must be preserved (issue #333)" - ); - assert_eq!( - custom.get("base_url").and_then(|v| v.as_str()), - Some("https://api.example.com/v1"), - "trailing slash should be trimmed from the endpoint" - ); - assert_eq!( - custom.get("requires_openai_auth").and_then(|v| v.as_bool()), - Some(true) - ); - assert_eq!( - parsed.get("model").and_then(|v| v.as_str()), - Some("gpt-5-codex"), - "omitted model should fall back to the default" - ); -} - #[test] fn deeplink_import_openclaw_provider_defaults_to_openai_completions_api() { let _guard = lock_test_mutex(); diff --git a/target/rust-analyzer/flycheck0/stderr b/target/rust-analyzer/flycheck0/stderr new file mode 100644 index 00000000..002790a3 --- /dev/null +++ b/target/rust-analyzer/flycheck0/stderr @@ -0,0 +1,90 @@ + 0.212059841s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: stale: changed "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/cli/tui/app/menu.rs" + 0.212081276s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: (vs) "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/.fingerprint/cc-switch-287f70b9be283cef/dep-lib-cc_switch_lib" + 0.212084097s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: FileTime { seconds: 1783474442, nanos: 766517298 } < FileTime { seconds: 1783474454, nanos: 60349984 } + 0.212532624s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: false }/TargetInner { ..: lib_target("cc_switch_lib", ["rlib"], "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/lib.rs", Edition2021) } + 0.212551159s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleItem(ChangedFile { reference: "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/.fingerprint/cc-switch-287f70b9be283cef/dep-lib-cc_switch_lib", reference_mtime: FileTime { seconds: 1783474442, nanos: 766517298 }, stale: "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/cli/tui/app/menu.rs", stale_mtime: FileTime { seconds: 1783474454, nanos: 60349984 } })) + 0.223941024s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: stale: changed "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/proxy/handlers.rs" + 0.223956195s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: (vs) "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/.fingerprint/cc-switch-f7640c01f82ba8a7/dep-test-lib-cc_switch_lib" + 0.223958336s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: FileTime { seconds: 1783474442, nanos: 766517298 } < FileTime { seconds: 1783474454, nanos: 72728821 } + 0.224044431s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { ..: lib_target("cc_switch_lib", ["rlib"], "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/lib.rs", Edition2021) } + 0.224053918s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc_switch_lib"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleItem(ChangedFile { reference: "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/.fingerprint/cc-switch-f7640c01f82ba8a7/dep-test-lib-cc_switch_lib", reference_mtime: FileTime { seconds: 1783474442, nanos: 766517298 }, stale: "/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/proxy/handlers.rs", stale_mtime: FileTime { seconds: 1783474454, nanos: 72728821 } })) + 0.225438795s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc-switch"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: false }/TargetInner { name: "cc-switch", doc: true, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/main.rs", Edition2021) } + 0.225446647s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc-switch"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.226143326s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc-switch"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { name: "cc-switch", doc: true, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/main.rs", Edition2021) } + 0.226148558s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="cc-switch"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.226829541s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="app_config_load"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "app_config_load", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/app_config_load.rs", Edition2021) } + 0.226835572s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="app_config_load"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.227514573s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="app_type_parse"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "app_type_parse", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/app_type_parse.rs", Edition2021) } + 0.227520266s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="app_type_parse"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.228194982s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="claude_plugin_integration_sync"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "claude_plugin_integration_sync", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/claude_plugin_integration_sync.rs", Edition2021) } + 0.228201989s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="claude_plugin_integration_sync"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.229079038s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="completions_command"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "completions_command", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/completions_command.rs", Edition2021) } + 0.229085117s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="completions_command"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.229805347s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="deeplink_import"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "deeplink_import", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/deeplink_import.rs", Edition2021) } + 0.229812238s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="deeplink_import"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.230494272s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="import_export_sync"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "import_export_sync", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/import_export_sync.rs", Edition2021) } + 0.230500655s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="import_export_sync"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.231164898s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="install_script"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "install_script", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/install_script.rs", Edition2021) } + 0.231169698s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="install_script"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.231825287s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="mcp_commands"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "mcp_commands", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/mcp_commands.rs", Edition2021) } + 0.231830230s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="mcp_commands"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.232485115s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="openclaw_config"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "openclaw_config", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/openclaw_config.rs", Edition2021) } + 0.232491150s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="openclaw_config"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.233156372s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="opencode_provider"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "opencode_provider", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/opencode_provider.rs", Edition2021) } + 0.233162463s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="opencode_provider"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.233849162s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="prompt_commands"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "prompt_commands", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/prompt_commands.rs", Edition2021) } + 0.233854470s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="prompt_commands"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.234600528s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_add_noninteractive"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "provider_add_noninteractive", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_add_noninteractive.rs", Edition2021) } + 0.234606268s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_add_noninteractive"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.235283366s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_commands"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "provider_commands", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_commands.rs", Edition2021) } + 0.235294264s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_commands"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.236163795s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_model_roundtrip"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "provider_model_roundtrip", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_model_roundtrip.rs", Edition2021) } + 0.236169207s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_model_roundtrip"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.236877136s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_service"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "provider_service", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_service.rs", Edition2021) } + 0.236883344s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_service"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.237581145s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_switch_settings_sync"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "provider_switch_settings_sync", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_switch_settings_sync.rs", Edition2021) } + 0.237587088s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="provider_switch_settings_sync"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.238256156s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_forwarder_alignment"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_claude_forwarder_alignment", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_forwarder_alignment.rs", Edition2021) } + 0.238261730s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_forwarder_alignment"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.238927299s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_model_mapping"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_claude_model_mapping", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_model_mapping.rs", Edition2021) } + 0.238934307s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_model_mapping"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.239594956s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_openai_chat"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_claude_openai_chat", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_openai_chat.rs", Edition2021) } + 0.239599974s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_openai_chat"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.240245938s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_response_parity"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_claude_response_parity", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_response_parity.rs", Edition2021) } + 0.240253964s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_response_parity"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.240916381s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_streaming"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_claude_streaming", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_streaming.rs", Edition2021) } + 0.240921853s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_claude_streaming"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.241576486s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_daemon"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_daemon", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_daemon.rs", Edition2021) } + 0.241583281s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_daemon"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.242288914s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_database"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_database", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_database.rs", Edition2021) } + 0.242296065s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_database"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.243135642s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_multi_app_passthrough"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_multi_app_passthrough", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_multi_app_passthrough.rs", Edition2021) } + 0.243141355s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_multi_app_passthrough"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.243820621s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_service"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_service", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_service.rs", Edition2021) } + 0.243826420s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_service"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.244578833s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_takeover"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_takeover", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_takeover.rs", Edition2021) } + 0.244583996s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_takeover"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.245228411s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_upstream_error_summary"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "proxy_upstream_error_summary", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_upstream_error_summary.rs", Edition2021) } + 0.245234358s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="proxy_upstream_error_summary"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.245891235s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="settings_claude_onboarding_skip"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "settings_claude_onboarding_skip", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/settings_claude_onboarding_skip.rs", Edition2021) } + 0.245898029s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="settings_claude_onboarding_skip"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.246572834s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="settings_current_provider"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "settings_current_provider", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/settings_current_provider.rs", Edition2021) } + 0.246579247s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="settings_current_provider"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.247234511s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="settings_visible_apps"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "settings_visible_apps", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/settings_visible_apps.rs", Edition2021) } + 0.247239155s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="settings_visible_apps"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.247886251s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="skills_service"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "skills_service", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/skills_service.rs", Edition2021) } + 0.247890638s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="skills_service"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.248566908s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="stream_check_claude_openai_responses"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "stream_check_claude_openai_responses", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/stream_check_claude_openai_responses.rs", Edition2021) } + 0.248572115s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="stream_check_claude_openai_responses"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.249300306s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="support"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "support", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/support.rs", Edition2021) } + 0.249310624s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="support"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.250093048s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="support_home_isolation"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "support_home_isolation", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/support_home_isolation.rs", Edition2021) } + 0.250100434s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="support_home_isolation"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.250778112s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="webdav_settings"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "webdav_settings", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/webdav_settings.rs", Edition2021) } + 0.250784621s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="webdav_settings"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.251426836s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="webdav_sync_service"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "webdav_sync_service", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/webdav_sync_service.rs", Edition2021) } + 0.251432691s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="webdav_sync_service"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + 0.252076611s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="workspace_commands"}: cargo::core::compiler::fingerprint: fingerprint dirty for cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri)/Check { test: true }/TargetInner { kind: "test", name: "workspace_commands", benched: false, ..: with_path("/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/workspace_commands.rs", Edition2021) } + 0.252083091s INFO prepare_target{force=false package_id=cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) target="workspace_commands"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "cc_switch_lib" }) + Checking cc-switch v5.8.7 (/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.92s diff --git a/target/rust-analyzer/flycheck0/stdout b/target/rust-analyzer/flycheck0/stdout new file mode 100644 index 00000000..03856624 --- /dev/null +++ b/target/rust-analyzer/flycheck0/stdout @@ -0,0 +1,438 @@ +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.101","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.101/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.101/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/proc-macro2-14351e3615cfd962/build-script-build"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.101","linked_libs":[],"linked_paths":[],"cfgs":["wrap_proc_macro","proc_macro_span_location","proc_macro_span_file"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/proc-macro2-1366fe10afec6484/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.19","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.19/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_ident","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.19/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_ident-d65fe7db1d8a401a.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_ident-d65fe7db1d8a401a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.41","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.41/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.41/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/quote-359af793157c725c/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.176","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.176/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.176/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/libc-a2ae8c0c50a1f23c/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfg_if","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcfg_if-a87a8cbc9c691da3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/find-msvc-tools-0.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"find_msvc_tools","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/find-msvc-tools-0.1.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfind_msvc_tools-38d4b5f303b20232.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfind_msvc_tools-38d4b5f303b20232.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shlex-1.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"shlex","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shlex-1.3.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libshlex-3b90976fe851454e.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libshlex-3b90976fe851454e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbitflags-c10f145ac5d2b8b0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#log@0.4.28","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.28/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"log","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.28/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblog-463da360455e9350.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","result","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/serde_core-4b6b5d3d504b832f/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/version_check-0.9.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"version_check","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/version_check-0.9.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libversion_check-e67c589c3c1ced77.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libversion_check-e67c589c3c1ced77.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.15","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.15/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itoa","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.15/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libitoa-bc1ca429a31fb99c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.21","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/allocator-api2-0.2.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"allocator_api2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/allocator-api2-0.2.21/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liballocator_api2-00a55c07d0ea4ce1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#foldhash@0.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/foldhash-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"foldhash","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/foldhash-0.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfoldhash-dcf7c55e46bf35d0.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.41","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/quote-cff79a282071f8ec/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.101","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.101/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.101/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproc_macro2-af8d234f1477174d.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproc_macro2-af8d234f1477174d.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.176","linked_libs":[],"linked_paths":[],"cfgs":["freebsd12"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/libc-7277e7d305b53ad7/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/serde_core-c114ab320840c5c5/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"once_cell","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","race","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libonce_cell-9dd2b08ed9c38c44.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"equivalent","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libequivalent-9b88d636aa69c8ed.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pin_project_lite","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpin_project_lite-49c22181e3d99132.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"memchr","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.7.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmemchr-4dc715cff86b0c47.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.32","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pkg-config-0.3.32/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pkg_config","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pkg-config-0.3.32/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpkg_config-f6783222fab12a6c.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpkg_config-f6783222fab12a6c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-core-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-core-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_core-f012fa4b73a1b51c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"smallvec","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["const_generics","const_new"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsmallvec-87b934370d092c9e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/typenum-059357c2950c45e7/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/generic-array-fa0747d719f0609b/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","serde_derive","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/serde-49a10a9683562367/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.41","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.41/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"quote","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.41/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libquote-02fd0fd45c0c67d3.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libquote-02fd0fd45c0c67d3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.176","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.176/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"libc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.176/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblibc-83f1e8173267ed71.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.176","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.176/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"libc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.176/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblibc-9d6473bdad39c6b7.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblibc-9d6473bdad39c6b7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","result","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_core-0a544ebb697e7a0a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.16.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.16.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["allocator-api2","default","default-hasher","equivalent","inline-more","raw-entry"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhashbrown-f4a043ca8af84610.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/typenum-f0d3a75958b66a8e/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytes@1.10.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.10.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytes","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.10.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbytes-8bce5f4da8dd4142.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","linked_libs":[],"linked_paths":[],"cfgs":["relaxed_coherence"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/generic-array-0e994d5e42b52327/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","linked_libs":[],"linked_paths":[],"cfgs":["if_docsrs_then_no_serde_core"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/serde-0c79bc1bb5bf9eba/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_sink","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_sink-ce2b11275642f9b5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fs","std","stdio","termios"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustix-42552b352fc7d51b/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stable_deref_trait-1.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"stable_deref_trait","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stable_deref_trait-1.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstable_deref_trait-b49a9366b01ce1e2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auxvec","elf","errno","general","ioctl","no_std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblinux_raw_sys-2e8425f9ea8d5640.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/build/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustversion-050205c5a595f792/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.106","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.106/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"syn","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.106/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","extra-traits","fold","full","parsing","printing","proc-macro","visit","visit-mut"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsyn-830832dae5b68f37.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsyn-830832dae5b68f37.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#jobserver@0.1.34","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jobserver-0.1.34/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"jobserver","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jobserver-0.1.34/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libjobserver-9b837c4a074a384b.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libjobserver-9b837c4a074a384b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-registry-1.4.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"signal_hook_registry","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-registry-1.4.6/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsignal_hook_registry-45e6490a2cc9a718.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#mio@1.0.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"mio","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","log","net","os-ext","os-poll"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmio-09fe24b18ef21e58.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"typenum","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtypenum-ed6fc27c53f13f56.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.2","linked_libs":[],"linked_paths":[],"cfgs":["static_assertions","lower_upper_exp_for_non_zero","rustc_diagnostics","linux_raw_dep","linux_raw","linux_like","linux_kernel"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustix-3deae543461cad2f/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustversion-d70f972e4b4cf0c8/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/slab-0.4.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"slab","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/slab-0.4.11/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libslab-7e13a7c437da02b2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.11.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-2.11.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-2.11.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libindexmap-d064a7f0d9a8500d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_channel","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","futures-sink","sink","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_channel-69a5a7754b260f66.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fnv","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfnv-a1342e578286103d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.20","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.20/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ryu","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libryu-e3abc3dffcbd16d9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-utils-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pin_utils","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-utils-0.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpin_utils-5072515886eff5e9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"subtle","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsubtle-861032bd49e5dc54.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cc@1.2.40","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cc-1.2.40/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cc-1.2.40/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["parallel"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcc-de2467171de5df63.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcc-de2467171de5df63.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/synstructure-0.13.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"synstructure","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/synstructure-0.13.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsynstructure-addd698eee47f8b9.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsynstructure-addd698eee47f8b9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive-1.0.228/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serde_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_derive-4b6817531bec564d.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"generic_array","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libgeneric_array-b574fdeef2bf1202.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fs","std","stdio","termios"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librustix-861dc96b1bf9c9e6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/displaydoc-0.2.5/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"displaydoc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/displaydoc-0.2.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdisplaydoc-ef8a657c15084547.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.11.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-derive-0.11.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zerovec_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-derive-0.11.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzerovec_derive-72070b0e80b78a09.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"rustversion","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librustversion-211bfd3963d3ccb8.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http@1.3.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhttp-d38f6992a8e73d23.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-macro-0.3.31/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"futures_macro","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-macro-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_macro-8e40459f56ef860b.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-macros-2.5.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"tokio_macros","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-macros-2.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtokio_macros-9d7de79431cf08b1.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/socket2-0.6.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"socket2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/socket2-0.6.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["all"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsocket2-67cb5fda7b11871d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-width-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_width","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-width-0.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["cjk","default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_width-bcecda4a5962effd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-task-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_task","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-task-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_task-98357e05b2b79cb3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-derive-0.1.6/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zerofrom_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-derive-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzerofrom_derive-4e42bea638e24153.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.8.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-derive-0.8.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"yoke_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-derive-0.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libyoke_derive-fa37a7f605ede1a1.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","serde_derive","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde-2c502f74abacb082.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crypto_common","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcrypto_common-4786fe7ffb19161e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"heck","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libheck-97d689db605b5137.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libheck-97d689db605b5137.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"strsim","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstrsim-cdfa80b3792e870c.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstrsim-cdfa80b3792e870c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#either@1.15.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.15.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"either","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.15.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std","use_std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libeither-3844ef204489ce2a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ident_case@1.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ident_case-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ident_case","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ident_case-1.0.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libident_case-649284103e8b873b.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libident_case-649284103e8b873b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-io-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_io","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-io-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_io-8d314c1cd239bf6c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio@1.47.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.47.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.47.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["bytes","default","fs","io-std","io-util","libc","macros","mio","net","process","rt","rt-multi-thread","signal","signal-hook-registry","socket2","sync","time","tokio-macros"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtokio-0ea8df3efe632458.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.34","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-core-0.1.34/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-core-0.1.34/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["once_cell","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtracing_core-46fd6cd7c2c06b15.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.17","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/thiserror-31df43c19cb96785/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http_body","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-1.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhttp_body-84a351a29693b0bb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.17","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"thiserror_impl","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libthiserror_impl-d0d33fce16d35ee2.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerofrom","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","derive"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzerofrom-ec2ea3973f2c7a3c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_util","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","async-await","async-await-macro","channel","futures-channel","futures-io","futures-macro","futures-sink","io","memchr","sink","slab","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_util-e786318c8cd3dd6a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling_core@0.23.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_core-0.23.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"darling_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_core-0.23.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["strsim","suggestions"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdarling_core-fa4f98df5c35e0d9.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdarling_core-fa4f98df5c35e0d9.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.17","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/thiserror-c4be12b08819dbfd/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.41","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.41/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.41/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["log","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtracing-e42f7919a0578cf6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zeroize_derive@1.4.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zeroize_derive-1.4.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zeroize_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zeroize_derive-1.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzeroize_derive-b30c6953a5a5e2a2.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/parking_lot_core-a1f54925c42d264a/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_segmentation","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_segmentation-9f2009d72cd5b583.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","dev_urandom_fallback"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/ring-4dac44f02ae554ac/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"block_buffer","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libblock_buffer-1cd5b47318c3cb74.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"scopeguard","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libscopeguard-881bf68c2f011ad5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"writeable","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwriteable-3d4339d03c3f4941.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_segmentation","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_segmentation-24996001357b4225.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_segmentation-24996001357b4225.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_service","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtower_service-7552ee2ff29ea54a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"yoke","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","derive","zerofrom"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libyoke-6d552562b73b3fbb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling_macro@0.23.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_macro-0.23.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"darling_macro","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_macro-0.23.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdarling_macro-e0307368893cae02.so"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/parking_lot_core-ffcaa5490a92af7a/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.17","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thiserror","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libthiserror-2abb728931908ff6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zeroize-1.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zeroize","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zeroize-1.8.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","zeroize_derive"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzeroize-8353c3dfd255ae3d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["channel","default","iterator"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/signal-hook-db6e08791665291e/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#litemap@0.8.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litemap-0.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"litemap","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litemap-0.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblitemap-a910b92b11aeba47.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/digest-0.10.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"digest","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/digest-0.10.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","block-buffer","core-api","default","mac","std","subtle"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdigest-30d2c35a73cff21f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.10.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/convert_case-0.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"convert_case","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/convert_case-0.10.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libconvert_case-e71aebe5e06ec10d.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libconvert_case-e71aebe5e06ec10d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lock_api","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["atomic_usize","default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblock_api-6f7c20f2ce02eb24.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","linked_libs":["static=ring_core_0_17_14_","static=ring_core_0_17_14__test"],"linked_paths":["native=/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/ring-9e8dad35fa0dcaed/out"],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/ring-9e8dad35fa0dcaed/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#strum_macros@0.27.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strum_macros-0.27.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"strum_macros","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strum_macros-0.27.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstrum_macros-8f7aca6a55eb11f8.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itertools-0.14.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itertools","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itertools-0.14.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["use_alloc","use_std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libitertools-7e48352b06e43afa.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#castaway@0.2.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/castaway-0.2.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"castaway","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/castaway-0.2.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcastaway-005708ebc9dc0dd7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerovec","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","derive","yoke"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzerovec-590667ca3b5bfc30.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling@0.23.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling-0.23.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"darling","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling-0.23.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","suggestions"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdarling-7a69e777afe0f7b7.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdarling-7a69e777afe0f7b7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerotrie-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerotrie","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerotrie-0.2.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["yoke","zerofrom"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzerotrie-00ba9728c399b809.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/signal-hook-0d3c91ec580c50d5/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking_lot_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libparking_lot_core-2809684df1b4fc3b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.0.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.0.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/icu_properties_data-3825217ac2ef7ed3/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/httparse-3cb5a33f1300a9eb/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.0.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.0.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/icu_normalizer_data-9430a40f6341083f/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indoc@2.0.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indoc-2.0.7/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"indoc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indoc-2.0.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libindoc-4d3659fd524a3fc8.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/static_assertions-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"static_assertions","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/static_assertions-1.1.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstatic_assertions-fdae80a8dbbd64a1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"percent_encoding","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpercent_encoding-2effb2ec806b20a0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-truncate@2.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-truncate-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_truncate","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-truncate-2.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_truncate-9038a7cebe808da3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#derive_more-impl@2.1.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-impl-2.1.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"derive_more_impl","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-impl-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","is_variant"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libderive_more_impl-b9b1783098a9c211.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.12.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pki-types-1.12.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls_pki_types","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pki-types-1.12.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librustls_pki_types-6b058c3fc95486ed.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.8.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tinystr","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","zerovec"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtinystr-6f319222d634d305.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#potential_utf@0.1.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/potential_utf-0.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"potential_utf","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/potential_utf-0.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["zerovec"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpotential_utf-81f74b51141db3ef.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1","linked_libs":[],"linked_paths":[],"cfgs":["httparse_simd_neon_intrinsics","httparse_simd"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/httparse-2b9f9144f3a78c86/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#compact_str@0.9.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/compact_str-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"compact_str","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/compact_str-0.9.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcompact_str-ac5686a7e3c37762.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot-0.12.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking_lot","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot-0.12.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libparking_lot-80cd29e251416440.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.0.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/icu_normalizer_data-e98409430d5b63c8/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"signal_hook","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["channel","default","iterator"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsignal_hook-e739337d58a160a1.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.0.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/icu_properties_data-2762c02062e6cc11/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#kasuari@0.4.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/kasuari-0.4.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"kasuari","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/kasuari-0.4.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libkasuari-eec47848758d4dfa.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#strum@0.27.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strum-0.27.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"strum","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strum-0.27.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","std","strum_macros"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstrum-f68cf3e85a9ace1f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-util-0.7.18/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio_util","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-util-0.7.18/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["codec","default","io"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtokio_util-3837a540d2ca7732.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#inout@0.1.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"inout","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libinout-f6e7080c89436f2b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd-sys@2.0.16+zstd.1.5.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-sys-2.0.16+zstd.1.5.7/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-sys-2.0.16+zstd.1.5.7/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zstd-sys-7609a5399dd19b1a/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.16","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libgetrandom-e2d17a6a8c5c1d7f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_locale_core@2.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_locale_core-2.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_locale_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_locale_core-2.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["zerovec"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_locale_core-2cfdee028d6f3156.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_collections@2.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_collections-2.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_collections","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_collections-2.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_collections-bad7d5686370a5a2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lru@0.16.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lru-0.16.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lru","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lru-0.16.3/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","hashbrown"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblru-23db3c5ff77276d1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8parse-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8parse","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8parse-0.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libutf8parse-55a20bf53ef149de.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atomic-waker-1.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"atomic_waker","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atomic-waker-1.1.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libatomic_waker-5d2db5f7d9229e4e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cpufeatures","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcpufeatures-716b7d326f337e94.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#instability@0.3.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/instability-0.3.11/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/instability-0.3.11/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/instability-1f75fbe7eba75291/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.27","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.27/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.27/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zerocopy-c75cde6346f2bda0/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#litrs@1.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litrs-1.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"litrs","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litrs-1.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblitrs-a07789e6698f91af.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblitrs-a07789e6698f91af.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_layer","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtower_layer-2818eae78a76061c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std","wasm_js"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/getrandom-f97c47fbf64dde70/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/untrusted-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"untrusted","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/untrusted-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libuntrusted-acabc4c26793167e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/powerfmt-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"powerfmt","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/powerfmt-0.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpowerfmt-278c2f20b8431963.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/try-lock-0.2.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"try_lock","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/try-lock-0.2.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtry_lock-d369a8cb96356c02.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_provider-2.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_provider","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_provider-2.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["baked","zerotrie"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_provider-d359d13bd09d80b8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstyle-parse-0.2.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anstyle_parse","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstyle-parse-0.2.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","utf8"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libanstyle_parse-16f697e230e43947.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#instability@0.3.11","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/instability-c56b4f305501f6bf/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#deranged@0.5.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deranged-0.5.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"deranged","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deranged-0.5.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","powerfmt"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libderanged-b8db5d155da1fd47.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.27","linked_libs":[],"linked_paths":[],"cfgs":["zerocopy_core_error_1_81_0","zerocopy_diagnostic_on_unimplemented_1_78_0","zerocopy_generic_bounds_in_const_fn_1_61_0","zerocopy_target_has_atomics_1_60_0","zerocopy_aarch64_simd_1_59_0","zerocopy_panic_in_const_and_vec_try_reserve_1_57_0"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zerocopy-0789fac8a25a9f68/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.3","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/getrandom-5e0a78fa91eb77ea/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#document-features@0.2.12","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/document-features-0.2.12/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"document_features","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/document-features-0.2.12/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdocument_features-fec4fde053197295.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ring","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","dev_urandom_fallback"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libring-d8cf1f4e28393293.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/want-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"want","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/want-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwant-e953bceaba135dcb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ratatui-core@0.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-core-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ratatui_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-core-0.1.0/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","layout-cache","std","underline-color"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libratatui_core-200a59755189b3ba.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#h2@0.4.13","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/h2-0.4.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"h2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/h2-0.4.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libh2-151c32cde80cd4e1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer_data","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_normalizer_data-e50077f8f492fd8b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cipher@0.4.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cipher-0.4.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cipher","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cipher-0.4.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcipher-3ffe70dde3bafecc.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook-mio@0.2.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-mio-0.2.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"signal_hook_mio","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-mio-0.2.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["mio-1_0","support-v1_0"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsignal_hook_mio-58f5163a54270004.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties_data","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_properties_data-c6b76b43fe538383.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"httparse","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhttparse-5f7771310d1a254c.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd-sys@2.0.16+zstd.1.5.7","linked_libs":["static=zstd"],"linked_paths":["native=/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zstd-sys-2104cb15b6514139/out"],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zstd-sys-2104cb15b6514139/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#derive_more@2.1.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"derive_more","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","is_variant","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libderive_more-97b3b72af93733d5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sync_wrapper","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["futures","futures-core"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsync_wrapper-c3e474a5d60d69c6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ahash-0.8.12/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ahash-0.8.12/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/ahash-6f404ae33ef8191d/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_conv","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libnum_conv-f4d38fd30e5a6901.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstyle-query-1.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anstyle_query","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstyle-query-1.1.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libanstyle_query-1ad539b71a901a98.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/prettyplease-d2977f31f7a8b736/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtime_core-14da72e184c39573.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/autocfg-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"autocfg","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/autocfg-1.5.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libautocfg-38d501c17e68b38c.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libautocfg-38d501c17e68b38c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/crc32fast-a1aeaaf666d10f76/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.32","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.32/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.32/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["ring","std","tls12"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustls-37d5c9402bc426fc/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httpdate-1.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"httpdate","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httpdate-1.0.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhttpdate-b44607faa77cae59.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num_threads@0.1.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num_threads-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_threads","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num_threads-0.1.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libnum_threads-32f6f559768571e3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/colorchoice-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"colorchoice","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/colorchoice-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcolorchoice-de3f67dda32db1ea.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is_terminal_polyfill-1.70.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"is_terminal_polyfill","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is_terminal_polyfill-1.70.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libis_terminal_polyfill-d2b73b6ef9035c76.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstyle-1.0.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anstyle","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstyle-1.0.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libanstyle-611a3923e72ac17f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ucd-trie@0.1.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ucd-trie-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ucd_trie","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ucd-trie-0.1.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libucd_trie-3db7031fc5d9f7f4.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libucd_trie-3db7031fc5d9f7f4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.44","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.38.44/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.38.44/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","fs","libc-extra-traits","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustix-c5abc72032785c0c/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"memchr","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.7.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmemchr-4eb4ede79c626bb5.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmemchr-4eb4ede79c626bb5.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/prettyplease-c6dfd71919123acf/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.32","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustls-26acd8a9aad2e105/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossterm@0.29.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossterm-0.29.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crossterm","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossterm-0.29.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["bracketed-paste","default","derive-more","events","windows"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcrossterm-3b42557a5dec51ac.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper@1.7.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-1.7.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-1.7.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["client","default","full","http1","http2","server"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhyper-eb8cfe53e38a1beb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_properties-2ec336f410820b7d.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","linked_libs":[],"linked_paths":[],"cfgs":["stable_arm_crc32_intrinsics"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/crc32fast-b09c59eae4a5ad74/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/num-traits-cfff573006a81481/build-script-build"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.44","linked_libs":[],"linked_paths":[],"cfgs":["static_assertions","linux_raw","linux_like","linux_kernel"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rustix-c2e60c23778fa7ab/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstream-0.6.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anstream","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anstream-0.6.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auto","default","wincon"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libanstream-8f8b1685be819caf.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pest@2.8.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest-2.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pest","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest-2.8.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","memchr","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest-ece3c1b8713b257f.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest-ece3c1b8713b257f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time@0.3.44","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-0.3.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-0.3.44/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","local-offset","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtime-af5d5d7fa876a992.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12","linked_libs":[],"linked_paths":[],"cfgs":["folded_multiply"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/ahash-4b346cb74d59522c/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std","wasm_js"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libgetrandom-17d75400f1ca03ac.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.27","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerocopy","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.27/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzerocopy-7b138f2be3f7213f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libicu_normalizer-4d6b23781f99544f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#instability@0.3.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/instability-0.3.11/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"instability","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/instability-0.3.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libinstability-9db62dbc41db0eff.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-webpki-0.103.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webpki","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-webpki-0.103.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","ring","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwebpki-db3c546905ef3b2b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"form_urlencoded","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libform_urlencoded-2036bef24213fd6d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hmac-0.12.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hmac","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hmac-0.12.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["reset"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhmac-b8035acb788d881f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-util-0.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http_body_util","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-util-0.1.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhttp_body_util-7794cde468079604.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bzip2-sys@0.1.13+1.0.8","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bzip2-sys-0.1.13+1.0.8/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bzip2-sys-0.1.13+1.0.8/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/bzip2-sys-12b92fec89126326/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rquickjs-sys@0.8.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-sys-0.8.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-sys-0.8.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rquickjs-sys-708e942e822faccf/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lzma-sys@0.1.20","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lzma-sys-0.1.20/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lzma-sys-0.1.20/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/lzma-sys-665b2ba7abbefc56/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aho_corasick","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["perf-literal","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libaho_corasick-1b60c427af86c8e6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd-safe@7.2.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-safe-7.2.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-safe-7.2.4/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zstd-safe-eaa4f52264c4e43c/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.15","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.4.15/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.4.15/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["elf","errno","general","ioctl","no_std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblinux_raw_sys-85164ddfc6b57610.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/base64-0.22.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"base64","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/base64-0.22.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbase64-77885587371c3c70.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.145","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.145/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.145/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","indexmap","preserve_order","raw_value","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/serde_json-8c63559288897de0/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_syntax","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libregex_syntax-ca45de62557e5c47.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.11.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ipnet-2.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ipnet","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ipnet-2.11.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libipnet-7644613948ff990b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/vcpkg-0.2.15/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"vcpkg","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/vcpkg-0.2.15/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libvcpkg-5425685a8637baec.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libvcpkg-5425685a8637baec.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pest_meta@2.8.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest_meta-2.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pest_meta","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest_meta-2.8.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest_meta-d2c115d607abb7f9.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest_meta-d2c115d607abb7f9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.32","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.32/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.32/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["ring","std","tls12"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librustls-5d2b149eadad7be6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna_adapter","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libidna_adapter-4d848fa9e87c299b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pbkdf2@0.12.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pbkdf2-0.12.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pbkdf2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pbkdf2-0.12.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","hmac"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpbkdf2-d17e74b330fec1d2.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#lzma-sys@0.1.20","linked_libs":["lzma"],"linked_paths":["native=/usr/lib64"],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/lzma-sys-ced0f6db4e8b64f1/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.145","linked_libs":[],"linked_paths":[],"cfgs":["fast_arithmetic=\"64\""],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/serde_json-a8959d8f570c924d/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.44","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.38.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.38.44/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","fs","libc-extra-traits","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librustix-caf3385130f79846.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_automata","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","dfa-onepass","hybrid","meta","nfa-backtrack","nfa-pikevm","nfa-thompson","perf-inline","perf-literal","perf-literal-multisubstring","perf-literal-substring","std","syntax","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment","unicode-word-boundary"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libregex_automata-7890a22fe0418c57.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd-safe@7.2.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zstd-safe-8612a4475817737a/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.17","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-util-0.1.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper_util","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-util-0.1.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["client","client-legacy","client-proxy","default","http1","server","service","tokio"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhyper_util-ae5dee5edf707c1c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.28.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libsqlite3-sys-0.28.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libsqlite3-sys-0.28.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["bundled","bundled_bindings","cc","default","min_sqlite_version_3_14_0","pkg-config","vcpkg"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/libsqlite3-sys-eafcd2ee8062e72e/build-script-build"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rquickjs-sys@0.8.1","linked_libs":["static=quickjs"],"linked_paths":["native=/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rquickjs-sys-b28ff370d42f9e69/out"],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rquickjs-sys-b28ff370d42f9e69/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#bzip2-sys@0.1.13+1.0.8","linked_libs":["bz2"],"linked_paths":["native=/usr/lib64"],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/bzip2-sys-3af1648d6fc804a2/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ahash-0.8.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ahash","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ahash-0.8.12/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libahash-9d442eeff06c3812.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crc32fast","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcrc32fast-2b91c74b19103bfe.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prettyplease","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprettyplease-0a68aa28fa4effb9.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprettyplease-0a68aa28fa4effb9.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","linked_libs":[],"linked_paths":[],"cfgs":["has_total_cmp"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/num-traits-4b7c5a2c9677cdf4/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower@0.5.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["__common","futures-core","futures-util","log","make","pin-project-lite","retry","sync_wrapper","timeout","tokio","tracing","util"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtower-79afed092ad58fa6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd-sys@2.0.16+zstd.1.5.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-sys-2.0.16+zstd.1.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zstd_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-sys-2.0.16+zstd.1.5.7/src/lib.rs","edition":"2018","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzstd_sys-f9a8849860bc9820.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.6.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_datetime","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.6.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtoml_datetime-09c865b93b546430.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#terminal_size@0.4.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/terminal_size-0.4.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"terminal_size","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/terminal_size-0.4.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libterminal_size-2d896c5b319aad73.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#line-clipping@0.3.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/line-clipping-0.3.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"line_clipping","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/line-clipping-0.3.5/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libline_clipping-66527d66b0099324.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8_iter","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libutf8_iter-39d2d421a5b8d55d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-no-stdlib@2.0.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_no_stdlib","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liballoc_no_stdlib-b9128e3f875e2a14.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#home@0.5.12","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/home-0.5.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"home","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/home-0.5.12/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhome-8a5370df5ef10936.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_lex-0.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"clap_lex","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_lex-0.7.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libclap_lex-05c2e4d52aed2caf.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"strsim","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstrsim-8095d292a6ea3ca3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-general-category@1.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-general-category-1.1.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-general-category-1.1.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/unicode-general-category-2acac2b4babf662d/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"adler2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libadler2-8ddbfa9477eb984f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.11.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/portable-atomic-1.11.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/portable-atomic-1.11.1/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","fallback"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/portable-atomic-d840e0f8d8f353c8/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc-catalog@2.4.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc-catalog-2.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crc_catalog","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc-catalog-2.4.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcrc_catalog-823615f0215e8730.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lzma-sys@0.1.20","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lzma-sys-0.1.20/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lzma_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lzma-sys-0.1.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblzma_sys-5ea2a340f425d7b8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","compiled_data","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libidna-16ddc46ca209c112.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"miniz_oxide","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["with-alloc"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libminiz_oxide-e40cde526625df25.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-stdlib@0.2.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_stdlib","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liballoc_stdlib-76cd445cf7ea7303.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.11.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/portable-atomic-f8ca4c6708397439/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ratatui-widgets@0.3.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-widgets-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ratatui_widgets","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-widgets-0.3.0/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["all-widgets","calendar","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libratatui_widgets-08cd46355dccbf55.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd-safe@7.2.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-safe-7.2.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zstd_safe","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-safe-7.2.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzstd_safe-bd7f9bf95af34f80.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc@3.3.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc-3.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc-3.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcrc-b6a585c93fca2ca2.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-general-category@1.1.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/unicode-general-category-fce63fab58ced6e0/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.53","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_builder-4.5.53/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"clap_builder","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_builder-4.5.53/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["cargo","color","env","error-context","help","std","suggestions","usage","wrap_help"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libclap_builder-3172786529bee666.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.145","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.145/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_json","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.145/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","indexmap","preserve_order","raw_value","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_json-614f7203aa99c836.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bon-macros@3.9.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bon-macros-3.9.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"bon_macros","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bon-macros-3.9.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbon_macros-1883eda44770eb0e.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rquickjs-sys@0.8.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-sys-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rquickjs_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-sys-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librquickjs_sys-ede825094d892130.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.28.0","linked_libs":["static=sqlite3"],"linked_paths":["native=/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/libsqlite3-sys-cdbf25130c7e5443/out"],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/libsqlite3-sys-cdbf25130c7e5443/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bzip2-sys@0.1.13+1.0.8","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bzip2-sys-0.1.13+1.0.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bzip2_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bzip2-sys-0.1.13+1.0.8/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbzip2_sys-49867503b18a75d8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_traits","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libnum_traits-9e8328f04141718a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex@1.11.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","perf","perf-backtrack","perf-cache","perf-dfa","perf-inline","perf-literal","perf-onepass","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libregex-70176299d6867582.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.14.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.14.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["ahash","inline-more"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhashbrown-580d82ba9e7d9800.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio_rustls","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["ring","tls12"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtokio_rustls-ec3266c38bd46a41.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pest_generator@2.8.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest_generator-2.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pest_generator","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest_generator-2.8.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest_generator-412ae6002f7c3323.rlib","/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest_generator-412ae6002f7c3323.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_urlencoded-0.7.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_urlencoded","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_urlencoded-0.7.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_urlencoded-64635343d0fea130.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.9","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha2-0.10.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha2-0.10.9/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsha2-04bda711014f264f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#webpki-roots@1.0.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webpki-roots-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webpki_roots","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webpki-roots-1.0.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwebpki_roots-1fefafa9d4372211.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-executor-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_executor","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-executor-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures_executor-e45e4fd3bd66a1c1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.9","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-0.6.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_spanned","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-0.6.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_spanned-f91d16eae129b413.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.49","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_derive-4.5.49/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"clap_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_derive-4.5.49/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libclap_derive-7c4dae4e89fa8106.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-trait-0.1.89/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"async_trait","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-trait-0.1.89/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libasync_trait-e76f2162034aef43.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thread_local-1.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thread_local","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thread_local-1.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libthread_local-da3e5772d4d33ab6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simd-adler32-0.3.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"simd_adler32","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simd-adler32-0.3.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsimd_adler32-031defae234395fb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/anyhow-f0f8ac34947eb6de/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.40","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.5.40/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"winnow","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.5.40/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwinnow-f352fc3b99476a5a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#option-ext@0.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/option-ext-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"option_ext","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/option-ext-0.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liboption_ext-caf8fe1b92ef816a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zip@2.4.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zip-2.4.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zip-2.4.2/src/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["_deflate-any","aes","aes-crypto","bzip2","constant_time_eq","default","deflate","deflate-flate2","deflate-zopfli","deflate64","flate2","getrandom","hmac","lzma","lzma-rs","pbkdf2","sha1","time","xz","zeroize","zopfli","zstd"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zip-547a2adde5c5677f/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ucd-trie@0.1.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ucd-trie-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ucd_trie","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ucd-trie-0.1.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libucd_trie-b9b24ae22a9b2ce6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"byteorder","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbyteorder-694546da291939c4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mime-0.3.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"mime","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mime-0.3.17/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmime-b6737001863fd4df.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#iri-string@0.7.8","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iri-string-0.7.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"iri_string","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iri-string-0.7.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libiri_string-201a803ce28f1251.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.19.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bumpalo-3.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bumpalo","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bumpalo-3.19.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbumpalo-ce7479aa4455bb83.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/thiserror-d2b20bc5606dd667/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rust_decimal@1.40.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust_decimal-1.40.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust_decimal-1.40.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rust_decimal-7a46643c0c9dc52f/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfastrand-b1d0e5731e475243.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fuzzy-matcher@0.3.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fuzzy-matcher-0.3.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fuzzy_matcher","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fuzzy-matcher-0.3.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfuzzy_matcher-1966aeaa461e3b22.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper-rustls@0.27.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-rustls-0.27.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper_rustls","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-rustls-0.27.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["http1","ring","tls12","webpki-roots","webpki-tokio"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhyper_rustls-bc95a0cccc1bef34.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/thiserror-b28f052e3ba5c2bc/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#axum-core@0.4.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-core-0.4.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"axum_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-core-0.4.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["tracing"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libaxum_core-d059b953adc4d8a8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.23.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.23.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tempfile","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.23.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","getrandom"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtempfile-642dfaf4741b339d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.4.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.4.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdirs_sys-f1219892dd450fe4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pest@2.8.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest-2.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pest","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest-2.8.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","memchr","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest-63ba717aa758edad.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lzma-rs@0.3.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lzma-rs-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lzma_rs","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lzma-rs-0.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["stream"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblzma_rs-2ff636bc77a8b0f7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zopfli@0.8.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zopfli-0.8.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zopfli","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zopfli-0.8.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","gzip","std","zlib"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzopfli-3cb05c1d887af8ae.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-http@0.6.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.6.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_http","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.6.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["follow-redirect","futures-util","iri-string","tower"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtower_http-7f9676983f9ee148.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rust_decimal@1.40.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/rust_decimal-aacfac97fb9a0804/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.20.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_edit","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.20.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtoml_edit-ece22ea661b153ba.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#zip@2.4.2","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/zip-7b3eeb9ab848bd8a/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","linked_libs":[],"linked_paths":[],"cfgs":["std_backtrace"],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/anyhow-7dffd08ca73f1c01/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.9.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashlink-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashlink","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashlink-0.9.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libhashlink-0df50aa026de65c8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#clap@4.5.53","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap-4.5.53/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"clap","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap-4.5.53/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["cargo","color","default","derive","env","error-context","help","std","suggestions","usage","wrap_help"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libclap-7eb93bbbf2dd5692.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#env_filter@0.1.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_filter-0.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"env_filter","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_filter-0.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["regex"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libenv_filter-2d13a65bf06a66bb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pest_derive@2.8.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest_derive-2.8.5/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"pest_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pest_derive-2.8.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libpest_derive-acb083813ae1090b.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","async-await","default","executor","futures-executor","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfutures-93e1d0abd1a5e135.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.28.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libsqlite3-sys-0.28.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"libsqlite3_sys","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libsqlite3-sys-0.28.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["bundled","bundled_bindings","cc","default","min_sqlite_version_3_14_0","pkg-config","vcpkg"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblibsqlite3_sys-d07699e5359a4b35.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#portable-atomic@1.11.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/portable-atomic-1.11.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"portable_atomic","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/portable-atomic-1.11.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","fallback"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libportable_atomic-0a283f737e90c99f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-general-category@1.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-general-category-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_general_category","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-general-category-1.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_general_category-68e90910e9f4dc19.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#url@2.5.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"url","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liburl-8e7cb58e5c5354b6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bon@3.9.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bon-3.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bon","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bon-3.9.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbon-bd0364dbf0651169.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"flate2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["any_impl","default","miniz_oxide","rust_backend"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libflate2-184a8f4c0f0078fb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli-decompressor@4.0.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-4.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli_decompressor","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-4.0.3/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbrotli_decompressor-ba95eb6361e1e69e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ratatui-macros@0.7.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-macros-0.7.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ratatui_macros","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-macros-0.7.0/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libratatui_macros-4b9740dcd40f2dc5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rquickjs-core@0.8.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-core-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rquickjs_core","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-core-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["array-buffer","classes","default","properties"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librquickjs_core-26ad6cbc3eece4b2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zstd@0.13.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-0.13.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zstd","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zstd-0.13.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzstd-62076e876ddaabc0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bzip2@0.5.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bzip2-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bzip2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bzip2-0.5.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbzip2-03d974c1ebd28ad3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#which@4.4.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/which-4.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"which","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/which-4.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwhich-541dcb99b9441a50.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xz2@0.1.7","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xz2-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"xz2","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xz2-0.1.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libxz2-3b934caf0c6a4a95.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ratatui-crossterm@0.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-crossterm-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ratatui_crossterm","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-crossterm-0.1.0/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["crossterm_0_29","default","underline-color"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libratatui_crossterm-84808b87c7fe3b16.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aes@0.8.4","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-0.8.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aes","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-0.8.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libaes-ec40ededcdcb1d56.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha1-0.10.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha1","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha1-0.10.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsha1-cbc4bedb941786ea.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#console@0.15.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/console-0.15.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"console","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/console-0.15.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["ansi-parsing","default","unicode-width"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libconsole-2ee52d1da7cb120c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xattr@1.6.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xattr-1.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"xattr","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xattr-1.6.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","unsupported"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libxattr-9d3ddd1941091a39.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-stream-impl@0.3.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-impl-0.3.6/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"async_stream_impl","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-impl-0.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libasync_stream_impl-bc7ae9c34d185ffb.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.69","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-1.0.69/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"thiserror_impl","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-1.0.69/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libthiserror_impl-2a13a234ea2fc0b1.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#filetime@0.2.27","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/filetime-0.2.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"filetime","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/filetime-0.2.27/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfiletime-ebcfaf30f060efd0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_path_to_error@0.1.20","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_path_to_error-0.1.20/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_path_to_error","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_path_to_error-0.1.20/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_path_to_error-1b6d5f8279304cdc.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrayvec-0.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"arrayvec","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrayvec-0.7.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libarrayvec-142cb6a20a4f56fc.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_write@0.1.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_write-0.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_write","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_write-0.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtoml_write-9778582e7f77367c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lazy_static","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/liblazy_static-9bf3f646e7e24520.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fallible-iterator@0.3.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fallible-iterator-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fallible_iterator","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fallible-iterator-0.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfallible_iterator-a1a1cc0a8799a1fc.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fallible-streaming-iterator@0.1.9","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fallible-streaming-iterator-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fallible_streaming_iterator","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fallible-streaming-iterator-0.1.9/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libfallible_streaming_iterator-c11280acf027972b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#matchit@0.7.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchit-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"matchit","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchit-0.7.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmatchit-2d0491ced8abf00d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dyn-clone@1.0.20","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dyn-clone-1.0.20/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dyn_clone","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dyn-clone-1.0.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdyn_clone-8cfca8e19eb79f10.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.13","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.7.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"winnow","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.7.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwinnow-13964fa7763299d2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#jiff@0.2.16","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"jiff","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.16/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libjiff-8cb07f0cc28d3d7a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unsafe-libyaml@0.2.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unsafe-libyaml-0.2.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unsafe_libyaml","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unsafe-libyaml-0.2.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunsafe_libyaml-5d3cabcbf60870d4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#number_prefix@0.4.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/number_prefix-0.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"number_prefix","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/number_prefix-0.4.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libnumber_prefix-6b130bdc63bf643b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.4.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.4.2/build.rs","edition":"2024","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/getrandom-607a51f3d2232f2d/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anpa@0.10.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anpa-0.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anpa","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anpa-0.10.0/src/lib/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libanpa-d60acc1b7c021703.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#micromath@2.1.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/micromath-2.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"micromath","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/micromath-2.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmicromath-e3a76c937e70eb6c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#deflate64@0.1.10","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deflate64-0.1.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"deflate64","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deflate64-0.1.10/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdeflate64-6d4c75caca02028b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.3.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/constant_time_eq-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"constant_time_eq","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/constant_time_eq-0.3.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libconstant_time_eq-adae28293778f877.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.64","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iana-time-zone-0.1.64/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"iana_time_zone","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iana-time-zone-0.1.64/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["fallback"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libiana_time_zone-64e2e7380d4390cc.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rusqlite@0.31.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rusqlite-0.31.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rusqlite","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rusqlite-0.31.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["backup","bundled","hooks","modern_sqlite"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librusqlite-c2ec8489ff4882e6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tachyonfx@0.25.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tachyonfx-0.25.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tachyonfx","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tachyonfx-0.25.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","dsl","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtachyonfx-10a1c156a895cdcf.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zip@2.4.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zip-2.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zip","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zip-2.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["_deflate-any","aes","aes-crypto","bzip2","constant_time_eq","default","deflate","deflate-flate2","deflate-zopfli","deflate64","flate2","getrandom","hmac","lzma","lzma-rs","pbkdf2","sha1","time","xz","zeroize","zopfli","zstd"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libzip-b7f6bf204ab7de69.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.22.27","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.22.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_edit","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.22.27/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","display","parse"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtoml_edit-4ffc088f41b716e6.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.2","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/build/getrandom-fce1cf69d000c5b9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.42","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/chrono-0.4.42/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"chrono","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/chrono-0.4.42/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","clock","default","iana-time-zone","js-sys","now","oldtime","serde","std","wasm-bindgen","wasmbind","winapi","windows-link"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libchrono-53f99dfaf4fa0190.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_yaml@0.9.34+deprecated","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_yaml-0.9.34+deprecated/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_yaml","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_yaml-0.9.34+deprecated/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserde_yaml-1837d5a245c1f04d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#inquire@0.9.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inquire-0.9.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"inquire","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inquire-0.9.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["crossterm","default","fuzzy","fuzzy-matcher","macros","one-liners"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libinquire-a3cec5c923155e85.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.11.8","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"env_logger","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/env_logger-0.11.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auto-color","color","default","humantime","regex"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libenv_logger-b57fe3f7babe44e0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#axum@0.7.9","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.7.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"axum","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.7.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","form","http1","json","matched-path","original-uri","query","tokio","tower-log","tracing"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libaxum-75e766401d201393.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indicatif@0.17.11","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indicatif-0.17.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indicatif","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indicatif-0.17.11/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","unicode-width"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libindicatif-6cb5e9bcca1970e6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thiserror","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libthiserror-6e99a53149028b57.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-stream@0.3.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-0.3.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_stream","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-0.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libasync_stream-cc63dcab8d3b625b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#edit@0.1.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/edit-0.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"edit","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/edit-0.1.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["better-path","default","which"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libedit-7dbae398cf01be08.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rust_decimal@1.40.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust_decimal-1.40.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rust_decimal","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust_decimal-1.40.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librust_decimal-258c410b1b536889.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tar@0.4.44","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tar-0.4.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tar","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tar-0.4.44/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","xattr"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtar-a09e53e914199be6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#colored@2.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/colored-2.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"colored","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/colored-2.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcolored-144b09567e8d8f5a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ratatui@0.30.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-0.30.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ratatui","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ratatui-0.30.0/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["all-widgets","crossterm","default","layout-cache","macros","std","underline-color","widget-calendar"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libratatui-b19928f780a79b84.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#json5@0.4.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json5-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"json5","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json5-0.4.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libjson5-0a75a6863c233230.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli@7.0.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-7.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-7.0.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libbrotli-99442ca7eb1e0e62.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rquickjs@0.8.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rquickjs","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rquickjs-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["array-buffer","classes","default","properties"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librquickjs-c7ccaa19ff2a39bb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#json-five@0.3.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-five-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"json_five","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-five-0.3.1/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libjson_five-110a2a5495434180.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#reqwest@0.12.23","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/reqwest-0.12.23/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"reqwest","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/reqwest-0.12.23/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["__rustls","__rustls-ring","__tls","json","rustls-tls","rustls-tls-webpki-roots","rustls-tls-webpki-roots-no-provider","socks","stream"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libreqwest-3bbb0bd86e735391.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anyhow","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libanyhow-a8e29080dfa88f6a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml@0.8.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.8.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","display","parse"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtoml-754c292c34fe33f6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#clap_complete@4.5.61","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_complete-4.5.61/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"clap_complete","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clap_complete-4.5.61/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libclap_complete-4db87a8d79d60fbd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs@5.0.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-5.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-5.0.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdirs-c5bd1a9cb12b498e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#which@6.0.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/which-6.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"which","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/which-6.0.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwhich-033be89e837da83a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#uuid@1.20.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.20.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"uuid","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.20.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","rng","std","v4"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libuuid-1adce523d1850090.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-http@0.5.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_http","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.5.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["cors","default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libtower_http-6aab35735c32863b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#comfy-table@7.2.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/comfy-table-7.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"comfy_table","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/comfy-table-7.2.1/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","tty"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcomfy_table-c10cf1699d46d029.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#salsa20@0.10.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/salsa20-0.10.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"salsa20","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/salsa20-0.10.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsalsa20-907f308e771f3798.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rtoolbox@0.0.3","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rtoolbox-0.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rtoolbox","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rtoolbox-0.0.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librtoolbox-ef7212cf24237299.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.27","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"semver","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsemver-878e377743bfc220.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sdd@3.0.10","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sdd-3.0.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sdd","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sdd-3.0.10/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsdd-4112b1251a0badc4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#minisign-verify@0.2.5","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/minisign-verify-0.2.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"minisign_verify","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/minisign-verify-0.2.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libminisign_verify-89f1a923e7c1fb1a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.1.14","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-width-0.1.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_width","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-width-0.1.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["cjk","default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libunicode_width-6fb0c670f166827b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.2","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.4.2/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libgetrandom-f8c9087bf1db0954.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serial_test_derive@3.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serial_test_derive-3.2.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serial_test_derive","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serial_test_derive-3.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["async","default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserial_test_derive-4e059bc914b73be0.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ct-codecs@1.1.6","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ct-codecs-1.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ct_codecs","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ct-codecs-1.1.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libct_codecs-20740f6aef678c55.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rpassword@7.4.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rpassword-7.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rpassword","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rpassword-7.4.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/librpassword-270eb5e3f1cc3908.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#scrypt@0.11.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scrypt-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"scrypt","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scrypt-0.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libscrypt-418504d94bedcced.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#scc@2.4.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scc-2.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"scc","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scc-2.4.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libscc-b55e27ca211dd04c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#minisign@0.9.1","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/minisign-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"minisign","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/minisign-0.9.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libminisign-cbd504dc38f5b6d8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serial_test@3.2.0","manifest_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serial_test-3.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serial_test","src_path":"/home/zhangyangrui/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serial_test-3.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["async","default","logging"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libserial_test-44f97d5dfe137151.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["rlib"],"crate_types":["rlib"],"name":"cc_switch_lib","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcc_switch_lib-287f70b9be283cef.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"install_script","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/install_script.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libinstall_script-c535052d6e5d6c46.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"cc-switch","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcc_switch-7f1cd29923c8f5b4.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"settings_claude_onboarding_skip","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/settings_claude_onboarding_skip.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsettings_claude_onboarding_skip-0bfd3168f7a5439b.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"support","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/support.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsupport-73023b6199fc275b.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"stream_check_claude_openai_responses","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/stream_check_claude_openai_responses.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libstream_check_claude_openai_responses-97b654f6c96833e7.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"opencode_provider","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/opencode_provider.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libopencode_provider-a3274051b922b94b.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"deeplink_import","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/deeplink_import.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libdeeplink_import-d2d35a5b90afc88d.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"provider_model_roundtrip","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_model_roundtrip.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprovider_model_roundtrip-2af02af083d12992.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"settings_current_provider","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/settings_current_provider.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsettings_current_provider-4f0067134022e258.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"prompt_commands","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/prompt_commands.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprompt_commands-2c3266f89c6cb1d1.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"import_export_sync","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/import_export_sync.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libimport_export_sync-f2aabb298ab96f06.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"provider_switch_settings_sync","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_switch_settings_sync.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprovider_switch_settings_sync-41d5c284860a7ac0.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"cc-switch","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcc_switch-a03ac5bb4939ceca.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"completions_command","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/completions_command.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcompletions_command-93d05e3bd0f839e0.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"claude_plugin_integration_sync","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/claude_plugin_integration_sync.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libclaude_plugin_integration_sync-130f83400a63ac0b.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_claude_openai_chat","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_openai_chat.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_claude_openai_chat-e15c13077aecefcc.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"app_config_load","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/app_config_load.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libapp_config_load-1aae94085147de89.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_multi_app_passthrough","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_multi_app_passthrough.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_multi_app_passthrough-57324d38d1a333f9.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"mcp_commands","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/mcp_commands.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libmcp_commands-dae7fc45aa642a7c.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"support_home_isolation","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/support_home_isolation.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsupport_home_isolation-036e1fc2cebc9654.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_database","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_database.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_database-95b3dd72dabaf4ad.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_claude_response_parity","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_response_parity.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_claude_response_parity-70c776db7d19d7d6.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"webdav_sync_service","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/webdav_sync_service.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwebdav_sync_service-f809b2f0b46fe6e9.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"skills_service","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/skills_service.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libskills_service-4bb8a6eb83e86e8c.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"openclaw_config","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/openclaw_config.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libopenclaw_config-4870dc26ad0ae381.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_upstream_error_summary","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_upstream_error_summary.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_upstream_error_summary-eed5edf3e1ffc7c8.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_daemon","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_daemon.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_daemon-6f15734be066ebdb.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_claude_model_mapping","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_model_mapping.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_claude_model_mapping-c42f3b17f5827118.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"provider_add_noninteractive","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_add_noninteractive.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprovider_add_noninteractive-943cc65b6cb157e9.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"workspace_commands","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/workspace_commands.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libworkspace_commands-23ffb216892bd84e.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"provider_commands","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_commands.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprovider_commands-d1d3481e70e202e1.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"provider_service","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/provider_service.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libprovider_service-0d7fc170843c52dd.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"webdav_settings","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/webdav_settings.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libwebdav_settings-2ee9999d191aa3fc.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"app_type_parse","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/app_type_parse.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libapp_type_parse-76287b25fac9e800.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_claude_forwarder_alignment","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_forwarder_alignment.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_claude_forwarder_alignment-9e0af0f2ba461935.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_claude_streaming","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_claude_streaming.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_claude_streaming-5d743660f33181ab.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"settings_visible_apps","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/settings_visible_apps.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libsettings_visible_apps-078c632d29aa2ba6.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_takeover","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_takeover.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_takeover-d892cef1e97f633b.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["test"],"crate_types":["bin"],"name":"proxy_service","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/tests/proxy_service.rs","edition":"2021","doc":false,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libproxy_service-d60b6154e819c837.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/zhangyangrui/my_programes/cc-switch-cli/src-tauri#cc-switch@5.8.7","manifest_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/Cargo.toml","target":{"kind":["rlib"],"crate_types":["rlib"],"name":"cc_switch_lib","src_path":"/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":["default"],"filenames":["/home/zhangyangrui/my_programes/cc-switch-cli/src-tauri/target/debug/deps/libcc_switch_lib-f7640c01f82ba8a7.rmeta"],"executable":null,"fresh":false} +{"reason":"build-finished","success":true}