feat(subscription): 增加账号批量操作 - #455
Conversation
📝 WalkthroughWalkthrough后端新增凭据全量批量操作和订阅组凭据下载接口。前端新增全量下载、批量同步、启用和停用交互,并更新账户卡片状态展示。OAuth JSON 导入支持一次选择多个文件,并显示导入结果统计。对话框、路由契约和英文、日文、中文文案同步更新。 Merge Risk: 🔵 Low · up to 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)
✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
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. Comment |
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
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
📒 Files selected for processing (23)
internal/container/route_contract_test.gointernal/control/credential_actions.gointernal/control/credential_actions_test.gointernal/control/credential_api_foundation_test.gointernal/control/credential_batch_order_test.gointernal/control/credential_mutations.gointernal/control/credentials.gointernal/control/http_routes.gointernal/control/server.goweb/src/api/control/types.tsweb/src/app/resources/credentials.tsweb/src/components/ui/AppConfirmDialog.vueweb/src/components/ui/AppDialog.vueweb/src/features/groups/credentials/GroupCredentialBatchBar.vueweb/src/features/groups/credentials/GroupCredentialsTab.vueweb/src/features/groups/credentials/SubscriptionAccountCard.vueweb/src/features/import/SubscriptionCredentialStager.vueweb/src/i18n/locales/en-US/group.tsweb/src/i18n/locales/en-US/import.tsweb/src/i18n/locales/ja-JP/group.tsweb/src/i18n/locales/ja-JP/import.tsweb/src/i18n/locales/zh-CN/group.tsweb/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.
There was a problem hiding this comment.
🟡 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
bulkActionsBusyintentionally includescredentialsQuery.isFetching, but the new batch toolbar only receivesbatchBusy || 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
batchObservationPendingas 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 untilPromise.allsettles, 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
succeededis 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.
|
已在 ae01681 一并修复 Review 摘要中的相关项:下载全部时缺失分组文案、暗色主题选择框、筛选/分页变化后的失败项选择恢复、批量栏 ARIA 语义,以及停用账号的手动额度同步。按已确认方案,批量同步不会无条件重新请求当前页。 |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
internal/control/operation_error_test.gointernal/control/server.goweb/src/features/groups/credentials/GroupCredentialBatchBar.vueweb/src/features/groups/credentials/GroupCredentialsTab.vueweb/src/features/groups/credentials/SubscriptionAccountCard.vue
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
关联 Issue / Related Issue
无 / None
变更内容 / Change Content
Bug 修复 / Bug fix
新功能 / New feature
其他改动 / Other changes
增加订阅账号批量操作菜单,支持多文件容错导入、全量导出和当前页额度刷新
批量刷新复用单账号接口并限制为 4 路并发,全部完成后统一刷新当前页
单账号和批量额度刷新期间使用整卡骨架屏,保持卡片尺寸稳定
新增全量订阅账号导出 API;响应禁止缓存,不涉及数据迁移
自查清单 / Checklist
make check,或在说明中写明无法运行的原因和未验证范围。 / I ranmake check, or documented why it could not run and what remains unverified.Summary by CodeRabbit
新功能
改进