fix(providers): bulk-add API keys no longer overwrite existing keys

Bulk-add named auto-generated keys by paste-line index, blind to existing
connection names. The backend upserts apikey connections by exact name
(connectionsRepo), so a colliding generated name silently replaced an
existing key instead of inserting a new one.

Add a collision-aware planner (src/shared/utils/bulkAdd.js) that gap-fills
the smallest free "<base> <n>" against both existing connection names and
names assigned earlier in the same batch, so a generated name is never
reused and the backend always inserts. Applies to auto-named lines, custom
name|apiKey lines, and Cloudflare name|apiKey|accountId lines.

Wire the planner into AddApiKeyModal and pass existing connection names
from the provider detail page. Add unit tests covering gap-fill, custom
names, Cloudflare 3-part format, and robustness.
This commit is contained in:
asynx6
2026-07-16 17:00:37 +07:00
committed by decolua
parent 6acc3bb965
commit de680e789f
7 changed files with 228 additions and 22 deletions

View File

@@ -4,10 +4,11 @@ import { useState } from "react";
import PropTypes from "prop-types";
import { Button, Badge, Input, Modal, Select } from "@/shared/components";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { planBulkAdd } from "@/shared/utils/bulkAdd";
const BULK_PLACEHOLDER = `name1|sk-key1\nname2|sk-key2\nsk-key-only-auto-named`;
export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, authType, authHint, website, proxyPools, error, onSave, onBulkDone, onClose }) {
export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, authType, authHint, website, proxyPools, error, existingNames, onSave, onBulkDone, onClose }) {
const NONE_PROXY_POOL_VALUE = "__none__";
const isOllamaLocal = provider === "ollama-local";
const isCookie = authType === "cookie";
@@ -131,38 +132,29 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
};
const handleBulkSubmit = async () => {
const lines = bulkText.split("\n").map(l => l.trim()).filter(Boolean);
const lines = bulkText.split("\n");
if (!lines.length) return;
// Plan collision-free names against existing connections so a generated
// "Key N" never matches a saved name (which the backend would upsert /
// overwrite instead of inserting). See bulkAdd.js for the full rationale.
const plan = planBulkAdd(lines, existingNames, { isCloudflareAi });
if (!plan.length) return;
setSaving(true);
setBulkResult(null);
let success = 0;
let failed = 0;
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split("|");
const baseName = parts.length >= 2 ? parts[0].trim() : "Key";
const name = `${baseName} ${i + 1}`;
let apiKey;
let providerSpecificData;
if (isCloudflareAi && parts.length >= 3) {
// Format: name|apiKey|accountId
apiKey = parts.slice(1, -1).join("|").trim();
providerSpecificData = { accountId: parts[parts.length - 1].trim() };
} else {
apiKey = parts.length >= 2 ? parts.slice(1).join("|").trim() : parts[0].trim();
}
for (const entry of plan) {
try {
const res = await fetch("/api/providers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider,
apiKey,
name,
apiKey: entry.apiKey,
name: entry.name,
priority: 1,
testStatus: "unknown",
...(providerSpecificData ? { providerSpecificData } : {}),
...(entry.providerSpecificData ? { providerSpecificData: entry.providerSpecificData } : {}),
}),
});
if (res.ok) success++;
@@ -409,6 +401,7 @@ AddApiKeyModal.propTypes = {
name: PropTypes.string,
})),
error: PropTypes.string,
existingNames: PropTypes.arrayOf(PropTypes.string),
onSave: PropTypes.func.isRequired,
onBulkDone: PropTypes.func,
onClose: PropTypes.func.isRequired,

View File

@@ -1690,6 +1690,7 @@ export default function ProviderDetailPage() {
website={providerInfo?.website}
proxyPools={proxyPools}
error={addConnectionError}
existingNames={connections.map((c) => c.name).filter(Boolean)}
onSave={handleSaveApiKey}
onBulkDone={fetchConnections}
onClose={() => {