From 21754a73499c8c2eaf3f5c97f5c531e6a8f35752 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 15 Jan 2026 16:25:14 +0800 Subject: [PATCH] feat(opencode): Phase 7 - Frontend TypeScript type definitions - Add "opencode" to AppId type in lib/api/types.ts - Extend McpApps interface with opencode field in types.ts - Add OpenCode-specific types: OpenCodeModel, OpenCodeProviderOptions, OpenCodeProviderConfig, OpenCodeMcpServerSpec - Add opencodeConfigDir to Settings interface - Add importOpenCodeFromLive() to providersApi - Fix type errors across components: - AppSwitcher: add opencode to icon/name mappings - McpFormModal: add opencode to enabledApps state - PromptFormModal/Panel: add opencode filename mapping - EndpointSpeedTest: add opencode timeout config - useBaseUrlState: add opencode to appType union - ProxyToggle: add opencode label - App.tsx: handle opencode fallback for SkillsPage - Update ProxyTakeoverStatus with opencode field (always false) - Fix test mocks in tests/msw/state.ts --- src/App.tsx | 2 +- src/components/AppSwitcher.tsx | 2 + src/components/mcp/McpFormModal.tsx | 2 + src/components/prompts/PromptFormModal.tsx | 1 + src/components/prompts/PromptFormPanel.tsx | 1 + .../providers/forms/EndpointSpeedTest.tsx | 7 +-- .../providers/forms/hooks/useBaseUrlState.ts | 2 +- src/components/proxy/ProxyToggle.tsx | 4 +- src/lib/api/providers.ts | 8 ++++ src/lib/api/types.ts | 2 +- src/types.ts | 45 +++++++++++++++++++ src/types/proxy.ts | 1 + tests/msw/state.ts | 12 +++-- 13 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 452afed5..32673da6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -454,7 +454,7 @@ function App() { /> ); case "skillsDiscovery": - return ; + return ; case "mcp": return ( = { claude: "Claude", codex: "Codex", gemini: "Gemini", + opencode: "OpenCode", }; return ( diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 5c608a4f..5904e49d 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -65,6 +65,7 @@ const McpFormModal: React.FC = ({ claude: boolean; codex: boolean; gemini: boolean; + opencode: boolean; }>(() => { if (initialData?.apps) { return { ...initialData.apps }; @@ -73,6 +74,7 @@ const McpFormModal: React.FC = ({ claude: defaultEnabledApps.includes("claude"), codex: defaultEnabledApps.includes("codex"), gemini: defaultEnabledApps.includes("gemini"), + opencode: defaultEnabledApps.includes("opencode"), }; }); diff --git a/src/components/prompts/PromptFormModal.tsx b/src/components/prompts/PromptFormModal.tsx index 194df686..8eb043e7 100644 --- a/src/components/prompts/PromptFormModal.tsx +++ b/src/components/prompts/PromptFormModal.tsx @@ -34,6 +34,7 @@ const PromptFormModal: React.FC = ({ claude: "CLAUDE.md", codex: "AGENTS.md", gemini: "GEMINI.md", + opencode: "OPENCODE.md", }; const filename = filenameMap[appId]; const [name, setName] = useState(""); diff --git a/src/components/prompts/PromptFormPanel.tsx b/src/components/prompts/PromptFormPanel.tsx index bdd9efbe..7f2f884c 100644 --- a/src/components/prompts/PromptFormPanel.tsx +++ b/src/components/prompts/PromptFormPanel.tsx @@ -28,6 +28,7 @@ const PromptFormPanel: React.FC = ({ claude: "CLAUDE.md", codex: "AGENTS.md", gemini: "GEMINI.md", + opencode: "OPENCODE.md", }; const filename = filenameMap[appId]; const [name, setName] = useState(""); diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx index 011116dd..5c217251 100644 --- a/src/components/providers/forms/EndpointSpeedTest.tsx +++ b/src/components/providers/forms/EndpointSpeedTest.tsx @@ -9,11 +9,12 @@ import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import type { CustomEndpoint, EndpointCandidate } from "@/types"; // 端点测速超时配置(秒) -const ENDPOINT_TIMEOUT_SECS = { +const ENDPOINT_TIMEOUT_SECS: Record = { codex: 12, claude: 8, - gemini: 8, // 新增 gemini -} as const; + gemini: 8, + opencode: 8, +}; interface TestResult { url: string; diff --git a/src/components/providers/forms/hooks/useBaseUrlState.ts b/src/components/providers/forms/hooks/useBaseUrlState.ts index 2deffcba..75e39594 100644 --- a/src/components/providers/forms/hooks/useBaseUrlState.ts +++ b/src/components/providers/forms/hooks/useBaseUrlState.ts @@ -6,7 +6,7 @@ import { import type { ProviderCategory } from "@/types"; interface UseBaseUrlStateProps { - appType: "claude" | "codex" | "gemini"; + appType: "claude" | "codex" | "gemini" | "opencode"; category: ProviderCategory | undefined; settingsConfig: string; codexConfig?: string; diff --git a/src/components/proxy/ProxyToggle.tsx b/src/components/proxy/ProxyToggle.tsx index 2b9b8cb9..ca526eb4 100644 --- a/src/components/proxy/ProxyToggle.tsx +++ b/src/components/proxy/ProxyToggle.tsx @@ -37,7 +37,9 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) { ? "Claude" : activeApp === "codex" ? "Codex" - : "Gemini"; + : activeApp === "gemini" + ? "Gemini" + : "OpenCode"; const tooltipText = takeoverEnabled ? isRunning diff --git a/src/lib/api/providers.ts b/src/lib/api/providers.ts index eb3dc9da..3e9d371b 100644 --- a/src/lib/api/providers.ts +++ b/src/lib/api/providers.ts @@ -74,6 +74,14 @@ export const providersApi = { async openTerminal(providerId: string, appId: AppId): Promise { return await invoke("open_provider_terminal", { providerId, app: appId }); }, + + /** + * 从 OpenCode live 配置导入供应商到数据库 + * OpenCode 特有功能:由于累加模式,用户可能已在 opencode.json 中配置供应商 + */ + async importOpenCodeFromLive(): Promise { + return await invoke("import_opencode_providers_from_live"); + }, }; // ============================================================================ diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index a51390e4..2e8e2b2a 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -1,2 +1,2 @@ // 前端统一使用 AppId 作为应用标识(与后端命令参数 `app` 一致) -export type AppId = "claude" | "codex" | "gemini"; // 新增 gemini +export type AppId = "claude" | "codex" | "gemini" | "opencode"; diff --git a/src/types.ts b/src/types.ts index a9234d0d..5980eaed 100644 --- a/src/types.ts +++ b/src/types.ts @@ -125,6 +125,8 @@ export interface Settings { codexConfigDir?: string; // 覆盖 Gemini 配置目录(可选) geminiConfigDir?: string; + // 覆盖 OpenCode 配置目录(可选) + opencodeConfigDir?: string; // ===== 当前供应商 ID(设备级)===== // 当前 Claude 供应商 ID(优先于数据库 is_current) @@ -156,6 +158,7 @@ export interface McpApps { claude: boolean; codex: boolean; gemini: boolean; + opencode: boolean; } // MCP 服务器条目(v3.7.0 统一结构) @@ -247,3 +250,45 @@ export interface UniversalProvider { // 统一供应商映射(id -> UniversalProvider) export type UniversalProvidersMap = Record; + +// ============================================================================ +// OpenCode 专属配置(v3.9.2+) +// ============================================================================ + +// OpenCode 模型配置 +export interface OpenCodeModel { + name: string; + limit?: { + context?: number; + output?: number; + }; +} + +// OpenCode 供应商选项 +export interface OpenCodeProviderOptions { + baseURL?: string; + apiKey?: string; + headers?: Record; +} + +// OpenCode 供应商配置(settings_config 结构) +export interface OpenCodeProviderConfig { + npm: string; // AI SDK 包名,如 "@ai-sdk/openai-compatible" + name?: string; // 供应商显示名称 + options: OpenCodeProviderOptions; + models: Record; +} + +// OpenCode MCP 服务器配置(与统一格式不同) +export interface OpenCodeMcpServerSpec { + type: "local" | "remote"; + // local 类型字段 + command?: string[]; // 与统一格式不同:命令和参数合并为数组 + environment?: Record; // 与统一格式不同:使用 environment 而非 env + // remote 类型字段 + url?: string; + headers?: Record; + // 通用字段 + enabled?: boolean; +} + diff --git a/src/types/proxy.ts b/src/types/proxy.ts index 4d11370a..705c2288 100644 --- a/src/types/proxy.ts +++ b/src/types/proxy.ts @@ -45,6 +45,7 @@ export interface ProxyTakeoverStatus { claude: boolean; codex: boolean; gemini: boolean; + opencode: boolean; } export interface ProviderHealth { diff --git a/tests/msw/state.ts b/tests/msw/state.ts index b5922a3a..ac385a67 100644 --- a/tests/msw/state.ts +++ b/tests/msw/state.ts @@ -57,12 +57,14 @@ const createDefaultProviders = (): ProvidersByApp => ({ createdAt: Date.now(), }, }, + opencode: {}, }); const createDefaultCurrent = (): CurrentProviderState => ({ claude: "claude-1", codex: "codex-1", gemini: "gemini-1", + opencode: "", }); let providers = createDefaultProviders(); @@ -82,7 +84,7 @@ let mcpConfigs: McpConfigState = { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false }, server: { type: "stdio", command: "claude-server", @@ -94,7 +96,7 @@ let mcpConfigs: McpConfigState = { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false }, server: { type: "http", url: "http://localhost:3000", @@ -102,6 +104,7 @@ let mcpConfigs: McpConfigState = { }, }, gemini: {}, + opencode: {}, }; const cloneProviders = (value: ProvidersByApp) => @@ -125,7 +128,7 @@ export const resetProviderState = () => { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false }, server: { type: "stdio", command: "claude-server", @@ -137,7 +140,7 @@ export const resetProviderState = () => { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false }, server: { type: "http", url: "http://localhost:3000", @@ -145,6 +148,7 @@ export const resetProviderState = () => { }, }, gemini: {}, + opencode: {}, }; };