Skip to content

feat(subscription): 增加账号批量操作 - #455

Merged
tbphp merged 7 commits into
v2from
codex/subscription-bulk-operations
Aug 22, 2026
Merged

feat(subscription): 增加账号批量操作#455
tbphp merged 7 commits into
v2from
codex/subscription-bulk-operations

Conversation

@tbphp

@tbphp tbphp commented Aug 22, 2026

Copy link
Copy Markdown
Owner

关联 Issue / Related Issue

无 / None

变更内容 / Change Content

  • Bug 修复 / Bug fix

  • 新功能 / New feature

  • 其他改动 / Other changes

  • 增加订阅账号批量操作菜单,支持多文件容错导入、全量导出和当前页额度刷新

  • 批量刷新复用单账号接口并限制为 4 路并发,全部完成后统一刷新当前页

  • 单账号和批量额度刷新期间使用整卡骨架屏,保持卡片尺寸稳定

  • 新增全量订阅账号导出 API;响应禁止缓存,不涉及数据迁移

自查清单 / Checklist

  • 我已运行 make check,或在说明中写明无法运行的原因和未验证范围。 / I ran make check, or documented why it could not run and what remains unverified.
  • 本 PR 范围聚焦,未包含无关改动。 / This PR is focused and contains no unrelated changes.
  • 我已更新必要的公开文档或发布说明。 / I updated any required public documentation or release notes.(本次无需公开文档或发布说明)
  • 我已确认提交、日志和测试数据不包含敏感信息。 / I confirmed that commits, logs, and fixtures contain no sensitive data.
  • 如适用,我已说明兼容性或数据迁移影响。 / Where applicable, I documented compatibility or data-migration impact.(新增兼容 API,无数据迁移)

Summary by CodeRabbit

  • 新功能

    • 支持按分组全选、启用、停用、同步及下载全部凭据。
    • 凭据列表新增搜索、选择状态、进度和结果提示。
    • 支持一次选择并导入多个 OAuth JSON 文件,并显示成功与失败数量。
    • 确认对话框新增警告样式,强化高风险操作提示。
  • 改进

    • 批量操作增加权限与状态校验,下载结果自动禁止缓存。
    • 优化订阅账户同步的加载状态、错误提示及响应式布局。
    • 更新中文、英文和日文界面文案。

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

后端新增凭据全量批量操作和订阅组凭据下载接口。前端新增全量下载、批量同步、启用和停用交互,并更新账户卡片状态展示。OAuth JSON 导入支持一次选择多个文件,并显示导入结果统计。对话框、路由契约和英文、日文、中文文案同步更新。

Merge Risk: 🔵 Low · up to ae016

The subscription bulk-operation changes are mergeable with owner follow-up: refresh loading placeholders may be clipped on shorter account cards, while missing-group errors and checkbox styling can display incorrectly in affected cases.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 18 files. (3 skipped: 3 unsupported.) Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了本次 PR 的主要变更,即增加订阅账号批量操作。
Description check ✅ Passed 描述包含关联 Issue、变更内容、详细说明和自查清单,且内容与 PR 目标一致。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch codex/subscription-bulk-operations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tbphp tbphp self-assigned this Aug 22, 2026
@tbphp tbphp added the enhancement New feature or request label Aug 22, 2026
@tbphp tbphp added this to the v2.0.0 milestone Aug 22, 2026
@tbphp
tbphp marked this pull request as ready for review August 22, 2026 03:51
Copilot AI lite review requested due to automatic review settings August 22, 2026 03:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4388323d2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/src/features/groups/credentials/GroupCredentialsTab.vue Outdated
Comment thread web/src/features/groups/credentials/GroupCredentialsTab.vue

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
internal/control/server.go-530-545 (1)

530-545: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

为不存在的分组返回正确的错误文案。

第 541 行使用的新操作名 download_all_group_credentials 未在 serviceErrorMessageID 的分组资源分支中处理。分组不存在时,服务返回 ErrResourceNotFound,响应会使用 credential.not_found,而不是 group.not_found。将该操作名加入分组资源操作列表。

建议修复
-       case "list_groups", "get_group_summary", "get_group_settings", "get_group_models",
+       case "list_groups", "get_group_summary", "get_group_settings", "get_group_models",
            "update_group_settings", "delete_group",
            "update_group_models", "import_group_credentials",
-           "discover_group_models", "list_group_credentials":
+           "discover_group_models", "list_group_credentials",
+           "download_all_group_credentials":
web/src/features/groups/credentials/SubscriptionAccountCard.vue-1178-1187 (1)

1178-1187: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

复选框背景色硬编码为 #fff,不随主题变化。

该文件其余样式全部使用主题变量,例如 var(--color-surface)var(--color-surface-sunken)。这里的 #fff 在深色主题下会保持白底,与卡片背景冲突。

🐛 建议修复
 .subscription-account__select-box {
   display: grid;
   width: 20px;
   height: 20px;
   place-items: center;
   border: 1px solid color-mix(in srgb, var(--color-text-faint) 42%, transparent);
   border-radius: 3px;
-  background: `#fff`;
+  background: var(--color-surface);
   color: var(--color-action);
 }
🧹 Nitpick comments (2)
web/src/features/groups/credentials/GroupCredentialBatchBar.vue (1)

27-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

role="status" 从按钮容器上移除。

role="status" 会把该容器声明为实时区域。容器内现在只有交互按钮,不再有独立的说明文本。选中数量变化时,屏幕阅读器会朗读整组按钮文本,而不是简洁的状态描述。

建议把实时区域缩小到计数本身,或改用独立的 sr-only 状态文本。

♻️ 建议改法
-  <div class="group-credential-batch" role="status">
+  <div class="group-credential-batch">
     <div class="group-credential-batch__actions">

并在计数徽章上声明实时区域:

-        <span v-if="selectedCount > 0" class="group-credential-batch__count">{{
-          n(selectedCount)
-        }}</span>
+        <span
+          v-if="selectedCount > 0"
+          class="group-credential-batch__count"
+          role="status"
+          aria-live="polite"
+          >{{ n(selectedCount) }}</span
+        >
web/src/features/groups/credentials/SubscriptionAccountCard.vue (1)

511-516: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

批量同步时,卡片级实时区域会产生大量重复通报。

骨架屏使用 role="status" aria-live="polite",错误区使用 role="alert"。批量同步会同时把这两类区域挂载到当前页的每一张卡片上。当前页最多 100 条。屏幕阅读器会连续通报同一条「正在同步配额信息」,失败时再连续通报每一条错误。

父组件 GroupCredentialsTab.vue 已在 section 上维护 aria-busy,并在批量结束时用 toast 汇总成功与失败数量。建议卡片只保留 aria-busy,把批量进度与结果的通报交给父组件统一处理。

♻️ 建议改法
     <div
       v-if="refreshingObservation"
       class="subscription-account__refresh-skeleton"
-      role="status"
-      aria-live="polite"
+      aria-hidden="true"
     >
-      <span class="sr-only">{{ t('group.credentials.subscription.syncingQuota') }}</span>

错误区同样去掉 role="alert",改为静态文本:

-      <div v-if="observationError" class="subscription-account__alert" role="alert">
+      <div v-if="observationError" class="subscription-account__alert">
         <span>{{ observationError }}</span>
       </div>

Also applies to: 683-685


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 98df01da-75cc-41f9-9d81-cb3786639727

📥 Commits

Reviewing files that changed from the base of the PR and between ed5622f and 4388323.

📒 Files selected for processing (23)
  • internal/container/route_contract_test.go
  • internal/control/credential_actions.go
  • internal/control/credential_actions_test.go
  • internal/control/credential_api_foundation_test.go
  • internal/control/credential_batch_order_test.go
  • internal/control/credential_mutations.go
  • internal/control/credentials.go
  • internal/control/http_routes.go
  • internal/control/server.go
  • web/src/api/control/types.ts
  • web/src/app/resources/credentials.ts
  • web/src/components/ui/AppConfirmDialog.vue
  • web/src/components/ui/AppDialog.vue
  • web/src/features/groups/credentials/GroupCredentialBatchBar.vue
  • web/src/features/groups/credentials/GroupCredentialsTab.vue
  • web/src/features/groups/credentials/SubscriptionAccountCard.vue
  • web/src/features/import/SubscriptionCredentialStager.vue
  • web/src/i18n/locales/en-US/group.ts
  • web/src/i18n/locales/en-US/import.ts
  • web/src/i18n/locales/ja-JP/group.ts
  • web/src/i18n/locales/ja-JP/import.ts
  • web/src/i18n/locales/zh-CN/group.ts
  • web/src/i18n/locales/zh-CN/import.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread web/src/features/groups/credentials/GroupCredentialsTab.vue
Comment thread web/src/features/groups/credentials/GroupCredentialsTab.vue

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two moderate issues remain in GroupCredentialsTab.vue involving stale batch data and untranslated single-account errors.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds bulk subscription-account management across the frontend and control API.

Changes:

  • Supports tolerant multi-file imports and aggregate results.
  • Adds selection, quota synchronization, skeleton loading, and batch actions.
  • Adds full-group export APIs with no-cache handling and backend coverage.
File summaries
File Reviewed changes Final comments
web/src/i18n/locales/zh-CN/import.ts Chinese import localization None
web/src/i18n/locales/zh-CN/group.ts Chinese bulk-operation localization None
web/src/i18n/locales/ja-JP/import.ts Japanese import localization None
web/src/i18n/locales/ja-JP/group.ts Japanese bulk-operation localization None
web/src/i18n/locales/en-US/import.ts English import localization None
web/src/i18n/locales/en-US/group.ts English bulk-operation localization None
web/src/features/import/SubscriptionCredentialStager.vue Multi-file import handling None
web/src/features/groups/credentials/SubscriptionAccountCard.vue Selection and refresh skeleton UI None
web/src/features/groups/credentials/GroupCredentialsTab.vue Batch operations and quota synchronization moderate, 3 votes: batch refresh does not refetch the active page after workers finish. moderate, 4 votes: single-account refresh can display a raw i18n key instead of translated text.
web/src/features/groups/credentials/GroupCredentialBatchBar.vue Batch action controls None
web/src/components/ui/AppDialog.vue Dialog description tone None
web/src/components/ui/AppConfirmDialog.vue Confirmation dialog tone support None
web/src/app/resources/credentials.ts Export and batch API resources None
web/src/api/control/types.ts Credential response types None
internal/control/server.go Export request handler None
internal/control/http_routes.go Export route registration None
internal/control/credentials.go Batch request and response types None
internal/control/credential_mutations.go Group-scoped batch mutations None
internal/control/credential_batch_order_test.go Batch mutation safety tests None
internal/control/credential_api_foundation_test.go Credential API route coverage None
internal/control/credential_actions.go Full credential export service None
internal/control/credential_actions_test.go Export behavior tests None
internal/container/route_contract_test.go Route contract coverage None
Review details

Suppressed comments (7)

web/src/features/groups/credentials/GroupCredentialsTab.vue:1307

  • bulkActionsBusy intentionally includes credentialsQuery.isFetching, but the new batch toolbar only receives batchBusy || singleBusy. During a page/filter/background refetch its controls remain enabled and can submit mutations using stale collection data; pass the full busy state here, as the full-action controls do.
          :pending="batchBusy || singleBusy"

web/src/features/groups/credentials/GroupCredentialsTab.vue:718

  • The per-account sync button is disabled when item.configured_status === 'disabled', but this batch path filters only by selection and capability. Selecting a disabled account therefore still calls the observation-refresh API, bypassing the existing UI restriction (and mixed selections silently include it). Exclude disabled accounts from this action and report them as skipped, or disable batch sync unless every selected account is refreshable.
  const items = (collection.value?.items ?? []).filter(({ credential_id }) =>
    selected.has(credential_id),
  )
  if (items.length === 0 || bulkActionsBusy.value || !channelCapabilities.value.quota_observation) {

web/src/features/groups/credentials/GroupCredentialsTab.vue:753

  • Removing each ID from batchObservationPending as its request completes makes that card leave the full-card skeleton while other selected requests are still running. This produces a mixture of refreshed and skeleton cards during one batch operation, contrary to the stated batch-refresh behavior. Keep the batch pending set until Promise.all settles, then clear the cards together.
        finishBatchObservation(item.credential_id)
        setPending(item.credential_id, 'observation', false)

web/src/features/groups/credentials/GroupCredentialsTab.vue:761

  • This unconditional assignment can restore failed IDs from the old view after the user changes the search, status, or page while the asynchronous batch is running; the watcher clears the current selection first, but this line then puts the stale IDs back. Those IDs can remain counted as selected despite not being visible and be acted on when the old view is revisited. Only restore failed IDs when the operation's filter/page context is still current.
    selectedIds.value = failedIDs

web/src/features/groups/credentials/GroupCredentialsTab.vue:687

  • For more than one credential, this loop triggers separate synthetic downloads after an awaited request. Browser multiple-download protection can block later clicks, but click() does not throw, so the success count and toast can claim every file was downloaded when only some were saved. Use one archive/single download response or an explicit per-file user flow, and report completion based on that single download.
      for (const file of result.files) downloadJSONFile(file.filename, file.credential)

web/src/features/groups/credentials/GroupCredentialsTab.vue:795

  • The selected-download worker has the same multiple-download failure mode: concurrent requests each call a synthetic anchor click, which browsers may suppress after the first download, while succeeded is incremented immediately. Users can therefore receive a misleading successful result. Prefer packaging the selected credentials into one archive/single download response.
        downloadJSONFile(result.filename, result.credential)

web/src/features/groups/credentials/GroupCredentialsTab.vue:807

  • As with the sync flow, this unconditional assignment can restore failed IDs from the pre-operation page after the user changes filters or pagination while downloads are in flight. The current view's selection is cleared by the watcher, but stale IDs are then reintroduced and counted as selected. Only restore failed IDs if the view context captured at operation start is still current.
    selectedIds.value = failedIDs
  • Files reviewed: 23/23 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/src/features/groups/credentials/GroupCredentialsTab.vue Outdated
Comment thread web/src/features/groups/credentials/GroupCredentialsTab.vue Outdated
@tbphp

tbphp commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

已在 ae01681 一并修复 Review 摘要中的相关项:下载全部时缺失分组文案、暗色主题选择框、筛选/分页变化后的失败项选择恢复、批量栏 ARIA 语义,以及停用账号的手动额度同步。按已确认方案,批量同步不会无条件重新请求当前页。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/features/groups/credentials/SubscriptionAccountCard.vue (1)

116-116: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

使刷新骨架屏匹配实际卡片高度。

quotaWindows.length 小于 2 时,Line 116 仍至少生成 2 行额度骨架。模板还固定显示标题和两个操作占位块。subscription-account__refresh-skeleton 使用绝对定位,不参与 article 高度计算;父级在 Line 1077 设置了 overflow: hidden。因此骨架屏可能高于现有卡片,底部占位内容会被裁剪。

请按当前卡片的可选区块和实际额度窗口数量生成骨架,或为刷新状态设置与骨架内容匹配的卡片高度。否则完整卡片骨架屏无法在短卡片上正常显示。

Also applies to: 510-535, 1090-1121

🟡 Other comments (1)
internal/control/operation_error_test.go-46-49 (1)

46-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

让新增用例真正覆盖操作映射分支。

groupNotFoundError() 是带资源类型的错误。serviceErrorMessageID 会在 internal/control/server.go 的 Line [1026-1032] 直接返回 group.not_found,因此不会执行 Line [1033-1038] 的操作名分支。即使移除 download_all_group_credentials,此测试仍会通过。

请补充使用不带 controlResourceNotFoundError 类型信息的 app_errors.ErrResourceNotFound 用例,并保留当前类型化错误用例用于覆盖另一条路径。


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 57f06f09-2642-4af0-848a-f15d7fddb9e3

📥 Commits

Reviewing files that changed from the base of the PR and between 4388323 and ae01681.

📒 Files selected for processing (5)
  • internal/control/operation_error_test.go
  • internal/control/server.go
  • web/src/features/groups/credentials/GroupCredentialBatchBar.vue
  • web/src/features/groups/credentials/GroupCredentialsTab.vue
  • web/src/features/groups/credentials/SubscriptionAccountCard.vue

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@tbphp
tbphp merged commit 92c52ec into v2 Aug 22, 2026
7 checks passed
@tbphp
tbphp deleted the codex/subscription-bulk-operations branch August 22, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants