From fb4c013ad82a5cc31d8272c3ff38b2ccbda2fda6 Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 26 Apr 2026 08:34:09 +0800 Subject: [PATCH] feat(anthropic): surface Claude Code OAuth in admin UI --- .../controller/ClaudeCodeOAuthController.java | 74 +++++++++++++++++++ .../llm/service/ModelProviderService.java | 32 +++++++- .../src/main/resources/db/data-en.sql | 13 +++- .../src/main/resources/db/data-mysql-en.sql | 11 ++- .../src/main/resources/db/data-mysql-zh.sql | 11 ++- .../src/main/resources/db/data-zh.sql | 11 ++- ...V43__rfc062_claude_code_oauth_provider.sql | 16 ++++ ...V43__rfc062_claude_code_oauth_provider.sql | 34 +++++++++ mateclaw-ui/src/api/index.ts | 8 ++ mateclaw-ui/src/i18n/locales/en-US.ts | 5 ++ mateclaw-ui/src/i18n/locales/zh-CN.ts | 5 ++ .../Models/modals/ProviderConfigModal.vue | 20 +++-- .../src/views/Settings/Models/useProviders.ts | 54 ++++++++++---- 13 files changed, 268 insertions(+), 26 deletions(-) create mode 100644 mateclaw-server/src/main/java/vip/mate/llm/controller/ClaudeCodeOAuthController.java create mode 100644 mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql diff --git a/mateclaw-server/src/main/java/vip/mate/llm/controller/ClaudeCodeOAuthController.java b/mateclaw-server/src/main/java/vip/mate/llm/controller/ClaudeCodeOAuthController.java new file mode 100644 index 00000000..31d0a897 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/llm/controller/ClaudeCodeOAuthController.java @@ -0,0 +1,74 @@ +package vip.mate.llm.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import vip.mate.common.result.R; +import vip.mate.llm.anthropic.oauth.ClaudeCodeOAuthService; +import vip.mate.llm.anthropic.oauth.ClaudeCodeOAuthService.OAuthStatus; + +/** + * RFC-062 PR-3: management-UI surface for the Claude Code OAuth provider. + * + *

Unlike the OpenAI ChatGPT OAuth flow, Claude Code OAuth piggybacks on + * the user's locally-installed Claude Code client — credentials live in the + * macOS Keychain or {@code ~/.claude/.credentials.json}, not in + * {@code mate_model_provider}. So this controller is read-only: + * + *

+ * + *

The PKCE-based in-app login flow (without requiring local Claude Code) + * lands in PR-4 and will add {@code /authorize} + {@code /callback-paste} + * endpoints here. + */ +@Slf4j +@Tag(name = "Anthropic Claude Code OAuth") +@RestController +@RequestMapping("/api/v1/oauth/anthropic") +@RequiredArgsConstructor +public class ClaudeCodeOAuthController { + + private final ClaudeCodeOAuthService oauthService; + + @Operation(summary = "Read current Claude Code OAuth credential status from local disk") + @GetMapping("/status") + public R status() { + return R.ok(oauthService.getStatus()); + } + + /** + * Re-detect credentials and force a refresh-if-near-expiry. Returns the + * post-action status so the UI can update without a follow-up GET. + * + *

If no credentials exist or refresh fails, the underlying exception is + * caught here and surfaced as {@code connected=false} status — the UI + * shouldn't show a red banner just because the user hasn't logged into + * Claude Code yet. + */ + @Operation(summary = "Force re-detect credentials and refresh if near expiry") + @PostMapping("/reload") + public R reload() { + try { + oauthService.getValidToken(); + } catch (Exception e) { + // Expected when not logged in or refresh upstream is down. + // Status response carries enough detail (connected/expired/source) + // for the UI to decide what to render. + log.debug("[ClaudeCodeOAuth] reload encountered: {}", e.getMessage()); + } + return R.ok(oauthService.getStatus()); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java index 8b148ad2..5e494d53 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java @@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import vip.mate.exception.MateClawException; +import vip.mate.llm.anthropic.oauth.ClaudeCodeOAuthService; import vip.mate.llm.event.ModelConfigChangedEvent; import vip.mate.llm.model.*; import vip.mate.llm.repository.ModelProviderMapper; @@ -22,9 +24,14 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class ModelProviderService { + /** Provider id whose OAuth token lives on local disk (Keychain / ~/.claude/.credentials.json) instead of the database. */ + private static final String CLAUDE_CODE_PROVIDER_ID = "anthropic-claude-code"; + private final ModelProviderMapper modelProviderMapper; private final ModelConfigService modelConfigService; private final ApplicationEventPublisher eventPublisher; + /** Lazy provider — avoids forcing the bean to exist in test contexts that don't load the anthropic package. */ + private final ObjectProvider claudeCodeOAuthServiceProvider; private final ObjectMapper objectMapper = new ObjectMapper(); /** Plugin-registered ChatModel instances: providerId -> ChatModel */ @@ -242,8 +249,24 @@ public class ModelProviderService { dto.setBaseUrl(provider.getBaseUrl()); dto.setGenerateKwargs(readJson(provider.getGenerateKwargs())); dto.setAuthType(provider.getAuthType() != null ? provider.getAuthType() : "api_key"); - dto.setOauthConnected(StringUtils.hasText(provider.getOauthAccessToken())); - dto.setOauthExpiresAt(provider.getOauthExpiresAt()); + if (CLAUDE_CODE_PROVIDER_ID.equals(provider.getProviderId())) { + // Claude Code OAuth credentials live on disk (RFC-062), not in the + // mate_model_provider row. Bypass the column lookup and ask the + // service directly. Falls back to false if the bean isn't present + // (e.g. minimal test contexts). + ClaudeCodeOAuthService svc = claudeCodeOAuthServiceProvider.getIfAvailable(); + if (svc != null) { + ClaudeCodeOAuthService.OAuthStatus status = svc.getStatus(); + dto.setOauthConnected(status.connected() && !status.expired()); + dto.setOauthExpiresAt(status.expiresAtMs() > 0L ? status.expiresAtMs() : null); + } else { + dto.setOauthConnected(false); + dto.setOauthExpiresAt(null); + } + } else { + dto.setOauthConnected(StringUtils.hasText(provider.getOauthAccessToken())); + dto.setOauthExpiresAt(provider.getOauthExpiresAt()); + } dto.setFallbackPriority(provider.getFallbackPriority() != null ? provider.getFallbackPriority() : 0); List builtinModels = new ArrayList<>(); List extraModels = new ArrayList<>(); @@ -278,6 +301,11 @@ public class ModelProviderService { // OAuth 认证的 provider:检查 OAuth token 是否存在 if ("oauth".equals(provider.getAuthType())) { + // Claude Code OAuth (RFC-062) — token lives on disk, not in DB. + if (CLAUDE_CODE_PROVIDER_ID.equals(provider.getProviderId())) { + ClaudeCodeOAuthService svc = claudeCodeOAuthServiceProvider.getIfAvailable(); + return svc != null && svc.isLoggedIn(); + } return StringUtils.hasText(provider.getOauthAccessToken()); } diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql index 4a830259..edc47c3a 100644 --- a/mateclaw-server/src/main/resources/db/data-en.sql +++ b/mateclaw-server/src/main/resources/db/data-en.sql @@ -118,6 +118,14 @@ MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, a KEY (provider_id) VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()); +-- RFC-062: Anthropic Claude Code OAuth provider. Credentials live on local +-- disk (Keychain / ~/.claude/.credentials.json), not in this row — leave +-- api_key + oauth_access_token blank. Bearer-auth requests bypass model +-- discovery + connection check, hence both FALSE. +MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time) +KEY (provider_id) +VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()); + -- ==================== Local model pre-configs (Ollama, disabled by default) ==================== MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) KEY (id) VALUES @@ -258,7 +266,10 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe (1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'Claude Opus 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), -(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0); +(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- RFC-062: Claude 4.7 via Claude Code OAuth subscription (Pro/Max plan). +(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0); -- Default system settings MERGE INTO mate_system_setting (id, setting_key, setting_value, description, create_time, update_time) diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql index f2578ae4..29e1a1ef 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql @@ -134,6 +134,12 @@ INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()) ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time); +-- RFC-062: Anthropic Claude Code OAuth provider. Credentials live on local +-- disk (Keychain / ~/.claude/.credentials.json), not in this row. +INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time) +VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()) +ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time); + -- ==================== Local model pre-configs (Ollama, disabled by default) ==================== INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) VALUES (1000000300, 'Gemma 3', 'ollama', 'gemma3:latest', 'Google Gemma 3, lightweight and efficient for local inference', 0.7, 4096, 0.8, TRUE, FALSE, FALSE, NOW(), NOW(), 0) @@ -298,7 +304,10 @@ VALUES (1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'Claude Opus 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), -(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0) +(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- RFC-062: Claude 4.7 via Claude Code OAuth subscription (Pro/Max plan). +(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), provider=VALUES(provider), model_name=VALUES(model_name), description=VALUES(description), temperature=VALUES(temperature), max_tokens=VALUES(max_tokens), top_p=VALUES(top_p), builtin=VALUES(builtin), enabled=VALUES(enabled), is_default=VALUES(is_default), update_time=VALUES(update_time), deleted=VALUES(deleted); -- Default system settings diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql index 6f4a6c02..0fa59483 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql @@ -134,6 +134,12 @@ INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()) ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time); +-- RFC-062:Anthropic Claude Code OAuth 订阅 provider。凭据存储在本地磁盘 +-- (macOS Keychain 或 ~/.claude/.credentials.json),不写入该行。 +INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time) +VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth 订阅)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()) +ON DUPLICATE KEY UPDATE name=VALUES(name), chat_model=VALUES(chat_model), base_url=VALUES(base_url), auth_type=VALUES(auth_type), update_time=VALUES(update_time); + -- ==================== 本地模型预配置(Ollama,默认禁用) ==================== INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) VALUES (1000000300, 'Gemma 3', 'ollama', 'gemma3:latest', 'Google Gemma 3,轻量高效,适合本地推理', 0.7, 4096, 0.8, TRUE, FALSE, FALSE, NOW(), NOW(), 0) @@ -298,7 +304,10 @@ VALUES (1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'OpenRouter 代理 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), -(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0) +(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- RFC-062:通过 Claude Code Pro/Max 订阅调用 Claude 4.7 +(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), provider=VALUES(provider), model_name=VALUES(model_name), description=VALUES(description), temperature=VALUES(temperature), max_tokens=VALUES(max_tokens), top_p=VALUES(top_p), builtin=VALUES(builtin), enabled=VALUES(enabled), is_default=VALUES(is_default), update_time=VALUES(update_time), deleted=VALUES(deleted); -- 默认系统设置 diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql index 706d161d..791ece92 100644 --- a/mateclaw-server/src/main/resources/db/data-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-zh.sql @@ -118,6 +118,12 @@ MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, a KEY (provider_id) VALUES ('openai-chatgpt', 'OpenAI ChatGPT (OAuth)', '', 'ChatGPTChatModel', '', 'https://chatgpt.com/backend-api', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()); +-- RFC-062:Anthropic Claude Code OAuth 订阅 provider。凭据存储在本地磁盘 +-- (macOS Keychain 或 ~/.claude/.credentials.json),不写入该行。 +MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time) +KEY (provider_id) +VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth 订阅)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()); + -- ==================== 本地模型预配置(Ollama,默认禁用,用户拉取后启用) ==================== MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) KEY (id) @@ -264,7 +270,10 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe (1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), (1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'OpenRouter 代理 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), -(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0); +(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- RFC-062:通过 Claude Code Pro/Max 订阅调用 Claude 4.7 +(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0); -- 默认系统设置 MERGE INTO mate_system_setting (id, setting_key, setting_value, description, create_time, update_time) diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql b/mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql new file mode 100644 index 00000000..86624151 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V43__rfc062_claude_code_oauth_provider.sql @@ -0,0 +1,16 @@ +-- RFC-062: Seed the Anthropic Claude Code OAuth provider + its Claude 4.7 +-- model bindings on existing deployments. New installs already get these +-- rows from data-{en,zh,mysql-en,mysql-zh}.sql via DatabaseBootstrapRunner; +-- this migration is for operators upgrading from <= V42. +-- +-- MERGE INTO is the H2 idempotent upsert; running this twice is a no-op. + +MERGE INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time) +KEY (provider_id) +VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()); + +MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) +KEY (id) +VALUES +(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql new file mode 100644 index 00000000..dcd148af --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V43__rfc062_claude_code_oauth_provider.sql @@ -0,0 +1,34 @@ +-- RFC-062: Seed the Anthropic Claude Code OAuth provider + its Claude 4.7 +-- model bindings on existing deployments. New installs already get these +-- rows from data-mysql-{en,zh}.sql via DatabaseBootstrapRunner; this +-- migration is for operators upgrading from <= V42. +-- +-- INSERT ... ON DUPLICATE KEY UPDATE is the MySQL idempotent upsert. +-- Same V number is used in h2/ for cross-dialect parity. + +INSERT INTO mate_model_provider (provider_id, name, api_key_prefix, chat_model, api_key, base_url, generate_kwargs, is_custom, is_local, support_model_discovery, support_connection_check, freeze_url, require_api_key, auth_type, create_time, update_time) +VALUES ('anthropic-claude-code', 'Anthropic Claude Code (OAuth)', '', 'ClaudeCodeChatModel', '', 'https://api.anthropic.com', '{}', FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, 'oauth', NOW(), NOW()) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + chat_model = VALUES(chat_model), + base_url = VALUES(base_url), + auth_type = VALUES(auth_type), + update_time = VALUES(update_time); + +INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) +VALUES +(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + provider = VALUES(provider), + model_name = VALUES(model_name), + description = VALUES(description), + temperature = VALUES(temperature), + max_tokens = VALUES(max_tokens), + top_p = VALUES(top_p), + builtin = VALUES(builtin), + enabled = VALUES(enabled), + is_default = VALUES(is_default), + update_time = VALUES(update_time), + deleted = VALUES(deleted); diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 6d664134..992f05f4 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -330,6 +330,14 @@ export const oauthApi = { revoke: () => http.delete('/oauth/openai/revoke'), } +// RFC-062: Claude Code OAuth piggybacks on the user's local Claude Code +// install — no in-app authorize/revoke flow yet (PR-4). Until then the UI +// can only check status + force a re-detect from disk. +export const claudeCodeOAuthApi = { + status: () => http.get('/oauth/anthropic/status'), + reload: () => http.post('/oauth/anthropic/reload'), +} + // ==================== Setup ==================== export const setupApi = { onboardingStatus: () => http.get('/setup/onboarding-status'), diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index cf4b498b..9ba3e05d 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -363,6 +363,11 @@ export default { oauthHint: 'Sign in with your OpenAI account to use ChatGPT Plus/Pro member quota (not API credits).', oauthLoginSuccess: 'OpenAI OAuth login successful', oauthRevokeSuccess: 'OpenAI OAuth disconnected', + // RFC-062: Claude Code OAuth (subscription piggyback) + claudeCodeOauthDetect: 'Detect Claude Code Login', + claudeCodeOauthHint: 'Reuses your local Claude Code Pro/Max subscription. Sign in via the Claude Code app first, then click "Detect" to pick up the credentials.', + claudeCodeOauthInstructions: 'No Claude Code credentials found. Install Claude Code, sign in with a Pro/Max account, then click Detect again.', + claudeCodeOauthRevokeHint: 'Sign out from the Claude Code app to revoke. MateClaw does not modify Claude Code\'s on-disk credentials.', fields: { providerId: 'Provider ID', providerName: 'Provider Name', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 1f199b9f..09e2f213 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -353,6 +353,11 @@ export default { oauthHint: '通过 OAuth 登录 OpenAI 账号,使用 ChatGPT Plus/Pro 会员额度(非 API 额度)。', oauthLoginSuccess: 'OpenAI OAuth 登录成功', oauthRevokeSuccess: '已断开 OpenAI OAuth 连接', + // RFC-062:Claude Code OAuth(订阅复用) + claudeCodeOauthDetect: '检测 Claude Code 登录态', + claudeCodeOauthHint: '复用本地 Claude Code Pro/Max 订阅。请先在 Claude Code 客户端中登录,再点击"检测"读取凭据。', + claudeCodeOauthInstructions: '未检测到 Claude Code 凭据。请安装 Claude Code 客户端,使用 Pro/Max 账号登录后再点击检测。', + claudeCodeOauthRevokeHint: '请在 Claude Code 客户端中退出登录。MateClaw 不会修改 Claude Code 的本地凭据。', fields: { providerId: 'Provider ID', providerName: 'Provider 名称', diff --git a/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue b/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue index 2a494681..92addb85 100644 --- a/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue +++ b/mateclaw-ui/src/views/Settings/Models/modals/ProviderConfigModal.vue @@ -38,20 +38,26 @@ v-if="!editingProvider?.oauthConnected" class="btn-oauth" type="button" - @click="$emit('oauthLogin')" + @click="$emit('oauthLogin', editingProvider?.id)" > - {{ t('settings.model.oauthLogin') }} + {{ editingProvider?.id === 'anthropic-claude-code' + ? t('settings.model.claudeCodeOauthDetect') + : t('settings.model.oauthLogin') }} -

{{ t('settings.model.oauthHint') }}
+
+ {{ editingProvider?.id === 'anthropic-claude-code' + ? t('settings.model.claudeCodeOauthHint') + : t('settings.model.oauthHint') }} +
@@ -182,8 +188,10 @@ defineEmits<{ close: [] save: [] toggleAdvanced: [] - oauthLogin: [] - oauthRevoke: [] + // RFC-062: providerId tells the handler which OAuth flow to dispatch + // (anthropic-claude-code reuses local creds, openai-chatgpt opens auth URL). + oauthLogin: [providerId?: string] + oauthRevoke: [providerId?: string] }>() diff --git a/mateclaw-ui/src/views/Settings/Models/useProviders.ts b/mateclaw-ui/src/views/Settings/Models/useProviders.ts index 8c2971fa..daf00542 100644 --- a/mateclaw-ui/src/views/Settings/Models/useProviders.ts +++ b/mateclaw-ui/src/views/Settings/Models/useProviders.ts @@ -1,7 +1,7 @@ import { computed, reactive, ref } from 'vue' import { useI18n } from 'vue-i18n' import { ElMessage } from 'element-plus' -import { modelApi, oauthApi, providerPoolApi } from '@/api' +import { claudeCodeOAuthApi, modelApi, oauthApi, providerPoolApi } from '@/api' import type { ProviderPoolEntry } from '@/api' import type { ActiveModelsInfo, DiscoverResult, ProviderInfo, ProviderModelInfo, TestResult } from '@/types' @@ -445,6 +445,7 @@ export function useProviders() { 'zhipu-intl': '/icons/providers/zhipu.svg', 'volcengine': '/icons/providers/volcengine.svg', 'openai-chatgpt': '/icons/providers/openai.svg', + 'anthropic-claude-code': '/icons/providers/anthropic.svg', } function getProviderIcon(providerId: string): string { @@ -453,7 +454,34 @@ export function useProviders() { // ==================== OAuth ==================== - async function handleOAuthLogin() { + /** Refresh editingProvider after a load so the modal state stays in sync. */ + async function reloadProvidersAndSync() { + await loadProviders() + if (editingProvider.value) { + const updated = providers.value.find(p => p.id === editingProvider.value!.id) + if (updated) editingProvider.value = updated + } + } + + async function handleOAuthLogin(providerId?: string) { + // RFC-062: Claude Code OAuth piggybacks on the user's local Claude Code + // install. There's no in-app authorize URL — the "Connect" button just + // re-reads credentials from disk so a user who logged in via Claude Code + // sees the connection appear without restarting the server. + if (providerId === 'anthropic-claude-code') { + try { + const res: any = await claudeCodeOAuthApi.reload() + if (res.data?.connected && !res.data?.expired) { + ElMessage.success(t('settings.model.oauthLoginSuccess')) + } else { + ElMessage.warning(t('settings.model.claudeCodeOauthInstructions')) + } + await reloadProvidersAndSync() + } catch (e: any) { + ElMessage.error(e.msg || 'Claude Code OAuth detection failed') + } + return + } try { const res: any = await oauthApi.authorize() const { authorizeUrl } = res.data @@ -467,12 +495,7 @@ export function useProviders() { clearInterval(pollInterval) if (authWindow && !authWindow.closed) authWindow.close() ElMessage.success(t('settings.model.oauthLoginSuccess')) - await loadProviders() - // 刷新当前编辑的 provider - if (editingProvider.value) { - const updated = providers.value.find(p => p.id === editingProvider.value!.id) - if (updated) editingProvider.value = updated - } + await reloadProvidersAndSync() } } catch { /* ignore polling errors */ } }, 2000) @@ -483,15 +506,18 @@ export function useProviders() { } } - async function handleOAuthRevoke() { + async function handleOAuthRevoke(providerId?: string) { + // Claude Code OAuth credentials live on disk — MateClaw doesn't manage + // them, so we don't expose a revoke that would clobber the user's + // Claude Code login. Direct them to log out from the Claude Code app. + if (providerId === 'anthropic-claude-code') { + ElMessage.info(t('settings.model.claudeCodeOauthRevokeHint')) + return + } try { await oauthApi.revoke() ElMessage.success(t('settings.model.oauthRevokeSuccess')) - await loadProviders() - if (editingProvider.value) { - const updated = providers.value.find(p => p.id === editingProvider.value!.id) - if (updated) editingProvider.value = updated - } + await reloadProvidersAndSync() } catch (e: any) { ElMessage.error(e.msg || 'OAuth revoke failed') }