From b94bd2b8226662a698186e854c4e28be6e9b4588 Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 07:38:55 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(account):=20=E6=94=AF=E6=8C=81=20Pro?= =?UTF-8?q?=20=E8=B4=A6=E5=8F=B7=E4=BD=BF=E7=94=A8=20Opus=204.5+=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus 4.5 已对 Claude Pro 用户开放,调整账户模型限制逻辑: - Pro 账号:支持 Opus 4.5+,不支持历史版本 (3.x/4.0/4.1) - Free 账号:不支持任何 Opus 模型 - Max 账号:支持所有 Opus 版本 修改内容: - 新增 isOpus45OrNewer() 函数用于精确识别模型版本 - 更新 claudeAccountService.js 中的账户选择逻辑 - 更新 unifiedClaudeScheduler.js 中的模型支持检查 - 新增测试脚本验证官方模型名称识别 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/test-official-models.js | 166 +++++++++++++++++++++++++ src/services/claudeAccountService.js | 55 +++++--- src/services/unifiedClaudeScheduler.js | 44 +++++-- src/utils/modelHelper.js | 75 ++++++++++- 4 files changed, 311 insertions(+), 29 deletions(-) create mode 100644 scripts/test-official-models.js diff --git a/scripts/test-official-models.js b/scripts/test-official-models.js new file mode 100644 index 00000000..0a0a328c --- /dev/null +++ b/scripts/test-official-models.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node +/** + * 官方模型版本识别测试 - 最终版 v2 + */ + +/** + * 检查模型是否为 Opus 4.5 或更新版本 + * 支持格式: + * - 新格式: claude-opus-{major}[-{minor}][-date] 如 claude-opus-4-5-20251101 + * - 新格式: claude-opus-{major}.{minor} 如 claude-opus-4.5 + * - 旧格式: claude-{version}-opus[-date] 如 claude-3-opus-20240229 + * + * @param {string} modelName - 模型名称 + * @returns {boolean} - 是否为 Opus 4.5+ + */ +function isOpus45OrNewer(modelName) { + if (!modelName) return false + + const lowerModel = modelName.toLowerCase() + if (!lowerModel.includes('opus')) return false + + // 处理 latest 特殊情况 + if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) { + return true + } + + // 旧格式: claude-{version}-opus (版本在 opus 前面) + // 例如: claude-3-opus-20240229, claude-3.5-opus + const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[\.-](\d+))?[- ]opus/) + if (oldFormatMatch) { + const majorVersion = parseInt(oldFormatMatch[1], 10) + const minorVersion = oldFormatMatch[2] ? parseInt(oldFormatMatch[2], 10) : 0 + + // 旧格式的版本号指的是 Claude 大版本 + if (majorVersion > 4) return true + if (majorVersion === 4 && minorVersion >= 5) return true + return false + } + + // 新格式 1: opus-{major}.{minor} (点分隔) + // 例如: claude-opus-4.5, opus-4.5 + const dotFormatMatch = lowerModel.match(/opus[- ]?(\d+)\.(\d+)/) + if (dotFormatMatch) { + const majorVersion = parseInt(dotFormatMatch[1], 10) + const minorVersion = parseInt(dotFormatMatch[2], 10) + + if (majorVersion > 4) return true + if (majorVersion === 4 && minorVersion >= 5) return true + return false + } + + // 新格式 2: opus-{major}[-{minor}][-date] (横线分隔) + // 例如: claude-opus-4-5-20251101, claude-opus-4-20250514, claude-opus-4-1-20250805 + // 关键:小版本号必须是 1 位数字,且后面紧跟 8 位日期或结束 + // 如果 opus-{major} 后面直接是 8 位日期,则没有小版本号 + + // 提取 opus 后面的部分 + const opusIndex = lowerModel.indexOf('opus') + const afterOpus = lowerModel.substring(opusIndex + 4) // 'opus' 后面的内容 + + // 尝试匹配: -{major}-{minor}-{date} 或 -{major}-{date} 或 -{major} + // 小版本号只能是 1 位数字 (如 1, 5),不会是 2 位以上 + const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d)(?=[- ]\d{8}|$))?/) + + if (versionMatch) { + const majorVersion = parseInt(versionMatch[1], 10) + const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 + + if (majorVersion > 4) return true + if (majorVersion === 4 && minorVersion >= 5) return true + return false + } + + // 其他包含 opus 但无法解析版本的情况,默认认为是旧版本 + return false +} + +// 官方模型 +const officialModels = [ + { name: 'claude-3-opus-20240229', desc: 'Opus 3 (已弃用)', expectPro: false }, + { name: 'claude-opus-4-20250514', desc: 'Opus 4.0', expectPro: false }, + { name: 'claude-opus-4-1-20250805', desc: 'Opus 4.1', expectPro: false }, + { name: 'claude-opus-4-5-20251101', desc: 'Opus 4.5', expectPro: true }, +] + +// 非 Opus 模型 +const nonOpusModels = [ + { name: 'claude-sonnet-4-20250514', desc: 'Sonnet 4' }, + { name: 'claude-sonnet-4-5-20250929', desc: 'Sonnet 4.5' }, + { name: 'claude-haiku-4-5-20251001', desc: 'Haiku 4.5' }, + { name: 'claude-3-5-haiku-20241022', desc: 'Haiku 3.5' }, + { name: 'claude-3-haiku-20240307', desc: 'Haiku 3' }, + { name: 'claude-3-7-sonnet-20250219', desc: 'Sonnet 3.7 (已弃用)' }, +] + +// 其他格式测试 +const otherFormats = [ + { name: 'claude-opus-4.5', expected: true, desc: 'Opus 4.5 点分隔' }, + { name: 'claude-opus-4-5', expected: true, desc: 'Opus 4.5 横线分隔' }, + { name: 'opus-4.5', expected: true, desc: 'Opus 4.5 无前缀' }, + { name: 'opus-4-5', expected: true, desc: 'Opus 4-5 无前缀' }, + { name: 'opus-latest', expected: true, desc: 'Opus latest' }, + { name: 'claude-opus-5', expected: true, desc: 'Opus 5 (未来)' }, + { name: 'claude-opus-5-0', expected: true, desc: 'Opus 5.0 (未来)' }, + { name: 'opus-4.0', expected: false, desc: 'Opus 4.0' }, + { name: 'opus-4.1', expected: false, desc: 'Opus 4.1' }, + { name: 'opus-4.4', expected: false, desc: 'Opus 4.4' }, + { name: 'opus-4', expected: false, desc: 'Opus 4' }, + { name: 'opus-4-0', expected: false, desc: 'Opus 4-0' }, + { name: 'opus-4-1', expected: false, desc: 'Opus 4-1' }, + { name: 'opus-4-4', expected: false, desc: 'Opus 4-4' }, + { name: 'opus', expected: false, desc: '仅 opus' }, + { name: null, expected: false, desc: 'null' }, + { name: '', expected: false, desc: '空字符串' }, +] + +console.log('='.repeat(90)) +console.log('官方模型版本识别测试 - 最终版 v2') +console.log('='.repeat(90)) +console.log() + +let passed = 0 +let failed = 0 + +// 测试官方 Opus 模型 +console.log('📌 官方 Opus 模型:') +for (const m of officialModels) { + const result = isOpus45OrNewer(m.name) + const status = result === m.expectPro ? '✅ PASS' : '❌ FAIL' + if (result === m.expectPro) passed++ + else failed++ + const proSupport = result ? 'Pro 可用 ✅' : 'Pro 不可用 ❌' + console.log(` ${status} | ${m.name.padEnd(32)} | ${m.desc.padEnd(18)} | ${proSupport}`) +} + +console.log() +console.log('📌 非 Opus 模型 (不受此函数影响):') +for (const m of nonOpusModels) { + const result = isOpus45OrNewer(m.name) + console.log(` ➖ | ${m.name.padEnd(32)} | ${m.desc.padEnd(18)} | ${result ? '⚠️ 异常' : '正确跳过'}`) + if (result) failed++ // 非 Opus 模型不应返回 true +} + +console.log() +console.log('📌 其他格式测试:') +for (const m of otherFormats) { + const result = isOpus45OrNewer(m.name) + const status = result === m.expected ? '✅ PASS' : '❌ FAIL' + if (result === m.expected) passed++ + else failed++ + const display = m.name === null ? 'null' : m.name === '' ? '""' : m.name + console.log(` ${status} | ${display.padEnd(25)} | ${m.desc.padEnd(18)} | ${result ? 'Pro 可用' : 'Pro 不可用'}`) +} + +console.log() +console.log('='.repeat(90)) +console.log('测试结果:', passed, '通过,', failed, '失败') +console.log('='.repeat(90)) + +if (failed > 0) { + console.log('\n❌ 有测试失败,请检查函数逻辑') + process.exit(1) +} else { + console.log('\n✅ 所有测试通过!函数可以安全使用') + process.exit(0) +} diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 29a7821e..ec06e0ea 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -16,6 +16,7 @@ const { const tokenRefreshService = require('./tokenRefreshService') const LRUCache = require('../utils/lruCache') const { formatDateWithTimezone, getISOStringWithTimezone } = require('../utils/dateHelper') +const { isOpus45OrNewer } = require('../utils/modelHelper') class ClaudeAccountService { constructor() { @@ -852,22 +853,32 @@ class ClaudeAccountService { !this.isSubscriptionExpired(account) ) - // 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号 + // 如果请求的是 Opus 模型,根据账号类型和模型版本过滤 if (modelName && modelName.toLowerCase().includes('opus')) { + const isNewOpus = isOpus45OrNewer(modelName) + activeAccounts = activeAccounts.filter((account) => { - // 检查账号的订阅信息 if (account.subscriptionInfo) { try { const info = JSON.parse(account.subscriptionInfo) - // Pro 和 Free 账号不支持 Opus + + // Free 账号不支持任何 Opus 模型 + if (info.accountType === 'claude_free' || info.accountType === 'free') { + return false + } + + // Pro 账号:仅支持 Opus 4.5+ if (info.hasClaudePro === true && info.hasClaudeMax !== true) { - return false // Claude Pro 不支持 Opus + return isNewOpus // 仅新版 Opus 支持 } - if (info.accountType === 'claude_pro' || info.accountType === 'claude_free') { - return false // 明确标记为 Pro 或 Free 的账号不支持 + if (info.accountType === 'claude_pro') { + return isNewOpus // 仅新版 Opus 支持 } + + // Max 账号支持所有 Opus 版本 + return true } catch (e) { - // 解析失败,假设为旧数据,默认支持(兼容旧数据为 Max) + // 解析失败,假设为旧数据(Max),默认支持 return true } } @@ -876,7 +887,8 @@ class ClaudeAccountService { }) if (activeAccounts.length === 0) { - throw new Error('No Claude accounts available that support Opus model') + const modelDesc = isNewOpus ? 'Opus 4.5+' : 'legacy Opus (requires Max subscription)' + throw new Error(`No Claude accounts available that support ${modelDesc} model`) } } @@ -970,22 +982,32 @@ class ClaudeAccountService { !this.isSubscriptionExpired(account) ) - // 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号 + // 如果请求的是 Opus 模型,根据账号类型和模型版本过滤 if (modelName && modelName.toLowerCase().includes('opus')) { + const isNewOpus = isOpus45OrNewer(modelName) + sharedAccounts = sharedAccounts.filter((account) => { - // 检查账号的订阅信息 if (account.subscriptionInfo) { try { const info = JSON.parse(account.subscriptionInfo) - // Pro 和 Free 账号不支持 Opus + + // Free 账号不支持任何 Opus 模型 + if (info.accountType === 'claude_free' || info.accountType === 'free') { + return false + } + + // Pro 账号:仅支持 Opus 4.5+ if (info.hasClaudePro === true && info.hasClaudeMax !== true) { - return false // Claude Pro 不支持 Opus + return isNewOpus // 仅新版 Opus 支持 } - if (info.accountType === 'claude_pro' || info.accountType === 'claude_free') { - return false // 明确标记为 Pro 或 Free 的账号不支持 + if (info.accountType === 'claude_pro') { + return isNewOpus // 仅新版 Opus 支持 } + + // Max 账号支持所有 Opus 版本 + return true } catch (e) { - // 解析失败,假设为旧数据,默认支持(兼容旧数据为 Max) + // 解析失败,假设为旧数据(Max),默认支持 return true } } @@ -994,7 +1016,8 @@ class ClaudeAccountService { }) if (sharedAccounts.length === 0) { - throw new Error('No shared Claude accounts available that support Opus model') + const modelDesc = isNewOpus ? 'Opus 4.5+' : 'legacy Opus (requires Max subscription)' + throw new Error(`No shared Claude accounts available that support ${modelDesc} model`) } } diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index e68d607e..99d81336 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -5,7 +5,7 @@ const ccrAccountService = require('./ccrAccountService') const accountGroupService = require('./accountGroupService') const redis = require('../models/redis') const logger = require('../utils/logger') -const { parseVendorPrefixedModel } = require('../utils/modelHelper') +const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../utils/modelHelper') class UnifiedClaudeScheduler { constructor() { @@ -48,6 +48,8 @@ class UnifiedClaudeScheduler { // 2. Opus 模型的订阅级别检查 if (requestedModel.toLowerCase().includes('opus')) { + const isNewOpus = isOpus45OrNewer(requestedModel) + if (account.subscriptionInfo) { try { const info = @@ -55,21 +57,39 @@ class UnifiedClaudeScheduler { ? JSON.parse(account.subscriptionInfo) : account.subscriptionInfo - // Pro 和 Free 账号不支持 Opus + // Free 账号不支持任何 Opus 模型 + if (info.accountType === 'claude_free' || info.accountType === 'free') { + logger.info( + `🚫 Claude account ${account.name} (Free) does not support Opus model${context ? ` ${context}` : ''}` + ) + return false + } + + // Pro 账号:仅支持 Opus 4.5+ if (info.hasClaudePro === true && info.hasClaudeMax !== true) { - logger.info( - `🚫 Claude account ${account.name} (Pro) does not support Opus model${context ? ` ${context}` : ''}` - ) - return false + if (!isNewOpus) { + logger.info( + `🚫 Claude account ${account.name} (Pro) does not support legacy Opus model${context ? ` ${context}` : ''}` + ) + return false + } + // Opus 4.5+ 支持 + return true } - if (info.accountType === 'claude_pro' || info.accountType === 'claude_free') { - logger.info( - `🚫 Claude account ${account.name} (${info.accountType}) does not support Opus model${context ? ` ${context}` : ''}` - ) - return false + if (info.accountType === 'claude_pro') { + if (!isNewOpus) { + logger.info( + `🚫 Claude account ${account.name} (Pro) does not support legacy Opus model${context ? ` ${context}` : ''}` + ) + return false + } + // Opus 4.5+ 支持 + return true } + + // Max 账号支持所有 Opus 版本 } catch (e) { - // 解析失败,假设为旧数据,默认支持(兼容旧数据为 Max) + // 解析失败,假设为旧数据(Max),默认支持 logger.debug( `Account ${account.name} has invalid subscriptionInfo${context ? ` ${context}` : ''}, assuming Max` ) diff --git a/src/utils/modelHelper.js b/src/utils/modelHelper.js index cc954cc2..ac704e5e 100644 --- a/src/utils/modelHelper.js +++ b/src/utils/modelHelper.js @@ -70,9 +70,82 @@ function getVendorType(modelStr) { return vendor } +/** + * 检查模型是否为 Opus 4.5 或更新版本 + * 支持格式: + * - 新格式: claude-opus-{major}[-{minor}][-date] 如 claude-opus-4-5-20251101 + * - 新格式: claude-opus-{major}.{minor} 如 claude-opus-4.5 + * - 旧格式: claude-{version}-opus[-date] 如 claude-3-opus-20240229 + * + * @param {string} modelName - 模型名称 + * @returns {boolean} - 是否为 Opus 4.5+ + */ +function isOpus45OrNewer(modelName) { + if (!modelName) return false + + const lowerModel = modelName.toLowerCase() + if (!lowerModel.includes('opus')) return false + + // 处理 latest 特殊情况 + if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) { + return true + } + + // 旧格式: claude-{version}-opus (版本在 opus 前面) + // 例如: claude-3-opus-20240229, claude-3.5-opus + const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[\.-](\d+))?[- ]opus/) + if (oldFormatMatch) { + const majorVersion = parseInt(oldFormatMatch[1], 10) + const minorVersion = oldFormatMatch[2] ? parseInt(oldFormatMatch[2], 10) : 0 + + // 旧格式的版本号指的是 Claude 大版本 + if (majorVersion > 4) return true + if (majorVersion === 4 && minorVersion >= 5) return true + return false + } + + // 新格式 1: opus-{major}.{minor} (点分隔) + // 例如: claude-opus-4.5, opus-4.5 + const dotFormatMatch = lowerModel.match(/opus[- ]?(\d+)\.(\d+)/) + if (dotFormatMatch) { + const majorVersion = parseInt(dotFormatMatch[1], 10) + const minorVersion = parseInt(dotFormatMatch[2], 10) + + if (majorVersion > 4) return true + if (majorVersion === 4 && minorVersion >= 5) return true + return false + } + + // 新格式 2: opus-{major}[-{minor}][-date] (横线分隔) + // 例如: claude-opus-4-5-20251101, claude-opus-4-20250514, claude-opus-4-1-20250805 + // 关键:小版本号必须是 1 位数字,且后面紧跟 8 位日期或结束 + // 如果 opus-{major} 后面直接是 8 位日期,则没有小版本号 + + // 提取 opus 后面的部分 + const opusIndex = lowerModel.indexOf('opus') + const afterOpus = lowerModel.substring(opusIndex + 4) // 'opus' 后面的内容 + + // 尝试匹配: -{major}-{minor}-{date} 或 -{major}-{date} 或 -{major} + // 小版本号只能是 1 位数字 (如 1, 5),不会是 2 位以上 + const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d)(?=[- ]\d{8}|$))?/) + + if (versionMatch) { + const majorVersion = parseInt(versionMatch[1], 10) + const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 + + if (majorVersion > 4) return true + if (majorVersion === 4 && minorVersion >= 5) return true + return false + } + + // 其他包含 opus 但无法解析版本的情况,默认认为是旧版本 + return false +} + module.exports = { parseVendorPrefixedModel, hasVendorPrefix, getEffectiveModel, - getVendorType + getVendorType, + isOpus45OrNewer } From b1dc27b5d77debaaa7abb42ad029796f71b818b6 Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 07:43:15 +0800 Subject: [PATCH 2/5] style: format test-official-models.js with Prettier --- scripts/test-official-models.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/test-official-models.js b/scripts/test-official-models.js index 0a0a328c..3c73a2b5 100644 --- a/scripts/test-official-models.js +++ b/scripts/test-official-models.js @@ -80,7 +80,7 @@ const officialModels = [ { name: 'claude-3-opus-20240229', desc: 'Opus 3 (已弃用)', expectPro: false }, { name: 'claude-opus-4-20250514', desc: 'Opus 4.0', expectPro: false }, { name: 'claude-opus-4-1-20250805', desc: 'Opus 4.1', expectPro: false }, - { name: 'claude-opus-4-5-20251101', desc: 'Opus 4.5', expectPro: true }, + { name: 'claude-opus-4-5-20251101', desc: 'Opus 4.5', expectPro: true } ] // 非 Opus 模型 @@ -90,7 +90,7 @@ const nonOpusModels = [ { name: 'claude-haiku-4-5-20251001', desc: 'Haiku 4.5' }, { name: 'claude-3-5-haiku-20241022', desc: 'Haiku 3.5' }, { name: 'claude-3-haiku-20240307', desc: 'Haiku 3' }, - { name: 'claude-3-7-sonnet-20250219', desc: 'Sonnet 3.7 (已弃用)' }, + { name: 'claude-3-7-sonnet-20250219', desc: 'Sonnet 3.7 (已弃用)' } ] // 其他格式测试 @@ -111,7 +111,7 @@ const otherFormats = [ { name: 'opus-4-4', expected: false, desc: 'Opus 4-4' }, { name: 'opus', expected: false, desc: '仅 opus' }, { name: null, expected: false, desc: 'null' }, - { name: '', expected: false, desc: '空字符串' }, + { name: '', expected: false, desc: '空字符串' } ] console.log('='.repeat(90)) @@ -137,7 +137,9 @@ console.log() console.log('📌 非 Opus 模型 (不受此函数影响):') for (const m of nonOpusModels) { const result = isOpus45OrNewer(m.name) - console.log(` ➖ | ${m.name.padEnd(32)} | ${m.desc.padEnd(18)} | ${result ? '⚠️ 异常' : '正确跳过'}`) + console.log( + ` ➖ | ${m.name.padEnd(32)} | ${m.desc.padEnd(18)} | ${result ? '⚠️ 异常' : '正确跳过'}` + ) if (result) failed++ // 非 Opus 模型不应返回 true } @@ -149,7 +151,9 @@ for (const m of otherFormats) { if (result === m.expected) passed++ else failed++ const display = m.name === null ? 'null' : m.name === '' ? '""' : m.name - console.log(` ${status} | ${display.padEnd(25)} | ${m.desc.padEnd(18)} | ${result ? 'Pro 可用' : 'Pro 不可用'}`) + console.log( + ` ${status} | ${display.padEnd(25)} | ${m.desc.padEnd(18)} | ${result ? 'Pro 可用' : 'Pro 不可用'}` + ) } console.log() From dc868522cfe1524368b37535a53c8f38caefc038 Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 07:49:55 +0800 Subject: [PATCH 3/5] fix: apply ESLint curly rule and remove useless escape chars --- scripts/test-official-models.js | 52 ++++++++++++++++++++++++--------- src/utils/modelHelper.js | 34 +++++++++++++++------ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/scripts/test-official-models.js b/scripts/test-official-models.js index 3c73a2b5..f7046e5b 100644 --- a/scripts/test-official-models.js +++ b/scripts/test-official-models.js @@ -14,10 +14,14 @@ * @returns {boolean} - 是否为 Opus 4.5+ */ function isOpus45OrNewer(modelName) { - if (!modelName) return false + if (!modelName) { + return false + } const lowerModel = modelName.toLowerCase() - if (!lowerModel.includes('opus')) return false + if (!lowerModel.includes('opus')) { + return false + } // 处理 latest 特殊情况 if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) { @@ -26,14 +30,18 @@ function isOpus45OrNewer(modelName) { // 旧格式: claude-{version}-opus (版本在 opus 前面) // 例如: claude-3-opus-20240229, claude-3.5-opus - const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[\.-](\d+))?[- ]opus/) + const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[.-](\d+))?[- ]opus/) if (oldFormatMatch) { const majorVersion = parseInt(oldFormatMatch[1], 10) const minorVersion = oldFormatMatch[2] ? parseInt(oldFormatMatch[2], 10) : 0 // 旧格式的版本号指的是 Claude 大版本 - if (majorVersion > 4) return true - if (majorVersion === 4 && minorVersion >= 5) return true + if (majorVersion > 4) { + return true + } + if (majorVersion === 4 && minorVersion >= 5) { + return true + } return false } @@ -44,8 +52,12 @@ function isOpus45OrNewer(modelName) { const majorVersion = parseInt(dotFormatMatch[1], 10) const minorVersion = parseInt(dotFormatMatch[2], 10) - if (majorVersion > 4) return true - if (majorVersion === 4 && minorVersion >= 5) return true + if (majorVersion > 4) { + return true + } + if (majorVersion === 4 && minorVersion >= 5) { + return true + } return false } @@ -66,8 +78,12 @@ function isOpus45OrNewer(modelName) { const majorVersion = parseInt(versionMatch[1], 10) const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 - if (majorVersion > 4) return true - if (majorVersion === 4 && minorVersion >= 5) return true + if (majorVersion > 4) { + return true + } + if (majorVersion === 4 && minorVersion >= 5) { + return true + } return false } @@ -127,8 +143,11 @@ console.log('📌 官方 Opus 模型:') for (const m of officialModels) { const result = isOpus45OrNewer(m.name) const status = result === m.expectPro ? '✅ PASS' : '❌ FAIL' - if (result === m.expectPro) passed++ - else failed++ + if (result === m.expectPro) { + passed++ + } else { + failed++ + } const proSupport = result ? 'Pro 可用 ✅' : 'Pro 不可用 ❌' console.log(` ${status} | ${m.name.padEnd(32)} | ${m.desc.padEnd(18)} | ${proSupport}`) } @@ -140,7 +159,9 @@ for (const m of nonOpusModels) { console.log( ` ➖ | ${m.name.padEnd(32)} | ${m.desc.padEnd(18)} | ${result ? '⚠️ 异常' : '正确跳过'}` ) - if (result) failed++ // 非 Opus 模型不应返回 true + if (result) { + failed++ // 非 Opus 模型不应返回 true + } } console.log() @@ -148,8 +169,11 @@ console.log('📌 其他格式测试:') for (const m of otherFormats) { const result = isOpus45OrNewer(m.name) const status = result === m.expected ? '✅ PASS' : '❌ FAIL' - if (result === m.expected) passed++ - else failed++ + if (result === m.expected) { + passed++ + } else { + failed++ + } const display = m.name === null ? 'null' : m.name === '' ? '""' : m.name console.log( ` ${status} | ${display.padEnd(25)} | ${m.desc.padEnd(18)} | ${result ? 'Pro 可用' : 'Pro 不可用'}` diff --git a/src/utils/modelHelper.js b/src/utils/modelHelper.js index ac704e5e..d27fea87 100644 --- a/src/utils/modelHelper.js +++ b/src/utils/modelHelper.js @@ -81,10 +81,14 @@ function getVendorType(modelStr) { * @returns {boolean} - 是否为 Opus 4.5+ */ function isOpus45OrNewer(modelName) { - if (!modelName) return false + if (!modelName) { + return false + } const lowerModel = modelName.toLowerCase() - if (!lowerModel.includes('opus')) return false + if (!lowerModel.includes('opus')) { + return false + } // 处理 latest 特殊情况 if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) { @@ -93,14 +97,18 @@ function isOpus45OrNewer(modelName) { // 旧格式: claude-{version}-opus (版本在 opus 前面) // 例如: claude-3-opus-20240229, claude-3.5-opus - const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[\.-](\d+))?[- ]opus/) + const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[.-](\d+))?[- ]opus/) if (oldFormatMatch) { const majorVersion = parseInt(oldFormatMatch[1], 10) const minorVersion = oldFormatMatch[2] ? parseInt(oldFormatMatch[2], 10) : 0 // 旧格式的版本号指的是 Claude 大版本 - if (majorVersion > 4) return true - if (majorVersion === 4 && minorVersion >= 5) return true + if (majorVersion > 4) { + return true + } + if (majorVersion === 4 && minorVersion >= 5) { + return true + } return false } @@ -111,8 +119,12 @@ function isOpus45OrNewer(modelName) { const majorVersion = parseInt(dotFormatMatch[1], 10) const minorVersion = parseInt(dotFormatMatch[2], 10) - if (majorVersion > 4) return true - if (majorVersion === 4 && minorVersion >= 5) return true + if (majorVersion > 4) { + return true + } + if (majorVersion === 4 && minorVersion >= 5) { + return true + } return false } @@ -133,8 +145,12 @@ function isOpus45OrNewer(modelName) { const majorVersion = parseInt(versionMatch[1], 10) const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 - if (majorVersion > 4) return true - if (majorVersion === 4 && minorVersion >= 5) return true + if (majorVersion > 4) { + return true + } + if (majorVersion === 4 && minorVersion >= 5) { + return true + } return false } From 12cb841a64ec54d0384dcde53b444d6cf3f988fa Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 07:56:53 +0800 Subject: [PATCH 4/5] refactor: address Copilot review feedback - Import isOpus45OrNewer from modelHelper instead of duplicating code - Remove invalid 'claude_free' check (only 'free' is used in practice) --- scripts/test-official-models.js | 88 +------------------------- src/services/claudeAccountService.js | 4 +- src/services/unifiedClaudeScheduler.js | 2 +- 3 files changed, 4 insertions(+), 90 deletions(-) diff --git a/scripts/test-official-models.js b/scripts/test-official-models.js index f7046e5b..d87953fa 100644 --- a/scripts/test-official-models.js +++ b/scripts/test-official-models.js @@ -3,93 +3,7 @@ * 官方模型版本识别测试 - 最终版 v2 */ -/** - * 检查模型是否为 Opus 4.5 或更新版本 - * 支持格式: - * - 新格式: claude-opus-{major}[-{minor}][-date] 如 claude-opus-4-5-20251101 - * - 新格式: claude-opus-{major}.{minor} 如 claude-opus-4.5 - * - 旧格式: claude-{version}-opus[-date] 如 claude-3-opus-20240229 - * - * @param {string} modelName - 模型名称 - * @returns {boolean} - 是否为 Opus 4.5+ - */ -function isOpus45OrNewer(modelName) { - if (!modelName) { - return false - } - - const lowerModel = modelName.toLowerCase() - if (!lowerModel.includes('opus')) { - return false - } - - // 处理 latest 特殊情况 - if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) { - return true - } - - // 旧格式: claude-{version}-opus (版本在 opus 前面) - // 例如: claude-3-opus-20240229, claude-3.5-opus - const oldFormatMatch = lowerModel.match(/claude[- ](\d+)(?:[.-](\d+))?[- ]opus/) - if (oldFormatMatch) { - const majorVersion = parseInt(oldFormatMatch[1], 10) - const minorVersion = oldFormatMatch[2] ? parseInt(oldFormatMatch[2], 10) : 0 - - // 旧格式的版本号指的是 Claude 大版本 - if (majorVersion > 4) { - return true - } - if (majorVersion === 4 && minorVersion >= 5) { - return true - } - return false - } - - // 新格式 1: opus-{major}.{minor} (点分隔) - // 例如: claude-opus-4.5, opus-4.5 - const dotFormatMatch = lowerModel.match(/opus[- ]?(\d+)\.(\d+)/) - if (dotFormatMatch) { - const majorVersion = parseInt(dotFormatMatch[1], 10) - const minorVersion = parseInt(dotFormatMatch[2], 10) - - if (majorVersion > 4) { - return true - } - if (majorVersion === 4 && minorVersion >= 5) { - return true - } - return false - } - - // 新格式 2: opus-{major}[-{minor}][-date] (横线分隔) - // 例如: claude-opus-4-5-20251101, claude-opus-4-20250514, claude-opus-4-1-20250805 - // 关键:小版本号必须是 1 位数字,且后面紧跟 8 位日期或结束 - // 如果 opus-{major} 后面直接是 8 位日期,则没有小版本号 - - // 提取 opus 后面的部分 - const opusIndex = lowerModel.indexOf('opus') - const afterOpus = lowerModel.substring(opusIndex + 4) // 'opus' 后面的内容 - - // 尝试匹配: -{major}-{minor}-{date} 或 -{major}-{date} 或 -{major} - // 小版本号只能是 1 位数字 (如 1, 5),不会是 2 位以上 - const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d)(?=[- ]\d{8}|$))?/) - - if (versionMatch) { - const majorVersion = parseInt(versionMatch[1], 10) - const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 - - if (majorVersion > 4) { - return true - } - if (majorVersion === 4 && minorVersion >= 5) { - return true - } - return false - } - - // 其他包含 opus 但无法解析版本的情况,默认认为是旧版本 - return false -} +const { isOpus45OrNewer } = require('../src/utils/modelHelper') // 官方模型 const officialModels = [ diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index ec06e0ea..9b00958b 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -863,7 +863,7 @@ class ClaudeAccountService { const info = JSON.parse(account.subscriptionInfo) // Free 账号不支持任何 Opus 模型 - if (info.accountType === 'claude_free' || info.accountType === 'free') { + if (info.accountType === 'free') { return false } @@ -992,7 +992,7 @@ class ClaudeAccountService { const info = JSON.parse(account.subscriptionInfo) // Free 账号不支持任何 Opus 模型 - if (info.accountType === 'claude_free' || info.accountType === 'free') { + if (info.accountType === 'free') { return false } diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 99d81336..3a944180 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -58,7 +58,7 @@ class UnifiedClaudeScheduler { : account.subscriptionInfo // Free 账号不支持任何 Opus 模型 - if (info.accountType === 'claude_free' || info.accountType === 'free') { + if (info.accountType === 'free') { logger.info( `🚫 Claude account ${account.name} (Free) does not support Opus model${context ? ` ${context}` : ''}` ) From 06b18b718605ab0a040736b000421e12fb19083e Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 08:12:51 +0800 Subject: [PATCH 5/5] refactor: extract isProAccount helper for Pro account detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract duplicate Pro account detection logic into a reusable helper function that handles both API-returned (hasClaudePro) and locally configured (accountType) data sources. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/services/claudeAccountService.js | 29 ++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 9b00958b..10e6e86a 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -18,6 +18,21 @@ const LRUCache = require('../utils/lruCache') const { formatDateWithTimezone, getISOStringWithTimezone } = require('../utils/dateHelper') const { isOpus45OrNewer } = require('../utils/modelHelper') +/** + * 判断账号是否为 Pro 账号(非 Max) + * 兼容两种数据来源:API 实时返回的 hasClaudePro 和本地配置的 accountType + * @param {Object} info - 订阅信息对象 + * @returns {boolean} + */ +function isProAccount(info) { + // API 返回的实时状态优先 + if (info.hasClaudePro === true && info.hasClaudeMax !== true) { + return true + } + // 本地配置的账户类型 + return info.accountType === 'claude_pro' +} + class ClaudeAccountService { constructor() { this.claudeApiUrl = 'https://console.anthropic.com/v1/oauth/token' @@ -868,11 +883,8 @@ class ClaudeAccountService { } // Pro 账号:仅支持 Opus 4.5+ - if (info.hasClaudePro === true && info.hasClaudeMax !== true) { - return isNewOpus // 仅新版 Opus 支持 - } - if (info.accountType === 'claude_pro') { - return isNewOpus // 仅新版 Opus 支持 + if (isProAccount(info)) { + return isNewOpus } // Max 账号支持所有 Opus 版本 @@ -997,11 +1009,8 @@ class ClaudeAccountService { } // Pro 账号:仅支持 Opus 4.5+ - if (info.hasClaudePro === true && info.hasClaudeMax !== true) { - return isNewOpus // 仅新版 Opus 支持 - } - if (info.accountType === 'claude_pro') { - return isNewOpus // 仅新版 Opus 支持 + if (isProAccount(info)) { + return isNewOpus } // Max 账号支持所有 Opus 版本