mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'main' into feature/claude-code-client-support
This commit is contained in:
395
cli/index.js
395
cli/index.js
@@ -14,6 +14,7 @@ const config = require('../config/config');
|
||||
const redis = require('../src/models/redis');
|
||||
const apiKeyService = require('../src/services/apiKeyService');
|
||||
const claudeAccountService = require('../src/services/claudeAccountService');
|
||||
const bedrockAccountService = require('../src/services/bedrockAccountService');
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -137,6 +138,50 @@ program
|
||||
await redis.disconnect();
|
||||
});
|
||||
|
||||
// ☁️ Bedrock 账户管理
|
||||
program
|
||||
.command('bedrock')
|
||||
.description('Bedrock 账户管理操作')
|
||||
.action(async () => {
|
||||
await initialize();
|
||||
|
||||
const { action } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: '请选择操作:',
|
||||
choices: [
|
||||
{ name: '📋 查看所有 Bedrock 账户', value: 'list' },
|
||||
{ name: '➕ 创建 Bedrock 账户', value: 'create' },
|
||||
{ name: '✏️ 编辑 Bedrock 账户', value: 'edit' },
|
||||
{ name: '🔄 切换账户状态', value: 'toggle' },
|
||||
{ name: '🧪 测试账户连接', value: 'test' },
|
||||
{ name: '🗑️ 删除账户', value: 'delete' }
|
||||
]
|
||||
}]);
|
||||
|
||||
switch (action) {
|
||||
case 'list':
|
||||
await listBedrockAccounts();
|
||||
break;
|
||||
case 'create':
|
||||
await createBedrockAccount();
|
||||
break;
|
||||
case 'edit':
|
||||
await editBedrockAccount();
|
||||
break;
|
||||
case 'toggle':
|
||||
await toggleBedrockAccount();
|
||||
break;
|
||||
case 'test':
|
||||
await testBedrockAccount();
|
||||
break;
|
||||
case 'delete':
|
||||
await deleteBedrockAccount();
|
||||
break;
|
||||
}
|
||||
|
||||
await redis.disconnect();
|
||||
});
|
||||
|
||||
// 实现具体功能函数
|
||||
|
||||
@@ -597,6 +642,355 @@ async function listClaudeAccounts() {
|
||||
}
|
||||
}
|
||||
|
||||
// ☁️ Bedrock 账户管理函数
|
||||
|
||||
async function listBedrockAccounts() {
|
||||
const spinner = ora('正在获取 Bedrock 账户...').start();
|
||||
|
||||
try {
|
||||
const result = await bedrockAccountService.getAllAccounts();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const accounts = result.data;
|
||||
spinner.succeed(`找到 ${accounts.length} 个 Bedrock 账户`);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
console.log(styles.warning('没有找到任何 Bedrock 账户'));
|
||||
return;
|
||||
}
|
||||
|
||||
const tableData = [
|
||||
['ID', '名称', '区域', '模型', '状态', '凭证类型', '创建时间']
|
||||
];
|
||||
|
||||
accounts.forEach(account => {
|
||||
tableData.push([
|
||||
account.id.substring(0, 8) + '...',
|
||||
account.name,
|
||||
account.region,
|
||||
account.defaultModel?.split('.').pop() || 'default',
|
||||
account.isActive ? (account.schedulable ? '🟢 活跃' : '🟡 不可调度') : '🔴 停用',
|
||||
account.credentialType,
|
||||
account.createdAt ? new Date(account.createdAt).toLocaleDateString() : '-'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log('\n☁️ Bedrock 账户列表:\n');
|
||||
console.log(table(tableData));
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('获取 Bedrock 账户失败');
|
||||
console.error(styles.error(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function createBedrockAccount() {
|
||||
console.log(styles.title('\n➕ 创建 Bedrock 账户\n'));
|
||||
|
||||
const questions = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '账户名称:',
|
||||
validate: input => input.trim() !== ''
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: '描述 (可选):'
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'region',
|
||||
message: '选择 AWS 区域:',
|
||||
choices: [
|
||||
{ name: 'us-east-1 (北弗吉尼亚)', value: 'us-east-1' },
|
||||
{ name: 'us-west-2 (俄勒冈)', value: 'us-west-2' },
|
||||
{ name: 'eu-west-1 (爱尔兰)', value: 'eu-west-1' },
|
||||
{ name: 'ap-southeast-1 (新加坡)', value: 'ap-southeast-1' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'credentialType',
|
||||
message: '凭证类型:',
|
||||
choices: [
|
||||
{ name: '默认凭证链 (环境变量/AWS配置)', value: 'default' },
|
||||
{ name: '访问密钥 (Access Key)', value: 'access_key' },
|
||||
{ name: 'Bearer Token (API Key)', value: 'bearer_token' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 根据凭证类型添加额外问题
|
||||
const answers = await inquirer.prompt(questions);
|
||||
|
||||
if (answers.credentialType === 'access_key') {
|
||||
const credQuestions = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'accessKeyId',
|
||||
message: 'AWS Access Key ID:',
|
||||
validate: input => input.trim() !== ''
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'secretAccessKey',
|
||||
message: 'AWS Secret Access Key:',
|
||||
validate: input => input.trim() !== ''
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'sessionToken',
|
||||
message: 'Session Token (可选,用于临时凭证):'
|
||||
}
|
||||
]);
|
||||
|
||||
answers.awsCredentials = {
|
||||
accessKeyId: credQuestions.accessKeyId,
|
||||
secretAccessKey: credQuestions.secretAccessKey
|
||||
};
|
||||
|
||||
if (credQuestions.sessionToken) {
|
||||
answers.awsCredentials.sessionToken = credQuestions.sessionToken;
|
||||
}
|
||||
}
|
||||
|
||||
const spinner = ora('正在创建 Bedrock 账户...').start();
|
||||
|
||||
try {
|
||||
const result = await bedrockAccountService.createAccount(answers);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
spinner.succeed('Bedrock 账户创建成功');
|
||||
console.log(styles.success(`账户 ID: ${result.data.id}`));
|
||||
console.log(styles.info(`名称: ${result.data.name}`));
|
||||
console.log(styles.info(`区域: ${result.data.region}`));
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('创建 Bedrock 账户失败');
|
||||
console.error(styles.error(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function testBedrockAccount() {
|
||||
const spinner = ora('正在获取 Bedrock 账户...').start();
|
||||
|
||||
try {
|
||||
const result = await bedrockAccountService.getAllAccounts();
|
||||
if (!result.success || result.data.length === 0) {
|
||||
spinner.fail('没有可测试的 Bedrock 账户');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed('账户列表获取成功');
|
||||
|
||||
const choices = result.data.map(account => ({
|
||||
name: `${account.name} (${account.region})`,
|
||||
value: account.id
|
||||
}));
|
||||
|
||||
const { accountId } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'accountId',
|
||||
message: '选择要测试的账户:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
const testSpinner = ora('正在测试账户连接...').start();
|
||||
|
||||
const testResult = await bedrockAccountService.testAccount(accountId);
|
||||
|
||||
if (testResult.success) {
|
||||
testSpinner.succeed('账户连接测试成功');
|
||||
console.log(styles.success(`状态: ${testResult.data.status}`));
|
||||
console.log(styles.info(`区域: ${testResult.data.region}`));
|
||||
console.log(styles.info(`可用模型数量: ${testResult.data.modelsCount || 'N/A'}`));
|
||||
} else {
|
||||
testSpinner.fail('账户连接测试失败');
|
||||
console.error(styles.error(testResult.error));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('测试过程中发生错误');
|
||||
console.error(styles.error(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBedrockAccount() {
|
||||
const spinner = ora('正在获取 Bedrock 账户...').start();
|
||||
|
||||
try {
|
||||
const result = await bedrockAccountService.getAllAccounts();
|
||||
if (!result.success || result.data.length === 0) {
|
||||
spinner.fail('没有可操作的 Bedrock 账户');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed('账户列表获取成功');
|
||||
|
||||
const choices = result.data.map(account => ({
|
||||
name: `${account.name} (${account.isActive ? '🟢 活跃' : '🔴 停用'})`,
|
||||
value: account.id
|
||||
}));
|
||||
|
||||
const { accountId } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'accountId',
|
||||
message: '选择要切换状态的账户:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
const toggleSpinner = ora('正在切换账户状态...').start();
|
||||
|
||||
// 获取当前状态
|
||||
const accountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
throw new Error('无法获取账户信息');
|
||||
}
|
||||
|
||||
const newStatus = !accountResult.data.isActive;
|
||||
const updateResult = await bedrockAccountService.updateAccount(accountId, { isActive: newStatus });
|
||||
|
||||
if (updateResult.success) {
|
||||
toggleSpinner.succeed('账户状态切换成功');
|
||||
console.log(styles.success(`新状态: ${newStatus ? '🟢 活跃' : '🔴 停用'}`));
|
||||
} else {
|
||||
throw new Error(updateResult.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('切换账户状态失败');
|
||||
console.error(styles.error(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function editBedrockAccount() {
|
||||
const spinner = ora('正在获取 Bedrock 账户...').start();
|
||||
|
||||
try {
|
||||
const result = await bedrockAccountService.getAllAccounts();
|
||||
if (!result.success || result.data.length === 0) {
|
||||
spinner.fail('没有可编辑的 Bedrock 账户');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed('账户列表获取成功');
|
||||
|
||||
const choices = result.data.map(account => ({
|
||||
name: `${account.name} (${account.region})`,
|
||||
value: account.id
|
||||
}));
|
||||
|
||||
const { accountId } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'accountId',
|
||||
message: '选择要编辑的账户:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
const accountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
throw new Error('无法获取账户信息');
|
||||
}
|
||||
|
||||
const account = accountResult.data;
|
||||
|
||||
const updates = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '账户名称:',
|
||||
default: account.name
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: '描述:',
|
||||
default: account.description
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'priority',
|
||||
message: '优先级 (1-100):',
|
||||
default: account.priority,
|
||||
validate: input => input >= 1 && input <= 100
|
||||
}
|
||||
]);
|
||||
|
||||
const updateSpinner = ora('正在更新账户...').start();
|
||||
|
||||
const updateResult = await bedrockAccountService.updateAccount(accountId, updates);
|
||||
|
||||
if (updateResult.success) {
|
||||
updateSpinner.succeed('账户更新成功');
|
||||
} else {
|
||||
throw new Error(updateResult.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('编辑账户失败');
|
||||
console.error(styles.error(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBedrockAccount() {
|
||||
const spinner = ora('正在获取 Bedrock 账户...').start();
|
||||
|
||||
try {
|
||||
const result = await bedrockAccountService.getAllAccounts();
|
||||
if (!result.success || result.data.length === 0) {
|
||||
spinner.fail('没有可删除的 Bedrock 账户');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed('账户列表获取成功');
|
||||
|
||||
const choices = result.data.map(account => ({
|
||||
name: `${account.name} (${account.region})`,
|
||||
value: { id: account.id, name: account.name }
|
||||
}));
|
||||
|
||||
const { account } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'account',
|
||||
message: '选择要删除的账户:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
const { confirm } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `确定要删除账户 "${account.name}" 吗?此操作无法撤销!`,
|
||||
default: false
|
||||
}]);
|
||||
|
||||
if (!confirm) {
|
||||
console.log(styles.info('已取消删除'));
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteSpinner = ora('正在删除账户...').start();
|
||||
|
||||
const deleteResult = await bedrockAccountService.deleteAccount(account.id);
|
||||
|
||||
if (deleteResult.success) {
|
||||
deleteSpinner.succeed('账户删除成功');
|
||||
} else {
|
||||
throw new Error(deleteResult.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('删除账户失败');
|
||||
console.error(styles.error(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
// 程序信息
|
||||
program
|
||||
.name('claude-relay-cli')
|
||||
@@ -612,6 +1006,7 @@ if (!process.argv.slice(2).length) {
|
||||
console.log('使用以下命令管理服务:\n');
|
||||
console.log(' claude-relay-cli admin - 创建初始管理员账户');
|
||||
console.log(' claude-relay-cli keys - API Key 管理(查看/修改过期时间/续期/删除)');
|
||||
console.log(' claude-relay-cli bedrock - Bedrock 账户管理(创建/查看/编辑/测试/删除)');
|
||||
console.log(' claude-relay-cli status - 查看系统状态');
|
||||
console.log('\n使用 --help 查看详细帮助信息');
|
||||
}
|
||||
@@ -39,6 +39,18 @@ const config = {
|
||||
betaHeader: process.env.CLAUDE_BETA_HEADER || 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
|
||||
},
|
||||
|
||||
// ☁️ Bedrock API配置
|
||||
bedrock: {
|
||||
enabled: process.env.CLAUDE_CODE_USE_BEDROCK === '1',
|
||||
defaultRegion: process.env.AWS_REGION || 'us-east-1',
|
||||
smallFastModelRegion: process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION,
|
||||
defaultModel: process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
smallFastModel: process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
maxOutputTokens: parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096,
|
||||
maxThinkingTokens: parseInt(process.env.MAX_THINKING_TOKENS) || 1024,
|
||||
enablePromptCaching: process.env.DISABLE_PROMPT_CACHING !== '1'
|
||||
},
|
||||
|
||||
// 🌐 代理配置
|
||||
proxy: {
|
||||
timeout: parseInt(process.env.DEFAULT_PROXY_TIMEOUT) || 30000,
|
||||
@@ -76,7 +88,7 @@ const config = {
|
||||
sessionSecret: process.env.WEB_SESSION_SECRET || 'CHANGE-THIS-SESSION-SECRET'
|
||||
},
|
||||
|
||||
// 🔒 客户端限制配置
|
||||
// 🔒 客户端限制配置
|
||||
clientRestrictions: {
|
||||
// 预定义的客户端列表
|
||||
predefinedClients: [
|
||||
|
||||
1490
package-lock.json
generated
1490
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,8 @@
|
||||
"test:pricing-fallback": "node scripts/test-pricing-fallback.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.861.0",
|
||||
"@aws-sdk/credential-providers": "^3.859.0",
|
||||
"axios": "^1.6.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.2",
|
||||
|
||||
35
scripts/test-bedrock-models.js
Normal file
35
scripts/test-bedrock-models.js
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const bedrockAccountService = require('../src/services/bedrockAccountService');
|
||||
const bedrockRelayService = require('../src/services/bedrockRelayService');
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
async function testBedrockModels() {
|
||||
try {
|
||||
console.log('🧪 测试Bedrock模型配置...');
|
||||
|
||||
// 测试可用模型列表
|
||||
const models = await bedrockRelayService.getAvailableModels();
|
||||
console.log(`📋 找到 ${models.length} 个可用模型:`);
|
||||
models.forEach(model => {
|
||||
console.log(` - ${model.id} (${model.name})`);
|
||||
});
|
||||
|
||||
// 测试默认模型
|
||||
console.log(`\n🎯 系统默认模型: ${bedrockRelayService.defaultModel}`);
|
||||
console.log(`🎯 系统默认小模型: ${bedrockRelayService.defaultSmallModel}`);
|
||||
|
||||
console.log('\n✅ Bedrock模型配置测试完成');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Bedrock模型测试失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
testBedrockModels();
|
||||
}
|
||||
|
||||
module.exports = { testBedrockModels };
|
||||
47
scripts/test-model-mapping.js
Normal file
47
scripts/test-model-mapping.js
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const bedrockRelayService = require('../src/services/bedrockRelayService');
|
||||
|
||||
function testModelMapping() {
|
||||
console.log('🧪 测试模型映射功能...');
|
||||
|
||||
// 测试用例
|
||||
const testCases = [
|
||||
// 标准Claude模型名
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-sonnet',
|
||||
'claude-3-5-haiku',
|
||||
'claude-sonnet-4',
|
||||
'claude-opus-4-1',
|
||||
'claude-3-7-sonnet',
|
||||
|
||||
// 已经是Bedrock格式的
|
||||
'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
'anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
|
||||
// 未知模型
|
||||
'unknown-model'
|
||||
];
|
||||
|
||||
console.log('\n📋 模型映射测试结果:');
|
||||
testCases.forEach(testModel => {
|
||||
const mappedModel = bedrockRelayService._mapToBedrockModel(testModel);
|
||||
const isChanged = mappedModel !== testModel;
|
||||
const status = isChanged ? '🔄' : '✅';
|
||||
|
||||
console.log(`${status} ${testModel}`);
|
||||
if (isChanged) {
|
||||
console.log(` → ${mappedModel}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n✅ 模型映射测试完成');
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
testModelMapping();
|
||||
}
|
||||
|
||||
module.exports = { testModelMapping };
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const apiKeyService = require('../services/apiKeyService');
|
||||
const claudeAccountService = require('../services/claudeAccountService');
|
||||
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService');
|
||||
const bedrockAccountService = require('../services/bedrockAccountService');
|
||||
const geminiAccountService = require('../services/geminiAccountService');
|
||||
const accountGroupService = require('../services/accountGroupService');
|
||||
const redis = require('../models/redis');
|
||||
@@ -1361,6 +1362,227 @@ router.put('/claude-console-accounts/:accountId/toggle-schedulable', authenticat
|
||||
}
|
||||
});
|
||||
|
||||
// ☁️ Bedrock 账户管理
|
||||
|
||||
// 获取所有Bedrock账户
|
||||
router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const result = await bedrockAccountService.getAllAccounts();
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: result.error });
|
||||
}
|
||||
|
||||
// 为每个账户添加使用统计信息
|
||||
const accountsWithStats = await Promise.all(result.data.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id);
|
||||
return {
|
||||
...account,
|
||||
usage: {
|
||||
daily: usageStats.daily,
|
||||
total: usageStats.total,
|
||||
averages: usageStats.averages
|
||||
}
|
||||
};
|
||||
} catch (statsError) {
|
||||
logger.warn(`⚠️ Failed to get usage stats for Bedrock account ${account.id}:`, statsError.message);
|
||||
return {
|
||||
...account,
|
||||
usage: {
|
||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
total: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
averages: { rpm: 0, tpm: 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: accountsWithStats });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get Bedrock accounts:', error);
|
||||
res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新的Bedrock账户
|
||||
router.post('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
region,
|
||||
awsCredentials,
|
||||
defaultModel,
|
||||
priority,
|
||||
accountType,
|
||||
credentialType
|
||||
} = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Name is required' });
|
||||
}
|
||||
|
||||
// 验证priority的有效性(1-100)
|
||||
if (priority !== undefined && (priority < 1 || priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
|
||||
}
|
||||
|
||||
// 验证accountType的有效性
|
||||
if (accountType && !['shared', 'dedicated'].includes(accountType)) {
|
||||
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
|
||||
}
|
||||
|
||||
// 验证credentialType的有效性
|
||||
if (credentialType && !['default', 'access_key', 'bearer_token'].includes(credentialType)) {
|
||||
return res.status(400).json({ error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' });
|
||||
}
|
||||
|
||||
const result = await bedrockAccountService.createAccount({
|
||||
name,
|
||||
description: description || '',
|
||||
region: region || 'us-east-1',
|
||||
awsCredentials,
|
||||
defaultModel,
|
||||
priority: priority || 50,
|
||||
accountType: accountType || 'shared',
|
||||
credentialType: credentialType || 'default'
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: 'Failed to create Bedrock account', message: result.error });
|
||||
}
|
||||
|
||||
logger.success(`☁️ Admin created Bedrock account: ${name}`);
|
||||
res.json({ success: true, data: result.data });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to create Bedrock account:', error);
|
||||
res.status(500).json({ error: 'Failed to create Bedrock account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新Bedrock账户
|
||||
router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// 验证priority的有效性(1-100)
|
||||
if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
|
||||
}
|
||||
|
||||
// 验证accountType的有效性
|
||||
if (updates.accountType && !['shared', 'dedicated'].includes(updates.accountType)) {
|
||||
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
|
||||
}
|
||||
|
||||
// 验证credentialType的有效性
|
||||
if (updates.credentialType && !['default', 'access_key', 'bearer_token'].includes(updates.credentialType)) {
|
||||
return res.status(400).json({ error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' });
|
||||
}
|
||||
|
||||
const result = await bedrockAccountService.updateAccount(accountId, updates);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: 'Failed to update Bedrock account', message: result.error });
|
||||
}
|
||||
|
||||
logger.success(`📝 Admin updated Bedrock account: ${accountId}`);
|
||||
res.json({ success: true, message: 'Bedrock account updated successfully' });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to update Bedrock account:', error);
|
||||
res.status(500).json({ error: 'Failed to update Bedrock account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除Bedrock账户
|
||||
router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const result = await bedrockAccountService.deleteAccount(accountId);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: 'Failed to delete Bedrock account', message: result.error });
|
||||
}
|
||||
|
||||
logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}`);
|
||||
res.json({ success: true, message: 'Bedrock account deleted successfully' });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to delete Bedrock account:', error);
|
||||
res.status(500).json({ error: 'Failed to delete Bedrock account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 切换Bedrock账户状态
|
||||
router.put('/bedrock-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const accountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
return res.status(404).json({ error: 'Account not found' });
|
||||
}
|
||||
|
||||
const newStatus = !accountResult.data.isActive;
|
||||
const updateResult = await bedrockAccountService.updateAccount(accountId, { isActive: newStatus });
|
||||
|
||||
if (!updateResult.success) {
|
||||
return res.status(500).json({ error: 'Failed to toggle account status', message: updateResult.error });
|
||||
}
|
||||
|
||||
logger.success(`🔄 Admin toggled Bedrock account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`);
|
||||
res.json({ success: true, isActive: newStatus });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle Bedrock account status:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle account status', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 切换Bedrock账户调度状态
|
||||
router.put('/bedrock-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const accountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
return res.status(404).json({ error: 'Account not found' });
|
||||
}
|
||||
|
||||
const newSchedulable = !accountResult.data.schedulable;
|
||||
const updateResult = await bedrockAccountService.updateAccount(accountId, { schedulable: newSchedulable });
|
||||
|
||||
if (!updateResult.success) {
|
||||
return res.status(500).json({ error: 'Failed to toggle schedulable status', message: updateResult.error });
|
||||
}
|
||||
|
||||
logger.success(`🔄 Admin toggled Bedrock account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
|
||||
res.json({ success: true, schedulable: newSchedulable });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle Bedrock account schedulable status:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 测试Bedrock账户连接
|
||||
router.post('/bedrock-accounts/:accountId/test', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const result = await bedrockAccountService.testAccount(accountId);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: 'Account test failed', message: result.error });
|
||||
}
|
||||
|
||||
logger.success(`🧪 Admin tested Bedrock account: ${accountId} - ${result.data.status}`);
|
||||
res.json({ success: true, data: result.data });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to test Bedrock account:', error);
|
||||
res.status(500).json({ error: 'Failed to test Bedrock account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 🤖 Gemini 账户管理
|
||||
|
||||
// 生成 Gemini OAuth 授权 URL
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const express = require('express');
|
||||
const claudeRelayService = require('../services/claudeRelayService');
|
||||
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService');
|
||||
const bedrockRelayService = require('../services/bedrockRelayService');
|
||||
const bedrockAccountService = require('../services/bedrockAccountService');
|
||||
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler');
|
||||
const apiKeyService = require('../services/apiKeyService');
|
||||
const { authenticateApiKey } = require('../middleware/auth');
|
||||
@@ -101,7 +103,7 @@ async function handleMessagesRequest(req, res) {
|
||||
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
} else if (accountType === 'claude-console') {
|
||||
// Claude Console账号使用Console转发服务(需要传递accountId)
|
||||
await claudeConsoleRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
|
||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||
@@ -135,6 +137,44 @@ async function handleMessagesRequest(req, res) {
|
||||
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
|
||||
}
|
||||
}, accountId);
|
||||
} else if (accountType === 'bedrock') {
|
||||
// Bedrock账号使用Bedrock转发服务
|
||||
try {
|
||||
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!bedrockAccountResult.success) {
|
||||
throw new Error('Failed to get Bedrock account details');
|
||||
}
|
||||
|
||||
const result = await bedrockRelayService.handleStreamRequest(req.body, bedrockAccountResult.data, res);
|
||||
|
||||
// 记录Bedrock使用统计
|
||||
if (result.usage) {
|
||||
const inputTokens = result.usage.input_tokens || 0;
|
||||
const outputTokens = result.usage.output_tokens || 0;
|
||||
|
||||
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, 0, 0, result.model, accountId).catch(error => {
|
||||
logger.error('❌ Failed to record Bedrock stream usage:', error);
|
||||
});
|
||||
|
||||
// 更新时间窗口内的token计数
|
||||
if (req.rateLimitInfo) {
|
||||
const totalTokens = inputTokens + outputTokens;
|
||||
redis.getClient().incrby(req.rateLimitInfo.tokenCountKey, totalTokens).catch(error => {
|
||||
logger.error('❌ Failed to update rate limit token count:', error);
|
||||
});
|
||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`);
|
||||
}
|
||||
|
||||
usageDataCaptured = true;
|
||||
logger.api(`📊 Bedrock stream usage recorded - Model: ${result.model}, Input: ${inputTokens}, Output: ${outputTokens}, Total: ${inputTokens + outputTokens} tokens`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Bedrock stream request failed:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Bedrock service error', message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 流式请求完成后 - 如果没有捕获到usage数据,记录警告但不进行估算
|
||||
@@ -166,10 +206,43 @@ async function handleMessagesRequest(req, res) {
|
||||
if (accountType === 'claude-official') {
|
||||
// 官方Claude账号使用原有的转发服务
|
||||
response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
|
||||
} else {
|
||||
} else if (accountType === 'claude-console') {
|
||||
// Claude Console账号使用Console转发服务
|
||||
logger.debug(`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`);
|
||||
response = await claudeConsoleRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers, accountId);
|
||||
} else if (accountType === 'bedrock') {
|
||||
// Bedrock账号使用Bedrock转发服务
|
||||
try {
|
||||
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!bedrockAccountResult.success) {
|
||||
throw new Error('Failed to get Bedrock account details');
|
||||
}
|
||||
|
||||
const result = await bedrockRelayService.handleNonStreamRequest(req.body, bedrockAccountResult.data, req.headers);
|
||||
|
||||
// 构建标准响应格式
|
||||
response = {
|
||||
statusCode: result.success ? 200 : 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(result.success ? result.data : { error: result.error }),
|
||||
accountId: accountId
|
||||
};
|
||||
|
||||
// 如果成功,添加使用统计到响应数据中
|
||||
if (result.success && result.usage) {
|
||||
const responseData = JSON.parse(response.body);
|
||||
responseData.usage = result.usage;
|
||||
response.body = JSON.stringify(responseData);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Bedrock non-stream request failed:', error);
|
||||
response = {
|
||||
statusCode: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ error: 'Bedrock service error', message: error.message }),
|
||||
accountId: accountId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('📡 Claude API response received', {
|
||||
|
||||
398
src/services/bedrockAccountService.js
Normal file
398
src/services/bedrockAccountService.js
Normal file
@@ -0,0 +1,398 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const crypto = require('crypto');
|
||||
const redis = require('../models/redis');
|
||||
const logger = require('../utils/logger');
|
||||
const config = require('../../config/config');
|
||||
const bedrockRelayService = require('./bedrockRelayService');
|
||||
|
||||
class BedrockAccountService {
|
||||
constructor() {
|
||||
// 加密相关常量
|
||||
this.ENCRYPTION_ALGORITHM = 'aes-256-cbc';
|
||||
this.ENCRYPTION_SALT = 'salt';
|
||||
}
|
||||
|
||||
// 🏢 创建Bedrock账户
|
||||
async createAccount(options = {}) {
|
||||
const {
|
||||
name = 'Unnamed Bedrock Account',
|
||||
description = '',
|
||||
region = process.env.AWS_REGION || 'us-east-1',
|
||||
awsCredentials = null, // { accessKeyId, secretAccessKey, sessionToken }
|
||||
defaultModel = 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
isActive = true,
|
||||
accountType = 'shared', // 'dedicated' or 'shared'
|
||||
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
||||
schedulable = true, // 是否可被调度
|
||||
credentialType = 'default' // 'default', 'access_key', 'bearer_token'
|
||||
} = options;
|
||||
|
||||
const accountId = uuidv4();
|
||||
|
||||
let accountData = {
|
||||
id: accountId,
|
||||
name,
|
||||
description,
|
||||
region,
|
||||
defaultModel,
|
||||
isActive,
|
||||
accountType,
|
||||
priority,
|
||||
schedulable,
|
||||
credentialType,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
type: 'bedrock' // 标识这是Bedrock账户
|
||||
};
|
||||
|
||||
// 加密存储AWS凭证
|
||||
if (awsCredentials) {
|
||||
accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials);
|
||||
}
|
||||
|
||||
const client = redis.getClientSafe();
|
||||
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData));
|
||||
|
||||
logger.info(`✅ 创建Bedrock账户成功 - ID: ${accountId}, 名称: ${name}, 区域: ${region}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: accountId,
|
||||
name,
|
||||
description,
|
||||
region,
|
||||
defaultModel,
|
||||
isActive,
|
||||
accountType,
|
||||
priority,
|
||||
schedulable,
|
||||
credentialType,
|
||||
createdAt: accountData.createdAt,
|
||||
type: 'bedrock'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 🔍 获取账户信息
|
||||
async getAccount(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe();
|
||||
const accountData = await client.get(`bedrock_account:${accountId}`);
|
||||
if (!accountData) {
|
||||
return { success: false, error: 'Account not found' };
|
||||
}
|
||||
|
||||
const account = JSON.parse(accountData);
|
||||
|
||||
// 解密AWS凭证用于内部使用
|
||||
if (account.awsCredentials) {
|
||||
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials);
|
||||
}
|
||||
|
||||
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: account
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`❌ 获取Bedrock账户失败 - ID: ${accountId}`, error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 📋 获取所有账户列表
|
||||
async getAllAccounts() {
|
||||
try {
|
||||
const client = redis.getClientSafe();
|
||||
const keys = await client.keys('bedrock_account:*');
|
||||
const accounts = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const accountData = await client.get(key);
|
||||
if (accountData) {
|
||||
const account = JSON.parse(accountData);
|
||||
|
||||
// 返回给前端时,不包含敏感信息,只显示掩码
|
||||
accounts.push({
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
description: account.description,
|
||||
region: account.region,
|
||||
defaultModel: account.defaultModel,
|
||||
isActive: account.isActive,
|
||||
accountType: account.accountType,
|
||||
priority: account.priority,
|
||||
schedulable: account.schedulable,
|
||||
credentialType: account.credentialType,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
type: 'bedrock',
|
||||
hasCredentials: !!account.awsCredentials
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级和名称排序
|
||||
accounts.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
logger.debug(`📋 获取所有Bedrock账户 - 共 ${accounts.length} 个`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accounts
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('❌ 获取Bedrock账户列表失败', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ✏️ 更新账户信息
|
||||
async updateAccount(accountId, updates = {}) {
|
||||
try {
|
||||
const accountResult = await this.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
return accountResult;
|
||||
}
|
||||
|
||||
const account = accountResult.data;
|
||||
|
||||
// 更新字段
|
||||
if (updates.name !== undefined) account.name = updates.name;
|
||||
if (updates.description !== undefined) account.description = updates.description;
|
||||
if (updates.region !== undefined) account.region = updates.region;
|
||||
if (updates.defaultModel !== undefined) account.defaultModel = updates.defaultModel;
|
||||
if (updates.isActive !== undefined) account.isActive = updates.isActive;
|
||||
if (updates.accountType !== undefined) account.accountType = updates.accountType;
|
||||
if (updates.priority !== undefined) account.priority = updates.priority;
|
||||
if (updates.schedulable !== undefined) account.schedulable = updates.schedulable;
|
||||
if (updates.credentialType !== undefined) account.credentialType = updates.credentialType;
|
||||
|
||||
// 更新AWS凭证
|
||||
if (updates.awsCredentials !== undefined) {
|
||||
if (updates.awsCredentials) {
|
||||
account.awsCredentials = this._encryptAwsCredentials(updates.awsCredentials);
|
||||
} else {
|
||||
delete account.awsCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
account.updatedAt = new Date().toISOString();
|
||||
|
||||
const client = redis.getClientSafe();
|
||||
await client.set(`bedrock_account:${accountId}`, JSON.stringify(account));
|
||||
|
||||
logger.info(`✅ 更新Bedrock账户成功 - ID: ${accountId}, 名称: ${account.name}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
description: account.description,
|
||||
region: account.region,
|
||||
defaultModel: account.defaultModel,
|
||||
isActive: account.isActive,
|
||||
accountType: account.accountType,
|
||||
priority: account.priority,
|
||||
schedulable: account.schedulable,
|
||||
credentialType: account.credentialType,
|
||||
updatedAt: account.updatedAt,
|
||||
type: 'bedrock'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`❌ 更新Bedrock账户失败 - ID: ${accountId}`, error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 🗑️ 删除账户
|
||||
async deleteAccount(accountId) {
|
||||
try {
|
||||
const accountResult = await this.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
return accountResult;
|
||||
}
|
||||
|
||||
const client = redis.getClientSafe();
|
||||
await client.del(`bedrock_account:${accountId}`);
|
||||
|
||||
logger.info(`✅ 删除Bedrock账户成功 - ID: ${accountId}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error(`❌ 删除Bedrock账户失败 - ID: ${accountId}`, error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 选择可用的Bedrock账户 (用于请求转发)
|
||||
async selectAvailableAccount() {
|
||||
try {
|
||||
const accountsResult = await this.getAllAccounts();
|
||||
if (!accountsResult.success) {
|
||||
return { success: false, error: 'Failed to get accounts' };
|
||||
}
|
||||
|
||||
const availableAccounts = accountsResult.data.filter(account =>
|
||||
account.isActive && account.schedulable
|
||||
);
|
||||
|
||||
if (availableAccounts.length === 0) {
|
||||
return { success: false, error: 'No available Bedrock accounts' };
|
||||
}
|
||||
|
||||
// 简单的轮询选择策略 - 选择优先级最高的账户
|
||||
const selectedAccount = availableAccounts[0];
|
||||
|
||||
// 获取完整账户信息(包含解密的凭证)
|
||||
const fullAccountResult = await this.getAccount(selectedAccount.id);
|
||||
if (!fullAccountResult.success) {
|
||||
return { success: false, error: 'Failed to get selected account details' };
|
||||
}
|
||||
|
||||
logger.debug(`🎯 选择Bedrock账户 - ID: ${selectedAccount.id}, 名称: ${selectedAccount.name}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: fullAccountResult.data
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('❌ 选择Bedrock账户失败', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 🧪 测试账户连接
|
||||
async testAccount(accountId) {
|
||||
try {
|
||||
const accountResult = await this.getAccount(accountId);
|
||||
if (!accountResult.success) {
|
||||
return accountResult;
|
||||
}
|
||||
|
||||
const account = accountResult.data;
|
||||
|
||||
logger.info(`🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}`);
|
||||
|
||||
// 尝试获取模型列表来测试连接
|
||||
const models = await bedrockRelayService.getAvailableModels(account);
|
||||
|
||||
if (models && models.length > 0) {
|
||||
logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
status: 'connected',
|
||||
modelsCount: models.length,
|
||||
region: account.region,
|
||||
credentialType: account.credentialType
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unable to retrieve models from Bedrock'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ 测试Bedrock账户失败 - ID: ${accountId}`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 🔐 加密AWS凭证
|
||||
_encryptAwsCredentials(credentials) {
|
||||
try {
|
||||
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest();
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
|
||||
|
||||
const credentialsString = JSON.stringify(credentials);
|
||||
let encrypted = cipher.update(credentialsString, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
return {
|
||||
encrypted: encrypted,
|
||||
iv: iv.toString('hex')
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('❌ AWS凭证加密失败', error);
|
||||
throw new Error('Credentials encryption failed');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔓 解密AWS凭证
|
||||
_decryptAwsCredentials(encryptedData) {
|
||||
try {
|
||||
// 检查数据格式
|
||||
if (!encryptedData || typeof encryptedData !== 'object') {
|
||||
logger.error('❌ 无效的加密数据格式:', encryptedData);
|
||||
throw new Error('Invalid encrypted data format');
|
||||
}
|
||||
|
||||
// 检查必要字段
|
||||
if (!encryptedData.encrypted || !encryptedData.iv) {
|
||||
logger.error('❌ 缺少加密数据字段:', {
|
||||
hasEncrypted: !!encryptedData.encrypted,
|
||||
hasIv: !!encryptedData.iv
|
||||
});
|
||||
throw new Error('Missing encrypted data fields');
|
||||
}
|
||||
|
||||
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest();
|
||||
const iv = Buffer.from(encryptedData.iv, 'hex');
|
||||
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
|
||||
|
||||
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return JSON.parse(decrypted);
|
||||
} catch (error) {
|
||||
logger.error('❌ AWS凭证解密失败', error);
|
||||
throw new Error('Credentials decryption failed');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 获取账户统计信息
|
||||
async getAccountStats() {
|
||||
try {
|
||||
const accountsResult = await this.getAllAccounts();
|
||||
if (!accountsResult.success) {
|
||||
return { success: false, error: accountsResult.error };
|
||||
}
|
||||
|
||||
const accounts = accountsResult.data;
|
||||
const stats = {
|
||||
total: accounts.length,
|
||||
active: accounts.filter(acc => acc.isActive).length,
|
||||
inactive: accounts.filter(acc => !acc.isActive).length,
|
||||
schedulable: accounts.filter(acc => acc.schedulable).length,
|
||||
byRegion: {},
|
||||
byCredentialType: {}
|
||||
};
|
||||
|
||||
// 按区域统计
|
||||
accounts.forEach(acc => {
|
||||
stats.byRegion[acc.region] = (stats.byRegion[acc.region] || 0) + 1;
|
||||
stats.byCredentialType[acc.credentialType] = (stats.byCredentialType[acc.credentialType] || 0) + 1;
|
||||
});
|
||||
|
||||
return { success: true, data: stats };
|
||||
} catch (error) {
|
||||
logger.error('❌ 获取Bedrock账户统计失败', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new BedrockAccountService();
|
||||
465
src/services/bedrockRelayService.js
Normal file
465
src/services/bedrockRelayService.js
Normal file
@@ -0,0 +1,465 @@
|
||||
const { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
|
||||
const { fromEnv } = require('@aws-sdk/credential-providers');
|
||||
const logger = require('../utils/logger');
|
||||
const config = require('../../config/config');
|
||||
|
||||
class BedrockRelayService {
|
||||
constructor() {
|
||||
this.defaultRegion = process.env.AWS_REGION || config.bedrock?.defaultRegion || 'us-east-1';
|
||||
this.smallFastModelRegion = process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION || this.defaultRegion;
|
||||
|
||||
// 默认模型配置
|
||||
this.defaultModel = process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0';
|
||||
this.defaultSmallModel = process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0';
|
||||
|
||||
// Token配置
|
||||
this.maxOutputTokens = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096;
|
||||
this.maxThinkingTokens = parseInt(process.env.MAX_THINKING_TOKENS) || 1024;
|
||||
this.enablePromptCaching = process.env.DISABLE_PROMPT_CACHING !== '1';
|
||||
|
||||
// 创建Bedrock客户端
|
||||
this.clients = new Map(); // 缓存不同区域的客户端
|
||||
}
|
||||
|
||||
// 获取或创建Bedrock客户端
|
||||
_getBedrockClient(region = null, bedrockAccount = null) {
|
||||
const targetRegion = region || this.defaultRegion;
|
||||
const clientKey = `${targetRegion}-${bedrockAccount?.id || 'default'}`;
|
||||
|
||||
if (this.clients.has(clientKey)) {
|
||||
return this.clients.get(clientKey);
|
||||
}
|
||||
|
||||
const clientConfig = {
|
||||
region: targetRegion
|
||||
};
|
||||
|
||||
// 如果账户配置了特定的AWS凭证,使用它们
|
||||
if (bedrockAccount?.awsCredentials) {
|
||||
clientConfig.credentials = {
|
||||
accessKeyId: bedrockAccount.awsCredentials.accessKeyId,
|
||||
secretAccessKey: bedrockAccount.awsCredentials.secretAccessKey,
|
||||
sessionToken: bedrockAccount.awsCredentials.sessionToken
|
||||
};
|
||||
} else {
|
||||
// 检查是否有环境变量凭证
|
||||
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
clientConfig.credentials = fromEnv();
|
||||
} else {
|
||||
throw new Error('AWS凭证未配置。请在Bedrock账户中配置AWS访问密钥,或设置环境变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY');
|
||||
}
|
||||
}
|
||||
|
||||
const client = new BedrockRuntimeClient(clientConfig);
|
||||
this.clients.set(clientKey, client);
|
||||
|
||||
logger.debug(`🔧 Created Bedrock client for region: ${targetRegion}, account: ${bedrockAccount?.name || 'default'}`);
|
||||
return client;
|
||||
}
|
||||
|
||||
// 处理非流式请求
|
||||
async handleNonStreamRequest(requestBody, bedrockAccount = null) {
|
||||
try {
|
||||
const modelId = this._selectModel(requestBody, bedrockAccount);
|
||||
const region = this._selectRegion(modelId, bedrockAccount);
|
||||
const client = this._getBedrockClient(region, bedrockAccount);
|
||||
|
||||
// 转换请求格式为Bedrock格式
|
||||
const bedrockPayload = this._convertToBedrockFormat(requestBody);
|
||||
|
||||
const command = new InvokeModelCommand({
|
||||
modelId: modelId,
|
||||
body: JSON.stringify(bedrockPayload),
|
||||
contentType: 'application/json',
|
||||
accept: 'application/json'
|
||||
});
|
||||
|
||||
logger.debug(`🚀 Bedrock非流式请求 - 模型: ${modelId}, 区域: ${region}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await client.send(command);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 解析响应
|
||||
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
|
||||
const claudeResponse = this._convertFromBedrockFormat(responseBody);
|
||||
|
||||
logger.info(`✅ Bedrock请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: claudeResponse,
|
||||
usage: claudeResponse.usage,
|
||||
model: modelId,
|
||||
duration
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Bedrock非流式请求失败:', error);
|
||||
throw this._handleBedrockError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理流式请求
|
||||
async handleStreamRequest(requestBody, bedrockAccount = null, res) {
|
||||
try {
|
||||
const modelId = this._selectModel(requestBody, bedrockAccount);
|
||||
const region = this._selectRegion(modelId, bedrockAccount);
|
||||
const client = this._getBedrockClient(region, bedrockAccount);
|
||||
|
||||
// 转换请求格式为Bedrock格式
|
||||
const bedrockPayload = this._convertToBedrockFormat(requestBody);
|
||||
|
||||
const command = new InvokeModelWithResponseStreamCommand({
|
||||
modelId: modelId,
|
||||
body: JSON.stringify(bedrockPayload),
|
||||
contentType: 'application/json',
|
||||
accept: 'application/json'
|
||||
});
|
||||
|
||||
logger.debug(`🌊 Bedrock流式请求 - 模型: ${modelId}, 区域: ${region}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await client.send(command);
|
||||
|
||||
// 设置SSE响应头
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
});
|
||||
|
||||
let totalUsage = null;
|
||||
let isFirstChunk = true;
|
||||
|
||||
// 处理流式响应
|
||||
for await (const chunk of response.body) {
|
||||
if (chunk.chunk) {
|
||||
const chunkData = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes));
|
||||
const claudeEvent = this._convertBedrockStreamToClaudeFormat(chunkData, isFirstChunk);
|
||||
|
||||
if (claudeEvent) {
|
||||
// 发送SSE事件
|
||||
res.write(`event: ${claudeEvent.type}\n`);
|
||||
res.write(`data: ${JSON.stringify(claudeEvent.data)}\n\n`);
|
||||
|
||||
// 提取使用统计
|
||||
if (claudeEvent.type === 'message_stop' && claudeEvent.data.usage) {
|
||||
totalUsage = claudeEvent.data.usage;
|
||||
}
|
||||
|
||||
isFirstChunk = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info(`✅ Bedrock流式请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
|
||||
|
||||
// 发送结束事件
|
||||
res.write('event: done\n');
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
usage: totalUsage,
|
||||
model: modelId,
|
||||
duration
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Bedrock流式请求失败:', error);
|
||||
|
||||
// 发送错误事件
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
}
|
||||
|
||||
res.write('event: error\n');
|
||||
res.write(`data: ${JSON.stringify({ error: this._handleBedrockError(error).message })}\n\n`);
|
||||
res.end();
|
||||
|
||||
throw this._handleBedrockError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 选择使用的模型
|
||||
_selectModel(requestBody, bedrockAccount) {
|
||||
let selectedModel;
|
||||
|
||||
// 优先使用账户配置的模型
|
||||
if (bedrockAccount?.defaultModel) {
|
||||
selectedModel = bedrockAccount.defaultModel;
|
||||
logger.info(`🎯 使用账户配置的模型: ${selectedModel}`, { metadata: { source: 'account', accountId: bedrockAccount.id } });
|
||||
}
|
||||
// 检查请求中指定的模型
|
||||
else if (requestBody.model) {
|
||||
selectedModel = requestBody.model;
|
||||
logger.info(`🎯 使用请求指定的模型: ${selectedModel}`, { metadata: { source: 'request' } });
|
||||
}
|
||||
// 使用默认模型
|
||||
else {
|
||||
selectedModel = this.defaultModel;
|
||||
logger.info(`🎯 使用系统默认模型: ${selectedModel}`, { metadata: { source: 'default' } });
|
||||
}
|
||||
|
||||
// 如果是标准Claude模型名,需要映射为Bedrock格式
|
||||
const bedrockModel = this._mapToBedrockModel(selectedModel);
|
||||
if (bedrockModel !== selectedModel) {
|
||||
logger.info(`🔄 模型映射: ${selectedModel} → ${bedrockModel}`, { metadata: { originalModel: selectedModel, bedrockModel } });
|
||||
}
|
||||
|
||||
return bedrockModel;
|
||||
}
|
||||
|
||||
// 将标准Claude模型名映射为Bedrock格式
|
||||
_mapToBedrockModel(modelName) {
|
||||
// 标准Claude模型名到Bedrock模型名的映射表
|
||||
const modelMapping = {
|
||||
// Claude Sonnet 4
|
||||
'claude-sonnet-4': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
'claude-sonnet-4-20250514': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
|
||||
// Claude Opus 4.1
|
||||
'claude-opus-4': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
'claude-opus-4-1': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
'claude-opus-4-1-20250805': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
|
||||
// Claude 3.7 Sonnet
|
||||
'claude-3-7-sonnet': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
||||
'claude-3-7-sonnet-20250219': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
||||
|
||||
// Claude 3.5 Sonnet v2
|
||||
'claude-3-5-sonnet': 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
'claude-3-5-sonnet-20241022': 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
|
||||
// Claude 3.5 Haiku
|
||||
'claude-3-5-haiku': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
'claude-3-5-haiku-20241022': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
|
||||
// Claude 3 Sonnet
|
||||
'claude-3-sonnet': 'us.anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
'claude-3-sonnet-20240229': 'us.anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
|
||||
// Claude 3 Haiku
|
||||
'claude-3-haiku': 'us.anthropic.claude-3-haiku-20240307-v1:0',
|
||||
'claude-3-haiku-20240307': 'us.anthropic.claude-3-haiku-20240307-v1:0'
|
||||
};
|
||||
|
||||
// 如果已经是Bedrock格式,直接返回
|
||||
// Bedrock模型格式:{region}.anthropic.{model-name} 或 anthropic.{model-name}
|
||||
if (modelName.includes('.anthropic.') || modelName.startsWith('anthropic.')) {
|
||||
return modelName;
|
||||
}
|
||||
|
||||
// 查找映射
|
||||
const mappedModel = modelMapping[modelName];
|
||||
if (mappedModel) {
|
||||
return mappedModel;
|
||||
}
|
||||
|
||||
// 如果没有找到映射,返回原始模型名(可能会导致错误,但保持向后兼容)
|
||||
logger.warn(`⚠️ 未找到模型映射: ${modelName},使用原始模型名`, { metadata: { originalModel: modelName } });
|
||||
return modelName;
|
||||
}
|
||||
|
||||
// 选择使用的区域
|
||||
_selectRegion(modelId, bedrockAccount) {
|
||||
// 优先使用账户配置的区域
|
||||
if (bedrockAccount?.region) {
|
||||
return bedrockAccount.region;
|
||||
}
|
||||
|
||||
// 对于小模型,使用专门的区域配置
|
||||
if (modelId.includes('haiku')) {
|
||||
return this.smallFastModelRegion;
|
||||
}
|
||||
|
||||
return this.defaultRegion;
|
||||
}
|
||||
|
||||
// 转换Claude格式请求到Bedrock格式
|
||||
_convertToBedrockFormat(requestBody) {
|
||||
const bedrockPayload = {
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: Math.min(requestBody.max_tokens || this.maxOutputTokens, this.maxOutputTokens),
|
||||
messages: requestBody.messages || []
|
||||
};
|
||||
|
||||
// 添加系统提示词
|
||||
if (requestBody.system) {
|
||||
bedrockPayload.system = requestBody.system;
|
||||
}
|
||||
|
||||
// 添加其他参数
|
||||
if (requestBody.temperature !== undefined) {
|
||||
bedrockPayload.temperature = requestBody.temperature;
|
||||
}
|
||||
|
||||
if (requestBody.top_p !== undefined) {
|
||||
bedrockPayload.top_p = requestBody.top_p;
|
||||
}
|
||||
|
||||
if (requestBody.top_k !== undefined) {
|
||||
bedrockPayload.top_k = requestBody.top_k;
|
||||
}
|
||||
|
||||
if (requestBody.stop_sequences) {
|
||||
bedrockPayload.stop_sequences = requestBody.stop_sequences;
|
||||
}
|
||||
|
||||
// 工具调用支持
|
||||
if (requestBody.tools) {
|
||||
bedrockPayload.tools = requestBody.tools;
|
||||
}
|
||||
|
||||
if (requestBody.tool_choice) {
|
||||
bedrockPayload.tool_choice = requestBody.tool_choice;
|
||||
}
|
||||
|
||||
return bedrockPayload;
|
||||
}
|
||||
|
||||
// 转换Bedrock响应到Claude格式
|
||||
_convertFromBedrockFormat(bedrockResponse) {
|
||||
return {
|
||||
id: `msg_${Date.now()}_bedrock`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: bedrockResponse.content || [],
|
||||
model: bedrockResponse.model || this.defaultModel,
|
||||
stop_reason: bedrockResponse.stop_reason || 'end_turn',
|
||||
stop_sequence: bedrockResponse.stop_sequence || null,
|
||||
usage: bedrockResponse.usage || {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 转换Bedrock流事件到Claude SSE格式
|
||||
_convertBedrockStreamToClaudeFormat(bedrockChunk) {
|
||||
if (bedrockChunk.type === 'message_start') {
|
||||
return {
|
||||
type: 'message_start',
|
||||
data: {
|
||||
type: 'message',
|
||||
id: `msg_${Date.now()}_bedrock`,
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
model: this.defaultModel,
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: bedrockChunk.message?.usage || { input_tokens: 0, output_tokens: 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (bedrockChunk.type === 'content_block_delta') {
|
||||
return {
|
||||
type: 'content_block_delta',
|
||||
data: {
|
||||
index: bedrockChunk.index || 0,
|
||||
delta: bedrockChunk.delta || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (bedrockChunk.type === 'message_delta') {
|
||||
return {
|
||||
type: 'message_delta',
|
||||
data: {
|
||||
delta: bedrockChunk.delta || {},
|
||||
usage: bedrockChunk.usage || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (bedrockChunk.type === 'message_stop') {
|
||||
return {
|
||||
type: 'message_stop',
|
||||
data: {
|
||||
usage: bedrockChunk.usage || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理Bedrock错误
|
||||
_handleBedrockError(error) {
|
||||
const errorMessage = error.message || 'Unknown Bedrock error';
|
||||
|
||||
if (error.name === 'ValidationException') {
|
||||
return new Error(`Bedrock参数验证失败: ${errorMessage}`);
|
||||
}
|
||||
|
||||
if (error.name === 'ThrottlingException') {
|
||||
return new Error('Bedrock请求限流,请稍后重试');
|
||||
}
|
||||
|
||||
if (error.name === 'AccessDeniedException') {
|
||||
return new Error('Bedrock访问被拒绝,请检查IAM权限');
|
||||
}
|
||||
|
||||
if (error.name === 'ModelNotReadyException') {
|
||||
return new Error('Bedrock模型未就绪,请稍后重试');
|
||||
}
|
||||
|
||||
return new Error(`Bedrock服务错误: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// 获取可用模型列表
|
||||
async getAvailableModels(bedrockAccount = null) {
|
||||
try {
|
||||
const region = bedrockAccount?.region || this.defaultRegion;
|
||||
|
||||
// Bedrock暂不支持列出推理配置文件的API,返回预定义的模型列表
|
||||
const models = [
|
||||
{
|
||||
id: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
name: 'Claude Sonnet 4',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
name: 'Claude Opus 4.1',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
||||
name: 'Claude 3.7 Sonnet',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
name: 'Claude 3.5 Sonnet v2',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
name: 'Claude 3.5 Haiku',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
}
|
||||
];
|
||||
|
||||
logger.debug(`📋 返回Bedrock可用模型 ${models.length} 个, 区域: ${region}`);
|
||||
return models;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ 获取Bedrock模型列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new BedrockRelayService();
|
||||
@@ -559,7 +559,7 @@ class ClaudeRelayService {
|
||||
}
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error(": ❌ ", error);
|
||||
console.error(': ❌ ', error);
|
||||
logger.error('❌ Claude API request error:', error.message, {
|
||||
code: error.code,
|
||||
errno: error.errno,
|
||||
@@ -724,7 +724,7 @@ class ClaudeRelayService {
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.error(": ❌ ", errorData);
|
||||
console.error(': ❌ ', errorData);
|
||||
logger.error('❌ Claude API error response:', errorData);
|
||||
if (!responseStream.destroyed) {
|
||||
// 发送错误事件
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const claudeAccountService = require('./claudeAccountService');
|
||||
const claudeConsoleAccountService = require('./claudeConsoleAccountService');
|
||||
const bedrockAccountService = require('./bedrockAccountService');
|
||||
const accountGroupService = require('./accountGroupService');
|
||||
const redis = require('../models/redis');
|
||||
const logger = require('../utils/logger');
|
||||
@@ -58,6 +59,20 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查Bedrock账户绑定
|
||||
if (apiKeyData.bedrockAccountId) {
|
||||
const boundBedrockAccountResult = await bedrockAccountService.getAccount(apiKeyData.bedrockAccountId);
|
||||
if (boundBedrockAccountResult.success && boundBedrockAccountResult.data.isActive === true) {
|
||||
logger.info(`🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId}) for API key ${apiKeyData.name}`);
|
||||
return {
|
||||
accountId: apiKeyData.bedrockAccountId,
|
||||
accountType: 'bedrock'
|
||||
};
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available, falling back to pool`);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有会话哈希,检查是否有已映射的账户
|
||||
if (sessionHash) {
|
||||
const mappedAccount = await this._getSessionMapping(sessionHash);
|
||||
@@ -155,6 +170,23 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查Bedrock账户绑定
|
||||
if (apiKeyData.bedrockAccountId) {
|
||||
const boundBedrockAccountResult = await bedrockAccountService.getAccount(apiKeyData.bedrockAccountId);
|
||||
if (boundBedrockAccountResult.success && boundBedrockAccountResult.data.isActive === true) {
|
||||
logger.info(`🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId})`);
|
||||
return [{
|
||||
...boundBedrockAccountResult.data,
|
||||
accountId: boundBedrockAccountResult.data.id,
|
||||
accountType: 'bedrock',
|
||||
priority: parseInt(boundBedrockAccountResult.data.priority) || 50,
|
||||
lastUsedAt: boundBedrockAccountResult.data.lastUsedAt || '0'
|
||||
}];
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取官方Claude账户(共享池)
|
||||
const claudeAccounts = await redis.getAllClaudeAccounts();
|
||||
for (const account of claudeAccounts) {
|
||||
@@ -227,8 +259,35 @@ class UnifiedClaudeScheduler {
|
||||
logger.info(`❌ Claude Console account ${account.name} not eligible - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Bedrock账户(共享池)
|
||||
const bedrockAccountsResult = await bedrockAccountService.getAllAccounts();
|
||||
if (bedrockAccountsResult.success) {
|
||||
const bedrockAccounts = bedrockAccountsResult.data;
|
||||
logger.info(`📋 Found ${bedrockAccounts.length} total Bedrock accounts`);
|
||||
|
||||
for (const account of bedrockAccounts) {
|
||||
logger.info(`🔍 Checking Bedrock account: ${account.name} - isActive: ${account.isActive}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`);
|
||||
|
||||
if (account.isActive === true &&
|
||||
account.accountType === 'shared' &&
|
||||
this._isSchedulable(account.schedulable)) { // 检查是否可调度
|
||||
|
||||
availableAccounts.push({
|
||||
...account,
|
||||
accountId: account.id,
|
||||
accountType: 'bedrock',
|
||||
priority: parseInt(account.priority) || 50,
|
||||
lastUsedAt: account.lastUsedAt || '0'
|
||||
});
|
||||
logger.info(`✅ Added Bedrock account to available pool: ${account.name} (priority: ${account.priority})`);
|
||||
} else {
|
||||
logger.info(`❌ Bedrock account ${account.name} not eligible - isActive: ${account.isActive}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`📊 Total available accounts: ${availableAccounts.length} (Claude: ${availableAccounts.filter(a => a.accountType === 'claude-official').length}, Console: ${availableAccounts.filter(a => a.accountType === 'claude-console').length})`);
|
||||
logger.info(`📊 Total available accounts: ${availableAccounts.length} (Claude: ${availableAccounts.filter(a => a.accountType === 'claude-official').length}, Console: ${availableAccounts.filter(a => a.accountType === 'claude-console').length}, Bedrock: ${availableAccounts.filter(a => a.accountType === 'bedrock').length})`);
|
||||
return availableAccounts;
|
||||
}
|
||||
|
||||
@@ -272,6 +331,18 @@ class UnifiedClaudeScheduler {
|
||||
return false;
|
||||
}
|
||||
return !(await claudeConsoleAccountService.isAccountRateLimited(accountId));
|
||||
} else if (accountType === 'bedrock') {
|
||||
const accountResult = await bedrockAccountService.getAccount(accountId);
|
||||
if (!accountResult.success || !accountResult.data.isActive) {
|
||||
return false;
|
||||
}
|
||||
// 检查是否可调度
|
||||
if (!this._isSchedulable(accountResult.data.schedulable)) {
|
||||
logger.info(`🚫 Bedrock account ${accountId} is not schedulable`);
|
||||
return false;
|
||||
}
|
||||
// Bedrock账户暂不需要限流检查,因为AWS管理限流
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
|
||||
@@ -83,10 +83,19 @@
|
||||
>
|
||||
<span class="text-sm text-gray-700">Gemini</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="form.platform"
|
||||
type="radio"
|
||||
value="bedrock"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">Bedrock</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEdit && form.platform !== 'claude-console'">
|
||||
<div v-if="!isEdit && form.platform !== 'claude-console' && form.platform !== 'bedrock'">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">添加方式</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
@@ -253,6 +262,157 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bedrock 特定字段 -->
|
||||
<div
|
||||
v-if="form.platform === 'bedrock' && !isEdit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">AWS 访问密钥 ID *</label>
|
||||
<input
|
||||
v-model="form.accessKeyId"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
:class="{ 'border-red-500': errors.accessKeyId }"
|
||||
placeholder="请输入 AWS Access Key ID"
|
||||
>
|
||||
<p
|
||||
v-if="errors.accessKeyId"
|
||||
class="text-red-500 text-xs mt-1"
|
||||
>
|
||||
{{ errors.accessKeyId }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">AWS 秘密访问密钥 *</label>
|
||||
<input
|
||||
v-model="form.secretAccessKey"
|
||||
type="password"
|
||||
required
|
||||
class="form-input w-full"
|
||||
:class="{ 'border-red-500': errors.secretAccessKey }"
|
||||
placeholder="请输入 AWS Secret Access Key"
|
||||
>
|
||||
<p
|
||||
v-if="errors.secretAccessKey"
|
||||
class="text-red-500 text-xs mt-1"
|
||||
>
|
||||
{{ errors.secretAccessKey }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">AWS 区域 *</label>
|
||||
<input
|
||||
v-model="form.region"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
:class="{ 'border-red-500': errors.region }"
|
||||
placeholder="例如:us-east-1"
|
||||
>
|
||||
<p
|
||||
v-if="errors.region"
|
||||
class="text-red-500 text-xs mt-1"
|
||||
>
|
||||
{{ errors.region }}
|
||||
</p>
|
||||
<div class="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-info-circle text-blue-600 mt-0.5" />
|
||||
<div class="text-xs text-blue-700">
|
||||
<p class="font-medium mb-1">
|
||||
常用 AWS 区域参考:
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-1 text-xs">
|
||||
<span>• us-east-1 (美国东部)</span>
|
||||
<span>• us-west-2 (美国西部)</span>
|
||||
<span>• eu-west-1 (欧洲爱尔兰)</span>
|
||||
<span>• ap-southeast-1 (新加坡)</span>
|
||||
<span>• ap-northeast-1 (东京)</span>
|
||||
<span>• eu-central-1 (法兰克福)</span>
|
||||
</div>
|
||||
<p class="mt-2 text-blue-600">
|
||||
💡 请输入完整的区域代码,如 us-east-1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">会话令牌 (可选)</label>
|
||||
<input
|
||||
v-model="form.sessionToken"
|
||||
type="password"
|
||||
class="form-input w-full"
|
||||
placeholder="如果使用临时凭证,请输入会话令牌"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
仅在使用临时 AWS 凭证时需要填写
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">默认主模型 (可选)</label>
|
||||
<input
|
||||
v-model="form.defaultModel"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:us.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
留空将使用系统默认模型。支持 inference profile ID 或 ARN
|
||||
</p>
|
||||
<div class="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-info-circle text-amber-600 mt-0.5" />
|
||||
<div class="text-xs text-amber-700">
|
||||
<p class="font-medium mb-1">
|
||||
Bedrock 模型配置说明:
|
||||
</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-xs">
|
||||
<li>支持 Inference Profile ID(推荐)</li>
|
||||
<li>支持 Application Inference Profile ARN</li>
|
||||
<li>常用模型:us.anthropic.claude-sonnet-4-20250514-v1:0</li>
|
||||
<li>留空将使用系统配置的默认模型</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">小快速模型 (可选)</label>
|
||||
<input
|
||||
v-model="form.smallFastModel"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:us.anthropic.claude-3-5-haiku-20241022-v1:0"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
用于快速响应的轻量级模型,留空将使用系统默认
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">限流时间 (分钟)</label>
|
||||
<input
|
||||
v-model.number="form.rateLimitDuration"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-input w-full"
|
||||
placeholder="默认60分钟"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
当账号返回429错误时,暂停调度的时间(分钟)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Console 特定字段 -->
|
||||
<div
|
||||
v-if="form.platform === 'claude-console' && !isEdit"
|
||||
@@ -295,7 +455,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">支持的模型 (可选)--注意,ClaudeCode必须加上hiku模型!</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">支持的模型 (可选)--注意,ClaudeCode必须加上haiku模型!</label>
|
||||
<div class="mb-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -323,7 +483,7 @@
|
||||
v-model="form.supportedModels"
|
||||
rows="3"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"
|
||||
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上haiku模型!"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号
|
||||
@@ -355,8 +515,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude和Claude Console的优先级设置 -->
|
||||
<div v-if="(form.platform === 'claude' || form.platform === 'claude-console')">
|
||||
<!-- Claude、Claude Console和Bedrock的优先级设置 -->
|
||||
<div v-if="(form.platform === 'claude' || form.platform === 'claude-console' || form.platform === 'bedrock')">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">调度优先级 (1-100)</label>
|
||||
<input
|
||||
v-model.number="form.priority"
|
||||
@@ -373,7 +533,7 @@
|
||||
|
||||
<!-- 手动输入 Token 字段 -->
|
||||
<div
|
||||
v-if="form.addType === 'manual' && form.platform !== 'claude-console'"
|
||||
v-if="form.addType === 'manual' && form.platform !== 'claude-console' && form.platform !== 'bedrock'"
|
||||
class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200"
|
||||
>
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
@@ -462,7 +622,7 @@
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
v-if="form.addType === 'oauth' && form.platform !== 'claude-console'"
|
||||
v-if="form.addType === 'oauth' && form.platform !== 'claude-console' && form.platform !== 'bedrock'"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
@@ -609,8 +769,8 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Claude和Claude Console的优先级设置(编辑模式) -->
|
||||
<div v-if="(form.platform === 'claude' || form.platform === 'claude-console')">
|
||||
<!-- Claude、Claude Console和Bedrock的优先级设置(编辑模式) -->
|
||||
<div v-if="(form.platform === 'claude' || form.platform === 'claude-console' || form.platform === 'bedrock')">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">调度优先级 (1-100)</label>
|
||||
<input
|
||||
v-model.number="form.priority"
|
||||
@@ -683,7 +843,7 @@
|
||||
v-model="form.supportedModels"
|
||||
rows="3"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"
|
||||
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上haiku模型!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -708,9 +868,115 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bedrock 特定字段(编辑模式)-->
|
||||
<div
|
||||
v-if="form.platform === 'bedrock'"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">AWS 访问密钥 ID</label>
|
||||
<input
|
||||
v-model="form.accessKeyId"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="留空表示不更新"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
留空表示不更新 AWS Access Key ID
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">AWS 秘密访问密钥</label>
|
||||
<input
|
||||
v-model="form.secretAccessKey"
|
||||
type="password"
|
||||
class="form-input w-full"
|
||||
placeholder="留空表示不更新"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
留空表示不更新 AWS Secret Access Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">AWS 区域</label>
|
||||
<input
|
||||
v-model="form.region"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:us-east-1"
|
||||
>
|
||||
<div class="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-info-circle text-blue-600 mt-0.5" />
|
||||
<div class="text-xs text-blue-700">
|
||||
<p class="font-medium mb-1">
|
||||
常用 AWS 区域参考:
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-1 text-xs">
|
||||
<span>• us-east-1 (美国东部)</span>
|
||||
<span>• us-west-2 (美国西部)</span>
|
||||
<span>• eu-west-1 (欧洲爱尔兰)</span>
|
||||
<span>• ap-southeast-1 (新加坡)</span>
|
||||
<span>• ap-northeast-1 (东京)</span>
|
||||
<span>• eu-central-1 (法兰克福)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">会话令牌 (可选)</label>
|
||||
<input
|
||||
v-model="form.sessionToken"
|
||||
type="password"
|
||||
class="form-input w-full"
|
||||
placeholder="留空表示不更新"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">默认主模型 (可选)</label>
|
||||
<input
|
||||
v-model="form.defaultModel"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:us.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
留空将使用系统默认模型。支持 inference profile ID 或 ARN
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">小快速模型 (可选)</label>
|
||||
<input
|
||||
v-model="form.smallFastModel"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:us.anthropic.claude-3-5-haiku-20241022-v1:0"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
用于快速响应的轻量级模型,留空将使用系统默认
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">限流时间 (分钟)</label>
|
||||
<input
|
||||
v-model.number="form.rateLimitDuration"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-input w-full"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token 更新 -->
|
||||
<div
|
||||
v-if="form.platform !== 'claude-console'"
|
||||
v-if="form.platform !== 'claude-console' && form.platform !== 'bedrock'"
|
||||
class="bg-amber-50 p-4 rounded-lg border border-amber-200"
|
||||
>
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
@@ -884,7 +1150,14 @@ const form = ref({
|
||||
return '';
|
||||
})(),
|
||||
userAgent: props.account?.userAgent || '',
|
||||
rateLimitDuration: props.account?.rateLimitDuration || 60
|
||||
rateLimitDuration: props.account?.rateLimitDuration || 60,
|
||||
// Bedrock 特定字段
|
||||
accessKeyId: props.account?.accessKeyId || '',
|
||||
secretAccessKey: props.account?.secretAccessKey || '',
|
||||
region: props.account?.region || '',
|
||||
sessionToken: props.account?.sessionToken || '',
|
||||
defaultModel: props.account?.defaultModel || '',
|
||||
smallFastModel: props.account?.smallFastModel || ''
|
||||
})
|
||||
|
||||
// 表单验证错误
|
||||
@@ -892,7 +1165,10 @@ const errors = ref({
|
||||
name: '',
|
||||
accessToken: '',
|
||||
apiUrl: '',
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
region: ''
|
||||
})
|
||||
|
||||
// 计算是否可以进入下一步
|
||||
@@ -1015,6 +1291,20 @@ const createAccount = async () => {
|
||||
errors.value.apiKey = '请填写 API Key'
|
||||
hasError = true
|
||||
}
|
||||
} else if (form.value.platform === 'bedrock') {
|
||||
// Bedrock 验证
|
||||
if (!form.value.accessKeyId || form.value.accessKeyId.trim() === '') {
|
||||
errors.value.accessKeyId = '请填写 AWS 访问密钥 ID'
|
||||
hasError = true
|
||||
}
|
||||
if (!form.value.secretAccessKey || form.value.secretAccessKey.trim() === '') {
|
||||
errors.value.secretAccessKey = '请填写 AWS 秘密访问密钥'
|
||||
hasError = true
|
||||
}
|
||||
if (!form.value.region || form.value.region.trim() === '') {
|
||||
errors.value.region = '请选择 AWS 区域'
|
||||
hasError = true
|
||||
}
|
||||
} else if (form.value.addType === 'manual' && (!form.value.accessToken || form.value.accessToken.trim() === '')) {
|
||||
errors.value.accessToken = '请填写 Access Token'
|
||||
hasError = true
|
||||
@@ -1086,6 +1376,18 @@ const createAccount = async () => {
|
||||
: []
|
||||
data.userAgent = form.value.userAgent || null
|
||||
data.rateLimitDuration = form.value.rateLimitDuration || 60
|
||||
} else if (form.value.platform === 'bedrock') {
|
||||
// Bedrock 账户特定数据 - 构造 awsCredentials 对象
|
||||
data.awsCredentials = {
|
||||
accessKeyId: form.value.accessKeyId,
|
||||
secretAccessKey: form.value.secretAccessKey,
|
||||
sessionToken: form.value.sessionToken || null
|
||||
}
|
||||
data.region = form.value.region
|
||||
data.defaultModel = form.value.defaultModel || null
|
||||
data.smallFastModel = form.value.smallFastModel || null
|
||||
data.priority = form.value.priority || 50
|
||||
data.rateLimitDuration = form.value.rateLimitDuration || 60
|
||||
}
|
||||
|
||||
let result
|
||||
@@ -1093,6 +1395,8 @@ const createAccount = async () => {
|
||||
result = await accountsStore.createClaudeAccount(data)
|
||||
} else if (form.value.platform === 'claude-console') {
|
||||
result = await accountsStore.createClaudeConsoleAccount(data)
|
||||
} else if (form.value.platform === 'bedrock') {
|
||||
result = await accountsStore.createBedrockAccount(data)
|
||||
} else {
|
||||
result = await accountsStore.createGeminiAccount(data)
|
||||
}
|
||||
@@ -1207,10 +1511,37 @@ const updateAccount = async () => {
|
||||
data.rateLimitDuration = form.value.rateLimitDuration || 60
|
||||
}
|
||||
|
||||
// Bedrock 特定更新
|
||||
if (props.account.platform === 'bedrock') {
|
||||
// 只有当有凭证变更时才构造 awsCredentials 对象
|
||||
if (form.value.accessKeyId || form.value.secretAccessKey || form.value.sessionToken) {
|
||||
data.awsCredentials = {}
|
||||
if (form.value.accessKeyId) {
|
||||
data.awsCredentials.accessKeyId = form.value.accessKeyId
|
||||
}
|
||||
if (form.value.secretAccessKey) {
|
||||
data.awsCredentials.secretAccessKey = form.value.secretAccessKey
|
||||
}
|
||||
if (form.value.sessionToken !== undefined) {
|
||||
data.awsCredentials.sessionToken = form.value.sessionToken || null
|
||||
}
|
||||
}
|
||||
if (form.value.region) {
|
||||
data.region = form.value.region
|
||||
}
|
||||
// 模型配置(支持设置为空来使用系统默认)
|
||||
data.defaultModel = form.value.defaultModel || null
|
||||
data.smallFastModel = form.value.smallFastModel || null
|
||||
data.priority = form.value.priority || 50
|
||||
data.rateLimitDuration = form.value.rateLimitDuration || 60
|
||||
}
|
||||
|
||||
if (props.account.platform === 'claude') {
|
||||
await accountsStore.updateClaudeAccount(props.account.id, data)
|
||||
} else if (props.account.platform === 'claude-console') {
|
||||
await accountsStore.updateClaudeConsoleAccount(props.account.id, data)
|
||||
} else if (props.account.platform === 'bedrock') {
|
||||
await accountsStore.updateBedrockAccount(props.account.id, data)
|
||||
} else {
|
||||
await accountsStore.updateGeminiAccount(props.account.id, data)
|
||||
}
|
||||
@@ -1290,8 +1621,8 @@ const handleGroupRefresh = async () => {
|
||||
// 监听平台变化,重置表单
|
||||
watch(() => form.value.platform, (newPlatform, oldPlatform) => {
|
||||
// 处理添加方式的自动切换
|
||||
if (newPlatform === 'claude-console') {
|
||||
form.value.addType = 'manual' // Claude Console 只支持手动模式
|
||||
if (newPlatform === 'claude-console' || newPlatform === 'bedrock') {
|
||||
form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式
|
||||
} else if (oldPlatform === 'claude-console' && (newPlatform === 'claude' || newPlatform === 'gemini')) {
|
||||
// 从 Claude Console 切换到其他平台时,恢复为 OAuth
|
||||
form.value.addType = 'oauth'
|
||||
@@ -1392,7 +1723,14 @@ watch(() => props.account, (newAccount) => {
|
||||
return '';
|
||||
})(),
|
||||
userAgent: newAccount.userAgent || '',
|
||||
rateLimitDuration: newAccount.rateLimitDuration || 60
|
||||
rateLimitDuration: newAccount.rateLimitDuration || 60,
|
||||
// Bedrock 特定字段
|
||||
accessKeyId: '', // 编辑模式不显示现有的访问密钥
|
||||
secretAccessKey: '', // 编辑模式不显示现有的秘密密钥
|
||||
region: newAccount.region || '',
|
||||
sessionToken: '', // 编辑模式不显示现有的会话令牌
|
||||
defaultModel: newAccount.defaultModel || '',
|
||||
smallFastModel: newAccount.smallFastModel || ''
|
||||
}
|
||||
|
||||
// 如果是分组类型,加载分组ID
|
||||
|
||||
@@ -6,6 +6,7 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
// 状态
|
||||
const claudeAccounts = ref([])
|
||||
const claudeConsoleAccounts = ref([])
|
||||
const bedrockAccounts = ref([])
|
||||
const geminiAccounts = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
@@ -52,6 +53,25 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Bedrock账户列表
|
||||
const fetchBedrockAccounts = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await apiClient.get('/admin/bedrock-accounts')
|
||||
if (response.success) {
|
||||
bedrockAccounts.value = response.data || []
|
||||
} else {
|
||||
throw new Error(response.message || '获取Bedrock账户失败')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Gemini账户列表
|
||||
const fetchGeminiAccounts = async () => {
|
||||
loading.value = true
|
||||
@@ -79,6 +99,7 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
await Promise.all([
|
||||
fetchClaudeAccounts(),
|
||||
fetchClaudeConsoleAccounts(),
|
||||
fetchBedrockAccounts(),
|
||||
fetchGeminiAccounts()
|
||||
])
|
||||
} catch (err) {
|
||||
@@ -129,6 +150,26 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Bedrock账户
|
||||
const createBedrockAccount = async (data) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await apiClient.post('/admin/bedrock-accounts', data)
|
||||
if (response.success) {
|
||||
await fetchBedrockAccounts()
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.message || '创建Bedrock账户失败')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Gemini账户
|
||||
const createGeminiAccount = async (data) => {
|
||||
loading.value = true
|
||||
@@ -189,6 +230,26 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Bedrock账户
|
||||
const updateBedrockAccount = async (id, data) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await apiClient.put(`/admin/bedrock-accounts/${id}`, data)
|
||||
if (response.success) {
|
||||
await fetchBedrockAccounts()
|
||||
return response
|
||||
} else {
|
||||
throw new Error(response.message || '更新Bedrock账户失败')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Gemini账户
|
||||
const updateGeminiAccount = async (id, data) => {
|
||||
loading.value = true
|
||||
@@ -219,6 +280,8 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
endpoint = `/admin/claude-accounts/${id}/toggle`
|
||||
} else if (platform === 'claude-console') {
|
||||
endpoint = `/admin/claude-console-accounts/${id}/toggle`
|
||||
} else if (platform === 'bedrock') {
|
||||
endpoint = `/admin/bedrock-accounts/${id}/toggle`
|
||||
} else {
|
||||
endpoint = `/admin/gemini-accounts/${id}/toggle`
|
||||
}
|
||||
@@ -229,6 +292,8 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
await fetchClaudeAccounts()
|
||||
} else if (platform === 'claude-console') {
|
||||
await fetchClaudeConsoleAccounts()
|
||||
} else if (platform === 'bedrock') {
|
||||
await fetchBedrockAccounts()
|
||||
} else {
|
||||
await fetchGeminiAccounts()
|
||||
}
|
||||
@@ -254,6 +319,8 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
endpoint = `/admin/claude-accounts/${id}`
|
||||
} else if (platform === 'claude-console') {
|
||||
endpoint = `/admin/claude-console-accounts/${id}`
|
||||
} else if (platform === 'bedrock') {
|
||||
endpoint = `/admin/bedrock-accounts/${id}`
|
||||
} else {
|
||||
endpoint = `/admin/gemini-accounts/${id}`
|
||||
}
|
||||
@@ -264,6 +331,8 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
await fetchClaudeAccounts()
|
||||
} else if (platform === 'claude-console') {
|
||||
await fetchClaudeConsoleAccounts()
|
||||
} else if (platform === 'bedrock') {
|
||||
await fetchBedrockAccounts()
|
||||
} else {
|
||||
await fetchGeminiAccounts()
|
||||
}
|
||||
@@ -374,6 +443,7 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
const reset = () => {
|
||||
claudeAccounts.value = []
|
||||
claudeConsoleAccounts.value = []
|
||||
bedrockAccounts.value = []
|
||||
geminiAccounts.value = []
|
||||
loading.value = false
|
||||
error.value = null
|
||||
@@ -385,6 +455,7 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
// State
|
||||
claudeAccounts,
|
||||
claudeConsoleAccounts,
|
||||
bedrockAccounts,
|
||||
geminiAccounts,
|
||||
loading,
|
||||
error,
|
||||
@@ -394,13 +465,16 @@ export const useAccountsStore = defineStore('accounts', () => {
|
||||
// Actions
|
||||
fetchClaudeAccounts,
|
||||
fetchClaudeConsoleAccounts,
|
||||
fetchBedrockAccounts,
|
||||
fetchGeminiAccounts,
|
||||
fetchAllAccounts,
|
||||
createClaudeAccount,
|
||||
createClaudeConsoleAccount,
|
||||
createBedrockAccount,
|
||||
createGeminiAccount,
|
||||
updateClaudeAccount,
|
||||
updateClaudeConsoleAccount,
|
||||
updateBedrockAccount,
|
||||
updateGeminiAccount,
|
||||
toggleAccount,
|
||||
deleteAccount,
|
||||
|
||||
@@ -245,6 +245,15 @@
|
||||
<span class="w-px h-4 bg-purple-300 mx-1" />
|
||||
<span class="text-xs font-medium text-purple-700">API Key</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="account.platform === 'bedrock'"
|
||||
class="flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-orange-100 to-red-100 rounded-lg border border-orange-200"
|
||||
>
|
||||
<i class="fab fa-aws text-orange-700 text-xs" />
|
||||
<span class="text-xs font-semibold text-orange-800">Bedrock</span>
|
||||
<span class="w-px h-4 bg-orange-300 mx-1" />
|
||||
<span class="text-xs font-medium text-orange-700">AWS</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-indigo-100 to-blue-100 rounded-lg border border-indigo-200"
|
||||
@@ -303,7 +312,7 @@
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap">
|
||||
<div
|
||||
v-if="account.platform === 'claude' || account.platform === 'claude-console'"
|
||||
v-if="account.platform === 'claude' || account.platform === 'claude-console' || account.platform === 'bedrock'"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<div class="w-16 bg-gray-200 rounded-full h-2">
|
||||
@@ -491,13 +500,16 @@
|
||||
'w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0',
|
||||
account.platform === 'claude'
|
||||
? 'bg-gradient-to-br from-purple-500 to-purple-600'
|
||||
: 'bg-gradient-to-br from-blue-500 to-blue-600'
|
||||
: account.platform === 'bedrock'
|
||||
? 'bg-gradient-to-br from-orange-500 to-red-600'
|
||||
: 'bg-gradient-to-br from-blue-500 to-blue-600'
|
||||
]"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-white text-sm',
|
||||
account.platform === 'claude' ? 'fas fa-brain' : 'fas fa-robot'
|
||||
account.platform === 'claude' ? 'fas fa-brain' :
|
||||
account.platform === 'bedrock' ? 'fab fa-aws' : 'fas fa-robot'
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@@ -765,9 +777,10 @@ const sortedAccounts = computed(() => {
|
||||
const loadAccounts = async () => {
|
||||
accountsLoading.value = true
|
||||
try {
|
||||
const [claudeData, claudeConsoleData, geminiData, apiKeysData, groupsData] = await Promise.all([
|
||||
const [claudeData, claudeConsoleData, bedrockData, geminiData, apiKeysData, groupsData] = await Promise.all([
|
||||
apiClient.get('/admin/claude-accounts'),
|
||||
apiClient.get('/admin/claude-console-accounts'),
|
||||
apiClient.get('/admin/bedrock-accounts'),
|
||||
apiClient.get('/admin/gemini-accounts'),
|
||||
apiClient.get('/admin/api-keys'),
|
||||
apiClient.get('/admin/account-groups')
|
||||
@@ -825,6 +838,15 @@ const loadAccounts = async () => {
|
||||
allAccounts.push(...claudeConsoleAccounts)
|
||||
}
|
||||
|
||||
if (bedrockData.success) {
|
||||
const bedrockAccounts = (bedrockData.data || []).map(acc => {
|
||||
// Bedrock账户暂时不支持直接绑定
|
||||
const groupInfo = accountGroupMap.get(acc.id) || null
|
||||
return { ...acc, platform: 'bedrock', boundApiKeysCount: 0, groupInfo }
|
||||
})
|
||||
allAccounts.push(...bedrockAccounts)
|
||||
}
|
||||
|
||||
if (geminiData.success) {
|
||||
const geminiAccounts = (geminiData.data || []).map(acc => {
|
||||
// 计算每个Gemini账户绑定的API Key数量
|
||||
@@ -1000,6 +1022,8 @@ const deleteAccount = async (account) => {
|
||||
endpoint = `/admin/claude-accounts/${account.id}`
|
||||
} else if (account.platform === 'claude-console') {
|
||||
endpoint = `/admin/claude-console-accounts/${account.id}`
|
||||
} else if (account.platform === 'bedrock') {
|
||||
endpoint = `/admin/bedrock-accounts/${account.id}`
|
||||
} else {
|
||||
endpoint = `/admin/gemini-accounts/${account.id}`
|
||||
}
|
||||
@@ -1050,6 +1074,8 @@ const toggleSchedulable = async (account) => {
|
||||
endpoint = `/admin/claude-accounts/${account.id}/toggle-schedulable`
|
||||
} else if (account.platform === 'claude-console') {
|
||||
endpoint = `/admin/claude-console-accounts/${account.id}/toggle-schedulable`
|
||||
} else if (account.platform === 'bedrock') {
|
||||
endpoint = `/admin/bedrock-accounts/${account.id}/toggle-schedulable`
|
||||
} else if (account.platform === 'gemini') {
|
||||
endpoint = `/admin/gemini-accounts/${account.id}/toggle-schedulable`
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user