diff --git a/src/auth/AuthSource.js b/src/auth/AuthSource.js
index 56e860a..119a0c0 100644
--- a/src/auth/AuthSource.js
+++ b/src/auth/AuthSource.js
@@ -18,8 +18,16 @@ class AuthSource {
this.logger = logger;
this.authMode = "file";
this.availableIndices = [];
+ // Indices used for rotation/switching (deduplicated by email, keeping the latest index per account)
+ this.rotationIndices = [];
+ // Duplicate auth indices detected (valid JSON but skipped from rotation due to same email)
+ this.duplicateIndices = [];
this.initialIndices = [];
this.accountNameMap = new Map();
+ // Map any valid index -> canonical (latest) index for the same account email
+ this.canonicalIndexMap = new Map();
+ // Duplicate groups (email -> kept + duplicates)
+ this.duplicateGroups = [];
this.lastScannedIndices = "[]"; // Cache to track changes
this.logger.info('[Auth] Using files in "configs/auth/" directory for authentication.');
@@ -96,13 +104,19 @@ class AuthSource {
_preValidateAndFilter() {
if (this.initialIndices.length === 0) {
this.availableIndices = [];
+ this.rotationIndices = [];
+ this.duplicateIndices = [];
this.accountNameMap.clear();
+ this.canonicalIndexMap.clear();
+ this.duplicateGroups = [];
return;
}
const validIndices = [];
const invalidSourceDescriptions = [];
this.accountNameMap.clear(); // Clear old names before re-validating
+ this.canonicalIndexMap.clear();
+ this.duplicateGroups = [];
for (const index of this.initialIndices) {
// Iterate over initial to check all, not just previously available
@@ -131,6 +145,73 @@ class AuthSource {
}
this.availableIndices = validIndices.sort((a, b) => a - b);
+ this._buildRotationIndices();
+ }
+
+ _normalizeEmailKey(accountName) {
+ if (typeof accountName !== "string") return null;
+ const trimmed = accountName.trim();
+ if (!trimmed) return null;
+ // Conservative: only deduplicate when the name looks like an email address.
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailPattern.test(trimmed)) return null;
+ return trimmed.toLowerCase();
+ }
+
+ _buildRotationIndices() {
+ this.rotationIndices = [];
+ this.duplicateIndices = [];
+ this.duplicateGroups = [];
+
+ const emailKeyToIndices = new Map();
+
+ for (const index of this.availableIndices) {
+ const accountName = this.accountNameMap.get(index);
+ const emailKey = this._normalizeEmailKey(accountName);
+
+ if (!emailKey) {
+ this.rotationIndices.push(index);
+ this.canonicalIndexMap.set(index, index);
+ continue;
+ }
+
+ const list = emailKeyToIndices.get(emailKey) || [];
+ list.push(index);
+ emailKeyToIndices.set(emailKey, list);
+ }
+
+ for (const [emailKey, indices] of emailKeyToIndices.entries()) {
+ indices.sort((a, b) => a - b);
+ const keptIndex = indices[indices.length - 1];
+ this.rotationIndices.push(keptIndex);
+
+ const duplicateIndices = [];
+ for (const index of indices) {
+ this.canonicalIndexMap.set(index, keptIndex);
+ if (index !== keptIndex) {
+ duplicateIndices.push(index);
+ }
+ }
+
+ if (duplicateIndices.length > 0) {
+ this.duplicateIndices.push(...duplicateIndices);
+ this.duplicateGroups.push({
+ email: emailKey,
+ keptIndex,
+ removedIndices: duplicateIndices,
+ });
+ }
+ }
+
+ this.rotationIndices = [...new Set(this.rotationIndices)].sort((a, b) => a - b);
+ this.duplicateIndices = [...new Set(this.duplicateIndices)].sort((a, b) => a - b);
+
+ if (this.duplicateIndices.length > 0) {
+ this.logger.warn(
+ `[Auth] Detected ${this.duplicateIndices.length} duplicate auth files (same email). ` +
+ `Rotation will only use latest index per account: [${this.rotationIndices.join(", ")}].`
+ );
+ }
}
_getAuthContent(index) {
@@ -162,6 +243,20 @@ class AuthSource {
return null;
}
}
+
+ getRotationIndices() {
+ return this.rotationIndices;
+ }
+
+ getCanonicalIndex(index) {
+ if (!Number.isInteger(index)) return null;
+ if (!this.availableIndices.includes(index)) return null;
+ return this.canonicalIndexMap.get(index) ?? index;
+ }
+
+ getDuplicateGroups() {
+ return this.duplicateGroups;
+ }
}
module.exports = AuthSource;
diff --git a/src/auth/AuthSwitcher.js b/src/auth/AuthSwitcher.js
index 2a0aa45..9780560 100644
--- a/src/auth/AuthSwitcher.js
+++ b/src/auth/AuthSwitcher.js
@@ -30,10 +30,14 @@ class AuthSwitcher {
}
getNextAuthIndex() {
- const available = this.authSource.availableIndices;
+ const available = this.authSource.getRotationIndices();
if (available.length === 0) return null;
- const currentIndexInArray = available.indexOf(this.currentAuthIndex);
+ const currentCanonicalIndex =
+ this.currentAuthIndex >= 0
+ ? this.authSource.getCanonicalIndex(this.currentAuthIndex)
+ : this.currentAuthIndex;
+ const currentIndexInArray = available.indexOf(currentCanonicalIndex);
if (currentIndexInArray === -1) {
this.logger.warn(
@@ -47,7 +51,7 @@ class AuthSwitcher {
}
async switchToNextAuth() {
- const available = this.authSource.availableIndices;
+ const available = this.authSource.getRotationIndices();
if (available.length === 0) {
throw new Error("No available authentication sources, cannot switch.");
@@ -86,7 +90,11 @@ class AuthSwitcher {
}
// Multi-account mode
- const currentIndexInArray = available.indexOf(this.currentAuthIndex);
+ const currentCanonicalIndex =
+ this.currentAuthIndex >= 0
+ ? this.authSource.getCanonicalIndex(this.currentAuthIndex)
+ : this.currentAuthIndex;
+ const currentIndexInArray = available.indexOf(currentCanonicalIndex);
const hasCurrentAccount = currentIndexInArray !== -1;
const startIndex = hasCurrentAccount ? currentIndexInArray : 0;
const originalStartAccount = hasCurrentAccount ? available[startIndex] : null;
@@ -94,7 +102,9 @@ class AuthSwitcher {
this.logger.info("==================================================");
this.logger.info(`🔄 [Auth] Multi-account mode: Starting intelligent account switching`);
this.logger.info(` • Current account: #${this.currentAuthIndex}`);
- this.logger.info(` • Available accounts: [${available.join(", ")}]`);
+ this.logger.info(
+ ` • Available accounts (dedup by email, keeping latest index): [${available.join(", ")}]`
+ );
if (hasCurrentAccount) {
this.logger.info(` • Starting from: #${originalStartAccount}`);
} else {
@@ -191,13 +201,22 @@ class AuthSwitcher {
this.logger.info("🔄 [Auth] Account switching in progress, skipping duplicate operation");
return { reason: "Switch already in progress.", success: false };
}
- if (!this.authSource.availableIndices.includes(targetIndex)) {
+
+ const canonicalIndex = this.authSource.getCanonicalIndex(targetIndex);
+ if (canonicalIndex === null) {
return {
reason: `Switch failed: Account #${targetIndex} invalid or does not exist.`,
success: false,
};
}
+ if (canonicalIndex !== targetIndex) {
+ this.logger.warn(
+ `[Auth] Requested account #${targetIndex} is a duplicate for the same email. Redirecting to latest auth index #${canonicalIndex}.`
+ );
+ }
+ targetIndex = canonicalIndex;
+
this.isSystemBusy = true;
try {
this.logger.info(`🔄 [Auth] Starting switch to specified account #${targetIndex}...`);
diff --git a/src/core/ProxyServerSystem.js b/src/core/ProxyServerSystem.js
index a83a1ed..1316e4c 100644
--- a/src/core/ProxyServerSystem.js
+++ b/src/core/ProxyServerSystem.js
@@ -61,6 +61,7 @@ class ProxyServerSystem extends EventEmitter {
this.logger.info(`[System] Proxy server system startup complete.`);
const allAvailableIndices = this.authSource.availableIndices;
+ const allRotationIndices = this.authSource.getRotationIndices();
if (allAvailableIndices.length === 0) {
this.logger.warn("[System] No available authentication source. Starting in account binding mode.");
@@ -68,16 +69,27 @@ class ProxyServerSystem extends EventEmitter {
return; // Exit early
}
- let startupOrder = [...allAvailableIndices];
- if (initialAuthIndex && allAvailableIndices.includes(initialAuthIndex)) {
- this.logger.info(`[System] Detected specified startup index #${initialAuthIndex}, will try it first.`);
- startupOrder = [initialAuthIndex, ...allAvailableIndices.filter(i => i !== initialAuthIndex)];
- } else {
- if (initialAuthIndex) {
+ let startupOrder = allRotationIndices.length > 0 ? [...allRotationIndices] : [...allAvailableIndices];
+ const hasInitialAuthIndex = Number.isInteger(initialAuthIndex);
+ if (hasInitialAuthIndex) {
+ const canonicalInitialIndex = this.authSource.getCanonicalIndex(initialAuthIndex);
+ if (canonicalInitialIndex !== null && startupOrder.includes(canonicalInitialIndex)) {
+ if (canonicalInitialIndex !== initialAuthIndex) {
+ this.logger.warn(
+ `[System] Specified startup index #${initialAuthIndex} is a duplicate for the same email, using latest auth index #${canonicalInitialIndex} instead.`
+ );
+ } else {
+ this.logger.info(
+ `[System] Detected specified startup index #${initialAuthIndex}, will try it first.`
+ );
+ }
+ startupOrder = [canonicalInitialIndex, ...startupOrder.filter(i => i !== canonicalInitialIndex)];
+ } else {
this.logger.warn(
`[System] Specified startup index #${initialAuthIndex} is invalid or unavailable, will start in default order.`
);
}
+ } else {
this.logger.info(
`[System] No valid startup index specified, will try in default order [${startupOrder.join(", ")}].`
);
@@ -121,6 +133,7 @@ class ProxyServerSystem extends EventEmitter {
"/health",
"/api/status",
"/api/accounts/current",
+ "/api/accounts/deduplicate",
"/api/settings/streaming-mode",
"/api/settings/force-thinking",
"/api/settings/force-web-search",
diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js
index cbccd5a..187e583 100644
--- a/src/core/RequestHandler.js
+++ b/src/core/RequestHandler.js
@@ -153,7 +153,7 @@ class RequestHandler {
} catch (error) {
this.logger.error(`❌ [System] Recovery failed: ${error.message}`);
- if (wasDirectRecovery && this.authSource.availableIndices.length > 1) {
+ if (wasDirectRecovery && this.authSource.getRotationIndices().length > 1) {
this.logger.warn("⚠️ [System] Attempting to switch to alternative account...");
try {
const result = await this.authSwitcher.switchToNextAuth();
diff --git a/src/routes/StatusRoutes.js b/src/routes/StatusRoutes.js
index b0b2645..6231048 100644
--- a/src/routes/StatusRoutes.js
+++ b/src/routes/StatusRoutes.js
@@ -150,7 +150,7 @@ class StatusRoutes {
}
} else {
this.logger.info("[WebUI] Received manual request to switch to next account...");
- if (this.serverSystem.authSource.availableIndices.length <= 1) {
+ if (this.serverSystem.authSource.getRotationIndices().length <= 1) {
return res.status(400).json({ message: "accountSwitchCancelledSingle" });
}
const result = await this.serverSystem.requestHandler._switchToNextAuth();
@@ -167,6 +167,90 @@ class StatusRoutes {
}
});
+ app.post("/api/accounts/deduplicate", isAuthenticated, async (req, res) => {
+ try {
+ const { authSource, requestHandler } = this.serverSystem;
+
+ // Force refresh to ensure dedup metadata is up-to-date even if file list didn't change.
+ authSource.reloadAuthSources(true);
+
+ const duplicateGroups = authSource.getDuplicateGroups() || [];
+ if (duplicateGroups.length === 0) {
+ return res.status(200).json({
+ message: "accountDedupNoop",
+ removedIndices: [],
+ rotationIndices: authSource.getRotationIndices(),
+ });
+ }
+
+ this.logger.warn(
+ "[Auth] Dedup cleanup will keep the auth file with the highest index per email and delete the other duplicates. " +
+ "Assumption: for the same account, auth indices are created in chronological order (higher index = newer)."
+ );
+
+ const currentAuthIndex = requestHandler.currentAuthIndex;
+ if (Number.isInteger(currentAuthIndex) && currentAuthIndex >= 0) {
+ const canonicalCurrent = authSource.getCanonicalIndex(currentAuthIndex);
+ if (canonicalCurrent !== null && canonicalCurrent !== currentAuthIndex) {
+ this.logger.warn(
+ `[Auth] Current active auth #${currentAuthIndex} is a duplicate. Switching to the latest auth #${canonicalCurrent} before cleanup.`
+ );
+ const switchResult = await requestHandler._switchToSpecificAuth(canonicalCurrent);
+ if (!switchResult.success) {
+ return res.status(409).json({
+ message: "accountDedupSwitchFailed",
+ reason: switchResult.reason,
+ });
+ }
+ }
+ }
+
+ const removedIndices = [];
+ const failed = [];
+
+ for (const group of duplicateGroups) {
+ const removed = Array.isArray(group.removedIndices) ? group.removedIndices : [];
+ if (removed.length === 0) continue;
+
+ this.logger.info(
+ `[Auth] Dedup: email ${group.email} -> keep auth-${group.keptIndex}.json, delete [${removed
+ .map(i => `auth-${i}.json`)
+ .join(", ")}]`
+ );
+
+ for (const index of removed) {
+ try {
+ authSource.removeAuth(index);
+ removedIndices.push(index);
+ } catch (error) {
+ failed.push({ error: error.message, index });
+ this.logger.error(`[Auth] Dedup delete failed for auth-${index}.json: ${error.message}`);
+ }
+ }
+ }
+
+ authSource.reloadAuthSources(true);
+
+ if (failed.length > 0) {
+ return res.status(500).json({
+ failed,
+ message: "accountDedupPartialFailed",
+ removedIndices,
+ rotationIndices: authSource.getRotationIndices(),
+ });
+ }
+
+ return res.status(200).json({
+ message: "accountDedupSuccess",
+ removedIndices,
+ rotationIndices: authSource.getRotationIndices(),
+ });
+ } catch (error) {
+ this.logger.error(`[Auth] Dedup cleanup failed: ${error.message}`);
+ return res.status(500).json({ error: error.message, message: "accountDedupFailed" });
+ }
+ });
+
app.delete("/api/accounts/:index", isAuthenticated, (req, res) => {
const rawIndex = req.params.index;
const targetIndex = Number(rawIndex);
@@ -340,12 +424,19 @@ class StatusRoutes {
const { config, requestHandler, authSource, browserManager } = this.serverSystem;
const initialIndices = authSource.initialIndices || [];
const invalidIndices = initialIndices.filter(i => !authSource.availableIndices.includes(i));
+ const rotationIndices = authSource.getRotationIndices();
+ const duplicateIndices = authSource.duplicateIndices || [];
const logs = this.logger.logBuffer || [];
const accountNameMap = authSource.accountNameMap;
const accountDetails = initialIndices.map(index => {
const isInvalid = invalidIndices.includes(index);
const name = isInvalid ? null : accountNameMap.get(index) || null;
- return { index, isInvalid, name };
+
+ const canonicalIndex = isInvalid ? null : authSource.getCanonicalIndex(index);
+ const isDuplicate = canonicalIndex !== null && canonicalIndex !== index;
+ const isRotation = rotationIndices.includes(index);
+
+ return { canonicalIndex, index, isDuplicate, isInvalid, isRotation, name };
});
const currentAuthIndex = requestHandler.currentAuthIndex;
@@ -371,6 +462,7 @@ class StatusRoutes {
currentAccountName,
currentAuthIndex,
debugMode: LoggingService.isDebugEnabled(),
+ duplicateIndicesRaw: duplicateIndices,
failureCount,
forceThinking: this.serverSystem.forceThinking,
forceUrlContext: this.serverSystem.forceUrlContext,
@@ -382,6 +474,7 @@ class StatusRoutes {
initialIndicesRaw: initialIndices,
invalidIndicesRaw: invalidIndices,
isSystemBusy: requestHandler.isSystemBusy,
+ rotationIndicesRaw: rotationIndices,
streamingMode: this.serverSystem.streamingMode,
usageCount,
},
diff --git a/ui/app/pages/StatusPage.vue b/ui/app/pages/StatusPage.vue
index 858fd02..71e85b6 100644
--- a/ui/app/pages/StatusPage.vue
+++ b/ui/app/pages/StatusPage.vue
@@ -111,7 +111,9 @@
{{ t('currentAccount') }}: #{{ state.currentAuthIndex }} ({{ currentAccountName }})
{{ t('usageCount') }}: {{ state.usageCount }}
{{ t('consecutiveFailures') }}: {{ state.failureCount }}
-{{ t('totalScanned') }}: {{ totalScannedAccountsText }}
+{{ t('totalScanned') }}: {{ totalScannedAccountsText }}
+{{ t('dedupedAvailable') }}: {{ rotationAccountsText }}
+{{ t('duplicateAuth') }}: {{ duplicateAuthText }}
{{ t('account') }} {{ account.index }}: {{ getAccountDisplayName(account) }}
{{ t('formatErrors') }}: {{ formatErrorsText }}
@@ -192,6 +194,26 @@
/>
+
{
return `[${indices.join(", ")}] (${t("total")}: ${indices.length})`;
});
+const duplicateAuthText = computed(() => {
+ const indices = state.duplicateIndicesRaw || [];
+ return `[${indices.join(", ")}] (${t("total")}: ${indices.length})`;
+});
+
const serviceConnectedClass = computed(() => (state.serviceConnected ? "status-ok" : "status-error"));
const serviceConnectedText = computed(() => (state.serviceConnected ? t("running") : t("disconnected")));
const streamingModeText = computed(() => (state.streamingModeReal ? t("real") : t("fake")));
+const rotationAccountsText = computed(() => {
+ const indices = state.rotationIndicesRaw || [];
+ return `[${indices.join(", ")}] (${t("total")}: ${indices.length})`;
+});
+
const totalScannedAccountsText = computed(() => {
const indices = state.initialIndicesRaw || [];
return `[${indices.join(", ")}] (${t("total")}: ${indices.length})`;
@@ -475,7 +509,11 @@ const getAccountDisplayName = account => {
if (account.isInvalid) {
return t("jsonFormatError");
}
- return account.name || t("unnamedAccount");
+ const name = account.name || t("unnamedAccount");
+ if (account.isDuplicate && account.canonicalIndex !== null && account.canonicalIndex !== undefined) {
+ return `${name} (${t("duplicateAuthHint", { index: account.canonicalIndex })})`;
+ }
+ return name;
};
const addUser = () => {
@@ -554,6 +592,56 @@ const deleteUser = async () => {
});
};
+const deduplicateAuth = () => {
+ ElMessageBox.confirm(t("accountDedupConfirm"), t("warningTitle"), {
+ cancelButtonText: t("cancel"),
+ confirmButtonText: t("ok"),
+ lockScroll: false,
+ type: "warning",
+ })
+ .then(async () => {
+ const notification = ElNotification({
+ duration: 0,
+ message: t("operationInProgress"),
+ title: t("warningTitle"),
+ type: "warning",
+ });
+ state.isSwitchingAccount = true;
+ try {
+ const res = await fetch("/api/accounts/deduplicate", { method: "POST" });
+ const data = await res.json();
+
+ const removedIndicesText = Array.isArray(data.removedIndices)
+ ? `[${data.removedIndices.join(", ")}]`
+ : "[]";
+ const failedText = Array.isArray(data.failed) ? JSON.stringify(data.failed) : "";
+
+ const message = t(data.message, {
+ ...data,
+ failed: failedText,
+ removedIndices: removedIndicesText,
+ });
+
+ if (res.ok) {
+ ElMessage.success(message);
+ } else {
+ ElMessage.error(message);
+ }
+ } catch (err) {
+ ElMessage.error(t("accountDedupFailed", { error: err.message || err }));
+ } finally {
+ state.isSwitchingAccount = false;
+ notification.close();
+ updateContent();
+ }
+ })
+ .catch(e => {
+ if (e !== "cancel") {
+ console.error(e);
+ }
+ });
+};
+
const handleForceThinkingBeforeChange = () => handleSettingChange("/api/settings/force-thinking", "forceThinking");
const handleForceUrlContextBeforeChange = () =>
@@ -734,7 +822,9 @@ const updateStatus = data => {
state.logCount = data.logCount || 0;
state.logs = data.logs || "";
state.initialIndicesRaw = data.status.initialIndicesRaw;
+ state.rotationIndicesRaw = data.status.rotationIndicesRaw || [];
state.invalidIndicesRaw = data.status.invalidIndicesRaw;
+ state.duplicateIndicesRaw = data.status.duplicateIndicesRaw || [];
state.isSystemBusy = data.status.isSystemBusy;
const isSelectedAccountValid = state.accountDetails.some(acc => acc.index === state.selectedAccount);
@@ -1032,6 +1122,10 @@ pre {
color: @error-color;
}
+ &.btn-warning:hover:not(:disabled) {
+ color: @warning-color;
+ }
+
// Primary button uses primary color on hover (already default)
&.btn-primary:hover:not(:disabled) {
color: @primary-color;
diff --git a/ui/locales/en.json b/ui/locales/en.json
index 1c974e0..f92ef8a 100644
--- a/ui/locales/en.json
+++ b/ui/locales/en.json
@@ -1,5 +1,11 @@
{
"account": "Account",
+ "accountDedupConfirm": "Deduplicate auth files by email, keeping the highest index and deleting other duplicates (assumes indices increase over time for the same account). Continue?",
+ "accountDedupFailed": "Deduplication failed: {error}",
+ "accountDedupNoop": "No duplicate auth files found to clean up.",
+ "accountDedupPartialFailed": "Partial deduplication failure. Deleted: {removedIndices}; Failed: {failed}",
+ "accountDedupSuccess": "Deduplication completed. Deleted: {removedIndices}",
+ "accountDedupSwitchFailed": "Failed to switch to latest auth before deduplication: {reason}",
"accountDeleteSuccess": "Account #{index} deleted successfully.",
"accountStatus": "Account Status",
"accountSwitchCancelledSingle": "Switch cancelled: Only one available account.",
@@ -51,6 +57,7 @@
"authVncNotConnected": "VNC session is not connected yet.",
"browserConnection": "Browser Connection",
"btnAddUser": "Add User",
+ "btnDeduplicateAuth": "Deduplicate Auth",
"btnDeleteUser": "Delete User",
"btnSwitchAccount": "Switch Account",
"cancel": "Cancel",
@@ -61,11 +68,14 @@
"currentAccount": "Current Active Account",
"custom": "Custom",
"debug": "Debug",
+ "dedupedAvailable": "Available Accounts (Deduped)",
"default": "Default",
"deleteFailed": "Delete failed: {message}",
"disabled": "Disabled",
"disconnected": "Disconnected",
"download": "Download Auth",
+ "duplicateAuth": "Duplicate Auth",
+ "duplicateAuthHint": "Duplicate, keep #{index}",
"enabled": "Enabled",
"entries": "entries",
"errorAccountNotFound": "Account #{index} not found or already removed.",
diff --git a/ui/locales/zh.json b/ui/locales/zh.json
index c1543c2..9028d55 100644
--- a/ui/locales/zh.json
+++ b/ui/locales/zh.json
@@ -1,5 +1,11 @@
{
"account": "账号",
+ "accountDedupConfirm": "将按同一邮箱保留最大 index 的 auth 文件并删除其余重复文件(前提:同一账号的多个 auth 按 index 递增生成,index 越大越新)。是否继续?",
+ "accountDedupFailed": "去重清理失败:{error}",
+ "accountDedupNoop": "未发现需要清理的重复 auth。",
+ "accountDedupPartialFailed": "部分去重清理失败。已删除:{removedIndices};失败:{failed}",
+ "accountDedupSuccess": "去重清理完成,已删除:{removedIndices}",
+ "accountDedupSwitchFailed": "去重前切换到最新 auth 失败:{reason}",
"accountDeleteSuccess": "账号 {index} 删除成功。",
"accountStatus": "账号状态",
"accountSwitchCancelledSingle": "切换取消:只有一个可用账号。",
@@ -51,6 +57,7 @@
"authVncNotConnected": "VNC 会话尚未连接。",
"browserConnection": "浏览器连接",
"btnAddUser": "添加账号",
+ "btnDeduplicateAuth": "去重清理",
"btnDeleteUser": "删除账号",
"btnSwitchAccount": "切换账号",
"cancel": "取消",
@@ -61,11 +68,14 @@
"currentAccount": "当前活跃账号",
"custom": "自定义",
"debug": "调试",
+ "dedupedAvailable": "去重后可用账号",
"default": "默认",
"deleteFailed": "删除失败:{message}",
"disabled": "已禁用",
"disconnected": "连接中断",
"download": "下载 Auth",
+ "duplicateAuth": "重复 Auth",
+ "duplicateAuthHint": "重复,保留 #{index}",
"enabled": "已启用",
"entries": "条",
"errorAccountNotFound": "账号 {index} 未找到或已被移除。",