fix(oauth): 优化自动关联凭证逻辑以支持单个凭证关联

- 修改 autoLinkProviderConfigs 函数,增加 onlyCurrentCred 选项
- 当 onlyCurrentCred 为 true 时,仅关联当前生成的凭证文件
- 避免批量导入凭证时重复扫描所有配置文件
- 在 OAuth 回调中传递 credPath 参数,确保正确关联新凭证
- 统一 install-and-run 脚本中的包管理器检测逻辑
- 优化 Claude 提供商的 token 计数方法,提高准确性
This commit is contained in:
hex2077 2026-02-03 12:15:35 +08:00
parent d6c2bd7919
commit 3a54404e0e
11 changed files with 5014 additions and 118 deletions

View file

@ -18,9 +18,9 @@ echo.
if !FORCE_PULL! equ 1 ( if !FORCE_PULL! equ 1 (
echo [更新] 正在从远程仓库拉取最新代码... echo [更新] 正在从远程仓库拉取最新代码...
git --version >nul 2>&1 git --version >nul 2>&1
if !errorlevel! equ 0 ( if %errorlevel% equ 0 (
git pull git pull
if !errorlevel! neq 0 ( if %errorlevel% neq 0 (
echo [警告] Git pull 失败,请检查网络或手动处理冲突。 echo [警告] Git pull 失败,请检查网络或手动处理冲突。
) else ( ) else (
echo [成功] 代码已更新。 echo [成功] 代码已更新。
@ -55,14 +55,22 @@ if not exist "package.json" (
echo [成功] 找到package.json文件 echo [成功] 找到package.json文件
echo [安装] 正在安装/更新依赖... :: 检查 pnpm 是否安装
where pnpm >nul 2>&1
if %errorlevel% equ 0 (
set PKG_MANAGER=pnpm
) else (
set PKG_MANAGER=npm
)
echo [安装] 正在使用 !PKG_MANAGER! 安装/更新依赖...
echo 这可能需要几分钟时间,请耐心等待... echo 这可能需要几分钟时间,请耐心等待...
echo 正在执行: npm install... echo 正在执行: !PKG_MANAGER! install...
:: 使用npm install并设置超时机制
call npm install --timeout=300000 call !PKG_MANAGER! install
if !errorlevel! neq 0 ( if %errorlevel% neq 0 (
echo [错误] 依赖安装失败 echo [错误] 依赖安装失败
echo 请检查网络连接或手动运行 'npm install' echo 请检查网络连接或手动运行 '!PKG_MANAGER! install'
pause pause
exit /b 1 exit /b 1
) )
@ -89,4 +97,4 @@ echo 按 Ctrl+C 停止服务器
echo. echo.
:: 启动服务器 :: 启动服务器
node src\core\master.js node src\core\master.js

View file

@ -64,13 +64,21 @@ fi
echo "[成功] 找到package.json文件" echo "[成功] 找到package.json文件"
echo "[安装] 正在安装/更新依赖..." # 检查 pnpm 是否安装
if command -v pnpm > /dev/null 2>&1; then
PKG_MANAGER=pnpm
else
PKG_MANAGER=npm
fi
echo "[安装] 正在使用 $PKG_MANAGER 安装/更新依赖..."
echo "这可能需要几分钟时间,请耐心等待..." echo "这可能需要几分钟时间,请耐心等待..."
echo "正在执行: npm install..." echo "正在执行: $PKG_MANAGER install..."
npm install
$PKG_MANAGER install
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "[错误] 依赖安装失败" echo "[错误] 依赖安装失败"
echo "请检查网络连接或运行 'npm install' 手动安装" echo "请检查网络连接或手动运行 '$PKG_MANAGER install'"
exit 1 exit 1
fi fi
echo "[成功] 依赖安装/更新完成" echo "[成功] 依赖安装/更新完成"

4766
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -740,7 +740,10 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: credentials.relativePath
});
logger.info('[Codex Auth] OAuth flow completed successfully'); logger.info('[Codex Auth] OAuth flow completed successfully');
} catch (error) { } catch (error) {
@ -844,7 +847,10 @@ export async function handleCodexOAuthCallback(code, state) {
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: result.relativePath
});
logger.info('[Codex Auth] OAuth callback processed successfully'); logger.info('[Codex Auth] OAuth callback processed successfully');

View file

@ -150,7 +150,10 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: relativePath
});
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '您可以关闭此页面')); res.end(generateResponsePage(true, '您可以关闭此页面'));

View file

@ -375,7 +375,10 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
}); });
// 6. 自动关联新生成的凭据到 Pools // 6. 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: relativePath
});
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`)); res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`));

View file

@ -413,7 +413,10 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: path.relative(process.cwd(), credPath)
});
return tokenData; return tokenData;
} }
@ -594,7 +597,10 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: path.relative(process.cwd(), credPath)
});
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '授权成功!您可以关闭此页面')); res.end(generateResponsePage(true, '授权成功!您可以关闭此页面'));
@ -846,7 +852,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools(批量导入时扫描所有)
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG);
} }
@ -979,7 +985,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region =
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools(批量导入时扫描所有)
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG);
} }
@ -1101,7 +1107,10 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: relativePath
});
return { return {
success: true, success: true,

View file

@ -212,7 +212,10 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
}); });
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG, {
onlyCurrentCred: true,
credPath: relativePath
});
return data; return data;
} }

View file

@ -711,6 +711,30 @@ async saveCredentialsToFile(filePath, newData) {
return String(message.content || message); return String(message.content || message);
} }
/**
* 统一处理内容将不同格式的内容转换为文本
* @param {any} content - 内容对象或数组
* @returns {string} 处理后的文本
*/
processContent(content) {
if (!content) return "";
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content.map(part => {
if (typeof part === 'string') return part;
if (part && typeof part === 'object') {
if (part.type === 'text') return part.text || "";
if (part.type === 'thinking') return part.thinking || part.text || "";
if (part.type === 'tool_result') return this.processContent(part.content);
if (part.type === 'tool_use' && part.input) return JSON.stringify(part.input);
if (part.text) return part.text;
}
return "";
}).join("");
}
return this.getContentText(content);
}
_normalizeThinkingBudgetTokens(budgetTokens) { _normalizeThinkingBudgetTokens(budgetTokens) {
let value = Number(budgetTokens); let value = Number(budgetTokens);
if (!Number.isFinite(value) || value <= 0) { if (!Number.isFinite(value) || value <= 0) {
@ -2366,52 +2390,34 @@ async saveCredentialsToFile(filePath, newData) {
* Calculate input tokens from request body using Claude's official tokenizer * Calculate input tokens from request body using Claude's official tokenizer
*/ */
estimateInputTokens(requestBody) { estimateInputTokens(requestBody) {
let totalTokens = 0; let allText = "";
// Count system prompt tokens // Count system prompt tokens
if (requestBody.system) { if (requestBody.system) {
const systemText = this.getContentText(requestBody.system); allText += this.processContent(requestBody.system);
totalTokens += this.countTextTokens(systemText);
} }
// Count thinking prefix tokens if thinking is enabled // Count thinking prefix tokens if thinking is enabled
if (requestBody.thinking?.type === 'enabled') { if (requestBody.thinking?.type === 'enabled') {
const budget = this._normalizeThinkingBudgetTokens(requestBody.thinking.budget_tokens); const budget = this._normalizeThinkingBudgetTokens(requestBody.thinking.budget_tokens);
const prefixText = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>`; allText += `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>`;
totalTokens += this.countTextTokens(prefixText);
} }
// Count all messages tokens // Count all messages tokens
if (requestBody.messages && Array.isArray(requestBody.messages)) { if (requestBody.messages && Array.isArray(requestBody.messages)) {
for (const message of requestBody.messages) { for (const message of requestBody.messages) {
if (message.content) { if (message.content) {
if (Array.isArray(message.content)) { allText += this.processContent(message.content);
for (const part of message.content) {
if (part.type === 'text' && part.text) {
totalTokens += this.countTextTokens(part.text);
} else if (part.type === 'thinking' && part.thinking) {
totalTokens += this.countTextTokens(part.thinking);
} else if (part.type === 'tool_result') {
const resultContent = this.getContentText(part.content);
totalTokens += this.countTextTokens(resultContent);
} else if (part.type === 'tool_use' && part.input) {
totalTokens += this.countTextTokens(JSON.stringify(part.input));
}
}
} else {
const contentText = this.getContentText(message);
totalTokens += this.countTextTokens(contentText);
}
} }
} }
} }
// Count tools definitions tokens if present // Count tools definitions tokens if present
if (requestBody.tools && Array.isArray(requestBody.tools)) { if (requestBody.tools && Array.isArray(requestBody.tools)) {
totalTokens += this.countTextTokens(JSON.stringify(requestBody.tools)); allText += JSON.stringify(requestBody.tools);
} }
return totalTokens; return this.countTextTokens(allText);
} }
/** /**
@ -2659,45 +2665,37 @@ async saveCredentialsToFile(filePath, newData) {
* @returns {Object} { input_tokens: number } * @returns {Object} { input_tokens: number }
*/ */
countTokens(requestBody) { countTokens(requestBody) {
let totalTokens = 0; let allText = "";
let extraTokens = 0;
// Count system prompt tokens // Count system prompt tokens
if (requestBody.system) { if (requestBody.system) {
const systemText = this.getContentText(requestBody.system); allText += this.processContent(requestBody.system);
totalTokens += this.countTextTokens(systemText);
} }
// Count all messages tokens // Count all messages tokens
if (requestBody.messages && Array.isArray(requestBody.messages)) { if (requestBody.messages && Array.isArray(requestBody.messages)) {
for (const message of requestBody.messages) { for (const message of requestBody.messages) {
if (message.content) { if (message.content) {
if (typeof message.content === 'string') { if (Array.isArray(message.content)) {
totalTokens += this.countTextTokens(message.content);
} else if (Array.isArray(message.content)) {
for (const block of message.content) { for (const block of message.content) {
if (block.type === 'text' && block.text) { if (block.type === 'image') {
totalTokens += this.countTextTokens(block.text);
} else if (block.type === 'tool_use') {
// Count tool use block tokens
totalTokens += this.countTextTokens(block.name || '');
totalTokens += this.countTextTokens(JSON.stringify(block.input || {}));
} else if (block.type === 'tool_result') {
// Count tool result block tokens
const resultContent = this.getContentText(block.content);
totalTokens += this.countTextTokens(resultContent);
} else if (block.type === 'image') {
// Images have a fixed token cost (approximately 1600 tokens for a typical image) // Images have a fixed token cost (approximately 1600 tokens for a typical image)
// This is an estimation as actual cost depends on image size // This is an estimation as actual cost depends on image size
totalTokens += 1600; extraTokens += 1600;
} else if (block.type === 'document') { } else if (block.type === 'document') {
// Documents - estimate based on content if available // Documents - estimate based on content if available
if (block.source?.data) { if (block.source?.data) {
// For base64 encoded documents, estimate tokens // For base64 encoded documents, estimate tokens
const estimatedChars = block.source.data.length * 0.75; // base64 to bytes ratio const estimatedChars = block.source.data.length * 0.75; // base64 to bytes ratio
totalTokens += Math.ceil(estimatedChars / 4); extraTokens += Math.ceil(estimatedChars / 4);
} }
} else {
allText += this.processContent([block]);
} }
} }
} else {
allText += this.processContent(message.content);
} }
} }
} }
@ -2705,18 +2703,10 @@ async saveCredentialsToFile(filePath, newData) {
// Count tools definitions tokens if present // Count tools definitions tokens if present
if (requestBody.tools && Array.isArray(requestBody.tools)) { if (requestBody.tools && Array.isArray(requestBody.tools)) {
for (const tool of requestBody.tools) { allText += JSON.stringify(requestBody.tools);
// Count tool name and description
totalTokens += this.countTextTokens(tool.name || '');
totalTokens += this.countTextTokens(tool.description || '');
// Count input schema
if (tool.input_schema) {
totalTokens += this.countTextTokens(JSON.stringify(tool.input_schema));
}
}
} }
return { input_tokens: totalTokens }; return { input_tokens: this.countTextTokens(allText) + extraTokens };
} }
/** /**

View file

@ -20,9 +20,12 @@ let providerPoolManager = null;
/** /**
* 扫描 configs 目录并自动关联未关联的配置文件到对应的提供商 * 扫描 configs 目录并自动关联未关联的配置文件到对应的提供商
* @param {Object} config - 服务器配置对象 * @param {Object} config - 服务器配置对象
* @param {Object} options - 可选参数
* @param {boolean} options.onlyCurrentCred - true 只自动关联当前凭证
* @param {string} options.credPath - 当前凭证的路径 onlyCurrentCred true 时必需
* @returns {Promise<Object>} 更新后的 providerPools 对象 * @returns {Promise<Object>} 更新后的 providerPools 对象
*/ */
export async function autoLinkProviderConfigs(config) { export async function autoLinkProviderConfigs(config, options = {}) {
// 确保 providerPools 对象存在 // 确保 providerPools 对象存在
if (!config.providerPools) { if (!config.providerPools) {
config.providerPools = {}; config.providerPools = {};
@ -31,43 +34,52 @@ export async function autoLinkProviderConfigs(config) {
let totalNewProviders = 0; let totalNewProviders = 0;
const allNewProviders = {}; const allNewProviders = {};
// 遍历所有提供商映射 // 如果只关联当前凭证
for (const mapping of PROVIDER_MAPPINGS) { if (options.onlyCurrentCred && options.credPath) {
const configsPath = path.join(process.cwd(), 'configs', mapping.dirName); const result = await linkSingleCredential(config, options.credPath);
const { providerType, credPathKey, defaultCheckModel, displayName, needsProjectId } = mapping; if (result) {
totalNewProviders = 1;
// 确保提供商类型数组存在 allNewProviders[result.displayName] = [result.provider];
if (!config.providerPools[providerType]) {
config.providerPools[providerType] = [];
} }
} else {
// 检查目录是否存在 // 遍历所有提供商映射
if (!fs.existsSync(configsPath)) { for (const mapping of PROVIDER_MAPPINGS) {
continue; const configsPath = path.join(process.cwd(), 'configs', mapping.dirName);
} const { providerType, credPathKey, defaultCheckModel, displayName, needsProjectId } = mapping;
// 获取已关联的配置文件路径集合 // 确保提供商类型数组存在
const linkedPaths = new Set(); if (!config.providerPools[providerType]) {
for (const provider of config.providerPools[providerType]) { config.providerPools[providerType] = [];
if (provider[credPathKey]) { }
// 使用公共方法添加路径的所有变体格式
addToUsedPaths(linkedPaths, provider[credPathKey]); // 检查目录是否存在
if (!fs.existsSync(configsPath)) {
continue;
}
// 获取已关联的配置文件路径集合
const linkedPaths = new Set();
for (const provider of config.providerPools[providerType]) {
if (provider[credPathKey]) {
// 使用公共方法添加路径的所有变体格式
addToUsedPaths(linkedPaths, provider[credPathKey]);
}
}
// 递归扫描目录
const newProviders = [];
await scanProviderDirectory(configsPath, linkedPaths, newProviders, {
credPathKey,
defaultCheckModel,
needsProjectId
});
// 如果有新的配置文件需要关联
if (newProviders.length > 0) {
config.providerPools[providerType].push(...newProviders);
totalNewProviders += newProviders.length;
allNewProviders[displayName] = newProviders;
} }
}
// 递归扫描目录
const newProviders = [];
await scanProviderDirectory(configsPath, linkedPaths, newProviders, {
credPathKey,
defaultCheckModel,
needsProjectId
});
// 如果有新的配置文件需要关联
if (newProviders.length > 0) {
config.providerPools[providerType].push(...newProviders);
totalNewProviders += newProviders.length;
allNewProviders[displayName] = newProviders;
} }
} }
@ -104,6 +116,94 @@ export async function autoLinkProviderConfigs(config) {
return config.providerPools; return config.providerPools;
} }
/**
* 关联单个凭证文件到对应的提供商
* @param {Object} config - 服务器配置对象
* @param {string} credPath - 凭证文件路径相对或绝对路径
* @returns {Promise<Object|null>} 返回关联结果或 null
*/
async function linkSingleCredential(config, credPath) {
try {
// 规范化路径
const absolutePath = path.isAbsolute(credPath) ? credPath : path.join(process.cwd(), credPath);
const relativePath = path.relative(process.cwd(), absolutePath);
// 检查文件是否存在
if (!fs.existsSync(absolutePath)) {
logger.warn(`[Auto-Link] Credential file not found: ${relativePath}`);
return null;
}
// 检查文件扩展名
const ext = path.extname(absolutePath).toLowerCase();
if (ext !== '.json') {
logger.warn(`[Auto-Link] Only JSON files are supported: ${relativePath}`);
return null;
}
// 根据文件路径确定提供商类型
let matchedMapping = null;
for (const mapping of PROVIDER_MAPPINGS) {
const configsPath = path.join(process.cwd(), 'configs', mapping.dirName);
// 检查文件是否在该提供商的配置目录下
if (absolutePath.startsWith(configsPath)) {
matchedMapping = mapping;
break;
}
}
if (!matchedMapping) {
logger.warn(`[Auto-Link] Could not determine provider type for: ${relativePath}`);
return null;
}
const { providerType, credPathKey, defaultCheckModel, displayName, needsProjectId } = matchedMapping;
// 确保提供商类型数组存在
if (!config.providerPools[providerType]) {
config.providerPools[providerType] = [];
}
// 检查是否已关联
const linkedPaths = new Set();
for (const provider of config.providerPools[providerType]) {
if (provider[credPathKey]) {
addToUsedPaths(linkedPaths, provider[credPathKey]);
}
}
const fileName = getFileName(absolutePath);
const isLinked = isPathUsed(relativePath, fileName, linkedPaths);
if (isLinked) {
logger.info(`[Auto-Link] Credential already linked: ${relativePath}`);
return null;
}
// 创建新的提供商配置
const newProvider = createProviderConfig({
credPathKey,
credPath: formatSystemPath(relativePath),
defaultCheckModel,
needsProjectId
});
// 添加到配置
config.providerPools[providerType].push(newProvider);
logger.info(`[Auto-Link] Successfully linked credential: ${relativePath} to ${displayName}`);
return {
provider: newProvider,
displayName,
providerType
};
} catch (error) {
logger.error(`[Auto-Link] Failed to link credential ${credPath}: ${error.message}`);
return null;
}
}
/** /**
* 递归扫描提供商配置目录 * 递归扫描提供商配置目录
* @param {string} dirPath - 目录路径 * @param {string} dirPath - 目录路径

View file

@ -19,12 +19,12 @@
<label for="configProviderFilter" data-i18n="upload.providerFilter">提供商类型</label> <label for="configProviderFilter" data-i18n="upload.providerFilter">提供商类型</label>
<select id="configProviderFilter" class="form-control"> <select id="configProviderFilter" class="form-control">
<option value="" data-i18n="upload.providerFilter.all">全部提供商</option> <option value="" data-i18n="upload.providerFilter.all">全部提供商</option>
<option value="claude-kiro-oauth" data-i18n="upload.providerFilter.kiro">Kiro OAuth</option> <option value="gemini-cli-oauth" data-i18n="upload.providerFilter.gemini">Gemini CLI OAuth</option>
<option value="gemini-cli-oauth" data-i18n="upload.providerFilter.gemini">Gemini OAuth</option> <option value="gemini-antigravity" data-i18n="upload.providerFilter.antigravity">Gemini Antigravity</option>
<option value="openai-qwen-oauth" data-i18n="upload.providerFilter.qwen">Qwen OAuth</option> <option value="claude-kiro-oauth" data-i18n="upload.providerFilter.kiro">Claude Kiro OAuth</option>
<option value="gemini-antigravity" data-i18n="upload.providerFilter.antigravity">Antigravity</option> <option value="openai-qwen-oauth" data-i18n="upload.providerFilter.qwen">OpenAI Qwen OAuth</option>
<option value="openai-codex-oauth" data-i18n="upload.providerFilter.codex">Codex OAuth</option> <option value="openai-iflow" data-i18n="upload.providerFilter.iflow">OpenAI iFlow</option>
<option value="openai-iflow-oauth" data-i18n="upload.providerFilter.iflow">iFlow OAuth</option> <option value="openai-codex-oauth" data-i18n="upload.providerFilter.codex">OpenAI Codex OAuth</option>
<option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option> <option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option>
</select> </select>
</div> </div>