diff --git a/app/products/web/admin/tokens.py b/app/products/web/admin/tokens.py index e1c9d903c..bf5bf10f6 100644 --- a/app/products/web/admin/tokens.py +++ b/app/products/web/admin/tokens.py @@ -87,6 +87,11 @@ class ToggleTokenDisabledRequest(BaseModel): disabled: bool +class ToggleTokensDisabledRequest(BaseModel): + tokens: list[str] + disabled: bool + + class TokenImportItem(BaseModel): token: str tags: list[str] = [] @@ -358,6 +363,69 @@ async def toggle_token_disabled( return _json({"status": "success", "token": token, "disabled": False}) +@router.post("/tokens/disabled/batch") +async def toggle_tokens_disabled( + req: ToggleTokensDisabledRequest, + repo: "AccountRepository" = Depends(get_repo), +): + cleaned: list[str] = [] + seen: set[str] = set() + for raw in req.tokens: + token = _sanitize(raw) + if token and token not in seen: + seen.add(token) + cleaned.append(token) + if not cleaned: + raise ValidationError("No valid tokens provided", param="tokens") + + records = await repo.get_accounts(cleaned) + if not records: + raise AppError( + "No matching accounts found", + kind=ErrorKind.VALIDATION, + code="account_not_found", + status=404, + ) + + ts = now_ms() + patches: list[AccountPatch] = [] + for record in records: + if req.disabled: + patches.append(AccountPatch( + token=record.token, + status=AccountStatus.DISABLED, + state_reason="operator_disabled", + ext_merge={ + **record.ext, + "disabled_at": ts, + "disabled_reason": "operator_disabled", + }, + )) + else: + patches.append(AccountPatch( + token=record.token, + status=AccountStatus.ACTIVE, + clear_failures=True, + )) + + result = await repo.patch_accounts(patches) + logger.info( + "admin tokens disabled batch updated: disabled={} requested_count={} patched_count={}", + req.disabled, + len(cleaned), + result.patched, + ) + return _json({ + "status": "success", + "disabled": req.disabled, + "summary": { + "total": len(cleaned), + "ok": result.patched, + "fail": max(0, len(cleaned) - result.patched), + }, + }) + + @router.put("/tokens/pool") async def replace_pool( req: ReplacePoolRequest, diff --git a/app/statics/admin/account.html b/app/statics/admin/account.html index d72e814c5..85f609a68 100644 --- a/app/statics/admin/account.html +++ b/app/statics/admin/account.html @@ -939,6 +939,12 @@ + + @@ -1119,6 +1125,8 @@ ['btn-export', 'account.export', '导出数据'], ['btn-nsfw', 'account.batchNsfw', '开启 NSFW'], ['btn-refresh', 'account.batchRefresh', '刷新选中'], + ['btn-disable', 'account.batchDisable', '禁用选中'], + ['btn-restore', 'account.batchRestore', '恢复选中'], ['btn-delete', 'account.batchDelete', '删除选中'], ['btn-batch-cancel', 'account.cancel', '取消'], ]; @@ -1580,7 +1588,7 @@ } function updateBatchBtns() { const show = sel.size > 0; - ['btn-nsfw','btn-refresh','btn-delete'].forEach(id => + ['btn-nsfw','btn-refresh','btn-disable','btn-restore','btn-delete'].forEach(id => document.getElementById(id).style.display = show ? '' : 'none'); } @@ -1811,7 +1819,7 @@ async function _runBatch(endpoint, tokens, label, onDone) { const btnCancel = document.getElementById('btn-batch-cancel'); - ['btn-nsfw','btn-refresh','btn-delete'].forEach(id => + ['btn-nsfw','btn-refresh','btn-disable','btn-restore','btn-delete'].forEach(id => document.getElementById(id).style.display = 'none'); btnCancel.style.display = ''; @@ -1930,6 +1938,45 @@ function disableOne(token) { setDisabled(token, true); } function restoreOne(token) { setDisabled(token, false); } +function batchSetDisabled(disabled) { + if (!sel.size) return; + const tokens = [...sel]; + const n = tokens.length; + const title = disabled + ? tr('account.batchDisableConfirmTitle', null, '批量禁用账号') + : tr('account.batchRestoreConfirmTitle', null, '批量恢复账号'); + const body = disabled + ? tr('account.batchDisableConfirmBody', { n }, `确认禁用选中的 ${n} 个账户?
禁用后这些账号不会参与请求分配,但可随时恢复。`) + : tr('account.batchRestoreConfirmBody', { n }, `确认恢复选中的 ${n} 个账户?
恢复后这些账号将重新参与请求分配。`); + + openConfirm(title, body, async () => { + try { + showToast( + disabled + ? tr('account.disablingMany', { n }, `正在禁用 ${n} 个账户…`) + : tr('account.restoringMany', { n }, `正在恢复 ${n} 个账户…`), + 'info', + ); + const d = await _api('POST', '/tokens/disabled/batch', { tokens, disabled }); + const ok = d.summary?.ok ?? 0; + const fail = d.summary?.fail ?? 0; + showToast( + disabled + ? tr('account.disableManyDone', { ok, fail }, `禁用完成:成功 ${ok} 个,失败 ${fail} 个`) + : tr('account.restoreManyDone', { ok, fail }, `恢复完成:成功 ${ok} 个,失败 ${fail} 个`), + fail > 0 ? 'error' : 'success', + ); + sel.clear(); + await load(); + } catch (e) { + showToast(`${tr('account.operationFailed', null, '操作失败')}: ${e.message}`, 'error'); + } + }); +} + +function batchDisableSel() { batchSetDisabled(true); } +function batchRestoreSel() { batchSetDisabled(false); } + async function batchRefreshSel() { if (!sel.size) return; await _runBatch('/batch/refresh', [...sel], diff --git a/app/statics/i18n/de.json b/app/statics/i18n/de.json index f81deac78..8b9c626a7 100644 --- a/app/statics/i18n/de.json +++ b/app/statics/i18n/de.json @@ -182,6 +182,8 @@ "batchNsfw": "NSFW aktivieren", "batchNsfwDisable": "NSFW deaktivieren", "batchRefresh": "Auswahl aktualisieren", + "batchDisable": "Auswahl deaktivieren", + "batchRestore": "Auswahl wiederherstellen", "batchDelete": "Auswahl löschen", "colToken": "Token", "colType": "Kontotyp", @@ -264,6 +266,14 @@ "restoreConfirmBody": "{token} wiederherstellen?
Das Konto nimmt danach wieder an der Anfrageverteilung teil.", "restoringOne": "Konto wird wiederhergestellt…", "restoreDone": "Konto wiederhergestellt", + "batchDisableConfirmTitle": "Konten deaktivieren", + "batchDisableConfirmBody": "Die ausgewählten {n} Konten deaktivieren?
Deaktivierte Konten werden nicht für Anfragen verwendet und können später wiederhergestellt werden.", + "disablingMany": "{n} Konten werden deaktiviert…", + "disableManyDone": "Deaktivierung abgeschlossen: {ok} erfolgreich, {fail} fehlgeschlagen", + "batchRestoreConfirmTitle": "Konten wiederherstellen", + "batchRestoreConfirmBody": "Die ausgewählten {n} Konten wiederherstellen?
Wiederhergestellte Konten werden erneut für Anfragen verwendet.", + "restoringMany": "{n} Konten werden wiederhergestellt…", + "restoreManyDone": "Wiederherstellung abgeschlossen: {ok} erfolgreich, {fail} fehlgeschlagen", "nsfwConfirmTitle": "NSFW aktivieren", "nsfwConfirmBody": "NSFW für {n} ausgewählte Konten aktivieren?", "nsfwEnablingOne": "NSFW wird aktiviert…", diff --git a/app/statics/i18n/en.json b/app/statics/i18n/en.json index 5f7f0f13d..479a0d673 100644 --- a/app/statics/i18n/en.json +++ b/app/statics/i18n/en.json @@ -183,6 +183,8 @@ "batchNsfw": "Enable NSFW", "batchNsfwDisable": "Disable NSFW", "batchRefresh": "Refresh Selected", + "batchDisable": "Disable Selected", + "batchRestore": "Restore Selected", "batchDelete": "Delete Selected", "colToken": "Token", "colType": "Account Type", @@ -265,6 +267,14 @@ "restoreConfirmBody": "Restore {token}?
The account will be returned to request allocation.", "restoringOne": "Restoring account…", "restoreDone": "Account restored", + "batchDisableConfirmTitle": "Disable Accounts", + "batchDisableConfirmBody": "Disable the selected {n} accounts?
Disabled accounts are removed from request allocation and can be restored later.", + "disablingMany": "Disabling {n} accounts…", + "disableManyDone": "Disable completed: {ok} succeeded, {fail} failed", + "batchRestoreConfirmTitle": "Restore Accounts", + "batchRestoreConfirmBody": "Restore the selected {n} accounts?
Restored accounts will be returned to request allocation.", + "restoringMany": "Restoring {n} accounts…", + "restoreManyDone": "Restore completed: {ok} succeeded, {fail} failed", "nsfwConfirmTitle": "Enable NSFW", "nsfwConfirmBody": "Enable NSFW for {n} selected accounts?", "nsfwEnablingOne": "Enabling NSFW…", diff --git a/app/statics/i18n/es.json b/app/statics/i18n/es.json index 76dfa9ac8..c94be4f85 100644 --- a/app/statics/i18n/es.json +++ b/app/statics/i18n/es.json @@ -182,6 +182,8 @@ "batchNsfw": "Activar NSFW", "batchNsfwDisable": "Desactivar NSFW", "batchRefresh": "Actualizar selección", + "batchDisable": "Desactivar selección", + "batchRestore": "Restaurar selección", "batchDelete": "Eliminar selección", "colToken": "Token", "colType": "Tipo de cuenta", @@ -264,6 +266,14 @@ "restoreConfirmBody": "¿Restaurar {token}?
La cuenta volverá a participar en la asignación de solicitudes.", "restoringOne": "Restaurando cuenta…", "restoreDone": "Cuenta restaurada", + "batchDisableConfirmTitle": "Desactivar cuentas", + "batchDisableConfirmBody": "¿Desactivar las {n} cuentas seleccionadas?
Las cuentas desactivadas no participarán en la asignación de solicitudes y podrán restaurarse más tarde.", + "disablingMany": "Desactivando {n} cuentas…", + "disableManyDone": "Desactivación completada: {ok} correctas, {fail} fallidas", + "batchRestoreConfirmTitle": "Restaurar cuentas", + "batchRestoreConfirmBody": "¿Restaurar las {n} cuentas seleccionadas?
Las cuentas restauradas volverán a participar en la asignación de solicitudes.", + "restoringMany": "Restaurando {n} cuentas…", + "restoreManyDone": "Restauración completada: {ok} correctas, {fail} fallidas", "nsfwConfirmTitle": "Activar NSFW", "nsfwConfirmBody": "¿Activar NSFW para {n} cuentas seleccionadas?", "nsfwEnablingOne": "Activando NSFW…", diff --git a/app/statics/i18n/fr.json b/app/statics/i18n/fr.json index 479c98205..68d0b55b7 100644 --- a/app/statics/i18n/fr.json +++ b/app/statics/i18n/fr.json @@ -182,6 +182,8 @@ "batchNsfw": "Activer NSFW", "batchNsfwDisable": "Désactiver NSFW", "batchRefresh": "Actualiser la sélection", + "batchDisable": "Désactiver la sélection", + "batchRestore": "Restaurer la sélection", "batchDelete": "Supprimer la sélection", "colToken": "Token", "colType": "Type de compte", @@ -264,6 +266,14 @@ "restoreConfirmBody": "Restaurer {token} ?
Le compte reprendra la participation à l’allocation des requêtes.", "restoringOne": "Restauration du compte…", "restoreDone": "Compte restauré", + "batchDisableConfirmTitle": "Désactiver les comptes", + "batchDisableConfirmBody": "Désactiver les {n} comptes sélectionnés ?
Les comptes désactivés ne participeront plus à l’allocation des requêtes et pourront être restaurés plus tard.", + "disablingMany": "Désactivation de {n} comptes…", + "disableManyDone": "Désactivation terminée : {ok} réussies, {fail} échouées", + "batchRestoreConfirmTitle": "Restaurer les comptes", + "batchRestoreConfirmBody": "Restaurer les {n} comptes sélectionnés ?
Les comptes restaurés participeront de nouveau à l’allocation des requêtes.", + "restoringMany": "Restauration de {n} comptes…", + "restoreManyDone": "Restauration terminée : {ok} réussies, {fail} échouées", "nsfwConfirmTitle": "Activer NSFW", "nsfwConfirmBody": "Activer NSFW pour {n} comptes sélectionnés ?", "nsfwEnablingOne": "Activation de NSFW…", diff --git a/app/statics/i18n/ja.json b/app/statics/i18n/ja.json index b43eb3586..9751a1853 100644 --- a/app/statics/i18n/ja.json +++ b/app/statics/i18n/ja.json @@ -182,6 +182,8 @@ "batchNsfw": "NSFW を有効化", "batchNsfwDisable": "NSFW を無効化", "batchRefresh": "選択項目を更新", + "batchDisable": "選択項目を無効化", + "batchRestore": "選択項目を復元", "batchDelete": "選択項目を削除", "colToken": "トークン", "colType": "アカウント種別", @@ -264,6 +266,14 @@ "restoreConfirmBody": "{token} を復元しますか?
復元後、このアカウントは再びリクエスト割り当てに参加します。", "restoringOne": "アカウントを復元しています…", "restoreDone": "アカウントを復元しました", + "batchDisableConfirmTitle": "アカウントを一括無効化", + "batchDisableConfirmBody": "選択した {n} 件のアカウントを無効化しますか?
無効化されたアカウントはリクエスト割り当てに参加せず、後で復元できます。", + "disablingMany": "{n} 件のアカウントを無効化しています…", + "disableManyDone": "無効化完了: 成功 {ok} 件、失敗 {fail} 件", + "batchRestoreConfirmTitle": "アカウントを一括復元", + "batchRestoreConfirmBody": "選択した {n} 件のアカウントを復元しますか?
復元後、これらのアカウントは再びリクエスト割り当てに参加します。", + "restoringMany": "{n} 件のアカウントを復元しています…", + "restoreManyDone": "復元完了: 成功 {ok} 件、失敗 {fail} 件", "nsfwConfirmTitle": "NSFW を有効化", "nsfwConfirmBody": "選択した {n} 件のアカウントで NSFW を有効にしますか?", "nsfwEnablingOne": "NSFW を有効化しています…", diff --git a/app/statics/i18n/zh.json b/app/statics/i18n/zh.json index b72a6522b..1e53feed3 100644 --- a/app/statics/i18n/zh.json +++ b/app/statics/i18n/zh.json @@ -183,6 +183,8 @@ "batchNsfw": "开启 NSFW", "batchNsfwDisable": "关闭 NSFW", "batchRefresh": "刷新选中", + "batchDisable": "禁用选中", + "batchRestore": "恢复选中", "batchDelete": "删除选中", "colToken": "Token", "colType": "账户类型", @@ -265,6 +267,14 @@ "restoreConfirmBody": "确认恢复 {token}?
恢复后该账号将重新参与请求分配。", "restoringOne": "正在恢复账号…", "restoreDone": "账号已恢复", + "batchDisableConfirmTitle": "批量禁用账号", + "batchDisableConfirmBody": "确认禁用选中的 {n} 个账户?
禁用后这些账号不会参与请求分配,但可随时恢复。", + "disablingMany": "正在禁用 {n} 个账户…", + "disableManyDone": "禁用完成:成功 {ok} 个,失败 {fail} 个", + "batchRestoreConfirmTitle": "批量恢复账号", + "batchRestoreConfirmBody": "确认恢复选中的 {n} 个账户?
恢复后这些账号将重新参与请求分配。", + "restoringMany": "正在恢复 {n} 个账户…", + "restoreManyDone": "恢复完成:成功 {ok} 个,失败 {fail} 个", "nsfwConfirmTitle": "启用 NSFW", "nsfwConfirmBody": "确认为选中的 {n} 个账户启用 NSFW?", "nsfwEnablingOne": "正在启用 NSFW…",