diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e51f4e8b..91e8b4e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,25 @@ pre-fills a new PR's base with the repo's default branch, which is opened against `main`, click *Edit* next to the PR title and change the base — the commits and the discussion carry over. A bot will remind you. +## Layout + +``` +src/ React 19 + Tailwind 4 interface +src-tauri/ Tauri 2 shell: supervises the local core, connects to a + local or remote core, draws the menu bar item and the tray + menu, sends system notifications, installs updates +src-tauri/crates/ tw-adopt (pointing clients at the gateway, MCP configuration) + and tw-scan (scanning client configuration) +scripts/shots/ the product screenshot pipeline (see CONTRIBUTING.md) +``` + +The gateway itself lives in ThinkWatch Core; this repository holds no routing, +forwarding, or accounting logic. The app reaches core's control channel over a +unix socket on macOS and Linux and over a loopback port on Windows, or over a +TCP port when core runs on a server. Every connection begins with an encrypted +handshake (Noise `NNpsk0`) keyed with the control key from core's +configuration (`listen.control.key`); no TLS certificates are involved. + ## Scope, so you don't build something that gets declined These decisions are settled and not up for a PR: diff --git a/README.md b/README.md index b779696f..def28f1a 100644 --- a/README.md +++ b/README.md @@ -11,566 +11,79 @@ **[English](README.md) | [中文](README.zh-CN.md)** -ThinkWatch Lite is a desktop app for macOS, Windows and Linux that runs an AI -API gateway on the local machine. Claude Code, Codex and other clients of the -OpenAI and Anthropic APIs send their requests through it, and the app records -what each request cost, which upstream served it and which keys were redacted -before it was sent. The gateway can also be deployed on a server; the app then -connects to ThinkWatch Core on that server over an encrypted control channel. +A local gateway for Claude Code, Codex and other AI clients, on macOS, Windows +and Linux. Each client is connected once; after that, upstreams and models +change without touching its configuration. Every request is recorded with its +cost and route, and the API keys in it can be replaced before it leaves the +machine. The overview for the last seven days: tokens, cost and requests against the seven days before, with the estimated part of the cost and the unpriced requests stated; a trend chart stacked by model with the periods that had failures marked; and the models ranked by tokens and cost -The app runs in the menu bar on macOS and in the system tray on Windows and -Linux. It requires macOS 12 or later on Apple silicon, Windows 10 21H2 or later -on x64 or ARM64, or Linux on x86_64 or aarch64 (Ubuntu 22.04, Debian 12, -Fedora 36 or later). The interface is available in English and Simplified -Chinese; it follows the system language and can be changed in Settings. The -app updates itself on all three platforms, except that a Homebrew installation -is updated through Homebrew. - -## Install - -| Platform | Install | -|---|---| -| macOS, Apple silicon | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`, or [`ThinkWatch-Lite--arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -| Windows, x64 | [`ThinkWatch-Lite--x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -| Windows, ARM64 | [`ThinkWatch-Lite--arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -| Linux, x86_64 or aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`, or [`ThinkWatch-Lite--.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | - -The [Lite page](https://thinkwat.ch/lite#install) offers a one-click download -of the latest version and selects the Windows or Linux architecture -automatically. The gateway, -[ThinkWatch Core](https://github.com/ThinkWatchProject/ThinkWatch-Core), ships -inside the app; nothing else needs to be installed. - -### macOS - -```bash -brew install --cask thinkwatchproject/tap/thinkwatch-lite -``` - -A disk image is also available from the -[latest release](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest): -download `ThinkWatch-Lite--arm64.dmg`, check it against the sha256 -published beside it, and drag ThinkWatch Lite into Applications. The app is -**not signed by a registered Apple developer**, so macOS quarantines a -downloaded copy and refuses to open it until the attribute is removed: - -```bash -xattr -dr com.apple.quarantine "/Applications/ThinkWatch Lite.app" -``` - -The same can be done without a terminal: after the first refused launch, -choose Open Anyway in System Settings › Privacy & Security. Removing that -attribute is the only thing -[the cask](https://github.com/ThinkWatchProject/homebrew-tap) does beyond -copying the app out of the disk image. - -### Windows - -Download the installer for the machine's architecture from the -[latest release](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest): -`ThinkWatch-Lite--x64-setup.exe` for most PCs, or -`ThinkWatch-Lite--arm64-setup.exe` for a PC with an ARM processor. -Check it against the sha256 published beside it: - -```powershell -Get-FileHash .\ThinkWatch-Lite--x64-setup.exe -``` - -The installer sets the app up for all users in Program Files, so Windows asks -for administrator permission. It requires Windows 10 21H2 or later; WebView2, -which Windows 11 already includes, is downloaded during installation if it is -missing. - -The installer is **not code-signed**, and no certificate will be bought. -Running a downloaded copy brings up SmartScreen's full-screen warning, -"Windows protected your PC". Choose **More info**, then **Run anyway**. - -Once installed, the app runs from the notification area. Data is kept in -`%APPDATA%\ThinkWatch`. - -### Linux - -```bash -curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh | sh -``` - -The script downloads the AppImage for the machine's architecture, checks it -against the sha256 published beside it, installs it as -`~/Applications/ThinkWatch-Lite.AppImage` and starts it. Running it again -installs the latest version over the old one. - -To install by hand, download `ThinkWatch-Lite--x86_64.AppImage` or -`ThinkWatch-Lite--aarch64.AppImage` from the -[latest release](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest), -check it with `sha256sum -c`, allow it to run (`chmod +x`, or Properties › -"Allow executing file as program" in the file manager) and open it. Keep it in -a folder the user can write to, such as `~/Applications`, so that it can -update itself. The first launch adds ThinkWatch Lite to the application menu, -together with its icon and the `thinkwatch://` link handler. Only the AppImage -is published; there are no deb, rpm, Flatpak or Snap packages. - -An AppImage mounts itself with FUSE and needs `fusermount3` from the fuse3 -package (libfuse2 is not needed). Most desktops already include it; otherwise: - -| Distribution | Command | -|---|---| -| Ubuntu, Debian | `sudo apt install fuse3` | -| Fedora | `sudo dnf install fuse3` | -| Arch Linux | `sudo pacman -S fuse3` | -| openSUSE | `sudo zypper install fuse3` | - -The tray icon relies on AppIndicator. Ubuntu ships the GNOME extension for it; -Fedora's stock GNOME does not, and the AppIndicator extension has to be added. -Without a tray, closing the window leaves the gateway running, and launching -ThinkWatch Lite again from the application menu brings the window back. Data is -kept in `~/.thinkwatch`. - -- **Blank window on NVIDIA under Wayland:** start the app with - `WEBKIT_DISABLE_DMABUF_RENDERER=1`. -- **Other machines cannot reach the gateway:** firewalld, which Fedora enables - by default, blocks the gateway port until it is opened; Settings shows a - note about this when the gateway listens on the local network. -- **Uninstalling:** use Settings › Full uninstall first, which restores the clients - the app configured and removes the autostart and application menu entries, - then delete the AppImage. - -## Features - -The main window has nine pages: Overview, Traffic, Clients, Keys, Upstreams, -Routing, Security, MCP and Settings. - -### Usage and cost - -The Overview page reports tokens, cost and requests for the last 24 hours, -7 days, 30 days or a custom range, each against the period before; a live view -follows the last ten minutes. A trend chart stacks tokens or cost by model, and -the model ranking beneath it opens the matching requests. Further sections -cover the prompt cache (hit rate, the net savings it brought and the hit rate -per model), latency (median and 95th-percentile time to first token, per model -and per upstream), generation speed (median tokens per second, per model and -per upstream) and what each protection found. - -The cost figure states how much of it is estimated, for instance for a -response that was cut off before it finished. Requests whose model has no -price, and requests whose upstream reported no usage, are counted separately -and never added in as zero. Prices come from price sheets: the default one -follows LiteLLM's public prices and is refreshed once a day, and a custom one -applies a multiplier and its own prices for particular models, as needed for a -relay whose prices differ from the official ones. A subscription account such -as a ChatGPT sign-in is priced from the price sheet like any other upstream, -and an upstream such as a local model can be set to free. Each request's cost -is fixed when the request finishes, and the request records the price sheet -and the date of the prices it was costed with. - -### Traffic and sessions - -The Traffic page lists requests as they arrive: status, key, model, upstream, -time to first token and total time (with the generation speed on hover), tokens -and cost, with marks for a converted -API format, redacted keys and a blocked or suspicious tool call. The list can -be filtered by key, upstream and model, or narrowed to failed or unpriced -requests. The Sessions view groups the requests of one conversation into -turns, with the input tokens and the cost of each turn. - -A request opens into its timeline, its routing (the rule it matched, the group -it went through and each attempt with its status and duration), the request -and response bodies, and its usage and cost. A request from DeepSeek Harness -also shows the size of the session log it carried, the whole conversation the -client attaches to every request; the gateway removes it before a request goes -to an upstream other than DeepSeek. A finished request can be sent -again, unchanged, to another upstream after an estimate of its cost, and the -two responses are shown side by side. - - - - The traffic page: each request with its key, model, upstream, time to first byte and total time, tokens and cost, with marks for converted formats, redacted keys and a blocked request, and one request answered locally by the gateway - - -### Client setup - -The Clients page points Claude Code, Claude Desktop, Codex, opencode, Zed, -Aider and DeepSeek Harness at the gateway. Before anything is written, it lists -the fields that change and what else the change affects (the ChatGPT desktop -app, for instance, reads the same configuration file as Codex), shows the full -diff and backs up the original file. Only the settings that point the client at -the gateway change, and each client receives a key of its own. Claude Desktop is -connected through its official third-party inference mode, and the page lists -each of the files that change for it; a Claude Desktop managed by an -organization is left as it is. A connected client can be restored at any -time, on its own or together with all the others; a restored Codex keeps a -plain OpenAI entry in place of the gateway's, so sessions started while it was -connected can still be opened. opencode (v1 and v2) also -gets the list of models its key can use on the gateway; when that list -changes, the page offers to update it, through the same diff. Cursor, Continue -and Antigravity CLI come with step-by-step instructions and a key created for -them. For every client the page shows whether it is in use, waiting for its first -request or not in effect, and its requests over the last 24 hours. - -On Windows, Claude Code and Codex installed inside WSL appear in a group of -their own for each distribution, next to the clients on the computer itself. -They are pointed at the gateway on Windows, restored and diagnosed the same -way, each with a key separate from the Windows copy, and their files are edited -through `\\wsl.localhost`. They are given `127.0.0.1`, the same address as the -clients on Windows, which WSL reaches in two setups: - -- **WSL 1**, which shares the network with Windows. -- **WSL 2 with mirrored networking**: `networkingMode=mirrored` under `[wsl2]` - (or the older `[experimental]`) in `%USERPROFILE%\.wslconfig`. It needs - Windows 11 22H2 or later and WSL 2.0.5 or later. - -WSL 2 uses NAT networking by default, and the gateway on Windows cannot be -reached from inside WSL that way; the gateway does not listen on the WSL -virtual adapter for it. The WSL group then explains this instead of offering to -connect, and offers to switch to mirrored networking: `networkingMode` in -`.wslconfig` is added or changed, and nothing else in the file is touched, -through the same diff, confirmation and full backup as a client. The switch -takes effect once WSL restarts, which the page also offers (`wsl --shutdown`, -which stops every running distribution). A full uninstall leaves `.wslconfig` -as it is, and its backup is kept. On Windows 10 and Windows 11 21H2, which -have no mirrored networking, and with a WSL older than 2.0.5, the group says -so. When connected to a remote core, clients in WSL are pointed at the server -like those on Windows, whatever the networking. - - - - The clients page: Claude Code and Codex connected, each with its own key and its requests over the last 24 hours; opencode not connected; Cursor set up by hand and in use; Continue and Antigravity CLI not yet set up; Zed and Aider not detected - - -### Keys - -Clients reach the gateway with a key, on the local machine as well. The Keys -page lists the default key, used by clients that were not given one of their -own, and a key for each connected client, labelled with the client it belongs -to so that its requests can be told apart in Traffic. Each key has a route, -the models it may use (all, none, or chosen models and patterns such as -`gpt-5*`), an optional limit on concurrent requests, and its requests and cost -over the last 24 hours. A key can be disabled, which rejects every request -made with it, or rotated; rotating writes the new key into the configuration -of the client that uses it. - - - - The keys page: the default key and one key each for Claude Code, Codex and Cursor, with the route each key uses, the models it may use, and its requests and cost over the last 24 hours - - -### Upstreams - -Upstreams are the services requests are forwarded to: API keys for Anthropic, -OpenAI, Google Gemini, DeepSeek or any compatible endpoint, a ChatGPT account -or a Z.ai / BigModel account signed in from the app, relays such as -OpenRouter, and local models such as Ollama. A ChatGPT account shows its usage -limits and reset times. So does an upstream on a GLM Coding Plan, that is, one -whose address is on `api.z.ai` or `open.bigmodel.cn`, whether it was signed in -from the app or added with a key: its 5-hour and weekly limits and, on a plan -billed in credits, the credits left (“1,976 / 2,000 credits left”). When a -client and an upstream use different API formats, requests are converted -between Anthropic Messages, OpenAI Chat Completions, OpenAI Responses and -Gemini, and the fields that cannot be carried over are listed on the request. -Upstreams can be reached through an outbound proxy and priced with a price -sheet of their own; proxies and price sheets have tabs on the same page. A -connection test times the DNS lookup and the TCP, TLS and proxy handshakes -without incurring any cost; an inference test measures the time to first token -and estimates its cost before it runs. - -API keys and header values can be written as `${NAME}` to read a system -environment variable. On macOS these come from the login shell, so variables -exported in `~/.zshrc` and similar files apply, and the same holds on Linux -(`~/.bashrc`, `~/.profile` and so on); on Windows they are the -environment variables configured in system settings. After a variable changes, -reopening the app picks it up. Proxy variables such as `HTTPS_PROXY`, and -`PATH`, are not read. - - - - The upstreams page: API-key upstreams for Anthropic, DeepSeek and Gemini, a relay priced with its own price sheet, a ChatGPT Plus account with 58% of its 5-hour limit used, OpenRouter through a proxy and a local Ollama set to free, each with its requests, cost and median time to first byte over 24 hours - - -A relay or vendor can hand out an import link, `thinkwatch://import?…` or its -web form `https://thinkwat.ch/import#…`, that pre-fills a new upstream with a -name, base URL, protocol, API key and model list. The app shows the settings -and the host that will receive requests and the key in a confirmation dialog, -and writes nothing and contacts nothing before Create is chosen. A link only -ever adds one upstream: it cannot change existing ones, headers, proxies, -pricing or routing, and a key that refers to an environment variable is -rejected. The parameters and a link builder are in -[Import links](https://thinkwat.ch/docs/lite/import-links). - -### Routing and failover - -Each key follows a route, and keys without one follow the default route. A -route is a list of rules evaluated in order. A rule matches on the model, the -key, the client's API format, input tokens, `max_tokens`, the number of tools, -images, extended thinking, streaming, prompt caching or the kind of auxiliary -request; it then forwards the request to an upstream or a group, or refuses -it, and can rewrite the model, `max_tokens` or extended thinking. A group puts -several upstreams behind one name and decides the order in which they are -tried: as listed, manually selected, in turn, lowest latency first or lowest -cost first. When an attempt fails, the request moves on to the next upstream, -and by default a session stays on one upstream so that its prompt cache keeps -hitting. A map at the top of the page traces every key through its route and -groups to the upstreams. - - - - The routing page: a map from keys through routes and groups to upstreams, and the three routes with the rules each applies in order - - -Auxiliary requests that clients send on their own (health checks, warm-ups, -titles, topic detection and input suggestions) can be answered locally at no -cost, passed through, or routed by the rules. - -Every request records the rule it matched, the group it went through and each -attempt with its status and duration. A dry run evaluates the rules for a -given request and shows where it would go and why, without sending anything -and without incurring any cost. - - - - A routing dry run: a request from the cursor key for claude-sonnet-5 in the OpenAI Chat Completions format does not match the gemini rule, which says why, matches the catch-all rule and goes to the lowest-cost group, which tries relay and then anthropic, converting the request to Anthropic Messages - - -### Security - -The Security page holds five protections. They apply to every upstream and -every key alike, and each runs in one of three modes: Off, Observe (detect and -record, change nothing) or Enforce. The output limit starts Off and the other -four start in Observe, so out of the box no request is changed or blocked. - -- **Outbound redaction** looks for credentials in a request before it leaves: - API keys and tokens for Anthropic, OpenAI, GitHub, Slack, AWS, Google, - GitLab, Stripe, npm, DigitalOcean and SendGrid, private keys, JWTs and - passwords in connection strings. In Enforce mode they are replaced with - placeholders and restored where the response repeats them. Rules for - internal IP addresses and internal domains are included and start off. -- **Tool-call inspection** checks the tool calls a model returns for commands - that download or decode code and run it, send out environment variables or - credential files, read private keys or cloud credentials, or install startup - items and scheduled jobs. In - Enforce mode such a call cuts the response off, so the client never receives - a complete call to run. Deleting the home or root directory and making files - world-writable are only recorded by default. -- **Hidden characters** looks for Unicode tag characters and bidirectional - control characters in what the client sends, tool results included, and in - Enforce mode refuses the request. -- **Content filter** matches keywords or regular expressions against the - messages the client sends, tool results included, and in Enforce mode - refuses a request that matches a blocking rule. Of the built-in rules, the - three against explicit "ignore previous instructions" phrasing are on by - default; rules for jailbreaks, persona manipulation, prompt extraction and - their Chinese counterparts can be switched on. -- **Output limit** stops an answer that grows past a set number of characters, - 100,000 by default: a streamed answer is cut off at that point and a - non-streamed one is replaced with an error. Reasoning and tool-call - arguments do not count towards the limit. - -The page lists every rule. Built-in rules can be switched on or off one at a -time, and those for tool calls and content can be set to act or only record in -Enforce mode. Custom rules are regular expressions, or keywords for the content -filter, and any rule can be tried on a sample text first. Everything the -protections find is kept in the log on the first tab, together with the -request it came from. +## Highlights + +- **Connect once, switch freely.** Claude Code, Claude Desktop, Codex, + opencode, Zed, Aider and DeepSeek Harness are pointed at the gateway in one + step, with the change previewed, the original file backed up and a restore + always available; Cursor, Continue and Antigravity CLI come with + instructions. Switching upstreams then happens in the gateway alone. +- **Keys replaced before sending, dangerous commands stopped.** Outbound + redaction swaps API keys, private keys, JWTs and connection-string passwords + for placeholders before a request leaves, so a relay never sees them. + Tool-call inspection cuts off download-and-run commands and the like, and + hidden characters and prompt injection can be refused. The protections start + in Observe and switch to Enforce one by one. +- **MCP servers, skills and hooks, scanned.** The MCP servers of eight clients + side by side, with third-party servers marked, and a scan of client + configuration, skills, hooks and project instructions for hidden characters, + prompt injection, dangerous commands and overly broad permissions. +- **Every request traceable.** The rule a request matched, each upstream it + tried, any conversion between API formats and how its cost was calculated; + a finished request can be replayed against another upstream and compared + side by side. +- **Routing and failover.** Rules by model, tools, images, extended thinking + and more. When an upstream fails before the answer begins the next one takes + over, and each session stays on one upstream so its prompt cache keeps + hitting. Auxiliary requests such as title generation can be answered locally. +- **Any upstream, any API format.** API keys, Amazon Bedrock, ChatGPT and Z.ai + accounts, relays such as OpenRouter and local models, with conversion between + the Anthropic, OpenAI and Gemini APIs. +- **Costs stated as they are.** Estimated amounts are marked and requests + without a price are counted separately instead of as zero. +- **Remote core.** The gateway can also run on a Linux server; the app + connects to it over an encrypted control channel. The security log: credentials replaced before a request left, one of them matched by a custom rule; a download-and-run tool call cut off; and hidden characters, a delete command and an injected instruction recorded. Each entry names the key, client, model and upstream of its request -### MCP - -The MCP page covers what clients load from their own configuration files, -which does not pass through the gateway. - -- **Servers:** the MCP servers configured in Claude Code, Claude Desktop, - Cursor, Codex, opencode, Antigravity CLI, Zed and DeepSeek Harness, side by - side. A server can be copied from one client to another or removed from a - client; the change is shown before anything is written, and the original file - is backed up. Copying and removing work for Claude Code, Claude Desktop, - Cursor and Codex; opencode, Antigravity CLI, Zed and DeepSeek Harness are - listed but not written to. A remote server on another host is marked as - third party, since using it sends the surrounding context to that host, and - a server configured differently in different clients is marked as well and - can be compared side by side. -- **Skills and hooks:** the installed skills and configured hooks, with the - client each belongs to. -- **Findings:** client configuration, skills, hooks, slash commands, subagents - and project instruction files are scanned for hidden characters, prompt - injection, dangerous commands and overly broad permissions, and each finding - is graded high, medium or low. The scan only reports; it never changes a - file. - -The app watches these files while it runs, and a new finding raises a system -notification. - - - - The MCP page: the MCP servers configured in Claude Code, Claude Desktop, Cursor, Codex, opencode, Antigravity CLI and Zed side by side, with remote third-party servers and a server configured differently in two clients marked; the header counts one high, one medium and one low finding in 11 scanned files - - -### Settings - -Settings has six sections. Connection lists the local core and the saved -remote cores, described [below](#connecting-to-a-remote-core). General sets -the language, the appearance, what the menu bar item shows on macOS, launch at -login, and whether notices arrive as system notifications, in the app only or -not at all. Listening sets who can reach the gateway (this machine only, the -local network of a chosen interface, or every interface), its port and the -allowed address ranges. Log retention sets how long request payloads and -request records are kept, and a size cap for payloads. About shows the -version, checks for updates and produces a diagnostics bundle with keys and -addresses masked. Uninstall restores every connected client and removes the -autostart entry, and is meant to be run before the app is deleted. - -### Menu bar, system tray and notifications - -On macOS the menu bar shows today's tokens above today's cost; the numbers turn -orange when a subscription quota is nearly used up and red when it is, and -Settings can reduce the item to the icon or to the numbers. - -

- - - The menu bar item: the ThinkWatch mark with a dot for a request in progress, and today's 13.3M tokens above today's cost of $9.34 - -

- -Clicking it opens a native menu with the gateway's address, its generation speed -over the last minute and its state, unread -notices, today's requests, tokens and cost, the quotas and reset times of each -subscription account and GLM Coding Plan upstream (with the credits left under -the bar on a plan billed in credits), and the requests in progress, followed by -actions: choosing the upstream of a manually selected group, copying the -gateway address or the default key, switching connections and checking for -updates, all without opening the main window. - - - - The menu it opens: the gateway's address, output speed and state; the ChatGPT account's 5-hour and weekly quotas with their reset times; today's requests, tokens and cost; the request in progress; and actions to copy the gateway address or the default key, undo the last configuration change, switch connections, open settings and check for updates - - -On Windows the icon sits in the notification area. Hovering over it shows the -gateway's state and today's tokens and cost; a left click opens the main -window, and a right click opens the same menu, with quota bars written out as -text. - -On Linux the icon sits in the system tray. Clicking it opens the same menu, -with Open ThinkWatch Lite as its first item and quota bars written out as text. - -System notifications, native on macOS and Windows and sent through the -desktop's notification service on Linux, report when the gateway stops -forwarding or keeps restarting, the connection to a remote core drops, a -subscription quota runs out, a sign-in expires or an upstream rejects its -credential, a proxy cannot be reached, the configuration file fails validation, -a tool call matches a rule that cuts the response off, or suspicious content -appears in a client's configuration. A new version found by the automatic check -is announced the same way (see [Updates](#updates)). An unreachable upstream, -which a fallback usually covers, is only listed in the app. Notices as a whole -can be set to system notifications, in-app only, or off. Marking a notice as -read stops the bell from counting it; the notice stays in the list until the -problem behind it clears or the list is cleared. - -## Connecting to a remote core - -The gateway can also run on a server, where ThinkWatch Core runs as a system -service and the app connects to it over the network. -[Server deployment](https://github.com/ThinkWatchProject/ThinkWatch-Core/blob/main/docs/server.md) -in the ThinkWatch Core repository covers installing `twcore` on Linux, -enabling its remote control port and reading its key. - -In the app, Settings › Connection › Add remote connection takes a name, the -server's address, the control port and the key that `twcore control-key` -prints on the server. Testing the connection completes the encrypted handshake -and reads the server's core version; the connection can then be saved and -switched to. The connection menu at the foot of the sidebar, and the -Connection submenu of the menu bar or tray menu, switch between the local core -and any saved server. - - - - Adding a remote connection in Settings: the server's address, its control port and the key from twcore control-key; the connection test has completed the handshake and reports core 0.48.0 and the server's gateway address - - -- The app connects to one core at a time. While it is connected to a server, - the local core stops once its requests in progress have finished; its - configuration, keys and request history are kept, and it starts again when - the app switches back. If the server cannot be reached, the app keeps - retrying and never falls back to the local core on its own; the local core - is always listed and can be switched back to in one step. -- Overview, Traffic, Keys, Upstreams, Routing and Security show and change the - server's configuration and data. Settings separates the app's own settings - from the server's configuration, and the remote control listener and its key - can only be changed on the server. The Clients and MCP pages always act on - the machine the app runs on: connecting a client points it at the server's - gateway. When switching from the local core to a server, the connected - clients that still point to the local gateway, those in WSL included, can be - pointed at the server in the same step; any left as they were can be pointed - at it later from the Clients page. -- The connection key is stored in a private file in the app's data directory, - readable only by the current user. -- A ChatGPT account is signed in with a device code, because a browser sign-in - returns to the machine that runs core. `${NAME}` in keys and header values - reads the environment of the core process on the server. The diagnostics - bundle is only offered for the local core. -- The server has to run the core version this release of the app expects. The - app checks this when it connects; if the versions differ, it names both and - gives the command that installs the expected version on the server, whether - it is newer or older than the installed one: - `sudo twcore upgrade --version --restart`. - - - - The connection menu at the foot of the sidebar while the app is connected to the remote core homelab: the local core on this Mac is stopped, and a second server is available to switch to - - -## Updates - -The app looks for a new version shortly after it starts and once a day after -that, reading a small manifest and nothing else. It can be turned off in -Settings. - -When the check finds one, the app sends a system notification, unless notices -are set to in-app only or off. The update window opens from that notification, -from the Install Version item that replaces Check for Updates in the menu bar or -tray menu, and from the update button in Settings › About. What happens next -depends on how the app was installed. +## Install -**Downloaded from the releases page on macOS:** one press on the install -button does the rest. The app downloads the update, verifies it against a key -compiled into itself, waits for the requests the gateway is serving to -finish — up to three minutes — then replaces itself and restarts. A Claude -Code task in the middle of a response is not cut off to make room for the -update. +| Platform | Install | +|---|---| +| macOS 12+, Apple silicon | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`, or [`ThinkWatch-Lite--arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | +| Windows 10 21H2+, x64 | [`ThinkWatch-Lite--x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | +| Windows 10 21H2+, ARM64 | [`ThinkWatch-Lite--arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | +| Linux, x86_64 or aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`, or [`ThinkWatch-Lite--.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -**On Windows:** the same single press. The app downloads the new installer, -verifies it against the key compiled into itself, waits for the requests in -flight to finish in the same way, then runs the installer, and the new version -starts once it is done. The app is installed for all users, so Windows asks for -administrator permission at every update; declining leaves the current version -running. +The gateway, [ThinkWatch Core](https://github.com/ThinkWatchProject/ThinkWatch-Core), +ships inside the app. The app is not signed by Apple or Microsoft, so the +first launch needs one extra step; [Install and update](https://thinkwat.ch/docs/lite/install) +covers it, along with how updates arrive. The interface is in English and +Simplified Chinese, and the app updates itself (a Homebrew installation +updates through Homebrew). -**On Linux:** the same single press, and no password is asked for. The app -downloads the new AppImage, verifies it against the key compiled into itself, -waits for the requests in flight to finish, then replaces its own file and -restarts. The AppImage has to be in a folder the user can write to. +## Documentation -**Installed with Homebrew:** the window gives the command to copy, and the app -never replaces itself. Homebrew records which version it put in -`/Applications`; an app that overwrote it would be written back over by the -next `brew upgrade`. For a Homebrew installation the check reads the version in -the tap's cask instead of the release manifest, so a new version is only -reported once the tap carries it, and the command always has something to -install: - -```bash -brew update && brew upgrade --cask thinkwatch-lite -``` - -`brew update` comes first because `brew upgrade` refreshes taps at most once a -day on its own. +- [Features](https://thinkwat.ch/docs/lite/features): every page, in detail +- [Install and update](https://thinkwat.ch/docs/lite/install) +- [Connecting to a remote core](https://thinkwat.ch/docs/lite/remote-core) and + [server deployment](https://thinkwat.ch/docs/core/server-deployment) +- [Import links](https://thinkwat.ch/docs/lite/import-links), for relays and vendors +- [Architecture](https://thinkwat.ch/docs/lite/architecture) ## Build from source @@ -580,29 +93,9 @@ bash src-tauri/scripts/fetch-core.sh pnpm tauri dev ``` -`fetch-core.sh` downloads the `twcore` release that `Cargo.lock` pins, checks -its sha256 and places it in `src-tauri/resources/`, which every build needs. -See [CONTRIBUTING.md](CONTRIBUTING.md) for the checks to run before opening a -pull request. - -## Layout - -``` -src/ React 19 + Tailwind 4 interface -src-tauri/ Tauri 2 shell: supervises the local core, connects to a - local or remote core, draws the menu bar item and the tray - menu, sends system notifications, installs updates -src-tauri/crates/ tw-adopt (pointing clients at the gateway, MCP configuration) - and tw-scan (scanning client configuration) -scripts/shots/ the product screenshot pipeline (see CONTRIBUTING.md) -``` - -The gateway itself lives in ThinkWatch Core; this repository holds no routing, -forwarding, or accounting logic. The app reaches core's control channel over a -unix socket on macOS and Linux and over a loopback port on Windows, or over a -TCP port when core runs on a server. Every connection begins with an encrypted -handshake (Noise `NNpsk0`) keyed with the control key from core's -configuration (`listen.control.key`); no TLS certificates are involved. +`fetch-core.sh` downloads the `twcore` release that `Cargo.lock` pins into +`src-tauri/resources/`. See [CONTRIBUTING.md](CONTRIBUTING.md) for the layout +of the repository and the checks to run before opening a pull request. ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 3a621eba..cff8bc40 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,257 +11,47 @@ **[English](README.md) | [中文](README.zh-CN.md)** -ThinkWatch Lite 是一款在本机运行 AI API 网关的桌面应用,支持 macOS、Windows 和 Linux。Claude Code、Codex 以及其他使用 OpenAI、Anthropic 接口的客户端经由它发出请求,应用记录每个请求的费用、所用的上游,以及发出前被脱敏的密钥。网关也可以部署在服务器上,此时应用通过加密的控制通道连接服务器上的 ThinkWatch Core。 +Claude Code、Codex 等 AI 客户端的本地网关,支持 macOS、Windows 与 Linux。客户端只需接入一次,此后更换上游或模型无需改动客户端配置。每个请求的费用与去向都有记录,发出前可替换其中的 API 密钥。 最近 7 天的概览:token、费用与请求数及其与前 7 天的对比,注明费用中的估算部分和无法计价的请求;按模型分层的趋势图,标出有失败的时段;以及按 token 与费用排列的模型 -应用在 macOS 上常驻菜单栏,在 Windows 和 Linux 上常驻系统托盘。支持 macOS 12 及以上版本(Apple 芯片)、Windows 10 21H2 及以上版本(x64 或 ARM64),以及 Ubuntu 22.04、Debian 12、Fedora 36 及以上版本的 Linux(x86_64 或 aarch64)。界面提供英文和简体中文,默认跟随系统语言,可在「设置」中切换。应用在三个平台上均可自行更新;通过 Homebrew 安装的由 Homebrew 更新。 +## 要点 -## 安装 - -| 平台 | 安装 | -|---|---| -| macOS,Apple 芯片 | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`,或 [`ThinkWatch-Lite-<版本>-arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -| Windows,x64 | [`ThinkWatch-Lite-<版本>-x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -| Windows,ARM64 | [`ThinkWatch-Lite-<版本>-arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -| Linux,x86_64 或 aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`,或 [`ThinkWatch-Lite-<版本>-<架构>.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | - -官网的 [Lite 页面](https://thinkwat.ch/zh-CN/lite#install)提供最新版本的一键下载,并自动选择 Windows 或 Linux 对应的架构。网关 [ThinkWatch Core](https://github.com/ThinkWatchProject/ThinkWatch-Core) 随应用一起安装,无需另行安装。 - -### macOS - -```bash -brew install --cask thinkwatchproject/tap/thinkwatch-lite -``` - -也可以从[最新版本的 release 页面](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest)下载 `ThinkWatch-Lite-<版本>-arm64.dmg`,与同页发布的 sha256 校验值核对后,将 ThinkWatch Lite 拖入「应用程序」。应用**未经 Apple 注册开发者签名**,macOS 会为下载的副本添加隔离属性并拒绝打开,需先移除该属性: - -```bash -xattr -dr com.apple.quarantine "/Applications/ThinkWatch Lite.app" -``` - -也可以在首次打开被拒绝后,前往「系统设置 › 隐私与安全性」点击「仍要打开」。[Homebrew cask](https://github.com/ThinkWatchProject/homebrew-tap) 在安装时会自动完成这一步,此外只是把应用从磁盘映像复制到「应用程序」。 - -### Windows - -从[最新版本的 release 页面](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest)下载与本机架构对应的安装程序:大多数电脑用 `ThinkWatch-Lite-<版本>-x64-setup.exe`,ARM 处理器的电脑用 `ThinkWatch-Lite-<版本>-arm64-setup.exe`。下载后与同页发布的 sha256 校验值核对: - -```powershell -Get-FileHash .\ThinkWatch-Lite-<版本>-x64-setup.exe -``` - -安装程序为所有用户安装,装入 Program Files,因此 Windows 会请求管理员权限。需要 Windows 10 21H2 及以上版本;缺少 WebView2 时安装程序会自动下载(Windows 11 已自带)。 - -安装程序**未经代码签名**,项目也不会购买证书。运行下载的安装程序时,SmartScreen 会显示全屏的蓝色警告「Windows 已保护你的电脑」,依次点击「更多信息」→「仍要运行」即可继续安装。 - -安装后应用常驻通知区域,数据保存在 `%APPDATA%\ThinkWatch`。 - -### Linux - -```bash -curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh | sh -``` - -脚本下载与本机架构对应的 AppImage,与同页发布的 sha256 校验值核对后安装为 `~/Applications/ThinkWatch-Lite.AppImage` 并启动。再次运行即用最新版本覆盖旧版本。 - -手动安装时,从[最新版本的 release 页面](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest)下载 `ThinkWatch-Lite-<版本>-x86_64.AppImage` 或 `ThinkWatch-Lite-<版本>-aarch64.AppImage`,用 `sha256sum -c` 核对后允许其执行(`chmod +x`,或在文件管理器的「属性」中勾选「允许作为程序执行文件」),然后打开。AppImage 应放在当前用户可写的目录中(如 `~/Applications`),以便自动更新替换。首次启动时会把 ThinkWatch Lite 添加到应用菜单,同时注册图标和 `thinkwatch://` 链接。Linux 版只发布 AppImage,不提供 deb、rpm、Flatpak 或 Snap 包。 - -AppImage 通过 FUSE 挂载自身,需要 fuse3 软件包中的 `fusermount3`(不需要 libfuse2)。多数桌面系统已自带;如缺少: - -| 发行版 | 命令 | -|---|---| -| Ubuntu、Debian | `sudo apt install fuse3` | -| Fedora | `sudo dnf install fuse3` | -| Arch Linux | `sudo pacman -S fuse3` | -| openSUSE | `sudo zypper install fuse3` | - -托盘图标依赖 AppIndicator。Ubuntu 已自带对应的 GNOME 扩展;Fedora 原生 GNOME 没有,需要另行安装 AppIndicator 扩展。没有托盘时,关闭窗口后网关继续运行,从应用菜单再次启动 ThinkWatch Lite 即可重新打开窗口。数据保存在 `~/.thinkwatch`。 - -- **NVIDIA 显卡在 Wayland 下窗口空白**:以 `WEBKIT_DISABLE_DMABUF_RENDERER=1` 启动应用。 -- **局域网内其他机器无法连接网关**:Fedora 默认启用的 firewalld 会拦截网关端口,需放行该端口;网关监听局域网时,设置页会给出相应提示。 -- **卸载**:先在「设置 › 完全卸载」中卸载,恢复应用接管过的客户端配置,并删除开机启动项和应用菜单项;再删除 AppImage 文件。 - -## 功能 - -主窗口共有九个页面:概览、流量、客户端、密钥、上游、路由、安全、MCP 和设置。 - -### 用量与费用 - -概览页按最近 24 小时、7 天、30 天或自定义区间统计 token、费用与请求数,并与上一个同等长度的区间对比;实时档显示最近十分钟。趋势图按模型分层显示 token 或费用,其下的模型排行可以直接打开对应的请求。页面下方依次是缓存(命中率、缓存带来的净节省、各模型的命中率)、延迟(首 token 时间的中位数与 P95,按模型和按上游)、生成速度(每秒 token 数的中位数,按模型和按上游)以及各项防护的检出情况。 - -费用会注明其中估算的部分,例如响应结束前被中断的请求。模型未定价的请求和上游未报告用量的请求单独计数,从不按零计入。价格来自价目表:默认价目表采用 LiteLLM 的公开价格,每天更新一次;自定义价目表在其基础上设置倍率,并可单独为个别模型定价,适用于价格与官方不同的中转服务。ChatGPT 这类订阅账号同样按价目表计价,本地模型等上游可设为不计费。每个请求的费用在请求结束时确定,并注明计价所用的价目表及其数据日期。 - -### 流量与会话 - -流量页实时列出请求:状态、密钥、模型、上游、首 token 时间与总耗时(悬停时显示生成速度)、token 和费用,并标出格式转换、被脱敏的密钥,以及被拦截或可疑的工具调用。列表可以按密钥、上游和模型筛选,或只看失败、无法计价的请求。「会话」视图把同一段对话的请求归为若干轮次,给出每一轮的输入 token 与费用。 - -打开一个请求可以查看时间线、路由(命中的规则、经过的策略组,以及每一次尝试的状态与耗时)、请求与响应正文、用量与费用。DeepSeek Harness 发出的请求还会显示所带会话日志的大小:这是客户端随每个请求附带的整段对话记录,发往 DeepSeek 以外的上游之前由网关去除。已结束的请求可以在预估费用后原样发送到另一个上游,两次的响应并排对照。 - - - - 流量页:每个请求的密钥、模型、上游、首字节时间与总耗时、token 和费用,标出格式转换、被脱敏的密钥和被拦截的请求,以及一个由网关本地应答的请求 - - -### 客户端接管 - -客户端页可以把 Claude Code、Claude Desktop、Codex、opencode、Zed、Aider 与 DeepSeek Harness 指向网关。写入之前,页面列出将要修改的字段和这次接管的其他影响(例如 ChatGPT 桌面版与 Codex 读取同一份配置文件),给出完整的改动差异,并完整备份原文件。只修改指向网关所需的配置,每个客户端使用各自的密钥。Claude Desktop 通过官方的第三方推理模式接入,页面逐一列出要修改的各个文件;由组织统一管理的 Claude Desktop 不做修改。已接管的客户端可以随时单独还原或全部还原;Codex 还原后保留一项直连 OpenAI 的配置,接管期间的会话仍可打开。opencode(v1 与 v2)的配置中同时写入其密钥在网关上可用的模型列表;网关上可用的模型变化后,页面提示更新,更新同样先给出改动差异。Cursor、Continue 与 Antigravity CLI 提供逐步的配置方法,并为其创建密钥。页面列出每个客户端处于使用中、等待首个请求还是未生效,以及最近 24 小时的请求。 - -在 Windows 上,安装在 WSL 中的 Claude Code 与 Codex 按发行版单独成组,列在这台电脑的客户端之后。它们同样可以指向 Windows 上的网关、还原和检查,使用与 Windows 上那一份分开的密钥,配置文件经由 `\\wsl.localhost` 修改。写入的地址与 Windows 上的客户端相同,是 `127.0.0.1`,WSL 在以下两种情况下可以访问: - -- **WSL 1**:与 Windows 共用网络。 -- **使用 mirrored 网络模式的 WSL 2**:在 `%USERPROFILE%\.wslconfig` 的 `[wsl2]` 段(或旧的 `[experimental]` 段)中设置 `networkingMode=mirrored`,需要 Windows 11 22H2 及以上版本、WSL 2.0.5 及以上版本。 - -WSL 2 默认使用 NAT 网络,此时 Windows 上的网关无法从 WSL 内访问,网关也不会为此另外监听 WSL 的虚拟网卡。WSL 分组因此不提供接管,改为说明原因,并提供「改为 mirrored 模式」:在 `.wslconfig` 中新增或修改 `networkingMode`,文件的其他内容保持不变,与接管客户端一样先给出完整差异,确认后全文备份再写入。修改在 WSL 重启后生效,页面同时提供「重启 WSL」(执行 `wsl --shutdown`,会停止所有正在运行的发行版)。完全卸载时 `.wslconfig` 不会改回,备份保留。Windows 10 与 Windows 11 21H2 没有 mirrored 网络模式,WSL 版本低于 2.0.5 时也无法使用,页面会分别说明。连接远程 core 时,WSL 中的客户端与 Windows 上的一样指向服务器,不受网络模式限制。 - - - - 客户端页:已接管的 Claude Code 与 Codex 各用一把专用密钥,附最近 24 小时的请求;未接管的 opencode;按配置方法手动设置并已在使用的 Cursor;尚未设置的 Continue 与 Antigravity CLI;以及未检测到的 Zed 与 Aider - - -### 密钥 - -客户端连接网关必须携带密钥,本机也不例外。密钥页列出默认密钥(供未单独分配密钥的客户端使用)和每个已接管客户端的专用密钥,并注明所属客户端,便于在流量页中区分各客户端的请求。每把密钥有各自的路由、可用模型(全部、无,或指定的模型与 `gpt-5*` 这类通配模式)、可选的并发上限,以及最近 24 小时的请求数与费用。密钥可以停用,停用后使用它的请求一律被拒绝;也可以更换,新密钥会写入使用它的客户端的配置。 - - - - 密钥页:默认密钥,以及分别供 Claude Code、Codex 与 Cursor 使用的密钥,列出各自的路由、可用模型和最近 24 小时的请求数与费用 - - -### 上游 - -上游是网关转发请求的目标:Anthropic、OpenAI、Google Gemini、DeepSeek 或任何兼容接口的 API 密钥,在应用内登录的 ChatGPT 账号或 Z.ai / BigModel 账号,OpenRouter 等中转服务,以及 Ollama 等本机模型。ChatGPT 账号显示订阅额度与重置时间;GLM Coding Plan 的上游(地址在 `api.z.ai` 或 `open.bigmodel.cn` 上,在应用内登录或手动填写密钥均可)同样显示:5 小时与每周额度,积分制套餐另外显示剩余积分(「剩余 1,976 / 2,000 积分」)。客户端与上游的 API 格式不同时,请求在 Anthropic Messages、OpenAI Chat Completions、OpenAI Responses 与 Gemini 之间自动转换,无法转换的字段会在请求上逐一列出。上游可以经出站代理访问,也可以使用单独的价目表计价,代理与价目表在同一页的标签中管理。链路测速测量 DNS 解析以及 TCP、TLS、代理握手的耗时,不产生费用;推理测速测量首个 token 的时间,运行前先给出费用预估。 - -API 密钥和请求头的值可以写成 `${变量名}`,读取系统环境变量。macOS 上读的是登录 shell 里的环境变量,`~/.zshrc` 等文件中 `export` 的变量都会生效,Linux 同理(`~/.bashrc`、`~/.profile` 等);Windows 上读的是系统设置里配置的环境变量。修改变量后,重新打开应用即可生效。代理相关的变量(`HTTPS_PROXY` 等)和 `PATH` 不会被读取。 - - - - 上游页:Anthropic、DeepSeek 与 Gemini 的 API 密钥上游,使用单独价目表的中转服务,5 小时额度已用 58% 的 ChatGPT Plus 账号,经代理访问的 OpenRouter,以及设为不计费的本机 Ollama,并列出各自 24 小时的请求数、费用与首字节时间中位数 - - -中转站或服务商可以提供导入链接(`thinkwatch://import?…`,或网页形式 `https://thinkwat.ch/import#…`),预填新上游的名称、接口地址、接口协议、API 密钥与模型清单。应用在确认对话框中列出这些设置,并写明请求与密钥将发往的主机;选择「创建」之前不写入配置,也不连接该地址。一条链接只能新增一个上游,不能修改已有的上游、请求头、代理、价目表或路由,引用环境变量的密钥一律拒绝。参数说明与链接生成器见[导入链接](https://thinkwat.ch/zh-CN/docs/lite/import-links)。 - -### 路由与故障转移 - -每把密钥使用一条路由,未指定的使用默认路由。路由由按顺序匹配的规则组成。规则的条件包括模型、密钥、客户端的 API 格式、输入 token 数、`max_tokens`、工具数量、图片、扩展思考、流式、提示缓存以及辅助请求的类型;命中后把请求交给某个上游或策略组,或拒绝请求,也可以改写模型、`max_tokens` 或扩展思考。策略组把多个上游放在同一个名字下,并决定尝试的先后:按顺序、手动选择、轮询、延迟最低优先或费用最低优先。一次尝试失败时,请求转到下一个上游;同一会话默认保持在同一个上游上,以便提示缓存持续命中。页面顶部的路由图显示每把密钥经过的路由、策略组和上游。 - - - - 路由页:从密钥经路由、策略组到上游的路由图,以及三条路由各自按顺序匹配的规则 - - -客户端自行发出的辅助请求(连通性检查、预热、生成标题、话题识别、输入建议)可以由网关在本地应答而不产生费用,也可以直接转发,或交给路由规则处理。 - -每个请求都记录命中的规则、经过的策略组,以及每一次尝试的状态与耗时。试算按给定的请求条件逐条匹配规则,说明请求会交给哪个上游及其原因,不发出请求,也不产生费用。 - - - - 路由试算:cursor 密钥以 OpenAI Chat Completions 格式请求 claude-sonnet-5,未命中「Gemini 模型」规则并说明原因,命中「兜底」规则,交给费用最低的策略组「低价」,依次尝试 relay 与 anthropic,并把请求转换为 Anthropic Messages 格式 - - -### 安全 - -安全页有五项防护,对所有上游和所有密钥统一生效,各有「关闭」「观察」「拦截」三档,其中「观察」只检测和记录,不做任何改动。输出长度出厂为「关闭」,其余四项出厂为「观察」,因此默认不会改动或拦截任何请求。 - -- **出站脱敏**:请求发出之前查找其中的凭据,包括 Anthropic、OpenAI、GitHub、Slack、AWS、Google、GitLab、Stripe、npm、DigitalOcean、SendGrid 的 API 密钥与令牌,以及私钥、JWT 和连接串中的口令。「拦截」档下把它们替换为占位符,响应中回显时再还原。内网 IP 地址和内网域名两条规则出厂为停用,可以按需启用。 -- **工具调用审查**:检查模型返回的工具调用中是否含有下载或解码后执行代码、外发环境变量或凭据文件、读取私钥或云服务凭据、写入启动项或定时任务等命令。「拦截」档下命中即切断响应,客户端收不到一个完整、可执行的调用。删除主目录或根目录、设置全员可写权限两条规则出厂只记录。 -- **隐藏字符**:检查客户端发送的内容(含工具结果)中的 Unicode 标签字符和双向控制符,「拦截」档下拒绝发出请求。 -- **内容过滤**:用关键词或正则表达式匹配客户端发送的消息(含工具结果),「拦截」档下拒绝命中拒绝类规则的请求。内置规则中,出厂只启用三条明确要求「忽略先前指令」的规则;越狱、身份操纵、套取提示词等规则及其中文版本可以按需启用。 -- **输出长度**:回答超过设定的字符数(默认 100,000)时,流式回答在超出处切断,非流式回答整份替换为错误。思考内容和工具调用的参数不计入。 - -安全页列出全部规则:内置规则可以逐条启用或停用,工具调用审查和内容过滤的内置规则还可以设定在「拦截」档下执行处置还是仅记录;自定义规则为正则表达式,内容过滤也可以使用关键词。任何规则都可以先用一段文本测试。各项防护检出的内容都记入第一个标签页的日志,并注明所属的请求。 +- **一次接入,随时切换。** Claude Code、Claude Desktop、Codex、opencode、Zed、Aider 与 DeepSeek Harness 可一键指向网关,写入前预览改动、备份原文件,随时可以还原;Cursor、Continue 与 Antigravity CLI 提供配置说明。此后切换上游只在网关中完成。 +- **发出前替换密钥,拦下危险命令。** 出站脱敏在请求发出前把 API 密钥、私钥、JWT 与连接串口令换成占位符,中转服务看不到原值。工具调用审查切断下载即执行等危险命令,隐藏字符与提示注入也可以直接拒绝。各项防护出厂只记录,逐项切换到拦截即可生效。 +- **扫描 MCP、技能与钩子。** 八款客户端的 MCP 服务器并列显示并标出第三方服务器;客户端配置、技能、钩子与项目指令中的隐藏字符、提示注入、危险命令与过宽权限会被找出。 +- **每个请求都可追溯。** 命中的规则、尝试过的每个上游、API 格式转换与费用的计算依据都在请求详情中;已结束的请求可以重放到另一个上游,并排对比。 +- **按规则分流,失败自动换。** 按模型、工具、图片、扩展思考等条件分流。回答开始前上游出错时换用下一个,同一会话固定使用同一上游,提示缓存保持有效。标题生成等辅助请求可在本地应答。 +- **多种上游,接口互转。** API 密钥、Amazon Bedrock、ChatGPT 与 Z.ai 账号、OpenRouter 等中转服务与本机模型均可作为上游,Anthropic、OpenAI、Gemini 接口之间自动转换。 +- **费用如实计算。** 估算的金额单独标注,无法计价的请求单独计数,不按零计入。 +- **连接远程 core。** 网关也可以部署在 Linux 服务器上,应用经加密的控制通道连接。 - 安全日志:发出前被替换的凭据,其中一条由自定义规则命中;被切断的下载并执行类工具调用;以及仅记录的隐藏字符、删除命令和注入指令。每一条都注明所属请求的密钥、客户端、模型与上游 + 安全日志:请求发出前替换的凭据(其中一条由自定义规则命中)、被切断的下载即执行工具调用,以及记录在案的隐藏字符、删除命令与注入指令,每条都注明所属请求的密钥、客户端、模型与上游 -### MCP - -MCP 页管理客户端从自己的配置文件中加载的内容,这些内容不经过网关。 - -- **服务器**:并排列出 Claude Code、Claude Desktop、Cursor、Codex、opencode、Antigravity CLI、Zed 与 DeepSeek Harness 配置的 MCP 服务器。可以把一个服务器从一个客户端复制到另一个客户端,或从某个客户端移除;写入前先显示改动,并备份原文件。复制与移除支持 Claude Code、Claude Desktop、Cursor 与 Codex,opencode、Antigravity CLI、Zed 与 DeepSeek Harness 只列出、不写入。位于其他主机的远程服务器标为「第三方」,使用它会把相关上下文发送到该地址;同名服务器在各客户端中配置不同时标为「配置不一致」,可以并排比较。 -- **技能与钩子**:列出已安装的技能和配置的钩子,以及各自所属的客户端。 -- **发现**:扫描客户端配置、技能、钩子、斜杠命令、subagent 与项目指令文件,检查隐藏字符、提示注入、危险命令与过宽权限四类问题,每项发现按高、中、低分级。扫描只报告,不修改任何文件。 - -应用运行期间会监视这些文件,出现新的发现时发送系统通知。 - - - - MCP 页:Claude Code、Claude Desktop、Cursor、Codex、opencode、Antigravity CLI 与 Zed 配置的 MCP 服务器并排列出,标出第三方远程服务器和在两个客户端中配置不一致的服务器;页头统计已扫描的 11 个文件中高、中、低风险发现各一项 - - -### 设置 - -设置页分为六节。「连接」列出本机 core 和已保存的远程 core,详见[下文](#连接远程-core)。「通用」设置语言、外观、菜单栏显示的内容(仅 macOS)、开机启动,以及提醒以系统通知发送、仅在应用内显示还是关闭。「网关监听」设置网关的访问范围(仅本机、所选网卡所在的局域网或所有网卡)、端口和放行网段。「日志保留」分别设置请求报文与请求记录的保留天数,以及报文的空间上限。「关于」显示版本、检查更新,并可生成诊断包,其中的密钥与地址均已脱敏。「卸载」还原所有已接管的客户端并取消开机启动,应在删除应用之前执行。 - -### 菜单栏、系统托盘与通知 - -macOS 菜单栏显示今日 token 与今日费用,订阅额度紧张时数字变橙、用完变红;设置里可以改为仅标识或仅数值。 - -

- - - 菜单栏图标:ThinkWatch 标识,其上的圆点表示有请求在进行,右侧上下两行为今日 token 13.3M 与今日费用 $9.34 - -

- -点开是原生菜单:网关地址、最近一分钟的生成速度与状态、未读的提醒、今日的请求数、token 与费用、各订阅账号与 GLM Coding Plan 上游的额度与重置时间(积分制套餐在额度条下方显示剩余积分)、进行中的请求,以及切换手动选择策略组中的上游、复制网关地址和默认密钥、切换连接、检查更新等常用操作,不必先打开主界面。 - - - - 点开的菜单:网关地址、输出速率与状态;ChatGPT 账号的 5 小时与每周额度及重置时间;今日的请求数、token 与费用;进行中的请求;以及复制网关地址和默认密钥、撤销上一次配置修改、切换连接、打开设置、检查更新等操作 - - -Windows 上图标位于通知区域:悬停显示网关状态与今日 token、费用;左键打开主界面,右键打开同一份菜单,其中的额度条改为文字。 - -Linux 上图标位于系统托盘:点击打开同一份菜单,第一项为「打开主界面」,额度条同样改为文字。 - -以下情况会发送系统通知(macOS 与 Windows 使用原生通知,Linux 通过桌面环境的通知服务):网关停止转发或反复重启、与远程 core 的连接断开、订阅额度用完、账号登录失效或上游拒绝当前凭据、代理不通、配置文件未通过校验、工具调用命中切断类规则、客户端配置中出现可疑内容。自动检查到新版本时也以系统通知告知(见[更新](#更新))。上游无法连接时通常由回退上游承接,因此只记录在应用内。提醒可以整体设为系统通知、仅在应用内显示或关闭。标为已读的提醒不再计入铃铛上的数字,但在问题解决或清空列表之前仍留在列表中。 - -## 连接远程 core - -网关也可以运行在服务器上:ThinkWatch Core 作为系统服务在服务器上运行,应用通过网络连接它。在 Linux 上安装 `twcore`、开启远程控制端口和获取密钥的步骤见 ThinkWatch Core 仓库的[服务器部署文档](https://github.com/ThinkWatchProject/ThinkWatch-Core/blob/main/docs/server.zh-CN.md)。 - -在应用中打开「设置 › 连接 › 添加远程连接」,填写名称、服务器地址、控制端口,以及在服务器上执行 `twcore control-key` 得到的密钥。测试连接会完成加密握手并读取服务器的 core 版本,通过后即可保存并切换。侧栏底部的连接菜单,以及菜单栏或托盘菜单中的「连接」子菜单,可以在本机 core 和已保存的服务器之间切换。 - - - - 在设置中添加远程连接:填写服务器地址、控制端口和 twcore control-key 给出的密钥;测试连接已完成握手,显示 core 0.48.0 和服务器的网关地址 - - -- 应用同一时间只连接一个 core。连接服务器期间,本机 core 在进行中的请求结束后停止运行,其配置、密钥和请求历史原样保留,切回本机时重新启动。服务器无法连接时,应用持续重试,不会自行退回本机;「本机」始终在连接列表中,任何时候都可以一步切回。 -- 概览、流量、密钥、上游、路由与安全页显示和修改的是服务器上的配置与数据。设置页把本机应用的设置和服务器的配置分为两组,远程控制的监听与密钥只能在服务器上修改。客户端页和 MCP 页始终作用于运行应用的这台电脑:接管客户端时,客户端指向服务器上的网关。从本机切换到服务器时,可以同时把仍指向本机网关的已接管客户端改为指向服务器,WSL 中的客户端一并修改;切换时未修改的,之后可以在客户端页修改。 -- 连接密钥保存在应用数据目录中的一个私有文件里,只有当前用户可以读取。 -- ChatGPT 账号只能用设备码登录,因为浏览器登录完成后会回到运行 core 的那台机器。API 密钥和请求头中的 `${变量名}` 读取的是服务器上 core 进程的环境变量。诊断包只对本机 core 提供。 -- 服务器上的 core 版本必须与这一版应用所需的版本一致;连接时应用会检查,版本不一致时列出两边的版本,并给出在服务器上安装所需版本的命令:`sudo twcore upgrade --version <版本> --restart`,服务器上现有的版本较新或较旧均适用。 - - - - 连接远程 core「homelab」时侧栏底部的连接菜单:这台 Mac 上的本机 core 已停止,另有一台服务器可以切换 - - -## 更新 - -应用启动后不久检查一次新版本,此后每天检查一次,只读取一份很小的版本清单。可以在「设置」中关闭。 - -检查到新版本时,应用发送一条系统通知;提醒设为仅在应用内显示或关闭时不发送。点击这条通知、菜单栏或托盘菜单中取代「检查更新」的「安装新版本」,或「设置 › 关于」中的更新按钮,都会打开更新窗口。之后的处理方式取决于安装方式。 +## 安装 -**在 macOS 上从 release 页面下载安装的**:点击一次安装按钮,其余步骤自动完成——下载更新包,用编译进应用的公钥验签,等待网关正在处理的请求结束(最多三分钟),然后替换并重新启动。正在输出的 Claude Code 任务不会因更新而中断。 +| 平台 | 安装 | +|---|---| +| macOS 12+,Apple 芯片 | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`,或 [`ThinkWatch-Lite-<版本>-arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | +| Windows 10 21H2+,x64 | [`ThinkWatch-Lite-<版本>-x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | +| Windows 10 21H2+,ARM64 | [`ThinkWatch-Lite-<版本>-arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | +| Linux,x86_64 或 aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`,或 [`ThinkWatch-Lite-<版本>-<架构>.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) | -**Windows 上**:同样点击一次即可。应用下载新版本的安装程序,用编译进应用的公钥验签,同样等待进行中的请求结束,然后运行安装程序,安装完成后新版本自动启动。应用为所有用户安装,因此每次更新 Windows 都会请求管理员权限;拒绝则继续运行当前版本。 +网关 [ThinkWatch Core](https://github.com/ThinkWatchProject/ThinkWatch-Core) 随应用一同安装。应用未经 Apple 与 Microsoft 签名,首次打开需要多一步操作,见[安装与更新](https://thinkwat.ch/zh-CN/docs/lite/install),其中也说明了各种安装方式如何更新。界面提供英文与简体中文,应用自动更新(通过 Homebrew 安装的随 Homebrew 更新)。 -**Linux 上**:同样点击一次即可,不需要输入密码。应用下载新版本的 AppImage,用编译进应用的公钥验签,等待进行中的请求结束,然后替换自身文件并重新启动。AppImage 须位于当前用户可写的目录中。 +## 文档 -**用 Homebrew 安装的**:窗口给出更新命令和复制按钮,应用不会替换自身。Homebrew 记录着它放入 `/Applications` 的版本,应用自行替换后,下一次 `brew upgrade` 会把旧版本写回。这种安装方式下,检查读取的是 tap 中 cask 的版本,而不是发布页的版本清单,因此 tap 包含新版本之后才会提示更新,给出的命令一定有可安装的内容: - -```bash -brew update && brew upgrade --cask thinkwatch-lite -``` - -命令先执行 `brew update`,是因为 `brew upgrade` 自身最多每天刷新一次 tap。 +- [功能详解](https://thinkwat.ch/zh-CN/docs/lite/features):逐页说明 +- [安装与更新](https://thinkwat.ch/zh-CN/docs/lite/install) +- [连接远程 core](https://thinkwat.ch/zh-CN/docs/lite/remote-core)与[服务器部署](https://thinkwat.ch/zh-CN/docs/core/server-deployment) +- [导入链接](https://thinkwat.ch/zh-CN/docs/lite/import-links)(面向中转站与服务商) +- [架构](https://thinkwat.ch/zh-CN/docs/lite/architecture) ## 从源码运行 @@ -271,20 +61,7 @@ bash src-tauri/scripts/fetch-core.sh pnpm tauri dev ``` -`fetch-core.sh` 下载 `Cargo.lock` 所锁定版本的 `twcore`,核对 sha256 后放入 `src-tauri/resources/`,每次构建都需要这个文件。提交代码前的检查见 [CONTRIBUTING.md](CONTRIBUTING.md)。 - -## 目录 - -``` -src/ React 19 + Tailwind 4 界面 -src-tauri/ Tauri 2 外壳:托管本机 core,连接本机或远程 core, - 绘制菜单栏图标与托盘菜单,发送系统通知,安装更新 -src-tauri/crates/ tw-adopt(接管客户端、读写 MCP 配置)与 - tw-scan(扫描客户端配置) -scripts/shots/ 产品截图流水线(见 CONTRIBUTING.md) -``` - -网关本体位于 ThinkWatch Core;本仓库不包含路由、转发或计费逻辑。应用通过 core 的控制通道与它通信:本机在 macOS 和 Linux 上走 unix socket,在 Windows 上走回环端口;core 运行在服务器上时走 TCP 端口。每条连接都先完成加密握手(Noise `NNpsk0`),密钥为 core 配置中的控制密钥 `listen.control.key`,不涉及 TLS 证书。 +`fetch-core.sh` 下载 `Cargo.lock` 钉住的 `twcore` 版本并放入 `src-tauri/resources/`。仓库结构与提交 PR 前要跑的检查见 [CONTRIBUTING.md](CONTRIBUTING.md)。 ## 许可证 diff --git a/docs/screenshots/en/clients-dark.png b/docs/screenshots/en/clients-dark.png index 7de547c2..5cb8e2d5 100644 Binary files a/docs/screenshots/en/clients-dark.png and b/docs/screenshots/en/clients-dark.png differ diff --git a/docs/screenshots/en/clients-light.png b/docs/screenshots/en/clients-light.png index aa82b540..0c166812 100644 Binary files a/docs/screenshots/en/clients-light.png and b/docs/screenshots/en/clients-light.png differ diff --git a/docs/screenshots/en/dry-run-dark.png b/docs/screenshots/en/dry-run-dark.png index e62aa8db..0d0cd68f 100644 Binary files a/docs/screenshots/en/dry-run-dark.png and b/docs/screenshots/en/dry-run-dark.png differ diff --git a/docs/screenshots/en/dry-run-light.png b/docs/screenshots/en/dry-run-light.png index 8eb5bbe6..0a3b96e4 100644 Binary files a/docs/screenshots/en/dry-run-light.png and b/docs/screenshots/en/dry-run-light.png differ diff --git a/docs/screenshots/en/keys-dark.png b/docs/screenshots/en/keys-dark.png index e4d378b9..8b6cf98a 100644 Binary files a/docs/screenshots/en/keys-dark.png and b/docs/screenshots/en/keys-dark.png differ diff --git a/docs/screenshots/en/keys-light.png b/docs/screenshots/en/keys-light.png index b52e2e23..201d8113 100644 Binary files a/docs/screenshots/en/keys-light.png and b/docs/screenshots/en/keys-light.png differ diff --git a/docs/screenshots/en/menubar-menu-dark.png b/docs/screenshots/en/menubar-menu-dark.png index 68bcb960..be60dd4f 100644 Binary files a/docs/screenshots/en/menubar-menu-dark.png and b/docs/screenshots/en/menubar-menu-dark.png differ diff --git a/docs/screenshots/en/menubar-menu-light.png b/docs/screenshots/en/menubar-menu-light.png index a97b9bad..7c626c52 100644 Binary files a/docs/screenshots/en/menubar-menu-light.png and b/docs/screenshots/en/menubar-menu-light.png differ diff --git a/docs/screenshots/en/overview-dark.png b/docs/screenshots/en/overview-dark.png index 2315f891..434115a9 100644 Binary files a/docs/screenshots/en/overview-dark.png and b/docs/screenshots/en/overview-dark.png differ diff --git a/docs/screenshots/en/overview-light.png b/docs/screenshots/en/overview-light.png index 90110bd6..61c21ba1 100644 Binary files a/docs/screenshots/en/overview-light.png and b/docs/screenshots/en/overview-light.png differ diff --git a/docs/screenshots/en/remote-add-dark.png b/docs/screenshots/en/remote-add-dark.png index 36e66504..a4e72229 100644 Binary files a/docs/screenshots/en/remote-add-dark.png and b/docs/screenshots/en/remote-add-dark.png differ diff --git a/docs/screenshots/en/remote-add-light.png b/docs/screenshots/en/remote-add-light.png index 0b7535f3..b68871ff 100644 Binary files a/docs/screenshots/en/remote-add-light.png and b/docs/screenshots/en/remote-add-light.png differ diff --git a/docs/screenshots/en/remote-clients-dark.png b/docs/screenshots/en/remote-clients-dark.png index 00d3bd10..df32373c 100644 Binary files a/docs/screenshots/en/remote-clients-dark.png and b/docs/screenshots/en/remote-clients-dark.png differ diff --git a/docs/screenshots/en/remote-clients-light.png b/docs/screenshots/en/remote-clients-light.png index 3ed9db96..8f82190b 100644 Binary files a/docs/screenshots/en/remote-clients-light.png and b/docs/screenshots/en/remote-clients-light.png differ diff --git a/docs/screenshots/en/remote-switcher-dark.png b/docs/screenshots/en/remote-switcher-dark.png index af25c06a..02ef66b8 100644 Binary files a/docs/screenshots/en/remote-switcher-dark.png and b/docs/screenshots/en/remote-switcher-dark.png differ diff --git a/docs/screenshots/en/remote-switcher-light.png b/docs/screenshots/en/remote-switcher-light.png index ddb6760d..b36305dd 100644 Binary files a/docs/screenshots/en/remote-switcher-light.png and b/docs/screenshots/en/remote-switcher-light.png differ diff --git a/docs/screenshots/en/routing-dark.png b/docs/screenshots/en/routing-dark.png index a4031fe3..6284cd16 100644 Binary files a/docs/screenshots/en/routing-dark.png and b/docs/screenshots/en/routing-dark.png differ diff --git a/docs/screenshots/en/routing-light.png b/docs/screenshots/en/routing-light.png index cb14d6f9..662420de 100644 Binary files a/docs/screenshots/en/routing-light.png and b/docs/screenshots/en/routing-light.png differ diff --git a/docs/screenshots/en/security-dark.png b/docs/screenshots/en/security-dark.png index ee07edae..632a7cb9 100644 Binary files a/docs/screenshots/en/security-dark.png and b/docs/screenshots/en/security-dark.png differ diff --git a/docs/screenshots/en/security-light.png b/docs/screenshots/en/security-light.png index 579e58af..e37b29fc 100644 Binary files a/docs/screenshots/en/security-light.png and b/docs/screenshots/en/security-light.png differ diff --git a/docs/screenshots/en/settings-dark.png b/docs/screenshots/en/settings-dark.png index 81a04ded..3a7ec81c 100644 Binary files a/docs/screenshots/en/settings-dark.png and b/docs/screenshots/en/settings-dark.png differ diff --git a/docs/screenshots/en/settings-light.png b/docs/screenshots/en/settings-light.png index 00cc0563..21fd5754 100644 Binary files a/docs/screenshots/en/settings-light.png and b/docs/screenshots/en/settings-light.png differ diff --git a/docs/screenshots/en/traffic-dark.png b/docs/screenshots/en/traffic-dark.png index a5e0dc43..32ccdc3c 100644 Binary files a/docs/screenshots/en/traffic-dark.png and b/docs/screenshots/en/traffic-dark.png differ diff --git a/docs/screenshots/en/traffic-light.png b/docs/screenshots/en/traffic-light.png index 1d56a676..d797e42d 100644 Binary files a/docs/screenshots/en/traffic-light.png and b/docs/screenshots/en/traffic-light.png differ diff --git a/docs/screenshots/en/upstreams-dark.png b/docs/screenshots/en/upstreams-dark.png index 4331d16f..7257b344 100644 Binary files a/docs/screenshots/en/upstreams-dark.png and b/docs/screenshots/en/upstreams-dark.png differ diff --git a/docs/screenshots/en/upstreams-light.png b/docs/screenshots/en/upstreams-light.png index b7d74281..7bfbdc06 100644 Binary files a/docs/screenshots/en/upstreams-light.png and b/docs/screenshots/en/upstreams-light.png differ diff --git a/docs/screenshots/menubar-dark.png b/docs/screenshots/menubar-dark.png index 356beef9..6a94e353 100644 Binary files a/docs/screenshots/menubar-dark.png and b/docs/screenshots/menubar-dark.png differ diff --git a/docs/screenshots/menubar-light.png b/docs/screenshots/menubar-light.png index f4177663..29091a90 100644 Binary files a/docs/screenshots/menubar-light.png and b/docs/screenshots/menubar-light.png differ diff --git a/docs/screenshots/web/clients-en.webp b/docs/screenshots/web/clients-en.webp index c81ec90f..82f5fa70 100644 Binary files a/docs/screenshots/web/clients-en.webp and b/docs/screenshots/web/clients-en.webp differ diff --git a/docs/screenshots/web/clients.webp b/docs/screenshots/web/clients.webp index e3b3b247..1764886b 100644 Binary files a/docs/screenshots/web/clients.webp and b/docs/screenshots/web/clients.webp differ diff --git a/docs/screenshots/web/dry-run-en.webp b/docs/screenshots/web/dry-run-en.webp index be2efb9d..99de9c03 100644 Binary files a/docs/screenshots/web/dry-run-en.webp and b/docs/screenshots/web/dry-run-en.webp differ diff --git a/docs/screenshots/web/dry-run.webp b/docs/screenshots/web/dry-run.webp index 755b4cde..e6fa70c8 100644 Binary files a/docs/screenshots/web/dry-run.webp and b/docs/screenshots/web/dry-run.webp differ diff --git a/docs/screenshots/web/keys-en.webp b/docs/screenshots/web/keys-en.webp index 4cab5c53..520fa6c0 100644 Binary files a/docs/screenshots/web/keys-en.webp and b/docs/screenshots/web/keys-en.webp differ diff --git a/docs/screenshots/web/keys.webp b/docs/screenshots/web/keys.webp index 03d318a8..fa83ea60 100644 Binary files a/docs/screenshots/web/keys.webp and b/docs/screenshots/web/keys.webp differ diff --git a/docs/screenshots/web/menubar-menu-en.webp b/docs/screenshots/web/menubar-menu-en.webp index 77a41801..876d12ca 100644 Binary files a/docs/screenshots/web/menubar-menu-en.webp and b/docs/screenshots/web/menubar-menu-en.webp differ diff --git a/docs/screenshots/web/menubar-menu.webp b/docs/screenshots/web/menubar-menu.webp index d710d9be..323e5c59 100644 Binary files a/docs/screenshots/web/menubar-menu.webp and b/docs/screenshots/web/menubar-menu.webp differ diff --git a/docs/screenshots/web/menubar.png b/docs/screenshots/web/menubar.png index bc2c71a2..093f4eba 100644 Binary files a/docs/screenshots/web/menubar.png and b/docs/screenshots/web/menubar.png differ diff --git a/docs/screenshots/web/overview-en.webp b/docs/screenshots/web/overview-en.webp index 0f6467b7..c2c7251c 100644 Binary files a/docs/screenshots/web/overview-en.webp and b/docs/screenshots/web/overview-en.webp differ diff --git a/docs/screenshots/web/overview.webp b/docs/screenshots/web/overview.webp index d82184ab..711398ed 100644 Binary files a/docs/screenshots/web/overview.webp and b/docs/screenshots/web/overview.webp differ diff --git a/docs/screenshots/web/remote-add-en.webp b/docs/screenshots/web/remote-add-en.webp index c9633797..3cb29eb8 100644 Binary files a/docs/screenshots/web/remote-add-en.webp and b/docs/screenshots/web/remote-add-en.webp differ diff --git a/docs/screenshots/web/remote-add.webp b/docs/screenshots/web/remote-add.webp index 6e4b3774..d037affc 100644 Binary files a/docs/screenshots/web/remote-add.webp and b/docs/screenshots/web/remote-add.webp differ diff --git a/docs/screenshots/web/remote-clients-en.webp b/docs/screenshots/web/remote-clients-en.webp index 9f085167..776cf655 100644 Binary files a/docs/screenshots/web/remote-clients-en.webp and b/docs/screenshots/web/remote-clients-en.webp differ diff --git a/docs/screenshots/web/remote-clients.webp b/docs/screenshots/web/remote-clients.webp index 878107bf..592f0b8a 100644 Binary files a/docs/screenshots/web/remote-clients.webp and b/docs/screenshots/web/remote-clients.webp differ diff --git a/docs/screenshots/web/remote-switcher-en.webp b/docs/screenshots/web/remote-switcher-en.webp index 83a3001a..5d1a9c49 100644 Binary files a/docs/screenshots/web/remote-switcher-en.webp and b/docs/screenshots/web/remote-switcher-en.webp differ diff --git a/docs/screenshots/web/remote-switcher.webp b/docs/screenshots/web/remote-switcher.webp index 276973b7..11ce0fa0 100644 Binary files a/docs/screenshots/web/remote-switcher.webp and b/docs/screenshots/web/remote-switcher.webp differ diff --git a/docs/screenshots/web/routing-en.webp b/docs/screenshots/web/routing-en.webp index 5a05efa6..bc78805f 100644 Binary files a/docs/screenshots/web/routing-en.webp and b/docs/screenshots/web/routing-en.webp differ diff --git a/docs/screenshots/web/routing.webp b/docs/screenshots/web/routing.webp index 4318d8bd..2b602c4c 100644 Binary files a/docs/screenshots/web/routing.webp and b/docs/screenshots/web/routing.webp differ diff --git a/docs/screenshots/web/security-en.webp b/docs/screenshots/web/security-en.webp index 68297474..0c23e8b0 100644 Binary files a/docs/screenshots/web/security-en.webp and b/docs/screenshots/web/security-en.webp differ diff --git a/docs/screenshots/web/security.webp b/docs/screenshots/web/security.webp index 0ceb8f3a..b29e79a7 100644 Binary files a/docs/screenshots/web/security.webp and b/docs/screenshots/web/security.webp differ diff --git a/docs/screenshots/web/settings-en.webp b/docs/screenshots/web/settings-en.webp index 38750090..81a67d07 100644 Binary files a/docs/screenshots/web/settings-en.webp and b/docs/screenshots/web/settings-en.webp differ diff --git a/docs/screenshots/web/settings.webp b/docs/screenshots/web/settings.webp index 5de9b3f7..1090447e 100644 Binary files a/docs/screenshots/web/settings.webp and b/docs/screenshots/web/settings.webp differ diff --git a/docs/screenshots/web/traffic-en.webp b/docs/screenshots/web/traffic-en.webp index 9a8ea47b..1d8ebbde 100644 Binary files a/docs/screenshots/web/traffic-en.webp and b/docs/screenshots/web/traffic-en.webp differ diff --git a/docs/screenshots/web/traffic.webp b/docs/screenshots/web/traffic.webp index 2b5369de..7c001922 100644 Binary files a/docs/screenshots/web/traffic.webp and b/docs/screenshots/web/traffic.webp differ diff --git a/docs/screenshots/web/upstreams-en.webp b/docs/screenshots/web/upstreams-en.webp index 982c15bf..d3a17243 100644 Binary files a/docs/screenshots/web/upstreams-en.webp and b/docs/screenshots/web/upstreams-en.webp differ diff --git a/docs/screenshots/web/upstreams.webp b/docs/screenshots/web/upstreams.webp index de2d7303..0692c911 100644 Binary files a/docs/screenshots/web/upstreams.webp and b/docs/screenshots/web/upstreams.webp differ diff --git a/docs/screenshots/zh/clients-dark.png b/docs/screenshots/zh/clients-dark.png index df7d6bbf..363c2ec0 100644 Binary files a/docs/screenshots/zh/clients-dark.png and b/docs/screenshots/zh/clients-dark.png differ diff --git a/docs/screenshots/zh/clients-light.png b/docs/screenshots/zh/clients-light.png index 56b112c7..51dc3e8b 100644 Binary files a/docs/screenshots/zh/clients-light.png and b/docs/screenshots/zh/clients-light.png differ diff --git a/docs/screenshots/zh/dry-run-dark.png b/docs/screenshots/zh/dry-run-dark.png index dde25197..3c66675f 100644 Binary files a/docs/screenshots/zh/dry-run-dark.png and b/docs/screenshots/zh/dry-run-dark.png differ diff --git a/docs/screenshots/zh/dry-run-light.png b/docs/screenshots/zh/dry-run-light.png index 9cd235d0..2b2add3b 100644 Binary files a/docs/screenshots/zh/dry-run-light.png and b/docs/screenshots/zh/dry-run-light.png differ diff --git a/docs/screenshots/zh/keys-dark.png b/docs/screenshots/zh/keys-dark.png index 9e7e32ac..5d05af39 100644 Binary files a/docs/screenshots/zh/keys-dark.png and b/docs/screenshots/zh/keys-dark.png differ diff --git a/docs/screenshots/zh/keys-light.png b/docs/screenshots/zh/keys-light.png index 0d4b0735..a8c89bf5 100644 Binary files a/docs/screenshots/zh/keys-light.png and b/docs/screenshots/zh/keys-light.png differ diff --git a/docs/screenshots/zh/menubar-menu-dark.png b/docs/screenshots/zh/menubar-menu-dark.png index c0845045..4ff0d06b 100644 Binary files a/docs/screenshots/zh/menubar-menu-dark.png and b/docs/screenshots/zh/menubar-menu-dark.png differ diff --git a/docs/screenshots/zh/menubar-menu-light.png b/docs/screenshots/zh/menubar-menu-light.png index c6132788..6b1c687c 100644 Binary files a/docs/screenshots/zh/menubar-menu-light.png and b/docs/screenshots/zh/menubar-menu-light.png differ diff --git a/docs/screenshots/zh/overview-dark.png b/docs/screenshots/zh/overview-dark.png index a8bbf68a..f8cb7a29 100644 Binary files a/docs/screenshots/zh/overview-dark.png and b/docs/screenshots/zh/overview-dark.png differ diff --git a/docs/screenshots/zh/overview-light.png b/docs/screenshots/zh/overview-light.png index 875aa7e8..7510f844 100644 Binary files a/docs/screenshots/zh/overview-light.png and b/docs/screenshots/zh/overview-light.png differ diff --git a/docs/screenshots/zh/remote-add-dark.png b/docs/screenshots/zh/remote-add-dark.png index 59e56f43..d2f56508 100644 Binary files a/docs/screenshots/zh/remote-add-dark.png and b/docs/screenshots/zh/remote-add-dark.png differ diff --git a/docs/screenshots/zh/remote-add-light.png b/docs/screenshots/zh/remote-add-light.png index e8a688e6..d3c9c379 100644 Binary files a/docs/screenshots/zh/remote-add-light.png and b/docs/screenshots/zh/remote-add-light.png differ diff --git a/docs/screenshots/zh/remote-clients-dark.png b/docs/screenshots/zh/remote-clients-dark.png index 726b0041..5f5e52cd 100644 Binary files a/docs/screenshots/zh/remote-clients-dark.png and b/docs/screenshots/zh/remote-clients-dark.png differ diff --git a/docs/screenshots/zh/remote-clients-light.png b/docs/screenshots/zh/remote-clients-light.png index bbbf6ef4..4564d292 100644 Binary files a/docs/screenshots/zh/remote-clients-light.png and b/docs/screenshots/zh/remote-clients-light.png differ diff --git a/docs/screenshots/zh/remote-switcher-dark.png b/docs/screenshots/zh/remote-switcher-dark.png index 64debfe1..a6be0dce 100644 Binary files a/docs/screenshots/zh/remote-switcher-dark.png and b/docs/screenshots/zh/remote-switcher-dark.png differ diff --git a/docs/screenshots/zh/remote-switcher-light.png b/docs/screenshots/zh/remote-switcher-light.png index cb16e418..b8d9734e 100644 Binary files a/docs/screenshots/zh/remote-switcher-light.png and b/docs/screenshots/zh/remote-switcher-light.png differ diff --git a/docs/screenshots/zh/routing-dark.png b/docs/screenshots/zh/routing-dark.png index cb58378a..f2db1ca2 100644 Binary files a/docs/screenshots/zh/routing-dark.png and b/docs/screenshots/zh/routing-dark.png differ diff --git a/docs/screenshots/zh/routing-light.png b/docs/screenshots/zh/routing-light.png index 78e19a55..88ba1c9d 100644 Binary files a/docs/screenshots/zh/routing-light.png and b/docs/screenshots/zh/routing-light.png differ diff --git a/docs/screenshots/zh/security-dark.png b/docs/screenshots/zh/security-dark.png index 4b37e234..9e73d2ff 100644 Binary files a/docs/screenshots/zh/security-dark.png and b/docs/screenshots/zh/security-dark.png differ diff --git a/docs/screenshots/zh/security-light.png b/docs/screenshots/zh/security-light.png index 9f8ddaf3..54a11960 100644 Binary files a/docs/screenshots/zh/security-light.png and b/docs/screenshots/zh/security-light.png differ diff --git a/docs/screenshots/zh/settings-dark.png b/docs/screenshots/zh/settings-dark.png index b74824dd..3daf5b9a 100644 Binary files a/docs/screenshots/zh/settings-dark.png and b/docs/screenshots/zh/settings-dark.png differ diff --git a/docs/screenshots/zh/settings-light.png b/docs/screenshots/zh/settings-light.png index 060a0105..e096c1ca 100644 Binary files a/docs/screenshots/zh/settings-light.png and b/docs/screenshots/zh/settings-light.png differ diff --git a/docs/screenshots/zh/traffic-dark.png b/docs/screenshots/zh/traffic-dark.png index d7f9ea2b..1a5bd5ea 100644 Binary files a/docs/screenshots/zh/traffic-dark.png and b/docs/screenshots/zh/traffic-dark.png differ diff --git a/docs/screenshots/zh/traffic-light.png b/docs/screenshots/zh/traffic-light.png index 84cd40d4..b1bd7ab5 100644 Binary files a/docs/screenshots/zh/traffic-light.png and b/docs/screenshots/zh/traffic-light.png differ diff --git a/docs/screenshots/zh/upstreams-dark.png b/docs/screenshots/zh/upstreams-dark.png index 257785d8..172e32b6 100644 Binary files a/docs/screenshots/zh/upstreams-dark.png and b/docs/screenshots/zh/upstreams-dark.png differ diff --git a/docs/screenshots/zh/upstreams-light.png b/docs/screenshots/zh/upstreams-light.png index 5fd06144..99493db0 100644 Binary files a/docs/screenshots/zh/upstreams-light.png and b/docs/screenshots/zh/upstreams-light.png differ