diff --git a/src/api/mcp_tokens.rs b/src/api/mcp_tokens.rs index def35d4..3392d22 100644 --- a/src/api/mcp_tokens.rs +++ b/src/api/mcp_tokens.rs @@ -305,8 +305,9 @@ pub async fn get_mcp_client_configs(token: String) -> Result --header ...`。 @@ -100,6 +103,20 @@ pub fn generate_client_configs(base_url: &str, token: &str) -> ClientConfigs { } }); + // --- OpenCode:根键 mcp(非 mcpServers),remote 端点用 type: "remote"(非 streamable-http) --- + // 带 $schema 与 enabled 字段(opencode.ai 官方文档要求)。 + let opencode_json = serde_json::json!({ + "$schema": "https://opencode.ai/config.json", + "mcp": { + SERVER_NAME: { + "type": "remote", + "url": mcp_url, + "enabled": true, + "headers": { "Authorization": auth_header } + } + } + }); + // --- 通用:单个 server entry 的纯净形式 --- let generic_json = serde_json::json!({ "type": "streamable-http", @@ -118,6 +135,7 @@ pub fn generate_client_configs(base_url: &str, token: &str) -> ClientConfigs { cursor_json: pretty_json(&cursor_json), cline_json: pretty_json(&cline_json), omp_json: pretty_json(&omp_json), + opencode_json: pretty_json(&opencode_json), generic_json: pretty_json(&generic_json), claude_cli, } @@ -208,6 +226,21 @@ mod tests { assert_eq!(entry["headers"]["Authorization"], format!("Bearer {TOKEN}")); } + #[test] + fn opencode_json_uses_mcp_root_key_and_remote_type() { + let cfg = generate_client_configs(BASE, TOKEN); + let v: serde_json::Value = serde_json::from_str(&cfg.opencode_json).unwrap(); + // 关键差异:根键是 mcp(非 mcpServers)。 + assert!(v.get("mcpServers").is_none(), "opencode 配置不应含 mcpServers 键"); + let entry = &v["mcp"]["yggdrasil"]; + // 远程端点用 type: "remote"(非 streamable-http)。 + assert_eq!(entry["type"], "remote"); + assert_eq!(entry["enabled"], true); + assert_eq!(v["$schema"], "https://opencode.ai/config.json"); + assert_eq!(entry["url"], "https://rua.plus/mcp"); + assert_eq!(entry["headers"]["Authorization"], format!("Bearer {TOKEN}")); + } + #[test] fn claude_cli_one_liner_contains_url_and_header() { let cfg = generate_client_configs(BASE, TOKEN); @@ -219,8 +252,7 @@ mod tests { #[test] fn all_json_is_pretty_indented() { let cfg = generate_client_configs(BASE, TOKEN); - // pretty JSON 至少含一个换行 + 缩进(非单行紧凑形式)。 - for s in [&cfg.claude_code_json, &cfg.cursor_json, &cfg.cline_json, &cfg.omp_json, &cfg.generic_json] { + for s in [&cfg.claude_code_json, &cfg.cursor_json, &cfg.cline_json, &cfg.omp_json, &cfg.opencode_json, &cfg.generic_json] { assert!(s.contains('\n'), "JSON 应是 pretty-printed: {s}"); assert!(s.contains(" "), "JSON 应含 2 空格缩进: {s}"); } diff --git a/src/pages/admin/mcp.rs b/src/pages/admin/mcp.rs index f7ec1f3..044ee5c 100644 --- a/src/pages/admin/mcp.rs +++ b/src/pages/admin/mcp.rs @@ -53,11 +53,12 @@ const LIFETIME_OPTIONS: &[(TokenLifetime, &str)] = &[ /// 配置骨架屏的尺寸变化:(标题宽度 px, 代码块高度 px)。 /// -/// 按 6 个真实配置片段的长短差异给出不同占位尺寸,让骨架屏更像即将出现的真实内容 +/// 按 7 个真实配置片段的长短差异给出不同占位尺寸,让骨架屏更像即将出现的真实内容 /// 而非千篇一律的等高块。 #[cfg(target_arch = "wasm32")] const CONFIG_SKELETON_SHAPES: &[(&str, &str)] = &[ ("width: 320px;", "height: 152px;"), // Oh-My-Pi JSON + ("width: 280px;", "height: 168px;"), // OpenCode JSON ("width: 260px;", "height: 168px;"), // Claude Code JSON ("width: 200px;", "height: 136px;"), // Cursor JSON ("width: 220px;", "height: 168px;"), // Cline JSON @@ -552,7 +553,7 @@ fn ConfigCard() -> Element { if loading() { div { class: "flex flex-col gap-4", - for i in 0..6 { + for i in 0..7 { div { key: "{i}", class: "flex flex-col gap-2", div { class: "flex items-center justify-between", SkeletonBox {