mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
feat(anthropic): surface Claude Code OAuth in admin UI
This commit is contained in:
parent
bf4e81c554
commit
fb4c013ad8
@ -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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code GET /status} — re-reads from disk and reports
|
||||
* connected / expired / expiry timestamp / source.</li>
|
||||
* <li>{@code POST /reload} — alias of status, semantically signals
|
||||
* "I just logged in via Claude Code, please re-detect" so the UI can
|
||||
* refresh state without polling. Also triggers an auto-refresh of the
|
||||
* access token when it's near expiry, so the user can verify end-to-end
|
||||
* token validity from the management page.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>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<OAuthStatus> 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.
|
||||
*
|
||||
* <p>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<OAuthStatus> 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());
|
||||
}
|
||||
}
|
||||
@ -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<ClaudeCodeOAuthService> 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<ModelInfoDTO> builtinModels = new ArrayList<>();
|
||||
List<ModelInfoDTO> 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());
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
-- 默认系统设置
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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'),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 名称',
|
||||
|
||||
@ -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') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn-oauth btn-oauth-revoke"
|
||||
type="button"
|
||||
@click="$emit('oauthRevoke')"
|
||||
@click="$emit('oauthRevoke', editingProvider?.id)"
|
||||
>
|
||||
{{ t('settings.model.oauthDisconnect') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="field-hint">{{ t('settings.model.oauthHint') }}</div>
|
||||
<div class="field-hint">
|
||||
{{ editingProvider?.id === 'anthropic-claude-code'
|
||||
? t('settings.model.claudeCodeOauthHint')
|
||||
: t('settings.model.oauthHint') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- API Key 输入区域(非 OAuth 时显示) -->
|
||||
<div v-else class="form-group">
|
||||
@ -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]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user