From b94bd2b8226662a698186e854c4e28be6e9b4588 Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 07:38:55 +0800 Subject: [PATCH 01/16] =?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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 版本 From 675e7b911143f22192697391a4eb28bbfcb122ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 00:15:42 +0000 Subject: [PATCH 06/16] chore: sync VERSION file with release v1.1.221 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 62242964..2f3faf60 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.220 +1.1.221 From 6ab91c0c75b0a2039031dc61770f476b63499a12 Mon Sep 17 00:00:00 2001 From: lusipad Date: Fri, 5 Dec 2025 08:25:42 +0800 Subject: [PATCH 07/16] chore: revert version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 2f3faf60..62242964 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.221 +1.1.220 From 530d38e4a417ac58cd08aa216cb7f6ea46f2ead2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 14:16:07 +0000 Subject: [PATCH 08/16] chore: sync VERSION file with release v1.1.223 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3e3c866e..33ba25d7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.222 +1.1.223 From c1c941aa4cec537f68d98c61e719b26236b6a750 Mon Sep 17 00:00:00 2001 From: lusipad Date: Sat, 6 Dec 2025 04:26:11 +0800 Subject: [PATCH 09/16] fix(opus): fix PR#762 review issues and add maintenance comments - Fix regex to support 2-digit minor versions (e.g., opus-4-10) - Prevent matching 8-digit dates as minor version numbers - Unify English comments for consistency across codebase - Extract isProAccount() helper to eliminate code duplication - Add detailed version logic comments for future maintenance Changes: - VERSION LOGIC: Opus 4.5+ returns true (Pro eligible), <4.5 returns false (Max only) - ACCOUNT RESTRICTIONS: Free=no Opus, Pro=Opus 4.5+, Max=all Opus versions - REGEX FIX: (\d{1,2}) limits minor version to 1-2 digits, avoiding date confusion Test: All 21 tests pass Format: Prettier validated --- src/services/unifiedClaudeScheduler.js | 57 +++++++++++++++------- src/utils/modelHelper.js | 67 +++++++++++++++++--------- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 3a944180..3f946402 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -7,6 +7,32 @@ const redis = require('../models/redis') const logger = require('../utils/logger') const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../utils/modelHelper') +/** + * Check if account is Pro (not Max) + * + * ACCOUNT TYPE判断逻辑 (2025-12-05): + * Pro accounts can be identified by either: + * 1. API real-time data: hasClaudePro=true && hasClaudeMax=false + * 2. Local config data: accountType='claude_pro' + * + * Account type restrictions for Opus models: + * - Free account: No Opus access at all + * - Pro account: Only Opus 4.5+ (new versions) + * - Max account: All Opus versions (legacy 3.x, 4.0, 4.1 and new 4.5+) + * + * Compatible with both API real-time data (hasClaudePro) and local config (accountType) + * @param {Object} info - Subscription info object + * @returns {boolean} - true if Pro account (not Free, not Max) + */ +function isProAccount(info) { + // API real-time status takes priority + if (info.hasClaudePro === true && info.hasClaudeMax !== true) { + return true + } + // Local configured account type + return info.accountType === 'claude_pro' +} + class UnifiedClaudeScheduler { constructor() { this.SESSION_MAPPING_PREFIX = 'unified_claude_session_mapping:' @@ -46,7 +72,11 @@ class UnifiedClaudeScheduler { return false } - // 2. Opus 模型的订阅级别检查 + // 2. Opus model subscription level check + // VERSION RESTRICTION LOGIC: + // - Free: No Opus models + // - Pro: Only Opus 4.5+ (isOpus45OrNewer = true) + // - Max: All Opus versions if (requestedModel.toLowerCase().includes('opus')) { const isNewOpus = isOpus45OrNewer(requestedModel) @@ -57,7 +87,7 @@ class UnifiedClaudeScheduler { ? JSON.parse(account.subscriptionInfo) : account.subscriptionInfo - // Free 账号不支持任何 Opus 模型 + // Free account: does not support any Opus model if (info.accountType === 'free') { logger.info( `🚫 Claude account ${account.name} (Free) does not support Opus model${context ? ` ${context}` : ''}` @@ -65,37 +95,28 @@ class UnifiedClaudeScheduler { return false } - // Pro 账号:仅支持 Opus 4.5+ - if (info.hasClaudePro === true && info.hasClaudeMax !== true) { + // Pro account: only supports Opus 4.5+ + // Reject legacy Opus (3.x, 4.0-4.4) but allow new Opus (4.5+) + if (isProAccount(info)) { 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') { - if (!isNewOpus) { - logger.info( - `🚫 Claude account ${account.name} (Pro) does not support legacy Opus model${context ? ` ${context}` : ''}` - ) - return false - } - // Opus 4.5+ 支持 + // Opus 4.5+ supported return true } - // Max 账号支持所有 Opus 版本 + // Max account: supports all Opus versions (no restriction) } catch (e) { - // 解析失败,假设为旧数据(Max),默认支持 + // Parse failed, assume legacy data (Max), default support logger.debug( `Account ${account.name} has invalid subscriptionInfo${context ? ` ${context}` : ''}, assuming Max` ) } } - // 没有订阅信息的账号,默认当作支持(兼容旧数据) + // Account without subscription info, default to supported (legacy data compatibility) } } diff --git a/src/utils/modelHelper.js b/src/utils/modelHelper.js index d27fea87..31c514c7 100644 --- a/src/utils/modelHelper.js +++ b/src/utils/modelHelper.js @@ -71,14 +71,20 @@ function getVendorType(modelStr) { } /** - * 检查模型是否为 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 + * Check if the model is Opus 4.5 or newer. * - * @param {string} modelName - 模型名称 - * @returns {boolean} - 是否为 Opus 4.5+ + * VERSION判断逻辑 (2025-12-05): + * - Opus 4.5+ (包括 5.0, 6.0 等) → 返回 true (Pro 账号可用) + * - Opus 4.4 及以下 (包括 3.x, 4.0, 4.1) → 返回 false (仅 Max 账号可用) + * + * 支持的命名格式: + * - New format: claude-opus-{major}[-{minor}][-date], e.g., claude-opus-4-5-20251101 + * - New format: claude-opus-{major}.{minor}, e.g., claude-opus-4.5 + * - Old format: claude-{version}-opus[-date], e.g., claude-3-opus-20240229 + * - Special: opus-latest, claude-opus-latest → always returns true + * + * @param {string} modelName - Model name + * @returns {boolean} - Whether the model is Opus 4.5 or newer */ function isOpus45OrNewer(modelName) { if (!modelName) { @@ -90,19 +96,22 @@ function isOpus45OrNewer(modelName) { return false } - // 处理 latest 特殊情况 + // Handle 'latest' special case if (lowerModel.includes('opus-latest') || lowerModel.includes('opus_latest')) { return true } - // 旧格式: claude-{version}-opus (版本在 opus 前面) - // 例如: claude-3-opus-20240229, claude-3.5-opus + // Old format: claude-{version}-opus (version before opus) + // e.g., 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 大版本 + // Old format version refers to Claude major version + // majorVersion > 4: 5.x, 6.x, ... → true + // majorVersion === 4 && minorVersion >= 5: 4.5, 4.6, ... → true + // Others (3.x, 4.0-4.4): → false if (majorVersion > 4) { return true } @@ -112,13 +121,17 @@ function isOpus45OrNewer(modelName) { return false } - // 新格式 1: opus-{major}.{minor} (点分隔) - // 例如: claude-opus-4.5, opus-4.5 + // New format 1: opus-{major}.{minor} (dot-separated) + // e.g., 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) + // Same version logic as old format + // opus-5.0, opus-6.0 → true + // opus-4.5, opus-4.6 → true + // opus-4.0, opus-4.4 → false if (majorVersion > 4) { return true } @@ -128,23 +141,31 @@ function isOpus45OrNewer(modelName) { 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 位日期,则没有小版本号 + // New format 2: opus-{major}[-{minor}][-date] (hyphen-separated) + // e.g., claude-opus-4-5-20251101, claude-opus-4-20250514, claude-opus-4-1-20250805 + // If opus-{major} is followed by 8-digit date, there's no minor version - // 提取 opus 后面的部分 + // Extract content after 'opus' const opusIndex = lowerModel.indexOf('opus') - const afterOpus = lowerModel.substring(opusIndex + 4) // 'opus' 后面的内容 + const afterOpus = lowerModel.substring(opusIndex + 4) - // 尝试匹配: -{major}-{minor}-{date} 或 -{major}-{date} 或 -{major} - // 小版本号只能是 1 位数字 (如 1, 5),不会是 2 位以上 - const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d)(?=[- ]\d{8}|$))?/) + // Match: -{major}-{minor}-{date} or -{major}-{date} or -{major} + // IMPORTANT: Minor version regex is (\d{1,2}) not (\d+) + // This prevents matching 8-digit dates as minor version + // Example: opus-4-20250514 → major=4, minor=undefined (not 20250514) + // Example: opus-4-5-20251101 → major=4, minor=5 + // Future-proof: Supports up to 2-digit minor versions (0-99) + const versionMatch = afterOpus.match(/^[- ](\d+)(?:[- ](\d{1,2})(?=[- ]\d{8}|$))?/) if (versionMatch) { const majorVersion = parseInt(versionMatch[1], 10) const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 + // Same version logic: >= 4.5 returns true + // opus-5-0-date, opus-6-date → true + // opus-4-5-date, opus-4-10-date → true (supports 2-digit minor) + // opus-4-date (no minor, treated as 4.0) → false + // opus-4-1-date, opus-4-4-date → false if (majorVersion > 4) { return true } @@ -154,7 +175,7 @@ function isOpus45OrNewer(modelName) { return false } - // 其他包含 opus 但无法解析版本的情况,默认认为是旧版本 + // Other cases containing 'opus' but cannot parse version, assume legacy return false } From 84a8fdeabad4919362ac371f20f3165a2f7d69a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 20:26:31 +0000 Subject: [PATCH 10/16] chore: sync VERSION file with release v1.1.224 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 33ba25d7..80f39145 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.223 +1.1.224 From ea053c6a161dac646ddec3d734640abbea54dbc7 Mon Sep 17 00:00:00 2001 From: lusipad Date: Sat, 6 Dec 2025 04:42:45 +0800 Subject: [PATCH 11/16] docs: convert Chinese comments to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change VERSION判断逻辑 to VERSION LOGIC - Change ACCOUNT TYPE判断逻辑 to ACCOUNT TYPE LOGIC - Translate remaining Chinese phrases to English - Keep all logic unchanged, only translation --- src/services/unifiedClaudeScheduler.js | 2 +- src/utils/modelHelper.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 3f946402..73a4bdf9 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -10,7 +10,7 @@ const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../utils/modelHel /** * Check if account is Pro (not Max) * - * ACCOUNT TYPE判断逻辑 (2025-12-05): + * ACCOUNT TYPE LOGIC (as of 2025-12-05): * Pro accounts can be identified by either: * 1. API real-time data: hasClaudePro=true && hasClaudeMax=false * 2. Local config data: accountType='claude_pro' diff --git a/src/utils/modelHelper.js b/src/utils/modelHelper.js index 31c514c7..a42ee317 100644 --- a/src/utils/modelHelper.js +++ b/src/utils/modelHelper.js @@ -73,11 +73,11 @@ function getVendorType(modelStr) { /** * Check if the model is Opus 4.5 or newer. * - * VERSION判断逻辑 (2025-12-05): - * - Opus 4.5+ (包括 5.0, 6.0 等) → 返回 true (Pro 账号可用) - * - Opus 4.4 及以下 (包括 3.x, 4.0, 4.1) → 返回 false (仅 Max 账号可用) + * VERSION LOGIC (as of 2025-12-05): + * - Opus 4.5+ (including 5.0, 6.0, etc.) → returns true (Pro account eligible) + * - Opus 4.4 and below (including 3.x, 4.0, 4.1) → returns false (Max account only) * - * 支持的命名格式: + * Supported naming formats: * - New format: claude-opus-{major}[-{minor}][-date], e.g., claude-opus-4-5-20251101 * - New format: claude-opus-{major}.{minor}, e.g., claude-opus-4.5 * - Old format: claude-{version}-opus[-date], e.g., claude-3-opus-20240229 From cfdcc97cc71913746c11a13380fc26f34f35ffda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 20:43:32 +0000 Subject: [PATCH 12/16] chore: sync VERSION file with release v1.1.225 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 80f39145..8df909f5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.224 +1.1.225 From 10a1d61427f28034054366fa8889c7e447ba6c2a Mon Sep 17 00:00:00 2001 From: lusipad Date: Sat, 6 Dec 2025 04:44:39 +0800 Subject: [PATCH 13/16] docs: translate remaining Chinese comments in claudeAccountService.js - Filter Opus models based on account type and model version - Free account: does not support any Opus model - Pro account: only supports Opus 4.5+ - Max account: supports all Opus versions - Account without subscription info defaults to supported All logic unchanged, only comment translation. --- src/services/claudeAccountService.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 10e6e86a..a7abe48e 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -868,7 +868,7 @@ class ClaudeAccountService { !this.isSubscriptionExpired(account) ) - // 如果请求的是 Opus 模型,根据账号类型和模型版本过滤 + // Filter Opus models based on account type and model version if (modelName && modelName.toLowerCase().includes('opus')) { const isNewOpus = isOpus45OrNewer(modelName) @@ -877,24 +877,24 @@ class ClaudeAccountService { try { const info = JSON.parse(account.subscriptionInfo) - // Free 账号不支持任何 Opus 模型 + // Free account: does not support any Opus model if (info.accountType === 'free') { return false } - // Pro 账号:仅支持 Opus 4.5+ + // Pro account: only supports Opus 4.5+ if (isProAccount(info)) { return isNewOpus } - // Max 账号支持所有 Opus 版本 + // Max account: supports all Opus versions return true } catch (e) { - // 解析失败,假设为旧数据(Max),默认支持 + // Parse failed, assume legacy data (Max), default support return true } } - // 没有订阅信息的账号,默认当作支持(兼容旧数据) + // Account without subscription info, default to supported (legacy data compatibility) return true }) @@ -994,7 +994,7 @@ class ClaudeAccountService { !this.isSubscriptionExpired(account) ) - // 如果请求的是 Opus 模型,根据账号类型和模型版本过滤 + // Filter Opus models based on account type and model version if (modelName && modelName.toLowerCase().includes('opus')) { const isNewOpus = isOpus45OrNewer(modelName) @@ -1003,24 +1003,24 @@ class ClaudeAccountService { try { const info = JSON.parse(account.subscriptionInfo) - // Free 账号不支持任何 Opus 模型 + // Free account: does not support any Opus model if (info.accountType === 'free') { return false } - // Pro 账号:仅支持 Opus 4.5+ + // Pro account: only supports Opus 4.5+ if (isProAccount(info)) { return isNewOpus } - // Max 账号支持所有 Opus 版本 + // Max account: supports all Opus versions return true } catch (e) { - // 解析失败,假设为旧数据(Max),默认支持 + // Parse failed, assume legacy data (Max), default support return true } } - // 没有订阅信息的账号,默认当作支持(兼容旧数据) + // Account without subscription info, default to supported (legacy data compatibility) return true }) From 065aa6d35e5135f868576ba9f42b84b5f38d01dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 20:45:07 +0000 Subject: [PATCH 14/16] chore: sync VERSION file with release v1.1.226 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8df909f5..40ff1f13 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.225 +1.1.226 From 849d8e047b7bc96c47a126c5b2514dd5e4034476 Mon Sep 17 00:00:00 2001 From: lusipad Date: Sat, 6 Dec 2025 04:54:18 +0800 Subject: [PATCH 15/16] docs: translate isProAccount function comments to English - Change function description from Chinese to English - Translate inline comments (API priority, local config) - Keep function logic unchanged This completes the full English comment translation for all modified files. --- src/services/claudeAccountService.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index a7abe48e..77630364 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -19,17 +19,17 @@ const { formatDateWithTimezone, getISOStringWithTimezone } = require('../utils/d const { isOpus45OrNewer } = require('../utils/modelHelper') /** - * 判断账号是否为 Pro 账号(非 Max) - * 兼容两种数据来源:API 实时返回的 hasClaudePro 和本地配置的 accountType - * @param {Object} info - 订阅信息对象 + * Check if account is Pro (not Max) + * Compatible with both API real-time data (hasClaudePro) and local config (accountType) + * @param {Object} info - Subscription info object * @returns {boolean} */ function isProAccount(info) { - // API 返回的实时状态优先 + // API real-time status takes priority if (info.hasClaudePro === true && info.hasClaudeMax !== true) { return true } - // 本地配置的账户类型 + // Local configured account type return info.accountType === 'claude_pro' } From c70070d9126fb1337870e2ac1f29136dae70d246 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Dec 2025 20:54:53 +0000 Subject: [PATCH 16/16] chore: sync VERSION file with release v1.1.227 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 40ff1f13..e58c88de 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.226 +1.1.227