mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 18:41:35 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2937eb6766 | |||
| 313a6e3f6c | |||
| 5566be2b4b | |||
| cfcf9452d0 |
@@ -11,6 +11,11 @@
|
|||||||
"updater:default",
|
"updater:default",
|
||||||
"core:window:allow-set-skip-taskbar",
|
"core:window:allow-set-skip-taskbar",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-is-maximized",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-set-decorations",
|
||||||
"process:allow-restart",
|
"process:allow-restart",
|
||||||
"dialog:default"
|
"dialog:default"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -971,6 +971,10 @@ pub fn run() {
|
|||||||
// 静默启动:根据设置决定是否显示主窗口
|
// 静默启动:根据设置决定是否显示主窗口
|
||||||
let settings = crate::settings::get_settings();
|
let settings = crate::settings::get_settings();
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
// 在窗口首次显示前同步装饰状态,避免前端加载后再切换导致标题栏闪烁
|
||||||
|
// 仅 Linux 生效:解决 Wayland 下系统窗口按钮不可用的问题
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let _ = window.set_decorations(!settings.use_app_window_controls);
|
||||||
if settings.silent_startup {
|
if settings.silent_startup {
|
||||||
// 静默启动模式:保持窗口隐藏
|
// 静默启动模式:保持窗口隐藏
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
|
|||||||
@@ -282,9 +282,9 @@ pub struct ProviderMeta {
|
|||||||
/// 是否将 base_url 视为完整 API 端点(不拼接 endpoint 路径)
|
/// 是否将 base_url 视为完整 API 端点(不拼接 endpoint 路径)
|
||||||
#[serde(rename = "isFullUrl", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "isFullUrl", skip_serializing_if = "Option::is_none")]
|
||||||
pub is_full_url: Option<bool>,
|
pub is_full_url: Option<bool>,
|
||||||
/// Prompt cache key for OpenAI-compatible endpoints.
|
/// Prompt cache key for OpenAI Responses-compatible endpoints.
|
||||||
/// When set, injected into converted requests to improve cache hit rate.
|
/// When set, injected into converted Responses requests to improve cache hit rate.
|
||||||
/// If not set, provider ID is used automatically during format conversion.
|
/// If not set, provider ID is used automatically during Claude -> Responses conversion.
|
||||||
#[serde(rename = "promptCacheKey", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "promptCacheKey", skip_serializing_if = "Option::is_none")]
|
||||||
pub prompt_cache_key: Option<String>,
|
pub prompt_cache_key: Option<String>,
|
||||||
/// 累加模式应用中,该 provider 是否已写入 live config。
|
/// 累加模式应用中,该 provider 是否已写入 live config。
|
||||||
|
|||||||
@@ -81,14 +81,13 @@ pub fn transform_claude_request_for_api_format(
|
|||||||
provider: &Provider,
|
provider: &Provider,
|
||||||
api_format: &str,
|
api_format: &str,
|
||||||
) -> Result<serde_json::Value, ProxyError> {
|
) -> Result<serde_json::Value, ProxyError> {
|
||||||
let cache_key = provider
|
|
||||||
.meta
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| m.prompt_cache_key.as_deref())
|
|
||||||
.unwrap_or(&provider.id);
|
|
||||||
|
|
||||||
match api_format {
|
match api_format {
|
||||||
"openai_responses" => {
|
"openai_responses" => {
|
||||||
|
let cache_key = provider
|
||||||
|
.meta
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|m| m.prompt_cache_key.as_deref())
|
||||||
|
.unwrap_or(&provider.id);
|
||||||
// Codex OAuth (ChatGPT Plus/Pro 反代) 需要在请求体里强制 store: false
|
// Codex OAuth (ChatGPT Plus/Pro 反代) 需要在请求体里强制 store: false
|
||||||
// + include: ["reasoning.encrypted_content"],由 transform 层统一处理。
|
// + include: ["reasoning.encrypted_content"],由 transform 层统一处理。
|
||||||
let is_codex_oauth = provider
|
let is_codex_oauth = provider
|
||||||
@@ -102,7 +101,7 @@ pub fn transform_claude_request_for_api_format(
|
|||||||
is_codex_oauth,
|
is_codex_oauth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"openai_chat" => super::transform::anthropic_to_openai(body, Some(cache_key)),
|
"openai_chat" => super::transform::anthropic_to_openai(body),
|
||||||
_ => Ok(body),
|
_ => Ok(body),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,10 +71,8 @@ pub fn resolve_reasoning_effort(body: &Value) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anthropic 请求 → OpenAI 请求
|
/// Anthropic 请求 → OpenAI Chat Completions 请求
|
||||||
///
|
pub fn anthropic_to_openai(body: Value) -> Result<Value, ProxyError> {
|
||||||
/// `cache_key`: optional prompt_cache_key to inject for improved cache routing
|
|
||||||
pub fn anthropic_to_openai(body: Value, cache_key: Option<&str>) -> Result<Value, ProxyError> {
|
|
||||||
let mut result = json!({});
|
let mut result = json!({});
|
||||||
|
|
||||||
// NOTE: 模型映射由上游统一处理(proxy::model_mapper),格式转换层只做结构转换。
|
// NOTE: 模型映射由上游统一处理(proxy::model_mapper),格式转换层只做结构转换。
|
||||||
@@ -175,11 +173,6 @@ pub fn anthropic_to_openai(body: Value, cache_key: Option<&str>) -> Result<Value
|
|||||||
result["tool_choice"] = v.clone();
|
result["tool_choice"] = v.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject prompt_cache_key for improved cache routing on OpenAI-compatible endpoints
|
|
||||||
if let Some(key) = cache_key {
|
|
||||||
result["prompt_cache_key"] = json!(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +199,10 @@ fn normalize_openai_system_messages(messages: &mut Vec<Value>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
let mut inherited_cache_control: Option<Value> = None;
|
||||||
|
let mut cache_control_conflict = false;
|
||||||
|
let mut saw_cache_control = false;
|
||||||
|
let mut saw_missing_cache_control = false;
|
||||||
messages.retain(|message| {
|
messages.retain(|message| {
|
||||||
if message.get("role").and_then(|value| value.as_str()) != Some("system") {
|
if message.get("role").and_then(|value| value.as_str()) != Some("system") {
|
||||||
return true;
|
return true;
|
||||||
@@ -226,11 +223,28 @@ fn normalize_openai_system_messages(messages: &mut Vec<Value>) {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(cache_control) = message.get("cache_control") {
|
||||||
|
saw_cache_control = true;
|
||||||
|
match &inherited_cache_control {
|
||||||
|
None => inherited_cache_control = Some(cache_control.clone()),
|
||||||
|
Some(existing) if existing == cache_control => {}
|
||||||
|
Some(_) => cache_control_conflict = true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saw_missing_cache_control = true;
|
||||||
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
});
|
});
|
||||||
|
|
||||||
if !parts.is_empty() {
|
if !parts.is_empty() {
|
||||||
messages.insert(0, json!({"role": "system", "content": parts.join("\n")}));
|
let mut merged = json!({"role": "system", "content": parts.join("\n")});
|
||||||
|
if !(cache_control_conflict || (saw_cache_control && saw_missing_cache_control)) {
|
||||||
|
if let Some(cache_control) = inherited_cache_control {
|
||||||
|
merged["cache_control"] = cache_control;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.insert(0, merged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,7 +583,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["model"], "claude-3-opus");
|
assert_eq!(result["model"], "claude-3-opus");
|
||||||
assert_eq!(result["max_tokens"], 1024);
|
assert_eq!(result["max_tokens"], 1024);
|
||||||
assert_eq!(result["messages"][0]["role"], "user");
|
assert_eq!(result["messages"][0]["role"], "user");
|
||||||
@@ -585,7 +599,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["messages"][0]["role"], "system");
|
assert_eq!(result["messages"][0]["role"], "system");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result["messages"][0]["content"],
|
result["messages"][0]["content"],
|
||||||
@@ -607,36 +621,76 @@ mod tests {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["tools"][0]["type"], "function");
|
assert_eq!(result["tools"][0]["type"], "function");
|
||||||
assert_eq!(result["tools"][0]["function"]["name"], "get_weather");
|
assert_eq!(result["tools"][0]["function"]["name"], "get_weather");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_anthropic_to_openai_normalizes_fragmented_system_messages() {
|
fn test_anthropic_to_openai_preserves_matching_system_cache_control_when_merging() {
|
||||||
let input = json!({
|
let input = json!({
|
||||||
"model": "claude-3-sonnet",
|
"model": "claude-3-sonnet",
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
"system": [
|
"system": [
|
||||||
{"type": "text", "text": "You are Claude Code."},
|
{"type": "text", "text": "You are Claude Code.", "cache_control": {"type": "ephemeral"}},
|
||||||
{"type": "text", "text": "Be concise."}
|
{"type": "text", "text": "Be concise.", "cache_control": {"type": "ephemeral"}}
|
||||||
],
|
],
|
||||||
"messages": [
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
{"role": "system", "content": "Follow repo conventions."},
|
|
||||||
{"role": "user", "content": "Hello"}
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["messages"].as_array().unwrap().len(), 2);
|
assert_eq!(result["messages"].as_array().unwrap().len(), 2);
|
||||||
assert_eq!(result["messages"][0]["role"], "system");
|
assert_eq!(result["messages"][0]["role"], "system");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result["messages"][0]["content"],
|
result["messages"][0]["content"],
|
||||||
"You are Claude Code.\nBe concise.\nFollow repo conventions."
|
"You are Claude Code.\nBe concise."
|
||||||
);
|
);
|
||||||
|
assert_eq!(result["messages"][0]["cache_control"]["type"], "ephemeral");
|
||||||
assert_eq!(result["messages"][1]["role"], "user");
|
assert_eq!(result["messages"][1]["role"], "user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_anthropic_to_openai_drops_mixed_present_absent_system_cache_control_when_merging() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "claude-3-sonnet",
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"system": [
|
||||||
|
{"type": "text", "text": "You are Claude Code.", "cache_control": {"type": "ephemeral"}},
|
||||||
|
{"type": "text", "text": "Be concise."}
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = anthropic_to_openai(input, None).unwrap();
|
||||||
|
assert_eq!(result["messages"][0]["role"], "system");
|
||||||
|
assert_eq!(
|
||||||
|
result["messages"][0]["content"],
|
||||||
|
"You are Claude Code.\nBe concise."
|
||||||
|
);
|
||||||
|
assert!(result["messages"][0].get("cache_control").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_anthropic_to_openai_drops_conflicting_system_cache_control_when_merging() {
|
||||||
|
let input = json!({
|
||||||
|
"model": "claude-3-sonnet",
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"system": [
|
||||||
|
{"type": "text", "text": "You are Claude Code.", "cache_control": {"type": "ephemeral"}},
|
||||||
|
{"type": "text", "text": "Be concise.", "cache_control": {"type": "ephemeral", "ttl": "5m"}}
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = anthropic_to_openai(input, None).unwrap();
|
||||||
|
assert_eq!(result["messages"][0]["role"], "system");
|
||||||
|
assert_eq!(
|
||||||
|
result["messages"][0]["content"],
|
||||||
|
"You are Claude Code.\nBe concise."
|
||||||
|
);
|
||||||
|
assert!(result["messages"][0].get("cache_control").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_anthropic_to_openai_tool_use() {
|
fn test_anthropic_to_openai_tool_use() {
|
||||||
let input = json!({
|
let input = json!({
|
||||||
@@ -651,7 +705,7 @@ mod tests {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
let msg = &result["messages"][0];
|
let msg = &result["messages"][0];
|
||||||
assert_eq!(msg["role"], "assistant");
|
assert_eq!(msg["role"], "assistant");
|
||||||
assert!(msg.get("tool_calls").is_some());
|
assert!(msg.get("tool_calls").is_some());
|
||||||
@@ -671,7 +725,7 @@ mod tests {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
let msg = &result["messages"][0];
|
let msg = &result["messages"][0];
|
||||||
assert_eq!(msg["role"], "tool");
|
assert_eq!(msg["role"], "tool");
|
||||||
assert_eq!(msg["tool_call_id"], "call_123");
|
assert_eq!(msg["tool_call_id"], "call_123");
|
||||||
@@ -743,31 +797,19 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["model"], "gpt-4o");
|
assert_eq!(result["model"], "gpt-4o");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_anthropic_to_openai_with_cache_key() {
|
fn test_anthropic_to_openai_does_not_inject_prompt_cache_key() {
|
||||||
let input = json!({
|
let input = json!({
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, Some("provider-123")).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["prompt_cache_key"], "provider-123");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_anthropic_to_openai_no_cache_key() {
|
|
||||||
let input = json!({
|
|
||||||
"model": "claude-3-opus",
|
|
||||||
"max_tokens": 1024,
|
|
||||||
"messages": [{"role": "user", "content": "Hello"}]
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
|
||||||
assert!(result.get("prompt_cache_key").is_none());
|
assert!(result.get("prompt_cache_key").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,7 +835,7 @@ mod tests {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
// System message cache_control preserved
|
// System message cache_control preserved
|
||||||
assert_eq!(result["messages"][0]["cache_control"]["type"], "ephemeral");
|
assert_eq!(result["messages"][0]["cache_control"]["type"], "ephemeral");
|
||||||
// Text block cache_control preserved
|
// Text block cache_control preserved
|
||||||
@@ -1047,7 +1089,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert!(result.get("reasoning_effort").is_none());
|
assert!(result.get("reasoning_effort").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,7 +1102,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["reasoning_effort"], "medium");
|
assert_eq!(result["reasoning_effort"], "medium");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1073,7 +1115,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["reasoning_effort"], "xhigh");
|
assert_eq!(result["reasoning_effort"], "xhigh");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,7 +1128,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["reasoning_effort"], "low");
|
assert_eq!(result["reasoning_effort"], "low");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,7 +1141,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["reasoning_effort"], "xhigh");
|
assert_eq!(result["reasoning_effort"], "xhigh");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,7 +1153,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert!(result.get("reasoning_effort").is_none());
|
assert!(result.get("reasoning_effort").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1124,7 +1166,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
result.get("max_tokens").is_none(),
|
result.get("max_tokens").is_none(),
|
||||||
"{model} should not have max_tokens"
|
"{model} should not have max_tokens"
|
||||||
@@ -1144,7 +1186,7 @@ mod tests {
|
|||||||
"messages": [{"role": "user", "content": "Hello"}]
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = anthropic_to_openai(input, None).unwrap();
|
let result = anthropic_to_openai(input).unwrap();
|
||||||
assert_eq!(result["max_tokens"], 1024);
|
assert_eq!(result["max_tokens"], 1024);
|
||||||
assert!(result.get("max_completion_tokens").is_none());
|
assert!(result.get("max_completion_tokens").is_none());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ use std::net::SocketAddr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{oneshot, RwLock};
|
use tokio::sync::{oneshot, RwLock};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
|
||||||
|
|
||||||
/// 代理服务器状态(共享)
|
/// 代理服务器状态(共享)
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -275,11 +274,6 @@ impl ProxyServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_router(&self) -> Router {
|
fn build_router(&self) -> Router {
|
||||||
let cors = CorsLayer::new()
|
|
||||||
.allow_origin(Any)
|
|
||||||
.allow_methods(Any)
|
|
||||||
.allow_headers(Any);
|
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
// 健康检查
|
// 健康检查
|
||||||
.route("/health", get(handlers::health_check))
|
.route("/health", get(handlers::health_check))
|
||||||
@@ -328,7 +322,6 @@ impl ProxyServer {
|
|||||||
.route("/gemini/v1beta/*path", post(handlers::handle_gemini))
|
.route("/gemini/v1beta/*path", post(handlers::handle_gemini))
|
||||||
// 提高默认请求体大小限制(避免 413 Payload Too Large)
|
// 提高默认请求体大小限制(避免 413 Payload Too Large)
|
||||||
.layer(DefaultBodyLimit::max(200 * 1024 * 1024))
|
.layer(DefaultBodyLimit::max(200 * 1024 * 1024))
|
||||||
.layer(cors)
|
|
||||||
.with_state(self.state.clone())
|
.with_state(self.state.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ impl StreamCheckService {
|
|||||||
anthropic_to_responses(anthropic_body, Some(&provider.id), is_codex_oauth)
|
anthropic_to_responses(anthropic_body, Some(&provider.id), is_codex_oauth)
|
||||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||||
} else if is_openai_chat {
|
} else if is_openai_chat {
|
||||||
anthropic_to_openai(anthropic_body, Some(&provider.id))
|
anthropic_to_openai(anthropic_body)
|
||||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||||
} else {
|
} else {
|
||||||
anthropic_body
|
anthropic_body
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ pub struct AppSettings {
|
|||||||
pub show_in_tray: bool,
|
pub show_in_tray: bool,
|
||||||
#[serde(default = "default_minimize_to_tray_on_close")]
|
#[serde(default = "default_minimize_to_tray_on_close")]
|
||||||
pub minimize_to_tray_on_close: bool,
|
pub minimize_to_tray_on_close: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub use_app_window_controls: bool,
|
||||||
/// 是否启用 Claude 插件联动
|
/// 是否启用 Claude 插件联动
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enable_claude_plugin_integration: bool,
|
pub enable_claude_plugin_integration: bool,
|
||||||
@@ -293,6 +295,7 @@ impl Default for AppSettings {
|
|||||||
Self {
|
Self {
|
||||||
show_in_tray: true,
|
show_in_tray: true,
|
||||||
minimize_to_tray_on_close: true,
|
minimize_to_tray_on_close: true,
|
||||||
|
use_app_window_controls: false,
|
||||||
enable_claude_plugin_integration: false,
|
enable_claude_plugin_integration: false,
|
||||||
skip_claude_onboarding: false,
|
skip_claude_onboarding: false,
|
||||||
launch_on_startup: false,
|
launch_on_startup: false,
|
||||||
|
|||||||
+143
-8
@@ -9,6 +9,10 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
Minus,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
|
X,
|
||||||
Book,
|
Book,
|
||||||
Wrench,
|
Wrench,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -22,6 +26,7 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
Cpu,
|
Cpu,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import type { Provider, VisibleApps } from "@/types";
|
import type { Provider, VisibleApps } from "@/types";
|
||||||
import type { EnvConflict } from "@/types/env";
|
import type { EnvConflict } from "@/types/env";
|
||||||
import { useProvidersQuery, useSettingsQuery } from "@/lib/query";
|
import { useProvidersQuery, useSettingsQuery } from "@/lib/query";
|
||||||
@@ -99,9 +104,8 @@ interface WebDavSyncStatusUpdatedPayload {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
const DEFAULT_DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||||
const HEADER_HEIGHT = 64; // px
|
const HEADER_HEIGHT = 64; // px
|
||||||
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
|
||||||
|
|
||||||
const STORAGE_KEY = "cc-switch-last-app";
|
const STORAGE_KEY = "cc-switch-last-app";
|
||||||
const VALID_APPS: AppId[] = [
|
const VALID_APPS: AppId[] = [
|
||||||
@@ -153,12 +157,17 @@ function App() {
|
|||||||
const [currentView, setCurrentView] = useState<View>(getInitialView);
|
const [currentView, setCurrentView] = useState<View>(getInitialView);
|
||||||
const [settingsDefaultTab, setSettingsDefaultTab] = useState("general");
|
const [settingsDefaultTab, setSettingsDefaultTab] = useState("general");
|
||||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||||
|
const [isWindowMaximized, setIsWindowMaximized] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
||||||
}, [currentView]);
|
}, [currentView]);
|
||||||
|
|
||||||
const { data: settingsData } = useSettingsQuery();
|
const { data: settingsData } = useSettingsQuery();
|
||||||
|
const useAppWindowControls =
|
||||||
|
isLinux() && (settingsData?.useAppWindowControls ?? false);
|
||||||
|
const dragBarHeight = useAppWindowControls ? 32 : DEFAULT_DRAG_BAR_HEIGHT;
|
||||||
|
const contentTopOffset = dragBarHeight + HEADER_HEIGHT;
|
||||||
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
||||||
claude: true,
|
claude: true,
|
||||||
codex: true,
|
codex: true,
|
||||||
@@ -392,6 +401,51 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [queryClient, t]);
|
}, [queryClient, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
let unlistenResize: (() => void) | undefined;
|
||||||
|
|
||||||
|
const setupWindowStateSync = async () => {
|
||||||
|
try {
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
const syncWindowMaximizedState = async () => {
|
||||||
|
const maximized = await currentWindow.isMaximized();
|
||||||
|
if (active) {
|
||||||
|
setIsWindowMaximized(maximized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await syncWindowMaximizedState();
|
||||||
|
unlistenResize = await currentWindow.onResized(() => {
|
||||||
|
void syncWindowMaximizedState();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to sync window maximized state", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupWindowStateSync();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
unlistenResize?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// settingsData 未加载时跳过,避免用 fallback false 覆盖 Rust 侧已设好的装饰状态
|
||||||
|
if (!settingsData) return;
|
||||||
|
|
||||||
|
const syncWindowDecorations = async () => {
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().setDecorations(!useAppWindowControls);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to update window decorations", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void syncWindowDecorations();
|
||||||
|
}, [useAppWindowControls, settingsData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkEnvOnStartup = async () => {
|
const checkEnvOnStartup = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -734,6 +788,44 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const notifyWindowControlError = (error: unknown) => {
|
||||||
|
toast.error(
|
||||||
|
t("notifications.windowControlFailed", {
|
||||||
|
defaultValue: "窗口控制失败:{{error}}",
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWindowMinimize = async () => {
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().minimize();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to minimize window", error);
|
||||||
|
notifyWindowControlError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWindowToggleMaximize = async () => {
|
||||||
|
try {
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
await currentWindow.toggleMaximize();
|
||||||
|
setIsWindowMaximized(await currentWindow.isMaximized());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to toggle maximize", error);
|
||||||
|
notifyWindowControlError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWindowClose = async () => {
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to close window", error);
|
||||||
|
notifyWindowControlError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const content = (() => {
|
const content = (() => {
|
||||||
switch (currentView) {
|
switch (currentView) {
|
||||||
@@ -880,14 +972,57 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
||||||
style={{ overflowX: "hidden", paddingTop: CONTENT_TOP_OFFSET }}
|
style={{ overflowX: "hidden", paddingTop: contentTopOffset }}
|
||||||
>
|
>
|
||||||
{DRAG_BAR_HEIGHT > 0 && (
|
{(dragBarHeight > 0 || useAppWindowControls) && (
|
||||||
<div
|
<div
|
||||||
className="fixed top-0 left-0 right-0 z-[60]"
|
className="fixed top-0 left-0 right-0 z-[70] flex items-center justify-end px-2"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
style={{ WebkitAppRegion: "drag", height: DRAG_BAR_HEIGHT } as any}
|
style={{ WebkitAppRegion: "drag", height: dragBarHeight } as any}
|
||||||
/>
|
>
|
||||||
|
{useAppWindowControls && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
style={{ WebkitAppRegion: "no-drag" } as any}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => void handleWindowMinimize()}
|
||||||
|
title={t("header.windowMinimize")}
|
||||||
|
className="h-7 w-7"
|
||||||
|
>
|
||||||
|
<Minus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => void handleWindowToggleMaximize()}
|
||||||
|
title={
|
||||||
|
isWindowMaximized
|
||||||
|
? t("header.windowRestore")
|
||||||
|
: t("header.windowMaximize")
|
||||||
|
}
|
||||||
|
className="h-7 w-7"
|
||||||
|
>
|
||||||
|
{isWindowMaximized ? (
|
||||||
|
<Minimize2 className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Maximize2 className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => void handleWindowClose()}
|
||||||
|
title={t("header.windowClose")}
|
||||||
|
className="h-7 w-7 hover:bg-red-500/15 hover:text-red-500"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{showEnvBanner && envConflicts.length > 0 && (
|
{showEnvBanner && envConflicts.length > 0 && (
|
||||||
<EnvWarningBanner
|
<EnvWarningBanner
|
||||||
@@ -920,7 +1055,7 @@ function App() {
|
|||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
...DRAG_REGION_STYLE,
|
...DRAG_REGION_STYLE,
|
||||||
top: DRAG_BAR_HEIGHT,
|
top: dragBarHeight,
|
||||||
height: HEADER_HEIGHT,
|
height: HEADER_HEIGHT,
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { SettingsFormState } from "@/hooks/useSettings";
|
|||||||
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
|
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
|
||||||
import { ToggleRow } from "@/components/ui/toggle-row";
|
import { ToggleRow } from "@/components/ui/toggle-row";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { isLinux } from "@/lib/platform";
|
||||||
|
|
||||||
interface WindowSettingsProps {
|
interface WindowSettingsProps {
|
||||||
settings: SettingsFormState;
|
settings: SettingsFormState;
|
||||||
@@ -75,6 +76,18 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
|
|||||||
onChange({ minimizeToTrayOnClose: value })
|
onChange({ minimizeToTrayOnClose: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isLinux() && (
|
||||||
|
<ToggleRow
|
||||||
|
icon={<AppWindow className="h-4 w-4 text-amber-500" />}
|
||||||
|
title={t("settings.useAppWindowControls")}
|
||||||
|
description={t("settings.useAppWindowControlsDescription")}
|
||||||
|
checked={!!settings.useAppWindowControls}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
onChange({ useAppWindowControls: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
|||||||
...data,
|
...data,
|
||||||
showInTray: data.showInTray ?? true,
|
showInTray: data.showInTray ?? true,
|
||||||
minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true,
|
minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true,
|
||||||
|
useAppWindowControls: data.useAppWindowControls ?? false,
|
||||||
enableClaudePluginIntegration:
|
enableClaudePluginIntegration:
|
||||||
data.enableClaudePluginIntegration ?? false,
|
data.enableClaudePluginIntegration ?? false,
|
||||||
silentStartup: data.silentStartup ?? false,
|
silentStartup: data.silentStartup ?? false,
|
||||||
@@ -105,6 +106,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
|||||||
({
|
({
|
||||||
showInTray: true,
|
showInTray: true,
|
||||||
minimizeToTrayOnClose: true,
|
minimizeToTrayOnClose: true,
|
||||||
|
useAppWindowControls: false,
|
||||||
enableClaudePluginIntegration: false,
|
enableClaudePluginIntegration: false,
|
||||||
skipClaudeOnboarding: false,
|
skipClaudeOnboarding: false,
|
||||||
language: readPersistedLanguage(),
|
language: readPersistedLanguage(),
|
||||||
@@ -139,6 +141,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
|||||||
...serverData,
|
...serverData,
|
||||||
showInTray: serverData.showInTray ?? true,
|
showInTray: serverData.showInTray ?? true,
|
||||||
minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true,
|
minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true,
|
||||||
|
useAppWindowControls: serverData.useAppWindowControls ?? false,
|
||||||
enableClaudePluginIntegration:
|
enableClaudePluginIntegration:
|
||||||
serverData.enableClaudePluginIntegration ?? false,
|
serverData.enableClaudePluginIntegration ?? false,
|
||||||
silentStartup: serverData.silentStartup ?? false,
|
silentStartup: serverData.silentStartup ?? false,
|
||||||
|
|||||||
@@ -91,7 +91,11 @@
|
|||||||
"switchToChinese": "Switch to Chinese",
|
"switchToChinese": "Switch to Chinese",
|
||||||
"switchToEnglish": "Switch to English",
|
"switchToEnglish": "Switch to English",
|
||||||
"enterEditMode": "Enter Edit Mode",
|
"enterEditMode": "Enter Edit Mode",
|
||||||
"exitEditMode": "Exit Edit Mode"
|
"exitEditMode": "Exit Edit Mode",
|
||||||
|
"windowMinimize": "Minimize window",
|
||||||
|
"windowMaximize": "Maximize window",
|
||||||
|
"windowRestore": "Restore window",
|
||||||
|
"windowClose": "Close window"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"tabProvider": "Provider",
|
"tabProvider": "Provider",
|
||||||
@@ -201,7 +205,8 @@
|
|||||||
"openclawDefaultModelSet": "Set as default model",
|
"openclawDefaultModelSet": "Set as default model",
|
||||||
"openclawDefaultModelSetFailed": "Failed to set default model",
|
"openclawDefaultModelSetFailed": "Failed to set default model",
|
||||||
"openclawNoModels": "No models configured",
|
"openclawNoModels": "No models configured",
|
||||||
"backfillWarning": "Switched successfully, but failed to save changes back to the previous provider"
|
"backfillWarning": "Switched successfully, but failed to save changes back to the previous provider",
|
||||||
|
"windowControlFailed": "Window control failed: {{error}}"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "Delete Provider",
|
"deleteProvider": "Delete Provider",
|
||||||
@@ -494,6 +499,8 @@
|
|||||||
"autoLaunchFailed": "Failed to set auto-launch",
|
"autoLaunchFailed": "Failed to set auto-launch",
|
||||||
"minimizeToTray": "Minimize to tray on close",
|
"minimizeToTray": "Minimize to tray on close",
|
||||||
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
||||||
|
"useAppWindowControls": "Enable app-level window controls",
|
||||||
|
"useAppWindowControlsDescription": "Use built-in minimize, maximize/restore, and close buttons in the app header.",
|
||||||
"enableClaudePluginIntegration": "Apply to Claude Code extension",
|
"enableClaudePluginIntegration": "Apply to Claude Code extension",
|
||||||
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
|
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
|
||||||
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
|
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
|
||||||
|
|||||||
@@ -91,7 +91,11 @@
|
|||||||
"switchToChinese": "中国語に切り替え",
|
"switchToChinese": "中国語に切り替え",
|
||||||
"switchToEnglish": "英語に切り替え",
|
"switchToEnglish": "英語に切り替え",
|
||||||
"enterEditMode": "編集モードに入る",
|
"enterEditMode": "編集モードに入る",
|
||||||
"exitEditMode": "編集モードを終了"
|
"exitEditMode": "編集モードを終了",
|
||||||
|
"windowMinimize": "ウィンドウを最小化",
|
||||||
|
"windowMaximize": "ウィンドウを最大化",
|
||||||
|
"windowRestore": "ウィンドウを元に戻す",
|
||||||
|
"windowClose": "ウィンドウを閉じる"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"tabProvider": "プロバイダー",
|
"tabProvider": "プロバイダー",
|
||||||
@@ -201,7 +205,8 @@
|
|||||||
"openclawDefaultModelSet": "デフォルトモデルに設定しました",
|
"openclawDefaultModelSet": "デフォルトモデルに設定しました",
|
||||||
"openclawDefaultModelSetFailed": "デフォルトモデルの設定に失敗しました",
|
"openclawDefaultModelSetFailed": "デフォルトモデルの設定に失敗しました",
|
||||||
"openclawNoModels": "モデルが設定されていません",
|
"openclawNoModels": "モデルが設定されていません",
|
||||||
"backfillWarning": "切り替え成功しましたが、前のプロバイダーへの設定保存に失敗しました"
|
"backfillWarning": "切り替え成功しましたが、前のプロバイダーへの設定保存に失敗しました",
|
||||||
|
"windowControlFailed": "ウィンドウ操作に失敗しました: {{error}}"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "プロバイダーを削除",
|
"deleteProvider": "プロバイダーを削除",
|
||||||
@@ -494,6 +499,8 @@
|
|||||||
"autoLaunchFailed": "自動起動の設定に失敗しました",
|
"autoLaunchFailed": "自動起動の設定に失敗しました",
|
||||||
"minimizeToTray": "閉じるときトレイへ最小化",
|
"minimizeToTray": "閉じるときトレイへ最小化",
|
||||||
"minimizeToTrayDescription": "チェックすると閉じるボタンでトレイに隠し、オフならアプリを終了します。",
|
"minimizeToTrayDescription": "チェックすると閉じるボタンでトレイに隠し、オフならアプリを終了します。",
|
||||||
|
"useAppWindowControls": "アプリ内ウィンドウボタンを有効化",
|
||||||
|
"useAppWindowControlsDescription": "有効にすると、アプリヘッダーに最小化・最大化/復元・閉じるボタンを表示します。",
|
||||||
"enableClaudePluginIntegration": "Claude Code 拡張に適用",
|
"enableClaudePluginIntegration": "Claude Code 拡張に適用",
|
||||||
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
|
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
|
||||||
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
|
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
|
||||||
|
|||||||
@@ -91,7 +91,11 @@
|
|||||||
"switchToChinese": "切换到中文",
|
"switchToChinese": "切换到中文",
|
||||||
"switchToEnglish": "切换到英文",
|
"switchToEnglish": "切换到英文",
|
||||||
"enterEditMode": "进入编辑模式",
|
"enterEditMode": "进入编辑模式",
|
||||||
"exitEditMode": "退出编辑模式"
|
"exitEditMode": "退出编辑模式",
|
||||||
|
"windowMinimize": "最小化窗口",
|
||||||
|
"windowMaximize": "最大化窗口",
|
||||||
|
"windowRestore": "还原窗口",
|
||||||
|
"windowClose": "关闭窗口"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"tabProvider": "供应商",
|
"tabProvider": "供应商",
|
||||||
@@ -201,7 +205,8 @@
|
|||||||
"openclawDefaultModelSet": "已设为默认模型",
|
"openclawDefaultModelSet": "已设为默认模型",
|
||||||
"openclawDefaultModelSetFailed": "设置默认模型失败",
|
"openclawDefaultModelSetFailed": "设置默认模型失败",
|
||||||
"openclawNoModels": "该供应商没有配置模型",
|
"openclawNoModels": "该供应商没有配置模型",
|
||||||
"backfillWarning": "切换成功,但旧供应商配置回填失败,您手动修改的配置可能未保存"
|
"backfillWarning": "切换成功,但旧供应商配置回填失败,您手动修改的配置可能未保存",
|
||||||
|
"windowControlFailed": "窗口控制失败:{{error}}"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "删除供应商",
|
"deleteProvider": "删除供应商",
|
||||||
@@ -494,6 +499,8 @@
|
|||||||
"autoLaunchFailed": "设置开机自启失败",
|
"autoLaunchFailed": "设置开机自启失败",
|
||||||
"minimizeToTray": "关闭时最小化到托盘",
|
"minimizeToTray": "关闭时最小化到托盘",
|
||||||
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
||||||
|
"useAppWindowControls": "启用应用级窗口按钮",
|
||||||
|
"useAppWindowControlsDescription": "开启后使用应用自建的最小化、最大化/还原、关闭按钮;关闭后沿用系统窗口模式。",
|
||||||
"enableClaudePluginIntegration": "应用到 Claude Code 插件",
|
"enableClaudePluginIntegration": "应用到 Claude Code 插件",
|
||||||
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
|
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
|
||||||
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
|
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
|
||||||
|
|||||||
+3
-1
@@ -166,7 +166,7 @@ export interface ProviderMeta {
|
|||||||
apiKeyField?: ClaudeApiKeyField;
|
apiKeyField?: ClaudeApiKeyField;
|
||||||
// 是否将 base_url 视为完整 API 端点(代理直接使用此 URL,不拼接路径)
|
// 是否将 base_url 视为完整 API 端点(代理直接使用此 URL,不拼接路径)
|
||||||
isFullUrl?: boolean;
|
isFullUrl?: boolean;
|
||||||
// Prompt cache key for OpenAI-compatible endpoints (improves cache hit rate)
|
// Prompt cache key for OpenAI Responses-compatible endpoints (improves cache hit rate)
|
||||||
promptCacheKey?: string;
|
promptCacheKey?: string;
|
||||||
// 供应商类型(用于识别 Copilot 等特殊供应商)
|
// 供应商类型(用于识别 Copilot 等特殊供应商)
|
||||||
providerType?: string;
|
providerType?: string;
|
||||||
@@ -244,6 +244,8 @@ export interface Settings {
|
|||||||
showInTray: boolean;
|
showInTray: boolean;
|
||||||
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
|
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
|
||||||
minimizeToTrayOnClose: boolean;
|
minimizeToTrayOnClose: boolean;
|
||||||
|
// 是否启用应用级窗口控制按钮(最小化/最大化/关闭)
|
||||||
|
useAppWindowControls?: boolean;
|
||||||
// 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey)
|
// 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey)
|
||||||
enableClaudePluginIntegration?: boolean;
|
enableClaudePluginIntegration?: boolean;
|
||||||
// 跳过 Claude Code 初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding)
|
// 跳过 Claude Code 初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding)
|
||||||
|
|||||||
Reference in New Issue
Block a user