mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eec05e704f | |||
| 8c20c57b29 | |||
| 5ef6f78fd0 | |||
| 44fba676d5 | |||
| e6f91541a3 | |||
| 828f839083 | |||
| 63b874aff1 | |||
| bdef49fe0f | |||
| bba6524979 |
@@ -20,8 +20,6 @@ jobs:
|
||||
include:
|
||||
- os: windows-2022
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-22.04-arm
|
||||
arch: arm64
|
||||
- os: macos-14
|
||||
|
||||
steps:
|
||||
@@ -59,8 +57,7 @@ jobs:
|
||||
rpm \
|
||||
flatpak \
|
||||
flatpak-builder \
|
||||
elfutils \
|
||||
xdg-utils
|
||||
elfutils
|
||||
# GTK/GLib stack for gdk-3.0, glib-2.0, gio-2.0
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libgtk-3-dev \
|
||||
@@ -88,8 +85,8 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install frontend deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -259,11 +256,10 @@ jobs:
|
||||
set -euxo pipefail
|
||||
mkdir -p release-assets
|
||||
VERSION="${GITHUB_REF_NAME}" # e.g., v3.5.0
|
||||
ARCH="${{ matrix.arch || 'x86_64' }}"
|
||||
# Updater artifact: AppImage(含对应 .sig)
|
||||
APPIMAGE=$(find src-tauri/target/release/bundle -name "*.AppImage" | head -1 || true)
|
||||
if [ -n "$APPIMAGE" ]; then
|
||||
NEW_APPIMAGE="CC-Switch-${VERSION}-Linux-${ARCH}.AppImage"
|
||||
NEW_APPIMAGE="CC-Switch-${VERSION}-Linux.AppImage"
|
||||
cp "$APPIMAGE" "release-assets/$NEW_APPIMAGE"
|
||||
[ -f "$APPIMAGE.sig" ] && cp "$APPIMAGE.sig" "release-assets/$NEW_APPIMAGE.sig" || echo ".sig for AppImage not found"
|
||||
echo "AppImage copied: $NEW_APPIMAGE"
|
||||
@@ -273,16 +269,18 @@ jobs:
|
||||
# 额外上传 .deb(用于手动安装,不参与 Updater)
|
||||
DEB=$(find src-tauri/target/release/bundle -name "*.deb" | head -1 || true)
|
||||
if [ -n "$DEB" ]; then
|
||||
cp "$DEB" "release-assets/CC-Switch-${VERSION}-Linux-${ARCH}.deb"
|
||||
echo "Deb package copied: CC-Switch-${VERSION}-Linux-${ARCH}.deb"
|
||||
NEW_DEB="CC-Switch-${VERSION}-Linux.deb"
|
||||
cp "$DEB" "release-assets/$NEW_DEB"
|
||||
echo "Deb package copied: $NEW_DEB"
|
||||
else
|
||||
echo "No .deb found (optional)"
|
||||
fi
|
||||
# 额外上传 .rpm(用于 Fedora/RHEL/openSUSE 等,不参与 Updater)
|
||||
RPM=$(find src-tauri/target/release/bundle -name "*.rpm" | head -1 || true)
|
||||
if [ -n "$RPM" ]; then
|
||||
cp "$RPM" "release-assets/CC-Switch-${VERSION}-Linux-${ARCH}.rpm"
|
||||
echo "RPM package copied: CC-Switch-${VERSION}-Linux-${ARCH}.rpm"
|
||||
NEW_RPM="CC-Switch-${VERSION}-Linux.rpm"
|
||||
cp "$RPM" "release-assets/$NEW_RPM"
|
||||
echo "RPM package copied: $NEW_RPM"
|
||||
else
|
||||
echo "No .rpm found (optional)"
|
||||
fi
|
||||
@@ -314,8 +312,7 @@ jobs:
|
||||
|
||||
- **macOS**: `CC-Switch-${{ github.ref_name }}-macOS.zip`(解压即用)或 `CC-Switch-${{ github.ref_name }}-macOS.tar.gz`(Homebrew)
|
||||
- **Windows**: `CC-Switch-${{ github.ref_name }}-Windows.msi`(安装版)或 `CC-Switch-${{ github.ref_name }}-Windows-Portable.zip`(绿色版)
|
||||
- **Linux (x86_64)**: `CC-Switch-${{ github.ref_name }}-Linux-x86_64.AppImage` / `.deb` / `.rpm`
|
||||
- **Linux (ARM64)**: `CC-Switch-${{ github.ref_name }}-Linux-arm64.AppImage` / `.deb` / `.rpm`
|
||||
- **Linux**: `CC-Switch-${{ github.ref_name }}-Linux.AppImage`(AppImage)或 `CC-Switch-${{ github.ref_name }}-Linux.deb`(Debian/Ubuntu)或 `CC-Switch-${{ github.ref_name }}-Linux.rpm`(Fedora/RHEL/openSUSE)
|
||||
|
||||
---
|
||||
提示:macOS 如遇"已损坏"提示,可在终端执行:`xattr -cr "/Applications/CC Switch.app"`
|
||||
@@ -363,8 +360,7 @@ jobs:
|
||||
# 初始化空平台映射
|
||||
mac_url=""; mac_sig=""
|
||||
win_url=""; win_sig=""
|
||||
linux_x64_url=""; linux_x64_sig=""
|
||||
linux_arm64_url=""; linux_arm64_sig=""
|
||||
linux_url=""; linux_sig=""
|
||||
shopt -s nullglob
|
||||
for sig in dl/*.sig; do
|
||||
base=${sig%.sig}
|
||||
@@ -375,10 +371,8 @@ jobs:
|
||||
*.tar.gz)
|
||||
# 视为 macOS updater artifact
|
||||
mac_url="$url"; mac_sig="$sig_content";;
|
||||
*-Linux-arm64.AppImage|*-Linux-arm64.appimage)
|
||||
linux_arm64_url="$url"; linux_arm64_sig="$sig_content";;
|
||||
*-Linux-x86_64.AppImage|*-Linux-x86_64.appimage)
|
||||
linux_x64_url="$url"; linux_x64_sig="$sig_content";;
|
||||
*.AppImage|*.appimage)
|
||||
linux_url="$url"; linux_sig="$sig_content";;
|
||||
*.msi|*.exe)
|
||||
win_url="$url"; win_sig="$sig_content";;
|
||||
esac
|
||||
@@ -405,14 +399,9 @@ jobs:
|
||||
echo " \"windows-x86_64\": {\"signature\": \"$win_sig\", \"url\": \"$win_url\"}"
|
||||
first=0
|
||||
fi
|
||||
if [ -n "$linux_x64_url" ] && [ -n "$linux_x64_sig" ]; then
|
||||
if [ -n "$linux_url" ] && [ -n "$linux_sig" ]; then
|
||||
[ $first -eq 0 ] && echo ','
|
||||
echo " \"linux-x86_64\": {\"signature\": \"$linux_x64_sig\", \"url\": \"$linux_x64_url\"}"
|
||||
first=0
|
||||
fi
|
||||
if [ -n "$linux_arm64_url" ] && [ -n "$linux_arm64_sig" ]; then
|
||||
[ $first -eq 0 ] && echo ','
|
||||
echo " \"linux-aarch64\": {\"signature\": \"$linux_arm64_sig\", \"url\": \"$linux_arm64_url\"}"
|
||||
echo " \"linux-x86_64\": {\"signature\": \"$linux_sig\", \"url\": \"$linux_url\"}"
|
||||
first=0
|
||||
fi
|
||||
echo ' }'
|
||||
|
||||
@@ -9,55 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [3.10.3] - 2026-01-30
|
||||
|
||||
### Feature Release
|
||||
|
||||
This release introduces a generic API format selector, pricing configuration enhancements, and multiple UX improvements.
|
||||
|
||||
### Added
|
||||
|
||||
- **API Key Link for OpenCode**: API key link support for OpenCode provider form, enabling quick access to provider key management pages
|
||||
- **AICodeMirror Partner Preset**: Added AICodeMirror partner preset for all apps (Claude, Codex, Gemini, OpenCode)
|
||||
- **API Format Selector**: Generic API format chooser for Claude providers, replacing the OpenRouter-specific toggle. Supports Anthropic Messages (native) and OpenAI Chat Completions format
|
||||
- **API Format Presets**: Allow preset providers to specify API format (anthropic or openai_chat) for third-party proxy services
|
||||
- **Proxy Hint**: Display info toast when switching to OpenAI Chat format provider, reminding users to enable proxy
|
||||
- **Pricing Config Enhancement**: Per-provider cost multiplier, pricing model source (request/response), request model logging, and enriched usage UI (#781)
|
||||
- **Skills ZIP Install**: Install skills directly from local ZIP files with recursive scanning support
|
||||
- **Preferred Terminal**: Choose preferred terminal app per platform (macOS: Terminal.app/iTerm2/Alacritty/Kitty/Ghostty; Windows: cmd/PowerShell/Windows Terminal; Linux: GNOME Terminal/Konsole/Xfce4/Alacritty/Kitty/Ghostty)
|
||||
- **Silent Startup**: Option to prevent window popup on launch (#713)
|
||||
- **OpenCode Environment Check**: Version detection with Go path scanning and one-click install from GitHub Releases
|
||||
- **OpenCode Directory Sync**: Auto-sync all providers to live config on directory change with additive mode support
|
||||
- **NVIDIA NIM Preset**: New provider preset for Claude and OpenCode with nvidia.svg icon
|
||||
- **n1n.ai Preset**: New provider preset (#667)
|
||||
- **Update Badge Icon**: Replace update badge dot with ArrowUpCircle icon
|
||||
- **Linux ARM64**: CI build support for Linux ARM64 architecture
|
||||
|
||||
### Changed
|
||||
|
||||
- **API Format Migration**: Migrate api_format from settings_config to ProviderMeta to prevent polluting ~/.claude/settings.json
|
||||
- **DeepSeek max_tokens**: Remove max_tokens clamp from proxy transform layer
|
||||
- **Terminal Functions**: Consolidate redundant terminal launch functions
|
||||
- **Home Dir Utility**: Consolidate get_home_dir into single public function
|
||||
- **Kimi/Moonshot**: Upgrade provider presets to k2.5 model
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Codex 404 & Timeout**: Fix 404 errors and connection timeout with custom base_url; improve /v1 prefix handling and system proxy detection (#760)
|
||||
- **Proxy URL Building**: Fix duplicate /v1/v1 in URL; extend ?beta=true to /v1/chat/completions endpoint
|
||||
- **OpenRouter Compat Mode**: Improve backward compatibility supporting number and string types
|
||||
- **Gemini Visibility**: Correct Gemini default visibility to true (#818)
|
||||
- **Footer Layout**: Correct footer layout in advanced settings tab
|
||||
- **Claude Code Detection**: Prioritize native install path for detection
|
||||
- **Tray Menu**: Simplify title labels and optimize menu separators (#796)
|
||||
- **Duplicate Skills**: Prevent duplicate skill installation from different repos (#778)
|
||||
- **Windows Tests**: Stabilize test environment (#644)
|
||||
- **i18n**: Update apiFormatOpenAIChat label to mention proxy requirement
|
||||
- **Error Display**: Use extractErrorMessage for complete error display in mutations
|
||||
- **Sponsors**: Add AICodeMirror and reorder sponsor list
|
||||
|
||||
---
|
||||
|
||||
## [3.10.2] - 2026-01-24
|
||||
|
||||
### Patch Release
|
||||
|
||||
@@ -33,9 +33,8 @@ This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.GLM
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=9915W3"><img src="assets/partners/logos/aicodemirror.jpg" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support.
|
||||
Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for CC Switch users: register via <a href="https://www.aicodemirror.com/register?invitecode=9915W3">this link</a> to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off!</td>
|
||||
<td width="180"><a href="https://www.dmxapi.cn/register?aff=bUHu"><img src="assets/partners/logos/dmx-en.jpg" alt="DMXAPI" width="150"></a></td>
|
||||
<td>Thanks to DMXAPI for sponsoring this project! DMXAPI provides global large model API services to 200+ enterprise users. One API key for all global models. Features include: instant invoicing, unlimited concurrency, starting from $0.15, 24/7 technical support. GPT/Claude/Gemini all at 32% off, domestic models 20-50% off, Claude Code exclusive models at 66% off! <a href="https://www.dmxapi.cn/register?aff=bUHu">Register here</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -43,11 +42,6 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
|
||||
<td>Thanks to Cubence for sponsoring this project! Cubence is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more with flexible billing options including pay-as-you-go and monthly plans. Cubence provides special discounts for CC Switch users: register using <a href="https://cubence.com/signup?code=CCSWITCH&source=ccs">this link</a> and enter the "CCSWITCH" promo code during recharge to get 10% off every top-up!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.dmxapi.cn/register?aff=bUHu"><img src="assets/partners/logos/dmx-en.jpg" alt="DMXAPI" width="150"></a></td>
|
||||
<td>Thanks to DMXAPI for sponsoring this project! DMXAPI provides global large model API services to 200+ enterprise users. One API key for all global models. Features include: instant invoicing, unlimited concurrency, starting from $0.15, 24/7 technical support. GPT/Claude/Gemini all at 32% off, domestic models 20-50% off, Claude Code exclusive models at 66% off! <a href="https://www.dmxapi.cn/register?aff=bUHu">Register here</a></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## Screenshots
|
||||
|
||||
+2
-8
@@ -33,9 +33,8 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=9915W3"><img src="assets/partners/logos/aicodemirror.jpg" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>AICodeMirror のご支援に感謝します!AICodeMirror は Claude Code / Codex / Gemini CLI の公式高安定リレーサービスを提供しており、エンタープライズ級の同時接続、迅速な請求書発行、24時間年中無休の専用テクニカルサポートを備えています。
|
||||
Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / 2% / 9%、チャージ時にはさらに割引!AICodeMirror は CC Switch ユーザー向けに特別特典を用意:<a href="https://www.aicodemirror.com/register?invitecode=9915W3">このリンク</a>から登録すると初回チャージ 20% オフ、法人のお客様は最大 25% オフ!</td>
|
||||
<td width="180"><a href="https://www.dmxapi.cn/register?aff=bUHu"><img src="assets/partners/logos/dmx-en.jpg" alt="DMXAPI" width="150"></a></td>
|
||||
<td>DMXAPI のご支援に感謝します!DMXAPI は 200 社以上の企業ユーザーにグローバル大規模モデル API サービスを提供しています。1 つの API キーで全世界のモデルにアクセス可能。即時請求書発行、同時接続数無制限、最低 $0.15 から、24 時間年中無休のテクニカルサポート。GPT/Claude/Gemini が全て 32% オフ、国内モデルは 20〜50% オフ、Claude Code 専用モデルは 66% オフ実施中!<a href="https://www.dmxapi.cn/register?aff=bUHu">登録はこちら</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -43,11 +42,6 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
|
||||
<td>Cubence のご支援に感謝します!Cubence は Claude Code、Codex、Gemini などのリレーサービスを提供する信頼性の高い API 中継プラットフォームで、従量課金や月額プランなど柔軟な料金体系を提供しています。CC Switch ユーザー向けの特別割引:<a href="https://cubence.com/signup?code=CCSWITCH&source=ccs">このリンク</a>で登録し、チャージ時に「CCSWITCH」クーポンを入力すると、毎回 10% オフになります!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.dmxapi.cn/register?aff=bUHu"><img src="assets/partners/logos/dmx-en.jpg" alt="DMXAPI" width="150"></a></td>
|
||||
<td>DMXAPI のご支援に感謝します!DMXAPI は 200 社以上の企業ユーザーにグローバル大規模モデル API サービスを提供しています。1 つの API キーで全世界のモデルにアクセス可能。即時請求書発行、同時接続数無制限、最低 $0.15 から、24 時間年中無休のテクニカルサポート。GPT/Claude/Gemini が全て 32% オフ、国内モデルは 20〜50% オフ、Claude Code 専用モデルは 66% オフ実施中!<a href="https://www.dmxapi.cn/register?aff=bUHu">登録はこちら</a></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## スクリーンショット
|
||||
|
||||
+3
-10
@@ -31,11 +31,10 @@
|
||||
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
|
||||
<td>感谢 AIGoCode 赞助了本项目!AIGoCode 是一个集成了 Claude Code、Codex 以及 Gemini 最新模型的一站式平台,为你提供稳定、高效且高性价比的AI编程服务。本站提供灵活的订阅计划,零封号风险,国内直连,无需魔法,极速响应。AIGoCode 为 CC Switch 的用户提供了特别福利,通过<a href="https://aigocode.com/invite/CC-SWITCH">此链接</a>注册的用户首次充值可以获得额外10%奖励额度!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=9915W3"><img src="assets/partners/logos/aicodemirror.jpg" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定中转服务,支持企业级高并发、极速开票、7×24 专属技术支持。
|
||||
Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更有折上折!AICodeMirror 为 CCSwitch 的用户提供了特别福利,通过<a href="https://www.aicodemirror.com/register?invitecode=9915W3">此链接</a>注册的用户,可享受首充8折,企业客户最高可享 7.5 折!</td>
|
||||
<td width="180"><a href="https://www.dmxapi.cn/register?aff=bUHu"><img src="assets/partners/logos/dmx-zh.jpeg" alt="DMXAPI" width="150"></a></td>
|
||||
<td>感谢 DMXAPI(大模型API)赞助了本项目! DMXAPI,一个Key用全球大模型。
|
||||
为200多家企业用户提供全球大模型API服务。· 充值即开票 ·当天开票 ·并发不限制 ·1元起充 · 7x24 在线技术辅导,GPT/Claude/Gemini全部6.8折,国内模型5~8折,Claude Code 专属模型3.4折进行中!<a href="https://www.dmxapi.cn/register?aff=bUHu">点击这里注册</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -43,12 +42,6 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
|
||||
<td>感谢 Cubence 赞助本项目!Cubence 是一家可靠高效的 API 中继服务提供商,提供对 Claude Code、Codex、Gemini 等模型的中继服务,并提供按量、包月等灵活的计费方式。Cubence 为 CC Switch 的用户提供了特别优惠:使用 <a href="https://cubence.com/signup?code=CCSWITCH&source=ccs">此链接</a> 注册,并在充值时输入 "CCSWITCH" 优惠码,每次充值均可享受九折优惠!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.dmxapi.cn/register?aff=bUHu"><img src="assets/partners/logos/dmx-zh.jpeg" alt="DMXAPI" width="150"></a></td>
|
||||
<td>感谢 DMXAPI(大模型API)赞助了本项目! DMXAPI,一个Key用全球大模型。
|
||||
为200多家企业用户提供全球大模型API服务。· 充值即开票 ·当天开票 ·并发不限制 ·1元起充 · 7x24 在线技术辅导,GPT/Claude/Gemini全部6.8折,国内模型5~8折,Claude Code 专属模型3.4折进行中!<a href="https://www.dmxapi.cn/register?aff=bUHu">点击这里注册</a></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## 界面预览
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* 统一供应商(Universal Provider)预设配置
|
||||
*
|
||||
* 统一供应商是跨应用共享的配置,修改后会自动同步到 Claude、Codex、Gemini 三个应用。
|
||||
* 适用于 NewAPI 等支持多种协议的 API 网关。
|
||||
*/
|
||||
|
||||
import type {
|
||||
UniversalProvider,
|
||||
UniversalProviderApps,
|
||||
UniversalProviderModels,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* 统一供应商预设接口
|
||||
*/
|
||||
export interface UniversalProviderPreset {
|
||||
/** 预设名称 */
|
||||
name: string;
|
||||
/** 供应商类型标识 */
|
||||
providerType: string;
|
||||
/** 默认启用的应用 */
|
||||
defaultApps: UniversalProviderApps;
|
||||
/** 默认模型配置 */
|
||||
defaultModels: UniversalProviderModels;
|
||||
/** 网站链接 */
|
||||
websiteUrl?: string;
|
||||
/** 图标名称 */
|
||||
icon?: string;
|
||||
/** 图标颜色 */
|
||||
iconColor?: string;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** 是否为自定义模板(允许用户完全自定义) */
|
||||
isCustomTemplate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* NewAPI 默认模型配置
|
||||
*/
|
||||
const NEWAPI_DEFAULT_MODELS: UniversalProviderModels = {
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
haikuModel: "claude-haiku-4-20250514",
|
||||
sonnetModel: "claude-sonnet-4-20250514",
|
||||
opusModel: "claude-sonnet-4-20250514",
|
||||
},
|
||||
codex: {
|
||||
model: "gpt-4o",
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-pro",
|
||||
},
|
||||
};
|
||||
|
||||
const N1N_DEFAULT_MODELS: UniversalProviderModels = {
|
||||
claude: {
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
haikuModel: "claude-3-haiku-20240307",
|
||||
sonnetModel: "claude-3-5-sonnet-20240620",
|
||||
opusModel: "claude-3-opus-20240229",
|
||||
},
|
||||
codex: {
|
||||
model: "gpt-4o",
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-1.5-pro-latest",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一供应商预设列表
|
||||
*/
|
||||
export const universalProviderPresets: UniversalProviderPreset[] = [
|
||||
{
|
||||
name: "n1n.ai",
|
||||
providerType: "n1n",
|
||||
defaultApps: {
|
||||
claude: true,
|
||||
codex: true,
|
||||
gemini: true,
|
||||
},
|
||||
defaultModels: N1N_DEFAULT_MODELS,
|
||||
websiteUrl: "https://n1n.ai",
|
||||
icon: "openai",
|
||||
iconColor: "#000000",
|
||||
description:
|
||||
"n1n.ai - 聚合 OpenAI, Anthropic, Google 等主流大模型的一站式 AI 服务平台",
|
||||
},
|
||||
{
|
||||
name: "NewAPI",
|
||||
providerType: "newapi",
|
||||
defaultApps: {
|
||||
claude: true,
|
||||
codex: true,
|
||||
gemini: true,
|
||||
},
|
||||
defaultModels: NEWAPI_DEFAULT_MODELS,
|
||||
websiteUrl: "https://www.newapi.pro",
|
||||
icon: "newapi",
|
||||
iconColor: "#00A67E",
|
||||
description:
|
||||
"NewAPI 是一个可自部署的 API 网关,支持 Anthropic、OpenAI、Gemini 等多种协议",
|
||||
},
|
||||
{
|
||||
name: "自定义网关",
|
||||
providerType: "custom_gateway",
|
||||
defaultApps: {
|
||||
claude: true,
|
||||
codex: true,
|
||||
gemini: true,
|
||||
},
|
||||
defaultModels: NEWAPI_DEFAULT_MODELS,
|
||||
icon: "openai",
|
||||
iconColor: "#6366F1",
|
||||
description: "自定义配置的 API 网关",
|
||||
isCustomTemplate: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 根据预设创建统一供应商
|
||||
*/
|
||||
export function createUniversalProviderFromPreset(
|
||||
preset: UniversalProviderPreset,
|
||||
id: string,
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
customName?: string,
|
||||
): UniversalProvider {
|
||||
return {
|
||||
id,
|
||||
name: customName || preset.name,
|
||||
providerType: preset.providerType,
|
||||
apps: { ...preset.defaultApps },
|
||||
baseUrl,
|
||||
apiKey,
|
||||
models: JSON.parse(JSON.stringify(preset.defaultModels)), // Deep copy
|
||||
websiteUrl: preset.websiteUrl,
|
||||
icon: preset.icon,
|
||||
iconColor: preset.iconColor,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预设的显示名称(用于 UI)
|
||||
*/
|
||||
export function getPresetDisplayName(preset: UniversalProviderPreset): string {
|
||||
return preset.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型查找预设
|
||||
*/
|
||||
export function findPresetByType(
|
||||
providerType: string,
|
||||
): UniversalProviderPreset | undefined {
|
||||
return universalProviderPresets.find((p) => p.providerType === providerType);
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cc-switch",
|
||||
"version": "3.10.3",
|
||||
"version": "3.10.2",
|
||||
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -701,7 +701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc-switch"
|
||||
version = "3.10.3"
|
||||
version = "3.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cc-switch"
|
||||
version = "3.10.3"
|
||||
version = "3.10.2"
|
||||
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
||||
authors = ["Jason Young"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -109,17 +109,3 @@ pub async fn open_file_dialog<R: tauri::Runtime>(
|
||||
|
||||
Ok(result.map(|p| p.to_string()))
|
||||
}
|
||||
|
||||
/// 打开 ZIP 文件选择对话框
|
||||
#[tauri::command]
|
||||
pub async fn open_zip_file_dialog<R: tauri::Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let dialog = app.dialog();
|
||||
let result = dialog
|
||||
.file()
|
||||
.add_filter("ZIP", &["zip"])
|
||||
.blocking_pick_file();
|
||||
|
||||
Ok(result.map(|p| p.to_string()))
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ pub struct ToolVersion {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
||||
let tools = vec!["claude", "codex", "gemini", "opencode"];
|
||||
let tools = vec!["claude", "codex", "gemini"];
|
||||
let mut results = Vec::new();
|
||||
|
||||
// 使用全局 HTTP 客户端(已包含代理配置)
|
||||
@@ -116,7 +116,6 @@ pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
||||
"claude" => fetch_npm_latest_version(&client, "@anthropic-ai/claude-code").await,
|
||||
"codex" => fetch_npm_latest_version(&client, "@openai/codex").await,
|
||||
"gemini" => fetch_npm_latest_version(&client, "@google/gemini-cli").await,
|
||||
"opencode" => fetch_github_latest_version(&client, "anomalyco/opencode").await,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -149,29 +148,6 @@ async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Op
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to fetch latest version from GitHub releases
|
||||
async fn fetch_github_latest_version(client: &reqwest::Client, repo: &str) -> Option<String> {
|
||||
let url = format!("https://api.github.com/repos/{repo}/releases/latest");
|
||||
match client
|
||||
.get(&url)
|
||||
.header("User-Agent", "cc-switch")
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||
json.get("tag_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.strip_prefix('v').unwrap_or(s).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 预编译的版本号正则表达式
|
||||
static VERSION_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").expect("Invalid version regex"));
|
||||
@@ -248,7 +224,7 @@ fn try_get_version_wsl(tool: &str, distro: &str) -> (Option<String>, Option<Stri
|
||||
|
||||
// 防御性断言:tool 只能是预定义的值
|
||||
debug_assert!(
|
||||
["claude", "codex", "gemini", "opencode"].contains(&tool),
|
||||
["claude", "codex", "gemini"].contains(&tool),
|
||||
"unexpected tool name: {tool}"
|
||||
);
|
||||
|
||||
@@ -319,10 +295,10 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
// 常见的安装路径(原生安装优先)
|
||||
// 常见的 npm 全局安装路径
|
||||
let mut search_paths: Vec<std::path::PathBuf> = vec![
|
||||
home.join(".local/bin"), // Native install (official recommended)
|
||||
home.join(".npm-global/bin"),
|
||||
home.join(".local/bin"),
|
||||
home.join("n/bin"), // n version manager
|
||||
];
|
||||
|
||||
@@ -372,14 +348,6 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 Go 路径支持 (opencode 使用 go install 安装)
|
||||
if tool == "opencode" {
|
||||
search_paths.push(home.join("go/bin")); // go install 默认路径
|
||||
if let Ok(gopath) = std::env::var("GOPATH") {
|
||||
search_paths.push(std::path::PathBuf::from(gopath).join("bin"));
|
||||
}
|
||||
}
|
||||
|
||||
// 在每个路径中查找工具
|
||||
for path in &search_paths {
|
||||
let tool_path = if cfg!(target_os = "windows") {
|
||||
@@ -437,7 +405,6 @@ fn wsl_distro_for_tool(tool: &str) -> Option<String> {
|
||||
"claude" => crate::settings::get_claude_override_dir(),
|
||||
"codex" => crate::settings::get_codex_override_dir(),
|
||||
"gemini" => crate::settings::get_gemini_override_dir(),
|
||||
"opencode" => crate::settings::get_opencode_override_dir(),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
|
||||
@@ -249,16 +249,3 @@ pub fn remove_skill_repo(
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 从 ZIP 文件安装 Skills
|
||||
#[tauri::command]
|
||||
pub fn install_skills_from_zip(
|
||||
file_path: String,
|
||||
current_app: String,
|
||||
app_state: State<'_, AppState>,
|
||||
) -> Result<Vec<InstalledSkill>, String> {
|
||||
let app_type = parse_app_type(¤t_app)?;
|
||||
let path = std::path::Path::new(&file_path);
|
||||
|
||||
SkillService::install_from_zip(&app_state.db, path, &app_type).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -876,13 +876,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap_err();
|
||||
// AppError::localized returns AppError::Localized variant
|
||||
assert!(matches!(
|
||||
err,
|
||||
AppError::Localized {
|
||||
key: "error.invalidMultiplier",
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(err, AppError::Localized { key: "error.invalidMultiplier", .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -903,13 +897,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap_err();
|
||||
// AppError::localized returns AppError::Localized variant
|
||||
assert!(matches!(
|
||||
err,
|
||||
AppError::Localized {
|
||||
key: "error.invalidPricingMode",
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(err, AppError::Localized { key: "error.invalidPricingMode", .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -6,6 +7,14 @@ use crate::config::atomic_write;
|
||||
use crate::error::AppError;
|
||||
use crate::gemini_config::get_gemini_settings_path;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpStatus {
|
||||
pub user_config_path: String,
|
||||
pub user_config_exists: bool,
|
||||
pub server_count: usize,
|
||||
}
|
||||
|
||||
/// 获取 Gemini MCP 配置文件路径(~/.gemini/settings.json)
|
||||
fn user_config_path() -> PathBuf {
|
||||
get_gemini_settings_path()
|
||||
|
||||
@@ -850,7 +850,6 @@ pub fn run() {
|
||||
commands::import_config_from_file,
|
||||
commands::save_file_dialog,
|
||||
commands::open_file_dialog,
|
||||
commands::open_zip_file_dialog,
|
||||
commands::sync_current_providers_live,
|
||||
// Deep link import
|
||||
commands::parse_deeplink,
|
||||
@@ -880,7 +879,6 @@ pub fn run() {
|
||||
commands::get_skill_repos,
|
||||
commands::add_skill_repo,
|
||||
commands::remove_skill_repo,
|
||||
commands::install_skills_from_zip,
|
||||
// Auto launch
|
||||
commands::set_auto_launch,
|
||||
commands::get_auto_launch_status,
|
||||
|
||||
@@ -230,11 +230,6 @@ pub struct ProviderMeta {
|
||||
/// 供应商单独的代理配置
|
||||
#[serde(rename = "proxyConfig", skip_serializing_if = "Option::is_none")]
|
||||
pub proxy_config: Option<ProviderProxyConfig>,
|
||||
/// Claude API 格式(仅 Claude 供应商使用)
|
||||
/// - "anthropic": 原生 Anthropic Messages API,直接透传
|
||||
/// - "openai_chat": OpenAI Chat Completions 格式,需要转换
|
||||
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
|
||||
pub api_format: Option<String>,
|
||||
}
|
||||
|
||||
impl ProviderManager {
|
||||
@@ -446,18 +441,11 @@ impl UniversalProvider {
|
||||
.and_then(|m| m.reasoning_effort.clone())
|
||||
.unwrap_or_else(|| "high".to_string());
|
||||
|
||||
// Codex/OpenAI 的 base_url 既可能是纯 origin(需要补 /v1),也可能包含自定义前缀(不应强行补版本)
|
||||
let base_trimmed = self.base_url.trim_end_matches('/');
|
||||
let origin_only = match base_trimmed.split_once("://") {
|
||||
Some((_scheme, rest)) => !rest.contains('/'),
|
||||
None => !base_trimmed.contains('/'),
|
||||
};
|
||||
let codex_base_url = if base_trimmed.ends_with("/v1") {
|
||||
base_trimmed.to_string()
|
||||
} else if origin_only {
|
||||
format!("{base_trimmed}/v1")
|
||||
// 确保 base_url 以 /v1 结尾(Codex 使用 OpenAI 兼容 API)
|
||||
let codex_base_url = if self.base_url.ends_with("/v1") {
|
||||
self.base_url.clone()
|
||||
} else {
|
||||
base_trimmed.to_string()
|
||||
format!("{}/v1", self.base_url.trim_end_matches('/'))
|
||||
};
|
||||
|
||||
// 生成 Codex 的 config.toml 内容
|
||||
@@ -892,47 +880,4 @@ mod tests {
|
||||
assert!(config.options.headers.is_none());
|
||||
assert!(config.options.extra.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn universal_codex_provider_origin_base_url_adds_v1() {
|
||||
let mut p = UniversalProvider::new(
|
||||
"id".to_string(),
|
||||
"Test".to_string(),
|
||||
"custom".to_string(),
|
||||
"https://api.openai.com".to_string(),
|
||||
"sk-test".to_string(),
|
||||
);
|
||||
p.apps.codex = true;
|
||||
|
||||
let provider = p.to_codex_provider().expect("should build codex provider");
|
||||
let toml = provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("config should be a toml string");
|
||||
|
||||
assert!(toml.contains("base_url = \"https://api.openai.com/v1\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn universal_codex_provider_custom_prefix_does_not_force_v1() {
|
||||
let mut p = UniversalProvider::new(
|
||||
"id".to_string(),
|
||||
"Test".to_string(),
|
||||
"custom".to_string(),
|
||||
"https://example.com/openai".to_string(),
|
||||
"sk-test".to_string(),
|
||||
);
|
||||
p.apps.codex = true;
|
||||
|
||||
let provider = p.to_codex_provider().expect("should build codex provider");
|
||||
let toml = provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("config should be a toml string");
|
||||
|
||||
assert!(toml.contains("base_url = \"https://example.com/openai\""));
|
||||
assert!(!toml.contains("https://example.com/openai/v1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,11 +666,7 @@ impl RequestForwarder {
|
||||
|
||||
// 输出请求信息日志
|
||||
let tag = adapter.name();
|
||||
let request_model = filtered_body
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("<none>");
|
||||
log::info!("[{tag}] >>> 请求 URL: {url} (model={request_model})");
|
||||
log::debug!("[{tag}] >>> 请求 URL: {url}");
|
||||
if let Ok(body_str) = serde_json::to_string(&filtered_body) {
|
||||
log::debug!(
|
||||
"[{tag}] >>> 请求体内容 ({}字节): {}",
|
||||
|
||||
@@ -284,7 +284,7 @@ pub async fn handle_chat_completions(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Codex,
|
||||
"/chat/completions",
|
||||
"/v1/chat/completions",
|
||||
body,
|
||||
headers,
|
||||
ctx.get_providers(),
|
||||
@@ -325,7 +325,7 @@ pub async fn handle_responses(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Codex,
|
||||
"/responses",
|
||||
"/v1/responses",
|
||||
body,
|
||||
headers,
|
||||
ctx.get_providers(),
|
||||
|
||||
@@ -17,33 +17,6 @@ static GLOBAL_CLIENT: OnceCell<RwLock<Client>> = OnceCell::new();
|
||||
/// 当前代理 URL(用于日志和状态查询)
|
||||
static CURRENT_PROXY_URL: OnceCell<RwLock<Option<String>>> = OnceCell::new();
|
||||
|
||||
/// CC Switch 代理服务器当前监听的端口
|
||||
static CC_SWITCH_PROXY_PORT: OnceCell<RwLock<u16>> = OnceCell::new();
|
||||
|
||||
/// 设置 CC Switch 代理服务器的监听端口
|
||||
///
|
||||
/// 应在代理服务器启动时调用,以便系统代理检测能正确识别自己的端口
|
||||
pub fn set_proxy_port(port: u16) {
|
||||
if let Some(lock) = CC_SWITCH_PROXY_PORT.get() {
|
||||
if let Ok(mut current_port) = lock.write() {
|
||||
*current_port = port;
|
||||
log::debug!("[GlobalProxy] Updated CC Switch proxy port to {port}");
|
||||
}
|
||||
} else {
|
||||
let _ = CC_SWITCH_PROXY_PORT.set(RwLock::new(port));
|
||||
log::debug!("[GlobalProxy] Initialized CC Switch proxy port to {port}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 CC Switch 代理服务器的监听端口
|
||||
fn get_proxy_port() -> u16 {
|
||||
CC_SWITCH_PROXY_PORT
|
||||
.get()
|
||||
.and_then(|lock| lock.read().ok())
|
||||
.map(|port| *port)
|
||||
.unwrap_or(15721) // 默认端口作为回退
|
||||
}
|
||||
|
||||
/// 初始化全局 HTTP 客户端
|
||||
///
|
||||
/// 应在应用启动时调用一次。
|
||||
@@ -285,17 +258,9 @@ fn proxy_points_to_loopback(value: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// 检查是否指向 CC Switch 自己的代理端口
|
||||
// 只有指向自己的代理才需要跳过,避免递归
|
||||
fn is_cc_switch_proxy_port(port: Option<u16>) -> bool {
|
||||
let cc_switch_port = get_proxy_port();
|
||||
port == Some(cc_switch_port)
|
||||
}
|
||||
|
||||
if let Ok(parsed) = url::Url::parse(value) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
// 只有当主机是 loopback 且端口是 CC Switch 的端口时才返回 true
|
||||
return host_is_loopback(host) && is_cc_switch_proxy_port(parsed.port());
|
||||
return host_is_loopback(host);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -303,7 +268,7 @@ fn proxy_points_to_loopback(value: &str) -> bool {
|
||||
let with_scheme = format!("http://{value}");
|
||||
if let Ok(parsed) = url::Url::parse(&with_scheme) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
return host_is_loopback(host) && is_cc_switch_proxy_port(parsed.port());
|
||||
return host_is_loopback(host);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,30 +448,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_proxy_points_to_loopback() {
|
||||
// 设置 CC Switch 代理端口为 15721(默认值)
|
||||
set_proxy_port(15721);
|
||||
|
||||
// 只有指向 CC Switch 自己端口的 loopback 地址才返回 true
|
||||
assert!(proxy_points_to_loopback("http://127.0.0.1:15721"));
|
||||
assert!(proxy_points_to_loopback("socks5://localhost:15721"));
|
||||
assert!(proxy_points_to_loopback("127.0.0.1:15721"));
|
||||
|
||||
// 其他 loopback 端口不应该被跳过(允许使用其他本地代理工具)
|
||||
assert!(!proxy_points_to_loopback("http://127.0.0.1:7890"));
|
||||
assert!(!proxy_points_to_loopback("socks5://localhost:1080"));
|
||||
|
||||
// 非 loopback 地址不应该被跳过
|
||||
assert!(proxy_points_to_loopback("http://127.0.0.1:7890"));
|
||||
assert!(proxy_points_to_loopback("socks5://localhost:1080"));
|
||||
assert!(proxy_points_to_loopback("127.0.0.1:7890"));
|
||||
assert!(!proxy_points_to_loopback("http://192.168.1.10:7890"));
|
||||
assert!(!proxy_points_to_loopback("http://192.168.1.10:15721"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_proxy_points_to_loopback() {
|
||||
let _guard = env_lock().lock().unwrap();
|
||||
|
||||
// 设置 CC Switch 代理端口
|
||||
set_proxy_port(15721);
|
||||
|
||||
let keys = [
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
@@ -520,15 +471,9 @@ mod tests {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
|
||||
// 指向 CC Switch 端口的代理应该被跳过
|
||||
std::env::set_var("HTTP_PROXY", "http://127.0.0.1:15721");
|
||||
std::env::set_var("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||
assert!(system_proxy_points_to_loopback());
|
||||
|
||||
// 指向其他端口的本地代理不应该被跳过
|
||||
std::env::set_var("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||
assert!(!system_proxy_points_to_loopback());
|
||||
|
||||
// 非 loopback 地址不应该被跳过
|
||||
std::env::set_var("HTTP_PROXY", "http://10.0.0.2:7890");
|
||||
assert!(!system_proxy_points_to_loopback());
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
//! Claude (Anthropic) Provider Adapter
|
||||
//!
|
||||
//! 支持透传模式和 OpenAI Chat Completions 格式转换模式
|
||||
//!
|
||||
//! ## API 格式
|
||||
//! - **anthropic** (默认): Anthropic Messages API 格式,直接透传
|
||||
//! - **openai_chat**: OpenAI Chat Completions 格式,需要 Anthropic ↔ OpenAI 转换
|
||||
//! 支持透传模式和 OpenRouter 兼容模式
|
||||
//!
|
||||
//! ## 认证模式
|
||||
//! - **Claude**: Anthropic 官方 API (x-api-key + anthropic-version)
|
||||
//! - **ClaudeAuth**: 中转服务 (仅 Bearer 认证,无 x-api-key)
|
||||
//! - **OpenRouter**: 已支持 Claude Code 兼容接口,默认透传
|
||||
//! - **OpenRouter**: 已支持 Claude Code 兼容接口,默认透传(保留旧转换逻辑备用)
|
||||
|
||||
use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType};
|
||||
use crate::provider::Provider;
|
||||
@@ -52,52 +48,22 @@ impl ClaudeAdapter {
|
||||
false
|
||||
}
|
||||
|
||||
/// 获取 API 格式
|
||||
///
|
||||
/// 从 provider.meta.api_format 读取格式设置:
|
||||
/// - "anthropic" (默认): Anthropic Messages API 格式,直接透传
|
||||
/// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||
fn get_api_format(&self, provider: &Provider) -> &'static str {
|
||||
// 1) Preferred: meta.apiFormat (SSOT, never written to Claude Code config)
|
||||
if let Some(meta) = provider.meta.as_ref() {
|
||||
if let Some(api_format) = meta.api_format.as_deref() {
|
||||
return if api_format == "openai_chat" {
|
||||
"openai_chat"
|
||||
} else {
|
||||
"anthropic"
|
||||
};
|
||||
}
|
||||
/// 检测 OpenRouter 是否启用兼容模式
|
||||
fn is_openrouter_compat_enabled(&self, provider: &Provider) -> bool {
|
||||
if !self.is_openrouter(provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) Backward compatibility: legacy settings_config.api_format
|
||||
if let Some(api_format) = provider
|
||||
.settings_config
|
||||
.get("api_format")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
return if api_format == "openai_chat" {
|
||||
"openai_chat"
|
||||
} else {
|
||||
"anthropic"
|
||||
};
|
||||
}
|
||||
|
||||
// 3) Backward compatibility: legacy openrouter_compat_mode (bool/number/string)
|
||||
let raw = provider.settings_config.get("openrouter_compat_mode");
|
||||
let enabled = match raw {
|
||||
Some(serde_json::Value::Bool(v)) => *v,
|
||||
match raw {
|
||||
Some(serde_json::Value::Bool(enabled)) => *enabled,
|
||||
Some(serde_json::Value::Number(num)) => num.as_i64().unwrap_or(0) != 0,
|
||||
Some(serde_json::Value::String(value)) => {
|
||||
let normalized = value.trim().to_lowercase();
|
||||
normalized == "true" || normalized == "1"
|
||||
}
|
||||
// OpenRouter now supports Claude Code compatible API, default to passthrough
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if enabled {
|
||||
"openai_chat"
|
||||
} else {
|
||||
"anthropic"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,23 +218,15 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
// 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。
|
||||
// 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。
|
||||
|
||||
let mut base = format!(
|
||||
let base = format!(
|
||||
"{}/{}",
|
||||
base_url.trim_end_matches('/'),
|
||||
endpoint.trim_start_matches('/')
|
||||
);
|
||||
|
||||
// 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致)
|
||||
while base.contains("/v1/v1") {
|
||||
base = base.replace("/v1/v1", "/v1");
|
||||
}
|
||||
|
||||
// 为 Claude 相关端点添加 ?beta=true 参数
|
||||
// 为 /v1/messages 端点添加 ?beta=true 参数
|
||||
// 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数
|
||||
// 注:openai_chat 模式下会转发到 /v1/chat/completions,此处也需要保持一致
|
||||
if (endpoint.contains("/v1/messages") || endpoint.contains("/v1/chat/completions"))
|
||||
&& !endpoint.contains('?')
|
||||
{
|
||||
if endpoint.contains("/v1/messages") && !endpoint.contains("?") {
|
||||
format!("{base}?beta=true")
|
||||
} else {
|
||||
base
|
||||
@@ -295,19 +253,21 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_transform(&self, provider: &Provider) -> bool {
|
||||
// 根据 api_format 配置决定是否需要格式转换
|
||||
// - "anthropic" (默认): 直接透传,无需转换
|
||||
// - "openai_chat": 需要 Anthropic ↔ OpenAI 格式转换
|
||||
self.get_api_format(provider) == "openai_chat"
|
||||
fn needs_transform(&self, _provider: &Provider) -> bool {
|
||||
// NOTE:
|
||||
// OpenRouter 已推出 Claude Code 兼容接口(可直接处理 `/v1/messages`),默认不再启用
|
||||
// Anthropic ↔ OpenAI 的格式转换。
|
||||
//
|
||||
// 如果未来需要回退到旧的 OpenAI Chat Completions 方案,可恢复下面这行:
|
||||
self.is_openrouter_compat_enabled(_provider)
|
||||
}
|
||||
|
||||
fn transform_request(
|
||||
&self,
|
||||
body: serde_json::Value,
|
||||
_provider: &Provider,
|
||||
provider: &Provider,
|
||||
) -> Result<serde_json::Value, ProxyError> {
|
||||
super::transform::anthropic_to_openai(body)
|
||||
super::transform::anthropic_to_openai(body, provider)
|
||||
}
|
||||
|
||||
fn transform_response(&self, body: serde_json::Value) -> Result<serde_json::Value, ProxyError> {
|
||||
@@ -318,7 +278,6 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::provider::ProviderMeta;
|
||||
use serde_json::json;
|
||||
|
||||
fn create_provider(config: serde_json::Value) -> Provider {
|
||||
@@ -338,23 +297,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_provider_with_meta(config: serde_json::Value, meta: ProviderMeta) -> Provider {
|
||||
Provider {
|
||||
id: "test".to_string(),
|
||||
name: "Test Claude".to_string(),
|
||||
settings_config: config,
|
||||
website_url: None,
|
||||
category: Some("claude".to_string()),
|
||||
created_at: None,
|
||||
sort_index: None,
|
||||
notes: None,
|
||||
meta: Some(meta),
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_base_url_from_env() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
@@ -517,7 +459,6 @@ mod tests {
|
||||
fn test_needs_transform() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
// Default: no transform (anthropic format) - no meta
|
||||
let anthropic_provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
|
||||
@@ -525,96 +466,29 @@ mod tests {
|
||||
}));
|
||||
assert!(!adapter.needs_transform(&anthropic_provider));
|
||||
|
||||
// Explicit anthropic format in meta: no transform
|
||||
let explicit_anthropic = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("anthropic".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(!adapter.needs_transform(&explicit_anthropic));
|
||||
|
||||
// Legacy settings_config.api_format: openai_chat should enable transform
|
||||
let legacy_settings_api_format = create_provider(json!({
|
||||
// OpenRouter provider without explicit setting now defaults to passthrough (no transform)
|
||||
let openrouter_provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
},
|
||||
"api_format": "openai_chat"
|
||||
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api"
|
||||
}
|
||||
}));
|
||||
assert!(adapter.needs_transform(&legacy_settings_api_format));
|
||||
assert!(!adapter.needs_transform(&openrouter_provider));
|
||||
|
||||
// Legacy openrouter_compat_mode: bool/number/string should enable transform
|
||||
let legacy_openrouter_bool = create_provider(json!({
|
||||
// OpenRouter provider with explicit compat mode enabled should transform
|
||||
let openrouter_enabled = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api"
|
||||
},
|
||||
"openrouter_compat_mode": true
|
||||
}));
|
||||
assert!(adapter.needs_transform(&legacy_openrouter_bool));
|
||||
assert!(adapter.needs_transform(&openrouter_enabled));
|
||||
|
||||
let legacy_openrouter_num = create_provider(json!({
|
||||
let openrouter_disabled = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api"
|
||||
},
|
||||
"openrouter_compat_mode": 1
|
||||
"openrouter_compat_mode": false
|
||||
}));
|
||||
assert!(adapter.needs_transform(&legacy_openrouter_num));
|
||||
|
||||
let legacy_openrouter_str = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
},
|
||||
"openrouter_compat_mode": "true"
|
||||
}));
|
||||
assert!(adapter.needs_transform(&legacy_openrouter_str));
|
||||
|
||||
// OpenAI Chat format in meta: needs transform
|
||||
let openai_chat_provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("openai_chat".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(adapter.needs_transform(&openai_chat_provider));
|
||||
|
||||
// meta takes precedence over legacy settings_config fields
|
||||
let meta_precedence_over_settings = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
},
|
||||
"api_format": "openai_chat",
|
||||
"openrouter_compat_mode": true
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("anthropic".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(!adapter.needs_transform(&meta_precedence_over_settings));
|
||||
|
||||
// Unknown format in meta: default to anthropic (no transform)
|
||||
let unknown_format = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("unknown".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(!adapter.needs_transform(&unknown_format));
|
||||
assert!(!adapter.needs_transform(&openrouter_disabled));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,33 +141,10 @@ impl ProviderAdapter for CodexAdapter {
|
||||
let base_trimmed = base_url.trim_end_matches('/');
|
||||
let endpoint_trimmed = endpoint.trim_start_matches('/');
|
||||
|
||||
// OpenAI/Codex 的 base_url 可能是:
|
||||
// - 纯 origin: https://api.openai.com (需要自动补 /v1)
|
||||
// - 已含 /v1: https://api.openai.com/v1 (直接拼接)
|
||||
// - 自定义前缀: https://xxx/openai (不添加 /v1,直接拼接)
|
||||
let mut url = format!("{base_trimmed}/{endpoint_trimmed}");
|
||||
|
||||
// 检查 base_url 是否已经包含 /v1
|
||||
let already_has_v1 = base_trimmed.ends_with("/v1");
|
||||
|
||||
// 检查是否是纯 origin(没有路径部分)
|
||||
let origin_only = match base_trimmed.split_once("://") {
|
||||
Some((_scheme, rest)) => !rest.contains('/'),
|
||||
None => !base_trimmed.contains('/'),
|
||||
};
|
||||
|
||||
let mut url = if already_has_v1 {
|
||||
// 已经有 /v1,直接拼接
|
||||
format!("{base_trimmed}/{endpoint_trimmed}")
|
||||
} else if origin_only {
|
||||
// 纯 origin,添加 /v1
|
||||
format!("{base_trimmed}/v1/{endpoint_trimmed}")
|
||||
} else {
|
||||
// 自定义前缀,不添加 /v1,直接拼接
|
||||
format!("{base_trimmed}/{endpoint_trimmed}")
|
||||
};
|
||||
|
||||
// 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致)
|
||||
while url.contains("/v1/v1") {
|
||||
// 去除重复的 /v1/v1
|
||||
if url.contains("/v1/v1") {
|
||||
url = url.replace("/v1/v1", "/v1");
|
||||
}
|
||||
|
||||
@@ -246,20 +223,6 @@ mod tests {
|
||||
assert_eq!(url, "https://api.openai.com/v1/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_origin_adds_v1() {
|
||||
let adapter = CodexAdapter::new();
|
||||
let url = adapter.build_url("https://api.openai.com", "/responses");
|
||||
assert_eq!(url, "https://api.openai.com/v1/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_custom_prefix_no_v1() {
|
||||
let adapter = CodexAdapter::new();
|
||||
let url = adapter.build_url("https://example.com/openai", "/responses");
|
||||
assert_eq!(url, "https://example.com/openai/responses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_dedup_v1() {
|
||||
let adapter = CodexAdapter::new();
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//!
|
||||
//! 用于 Anthropic Messages API 的请求/响应格式转换
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//!
|
||||
//! 用于 OpenAI Chat Completions API 的请求/响应格式转换
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -3,16 +3,77 @@
|
||||
//! 实现 Anthropic ↔ OpenAI 格式转换,用于 OpenRouter 支持
|
||||
//! 参考: anthropic-proxy-rs
|
||||
|
||||
use crate::provider::Provider;
|
||||
use crate::proxy::error::ProxyError;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 从 Provider 配置中获取模型映射
|
||||
fn get_model_from_provider(model: &str, provider: &Provider, body: &Value) -> String {
|
||||
let env = provider.settings_config.get("env");
|
||||
let model_lower = model.to_lowercase();
|
||||
|
||||
// 检测 thinking 参数
|
||||
let has_thinking = body
|
||||
.get("thinking")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|o| o.get("type"))
|
||||
.and_then(|t| t.as_str())
|
||||
== Some("enabled");
|
||||
|
||||
if let Some(env) = env {
|
||||
// 如果启用 thinking,优先使用推理模型
|
||||
if has_thinking {
|
||||
if let Some(m) = env
|
||||
.get("ANTHROPIC_REASONING_MODEL")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
log::debug!("[Transform] 使用推理模型: {m}");
|
||||
return m.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 根据模型类型选择配置模型
|
||||
if model_lower.contains("haiku") {
|
||||
if let Some(m) = env
|
||||
.get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
return m.to_string();
|
||||
}
|
||||
}
|
||||
if model_lower.contains("opus") {
|
||||
if let Some(m) = env
|
||||
.get("ANTHROPIC_DEFAULT_OPUS_MODEL")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
return m.to_string();
|
||||
}
|
||||
}
|
||||
if model_lower.contains("sonnet") {
|
||||
if let Some(m) = env
|
||||
.get("ANTHROPIC_DEFAULT_SONNET_MODEL")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
return m.to_string();
|
||||
}
|
||||
}
|
||||
// 默认使用 ANTHROPIC_MODEL
|
||||
if let Some(m) = env.get("ANTHROPIC_MODEL").and_then(|v| v.as_str()) {
|
||||
return m.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
model.to_string()
|
||||
}
|
||||
|
||||
/// Anthropic 请求 → OpenAI 请求
|
||||
pub fn anthropic_to_openai(body: Value) -> Result<Value, ProxyError> {
|
||||
pub fn anthropic_to_openai(body: Value, provider: &Provider) -> Result<Value, ProxyError> {
|
||||
let mut result = json!({});
|
||||
|
||||
// NOTE: 模型映射由上游统一处理(proxy::model_mapper),格式转换层只做结构转换。
|
||||
// 模型映射:使用 Provider 配置中的模型(支持 thinking 参数)
|
||||
if let Some(model) = body.get("model").and_then(|m| m.as_str()) {
|
||||
result["model"] = json!(model);
|
||||
let mapped_model = get_model_from_provider(model, provider, &body);
|
||||
result["model"] = json!(mapped_model);
|
||||
}
|
||||
|
||||
let mut messages = Vec::new();
|
||||
@@ -320,16 +381,45 @@ pub fn openai_to_anthropic(body: Value) -> Result<Value, ProxyError> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_provider(env_config: Value) -> Provider {
|
||||
Provider {
|
||||
id: "test".to_string(),
|
||||
name: "Test Provider".to_string(),
|
||||
settings_config: json!({"env": env_config}),
|
||||
website_url: None,
|
||||
category: None,
|
||||
created_at: None,
|
||||
sort_index: None,
|
||||
notes: None,
|
||||
meta: None,
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_openrouter_provider() -> Provider {
|
||||
create_provider(json!({
|
||||
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api",
|
||||
"ANTHROPIC_MODEL": "anthropic/claude-sonnet-4.5",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "anthropic/claude-haiku-4.5",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "anthropic/claude-sonnet-4.5",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "anthropic/claude-opus-4.5"
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_openai_simple() {
|
||||
let provider = create_openrouter_provider();
|
||||
let input = json!({
|
||||
"model": "claude-3-opus",
|
||||
"max_tokens": 1024,
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input).unwrap();
|
||||
assert_eq!(result["model"], "claude-3-opus");
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
// opus 模型映射到配置的 ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
assert_eq!(result["model"], "anthropic/claude-opus-4.5");
|
||||
assert_eq!(result["max_tokens"], 1024);
|
||||
assert_eq!(result["messages"][0]["role"], "user");
|
||||
assert_eq!(result["messages"][0]["content"], "Hello");
|
||||
@@ -337,6 +427,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_openai_with_system() {
|
||||
let provider = create_openrouter_provider();
|
||||
let input = json!({
|
||||
"model": "claude-3-sonnet",
|
||||
"max_tokens": 1024,
|
||||
@@ -344,7 +435,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input).unwrap();
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
assert_eq!(result["messages"][0]["role"], "system");
|
||||
assert_eq!(
|
||||
result["messages"][0]["content"],
|
||||
@@ -355,6 +446,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_openai_with_tools() {
|
||||
let provider = create_openrouter_provider();
|
||||
let input = json!({
|
||||
"model": "claude-3-opus",
|
||||
"max_tokens": 1024,
|
||||
@@ -366,13 +458,14 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input).unwrap();
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
assert_eq!(result["tools"][0]["type"], "function");
|
||||
assert_eq!(result["tools"][0]["function"]["name"], "get_weather");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_openai_tool_use() {
|
||||
let provider = create_openrouter_provider();
|
||||
let input = json!({
|
||||
"model": "claude-3-opus",
|
||||
"max_tokens": 1024,
|
||||
@@ -385,7 +478,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input).unwrap();
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
let msg = &result["messages"][0];
|
||||
assert_eq!(msg["role"], "assistant");
|
||||
assert!(msg.get("tool_calls").is_some());
|
||||
@@ -394,6 +487,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_openai_tool_result() {
|
||||
let provider = create_openrouter_provider();
|
||||
let input = json!({
|
||||
"model": "claude-3-opus",
|
||||
"max_tokens": 1024,
|
||||
@@ -405,7 +499,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input).unwrap();
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
let msg = &result["messages"][0];
|
||||
assert_eq!(msg["role"], "tool");
|
||||
assert_eq!(msg["tool_call_id"], "call_123");
|
||||
@@ -469,15 +563,78 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_passthrough() {
|
||||
// 格式转换层只做结构转换,模型映射由上游 proxy::model_mapper 处理
|
||||
fn test_model_mapping_from_provider() {
|
||||
let provider = create_openrouter_provider();
|
||||
let body = json!({"model": "test"});
|
||||
|
||||
// sonnet 模型
|
||||
assert_eq!(
|
||||
get_model_from_provider("claude-sonnet-4-5-20250929", &provider, &body),
|
||||
"anthropic/claude-sonnet-4.5"
|
||||
);
|
||||
|
||||
// haiku 模型
|
||||
assert_eq!(
|
||||
get_model_from_provider("claude-haiku-4-5-20250929", &provider, &body),
|
||||
"anthropic/claude-haiku-4.5"
|
||||
);
|
||||
|
||||
// opus 模型
|
||||
assert_eq!(
|
||||
get_model_from_provider("claude-opus-4-5", &provider, &body),
|
||||
"anthropic/claude-opus-4.5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_openai_model_mapping() {
|
||||
let provider = create_openrouter_provider();
|
||||
let input = json!({
|
||||
"model": "gpt-4o",
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 1024,
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input).unwrap();
|
||||
assert_eq!(result["model"], "gpt-4o");
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
assert_eq!(result["model"], "anthropic/claude-sonnet-4.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_parameter_detection() {
|
||||
let mut provider = create_openrouter_provider();
|
||||
// 添加推理模型配置
|
||||
if let Some(env) = provider.settings_config.get_mut("env") {
|
||||
env["ANTHROPIC_REASONING_MODEL"] = json!("anthropic/claude-sonnet-4.5:extended");
|
||||
}
|
||||
|
||||
let input = json!({
|
||||
"model": "claude-sonnet-4-5",
|
||||
"max_tokens": 1024,
|
||||
"thinking": {"type": "enabled"},
|
||||
"messages": [{"role": "user", "content": "Solve this problem"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
// 应该使用推理模型
|
||||
assert_eq!(result["model"], "anthropic/claude-sonnet-4.5:extended");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_parameter_disabled() {
|
||||
let mut provider = create_openrouter_provider();
|
||||
if let Some(env) = provider.settings_config.get_mut("env") {
|
||||
env["ANTHROPIC_REASONING_MODEL"] = json!("anthropic/claude-sonnet-4.5:extended");
|
||||
}
|
||||
|
||||
let input = json!({
|
||||
"model": "claude-sonnet-4-5",
|
||||
"max_tokens": 1024,
|
||||
"thinking": {"type": "disabled"},
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_openai(input, &provider).unwrap();
|
||||
// 应该使用普通模型
|
||||
assert_eq!(result["model"], "anthropic/claude-sonnet-4.5");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,6 @@ impl ProxyServer {
|
||||
|
||||
log::info!("[{}] 代理服务器启动于 {addr}", log_srv::STARTED);
|
||||
|
||||
// 更新全局代理端口,用于系统代理检测
|
||||
crate::proxy::http_client::set_proxy_port(self.config.listen_port);
|
||||
|
||||
// 保存关闭句柄
|
||||
*self.shutdown_tx.write().await = Some(shutdown_tx);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::provider::{sanitize_claude_settings_for_live, ProviderService};
|
||||
use super::provider::ProviderService;
|
||||
use crate::app_config::{AppType, MultiAppConfig};
|
||||
use crate::error::AppError;
|
||||
use crate::provider::Provider;
|
||||
@@ -181,8 +181,7 @@ impl ConfigService {
|
||||
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||||
}
|
||||
|
||||
let settings = sanitize_claude_settings_for_live(&provider.settings_config);
|
||||
write_json_file(&settings_path, &settings)?;
|
||||
write_json_file(&settings_path, &provider.settings_config)?;
|
||||
|
||||
let live_after = read_json_file::<serde_json::Value>(&settings_path)?;
|
||||
if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
|
||||
|
||||
@@ -19,18 +19,6 @@ use super::gemini_auth::{
|
||||
};
|
||||
use super::normalize_claude_models_in_value;
|
||||
|
||||
pub(crate) fn sanitize_claude_settings_for_live(settings: &Value) -> Value {
|
||||
let mut v = settings.clone();
|
||||
if let Some(obj) = v.as_object_mut() {
|
||||
// Internal-only fields - never write to Claude Code settings.json
|
||||
obj.remove("api_format");
|
||||
obj.remove("apiFormat");
|
||||
obj.remove("openrouter_compat_mode");
|
||||
obj.remove("openrouterCompatMode");
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Live configuration snapshot for backup/restore
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
@@ -109,8 +97,7 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
let path = get_claude_settings_path();
|
||||
let settings = sanitize_claude_settings_for_live(&provider.settings_config);
|
||||
write_json_file(&path, &settings)?;
|
||||
write_json_file(&path, &provider.settings_config)?;
|
||||
}
|
||||
AppType::Codex => {
|
||||
let obj = provider
|
||||
|
||||
@@ -26,7 +26,6 @@ pub use live::{
|
||||
};
|
||||
|
||||
// Internal re-exports (pub(crate))
|
||||
pub(crate) use live::sanitize_claude_settings_for_live;
|
||||
pub(crate) use live::write_live_snapshot;
|
||||
|
||||
// Internal re-exports
|
||||
|
||||
@@ -1654,8 +1654,7 @@ impl ProxyService {
|
||||
|
||||
fn write_claude_live(&self, config: &Value) -> Result<(), String> {
|
||||
let path = get_claude_settings_path();
|
||||
let settings = crate::services::provider::sanitize_claude_settings_for_live(config);
|
||||
write_json_file(&path, &settings).map_err(|e| format!("写入 Claude 配置失败: {e}"))
|
||||
write_json_file(&path, config).map_err(|e| format!("写入 Claude 配置失败: {e}"))
|
||||
}
|
||||
|
||||
fn read_codex_live(&self) -> Result<Value, String> {
|
||||
|
||||
@@ -1110,193 +1110,6 @@ impl SkillService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========== 从 ZIP 文件安装 ==========
|
||||
|
||||
/// 从本地 ZIP 文件安装 Skills
|
||||
///
|
||||
/// 流程:
|
||||
/// 1. 解压 ZIP 到临时目录
|
||||
/// 2. 扫描目录查找包含 SKILL.md 的技能
|
||||
/// 3. 复制到 SSOT 并保存到数据库
|
||||
/// 4. 同步到当前应用目录
|
||||
pub fn install_from_zip(
|
||||
db: &Arc<Database>,
|
||||
zip_path: &Path,
|
||||
current_app: &AppType,
|
||||
) -> Result<Vec<InstalledSkill>> {
|
||||
// 解压到临时目录
|
||||
let temp_dir = Self::extract_local_zip(zip_path)?;
|
||||
|
||||
// 扫描所有包含 SKILL.md 的目录
|
||||
let skill_dirs = Self::scan_skills_in_dir(&temp_dir)?;
|
||||
|
||||
if skill_dirs.is_empty() {
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
return Err(anyhow!(format_skill_error(
|
||||
"NO_SKILLS_IN_ZIP",
|
||||
&[],
|
||||
Some("checkZipContent"),
|
||||
)));
|
||||
}
|
||||
|
||||
let ssot_dir = Self::get_ssot_dir()?;
|
||||
let mut installed = Vec::new();
|
||||
let existing_skills = db.get_all_installed_skills()?;
|
||||
|
||||
for skill_dir in skill_dirs {
|
||||
// 获取目录名称作为安装名
|
||||
let install_name = skill_dir
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// 检查是否已有同名 directory 的 skill
|
||||
let conflict = existing_skills
|
||||
.values()
|
||||
.find(|s| s.directory.eq_ignore_ascii_case(&install_name));
|
||||
|
||||
if let Some(existing) = conflict {
|
||||
log::warn!(
|
||||
"Skill directory '{}' already exists (from {}), skipping",
|
||||
install_name,
|
||||
existing.id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析元数据
|
||||
let skill_md = skill_dir.join("SKILL.md");
|
||||
let (name, description) = if skill_md.exists() {
|
||||
match Self::parse_skill_metadata_static(&skill_md) {
|
||||
Ok(meta) => (
|
||||
meta.name.unwrap_or_else(|| install_name.clone()),
|
||||
meta.description,
|
||||
),
|
||||
Err(_) => (install_name.clone(), None),
|
||||
}
|
||||
} else {
|
||||
(install_name.clone(), None)
|
||||
};
|
||||
|
||||
// 复制到 SSOT
|
||||
let dest = ssot_dir.join(&install_name);
|
||||
if dest.exists() {
|
||||
let _ = fs::remove_dir_all(&dest);
|
||||
}
|
||||
Self::copy_dir_recursive(&skill_dir, &dest)?;
|
||||
|
||||
// 创建 InstalledSkill 记录
|
||||
let skill = InstalledSkill {
|
||||
id: format!("local:{install_name}"),
|
||||
name,
|
||||
description,
|
||||
directory: install_name.clone(),
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
repo_branch: None,
|
||||
readme_url: None,
|
||||
apps: SkillApps::only(current_app),
|
||||
installed_at: chrono::Utc::now().timestamp(),
|
||||
};
|
||||
|
||||
// 保存到数据库
|
||||
db.save_skill(&skill)?;
|
||||
|
||||
// 同步到当前应用目录
|
||||
Self::sync_to_app_dir(&install_name, current_app)?;
|
||||
|
||||
log::info!(
|
||||
"Skill {} installed from ZIP, enabled for {:?}",
|
||||
skill.name,
|
||||
current_app
|
||||
);
|
||||
installed.push(skill);
|
||||
}
|
||||
|
||||
// 清理临时目录
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
Ok(installed)
|
||||
}
|
||||
|
||||
/// 解压本地 ZIP 文件到临时目录
|
||||
fn extract_local_zip(zip_path: &Path) -> Result<PathBuf> {
|
||||
let file = fs::File::open(zip_path)
|
||||
.with_context(|| format!("Failed to open ZIP file: {}", zip_path.display()))?;
|
||||
|
||||
let mut archive = zip::ZipArchive::new(file)
|
||||
.with_context(|| format!("Failed to read ZIP file: {}", zip_path.display()))?;
|
||||
|
||||
if archive.is_empty() {
|
||||
return Err(anyhow!(format_skill_error(
|
||||
"EMPTY_ARCHIVE",
|
||||
&[],
|
||||
Some("checkZipContent"),
|
||||
)));
|
||||
}
|
||||
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let temp_path = temp_dir.path().to_path_buf();
|
||||
let _ = temp_dir.keep(); // Keep the directory, we'll clean up later
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let file_path = match file.enclosed_name() {
|
||||
Some(path) => path.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let outpath = temp_path.join(&file_path);
|
||||
|
||||
if file.is_dir() {
|
||||
fs::create_dir_all(&outpath)?;
|
||||
} else {
|
||||
if let Some(parent) = outpath.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut outfile = fs::File::create(&outpath)?;
|
||||
std::io::copy(&mut file, &mut outfile)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(temp_path)
|
||||
}
|
||||
|
||||
/// 递归扫描目录查找包含 SKILL.md 的技能目录
|
||||
fn scan_skills_in_dir(dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut skill_dirs = Vec::new();
|
||||
Self::scan_skills_recursive(dir, &mut skill_dirs)?;
|
||||
Ok(skill_dirs)
|
||||
}
|
||||
|
||||
/// 递归扫描辅助函数
|
||||
fn scan_skills_recursive(current: &Path, results: &mut Vec<PathBuf>) -> Result<()> {
|
||||
// 检查当前目录是否包含 SKILL.md
|
||||
let skill_md = current.join("SKILL.md");
|
||||
if skill_md.exists() {
|
||||
results.push(current.to_path_buf());
|
||||
// 找到后不再递归子目录(一个 skill 目录)
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 递归子目录
|
||||
if let Ok(entries) = fs::read_dir(current) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// 跳过隐藏目录
|
||||
let dir_name = entry.file_name().to_string_lossy().to_string();
|
||||
if dir_name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
Self::scan_skills_recursive(&path, results)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========== 仓库管理(保留原有逻辑)==========
|
||||
|
||||
/// 列出仓库
|
||||
|
||||
@@ -373,15 +373,11 @@ impl StreamCheckService {
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<(u16, String), AppError> {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
// Codex CLI 的 base_url 语义:base_url 是 API base(可能已包含 /v1 或其他自定义前缀),
|
||||
// Responses 端点为 `/responses`。
|
||||
//
|
||||
// 兼容:如果 base_url 配成纯 origin(如 https://api.openai.com),则需要补 `/v1`。
|
||||
// 优先尝试 `{base}/responses`,若 404 再回退 `{base}/v1/responses`。
|
||||
let urls = if base.ends_with("/v1") {
|
||||
vec![format!("{base}/responses")]
|
||||
// Codex CLI 使用 /v1/responses 端点 (OpenAI Responses API)
|
||||
let url = if base.ends_with("/v1") {
|
||||
format!("{base}/responses")
|
||||
} else {
|
||||
vec![format!("{base}/responses"), format!("{base}/v1/responses")]
|
||||
format!("{base}/v1/responses")
|
||||
};
|
||||
|
||||
// 解析模型名和推理等级 (支持 model@level 或 model#level 格式)
|
||||
@@ -403,50 +399,40 @@ impl StreamCheckService {
|
||||
body["reasoning"] = json!({ "effort": effort });
|
||||
}
|
||||
|
||||
for (i, url) in urls.iter().enumerate() {
|
||||
// 严格按照 Codex CLI 请求格式设置 headers
|
||||
let response = client
|
||||
.post(url)
|
||||
.header("authorization", format!("Bearer {}", auth.api_key))
|
||||
.header("content-type", "application/json")
|
||||
.header("accept", "text/event-stream")
|
||||
.header("accept-encoding", "identity")
|
||||
.header(
|
||||
"user-agent",
|
||||
format!("codex_cli_rs/0.80.0 ({os_name} 15.7.2; {arch_name}) Terminal"),
|
||||
)
|
||||
.header("originator", "codex_cli_rs")
|
||||
.timeout(timeout)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Self::map_request_error)?;
|
||||
// 严格按照 Codex CLI 请求格式设置 headers
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("authorization", format!("Bearer {}", auth.api_key))
|
||||
.header("content-type", "application/json")
|
||||
.header("accept", "text/event-stream")
|
||||
.header("accept-encoding", "identity")
|
||||
.header(
|
||||
"user-agent",
|
||||
format!("codex_cli_rs/0.80.0 ({os_name} 15.7.2; {arch_name}) Terminal"),
|
||||
)
|
||||
.header("originator", "codex_cli_rs")
|
||||
.timeout(timeout)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Self::map_request_error)?;
|
||||
|
||||
let status = response.status().as_u16();
|
||||
let status = response.status().as_u16();
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
// 回退策略:仅当首选 URL 返回 404 时尝试下一个
|
||||
if i == 0 && status == 404 && urls.len() > 1 {
|
||||
continue;
|
||||
}
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
if let Some(chunk) = stream.next().await {
|
||||
match chunk {
|
||||
Ok(_) => return Ok((status, actual_model)),
|
||||
Err(e) => return Err(AppError::Message(format!("Stream read failed: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
return Err(AppError::Message("No response data received".to_string()));
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
}
|
||||
|
||||
Err(AppError::Message(
|
||||
"No valid Codex responses endpoint found".to_string(),
|
||||
))
|
||||
let mut stream = response.bytes_stream();
|
||||
if let Some(chunk) = stream.next().await {
|
||||
match chunk {
|
||||
Ok(_) => Ok((status, model.to_string())),
|
||||
Err(e) => Err(AppError::Message(format!("Stream read failed: {e}"))),
|
||||
}
|
||||
} else {
|
||||
Err(AppError::Message("No response data received".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gemini 流式检查
|
||||
|
||||
@@ -63,7 +63,7 @@ pub const TRAY_SECTIONS: [TrayAppSection; 3] = [
|
||||
prefix: "claude_",
|
||||
header_id: "claude_header",
|
||||
empty_id: "claude_empty",
|
||||
header_label: "Claude",
|
||||
header_label: "─── Claude ───",
|
||||
log_name: "Claude",
|
||||
},
|
||||
TrayAppSection {
|
||||
@@ -71,7 +71,7 @@ pub const TRAY_SECTIONS: [TrayAppSection; 3] = [
|
||||
prefix: "codex_",
|
||||
header_id: "codex_header",
|
||||
empty_id: "codex_empty",
|
||||
header_label: "Codex",
|
||||
header_label: "─── Codex ───",
|
||||
log_name: "Codex",
|
||||
},
|
||||
TrayAppSection {
|
||||
@@ -79,7 +79,7 @@ pub const TRAY_SECTIONS: [TrayAppSection; 3] = [
|
||||
prefix: "gemini_",
|
||||
header_id: "gemini_header",
|
||||
empty_id: "gemini_empty",
|
||||
header_label: "Gemini",
|
||||
header_label: "─── Gemini ───",
|
||||
log_name: "Gemini",
|
||||
},
|
||||
];
|
||||
@@ -391,16 +391,13 @@ pub fn create_tray_menu(
|
||||
&tray_texts,
|
||||
app_state,
|
||||
)?;
|
||||
|
||||
// 在每个 section 后添加分隔符
|
||||
menu_builder = menu_builder.separator();
|
||||
}
|
||||
|
||||
// 退出菜单(分隔符已在上面的 section 循环中添加)
|
||||
// 分隔符和退出菜单
|
||||
let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>)
|
||||
.map_err(|e| AppError::Message(format!("创建退出菜单失败: {e}")))?;
|
||||
|
||||
menu_builder = menu_builder.item(&quit_item);
|
||||
menu_builder = menu_builder.separator().item(&quit_item);
|
||||
|
||||
menu_builder
|
||||
.build()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Switch",
|
||||
"version": "3.10.3",
|
||||
"version": "3.10.2",
|
||||
"identifier": "com.ccswitch.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
+2
-15
@@ -15,7 +15,6 @@ import {
|
||||
Search,
|
||||
Download,
|
||||
BarChart2,
|
||||
FolderArchive,
|
||||
} from "lucide-react";
|
||||
import type { Provider, VisibleApps } from "@/types";
|
||||
import type { EnvConflict } from "@/types/env";
|
||||
@@ -636,7 +635,6 @@ function App() {
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentView}
|
||||
className="flex-1 min-h-0"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
@@ -830,17 +828,6 @@ function App() {
|
||||
)}
|
||||
{currentView === "skills" && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
unifiedSkillsPanelRef.current?.openInstallFromZip()
|
||||
}
|
||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||
>
|
||||
<FolderArchive className="w-4 h-4 mr-2" />
|
||||
{t("skills.installFromZip.button")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -972,8 +959,8 @@ function App() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 min-h-0 flex flex-col animate-fade-in">
|
||||
{renderContent()}
|
||||
<main className="flex-1 pb-12 animate-fade-in ">
|
||||
<div className="pb-12">{renderContent()}</div>
|
||||
</main>
|
||||
|
||||
<AddProviderDialog
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import EndpointSpeedTest from "./EndpointSpeedTest";
|
||||
import { ApiKeySection, EndpointField } from "./shared";
|
||||
import type { ProviderCategory, ClaudeApiFormat } from "@/types";
|
||||
import type { ProviderCategory } from "@/types";
|
||||
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
|
||||
|
||||
interface EndpointCandidate {
|
||||
@@ -65,9 +59,10 @@ interface ClaudeFormFieldsProps {
|
||||
// Speed Test Endpoints
|
||||
speedTestEndpoints: EndpointCandidate[];
|
||||
|
||||
// API Format (for third-party providers that use OpenAI Chat Completions format)
|
||||
apiFormat: ClaudeApiFormat;
|
||||
onApiFormatChange: (format: ClaudeApiFormat) => void;
|
||||
// OpenRouter Compat
|
||||
showOpenRouterCompatToggle: boolean;
|
||||
openRouterCompatEnabled: boolean;
|
||||
onOpenRouterCompatChange: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function ClaudeFormFields({
|
||||
@@ -100,8 +95,9 @@ export function ClaudeFormFields({
|
||||
defaultOpusModel,
|
||||
onModelChange,
|
||||
speedTestEndpoints,
|
||||
apiFormat,
|
||||
onApiFormatChange,
|
||||
showOpenRouterCompatToggle,
|
||||
openRouterCompatEnabled,
|
||||
onOpenRouterCompatChange,
|
||||
}: ClaudeFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -184,34 +180,25 @@ export function ClaudeFormFields({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* API 格式选择(仅非官方供应商显示) */}
|
||||
{shouldShowModelSelector && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="apiFormat">
|
||||
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
|
||||
</FormLabel>
|
||||
<Select value={apiFormat} onValueChange={onApiFormatChange}>
|
||||
<SelectTrigger id="apiFormat" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="anthropic">
|
||||
{t("providerForm.apiFormatAnthropic", {
|
||||
defaultValue: "Anthropic Messages (原生)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai_chat">
|
||||
{t("providerForm.apiFormatOpenAIChat", {
|
||||
defaultValue: "OpenAI Chat Completions (需转换)",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("providerForm.apiFormatHint", {
|
||||
defaultValue: "选择供应商 API 的输入格式",
|
||||
})}
|
||||
</p>
|
||||
{showOpenRouterCompatToggle && (
|
||||
<div className="flex items-center justify-between rounded-lg border border-white/10 bg-background/60 p-4">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>
|
||||
{t("providerForm.openrouterCompatMode", {
|
||||
defaultValue: "OpenRouter 兼容模式",
|
||||
})}
|
||||
</FormLabel>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("providerForm.openrouterCompatModeHint", {
|
||||
defaultValue:
|
||||
"使用 OpenAI Chat Completions 接口并转换为 Anthropic SSE。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={openRouterCompatEnabled}
|
||||
onCheckedChange={onOpenRouterCompatChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -139,8 +139,6 @@ interface OpenCodeFormFieldsProps {
|
||||
category?: ProviderCategory;
|
||||
shouldShowApiKeyLink: boolean;
|
||||
websiteUrl: string;
|
||||
isPartner?: boolean;
|
||||
partnerPromotionKey?: string;
|
||||
|
||||
// Base URL
|
||||
baseUrl: string;
|
||||
@@ -163,8 +161,6 @@ export function OpenCodeFormFields({
|
||||
category,
|
||||
shouldShowApiKeyLink,
|
||||
websiteUrl,
|
||||
isPartner,
|
||||
partnerPromotionKey,
|
||||
baseUrl,
|
||||
onBaseUrlChange,
|
||||
models,
|
||||
@@ -380,8 +376,6 @@ export function OpenCodeFormFields({
|
||||
category={category}
|
||||
shouldShowLink={shouldShowApiKeyLink}
|
||||
websiteUrl={websiteUrl}
|
||||
isPartner={isPartner}
|
||||
partnerPromotionKey={partnerPromotionKey}
|
||||
/>
|
||||
|
||||
{/* Base URL */}
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
ProviderMeta,
|
||||
ProviderTestConfig,
|
||||
ProviderProxyConfig,
|
||||
ClaudeApiFormat,
|
||||
} from "@/types";
|
||||
import {
|
||||
providerPresets,
|
||||
@@ -245,6 +244,8 @@ export function ProviderForm({
|
||||
mode: "onSubmit",
|
||||
});
|
||||
|
||||
const settingsConfigValue = form.getValues("settingsConfig");
|
||||
|
||||
// 使用 API Key hook
|
||||
const {
|
||||
apiKey,
|
||||
@@ -283,16 +284,52 @@ export function ProviderForm({
|
||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||
});
|
||||
|
||||
// Claude API Format state - stored in meta, not settingsConfig
|
||||
// Read initial value from meta.apiFormat, default to "anthropic"
|
||||
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
||||
if (appId !== "claude") return "anthropic";
|
||||
return initialData?.meta?.apiFormat ?? "anthropic";
|
||||
});
|
||||
const isOpenRouterProvider = useMemo(() => {
|
||||
if (appId !== "claude") return false;
|
||||
const normalized = baseUrl.trim().toLowerCase();
|
||||
if (normalized.includes("openrouter.ai")) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const config = JSON.parse(settingsConfigValue || "{}");
|
||||
const envUrl = config?.env?.ANTHROPIC_BASE_URL;
|
||||
return typeof envUrl === "string" && envUrl.includes("openrouter.ai");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, [appId, baseUrl, settingsConfigValue]);
|
||||
|
||||
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
||||
setLocalApiFormat(format);
|
||||
}, []);
|
||||
const openRouterCompatEnabled = useMemo(() => {
|
||||
if (!isOpenRouterProvider) return false;
|
||||
try {
|
||||
const config = JSON.parse(settingsConfigValue || "{}");
|
||||
const raw = config?.openrouter_compat_mode;
|
||||
if (typeof raw === "boolean") return raw;
|
||||
if (typeof raw === "number") return raw !== 0;
|
||||
if (typeof raw === "string") {
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
return normalized === "true" || normalized === "1";
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return false; // OpenRouter now supports Claude Code compatible API, no need for transform
|
||||
}, [isOpenRouterProvider, settingsConfigValue]);
|
||||
|
||||
const handleOpenRouterCompatChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
try {
|
||||
const currentConfig = JSON.parse(
|
||||
form.getValues("settingsConfig") || "{}",
|
||||
);
|
||||
currentConfig.openrouter_compat_mode = enabled;
|
||||
form.setValue("settingsConfig", JSON.stringify(currentConfig, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
// 使用 Codex 配置 hook (仅 Codex 模式)
|
||||
const {
|
||||
@@ -938,11 +975,6 @@ export function ProviderForm({
|
||||
pricingConfig.enabled && pricingConfig.pricingModelSource !== "inherit"
|
||||
? pricingConfig.pricingModelSource
|
||||
: undefined,
|
||||
// Claude API 格式(仅非官方 Claude 供应商使用)
|
||||
apiFormat:
|
||||
appId === "claude" && category !== "official"
|
||||
? localApiFormat
|
||||
: undefined,
|
||||
};
|
||||
|
||||
onSubmit(payload);
|
||||
@@ -1010,20 +1042,6 @@ export function ProviderForm({
|
||||
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||
});
|
||||
|
||||
// 使用 API Key 链接 hook (OpenCode)
|
||||
const {
|
||||
shouldShowApiKeyLink: shouldShowOpencodeApiKeyLink,
|
||||
websiteUrl: opencodeWebsiteUrl,
|
||||
isPartner: isOpencodePartner,
|
||||
partnerPromotionKey: opencodePartnerPromotionKey,
|
||||
} = useApiKeyLink({
|
||||
appId: "opencode",
|
||||
category,
|
||||
selectedPresetId,
|
||||
presetEntries,
|
||||
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||
});
|
||||
|
||||
// 使用端点测速候选 hook
|
||||
const speedTestEndpoints = useSpeedTestEndpoints({
|
||||
appId,
|
||||
@@ -1153,14 +1171,6 @@ export function ProviderForm({
|
||||
preset.templateValues,
|
||||
);
|
||||
|
||||
// Sync preset's apiFormat to local state (for Claude providers)
|
||||
if (preset.apiFormat) {
|
||||
setLocalApiFormat(preset.apiFormat);
|
||||
} else {
|
||||
// Reset to default if preset doesn't specify apiFormat
|
||||
setLocalApiFormat("anthropic");
|
||||
}
|
||||
|
||||
form.reset({
|
||||
name: preset.name,
|
||||
websiteUrl: preset.websiteUrl ?? "",
|
||||
@@ -1284,8 +1294,9 @@ export function ProviderForm({
|
||||
defaultOpusModel={defaultOpusModel}
|
||||
onModelChange={handleModelChange}
|
||||
speedTestEndpoints={speedTestEndpoints}
|
||||
apiFormat={localApiFormat}
|
||||
onApiFormatChange={handleApiFormatChange}
|
||||
showOpenRouterCompatToggle={false}
|
||||
openRouterCompatEnabled={openRouterCompatEnabled}
|
||||
onOpenRouterCompatChange={handleOpenRouterCompatChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1355,10 +1366,8 @@ export function ProviderForm({
|
||||
apiKey={opencodeApiKey}
|
||||
onApiKeyChange={handleOpencodeApiKeyChange}
|
||||
category={category}
|
||||
shouldShowApiKeyLink={shouldShowOpencodeApiKeyLink}
|
||||
websiteUrl={opencodeWebsiteUrl}
|
||||
isPartner={isOpencodePartner}
|
||||
partnerPromotionKey={opencodePartnerPromotionKey}
|
||||
shouldShowApiKeyLink={false}
|
||||
websiteUrl=""
|
||||
baseUrl={opencodeBaseUrl}
|
||||
onBaseUrlChange={handleOpencodeBaseUrlChange}
|
||||
models={opencodeModels}
|
||||
|
||||
@@ -4,15 +4,10 @@ import type { ProviderCategory } from "@/types";
|
||||
import type { ProviderPreset } from "@/config/claudeProviderPresets";
|
||||
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
|
||||
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
|
||||
import type { OpenCodeProviderPreset } from "@/config/opencodeProviderPresets";
|
||||
|
||||
type PresetEntry = {
|
||||
id: string;
|
||||
preset:
|
||||
| ProviderPreset
|
||||
| CodexProviderPreset
|
||||
| GeminiProviderPreset
|
||||
| OpenCodeProviderPreset;
|
||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset;
|
||||
};
|
||||
|
||||
interface UseApiKeyLinkProps {
|
||||
@@ -79,10 +74,7 @@ export function useApiKeyLink({
|
||||
|
||||
return {
|
||||
shouldShowApiKeyLink:
|
||||
appId === "claude" ||
|
||||
appId === "codex" ||
|
||||
appId === "gemini" ||
|
||||
appId === "opencode"
|
||||
appId === "claude" || appId === "codex" || appId === "gemini"
|
||||
? shouldShowApiKeyLink
|
||||
: false,
|
||||
websiteUrl: getWebsiteUrl,
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { AppId } from "@/lib/api";
|
||||
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||
import { codexProviderPresets } from "@/config/codexProviderPresets";
|
||||
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
|
||||
import { opencodeProviderPresets } from "@/config/opencodeProviderPresets";
|
||||
|
||||
interface UseProviderCategoryProps {
|
||||
appId: AppId;
|
||||
@@ -43,9 +42,7 @@ export function useProviderCategory({
|
||||
if (!selectedPresetId) return;
|
||||
|
||||
// 从预设 ID 提取索引
|
||||
const match = selectedPresetId.match(
|
||||
/^(claude|codex|gemini|opencode)-(\d+)$/,
|
||||
);
|
||||
const match = selectedPresetId.match(/^(claude|codex|gemini)-(\d+)$/);
|
||||
if (!match) return;
|
||||
|
||||
const [, type, indexStr] = match;
|
||||
@@ -70,11 +67,6 @@ export function useProviderCategory({
|
||||
if (preset) {
|
||||
setCategory(preset.category || undefined);
|
||||
}
|
||||
} else if (type === "opencode" && appId === "opencode") {
|
||||
const preset = opencodeProviderPresets[index];
|
||||
if (preset) {
|
||||
setCategory(preset.category || undefined);
|
||||
}
|
||||
}
|
||||
}, [appId, selectedPresetId, isEditMode, initialCategory]);
|
||||
|
||||
|
||||
@@ -32,14 +32,9 @@ interface ToolVersion {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const ONE_CLICK_INSTALL_COMMANDS = `# Claude Code (Native install - recommended)
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
# Codex
|
||||
const ONE_CLICK_INSTALL_COMMANDS = `npm i -g @anthropic-ai/claude-code@latest
|
||||
npm i -g @openai/codex@latest
|
||||
# Gemini CLI
|
||||
npm i -g @google/gemini-cli@latest
|
||||
# OpenCode
|
||||
curl -fsSL https://opencode.ai/install | bash`;
|
||||
npm i -g @google/gemini-cli@latest`;
|
||||
|
||||
export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
// ... (use hooks as before) ...
|
||||
@@ -317,14 +312,10 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
{isLoadingTools ? t("common.refreshing") : t("common.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 px-1">
|
||||
{["claude", "codex", "gemini", "opencode"].map((toolName, index) => {
|
||||
<div className="grid gap-3 sm:grid-cols-3 px-1">
|
||||
{["claude", "codex", "gemini"].map((toolName, index) => {
|
||||
const tool = toolVersions.find((item) => item.name === toolName);
|
||||
// Special case for OpenCode (capital C), others use capitalize
|
||||
const displayName =
|
||||
toolName === "opencode"
|
||||
? "OpenCode"
|
||||
: toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
||||
const displayName = tool?.name ?? toolName;
|
||||
const title = tool?.version || tool?.error || t("common.unknown");
|
||||
|
||||
return (
|
||||
@@ -339,7 +330,9 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{displayName}</span>
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
{isLoadingTools ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
|
||||
@@ -31,7 +31,7 @@ export function AppVisibilitySettings({
|
||||
const visibleApps: VisibleApps = settings.visibleApps ?? {
|
||||
claude: true,
|
||||
codex: true,
|
||||
gemini: true,
|
||||
gemini: false,
|
||||
opencode: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export function SettingsPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden px-6">
|
||||
<div className="flex flex-col h-[calc(100vh-8rem)] overflow-hidden px-6">
|
||||
{isBusy ? (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
@@ -229,432 +229,427 @@ export function SettingsPage({
|
||||
<TabsTrigger value="about">{t("common.about")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden pr-2">
|
||||
<TabsContent value="general" className="space-y-6 mt-0">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<LanguageSettings
|
||||
value={settings.language}
|
||||
onChange={(lang) => handleAutoSave({ language: lang })}
|
||||
/>
|
||||
<ThemeSettings />
|
||||
<AppVisibilitySettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<WindowSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<SkillSyncMethodSettings
|
||||
value={settings.skillSyncMethod ?? "auto"}
|
||||
onChange={(method) =>
|
||||
handleAutoSave({ skillSyncMethod: method })
|
||||
}
|
||||
/>
|
||||
<TerminalSettings
|
||||
value={settings.preferredTerminal}
|
||||
onChange={(terminal) =>
|
||||
handleAutoSave({ preferredTerminal: terminal })
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden pr-2">
|
||||
<TabsContent value="general" className="space-y-6 mt-0">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<LanguageSettings
|
||||
value={settings.language}
|
||||
onChange={(lang) => handleAutoSave({ language: lang })}
|
||||
/>
|
||||
<ThemeSettings />
|
||||
<AppVisibilitySettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<WindowSettings
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<SkillSyncMethodSettings
|
||||
value={settings.skillSyncMethod ?? "auto"}
|
||||
onChange={(method) =>
|
||||
handleAutoSave({ skillSyncMethod: method })
|
||||
}
|
||||
/>
|
||||
<TerminalSettings
|
||||
value={settings.preferredTerminal}
|
||||
onChange={(terminal) =>
|
||||
handleAutoSave({ preferredTerminal: terminal })
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced" className="space-y-6 mt-0 pb-4">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-4"
|
||||
<TabsContent value="advanced" className="space-y-6 mt-0 pb-6">
|
||||
{settings ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[]}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[]}
|
||||
className="w-full space-y-4"
|
||||
<AccordionItem
|
||||
value="directory"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionItem
|
||||
value="directory"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderSearch className="h-5 w-5 text-primary" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.configDir.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.configDir.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<DirectorySettings
|
||||
appConfigDir={appConfigDir}
|
||||
resolvedDirs={resolvedDirs}
|
||||
onAppConfigChange={updateAppConfigDir}
|
||||
onBrowseAppConfig={browseAppConfigDir}
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
opencodeDir={settings.opencodeConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="proxy"
|
||||
className="rounded-xl glass-card overflow-hidden [&[data-state=open]>.accordion-header]:bg-muted/50"
|
||||
>
|
||||
<AccordionPrimitive.Header className="accordion-header flex items-center justify-between px-6 py-4 hover:bg-muted/50">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center justify-between hover:no-underline [&[data-state=open]>svg]:rotate-180">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderSearch className="h-5 w-5 text-primary" />
|
||||
<Server className="h-5 w-5 text-green-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.configDir.title")}
|
||||
{t("settings.advanced.proxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.configDir.description")}
|
||||
{t("settings.advanced.proxy.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<DirectorySettings
|
||||
appConfigDir={appConfigDir}
|
||||
resolvedDirs={resolvedDirs}
|
||||
onAppConfigChange={updateAppConfigDir}
|
||||
onBrowseAppConfig={browseAppConfigDir}
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
opencodeDir={settings.opencodeConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
|
||||
<AccordionItem
|
||||
value="proxy"
|
||||
className="rounded-xl glass-card overflow-hidden [&[data-state=open]>.accordion-header]:bg-muted/50"
|
||||
>
|
||||
<AccordionPrimitive.Header className="accordion-header flex items-center justify-between px-6 py-4 hover:bg-muted/50">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center justify-between hover:no-underline [&[data-state=open]>svg]:rotate-180">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-green-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.proxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.proxy.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
|
||||
<div className="flex items-center gap-4 pl-4">
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{isRunning
|
||||
? t("settings.advanced.proxy.running")
|
||||
: t("settings.advanced.proxy.stopped")}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
<div className="flex items-center gap-4 pl-4">
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="px-6 pb-6 pt-0 border-t border-border/50">
|
||||
<ProxyPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="failover"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.failover.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.failover.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<div className="space-y-6">
|
||||
{/* 代理未运行时的提示 */}
|
||||
{!isRunning && (
|
||||
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
{t("proxy.failover.proxyRequired", {
|
||||
defaultValue:
|
||||
"需要先启动代理服务才能配置故障转移",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 故障转移设置 - 按应用分组 */}
|
||||
<Tabs defaultValue="claude" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="claude">Claude</TabsTrigger>
|
||||
<TabsTrigger value="codex">Codex</TabsTrigger>
|
||||
<TabsTrigger value="gemini">Gemini</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="claude"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="codex"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gemini"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="rectifier"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="h-5 w-5 text-purple-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.rectifier.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.rectifier.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<RectifierConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="test"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-indigo-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.modelTest.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.modelTest.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ModelTestConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="pricing"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Coins className="h-5 w-5 text-yellow-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.pricing.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.pricing.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<PricingConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="globalProxy"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.globalProxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.globalProxy.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<GlobalProxySettings />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="data"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-blue-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.data.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.data.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ImportExportSection
|
||||
status={importStatus}
|
||||
selectedFile={selectedFile}
|
||||
errorMessage={errorMessage}
|
||||
backupId={backupId}
|
||||
isImporting={isImporting}
|
||||
onSelectFile={selectImportFile}
|
||||
onImport={importConfig}
|
||||
onExport={exportConfig}
|
||||
onClear={clearSelection}
|
||||
{isRunning
|
||||
? t("settings.advanced.proxy.running")
|
||||
: t("settings.advanced.proxy.stopped")}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="px-6 pb-6 pt-0 border-t border-border/50">
|
||||
<ProxyPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="logConfig"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<ScrollText className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.logConfig.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.logConfig.description")}
|
||||
<AccordionItem
|
||||
value="failover"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.failover.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.failover.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<div className="space-y-6">
|
||||
{/* 代理未运行时的提示 */}
|
||||
{!isRunning && (
|
||||
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
{t("proxy.failover.proxyRequired", {
|
||||
defaultValue:
|
||||
"需要先启动代理服务才能配置故障转移",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 故障转移设置 - 按应用分组 */}
|
||||
<Tabs defaultValue="claude" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="claude">Claude</TabsTrigger>
|
||||
<TabsTrigger value="codex">Codex</TabsTrigger>
|
||||
<TabsTrigger value="gemini">Gemini</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="claude"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="claude"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="codex"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="codex"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gemini"
|
||||
className="mt-4 space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.failoverQueue.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failoverQueue.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FailoverQueueManager
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/50 pt-6">
|
||||
<AutoFailoverConfigPanel
|
||||
appType="gemini"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="rectifier"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="h-5 w-5 text-purple-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.rectifier.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.rectifier.description")}
|
||||
</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<LogConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<RectifierConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<TabsContent value="about" className="mt-0">
|
||||
<AboutSection isPortable={isPortable} />
|
||||
</TabsContent>
|
||||
<AccordionItem
|
||||
value="test"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-indigo-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.modelTest.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.modelTest.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ModelTestConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<TabsContent value="usage" className="mt-0">
|
||||
<UsageDashboard />
|
||||
</TabsContent>
|
||||
</div>
|
||||
<AccordionItem
|
||||
value="pricing"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Coins className="h-5 w-5 text-yellow-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.pricing.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.pricing.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<PricingConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{activeTab === "advanced" && settings && (
|
||||
<div
|
||||
className="flex-shrink-0 py-4 border-t border-border-default"
|
||||
style={{ backgroundColor: "hsl(var(--background))" }}
|
||||
>
|
||||
<div className="px-6 flex items-center justify-end gap-3">
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t("settings.saving")}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{t("common.save")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AccordionItem
|
||||
value="globalProxy"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.globalProxy.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.globalProxy.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<GlobalProxySettings />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="data"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-blue-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.data.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.data.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<ImportExportSection
|
||||
status={importStatus}
|
||||
selectedFile={selectedFile}
|
||||
errorMessage={errorMessage}
|
||||
backupId={backupId}
|
||||
isImporting={isImporting}
|
||||
onSelectFile={selectImportFile}
|
||||
onImport={importConfig}
|
||||
onExport={exportConfig}
|
||||
onClear={clearSelection}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="logConfig"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<ScrollText className="h-5 w-5 text-cyan-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("settings.advanced.logConfig.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
{t("settings.advanced.logConfig.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<LogConfigPanel />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="w-full h-12 text-base font-medium"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
{t("settings.saving")}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-5 w-5" />
|
||||
{t("common.save")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="about" className="mt-0">
|
||||
<AboutSection isPortable={isPortable} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="usage" className="mt-0">
|
||||
<UsageDashboard />
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
useUninstallSkill,
|
||||
useScanUnmanagedSkills,
|
||||
useImportSkillsFromApps,
|
||||
useInstallSkillsFromZip,
|
||||
type InstalledSkill,
|
||||
type AppType,
|
||||
} from "@/hooks/useSkills";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { settingsApi, skillsApi } from "@/lib/api";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface UnifiedSkillsPanelProps {
|
||||
@@ -28,7 +27,6 @@ interface UnifiedSkillsPanelProps {
|
||||
export interface UnifiedSkillsPanelHandle {
|
||||
openDiscovery: () => void;
|
||||
openImport: () => void;
|
||||
openInstallFromZip: () => void;
|
||||
}
|
||||
|
||||
const UnifiedSkillsPanel = React.forwardRef<
|
||||
@@ -51,7 +49,6 @@ const UnifiedSkillsPanel = React.forwardRef<
|
||||
const { data: unmanagedSkills, refetch: scanUnmanaged } =
|
||||
useScanUnmanagedSkills();
|
||||
const importMutation = useImportSkillsFromApps();
|
||||
const installFromZipMutation = useInstallSkillsFromZip();
|
||||
|
||||
// Count enabled skills per app
|
||||
const enabledCounts = useMemo(() => {
|
||||
@@ -130,52 +127,9 @@ const UnifiedSkillsPanel = React.forwardRef<
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallFromZip = async () => {
|
||||
try {
|
||||
// 打开文件选择对话框
|
||||
const filePath = await skillsApi.openZipFileDialog();
|
||||
if (!filePath) {
|
||||
// 用户取消选择
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认使用 claude 作为当前应用
|
||||
const currentApp: AppType = "claude";
|
||||
|
||||
// 安装 Skills
|
||||
const installed = await installFromZipMutation.mutateAsync({
|
||||
filePath,
|
||||
currentApp,
|
||||
});
|
||||
|
||||
if (installed.length === 0) {
|
||||
toast.info(t("skills.installFromZip.noSkillsFound"), {
|
||||
closeButton: true,
|
||||
});
|
||||
} else if (installed.length === 1) {
|
||||
toast.success(
|
||||
t("skills.installFromZip.successSingle", { name: installed[0].name }),
|
||||
{ closeButton: true },
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
t("skills.installFromZip.successMultiple", {
|
||||
count: installed.length,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("skills.installFailed"), {
|
||||
description: String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
openDiscovery: onOpenDiscovery,
|
||||
openImport: handleOpenImport,
|
||||
openInstallFromZip: handleInstallFromZip,
|
||||
}));
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,11 +43,6 @@ export interface ProviderPreset {
|
||||
// 图标配置
|
||||
icon?: string; // 图标名称
|
||||
iconColor?: string; // 图标颜色
|
||||
|
||||
// Claude API 格式(仅 Claude 供应商使用)
|
||||
// - "anthropic" (默认): Anthropic Messages API 格式,直接透传
|
||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||
apiFormat?: "anthropic" | "openai_chat";
|
||||
}
|
||||
|
||||
export const providerPresets: ProviderPreset[] = [
|
||||
@@ -148,10 +143,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "kimi-k2.5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "kimi-k2.5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "kimi-k2.5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "kimi-k2.5",
|
||||
ANTHROPIC_MODEL: "kimi-k2-thinking",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "kimi-k2-thinking",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "kimi-k2-thinking",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "kimi-k2-thinking",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -165,6 +160,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.kimi.com/coding/",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "kimi-for-coding",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "kimi-for-coding",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "kimi-for-coding",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "kimi-for-coding",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -466,26 +465,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "rc",
|
||||
iconColor: "#E96B2C",
|
||||
},
|
||||
{
|
||||
name: "AICodeMirror",
|
||||
websiteUrl: "https://www.aicodemirror.com",
|
||||
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.aicodemirror.com/api/claudecode",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
},
|
||||
},
|
||||
endpointCandidates: [
|
||||
"https://api.aicodemirror.com/api/claudecode",
|
||||
"https://api.claudecode.net.cn/api/claudecode",
|
||||
],
|
||||
category: "third_party",
|
||||
isPartner: true, // 合作伙伴
|
||||
partnerPromotionKey: "aicodemirror", // 促销信息 i18n key
|
||||
icon: "aicodemirror",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "OpenRouter",
|
||||
websiteUrl: "https://openrouter.ai",
|
||||
@@ -504,25 +483,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "openrouter",
|
||||
iconColor: "#6566F1",
|
||||
},
|
||||
{
|
||||
name: "Nvidia",
|
||||
websiteUrl: "https://build.nvidia.com",
|
||||
apiKeyUrl: "https://build.nvidia.com/settings/api-keys",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://integrate.api.nvidia.com",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "moonshotai/kimi-k2.5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "moonshotai/kimi-k2.5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "moonshotai/kimi-k2.5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "moonshotai/kimi-k2.5",
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
apiFormat: "openai_chat",
|
||||
icon: "nvidia",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "Xiaomi MiMo",
|
||||
websiteUrl: "https://platform.xiaomimimo.com",
|
||||
|
||||
@@ -208,25 +208,6 @@ requires_openai_auth = true`,
|
||||
icon: "rc",
|
||||
iconColor: "#E96B2C",
|
||||
},
|
||||
{
|
||||
name: "AICodeMirror",
|
||||
websiteUrl: "https://www.aicodemirror.com",
|
||||
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
|
||||
auth: generateThirdPartyAuth(""),
|
||||
config: generateThirdPartyConfig(
|
||||
"aicodemirror",
|
||||
"https://api.aicodemirror.com/api/codex/backend-api/codex",
|
||||
"gpt-5.2",
|
||||
),
|
||||
endpointCandidates: [
|
||||
"https://api.aicodemirror.com/api/codex/backend-api/codex",
|
||||
"https://api.claudecode.net.cn/api/codex/backend-api/codex",
|
||||
],
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "aicodemirror",
|
||||
icon: "aicodemirror",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "OpenRouter",
|
||||
websiteUrl: "https://openrouter.ai",
|
||||
|
||||
@@ -116,29 +116,6 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
|
||||
icon: "aigocode",
|
||||
iconColor: "#5B7FFF",
|
||||
},
|
||||
{
|
||||
name: "AICodeMirror",
|
||||
websiteUrl: "https://www.aicodemirror.com",
|
||||
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
GOOGLE_GEMINI_BASE_URL: "https://api.aicodemirror.com/api/gemini",
|
||||
GEMINI_MODEL: "gemini-3-pro",
|
||||
},
|
||||
},
|
||||
baseURL: "https://api.aicodemirror.com/api/gemini",
|
||||
model: "gemini-3-pro",
|
||||
description: "AICodeMirror",
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "aicodemirror",
|
||||
endpointCandidates: [
|
||||
"https://api.aicodemirror.com/api/gemini",
|
||||
"https://api.claudecode.net.cn/api/gemini",
|
||||
],
|
||||
icon: "aicodemirror",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "OpenRouter",
|
||||
websiteUrl: "https://openrouter.ai",
|
||||
|
||||
@@ -169,18 +169,18 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Kimi k2.5",
|
||||
name: "Kimi k2",
|
||||
websiteUrl: "https://platform.moonshot.cn/console",
|
||||
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "Kimi k2.5",
|
||||
name: "Kimi k2",
|
||||
options: {
|
||||
baseURL: "https://api.moonshot.cn/v1",
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"kimi-k2.5": { name: "Kimi K2.5" },
|
||||
"kimi-k2-thinking": { name: "Kimi K2 Thinking" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -558,32 +558,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nvidia",
|
||||
websiteUrl: "https://build.nvidia.com",
|
||||
apiKeyUrl: "https://build.nvidia.com/settings/api-keys",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "Nvidia",
|
||||
options: {
|
||||
baseURL: "https://integrate.api.nvidia.com/v1",
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"moonshotai/kimi-k2.5": { name: "Kimi K2.5" },
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
icon: "nvidia",
|
||||
iconColor: "#000000",
|
||||
templateValues: {
|
||||
apiKey: {
|
||||
label: "API Key",
|
||||
placeholder: "",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ========== 第三方合作伙伴 ==========
|
||||
{
|
||||
@@ -701,35 +675,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AICodeMirror",
|
||||
websiteUrl: "https://www.aicodemirror.com",
|
||||
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/anthropic",
|
||||
name: "AICodeMirror",
|
||||
options: {
|
||||
baseURL: "https://api.aicodemirror.com/api/claudecode",
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"claude-sonnet-4.5": { name: "Claude Sonnet 4.5" },
|
||||
"claude-opus-4.5": { name: "Claude Opus 4.5" },
|
||||
},
|
||||
},
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "aicodemirror",
|
||||
icon: "aicodemirror",
|
||||
iconColor: "#000000",
|
||||
templateValues: {
|
||||
apiKey: {
|
||||
label: "API Key",
|
||||
placeholder: "",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ========== 自定义模板 ==========
|
||||
{
|
||||
|
||||
@@ -84,43 +84,11 @@ export function useProviderActions(activeApp: AppId) {
|
||||
try {
|
||||
await switchProviderMutation.mutateAsync(provider.id);
|
||||
await syncClaudePlugin(provider);
|
||||
|
||||
// 根据供应商类型显示不同的成功提示
|
||||
if (
|
||||
activeApp === "claude" &&
|
||||
provider.category !== "official" &&
|
||||
provider.meta?.apiFormat === "openai_chat"
|
||||
) {
|
||||
// OpenAI Chat 格式供应商:显示代理提示
|
||||
toast.info(
|
||||
t("notifications.openAIChatFormatHint", {
|
||||
defaultValue:
|
||||
"此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用",
|
||||
}),
|
||||
{
|
||||
duration: 5000,
|
||||
closeButton: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 普通供应商:显示切换成功
|
||||
// OpenCode: show "added to config" message instead of "switched"
|
||||
const messageKey =
|
||||
activeApp === "opencode"
|
||||
? "notifications.addToConfigSuccess"
|
||||
: "notifications.switchSuccess";
|
||||
const defaultMessage =
|
||||
activeApp === "opencode" ? "已添加到配置" : "切换成功!";
|
||||
|
||||
toast.success(t(messageKey, { defaultValue: defaultMessage }), {
|
||||
closeButton: true,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// 错误提示由 mutation 处理
|
||||
// 错误提示由 mutation 与同步函数处理
|
||||
}
|
||||
},
|
||||
[switchProviderMutation, syncClaudePlugin, activeApp, t],
|
||||
[switchProviderMutation, syncClaudePlugin],
|
||||
);
|
||||
|
||||
// 删除供应商
|
||||
|
||||
@@ -147,26 +147,6 @@ export function useRemoveSkillRepo() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ZIP 文件安装 Skills
|
||||
*/
|
||||
export function useInstallSkillsFromZip() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
filePath,
|
||||
currentApp,
|
||||
}: {
|
||||
filePath: string;
|
||||
currentApp: AppType;
|
||||
}) => skillsApi.installFromZip(filePath, currentApp),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 辅助类型 ==========
|
||||
|
||||
export type { InstalledSkill, DiscoverableSkill, AppType };
|
||||
|
||||
@@ -155,8 +155,7 @@
|
||||
"deleteSuccess": "Provider deleted",
|
||||
"deleteFailed": "Failed to delete provider: {{error}}",
|
||||
"settingsSaved": "Settings saved",
|
||||
"settingsSaveFailed": "Failed to save settings: {{error}}",
|
||||
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled"
|
||||
"settingsSaveFailed": "Failed to save settings: {{error}}"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "Delete Provider",
|
||||
@@ -352,7 +351,7 @@
|
||||
"viewReleaseNotes": "View release notes for this version",
|
||||
"viewCurrentReleaseNotes": "View current version release notes",
|
||||
"oneClickInstall": "One-click Install",
|
||||
"oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI / OpenCode",
|
||||
"oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI",
|
||||
"localEnvCheck": "Local environment check",
|
||||
"installCommandsCopied": "Install commands copied",
|
||||
"installCommandsCopyFailed": "Copy failed, please copy manually.",
|
||||
@@ -448,8 +447,7 @@
|
||||
"dmxapi": "Claude Code exclusive model 66% OFF now!",
|
||||
"cubence": "Cubence is an official partner of CC Switch. Register using this link and enter \"CCSWITCH\" promo code during recharge to get 10% off every top-up",
|
||||
"aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!",
|
||||
"rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!",
|
||||
"aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!"
|
||||
"rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!"
|
||||
},
|
||||
"parameterConfig": "Parameter Config - {{name}} *",
|
||||
"mainModel": "Main Model (optional)",
|
||||
@@ -474,10 +472,8 @@
|
||||
"anthropicModel": "Main Model",
|
||||
"anthropicSmallFastModel": "Fast Model",
|
||||
"anthropicReasoningModel": "Reasoning Model (Thinking)",
|
||||
"apiFormat": "API Format",
|
||||
"apiFormatHint": "Select the input format for the provider's API",
|
||||
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
|
||||
"openrouterCompatMode": "OpenRouter Compatibility Mode",
|
||||
"openrouterCompatModeHint": "Use OpenAI Chat Completions interface and convert to Anthropic SSE.",
|
||||
"anthropicDefaultHaikuModel": "Default Haiku Model",
|
||||
"anthropicDefaultSonnetModel": "Default Sonnet Model",
|
||||
"anthropicDefaultOpusModel": "Default Opus Model",
|
||||
@@ -1058,7 +1054,6 @@
|
||||
"http429": "Too many requests, please wait and retry",
|
||||
"parseMetadataFailed": "Failed to parse skill metadata",
|
||||
"getHomeDirFailed": "Unable to get user home directory",
|
||||
"noSkillsInZip": "No skills found in ZIP file (requires SKILL.md file)",
|
||||
"networkError": "Network error",
|
||||
"fsError": "File system error",
|
||||
"unknownError": "Unknown error",
|
||||
@@ -1069,8 +1064,7 @@
|
||||
"checkRepoUrl": "Please check repository URL and branch name",
|
||||
"checkDiskSpace": "Please check disk space",
|
||||
"checkPermission": "Please check directory permissions",
|
||||
"uninstallFirst": "Please uninstall the existing skill with the same name first",
|
||||
"checkZipContent": "Please verify the ZIP file contains valid skill directories (with SKILL.md files)"
|
||||
"uninstallFirst": "Please uninstall the existing skill with the same name first"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
@@ -1119,13 +1113,6 @@
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini",
|
||||
"opencode": "OpenCode"
|
||||
},
|
||||
"installFromZip": {
|
||||
"button": "Install from ZIP",
|
||||
"installing": "Installing...",
|
||||
"successSingle": "Skill {{name}} installed",
|
||||
"successMultiple": "Successfully installed {{count}} skills",
|
||||
"noSkillsFound": "No skills found in ZIP file (requires SKILL.md file)"
|
||||
}
|
||||
},
|
||||
"deeplink": {
|
||||
|
||||
@@ -155,8 +155,7 @@
|
||||
"deleteSuccess": "プロバイダーを削除しました",
|
||||
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
|
||||
"settingsSaved": "設定を保存しました",
|
||||
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
|
||||
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です"
|
||||
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
@@ -352,7 +351,7 @@
|
||||
"viewReleaseNotes": "このバージョンのリリースノートを見る",
|
||||
"viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る",
|
||||
"oneClickInstall": "ワンクリックインストール",
|
||||
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode をインストール",
|
||||
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI をインストール",
|
||||
"localEnvCheck": "ローカル環境チェック",
|
||||
"installCommandsCopied": "インストールコマンドをコピーしました",
|
||||
"installCommandsCopyFailed": "コピーに失敗しました。手動でコピーしてください。",
|
||||
@@ -448,8 +447,7 @@
|
||||
"dmxapi": "Claude Code 専用モデル 66% OFF 実施中!",
|
||||
"cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ",
|
||||
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!",
|
||||
"rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!",
|
||||
"aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!"
|
||||
"rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!"
|
||||
},
|
||||
"parameterConfig": "パラメーター設定 - {{name}} *",
|
||||
"mainModel": "メインモデル(任意)",
|
||||
@@ -474,10 +472,8 @@
|
||||
"anthropicModel": "メインモデル",
|
||||
"anthropicSmallFastModel": "高速モデル",
|
||||
"anthropicReasoningModel": "推論モデル(Thinking)",
|
||||
"apiFormat": "API フォーマット",
|
||||
"apiFormatHint": "プロバイダー API の入力フォーマットを選択",
|
||||
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
||||
"openrouterCompatMode": "OpenRouter 互換モード",
|
||||
"openrouterCompatModeHint": "OpenAI Chat Completions インターフェースを使用し、Anthropic SSE に変換します。",
|
||||
"anthropicDefaultHaikuModel": "既定 Haiku モデル",
|
||||
"anthropicDefaultSonnetModel": "既定 Sonnet モデル",
|
||||
"anthropicDefaultOpusModel": "既定 Opus モデル",
|
||||
|
||||
@@ -155,8 +155,7 @@
|
||||
"deleteSuccess": "供应商已删除",
|
||||
"deleteFailed": "删除供应商失败:{{error}}",
|
||||
"settingsSaved": "设置已保存",
|
||||
"settingsSaveFailed": "保存设置失败:{{error}}",
|
||||
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用"
|
||||
"settingsSaveFailed": "保存设置失败:{{error}}"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "删除供应商",
|
||||
@@ -352,7 +351,7 @@
|
||||
"viewReleaseNotes": "查看该版本更新日志",
|
||||
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
||||
"oneClickInstall": "一键安装",
|
||||
"oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI / OpenCode",
|
||||
"oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI",
|
||||
"localEnvCheck": "本地环境检查",
|
||||
"installCommandsCopied": "安装命令已复制",
|
||||
"installCommandsCopyFailed": "复制失败,请手动复制。",
|
||||
@@ -448,8 +447,7 @@
|
||||
"dmxapi": "Claude Code 专属模型 3.4 折优惠进行中!",
|
||||
"cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠",
|
||||
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!",
|
||||
"rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!",
|
||||
"aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!"
|
||||
"rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!"
|
||||
},
|
||||
"parameterConfig": "参数配置 - {{name}} *",
|
||||
"mainModel": "主模型 (可选)",
|
||||
@@ -474,10 +472,8 @@
|
||||
"anthropicModel": "主模型",
|
||||
"anthropicSmallFastModel": "快速模型",
|
||||
"anthropicReasoningModel": "推理模型 (Thinking)",
|
||||
"apiFormat": "API 格式",
|
||||
"apiFormatHint": "选择供应商 API 的输入格式",
|
||||
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
||||
"openrouterCompatMode": "OpenRouter 兼容模式",
|
||||
"openrouterCompatModeHint": "使用 OpenAI Chat Completions 接口并转换为 Anthropic SSE。",
|
||||
"anthropicDefaultHaikuModel": "Haiku 默认模型",
|
||||
"anthropicDefaultSonnetModel": "Sonnet 默认模型",
|
||||
"anthropicDefaultOpusModel": "Opus 默认模型",
|
||||
@@ -1058,7 +1054,6 @@
|
||||
"http429": "请求过于频繁,请等待后重试",
|
||||
"parseMetadataFailed": "解析技能元数据失败",
|
||||
"getHomeDirFailed": "无法获取用户主目录",
|
||||
"noSkillsInZip": "ZIP 文件中未找到技能(需包含 SKILL.md 文件)",
|
||||
"networkError": "网络错误",
|
||||
"fsError": "文件系统错误",
|
||||
"unknownError": "未知错误",
|
||||
@@ -1069,8 +1064,7 @@
|
||||
"checkRepoUrl": "请检查仓库地址和分支名称",
|
||||
"checkDiskSpace": "请检查磁盘空间",
|
||||
"checkPermission": "请检查目录权限",
|
||||
"uninstallFirst": "请先卸载已安装的同名技能",
|
||||
"checkZipContent": "请确认 ZIP 文件包含有效的技能目录(含 SKILL.md 文件)"
|
||||
"uninstallFirst": "请先卸载已安装的同名技能"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
@@ -1119,13 +1113,6 @@
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini",
|
||||
"opencode": "OpenCode"
|
||||
},
|
||||
"installFromZip": {
|
||||
"button": "从 ZIP 安装",
|
||||
"installing": "安装中...",
|
||||
"successSingle": "技能 {{name}} 已安装",
|
||||
"successMultiple": "成功安装 {{count}} 个技能",
|
||||
"noSkillsFound": "ZIP 文件中未找到技能(需包含 SKILL.md 文件)"
|
||||
}
|
||||
},
|
||||
"deeplink": {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="1209px" height="1255px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 1017.97 1056.47"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#E4906E;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="圖層_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<path class="fil0" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -2,7 +2,6 @@
|
||||
// Do not edit manually
|
||||
|
||||
export const icons: Record<string, string> = {
|
||||
aicodemirror: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 1017.97 1056.47"><title>AICodeMirror</title><path fill="#E4906E" fill-rule="nonzero" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/></svg>`,
|
||||
aigocode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 648 564" xmlns="http://www.w3.org/2000/svg"><title>AiGoCode</title><g transform="translate(0,564) scale(0.1,-0.1)"><path fill="#7C6AEF" d="M5392 5379 c-26 -10 -36 -28 -56 -108 -24 -94 -47 -140 -101 -199 -56 -62 -117 -96 -219 -121 -101 -25 -116 -35 -116 -75 0 -45 21 -60 123 -89 190 -53 271 -147 336 -384 13 -46 38 -61 85 -49 25 6 36 28 56 111 7 28 23 72 36 99 13 28 21 54 18 58 -3 5 -2 7 3 6 4 -2 29 18 55 44 41 41 145 110 173 114 56 8 126 27 131 36 4 6 7 30 8 54 1 49 -5 54 -104 74 -164 33 -280 148 -320 320 -23 101 -52 129 -108 109z"/><path fill="#5B7FFF" d="M1770 4814 c-138 -25 -301 -93 -425 -177 -80 -55 -227 -197 -280 -272 -98 -138 -161 -279 -201 -447 -16 -66 -18 -164 -21 -1098 -4 -1130 -4 -1144 57 -1331 80 -242 222 -434 444 -598 l56 -42 0 -337 c0 -309 2 -340 19 -379 31 -66 87 -93 158 -74 22 6 202 117 404 249 200 131 398 259 439 285 144 89 48 81 1095 88 912 5 932 6 1012 27 243 64 441 178 599 344 166 176 274 408 304 648 13 110 13 2016 0 2120 -25 190 -83 347 -183 498 -160 240 -384 400 -677 484 l-75 22 -1325 2 c-1086 2 -1339 0 -1400 -12z m1068 -1455 l-3 -751 -71 -18 c-71 -18 -154 -55 -205 -91 -14 -10 -29 -16 -33 -12 -3 3 -6 91 -6 195 l0 188 -306 0 -305 0 -21 -47 c-11 -27 -33 -75 -48 -108 -16 -33 -44 -96 -62 -140 l-34 -80 -172 -3 c-101 -1 -172 1 -172 7 0 5 14 40 31 78 31 68 101 227 254 578 335 769 358 814 434 874 94 74 122 79 449 80 l272 1 -2 -751z m794 717 c41 -13 103 -42 139 -62 67 -40 202 -168 232 -221 l17 -31 -77 -65 c-43 -35 -101 -80 -129 -101 l-52 -36 -39 57 c-79 116 -204 177 -313 154 -66 -14 -105 -42 -130 -91 -19 -37 -20 -60 -20 -400 0 -339 1 -363 20 -399 26 -51 61 -78 128 -97 143 -42 327 52 366 186 l13 45 -163 3 -163 2 -3 157 c-2 111 0 157 9 160 6 2 263 3 570 1 487 -3 562 -5 593 -19 98 -45 125 -159 58 -244 -39 -50 -66 -55 -322 -55 l-236 0 -6 -27 c-18 -83 -29 -115 -62 -183 -69 -143 -230 -269 -402 -316 -81 -22 -271 -25 -350 -5 -173 43 -289 145 -341 298 -19 57 -20 81 -17 534 l3 474 33 67 c55 112 168 199 307 235 79 20 246 10 337 -21z m828 -1515 c67 -71 67 -73 -62 -396 -45 -110 -102 -254 -128 -320 -254 -639 -272 -681 -298 -702 -74 -62 -192 -15 -192 77 0 12 39 116 86 233 48 117 97 239 110 272 119 311 336 833 354 852 36 37 85 32 130 -16z m-1574 -205 c16 -13 19 -29 22 -128 l3 -113 -195 -155 c-108 -85 -196 -158 -196 -161 0 -4 17 -19 38 -35 128 -96 327 -272 339 -301 16 -39 17 -143 2 -177 -15 -33 -34 -39 -67 -22 -48 26 -566 446 -584 474 -23 35 -23 89 1 128 16 27 150 143 376 326 39 31 79 65 90 75 44 41 128 103 139 103 7 0 21 -6 32 -14z m713 -25 c52 -37 53 -33 -92 -551 -36 -129 -80 -289 -98 -355 -39 -143 -62 -175 -129 -175 -47 0 -92 20 -114 52 -24 34 -19 90 18 217 19 64 78 271 131 461 53 190 98 353 101 364 6 16 14 18 79 14 54 -4 81 -11 104 -27z m1116 -351 c55 -45 117 -98 138 -120 61 -64 48 -115 -50 -192 -32 -25 -118 -94 -191 -152 -136 -108 -163 -120 -217 -100 -26 10 -47 62 -39 97 10 46 22 62 94 119 36 29 93 77 128 106 l62 55 -65 59 c-71 65 -77 84 -49 141 24 47 44 67 68 67 12 0 66 -36 121 -80z"/><path fill="#5B7FFF" d="M2328 3755 c-37 -20 -54 -53 -153 -280 -48 -110 -96 -219 -106 -242 l-18 -43 234 0 235 0 0 290 0 290 -82 0 c-53 -1 -93 -6 -110 -15z"/></g></svg>`,
|
||||
alibaba: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Alibaba</title><path d="M24 14.014c-2.8 1.512-5.62 2.896-8.759 3.524-.7.139-1.476.139-2.187.043-.678-.085-1.017-.682-.776-1.31.23-.585.536-1.181.93-1.671.852-1.065 1.814-2.034 2.678-3.088a15.75 15.75 0 001.422-2.054c.306-.511.164-1.129-.372-1.384-.897-.437-1.859-.745-2.81-1.075-.11-.043-.274.074-.492.149.273.244.47.425.743.67-2.821.48-5.49 1.16-8.08 2.098-.012.053-.033.095-.023.117.383.585.208 1.032-.35 1.394a2.365 2.365 0 00-.568.522c1.706.5 3.226.213 4.68-.735-.087-.127-.175-.244-.262-.372.546.096.874.394.918.862.011.107-.054.213-.087.32-.077-.086-.175-.17-.24-.267-.045-.064-.056-.138-.088-.245-1.728 1.15-3.587 1.438-5.632.842 0 .404-.022.745.011 1.075.022.287-.098.415-.36.564-.591.362-1.204.735-1.696 1.214-.59.585-.371 1.299.427 1.597.907.34 1.859.35 2.81.234 1.126-.139 2.23-.32 3.456-.49-1.433.67-2.844 1.14-4.33 1.33-1.04.14-2.078.214-3.106-.084-1.476-.415-2.133-1.501-1.75-2.96.361-1.363 1.236-2.449 2.176-3.45 3.139-3.332 7.108-5.024 11.7-5.365 1.072-.074 2.155.064 3.16.511 1.411.639 2.002 1.99 1.313 3.354-.448.905-1.072 1.735-1.695 2.555-.612.809-1.301 1.554-1.946 2.331-.186.234-.361.48-.503.745-.274.5-.088.83.492.778 1.213-.118 2.45-.213 3.62-.511 1.716-.437 3.389-1.054 5.084-1.597.175-.043.339-.107.492-.17z" fill="#FF6003" fill-rule="evenodd"></path></svg>`,
|
||||
anthropic: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>`,
|
||||
@@ -59,7 +58,6 @@ export const icons: Record<string, string> = {
|
||||
siliconflow: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></svg>`,
|
||||
catcoder: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>KwaiKAT</title><path d="M20.42 19.311h3.418V1l-6.781 4.177-6.778-4.111.026 7.868h3.418l-.026-2.222 3.42 2.035 3.303-2.035v12.6z"></path><path d="M3.064 10.734c2.784-2.07 6.942-2.394 9.941.907l.01.01.01.013 9.16 12.24h-3.84l-7.69-10.217c-1.63-1.737-3.891-1.689-5.515-.638-1.624 1.05-2.563 3.073-1.548 5.28 1.494 3.246 6.152 3.275 7.725.108l.032-.064 2.02 2.629c-2.98 3.968-9.329 3.926-12.165-.552-2.395-3.78-.926-7.645 1.86-9.716z"></path></svg>`,
|
||||
mcp: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>`,
|
||||
nvidia: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Nvidia</title><path d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" fill="#74B71B" fill-rule="nonzero"></path></svg>`,
|
||||
};
|
||||
|
||||
export const iconList = Object.keys(icons);
|
||||
|
||||
@@ -359,13 +359,6 @@ export const iconMetadata: Record<string, IconMetadata> = {
|
||||
keywords: ["xiaomimimo", "xiaomi", "mimo"],
|
||||
defaultColor: "#000000",
|
||||
},
|
||||
nvidia: {
|
||||
name: "nvidia",
|
||||
displayName: "NVIDIA",
|
||||
category: "ai-provider",
|
||||
keywords: ["nvidia", "nim", "gpu"],
|
||||
defaultColor: "#74B71B",
|
||||
},
|
||||
};
|
||||
|
||||
export function getIconMetadata(name: string): IconMetadata | undefined {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Nvidia</title><path d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" fill="#74B71B" fill-rule="nonzero"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -3,7 +3,6 @@ export { providersApi, universalProvidersApi } from "./providers";
|
||||
export { settingsApi } from "./settings";
|
||||
export { mcpApi } from "./mcp";
|
||||
export { promptsApi } from "./prompts";
|
||||
export { skillsApi } from "./skills";
|
||||
export { usageApi } from "./usage";
|
||||
export { vscodeApi } from "./vscode";
|
||||
export { proxyApi } from "./proxy";
|
||||
|
||||
@@ -159,19 +159,4 @@ export const skillsApi = {
|
||||
async removeRepo(owner: string, name: string): Promise<boolean> {
|
||||
return await invoke("remove_skill_repo", { owner, name });
|
||||
},
|
||||
|
||||
// ========== ZIP 安装 ==========
|
||||
|
||||
/** 打开 ZIP 文件选择对话框 */
|
||||
async openZipFileDialog(): Promise<string | null> {
|
||||
return await invoke("open_zip_file_dialog");
|
||||
},
|
||||
|
||||
/** 从 ZIP 文件安装 Skills */
|
||||
async installFromZip(
|
||||
filePath: string,
|
||||
currentApp: AppType,
|
||||
): Promise<InstalledSkill[]> {
|
||||
return await invoke("install_skills_from_zip", { filePath, currentApp });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@ function getErrorI18nKey(code: string): string {
|
||||
SKILL_DIRECTORY_CONFLICT: "skills.error.directoryConflict",
|
||||
EMPTY_ARCHIVE: "skills.error.emptyArchive",
|
||||
GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed",
|
||||
NO_SKILLS_IN_ZIP: "skills.error.noSkillsInZip",
|
||||
};
|
||||
|
||||
return mapping[code] || "skills.error.unknownError";
|
||||
@@ -55,7 +54,6 @@ function getSuggestionI18nKey(suggestion: string): string {
|
||||
checkRepoUrl: "skills.error.suggestion.checkRepoUrl",
|
||||
checkPermission: "skills.error.suggestion.checkPermission",
|
||||
uninstallFirst: "skills.error.suggestion.uninstallFirst",
|
||||
checkZipContent: "skills.error.suggestion.checkZipContent",
|
||||
http403: "skills.error.http403",
|
||||
http404: "skills.error.http404",
|
||||
http429: "skills.error.http429",
|
||||
|
||||
@@ -61,11 +61,10 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(
|
||||
t("notifications.addFailed", {
|
||||
defaultValue: "添加供应商失败: {{error}}",
|
||||
error: detail,
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -93,11 +92,10 @@ export const useUpdateProviderMutation = (appId: AppId) => {
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(
|
||||
t("notifications.updateFailed", {
|
||||
defaultValue: "更新供应商失败: {{error}}",
|
||||
error: detail,
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -135,11 +133,10 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(
|
||||
t("notifications.deleteFailed", {
|
||||
defaultValue: "删除供应商失败: {{error}}",
|
||||
error: detail,
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -174,8 +171,17 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Success toast is handled by useProviderActions.switchProvider
|
||||
// to allow customization based on provider properties (e.g., apiFormat)
|
||||
// OpenCode: show "added to config" message instead of "switched"
|
||||
const messageKey =
|
||||
appId === "opencode"
|
||||
? "notifications.addToConfigSuccess"
|
||||
: "notifications.switchSuccess";
|
||||
const defaultMessage =
|
||||
appId === "opencode" ? "已添加到配置" : "切换供应商成功";
|
||||
|
||||
toast.success(t(messageKey, { defaultValue: defaultMessage }), {
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
|
||||
@@ -139,20 +139,11 @@ export interface ProviderMeta {
|
||||
costMultiplier?: string;
|
||||
// 供应商计费模式来源
|
||||
pricingModelSource?: string;
|
||||
// Claude API 格式(仅 Claude 供应商使用)
|
||||
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||
apiFormat?: "anthropic" | "openai_chat";
|
||||
}
|
||||
|
||||
// Skill 同步方式
|
||||
export type SkillSyncMethod = "auto" | "symlink" | "copy";
|
||||
|
||||
// Claude API 格式类型
|
||||
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||
export type ClaudeApiFormat = "anthropic" | "openai_chat";
|
||||
|
||||
// 主页面显示的应用配置
|
||||
export interface VisibleApps {
|
||||
claude: boolean;
|
||||
|
||||
Reference in New Issue
Block a user