mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'Wei-Shaw:main' into main
This commit is contained in:
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Your GitHub username for GitHub Sponsors
|
||||||
|
patreon: # Replace with your Patreon username if you have one
|
||||||
|
open_collective: # Replace with your Open Collective username if you have one
|
||||||
|
ko_fi: # Replace with your Ko-fi username if you have one
|
||||||
|
tidelift: # Replace with your Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with your Community Bridge project-name
|
||||||
|
liberapay: # Replace with your Liberapay username
|
||||||
|
issuehunt: # Replace with your IssueHunt username
|
||||||
|
otechie: # Replace with your Otechie username
|
||||||
|
custom: ['https://afdian.com/a/claude-relay-service'] # Your custom donation link (Afdian)
|
||||||
29
README.md
29
README.md
@@ -396,13 +396,6 @@ export ANTHROPIC_BASE_URL="http://127.0.0.1:3000/api/" # 根据实际填写你
|
|||||||
export ANTHROPIC_AUTH_TOKEN="后台创建的API密钥"
|
export ANTHROPIC_AUTH_TOKEN="后台创建的API密钥"
|
||||||
```
|
```
|
||||||
|
|
||||||
如果后台添加了 Droid 类型账号池,请将基础地址改为:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export ANTHROPIC_BASE_URL="http://127.0.0.1:3000/droid/claude" # 根据实际情况替换域名/IP
|
|
||||||
export ANTHROPIC_AUTH_TOKEN="后台创建的API密钥"
|
|
||||||
```
|
|
||||||
|
|
||||||
**VSCode Claude 插件配置:**
|
**VSCode Claude 插件配置:**
|
||||||
|
|
||||||
如果使用 VSCode 的 Claude 插件,需要在 `~/.claude/config.json` 文件中配置:
|
如果使用 VSCode 的 Claude 插件,需要在 `~/.claude/config.json` 文件中配置:
|
||||||
@@ -454,8 +447,6 @@ requires_openai_auth = true
|
|||||||
env_key = "CRS_OAI_KEY"
|
env_key = "CRS_OAI_KEY"
|
||||||
```
|
```
|
||||||
|
|
||||||
如需通过 Droid 类型账号池访问 Codex CLI,只需将 `base_url` 改为 `http://127.0.0.1:3000/droid/openai`(其余配置保持不变)。
|
|
||||||
|
|
||||||
在 `~/.codex/auth.json` 文件中配置API密钥为 null:
|
在 `~/.codex/auth.json` 文件中配置API密钥为 null:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -555,23 +546,7 @@ gpt-5 # Codex使用固定模型ID
|
|||||||
- API地址填入:`http://你的服务器:3000/openai`
|
- API地址填入:`http://你的服务器:3000/openai`
|
||||||
- API Key填入:后台创建的API密钥(cr_开头)
|
- API Key填入:后台创建的API密钥(cr_开头)
|
||||||
- **重要**:Codex只支持Openai-Response标准
|
- **重要**:Codex只支持Openai-Response标准
|
||||||
- 💡 如果希望在 Cherry Studio 中使用 Droid 类型账号,请改填 `http://你的服务器:3000/droid/openai`,并保持其他设置不变。
|
|
||||||
|
|
||||||
**4. Droid账号接入:**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Claude Code / Droid CLI 使用的 API 地址
|
|
||||||
http://你的服务器:3000/droid/claude
|
|
||||||
|
|
||||||
# Codex CLI 使用的 API 地址
|
|
||||||
http://你的服务器:3000/droid/openai
|
|
||||||
```
|
|
||||||
|
|
||||||
配置步骤:
|
|
||||||
- 供应商类型选择"Anthropic"或"Openai-Response"(根据模型类型)
|
|
||||||
- API地址填入:`http://你的服务器:3000/droid/claude` 或 `http://你的服务器:3000/droid/openai`
|
|
||||||
- API Key填入:后台创建的API密钥(cr_开头)
|
|
||||||
- 建议自定义模型名称以区分 Droid 账号池
|
|
||||||
|
|
||||||
**Cherry Studio 地址格式重要说明:**
|
**Cherry Studio 地址格式重要说明:**
|
||||||
|
|
||||||
@@ -587,10 +562,10 @@ http://你的服务器:3000/droid/openai
|
|||||||
- 所有账号类型都使用相同的API密钥(在后台统一创建)
|
- 所有账号类型都使用相同的API密钥(在后台统一创建)
|
||||||
- 根据不同的路由前缀自动识别账号类型
|
- 根据不同的路由前缀自动识别账号类型
|
||||||
- `/claude/` - 使用Claude账号池
|
- `/claude/` - 使用Claude账号池
|
||||||
- `/droid/claude/` - 使用Droid类型Claude账号池(服务于 Claude Code / Droid CLI)
|
- `/droid/claude/` - 使用Droid类型Claude账号池(只建议api调用或Droid Cli中使用)
|
||||||
- `/gemini/` - 使用Gemini账号池
|
- `/gemini/` - 使用Gemini账号池
|
||||||
- `/openai/` - 使用Codex账号(只支持Openai-Response格式)
|
- `/openai/` - 使用Codex账号(只支持Openai-Response格式)
|
||||||
- `/droid/openai/` - 使用Droid类型OpenAI兼容账号池(服务于 Codex CLI)
|
- `/droid/openai/` - 使用Droid类型OpenAI兼容账号池(只建议api调用或Droid Cli中使用)
|
||||||
- 支持所有标准API端点(messages、models等)
|
- 支持所有标准API端点(messages、models等)
|
||||||
|
|
||||||
**重要说明:**
|
**重要说明:**
|
||||||
|
|||||||
@@ -7,6 +7,57 @@ const redis = require('../models/redis')
|
|||||||
// const { RateLimiterRedis } = require('rate-limiter-flexible') // 暂时未使用
|
// const { RateLimiterRedis } = require('rate-limiter-flexible') // 暂时未使用
|
||||||
const ClientValidator = require('../validators/clientValidator')
|
const ClientValidator = require('../validators/clientValidator')
|
||||||
|
|
||||||
|
const FALLBACK_CONCURRENCY_CONFIG = {
|
||||||
|
leaseSeconds: 300,
|
||||||
|
renewIntervalSeconds: 30,
|
||||||
|
cleanupGraceSeconds: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveConcurrencyConfig = () => {
|
||||||
|
if (typeof redis._getConcurrencyConfig === 'function') {
|
||||||
|
return redis._getConcurrencyConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = {
|
||||||
|
...FALLBACK_CONCURRENCY_CONFIG,
|
||||||
|
...(config.concurrency || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNumber = (value, fallback) => {
|
||||||
|
const parsed = Number(value)
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaseSeconds = Math.max(
|
||||||
|
toNumber(raw.leaseSeconds, FALLBACK_CONCURRENCY_CONFIG.leaseSeconds),
|
||||||
|
30
|
||||||
|
)
|
||||||
|
|
||||||
|
let renewIntervalSeconds
|
||||||
|
if (raw.renewIntervalSeconds === 0 || raw.renewIntervalSeconds === '0') {
|
||||||
|
renewIntervalSeconds = 0
|
||||||
|
} else {
|
||||||
|
renewIntervalSeconds = Math.max(
|
||||||
|
toNumber(raw.renewIntervalSeconds, FALLBACK_CONCURRENCY_CONFIG.renewIntervalSeconds),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupGraceSeconds = Math.max(
|
||||||
|
toNumber(raw.cleanupGraceSeconds, FALLBACK_CONCURRENCY_CONFIG.cleanupGraceSeconds),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
leaseSeconds,
|
||||||
|
renewIntervalSeconds,
|
||||||
|
cleanupGraceSeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const TOKEN_COUNT_PATHS = new Set([
|
const TOKEN_COUNT_PATHS = new Set([
|
||||||
'/v1/messages/count_tokens',
|
'/v1/messages/count_tokens',
|
||||||
'/api/v1/messages/count_tokens',
|
'/api/v1/messages/count_tokens',
|
||||||
@@ -116,13 +167,10 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
// 检查并发限制
|
// 检查并发限制
|
||||||
const concurrencyLimit = validation.keyData.concurrencyLimit || 0
|
const concurrencyLimit = validation.keyData.concurrencyLimit || 0
|
||||||
if (!skipKeyRestrictions && concurrencyLimit > 0) {
|
if (!skipKeyRestrictions && concurrencyLimit > 0) {
|
||||||
const concurrencyConfig = config.concurrency || {}
|
const { leaseSeconds: configLeaseSeconds, renewIntervalSeconds: configRenewIntervalSeconds } =
|
||||||
const leaseSeconds = Math.max(concurrencyConfig.leaseSeconds || 900, 30)
|
resolveConcurrencyConfig()
|
||||||
const rawRenewInterval =
|
const leaseSeconds = Math.max(Number(configLeaseSeconds) || 300, 30)
|
||||||
typeof concurrencyConfig.renewIntervalSeconds === 'number'
|
let renewIntervalSeconds = configRenewIntervalSeconds
|
||||||
? concurrencyConfig.renewIntervalSeconds
|
|
||||||
: 60
|
|
||||||
let renewIntervalSeconds = rawRenewInterval
|
|
||||||
if (renewIntervalSeconds > 0) {
|
if (renewIntervalSeconds > 0) {
|
||||||
const maxSafeRenew = Math.max(leaseSeconds - 5, 15)
|
const maxSafeRenew = Math.max(leaseSeconds - 5, 15)
|
||||||
renewIntervalSeconds = Math.min(Math.max(renewIntervalSeconds, 15), maxSafeRenew)
|
renewIntervalSeconds = Math.min(Math.max(renewIntervalSeconds, 15), maxSafeRenew)
|
||||||
@@ -215,6 +263,29 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
decrementConcurrency()
|
decrementConcurrency()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
req.once('aborted', () => {
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ Request aborted for key: ${validation.keyData.id} (${validation.keyData.name})`
|
||||||
|
)
|
||||||
|
decrementConcurrency()
|
||||||
|
})
|
||||||
|
|
||||||
|
req.once('error', (error) => {
|
||||||
|
logger.error(
|
||||||
|
`❌ Request error for key ${validation.keyData.id} (${validation.keyData.name}):`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
decrementConcurrency()
|
||||||
|
})
|
||||||
|
|
||||||
|
res.once('error', (error) => {
|
||||||
|
logger.error(
|
||||||
|
`❌ Response error for key ${validation.keyData.id} (${validation.keyData.name}):`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
decrementConcurrency()
|
||||||
|
})
|
||||||
|
|
||||||
// res.on('finish') 处理正常完成的情况
|
// res.on('finish') 处理正常完成的情况
|
||||||
res.once('finish', () => {
|
res.once('finish', () => {
|
||||||
logger.api(
|
logger.api(
|
||||||
|
|||||||
@@ -1575,13 +1575,53 @@ class RedisClient {
|
|||||||
// 获取并发配置
|
// 获取并发配置
|
||||||
_getConcurrencyConfig() {
|
_getConcurrencyConfig() {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
leaseSeconds: 900,
|
leaseSeconds: 300,
|
||||||
|
renewIntervalSeconds: 30,
|
||||||
cleanupGraceSeconds: 30
|
cleanupGraceSeconds: 30
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
|
const configValues = {
|
||||||
...defaults,
|
...defaults,
|
||||||
...(config.concurrency || {})
|
...(config.concurrency || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeNumber = (value, fallback, options = {}) => {
|
||||||
|
const parsed = Number(value)
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.allowZero && parsed === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.min !== undefined && parsed < options.min) {
|
||||||
|
return options.min
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
leaseSeconds: normalizeNumber(configValues.leaseSeconds, defaults.leaseSeconds, {
|
||||||
|
min: 30
|
||||||
|
}),
|
||||||
|
renewIntervalSeconds: normalizeNumber(
|
||||||
|
configValues.renewIntervalSeconds,
|
||||||
|
defaults.renewIntervalSeconds,
|
||||||
|
{
|
||||||
|
allowZero: true,
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cleanupGraceSeconds: normalizeNumber(
|
||||||
|
configValues.cleanupGraceSeconds,
|
||||||
|
defaults.cleanupGraceSeconds,
|
||||||
|
{
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加并发计数(基于租约的有序集合)
|
// 增加并发计数(基于租约的有序集合)
|
||||||
@@ -1650,10 +1690,10 @@ class RedisClient {
|
|||||||
local now = tonumber(ARGV[3])
|
local now = tonumber(ARGV[3])
|
||||||
local ttl = tonumber(ARGV[4])
|
local ttl = tonumber(ARGV[4])
|
||||||
|
|
||||||
local exists = redis.call('ZSCORE', key, member)
|
|
||||||
|
|
||||||
redis.call('ZREMRANGEBYSCORE', key, '-inf', now)
|
redis.call('ZREMRANGEBYSCORE', key, '-inf', now)
|
||||||
|
|
||||||
|
local exists = redis.call('ZSCORE', key, member)
|
||||||
|
|
||||||
if exists then
|
if exists then
|
||||||
redis.call('ZADD', key, expireAt, member)
|
redis.call('ZADD', key, expireAt, member)
|
||||||
if ttl > 0 then
|
if ttl > 0 then
|
||||||
|
|||||||
@@ -32,6 +32,36 @@ const ProxyHelper = require('../utils/proxyHelper')
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
function normalizeNullableDate(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return trimmed === '' ? null : trimmed
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSubscriptionExpiry(account) {
|
||||||
|
if (!account || typeof account !== 'object') {
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawSubscription = account.subscriptionExpiresAt
|
||||||
|
const rawToken = account.tokenExpiresAt !== undefined ? account.tokenExpiresAt : account.expiresAt
|
||||||
|
|
||||||
|
const subscriptionExpiresAt = normalizeNullableDate(rawSubscription)
|
||||||
|
const tokenExpiresAt = normalizeNullableDate(rawToken)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
subscriptionExpiresAt,
|
||||||
|
tokenExpiresAt,
|
||||||
|
expiresAt: subscriptionExpiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 👥 用户管理
|
// 👥 用户管理
|
||||||
|
|
||||||
// 获取所有用户列表(用于API Key分配)
|
// 获取所有用户列表(用于API Key分配)
|
||||||
@@ -2082,6 +2112,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
|
|
||||||
// 获取会话窗口使用统计(仅对有活跃窗口的账户)
|
// 获取会话窗口使用统计(仅对有活跃窗口的账户)
|
||||||
let sessionWindowUsage = null
|
let sessionWindowUsage = null
|
||||||
@@ -2124,7 +2155,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 转换schedulable为布尔值
|
// 转换schedulable为布尔值
|
||||||
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -2140,8 +2171,9 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
// 如果获取统计失败,返回空统计
|
// 如果获取统计失败,返回空统计
|
||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -2155,8 +2187,9 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
`⚠️ Failed to get group info for account ${account.id}:`,
|
`⚠️ Failed to get group info for account ${account.id}:`,
|
||||||
groupError.message
|
groupError.message
|
||||||
)
|
)
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -2170,7 +2203,8 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
return res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get Claude accounts:', error)
|
logger.error('❌ Failed to get Claude accounts:', error)
|
||||||
return res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message })
|
return res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message })
|
||||||
@@ -2267,7 +2301,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
useUnifiedUserAgent,
|
useUnifiedUserAgent,
|
||||||
useUnifiedClientId,
|
useUnifiedClientId,
|
||||||
unifiedClientId,
|
unifiedClientId,
|
||||||
expiresAt
|
expiresAt,
|
||||||
|
subscriptionExpiresAt
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -2311,7 +2346,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
|
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
|
||||||
useUnifiedClientId: useUnifiedClientId === true, // 默认为false
|
useUnifiedClientId: useUnifiedClientId === true, // 默认为false
|
||||||
unifiedClientId: unifiedClientId || '', // 统一的客户端标识
|
unifiedClientId: unifiedClientId || '', // 统一的客户端标识
|
||||||
expiresAt: expiresAt || null // 账户订阅到期时间
|
expiresAt: subscriptionExpiresAt ?? expiresAt ?? null // 账户订阅到期时间
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果是分组类型,将账户添加到分组
|
// 如果是分组类型,将账户添加到分组
|
||||||
@@ -2326,7 +2361,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`)
|
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`)
|
||||||
return res.json({ success: true, data: newAccount })
|
const responseAccount = formatSubscriptionExpiry(newAccount)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to create Claude account:', error)
|
logger.error('❌ Failed to create Claude account:', error)
|
||||||
return res
|
return res
|
||||||
@@ -2400,8 +2436,12 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2605,14 +2645,16 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 为每个账户添加使用统计信息
|
// 为每个账户添加使用统计信息
|
||||||
|
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 转换schedulable为布尔值
|
// 转换schedulable为布尔值
|
||||||
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -2630,7 +2672,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 转换schedulable为布尔值
|
// 转换schedulable为布尔值
|
||||||
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -2646,7 +2688,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
groupError.message
|
groupError.message
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -2659,7 +2701,8 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
return res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get Claude Console accounts:', error)
|
logger.error('❌ Failed to get Claude Console accounts:', error)
|
||||||
return res
|
return res
|
||||||
@@ -2730,7 +2773,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🎮 Admin created Claude Console account: ${name}`)
|
logger.success(`🎮 Admin created Claude Console account: ${name}`)
|
||||||
return res.json({ success: true, data: newAccount })
|
const responseAccount = formatSubscriptionExpiry(newAccount)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to create Claude Console account:', error)
|
logger.error('❌ Failed to create Claude Console account:', error)
|
||||||
return res
|
return res
|
||||||
@@ -2797,8 +2841,12 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3028,12 +3076,13 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
// 为每个账户添加使用统计信息
|
// 为每个账户添加使用统计信息
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 转换schedulable为布尔值
|
// 转换schedulable为布尔值
|
||||||
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -3051,7 +3100,7 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 转换schedulable为布尔值
|
// 转换schedulable为布尔值
|
||||||
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
schedulable: account.schedulable === 'true' || account.schedulable === true,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -3067,7 +3116,7 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
groupError.message
|
groupError.message
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -3080,7 +3129,8 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
return res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get CCR accounts:', error)
|
logger.error('❌ Failed to get CCR accounts:', error)
|
||||||
return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
|
return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
|
||||||
@@ -3149,7 +3199,8 @@ router.post('/ccr-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🔧 Admin created CCR account: ${name}`)
|
logger.success(`🔧 Admin created CCR account: ${name}`)
|
||||||
return res.json({ success: true, data: newAccount })
|
const responseAccount = formatSubscriptionExpiry(newAccount)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to create CCR account:', error)
|
logger.error('❌ Failed to create CCR account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
|
return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
|
||||||
@@ -3214,8 +3265,12 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3433,12 +3488,13 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
// 为每个账户添加使用统计信息
|
// 为每个账户添加使用统计信息
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
@@ -3454,7 +3510,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -3468,7 +3524,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
groupError.message
|
groupError.message
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -3481,7 +3537,8 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
return res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get Bedrock accounts:', error)
|
logger.error('❌ Failed to get Bedrock accounts:', error)
|
||||||
return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message })
|
return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message })
|
||||||
@@ -3543,7 +3600,8 @@ router.post('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`☁️ Admin created Bedrock account: ${name}`)
|
logger.success(`☁️ Admin created Bedrock account: ${name}`)
|
||||||
return res.json({ success: true, data: result.data })
|
const responseAccount = formatSubscriptionExpiry(result.data)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to create Bedrock account:', error)
|
logger.error('❌ Failed to create Bedrock account:', error)
|
||||||
return res
|
return res
|
||||||
@@ -3582,8 +3640,12 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) =
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3906,14 +3968,13 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
// 为每个账户添加使用统计信息(与Claude账户相同的逻辑)
|
// 为每个账户添加使用统计信息(与Claude账户相同的逻辑)
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt
|
|
||||||
expiresAt: account.subscriptionExpiresAt || null,
|
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
@@ -3930,9 +3991,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt
|
|
||||||
expiresAt: account.subscriptionExpiresAt || null,
|
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -3946,7 +4005,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
groupError.message
|
groupError.message
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
@@ -3959,7 +4018,8 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
return res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get Gemini accounts:', error)
|
logger.error('❌ Failed to get Gemini accounts:', error)
|
||||||
return res.status(500).json({ error: 'Failed to get accounts', message: error.message })
|
return res.status(500).json({ error: 'Failed to get accounts', message: error.message })
|
||||||
@@ -3999,7 +4059,8 @@ router.post('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`)
|
logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`)
|
||||||
return res.json({ success: true, data: newAccount })
|
const responseAccount = formatSubscriptionExpiry(newAccount)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to create Gemini account:', error)
|
logger.error('❌ Failed to create Gemini account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to create account', message: error.message })
|
return res.status(500).json({ error: 'Failed to create account', message: error.message })
|
||||||
@@ -4059,15 +4120,20 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAccount = await geminiAccountService.updateAccount(accountId, mappedUpdates)
|
const updatedAccount = await geminiAccountService.updateAccount(accountId, mappedUpdates)
|
||||||
|
|
||||||
logger.success(`📝 Admin updated Gemini account: ${accountId}`)
|
logger.success(`📝 Admin updated Gemini account: ${accountId}`)
|
||||||
return res.json({ success: true, data: updatedAccount })
|
const responseAccount = formatSubscriptionExpiry(updatedAccount)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to update Gemini account:', error)
|
logger.error('❌ Failed to update Gemini account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to update account', message: error.message })
|
return res.status(500).json({ error: 'Failed to update account', message: error.message })
|
||||||
@@ -7215,8 +7281,9 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||||
const groupInfos = await fetchAccountGroups(account.id)
|
const groupInfos = await fetchAccountGroups(account.id)
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
@@ -7227,8 +7294,9 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error)
|
logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error)
|
||||||
const groupInfos = await fetchAccountGroups(account.id)
|
const groupInfos = await fetchAccountGroups(account.id)
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
@@ -7242,9 +7310,11 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
logger.info(`获取 OpenAI 账户列表: ${accountsWithStats.length} 个账户`)
|
logger.info(`获取 OpenAI 账户列表: ${accountsWithStats.length} 个账户`)
|
||||||
|
|
||||||
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: accountsWithStats
|
data: formattedAccounts
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('获取 OpenAI 账户列表失败:', error)
|
logger.error('获取 OpenAI 账户列表失败:', error)
|
||||||
@@ -7270,7 +7340,8 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
rateLimitDuration,
|
rateLimitDuration,
|
||||||
priority,
|
priority,
|
||||||
needsImmediateRefresh, // 是否需要立即刷新
|
needsImmediateRefresh, // 是否需要立即刷新
|
||||||
requireRefreshSuccess // 是否必须刷新成功才能创建
|
requireRefreshSuccess, // 是否必须刷新成功才能创建
|
||||||
|
subscriptionExpiresAt
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -7292,7 +7363,8 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
accountInfo: accountInfo || {},
|
accountInfo: accountInfo || {},
|
||||||
proxy: proxy || null,
|
proxy: proxy || null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
schedulable: true
|
schedulable: true,
|
||||||
|
subscriptionExpiresAt: subscriptionExpiresAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果需要立即刷新且必须成功(OpenAI 手动模式)
|
// 如果需要立即刷新且必须成功(OpenAI 手动模式)
|
||||||
@@ -7328,9 +7400,11 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
logger.success(`✅ 创建并验证 OpenAI 账户成功: ${name} (ID: ${tempAccount.id})`)
|
logger.success(`✅ 创建并验证 OpenAI 账户成功: ${name} (ID: ${tempAccount.id})`)
|
||||||
|
|
||||||
|
const responseAccount = formatSubscriptionExpiry(refreshedAccount)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: refreshedAccount,
|
data: responseAccount,
|
||||||
message: '账户创建成功,并已获取完整 token 信息'
|
message: '账户创建成功,并已获取完整 token 信息'
|
||||||
})
|
})
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
@@ -7392,9 +7466,11 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${createdAccount.id})`)
|
logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${createdAccount.id})`)
|
||||||
|
|
||||||
|
const responseAccount = formatSubscriptionExpiry(createdAccount)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: createdAccount
|
data: responseAccount
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('创建 OpenAI 账户失败:', error)
|
logger.error('创建 OpenAI 账户失败:', error)
|
||||||
@@ -7571,10 +7647,19 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
|
|||||||
: currentAccount.emailVerified
|
: currentAccount.emailVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt (订阅过期时间)
|
const hasOauthExpiry = Boolean(updates.openaiOauth?.expires_in)
|
||||||
// 注意:这里不影响上面 OAuth token 的 expiresAt 字段
|
|
||||||
if ('expiresAt' in updates && !updates.openaiOauth?.expires_in) {
|
// 处理订阅过期时间字段:优先使用 subscriptionExpiresAt,兼容旧版的 expiresAt
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updateData.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt') && !hasOauthExpiry) {
|
||||||
updateData.subscriptionExpiresAt = updates.expiresAt
|
updateData.subscriptionExpiresAt = updates.expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasOauthExpiry &&
|
||||||
|
Object.prototype.hasOwnProperty.call(updateData, 'subscriptionExpiresAt')
|
||||||
|
) {
|
||||||
delete updateData.expiresAt
|
delete updateData.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7592,7 +7677,8 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`📝 Admin updated OpenAI account: ${id}`)
|
logger.success(`📝 Admin updated OpenAI account: ${id}`)
|
||||||
return res.json({ success: true, data: updatedAccount })
|
const responseAccount = formatSubscriptionExpiry(updatedAccount)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to update OpenAI account:', error)
|
logger.error('❌ Failed to update OpenAI account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to update account', message: error.message })
|
return res.status(500).json({ error: 'Failed to update account', message: error.message })
|
||||||
@@ -7673,9 +7759,11 @@ router.put('/openai-accounts/:id/toggle', authenticateAdmin, async (req, res) =>
|
|||||||
`✅ ${account.enabled ? '启用' : '禁用'} OpenAI 账户: ${account.name} (ID: ${id})`
|
`✅ ${account.enabled ? '启用' : '禁用'} OpenAI 账户: ${account.name} (ID: ${id})`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const responseAccount = formatSubscriptionExpiry(account)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: account
|
data: responseAccount
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('切换 OpenAI 账户状态失败:', error)
|
logger.error('切换 OpenAI 账户状态失败:', error)
|
||||||
@@ -7781,11 +7869,12 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
// 为每个账户添加使用统计信息和分组信息
|
// 为每个账户添加使用统计信息和分组信息
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
@@ -7798,7 +7887,7 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
usage: {
|
usage: {
|
||||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
@@ -7809,7 +7898,7 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
} catch (groupError) {
|
} catch (groupError) {
|
||||||
logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
|
logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
@@ -7822,9 +7911,11 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: accountsWithStats
|
data: formattedAccounts
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch Azure OpenAI accounts:', error)
|
logger.error('Failed to fetch Azure OpenAI accounts:', error)
|
||||||
@@ -7943,9 +8034,11 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseAccount = formatSubscriptionExpiry(account)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: account,
|
data: responseAccount,
|
||||||
message: 'Azure OpenAI account created successfully'
|
message: 'Azure OpenAI account created successfully'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -7966,16 +8059,21 @@ router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) =>
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await azureOpenaiAccountService.updateAccount(id, mappedUpdates)
|
const account = await azureOpenaiAccountService.updateAccount(id, mappedUpdates)
|
||||||
|
const responseAccount = formatSubscriptionExpiry(account)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: account,
|
data: responseAccount,
|
||||||
message: 'Azure OpenAI account updated successfully'
|
message: 'Azure OpenAI account updated successfully'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -8228,6 +8326,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
|||||||
// 处理额度信息、使用统计和绑定的 API Key 数量
|
// 处理额度信息、使用统计和绑定的 API Key 数量
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
// 检查是否需要重置额度
|
// 检查是否需要重置额度
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
@@ -8282,7 +8381,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
boundApiKeysCount: boundCount,
|
boundApiKeysCount: boundCount,
|
||||||
usage: {
|
usage: {
|
||||||
daily: usageStats.daily,
|
daily: usageStats.daily,
|
||||||
@@ -8293,7 +8392,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to process OpenAI-Responses account ${account.id}:`, error)
|
logger.error(`Failed to process OpenAI-Responses account ${account.id}:`, error)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
boundApiKeysCount: 0,
|
boundApiKeysCount: 0,
|
||||||
usage: {
|
usage: {
|
||||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||||
@@ -8305,7 +8404,9 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
|
||||||
|
res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get OpenAI-Responses accounts:', error)
|
logger.error('Failed to get OpenAI-Responses accounts:', error)
|
||||||
res.status(500).json({ success: false, message: error.message })
|
res.status(500).json({ success: false, message: error.message })
|
||||||
@@ -8316,7 +8417,8 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
|
|||||||
router.post('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
|
router.post('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const account = await openaiResponsesAccountService.createAccount(req.body)
|
const account = await openaiResponsesAccountService.createAccount(req.body)
|
||||||
res.json({ success: true, account })
|
const responseAccount = formatSubscriptionExpiry(account)
|
||||||
|
res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to create OpenAI-Responses account:', error)
|
logger.error('Failed to create OpenAI-Responses account:', error)
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -8346,8 +8448,12 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res)
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8357,7 +8463,13 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res)
|
|||||||
return res.status(400).json(result)
|
return res.status(400).json(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, ...result })
|
const updatedAccountData = await openaiResponsesAccountService.getAccount(id)
|
||||||
|
if (updatedAccountData) {
|
||||||
|
updatedAccountData.apiKey = '***'
|
||||||
|
}
|
||||||
|
const responseAccount = formatSubscriptionExpiry(updatedAccountData)
|
||||||
|
|
||||||
|
res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update OpenAI-Responses account:', error)
|
logger.error('Failed to update OpenAI-Responses account:', error)
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -8687,6 +8799,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
// 添加使用统计
|
// 添加使用统计
|
||||||
const accountsWithStats = await Promise.all(
|
const accountsWithStats = await Promise.all(
|
||||||
accounts.map(async (account) => {
|
accounts.map(async (account) => {
|
||||||
|
const formattedAccount = formatSubscriptionExpiry(account)
|
||||||
try {
|
try {
|
||||||
const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
|
const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
|
||||||
let groupInfos = []
|
let groupInfos = []
|
||||||
@@ -8716,10 +8829,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt
|
|
||||||
// OAuth token 的原始 expiresAt 保留在内部使用
|
|
||||||
expiresAt: account.subscriptionExpiresAt || null,
|
|
||||||
schedulable: account.schedulable === 'true',
|
schedulable: account.schedulable === 'true',
|
||||||
boundApiKeysCount,
|
boundApiKeysCount,
|
||||||
groupInfos,
|
groupInfos,
|
||||||
@@ -8732,9 +8842,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
|
logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
|
||||||
return {
|
return {
|
||||||
...account,
|
...formattedAccount,
|
||||||
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt
|
|
||||||
expiresAt: account.subscriptionExpiresAt || null,
|
|
||||||
boundApiKeysCount: 0,
|
boundApiKeysCount: 0,
|
||||||
groupInfos: [],
|
groupInfos: [],
|
||||||
usage: {
|
usage: {
|
||||||
@@ -8747,7 +8855,9 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.json({ success: true, data: accountsWithStats })
|
const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
|
||||||
|
|
||||||
|
return res.json({ success: true, data: formattedAccounts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get Droid accounts:', error)
|
logger.error('Failed to get Droid accounts:', error)
|
||||||
return res.status(500).json({ error: 'Failed to get Droid accounts', message: error.message })
|
return res.status(500).json({ error: 'Failed to get Droid accounts', message: error.message })
|
||||||
@@ -8804,7 +8914,8 @@ router.post('/droid-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`Created Droid account: ${account.name} (${account.id})`)
|
logger.success(`Created Droid account: ${account.name} (${account.id})`)
|
||||||
return res.json({ success: true, data: account })
|
const responseAccount = formatSubscriptionExpiry(account)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to create Droid account:', error)
|
logger.error('Failed to create Droid account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to create Droid account', message: error.message })
|
return res.status(500).json({ error: 'Failed to create Droid account', message: error.message })
|
||||||
@@ -8851,8 +8962,12 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
// 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
|
||||||
const mappedUpdates = { ...updates }
|
const mappedUpdates = { ...updates }
|
||||||
if ('expiresAt' in mappedUpdates) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
|
||||||
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
|
||||||
delete mappedUpdates.expiresAt
|
delete mappedUpdates.expiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8888,7 +9003,8 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({ success: true, data: account })
|
const responseAccount = formatSubscriptionExpiry(account)
|
||||||
|
return res.json({ success: true, data: responseAccount })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update Droid account ${req.params.id}:`, error)
|
logger.error(`Failed to update Droid account ${req.params.id}:`, error)
|
||||||
return res.status(500).json({ error: 'Failed to update Droid account', message: error.message })
|
return res.status(500).json({ error: 'Failed to update Droid account', message: error.message })
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ const AZURE_OPENAI_ACCOUNT_KEY_PREFIX = 'azure_openai:account:'
|
|||||||
const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts'
|
const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts'
|
||||||
const ACCOUNT_SESSION_MAPPING_PREFIX = 'azure_openai_session_account_mapping:'
|
const ACCOUNT_SESSION_MAPPING_PREFIX = 'azure_openai_session_account_mapping:'
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
// 加密函数
|
// 加密函数
|
||||||
function encrypt(text) {
|
function encrypt(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -133,6 +146,7 @@ async function createAccount(accountData) {
|
|||||||
isActive: accountData.isActive !== false ? 'true' : 'false',
|
isActive: accountData.isActive !== false ? 'true' : 'false',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
schedulable: accountData.schedulable !== false ? 'true' : 'false',
|
schedulable: accountData.schedulable !== false ? 'true' : 'false',
|
||||||
|
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(accountData.subscriptionExpiresAt || ''),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now
|
||||||
}
|
}
|
||||||
@@ -152,7 +166,10 @@ async function createAccount(accountData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Created Azure OpenAI account: ${accountId}`)
|
logger.info(`Created Azure OpenAI account: ${accountId}`)
|
||||||
return account
|
return {
|
||||||
|
...account,
|
||||||
|
subscriptionExpiresAt: account.subscriptionExpiresAt || null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取账户
|
// 获取账户
|
||||||
@@ -187,6 +204,11 @@ async function getAccount(accountId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountData.subscriptionExpiresAt =
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
return accountData
|
return accountData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +240,13 @@ async function updateAccount(accountId, updates) {
|
|||||||
: JSON.stringify(updates.supportedModels)
|
: JSON.stringify(updates.supportedModels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
|
||||||
|
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
|
||||||
|
delete updates.expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
// 更新账户类型时处理共享账户集合
|
// 更新账户类型时处理共享账户集合
|
||||||
const client = redisClient.getClientSafe()
|
const client = redisClient.getClientSafe()
|
||||||
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
|
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
|
||||||
@@ -244,6 +273,10 @@ async function updateAccount(accountId, updates) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!updatedAccount.subscriptionExpiresAt) {
|
||||||
|
updatedAccount.subscriptionExpiresAt = null
|
||||||
|
}
|
||||||
|
|
||||||
return updatedAccount
|
return updatedAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +336,8 @@ async function getAllAccounts() {
|
|||||||
accounts.push({
|
accounts.push({
|
||||||
...accountData,
|
...accountData,
|
||||||
isActive: accountData.isActive === 'true',
|
isActive: accountData.isActive === 'true',
|
||||||
schedulable: accountData.schedulable !== 'false'
|
schedulable: accountData.schedulable !== 'false',
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ const config = require('../../config/config')
|
|||||||
const bedrockRelayService = require('./bedrockRelayService')
|
const bedrockRelayService = require('./bedrockRelayService')
|
||||||
const LRUCache = require('../utils/lruCache')
|
const LRUCache = require('../utils/lruCache')
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
class BedrockAccountService {
|
class BedrockAccountService {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 加密相关常量
|
// 加密相关常量
|
||||||
@@ -40,7 +53,8 @@ class BedrockAccountService {
|
|||||||
accountType = 'shared', // 'dedicated' or 'shared'
|
accountType = 'shared', // 'dedicated' or 'shared'
|
||||||
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
credentialType = 'default' // 'default', 'access_key', 'bearer_token'
|
credentialType = 'default', // 'default', 'access_key', 'bearer_token'
|
||||||
|
subscriptionExpiresAt = null
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const accountId = uuidv4()
|
const accountId = uuidv4()
|
||||||
@@ -56,6 +70,7 @@ class BedrockAccountService {
|
|||||||
priority,
|
priority,
|
||||||
schedulable,
|
schedulable,
|
||||||
credentialType,
|
credentialType,
|
||||||
|
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
type: 'bedrock' // 标识这是Bedrock账户
|
type: 'bedrock' // 标识这是Bedrock账户
|
||||||
@@ -84,6 +99,7 @@ class BedrockAccountService {
|
|||||||
priority,
|
priority,
|
||||||
schedulable,
|
schedulable,
|
||||||
credentialType,
|
credentialType,
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
|
||||||
createdAt: accountData.createdAt,
|
createdAt: accountData.createdAt,
|
||||||
type: 'bedrock'
|
type: 'bedrock'
|
||||||
}
|
}
|
||||||
@@ -106,6 +122,11 @@ class BedrockAccountService {
|
|||||||
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
|
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
account.subscriptionExpiresAt =
|
||||||
|
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
|
||||||
|
? account.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
|
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -145,7 +166,9 @@ class BedrockAccountService {
|
|||||||
createdAt: account.createdAt,
|
createdAt: account.createdAt,
|
||||||
updatedAt: account.updatedAt,
|
updatedAt: account.updatedAt,
|
||||||
type: 'bedrock',
|
type: 'bedrock',
|
||||||
hasCredentials: !!account.awsCredentials
|
hasCredentials: !!account.awsCredentials,
|
||||||
|
expiresAt: account.expiresAt || null,
|
||||||
|
subscriptionExpiresAt: account.subscriptionExpiresAt || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +234,14 @@ class BedrockAccountService {
|
|||||||
account.credentialType = updates.credentialType
|
account.credentialType = updates.credentialType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
account.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
|
||||||
|
updates.subscriptionExpiresAt
|
||||||
|
)
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
|
||||||
|
account.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
// 更新AWS凭证
|
// 更新AWS凭证
|
||||||
if (updates.awsCredentials !== undefined) {
|
if (updates.awsCredentials !== undefined) {
|
||||||
if (updates.awsCredentials) {
|
if (updates.awsCredentials) {
|
||||||
@@ -245,7 +276,9 @@ class BedrockAccountService {
|
|||||||
schedulable: account.schedulable,
|
schedulable: account.schedulable,
|
||||||
credentialType: account.credentialType,
|
credentialType: account.credentialType,
|
||||||
updatedAt: account.updatedAt,
|
updatedAt: account.updatedAt,
|
||||||
type: 'bedrock'
|
type: 'bedrock',
|
||||||
|
expiresAt: account.expiresAt || null,
|
||||||
|
subscriptionExpiresAt: account.subscriptionExpiresAt || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ const logger = require('../utils/logger')
|
|||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const LRUCache = require('../utils/lruCache')
|
const LRUCache = require('../utils/lruCache')
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
class CcrAccountService {
|
class CcrAccountService {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 加密相关常量
|
// 加密相关常量
|
||||||
@@ -49,7 +62,8 @@ class CcrAccountService {
|
|||||||
accountType = 'shared', // 'dedicated' or 'shared'
|
accountType = 'shared', // 'dedicated' or 'shared'
|
||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
||||||
quotaResetTime = '00:00' // 额度重置时间(HH:mm格式)
|
quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
|
||||||
|
subscriptionExpiresAt = null
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
@@ -91,7 +105,8 @@ class CcrAccountService {
|
|||||||
// 使用与统计一致的时区日期,避免边界问题
|
// 使用与统计一致的时区日期,避免边界问题
|
||||||
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
||||||
quotaResetTime, // 额度重置时间
|
quotaResetTime, // 额度重置时间
|
||||||
quotaStoppedAt: '' // 因额度停用的时间
|
quotaStoppedAt: '', // 因额度停用的时间
|
||||||
|
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
@@ -127,7 +142,8 @@ class CcrAccountService {
|
|||||||
dailyUsage: 0,
|
dailyUsage: 0,
|
||||||
lastResetDate: accountData.lastResetDate,
|
lastResetDate: accountData.lastResetDate,
|
||||||
quotaResetTime,
|
quotaResetTime,
|
||||||
quotaStoppedAt: null
|
quotaStoppedAt: null,
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +186,9 @@ class CcrAccountService {
|
|||||||
dailyUsage: parseFloat(accountData.dailyUsage || '0'),
|
dailyUsage: parseFloat(accountData.dailyUsage || '0'),
|
||||||
lastResetDate: accountData.lastResetDate || '',
|
lastResetDate: accountData.lastResetDate || '',
|
||||||
quotaResetTime: accountData.quotaResetTime || '00:00',
|
quotaResetTime: accountData.quotaResetTime || '00:00',
|
||||||
quotaStoppedAt: accountData.quotaStoppedAt || null
|
quotaStoppedAt: accountData.quotaStoppedAt || null,
|
||||||
|
expiresAt: accountData.expiresAt || null,
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +243,11 @@ class CcrAccountService {
|
|||||||
`[DEBUG] Final CCR account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
|
`[DEBUG] Final CCR account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
accountData.subscriptionExpiresAt =
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
return accountData
|
return accountData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +311,14 @@ class CcrAccountService {
|
|||||||
updatedData.quotaResetTime = updates.quotaResetTime
|
updatedData.quotaResetTime = updates.quotaResetTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
|
||||||
|
updates.subscriptionExpiresAt
|
||||||
|
)
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
|
||||||
|
updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData)
|
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData)
|
||||||
|
|
||||||
// 处理共享账户集合变更
|
// 处理共享账户集合变更
|
||||||
|
|||||||
@@ -185,6 +185,10 @@ class ClaudeAccountService {
|
|||||||
status: accountData.status,
|
status: accountData.status,
|
||||||
createdAt: accountData.createdAt,
|
createdAt: accountData.createdAt,
|
||||||
expiresAt: accountData.expiresAt,
|
expiresAt: accountData.expiresAt,
|
||||||
|
subscriptionExpiresAt:
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null,
|
||||||
scopes: claudeAiOauth ? claudeAiOauth.scopes : [],
|
scopes: claudeAiOauth ? claudeAiOauth.scopes : [],
|
||||||
autoStopOnWarning,
|
autoStopOnWarning,
|
||||||
useUnifiedUserAgent,
|
useUnifiedUserAgent,
|
||||||
@@ -491,7 +495,11 @@ class ClaudeAccountService {
|
|||||||
createdAt: account.createdAt,
|
createdAt: account.createdAt,
|
||||||
lastUsedAt: account.lastUsedAt,
|
lastUsedAt: account.lastUsedAt,
|
||||||
lastRefreshAt: account.lastRefreshAt,
|
lastRefreshAt: account.lastRefreshAt,
|
||||||
expiresAt: account.subscriptionExpiresAt || null, // 账户订阅到期时间
|
expiresAt: account.expiresAt || null,
|
||||||
|
subscriptionExpiresAt:
|
||||||
|
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
|
||||||
|
? account.subscriptionExpiresAt
|
||||||
|
: null,
|
||||||
// 添加 scopes 字段用于判断认证方式
|
// 添加 scopes 字段用于判断认证方式
|
||||||
// 处理空字符串的情况,避免返回 ['']
|
// 处理空字符串的情况,避免返回 ['']
|
||||||
scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [],
|
scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [],
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ const logger = require('../utils/logger')
|
|||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const LRUCache = require('../utils/lruCache')
|
const LRUCache = require('../utils/lruCache')
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
class ClaudeConsoleAccountService {
|
class ClaudeConsoleAccountService {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 加密相关常量
|
// 加密相关常量
|
||||||
@@ -52,7 +65,8 @@ class ClaudeConsoleAccountService {
|
|||||||
accountType = 'shared', // 'dedicated' or 'shared'
|
accountType = 'shared', // 'dedicated' or 'shared'
|
||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
||||||
quotaResetTime = '00:00' // 额度重置时间(HH:mm格式)
|
quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
|
||||||
|
subscriptionExpiresAt = null
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
@@ -94,7 +108,8 @@ class ClaudeConsoleAccountService {
|
|||||||
// 使用与统计一致的时区日期,避免边界问题
|
// 使用与统计一致的时区日期,避免边界问题
|
||||||
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
||||||
quotaResetTime, // 额度重置时间
|
quotaResetTime, // 额度重置时间
|
||||||
quotaStoppedAt: '' // 因额度停用的时间
|
quotaStoppedAt: '', // 因额度停用的时间
|
||||||
|
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
@@ -130,7 +145,8 @@ class ClaudeConsoleAccountService {
|
|||||||
dailyUsage: 0,
|
dailyUsage: 0,
|
||||||
lastResetDate: accountData.lastResetDate,
|
lastResetDate: accountData.lastResetDate,
|
||||||
quotaResetTime,
|
quotaResetTime,
|
||||||
quotaStoppedAt: null
|
quotaStoppedAt: null,
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +189,9 @@ class ClaudeConsoleAccountService {
|
|||||||
dailyUsage: parseFloat(accountData.dailyUsage || '0'),
|
dailyUsage: parseFloat(accountData.dailyUsage || '0'),
|
||||||
lastResetDate: accountData.lastResetDate || '',
|
lastResetDate: accountData.lastResetDate || '',
|
||||||
quotaResetTime: accountData.quotaResetTime || '00:00',
|
quotaResetTime: accountData.quotaResetTime || '00:00',
|
||||||
quotaStoppedAt: accountData.quotaStoppedAt || null
|
quotaStoppedAt: accountData.quotaStoppedAt || null,
|
||||||
|
expiresAt: accountData.expiresAt || null,
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,6 +242,11 @@ class ClaudeConsoleAccountService {
|
|||||||
accountData.proxy = JSON.parse(accountData.proxy)
|
accountData.proxy = JSON.parse(accountData.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountData.subscriptionExpiresAt =
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
|
`[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
|
||||||
)
|
)
|
||||||
@@ -318,6 +341,14 @@ class ClaudeConsoleAccountService {
|
|||||||
updatedData.quotaStoppedAt = updates.quotaStoppedAt
|
updatedData.quotaStoppedAt = updates.quotaStoppedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
|
||||||
|
updates.subscriptionExpiresAt
|
||||||
|
)
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
|
||||||
|
updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
// 处理账户类型变更
|
// 处理账户类型变更
|
||||||
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
|
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
|
||||||
updatedData.accountType = updates.accountType
|
updatedData.accountType = updates.accountType
|
||||||
|
|||||||
@@ -487,9 +487,12 @@ class ClaudeConsoleRelayService {
|
|||||||
|
|
||||||
// 解析SSE数据寻找usage信息
|
// 解析SSE数据寻找usage信息
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ') && line.length > 6) {
|
if (line.startsWith('data:')) {
|
||||||
|
const jsonStr = line.slice(5).trimStart()
|
||||||
|
if (!jsonStr || jsonStr === '[DONE]') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const jsonStr = line.slice(6)
|
|
||||||
const data = JSON.parse(jsonStr)
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
// 收集usage数据
|
// 收集usage数据
|
||||||
|
|||||||
@@ -1487,9 +1487,12 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// 解析SSE数据寻找usage信息
|
// 解析SSE数据寻找usage信息
|
||||||
if (line.startsWith('data: ') && line.length > 6) {
|
if (line.startsWith('data:')) {
|
||||||
|
const jsonStr = line.slice(5).trimStart()
|
||||||
|
if (!jsonStr || jsonStr === '[DONE]') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const jsonStr = line.slice(6)
|
|
||||||
const data = JSON.parse(jsonStr)
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
// 收集来自不同事件的usage数据
|
// 收集来自不同事件的usage数据
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ function generateEncryptionKey() {
|
|||||||
return _encryptionKeyCache
|
return _encryptionKeyCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
// Gemini 账户键前缀
|
// Gemini 账户键前缀
|
||||||
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
|
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
|
||||||
const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts'
|
const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts'
|
||||||
@@ -333,6 +346,10 @@ async function createAccount(accountData) {
|
|||||||
let refreshToken = ''
|
let refreshToken = ''
|
||||||
let expiresAt = ''
|
let expiresAt = ''
|
||||||
|
|
||||||
|
const subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
|
||||||
|
accountData.subscriptionExpiresAt || ''
|
||||||
|
)
|
||||||
|
|
||||||
if (accountData.geminiOauth || accountData.accessToken) {
|
if (accountData.geminiOauth || accountData.accessToken) {
|
||||||
// 如果提供了完整的 OAuth 数据
|
// 如果提供了完整的 OAuth 数据
|
||||||
if (accountData.geminiOauth) {
|
if (accountData.geminiOauth) {
|
||||||
@@ -404,7 +421,8 @@ async function createAccount(accountData) {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
lastUsedAt: '',
|
lastUsedAt: '',
|
||||||
lastRefreshAt: ''
|
lastRefreshAt: '',
|
||||||
|
subscriptionExpiresAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到 Redis
|
// 保存到 Redis
|
||||||
@@ -428,6 +446,10 @@ async function createAccount(accountData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!returnAccount.subscriptionExpiresAt) {
|
||||||
|
returnAccount.subscriptionExpiresAt = null
|
||||||
|
}
|
||||||
|
|
||||||
return returnAccount
|
return returnAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +486,10 @@ async function getAccount(accountId) {
|
|||||||
// 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致)
|
// 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致)
|
||||||
accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false
|
accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false
|
||||||
|
|
||||||
|
if (!accountData.subscriptionExpiresAt) {
|
||||||
|
accountData.subscriptionExpiresAt = null
|
||||||
|
}
|
||||||
|
|
||||||
return accountData
|
return accountData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,6 +503,10 @@ async function updateAccount(accountId, updates) {
|
|||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
updates.updatedAt = now
|
updates.updatedAt = now
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否新增了 refresh token
|
// 检查是否新增了 refresh token
|
||||||
// existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回)
|
// existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回)
|
||||||
const oldRefreshToken = existingAccount.refreshToken || ''
|
const oldRefreshToken = existingAccount.refreshToken || ''
|
||||||
@@ -586,6 +616,10 @@ async function updateAccount(accountId, updates) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!updatedAccount.subscriptionExpiresAt) {
|
||||||
|
updatedAccount.subscriptionExpiresAt = null
|
||||||
|
}
|
||||||
|
|
||||||
return updatedAccount
|
return updatedAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,6 +683,7 @@ async function getAllAccounts() {
|
|||||||
geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '',
|
geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '',
|
||||||
accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
|
accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
|
||||||
refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
|
refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
|
||||||
// 添加 scopes 字段用于判断认证方式
|
// 添加 scopes 字段用于判断认证方式
|
||||||
// 处理空字符串和默认值的情况
|
// 处理空字符串和默认值的情况
|
||||||
scopes:
|
scopes:
|
||||||
|
|||||||
@@ -194,6 +194,19 @@ function buildCodexUsageSnapshot(accountData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新访问令牌
|
// 刷新访问令牌
|
||||||
async function refreshAccessToken(refreshToken, proxy = null) {
|
async function refreshAccessToken(refreshToken, proxy = null) {
|
||||||
try {
|
try {
|
||||||
@@ -517,6 +530,13 @@ async function createAccount(accountData) {
|
|||||||
// 处理账户信息
|
// 处理账户信息
|
||||||
const accountInfo = accountData.accountInfo || {}
|
const accountInfo = accountData.accountInfo || {}
|
||||||
|
|
||||||
|
const tokenExpiresAt = oauthData.expires_in
|
||||||
|
? new Date(Date.now() + oauthData.expires_in * 1000).toISOString()
|
||||||
|
: ''
|
||||||
|
const subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
|
||||||
|
accountData.subscriptionExpiresAt || accountInfo.subscriptionExpiresAt || ''
|
||||||
|
)
|
||||||
|
|
||||||
// 检查邮箱是否已经是加密格式(包含冒号分隔的32位十六进制字符)
|
// 检查邮箱是否已经是加密格式(包含冒号分隔的32位十六进制字符)
|
||||||
const isEmailEncrypted =
|
const isEmailEncrypted =
|
||||||
accountInfo.email && accountInfo.email.length >= 33 && accountInfo.email.charAt(32) === ':'
|
accountInfo.email && accountInfo.email.length >= 33 && accountInfo.email.charAt(32) === ':'
|
||||||
@@ -553,9 +573,8 @@ async function createAccount(accountData) {
|
|||||||
email: isEmailEncrypted ? accountInfo.email : encrypt(accountInfo.email || ''),
|
email: isEmailEncrypted ? accountInfo.email : encrypt(accountInfo.email || ''),
|
||||||
emailVerified: accountInfo.emailVerified === true ? 'true' : 'false',
|
emailVerified: accountInfo.emailVerified === true ? 'true' : 'false',
|
||||||
// 过期时间
|
// 过期时间
|
||||||
expiresAt: oauthData.expires_in
|
expiresAt: tokenExpiresAt,
|
||||||
? new Date(Date.now() + oauthData.expires_in * 1000).toISOString()
|
subscriptionExpiresAt,
|
||||||
: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 默认1年
|
|
||||||
// 状态字段
|
// 状态字段
|
||||||
isActive: accountData.isActive !== false ? 'true' : 'false',
|
isActive: accountData.isActive !== false ? 'true' : 'false',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
@@ -580,7 +599,10 @@ async function createAccount(accountData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Created OpenAI account: ${accountId}`)
|
logger.info(`Created OpenAI account: ${accountId}`)
|
||||||
return account
|
return {
|
||||||
|
...account,
|
||||||
|
subscriptionExpiresAt: account.subscriptionExpiresAt || null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取账户
|
// 获取账户
|
||||||
@@ -623,6 +645,11 @@ async function getAccount(accountId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountData.subscriptionExpiresAt =
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
return accountData
|
return accountData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,6 +683,10 @@ async function updateAccount(accountId, updates) {
|
|||||||
updates.email = encrypt(updates.email)
|
updates.email = encrypt(updates.email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
// 处理代理配置
|
// 处理代理配置
|
||||||
if (updates.proxy) {
|
if (updates.proxy) {
|
||||||
updates.proxy =
|
updates.proxy =
|
||||||
@@ -688,6 +719,10 @@ async function updateAccount(accountId, updates) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!updatedAccount.subscriptionExpiresAt) {
|
||||||
|
updatedAccount.subscriptionExpiresAt = null
|
||||||
|
}
|
||||||
|
|
||||||
return updatedAccount
|
return updatedAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,6 +805,8 @@ async function getAllAccounts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscriptionExpiresAt = accountData.subscriptionExpiresAt || null
|
||||||
|
|
||||||
// 不解密敏感字段,只返回基本信息
|
// 不解密敏感字段,只返回基本信息
|
||||||
accounts.push({
|
accounts.push({
|
||||||
...accountData,
|
...accountData,
|
||||||
@@ -784,6 +821,7 @@ async function getAllAccounts() {
|
|||||||
accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [],
|
accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [],
|
||||||
// 添加 hasRefreshToken 标记
|
// 添加 hasRefreshToken 标记
|
||||||
hasRefreshToken: hasRefreshTokenFlag,
|
hasRefreshToken: hasRefreshTokenFlag,
|
||||||
|
subscriptionExpiresAt,
|
||||||
// 添加限流状态信息(统一格式)
|
// 添加限流状态信息(统一格式)
|
||||||
rateLimitStatus: rateLimitInfo
|
rateLimitStatus: rateLimitInfo
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ const logger = require('../utils/logger')
|
|||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const LRUCache = require('../utils/lruCache')
|
const LRUCache = require('../utils/lruCache')
|
||||||
|
|
||||||
|
function normalizeSubscriptionExpiresAt(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
class OpenAIResponsesAccountService {
|
class OpenAIResponsesAccountService {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 加密相关常量
|
// 加密相关常量
|
||||||
@@ -49,7 +62,8 @@ class OpenAIResponsesAccountService {
|
|||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
||||||
quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
|
quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
|
||||||
rateLimitDuration = 60 // 限流时间(分钟)
|
rateLimitDuration = 60, // 限流时间(分钟)
|
||||||
|
subscriptionExpiresAt = null
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
@@ -88,7 +102,8 @@ class OpenAIResponsesAccountService {
|
|||||||
dailyUsage: '0',
|
dailyUsage: '0',
|
||||||
lastResetDate: redis.getDateStringInTimezone(),
|
lastResetDate: redis.getDateStringInTimezone(),
|
||||||
quotaResetTime,
|
quotaResetTime,
|
||||||
quotaStoppedAt: ''
|
quotaStoppedAt: '',
|
||||||
|
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到 Redis
|
// 保存到 Redis
|
||||||
@@ -98,6 +113,7 @@ class OpenAIResponsesAccountService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...accountData,
|
...accountData,
|
||||||
|
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
|
||||||
apiKey: '***' // 返回时隐藏敏感信息
|
apiKey: '***' // 返回时隐藏敏感信息
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +140,11 @@ class OpenAIResponsesAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountData.subscriptionExpiresAt =
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
return accountData
|
return accountData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +172,13 @@ class OpenAIResponsesAccountService {
|
|||||||
: updates.baseApi
|
: updates.baseApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
|
||||||
|
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
|
||||||
|
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
|
||||||
|
delete updates.expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
// 更新 Redis
|
// 更新 Redis
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
|
const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
|
||||||
@@ -257,6 +285,10 @@ class OpenAIResponsesAccountService {
|
|||||||
accountData.schedulable = accountData.schedulable !== 'false'
|
accountData.schedulable = accountData.schedulable !== 'false'
|
||||||
// 转换 isActive 字段为布尔值
|
// 转换 isActive 字段为布尔值
|
||||||
accountData.isActive = accountData.isActive === 'true'
|
accountData.isActive = accountData.isActive === 'true'
|
||||||
|
accountData.subscriptionExpiresAt =
|
||||||
|
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
|
||||||
|
? accountData.subscriptionExpiresAt
|
||||||
|
: null
|
||||||
|
|
||||||
accounts.push(accountData)
|
accounts.push(accountData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,6 +261,10 @@
|
|||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .modal {
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: rgba(255, 255, 255, 0.98);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
@@ -271,6 +275,14 @@
|
|||||||
/* 移除模糊效果 */
|
/* 移除模糊效果 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .modal-content {
|
||||||
|
background: rgba(17, 24, 39, 0.95);
|
||||||
|
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 25px -5px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
/* 弹窗滚动内容样式 */
|
/* 弹窗滚动内容样式 */
|
||||||
.modal-scroll-content {
|
.modal-scroll-content {
|
||||||
max-height: calc(90vh - 160px);
|
max-height: calc(90vh - 160px);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||||||
<div
|
<div
|
||||||
class="modal-content custom-scrollbar mx-auto max-h-[90vh] w-full max-w-2xl overflow-y-auto p-4 sm:p-6 md:p-8"
|
class="modal-content custom-scrollbar mx-auto max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-2xl bg-white/90 p-4 shadow-xl backdrop-blur-xl dark:bg-gray-800/95 dark:shadow-2xl sm:p-6 md:p-8"
|
||||||
>
|
>
|
||||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||||
<div class="flex items-center gap-2 sm:gap-3">
|
<div class="flex items-center gap-2 sm:gap-3">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
|
class="p-1 text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-lg sm:text-xl" />
|
<i class="fas fa-times text-lg sm:text-xl" />
|
||||||
@@ -419,18 +419,6 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.modal-content {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 16px;
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .modal-content {
|
|
||||||
background: rgba(17, 24, 39, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|||||||
@@ -346,151 +346,76 @@
|
|||||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
>Claude 专属账号</label
|
>Claude 专属账号</label
|
||||||
>
|
>
|
||||||
<select
|
<AccountSelector
|
||||||
v-model="form.claudeAccountId"
|
v-model="claudeAccountSelectorValue"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
:accounts="localAccounts.claude"
|
||||||
:disabled="form.permissions && !['all', 'claude'].includes(form.permissions)"
|
default-option-text="请选择Claude账号"
|
||||||
>
|
:disabled="!isServiceSelectable('claude')"
|
||||||
<option value="">不修改</option>
|
:groups="localAccounts.claudeGroups"
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
placeholder="请选择Claude账号"
|
||||||
<optgroup v-if="localAccounts.claudeGroups.length > 0" label="账号分组">
|
platform="claude"
|
||||||
<option
|
:special-options="accountSpecialOptions"
|
||||||
v-for="group in localAccounts.claudeGroups"
|
/>
|
||||||
:key="group.id"
|
|
||||||
:value="`group:${group.id}`"
|
|
||||||
>
|
|
||||||
分组 - {{ group.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup v-if="localAccounts.claude.length > 0" label="专属账号">
|
|
||||||
<option
|
|
||||||
v-for="account in localAccounts.claude"
|
|
||||||
:key="account.id"
|
|
||||||
:value="
|
|
||||||
account.platform === 'claude-console' ? `console:${account.id}` : account.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ account.name }} ({{
|
|
||||||
account.platform === 'claude-console' ? 'Console' : 'OAuth'
|
|
||||||
}})
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
>Gemini 专属账号</label
|
>Gemini 专属账号</label
|
||||||
>
|
>
|
||||||
<select
|
<AccountSelector
|
||||||
v-model="form.geminiAccountId"
|
v-model="geminiAccountSelectorValue"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
:accounts="localAccounts.gemini"
|
||||||
:disabled="form.permissions && !['all', 'gemini'].includes(form.permissions)"
|
default-option-text="请选择Gemini账号"
|
||||||
>
|
:disabled="!isServiceSelectable('gemini')"
|
||||||
<option value="">不修改</option>
|
:groups="localAccounts.geminiGroups"
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
placeholder="请选择Gemini账号"
|
||||||
<optgroup v-if="localAccounts.geminiGroups.length > 0" label="账号分组">
|
platform="gemini"
|
||||||
<option
|
:special-options="accountSpecialOptions"
|
||||||
v-for="group in localAccounts.geminiGroups"
|
/>
|
||||||
:key="group.id"
|
|
||||||
:value="`group:${group.id}`"
|
|
||||||
>
|
|
||||||
分组 - {{ group.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup v-if="localAccounts.gemini.length > 0" label="专属账号">
|
|
||||||
<option
|
|
||||||
v-for="account in localAccounts.gemini"
|
|
||||||
:key="account.id"
|
|
||||||
:value="account.id"
|
|
||||||
>
|
|
||||||
{{ account.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
>OpenAI 专属账号</label
|
>OpenAI 专属账号</label
|
||||||
>
|
>
|
||||||
<select
|
<AccountSelector
|
||||||
v-model="form.openaiAccountId"
|
v-model="openaiAccountSelectorValue"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
:accounts="localAccounts.openai"
|
||||||
:disabled="form.permissions && !['all', 'openai'].includes(form.permissions)"
|
default-option-text="请选择OpenAI账号"
|
||||||
>
|
:disabled="!isServiceSelectable('openai')"
|
||||||
<option value="">不修改</option>
|
:groups="localAccounts.openaiGroups"
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
placeholder="请选择OpenAI账号"
|
||||||
<optgroup v-if="localAccounts.openaiGroups.length > 0" label="账号分组">
|
platform="openai"
|
||||||
<option
|
:special-options="accountSpecialOptions"
|
||||||
v-for="group in localAccounts.openaiGroups"
|
/>
|
||||||
:key="group.id"
|
|
||||||
:value="`group:${group.id}`"
|
|
||||||
>
|
|
||||||
分组 - {{ group.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup v-if="localAccounts.openai.length > 0" label="专属账号">
|
|
||||||
<option
|
|
||||||
v-for="account in localAccounts.openai"
|
|
||||||
:key="account.id"
|
|
||||||
:value="account.id"
|
|
||||||
>
|
|
||||||
{{ account.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
>Bedrock 专属账号</label
|
>Bedrock 专属账号</label
|
||||||
>
|
>
|
||||||
<select
|
<AccountSelector
|
||||||
v-model="form.bedrockAccountId"
|
v-model="bedrockAccountSelectorValue"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
:accounts="localAccounts.bedrock"
|
||||||
:disabled="form.permissions && !['all', 'openai'].includes(form.permissions)"
|
default-option-text="请选择Bedrock账号"
|
||||||
>
|
:disabled="!isServiceSelectable('openai')"
|
||||||
<option value="">不修改</option>
|
:groups="[]"
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
placeholder="请选择Bedrock账号"
|
||||||
<optgroup v-if="localAccounts.bedrock.length > 0" label="专属账号">
|
platform="bedrock"
|
||||||
<option
|
:special-options="accountSpecialOptions"
|
||||||
v-for="account in localAccounts.bedrock"
|
/>
|
||||||
:key="account.id"
|
|
||||||
:value="account.id"
|
|
||||||
>
|
|
||||||
{{ account.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||||
>Droid 专属账号</label
|
>Droid 专属账号</label
|
||||||
>
|
>
|
||||||
<select
|
<AccountSelector
|
||||||
v-model="form.droidAccountId"
|
v-model="droidAccountSelectorValue"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
:accounts="localAccounts.droid"
|
||||||
:disabled="form.permissions && !['all', 'droid'].includes(form.permissions)"
|
default-option-text="请选择Droid账号"
|
||||||
>
|
:disabled="!isServiceSelectable('droid')"
|
||||||
<option value="">不修改</option>
|
:groups="localAccounts.droidGroups"
|
||||||
<option value="SHARED_POOL">使用共享账号池</option>
|
placeholder="请选择Droid账号"
|
||||||
<optgroup v-if="localAccounts.droidGroups.length > 0" label="账号分组">
|
platform="droid"
|
||||||
<option
|
:special-options="accountSpecialOptions"
|
||||||
v-for="group in localAccounts.droidGroups"
|
/>
|
||||||
:key="group.id"
|
|
||||||
:value="`group:${group.id}`"
|
|
||||||
>
|
|
||||||
分组 - {{ group.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup v-if="localAccounts.droid.length > 0" label="专属账号">
|
|
||||||
<option
|
|
||||||
v-for="account in localAccounts.droid"
|
|
||||||
:key="account.id"
|
|
||||||
:value="account.id"
|
|
||||||
>
|
|
||||||
{{ account.name }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -524,6 +449,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
|||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
|
import AccountSelector from '@/components/common/AccountSelector.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectedKeys: {
|
selectedKeys: {
|
||||||
@@ -594,6 +520,37 @@ const form = reactive({
|
|||||||
isActive: null // null表示不修改
|
isActive: null // null表示不修改
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const UNCHANGED_OPTION_VALUE = '__KEEP_ORIGINAL__'
|
||||||
|
|
||||||
|
const accountSpecialOptions = [
|
||||||
|
{ value: UNCHANGED_OPTION_VALUE, label: '不修改' },
|
||||||
|
{ value: 'SHARED_POOL', label: '使用共享账号池' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const createAccountSelectorModel = (field) =>
|
||||||
|
computed({
|
||||||
|
get: () => (form[field] === '' ? UNCHANGED_OPTION_VALUE : form[field]),
|
||||||
|
set: (value) => {
|
||||||
|
if (!value || value === UNCHANGED_OPTION_VALUE) {
|
||||||
|
form[field] = ''
|
||||||
|
} else {
|
||||||
|
form[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const claudeAccountSelectorValue = createAccountSelectorModel('claudeAccountId')
|
||||||
|
const geminiAccountSelectorValue = createAccountSelectorModel('geminiAccountId')
|
||||||
|
const openaiAccountSelectorValue = createAccountSelectorModel('openaiAccountId')
|
||||||
|
const bedrockAccountSelectorValue = createAccountSelectorModel('bedrockAccountId')
|
||||||
|
const droidAccountSelectorValue = createAccountSelectorModel('droidAccountId')
|
||||||
|
|
||||||
|
const isServiceSelectable = (service) => {
|
||||||
|
if (!form.permissions) return true
|
||||||
|
if (form.permissions === 'all') return true
|
||||||
|
return form.permissions === service
|
||||||
|
}
|
||||||
|
|
||||||
// 标签管理方法
|
// 标签管理方法
|
||||||
const addTag = () => {
|
const addTag = () => {
|
||||||
if (newTag.value && newTag.value.trim()) {
|
if (newTag.value && newTag.value.trim()) {
|
||||||
|
|||||||
@@ -200,10 +200,6 @@ const getDisplayedApiKey = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const droidEndpoint = computed(() => {
|
|
||||||
return getBaseUrlPrefix() + '/droid/claude'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 通用复制工具,包含降级处理
|
// 通用复制工具,包含降级处理
|
||||||
const copyTextWithFallback = async (text, successMessage) => {
|
const copyTextWithFallback = async (text, successMessage) => {
|
||||||
try {
|
try {
|
||||||
@@ -235,9 +231,7 @@ const copyFullConfig = async () => {
|
|||||||
|
|
||||||
// 构建环境变量配置格式
|
// 构建环境变量配置格式
|
||||||
const configText = `ANTHROPIC_BASE_URL="${currentBaseUrl.value}"
|
const configText = `ANTHROPIC_BASE_URL="${currentBaseUrl.value}"
|
||||||
ANTHROPIC_AUTH_TOKEN="${key}"
|
ANTHROPIC_AUTH_TOKEN="${key}"`
|
||||||
|
|
||||||
# 提示:如需调用 /droid/claude 端点(已在后台添加 Droid 账号),请将 ANTHROPIC_BASE_URL 改为 "${droidEndpoint.value}" 或根据实际环境调整。`
|
|
||||||
|
|
||||||
await copyTextWithFallback(configText, '配置信息已复制到剪贴板')
|
await copyTextWithFallback(configText, '配置信息已复制到剪贴板')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,28 @@
|
|||||||
|
|
||||||
<!-- 选项列表 -->
|
<!-- 选项列表 -->
|
||||||
<div class="custom-scrollbar flex-1 overflow-y-auto">
|
<div class="custom-scrollbar flex-1 overflow-y-auto">
|
||||||
|
<!-- 特殊选项 -->
|
||||||
|
<div
|
||||||
|
v-if="specialOptionsList.length > 0"
|
||||||
|
class="border-b border-gray-200 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="option in specialOptionsList"
|
||||||
|
:key="`special-${option.value}`"
|
||||||
|
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
|
:class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === option.value }"
|
||||||
|
@click="selectAccount(option.value)"
|
||||||
|
>
|
||||||
|
<span class="text-gray-700 dark:text-gray-300">{{ option.label }}</span>
|
||||||
|
<span
|
||||||
|
v-if="option.description"
|
||||||
|
class="ml-2 text-xs text-gray-400 dark:text-gray-500"
|
||||||
|
>
|
||||||
|
{{ option.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 默认选项 -->
|
<!-- 默认选项 -->
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
@@ -264,6 +286,10 @@ const props = defineProps({
|
|||||||
defaultOptionText: {
|
defaultOptionText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '使用共享账号池'
|
default: '使用共享账号池'
|
||||||
|
},
|
||||||
|
specialOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -276,9 +302,17 @@ const dropdownRef = ref(null)
|
|||||||
const dropdownStyle = ref({})
|
const dropdownStyle = ref({})
|
||||||
const triggerRef = ref(null)
|
const triggerRef = ref(null)
|
||||||
const lastDirection = ref('') // 记住上次的显示方向
|
const lastDirection = ref('') // 记住上次的显示方向
|
||||||
|
const specialOptionsList = computed(() => props.specialOptions || [])
|
||||||
|
|
||||||
// 获取选中的标签
|
// 获取选中的标签
|
||||||
const selectedLabel = computed(() => {
|
const selectedLabel = computed(() => {
|
||||||
|
const matchedSpecial = specialOptionsList.value.find(
|
||||||
|
(option) => option.value === props.modelValue
|
||||||
|
)
|
||||||
|
if (matchedSpecial) {
|
||||||
|
return matchedSpecial.label
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有选中值,显示默认选项文本
|
// 如果没有选中值,显示默认选项文本
|
||||||
if (!props.modelValue) return props.defaultOptionText
|
if (!props.modelValue) return props.defaultOptionText
|
||||||
|
|
||||||
|
|||||||
@@ -296,15 +296,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-purple-700 dark:text-purple-300 sm:text-sm">
|
|
||||||
🚀 如果你在后台添加了 <strong>Droid</strong> 类型账号,请将上述命令中的
|
|
||||||
<code class="rounded bg-purple-100 px-1 dark:bg-purple-900">{{ currentBaseUrl }}</code>
|
|
||||||
替换为
|
|
||||||
<code class="rounded bg-purple-100 px-1 dark:bg-purple-900">{{
|
|
||||||
droidClaudeBaseUrl
|
|
||||||
}}</code
|
|
||||||
>,其余配置保持不变。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VSCode 插件配置 -->
|
<!-- VSCode 插件配置 -->
|
||||||
@@ -514,17 +505,6 @@
|
|||||||
{{ line }}
|
{{ line }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-yellow-700 dark:text-yellow-300 sm:text-sm">
|
|
||||||
🚀 如果你要使用 <strong>Droid</strong> 类型账号池,请把配置中的
|
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900">{{
|
|
||||||
openaiBaseUrl
|
|
||||||
}}</code>
|
|
||||||
替换为
|
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900">{{
|
|
||||||
droidOpenaiBaseUrl
|
|
||||||
}}</code
|
|
||||||
>。
|
|
||||||
</p>
|
|
||||||
<p class="mt-3 text-sm text-yellow-700 dark:text-yellow-300">
|
<p class="mt-3 text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
在
|
在
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900"
|
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900"
|
||||||
@@ -1009,15 +989,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-orange-700 dark:text-orange-300 sm:text-sm">
|
|
||||||
🚀 如果你创建了 <strong>Droid</strong> 类型账号,请把上述命令中的
|
|
||||||
<code class="rounded bg-orange-100 px-1 dark:bg-orange-900">{{ currentBaseUrl }}</code>
|
|
||||||
替换为
|
|
||||||
<code class="rounded bg-orange-100 px-1 dark:bg-orange-900">{{
|
|
||||||
droidClaudeBaseUrl
|
|
||||||
}}</code
|
|
||||||
>,其余配置保持不变。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VSCode 插件配置 (macOS) -->
|
<!-- VSCode 插件配置 (macOS) -->
|
||||||
@@ -1185,17 +1156,6 @@
|
|||||||
{{ line }}
|
{{ line }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-yellow-700 dark:text-yellow-300 sm:text-sm">
|
|
||||||
🚀 如果你要使用 <strong>Droid</strong> 类型账号池,请把配置中的
|
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900">{{
|
|
||||||
openaiBaseUrl
|
|
||||||
}}</code>
|
|
||||||
替换为
|
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900">{{
|
|
||||||
droidOpenaiBaseUrl
|
|
||||||
}}</code
|
|
||||||
>。
|
|
||||||
</p>
|
|
||||||
<p class="mt-3 text-sm text-yellow-700 dark:text-yellow-300">
|
<p class="mt-3 text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
在
|
在
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900"
|
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900"
|
||||||
@@ -1674,15 +1634,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-orange-700 dark:text-orange-300 sm:text-sm">
|
|
||||||
🚀 如果你创建了 <strong>Droid</strong> 类型账号,请把上述命令中的
|
|
||||||
<code class="rounded bg-orange-100 px-1 dark:bg-orange-900">{{ currentBaseUrl }}</code>
|
|
||||||
替换为
|
|
||||||
<code class="rounded bg-orange-100 px-1 dark:bg-orange-900">{{
|
|
||||||
droidClaudeBaseUrl
|
|
||||||
}}</code
|
|
||||||
>,其余配置保持不变。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gemini CLI 环境变量设置 -->
|
<!-- Gemini CLI 环境变量设置 -->
|
||||||
@@ -1818,17 +1769,6 @@
|
|||||||
{{ line }}
|
{{ line }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-yellow-700 dark:text-yellow-300 sm:text-sm">
|
|
||||||
🚀 如果你要使用 <strong>Droid</strong> 类型账号池,请把配置中的
|
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900">{{
|
|
||||||
openaiBaseUrl
|
|
||||||
}}</code>
|
|
||||||
替换为
|
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900">{{
|
|
||||||
droidOpenaiBaseUrl
|
|
||||||
}}</code
|
|
||||||
>。
|
|
||||||
</p>
|
|
||||||
<p class="mt-3 text-sm text-yellow-700 dark:text-yellow-300">
|
<p class="mt-3 text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
在
|
在
|
||||||
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900"
|
<code class="rounded bg-yellow-100 px-1 dark:bg-yellow-900"
|
||||||
@@ -2293,8 +2233,6 @@ const codexConfigContent = computed(() => {
|
|||||||
'[model_providers.crs]',
|
'[model_providers.crs]',
|
||||||
'name = "crs"',
|
'name = "crs"',
|
||||||
`base_url = "${openaiBaseUrl.value}"`,
|
`base_url = "${openaiBaseUrl.value}"`,
|
||||||
'# 若使用 Droid 类型账号,请改为以下地址',
|
|
||||||
`# base_url = "${droidOpenaiBaseUrl.value}"`,
|
|
||||||
'wire_api = "responses"',
|
'wire_api = "responses"',
|
||||||
'requires_openai_auth = true',
|
'requires_openai_auth = true',
|
||||||
'env_key = "CRS_OAI_KEY"'
|
'env_key = "CRS_OAI_KEY"'
|
||||||
|
|||||||
Reference in New Issue
Block a user