mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-14 22:21:31 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 831d2c962c | |||
| 75b4ef2299 | |||
| fab9874b2c | |||
| 84668e2307 | |||
| b4033fdd29 | |||
| 68da178811 | |||
| 471c0d9990 | |||
| a9c36381fc | |||
| 3b3d1cd0ba | |||
| 3fa3558bd4 | |||
| 788c221d39 | |||
| a86f22422b | |||
| 6427ab2128 | |||
| 87b08ce242 |
@@ -24,3 +24,4 @@ flatpak/cc-switch.deb
|
||||
flatpak-build/
|
||||
flatpak-repo/
|
||||
.worktrees/
|
||||
.spec-workflow/
|
||||
@@ -33,6 +33,7 @@ Presets are pre-configured provider templates that only require an API Key to us
|
||||
| Bailian | Alibaba Cloud Bailian (Qwen) |
|
||||
| Kimi | Moonshot Kimi model |
|
||||
| Kimi For Coding | Kimi coding-specific model |
|
||||
| StepFun | StepFun model |
|
||||
| ModelScope | ModelScope community |
|
||||
| KAT-Coder | KAT-Coder model |
|
||||
| Longcat | Longcat AI |
|
||||
@@ -92,6 +93,7 @@ Presets are pre-configured provider templates that only require an API Key to us
|
||||
| Bailian | Alibaba Cloud Bailian |
|
||||
| Kimi k2.5 | Moonshot Kimi-k2.5 model |
|
||||
| Kimi For Coding | Kimi coding-specific model |
|
||||
| StepFun | StepFun model |
|
||||
| ModelScope | ModelScope community |
|
||||
| KAT-Coder | KAT-Coder model |
|
||||
| Longcat | Longcat AI |
|
||||
@@ -124,6 +126,7 @@ Presets are pre-configured provider templates that only require an API Key to us
|
||||
| Qwen Coder | Qwen coding model |
|
||||
| Kimi k2.5 | Moonshot Kimi-k2.5 model |
|
||||
| Kimi For Coding | Kimi coding-specific model |
|
||||
| StepFun | StepFun model |
|
||||
| MiniMax | MiniMax model |
|
||||
| MiniMax en | MiniMax (English version) |
|
||||
| KAT-Coder | KAT-Coder model |
|
||||
|
||||
@@ -238,10 +238,14 @@ CC Switch includes preset official prices for common models (per million tokens)
|
||||
| gemini-2.5-pro | $1.25 | $10 | $0.125 |
|
||||
| gemini-2.5-flash | $0.30 | $2.50 | $0.03 |
|
||||
|
||||
**Chinese Provider Models (CNY)**:
|
||||
**Chinese Provider Models**:
|
||||
|
||||
> Note: Currency follows each provider's official pricing page. StepFun is currently listed in USD.
|
||||
|
||||
| Model | Input | Output | Cache Read |
|
||||
|-------|-------|--------|------------|
|
||||
| **StepFun** | | | |
|
||||
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
|
||||
| **DeepSeek** | | | |
|
||||
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
|
||||
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| 百炼 | アリクラウド百炼(通义千問) |
|
||||
| Kimi | Moonshot Kimi モデル |
|
||||
| Kimi For Coding | Kimi プログラミング専用モデル |
|
||||
| StepFun | StepFun モデル |
|
||||
| ModelScope | 魔搭コミュニティ |
|
||||
| KAT-Coder | KAT-Coder モデル |
|
||||
| Longcat | Longcat AI |
|
||||
@@ -92,6 +93,7 @@
|
||||
| 百炼 | アリクラウド百炼 |
|
||||
| Kimi k2.5 | Moonshot Kimi-k2.5 モデル |
|
||||
| Kimi For Coding | Kimi プログラミング専用モデル |
|
||||
| StepFun | StepFun モデル |
|
||||
| ModelScope | 魔搭コミュニティ |
|
||||
| KAT-Coder | KAT-Coder モデル |
|
||||
| Longcat | Longcat AI |
|
||||
@@ -124,6 +126,7 @@
|
||||
| Qwen Coder | 通义千問コーディングモデル |
|
||||
| Kimi k2.5 | Moonshot Kimi-k2.5 モデル |
|
||||
| Kimi For Coding | Kimi プログラミング専用モデル |
|
||||
| StepFun | StepFun モデル |
|
||||
| MiniMax | MiniMax モデル |
|
||||
| MiniMax en | MiniMax(英語版) |
|
||||
| KAT-Coder | KAT-Coder モデル |
|
||||
|
||||
@@ -238,10 +238,14 @@ CC Switch は一般的なモデルの公式価格(100 万 Token あたり)
|
||||
| gemini-2.5-pro | $1.25 | $10 | $0.125 |
|
||||
| gemini-2.5-flash | $0.30 | $2.50 | $0.03 |
|
||||
|
||||
**中国メーカーのモデル(人民元)**:
|
||||
**中国メーカーのモデル**:
|
||||
|
||||
> 注: 通貨は各プロバイダーの公式料金ページに従います。StepFun は現在 USD 表記です。
|
||||
|
||||
| モデル | 入力 | 出力 | キャッシュ読取 |
|
||||
|------|------|------|----------|
|
||||
| **StepFun** | | | |
|
||||
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
|
||||
| **DeepSeek** | | | |
|
||||
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
|
||||
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| 百炼 | 阿里云百炼(通义千问) |
|
||||
| Kimi | Moonshot Kimi 模型 |
|
||||
| Kimi For Coding | Kimi 编程专用模型 |
|
||||
| StepFun | 阶跃星辰 Step模型 |
|
||||
| ModelScope | 魔搭社区 |
|
||||
| KAT-Coder | KAT-Coder 模型 |
|
||||
| Longcat | Longcat AI |
|
||||
@@ -92,6 +93,7 @@
|
||||
| 百炼 | 阿里云百炼 |
|
||||
| Kimi k2.5 | Moonshot Kimi-k2.5 模型 |
|
||||
| Kimi For Coding | Kimi 编程专用模型 |
|
||||
| StepFun | 阶跃星辰 Step模型 |
|
||||
| ModelScope | 魔搭社区 |
|
||||
| KAT-Coder | KAT-Coder 模型 |
|
||||
| Longcat | Longcat AI |
|
||||
@@ -124,6 +126,7 @@
|
||||
| Qwen Coder | 通义千问编码模型 |
|
||||
| Kimi k2.5 | Moonshot Kimi-k2.5 模型 |
|
||||
| Kimi For Coding | Kimi 编程专用模型 |
|
||||
| StepFun | 阶跃星辰 Step模型 |
|
||||
| MiniMax | MiniMax 模型 |
|
||||
| MiniMax en | MiniMax(英文版) |
|
||||
| KAT-Coder | KAT-Coder 模型 |
|
||||
@@ -352,4 +355,3 @@ CC Switch 支持两种方式导入供应商配置:
|
||||
- 🔴 红色:延迟 > 1000ms(较慢)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -238,10 +238,14 @@ CC Switch 预设了常用模型的官方价格(每百万 Token):
|
||||
| gemini-2.5-pro | $1.25 | $10 | $0.125 |
|
||||
| gemini-2.5-flash | $0.30 | $2.50 | $0.03 |
|
||||
|
||||
**中国厂商模型(人民币)**:
|
||||
**中国厂商模型**:
|
||||
|
||||
> 注:币种遵循各供应商官方定价页面。StepFun 当前按美元列出。
|
||||
|
||||
| 模型 | 输入 | 输出 | 缓存读取 |
|
||||
|------|------|------|----------|
|
||||
| **StepFun** | | | |
|
||||
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
|
||||
| **DeepSeek** | | | |
|
||||
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
|
||||
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
|
||||
|
||||
@@ -6,7 +6,7 @@ const ICONS_TO_EXTRACT = {
|
||||
// AI 服务商(必需)
|
||||
aiProviders: [
|
||||
'openai', 'anthropic', 'claude', 'google', 'gemini',
|
||||
'deepseek', 'kimi', 'moonshot', 'zhipu', 'minimax',
|
||||
'deepseek', 'kimi', 'moonshot', 'stepfun', 'zhipu', 'minimax',
|
||||
'baidu', 'alibaba', 'tencent', 'meta', 'microsoft',
|
||||
'cohere', 'perplexity', 'mistral', 'huggingface'
|
||||
],
|
||||
@@ -60,6 +60,9 @@ ALL_ICONS.forEach(iconName => {
|
||||
fs.copyFileSync(sourceFile, targetFile);
|
||||
console.log(` ✓ ${iconName}.svg`);
|
||||
extracted++;
|
||||
} else if (fs.existsSync(targetFile)) {
|
||||
console.log(` ✓ ${iconName}.svg (kept local custom icon)`);
|
||||
extracted++;
|
||||
} else {
|
||||
console.log(` ✗ ${iconName}.svg (not found)`);
|
||||
notFound.push(iconName);
|
||||
@@ -110,6 +113,7 @@ export const iconMetadata: Record<string, IconMetadata> = {
|
||||
deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' },
|
||||
moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' },
|
||||
kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' },
|
||||
stepfun: { name: 'stepfun', displayName: 'StepFun', category: 'ai-provider', keywords: ['stepfun', 'step', 'jieyue', '阶跃星辰'], defaultColor: '#005AFF' },
|
||||
zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' },
|
||||
minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' },
|
||||
baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' },
|
||||
|
||||
@@ -10,7 +10,7 @@ const KEEP_LIST = [
|
||||
'openai', 'anthropic', 'claude', 'google', 'gemini', 'gemma', 'palm',
|
||||
'microsoft', 'azure', 'copilot', 'meta', 'llama',
|
||||
'alibaba', 'qwen', 'tencent', 'hunyuan', 'baidu', 'wenxin',
|
||||
'bytedance', 'doubao', 'deepseek', 'moonshot', 'kimi',
|
||||
'bytedance', 'doubao', 'deepseek', 'moonshot', 'kimi', 'stepfun',
|
||||
'zhipu', 'chatglm', 'glm', 'minimax', 'mistral', 'cohere',
|
||||
'perplexity', 'huggingface', 'midjourney', 'stability',
|
||||
'xai', 'grok', 'yi', 'zeroone', 'ollama',
|
||||
|
||||
@@ -15,6 +15,7 @@ const KNOWN_METADATA = {
|
||||
deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' },
|
||||
moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' },
|
||||
kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' },
|
||||
stepfun: { name: 'stepfun', displayName: 'StepFun', category: 'ai-provider', keywords: ['stepfun', 'step', 'jieyue', '阶跃星辰'], defaultColor: '#005AFF' },
|
||||
zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' },
|
||||
minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' },
|
||||
baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' },
|
||||
|
||||
@@ -1241,6 +1241,15 @@ impl Database {
|
||||
"0.03",
|
||||
"0",
|
||||
),
|
||||
// StepFun 系列
|
||||
(
|
||||
"step-3.5-flash",
|
||||
"Step 3.5 Flash",
|
||||
"0.10",
|
||||
"0.30",
|
||||
"0.02",
|
||||
"0",
|
||||
),
|
||||
// ====== 国产模型 (CNY/1M tokens) ======
|
||||
// Doubao (字节跳动)
|
||||
(
|
||||
|
||||
@@ -242,9 +242,14 @@ pub struct ProviderMeta {
|
||||
/// - "openai_responses": OpenAI Responses API 格式,需要转换
|
||||
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
|
||||
pub api_format: Option<String>,
|
||||
/// Claude 认证字段名("ANTHROPIC_AUTH_TOKEN" 或 "ANTHROPIC_API_KEY")
|
||||
/// Claude 认证字段名(仅 Claude 供应商使用)
|
||||
/// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商
|
||||
/// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
|
||||
#[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")]
|
||||
pub api_key_field: Option<String>,
|
||||
/// 是否将 base_url 视为完整 API 端点(不拼接 endpoint 路径)
|
||||
#[serde(rename = "isFullUrl", skip_serializing_if = "Option::is_none")]
|
||||
pub is_full_url: Option<bool>,
|
||||
/// Prompt cache key for OpenAI-compatible endpoints.
|
||||
/// When set, injected into converted requests to improve cache hit rate.
|
||||
/// If not set, provider ID is used automatically during format conversion.
|
||||
|
||||
@@ -792,21 +792,61 @@ impl RequestForwarder {
|
||||
// 检查是否需要格式转换
|
||||
let needs_transform = adapter.needs_transform(provider);
|
||||
|
||||
let effective_endpoint =
|
||||
if needs_transform && adapter.name() == "Claude" && endpoint == "/v1/messages" {
|
||||
// 根据 api_format 选择目标端点
|
||||
let api_format = super::providers::get_claude_api_format(provider);
|
||||
if api_format == "openai_responses" {
|
||||
"/v1/responses"
|
||||
// 检查 isFullUrl 模式:直接使用 base_url 作为完整 API 端点
|
||||
let is_full_url = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.is_full_url)
|
||||
.unwrap_or(false);
|
||||
|
||||
let url = if is_full_url {
|
||||
// 全链接模式:直接使用 base_url,将客户端 query 追加
|
||||
let query = endpoint.split_once('?').map(|(_, q)| q);
|
||||
match query {
|
||||
Some(q) if !q.is_empty() => {
|
||||
if base_url.contains('?') {
|
||||
format!("{base_url}&{q}")
|
||||
} else {
|
||||
format!("{base_url}?{q}")
|
||||
}
|
||||
}
|
||||
_ => base_url.clone(),
|
||||
}
|
||||
} else {
|
||||
// 正常模式:endpoint 可能带 query string,需拆分路径和参数
|
||||
let effective_endpoint: String = if needs_transform && adapter.name() == "Claude" {
|
||||
let (path, query) = match endpoint.split_once('?') {
|
||||
Some((p, q)) => (p, Some(q)),
|
||||
None => (endpoint, None),
|
||||
};
|
||||
if path == "/v1/messages" || path == "/claude/v1/messages" {
|
||||
// 转换到 OpenAI 兼容端点时剥离 beta=true(Anthropic 专有参数)
|
||||
let api_format = super::providers::get_claude_api_format(provider);
|
||||
let target_path = if api_format == "openai_responses" {
|
||||
"/v1/responses"
|
||||
} else {
|
||||
"/v1/chat/completions"
|
||||
};
|
||||
let filtered = query.map(|q| {
|
||||
q.split('&')
|
||||
.filter(|p| !p.starts_with("beta="))
|
||||
.collect::<Vec<_>>()
|
||||
.join("&")
|
||||
});
|
||||
match filtered.as_deref() {
|
||||
Some(q) if !q.is_empty() => format!("{target_path}?{q}"),
|
||||
_ => target_path.to_string(),
|
||||
}
|
||||
} else {
|
||||
"/v1/chat/completions"
|
||||
endpoint.to_string()
|
||||
}
|
||||
} else {
|
||||
endpoint
|
||||
endpoint.to_string()
|
||||
};
|
||||
|
||||
// 使用适配器构建 URL
|
||||
let url = adapter.build_url(&base_url, effective_endpoint);
|
||||
// 使用适配器构建 URL
|
||||
adapter.build_url(&base_url, &effective_endpoint)
|
||||
};
|
||||
|
||||
// 应用模型映射(独立于格式转换)
|
||||
let (mapped_body, _original_model, _mapped_model) =
|
||||
|
||||
@@ -61,12 +61,19 @@ pub async fn get_status(State(state): State<ProxyState>) -> Result<Json<ProxySta
|
||||
/// - 现在 OpenRouter 已推出 Claude Code 兼容接口,默认不再启用该转换(逻辑保留以备回退)
|
||||
pub async fn handle_messages(
|
||||
State(state): State<ProxyState>,
|
||||
uri: axum::http::Uri,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<Value>,
|
||||
) -> Result<axum::response::Response, ProxyError> {
|
||||
let mut ctx =
|
||||
RequestContext::new(&state, &body, &headers, AppType::Claude, "Claude", "claude").await?;
|
||||
|
||||
// 提取完整路径+query(如 "/v1/messages?beta=true"),透传客户端 URI
|
||||
let endpoint = uri
|
||||
.path_and_query()
|
||||
.map(|pq| pq.as_str())
|
||||
.unwrap_or(uri.path());
|
||||
|
||||
let is_stream = body
|
||||
.get("stream")
|
||||
.and_then(|s| s.as_bool())
|
||||
@@ -77,7 +84,7 @@ pub async fn handle_messages(
|
||||
let result = match forwarder
|
||||
.forward_with_retry(
|
||||
&AppType::Claude,
|
||||
"/v1/messages",
|
||||
endpoint,
|
||||
body.clone(),
|
||||
headers,
|
||||
ctx.get_providers(),
|
||||
|
||||
@@ -261,6 +261,8 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
//
|
||||
// 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。
|
||||
// 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。
|
||||
//
|
||||
// ?beta=true 不再由代理注入,而是由客户端自身发送并透传。
|
||||
|
||||
let mut base = format!(
|
||||
"{}/{}",
|
||||
@@ -273,19 +275,7 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
base = base.replace("/v1/v1", "/v1");
|
||||
}
|
||||
|
||||
// 为 Claude 原生 /v1/messages 端点添加 ?beta=true 参数
|
||||
// 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数
|
||||
// 注意:不要为 OpenAI Chat Completions (/v1/chat/completions) 添加此参数
|
||||
// 当 apiFormat="openai_chat" 时,请求会转发到 /v1/chat/completions,
|
||||
// 但该端点是 OpenAI 标准,不支持 ?beta=true 参数
|
||||
if endpoint.contains("/v1/messages")
|
||||
&& !endpoint.contains("/v1/chat/completions")
|
||||
&& !endpoint.contains('?')
|
||||
{
|
||||
format!("{base}?beta=true")
|
||||
} else {
|
||||
base
|
||||
}
|
||||
base
|
||||
}
|
||||
|
||||
fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder {
|
||||
@@ -522,23 +512,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_url_anthropic() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// /v1/messages 端点会自动添加 ?beta=true 参数
|
||||
// ?beta=true 不再由代理注入,而是由客户端自身发送并透传
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_openrouter() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// /v1/messages 端点会自动添加 ?beta=true 参数
|
||||
let url = adapter.build_url("https://openrouter.ai/api", "/v1/messages");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_no_beta_for_other_endpoints() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// 非 /v1/messages 端点不添加 ?beta=true
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/complete");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/complete");
|
||||
}
|
||||
@@ -546,16 +534,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_url_preserve_existing_query() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// 已有查询参数时不重复添加
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages?foo=bar");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?foo=bar");
|
||||
// 客户端携带的 query 参数原样透传
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_no_beta_for_openai_chat_completions() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
// OpenAI Chat Completions 端点不添加 ?beta=true
|
||||
// 这是 Nvidia 等 apiFormat="openai_chat" 供应商使用的端点
|
||||
let url = adapter.build_url("https://integrate.api.nvidia.com", "/v1/chat/completions");
|
||||
assert_eq!(url, "https://integrate.api.nvidia.com/v1/chat/completions");
|
||||
}
|
||||
|
||||
@@ -313,8 +313,19 @@ impl StreamCheckService {
|
||||
|
||||
let is_openai_chat = api_format == "openai_chat";
|
||||
|
||||
// URL: /v1/chat/completions for openai_chat, /v1/messages?beta=true for anthropic
|
||||
let url = if is_openai_chat {
|
||||
let is_full_url = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.is_full_url)
|
||||
.unwrap_or(false);
|
||||
|
||||
// URL rules:
|
||||
// - full URL mode: use base_url as-is
|
||||
// - openai_chat: /v1/chat/completions
|
||||
// - anthropic: /v1/messages?beta=true
|
||||
let url = if is_full_url {
|
||||
base_url.to_string()
|
||||
} else if is_openai_chat {
|
||||
if base.ends_with("/v1") {
|
||||
format!("{base}/chat/completions")
|
||||
} else {
|
||||
|
||||
@@ -661,11 +661,8 @@ fn validate_artifact_size_limit(artifact_name: &str, size: u64) -> Result<(), Ap
|
||||
let max_mb = MAX_SYNC_ARTIFACT_BYTES / 1024 / 1024;
|
||||
return Err(localized(
|
||||
"webdav.sync.artifact_too_large",
|
||||
format!("artifact {artifact_name} 超过下载上限({} MB)", max_mb),
|
||||
format!(
|
||||
"Artifact {artifact_name} exceeds download limit ({} MB)",
|
||||
max_mb
|
||||
),
|
||||
format!("artifact {artifact_name} 超过下载上限({max_mb} MB)"),
|
||||
format!("Artifact {artifact_name} exceeds download limit ({max_mb} MB)"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -350,8 +350,8 @@ fn copy_entry_with_total_limit<R: Read, W: Write>(
|
||||
let max_mb = max_total_bytes / 1024 / 1024;
|
||||
return Err(localized(
|
||||
"webdav.sync.skills_zip_too_large",
|
||||
format!("skills.zip 解压后体积超过上限({} MB)", max_mb),
|
||||
format!("skills.zip extracted size exceeds limit ({} MB)", max_mb),
|
||||
format!("skills.zip 解压后体积超过上限({max_mb} MB)"),
|
||||
format!("skills.zip extracted size exceeds limit ({max_mb} MB)"),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -255,7 +255,7 @@ function App() {
|
||||
deleteProvider,
|
||||
saveUsageScript,
|
||||
setAsDefaultModel,
|
||||
} = useProviderActions(activeApp);
|
||||
} = useProviderActions(activeApp, isProxyRunning);
|
||||
|
||||
const disableOmoMutation = useDisableCurrentOmo();
|
||||
const handleDisableOmo = () => {
|
||||
@@ -996,10 +996,10 @@ function App() {
|
||||
)}
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
className="flex flex-1 min-w-0 overflow-x-hidden justify-end items-center"
|
||||
className="flex flex-1 min-w-0 overflow-x-hidden items-center"
|
||||
>
|
||||
<div
|
||||
className="flex shrink-0 items-center gap-1.5"
|
||||
className="flex shrink-0 items-center gap-1.5 ml-auto"
|
||||
style={{ WebkitAppRegion: "no-drag" } as any}
|
||||
>
|
||||
{currentView === "prompts" && (
|
||||
|
||||
@@ -77,10 +77,10 @@ const ToolsPanel: React.FC = () => {
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const { allow, deny, ...other } = config;
|
||||
const { profile, allow, deny, ...other } = config;
|
||||
const newConfig: OpenClawToolsConfig = {
|
||||
...other,
|
||||
profile: config.profile,
|
||||
profile,
|
||||
allow: allowList.map((item) => item.value).filter((s) => s.trim()),
|
||||
deny: denyList.map((item) => item.value).filter((s) => s.trim()),
|
||||
};
|
||||
|
||||
@@ -73,9 +73,13 @@ interface ClaudeFormFieldsProps {
|
||||
apiFormat: ClaudeApiFormat;
|
||||
onApiFormatChange: (format: ClaudeApiFormat) => void;
|
||||
|
||||
// Auth Field (ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY)
|
||||
// Auth Key Field (ANTHROPIC_AUTH_TOKEN vs ANTHROPIC_API_KEY)
|
||||
apiKeyField: ClaudeApiKeyField;
|
||||
onApiKeyFieldChange: (field: ClaudeApiKeyField) => void;
|
||||
|
||||
// Full URL mode
|
||||
isFullUrl: boolean;
|
||||
onFullUrlChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function ClaudeFormFields({
|
||||
@@ -112,6 +116,8 @@ export function ClaudeFormFields({
|
||||
onApiFormatChange,
|
||||
apiKeyField,
|
||||
onApiKeyFieldChange,
|
||||
isFullUrl,
|
||||
onFullUrlChange,
|
||||
}: ClaudeFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -181,6 +187,9 @@ export function ClaudeFormFields({
|
||||
: t("providerForm.apiHint")
|
||||
}
|
||||
onManageClick={() => onEndpointModalToggle(true)}
|
||||
showFullUrlToggle={true}
|
||||
isFullUrl={isFullUrl}
|
||||
onFullUrlChange={onFullUrlChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -236,17 +245,17 @@ export function ClaudeFormFields({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 认证字段选择器 */}
|
||||
{/* 认证字段选择(仅非官方供应商显示) */}
|
||||
{shouldShowModelSelector && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
<FormLabel htmlFor="apiKeyField">
|
||||
{t("providerForm.authField", { defaultValue: "认证字段" })}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={apiKeyField}
|
||||
onValueChange={(v) => onApiKeyFieldChange(v as ClaudeApiKeyField)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger id="apiKeyField" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ApiKeySection } from "./shared";
|
||||
import { openclawApiProtocols } from "@/config/openclawProviderPresets";
|
||||
import type { ProviderCategory, OpenClawModel } from "@/types";
|
||||
@@ -101,6 +102,7 @@ export function OpenClawFormFields({
|
||||
contextWindow: undefined,
|
||||
maxTokens: undefined,
|
||||
cost: undefined,
|
||||
input: ["text"],
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -339,7 +341,66 @@ export function OpenClawFormFields({
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 pt-2">
|
||||
{/* Context Window, Max Tokens and Reasoning row */}
|
||||
{/* Reasoning, Input Types row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.reasoning", {
|
||||
defaultValue: "推理模式",
|
||||
})}
|
||||
</label>
|
||||
<div className="flex items-center h-9 gap-2">
|
||||
<Switch
|
||||
checked={model.reasoning ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleModelChange(index, "reasoning", checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.reasoning
|
||||
? t("openclaw.reasoningOn", {
|
||||
defaultValue: "启用",
|
||||
})
|
||||
: t("openclaw.reasoningOff", {
|
||||
defaultValue: "关闭",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.inputTypes", {
|
||||
defaultValue: "输入类型",
|
||||
})}
|
||||
</label>
|
||||
{/* "text" is checked by default but can be unchecked —
|
||||
some models genuinely don't support text input, and
|
||||
OpenClaw works fine with an empty or image-only array. */}
|
||||
<div className="flex items-center gap-4 h-9">
|
||||
{(["text", "image"] as const).map((type) => (
|
||||
<label
|
||||
key={type}
|
||||
className="flex items-center gap-1.5 cursor-pointer select-none"
|
||||
>
|
||||
<Checkbox
|
||||
checked={(model.input ?? ["text"]).includes(type)}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = model.input ?? ["text"];
|
||||
const next = checked
|
||||
? [...new Set([...current, type])]
|
||||
: current.filter((v) => v !== type);
|
||||
handleModelChange(index, "input", next);
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{type}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Context Window and Max Tokens row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
@@ -383,30 +444,7 @@ export function OpenClawFormFields({
|
||||
placeholder="32000"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.reasoning", {
|
||||
defaultValue: "推理模式",
|
||||
})}
|
||||
</label>
|
||||
<div className="flex items-center h-9 gap-2">
|
||||
<Switch
|
||||
checked={model.reasoning ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleModelChange(index, "reasoning", checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.reasoning
|
||||
? t("openclaw.reasoningOn", {
|
||||
defaultValue: "启用",
|
||||
})
|
||||
: t("openclaw.reasoningOff", {
|
||||
defaultValue: "关闭",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Cost row */}
|
||||
|
||||
@@ -197,6 +197,9 @@ export function ProviderForm({
|
||||
setDraftCustomEndpoints([]);
|
||||
}
|
||||
setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true);
|
||||
setLocalIsFullUrl(
|
||||
appId === "claude" ? (initialData?.meta?.isFullUrl ?? false) : false,
|
||||
);
|
||||
setTestConfig(initialData?.meta?.testConfig ?? { enabled: false });
|
||||
setProxyConfig(initialData?.meta?.proxyConfig ?? { enabled: false });
|
||||
setPricingConfig({
|
||||
@@ -245,6 +248,20 @@ export function ProviderForm({
|
||||
[form],
|
||||
);
|
||||
|
||||
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
||||
if (appId !== "claude") return "anthropic";
|
||||
return initialData?.meta?.apiFormat ?? "anthropic";
|
||||
});
|
||||
|
||||
const [localIsFullUrl, setLocalIsFullUrl] = useState<boolean>(() => {
|
||||
if (appId !== "claude") return false;
|
||||
return initialData?.meta?.isFullUrl ?? false;
|
||||
});
|
||||
|
||||
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
||||
setLocalApiFormat(format);
|
||||
}, []);
|
||||
|
||||
const [localApiKeyField, setLocalApiKeyField] = useState<ClaudeApiKeyField>(
|
||||
() => {
|
||||
if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN";
|
||||
@@ -256,7 +273,6 @@ export function ProviderForm({
|
||||
return "ANTHROPIC_AUTH_TOKEN";
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
handleApiKeyChange,
|
||||
@@ -291,15 +307,6 @@ export function ProviderForm({
|
||||
onConfigChange: handleSettingsConfigChange,
|
||||
});
|
||||
|
||||
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
||||
if (appId !== "claude") return "anthropic";
|
||||
return initialData?.meta?.apiFormat ?? "anthropic";
|
||||
});
|
||||
|
||||
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
||||
setLocalApiFormat(format);
|
||||
}, []);
|
||||
|
||||
const handleApiKeyFieldChange = useCallback(
|
||||
(field: ClaudeApiKeyField) => {
|
||||
const prev = localApiKeyField;
|
||||
@@ -323,7 +330,6 @@ export function ProviderForm({
|
||||
},
|
||||
[localApiKeyField, form, handleSettingsConfigChange],
|
||||
);
|
||||
|
||||
const {
|
||||
codexAuth,
|
||||
codexConfig,
|
||||
@@ -888,6 +894,10 @@ export function ProviderForm({
|
||||
localApiKeyField !== "ANTHROPIC_AUTH_TOKEN"
|
||||
? localApiKeyField
|
||||
: undefined,
|
||||
isFullUrl:
|
||||
appId === "claude" && category !== "official" && localIsFullUrl
|
||||
? true
|
||||
: undefined,
|
||||
};
|
||||
|
||||
onSubmit(payload);
|
||||
@@ -1335,6 +1345,8 @@ export function ProviderForm({
|
||||
onApiFormatChange={handleApiFormatChange}
|
||||
apiKeyField={localApiKeyField}
|
||||
onApiKeyFieldChange={handleApiKeyFieldChange}
|
||||
isFullUrl={localIsFullUrl}
|
||||
onFullUrlChange={setLocalIsFullUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useApiKeyState({
|
||||
}: UseApiKeyStateProps) {
|
||||
const [apiKey, setApiKey] = useState(() => {
|
||||
if (initialConfig) {
|
||||
return getApiKeyFromConfig(initialConfig, appType);
|
||||
return getApiKeyFromConfig(initialConfig, appType, apiKeyField);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
@@ -46,11 +46,11 @@ export function useApiKeyState({
|
||||
}
|
||||
|
||||
// 从配置中提取 API Key(如果不存在则返回空字符串)
|
||||
const extracted = getApiKeyFromConfig(initialConfig, appType);
|
||||
const extracted = getApiKeyFromConfig(initialConfig, appType, apiKeyField);
|
||||
if (extracted !== apiKey) {
|
||||
setApiKey(extracted);
|
||||
}
|
||||
}, [initialConfig, appType, apiKey]);
|
||||
}, [initialConfig, appType, apiKeyField, apiKey]);
|
||||
|
||||
const handleApiKeyChange = useCallback(
|
||||
(key: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Zap } from "lucide-react";
|
||||
import { Zap, Link2 } from "lucide-react";
|
||||
|
||||
interface EndpointFieldProps {
|
||||
id: string;
|
||||
@@ -13,6 +13,9 @@ interface EndpointFieldProps {
|
||||
showManageButton?: boolean;
|
||||
onManageClick?: () => void;
|
||||
manageButtonLabel?: string;
|
||||
showFullUrlToggle?: boolean;
|
||||
isFullUrl?: boolean;
|
||||
onFullUrlChange?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function EndpointField({
|
||||
@@ -25,6 +28,9 @@ export function EndpointField({
|
||||
showManageButton = true,
|
||||
onManageClick,
|
||||
manageButtonLabel,
|
||||
showFullUrlToggle = false,
|
||||
isFullUrl = false,
|
||||
onFullUrlChange,
|
||||
}: EndpointFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -55,6 +61,35 @@ export function EndpointField({
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{showFullUrlToggle && onFullUrlChange && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFullUrlChange(!isFullUrl)}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border transition-colors ${
|
||||
isFullUrl
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/30"
|
||||
}`}
|
||||
>
|
||||
<Link2 className="h-3.5 w-3.5" />
|
||||
{isFullUrl
|
||||
? t("providerForm.fullUrlEnabled", {
|
||||
defaultValue: "完整 URL 模式",
|
||||
})
|
||||
: t("providerForm.fullUrlDisabled", {
|
||||
defaultValue: "标记为完整 URL",
|
||||
})}
|
||||
</button>
|
||||
{isFullUrl && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("providerForm.fullUrlHint", {
|
||||
defaultValue: "代理将直接使用此 URL,不拼接路径",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hint ? (
|
||||
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">{hint}</p>
|
||||
|
||||
@@ -178,6 +178,26 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "kimi",
|
||||
iconColor: "#6366F1",
|
||||
},
|
||||
{
|
||||
name: "StepFun",
|
||||
websiteUrl: "https://platform.stepfun.ai",
|
||||
apiKeyUrl: "https://platform.stepfun.ai/interface-key",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.stepfun.ai/v1",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "step-3.5-flash",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "step-3.5-flash",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "step-3.5-flash",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "step-3.5-flash",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
endpointCandidates: ["https://api.stepfun.ai/v1"],
|
||||
apiFormat: "openai_chat",
|
||||
icon: "stepfun",
|
||||
iconColor: "#005AFF",
|
||||
},
|
||||
{
|
||||
name: "ModelScope",
|
||||
websiteUrl: "https://modelscope.cn",
|
||||
|
||||
@@ -15,6 +15,8 @@ const iconMappings = {
|
||||
aliyun: { icon: "alibaba", iconColor: "#FF6A00" },
|
||||
kimi: { icon: "kimi", iconColor: "#6366F1" },
|
||||
moonshot: { icon: "moonshot", iconColor: "#6366F1" },
|
||||
stepfun: { icon: "stepfun", iconColor: "#005AFF" },
|
||||
step: { icon: "stepfun", iconColor: "#005AFF" },
|
||||
baidu: { icon: "baidu", iconColor: "#2932E1" },
|
||||
tencent: { icon: "tencent", iconColor: "#00A4FF" },
|
||||
hunyuan: { icon: "hunyuan", iconColor: "#00A4FF" },
|
||||
|
||||
@@ -292,6 +292,43 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
modelCatalog: { "kimi-coding/kimi-for-coding": { alias: "Kimi" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "StepFun",
|
||||
websiteUrl: "https://platform.stepfun.ai",
|
||||
apiKeyUrl: "https://platform.stepfun.ai/interface-key",
|
||||
settingsConfig: {
|
||||
baseUrl: "https://api.stepfun.ai/v1",
|
||||
apiKey: "",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "step-3.5-flash",
|
||||
name: "Step 3.5 Flash",
|
||||
contextWindow: 262144,
|
||||
},
|
||||
],
|
||||
},
|
||||
category: "cn_official",
|
||||
icon: "stepfun",
|
||||
iconColor: "#005AFF",
|
||||
templateValues: {
|
||||
baseUrl: {
|
||||
label: "Base URL",
|
||||
placeholder: "https://api.stepfun.ai/v1",
|
||||
defaultValue: "https://api.stepfun.ai/v1",
|
||||
editorValue: "",
|
||||
},
|
||||
apiKey: {
|
||||
label: "API Key",
|
||||
placeholder: "step-...",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "stepfun/step-3.5-flash" },
|
||||
modelCatalog: { "stepfun/step-3.5-flash": { alias: "StepFun" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MiniMax",
|
||||
websiteUrl: "https://platform.minimaxi.com",
|
||||
@@ -421,6 +458,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
baseUrl: "https://api.longcat.chat/v1",
|
||||
apiKey: "",
|
||||
api: "openai-completions",
|
||||
authHeader: true,
|
||||
models: [
|
||||
{
|
||||
id: "LongCat-Flash-Chat",
|
||||
|
||||
@@ -61,6 +61,11 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
|
||||
outputLimit: 262144,
|
||||
modalities: { input: ["text", "image", "video"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "step-3.5-flash",
|
||||
name: "Step 3.5 Flash",
|
||||
contextLimit: 262144,
|
||||
},
|
||||
],
|
||||
"@ai-sdk/google": [
|
||||
{
|
||||
@@ -469,6 +474,38 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "StepFun",
|
||||
websiteUrl: "https://platform.stepfun.ai",
|
||||
apiKeyUrl: "https://platform.stepfun.ai/interface-key",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "StepFun",
|
||||
options: {
|
||||
baseURL: "https://api.stepfun.ai/v1",
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"step-3.5-flash": { name: "Step 3.5 Flash" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
icon: "stepfun",
|
||||
iconColor: "#005AFF",
|
||||
templateValues: {
|
||||
baseURL: {
|
||||
label: "Base URL",
|
||||
placeholder: "https://api.stepfun.ai/v1",
|
||||
defaultValue: "https://api.stepfun.ai/v1",
|
||||
editorValue: "",
|
||||
},
|
||||
apiKey: {
|
||||
label: "API Key",
|
||||
placeholder: "step-...",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ModelScope",
|
||||
websiteUrl: "https://modelscope.cn",
|
||||
|
||||
@@ -23,7 +23,7 @@ import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||
* Hook for managing provider actions (add, update, delete, switch)
|
||||
* Extracts business logic from App.tsx
|
||||
*/
|
||||
export function useProviderActions(activeApp: AppId) {
|
||||
export function useProviderActions(activeApp: AppId, isProxyRunning?: boolean) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -139,6 +139,23 @@ export function useProviderActions(activeApp: AppId) {
|
||||
// 切换供应商
|
||||
const switchProvider = useCallback(
|
||||
async (provider: Provider) => {
|
||||
// 阻断逻辑:需要代理的供应商在代理未运行时不允许切换
|
||||
if (
|
||||
activeApp === "claude" &&
|
||||
provider.category !== "official" &&
|
||||
(provider.meta?.isFullUrl ||
|
||||
provider.meta?.apiFormat === "openai_chat" ||
|
||||
provider.meta?.apiFormat === "openai_responses") &&
|
||||
!isProxyRunning
|
||||
) {
|
||||
toast.warning(
|
||||
t("notifications.proxyRequiredForSwitch", {
|
||||
defaultValue: "此供应商需要代理服务,请先启动代理",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await switchProviderMutation.mutateAsync(provider.id);
|
||||
await syncClaudePlugin(provider);
|
||||
@@ -192,7 +209,7 @@ export function useProviderActions(activeApp: AppId) {
|
||||
// 错误提示由 mutation 处理
|
||||
}
|
||||
},
|
||||
[switchProviderMutation, syncClaudePlugin, activeApp, t],
|
||||
[switchProviderMutation, syncClaudePlugin, activeApp, isProxyRunning, t],
|
||||
);
|
||||
|
||||
// 删除供应商
|
||||
|
||||
@@ -43,6 +43,8 @@ export function useProxyStatus() {
|
||||
onSuccess: (info) => {
|
||||
toast.success(
|
||||
t("proxy.server.started", {
|
||||
address: info.address,
|
||||
port: info.port,
|
||||
defaultValue: `代理服务已启动 - ${info.address}:${info.port}`,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"settingsSaveFailed": "Failed to save settings: {{error}}",
|
||||
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled",
|
||||
"openAIFormatHint": "This provider uses OpenAI-compatible format and requires the proxy service to be enabled",
|
||||
"proxyRequiredForSwitch": "This provider requires the proxy service. Please start the proxy first",
|
||||
"openLinkFailed": "Failed to open link",
|
||||
"openclawModelsRegistered": "Models have been registered to /model list",
|
||||
"openclawDefaultModelSet": "Set as default model",
|
||||
@@ -736,6 +737,9 @@
|
||||
"anthropicReasoningModel": "Reasoning Model (Thinking)",
|
||||
"apiFormat": "API Format",
|
||||
"apiFormatHint": "Select the input format for the provider's API",
|
||||
"fullUrlEnabled": "Full URL Mode",
|
||||
"fullUrlDisabled": "Mark as Full URL",
|
||||
"fullUrlHint": "Proxy will use this URL as-is",
|
||||
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (Requires proxy)",
|
||||
@@ -1336,6 +1340,7 @@
|
||||
"reasoning": "Reasoning Mode",
|
||||
"reasoningOn": "Enabled",
|
||||
"reasoningOff": "Disabled",
|
||||
"inputTypes": "Input Types",
|
||||
"inputCost": "Input Cost ($/M tokens)",
|
||||
"outputCost": "Output Cost ($/M tokens)",
|
||||
"advancedOptions": "Advanced Options",
|
||||
@@ -1372,12 +1377,6 @@
|
||||
"unsupportedProfileTitle": "Unsupported tools profile detected",
|
||||
"unsupportedProfileDescription": "The current tools.profile value '{{value}}' is not in the supported OpenClaw list. It will be preserved until you choose a new value.",
|
||||
"unsupportedProfileLabel": "unsupported",
|
||||
"profiles": {
|
||||
"default": "Default",
|
||||
"strict": "Strict",
|
||||
"permissive": "Permissive",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"allowList": "Allow List",
|
||||
"denyList": "Deny List",
|
||||
"patternPlaceholder": "Tool name or pattern",
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
|
||||
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です",
|
||||
"openAIFormatHint": "このプロバイダーは OpenAI 互換フォーマットを使用しており、プロキシサービスの有効化が必要です",
|
||||
"proxyRequiredForSwitch": "このプロバイダーにはプロキシが必要です。先にプロキシを起動してください",
|
||||
"openLinkFailed": "リンクを開けませんでした",
|
||||
"openclawModelsRegistered": "モデルが /model リストに登録されました",
|
||||
"openclawDefaultModelSet": "デフォルトモデルに設定しました",
|
||||
@@ -736,6 +737,9 @@
|
||||
"anthropicReasoningModel": "推論モデル(Thinking)",
|
||||
"apiFormat": "API フォーマット",
|
||||
"apiFormatHint": "プロバイダー API の入力フォーマットを選択",
|
||||
"fullUrlEnabled": "フル URL モード",
|
||||
"fullUrlDisabled": "フル URL として設定",
|
||||
"fullUrlHint": "プロキシはこの URL をそのまま使用",
|
||||
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API(プロキシが必要)",
|
||||
@@ -1336,6 +1340,7 @@
|
||||
"reasoning": "推論モード",
|
||||
"reasoningOn": "有効",
|
||||
"reasoningOff": "無効",
|
||||
"inputTypes": "入力タイプ",
|
||||
"inputCost": "入力コスト ($/M トークン)",
|
||||
"outputCost": "出力コスト ($/M トークン)",
|
||||
"advancedOptions": "詳細オプション",
|
||||
@@ -1372,12 +1377,6 @@
|
||||
"unsupportedProfileTitle": "未対応のツールプロファイルを検出しました",
|
||||
"unsupportedProfileDescription": "現在の tools.profile の値 '{{value}}' は OpenClaw の対応リストにありません。新しい値を選択するまでこの値を保持します。",
|
||||
"unsupportedProfileLabel": "未対応",
|
||||
"profiles": {
|
||||
"default": "デフォルト",
|
||||
"strict": "厳格",
|
||||
"permissive": "寛容",
|
||||
"custom": "カスタム"
|
||||
},
|
||||
"allowList": "許可リスト",
|
||||
"denyList": "拒否リスト",
|
||||
"patternPlaceholder": "ツール名またはパターン",
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"settingsSaveFailed": "保存设置失败:{{error}}",
|
||||
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用",
|
||||
"openAIFormatHint": "此供应商使用 OpenAI 兼容格式,需要开启代理服务才能正常使用",
|
||||
"proxyRequiredForSwitch": "此供应商需要代理服务,请先启动代理",
|
||||
"openLinkFailed": "链接打开失败",
|
||||
"openclawModelsRegistered": "模型已注册到 /model 列表",
|
||||
"openclawDefaultModelSet": "已设为默认模型",
|
||||
@@ -736,6 +737,9 @@
|
||||
"anthropicReasoningModel": "推理模型 (Thinking)",
|
||||
"apiFormat": "API 格式",
|
||||
"apiFormatHint": "选择供应商 API 的输入格式",
|
||||
"fullUrlEnabled": "完整 URL 模式",
|
||||
"fullUrlDisabled": "标记为完整 URL",
|
||||
"fullUrlHint": "代理将直接使用此 URL,不拼接路径",
|
||||
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (需开启代理)",
|
||||
@@ -1336,6 +1340,7 @@
|
||||
"reasoning": "推理模式",
|
||||
"reasoningOn": "启用",
|
||||
"reasoningOff": "关闭",
|
||||
"inputTypes": "输入类型",
|
||||
"inputCost": "输入价格 ($/M tokens)",
|
||||
"outputCost": "输出价格 ($/M tokens)",
|
||||
"advancedOptions": "高级选项",
|
||||
@@ -1372,12 +1377,6 @@
|
||||
"unsupportedProfileTitle": "检测到不受支持的工具配置",
|
||||
"unsupportedProfileDescription": "当前 tools.profile 的值“{{value}}”不在 OpenClaw 支持列表内。在你手动选择新值之前,它会被保留。",
|
||||
"unsupportedProfileLabel": "不受支持",
|
||||
"profiles": {
|
||||
"default": "默认",
|
||||
"strict": "严格",
|
||||
"permissive": "宽松",
|
||||
"custom": "自定义"
|
||||
},
|
||||
"allowList": "允许列表",
|
||||
"denyList": "拒绝列表",
|
||||
"patternPlaceholder": "工具名称或模式",
|
||||
|
||||
@@ -64,6 +64,7 @@ export const icons: Record<string, string> = {
|
||||
micu: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 241.39 240.6"><title>Micu</title><defs><style>.mc-1{fill:#068cde}.mc-2{fill:#fff}.mc-3{fill:#02a6ff}</style></defs><g><path class="mc-1" d="M226.14,157.63c-3.62,0-7.24,0-10.95,0v-24.96c5.2,0,10.17,0,15.13,0,5.55-.01,8.52-4.01,10.18-8.16,1.34-3.37,1.36-7.51-1.34-11.1-3.16-4.2-7.23-5.72-12.25-5.63-3.87.07-7.74.01-11.66.01v-24.79c6.56-.43,12.93.45,19.3-1.13.4-.42.85-1.05,1.44-1.49,4.61-3.47,6.48-9.22,4.67-14.51-1.63-4.76-6.67-8.03-12.22-7.99-4.37.03-8.74,0-13.42,0,0-5.79.1-11.16-.04-16.54-.08-3.23-1.09-6.23-3.22-8.79-7.6-9.17-17.84-6.02-28.13-6.29-.39-.23-.63-1.14-.45-2.35.73-4.72.37-9.44-.24-14.13-.51-3.95-5.79-9.24-9.1-9.56-10.42-1-15.88,5.21-15.83,14.89.02,3.54,0,7.08,0,10.92h-24.44c-.76-1.03-.45-2.13-.42-3.19.15-5.2.71-10.35-1.11-15.5-2.15-6.08-11.68-9.27-17.05-5.79-5.27,3.41-7.22,7.95-6.99,13.99.14,3.56.53,7.22-.44,10.6h-23.98c-.22-.38-.37-.51-.37-.66-.05-4.56,0-9.12-.14-13.68-.15-5.18-4.72-10.76-9.31-11.57-8.81-1.55-15.64,4.23-15.64,13.24,0,4.11,0,8.21,0,12.61-4.92,0-9.32.08-13.72-.02-10.41-.22-18.81,7.79-17.84,18.57.39,4.32.06,8.7.06,13.34-5.17,0-9.9-.05-14.63.01-5.36.07-11.08,5.57-11.83,10.1-1.39,8.44,6.23,15.25,14.55,14.78,3.86-.22,7.74-.04,11.75-.04v24.92c-4.45,0-8.67-.07-12.89.02-3.2.07-6.5.62-8.75,2.93-3.6,3.69-6.1,8.11-4.12,13.5,2.13,5.8,6.42,8.43,12.98,8.43,4.21,0,8.42,0,12.83,0v24.95c-3.48,0-6.79-.12-10.08.03-3.74.18-7.43.36-10.78,2.69-4.11,2.87-6.48,8.21-5.32,12.83,1.1,4.36,7.17,9.83,11.59,9.41,4.82-.46,9.72-.1,14.69-.1,0,5.18.51,9.88-.1,14.44-1.36,10,8.64,18.06,17.22,17.5,4.62-.3,9.28-.05,14.15-.05,1.26,9.11-3.28,19.95,9.5,25.9,10.27,1.43,15.74-3.33,15.75-15.14,0-3.45,0-6.9,0-10.55h24.94c0,3.39,0,6.6,0,9.8,0,3.81.26,7.41,2.73,10.76,3.09,4.2,8.64,6.68,14,4.87,3.62-1.22,8.31-5.43,8.3-10.7,0-4.85,0-9.7,0-14.7h24.92c0,3.98-.14,7.7.03,11.41.22,4.83,1.4,9.35,5.68,12.32,5.16,3.59,12.81,2.96,17.28-2.41,3.93-7.43,2.05-14.57,2.39-21.58,5.71,0,11.1.03,16.49-.02,1.98-.02,4.05-.06,5.8-1.09,6.78-3.99,9.8-9.94,9.36-17.81-.23-4.18-.04-8.39-.04-12.56,8.59-1.68,18.23,2.96,24.73-6.3.08-.31.31-1.1.5-1.9.97-4.19,1.82-8.06-1.59-12.01-3.49-4.05-7.66-5.05-12.52-5.04ZM168.3,160.54c0,3.41-1.48,4.58-4.69,4.49-4.41-.12-8.82-.09-13.23-.05-3.15.03-4.43-1.39-4.41-4.56.06-16.85.02-33.69-.03-50.54,0-.99.44-2.13-.73-3.15-4.49,13.97-8.94,27.8-13.44,41.79h-21.8c-4.39-13.78-8.8-27.62-13.55-42.54-.15,1.94-.29,2.89-.29,3.84-.01,16.68-.08,33.36.04,50.04.03,3.7-1.18,5.38-5.05,5.16-5.51-.31-11.08.38-16.22-.44-1.24-1.59-1.21-2.95-1.21-4.26.07-27.3.18-54.6.22-81.9,0-2.16.89-3.46,2.97-3.48,9.9-.06,19.8,0,29.7.05.31,0,.63.19,1.39.44,4.22,14.29,8.51,28.79,12.8,43.3.29.05.58.1.87.15,4.53-14.59,9.07-29.17,13.66-43.95,10.35,0,20.48-.03,30.62.03,1.76.01,2.38,1.37,2.42,2.92.08,2.57.1,5.14.1,7.71-.06,24.98-.16,49.95-.15,74.93Z"/><rect class="mc-3" x="48.86" y="48.46" width="143.67" height="143.67" rx="10.57" ry="10.57"/><path class="mc-2" d="M165.55,75.28c-10.14-.06-20.27-.03-30.62-.03-4.59,14.78-9.12,29.36-13.66,43.95-.29-.05-.58-.1-.87-.15-4.29-14.51-8.58-29.01-12.8-43.3-.77-.25-1.08-.44-1.39-.44-9.9-.04-19.8-.1-29.7-.05-2.08.01-2.96,1.32-2.97,3.48-.04,27.3-.15,54.6-.22,81.9,0,1.31-.03,2.67,1.21,4.26,5.13.82,10.7.13,16.22.44,3.87.22,5.07-1.46,5.05-5.16-.12-16.68-.06-33.36-.04-50.04,0-.95.14-1.9.29-3.84,4.75,14.91,9.16,28.76,13.55,42.54h21.8c4.5-13.99,8.94-27.82,13.44-41.79,1.18,1.01.73,2.16.73,3.15.04,16.85.09,33.69.03,50.54-.01,3.17,1.26,4.59,4.41,4.56,4.41-.04,8.82-.07,13.23.05,3.21.09,4.69-1.08,4.69-4.49-.01-24.98.09-49.95.15-74.93,0-2.57-.02-5.14-.1-7.71-.05-1.55-.67-2.91-2.42-2.92Z"/></g></svg>`,
|
||||
ucloud: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 172.11 172.11"><title>UCloud</title><defs><style>.cls-1{fill:url(#uc-g56)}.cls-2{fill:url(#uc-g37)}.cls-3{fill:url(#uc-g37-2)}.cls-4{fill:url(#uc-g37-3)}.cls-5{fill:url(#uc-g37-4)}.cls-6{fill:url(#uc-g37-5)}.cls-7{fill:url(#uc-g37-6)}.cls-8{fill:url(#uc-g37-7)}.cls-9{fill:#fff}.cls-10{fill:url(#uc-g38)}</style><linearGradient id="uc-g56" x1="86.06" y1="-6.73" x2="86.06" y2="185.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32303a"/><stop offset=".36" stop-color="#34323d"/><stop offset=".62" stop-color="#3a3946"/><stop offset=".85" stop-color="#444556"/><stop offset="1" stop-color="#4e5065"/></linearGradient><linearGradient id="uc-g37" x1="143.96" y1="73.06" x2="71.52" y2="34.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4043ff"/><stop offset="1" stop-color="#f0f5fa"/></linearGradient><linearGradient id="uc-g37-2" x1="104.88" y1="118.84" x2="71.68" y2="66.44" xlink:href="#uc-g37"/><linearGradient id="uc-g37-3" x1="95.68" y1="72.87" x2="33.68" y2="76.43" xlink:href="#uc-g37"/><linearGradient id="uc-g37-4" x1="70" y1="130.18" x2="46.68" y2="73.38" xlink:href="#uc-g37"/><linearGradient id="uc-g37-5" x1="107.49" y1="106.07" x2="147.27" y2="159.57" xlink:href="#uc-g37"/><linearGradient id="uc-g37-6" x1="106.69" y1="50.6" x2="142.65" y2="131.51" xlink:href="#uc-g37"/><linearGradient id="uc-g37-7" x1="111.35" y1="152.42" x2="82.55" y2="89.87" xlink:href="#uc-g37"/><linearGradient id="uc-g38" x1="64.73" y1="89.4" x2="83.39" y2="89.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5b5dfe"/><stop offset=".18" stop-color="#5b5dfe" stop-opacity=".99"/><stop offset=".31" stop-color="#5b5dfe" stop-opacity=".95"/><stop offset=".43" stop-color="#5b5cfe" stop-opacity=".89"/><stop offset=".54" stop-color="#5b5cfe" stop-opacity=".8"/><stop offset=".64" stop-color="#5b5bfe" stop-opacity=".68"/><stop offset=".74" stop-color="#5b5afe" stop-opacity=".54"/><stop offset=".83" stop-color="#5a59ff" stop-opacity=".38"/><stop offset=".92" stop-color="#5a58ff" stop-opacity=".19"/><stop offset="1" stop-color="#5a57ff" stop-opacity="0"/></linearGradient></defs><rect class="cls-1" width="172.11" height="172.11" rx="46.24"/><polygon class="cls-2" points="124.1 51.65 104.14 63.16 104.08 63.12 84.2 51.65 104.08 40.17 104.14 40.14 124.1 51.65"/><path class="cls-3" d="M111.61,90.51l-.1-.06-2.92-1.69a8.9,8.9,0,0,1-4.46-7.69l0-17.95L84.2,51.65l-.12.07V69.59a8.91,8.91,0,0,1-4.45,7.71L64.26,86.17v23l12.4-7.15,3.09-1.78a8.92,8.92,0,0,1,8.91,0l15.42,8.91.06,0,19.93-11.49h0Z"/><path class="cls-4" d="M84.08,66v3.55a8.91,8.91,0,0,1-4.45,7.71L64.26,86.17,44.32,74.68v0L64.26,63.17l12.41,7.15A4.94,4.94,0,0,0,84.08,66Z"/><polygon class="cls-5" points="64.26 86.17 64.26 109.2 44.32 97.7 44.32 74.68 64.26 86.17"/><polygon class="cls-6" points="124.1 97.7 124.1 120.72 124.08 120.72 104.14 132.23 104.08 132.21 104.08 109.25 104.14 109.2 124.08 97.7 124.1 97.7"/><path class="cls-7" d="M124.1,51.65v23h0l-12.48,7.21c-3.28,1.89-3,6.87-3,6.87a8.9,8.9,0,0,1-4.46-7.69l0-17.89.06,0Z"/><path class="cls-8" d="M104.08,109.18v23L84.2,120.72l-.12-.07V106.33a4.94,4.94,0,0,0-7.41-4.28l3-1.72a8.89,8.89,0,0,1,8.87,0Z"/><path class="cls-9" d="M85.28,81.09V91.24a2.56,2.56,0,0,0,3.85,2.22l8.81-5.09a2.56,2.56,0,0,0,0-4.44l-8.82-5.06A2.56,2.56,0,0,0,85.28,81.09Z"/><path class="cls-10" d="M84.08,69.59a8.91,8.91,0,0,1-4.45,7.71L64.26,86.17v23l12.4-7.15,3.09-1.78a8.82,8.82,0,0,1,4.33-1.19Z"/></svg>`,
|
||||
sssaicode: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 512 512"><title>SSAI Code</title><defs><linearGradient id="ssc-gradLeft" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#0ff5ce" /><stop offset="100%" stop-color="#147a8a" /></linearGradient><linearGradient id="ssc-gradRight" x1="100%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#d0e4f5" /><stop offset="100%" stop-color="#6a9ec4" /></linearGradient><linearGradient id="ssc-gradTop" x1="50%" y1="0%" x2="50%" y2="100%"><stop offset="0%" stop-color="#a0d8e8" /><stop offset="100%" stop-color="#4aafbf" /></linearGradient><linearGradient id="ssc-gradText" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#0ff5ce" /><stop offset="35%" stop-color="#4abfcf" /><stop offset="65%" stop-color="#7badd4" /><stop offset="100%" stop-color="#c0daf0" /></linearGradient><linearGradient id="ssc-gradS" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#f0f8ff" /><stop offset="100%" stop-color="#6cbfcf" /></linearGradient><filter id="ssc-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="4" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter><pattern id="ssc-binL" x="0" y="0" width="55" height="16" patternUnits="userSpaceOnUse" patternTransform="rotate(-3)"><text x="0" y="11" font-family="monospace" font-size="8" fill="rgba(0,255,210,0.25)">1001 1101</text></pattern><pattern id="ssc-binR" x="0" y="0" width="55" height="16" patternUnits="userSpaceOnUse" patternTransform="rotate(3)"><text x="0" y="11" font-family="monospace" font-size="8" fill="rgba(180,210,240,0.25)">0110 1011</text></pattern><pattern id="ssc-binT" x="0" y="0" width="50" height="16" patternUnits="userSpaceOnUse"><text x="2" y="11" font-family="monospace" font-size="8" fill="rgba(120,200,220,0.2)">10 110</text></pattern></defs><rect width="512" height="512" rx="72" fill="#08080e" /><polygon points="90,350 250,350 170,228" fill="url(#ssc-gradLeft)" opacity="0.8" /><polygon points="90,350 250,350 170,228" fill="url(#ssc-binL)" /><polygon points="262,350 422,350 342,228" fill="url(#ssc-gradRight)" opacity="0.8" /><polygon points="262,350 422,350 342,228" fill="url(#ssc-binR)" /><polygon points="176,290 336,290 256,168" fill="none" stroke="url(#ssc-gradTop)" stroke-width="2.5" opacity="0.85" /><polygon points="192,280 320,280 256,184" fill="none" stroke="url(#ssc-gradTop)" stroke-width="0.8" opacity="0.35" /><text x="256" y="316" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="120" font-weight="bold" fill="url(#ssc-gradS)" filter="url(#ssc-glow)">S</text><text x="256" y="425" text-anchor="middle" font-family="'Helvetica Neue', 'Segoe UI', Arial, sans-serif" font-size="40" font-weight="300" letter-spacing="5" fill="url(#ssc-gradText)">SSSAiCode</text></svg>`,
|
||||
stepfun: `<svg width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><title>StepFun</title><g clip-path="url(#clip0_10683_3111)"><path d="M23.2964 14.7395H17.4448V23.2981H12.9565V13.2307H23.2964V14.7395ZM20.355 8.24341H10.4683V20.1165H0.63916V15.76H5.94385L5.94678 3.90942H20.355V8.24341ZM4.02002 12.5881H2.48779V2.51685H4.02002V12.5881ZM22.4272 1.60962H23.3394V2.51587H22.4272V4.32544H21.519V2.51587H19.6997V1.60962H21.519V0.702393H22.4272V1.60962Z" fill="#005AFF"/></g><defs><clipPath id="clip0_10683_3111"><rect width="24" height="24" fill="white"/></clipPath></defs></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>`,
|
||||
novita: `<svg width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Novita</title><g clip-path="url(#clip0_3135_1230)"><path d="M15.5564 8.26172V16.5239L2.1875 29.8928H15.5564V21.6302L23.8194 29.8928H37.1875L15.5564 8.26172Z" fill="#000000"/></g><defs><clipPath id="clip0_3135_1230"><rect width="35" height="21.6311" fill="white" transform="translate(2.1875 8.26172)"/></clipPath></defs></svg>`,
|
||||
|
||||
@@ -387,6 +387,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
|
||||
keywords: ["nvidia", "nim", "gpu"],
|
||||
defaultColor: "#74B71B",
|
||||
},
|
||||
stepfun: {
|
||||
name: "stepfun",
|
||||
displayName: "StepFun",
|
||||
category: "ai-provider",
|
||||
keywords: ["stepfun", "step", "jieyue", "阶跃星辰"],
|
||||
defaultColor: "#005AFF",
|
||||
},
|
||||
};
|
||||
|
||||
export function getIconMetadata(name: string): IconMetadata | undefined {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10683_3111)">
|
||||
<path d="M23.2964 14.7395H17.4448V23.2981H12.9565V13.2307H23.2964V14.7395ZM20.355 8.24341H10.4683V20.1165H0.63916V15.76H5.94385L5.94678 3.90942H20.355V8.24341ZM4.02002 12.5881H2.48779V2.51685H4.02002V12.5881ZM22.4272 1.60962H23.3394V2.51587H22.4272V4.32544H21.519V2.51587H19.6997V1.60962H21.519V0.702393H22.4272V1.60962Z" fill="#005AFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10683_3111">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 590 B |
@@ -151,6 +151,8 @@ export interface ProviderMeta {
|
||||
apiFormat?: "anthropic" | "openai_chat" | "openai_responses";
|
||||
// Claude 认证字段名
|
||||
apiKeyField?: ClaudeApiKeyField;
|
||||
// 是否将 base_url 视为完整 API 端点(代理直接使用此 URL,不拼接路径)
|
||||
isFullUrl?: boolean;
|
||||
// Prompt cache key for OpenAI-compatible endpoints (improves cache hit rate)
|
||||
promptCacheKey?: string;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ export const hasCommonConfigSnippet = (
|
||||
export const getApiKeyFromConfig = (
|
||||
jsonString: string,
|
||||
appType?: string,
|
||||
apiKeyField?: string,
|
||||
): string => {
|
||||
try {
|
||||
const config = JSON.parse(jsonString);
|
||||
@@ -203,11 +204,17 @@ export const getApiKeyFromConfig = (
|
||||
const token = env.ANTHROPIC_AUTH_TOKEN;
|
||||
const apiKey = env.ANTHROPIC_API_KEY;
|
||||
const value =
|
||||
typeof token === "string"
|
||||
? token
|
||||
: typeof apiKey === "string"
|
||||
apiKeyField === "ANTHROPIC_API_KEY"
|
||||
? typeof apiKey === "string"
|
||||
? apiKey
|
||||
: "";
|
||||
: typeof token === "string"
|
||||
? token
|
||||
: ""
|
||||
: typeof token === "string"
|
||||
? token
|
||||
: typeof apiKey === "string"
|
||||
? apiKey
|
||||
: "";
|
||||
return value;
|
||||
} catch (err) {
|
||||
return "";
|
||||
@@ -341,13 +348,20 @@ export const setApiKeyInConfig = (
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
// Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 或默认 AUTH_TOKEN 字段)
|
||||
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
||||
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
||||
} else if ("ANTHROPIC_API_KEY" in env) {
|
||||
env.ANTHROPIC_API_KEY = apiKey;
|
||||
// Claude API Key: follow the selected field when provided.
|
||||
const preferredClaudeField = apiKeyField ?? "ANTHROPIC_AUTH_TOKEN";
|
||||
const alternateClaudeField =
|
||||
preferredClaudeField === "ANTHROPIC_API_KEY"
|
||||
? "ANTHROPIC_AUTH_TOKEN"
|
||||
: "ANTHROPIC_API_KEY";
|
||||
|
||||
if (preferredClaudeField in env) {
|
||||
env[preferredClaudeField] = apiKey;
|
||||
} else if (alternateClaudeField in env) {
|
||||
env[preferredClaudeField] = apiKey;
|
||||
delete env[alternateClaudeField];
|
||||
} else if (createIfMissing) {
|
||||
env[apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"] = apiKey;
|
||||
env[preferredClaudeField] = apiKey;
|
||||
} else {
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { createTestQueryClient } from "../utils/testQueryClient";
|
||||
|
||||
const toastSuccessMock = vi.fn();
|
||||
const toastErrorMock = vi.fn();
|
||||
const invokeMock = vi.fn();
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: (...args: unknown[]) => toastSuccessMock(...args),
|
||||
error: (...args: unknown[]) => toastErrorMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: (...args: unknown[]) => invokeMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (key === "proxy.server.started") {
|
||||
return `代理服务已启动 - ${options?.address}:${options?.port}`;
|
||||
}
|
||||
|
||||
if (typeof options?.defaultValue === "string") {
|
||||
return options.defaultValue;
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
interface WrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const wrapper = ({ children }: WrapperProps) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
return { wrapper, queryClient };
|
||||
}
|
||||
|
||||
describe("useProxyStatus", () => {
|
||||
beforeEach(() => {
|
||||
invokeMock.mockReset();
|
||||
toastSuccessMock.mockReset();
|
||||
toastErrorMock.mockReset();
|
||||
|
||||
invokeMock.mockImplementation((command: string) => {
|
||||
if (command === "get_proxy_status") {
|
||||
return Promise.resolve({
|
||||
running: false,
|
||||
address: "127.0.0.1",
|
||||
port: 15721,
|
||||
active_connections: 0,
|
||||
total_requests: 0,
|
||||
success_requests: 0,
|
||||
failed_requests: 0,
|
||||
success_rate: 0,
|
||||
uptime_seconds: 0,
|
||||
current_provider: null,
|
||||
current_provider_id: null,
|
||||
last_request_at: null,
|
||||
last_error: null,
|
||||
failover_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (command === "get_proxy_takeover_status") {
|
||||
return Promise.resolve({
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (command === "start_proxy_server") {
|
||||
return Promise.resolve({
|
||||
address: "127.0.0.1",
|
||||
port: 15721,
|
||||
started_at: "2026-03-10T00:00:00Z",
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows interpolated address and port after proxy server starts", async () => {
|
||||
const { wrapper } = createWrapper();
|
||||
const { result } = renderHook(() => useProxyStatus(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.startProxyServer();
|
||||
});
|
||||
|
||||
expect(toastSuccessMock).toHaveBeenCalledWith(
|
||||
"代理服务已启动 - 127.0.0.1:15721",
|
||||
{ closeButton: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -52,4 +52,3 @@ describe("Codex TOML utils", () => {
|
||||
expect(extractCodexModelName(output2)).toBe("new-model");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user