feat: enhance API key management and fix routing logic
* ``` feat(accounts): 支持批量添加API密钥连接 - 添加accessTokens字段到账户创建载荷模式,允许传递多个API密钥 - 实现批量API密钥解析功能,支持换行符、空格和逗号分隔 - 重构账户创建逻辑以支持单个和批量API密钥处理 - 在前端界面中添加批量API密钥输入检测和处理 - 添加批量创建结果返回和错误处理 - 更新验证流程以支持批量模式下的逐条验证 - 添加详细的批量API密钥测试用例 该更改允许用户一次提交多个API密钥,系统将为每个密钥创建独立的连接并参与负载均衡。 ``` * ``` fix(tokenRouter): 调整路由匹配优先级以优先匹配显示名称别名 当精确路由和显式组显示名称冲突时,现在优先选择显式组显示名称 而不是精确路由模式。这修正了路由决策逻辑,使显式组路由能够正 确覆盖精确模型匹配。 相关的测试用例已更新以反映新的匹配行为,确保显式组路由在冲 突情况下获得更高优先级。 ``` * ``` fix(accounts): 修复API密钥验证错误消息显示逻辑 当凭证模式不为apikey时显示会话令牌提示信息,否则显示API密钥验证失败信息 feat(accounts): 更新API密钥输入界面提示文本 将批量添加说明从输入框占位符移至独立提示文本, 使界面更清晰易懂 ``` * ``` feat(workflow): 在CI/CD工作流中使用动态Docker Hub镜像名称 - 在ci.yml和release.yml工作流中添加DOCKERHUB_IMAGE环境变量 - 将硬编码的Docker Hub用户名替换为从secrets.DOCKERHUB_USERNAME动态获取 - 更新docker.metadata-action配置以使用环境变量中的镜像名称 - 添加测试用例验证Docker Hub镜像名称的正确配置 ``` * fix(accounts): 修复批量创建回归并下沉创建流程
This commit is contained in:
@@ -29,6 +29,7 @@ import { buildCustomReorderUpdates, sortItemsForDisplay, type SortMode } from '.
|
||||
import { shouldIgnoreRowSelectionClick } from './helpers/rowSelection.js';
|
||||
import { SITE_DOCS_URL } from '../docsLink.js';
|
||||
import { getSiteInitializationPreset } from '../../shared/siteInitializationPresets.js';
|
||||
import { parseBatchApiKeys } from '../../shared/apiKeyBatch.js';
|
||||
|
||||
type ConnectionsSegment = 'session' | 'apikey' | 'tokens';
|
||||
|
||||
@@ -177,6 +178,11 @@ export default function Accounts() {
|
||||
() => sites.find((item) => item.id === tokenForm.siteId) || null,
|
||||
[sites, tokenForm.siteId],
|
||||
);
|
||||
const parsedApiKeys = useMemo(
|
||||
() => activeSegment === 'apikey' ? parseBatchApiKeys(tokenForm.accessToken) : [],
|
||||
[activeSegment, tokenForm.accessToken],
|
||||
);
|
||||
const isBatchApiKeyInput = activeSegment === 'apikey' && parsedApiKeys.length > 1;
|
||||
const siteSelectOptions = useMemo(
|
||||
() => [
|
||||
{ value: '0', label: '选择站点' },
|
||||
@@ -322,6 +328,10 @@ export default function Accounts() {
|
||||
|
||||
const handleVerifyToken = async () => {
|
||||
if (!tokenForm.siteId || !tokenForm.accessToken) return;
|
||||
if (isBatchApiKeyInput) {
|
||||
toast.info(`检测到 ${parsedApiKeys.length} 个 API Key,批量模式会在添加时逐条校验`);
|
||||
return;
|
||||
}
|
||||
const credentialMode = activeSegment === 'apikey' ? 'apikey' : 'session';
|
||||
setVerifying(true);
|
||||
setVerifyResult(null);
|
||||
@@ -352,7 +362,7 @@ export default function Accounts() {
|
||||
|
||||
const handleTokenAdd = async () => {
|
||||
if (!tokenForm.siteId || !tokenForm.accessToken) return;
|
||||
if (!verifyResult?.success && !tokenForm.skipModelFetch) {
|
||||
if (!isBatchApiKeyInput && !verifyResult?.success && !tokenForm.skipModelFetch) {
|
||||
toast.error('请先验证 Token 成功后再添加账号');
|
||||
return;
|
||||
}
|
||||
@@ -364,6 +374,7 @@ export default function Accounts() {
|
||||
siteId: tokenForm.siteId,
|
||||
username: tokenForm.username.trim() || undefined,
|
||||
accessToken: tokenForm.accessToken,
|
||||
accessTokens: isBatchApiKeyInput ? parsedApiKeys : undefined,
|
||||
platformUserId: tokenForm.platformUserId ? parseInt(tokenForm.platformUserId) : undefined,
|
||||
refreshToken: isSub2ApiSelected && tokenForm.refreshToken.trim()
|
||||
? tokenForm.refreshToken.trim()
|
||||
@@ -374,6 +385,23 @@ export default function Accounts() {
|
||||
credentialMode,
|
||||
skipModelFetch: tokenForm.skipModelFetch,
|
||||
});
|
||||
if (result?.batch) {
|
||||
closeAddPanel();
|
||||
const createdCount = Number(result.createdCount) || 0;
|
||||
const failedCount = Number(result.failedCount) || 0;
|
||||
if (createdCount > 0) {
|
||||
toast.success(`批量添加完成:成功 ${createdCount},失败 ${failedCount}`);
|
||||
}
|
||||
const failedItems = Array.isArray(result.items)
|
||||
? result.items.filter((item: any) => item?.status === 'failed')
|
||||
: [];
|
||||
if (failedItems.length > 0) {
|
||||
const firstMessage = failedItems[0]?.message || '创建失败';
|
||||
toast.error(`失败 ${failedItems.length} 条:${firstMessage}`);
|
||||
}
|
||||
load();
|
||||
return;
|
||||
}
|
||||
let seededRecommendedModels = false;
|
||||
const recommendedModels = initializationPreset?.recommendedModels || [];
|
||||
const createdAccountId = Number(result?.id) || 0;
|
||||
@@ -951,6 +979,9 @@ export default function Accounts() {
|
||||
|| (activeSegment === 'session' && verifyResult.tokenType === 'session')
|
||||
),
|
||||
);
|
||||
const canSubmitApiKeyConnection = activeSegment === 'apikey'
|
||||
? (isBatchApiKeyInput || canAddVerifiedConnection || !!tokenForm.skipModelFetch)
|
||||
: canAddVerifiedConnection;
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in">
|
||||
@@ -1421,9 +1452,18 @@ export default function Accounts() {
|
||||
<textarea
|
||||
placeholder="粘贴 API Key"
|
||||
value={tokenForm.accessToken}
|
||||
onChange={(e) => { setTokenForm((f) => ({ ...f, accessToken: e.target.value.trim(), credentialMode: 'apikey' })); setVerifyResult(null); }}
|
||||
onChange={(e) => { setTokenForm((f) => ({ ...f, accessToken: e.target.value, credentialMode: 'apikey' })); setVerifyResult(null); }}
|
||||
style={{ ...inputStyle, fontFamily: 'var(--font-mono)', height: 72, resize: 'none' as const }}
|
||||
/>
|
||||
{parsedApiKeys.length > 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||
已识别 {parsedApiKeys.length} 个 API Key
|
||||
{isBatchApiKeyInput ? ',添加时会逐条创建同站点连接并参与轮询' : ''}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||
支持换行、空格、逗号批量粘贴多个 API Key。
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<input
|
||||
placeholder="用户 ID(可选)"
|
||||
@@ -1479,23 +1519,25 @@ export default function Accounts() {
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
onClick={handleVerifyToken}
|
||||
disabled={verifying || !tokenForm.siteId || !tokenForm.accessToken}
|
||||
disabled={verifying || !tokenForm.siteId || !tokenForm.accessToken || isBatchApiKeyInput}
|
||||
className="btn btn-ghost"
|
||||
style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }}
|
||||
>
|
||||
{verifying ? <><span className="spinner spinner-sm" />验证中...</> : '验证 API Key'}
|
||||
{verifying ? <><span className="spinner spinner-sm" />验证中...</> : (isBatchApiKeyInput ? '批量添加时校验' : '验证 API Key')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleTokenAdd}
|
||||
disabled={saving || !tokenForm.siteId || !tokenForm.accessToken || (!canAddVerifiedConnection && !tokenForm.skipModelFetch)}
|
||||
disabled={saving || !tokenForm.siteId || !tokenForm.accessToken || !canSubmitApiKeyConnection}
|
||||
className="btn btn-success"
|
||||
>
|
||||
{saving ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} />添加中...</> : '添加连接'}
|
||||
{saving ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} />添加中...</> : (isBatchApiKeyInput ? '批量添加连接' : '添加连接')}
|
||||
</button>
|
||||
</div>
|
||||
{!verifyResult?.success && (
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||
{addAccountPrereqHint}
|
||||
{isBatchApiKeyInput
|
||||
? '批量模式下无需先点验证,提交后会逐条校验并创建。'
|
||||
: addAccountPrereqHint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user