From 94819273c6e2ac19cdd4ee35dbbeca03e52e5577 Mon Sep 17 00:00:00 2001 From: GofMan5 Date: Fri, 30 Jan 2026 11:46:35 +0300 Subject: [PATCH 01/78] chore: bump version to 0.0.1 and update updater url --- package-lock.json | 511 +++++- package.json | 7 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/mod.rs | 4 +- src-tauri/src/constants.rs | 106 +- src-tauri/src/modules/account.rs | 59 +- src-tauri/src/modules/account_service.rs | 4 +- src-tauri/src/modules/http_api.rs | 2 +- src-tauri/src/modules/proxy_db.rs | 402 +++-- src-tauri/src/modules/quota.rs | 4 +- src-tauri/src/modules/scheduler.rs | 2 +- src-tauri/src/modules/tray.rs | 2 +- src-tauri/src/modules/update_checker.rs | 2 +- src-tauri/src/proxy/mappers/claude/request.rs | 580 ++++--- .../src/proxy/mappers/openai/streaming.rs | 29 +- src-tauri/src/proxy/middleware/auth.rs | 4 +- src-tauri/src/proxy/monitor.rs | 1 + src-tauri/src/proxy/server.rs | 2 +- src-tauri/src/proxy/sticky_config.rs | 9 + src-tauri/src/proxy/token_manager.rs | 306 +++- src-tauri/src/proxy/upstream/retry.rs | 4 +- src-tauri/tauri.conf.json | 4 +- src/components/accounts/AccountCard.tsx | 498 +++--- src/components/accounts/AccountGrid.tsx | 70 +- src/components/accounts/AccountTable.tsx | 790 +++------ src/components/accounts/AddAccountDialog.tsx | 29 +- src/components/dashboard/BestAccounts.tsx | 235 +-- src/components/dashboard/CurrentAccount.tsx | 319 ++-- .../dashboard/DashboardSkeleton.tsx | 50 + src/components/dashboard/StatCard.tsx | 86 + src/components/dashboard/StatsRow.tsx | 122 ++ src/components/layout/Layout.tsx | 19 +- src/components/layout/Navbar.tsx | 270 ++- .../layout/navbar/LanguageSelector.tsx | 97 ++ src/components/layout/navbar/NavLink.tsx | 32 + src/components/settings/CircuitBreaker.tsx | 140 +- src/components/settings/PinnedQuotaModels.tsx | 126 +- src/components/settings/QuotaProtection.tsx | 229 +-- .../settings/SchedulingSettings.tsx | 423 +++++ src/components/settings/SmartWarmup.tsx | 128 +- src/components/stats/RequestHealthCards.tsx | 195 +++ src/components/stats/StatsCharts.tsx | 294 ++++ src/components/stats/StatsSummary.tsx | 131 ++ src/components/ui/badge.tsx | 30 + src/components/ui/button.tsx | 52 + src/components/ui/card.tsx | 80 + src/components/ui/input.tsx | 24 + src/components/ui/label.tsx | 19 + src/components/ui/progress.tsx | 24 + src/components/ui/select.tsx | 149 ++ src/components/ui/skeleton.tsx | 15 + src/components/ui/switch.tsx | 48 + src/components/ui/tooltip.tsx | 28 + src/hooks/queries/useAccounts.ts | 55 + src/hooks/queries/useCurrentAccount.ts | 35 + src/hooks/useProxyModels.tsx | 18 +- src/lib/query-client.ts | 13 + src/lib/utils.ts | 6 + src/locales/en.json | 71 +- src/locales/ru.json | 74 +- src/locales/zh.json | 1 + src/main.tsx | 9 +- src/pages/Accounts.tsx | 614 ++++--- src/pages/ApiProxy.tsx | 120 +- src/pages/Dashboard.tsx | 303 ++-- src/pages/Settings.tsx | 1473 ++++++----------- src/pages/TokenStats.tsx | 725 ++------ src/types/config.ts | 163 +- tailwind.config.js | 19 +- 70 files changed, 5975 insertions(+), 4524 deletions(-) create mode 100644 src/components/dashboard/DashboardSkeleton.tsx create mode 100644 src/components/dashboard/StatCard.tsx create mode 100644 src/components/dashboard/StatsRow.tsx create mode 100644 src/components/layout/navbar/LanguageSelector.tsx create mode 100644 src/components/layout/navbar/NavLink.tsx create mode 100644 src/components/settings/SchedulingSettings.tsx create mode 100644 src/components/stats/RequestHealthCards.tsx create mode 100644 src/components/stats/StatsCharts.tsx create mode 100644 src/components/stats/StatsSummary.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/hooks/queries/useAccounts.ts create mode 100644 src/hooks/queries/useCurrentAccount.ts create mode 100644 src/lib/query-client.ts create mode 100644 src/lib/utils.ts diff --git a/package-lock.json b/package-lock.json index 7d8c2c88b..55b4fa3aa 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,19 @@ { "name": "antigravity-tools", - "version": "4.0.5", + "version": "4.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antigravity-tools", - "version": "4.0.5", + "version": "4.0.8", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-virtual": "^3.13.18", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "^2.5.1", @@ -1170,6 +1173,44 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.67", "resolved": "https://registry.npmmirror.com/@iconify-json/simple-icons/-/simple-icons-1.2.67.tgz", @@ -1275,6 +1316,415 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.1", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz", @@ -1725,6 +2175,60 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.92.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.92.0.tgz", + "integrity": "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.91.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.2.tgz", + "integrity": "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.92.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.14", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.18", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", @@ -2216,8 +2720,9 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } diff --git a/package.json b/package.json index 0ac9d8c8f..8263877d1 100755 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "4.0.8", + "version": "0.0.1", "name": "antigravity-tools", "type": "module", "private": true, @@ -14,6 +14,9 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-virtual": "^3.13.18", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "^2.5.1", @@ -50,4 +53,4 @@ "vitepress": "^1.6.4", "vue": "^3.5.27" } -} \ No newline at end of file +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4e7a7e764..f65ae93e5 100755 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -55,7 +55,7 @@ dependencies = [ [[package]] name = "antigravity_tools" -version = "4.0.8" +version = "0.0.1" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d303b9528..898bdf38c 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "antigravity_tools" -version = "4.0.8" +version = "0.0.1" description = "A Tauri App" authors = ["you"] license = "CC-BY-NC-SA-4.0" diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index f16fd9bc6..65b41c8ea 100755 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -13,7 +13,7 @@ pub mod cloudflared; /// 列出所有账号 #[tauri::command] pub async fn list_accounts() -> Result, String> { - modules::list_accounts() + modules::list_accounts().await } /// 添加账号 @@ -340,6 +340,8 @@ pub async fn save_config( instance.axum_server.update_debug_logging(&config.proxy).await; // 更新熔断配置 instance.token_manager.update_circuit_breaker_config(config.circuit_breaker.clone()).await; + // [FIX] Update sticky scheduling config + instance.token_manager.update_sticky_config(config.proxy.scheduling.clone()).await; tracing::debug!("已同步热更新反代服务配置"); } diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 0215cb790..a8b8044c5 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -1,113 +1,15 @@ use std::sync::LazyLock; -use regex::Regex; - -/// URL to fetch the latest Antigravity version -const VERSION_URL: &str = "https://antigravity-auto-updater-974169037036.us-central1.run.app"; - -/// Fallback version derived from Cargo.toml at compile time -const FALLBACK_VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// Pre-compiled regex for version parsing (X.Y.Z pattern) -static VERSION_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"\d+\.\d+\.\d+").expect("Invalid version regex") -}); - -/// Parse version from response text using pre-compiled regex -/// Matches semver pattern: X.Y.Z (e.g., "1.15.8") -fn parse_version(text: &str) -> Option { - VERSION_REGEX.find(text).map(|m| m.as_str().to_string()) -} - -/// Version source for logging -#[derive(Debug)] -enum VersionSource { - Remote, - CargoToml, -} - -/// Fetch version from remote endpoint, with fallback to Cargo.toml -/// Uses a separate thread to avoid blocking the main/UI thread -fn fetch_remote_version() -> (String, VersionSource) { - // Spawn a named thread for the blocking HTTP call - let handle = std::thread::Builder::new() - .name("version-fetch".to_string()) - .spawn(|| { - let client = reqwest::blocking::Client::builder() - .timeout(std::time::Duration::from_secs(3)) - .build() - .ok()?; - - let response = client.get(VERSION_URL).send().ok()?; - let text = response.text().ok()?; - parse_version(&text) - }); - - // Wait for the thread - match handle { - Ok(h) => { - if let Ok(Some(version)) = h.join() { - return (version, VersionSource::Remote); - } - } - Err(e) => { - tracing::debug!("Failed to spawn version-fetch thread: {}", e); - } - } - - // Fallback: Cargo.toml version (always valid at compile time) - (FALLBACK_VERSION.to_string(), VersionSource::CargoToml) -} /// Shared User-Agent string for all upstream API requests. -/// Format: antigravity/{version} {os}/{arch} -/// Version priority: remote endpoint > Cargo.toml -/// OS and architecture are detected at runtime. -pub static USER_AGENT: LazyLock = LazyLock::new(|| { - let (version, source) = fetch_remote_version(); - - tracing::info!( - version = %version, - source = ?source, - "User-Agent initialized" - ); - - format!( - "antigravity/{} {}/{}", - version, - std::env::consts::OS, - std::env::consts::ARCH - ) -}); +pub static USER_AGENT: LazyLock = + LazyLock::new(|| "antigravity/1.15.8 windows/amd64".to_string()); #[cfg(test)] mod tests { use super::*; #[test] - fn test_parse_version_from_updater_response() { - let text = "Auto updater is running. Stable Version: 1.15.8-5724687216017408"; - assert_eq!(parse_version(text), Some("1.15.8".to_string())); - } - - #[test] - fn test_parse_version_simple() { - assert_eq!(parse_version("1.15.8"), Some("1.15.8".to_string())); - assert_eq!(parse_version("Version: 2.0.0"), Some("2.0.0".to_string())); - assert_eq!(parse_version("v1.2.3"), Some("1.2.3".to_string())); - } - - #[test] - fn test_parse_version_invalid() { - assert_eq!(parse_version("no version here"), None); - assert_eq!(parse_version(""), None); - assert_eq!(parse_version("1.2"), None); // Only X.Y, not X.Y.Z - } - - #[test] - fn test_parse_version_with_suffix() { - // Regex only matches X.Y.Z, suffix is naturally excluded - let text = "antigravity/1.15.8 windows/amd64"; - assert_eq!(parse_version(text), Some("1.15.8".to_string())); + fn test_user_agent_format() { + assert_eq!(USER_AGENT.as_str(), "antigravity/1.15.8 windows/amd64"); } } - diff --git a/src-tauri/src/modules/account.rs b/src-tauri/src/modules/account.rs index 01b319036..7f78770e2 100755 --- a/src-tauri/src/modules/account.rs +++ b/src-tauri/src/modules/account.rs @@ -117,23 +117,48 @@ pub fn save_account(account: &Account) -> Result<(), String> { .map_err(|e| format!("failed_to_save_account_data: {}", e)) } -/// List all accounts -pub fn list_accounts() -> Result, String> { - crate::modules::logger::log_info("Listing accounts..."); +/// Load account data (Async) +pub async fn load_account_async(account_id: &str) -> Result { + let accounts_dir = get_accounts_dir()?; + let account_path = accounts_dir.join(format!("{}.json", account_id)); + + // Check existence asynchronously or just let read fail? + // tokio::fs::read_to_string handles "NotFound" usually, but let's be explicit if we want custom message. + if !account_path.exists() { // Sync check is fast enough for existence usually, but proper async is better. + return Err(format!("Account not found: {}", account_id)); + } + + let content = tokio::fs::read_to_string(&account_path).await + .map_err(|e| format!("failed_to_read_account_data: {}", e))?; + + serde_json::from_str(&content) + .map_err(|e| format!("failed_to_parse_account_data: {}", e)) +} + +/// List all accounts (Async + Parallel) +pub async fn list_accounts() -> Result, String> { + crate::modules::logger::log_info("Listing accounts (Parallel Async)..."); let index = load_account_index()?; - let mut accounts = Vec::new(); - for summary in &index.accounts { - match load_account(&summary.id) { - Ok(account) => accounts.push(account), - Err(e) => { - crate::modules::logger::log_error(&format!("Failed to load account {}: {}", summary.id, e)); - // [FIX #929] Removed auto-repair logic. - // We no longer silently delete account IDs from the index if the file is missing. - // This prevents account loss during version upgrades or temporary FS issues. - }, + // Create futures for all accounts + let futures: Vec<_> = index.accounts.iter().map(|summary| { + let id = summary.id.clone(); + async move { + match load_account_async(&id).await { + Ok(account) => Some(account), + Err(e) => { + crate::modules::logger::log_error(&format!("Failed to load account {}: {}", id, e)); + None + } + } } - } + }).collect(); + + // Run all futures concurrently + let results = futures::future::join_all(futures).await; + + // Collect valid results + let accounts: Vec = results.into_iter().flatten().collect(); Ok(accounts) } @@ -647,8 +672,8 @@ pub fn toggle_proxy_status(account_id: &str, enable: bool, reason: Option<&str>) /// Export all accounts' refresh_tokens #[allow(dead_code)] -pub fn export_accounts() -> Result, String> { - let accounts = list_accounts()?; +pub async fn export_accounts() -> Result, String> { + let accounts = list_accounts().await?; let mut exports = Vec::new(); for account in accounts { @@ -832,7 +857,7 @@ pub async fn refresh_all_quotas_logic() -> Result { "Starting batch refresh of all account quotas (Concurrent mode, max: {})", MAX_CONCURRENT )); - let accounts = list_accounts()?; + let accounts = list_accounts().await?; let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT)); diff --git a/src-tauri/src/modules/account_service.rs b/src-tauri/src/modules/account_service.rs index 1cafd319b..a3b4562b1 100644 --- a/src-tauri/src/modules/account_service.rs +++ b/src-tauri/src/modules/account_service.rs @@ -58,8 +58,8 @@ impl AccountService { } /// 列表获取 - pub fn list_accounts(&self) -> Result, String> { - modules::list_accounts() + pub async fn list_accounts(&self) -> Result, String> { + modules::list_accounts().await } /// 获取当前 ID diff --git a/src-tauri/src/modules/http_api.rs b/src-tauri/src/modules/http_api.rs index de7457a83..1cff92c0f 100644 --- a/src-tauri/src/modules/http_api.rs +++ b/src-tauri/src/modules/http_api.rs @@ -228,7 +228,7 @@ async fn health() -> impl IntoResponse { /// GET /accounts - Get all accounts async fn list_accounts() -> Result)> { - let accounts = account::list_accounts().map_err(|e| { + let accounts = account::list_accounts().await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e }), diff --git a/src-tauri/src/modules/proxy_db.rs b/src-tauri/src/modules/proxy_db.rs index 9537d2b51..aca9340c5 100644 --- a/src-tauri/src/modules/proxy_db.rs +++ b/src-tauri/src/modules/proxy_db.rs @@ -1,6 +1,6 @@ +use crate::proxy::monitor::ProxyRequestLog; use rusqlite::{params, Connection}; use std::path::PathBuf; -use crate::proxy::monitor::ProxyRequestLog; pub fn get_proxy_db_path() -> Result { let data_dir = crate::modules::account::get_data_dir()?; @@ -10,23 +10,26 @@ pub fn get_proxy_db_path() -> Result { fn connect_db() -> Result { let db_path = get_proxy_db_path()?; let conn = Connection::open(db_path).map_err(|e| e.to_string())?; - + // Enable WAL mode for better concurrency - conn.pragma_update(None, "journal_mode", "WAL").map_err(|e| e.to_string())?; - + conn.pragma_update(None, "journal_mode", "WAL") + .map_err(|e| e.to_string())?; + // Set busy timeout to 5000ms to avoid "database is locked" errors - conn.pragma_update(None, "busy_timeout", 5000).map_err(|e| e.to_string())?; - + conn.pragma_update(None, "busy_timeout", 5000) + .map_err(|e| e.to_string())?; + // Synchronous NORMAL is faster and safe enough for WAL - conn.pragma_update(None, "synchronous", "NORMAL").map_err(|e| e.to_string())?; - + conn.pragma_update(None, "synchronous", "NORMAL") + .map_err(|e| e.to_string())?; + Ok(conn) } pub fn init_db() -> Result<(), String> { // connect_db will initialize WAL mode and other pragmas let conn = connect_db()?; - + conn.execute( "CREATE TABLE IF NOT EXISTS request_logs ( id TEXT PRIMARY KEY, @@ -39,13 +42,20 @@ pub fn init_db() -> Result<(), String> { error TEXT )", [], - ).map_err(|e| e.to_string())?; + ) + .map_err(|e| e.to_string())?; // Try to add new columns (ignore errors if they exist) let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN request_body TEXT", []); let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN response_body TEXT", []); - let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN input_tokens INTEGER", []); - let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN output_tokens INTEGER", []); + let _ = conn.execute( + "ALTER TABLE request_logs ADD COLUMN input_tokens INTEGER", + [], + ); + let _ = conn.execute( + "ALTER TABLE request_logs ADD COLUMN output_tokens INTEGER", + [], + ); let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN account_email TEXT", []); let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN mapped_model TEXT", []); let _ = conn.execute("ALTER TABLE request_logs ADD COLUMN protocol TEXT", []); @@ -53,13 +63,15 @@ pub fn init_db() -> Result<(), String> { conn.execute( "CREATE INDEX IF NOT EXISTS idx_timestamp ON request_logs (timestamp DESC)", [], - ).map_err(|e| e.to_string())?; + ) + .map_err(|e| e.to_string())?; // Add status index for faster stats queries conn.execute( "CREATE INDEX IF NOT EXISTS idx_status ON request_logs (status)", [], - ).map_err(|e| e.to_string())?; + ) + .map_err(|e| e.to_string())?; Ok(()) } @@ -96,34 +108,38 @@ pub fn save_log(log: &ProxyRequestLog) -> Result<(), String> { pub fn get_logs_summary(limit: usize, offset: usize) -> Result, String> { let conn = connect_db()?; - let mut stmt = conn.prepare( - "SELECT id, timestamp, method, url, status, duration, model, error, + let mut stmt = conn + .prepare( + "SELECT id, timestamp, method, url, status, duration, model, error, NULL as request_body, NULL as response_body, input_tokens, output_tokens, account_email, mapped_model, protocol FROM request_logs ORDER BY timestamp DESC - LIMIT ?1 OFFSET ?2" - ).map_err(|e| e.to_string())?; + LIMIT ?1 OFFSET ?2", + ) + .map_err(|e| e.to_string())?; - let logs_iter = stmt.query_map([limit, offset], |row| { - Ok(ProxyRequestLog { - id: row.get(0)?, - timestamp: row.get(1)?, - method: row.get(2)?, - url: row.get(3)?, - status: row.get(4)?, - duration: row.get(5)?, - model: row.get(6)?, - mapped_model: row.get(13).unwrap_or(None), - account_email: row.get(12).unwrap_or(None), - error: row.get(7)?, - request_body: None, // Don't query large fields for list view - response_body: None, // Don't query large fields for list view - input_tokens: row.get(10).unwrap_or(None), - output_tokens: row.get(11).unwrap_or(None), - protocol: row.get(14).unwrap_or(None), + let logs_iter = stmt + .query_map([limit, offset], |row| { + Ok(ProxyRequestLog { + id: row.get(0)?, + timestamp: row.get(1)?, + method: row.get(2)?, + url: row.get(3)?, + status: row.get(4)?, + duration: row.get(5)?, + model: row.get(6)?, + mapped_model: row.get(13).unwrap_or(None), + account_email: row.get(12).unwrap_or(None), + error: row.get(7)?, + request_body: None, // Don't query large fields for list view + response_body: None, // Don't query large fields for list view + input_tokens: row.get(10).unwrap_or(None), + output_tokens: row.get(11).unwrap_or(None), + protocol: row.get(14).unwrap_or(None), + }) }) - }).map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; let mut logs = Vec::new(); for log in logs_iter { @@ -141,20 +157,24 @@ pub fn get_stats() -> Result { let conn = connect_db()?; // Optimized: Use single query instead of three separate queries - let (total_requests, success_count, error_count): (u64, u64, u64) = conn.query_row( - "SELECT + let (total_requests, success_count, error_count, avg_latency): (u64, u64, u64, f64) = conn + .query_row( + "SELECT COUNT(*) as total, - SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) as success, - SUM(CASE WHEN status < 200 OR status >= 400 THEN 1 ELSE 0 END) as error + COALESCE(SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END), 0) as success, + COALESCE(SUM(CASE WHEN status < 200 OR status >= 400 THEN 1 ELSE 0 END), 0) as error, + COALESCE(AVG(duration), 0.0) as avg_latency FROM request_logs", - [], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), - ).map_err(|e| e.to_string())?; + [], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), + ) + .map_err(|e| e.to_string())?; Ok(crate::proxy::monitor::ProxyStats { total_requests, success_count, error_count, + avg_latency, }) } @@ -162,13 +182,15 @@ pub fn get_stats() -> Result { pub fn get_log_detail(log_id: &str) -> Result { let conn = connect_db()?; - let mut stmt = conn.prepare( - "SELECT id, timestamp, method, url, status, duration, model, error, + let mut stmt = conn + .prepare( + "SELECT id, timestamp, method, url, status, duration, model, error, request_body, response_body, input_tokens, output_tokens, account_email, mapped_model, protocol FROM request_logs - WHERE id = ?1" - ).map_err(|e| e.to_string())?; + WHERE id = ?1", + ) + .map_err(|e| e.to_string())?; stmt.query_row([log_id], |row| { Ok(ProxyRequestLog { @@ -188,23 +210,26 @@ pub fn get_log_detail(log_id: &str) -> Result { output_tokens: row.get(11).unwrap_or(None), protocol: row.get(14).unwrap_or(None), }) - }).map_err(|e| e.to_string()) + }) + .map_err(|e| e.to_string()) } /// Cleanup old logs (keep last N days) pub fn cleanup_old_logs(days: i64) -> Result { let conn = connect_db()?; - + let cutoff_timestamp = chrono::Utc::now().timestamp() - (days * 24 * 3600); - - let deleted = conn.execute( - "DELETE FROM request_logs WHERE timestamp < ?1", - [cutoff_timestamp], - ).map_err(|e| e.to_string())?; - + + let deleted = conn + .execute( + "DELETE FROM request_logs WHERE timestamp < ?1", + [cutoff_timestamp], + ) + .map_err(|e| e.to_string())?; + // Execute VACUUM to reclaim disk space conn.execute("VACUUM", []).map_err(|e| e.to_string())?; - + Ok(deleted) } @@ -212,35 +237,36 @@ pub fn cleanup_old_logs(days: i64) -> Result { #[allow(dead_code)] pub fn limit_max_logs(max_count: usize) -> Result { let conn = connect_db()?; - - let deleted = conn.execute( - "DELETE FROM request_logs WHERE id NOT IN ( + + let deleted = conn + .execute( + "DELETE FROM request_logs WHERE id NOT IN ( SELECT id FROM request_logs ORDER BY timestamp DESC LIMIT ?1 )", - [max_count], - ).map_err(|e| e.to_string())?; - + [max_count], + ) + .map_err(|e| e.to_string())?; + conn.execute("VACUUM", []).map_err(|e| e.to_string())?; - + Ok(deleted) } pub fn clear_logs() -> Result<(), String> { let conn = connect_db()?; - conn.execute("DELETE FROM request_logs", []).map_err(|e| e.to_string())?; + conn.execute("DELETE FROM request_logs", []) + .map_err(|e| e.to_string())?; Ok(()) } /// Get total count of logs in database pub fn get_logs_count() -> Result { let conn = connect_db()?; - - let count: u64 = conn.query_row( - "SELECT COUNT(*) FROM request_logs", - [], - |row| row.get(0), - ).map_err(|e| e.to_string())?; - + + let count: u64 = conn + .query_row("SELECT COUNT(*) FROM request_logs", [], |row| row.get(0)) + .map_err(|e| e.to_string())?; + Ok(count) } @@ -249,9 +275,9 @@ pub fn get_logs_count() -> Result { /// errors_only: if true, only count logs with status < 200 or >= 400 pub fn get_logs_count_filtered(filter: &str, errors_only: bool) -> Result { let conn = connect_db()?; - + let filter_pattern = format!("%{}%", filter); - + let sql = if errors_only { "SELECT COUNT(*) FROM request_logs WHERE (status < 200 OR status >= 400)" } else if filter.is_empty() { @@ -260,26 +286,32 @@ pub fn get_logs_count_filtered(filter: &str, errors_only: bool) -> Result= 400 -pub fn get_logs_filtered(filter: &str, errors_only: bool, limit: usize, offset: usize) -> Result, String> { +pub fn get_logs_filtered( + filter: &str, + errors_only: bool, + limit: usize, + offset: usize, +) -> Result, String> { let conn = connect_db()?; let filter_pattern = format!("%{}%", filter); - + let sql = if errors_only { "SELECT id, timestamp, method, url, status, duration, model, error, NULL as request_body, NULL as response_body, @@ -307,69 +339,75 @@ pub fn get_logs_filtered(filter: &str, errors_only: bool, limit: usize, offset: let logs: Vec = if filter.is_empty() && !errors_only { let mut stmt = conn.prepare(sql).map_err(|e| e.to_string())?; - let logs_iter = stmt.query_map([limit, offset], |row| { - Ok(ProxyRequestLog { - id: row.get(0)?, - timestamp: row.get(1)?, - method: row.get(2)?, - url: row.get(3)?, - status: row.get(4)?, - duration: row.get(5)?, - model: row.get(6)?, - mapped_model: row.get(13).unwrap_or(None), - account_email: row.get(12).unwrap_or(None), - error: row.get(7)?, - request_body: None, - response_body: None, - input_tokens: row.get(10).unwrap_or(None), - output_tokens: row.get(11).unwrap_or(None), - protocol: row.get(14).unwrap_or(None), + let logs_iter = stmt + .query_map([limit, offset], |row| { + Ok(ProxyRequestLog { + id: row.get(0)?, + timestamp: row.get(1)?, + method: row.get(2)?, + url: row.get(3)?, + status: row.get(4)?, + duration: row.get(5)?, + model: row.get(6)?, + mapped_model: row.get(13).unwrap_or(None), + account_email: row.get(12).unwrap_or(None), + error: row.get(7)?, + request_body: None, + response_body: None, + input_tokens: row.get(10).unwrap_or(None), + output_tokens: row.get(11).unwrap_or(None), + protocol: row.get(14).unwrap_or(None), + }) }) - }).map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; logs_iter.filter_map(|r| r.ok()).collect() } else if errors_only { let mut stmt = conn.prepare(sql).map_err(|e| e.to_string())?; - let logs_iter = stmt.query_map([limit, offset], |row| { - Ok(ProxyRequestLog { - id: row.get(0)?, - timestamp: row.get(1)?, - method: row.get(2)?, - url: row.get(3)?, - status: row.get(4)?, - duration: row.get(5)?, - model: row.get(6)?, - mapped_model: row.get(13).unwrap_or(None), - account_email: row.get(12).unwrap_or(None), - error: row.get(7)?, - request_body: None, - response_body: None, - input_tokens: row.get(10).unwrap_or(None), - output_tokens: row.get(11).unwrap_or(None), - protocol: row.get(14).unwrap_or(None), + let logs_iter = stmt + .query_map([limit, offset], |row| { + Ok(ProxyRequestLog { + id: row.get(0)?, + timestamp: row.get(1)?, + method: row.get(2)?, + url: row.get(3)?, + status: row.get(4)?, + duration: row.get(5)?, + model: row.get(6)?, + mapped_model: row.get(13).unwrap_or(None), + account_email: row.get(12).unwrap_or(None), + error: row.get(7)?, + request_body: None, + response_body: None, + input_tokens: row.get(10).unwrap_or(None), + output_tokens: row.get(11).unwrap_or(None), + protocol: row.get(14).unwrap_or(None), + }) }) - }).map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; logs_iter.filter_map(|r| r.ok()).collect() } else { let mut stmt = conn.prepare(sql).map_err(|e| e.to_string())?; - let logs_iter = stmt.query_map(rusqlite::params![limit, offset, filter_pattern], |row| { - Ok(ProxyRequestLog { - id: row.get(0)?, - timestamp: row.get(1)?, - method: row.get(2)?, - url: row.get(3)?, - status: row.get(4)?, - duration: row.get(5)?, - model: row.get(6)?, - mapped_model: row.get(13).unwrap_or(None), - account_email: row.get(12).unwrap_or(None), - error: row.get(7)?, - request_body: None, - response_body: None, - input_tokens: row.get(10).unwrap_or(None), - output_tokens: row.get(11).unwrap_or(None), - protocol: row.get(14).unwrap_or(None), + let logs_iter = stmt + .query_map(rusqlite::params![limit, offset, filter_pattern], |row| { + Ok(ProxyRequestLog { + id: row.get(0)?, + timestamp: row.get(1)?, + method: row.get(2)?, + url: row.get(3)?, + status: row.get(4)?, + duration: row.get(5)?, + model: row.get(6)?, + mapped_model: row.get(13).unwrap_or(None), + account_email: row.get(12).unwrap_or(None), + error: row.get(7)?, + request_body: None, + response_body: None, + input_tokens: row.get(10).unwrap_or(None), + output_tokens: row.get(11).unwrap_or(None), + protocol: row.get(14).unwrap_or(None), + }) }) - }).map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; logs_iter.filter_map(|r| r.ok()).collect() }; @@ -380,33 +418,37 @@ pub fn get_logs_filtered(filter: &str, errors_only: bool, limit: usize, offset: pub fn get_all_logs_for_export() -> Result, String> { let conn = connect_db()?; - let mut stmt = conn.prepare( - "SELECT id, timestamp, method, url, status, duration, model, error, + let mut stmt = conn + .prepare( + "SELECT id, timestamp, method, url, status, duration, model, error, request_body, response_body, input_tokens, output_tokens, account_email, mapped_model, protocol FROM request_logs - ORDER BY timestamp DESC" - ).map_err(|e| e.to_string())?; + ORDER BY timestamp DESC", + ) + .map_err(|e| e.to_string())?; - let logs_iter = stmt.query_map([], |row| { - Ok(ProxyRequestLog { - id: row.get(0)?, - timestamp: row.get(1)?, - method: row.get(2)?, - url: row.get(3)?, - status: row.get(4)?, - duration: row.get(5)?, - model: row.get(6)?, - mapped_model: row.get(13).unwrap_or(None), - account_email: row.get(12).unwrap_or(None), - error: row.get(7)?, - request_body: row.get(8).unwrap_or(None), - response_body: row.get(9).unwrap_or(None), - input_tokens: row.get(10).unwrap_or(None), - output_tokens: row.get(11).unwrap_or(None), - protocol: row.get(14).unwrap_or(None), + let logs_iter = stmt + .query_map([], |row| { + Ok(ProxyRequestLog { + id: row.get(0)?, + timestamp: row.get(1)?, + method: row.get(2)?, + url: row.get(3)?, + status: row.get(4)?, + duration: row.get(5)?, + model: row.get(6)?, + mapped_model: row.get(13).unwrap_or(None), + account_email: row.get(12).unwrap_or(None), + error: row.get(7)?, + request_body: row.get(8).unwrap_or(None), + response_body: row.get(9).unwrap_or(None), + input_tokens: row.get(10).unwrap_or(None), + output_tokens: row.get(11).unwrap_or(None), + protocol: row.get(14).unwrap_or(None), + }) }) - }).map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; let mut logs = Vec::new(); for log in logs_iter { @@ -421,11 +463,15 @@ pub fn get_logs_by_ids(ids: &[String]) -> Result, String> { if ids.is_empty() { return Ok(Vec::new()); } - + let conn = connect_db()?; - + // Build placeholders for IN clause - let placeholders: Vec = ids.iter().enumerate().map(|(i, _)| format!("?{}", i + 1)).collect(); + let placeholders: Vec = ids + .iter() + .enumerate() + .map(|(i, _)| format!("?{}", i + 1)) + .collect(); let sql = format!( "SELECT id, timestamp, method, url, status, duration, model, error, request_body, response_body, input_tokens, output_tokens, @@ -437,29 +483,31 @@ pub fn get_logs_by_ids(ids: &[String]) -> Result, String> { ); let mut stmt = conn.prepare(&sql).map_err(|e| e.to_string())?; - + // Convert ids to params let params: Vec<&dyn rusqlite::ToSql> = ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect(); - - let logs_iter = stmt.query_map(params.as_slice(), |row| { - Ok(ProxyRequestLog { - id: row.get(0)?, - timestamp: row.get(1)?, - method: row.get(2)?, - url: row.get(3)?, - status: row.get(4)?, - duration: row.get(5)?, - model: row.get(6)?, - mapped_model: row.get(13).unwrap_or(None), - account_email: row.get(12).unwrap_or(None), - error: row.get(7)?, - request_body: row.get(8).unwrap_or(None), - response_body: row.get(9).unwrap_or(None), - input_tokens: row.get(10).unwrap_or(None), - output_tokens: row.get(11).unwrap_or(None), - protocol: row.get(14).unwrap_or(None), + + let logs_iter = stmt + .query_map(params.as_slice(), |row| { + Ok(ProxyRequestLog { + id: row.get(0)?, + timestamp: row.get(1)?, + method: row.get(2)?, + url: row.get(3)?, + status: row.get(4)?, + duration: row.get(5)?, + model: row.get(6)?, + mapped_model: row.get(13).unwrap_or(None), + account_email: row.get(12).unwrap_or(None), + error: row.get(7)?, + request_body: row.get(8).unwrap_or(None), + response_body: row.get(9).unwrap_or(None), + input_tokens: row.get(10).unwrap_or(None), + output_tokens: row.get(11).unwrap_or(None), + protocol: row.get(14).unwrap_or(None), + }) }) - }).map_err(|e| e.to_string())?; + .map_err(|e| e.to_string())?; let mut logs = Vec::new(); for log in logs_iter { diff --git a/src-tauri/src/modules/quota.rs b/src-tauri/src/modules/quota.rs index 9d99d74a8..a3ac66f1e 100755 --- a/src-tauri/src/modules/quota.rs +++ b/src-tauri/src/modules/quota.rs @@ -319,7 +319,7 @@ pub async fn warm_up_all_accounts() -> Result { let mut retry_count = 0; loop { - let target_accounts = crate::modules::account::list_accounts().unwrap_or_default(); + let target_accounts = crate::modules::account::list_accounts().await.unwrap_or_default(); if target_accounts.is_empty() { return Ok("No accounts available".to_string()); @@ -460,7 +460,7 @@ pub async fn warm_up_all_accounts() -> Result { /// Warmup for single account pub async fn warm_up_account(account_id: &str) -> Result { - let accounts = crate::modules::account::list_accounts().unwrap_or_default(); + let accounts = crate::modules::account::list_accounts().await.unwrap_or_default(); let account_owned = accounts.iter().find(|a| a.id == account_id).cloned().ok_or_else(|| "Account not found".to_string())?; let email = account_owned.email.clone(); diff --git a/src-tauri/src/modules/scheduler.rs b/src-tauri/src/modules/scheduler.rs index 2486e13c9..612f785dc 100644 --- a/src-tauri/src/modules/scheduler.rs +++ b/src-tauri/src/modules/scheduler.rs @@ -71,7 +71,7 @@ pub fn start_scheduler(app_handle: Option, proxy_state: crate: } // Get all accounts (no longer filtering by level) - let Ok(accounts) = account::list_accounts() else { + let Ok(accounts) = account::list_accounts().await else { continue; }; diff --git a/src-tauri/src/modules/tray.rs b/src-tauri/src/modules/tray.rs index 4c5d37244..70bb74e3f 100644 --- a/src-tauri/src/modules/tray.rs +++ b/src-tauri/src/modules/tray.rs @@ -111,7 +111,7 @@ pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { "switch_next" => { tauri::async_runtime::spawn(async move { // 1. Get all accounts - if let Ok(accounts) = modules::list_accounts() { + if let Ok(accounts) = modules::list_accounts().await { if accounts.is_empty() { return; } let current_id = modules::get_current_account_id().unwrap_or(None); diff --git a/src-tauri/src/modules/update_checker.rs b/src-tauri/src/modules/update_checker.rs index 333d2e789..e8f4de58a 100644 --- a/src-tauri/src/modules/update_checker.rs +++ b/src-tauri/src/modules/update_checker.rs @@ -49,7 +49,7 @@ struct GitHubRelease { /// Check for updates from GitHub releases pub async fn check_for_updates() -> Result { let client = reqwest::Client::builder() - .user_agent("Antigravity-Manager") + .user_agent(crate::constants::USER_AGENT.as_str()) .timeout(std::time::Duration::from_secs(10)) .build() .map_err(|e| { diff --git a/src-tauri/src/proxy/mappers/claude/request.rs b/src-tauri/src/proxy/mappers/claude/request.rs index 194c8739a..d1d81857c 100644 --- a/src-tauri/src/proxy/mappers/claude/request.rs +++ b/src-tauri/src/proxy/mappers/claude/request.rs @@ -66,22 +66,22 @@ fn build_safety_settings() -> Value { } /// 清理消息中的 cache_control 字段 -/// +/// /// 这个函数会深度遍历所有消息内容块,移除 cache_control 字段。 /// 这是必要的,因为: /// 1. VS Code 等客户端会将历史消息(包含 cache_control)原封不动发回 /// 2. Anthropic API 不接受请求中包含 cache_control 字段 /// 3. 即使是转发到 Gemini,也应该清理以保持协议纯净性 -/// +/// /// [FIX #593] 增强版本:添加详细日志用于调试 MCP 工具兼容性问题 pub fn clean_cache_control_from_messages(messages: &mut [Message]) { tracing::info!( "[DEBUG-593] Starting cache_control cleanup for {} messages", messages.len() ); - + let mut total_cleaned = 0; - + for (idx, msg) in messages.iter_mut().enumerate() { if let MessageContent::Array(blocks) = &mut msg.content { for (block_idx, block) in blocks.iter_mut().enumerate() { @@ -136,7 +136,7 @@ pub fn clean_cache_control_from_messages(messages: &mut [Message]) { } } } - + if total_cleaned > 0 { tracing::info!( "[DEBUG-593] Cache control cleanup complete: removed {} cache_control fields", @@ -148,7 +148,7 @@ pub fn clean_cache_control_from_messages(messages: &mut [Message]) { } /// [FIX #593] 递归深度清理 JSON 中的 cache_control 字段 -/// +/// /// 用于处理嵌套结构和非标准位置的 cache_control。 /// 这是最后一道防线,确保发送给 Antigravity 的请求中不包含任何 cache_control。 fn deep_clean_cache_control(value: &mut Value) { @@ -171,7 +171,7 @@ fn deep_clean_cache_control(value: &mut Value) { } /// [FIX #564] Sort blocks in assistant messages to ensure thinking blocks are first -/// +/// /// When context compression (kilo) reorders message blocks, thinking blocks may appear /// after text blocks. Claude/Anthropic API requires thinking blocks to be first if /// any thinking blocks exist in the message. This function pre-sorts blocks to ensure @@ -182,12 +182,12 @@ fn sort_thinking_blocks_first(messages: &mut [Message]) { if let MessageContent::Array(blocks) = &mut msg.content { // [FIX #709] Triple-stage partition: [Thinking, Text, ToolUse] // This ensures protocol compliance while maintaining logical order. - + let mut thinking_blocks: Vec = Vec::new(); let mut text_blocks: Vec = Vec::new(); let mut tool_blocks: Vec = Vec::new(); let mut other_blocks: Vec = Vec::new(); - + let original_len = blocks.len(); let mut needs_reorder = false; let mut saw_non_thinking = false; @@ -215,7 +215,8 @@ fn sort_thinking_blocks_first(messages: &mut [Message]) { // This also handles empty text block filtering. for block in blocks.drain(..) { match &block { - ContentBlock::Thinking { .. } | ContentBlock::RedactedThinking { .. } => { + ContentBlock::Thinking { .. } + | ContentBlock::RedactedThinking { .. } => { thinking_blocks.push(block); } ContentBlock::Text { text } => { @@ -251,7 +252,7 @@ fn sort_thinking_blocks_first(messages: &mut [Message]) { } /// 合并 ClaudeRequest 中连续的同角色消息 -/// +/// /// 场景: 当从 Spec/Plan 模式切换回编码模式时,可能出现连续两条 "user" 消息 /// (一条是 ToolResult,一条是 )。 /// 这会违反角色交替规则,导致 400 报错。 @@ -279,7 +280,9 @@ pub fn merge_consecutive_messages(messages: &mut Vec) { *current_text = format!("{}\n\n{}", current_text, next_text); } (MessageContent::String(current_text), MessageContent::Array(next_blocks)) => { - let mut new_blocks = vec![ContentBlock::Text { text: current_text.clone() }]; + let mut new_blocks = vec![ContentBlock::Text { + text: current_text.clone(), + }]; new_blocks.extend(next_blocks); current.content = MessageContent::Array(new_blocks); } @@ -344,11 +347,11 @@ pub fn transform_claude_request_in( merge_consecutive_messages(&mut cleaned_req.messages); clean_cache_control_from_messages(&mut cleaned_req.messages); - + // [FIX #564] Pre-sort thinking blocks to be first in assistant messages // This handles cases where context compression (kilo) incorrectly reorders blocks sort_thinking_blocks_first(&mut cleaned_req.messages); - + let claude_req = &cleaned_req; // 后续使用清理后的请求 // [NEW] Generate session ID for signature tracking @@ -362,7 +365,7 @@ pub fn transform_claude_request_in( .as_ref() .map(|tools| { tools.iter().any(|t| { - t.is_web_search() + t.is_web_search() || t.name.as_deref() == Some("google_search") || t.type_.as_deref() == Some("web_search_20250305") }) @@ -378,7 +381,10 @@ pub fn transform_claude_request_in( .as_ref() .map(|tools| { tools.iter().any(|t| { - t.name.as_deref().map(|n| n.starts_with("mcp__")).unwrap_or(false) + t.name + .as_deref() + .map(|n| n.starts_with("mcp__")) + .unwrap_or(false) }) }) .unwrap_or(false); @@ -394,7 +400,8 @@ pub fn transform_claude_request_in( } // 1. System Instruction (注入动态身份防护 & MCP XML 协议) - let system_instruction = build_system_instruction(&claude_req.system, &claude_req.model, has_mcp_tools); + let system_instruction = + build_system_instruction(&claude_req.system, &claude_req.model, has_mcp_tools); // Map model name (Use standard mapping) // [IMPROVED] 提取 web search 模型为常量,便于维护 @@ -409,28 +416,29 @@ pub fn transform_claude_request_in( } else { crate::proxy::common::model_mapping::map_claude_model_to_gemini(&claude_req.model) }; - + // 将 Claude 工具转为 Value 数组以便探测联网 let tools_val: Option> = claude_req.tools.as_ref().map(|list| { - list.iter().map(|t| serde_json::to_value(t).unwrap_or(json!({}))).collect() + list.iter() + .map(|t| serde_json::to_value(t).unwrap_or(json!({}))) + .collect() }); - // Resolve grounding config let config = crate::proxy::mappers::common_utils::resolve_request_config( &claude_req.model, &mapped_model, &tools_val, - claude_req.size.as_deref(), // [NEW] Pass size parameter - claude_req.quality.as_deref() // [NEW] Pass quality parameter + claude_req.size.as_deref(), // [NEW] Pass size parameter + claude_req.quality.as_deref(), // [NEW] Pass quality parameter ); - + // [CRITICAL FIX] Disable dummy thought injection for Vertex AI // [CRITICAL FIX] Disable dummy thought injection for Vertex AI // Vertex AI rejects thinking blocks without valid signatures // Even if thinking is enabled, we should NOT inject dummy blocks for historical messages let allow_dummy_thought = false; - + // Check if thinking is enabled in the request let mut is_thinking_enabled = claude_req .thinking @@ -445,9 +453,10 @@ pub fn transform_claude_request_in( // [NEW FIX] Check if target model supports thinking // Only models with "-thinking" suffix or Claude models support thinking // Regular Gemini models (gemini-2.5-flash, gemini-2.5-pro) do NOT support thinking - let target_model_supports_thinking = mapped_model.contains("-thinking") - || mapped_model.starts_with("claude-"); - + let target_model_supports_thinking = mapped_model.contains("-thinking") + || mapped_model.starts_with("claude-") + || mapped_model.contains("gemini-"); + if is_thinking_enabled && !target_model_supports_thinking { tracing::warn!( "[Thinking-Mode] Target model '{}' does not support thinking. Force disabling thinking mode.", @@ -461,8 +470,8 @@ pub fn transform_claude_request_in( if is_thinking_enabled { let should_disable = should_disable_thinking_due_to_history(&claude_req.messages); if should_disable { - tracing::warn!("[Thinking-Mode] Automatically disabling thinking checks due to incompatible tool-use history (mixed application)"); - is_thinking_enabled = false; + tracing::warn!("[Thinking-Mode] Automatically disabling thinking checks due to incompatible tool-use history (mixed application)"); + is_thinking_enabled = false; } } @@ -470,17 +479,19 @@ pub fn transform_claude_request_in( // disable thinking to prevent Gemini 3 Pro rejection if is_thinking_enabled { let global_sig = get_thought_signature(); - + // Check if there are any thinking blocks in message history let has_thinking_history = claude_req.messages.iter().any(|m| { if m.role == "assistant" { if let MessageContent::Array(blocks) = &m.content { - return blocks.iter().any(|b| matches!(b, ContentBlock::Thinking { .. })); + return blocks + .iter() + .any(|b| matches!(b, ContentBlock::Thinking { .. })); } } false }); - + // Check if there are function calls in the request let has_function_calls = claude_req.messages.iter().any(|m| { if let MessageContent::Array(blocks) = &m.content { @@ -496,16 +507,20 @@ pub fn transform_claude_request_in( // we use permissive mode and let upstream handle validation. // We only enforce strict signature checks when function calls are involved. let needs_signature_check = has_function_calls; - + if !has_thinking_history && is_thinking_enabled { - tracing::info!( + tracing::info!( "[Thinking-Mode] First thinking request detected. Using permissive mode - \ signature validation will be handled by upstream API." ); } if needs_signature_check - && !has_valid_signature_for_function_calls(&claude_req.messages, &global_sig, &session_id) + && !has_valid_signature_for_function_calls( + &claude_req.messages, + &global_sig, + &session_id, + ) { tracing::warn!( "[Thinking-Mode] [FIX #295] No valid signature found for function calls. \ @@ -516,7 +531,8 @@ pub fn transform_claude_request_in( } // 4. Generation Config & Thinking (Pass final is_thinking_enabled) - let generation_config = build_generation_config(claude_req, has_web_search_tool, is_thinking_enabled); + let generation_config = + build_generation_config(claude_req, has_web_search_tool, is_thinking_enabled); // 2. Contents (Messages) let contents = build_google_contents( @@ -618,7 +634,7 @@ pub fn transform_claude_request_in( } /// 检查是否因为历史消息原因需要禁用 Thinking -/// +/// /// 场景: 如果最后一条 Assistant 消息处于 Tool Use 流程中,但没有 Thinking 块, /// 说明这是一个由非 Thinking 模型发起的流程。此时强制开启 Thinking 会导致: /// "final assistant message must start with a thinking block" 错误。 @@ -628,9 +644,13 @@ fn should_disable_thinking_due_to_history(messages: &[Message]) -> bool { for msg in messages.iter().rev() { if msg.role == "assistant" { if let MessageContent::Array(blocks) = &msg.content { - let has_tool_use = blocks.iter().any(|b| matches!(b, ContentBlock::ToolUse { .. })); - let has_thinking = blocks.iter().any(|b| matches!(b, ContentBlock::Thinking { .. })); - + let has_tool_use = blocks + .iter() + .any(|b| matches!(b, ContentBlock::ToolUse { .. })); + let has_thinking = blocks + .iter() + .any(|b| matches!(b, ContentBlock::Thinking { .. })); + // 如果有工具调用,但没有 Thinking 块 -> 不兼容 if has_tool_use && !has_thinking { tracing::info!("[Thinking-Mode] Detected ToolUse without Thinking in history. Requesting disable."); @@ -675,12 +695,12 @@ const MIN_SIGNATURE_LENGTH: usize = 50; /// [FIX #295] Check if we have any valid signature available for function calls /// This prevents Gemini 3 Pro from rejecting requests due to missing thought_signature -/// +/// /// [NEW FIX] Now also checks Session Cache to support retry scenarios fn has_valid_signature_for_function_calls( messages: &[Message], global_sig: &Option, - session_id: &str, // NEW: Add session_id parameter + session_id: &str, // NEW: Add session_id parameter ) -> bool { // 1. Check global store (deprecated but kept for compatibility) if let Some(sig) = global_sig { @@ -728,7 +748,7 @@ fn has_valid_signature_for_function_calls( } } } - + tracing::warn!( "[Signature-Check] No valid signature found (session: {}, checked: global store, session cache, message history)", session_id @@ -737,7 +757,11 @@ fn has_valid_signature_for_function_calls( } /// 构建 System Instruction (支持动态身份映射与 Prompt 隔离) -fn build_system_instruction(system: &Option, _model_name: &str, has_mcp_tools: bool) -> Option { +fn build_system_instruction( + system: &Option, + _model_name: &str, + has_mcp_tools: bool, +) -> Option { let mut parts = Vec::new(); // [NEW] Antigravity 身份指令 (原始简化版) @@ -745,7 +769,7 @@ fn build_system_instruction(system: &Option, _model_name: &str, ha You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.\n\ **Absolute paths only**\n\ **Proactiveness**"; - + // [HYBRID] 检查用户是否已提供 Antigravity 身份 let mut user_has_antigravity = false; if let Some(sys) = system { @@ -857,8 +881,11 @@ fn build_contents( // 检查该文本是否与上一轮任务描述完全一致。 if !is_assistant && *previous_was_tool_result { if let Some(last_task) = last_user_task_text_normalized { - let current_normalized = text.replace(|c: char| c.is_whitespace(), ""); - if !current_normalized.is_empty() && current_normalized == *last_task { + let current_normalized = + text.replace(|c: char| c.is_whitespace(), ""); + if !current_normalized.is_empty() + && current_normalized == *last_task + { tracing::info!("[Claude-Request] Dropping duplicated task text echo (len: {})", text.len()); continue; } @@ -870,13 +897,21 @@ fn build_contents( // 记录最近一次 User 任务文本用于后续比对 if !is_assistant { - *last_user_task_text_normalized = Some(text.replace(|c: char| c.is_whitespace(), "")); + *last_user_task_text_normalized = + Some(text.replace(|c: char| c.is_whitespace(), "")); } *previous_was_tool_result = false; } } - ContentBlock::Thinking { thinking, signature, .. } => { - tracing::debug!("[DEBUG-TRANSFORM] Processing thinking block. Sig: {:?}", signature); + ContentBlock::Thinking { + thinking, + signature, + .. + } => { + tracing::debug!( + "[DEBUG-TRANSFORM] Processing thinking block. Sig: {:?}", + signature + ); // [HOTFIX] Gemini Protocol Enforcement: Thinking block MUST be the first block. // If we already have content (like Text), we must downgrade this thinking block to Text. @@ -926,16 +961,18 @@ fn build_contents( saw_non_thinking = true; continue; } - - let cached_family = crate::proxy::SignatureCache::global().get_signature_family(sig); + + let cached_family = + crate::proxy::SignatureCache::global().get_signature_family(sig); match cached_family { Some(family) => { // Check compatibility // [NEW] If is_retry is true, force incompatibility to strip historical signatures // which likely caused the previous 400 error. - let compatible = !is_retry && is_model_compatible(&family, mapped_model); - + let compatible = + !is_retry && is_model_compatible(&family, mapped_model); + if !compatible { tracing::warn!( "[Thinking-Signature] {} signature (Family: {}, Target: {}). Downgrading to text.", @@ -970,7 +1007,9 @@ fn build_contents( "thought": true, "thoughtSignature": sig }); - crate::proxy::common::json_schema::clean_json_schema(&mut part); + crate::proxy::common::json_schema::clean_json_schema( + &mut part, + ); parts.push(part); } else { // Unknown and too short: downgrade to text for safety @@ -986,7 +1025,9 @@ fn build_contents( } } else { // No signature: downgrade to text - tracing::warn!("[Thinking-Signature] No signature provided. Downgrading to text."); + tracing::warn!( + "[Thinking-Signature] No signature provided. Downgrading to text." + ); parts.push(json!({"text": thinking})); saw_non_thinking = true; } @@ -1022,12 +1063,21 @@ fn build_contents( saw_non_thinking = true; } } - ContentBlock::ToolUse { id, name, input, signature, .. } => { + ContentBlock::ToolUse { + id, + name, + input, + signature, + .. + } => { let mut final_input = input.clone(); - + // [New] 利用通用引擎修正参数类型 (替代以前硬编码的 shell 工具修复逻辑) if let Some(original_schema) = tool_name_to_schema.get(name) { - crate::proxy::common::json_schema::fix_tool_call_args(&mut final_input, original_schema); + crate::proxy::common::json_schema::fix_tool_call_args( + &mut final_input, + original_schema, + ); } let mut part = json!({ @@ -1152,7 +1202,8 @@ fn build_contents( let is_google_cloud = mapped_model.starts_with("projects/"); if is_thinking_enabled && !is_google_cloud { tracing::debug!("[Tool-Signature] Adding GEMINI_SKIP_SIGNATURE for tool_use: {}", id); - part["thoughtSignature"] = json!("skip_thought_signature_validator"); + part["thoughtSignature"] = + json!("skip_thought_signature_validator"); } } parts.push(part); @@ -1190,7 +1241,9 @@ fn build_contents( Some(text.to_string()) } else if block.get("source").is_some() { // If it's an image/document, replace with placeholder - if block.get("type").and_then(|v| v.as_str()) == Some("image") { + if block.get("type").and_then(|v| v.as_str()) + == Some("image") + { Some("[image omitted to save context]".to_string()) } else { None @@ -1207,8 +1260,15 @@ fn build_contents( // Smart Truncation: max chars limit const MAX_TOOL_RESULT_CHARS: usize = 200_000; if merged_content.len() > MAX_TOOL_RESULT_CHARS { - tracing::warn!("Truncating tool result from {} chars to {}", merged_content.len(), MAX_TOOL_RESULT_CHARS); - let mut truncated = merged_content.chars().take(MAX_TOOL_RESULT_CHARS).collect::(); + tracing::warn!( + "Truncating tool result from {} chars to {}", + merged_content.len(), + MAX_TOOL_RESULT_CHARS + ); + let mut truncated = merged_content + .chars() + .take(MAX_TOOL_RESULT_CHARS) + .collect::(); truncated.push_str("\n...[truncated output]"); merged_content = truncated; } @@ -1242,7 +1302,8 @@ fn build_contents( *previous_was_tool_result = true; } // ContentBlock::RedactedThinking handled above at line 583 - ContentBlock::ServerToolUse { .. } | ContentBlock::WebSearchToolResult { .. } => { + ContentBlock::ServerToolUse { .. } + | ContentBlock::WebSearchToolResult { .. } => { // 搜索结果 block 不应由客户端发回给上游 (已由 tool_result 替代) continue; } @@ -1253,14 +1314,16 @@ fn build_contents( // If this is a User message, check if we need to inject missing tool results if !is_assistant && !pending_tool_use_ids.is_empty() { - let missing_ids: Vec<_> = pending_tool_use_ids.iter() + let missing_ids: Vec<_> = pending_tool_use_ids + .iter() .filter(|id| !current_turn_tool_result_ids.contains(*id)) .cloned() .collect(); if !missing_ids.is_empty() { tracing::warn!("[Elastic-Recovery] Injecting {} missing tool results into User message (IDs: {:?})", missing_ids.len(), missing_ids); - for id in missing_ids.iter().rev() { // Insert in reverse order to maintain order at index 0? No, just insert at 0. + for id in missing_ids.iter().rev() { + // Insert in reverse order to maintain order at index 0? No, just insert at 0. let name = tool_id_to_name.get(id).cloned().unwrap_or(id.clone()); let synthetic_part = json!({ "functionResponse": { @@ -1283,13 +1346,11 @@ fn build_contents( // [Optimization] Apply this to ALL assistant messages in history, not just the last one. // Vertex AI requires every assistant message to start with a thinking block when thinking is enabled. if allow_dummy_thought && is_assistant && is_thinking_enabled { - let has_thought_part = parts - .iter() - .any(|p| { - p.get("thought").and_then(|v| v.as_bool()).unwrap_or(false) - || p.get("thoughtSignature").is_some() - || p.get("thought").and_then(|v| v.as_str()).is_some() // 某些情况下可能是 text + thought: true 的组合 - }); + let has_thought_part = parts.iter().any(|p| { + p.get("thought").and_then(|v| v.as_bool()).unwrap_or(false) + || p.get("thoughtSignature").is_some() + || p.get("thought").and_then(|v| v.as_str()).is_some() // 某些情况下可能是 text + thought: true 的组合 + }); if !has_thought_part { // Prepend a dummy thinking block to satisfy Gemini v1internal requirements @@ -1300,7 +1361,10 @@ fn build_contents( "thought": true }), ); - tracing::debug!("Injected dummy thought block for historical assistant message at index {}", parts.len()); + tracing::debug!( + "Injected dummy thought block for historical assistant message at index {}", + parts.len() + ); } else { // [Crucial Check] 即使有 thought 块,也必须保证它位于 parts 的首位 (Index 0) // 且必须包含 thought: true 标记 @@ -1323,7 +1387,8 @@ fn build_contents( // 确保首项包含了 thought: true (防止只有 signature 的情况) if let Some(p0) = parts.get_mut(0) { if p0.get("thought").is_none() { - p0.as_object_mut().map(|obj| obj.insert("thought".to_string(), json!(true))); + p0.as_object_mut() + .map(|obj| obj.insert("thought".to_string(), json!(true))); } } } @@ -1363,7 +1428,8 @@ fn build_google_content( if role == "model" && !pending_tool_use_ids.is_empty() { tracing::warn!("[Elastic-Recovery] Detected interrupted tool chain (Assistant -> Assistant). Injecting synthetic User message for IDs: {:?}", pending_tool_use_ids); - let synthetic_parts: Vec = pending_tool_use_ids.iter() + let synthetic_parts: Vec = pending_tool_use_ids + .iter() .filter(|id| !existing_tool_result_ids.contains(*id)) // [FIX #632] Only inject if ID is truly missing .map(|id| { let name = tool_id_to_name.get(id).cloned().unwrap_or(id.clone()); @@ -1376,7 +1442,8 @@ fn build_google_content( "id": id } }) - }).collect(); + }) + .collect(); if !synthetic_parts.is_empty() { return Ok(json!({ @@ -1511,7 +1578,8 @@ fn merge_adjacent_roles(mut contents: Vec) -> Vec { if current_role == next_role { // Merge parts - if let Some(current_parts) = current_msg.get_mut("parts").and_then(|p| p.as_array_mut()) { + if let Some(current_parts) = current_msg.get_mut("parts").and_then(|p| p.as_array_mut()) + { if let Some(next_parts) = msg.get("parts").and_then(|p| p.as_array()) { current_parts.extend(next_parts.clone()); @@ -1579,7 +1647,10 @@ fn build_tools(tools: &Option>, has_web_search: bool) -> Result