mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
docs: add Codex DeepSeek routing guides
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# Using DeepSeek-Style Chat APIs in Codex: CC Switch Local Routing Guide
|
||||
|
||||
> Applies to CC Switch 3.16.0 and nearby versions. This guide is based on the repository documentation and code, and uses DeepSeek as an example of an OpenAI Chat Completions-compatible API. Screenshots are generated from the current frontend UI with de-identified sample data to avoid exposing a real API key or account balance.
|
||||
|
||||
## Why local routing is needed
|
||||
|
||||
The newer Codex CLI targets the OpenAI Responses API, while DeepSeek, Kimi, MiniMax, SiliconFlow, and many other providers expose the OpenAI Chat Completions shape, usually `/chat/completions`. These two protocols use different request bodies, streaming events, and response structures. If you put a Chat endpoint directly into Codex configuration, common results include an incorrect model list, 404/400 requests, or streaming responses that Codex cannot parse correctly.
|
||||
|
||||
CC Switch solves this by making Codex always talk to a local route and continue sending Responses API requests. The route detects whether the active provider is Chat-format, rewrites the request into Chat Completions for the upstream provider, and finally converts the Chat response back into the Responses shape that Codex understands.
|
||||
|
||||

|
||||
|
||||
The chain has four main steps:
|
||||
|
||||
1. When Codex routing is enabled, the local configuration is written as `http://127.0.0.1:15721/v1`, while `wire_api = "responses"` is kept in place.
|
||||
2. The provider's `meta.apiFormat = "openai_chat"` tells the route that the real upstream is Chat Completions.
|
||||
3. The route rewrites `/responses` or `/v1/responses` to `/chat/completions`, and converts the Responses request body into a Chat request body.
|
||||
4. After the upstream responds, the route converts the Chat JSON or SSE stream back into Responses JSON/SSE.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Prepare these three things first:
|
||||
|
||||
- CC Switch installed and able to start.
|
||||
- Codex CLI installed and run at least once, so the `~/.codex/config.toml` directory structure exists.
|
||||
- An API key from DeepSeek or another Chat Completions provider.
|
||||
|
||||
DeepSeek's official documentation currently lists the OpenAI-compatible base URL as `https://api.deepseek.com` (other providers often use a base URL with a `/v1` suffix), and the Chat API path as `/chat/completions`. CC Switch's DeepSeek preset already contains these details, so prefer the preset and do not manually assemble the endpoint path.
|
||||
|
||||
## Step 1: Add a Codex provider
|
||||
|
||||
Open CC Switch, switch to the top-level `Codex` tab, and click the plus button in the upper-right corner to add a provider.
|
||||
|
||||
Choose the built-in `DeepSeek` preset. You only need to do two things:
|
||||
|
||||
- Enter your DeepSeek API key.
|
||||
- Save the provider.
|
||||
|
||||

|
||||
|
||||
The preset already includes DeepSeek's request base URL, default model, model menu, thinking/reasoning parameters, and automatically enables `Needs Local Routing`. You can adjust the default model or model display names if needed; the protocol conversion is handled by the routing layer.
|
||||
|
||||
## Step 2: Enable local routing and route Codex
|
||||
|
||||
Go to the `Routing` page in Settings, expand `Local Routing`, and complete two toggles:
|
||||
|
||||
1. Turn on the main routing switch to start the local service. The default address is `127.0.0.1:15721`.
|
||||
2. Turn on `Codex` under `Routing Enabled`. If you only want Codex to use local routing, you can leave Claude and Gemini off.
|
||||
|
||||

|
||||
|
||||
After routing is enabled, CC Switch points Codex's live configuration to the local route and manages authentication with a placeholder. The real DeepSeek key stays in the CC Switch provider configuration and is injected by the local route while forwarding requests, so you do not need to expose the key in Codex's live configuration.
|
||||
|
||||
## Step 3: Switch providers and restart Codex
|
||||
|
||||
Return to the Codex provider list and click `Enable` on the DeepSeek provider. If you see the `Needs Routing` marker, that provider must be used while routing is running; when the route is not started, CC Switch shows a prompt saying the routing service is required.
|
||||
|
||||
After switching, restart the current Codex terminal session. This is recommended because:
|
||||
|
||||
- The Codex process may already have read the old `config.toml`.
|
||||
- After `model_catalog_json` is generated, the `/model` menu usually needs a fresh process before it refreshes.
|
||||
|
||||
Inside Codex, use `/model` to check whether the current model comes from the DeepSeek preset, such as `DeepSeek V4 Flash`. The Codex app currently does not support multi-model selection, so it defaults to the first configured model. Then send a small test prompt and confirm that the request count increases in the routing panel, or that a Codex request appears in usage/request logs.
|
||||
|
||||
## How to handle other Chat providers
|
||||
|
||||
DeepSeek, Kimi, MiniMax, SiliconFlow, and other common Chat-format providers already have presets in CC Switch, so use presets first. Only choose custom configuration for providers that are not covered by presets; in that case, fill in the API key, base URL, and models according to the provider's documentation, and set `API Format` to `OpenAI Chat Completions (requires routing)`.
|
||||
|
||||
If the upstream provider directly supports the OpenAI Responses API, you do not need to enable `Needs Local Routing`; CC Switch can connect through Responses directly without Chat conversion.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Codex reports 404 or cannot find `/responses`**
|
||||
|
||||
Usually Codex routing is not enabled, or the upstream Chat base URL was written directly into Codex manually. Check whether `~/.codex/config.toml` points to `http://127.0.0.1:15721/v1`.
|
||||
|
||||
**DeepSeek upstream reports 404**
|
||||
|
||||
If you are using the built-in DeepSeek preset, first confirm that the active provider really comes from the preset and that Codex routing is enabled. Only custom providers require extra base URL checks: the base URL should be the service root, not the full endpoint path with `/chat/completions`.
|
||||
|
||||
**`/model` does not show DeepSeek models**
|
||||
|
||||
Restart Codex after saving the provider. CC Switch generates `cc-switch-model-catalog.json` and writes its path to `model_catalog_json`, but a running Codex process may not hot-load the model catalog.
|
||||
The Codex app currently does not support multi-model selection, so it uses the first configured model by default.
|
||||
|
||||
**Routing is enabled, but requests still go to the wrong provider**
|
||||
|
||||
Confirm that all three states match: the current provider under the Codex tab is DeepSeek; the local routing service is running; and the Codex toggle is enabled under `Routing Enabled`.
|
||||
|
||||
**Can I use an official OpenAI Codex account through local routing?**
|
||||
|
||||
Not recommended. CC Switch blocks switching to official providers while local routing takeover is enabled, because accessing official APIs through a proxy may create account risk. Routing is mainly intended for third-party, aggregator, or protocol-conversion scenarios.
|
||||
|
||||
## References
|
||||
|
||||
- [CC Switch User Manual: Add Provider](../user-manual/en/2-providers/2.1-add.md)
|
||||
- [CC Switch User Manual: Proxy Service](../user-manual/en/4-proxy/4.1-service.md)
|
||||
- [CC Switch User Manual: App Routing](../user-manual/en/4-proxy/4.2-routing.md)
|
||||
- [DeepSeek API Docs: Your First API Call](https://api-docs.deepseek.com/)
|
||||
- [DeepSeek API Docs: Create Chat Completion](https://api-docs.deepseek.com/api/create-chat-completion)
|
||||
- [DeepSeek API Docs: Multi-round Conversation](https://api-docs.deepseek.com/guides/multi_round_chat)
|
||||
@@ -0,0 +1,101 @@
|
||||
# Codex で DeepSeek などの Chat 形式 API を使う: CC Switch ローカルルーティングガイド
|
||||
|
||||
> 対象バージョン: CC Switch 3.16.0 およびその前後のバージョン。本記事はリポジトリ内のドキュメントとコードをもとに整理し、OpenAI Chat Completions 互換 API の例として DeepSeek を使用します。スクリーンショットは現在のフロントエンド UI から、実際の API Key やアカウント残高が漏れないよう匿名化したサンプルデータで生成しています。
|
||||
|
||||
## ローカルルーティングが必要な理由
|
||||
|
||||
新しい Codex CLI は OpenAI Responses API を前提にしています。一方で DeepSeek、Kimi、MiniMax、SiliconFlow など多くのプロバイダーが実際に公開しているのは OpenAI Chat Completions 形式、つまり `/chat/completions` です。この 2 つのプロトコルは、リクエストボディ、ストリーミングイベント、レスポンス構造が異なります。Chat エンドポイントをそのまま Codex 設定に入れると、モデル一覧が合わない、リクエストが 404/400 になる、ストリーミングレスポンスを Codex が正しく解析できない、といった問題が起きがちです。
|
||||
|
||||
CC Switch では、Codex が常にローカルルートへ接続し、Responses API のままリクエストを送るようにします。ルート内部で現在のプロバイダーが Chat 形式かどうかを判定し、必要ならリクエストを Chat Completions に書き換えて上流へ送り、最後に Chat レスポンスを Codex が理解できる Responses 形式へ戻します。
|
||||
|
||||

|
||||
|
||||
この経路は主に 4 つのステップに分かれます:
|
||||
|
||||
1. Codex ルーティングを有効にすると、ローカル設定は `http://127.0.0.1:15721/v1` に書き換えられ、`wire_api = "responses"` は維持されます。
|
||||
2. Provider の `meta.apiFormat = "openai_chat"` が、実際の上流は Chat Completions だとルートに伝えます。
|
||||
3. ルートは `/responses` または `/v1/responses` を `/chat/completions` に書き換え、Responses のリクエストボディを Chat のリクエストボディへ変換します。
|
||||
4. 上流から返ってきた後、ルートは Chat の JSON または SSE ストリームを Responses JSON/SSE へ変換して返します。
|
||||
|
||||
## 事前準備
|
||||
|
||||
先に次の 3 つを用意してください:
|
||||
|
||||
- インストール済みで起動できる CC Switch。
|
||||
- インストール済みの Codex CLI。少なくとも 1 回は実行し、`~/.codex/config.toml` のディレクトリ構造が存在していること。
|
||||
- DeepSeek または同種の Chat Completions プロバイダーの API Key。
|
||||
|
||||
DeepSeek 公式ドキュメントでは、OpenAI 互換 base URL は現在 `https://api.deepseek.com`(他のプロバイダーでは `/v1` 付きの base URL もよくあります)、Chat API のパスは `/chat/completions` と記載されています。CC Switch の DeepSeek プリセットにはこれらの情報がすでに入っているため、まずはプリセットを使い、エンドポイントパスを手で組み立てる必要はありません。
|
||||
|
||||
## Step 1: Codex プロバイダーを追加する
|
||||
|
||||
CC Switch を開き、上部の `Codex` タブへ切り替え、右上のプラスボタンからプロバイダーを追加します。
|
||||
|
||||
内蔵プリセットの `DeepSeek` を選びます。必要なのは次の 2 つだけです:
|
||||
|
||||
- DeepSeek API Key を入力する。
|
||||
- プロバイダーを保存する。
|
||||
|
||||

|
||||
|
||||
プリセットには DeepSeek のリクエスト先、デフォルトモデル、モデルメニュー、thinking/reasoning パラメータがすでに含まれており、`ローカルルーティングが必要` も自動的に有効になります。必要に応じてデフォルトモデルやモデル表示名を調整できますが、プロトコル変換はルーティング層に任せれば十分です。
|
||||
|
||||
## Step 2: ローカルルーティングを有効にして Codex をルーティングする
|
||||
|
||||
設定の `ルーティング` ページに入り、`ローカルルーティング` を展開して、次の 2 つのスイッチを設定します:
|
||||
|
||||
1. `ルーティング総スイッチ` をオンにしてローカルサービスを起動します。デフォルトアドレスは `127.0.0.1:15721` です。
|
||||
2. `ルーティング有効` で `Codex` をオンにします。Codex だけをルーティングしたい場合は、Claude と Gemini はオフのままで構いません。
|
||||
|
||||

|
||||
|
||||
ルーティングを有効にすると、CC Switch は Codex の live 設定をローカルルートへ向け、認証はプレースホルダーで管理します。実際の DeepSeek Key は CC Switch の Provider 設定内に残り、ローカルルートが転送時に注入します。そのため、Codex の live 設定に Key を露出させる必要はありません。
|
||||
|
||||
## Step 3: プロバイダーを切り替えて Codex を再起動する
|
||||
|
||||
Codex プロバイダー一覧に戻り、DeepSeek プロバイダーの `有効化` をクリックします。`ルーティングが必要` の表示が見える場合、そのプロバイダーはルーティング実行中に使う必要があります。ルーティングが起動していない場合、CC Switch は「ルーティングサービスが必要」という趣旨のメッセージを表示します。
|
||||
|
||||
切り替え後は、現在の Codex ターミナルセッションを再起動することをおすすめします。理由は次のとおりです:
|
||||
|
||||
- Codex プロセスがすでに古い `config.toml` を読み込んでいる可能性があります。
|
||||
- `model_catalog_json` の生成後、`/model` メニューの更新には通常、新しいプロセスが必要です。
|
||||
|
||||
Codex に入ったら、`/model` で現在のモデルが DeepSeek プリセット由来かどうかを確認します。たとえば `DeepSeek V4 Flash` などです。現在の Codex app は複数モデル選択に対応していないため、設定内の最初のモデルをデフォルトで使用します。その後、小さな質問を 1 つ送って、ルーティングパネルのリクエスト数が増えるか、usage / リクエストログに Codex リクエストが出るかを確認します。
|
||||
|
||||
## 他の Chat プロバイダーの場合
|
||||
|
||||
DeepSeek、Kimi、MiniMax、SiliconFlow など一般的な Chat 形式プロバイダーは CC Switch にプリセットがあるため、まずはプリセットを使ってください。プリセットにないプロバイダーだけ、カスタム設定を選びます。その場合は相手側のドキュメントに従って API Key、base URL、モデルを入力し、`API 形式` を `OpenAI Chat Completions (ルーティングが必要)` に設定します。
|
||||
|
||||
上流が OpenAI Responses API を直接サポートしている場合は、`ローカルルーティングが必要` を有効にする必要はありません。その場合、CC Switch は Responses のまま直結でき、Chat 変換は行いません。
|
||||
|
||||
## よくある質問
|
||||
|
||||
**Codex が 404 を返す、または `/responses` が見つからない**
|
||||
|
||||
多くの場合、Codex ルーティングが有効になっていないか、上流 Chat base URL を手動で Codex に直接書いています。`~/.codex/config.toml` が `http://127.0.0.1:15721/v1` を指しているか確認してください。
|
||||
|
||||
**DeepSeek 上流が 404 を返す**
|
||||
|
||||
内蔵 DeepSeek プリセットを使っている場合は、まず現在のプロバイダーが本当にプリセット由来であること、そして Codex ルーティングが有効であることを確認してください。カスタムプロバイダーを使っている場合だけ、base URL を追加で確認します。base URL はサービスのルートであり、`/chat/completions` 付きの完全なエンドポイントパスではありません。
|
||||
|
||||
**`/model` に DeepSeek モデルが表示されない**
|
||||
|
||||
プロバイダーを保存した後、Codex を再起動してください。CC Switch は `cc-switch-model-catalog.json` を生成し、そのパスを `model_catalog_json` に書き込みますが、実行中の Codex プロセスがモデルカタログをホットロードするとは限りません。
|
||||
現在の Codex app は複数モデル選択に対応していないため、設定内の最初のモデルをデフォルトで使用します。
|
||||
|
||||
**ルーティングを有効にしたのに、リクエストが別のプロバイダーへ行く**
|
||||
|
||||
次の 3 つの状態が一致しているか確認してください:Codex タブの現在のプロバイダーが DeepSeek であること、ローカルルーティングサービスが実行中であること、`ルーティング有効` で Codex スイッチがオンであること。
|
||||
|
||||
**公式 OpenAI Codex アカウントをローカルルーティング経由で使えますか**
|
||||
|
||||
おすすめしません。CC Switch はローカルルーティング有効中、公式プロバイダーへの切り替えをブロックします。プロキシ経由で公式 API にアクセスすると、アカウントリスクが発生する可能性があるためです。ルーティングは主にサードパーティ、集約サービス、またはプロトコル変換のための機能です。
|
||||
|
||||
## 参考リンク
|
||||
|
||||
- [CC Switch ユーザーマニュアル: プロバイダーの追加](../user-manual/ja/2-providers/2.1-add.md)
|
||||
- [CC Switch ユーザーマニュアル: プロキシサービス](../user-manual/ja/4-proxy/4.1-service.md)
|
||||
- [CC Switch ユーザーマニュアル: アプリケーションルーティング](../user-manual/ja/4-proxy/4.2-routing.md)
|
||||
- [DeepSeek API Docs: Your First API Call](https://api-docs.deepseek.com/)
|
||||
- [DeepSeek API Docs: Create Chat Completion](https://api-docs.deepseek.com/api/create-chat-completion)
|
||||
- [DeepSeek API Docs: Multi-round Conversation](https://api-docs.deepseek.com/guides/multi_round_chat)
|
||||
@@ -0,0 +1,101 @@
|
||||
# 在 Codex 中用 DeepSeek 这类 Chat 格式 API:CC Switch 本地路由攻略
|
||||
|
||||
> 适用版本:CC Switch 3.16.0 及附近版本。本文根据仓库内文档与代码整理,并用 DeepSeek 作为 OpenAI Chat Completions 兼容接口的示例。截图来自当前前端界面,使用去敏示例数据生成,避免泄露真实 API Key 或账户余额。
|
||||
|
||||
## 为什么需要本地路由
|
||||
|
||||
新版 Codex CLI 面向的是 OpenAI Responses API,而 DeepSeek、Kimi、MiniMax、SiliconFlow 等很多供应商实际暴露的是 OpenAI Chat Completions 形态,也就是 `/chat/completions`。这两种协议的请求体、流式事件和返回结构不同,直接把 Chat 接口填进 Codex 配置里,常见结果就是模型列表不对、请求 404/400,或者流式响应无法被 Codex 正确解析。
|
||||
|
||||
CC Switch 的做法是让 Codex 始终连本机路由,仍以 Responses API 发送请求;路由在内部识别当前供应商是否是 Chat 格式,再把请求改写成 Chat Completions 发给上游,最后把 Chat 响应转换回 Responses 形态返回给 Codex。
|
||||
|
||||

|
||||
|
||||
这条链路主要分成四步:
|
||||
|
||||
1. Codex 接管时,本地配置会被写成 `http://127.0.0.1:15721/v1`,并强制保持 `wire_api = "responses"`。
|
||||
2. Provider 的 `meta.apiFormat = "openai_chat"` 会告诉路由:真实上游是 Chat Completions。
|
||||
3. 路由把 `/responses` 或 `/v1/responses` 改写到 `/chat/completions`,并把 Responses 请求体转换成 Chat 请求体。
|
||||
4. 上游返回后,路由再把 Chat 的 JSON 或 SSE 转回 Codex 能理解的 Responses JSON/SSE。
|
||||
|
||||
## 准备工作
|
||||
|
||||
你需要先准备好三样东西:
|
||||
|
||||
- 已安装并能启动的 CC Switch。
|
||||
- 已安装 Codex CLI,并至少运行过一次,让 `~/.codex/config.toml` 目录结构存在。
|
||||
- DeepSeek 或同类 Chat Completions 供应商的 API Key。
|
||||
|
||||
DeepSeek 官方文档目前写明 OpenAI 兼容 base URL 是 `https://api.deepseek.com`(其他供应商常见的是带 `/v1` 后缀的 base URL),Chat API 路径是 `/chat/completions`;CC Switch 的 DeepSeek 预设已经按这些信息配好,请优先使用预设,不需要手动拼接口路径。
|
||||
|
||||
## 第一步:添加 Codex 供应商
|
||||
|
||||
打开 CC Switch,切到顶部的 `Codex` 标签,点击右上角的加号添加供应商。
|
||||
|
||||
选择内置预设里的 `DeepSeek`,只需要做两件事:
|
||||
|
||||
- 填入 DeepSeek API Key。
|
||||
- 保存供应商。
|
||||
|
||||

|
||||
|
||||
预设已经内置 DeepSeek 的请求地址、默认模型、模型菜单、thinking/reasoning 参数,并会自动打开 `需要本地路由映射`。你可以按需调整默认模型或模型显示名;协议转换交给路由层完成即可。
|
||||
|
||||
## 第二步:开启本地路由并接管 Codex
|
||||
|
||||
进入设置里的 `路由` 页面,展开 `本地路由`,完成两个开关:
|
||||
|
||||
1. 打开 `路由总开关`,启动本地服务。默认地址是 `127.0.0.1:15721`。
|
||||
2. 在 `路由启用` 中打开 `Codex`。如果只想让 Codex 走路由,可以保持 Claude、Gemini 关闭。
|
||||
|
||||

|
||||
|
||||
接管后,CC Switch 会把 Codex 的 live 配置指向本机路由,并用占位符管理认证。真实 DeepSeek Key 仍保存在 CC Switch 的 Provider 配置里,由本地路由在转发时注入,不需要你把 Key 暴露给 Codex live 配置。
|
||||
|
||||
## 第三步:切换供应商并重启 Codex
|
||||
|
||||
回到 Codex 供应商列表,点击 DeepSeek 供应商的 `启用`。如果看到 `需要路由` 标记,说明这个供应商必须在路由运行时使用;没有启动路由时,CC Switch 会弹出“需要路由服务才能正常使用”的提示。
|
||||
|
||||
切换后建议重启当前 Codex 终端会话。原因是:
|
||||
|
||||
- Codex 进程可能已经读取过旧的 `config.toml`。
|
||||
- `model_catalog_json` 生成后,`/model` 菜单通常需要新进程才能刷新。
|
||||
|
||||
进入 Codex 后,可以用 `/model` 查看当前模型是否来自 DeepSeek 预设,例如 `DeepSeek V4 Flash`。目前 Codex app 不支持多模型选择时,会默认使用配置里的第一个模型。随后发一个小问题,确认路由面板的请求数增长,或者在用量/请求日志里看到 Codex 请求即可。
|
||||
|
||||
## 其它 Chat 供应商怎么处理
|
||||
|
||||
DeepSeek、Kimi、MiniMax、SiliconFlow 等常见 Chat 格式供应商在 CC Switch 里已有预设,优先用预设即可。只有预设里没有的供应商,才需要选择自定义配置;这时按对方文档填 API Key、base URL 和模型,并把 `API 格式` 选为 `OpenAI Chat Completions (需开启路由)`。
|
||||
|
||||
如果上游直接支持 OpenAI Responses API,就不需要打开 `需要本地路由映射`;这时 CC Switch 可以按 Responses 直连,不做 Chat 转换。
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Codex 报 404 或找不到 `/responses`**
|
||||
|
||||
通常是没有开启 Codex 接管,或者你手动把上游 Chat base URL 直接写给了 Codex。检查 `~/.codex/config.toml` 是否指向 `http://127.0.0.1:15721/v1`。
|
||||
|
||||
**DeepSeek 上游报 404**
|
||||
|
||||
如果用的是内置 DeepSeek 预设,先确认当前供应商确实来自预设,并且 Codex 路由已启用。只有在使用自定义供应商时,才需要额外检查 base URL:它应该是服务根地址,而不是带 `/chat/completions` 的完整接口路径。
|
||||
|
||||
**`/model` 看不到 DeepSeek 模型**
|
||||
|
||||
保存供应商后重启 Codex。CC Switch 会生成 `cc-switch-model-catalog.json` 并把路径写入 `model_catalog_json`,但正在运行的 Codex 进程不一定会热加载模型目录。
|
||||
目前 Codex app 不支持多模型选择,默认使用配置的第一个模型。
|
||||
|
||||
**开了路由但请求仍走错供应商**
|
||||
|
||||
确认三处状态一致:Codex 标签下当前供应商是 DeepSeek;本地路由服务正在运行;`路由启用` 里 Codex 开关已打开。
|
||||
|
||||
**可以用官方 OpenAI Codex 账号走本地路由吗**
|
||||
|
||||
不建议。CC Switch 会在本地路由接管模式下阻止切到官方供应商,因为用代理访问官方 API 可能带来账号风险。路由主要用于第三方、聚合或协议转换场景。
|
||||
|
||||
## 参考链接
|
||||
|
||||
- [CC Switch 用户手册:添加供应商](../user-manual/zh/2-providers/2.1-add.md)
|
||||
- [CC Switch 用户手册:代理服务](../user-manual/zh/4-proxy/4.1-service.md)
|
||||
- [CC Switch 用户手册:应用路由](../user-manual/zh/4-proxy/4.2-routing.md)
|
||||
- [DeepSeek API 文档:Your First API Call](https://api-docs.deepseek.com/)
|
||||
- [DeepSeek API 文档:Create Chat Completion](https://api-docs.deepseek.com/api/create-chat-completion)
|
||||
- [DeepSeek API 文档:Multi-round Conversation](https://api-docs.deepseek.com/guides/multi_round_chat)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -6,6 +6,16 @@
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
|
||||
The two headline capabilities in this release are **Codex third-party provider Chat Completions routing** and **in-app managed CLI tool management**. If you want providers that only speak the OpenAI Chat protocol (DeepSeek, Kimi, MiniMax, etc.) to work directly in Codex, or want to install / upgrade CLI tools from one place inside the app, start with these guides:
|
||||
|
||||
- **[Using DeepSeek in Codex: local routing hands-on guide](../guides/codex-deepseek-routing-guide-en.md)** — uses the built-in DeepSeek preset to walk through adding a Codex provider, enabling local routing, and verifying request forwarding.
|
||||
- **[Add a Codex provider: Chat Completions routing and model mapping](../user-manual/en/2-providers/2.1-add.md)** — covers the "Needs Local Routing" toggle, the model mapping table, and reasoning (thinking) auto-detection.
|
||||
- **[Settings → About: managed CLI tool management](../user-manual/en/1-getting-started/1.5-settings.md)** — covers version detection, per-tool / update-all upgrades, conflict diagnostics, and source-anchored upgrade commands.
|
||||
|
||||
---
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> ## Only Official Channels (Please Read)
|
||||
@@ -24,15 +34,6 @@
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
|
||||
The two headline capabilities in this release are **Codex third-party provider Chat Completions routing** and **in-app managed CLI tool management**. If you want providers that only speak the OpenAI Chat protocol (DeepSeek, Kimi, MiniMax, etc.) to work directly in Codex, or want to install / upgrade CLI tools from one place inside the app, start with these two:
|
||||
|
||||
- **[Add a Codex provider: Chat Completions routing and model mapping](../user-manual/en/2-providers/2.1-add.md)** — covers the "Needs Local Routing" toggle, the model mapping table, and reasoning (thinking) auto-detection.
|
||||
- **[Settings → About: managed CLI tool management](../user-manual/en/1-getting-started/1.5-settings.md)** — covers version detection, per-tool / update-all upgrades, conflict diagnostics, and source-anchored upgrade commands.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CC Switch v3.16.0's development since v3.15.0 centers on **promoting third-party Codex providers to first-class citizens through Chat Completions routing**. Codex natively only speaks the OpenAI Responses API and GPT-family models; this release lets CC Switch's local proxy convert Codex's outgoing Responses requests into Chat Completions and rebuild the JSON and SSE streaming responses back into Responses shape, preserving `reasoning_content` / inline `<think>` blocks / streamed reasoning summaries / tool calls / `previous_response_id` follow-ups along the way, normalizing error envelopes, and probing Chat-format providers correctly in Stream Check. It ships 22 Chat-routing presets with explicit model catalogs (DeepSeek, Zhipu GLM, Kimi, MiniMax, StepFun, Baidu Qianfan, Bailian, ModelScope, Longcat, BaiLing, Xiaomi MiMo, Volcengine Agentplan, BytePlus, DouBao Seed, SiliconFlow, Novita AI, Nvidia, and more).
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 利用ガイド
|
||||
|
||||
本リリースの主役となる 2 つの機能は、**Codex サードパーティプロバイダーの Chat Completions ルーティング**と**アプリ内の管理対象 CLI ツール管理**です。OpenAI Chat プロトコルにしか対応していないプロバイダー(DeepSeek、Kimi、MiniMax など)を Codex で直接使いたい場合、またはアプリ内で CLI ツールの一括インストール / アップグレードをしたい場合は、まずこちらをご覧ください:
|
||||
|
||||
- **[Codex で DeepSeek を使う: ローカルルーティング実践ガイド](../guides/codex-deepseek-routing-guide-ja.md)** —— DeepSeek 内蔵プリセットを例に、Codex プロバイダーの追加、ローカルルーティングの有効化、リクエスト転送の確認までを説明します。
|
||||
- **[Codex プロバイダーの追加: Chat Completions ルーティングとモデルマッピング](../user-manual/ja/2-providers/2.1-add.md)** —— 「ローカルルーティングが必要」トグル、モデルマッピングテーブル、思考能力(reasoning)の自動判別までの一連の流れを説明しています。
|
||||
- **[設定 → バージョン情報: 管理対象 CLI ツール管理](../user-manual/ja/1-getting-started/1.5-settings.md)** —— バージョン検出、個別アップグレード / 全体アップグレード、競合診断、インストール元にアンカーされたアップグレードコマンドを説明しています。
|
||||
|
||||
---
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> ## 唯一の公式チャネル(必ずお読みください)
|
||||
@@ -24,15 +34,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 利用ガイド
|
||||
|
||||
本リリースの主役となる 2 つの機能は、**Codex サードパーティプロバイダーの Chat Completions ルーティング**と**アプリ内蔵の受託 CLI ツール管理**です。OpenAI Chat プロトコルにしか対応していないプロバイダー(DeepSeek、Kimi、MiniMax など)を Codex で直接使いたい場合、またはアプリ内で CLI ツールの一括インストール / アップグレードをしたい場合は、まずこの 2 つをご覧ください:
|
||||
|
||||
- **[Codex プロバイダーの追加: Chat Completions ルーティングとモデルマッピング](../user-manual/ja/2-providers/2.1-add.md)** —— 「ローカルルーティングが必要」トグル、モデルマッピングテーブル、思考能力(reasoning)の自動判別までの一連の流れを説明しています。
|
||||
- **[設定 → バージョン情報: 受託 CLI ツール管理](../user-manual/ja/1-getting-started/1.5-settings.md)** —— バージョン検出、個別アップグレード / 全体アップグレード、競合診断、インストール元にアンカーされたアップグレードコマンドを説明しています。
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
CC Switch v3.16.0 の v3.15.0 以降の開発のコアは、**サードパーティ Codex プロバイダーを Chat Completions ルーティングによって一等市民へ昇格させること**です。Codex はネイティブには OpenAI Responses API と GPT 系モデルしか認識しませんが、本リリースでは CC Switch のローカルプロキシが Codex の送出する Responses リクエストを Chat Completions に変換し、JSON と SSE のストリーミングレスポンスを Responses 形態へ再構築します。その道中で `reasoning_content` / インライン `<think>` ブロック / ストリーミング推論サマリー / ツール呼び出し / `previous_response_id` の継続を保持し、エラーエンベロープを正規化し、Stream Check で Chat 形式プロバイダーを正しくプローブします。あわせて明示的なモデルカタログ付きの 22 個の Chat ルーティングプリセット(DeepSeek、Zhipu GLM、Kimi、MiniMax、StepFun、Baidu Qianfan、Bailian、ModelScope、Longcat、BaiLing、Xiaomi MiMo、Volcengine Agentplan、BytePlus、DouBao Seed、SiliconFlow、Novita AI、Nvidia など)を出荷します。
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 使用攻略
|
||||
|
||||
本版本最主打的两块能力是 **Codex 第三方供应商 Chat Completions 路由**与**应用内受管 CLI 工具管理**。如果你想让 DeepSeek、Kimi、MiniMax 这类只支持 OpenAI Chat 协议的供应商在 Codex 里直接可用,或者想在应用内一站式安装 / 升级 CLI 工具,建议先读这几篇:
|
||||
|
||||
- **[在 Codex 中使用 DeepSeek:本地路由实战攻略](../guides/codex-deepseek-routing-guide-zh.md)** —— 以 DeepSeek 内置预设为例,演示从添加 Codex 供应商、开启本地路由到验证请求转发的完整路径。
|
||||
- **[添加 Codex 供应商:Chat Completions 路由与模型映射](../user-manual/zh/2-providers/2.1-add.md)** —— 覆盖「需要本地路由映射」开关、模型映射表、思考能力(reasoning)自适应识别的完整流程。
|
||||
- **[设置 → 关于:受管 CLI 工具管理](../user-manual/zh/1-getting-started/1.5-settings.md)** —— 覆盖版本检测、单独升级 / 全部升级、冲突诊断、按安装来源锚定的升级命令。
|
||||
|
||||
---
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> ## 唯一官方渠道声明(请务必阅读)
|
||||
@@ -24,15 +34,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 使用攻略
|
||||
|
||||
本版本最主打的两块能力是 **Codex 第三方供应商 Chat Completions 路由**与**应用内受管 CLI 工具管理**。如果你想让 DeepSeek、Kimi、MiniMax 这类只支持 OpenAI Chat 协议的供应商在 Codex 里直接可用,或者想在应用内一站式安装 / 升级 CLI 工具,建议先读这两篇:
|
||||
|
||||
- **[添加 Codex 供应商:Chat Completions 路由与模型映射](../user-manual/zh/2-providers/2.1-add.md)** —— 覆盖「需要本地路由映射」开关、模型映射表、思考能力(reasoning)自适应识别的完整流程。
|
||||
- **[设置 → 关于:受管 CLI 工具管理](../user-manual/zh/1-getting-started/1.5-settings.md)** —— 覆盖版本检测、单独升级 / 全部升级、冲突诊断、按安装来源锚定的升级命令。
|
||||
|
||||
---
|
||||
|
||||
## 概览
|
||||
|
||||
CC Switch v3.16.0 自 v3.15.0 以来的开发核心,是把**第三方 Codex 供应商通过 Chat Completions 路由升级为一等公民**。Codex 原生只认 OpenAI Responses API 与 GPT 系列模型,本版本让 CC Switch 的本地代理把 Codex 发出的 Responses 请求转换为上游的 Chat Completions,再把 JSON 与 SSE 流式响应重建回 Responses 形态,沿途保留 `reasoning_content` / 内联 `<think>` 块 / 流式推理摘要 / 工具调用 / `previous_response_id` 续接状态,并把错误信封规范化、在 Stream Check 中正确探测 Chat 格式供应商。配套上货 22 个带显式模型目录的 Chat 路由预设(DeepSeek、智谱 GLM、Kimi、MiniMax、StepFun、百度千帆、百炼、ModelScope、Longcat、百灵、小米 MiMo、火山 Agentplan、BytePlus、豆包 Seed、SiliconFlow、Novita AI、Nvidia 等)。
|
||||
|
||||
@@ -1,597 +0,0 @@
|
||||
# CC-Switch "工作目录" 功能 — 实施方案
|
||||
|
||||
## Context
|
||||
|
||||
CC-Switch 管理 5 个 CLI 工具(Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw)的供应商、MCP 服务器、Skills、提示词配置。当前所有启用状态是全局的——用户在不同项目间切换时需要手动 toggle。
|
||||
|
||||
本功能允许用户注册多个工作目录(项目文件夹),切换目录时自动保存/恢复各实体的启用状态。**不做数据隔离**——所有实体共享全局池,仅 "谁是激活的" 按目录区分。
|
||||
|
||||
---
|
||||
|
||||
## 一、需要按目录区分的实体(完整清单)
|
||||
|
||||
| 实体 | 当前状态字段 | 存储方式 | 需要区分? | 理由 |
|
||||
|------|-------------|---------|-----------|------|
|
||||
| **Provider** | `is_current` | per `(id, app_type)` | **YES** | 不同项目用不同供应商 |
|
||||
| **Provider (Failover)** | `in_failover_queue` | per `(id, app_type)` | **YES** | 备用供应商队列跟随主供应商配置 |
|
||||
| **MCP Server** | `enabled_claude/codex/gemini/opencode` | per `id`, 4列 | **YES** | 不同项目需要不同 MCP 工具 |
|
||||
| **Skill** | `enabled_claude/codex/gemini/opencode` | per `id`, 4列 | **YES** | 不同项目需要不同 Skills |
|
||||
| **Prompt** | `enabled` | per `(id, app_type)`, 单选 | **YES** | 不同项目用不同系统提示词 |
|
||||
| Proxy Config | `enabled`, thresholds | per `app_type` | NO | 基础设施级别,非项目相关 |
|
||||
| Settings | key-value | flat table | NO | 全局用户偏好 |
|
||||
| Provider Health | failures, errors | runtime | **CLEAR** | 切换时清除,重新计算 |
|
||||
| Common Config | `common_config_{app}` | settings table | NO | 全局模板,非项目相关 |
|
||||
| Usage/Logs | historical | various tables | NO | 历史数据,不应分区 |
|
||||
|
||||
> 原计划遗漏了 **Failover Queue** 和 **Provider Health 清除**。
|
||||
|
||||
---
|
||||
|
||||
## 二、数据库变更(Schema v8 → v9)
|
||||
|
||||
### 新增 5 张表
|
||||
|
||||
```sql
|
||||
-- 1. 工作目录注册表
|
||||
CREATE TABLE IF NOT EXISTS working_directories (
|
||||
id TEXT PRIMARY KEY,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
name TEXT,
|
||||
is_current BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- 2. Provider 状态快照 (is_current + in_failover_queue)
|
||||
-- 每个目录保存所有 provider 的两个状态标志
|
||||
CREATE TABLE IF NOT EXISTS dir_provider_state (
|
||||
dir_id TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
provider_id TEXT NOT NULL,
|
||||
is_current BOOLEAN NOT NULL DEFAULT 0,
|
||||
in_failover_queue BOOLEAN NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (dir_id, app_type, provider_id)
|
||||
);
|
||||
|
||||
-- 3. MCP 启用状态快照 (直接镜像 4 列,不做行展开)
|
||||
CREATE TABLE IF NOT EXISTS dir_mcp_state (
|
||||
dir_id TEXT NOT NULL,
|
||||
mcp_id TEXT NOT NULL,
|
||||
enabled_claude BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_codex BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_gemini BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (dir_id, mcp_id)
|
||||
);
|
||||
|
||||
-- 4. Skill 启用状态快照 (直接镜像 4 列)
|
||||
CREATE TABLE IF NOT EXISTS dir_skill_state (
|
||||
dir_id TEXT NOT NULL,
|
||||
skill_id TEXT NOT NULL,
|
||||
enabled_claude BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_codex BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_gemini BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (dir_id, skill_id)
|
||||
);
|
||||
|
||||
-- 5. Prompt 启用状态快照 (每个 app_type 只存激活的 prompt_id)
|
||||
CREATE TABLE IF NOT EXISTS dir_prompt_state (
|
||||
dir_id TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
prompt_id TEXT NOT NULL,
|
||||
PRIMARY KEY (dir_id, app_type)
|
||||
);
|
||||
```
|
||||
|
||||
### 设计决策说明
|
||||
|
||||
**MCP/Skill 用 4 列镜像而非 `(entity_id, app_type, enabled)` 行展开**:
|
||||
- 与主表 `mcp_servers` / `skills` 结构一致,snapshot/apply 代码直接 copy 4 列
|
||||
- 避免 4 倍行膨胀(每个 MCP 服务器 1 行 vs 4 行)
|
||||
- 未来增加新 app 时,两边同步加列即可
|
||||
|
||||
**Prompt 只存 `(dir_id, app_type, prompt_id)`**:
|
||||
- 每个 app_type 最多一个 enabled prompt,不需要存 boolean
|
||||
- 无记录 = 该 app 无激活 prompt
|
||||
|
||||
**Provider 合并 `is_current` + `in_failover_queue`**:
|
||||
- 两个标志都是 per `(app_type, provider_id)` 的状态
|
||||
- 存在同一表中避免多表 JOIN
|
||||
|
||||
### 迁移脚本
|
||||
|
||||
在 `schema.rs` 中:
|
||||
- `create_tables_on_conn()` 添加 5 个 CREATE TABLE
|
||||
- 新增 `migrate_v8_to_v9(conn)`: 创建 5 张表 + 插入 `__default__` 行
|
||||
- `SCHEMA_VERSION` 升至 9
|
||||
- 迁移循环添加 `7 => ...` 后加 `8 => { Self::migrate_v8_to_v9(conn)?; Self::set_user_version(conn, 9)?; }`
|
||||
|
||||
```rust
|
||||
fn migrate_v8_to_v9(conn: &Connection) -> Result<(), AppError> {
|
||||
// 创建 5 张表(使用 IF NOT EXISTS,幂等)
|
||||
// ...
|
||||
// 插入 __default__ 虚拟目录,代表"全局默认"状态
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO working_directories (id, path, name, is_current, created_at) \
|
||||
VALUES ('__default__', '__default__', NULL, 0, ?1)",
|
||||
[crate::database::get_unix_timestamp()?],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、后端实现
|
||||
|
||||
### 3.1 DAO 层 — `src-tauri/src/database/dao/working_dir.rs`
|
||||
|
||||
所有方法都是 `impl Database` 块,遵循现有 DAO 模式。
|
||||
|
||||
**关键方法签名**(需要 `_on_conn` 变体支持事务):
|
||||
|
||||
```rust
|
||||
// ═══ 工作目录 CRUD ═══
|
||||
pub fn list_working_directories(&self) -> Result<Vec<WorkingDirectory>, AppError>
|
||||
pub fn add_working_directory(&self, id: &str, path: &str, name: Option<&str>) -> Result<(), AppError>
|
||||
pub fn delete_working_directory(&self, id: &str) -> Result<(), AppError>
|
||||
pub fn rename_working_directory(&self, id: &str, name: &str) -> Result<(), AppError>
|
||||
pub fn get_current_working_directory(&self) -> Result<Option<WorkingDirectory>, AppError>
|
||||
|
||||
// 使用 _on_conn 变体,在 Service 层的事务中调用
|
||||
fn set_current_working_directory_on_conn(conn: &Connection, id: &str) -> Result<(), AppError>
|
||||
|
||||
// ═══ 快照写入 ═══ (都有 _on_conn 变体)
|
||||
fn snapshot_providers_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError>
|
||||
fn snapshot_mcp_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError>
|
||||
fn snapshot_skills_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError>
|
||||
fn snapshot_prompts_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError>
|
||||
|
||||
// ═══ 快照恢复 ═══ (都有 _on_conn 变体, 返回 bool = 是否有快照)
|
||||
fn apply_provider_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result<bool, AppError>
|
||||
fn apply_mcp_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result<bool, AppError>
|
||||
fn apply_skill_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result<bool, AppError>
|
||||
fn apply_prompt_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result<bool, AppError>
|
||||
```
|
||||
|
||||
**snapshot_providers 实现思路**:
|
||||
```sql
|
||||
-- 先清除旧快照
|
||||
DELETE FROM dir_provider_state WHERE dir_id = ?1;
|
||||
-- 从主表复制当前状态
|
||||
INSERT INTO dir_provider_state (dir_id, app_type, provider_id, is_current, in_failover_queue)
|
||||
SELECT ?1, app_type, id, is_current, in_failover_queue
|
||||
FROM providers
|
||||
WHERE is_current = 1 OR in_failover_queue = 1;
|
||||
```
|
||||
|
||||
**apply_provider_snapshot 实现思路**:
|
||||
```sql
|
||||
-- 检查是否有快照
|
||||
SELECT COUNT(*) FROM dir_provider_state WHERE dir_id = ?1; -- 如果 0,返回 false
|
||||
|
||||
-- 在事务中:先清除主表所有 is_current 和 in_failover_queue
|
||||
UPDATE providers SET is_current = 0;
|
||||
UPDATE providers SET in_failover_queue = 0;
|
||||
|
||||
-- 从快照恢复
|
||||
UPDATE providers SET is_current = 1
|
||||
WHERE (id, app_type) IN (SELECT provider_id, app_type FROM dir_provider_state WHERE dir_id = ?1 AND is_current = 1);
|
||||
|
||||
UPDATE providers SET in_failover_queue = 1
|
||||
WHERE (id, app_type) IN (SELECT provider_id, app_type FROM dir_provider_state WHERE dir_id = ?1 AND in_failover_queue = 1);
|
||||
```
|
||||
|
||||
**snapshot_mcp / snapshot_skills 实现思路**(直接镜像 4 列):
|
||||
```sql
|
||||
DELETE FROM dir_mcp_state WHERE dir_id = ?1;
|
||||
INSERT INTO dir_mcp_state (dir_id, mcp_id, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode)
|
||||
SELECT ?1, id, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode
|
||||
FROM mcp_servers;
|
||||
```
|
||||
|
||||
**apply_mcp_snapshot 实现思路**:
|
||||
```sql
|
||||
-- 先全部禁用
|
||||
UPDATE mcp_servers SET enabled_claude = 0, enabled_codex = 0, enabled_gemini = 0, enabled_opencode = 0;
|
||||
|
||||
-- 从快照恢复
|
||||
UPDATE mcp_servers SET
|
||||
enabled_claude = (SELECT enabled_claude FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id),
|
||||
enabled_codex = (SELECT enabled_codex FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id),
|
||||
enabled_gemini = (SELECT enabled_gemini FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id),
|
||||
enabled_opencode = (SELECT enabled_opencode FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id)
|
||||
WHERE id IN (SELECT mcp_id FROM dir_mcp_state WHERE dir_id = ?1);
|
||||
```
|
||||
|
||||
### 3.2 Service 层 — `src-tauri/src/services/working_dir.rs`
|
||||
|
||||
```rust
|
||||
use crate::store::AppState;
|
||||
use crate::error::AppError;
|
||||
use crate::database::lock_conn;
|
||||
use crate::app_config::AppType;
|
||||
use crate::services::{McpService, ProviderService, SkillService};
|
||||
use crate::config::write_text_file;
|
||||
use crate::prompt_files::prompt_file_path;
|
||||
|
||||
pub struct WorkingDirService;
|
||||
|
||||
impl WorkingDirService {
|
||||
/// 核心切换逻辑
|
||||
pub fn switch(state: &AppState, target_dir_id: &str) -> Result<(), AppError> {
|
||||
// ═══ 前置检查 ═══
|
||||
// 1. 检查代理接管状态,若活跃则拒绝切换
|
||||
// 使用 db.is_live_takeover_active() 或同步检查 proxy_config.live_takeover_active
|
||||
// (因为 ProxyService::is_running() 是 async,而此函数是 sync)
|
||||
Self::check_proxy_not_active(state)?;
|
||||
|
||||
// ═══ Phase 1: 回填 Prompt ═══
|
||||
// 在 snapshot 之前,将 live 文件内容回填到当前 enabled prompt
|
||||
// 这样即使用户手动编辑了 live 文件,内容也不会丢失
|
||||
Self::backfill_prompt_content(state)?;
|
||||
|
||||
// ═══ Phase 2: 数据库操作(事务) ═══
|
||||
{
|
||||
let conn = lock_conn!(state.db.conn);
|
||||
conn.execute("BEGIN IMMEDIATE", [])?;
|
||||
|
||||
let result = (|| -> Result<(), AppError> {
|
||||
// 获取当前工作目录
|
||||
let current = Self::get_current_dir_id_on_conn(&conn)?;
|
||||
|
||||
// 保存当前状态到旧目录
|
||||
if let Some(old_id) = ¤t {
|
||||
Database::snapshot_providers_on_conn(&conn, old_id)?;
|
||||
Database::snapshot_mcp_on_conn(&conn, old_id)?;
|
||||
Database::snapshot_skills_on_conn(&conn, old_id)?;
|
||||
Database::snapshot_prompts_on_conn(&conn, old_id)?;
|
||||
} else {
|
||||
// 无当前目录 = 全局模式,保存到 __default__
|
||||
Database::snapshot_providers_on_conn(&conn, "__default__")?;
|
||||
Database::snapshot_mcp_on_conn(&conn, "__default__")?;
|
||||
Database::snapshot_skills_on_conn(&conn, "__default__")?;
|
||||
Database::snapshot_prompts_on_conn(&conn, "__default__")?;
|
||||
}
|
||||
|
||||
// 加载目标目录快照(如果有的话)
|
||||
// 如果无快照(首次进入),保持主表不变
|
||||
Database::apply_provider_snapshot_on_conn(&conn, target_dir_id)?;
|
||||
Database::apply_mcp_snapshot_on_conn(&conn, target_dir_id)?;
|
||||
Database::apply_skill_snapshot_on_conn(&conn, target_dir_id)?;
|
||||
Database::apply_prompt_snapshot_on_conn(&conn, target_dir_id)?;
|
||||
|
||||
// 更新 is_current 标记
|
||||
Database::set_current_working_directory_on_conn(&conn, target_dir_id)?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => conn.execute("COMMIT", [])?,
|
||||
Err(e) => {
|
||||
let _ = conn.execute("ROLLBACK", []);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
// conn 锁在此处释放
|
||||
|
||||
// ═══ Phase 3: 同步 live 配置文件 ═══
|
||||
Self::sync_all_live(state)?;
|
||||
|
||||
// ═══ Phase 4: 清除 Provider Health ═══
|
||||
state.db.clear_all_provider_health()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 回填 live prompt 文件内容到 DB(切换前调用)
|
||||
fn backfill_prompt_content(state: &AppState) -> Result<(), AppError> {
|
||||
for app in AppType::all() {
|
||||
let path = prompt_file_path(&app)?;
|
||||
if !path.exists() { continue; }
|
||||
let live_content = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
if live_content.trim().is_empty() { continue; }
|
||||
|
||||
let mut prompts = state.db.get_prompts(app.as_str())?;
|
||||
if let Some((_, prompt)) = prompts.iter_mut().find(|(_, p)| p.enabled) {
|
||||
prompt.content = live_content;
|
||||
prompt.updated_at = Some(get_unix_timestamp()?);
|
||||
state.db.save_prompt(app.as_str(), prompt)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 DB 中的 enabled prompt 内容写入 live 文件(切换后调用)
|
||||
/// 注意:不做回填!只写入。区别于 PromptService::enable_prompt()
|
||||
fn write_prompts_to_live(state: &AppState) -> Result<(), AppError> {
|
||||
for app in AppType::all() {
|
||||
let path = prompt_file_path(&app)?;
|
||||
let prompts = state.db.get_prompts(app.as_str())?;
|
||||
if let Some(prompt) = prompts.values().find(|p| p.enabled) {
|
||||
write_text_file(&path, &prompt.content)?;
|
||||
}
|
||||
// 无 enabled prompt 时不清空文件(保留现状)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 同步所有 live 配置(Provider + MCP + Skill + Prompt)
|
||||
fn sync_all_live(state: &AppState) -> Result<(), AppError> {
|
||||
// 1. Provider → live files
|
||||
ProviderService::sync_current_to_live(state)?;
|
||||
// sync_current_to_live 内部已调用 McpService::sync_all_enabled()
|
||||
|
||||
// 2. Skills → app dirs (循环每个 app)
|
||||
for app in AppType::all() {
|
||||
let _ = SkillService::sync_to_app(&state.db, &app);
|
||||
}
|
||||
|
||||
// 3. Prompts → live files
|
||||
Self::write_prompts_to_live(state)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查代理是否活跃(同步检查数据库标志)
|
||||
fn check_proxy_not_active(state: &AppState) -> Result<(), AppError> {
|
||||
// 检查 proxy_config 表中 live_takeover_active 列
|
||||
// 如果有任何 app 的 live_takeover_active = 1,拒绝切换
|
||||
let conn = lock_conn!(state.db.conn);
|
||||
let active: bool = conn.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM proxy_config WHERE live_takeover_active = 1)",
|
||||
[], |r| r.get(0)
|
||||
).unwrap_or(false);
|
||||
|
||||
if active {
|
||||
return Err(AppError::Message(
|
||||
"代理接管模式运行中,请先停止代理再切换工作目录".into()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Command 层 — `src-tauri/src/commands/working_dir.rs`
|
||||
|
||||
遵循现有模式:`State<'_, AppState>` + `Result<T, String>` + `.map_err(|e| e.to_string())`。
|
||||
|
||||
```rust
|
||||
#[tauri::command]
|
||||
pub fn list_working_directories(state: State<'_, AppState>) -> Result<Vec<WorkingDirectory>, String>
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_working_directory(state: State<'_, AppState>, path: String, name: Option<String>) -> Result<WorkingDirectory, String>
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_working_directory(state: State<'_, AppState>, id: String) -> Result<(), String>
|
||||
|
||||
#[tauri::command]
|
||||
pub fn rename_working_directory(state: State<'_, AppState>, id: String, name: String) -> Result<(), String>
|
||||
|
||||
#[tauri::command]
|
||||
pub fn switch_working_directory(state: State<'_, AppState>, id: String) -> Result<(), String>
|
||||
// 调用 WorkingDirService::switch()
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_current_working_directory(state: State<'_, AppState>) -> Result<Option<WorkingDirectory>, String>
|
||||
```
|
||||
|
||||
### 3.4 需修改的现有文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src-tauri/src/database/schema.rs` | 添加 5 个 CREATE TABLE + `migrate_v8_to_v9()` |
|
||||
| `src-tauri/src/database/mod.rs` | `SCHEMA_VERSION = 9` + 迁移循环加 `8 => ...` + `pub mod working_dir` in dao |
|
||||
| `src-tauri/src/database/dao/mod.rs` | 添加 `pub mod working_dir;` |
|
||||
| `src-tauri/src/services/mod.rs` | 添加 `pub mod working_dir;` + `pub use working_dir::WorkingDirService;` |
|
||||
| `src-tauri/src/commands/mod.rs` | 添加 `mod working_dir;` + `pub use working_dir::*;` |
|
||||
| `src-tauri/src/lib.rs` | invoke_handler 注册 6 个新命令 |
|
||||
|
||||
### 3.5 可能需要新增的 DAO 辅助方法
|
||||
|
||||
`src-tauri/src/database/dao/failover.rs`:
|
||||
```rust
|
||||
/// 清除所有 provider_health 记录(切换目录时调用)
|
||||
pub fn clear_all_provider_health(&self) -> Result<(), AppError>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、前端实现
|
||||
|
||||
### 4.1 API — `src/lib/api/workingDir.ts`
|
||||
|
||||
```typescript
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface WorkingDirectory {
|
||||
id: string;
|
||||
path: string;
|
||||
name?: string;
|
||||
isCurrent: boolean;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export const workingDirApi = {
|
||||
list: () => invoke<WorkingDirectory[]>("list_working_directories"),
|
||||
add: (path: string, name?: string) =>
|
||||
invoke<WorkingDirectory>("add_working_directory", { path, name }),
|
||||
delete: (id: string) => invoke<void>("delete_working_directory", { id }),
|
||||
rename: (id: string, name: string) =>
|
||||
invoke<void>("rename_working_directory", { id, name }),
|
||||
switch: (id: string) => invoke<void>("switch_working_directory", { id }),
|
||||
getCurrent: () =>
|
||||
invoke<WorkingDirectory | null>("get_current_working_directory"),
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 组件 — `src/components/WorkingDirSwitcher.tsx`
|
||||
|
||||
**位置**:Header toolbar,靠近 AppSwitcher。
|
||||
|
||||
**功能**:
|
||||
- 下拉菜单显示已注册目录列表
|
||||
- 当前目录高亮
|
||||
- "浏览…" 按钮调用 Tauri 文件夹选择对话框
|
||||
- 右键菜单:重命名、删除
|
||||
- "__default__(全局)" 选项恢复到全局状态
|
||||
- 切换后 invalidate 所有相关 React Query
|
||||
|
||||
**切换后的 Query Invalidation**:
|
||||
```typescript
|
||||
// 需要验证实际的 queryKey 名称
|
||||
queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp-servers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["installed-skills"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["prompts"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["workingDirectories"] });
|
||||
```
|
||||
|
||||
### 4.3 i18n
|
||||
|
||||
三个文件都需更新:
|
||||
- `src/i18n/locales/zh.json`
|
||||
- `src/i18n/locales/en.json`
|
||||
- `src/i18n/locales/ja.json`
|
||||
|
||||
---
|
||||
|
||||
## 五、切换流程时序
|
||||
|
||||
```
|
||||
用户选择目录 B
|
||||
│
|
||||
├── 1. check_proxy_not_active()
|
||||
│ → 如果代理接管中,返回错误,终止
|
||||
│
|
||||
├── 2. backfill_prompt_content()
|
||||
│ → 读 live prompt 文件 → 更新 DB 中已启用 prompt 的 content
|
||||
│ → 保护用户手动编辑的 prompt 不丢失
|
||||
│
|
||||
├── 3. BEGIN TRANSACTION
|
||||
│ ├── snapshot(old_dir / __default__)
|
||||
│ │ ├── providers → dir_provider_state (is_current + in_failover_queue)
|
||||
│ │ ├── mcp_servers → dir_mcp_state (4 列直接复制)
|
||||
│ │ ├── skills → dir_skill_state (4 列直接复制)
|
||||
│ │ └── prompts → dir_prompt_state (enabled prompt_id)
|
||||
│ │
|
||||
│ ├── apply(target_dir)
|
||||
│ │ ├── dir_provider_state → providers
|
||||
│ │ ├── dir_mcp_state → mcp_servers
|
||||
│ │ ├── dir_skill_state → skills
|
||||
│ │ └── dir_prompt_state → prompts
|
||||
│ │
|
||||
│ └── set_current_working_directory(target_dir)
|
||||
│
|
||||
├── COMMIT
|
||||
│
|
||||
├── 4. sync_all_live()
|
||||
│ ├── ProviderService::sync_current_to_live(state)
|
||||
│ │ └── 内部已调用 McpService::sync_all_enabled()
|
||||
│ ├── for app in AppType::all() { SkillService::sync_to_app(&db, &app) }
|
||||
│ └── write_prompts_to_live() ← 无回填,直接写
|
||||
│
|
||||
└── 5. clear_all_provider_health()
|
||||
→ 清除运行时熔断器状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、边界情况处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| **首次进入目录(无快照)** | `apply_*_snapshot()` 返回 false,主表保持不变。用户调整后,下次切走时自动保存。 |
|
||||
| **全局模式 → 目录** | 自动将当前状态 snapshot 到 `__default__` 虚拟目录。`__default__` 在 v9 迁移中预创建。 |
|
||||
| **目录 → 全局模式** | 用户选择 `__default__`,恢复全局状态。 |
|
||||
| **新增 MCP/Skill/Provider** | 新实体在 dir_*_state 中无记录。apply 时只更新有记录的实体,新增的保持 DB 默认值。 |
|
||||
| **删除 MCP/Skill/Provider** | dir_*_state 中对应记录在 apply 时找不到主表行,UPDATE 影响 0 行,静默跳过。 |
|
||||
| **删除工作目录** | 级联删除 dir_*_state 中所有 `dir_id` 匹配的行。若为当前目录,回退到 `__default__`。 |
|
||||
| **代理接管中切换** | `check_proxy_not_active()` 检测到 `live_takeover_active = 1`,拒绝切换并提示用户先停止代理。 |
|
||||
| **切换中途崩溃** | 事务保护 DB 操作的原子性。最坏情况:DB 已更新但 live 文件未同步。下次启动可添加恢复检查(Phase 2 优化)。 |
|
||||
| **用户手动编辑了 prompt 文件** | `backfill_prompt_content()` 在切换前读取 live 文件回填到 DB,保护手动修改。 |
|
||||
|
||||
---
|
||||
|
||||
## 七、实施顺序
|
||||
|
||||
### Phase 1: 数据库
|
||||
1. `database/schema.rs` — 5 个 CREATE TABLE + `migrate_v8_to_v9()`
|
||||
2. `database/mod.rs` — `SCHEMA_VERSION = 9` + 迁移分支
|
||||
3. `database/dao/working_dir.rs` — 全部 DAO 方法(`_on_conn` 变体)
|
||||
4. `database/dao/failover.rs` — 新增 `clear_all_provider_health()`
|
||||
5. `database/dao/mod.rs` — 注册模块
|
||||
|
||||
### Phase 2: 服务 + 命令
|
||||
6. `services/working_dir.rs` — `WorkingDirService::switch()` 等
|
||||
7. `commands/working_dir.rs` — 6 个 Tauri 命令
|
||||
8. `services/mod.rs` — 注册模块
|
||||
9. `commands/mod.rs` — 注册模块
|
||||
10. `lib.rs` — invoke_handler 注册
|
||||
|
||||
### Phase 3: 前端
|
||||
11. `src/lib/api/workingDir.ts` — API 封装
|
||||
12. `src/types.ts` — WorkingDirectory 类型
|
||||
13. `src/components/WorkingDirSwitcher.tsx` — UI 组件
|
||||
14. `src/App.tsx` — 集成到 header toolbar
|
||||
15. `src/i18n/locales/{zh,en,ja}.json` — 国际化
|
||||
|
||||
### Phase 4: 优化(可选)
|
||||
16. 启动恢复检查(DB 状态 vs live 文件一致性)
|
||||
17. 托盘菜单显示当前工作目录
|
||||
|
||||
---
|
||||
|
||||
## 八、关键文件索引
|
||||
|
||||
### 新增文件(5 个)
|
||||
- `src-tauri/src/database/dao/working_dir.rs`
|
||||
- `src-tauri/src/services/working_dir.rs`
|
||||
- `src-tauri/src/commands/working_dir.rs`
|
||||
- `src/lib/api/workingDir.ts`
|
||||
- `src/components/WorkingDirSwitcher.tsx`
|
||||
|
||||
### 必须修改的文件(7 个)
|
||||
- `src-tauri/src/database/schema.rs` — CREATE TABLE + 迁移
|
||||
- `src-tauri/src/database/mod.rs` — 版本号 + 迁移循环
|
||||
- `src-tauri/src/database/dao/mod.rs` — 模块注册
|
||||
- `src-tauri/src/database/dao/failover.rs` — clear_all_provider_health
|
||||
- `src-tauri/src/services/mod.rs` — 模块注册
|
||||
- `src-tauri/src/commands/mod.rs` — 模块注册
|
||||
- `src-tauri/src/lib.rs` — invoke_handler
|
||||
|
||||
### 必须修改的前端文件(4 个)
|
||||
- `src/App.tsx` — 集成 WorkingDirSwitcher
|
||||
- `src/types.ts` — WorkingDirectory 接口
|
||||
- `src/i18n/locales/zh.json` — 中文
|
||||
- `src/i18n/locales/en.json` — 英文
|
||||
- `src/i18n/locales/ja.json` — 日文
|
||||
|
||||
### 参考文件(理解现有模式)
|
||||
- `src-tauri/src/services/mcp.rs` — `sync_all_enabled()` (line 165)
|
||||
- `src-tauri/src/services/skill.rs` — `sync_to_app()` (line 1707)
|
||||
- `src-tauri/src/services/provider/mod.rs` — `sync_current_to_live()` (line 1552)
|
||||
- `src-tauri/src/services/prompt.rs` — `enable_prompt()` (line 73) — 理解回填逻辑
|
||||
- `src-tauri/src/prompt_files.rs` — prompt 文件路径
|
||||
- `src-tauri/src/config.rs` — `write_text_file()` (line 176)
|
||||
|
||||
---
|
||||
|
||||
## 九、验证计划
|
||||
|
||||
### 后端验证
|
||||
1. `cargo test` — DAO 层单元测试(使用 `Database::memory()`)
|
||||
- 快照/恢复往返一致性
|
||||
- 新增/删除实体后的 apply 行为
|
||||
- `__default__` 全局状态保护
|
||||
- 事务回滚测试
|
||||
2. 手动测试 — 启动应用,创建两个目录,切换并验证 live 文件变化
|
||||
|
||||
### 前端验证
|
||||
1. `pnpm typecheck` — TypeScript 类型检查
|
||||
2. `pnpm lint` — ESLint 检查
|
||||
3. 手动 UI 测试 — 工作目录切换器交互、query invalidation 后数据刷新
|
||||
Reference in New Issue
Block a user