From fbf942a5fd3d5bc8a68ebc571b5c3f78423d9473 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 15 Jul 2025 19:28:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Claude=20=E8=B4=A6=E6=88=B7=20Access=20Tok?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 OAuth 和手动输入两种账户添加方式 - 支持直接输入 Access Token 和 Refresh Token - 新增账户编辑功能,可更新 Token 和代理设置 - 优化 Token 刷新失败时的回退机制 - 改进用户体验,支持手动维护长期有效的 Token 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 20 +-- src/services/claudeAccountService.js | 33 +++- web/admin/app.js | 201 +++++++++++++++++++++++ web/admin/index.html | 235 +++++++++++++++++++++++++++ 4 files changed, 468 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a4fc3e79..3edc889d 100644 --- a/README.md +++ b/README.md @@ -237,28 +237,16 @@ npm run service:status 4. 设置使用限制(可选) 5. 保存,记下生成的Key -### 4. 开始使用API +### 4. 开始使用Claude code 现在你可以用自己的服务替换官方API了: -**原来的请求:** +**设置环境变量:** ```bash -curl https://api.anthropic.com/v1/messages \ - -H "x-api-key: 官方的key" \ - -H "content-type: application/json" \ - -d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"你好"}]}' +export ANTHROPIC_BASE_URL="http://127.0.0.1:3000/api/" # 根据实际填写你服务器的ip地址或者域名 +export ANTHROPIC_AUTH_TOKEN="后台创建的API密钥" ``` -**现在的请求:** -```bash -curl http://你的域名:3000/api/v1/messages \ - -H "x-api-key: cr_你创建的key" \ - -H "content-type: application/json" \ - -d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"你好"}]}' -``` - -就是把域名换一下,API Key换成你自己生成的,其他都一样。 - --- ## 🔧 日常维护 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index c37bbf03..e4c67eb9 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -107,7 +107,7 @@ class ClaudeAccountService { const refreshToken = this._decryptSensitiveData(accountData.refreshToken); if (!refreshToken) { - throw new Error('No refresh token available'); + throw new Error('No refresh token available - manual token update required'); } // 创建代理agent @@ -183,9 +183,20 @@ class ClaudeAccountService { const now = Date.now(); if (!expiresAt || now >= (expiresAt - 10000)) { // 10秒提前刷新 - logger.info(`🔄 Token expired/expiring for account ${accountId}, refreshing...`); - const refreshResult = await this.refreshAccountToken(accountId); - return refreshResult.accessToken; + logger.info(`🔄 Token expired/expiring for account ${accountId}, attempting refresh...`); + try { + const refreshResult = await this.refreshAccountToken(accountId); + return refreshResult.accessToken; + } catch (refreshError) { + logger.warn(`⚠️ Token refresh failed for account ${accountId}: ${refreshError.message}`); + // 如果刷新失败,仍然尝试使用当前token(可能是手动添加的长期有效token) + const currentToken = this._decryptSensitiveData(accountData.accessToken); + if (currentToken) { + logger.info(`🔄 Using current token for account ${accountId} (refresh failed)`); + return currentToken; + } + throw refreshError; + } } const accessToken = this._decryptSensitiveData(accountData.accessToken); @@ -240,7 +251,7 @@ class ClaudeAccountService { throw new Error('Account not found'); } - const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive']; + const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive', 'claudeAiOauth']; const updatedData = { ...accountData }; for (const [field, value] of Object.entries(updates)) { @@ -249,6 +260,18 @@ class ClaudeAccountService { updatedData[field] = this._encryptSensitiveData(value); } else if (field === 'proxy') { updatedData[field] = value ? JSON.stringify(value) : ''; + } else if (field === 'claudeAiOauth') { + // 更新 Claude AI OAuth 数据 + if (value) { + updatedData.claudeAiOauth = this._encryptSensitiveData(JSON.stringify(value)); + updatedData.accessToken = this._encryptSensitiveData(value.accessToken); + updatedData.refreshToken = this._encryptSensitiveData(value.refreshToken); + updatedData.expiresAt = value.expiresAt.toString(); + updatedData.scopes = value.scopes.join(' '); + updatedData.status = 'active'; + updatedData.errorMessage = ''; + updatedData.lastRefreshAt = new Date().toISOString(); + } } else { updatedData[field] = value.toString(); } diff --git a/web/admin/app.js b/web/admin/app.js index 10d986e0..d86443af 100644 --- a/web/admin/app.js +++ b/web/admin/app.js @@ -140,6 +140,25 @@ const app = createApp({ accountForm: { name: '', description: '', + addType: 'oauth', // 'oauth' 或 'manual' + accessToken: '', + refreshToken: '', + proxyType: '', + proxyHost: '', + proxyPort: '', + proxyUsername: '', + proxyPassword: '' + }, + + // 编辑账户相关 + showEditAccountModal: false, + editAccountLoading: false, + editAccountForm: { + id: '', + name: '', + description: '', + accessToken: '', + refreshToken: '', proxyType: '', proxyHost: '', proxyPort: '', @@ -294,11 +313,123 @@ const app = createApp({ this.resetAccountForm(); }, + // 打开编辑账户模态框 + openEditAccountModal(account) { + this.editAccountForm = { + id: account.id, + name: account.name, + description: account.description || '', + accessToken: '', + refreshToken: '', + proxyType: account.proxy ? account.proxy.type : '', + proxyHost: account.proxy ? account.proxy.host : '', + proxyPort: account.proxy ? account.proxy.port : '', + proxyUsername: account.proxy ? account.proxy.username : '', + proxyPassword: account.proxy ? account.proxy.password : '' + }; + this.showEditAccountModal = true; + }, + + // 关闭编辑账户模态框 + closeEditAccountModal() { + this.showEditAccountModal = false; + this.editAccountForm = { + id: '', + name: '', + description: '', + accessToken: '', + refreshToken: '', + proxyType: '', + proxyHost: '', + proxyPort: '', + proxyUsername: '', + proxyPassword: '' + }; + }, + + // 更新账户 + async updateAccount() { + this.editAccountLoading = true; + try { + // 构建更新数据 + let updateData = { + name: this.editAccountForm.name, + description: this.editAccountForm.description + }; + + // 只在有值时才更新 token + if (this.editAccountForm.accessToken.trim()) { + // 构建新的 OAuth 数据 + const newOauthData = { + accessToken: this.editAccountForm.accessToken, + refreshToken: this.editAccountForm.refreshToken || '', + expiresAt: Date.now() + (365 * 24 * 60 * 60 * 1000), // 默认设置1年后过期 + scopes: ['user:inference'] + }; + updateData.claudeAiOauth = newOauthData; + } + + // 更新代理配置 + if (this.editAccountForm.proxyType) { + updateData.proxy = { + type: this.editAccountForm.proxyType, + host: this.editAccountForm.proxyHost, + port: parseInt(this.editAccountForm.proxyPort), + username: this.editAccountForm.proxyUsername || null, + password: this.editAccountForm.proxyPassword || null + }; + } else { + updateData.proxy = null; + } + + const response = await fetch(`/admin/claude-accounts/${this.editAccountForm.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.authToken + }, + body: JSON.stringify(updateData) + }); + + const data = await response.json(); + + if (data.success) { + this.showToast('账户更新成功!', 'success', '更新成功'); + this.closeEditAccountModal(); + await this.loadAccounts(); + } else { + this.showToast(data.message || 'Account update failed', 'error', 'Update Failed'); + } + } catch (error) { + console.error('Error updating account:', error); + + let errorMessage = '更新失败,请检查网络连接'; + + if (error.response) { + try { + const errorData = await error.response.json(); + errorMessage = errorData.message || errorMessage; + } catch (parseError) { + console.error('Failed to parse error response:', parseError); + } + } else if (error.message) { + errorMessage = error.message; + } + + this.showToast(errorMessage, 'error', '网络错误', 8000); + } finally { + this.editAccountLoading = false; + } + }, + // 重置账户表单 resetAccountForm() { this.accountForm = { name: '', description: '', + addType: 'oauth', + accessToken: '', + refreshToken: '', proxyType: '', proxyHost: '', proxyPort: '', @@ -462,6 +593,76 @@ const app = createApp({ } }, + // 创建手动账户 + async createManualAccount() { + this.createAccountLoading = true; + try { + // 构建代理配置 + let proxy = null; + if (this.accountForm.proxyType) { + proxy = { + type: this.accountForm.proxyType, + host: this.accountForm.proxyHost, + port: parseInt(this.accountForm.proxyPort), + username: this.accountForm.proxyUsername || null, + password: this.accountForm.proxyPassword || null + }; + } + + // 构建手动 OAuth 数据 + const manualOauthData = { + accessToken: this.accountForm.accessToken, + refreshToken: this.accountForm.refreshToken || '', + expiresAt: Date.now() + (365 * 24 * 60 * 60 * 1000), // 默认设置1年后过期 + scopes: ['user:inference'] // 默认权限 + }; + + // 创建账户 + const createResponse = await fetch('/admin/claude-accounts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.authToken + }, + body: JSON.stringify({ + name: this.accountForm.name, + description: this.accountForm.description, + claudeAiOauth: manualOauthData, + proxy: proxy + }) + }); + + const createData = await createResponse.json(); + + if (createData.success) { + this.showToast('手动账户创建成功!', 'success', '账户创建成功'); + this.closeCreateAccountModal(); + await this.loadAccounts(); + } else { + this.showToast(createData.message || 'Account creation failed', 'error', 'Creation Failed'); + } + } catch (error) { + console.error('Error creating manual account:', error); + + let errorMessage = '创建失败,请检查网络连接'; + + if (error.response) { + try { + const errorData = await error.response.json(); + errorMessage = errorData.message || errorMessage; + } catch (parseError) { + console.error('Failed to parse error response:', parseError); + } + } else if (error.message) { + errorMessage = error.message; + } + + this.showToast(errorMessage, 'error', '网络错误', 8000); + } finally { + this.createAccountLoading = false; + } + }, + // 根据当前标签页加载数据 loadCurrentTabData() { diff --git a/web/admin/index.html b/web/admin/index.html index efdcc92d..4a8d2961 100644 --- a/web/admin/index.html +++ b/web/admin/index.html @@ -720,6 +720,12 @@ {{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }} + + @@ -2031,6 +2109,163 @@ + + +