feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮#81
Conversation
为订阅和查看GitHub按钮添加中文标题支持,根据当前语言环境显示相应文本
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a tabbed SettingsPanel and multiple settings subpanels (AI, WebDAV, Backup, Backend, Category, Data Management, General); implements repository multi-select and bulk actions with toolbar/modals; introduces README modal + Markdown renderer, AI analysis optimizer, BackToTop, ErrorBoundary, polyfills, many store/type migrations, UI/UX refinements, and backend request/validation hardening. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant BackendPanel
participant BackendService
participant AppStore
participant Alert
User->>BackendPanel: Click "Test Connection" / "Sync to Backend" / "Sync from Backend"
BackendPanel->>AppStore: setBackendApiSecret (if provided)
BackendPanel->>BackendService: init()
BackendService-->>BackendPanel: init result
BackendPanel->>BackendService: checkHealth() / verifyAuth()
BackendService-->>BackendPanel: health/auth result
BackendPanel->>BackendService: syncToBackend(data) / fetchFromBackend()
BackendService-->>BackendPanel: sync/fetch result
BackendPanel->>AppStore: apply fetched data (when present)
BackendPanel->>Alert: show success/failure
sequenceDiagram
participant User
participant BackupPanel
participant WebDAVService
participant AppStore
participant Alert
User->>BackupPanel: Click "Backup"
BackupPanel->>AppStore: gather data (repos, releases, configs, categories)
BackupPanel->>WebDAVService: uploadBackup(jsonPayload)
WebDAVService-->>BackupPanel: upload result
BackupPanel->>AppStore: set lastBackup (on success)
BackupPanel->>Alert: show success/failure
User->>BackupPanel: Click "Restore"
BackupPanel->>WebDAVService: list backups -> download latest
WebDAVService-->>BackupPanel: backup JSON
BackupPanel->>AppStore: restore repos/releases/configs/categories (preserve masked secrets)
BackupPanel->>Alert: summary success/failure
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
新增设置模块的多个面板组件,包括通用设置、WebDAV配置、备份恢复、分类管理、后端服务和AI配置。每个面板提供相应的功能界面和交互逻辑,支持多语言切换。 - 通用设置面板支持语言切换和版本检查 - WebDAV面板提供配置管理和测试功能 - 备份面板支持数据备份和恢复操作 - 分类面板管理自定义分类和默认分类显示 - 后端面板处理服务器连接和数据同步 - AI面板配置AI服务参数和测试连接
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (8)
src/components/settings/GeneralPanel.tsx (1)
81-81: Hardcoded version string should be centralized.The version
v0.3.0is hardcoded in the translation string. This will likely get out of sync with actual releases. Consider importing from a central constant or package.json.♻️ Proposed centralized version approach
+// At the top of the file or in a constants file: +const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'v0.3.0'; // In the component: - <p className="text-sm text-gray-600 dark:text-gray-400 mb-1"> - {t('当前版本: v0.3.0', 'Current Version: v0.3.0')} - </p> + <p className="text-sm text-gray-600 dark:text-gray-400 mb-1"> + {t(`当前版本: ${APP_VERSION}`, `Current Version: ${APP_VERSION}`)} + </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/GeneralPanel.tsx` at line 81, Replace the hardcoded "v0.3.0" in the GeneralPanel JSX translation call by sourcing the app version from a single source of truth (e.g., import VERSION from package.json or a central constants module) and pass it into the translation (use a translation key with a placeholder or build the localized string dynamically) so the displayed version is always the imported VERSION; update the call site where t(...) is used in GeneralPanel to use that imported VERSION value instead of the literal string.src/components/settings/CategoryPanel.tsx (2)
41-46: ID generation withDate.now()could cause collisions.Using
Date.now()for ID generation is simple but could produce duplicates if a user rapidly adds multiple categories. Consider using a more robust ID generation approach.♻️ Use crypto.randomUUID() for robust ID generation
const newCategory = { - id: `custom-${Date.now()}`, + id: `custom-${crypto.randomUUID()}`, name: newCategoryName.trim(), icon: newCategoryIcon, isCustom: true, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 41 - 46, The newCategory object uses id: `custom-${Date.now()}` which can collide; update the ID generation in the CategoryPanel component to use a robust UUID (e.g., call crypto.randomUUID()) instead of Date.now() when constructing newCategory (referencing the newCategory variable and newCategoryName/newCategoryIcon fields), and add a small fallback (e.g., fallback to a random string) if crypto.randomUUID is undefined to preserve compatibility.
54-58: Type mismatch inhandleStartEditparameter.The function parameter uses
{ id: string; name: string; icon: string }but based on the context snippet, the category objects fromgetAllCategorieshave alabelfield instead ofname. This may causecategory.nameto beundefinedwhen editing default categories (though this function is only called for custom categories which do havename).The current code works because it's only invoked for
customCategories, but the type annotation is misleading.♻️ Clarify type or use existing Category type
-const handleStartEdit = (category: { id: string; name: string; icon: string }) => { +const handleStartEdit = (category: typeof customCategories[number]) => { setEditingId(category.id); setEditName(category.name); setEditIcon(category.icon); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 54 - 58, The parameter type for handleStartEdit is incorrect—replace the inline type { id: string; name: string; icon: string } with the shared Category type (or a union that matches both default and custom shapes) and use the correct property (label for defaults) when reading values; specifically update handleStartEdit to accept Category (or Category | CustomCategory) and setEditName to use category.name ?? category.label, keeping calls to setEditingId and setEditIcon unchanged so editing works for both default (label) and custom (name) categories.src/components/settings/AIConfigPanel.tsx (1)
107-107: Undocumented migration from 'minimal' to 'low' forreasoningEffort.The ternary silently maps
'minimal'to'low'. If this is intentional (e.g., deprecated value migration), consider adding a comment explaining why. If'minimal'is no longer valid, this migration should perhaps also happen on save to persist the corrected value.- reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || '', + // Migration: 'minimal' was renamed to 'low' in a previous version + reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || '',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/AIConfigPanel.tsx` at line 107, The code silently maps the deprecated 'minimal' value to 'low' when computing reasoningEffort in AIConfigPanel (reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || ''), so add a short inline comment explaining this migration and its reason, and also update the save/update path that persists settings (the form submit or save handler used by AIConfigPanel) to normalize config.reasoningEffort from 'minimal' to 'low' before writing so the corrected value is persisted.src/components/settings/BackupPanel.tsx (1)
105-106: Backup file selection relies on alphabetical sort of filenames.The code selects the latest backup via
backupFiles.sort().reverse()[0], which works because ISO date strings (github-stars-backup-YYYY-MM-DD.json) sort correctly alphabetically. However, this is fragile if:
- The filename format changes
- Multiple backups occur on the same day (no timestamp disambiguation)
Consider adding a timestamp to filenames or using file metadata for selection.
♻️ Add timestamp to backup filename for robustness
- const filename = `github-stars-backup-${new Date().toISOString().split('T')[0]}.json`; + const filename = `github-stars-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;This produces filenames like
github-stars-backup-2026-04-14T18-30-00-000Z.jsonwhich:
- Still sort correctly alphabetically
- Support multiple backups per day
- Include full timestamp for clarity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 105 - 106, The current selection of the latest backup uses alphabetical sorting of backupFiles and picks latestBackup via backupFiles.sort().reverse()[0], which is fragile; change the backup creation and selection logic so filenames include ISO timestamps (e.g., use Date.toISOString with characters safe for filenames) and/or select latest by file metadata: when creating backups, generate names like github-stars-backup-YYYY-MM-DDTHH-mm-ss-SSSZ.json, and when picking the latest in BackupPanel.tsx replace the alphabetical heuristic with either (a) sorting backupFiles by parsed timestamp extracted from the filename (from the new timestamp suffix) or (b) fetching file metadata and choosing the file with the newest modified timestamp before calling webdavService.downloadFile(latestBackup). Ensure to update any code that produces the backup filename to the new timestamp format so the selector works reliably.src/components/SettingsPanel.tsx (1)
143-161: Mobile tab navigation may have horizontal scrolling issues.The mobile tab selector uses
overflow-x-autowhich allows horizontal scrolling, but there's no visual indicator (like a scroll shadow or fade) to hint that more tabs exist off-screen. With 6 tabs, users on small screens may not realize they can scroll.Consider adding scroll indicators or restructuring mobile navigation:
- Add gradient fade on edges when scrollable
- Use a dropdown/select for mobile instead of horizontal tabs
- Add left/right scroll arrows
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 143 - 161, The mobile tab strip (the container with className "md:hidden ... overflow-x-auto" that renders tabs from the tabs array and uses setActiveTab/activeTab) needs a visual scroll indicator; add left/right gradient overlay elements positioned at the container edges and toggle their visibility based on the nav's scroll position (attach an onScroll handler to the <nav> element to compute scrollLeft, scrollWidth and clientWidth), or alternatively replace the horizontal strip with a native <select> for very small screens; ensure the gradient overlays are only shown when scrollLeft>0 (show left) or scrollLeft+clientWidth<scrollWidth (show right) so users know more tabs exist.src/components/settings/BackendPanel.tsx (2)
68-90: Partial sync failure leaves data in inconsistent state.If any sync operation in
handleSyncToBackendthrows (e.g.,syncReleasesfails aftersyncRepositoriessucceeds), the user gets an error alert but the backend is left in a partially synced state. Consider either:
- Adding per-operation error handling with rollback
- Documenting this behavior to users
- Implementing a transactional approach if the backend supports it
♻️ Add per-operation tracking for better error reporting
const handleSyncToBackend = async () => { if (!backend.isAvailable) { alert(t('后端不可用', 'Backend not available')); return; } setIsSyncingToBackend(true); + const results = { repos: false, releases: false, ai: false, webdav: false, settings: false }; try { await backend.syncRepositories(repositories); + results.repos = true; await backend.syncReleases(releases); + results.releases = true; await backend.syncAIConfigs(aiConfigs); + results.ai = true; await backend.syncWebDAVConfigs(webdavConfigs); + results.webdav = true; await backend.syncSettings({ hiddenDefaultCategoryIds }); + results.settings = true; alert(t( `已同步到后端:仓库 ${repositories.length},发布 ${releases.length},AI配置 ${aiConfigs.length},WebDAV配置 ${webdavConfigs.length}`, `Synced to backend: repos ${repositories.length}, releases ${releases.length}, AI configs ${aiConfigs.length}, WebDAV configs ${webdavConfigs.length}` )); } catch (error) { console.error('Sync to backend failed:', error); - alert(`${t('同步失败', 'Sync failed')}: ${(error as Error).message}`); + const syncedItems = Object.entries(results).filter(([, v]) => v).map(([k]) => k).join(', '); + alert(`${t('同步部分失败', 'Sync partially failed')}: ${(error as Error).message}\n${t('已同步', 'Synced')}: ${syncedItems || t('无', 'none')}`); } finally { setIsSyncingToBackend(false); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackendPanel.tsx` around lines 68 - 90, handleSyncToBackend performs several sequential backend calls (backend.syncRepositories, syncReleases, syncAIConfigs, syncWebDAVConfigs, syncSettings) and currently stops on the first error leaving the backend partially updated; update handleSyncToBackend to wrap each sync call with per-operation try/catch that records which operations succeeded and either attempts compensating rollback calls for already-applied operations (using the backend's delete/undo APIs or a new backend.rollback/transaction API if available) or collects and surfaces a detailed error summary to the user, and ensure setIsSyncingToBackend(false) still runs in finally; reference these symbols: handleSyncToBackend, backend.syncRepositories, backend.syncReleases, backend.syncAIConfigs, backend.syncWebDAVConfigs, backend.syncSettings.
111-122: Empty data arrays are silently ignored during restore.The conditionals like
if (repoData.repositories.length > 0)mean that if the backend returns an empty array (legitimate case: user cleared all repos), the local data won't be updated. This may be intentional to prevent accidental data loss, but could also cause confusion when users expect their empty state to sync.Consider either:
- Adding a comment explaining this is intentional protection
- Or allowing empty arrays to clear local state (after the confirm dialog already warned about overwrite)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackendPanel.tsx` around lines 111 - 122, The current checks (e.g., if (repoData.repositories.length > 0) setRepositories(...)) silently ignore legitimate empty arrays and therefore won't clear local state during restore; remove those length checks and always call setRepositories(repoData.repositories ?? []) and likewise always call setReleases(releaseData.releases ?? []), setAIConfigs(aiConfigData ?? []), and setWebDAVConfigs(webdavConfigData ?? []), so empty backend arrays correctly clear local state (keep the existing overwrite/confirm dialog as the guard). If you intended to keep the protection instead, add a clear comment above these conditionals explaining that empty arrays are intentionally ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Around line 88-92: When saving an edited config, don't pass the entire config
object to updateAIConfig because the store expects a partial update and
config.isActive is currently being reset to false; instead, look up the existing
config by editingId (e.g., from the aiConfigs state or a getAIConfigById
helper), build a minimal updates object that excludes id and merges fields to
change while explicitly preserving existingConfig.isActive, and call
updateAIConfig(editingId, updates); for new entries continue to call
addAIConfig(config).
In `@src/components/settings/BackendPanel.tsx`:
- Around line 123-132: The reconciliation currently iterates over
hiddenDefaultCategoryIds (local) to show categories then over
settingsData.hiddenDefaultCategoryIds to hide — missing categories that are
visible locally but should be hidden per fetched data. Replace this by either:
(A) iterating the fetched list settingsData.hiddenDefaultCategoryIds and calling
hideDefaultCategory for each string, and then iterating the local
hiddenDefaultCategoryIds and calling showDefaultCategory for any id not present
in the fetched list; or (B) atomically replace the store value with
settingsData.hiddenDefaultCategoryIds using setHiddenDefaultCategoryIds if
available. Use the variables hiddenDefaultCategoryIds,
settingsData.hiddenDefaultCategoryIds and the functions
showDefaultCategory/hideDefaultCategory (or setHiddenDefaultCategoryIds) to
implement the fix.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 321-322: The backup description text in BackupPanel.tsx
incorrectly says secrets are "encrypted" — update the translation strings used
in the two list items (the t(...) calls that currently read 'AI 服务配置(密钥已加密)' /
'AI service configurations (keys encrypted)' and 'WebDAV 配置(密码已加密)' / 'WebDAV
configurations (passwords encrypted)') to indicate that secrets are
masked/obfuscated (e.g., "密钥已脱敏" / "keys masked" and "密码已脱敏" / "passwords
masked") so the UI correctly reflects that values are replaced with '***' rather
than actually encrypted.
- Around line 133-146: The code uses the stale local variable
hiddenDefaultCategoryIds (from earlier destructuring) when calling
showDefaultCategory, so visibility reconciliation can be wrong after restore;
instead, read the fresh/current hiddenDefaultCategoryIds from the store right
before you iterate (e.g. via store.getState() or the component selector) and
iterate that fresh array to call showDefaultCategory, then iterate
backupData.hiddenDefaultCategoryIds to call hideDefaultCategory; reference the
showDefaultCategory and hideDefaultCategory calls and replace the first loop's
source with the live state value rather than the previously destructured
hiddenDefaultCategoryIds.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 60-79: handleSaveEdit currently finds the category by editingId
and does a deleteCustomCategory + addCustomCategory which causes UI flicker and
loss of cascading updates; instead call the store's updateCustomCategory with
the category id and the updated fields (name: editName.trim(), icon: editIcon)
from useAppStore to perform an atomic update (preserve list position and trigger
repository cascade), and then clear state (setEditingId(null), setEditName(''),
setEditIcon('')) as before while keeping the empty-name validation.
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 52-60: The current config object creation in WebDAVPanel (variable
config) always sets isActive: false which deactivates an existing config when
editing; change the logic in the save/submit handler that constructs the
WebDAVConfig so that when editingId is present you lookup the existing
WebDAVConfig (by editingId) from the component's configs/state/props and reuse
its isActive value, otherwise default to false for new configs; update the code
path that constructs config (the block referencing id: editingId ||
Date.now().toString(), name: form.name, url: ..., etc.) to set isActive =
(editingId ? existingConfig.isActive : false).
- Around line 45-69: handleSave currently displays raw messages from
WebDAVService.validateConfig (which are hardcoded Chinese) via
alert(errors.join('\n')), breaking localization; update handleSave to translate
those errors before showing them: call WebDAVService.validateConfig(form) as
before, then map each returned error string to a translation key (or fallback)
using the i18n t function (e.g., translate known Chinese messages like "WebDAV
URL是必需的" to a t('settings.webdav.validation.urlRequired') key), and finally call
alert(translatedErrors.join('\n')); keep references to handleSave,
WebDAVService.validateConfig, and the alert call so reviewers can locate and
verify the change.
---
Nitpick comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Line 107: The code silently maps the deprecated 'minimal' value to 'low' when
computing reasoningEffort in AIConfigPanel (reasoningEffort:
(config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || ''),
so add a short inline comment explaining this migration and its reason, and also
update the save/update path that persists settings (the form submit or save
handler used by AIConfigPanel) to normalize config.reasoningEffort from
'minimal' to 'low' before writing so the corrected value is persisted.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 68-90: handleSyncToBackend performs several sequential backend
calls (backend.syncRepositories, syncReleases, syncAIConfigs, syncWebDAVConfigs,
syncSettings) and currently stops on the first error leaving the backend
partially updated; update handleSyncToBackend to wrap each sync call with
per-operation try/catch that records which operations succeeded and either
attempts compensating rollback calls for already-applied operations (using the
backend's delete/undo APIs or a new backend.rollback/transaction API if
available) or collects and surfaces a detailed error summary to the user, and
ensure setIsSyncingToBackend(false) still runs in finally; reference these
symbols: handleSyncToBackend, backend.syncRepositories, backend.syncReleases,
backend.syncAIConfigs, backend.syncWebDAVConfigs, backend.syncSettings.
- Around line 111-122: The current checks (e.g., if
(repoData.repositories.length > 0) setRepositories(...)) silently ignore
legitimate empty arrays and therefore won't clear local state during restore;
remove those length checks and always call setRepositories(repoData.repositories
?? []) and likewise always call setReleases(releaseData.releases ?? []),
setAIConfigs(aiConfigData ?? []), and setWebDAVConfigs(webdavConfigData ?? []),
so empty backend arrays correctly clear local state (keep the existing
overwrite/confirm dialog as the guard). If you intended to keep the protection
instead, add a clear comment above these conditionals explaining that empty
arrays are intentionally ignored.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 105-106: The current selection of the latest backup uses
alphabetical sorting of backupFiles and picks latestBackup via
backupFiles.sort().reverse()[0], which is fragile; change the backup creation
and selection logic so filenames include ISO timestamps (e.g., use
Date.toISOString with characters safe for filenames) and/or select latest by
file metadata: when creating backups, generate names like
github-stars-backup-YYYY-MM-DDTHH-mm-ss-SSSZ.json, and when picking the latest
in BackupPanel.tsx replace the alphabetical heuristic with either (a) sorting
backupFiles by parsed timestamp extracted from the filename (from the new
timestamp suffix) or (b) fetching file metadata and choosing the file with the
newest modified timestamp before calling
webdavService.downloadFile(latestBackup). Ensure to update any code that
produces the backup filename to the new timestamp format so the selector works
reliably.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 41-46: The newCategory object uses id: `custom-${Date.now()}`
which can collide; update the ID generation in the CategoryPanel component to
use a robust UUID (e.g., call crypto.randomUUID()) instead of Date.now() when
constructing newCategory (referencing the newCategory variable and
newCategoryName/newCategoryIcon fields), and add a small fallback (e.g.,
fallback to a random string) if crypto.randomUUID is undefined to preserve
compatibility.
- Around line 54-58: The parameter type for handleStartEdit is incorrect—replace
the inline type { id: string; name: string; icon: string } with the shared
Category type (or a union that matches both default and custom shapes) and use
the correct property (label for defaults) when reading values; specifically
update handleStartEdit to accept Category (or Category | CustomCategory) and
setEditName to use category.name ?? category.label, keeping calls to
setEditingId and setEditIcon unchanged so editing works for both default (label)
and custom (name) categories.
In `@src/components/settings/GeneralPanel.tsx`:
- Line 81: Replace the hardcoded "v0.3.0" in the GeneralPanel JSX translation
call by sourcing the app version from a single source of truth (e.g., import
VERSION from package.json or a central constants module) and pass it into the
translation (use a translation key with a placeholder or build the localized
string dynamically) so the displayed version is always the imported VERSION;
update the call site where t(...) is used in GeneralPanel to use that imported
VERSION value instead of the literal string.
In `@src/components/SettingsPanel.tsx`:
- Around line 143-161: The mobile tab strip (the container with className
"md:hidden ... overflow-x-auto" that renders tabs from the tabs array and uses
setActiveTab/activeTab) needs a visual scroll indicator; add left/right gradient
overlay elements positioned at the container edges and toggle their visibility
based on the nav's scroll position (attach an onScroll handler to the <nav>
element to compute scrollLeft, scrollWidth and clientWidth), or alternatively
replace the horizontal strip with a native <select> for very small screens;
ensure the gradient overlays are only shown when scrollLeft>0 (show left) or
scrollLeft+clientWidth<scrollWidth (show right) so users know more tabs exist.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 57bf0ba5-40c3-46cc-9189-078556f65dfc
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (8)
src/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.ts
添加数据管理面板组件,支持查看数据统计和选择性删除不同类型的数据 包括仓库数据、发布信息、AI配置、WebDAV配置和分类设置 提供危险区域可删除所有数据,需GitHub用户名验证
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/components/settings/DataManagementPanel.tsx (2)
79-87: Toast timeouts may fire after component unmount.The
setTimeoutcalls inshowSuccessandshowErroraren't cleaned up on unmount, which could cause state updates on an unmounted component.Consider using
useEffectcleanup or storing timeout IDs in refs for cancellation.♻️ Optional: Add timeout cleanup
+import React, { useState, useCallback, useRef, useEffect } from 'react'; + +// Inside component: +const successTimeoutRef = useRef<NodeJS.Timeout>(); +const errorTimeoutRef = useRef<NodeJS.Timeout>(); + +useEffect(() => { + return () => { + if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current); + if (errorTimeoutRef.current) clearTimeout(errorTimeoutRef.current); + }; +}, []); const showSuccess = useCallback((message: string) => { setShowSuccessMessage(message); - setTimeout(() => setShowSuccessMessage(null), 3000); + successTimeoutRef.current = setTimeout(() => setShowSuccessMessage(null), 3000); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 79 - 87, The setTimeouts in showSuccess and showError can run after unmount causing state updates on an unmounted component; modify these functions to store their timeout IDs (e.g., in refs like successTimeoutRef and errorTimeoutRef) and clear any existing timeout before creating a new one, and add a useEffect cleanup that clears both refs on unmount; ensure you still call setShowSuccessMessage / setShowErrorMessage and clear the timeout refs when the timeout runs or when unmounting.
183-204: Category deletion uses potentially unsafe iteration pattern.The
forEachloop deletes categories one by one, but ifdeleteCustomCategorytriggers any async side effects, they won't be awaited. Additionally, iterating overstore.customCategorieswhile mutating it viadeleteCustomCategorycould cause unexpected behavior if the store's array reference changes mid-iteration.Consider using a safer approach:
♻️ Suggested safer deletion pattern
const deleteCategorySettings = async () => { try { const store = useAppStore.getState(); - // Reset category-related state - store.customCategories.forEach((cat) => { - store.deleteCustomCategory(cat.id); - }); + // Get category IDs first to avoid mutation during iteration + const categoryIds = store.customCategories.map((cat) => cat.id); + categoryIds.forEach((id) => store.deleteCustomCategory(id)); // Clear hidden default categories useAppStore.setState({ hiddenDefaultCategoryIds: [] }); - await clearAllStorage(); addLog(t('删除分类显示设置数据', 'Delete category display settings'), true);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 183 - 204, The deleteCategorySettings function is mutating store.customCategories while iterating it with forEach which is unsafe if deleteCustomCategory has async side-effects or changes the array; fix by capturing a stable list of IDs first (const ids = useAppStore.getState().customCategories.map(c=>c.id)), then iterate that list using a sequential for...of that awaits deleteCustomCategory(id) (or Promise.all(ids.map(id=>deleteCustomCategory(id))) if parallel deletion is safe), and avoid referencing store.customCategories inside the deletion loop; keep useAppStore.getState(), deleteCustomCategory, clearAllStorage, addLog and showSuccess/showError usages intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 89-109: The clearAllStorage function is being invoked by selective
delete handlers (deleteRepositories, deleteReleases, deleteAIConfigs,
deleteWebDAVConfigs, deleteCategorySettings) and it unconditionally wipes the
github-stars-manager IndexedDB entry and other persisted state; remove calls to
clearAllStorage from those selective delete functions and instead only perform
the in-memory Zustand slice resets (i.e., call the specific slice reset/clear
methods you already have for repositories, releases, aiConfigs, webDAVConfigs,
categorySettings) so the persist middleware updates storage automatically; keep
clearAllStorage reserved only for deleteAllData which should continue to fully
clear localStorage, sessionStorage and IndexedDB.
- Around line 104-108: The catch in the indexedDBStorage.removeItem call inside
the clearAllStorage function silently swallows failures; change it to surface
the error (either rethrow the caught error or return a failure status) so
callers can detect partial failure. Specifically, update the catch around
indexedDBStorage.removeItem('github-stars-manager') to either throw the error
(throw error) or set/return a boolean/result object (e.g., { success: false,
reason: error }) and then adjust callers that show the success toast to check
this result before reporting success.
In `@src/components/SettingsPanel.tsx`:
- Around line 131-177: The layout bug comes from the top-level container div
whose class is "flex flex-1 overflow-hidden" causing the mobile nav (the div
with "md:hidden" that renders the mobile tab selector) and the content area (the
div containing renderTabContent()) to sit in a row; change that container to use
a column layout on small screens and switch to row at md (e.g., make it
responsive like "flex flex-col md:flex-row ...") so the mobile nav stacks above
the content while desktop keeps the sidebar/content row arrangement.
---
Nitpick comments:
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 79-87: The setTimeouts in showSuccess and showError can run after
unmount causing state updates on an unmounted component; modify these functions
to store their timeout IDs (e.g., in refs like successTimeoutRef and
errorTimeoutRef) and clear any existing timeout before creating a new one, and
add a useEffect cleanup that clears both refs on unmount; ensure you still call
setShowSuccessMessage / setShowErrorMessage and clear the timeout refs when the
timeout runs or when unmounting.
- Around line 183-204: The deleteCategorySettings function is mutating
store.customCategories while iterating it with forEach which is unsafe if
deleteCustomCategory has async side-effects or changes the array; fix by
capturing a stable list of IDs first (const ids =
useAppStore.getState().customCategories.map(c=>c.id)), then iterate that list
using a sequential for...of that awaits deleteCustomCategory(id) (or
Promise.all(ids.map(id=>deleteCustomCategory(id))) if parallel deletion is
safe), and avoid referencing store.customCategories inside the deletion loop;
keep useAppStore.getState(), deleteCustomCategory, clearAllStorage, addLog and
showSuccess/showError usages intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 708c2265-3b60-4586-8c41-ce051c81d26c
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (3)
src/components/SettingsPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/settings/index.ts
- 新增侧边栏折叠状态管理,支持快捷键(Ctrl+B)切换 - 为移动端和桌面端分别优化侧边栏显示方式 - 添加返回顶部按钮组件,提升长页面浏览体验 - 更新相关状态管理以支持新功能
|
Tip For best results, initiate chat on the files or code changes.
Feel free to share more details and I'll be happy to help! |
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/CategorySidebar.tsx (1)
273-292: Consider the UX impact of limiting collapsed categories to 8.The collapsed sidebar shows only the first 8 categories (
allCategories.slice(0, 8)). If a user has many custom categories, they won't be accessible in collapsed mode without expanding the sidebar first. This may be intentional, but worth noting that custom categories added after the 8th position won't have quick access in collapsed mode.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 273 - 292, The collapsed sidebar currently limits rendered icons with allCategories.slice(0, 8), which hides user-added categories beyond the 8th; change this by removing the hard-coded slice and either render allCategories (with a vertical scroll via CSS) or make the limit configurable (e.g., collapsedLimit prop/constant) and use that instead; update the map call around allCategories.slice(0, 8) in the CategorySidebar component (the block using selectedCategory, onCategorySelect, and category.icon) so collapsed mode shows all accessible categories or exposes a "more" affordance rather than silently truncating.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/BackToTop.tsx`:
- Around line 52-53: The aria-label and title in BackToTop.tsx are hardcoded in
Chinese; replace them with i18n lookups. Import and use the project's i18n
utility (e.g., useTranslation/t or translate function used elsewhere) inside the
BackToTop component and replace aria-label="回到顶部" and title="回到顶部" with the
translated key (e.g., t('backToTop') or translate('backToTop')); add the
corresponding i18n key/value entries for both Chinese and English in the locale
files. Ensure the call is available in the component scope so both aria-label
and title use the same translated string.
In `@src/components/CategorySidebar.tsx`:
- Around line 58-68: The global keyboard handler inside the useEffect
(handleKeyDown) toggles the sidebar on Ctrl/Cmd+B even when focus is inside
inputs, textareas, or contenteditable elements; update handleKeyDown to first
inspect document.activeElement and skip handling if the active element is an
input, textarea, or has isContentEditable === true (also consider checking
common editable roles like role="textbox"), then only call toggleSidebar() when
not focused in an editable element; keep the existing e.preventDefault() and key
checks but add this early-return guard within handleKeyDown to avoid interfering
with in-field shortcuts.
---
Nitpick comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 273-292: The collapsed sidebar currently limits rendered icons
with allCategories.slice(0, 8), which hides user-added categories beyond the
8th; change this by removing the hard-coded slice and either render
allCategories (with a vertical scroll via CSS) or make the limit configurable
(e.g., collapsedLimit prop/constant) and use that instead; update the map call
around allCategories.slice(0, 8) in the CategorySidebar component (the block
using selectedCategory, onCategorySelect, and category.icon) so collapsed mode
shows all accessible categories or exposes a "more" affordance rather than
silently truncating.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5742860d-eb4f-481f-a06d-0ac5053ac17b
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (5)
src/App.tsxsrc/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/store/useAppStore.tssrc/types/index.ts
✅ Files skipped from review due to trivial changes (1)
- src/types/index.ts
|
@coderabbitai generate docstrings |
✅ Actions performedInitiated docstring generation; will generate only if new commits exist. |
|
Caution Docstrings generation - FAILED An unexpected error occurred while opening a pull request: Cannot open a stacked PR on a forked repository. |
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/components/settings/WebDAVPanel.tsx (1)
61-78:⚠️ Potential issue | 🟡 MinorBug: Editing a config always sets
isActive: false, potentially deactivating an active config.When editing an existing WebDAV config, the saved config always has
isActive: false(line 68). If the user edits their currently active config, it will be deactivated upon save.This mirrors the fix already applied in
AIConfigPanel.tsx(lines 74-90).🐛 Proposed fix to preserve isActive state
const handleSave = () => { const errors = WebDAVService.validateConfig(form); if (errors.length > 0) { // ... translation logic } + const existingConfig = editingId ? webdavConfigs.find(c => c.id === editingId) : null; + const config: WebDAVConfig = { id: editingId || Date.now().toString(), name: form.name, url: form.url.replace(/\/$/, ''), username: form.username, password: form.password, path: form.path, - isActive: false, + isActive: existingConfig?.isActive ?? false, }; if (editingId) { updateWebDAVConfig(editingId, config); } else { addWebDAVConfig(config); } resetForm(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 61 - 78, When building the config in WebDAVPanel.tsx, you currently always set isActive: false which deactivates an existing config on edit; change the logic so that when editing (editingId is truthy) you preserve the existing config's isActive value instead of forcing false — retrieve the current config's isActive (or read it from state/props) and assign that to config.isActive before calling updateWebDAVConfig(editingId, config); keep addWebDAVConfig(config) creating new configs with isActive: false, and mirror the same preservation approach used in AIConfigPanel.tsx for reference.
🧹 Nitpick comments (5)
src/components/CategorySidebar.tsx (1)
282-298: Consider indicating when categories are truncated.The collapsed view displays only the first 8 categories via
allCategories.slice(0, 8). If users have more categories, there's no visual indication that additional categories exist. Consider adding a visual cue (e.g., a badge showing "+N more" or an overflow indicator) whenallCategories.length > 8.Additionally, the collapsed view loses the drag-and-drop functionality for quick category assignment that exists in the expanded view.
💡 Suggested improvement to show overflow indicator
{/* 折叠状态下的分类图标列表 */} <div className="flex flex-col items-center space-y-2"> {allCategories.slice(0, 8).map((category) => { const isSelected = selectedCategory === category.id; return ( <button key={category.id} onClick={() => onCategorySelect(category.id)} className={`w-8 h-8 flex items-center justify-center rounded-lg text-lg transition-all duration-200 ${ isSelected ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 ring-2 ring-blue-400' : 'hover:bg-gray-100 dark:hover:bg-gray-700' }`} title={category.name} > {category.icon} </button> ); })} + {allCategories.length > 8 && ( + <button + onClick={toggleSidebar} + className="w-8 h-8 flex items-center justify-center rounded-lg text-xs text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700" + title={t(`还有 ${allCategories.length - 8} 个分类`, `${allCategories.length - 8} more categories`)} + > + +{allCategories.length - 8} + </button> + )} </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 282 - 298, The collapsed category list currently slices allCategories to 8 items in CategorySidebar, hiding the rest without indication and dropping drag-and-drop; update the render logic around allCategories.slice(0, 8).map(...) to show an overflow indicator when allCategories.length > 8 (e.g., a button/badge like "+N more" using the remaining count) and ensure that the overflow indicator preserves the same keyboard/click behavior (calls onCategorySelect or opens the full list) and drag-and-drop affordance present in the expanded view; make the indicator use the same styling toggles as the category buttons (respecting selectedCategory) and reuse existing handlers/components so you don’t duplicate drag-and-drop logic.src/components/settings/CategoryPanel.tsx (1)
42-49: Minor:isCustom: trueis set redundantly.The store's
addCustomCategorymethod already addsisCustom: true(persrc/store/useAppStore.ts:413-415). Setting it here is harmless but redundant.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 42 - 49, The newCategory object in CategoryPanel.tsx redundantly sets isCustom: true before calling addCustomCategory; remove the isCustom property from the newCategory literal (keep id, name, icon) and let addCustomCategory (in useAppStore) set isCustom internally to avoid duplication and keep responsibility in one place.src/components/settings/AIConfigPanel.tsx (1)
121-121: Minor: Defensive migration from deprecated'minimal'to'low'.The ternary handles legacy
reasoningEffortvalues, but theAIReasoningEfforttype (from imports) likely doesn't include'minimal'. Consider adding a type annotation or comment explaining this is a migration for legacy data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/AIConfigPanel.tsx` at line 121, The mapping for config.reasoningEffort converts legacy 'minimal' to 'low' but lacks explicit typing/documentation; update the assignment for reasoningEffort to either cast the result to the AIReasoningEffort type or add a brief inline comment explaining this is a defensive migration for legacy values, referencing the config object and AIReasoningEffort type, so maintainers know why 'minimal' is handled despite not being in the current type.src/components/settings/BackupPanel.tsx (1)
99-106: Note: Backup filename uses date only, so multiple backups per day overwrite.Line 66 generates filenames using only the date (
YYYY-MM-DD), so multiple backups on the same day will overwrite each other. The restore logic (lines 105-106) correctly selects the latest by sorting. This is acceptable behavior but worth noting for users who expect to keep multiple backups per day.💡 Optional: Include timestamp for multiple backups per day
-const filename = `github-stars-backup-${new Date().toISOString().split('T')[0]}.json`; +const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); +const filename = `github-stars-backup-${timestamp}.json`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 99 - 106, Backup filenames currently use only the date so multiple backups in one day overwrite; update the backup filename generation (the code that builds the 'github-stars-backup-YYYY-MM-DD' name) to include a time component (e.g., HHMMSS or ISO timestamp) so each backup is unique per run, and keep the restore logic that filters via backupFiles and selects latestBackup (backupFiles.sort().reverse()[0]) unchanged so it continues to pick the newest file; ensure webdavService.downloadFile(latestBackup) still works with the new filename format.src/components/settings/BackendPanel.tsx (1)
123-136: Optimize category visibility reconciliation to match the pattern in autoSync.ts.The current implementation calls
hideDefaultCategory()for all server categories, relying on idempotency to avoid redundant operations. However,src/services/autoSync.ts(lines 153–164) already demonstrates a more efficient pattern: check the current state before calling hide/show functions, only triggering them when necessary.♻️ Refactor to match autoSync pattern
// 从服务端数据中隐藏所有应隐藏的分类 if (Array.isArray(settingsData.hiddenDefaultCategoryIds)) { - for (const categoryId of settingsData.hiddenDefaultCategoryIds) { - if (typeof categoryId === 'string') hideDefaultCategory(categoryId); - } - } - // 显示本地隐藏列表中但服务端没有隐藏的分类(即本地手动显示的) - if (Array.isArray(hiddenDefaultCategoryIds)) { - for (const categoryId of hiddenDefaultCategoryIds) { - if (typeof categoryId === 'string' && !settingsData.hiddenDefaultCategoryIds?.includes(categoryId)) { - showDefaultCategory(categoryId); - } + const nextHiddenIds = settingsData.hiddenDefaultCategoryIds.filter((id): id is string => typeof id === 'string'); + const currentHiddenIds = hiddenDefaultCategoryIds || []; + for (const id of currentHiddenIds) { + if (!nextHiddenIds.includes(id)) { + showDefaultCategory(id); + } + } + for (const id of nextHiddenIds) { + if (!currentHiddenIds.includes(id)) { + hideDefaultCategory(id); } } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackendPanel.tsx` around lines 123 - 136, The reconciliation loop currently blindly calls hideDefaultCategory/showDefaultCategory for IDs from settingsData.hiddenDefaultCategoryIds and hiddenDefaultCategoryIds; update it to follow the autoSync.ts pattern by checking current visibility before invoking actions so we only call hideDefaultCategory(categoryId) or showDefaultCategory(categoryId) when the category's existing visibility differs from the desired state. Use the same identifiers from this diff (settingsData.hiddenDefaultCategoryIds, hiddenDefaultCategoryIds) and the functions hideDefaultCategory / showDefaultCategory to implement the conditional checks and avoid redundant calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 61-78: When building the config in WebDAVPanel.tsx, you currently
always set isActive: false which deactivates an existing config on edit; change
the logic so that when editing (editingId is truthy) you preserve the existing
config's isActive value instead of forcing false — retrieve the current config's
isActive (or read it from state/props) and assign that to config.isActive before
calling updateWebDAVConfig(editingId, config); keep addWebDAVConfig(config)
creating new configs with isActive: false, and mirror the same preservation
approach used in AIConfigPanel.tsx for reference.
---
Nitpick comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 282-298: The collapsed category list currently slices
allCategories to 8 items in CategorySidebar, hiding the rest without indication
and dropping drag-and-drop; update the render logic around
allCategories.slice(0, 8).map(...) to show an overflow indicator when
allCategories.length > 8 (e.g., a button/badge like "+N more" using the
remaining count) and ensure that the overflow indicator preserves the same
keyboard/click behavior (calls onCategorySelect or opens the full list) and
drag-and-drop affordance present in the expanded view; make the indicator use
the same styling toggles as the category buttons (respecting selectedCategory)
and reuse existing handlers/components so you don’t duplicate drag-and-drop
logic.
In `@src/components/settings/AIConfigPanel.tsx`:
- Line 121: The mapping for config.reasoningEffort converts legacy 'minimal' to
'low' but lacks explicit typing/documentation; update the assignment for
reasoningEffort to either cast the result to the AIReasoningEffort type or add a
brief inline comment explaining this is a defensive migration for legacy values,
referencing the config object and AIReasoningEffort type, so maintainers know
why 'minimal' is handled despite not being in the current type.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 123-136: The reconciliation loop currently blindly calls
hideDefaultCategory/showDefaultCategory for IDs from
settingsData.hiddenDefaultCategoryIds and hiddenDefaultCategoryIds; update it to
follow the autoSync.ts pattern by checking current visibility before invoking
actions so we only call hideDefaultCategory(categoryId) or
showDefaultCategory(categoryId) when the category's existing visibility differs
from the desired state. Use the same identifiers from this diff
(settingsData.hiddenDefaultCategoryIds, hiddenDefaultCategoryIds) and the
functions hideDefaultCategory / showDefaultCategory to implement the conditional
checks and avoid redundant calls.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 99-106: Backup filenames currently use only the date so multiple
backups in one day overwrite; update the backup filename generation (the code
that builds the 'github-stars-backup-YYYY-MM-DD' name) to include a time
component (e.g., HHMMSS or ISO timestamp) so each backup is unique per run, and
keep the restore logic that filters via backupFiles and selects latestBackup
(backupFiles.sort().reverse()[0]) unchanged so it continues to pick the newest
file; ensure webdavService.downloadFile(latestBackup) still works with the new
filename format.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 42-49: The newCategory object in CategoryPanel.tsx redundantly
sets isCustom: true before calling addCustomCategory; remove the isCustom
property from the newCategory literal (keep id, name, icon) and let
addCustomCategory (in useAppStore) set isCustom internally to avoid duplication
and keep responsibility in one place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5294dc69-4871-43c3-93ea-440ebc24e21d
📒 Files selected for processing (9)
src/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/WebDAVPanel.tsx
✅ Files skipped from review due to trivial changes (1)
- src/components/settings/DataManagementPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/BackToTop.tsx
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
src/components/settings/WebDAVPanel.tsx (1)
61-75:⚠️ Potential issue | 🟠 MajorPreserve
isActivewhen editing an existing WebDAV config.Line 68 still hardcodes
isActive: false, so saving an edited active config will deactivate it.🐛 Suggested fix
- const config: WebDAVConfig = { - id: editingId || Date.now().toString(), - name: form.name, - url: form.url.replace(/\/$/, ''), - username: form.username, - password: form.password, - path: form.path, - isActive: false, - }; - if (editingId) { - updateWebDAVConfig(editingId, config); + const existingConfig = webdavConfigs.find(c => c.id === editingId); + if (!existingConfig) return; + + updateWebDAVConfig(editingId, { + name: form.name, + url: form.url.replace(/\/$/, ''), + username: form.username, + password: form.password, + path: form.path, + isActive: existingConfig.isActive, + }); } else { - addWebDAVConfig(config); + addWebDAVConfig({ + id: Date.now().toString(), + name: form.name, + url: form.url.replace(/\/$/, ''), + username: form.username, + password: form.password, + path: form.path, + isActive: false, + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 61 - 75, The code always sets isActive: false when building the WebDAVConfig, so edits will deactivate an active entry; change the construction to preserve the original isActive when editing by reading the existing config's isActive (e.g., fetch the current WebDAVConfig by editingId) and assign isActive: existing.isActive when editing, otherwise default to false for new configs; update the logic around updateWebDAVConfig and addWebDAVConfig to use this adjusted config so editingId paths keep the prior isActive value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/BackToTop.tsx`:
- Around line 24-29: The BackToTop component's useEffect currently only
registers the scroll listener, so initial visibility can be stale; inside the
same effect (where window.addEventListener and removeEventListener are used)
call toggleVisibility() once after adding the listener to initialize the button
state on mount, ensuring you still clean up with
window.removeEventListener(toggleVisibility) in the returned function.
- Around line 47-56: The BackToTop component currently only toggles
pointer-events via the isVisible CSS classes, which doesn't prevent keyboard
focus; update the interactive element (the BackToTop button rendered in
BackToTop) to be removed from the accessibility tree when hidden by: setting
aria-hidden={ !isVisible } and setting tabIndex to -1 when isVisible is false
(and restore tabIndex to 0 or undefined when true). Locate the root interactive
element in BackToTop (the element using isVisible classes and aria-label/title)
and add those accessibility guards so it cannot be focused or reached by
keyboard when visually hidden.
In `@src/components/CategorySidebar.tsx`:
- Around line 190-196: The icon-only sidebar buttons (e.g., the add-category
button wired to handleAddCategory and the collapsed category buttons in the same
component) rely only on title attributes which is not a robust accessible name;
update those buttons in CategorySidebar (including the button rendering the Plus
icon and the collapsed category buttons) to provide explicit accessible names by
adding aria-label attributes (use the same i18n key via t('添加分类','Add Category')
or equivalent) or include visually-hidden text inside the button so screen
readers get a proper name; ensure the aria-label text matches the
visible/tooltip text and keep the Plus icon as decorative (no role change
needed) so the screen reader announces the provided label.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 155-211: The restore currently merges configs; change it to fully
replace by first building the set of backup IDs (from backupData.aiConfigs and
backupData.webdavConfigs), then remove any local entries whose id is NOT in that
set (use removeAIConfig/removeWebDAVConfig), and only after pruning proceed with
the existing loop logic that calls updateAIConfig/addAIConfig and
updateWebDAVConfig/addWebDAVConfig; when applying entries preserve secrets if
the backup value is the mask ('***') by using the existing config's
apiKey/password as you already do. Ensure you compute currentMap and backupId
sets from aiConfigs/webdavConfigs and backupData.* before modifying collections
to avoid iteration issues.
- Around line 67-72: The current uploadFile(success) branch only handles the
true case and leaves users without feedback on failure; after calling
webdavService.uploadFile(filename, JSON.stringify(backupData, null, 2)) check
for a falsy return and in that branch call an error notification (e.g.,
alert(t('数据备份失败!','Data backup failed!')) or your app's toast), do not call
setLastBackup on failure, and also log the error (console.error or process
logger) so the failure path for uploadFile, filename, and backupData is explicit
and the UI spinner/disabled state is handled consistently.
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 89-109: clearAllStorage currently wipes too broadly (uses
sessionStorage.clear() and substring matches like "zustand"), risking deletion
of other apps' data; update clearAllStorage to only delete explicit app-owned
keys by maintaining a whitelist/prefix list (e.g., APP_KEY_PREFIX and explicit
key names) and iterate over localStorage and sessionStorage removing keys that
exactly match or startWith those prefixes, and remove only the specific
IndexedDB database/store name used by this app (retain the
indexedDBStorage.removeItem('github-stars-manager') call but do not call
sessionStorage.clear() or generic "zustand" substring matching); adjust the
logic in the clearAllStorage function to reference those explicit key
names/prefixes when deleting.
---
Duplicate comments:
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 61-75: The code always sets isActive: false when building the
WebDAVConfig, so edits will deactivate an active entry; change the construction
to preserve the original isActive when editing by reading the existing config's
isActive (e.g., fetch the current WebDAVConfig by editingId) and assign
isActive: existing.isActive when editing, otherwise default to false for new
configs; update the logic around updateWebDAVConfig and addWebDAVConfig to use
this adjusted config so editingId paths keep the prior isActive value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9151590d-c25a-42fc-b1fd-5a3ef6308a12
📒 Files selected for processing (9)
src/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/WebDAVPanel.tsx
✅ Files skipped from review due to trivial changes (1)
- src/components/settings/BackendPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/settings/CategoryPanel.tsx
为多个按钮组件添加aria-label属性,确保屏幕阅读器能正确识别按钮功能。同时修复BackToTop组件初始可见状态和DataManagementPanel中存储清理的逻辑
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/components/CategorySidebar.tsx (1)
395-425:⚠️ Potential issue | 🟡 MinorAdd explicit labels to the icon-only row actions.
The edit/delete/hide buttons still rely on
titleonly. Please add matchingaria-labels so screen readers can announce them reliably.♿ Minimal fix
<button onClick={(e) => { e.stopPropagation(); handleEditCategory(category); }} className="p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600" title={t('编辑分类', 'Edit category')} + aria-label={t('编辑分类', 'Edit category')} > <Edit3 className="w-3.5 h-3.5" /> </button> {category.isCustom ? ( <button onClick={(e) => { e.stopPropagation(); void handleDeleteCategory(category); }} className="p-1 rounded-md text-red-500 hover:bg-red-100 dark:hover:bg-red-900/40" title={t('删除分类', 'Delete category')} + aria-label={t('删除分类', 'Delete category')} > <Trash2 className="w-3.5 h-3.5" /> </button> ) : ( <button onClick={(e) => { e.stopPropagation(); void handleHideDefaultCategory(category); }} className="p-1 rounded-md text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-600" title={t('隐藏默认分类', 'Hide default category')} + aria-label={t('隐藏默认分类', 'Hide default category')} > <EyeOff className="w-3.5 h-3.5" /> </button> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 395 - 425, The three icon-only buttons in CategorySidebar.tsx (the Edit, Delete, and Hide buttons that call handleEditCategory, handleDeleteCategory, and handleHideDefaultCategory) rely only on title attributes; add matching aria-label attributes to each button (e.g., aria-label={t('编辑分类','Edit category')} for the Edit button, aria-label={t('删除分类','Delete category')} for Delete, and aria-label={t('隐藏默认分类','Hide default category')} for the Hide button) so screen readers can announce them reliably, leaving existing title and click handlers intact.
🧹 Nitpick comments (1)
src/components/settings/BackupPanel.tsx (1)
124-130: Consider using fresh state forcustomCategoriesfor consistency.The code correctly uses
useAppStore.getState().hiddenDefaultCategoryIds(line 139) to get fresh state, but uses the potentially stalecustomCategoriesfrom the destructured render-time state here. While this works in practice (since the array reference is stable during iteration), using fresh state would be more consistent.♻️ Optional consistency fix
try { - if (Array.isArray(customCategories)) { - for (const cat of customCategories) { + const currentCustomCategories = useAppStore.getState().customCategories; + if (Array.isArray(currentCustomCategories)) { + for (const cat of currentCustomCategories) { if (cat && cat.id) { deleteCustomCategory(cat.id); } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 124 - 130, Use fresh runtime state instead of the render-time destructured customCategories: call useAppStore.getState().customCategories to obtain the up-to-date array and iterate that when calling deleteCustomCategory(cat.id). Update the loop that currently references the destructured customCategories so it reads from useAppStore.getState().customCategories, still checking for cat && cat.id and calling deleteCustomCategory(cat.id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 41-51: The desktop-first flash occurs because isMobile is
initialized to false; in CategorySidebar initialize isMobile from the actual
breakpoint or defer rendering until breakpoint is known: set the initial state
using a function that checks typeof window !== 'undefined' and returns
window.innerWidth < 1024 (or use null/undefined and render nothing until
checked), and keep the existing useEffect (or switch to useLayoutEffect) to
update on resize; update any conditional rendering in CategorySidebar to account
for the deferred/initial value so the desktop branch is not rendered on narrow
viewports before the effect runs.
- Line 272: Update the tooltip text to reflect the real shortcut (Ctrl OR Cmd)
in CategorySidebar: replace the hardcoded "Ctrl+B" string passed to the title
prop (the t(...) calls currently showing '展开侧栏 (Ctrl+B)' / 'Expand Sidebar
(Ctrl+B)') with either a platform-aware label (detect macOS and display 'Cmd+B',
otherwise 'Ctrl+B') or a combined label like 'Ctrl/Cmd+B'; make the same change
for the other title instance around the second occurrence so both tooltips match
the handler that accepts Ctrl or Cmd.
- Around line 58-75: The global keyboard handler in useEffect (handleKeyDown)
should ignore the shortcut when isMobile is true to avoid mutating persisted
isSidebarCollapsed while mobile layout always shows expanded; inside
handleKeyDown (or at top of the effect) return early when isMobile, and add
isMobile to the useEffect dependency array so the listener is added/removed
correctly when mobile state changes; update references to toggleSidebar and
handleKeyDown accordingly so the listener respects isMobile.
- Around line 352-428: The category row currently renders as a single <button>
(onCategorySelect) containing other interactive buttons (handleEditCategory,
handleDeleteCategory, handleHideDefaultCategory), which creates invalid nested
interactive elements; move the action toolbar out of the outer button into a
sibling absolutely positioned wrapper so the edit/delete/hide buttons are not
children of the row button. Concretely, wrap the row into a container (e.g., a
div with the current group classes), keep the primary selection element as a
single button (the one using onCategorySelect/onDragOver/onDrop), and render the
toolbar as a separate div sibling (the current absolute right-3 top-1/2 wrapper)
that uses the container's hover/focus-within state to reveal itself; preserve
existing event handlers (e.stopPropagation() calls) on
handleEditCategory/handleDeleteCategory/handleHideDefaultCategory and maintain
pointer-events toggling so toolbar buttons remain clickable. Also ensure the
toolbar becomes visible on keyboard focus by using focus-within on the shared
container (so keyboard users can access the actions).
In `@src/components/settings/BackupPanel.tsx`:
- Around line 111-113: The code currently assigns const backupContent = await
webdavService.downloadFile(latestBackup) and then proceeds only if truthy, but
if downloadFile returns null the restore silently does nothing; update the
restore flow in BackupPanel (the function handling the restore) to explicitly
handle a null return from webdavService.downloadFile(latestBackup): detect if
backupContent === null, show a user-facing error/notification (e.g., toast or
alert) indicating the backup download failed or backup not found, and abort the
restore early; alternatively you can remove the conditional and let JSON.parse
fail into the existing catch, but prefer explicit null handling for clearer user
feedback.
---
Duplicate comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 395-425: The three icon-only buttons in CategorySidebar.tsx (the
Edit, Delete, and Hide buttons that call handleEditCategory,
handleDeleteCategory, and handleHideDefaultCategory) rely only on title
attributes; add matching aria-label attributes to each button (e.g.,
aria-label={t('编辑分类','Edit category')} for the Edit button,
aria-label={t('删除分类','Delete category')} for Delete, and
aria-label={t('隐藏默认分类','Hide default category')} for the Hide button) so screen
readers can announce them reliably, leaving existing title and click handlers
intact.
---
Nitpick comments:
In `@src/components/settings/BackupPanel.tsx`:
- Around line 124-130: Use fresh runtime state instead of the render-time
destructured customCategories: call useAppStore.getState().customCategories to
obtain the up-to-date array and iterate that when calling
deleteCustomCategory(cat.id). Update the loop that currently references the
destructured customCategories so it reads from
useAppStore.getState().customCategories, still checking for cat && cat.id and
calling deleteCustomCategory(cat.id).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bcdb68cc-4177-4895-8214-67b5739b2b6d
📒 Files selected for processing (4)
src/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/DataManagementPanel.tsx
✅ Files skipped from review due to trivial changes (1)
- src/components/settings/DataManagementPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/BackToTop.tsx
添加移动端标签导航组件,支持平滑滚动和触摸滑动切换 引入滚动条隐藏和触摸优化样式类 重构设置面板的标签切换逻辑,添加过渡动画效果
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/CategorySidebar.tsx (3)
202-218:⚠️ Potential issue | 🔴 Critical
showDefaultCategoryis undefined in the rollback path.The catch block calls
showDefaultCategory(category.id), but this function is never read fromuseAppStore(). In TS this fails compilation, and even in JS the rollback path would throw instead of restoring the hidden category.🐛 Minimal fix
const { customCategories, hiddenDefaultCategoryIds, categoryOrder, collapsedSidebarCategoryCount, deleteCustomCategory, hideDefaultCategory, + showDefaultCategory, language, updateRepository, isSidebarCollapsed, setSidebarCollapsed, } = useAppStore();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 202 - 218, The catch block in handleHideDefaultCategory calls showDefaultCategory(category.id) but showDefaultCategory is never obtained from useAppStore(), causing a runtime/compile error; fix by reading showDefaultCategory (and hideDefaultCategory if not already) from useAppStore() at the top of the component so both functions are defined, then use those store methods for the optimistic hide and rollback around forceSyncToBackend(); ensure the symbols showDefaultCategory, hideDefaultCategory, useAppStore, handleHideDefaultCategory, and forceSyncToBackend are the ones referenced/updated.
250-255:⚠️ Potential issue | 🟠 MajorDon't persist the localized display name as the category key.
getAllCategories(...)localizes built-in category names, and this path storescategory.nameintocustom_category. After a language switch, that stored value no longer matches the rebuilt category list, so manual categorization can disappear or be counted under the wrong category. Persist a stable id or canonical unlocalized value instead. This same issue also applies to the bulk categorize flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 250 - 255, The code is persisting the localized display name into custom_category (see nextRepo construction in CategorySidebar.tsx), which breaks after language switches because getAllCategories(...) returns localized names; change the stored value to a stable identifier instead (e.g., category.id or a canonical/unlocalized key like category.key/canonical_name) and update any other paths that set custom_category (including the bulk categorize flow) to persist and read that stable id so category lookups use the canonical value rather than the localized display name.
183-199:⚠️ Potential issue | 🟠 MajorFailed deletes are not rolled back.
deleteCustomCategory(category.id)happens beforeforceSyncToBackend(), but the catch only shows an alert. If backend sync fails, the category stays deleted locally even though the remote write was rejected, so frontend and backend drift immediately.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 183 - 199, handleDeleteCategory currently calls deleteCustomCategory(category.id) before awaiting forceSyncToBackend, but on sync failure it only alerts and does not restore state; save a backup of the category object first, then either (A) perform the remote deletion first (call forceSyncToBackend with an intent or API delete) and only call deleteCustomCategory(category.id) after a successful backend response, or (B) keep the optimistic delete but wrap deleteCustomCategory(category.id) with try/await so that if forceSyncToBackend throws you immediately call addCustomCategory(backup) (or a restoreCategory method) to revert local state; reference handleDeleteCategory, deleteCustomCategory, addCustomCategory/restoreCategory, and forceSyncToBackend when implementing the rollback/ordering change and ensure UI state is consistent while the operation is in flight.
♻️ Duplicate comments (7)
src/components/ReleaseTimeline.tsx (1)
65-82:⚠️ Potential issue | 🟠 MajorUse the persisted preset filters as the source of truth.
matchesActiveFiltersstill pulls preset keywords fromPRESET_FILTERS, while user-edited preset filters live inassetFilters. That means changes made inAssetFilterManagerstill won't affect matching, counts, or highlighting here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 65 - 82, matchesActiveFilters currently reads preset keywords from PRESET_FILTERS instead of the persisted preset entries in assetFilters, so update it to derive both activeCustomFilters and activePresetFilters from assetFilters (e.g., activeCustomFilters = assetFilters.filter(f => !f.isPreset && selectedFilters.includes(f.id)) and activePresetFilters = assetFilters.filter(f => f.isPreset && selectedFilters.includes(f.id))) and then run the same keyword matching logic against those sets; keep the useCallback signature and dependency array (selectedFilters, assetFilters) and reference the existing function name matchesActiveFilters and the AssetFilterManager-managed assetFilters structure.src/components/ReleaseCard.tsx (1)
252-269:⚠️ Potential issue | 🟡 MinorExpose the release-notes expanded state semantically.
The "Release Notes" toggle button is missing
aria-expandedandaria-controls, and the content container has no matching id. Screen readers still can't tell whether that section is open.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseCard.tsx` around lines 252 - 269, The Release Notes toggle button lacks accessibility attributes; update the button in ReleaseCard (the onClick that calls onToggleReleaseNotes and reads isReleaseNotesCollapsed) to include aria-expanded set to the expanded state (e.g., !isReleaseNotesCollapsed) and aria-controls pointing to a unique id, and add a matching id on the content container div rendered when !isReleaseNotesCollapsed (the div below the button) so screen readers can associate the button with the collapsible panel.src/components/SettingsPanel.tsx (1)
332-397:⚠️ Potential issue | 🟠 MajorComplete the modal's keyboard interaction model.
The dialog now has ARIA metadata, but focus is still not moved into it, trapped there, restored on close, or closable via Escape. Keyboard users can still tab behind the overlay and lose context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 332 - 397, The modal lacks keyboard focus management and Escape-to-close behavior; update the SettingsPanel component to (1) on open save document.activeElement and move focus into the dialog (e.g., to the first focusable in the dialog or the close button), (2) trap focus inside the dialog while open (implement a simple focus loop using tabbable selectors or use a focus-trap helper) so Tab/Shift+Tab never move focus behind the overlay, (3) listen for Escape and call handleClose to close the modal, and (4) on close restore focus to the previously focused element. Add these effects in a useEffect tied to the modal-open state and reference the dialog container (the outer div with role="dialog"), the close button (handleClose), and renderTabContent/activeTab as needed to find the initial focus target.src/components/settings/BackupPanel.tsx (1)
181-198:⚠️ Potential issue | 🟠 MajorRestore still does not apply the backed-up active config state.
For existing configs you keep
existing.isActive, and new WebDAV configs are forced tofalse. A restore can therefore finish with a different active AI/WebDAV profile than the backup describes. The restore path should applycfg.isActiveconsistently, and ifactiveAIConfig/activeWebDAVConfigare the real selectors, make sure those ids are restored too.Also applies to: 221-235
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 181 - 198, The restore logic incorrectly preserves existing.isActive and forces new WebDAV configs to false, so the restored active state doesn't match the backup; modify both AI and WebDAV restore branches (the calls to updateAIConfig/addAIConfig and the analogous updateWebDAVConfig/addWebDAVConfig blocks) to always apply cfg.isActive when restoring (use cfg.isActive for the isActive field instead of existing.isActive or hardcoding false) and ensure the app-level active selectors (activeAIConfig and activeWebDAVConfig) are updated to the restored ids so the same profiles become active after restore.src/components/settings/CategoryPanel.tsx (1)
109-115:⚠️ Potential issue | 🟠 MajorHidden ids still lose their saved position during reorder.
This still rewrites the persisted order as
visibleIds + hiddenIds. Hidden ids are no longer dropped, but they are always pushed to the tail, so unhiding a default category still loses its previous slot. Merge the reordered visible ids back into the existingcategoryOrderarray instead of concatenating them.Also applies to: 123-125, 133-135, 185-190
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 109 - 115, The current logic builds the new categoryOrder as visibleIds + hiddenIds which moves all hidden IDs to the end; instead merge the reordered visibleIds back into the existing categoryOrder to preserve hidden IDs' original slots. Replace the concatenation step in the handlers that use visibleIds, categoryOrder and setCategoryOrder (the blocks around visibleIds.splice(...), hiddenIds = ..., and setCategoryOrder([...visibleIds, ...hiddenIds])) with logic that iterates the original categoryOrder and for each id: if it is a visible id, consume and place the next id from the reordered visibleIds sequence, otherwise keep the original hidden id in-place; then call setCategoryOrder with that merged array. Apply the same replacement for the other similar blocks referenced (lines ~123-125, ~133-135, ~185-190).src/components/RepositoryList.tsx (1)
523-557:⚠️ Potential issue | 🟠 MajorUse a functional progress update in the bulk AI loop.
The per-repo callback closes over a stale
analysisProgressvalue. Withconcurrency > 1, multiple completions in the same batch can all write the samecurrent + 1, so the progress bar lags or stops advancing. UsesetAnalysisProgress(prev => ...)here.Suggested fix
- const newCurrent = Math.min(analysisProgress.current + 1, analysisProgress.total); - setAnalysisProgress({ current: newCurrent, total: analysisProgress.total }); + setAnalysisProgress(prev => ({ + ...prev, + current: Math.min(prev.current + 1, prev.total), + }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 523 - 557, The progress update inside the per-repo callback closes over a stale analysisProgress value, causing concurrent completions to overwrite each other; replace the direct setAnalysisProgress({ current: newCurrent, total: ... }) with a functional updater like setAnalysisProgress(prev => ({ current: Math.min(prev.current + 1, prev.total), total: prev.total })) so each completion increments based on the latest state; update any other places in the batch (including failure/error branches) that currently compute newCurrent from analysisProgress to use the functional form; look for usages of setAnalysisProgress, analysisProgress, and the per-repo async callback (where aiService.analyzeRepository, updateRepository, resolveCategoryAssignment are invoked) to apply the change.src/components/settings/DataManagementPanel.tsx (1)
121-127:⚠️ Potential issue | 🟠 Major
clearAllStorage()still cannot detect IndexedDB deletion failures.This
try/catchonly helps ifindexedDBStorage.removeItem()rejects, butsrc/services/indexedDbStorage.tscurrently swallows IndexedDB errors and resolvesvoid.deleteAllData()can therefore log success and reload even when IndexedDB cleanup failed. Please propagate a failure signal from the storage layer (or return a status) before treating the wipe as successful.#!/bin/bash fd -i 'indexedDbStorage.ts' . -x sed -n '153,169p' {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 121 - 127, clearAllStorage() currently assumes indexedDBStorage.removeItem('github-stars-manager') will reject on failure, but indexedDbStorage.ts swallows errors and resolves; update the storage layer (the removeItem function in src/services/indexedDbStorage.ts) to propagate failures (either by rejecting the Promise on error or returning a boolean status), then change clearAllStorage()/deleteAllData() to check that result and throw/log and abort the reload when the storage layer indicates failure rather than always treating the wipe as successful.
🧹 Nitpick comments (3)
src/components/settings/WebDAVPanel.tsx (2)
259-288: Icon-only buttons rely ontitlefor accessible name.The Test, Edit, and Delete buttons use only
titleattributes, which are not reliably announced by screen readers and unavailable on touch devices. Addaria-labelfor accessibility.♿ Proposed accessibility fix
<button onClick={() => handleTest(config)} disabled={testingId === config.id} className="p-2 rounded-lg bg-blue-100 text-blue-600 ..." title={t('测试连接', 'Test Connection')} + aria-label={t('测试连接', 'Test Connection')} >Apply similar changes to the Edit and Delete buttons.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 259 - 288, The Test, Edit and Delete buttons use only title attributes which aren't reliable for screen readers or touch; update the three button elements (the one invoking handleTest(config) that checks testingId === config.id, the one invoking handleEdit(config), and the one calling deleteWebDAVConfig(config.id)) to include aria-label attributes using the same localized strings you pass to t(...) (e.g. t('测试连接', 'Test Connection'), t('编辑','Edit'), t('删除','Delete')) so screen readers and touch users get an accessible name.
138-149: Consider addingnamefield validation.The form validates URL, username, password, and path via
WebDAVService.validateConfig, but thenamefield can be saved empty. If a config name is required for identification in the list, consider adding validation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 138 - 149, The form allows saving a WebDAV config with an empty name; update WebDAVPanel to validate the name before submit (same place other validations are enforced via WebDAVService.validateConfig) by checking form.name is non-empty and showing an inline error state/message and preventing submit; adjust the submit handler (in WebDAVPanel's save/submit function) to call the new name check, setForm or a local error state for the name field when empty, and only proceed to call WebDAVService.validateConfig and save when name is present.src/components/settings/GeneralPanel.tsx (1)
31-68: Consider extracting the language option as a reusable component.The language radio button markup is duplicated for Chinese and English options. While functional, extracting a
LanguageOptioncomponent would reduce repetition.♻️ Optional refactor to reduce duplication
+const LanguageOption: React.FC<{ + value: 'zh' | 'en'; + checked: boolean; + onChange: (value: 'zh' | 'en') => void; + label: string; + sublabel: string; +}> = ({ value, checked, onChange, label, sublabel }) => ( + <label className="flex items-center space-x-3 cursor-pointer p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"> + <input + type="radio" + name="language" + value={value} + checked={checked} + onChange={(e) => onChange(e.target.value as 'zh' | 'en')} + className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" + /> + <div> + <span className="text-base font-medium text-gray-900 dark:text-white">{label}</span> + <p className="text-xs text-gray-500 dark:text-gray-400">{sublabel}</p> + </div> + </label> +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/GeneralPanel.tsx` around lines 31 - 68, The language radio markup is duplicated; extract a reusable LanguageOption component and replace the two repeated label blocks with it. Create a LanguageOption functional component that accepts props like value: 'zh' | 'en', label, sublabel, currentLanguage (or language) and onChange (e.g., setLanguage) and renders the input and accompanying text exactly as in the existing blocks; then use <LanguageOption value="zh" label="中文" sublabel="Simplified Chinese" language={language} onChange={setLanguage} /> and similarly for "en". Ensure the input's checked and onChange behavior uses the passed props so existing behavior of setLanguage and language is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 94-149: The rollback uses stale closure values for assetFilters
and selectedFilters; update the catch block to read fresh state from the store
(e.g. call useAppStore.getState() and destructure current assetFilters and
selectedFilters) and use those current arrays in all rollback checks (the
existence checks around deleteAssetFilter/addAssetFilter and the loop that
clears/restores selection) instead of the closure-captured
assetFilters/selectedFilters; keep using addedFilterIds, deleteAssetFilter,
addAssetFilter and onFilterToggle as before but perform existence checks against
the freshly-read currentFilters/currentSelected.
In `@src/components/BulkActionToolbar.tsx`:
- Around line 90-104: The delayed callbacks in handleClose and handleDeselectAll
currently call setIsClosing(false) after invoking the parent callbacks, which
can trigger a state update on an unmounted component; remove the trailing
setIsClosing(false) calls from both timeouts so you only setIsClosing(true) then
call onClose() / onDeselectAll() after the delay. Locate the two functions named
handleClose and handleDeselectAll in BulkActionToolbar and delete the
setIsClosing(false) lines inside their setTimeout callbacks (or alternatively
guard them with a mounted check or clear the timeout on unmount if you prefer),
ensuring no state is reset after the parent hides the toolbar.
In `@src/components/MarkdownRenderer.tsx`:
- Around line 82-87: The image error fallback in MarkdownRenderer/MarkdownImage
currently uses hardcoded Chinese text ("[图片加载失败: ...]"); change it to use the
app i18n instead by either adding a language prop to MarkdownRendererProps and
passing it into MarkdownImage, or by importing and using useAppStore (or the
existing i18n selector) directly inside MarkdownImage; replace the hardcoded
string in the hasError branch with a localized message (e.g., use
t('image.loadFailed') or a language-based ternary) and keep the alt fallback
behavior (alt || 'image') intact so the displayed placeholder is localized while
preserving the alt text.
In `@src/components/RepositoryCard.tsx`:
- Line 445: The card currently disables drag based on presence of the onSelect
callback (draggable={!onSelect}), which breaks CategorySidebar drop flow; change
the gating to use the selection mode flag instead (e.g., use
draggable={!selectionMode} or draggable={selectionMode === 'none'} depending on
how selectionMode is represented) inside the RepositoryCard component so cards
remain draggable when selection support exists but bulk-selection mode is off;
update any related drag/dragStart logic in RepositoryCard to reference
selectionMode rather than onSelect.
In `@src/components/RepositoryList.tsx`:
- Around line 510-513: The bulk analysis path sets isAnalyzingRef.current and
shows the shared pause UI but never honors pause—mirror handleAIAnalyze’s pause
gate inside the repository iteration: inside the bulk analysis loop (the
function that iterates repos and updates setAnalysisProgress /
isAnalyzingRef.current) add a wait loop like while (isPaused &&
!shouldStopRef.current) await new Promise(r => setTimeout(r, 200)); so the Pause
button actually pauses processing; ensure you reference the same pause flag
(isPaused or isPausedRef) and shouldStopRef.current and keep
isAnalyzingRef.current behavior unchanged.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 89-93: The backend sync call is only persisting
hiddenDefaultCategoryIds; update the payloads sent to and received from the
backend so full category/sidebar metadata is round-tripped: include
customCategories, categoryOrder, collapsedSidebarCategoryCount, and
isSidebarCollapsed alongside hiddenDefaultCategoryIds when calling
backend.syncSettings and when handling backend.restore responses (locate the
calls to backend.syncSettings and the restore/restore handlers in
BackendPanel.tsx and add those properties to the settings object and to any
state updates that apply to repositories' custom_category and sidebar
rendering). Ensure the same expanded payload is used in both sync directions so
repositories with custom_category map to actual categories and ordering is
preserved across devices.
- Around line 33-50: The effect in BackendPanel uses backend.checkHealth()
before the backend adapter is initialized, so on fresh load it reports
disconnected; update the useEffect to call and await backend.init() (from
src/services/backendAdapter.ts which discovers _backendUrl) before calling
backend.checkHealth(), handle/init errors by catching and setting status/health
consistently, and ensure you only call setStatus('connected') when checkHealth()
returns valid data after successful init; reference the useEffect in
BackendPanel, backend.init(), backend.checkHealth(), and the adapter's
_backendUrl discovery.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 49-67: Prevent duplicate category names when adding or renaming by
checking the full category list for an existing name (case-insensitive) before
calling addCustomCategory or updateCustomCategory; in handleAddCategory (and the
edit handler around the same file) compute if any existing category.name equals
the new name excluding the category being edited (use its id when editing) and
show the same validation alert if a duplicate is found. Also ensure
updateCustomCategory and deleteCustomCategory logic in useAppStore.ts relies on
category id (not repo.custom_category name) when matching categories to repos or
update the store callers to pass/compare ids consistently to avoid cascading
changes via repo.custom_category string comparisons.
In `@src/components/SettingsPanel.tsx`:
- Around line 153-155: The mobile tab nav is using
aria-controls="mobile-tabpanel-*" while the tab panels are rendered with a
different id scheme via renderTabContent('desktop'), causing broken references;
update MobileTabNav (and the other occurrences you noted) so aria-controls and
id use a shared scheme (e.g., tabpanel-{tab.id}) or make renderTabContent emit
matching "mobile-tabpanel-{tab.id}" ids when the mobile tablist is active;
specifically, change the id/aria-controls logic in the MobileTabNav component
and the renderTabContent function (and the TabPanel rendering sites referenced
around lines you noted) so both nav variants point to the same panel id format
and aria-controls always matches the actual panel id in the DOM.
In `@src/index.css`:
- Around line 44-57: There are two conflicting animations: the CSS keyframes
named "slide-down" and class ".animate-slide-down" (translating to 100%) vs the
Tailwind keyframe "slideDown" (10px) in tailwind.config.js; resolve by renaming
or consolidating so names and intent are unique—e.g., rename the custom
keyframes and class to "slide-down-full" (update "@keyframes slide-down" ->
"@keyframes slide-down-full" and ".animate-slide-down" ->
".animate-slide-down-full") or alternatively remove the custom definition and
map the Tailwind "slideDown" to the desired full-screen behavior, then update
all usages of ".animate-slide-down" or the Tailwind utility to the chosen name
to avoid load-order conflicts.
In `@src/store/useAppStore.ts`:
- Around line 272-279: The preset keywords in defaultPresetFilters (constant
defaultPresetFilters in useAppStore.ts) are inconsistent with the canonical list
in presetFilters (e.g., removing archive extensions like ".zip" and "tar.gz"
from platform presets); update defaultPresetFilters to match the canonical
keywords by removing archive extensions from platform entries OR replace the
literal array with an import of the canonical preset list and map over it to add
the UI-only fields (isPreset, icon, id/name as needed) just like
AssetFilterManager/DEFAULT_PRESET_FILTERS does so both sources share identical
keyword sets.
---
Outside diff comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 202-218: The catch block in handleHideDefaultCategory calls
showDefaultCategory(category.id) but showDefaultCategory is never obtained from
useAppStore(), causing a runtime/compile error; fix by reading
showDefaultCategory (and hideDefaultCategory if not already) from useAppStore()
at the top of the component so both functions are defined, then use those store
methods for the optimistic hide and rollback around forceSyncToBackend(); ensure
the symbols showDefaultCategory, hideDefaultCategory, useAppStore,
handleHideDefaultCategory, and forceSyncToBackend are the ones
referenced/updated.
- Around line 250-255: The code is persisting the localized display name into
custom_category (see nextRepo construction in CategorySidebar.tsx), which breaks
after language switches because getAllCategories(...) returns localized names;
change the stored value to a stable identifier instead (e.g., category.id or a
canonical/unlocalized key like category.key/canonical_name) and update any other
paths that set custom_category (including the bulk categorize flow) to persist
and read that stable id so category lookups use the canonical value rather than
the localized display name.
- Around line 183-199: handleDeleteCategory currently calls
deleteCustomCategory(category.id) before awaiting forceSyncToBackend, but on
sync failure it only alerts and does not restore state; save a backup of the
category object first, then either (A) perform the remote deletion first (call
forceSyncToBackend with an intent or API delete) and only call
deleteCustomCategory(category.id) after a successful backend response, or (B)
keep the optimistic delete but wrap deleteCustomCategory(category.id) with
try/await so that if forceSyncToBackend throws you immediately call
addCustomCategory(backup) (or a restoreCategory method) to revert local state;
reference handleDeleteCategory, deleteCustomCategory,
addCustomCategory/restoreCategory, and forceSyncToBackend when implementing the
rollback/ordering change and ensure UI state is consistent while the operation
is in flight.
---
Duplicate comments:
In `@src/components/ReleaseCard.tsx`:
- Around line 252-269: The Release Notes toggle button lacks accessibility
attributes; update the button in ReleaseCard (the onClick that calls
onToggleReleaseNotes and reads isReleaseNotesCollapsed) to include aria-expanded
set to the expanded state (e.g., !isReleaseNotesCollapsed) and aria-controls
pointing to a unique id, and add a matching id on the content container div
rendered when !isReleaseNotesCollapsed (the div below the button) so screen
readers can associate the button with the collapsible panel.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 65-82: matchesActiveFilters currently reads preset keywords from
PRESET_FILTERS instead of the persisted preset entries in assetFilters, so
update it to derive both activeCustomFilters and activePresetFilters from
assetFilters (e.g., activeCustomFilters = assetFilters.filter(f => !f.isPreset
&& selectedFilters.includes(f.id)) and activePresetFilters =
assetFilters.filter(f => f.isPreset && selectedFilters.includes(f.id))) and then
run the same keyword matching logic against those sets; keep the useCallback
signature and dependency array (selectedFilters, assetFilters) and reference the
existing function name matchesActiveFilters and the AssetFilterManager-managed
assetFilters structure.
In `@src/components/RepositoryList.tsx`:
- Around line 523-557: The progress update inside the per-repo callback closes
over a stale analysisProgress value, causing concurrent completions to overwrite
each other; replace the direct setAnalysisProgress({ current: newCurrent, total:
... }) with a functional updater like setAnalysisProgress(prev => ({ current:
Math.min(prev.current + 1, prev.total), total: prev.total })) so each completion
increments based on the latest state; update any other places in the batch
(including failure/error branches) that currently compute newCurrent from
analysisProgress to use the functional form; look for usages of
setAnalysisProgress, analysisProgress, and the per-repo async callback (where
aiService.analyzeRepository, updateRepository, resolveCategoryAssignment are
invoked) to apply the change.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 181-198: The restore logic incorrectly preserves existing.isActive
and forces new WebDAV configs to false, so the restored active state doesn't
match the backup; modify both AI and WebDAV restore branches (the calls to
updateAIConfig/addAIConfig and the analogous updateWebDAVConfig/addWebDAVConfig
blocks) to always apply cfg.isActive when restoring (use cfg.isActive for the
isActive field instead of existing.isActive or hardcoding false) and ensure the
app-level active selectors (activeAIConfig and activeWebDAVConfig) are updated
to the restored ids so the same profiles become active after restore.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 109-115: The current logic builds the new categoryOrder as
visibleIds + hiddenIds which moves all hidden IDs to the end; instead merge the
reordered visibleIds back into the existing categoryOrder to preserve hidden
IDs' original slots. Replace the concatenation step in the handlers that use
visibleIds, categoryOrder and setCategoryOrder (the blocks around
visibleIds.splice(...), hiddenIds = ..., and setCategoryOrder([...visibleIds,
...hiddenIds])) with logic that iterates the original categoryOrder and for each
id: if it is a visible id, consume and place the next id from the reordered
visibleIds sequence, otherwise keep the original hidden id in-place; then call
setCategoryOrder with that merged array. Apply the same replacement for the
other similar blocks referenced (lines ~123-125, ~133-135, ~185-190).
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 121-127: clearAllStorage() currently assumes
indexedDBStorage.removeItem('github-stars-manager') will reject on failure, but
indexedDbStorage.ts swallows errors and resolves; update the storage layer (the
removeItem function in src/services/indexedDbStorage.ts) to propagate failures
(either by rejecting the Promise on error or returning a boolean status), then
change clearAllStorage()/deleteAllData() to check that result and throw/log and
abort the reload when the storage layer indicates failure rather than always
treating the wipe as successful.
In `@src/components/SettingsPanel.tsx`:
- Around line 332-397: The modal lacks keyboard focus management and
Escape-to-close behavior; update the SettingsPanel component to (1) on open save
document.activeElement and move focus into the dialog (e.g., to the first
focusable in the dialog or the close button), (2) trap focus inside the dialog
while open (implement a simple focus loop using tabbable selectors or use a
focus-trap helper) so Tab/Shift+Tab never move focus behind the overlay, (3)
listen for Escape and call handleClose to close the modal, and (4) on close
restore focus to the previously focused element. Add these effects in a
useEffect tied to the modal-open state and reference the dialog container (the
outer div with role="dialog"), the close button (handleClose), and
renderTabContent/activeTab as needed to find the initial focus target.
---
Nitpick comments:
In `@src/components/settings/GeneralPanel.tsx`:
- Around line 31-68: The language radio markup is duplicated; extract a reusable
LanguageOption component and replace the two repeated label blocks with it.
Create a LanguageOption functional component that accepts props like value: 'zh'
| 'en', label, sublabel, currentLanguage (or language) and onChange (e.g.,
setLanguage) and renders the input and accompanying text exactly as in the
existing blocks; then use <LanguageOption value="zh" label="中文"
sublabel="Simplified Chinese" language={language} onChange={setLanguage} /> and
similarly for "en". Ensure the input's checked and onChange behavior uses the
passed props so existing behavior of setLanguage and language is preserved.
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 259-288: The Test, Edit and Delete buttons use only title
attributes which aren't reliable for screen readers or touch; update the three
button elements (the one invoking handleTest(config) that checks testingId ===
config.id, the one invoking handleEdit(config), and the one calling
deleteWebDAVConfig(config.id)) to include aria-label attributes using the same
localized strings you pass to t(...) (e.g. t('测试连接', 'Test Connection'),
t('编辑','Edit'), t('删除','Delete')) so screen readers and touch users get an
accessible name.
- Around line 138-149: The form allows saving a WebDAV config with an empty
name; update WebDAVPanel to validate the name before submit (same place other
validations are enforced via WebDAVService.validateConfig) by checking form.name
is non-empty and showing an inline error state/message and preventing submit;
adjust the submit handler (in WebDAVPanel's save/submit function) to call the
new name check, setForm or a local error state for the name field when empty,
and only proceed to call WebDAVService.validateConfig and save when name is
present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 952e3718-741b-4f4e-927f-fed27e09f1ae
⛔ Files ignored due to path filters (2)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (29)
.github/workflows/build-desktop.ymlpackage.jsonsrc/App.tsxsrc/components/AssetFilterManager.tsxsrc/components/BackToTop.tsxsrc/components/BulkActionToolbar.tsxsrc/components/BulkCategorizeModal.tsxsrc/components/CategorySidebar.tsxsrc/components/MarkdownRenderer.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseCard.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryList.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.tssrc/constants/presetFilters.tssrc/index.csssrc/services/githubApi.tssrc/store/useAppStore.tssrc/types/index.tstailwind.config.js
|
感谢贡献,十分优质的PR。我预编译了桌面端深度体验了一下,发现有几个问题:
|
添加Vite legacy插件支持旧版浏览器,引入polyfills和ErrorBoundary组件 优化分类管理功能,新增分类锁定/解锁操作,改进侧边栏交互体验 重构数据管理面板,确保状态重置时数据一致性 改进仓库列表性能,优化批量操作和筛选功能 添加滚动条样式和动画,提升用户体验
|
还有什么要改的地方? |
编译失败了... |
更新 acorn 到 8.16.0 版本 添加 @jridgewell/source-map、buffer-from、source-map、source-map-support 和 terser 依赖 移除 @types/prop-types 和 @types/react 的 dev 标记
|
Tip For best results, initiate chat on the files or code changes.
PR
|
| 组件 | 功能 |
|---|---|
GeneralPanel |
语言切换、版本更新检测、联系方式 |
AIConfigPanel |
AI 服务配置增删改查 + 连接测试 |
WebDAVPanel |
WebDAV 配置管理 + 连接测试 |
BackupPanel |
基于 WebDAV 的备份与恢复(密钥脱敏处理) |
BackendPanel |
后端健康检测、鉴权、双向数据同步 |
CategoryPanel |
自定义分类增删改,默认分类显隐控制 |
DataManagementPanel |
数据统计、分类删除、一键清除全部数据 |
index.ts |
统一桶文件导出 |
4. 📂 侧栏优化(CategorySidebar.tsx)
- 响应式布局:移动端始终显示完整侧栏;桌面端支持折叠/展开。
- 折叠后仅显示图标(最多 8 个分类),
Ctrl/Cmd + B快捷键切换。 isSidebarCollapsed状态持久化到 Zustand Store。
5. ⬆️ 返回顶部按钮(BackToTop.tsx + App.tsx)
- 滚动超过 300px 时显示,支持平滑滚动与无障碍属性,文案随语言切换。
6. 📦 批量操作(BulkActionToolbar.tsx、BulkCategorizeModal.tsx)
- 多选仓库卡片,支持全选/取消全选。
- 批量操作:取消 Star、批量分类、批量 AI 摘要、批量订阅发布。
7. 🔍 资产过滤器预设(AssetFilterManager.tsx、presetFilters.ts)
- 内置 Windows、macOS、Linux、Android、Source 五个预设过滤器,带重置预设功能。
8. 📖 README 弹窗(ReadmeModal.tsx)
- 内联渲染仓库 README,使用
ReactMarkdown+remark-gfm+rehype-sanitize,支持 UTF-8 解码与焦点管理。
9. 🎨 CSS 工具类(index.css)
.scrollbar-hide、.touch-manipulation,以及slide-up、slide-down、fade-in等新动画。
⚠️ 合并前需解决的问题
以下问题已由仓库 Owner
@AmintaCCCP反馈或由代码审查识别:
-
❌ 编译失败 — 目前存在编译错误,需优先排查修复(可能与
src/components/ReadmeModal.tsx或src/components/RepositoryList.tsx中未解决的 merge conflict 标记有关)。 -
📂 侧栏折叠后分类显示不全且无法滚动 — 折叠状态下无法上下滑动浏览分类列表。
-
🖱️ 拖拽仓库卡片到分类列表的交互失效 — 拖拽与点击交互存在冲突,建议在卡片上新增专属拖拽热区图标以区分两种交互。
-
📋 发布页展开体验下降 — 点击文件列表时会同时展开更新日志,建议将"展开日志"与"展开下载资产"分为两个独立按钮;过滤器改为高亮而非过滤也增加了读取成本,建议恢复过滤逻辑;列表项内容与容器边距也可适当加大。
-
🔑
BackupPanel密钥脱敏恢复逻辑 — 确保恢复时对脱敏字段(***)正确保留原有凭证,避免数据丢失。 -
🔄
DataManagementPanel调用window.location.reload()— "清除全部数据"后强制刷新页面,确认该 UX 符合预期。
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 15
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/CategorySidebar.tsx (1)
211-228:⚠️ Potential issue | 🟠 MajorRollback the local delete when backend sync fails.
deleteCustomCategory(category.id)runs beforeforceSyncToBackend(), but the catch only shows an alert. If sync fails, the category is gone locally and any clearedrepo.custom_categoryassignments stay cleared, while the backend still has the old state.Suggested direction
const handleDeleteCategory = async (category: Category) => { + const originalRepositories = repositories.filter( + repo => repo.custom_category === category.name + ); + ... deleteCustomCategory(category.id); try { await forceSyncToBackend(); } catch (error) { - // Revert local change on failure + // Revert local change on failure + useAppStore.getState().addCustomCategory(category); + for (const repo of originalRepositories) { + updateRepository(repo); + } alert(t('删除分类失败,请检查后端连接。', 'Failed to delete category. Please check backend connection.')); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 211 - 228, handleDeleteCategory is doing an optimistic local delete (deleteCustomCategory(category.id)) before awaiting forceSyncToBackend(), but on sync failure it only alerts and does not restore local state; capture the pre-delete state (e.g., previousCategories and the list of repos with their repo.custom_category values) before calling deleteCustomCategory, and in the catch block restore that state (re-add the category and reset affected repos' custom_category) so local state is rolled back if forceSyncToBackend() throws; keep using handleDeleteCategory, deleteCustomCategory and forceSyncToBackend names to locate the code and perform the restore via your existing state updater or dispatch functions.src/components/RepositoryCard.tsx (1)
780-790:⚠️ Potential issue | 🟡 MinorAvoid rendering the same topics twice.
When there are no custom tags or AI tags,
displayTags.tagsalready comes fromrepository.topics. The second block appendsrepository.topics.slice(0, 2)again, so plain-topic cards show duplicates.Suggested fix
- {repository.topics && repository.topics.length > 0 && !displayTags.isCustom && ( + {repository.ai_tags && repository.ai_tags.length > 0 && repository.topics && repository.topics.length > 0 && !displayTags.isCustom && (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryCard.tsx` around lines 780 - 790, The duplicated topics are rendered because repository.topics is shown twice: once via displayTags.tags and again in the explicit repository.topics.slice(0,2) JSX; to fix, modify the conditional on that second block (the JSX that maps repository.topics.slice(0,2)) to only render when displayTags.tags is not already the same as repository.topics — e.g., add a guard like && JSON.stringify(displayTags.tags) !== JSON.stringify(repository.topics) (or an equivalent element-wise comparison) in addition to the existing !displayTags.isCustom check so the topics block is skipped when displayTags.tags already comes from repository.topics.
♻️ Duplicate comments (8)
src/components/AssetFilterManager.tsx (1)
97-117:⚠️ Potential issue | 🟠 MajorPreset reset uses stale state and can delete presets instead of restoring them.
deleteAssetFilter/addAssetFiltermutate the store, but every existence check here still reads the oldassetFilterssnapshot. That means the add phase can skip re-adding defaults for ids that were just deleted, so “reset” can leave the preset list empty/partial instead of restored. The same stale reads also weaken the rollback path.Suggested fix
try { + const hasFilter = (id: string) => + useAppStore.getState().assetFilters.some(f => f.id === id); + // 删除所有现有的预设筛选器 presetFilters.forEach(filter => { - if (assetFilters.find(f => f.id === filter.id)) { + if (hasFilter(filter.id)) { deleteAssetFilter(filter.id); } if (selectedFilters.includes(filter.id)) { onFilterToggle(filter.id); } }); // 添加默认预设筛选器 DEFAULT_PRESET_FILTERS.forEach(filter => { - if (!assetFilters.find(f => f.id === filter.id)) { + if (!hasFilter(filter.id)) { addAssetFilter(filter); addedFilterIds.push(filter.id); } }); } catch (error) { @@ // 1. 移除新添加的筛选器 addedFilterIds.forEach(id => { - if (assetFilters.find(f => f.id === id)) { + if (hasFilter(id)) { deleteAssetFilter(id); } }); @@ // 2. 恢复之前的筛选器(包括被删除的) previousFilters.forEach(filter => { - if (!assetFilters.find(f => f.id === filter.id)) { + if (!hasFilter(filter.id)) { addAssetFilter(filter); } });Also applies to: 123-143
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 97 - 117, The reset logic uses a stale snapshot (assetFilters/selectedFilters) while calling mutating helpers (deleteAssetFilter, addAssetFilter, onFilterToggle), causing skipped adds and broken rollback; fix by operating on and comparing to a fresh snapshot or by using atomic store updates: capture the intended final preset set (e.g., compute desiredPresetIds from DEFAULT_PRESET_FILTERS and presetFilters), then call store-level operations that replace the presets in one update (or re-query assetFilters after each mutation) instead of repeatedly reading the old assetFilters; ensure rollback uses the original previousFilters/previousSelected snapshots to fully restore and use addedFilterIds only from actual successful addAssetFilter results.src/components/MarkdownRenderer.tsx (1)
82-87:⚠️ Potential issue | 🟡 MinorHardcoded Chinese text in image error fallback.
The error placeholder
[图片加载失败: {alt || 'image'}]is hardcoded in Chinese, but the app supports both locales. SinceMarkdownRendererdoesn't receive the language prop, consider accessinguseAppStoredirectly inMarkdownImageor adding alanguageprop to the component.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/MarkdownRenderer.tsx` around lines 82 - 87, The image-error fallback in MarkdownRenderer renders a hardcoded Chinese string; update MarkdownImage (or MarkdownRenderer) to use the app locale instead by either reading language from useAppStore inside the MarkdownImage component or by adding a language prop passed into MarkdownImage from MarkdownRenderer; then replace the hardcoded "[图片加载失败: ...]" with a localized message (e.g., using a simple conditional or i18n lookup based on language) so the alt/error text respects the current locale.src/components/SettingsPanel.tsx (2)
380-386:⚠️ Potential issue | 🟠 MajorMobile tabpanel IDs don't match the rendered content panel IDs.
MobileTabNavusesaria-controls="mobile-tabpanel-${tab.id}"(line 155), butrenderTabContent()is always called with'desktop'prefix (lines 392, 450), generating IDs likedesktop-tabpanel-general. This breaks ARIA associations for mobile users.Either pass
'mobile'torenderTabContent()when rendering for mobile, or use a shared ID scheme.Also applies to: 438-444
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 380 - 386, The mobile tab nav's aria-controls IDs don't match the content panels because renderTabContent() is always called with the 'desktop' prefix; update the mobile rendering to call renderTabContent('mobile') (or otherwise switch to a shared ID scheme) so the IDs created by renderTabContent match MobileTabNav's aria-controls; locate the MobileTabNav usage in SettingsPanel.tsx and change the renderTabContent call within the md:hidden block (and the similar block around lines 438-444) to pass 'mobile' (or adjust both MobileTabNav and renderTabContent to use the same prefix variable) so ARIA associations line up.
329-397:⚠️ Potential issue | 🟠 MajorModal still lacks focus trap and proper keyboard navigation.
The modal has ARIA metadata but focus is never trapped within it. Users can Tab into background content, and focus isn't properly restored on close. This was flagged in a previous review but appears unaddressed in the current implementation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 329 - 397, The modal rendered when isModal is true lacks a focus trap and focus restoration; update the modal logic around the main dialog div (the element with role="dialog" rendered in the isModal branch) and the handler handleClose to implement focus management: on open save document.activeElement, move focus to a designated initial focusable element inside the dialog (create a ref like modalRef or initialFocusRef), add a keydown listener to trap Tab/Shift+Tab within modalRef and to close on Escape, and on close/cleanup restore focus to the previously focused element; implement these behaviors in a useEffect tied to isModal and ensure cleanup removes listeners and nulls refs so background content is not tabbable while the modal is open.src/components/settings/BackupPanel.tsx (1)
51-66:⚠️ Potential issue | 🟠 MajorRestored config activation is still inconsistent.
The restore path still keeps
existing.isActivefor updated AI/WebDAV configs and forces new WebDAV configs inactive, so the restored environment does not match the backup. On top of that, the payload does not capture the separateactiveAIConfig/activeWebDAVConfigids that other components read from the store.Also applies to: 181-198, 221-235
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 51 - 66, The backup payload in BackupPanel.tsx must include the active config ids and preserve activation state on restore: add activeAIConfig and activeWebDAVConfig fields to the exported backupData alongside aiConfigs/webdavConfigs (still masking apiKey/password), and update the restore logic that currently references existing.isActive and forces new WebDAV configs inactive so it instead applies the isActive value from the backed-up config (for both updated and new entries) and uses the exported activeAIConfig/activeWebDAVConfig ids to set the store’s active pointers; update code paths that handle aiConfigs, webdavConfigs, and the existing.isActive checks to follow the backed-up state rather than defaulting WebDAV to inactive.src/store/useAppStore.ts (1)
294-301:⚠️ Potential issue | 🟠 MajorKeep
defaultPresetFiltersaligned with the canonical preset list.This store copy still reintroduces
.zipfor Windows andtar.gzfor Linux, even thoughsrc/constants/presetFilters.tsalready removed those archive extensions to avoid false positives. Different code paths will now classify the same asset differently again. Prefer importing the canonical presets instead of duplicating them here.Suggested fix
const defaultPresetFilters: AssetFilter[] = [ - { id: 'preset-windows', name: 'Windows', keywords: ['windows', 'win', 'exe', 'msi', '.zip'], isPreset: true, icon: 'Monitor' }, + { id: 'preset-windows', name: 'Windows', keywords: ['windows', 'win', 'exe', 'msi'], isPreset: true, icon: 'Monitor' }, { id: 'preset-macos', name: 'macOS', keywords: ['mac', 'macos', 'darwin', 'dmg', 'pkg'], isPreset: true, icon: 'Apple' }, - { id: 'preset-linux', name: 'Linux', keywords: ['linux', 'appimage', 'deb', 'rpm', 'tar.gz'], isPreset: true, icon: 'Terminal' }, + { id: 'preset-linux', name: 'Linux', keywords: ['linux', 'appimage', 'deb', 'rpm'], isPreset: true, icon: 'Terminal' }, { id: 'preset-android', name: 'Android', keywords: ['android', 'apk'], isPreset: true, icon: 'Smartphone' }, { id: 'preset-source', name: 'Source', keywords: ['source', 'src', 'tar.gz', 'tar.xz', 'zip'], isPreset: true, icon: 'Package' }, ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/store/useAppStore.ts` around lines 294 - 301, The local constant defaultPresetFilters in useAppStore.ts duplicates canonical presets and reintroduces removed archive extensions; replace this local array with an import of the canonical presets (e.g., import { presetFilters } from 'src/constants/presetFilters' or the actual exported name) and use that imported symbol wherever defaultPresetFilters is referenced (remove the local declaration of defaultPresetFilters and update usages to reference the imported presetFilters to keep classification consistent).src/components/RepositoryList.tsx (2)
123-127:⚠️ Potential issue | 🟠 MajorUse the derived selection for the toolbar count too.
selectedRepositoriesis already scoped to the current filtered list, butselectedCountstill uses the rawselectedRepoIdsset. After filtering or deleting repos, the toolbar can say “N selected” while actions only receive fewer repositories.Suggested fix
<BulkActionToolbar - selectedCount={selectedRepoIds.size} + selectedCount={selectedRepositories.length} repositories={selectedRepositories} onSelectAll={handleSelectAll} onDeselectAll={handleDeselectAll}Also applies to: 1103-1106
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 123 - 127, The toolbar uses the raw selectedRepoIds set for the selected count while actions use the filtered selection; update any count/display logic (e.g., the selectedCount used in the toolbar) to derive its value from the filtered selectedRepositories (use selectedRepositories.length) instead of selectedRepoIds.size so the UI and action payloads stay consistent; also replace other occurrences that display or rely on the raw set (refer to selectedRepositories and selectedRepoIds in RepositoryList.tsx) so all counts and labels reflect the current filtered selection.
563-621:⚠️ Potential issue | 🟠 MajorBulk AI still doesn't keep pause/progress state truthful.
This branch enables the shared pause/progress UI, but the worker loop never waits on a live pause flag, and the per-repo progress update reads
analysisProgressfrom the render that started the action. In practice the Pause button is cosmetic here, and the progress bar can stall or jump under concurrency.Also applies to: 969-994
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 563 - 621, The loop never honors a live pause flag and reads analysisProgress from a stale closure; fix by (1) introducing/using a shared pause ref (e.g., shouldPauseRef or isPausedRef) inside the batch worker and, before processing each repo (inside batch.map and/or before starting each batch), await a small async loop that yields until shouldPauseRef.current is false so the worker actually pauses; and (2) update progress using the functional state updater for setAnalysisProgress instead of reading analysisProgress from the render closure—replace the per-repo increment logic with setAnalysisProgress(prev => ({ current: Math.min(prev.current + 1, prev.total), total: prev.total })) so progress is consistent under concurrency and not using stale values from the initial render.
🟡 Minor comments (6)
src/main.tsx-13-15 (1)
13-15:⚠️ Potential issue | 🟡 MinorFallback UI still never renders when
#rootis missing.The catch path looks up
#rootagain before injecting the error screen. If the thrown error is exactly “Root element not found”, this branch stays empty and the user still gets a blank page.Suggested fix
} catch (error) { console.error('Failed to render React app:', error); - // Display a fallback error message - const rootElement = document.getElementById('root'); - if (rootElement) { - rootElement.innerHTML = ` + const rootElement = + document.getElementById('root') ?? + document.body.appendChild(document.createElement('div')); + + rootElement.innerHTML = ` <div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; font-family: system-ui, -apple-system, sans-serif;"> <div style="max-width: 400px; text-align: center;"> <div style="font-size: 48px; margin-bottom: 16px;">😵</div> <h1 style="font-size: 20px; font-weight: bold; margin-bottom: 8px; color: `#333`;">应用加载失败</h1> <p style="color: `#666`; margin-bottom: 16px;">您的浏览器可能不支持运行此应用。请尝试使用最新版本的 Chrome、Firefox、Safari 或 Edge。</p> <button onclick="window.location.reload()" style="padding: 8px 16px; background: `#3b82f6`; color: white; border: none; border-radius: 6px; cursor: pointer;">重新加载</button> </div> </div> - `; - } + `; }Also applies to: 33-44
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.tsx` around lines 13 - 15, The catch path re-queries '#root' and aborts rendering the fallback UI if the original thrown error is "Root element not found"; update the error handling in main.tsx so it does not depend on finding '#root' again: inside the catch block (where the thrown Error('Root element not found') is handled) create or select a safe fallback container (e.g., document.body.appendChild(document.createElement('div')) or a dedicated fallbackRoot) and inject the error screen there instead of re-checking rootElement; ensure the logic around rootElement and the error case always renders the fallback UI (reference rootElement and the catch/error-rendering block in main.tsx).src/components/BackToTop.tsx-84-89 (1)
84-89:⚠️ Potential issue | 🟡 MinorThe
/* ... */notes insideclassNameare emitted as real classes.Inside a template string, those aren’t comments. They end up as junk class tokens on the button and make the styling harder to reason about. Move the notes to JSX comments outside the string.
Suggested fix
className={` fixed z-50 flex items-center justify-center w-12 h-12 @@ ${isVisible ? 'opacity-100 translate-y-0 pointer-events-auto' : 'opacity-0 translate-y-4 pointer-events-none' } ${isBouncing ? 'animate-bounce-twice' : ''} - /* 移动端:大幅上移避免遮挡底部多选工具栏 */ bottom-24 right-4 - /* 平板:继续上移 */ sm:bottom-28 sm:right-6 - /* 桌面:适度上移 */ lg:bottom-24 lg:right-10 `}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/BackToTop.tsx` around lines 84 - 89, The class comment tokens (/* ... */) inside the template string for the BackToTop component's className are being emitted as real classes; open the BackToTop component and remove those inline /* ... */ notes from the className value (the template literal used for the button/container) and place them as JSX comments outside the string (e.g., {/* 移动端:... */} above or beside the element) so only valid Tailwind class tokens remain in the className and spacing/conditional classes (bottom-24 right-4, sm:bottom-28 sm:right-6, lg:bottom-24 lg:right-10) are preserved.src/components/ErrorBoundary.tsx-60-65 (1)
60-65:⚠️ Potential issue | 🟡 MinorHardcoded Chinese text in error boundary fallback.
All user-facing strings are in Chinese only, but the app supports English. Since
ErrorBoundaryis a class component wrapping the app, accessing the store's language state is problematic. Consider:
- Accept a
languageprop from the parent (though this may not be available during errors)- Read
localStoragedirectly for the language preference as a fallback- Accept this limitation since error boundaries handle catastrophic failures
This is a minor issue given the fallback nature of this component.
Also applies to: 80-80, 86-86, 91-92
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ErrorBoundary.tsx` around lines 60 - 65, The ErrorBoundary render fallback currently contains hardcoded Chinese strings; update the ErrorBoundary class (render method/fallback UI) to use a localized message based on a language preference: add an optional language prop to the ErrorBoundary constructor/props and prefer that, and if not present read a fallback from localStorage (e.g. localStorage.getItem('language') or similar) to choose English vs Chinese messages; replace the hardcoded headings and paragraphs with conditional strings (or a small lookup object) keyed by that language value so the fallback UI shows the correct language even when the store is inaccessible.src/components/ErrorBoundary.tsx-38-38 (1)
38-38:⚠️ Potential issue | 🟡 MinorImport database name constant to avoid duplication.
The database name is hardcoded as
'github-stars-manager-db', but it's already defined asDB_NAMEinsrc/services/indexedDbStorage.ts. Import and use the constant instead to ensure consistency and avoid maintenance issues if the database name changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ErrorBoundary.tsx` at line 38, Replace the hardcoded string 'github-stars-manager-db' in ErrorBoundary where you call indexedDB.deleteDatabase with the exported DB_NAME constant from indexedDbStorage.ts: add an import for DB_NAME and use DB_NAME in the deleteDatabase call (reference: DB_NAME and the indexedDB.deleteDatabase invocation) so the code uses the single source of truth for the DB name.src/components/BulkActionToolbar.tsx-48-55 (1)
48-55:⚠️ Potential issue | 🟡 MinorClean up
confirmTimeoutRefon unmount too.A pending 3-second confirm timer can still fire after the toolbar unmounts and call
setShowConfirm(null). AddconfirmTimeoutRefto the existing cleanup effect alongsideshakeTimeoutRef.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/BulkActionToolbar.tsx` around lines 48 - 55, The cleanup effect in React.useEffect currently only clears shakeTimeoutRef on unmount; also clear confirmTimeoutRef to prevent the pending 3s confirm timer from firing and calling setShowConfirm(null) after unmount. Update the existing cleanup returned function inside React.useEffect to check and clear confirmTimeoutRef.current (like shakeTimeoutRef.current) so both timeouts are cleared on unmount.src/components/ReleaseTimeline.tsx-239-245 (1)
239-245:⚠️ Potential issue | 🟡 MinorClamp
currentPagewhen the result count shrinks.
totalPagescan drop after refreshes, unsubscribes, or other store-driven result changes, butcurrentPageis never corrected. The component can then land on an empty slice and show the “No matching results” state even though earlier pages still have data.Also applies to: 746-766
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 239 - 245, After computing totalPages (from totalPages = ...), clamp currentPage into the valid range (e.g. set currentPage = Math.min(Math.max(currentPage, 1), Math.max(totalPages, 1)) or update the local pagination var) before calculating startIndex and slicing; then recompute startIndex, paginatedReleases and paginatedRepositoryGroups so slices never end up empty when totalPages has shrunk. Apply the same clamp logic wherever pagination is computed (the other pagination block that builds startIndex/paginated slices) to ensure the component always falls back to the last available page.
🧹 Nitpick comments (3)
src/components/settings/GeneralPanel.tsx (1)
105-116: Open external links withoutwindow.opener.Both buttons use
window.open(..., '_blank'), which leaves the opened page able to navigate the original window. Please addnoopener,noreferreror route this through a shared external-link helper.Suggested fix
+ const openExternal = (url: string) => { + window.open(url, '_blank', 'noopener,noreferrer'); + }; + return ( @@ <button - onClick={() => window.open('https://x.com/GoodMan_Lee', '_blank')} + onClick={() => openExternal('https://x.com/GoodMan_Lee')} className="flex items-center justify-center space-x-2 px-4 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors" > @@ <button - onClick={() => window.open('https://github.com/AmintaCCCP/GithubStarsManager', '_blank')} + onClick={() => openExternal('https://github.com/AmintaCCCP/GithubStarsManager')} className="flex items-center justify-center space-x-2 px-4 py-3 bg-gray-800 hover:bg-gray-900 text-white rounded-lg transition-colors" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/GeneralPanel.tsx` around lines 105 - 116, The onClick handlers for the Twitter and GitHub buttons open external pages with window.open('_blank') which leaves window.opener set; update those handlers in GeneralPanel.tsx (the button elements with onClick={() => window.open(..., '_blank')}) to open safely by passing a feature string and nullifying opener: call const w = window.open(url, '_blank', 'noopener,noreferrer'); if (w) w.opener = null; or alternatively route these through a shared openExternal helper that performs the same steps (open with 'noopener,noreferrer' and set w.opener = null) and use that helper in the two button onClick handlers.src/components/ReadmeModal.tsx (1)
40-46: Consider passing AbortSignal to the API for actual request cancellation.The
AbortControlleris used to prevent stale state updates, but the signal isn't passed togetRepositoryReadme(). The network request continues even after abort. For better efficiency, pass the signal to enable actual request cancellation:💡 Suggested improvement
- const content = await githubApi.getRepositoryReadme(owner, name); + const content = await githubApi.getRepositoryReadme(owner, name, { signal: abortController.signal });This requires updating
getRepositoryReadmeto accept and forward the signal tomakeRequest.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReadmeModal.tsx` around lines 40 - 46, The fetch in ReadmeModal uses an AbortController only to avoid stale state but doesn't cancel the underlying request; update the call in ReadmeModal (where you create GitHubApiService and call getRepositoryReadme) to pass abortController.signal into getRepositoryReadme, and update GitHubApiService.getRepositoryReadme to accept an optional AbortSignal and forward it into the underlying HTTP helper (e.g., makeRequest or fetch) so the network request is actually aborted when abortController.abort() is called; keep existing signal check in ReadmeModal to avoid state updates after abort.src/components/MarkdownRenderer.tsx (1)
129-137: Inline code detection relies on assumed node structure.The check
(node as { parent?: { tagName?: string } }).parent?.tagName !== 'pre'assumes a specific node structure that may vary across react-markdown versions. This pattern is common but consider adding a fallback:💡 Safer approach
code: ({ node, children }) => { - const isInline = !node || (node as { parent?: { tagName?: string } }).parent?.tagName !== 'pre'; + // Fallback to inline styling if parent info unavailable + const parent = (node as { parent?: { tagName?: string } })?.parent; + const isInline = !parent || parent.tagName !== 'pre';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/MarkdownRenderer.tsx` around lines 129 - 137, The inline-code detection in the code renderer uses a brittle cast: (node as { parent?: { tagName?: string } }).parent?.tagName !== 'pre'; update the isInline calculation inside the code renderer to safely guard the parent and tagName checks and provide a safe fallback (treat as inline) if the expected properties are missing; specifically, reference the code renderer's isInline, node and parent.tagName and implement something like a guarded check that first verifies node and node.parent exist and typeof node.parent.tagName === 'string' before comparing to 'pre', otherwise default to inline mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/BulkCategorizeModal.tsx`:
- Around line 32-52: The helper t is referenced inside handleCategorize but
declared later as a const, causing a runtime ReferenceError; move the t helper
before handleCategorize (or convert it to a function declaration) so it is
defined when handleCategorize executes, e.g., place the t function above the
handleCategorize definition or change "const t = (zh,en) => ..." to "function
t(zh,en) { ... }" and keep usages in handleCategorize, setError and elsewhere
unchanged.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 422-428: releasesTruncatedBody is built only from
paginatedReleases so releases rendered from paginatedRepositoryGroups end up
with undefined truncated bodies and fall back to full release.body; update the
computation of releasesTruncatedBody to iterate both paginatedReleases and
paginatedRepositoryGroups (iterate each group and its releases) and call
getTruncatedBody(release.body, 500) for every release id, returning a Map used
by ReleaseCard; apply the same fix to the other analogous block that builds
truncated bodies (the second occurrence around the grouped-releases rendering).
In `@src/components/RepositoryCard.tsx`:
- Around line 549-559: The card-level key handler handleCardKeyDown should
ignore events originating from nested interactive elements; update it to bail
out early when event.target !== event.currentTarget (or reuse the same
interactive-target check used in handleCardClick) so Enter/Space pressed on
inner buttons/links don't toggle selection or open the README; keep the rest of
the logic using selectionMode, onSelect(repository.id) and
setReadmeModalOpen(true) unchanged.
- Around line 909-929: The custom comparator on React.memo for RepositoryCard
(wrapping RepositoryCardComponent) omits many rendered props so updates to
fields like name, full_name, owner, html_url, description, topics, ai_platforms,
language, and last_edited won’t trigger re-renders; fix by either removing the
custom comparator and exporting RepositoryCard as
React.memo(RepositoryCardComponent) to use React’s default shallow comparison,
or update the comparator to also compare each missing prop (e.g.,
prevProps.repository.name, prevProps.repository.full_name,
prevProps.repository.owner, prevProps.repository.html_url,
prevProps.repository.description, prevProps.repository.topics,
prevProps.repository.ai_platforms, prevProps.repository.language,
prevProps.repository.last_edited) as well as any other rendered fields so the
function correctly returns false when any of those change.
In `@src/components/RepositoryEditModal.tsx`:
- Around line 229-250: The toggle is misleading because handleSave()
unconditionally sets category_locked=true when it detects a category change,
ignoring the current formData.categoryLocked; update handleSave to respect
formData.categoryLocked (use the value instead of forcing true) and only
override it if you explicitly want to enforce automatic locking (e.g., change
logic that sets categoryLocked in the category change handler). Locate
handleSave, the category change handler that calls setFormData(...,
categoryLocked: true), and the formData.categoryLocked checkbox; either stop
forcing categoryLocked in the category-change handler and let the user control
the toggle, or keep auto-enable but disable the checkbox in the UI when a manual
category change occurs (so the visual state matches what handleSave will
persist).
In `@src/components/settings/AIConfigPanel.tsx`:
- Around line 200-206: The "Add AI Config" button only toggles visibility so
opening the form while an existing config is being edited leaves editingId and
form values intact; update the button handler in AIConfigPanel to explicitly
clear the edit state before showing the form (call setEditingId(null) and reset
any form state like setFormValues({}) or invoke the form reset method you use)
so a new config is created instead of updating the existing one; ensure you
reference the same state setters (setShowForm, setEditingId,
setFormValues/resetForm) used elsewhere in the component.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 255-306: Both sync buttons can run concurrently because only their
own disabled flags (isSyncingToBackend / isSyncingFromBackend) are used; update
the handlers and UI to prevent simultaneous push/pull by introducing a shared
guard (e.g., a single isSyncing boolean or checking the opposite flag) inside
handleSyncToBackend and handleSyncFromBackend to return early if the other sync
is active, set/clear the shared flag when starting/finishing a sync, and change
each button's disabled prop to use the combined guard (disabled={isSyncing ||
isSyncingToBackend} and disabled={isSyncing || isSyncingFromBackend} or
equivalent) so only one sync can run at a time.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 51-66: The backup payload in BackupPanel.tsx (the backupData
object) omits the new sidebar/category preference fields; add categoryOrder,
collapsedSidebarCategoryCount, and isSidebarCollapsed to the backupData object
(similarly include them where the payload is built in the 122-161 block) and
ensure the restore code that consumes this payload applies these values back to
the app state (e.g., via the same handlers that set category order and sidebar
state); reference the backupData construction and the restore/import handler to
locate where to add reading/writing of categoryOrder,
collapsedSidebarCategoryCount, and isSidebarCollapsed.
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 303-307: The confirmation logic blocks “Delete All Data” for
logged-out users by always comparing confirmation.githubUsernameInput to
user.login; update the checks in DataManagementPanel so that when user is null
the username comparison is skipped (i.e., allow confirmation if
confirmation.type === 'all' and there is no user), and apply the same change to
the equivalent checks around the other affected blocks (the sections around
lines 641-665 and 677-681) so the confirm button can succeed for logged-out
sessions while preserving the username verification when a user is present.
- Around line 198-211: The reset action currently only checks
customCategories.length, so users who changed sidebar/display settings cannot
reset them; update the logic around deleteCategorySettings and the corresponding
UI disabled/count calculations (also adjust the similar blocks referenced later)
to compute a combined "resettable changes" indicator: include
customCategories.length plus presence/length of
useAppStore.getState().hiddenDefaultCategoryIds,
useAppStore.getState().categoryOrder, and boolean checks for
collapsedSidebarCategoryCount !== 20 and isSidebarCollapsed !== false (or their
defaults) to determine the row's count and disabled state; ensure the
deleteCategorySettings function still clears all those keys
(hiddenDefaultCategoryIds, categoryOrder, collapsedSidebarCategoryCount,
isSidebarCollapsed) and that the UI label/count reflects this aggregated
presence so the reset button is enabled when any of these persisted settings
exist.
- Around line 231-278: The deleteAllData path currently calls
useAppStore.setState(...) which only patches the store and allows
persist.partialize to re-save omitted keys; change deleteAllData() to perform a
full store reset by replacing the entire state (e.g. call
useAppStore.setState(initialState, true) or otherwise supply the complete
initial state with the replace flag) and then clear persisted storage via the
persistence API (e.g. call useAppStore.persist?.clearStorage() or equivalent)
before triggering reload so keys like theme/currentView/language/etc. are not
re-persisted; reference the deleteAllData function, useAppStore.setState, and
the persist.clearStorage (or persist API) to locate and update the code.
In `@src/components/settings/GeneralPanel.tsx`:
- Line 5: The import of package.json version in
src/components/settings/GeneralPanel.tsx (import { version } from
'../../../package.json') requires TypeScript JSON module resolution but
tsconfig.app.json is missing the flag; open tsconfig.app.json and add
"resolveJsonModule": true to the "compilerOptions" so the compiler allows
importing JSON modules (keep the rest of compilerOptions intact).
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 122-128: The "Add WebDAV" button currently only calls
setShowForm(true) so if editingId is set the form opens in edit mode and saving
will overwrite the existing config; update the onClick handler for the Add
WebDAV button to clear the edit state before showing the form (e.g., call
setEditingId(null) or resetEditingId and reset form fields/state) so that
opening via this path always starts a fresh create flow; ensure you reference
the existing handlers/state setters (setShowForm, setEditingId and any form
reset function used by the WebDAV form component) when making the change.
In `@src/index.css`:
- Around line 29-33: The .scrollbar-auto CSS rule uses the deprecated overflow:
overlay; update the .scrollbar-auto class to use overflow: auto instead to
ensure cross-browser compatibility while keeping the existing scrollbar-width
and scrollbar-color declarations; locate the .scrollbar-auto selector in
src/index.css and replace the overflow declaration accordingly.
In `@src/polyfills.ts`:
- Around line 4-53: Remove the hand-rolled, non-spec-compliant polyfills in
src/polyfills.ts (the Promise.allSettled assignment, Object.values,
Object.entries and Array.from overrides) and instead rely on vetted polyfills
provided by your build (e.g., `@vitejs/plugin-legacy`) or include a full,
spec-compliant polyfill package (e.g., core-js) in the build pipeline; locate
the file by the symbols Promise.allSettled, Object.values, Object.entries and
Array.from and either delete these overrides or replace them with proper imports
from a tested polyfill library so native behavior for iterables, plain values,
mapFn and thisArg is preserved.
---
Outside diff comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 211-228: handleDeleteCategory is doing an optimistic local delete
(deleteCustomCategory(category.id)) before awaiting forceSyncToBackend(), but on
sync failure it only alerts and does not restore local state; capture the
pre-delete state (e.g., previousCategories and the list of repos with their
repo.custom_category values) before calling deleteCustomCategory, and in the
catch block restore that state (re-add the category and reset affected repos'
custom_category) so local state is rolled back if forceSyncToBackend() throws;
keep using handleDeleteCategory, deleteCustomCategory and forceSyncToBackend
names to locate the code and perform the restore via your existing state updater
or dispatch functions.
In `@src/components/RepositoryCard.tsx`:
- Around line 780-790: The duplicated topics are rendered because
repository.topics is shown twice: once via displayTags.tags and again in the
explicit repository.topics.slice(0,2) JSX; to fix, modify the conditional on
that second block (the JSX that maps repository.topics.slice(0,2)) to only
render when displayTags.tags is not already the same as repository.topics —
e.g., add a guard like && JSON.stringify(displayTags.tags) !==
JSON.stringify(repository.topics) (or an equivalent element-wise comparison) in
addition to the existing !displayTags.isCustom check so the topics block is
skipped when displayTags.tags already comes from repository.topics.
---
Minor comments:
In `@src/components/BackToTop.tsx`:
- Around line 84-89: The class comment tokens (/* ... */) inside the template
string for the BackToTop component's className are being emitted as real
classes; open the BackToTop component and remove those inline /* ... */ notes
from the className value (the template literal used for the button/container)
and place them as JSX comments outside the string (e.g., {/* 移动端:... */} above
or beside the element) so only valid Tailwind class tokens remain in the
className and spacing/conditional classes (bottom-24 right-4, sm:bottom-28
sm:right-6, lg:bottom-24 lg:right-10) are preserved.
In `@src/components/BulkActionToolbar.tsx`:
- Around line 48-55: The cleanup effect in React.useEffect currently only clears
shakeTimeoutRef on unmount; also clear confirmTimeoutRef to prevent the pending
3s confirm timer from firing and calling setShowConfirm(null) after unmount.
Update the existing cleanup returned function inside React.useEffect to check
and clear confirmTimeoutRef.current (like shakeTimeoutRef.current) so both
timeouts are cleared on unmount.
In `@src/components/ErrorBoundary.tsx`:
- Around line 60-65: The ErrorBoundary render fallback currently contains
hardcoded Chinese strings; update the ErrorBoundary class (render
method/fallback UI) to use a localized message based on a language preference:
add an optional language prop to the ErrorBoundary constructor/props and prefer
that, and if not present read a fallback from localStorage (e.g.
localStorage.getItem('language') or similar) to choose English vs Chinese
messages; replace the hardcoded headings and paragraphs with conditional strings
(or a small lookup object) keyed by that language value so the fallback UI shows
the correct language even when the store is inaccessible.
- Line 38: Replace the hardcoded string 'github-stars-manager-db' in
ErrorBoundary where you call indexedDB.deleteDatabase with the exported DB_NAME
constant from indexedDbStorage.ts: add an import for DB_NAME and use DB_NAME in
the deleteDatabase call (reference: DB_NAME and the indexedDB.deleteDatabase
invocation) so the code uses the single source of truth for the DB name.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 239-245: After computing totalPages (from totalPages = ...), clamp
currentPage into the valid range (e.g. set currentPage =
Math.min(Math.max(currentPage, 1), Math.max(totalPages, 1)) or update the local
pagination var) before calculating startIndex and slicing; then recompute
startIndex, paginatedReleases and paginatedRepositoryGroups so slices never end
up empty when totalPages has shrunk. Apply the same clamp logic wherever
pagination is computed (the other pagination block that builds
startIndex/paginated slices) to ensure the component always falls back to the
last available page.
In `@src/main.tsx`:
- Around line 13-15: The catch path re-queries '#root' and aborts rendering the
fallback UI if the original thrown error is "Root element not found"; update the
error handling in main.tsx so it does not depend on finding '#root' again:
inside the catch block (where the thrown Error('Root element not found') is
handled) create or select a safe fallback container (e.g.,
document.body.appendChild(document.createElement('div')) or a dedicated
fallbackRoot) and inject the error screen there instead of re-checking
rootElement; ensure the logic around rootElement and the error case always
renders the fallback UI (reference rootElement and the catch/error-rendering
block in main.tsx).
---
Duplicate comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 97-117: The reset logic uses a stale snapshot
(assetFilters/selectedFilters) while calling mutating helpers
(deleteAssetFilter, addAssetFilter, onFilterToggle), causing skipped adds and
broken rollback; fix by operating on and comparing to a fresh snapshot or by
using atomic store updates: capture the intended final preset set (e.g., compute
desiredPresetIds from DEFAULT_PRESET_FILTERS and presetFilters), then call
store-level operations that replace the presets in one update (or re-query
assetFilters after each mutation) instead of repeatedly reading the old
assetFilters; ensure rollback uses the original previousFilters/previousSelected
snapshots to fully restore and use addedFilterIds only from actual successful
addAssetFilter results.
In `@src/components/MarkdownRenderer.tsx`:
- Around line 82-87: The image-error fallback in MarkdownRenderer renders a
hardcoded Chinese string; update MarkdownImage (or MarkdownRenderer) to use the
app locale instead by either reading language from useAppStore inside the
MarkdownImage component or by adding a language prop passed into MarkdownImage
from MarkdownRenderer; then replace the hardcoded "[图片加载失败: ...]" with a
localized message (e.g., using a simple conditional or i18n lookup based on
language) so the alt/error text respects the current locale.
In `@src/components/RepositoryList.tsx`:
- Around line 123-127: The toolbar uses the raw selectedRepoIds set for the
selected count while actions use the filtered selection; update any
count/display logic (e.g., the selectedCount used in the toolbar) to derive its
value from the filtered selectedRepositories (use selectedRepositories.length)
instead of selectedRepoIds.size so the UI and action payloads stay consistent;
also replace other occurrences that display or rely on the raw set (refer to
selectedRepositories and selectedRepoIds in RepositoryList.tsx) so all counts
and labels reflect the current filtered selection.
- Around line 563-621: The loop never honors a live pause flag and reads
analysisProgress from a stale closure; fix by (1) introducing/using a shared
pause ref (e.g., shouldPauseRef or isPausedRef) inside the batch worker and,
before processing each repo (inside batch.map and/or before starting each
batch), await a small async loop that yields until shouldPauseRef.current is
false so the worker actually pauses; and (2) update progress using the
functional state updater for setAnalysisProgress instead of reading
analysisProgress from the render closure—replace the per-repo increment logic
with setAnalysisProgress(prev => ({ current: Math.min(prev.current + 1,
prev.total), total: prev.total })) so progress is consistent under concurrency
and not using stale values from the initial render.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 51-66: The backup payload in BackupPanel.tsx must include the
active config ids and preserve activation state on restore: add activeAIConfig
and activeWebDAVConfig fields to the exported backupData alongside
aiConfigs/webdavConfigs (still masking apiKey/password), and update the restore
logic that currently references existing.isActive and forces new WebDAV configs
inactive so it instead applies the isActive value from the backed-up config (for
both updated and new entries) and uses the exported
activeAIConfig/activeWebDAVConfig ids to set the store’s active pointers; update
code paths that handle aiConfigs, webdavConfigs, and the existing.isActive
checks to follow the backed-up state rather than defaulting WebDAV to inactive.
In `@src/components/SettingsPanel.tsx`:
- Around line 380-386: The mobile tab nav's aria-controls IDs don't match the
content panels because renderTabContent() is always called with the 'desktop'
prefix; update the mobile rendering to call renderTabContent('mobile') (or
otherwise switch to a shared ID scheme) so the IDs created by renderTabContent
match MobileTabNav's aria-controls; locate the MobileTabNav usage in
SettingsPanel.tsx and change the renderTabContent call within the md:hidden
block (and the similar block around lines 438-444) to pass 'mobile' (or adjust
both MobileTabNav and renderTabContent to use the same prefix variable) so ARIA
associations line up.
- Around line 329-397: The modal rendered when isModal is true lacks a focus
trap and focus restoration; update the modal logic around the main dialog div
(the element with role="dialog" rendered in the isModal branch) and the handler
handleClose to implement focus management: on open save document.activeElement,
move focus to a designated initial focusable element inside the dialog (create a
ref like modalRef or initialFocusRef), add a keydown listener to trap
Tab/Shift+Tab within modalRef and to close on Escape, and on close/cleanup
restore focus to the previously focused element; implement these behaviors in a
useEffect tied to isModal and ensure cleanup removes listeners and nulls refs so
background content is not tabbable while the modal is open.
In `@src/store/useAppStore.ts`:
- Around line 294-301: The local constant defaultPresetFilters in useAppStore.ts
duplicates canonical presets and reintroduces removed archive extensions;
replace this local array with an import of the canonical presets (e.g., import {
presetFilters } from 'src/constants/presetFilters' or the actual exported name)
and use that imported symbol wherever defaultPresetFilters is referenced (remove
the local declaration of defaultPresetFilters and update usages to reference the
imported presetFilters to keep classification consistent).
---
Nitpick comments:
In `@src/components/MarkdownRenderer.tsx`:
- Around line 129-137: The inline-code detection in the code renderer uses a
brittle cast: (node as { parent?: { tagName?: string } }).parent?.tagName !==
'pre'; update the isInline calculation inside the code renderer to safely guard
the parent and tagName checks and provide a safe fallback (treat as inline) if
the expected properties are missing; specifically, reference the code renderer's
isInline, node and parent.tagName and implement something like a guarded check
that first verifies node and node.parent exist and typeof node.parent.tagName
=== 'string' before comparing to 'pre', otherwise default to inline mode.
In `@src/components/ReadmeModal.tsx`:
- Around line 40-46: The fetch in ReadmeModal uses an AbortController only to
avoid stale state but doesn't cancel the underlying request; update the call in
ReadmeModal (where you create GitHubApiService and call getRepositoryReadme) to
pass abortController.signal into getRepositoryReadme, and update
GitHubApiService.getRepositoryReadme to accept an optional AbortSignal and
forward it into the underlying HTTP helper (e.g., makeRequest or fetch) so the
network request is actually aborted when abortController.abort() is called; keep
existing signal check in ReadmeModal to avoid state updates after abort.
In `@src/components/settings/GeneralPanel.tsx`:
- Around line 105-116: The onClick handlers for the Twitter and GitHub buttons
open external pages with window.open('_blank') which leaves window.opener set;
update those handlers in GeneralPanel.tsx (the button elements with onClick={()
=> window.open(..., '_blank')}) to open safely by passing a feature string and
nullifying opener: call const w = window.open(url, '_blank',
'noopener,noreferrer'); if (w) w.opener = null; or alternatively route these
through a shared openExternal helper that performs the same steps (open with
'noopener,noreferrer' and set w.opener = null) and use that helper in the two
button onClick handlers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2111fcc0-fe89-461e-a06d-0ebce9fcfee0
⛔ Files ignored due to path filters (2)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (36)
.github/workflows/build-desktop.ymlpackage.jsonsrc/App.tsxsrc/components/AssetFilterManager.tsxsrc/components/BackToTop.tsxsrc/components/BulkActionToolbar.tsxsrc/components/BulkCategorizeModal.tsxsrc/components/CategorySidebar.tsxsrc/components/ErrorBoundary.tsxsrc/components/Header.tsxsrc/components/MarkdownRenderer.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseCard.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryEditModal.tsxsrc/components/RepositoryList.tsxsrc/components/SearchBar.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.tssrc/constants/presetFilters.tssrc/index.csssrc/main.tsxsrc/polyfills.tssrc/services/githubApi.tssrc/store/useAppStore.tssrc/types/index.tstailwind.config.jsvite.config.ts
refactor(Modal): 改进点击事件处理和样式过渡 refactor(ErrorBoundary): 增强错误处理和多语言支持 refactor(ReadmeModal): 优化数据获取逻辑和错误处理 refactor(BackendPanel): 改进后端连接检查逻辑 refactor(DataManagementPanel): 优化数据删除逻辑 refactor(CategoryPanel): 修复拖拽排序问题 refactor(AssetFilterManager): 优化筛选器重置逻辑 refactor(CategorySidebar): 改进分类计算逻辑 refactor(MarkdownRenderer): 增强代码块渲染和图片处理 refactor(SettingsPanel): 优化标签切换动画 refactor(webdavService): 改进错误处理和重试逻辑 refactor(githubApi): 增强类型定义和请求处理 refactor(useAppStore): 优化状态管理和数据迁移 refactor(categoryUtils): 重构分类计算逻辑 fix: 修复各种边界条件和潜在问题 fix(polyfills): 修正 CustomEvent 类型断言 fix(FilterModal): 移除未使用的图标导入 fix(indexedDbStorage): 导出 DB_NAME 常量 fix(updateService): 增强窗口打开安全性 fix(backendAdapter): 增加请求超时设置 fix(auth): 添加时序攻击防护注释 fix(Header): 移除未使用的图标导入 fix(types): 修正 Release 类型定义 fix(releases): 增强 ID 验证 fix(proxyService): 加强 URL 验证 fix(repositories): 增强数据验证 fix(AIConfigPanel): 修正推理努力设置 fix(Modal): 修复点击事件冒泡 fix(autoSync): 优化同步状态管理 fix(BackToTop): 优化过渡效果 fix(connection): 增强数据库初始化错误处理 fix(sync): 增强数据导入验证 fix(BackupPanel): 优化分类恢复逻辑 fix(GeneralPanel): 增强社交链接安全性 fix(tailwind): 优化动画配置 fix(index.css): 移除未使用的样式 fix(main): 增强错误回退处理 fix(MarkdownRenderer): 优化代码块渲染 fix(AssetFilterManager): 修复展开动画 fix(CategorySidebar): 优化分类计算 fix(ErrorBoundary): 增强错误处理 fix(ReadmeModal): 优化数据获取 fix(useAppStore): 修复数据迁移 fix(webdavService): 改进错误处理 chore: 更新依赖和配置 chore: 更新构建配置和依赖
|
@coderabbitai full review and summary |
|
PR
|
| Component | Responsibility |
|---|---|
GeneralPanel |
Language toggle, update checker, contact links |
AIConfigPanel |
AI service config CRUD + connection testing |
WebDAVPanel |
WebDAV config management + connection testing |
BackupPanel |
Backup/restore via WebDAV (masked secrets for API keys/passwords) |
BackendPanel |
Backend health check, auth verification, bidirectional data sync |
CategoryPanel |
Custom category CRUD, default category show/hide, reorder |
DataManagementPanel |
Data statistics, selective deletion per data type, full data wipe |
index.ts |
Barrel export for all subcomponents |
4. 📂 Sidebar Optimization (CategorySidebar.tsx)
- Responsive layout: mobile always shows full sidebar; desktop supports collapse/expand.
- Collapsed mode shows icon-only view,
Ctrl/Cmd + Bkeyboard shortcut to toggle. isSidebarCollapsedpersisted in Zustand store across reloads.
5. ⬆️ Back-to-Top Button (BackToTop.tsx + App.tsx)
- Fixed-position button visible when
scrollY > 300px. - Smooth scrolling, ARIA accessibility attributes, and localized tooltip text.
6. 📦 Bulk Selection & Actions (RepositoryList.tsx, BulkActionToolbar.tsx, BulkCategorizeModal.tsx)
- Multi-select repository cards with select-all/deselect-all.
- Bulk actions: unstar, categorize, AI summary, subscribe/unsubscribe to releases.
- Two-step confirmation with 3-second auto-clear for safety.
7. 🔍 Asset Filter Presets (AssetFilterManager.tsx, presetFilters.ts)
PRESET_FILTERSfor Windows, macOS, Linux, Android, and Source.- Expandable filter panel with preset chips and reset-presets action.
8. 📖 README Modal (ReadmeModal.tsx)
- Fetches and renders README via
ReactMarkdown+remark-gfm+rehype-sanitize. - UTF-8 decoding, relative link/image resolution, focus management, Escape-to-close.
9. 🎨 CSS Utilities & Animations (index.css, tailwind.config.js)
.scrollbar-hide,.touch-manipulation,.gpu-accelerated.- New animation keyframes:
slide-up,slide-down,fade-in,selection-exit,bounce-twice,shake.
10. 🗄️ Store & Type Updates (useAppStore.ts, types/index.ts)
isSidebarCollapsed,categoryOrder,collapsedSidebarCategoryCountadded toAppStateand persisted store.- New Release Timeline state fields:
releaseViewMode,releaseSelectedFilters,releaseSearchQuery,releaseExpandedRepositories. AssetFilterextended with optionalisPresetandiconfields.
11. 🛡️ Other Additions
ErrorBoundaryclass component wrapping the app with graceful error fallback UI.MarkdownRenderermemoized component with customized element rendering.ReleaseCardcomponent with separate asset and release-notes expansion controls.polyfills.tsforPromise.allSettled,Object.values,CustomEvent, etc.- Node.js version bumped to 22 in
build-desktop.yml.
⚠️ Issues to Address Before Merging
-
❌ Compilation failure — Reported by
@AmintaCCCP. Likely caused by unresolved merge conflict markers (<<<<<<< HEAD/=======/>>>>>>>) insrc/components/ReadmeModal.tsxand/orsrc/components/RepositoryList.tsx. This must be fixed first. -
📂 Sidebar collapsed — categories not fully visible / not scrollable — Flagged by
@AmintaCCCP: collapsed sidebar doesn't allow scrolling through categories. -
🖱️ Drag-and-drop to category sidebar broken — Drag + click interaction conflict on
RepositoryCard. Suggestion: add a dedicated drag handle icon as the drag hotspot to separate the two interactions. -
📋 Release page UX regression — Clicking to expand assets also expands the release changelog simultaneously. Recommendation: separate "expand changelog" and "expand download assets" into two distinct buttons. Also, filters now highlight instead of filter (increasing noise), and list item padding is too tight against container edges.
-
🔑
BackupPanelmasked secret restore — Ensure restore logic correctly preserves existing credentials when backup contains masked***values. -
🔄
DataManagementPanelwindow.location.reload()— Forced reload after "delete all data" — confirm this is the intended UX.
✅ Actions performed
Full review triggered.
|
需要测试从旧版本升级的数据迁移 |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/services/webdavService.ts (1)
343-353:⚠️ Potential issue | 🟡 MinorDead code:
HTTP 404string is never produced.The inner 404 branch (line 325–327) already returns
nullbefore throwing, and the only non-404 throw uses the message下载失败,HTTP状态码 ${status}: ${statusText}— no code path produces a message containingHTTP 404. Thisincludes('HTTP 404')check therefore never matches. Drop the branch (or, if the intent was to translate a generic 404 fromhandleNetworkErrorpaths, fix the matcher to the actual message).🧹 Proposed fix
} catch (error: unknown) { const err = error as Error; if (err.message.includes('身份验证失败') || err.message.includes('下载超时')) { throw error; } - if (err.message.includes('HTTP 404')) { - return null; - } this.handleNetworkError(error, '下载'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/webdavService.ts` around lines 343 - 353, The catch block in src/services/webdavService.ts contains a dead branch checking err.message.includes('HTTP 404') that never matches because 404s are produced as "下载失败,HTTP状态码 ${status}: ${statusText}" earlier; remove that unreachable includes('HTTP 404') branch (or replace it with a check that matches the actual message, e.g., matching 'HTTP状态码 404' or inspecting a numeric status code if available) and keep using this.handleNetworkError(error, '下载') for other cases; update references in this catch to only handle authentication/timeout rethrows, the 404 null-return behavior, and the general network error path, and ensure to use handleNetworkError for remaining errors.src/components/SearchBar.tsx (1)
236-237:⚠️ Potential issue | 🟠 MajorAvoid sorting the input array in place.
filteredaliasesrepos, sofiltered.sort(...)mutates the caller array. WhenapplyFilters(repositories)runs, that mutates the source repository order in place and can leak across renders/state updates. Sort a copy before returning.Proposed fix
- filtered.sort((a, b) => { + filtered = [...filtered].sort((a, b) => { const aValue = getSortValue(a); const bValue = getSortValue(b); if (aValue < bValue) return searchFilters.sortOrder === 'desc' ? 1 : -1; if (aValue > bValue) return searchFilters.sortOrder === 'desc' ? -1 : 1; return 0; });Also applies to: 348-354
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SearchBar.tsx` around lines 236 - 237, The applyFilters function currently aliases the input array via let filtered = repos and calls filtered.sort(...), which mutates the caller's array; change it to operate on a shallow copy (e.g., const filtered = [...repos] or repos.slice()) before sorting or filtering so the original repositories array is not mutated, and apply the same copy-before-sort fix to the other sort call in the same file (the second sort around the other sort block referenced in the review).src/components/CategorySidebar.tsx (1)
214-230:⚠️ Potential issue | 🟠 MajorRollback is missing on failed category deletion sync.
This path deletes the category locally before
forceSyncToBackend(), but thecatchonly shows an alert. If sync fails, the client is left with the category removed and any cascaded local assignment changes already applied, even though the backend still has the old state.Suggested direction
Either snapshot and restore the affected local state in the
catch, or defer the destructive local mutation until after the backend write succeeds. The current comment already says "Revert local change on failure", but that rollback never happens.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 214 - 230, The deletion flow in handleDeleteCategory currently calls deleteCustomCategory(category.id) before awaiting forceSyncToBackend(), but the catch only alerts and does not restore the local state; update handleDeleteCategory to either (A) perform the backend sync first and only call deleteCustomCategory(category.id) after forceSyncToBackend() succeeds, or (B) snapshot the affected local state (category list and any repository category assignments) before calling deleteCustomCategory, then if forceSyncToBackend() throws restore that snapshot in the catch; reference the existing functions/identifiers deleteCustomCategory, forceSyncToBackend, and handleDeleteCategory when making this change.
♻️ Duplicate comments (3)
src/index.css (1)
29-33:⚠️ Potential issue | 🟡 MinorDeprecated
overflow: overlaystill present.
overflow: overlaywas flagged previously and is also reported by Stylelint. It's a non-standard value that Chrome/Edge 114+ alias toautoand Firefox/Safari never supported. Replace withoverflow: auto— the thin/transparentscrollbar-width/scrollbar-colordeclarations already provide the overlay-like visual treatment.💡 Suggested fix
.scrollbar-auto { scrollbar-width: thin; scrollbar-color: transparent transparent; - overflow: overlay; + overflow: auto; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/index.css` around lines 29 - 33, The .scrollbar-auto CSS rule uses the non-standard overflow: overlay; replace that with overflow: auto in the .scrollbar-auto rule so browsers use the standard behavior while keeping the existing scrollbar-width and scrollbar-color rules unchanged (i.e., edit the .scrollbar-auto selector to remove overflow: overlay and set overflow: auto).src/components/RepositoryList.tsx (1)
1127-1129:⚠️ Potential issue | 🟠 MajorKeep the toolbar count derived from the same selection payload.
selectedCountstill usesselectedRepoIds.size, while bulk actions operate onselectedRepositories. After a filter change, the toolbar can say “N selected” even though the action only runs on the visible subset. UseselectedRepositories.lengthhere.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 1127 - 1129, The toolbar count is derived from selectedRepoIds.size while bulk actions use selectedRepositories, causing a mismatch after filtering; update the BulkActionToolbar prop by passing selectedCount={selectedRepositories.length} instead of selectedRepoIds.size so the displayed "N selected" matches the set acted on by the BulkActionToolbar and functions that consume selectedRepositories.src/components/ReleaseTimeline.tsx (1)
63-80:⚠️ Potential issue | 🟠 MajorUse the persisted preset filters in
matchesActiveFilters().This matcher still reads preset keywords from
PRESET_FILTERS, so edits made throughAssetFilterManagerwon't affect release filtering, counts, or displayed assets. Build the active filter set fromassetFiltersand only fall back to static presets when a persisted id is missing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 63 - 80, matchesActiveFilters currently reads preset keywords directly from PRESET_FILTERS instead of using the persisted assetFilters, so changes made in AssetFilterManager aren’t reflected; modify matchesActiveFilters to build the active filter set by looking up each selectedFilters id in assetFilters first (use the matching assetFilters entry for keywords) and only fall back to the static PRESET_FILTERS entry when an id is not found in assetFilters, then run the existing keyword matching logic against that combined active set (ensure you still reference selectedFilters and assetFilters in the hook dependencies and keep the function name matchesActiveFilters).
🧹 Nitpick comments (15)
src/services/updateService.ts (1)
117-120: Dead code:noopeneralready nullsopener, andwindow.openreturnsnull.Per the HTML spec, passing
noopenertowindow.opencauses it to returnnulland severs theopenerrelationship. Theif (newWindow) { newWindow.opener = null; }block is therefore unreachable and redundant. The security hardening is already fully achieved by the'noopener,noreferrer'features string alone.♻️ Proposed simplification
static openDownloadUrl(url: string): void { - const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); - if (newWindow) { - newWindow.opener = null; - } + window.open(url, '_blank', 'noopener,noreferrer'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/updateService.ts` around lines 117 - 120, Remove the redundant runtime assignment that tries to nullify opener after calling window.open: the variable newWindow from the window.open(url, '_blank', 'noopener,noreferrer') call can be removed along with the if (newWindow) { newWindow.opener = null; } block; instead rely on the feature string 'noopener,noreferrer' passed to window.open (leave the window.open invocation inline or assign to a throwaway if needed) so the code no longer contains unreachable dead code related to newWindow/opener.server/src/middleware/auth.ts (1)
33-33: Remove duplicate comment.The Chinese comment on line 33 duplicates the English comment on line 32. Both describe the same constant-time comparison logic. Maintaining duplicate comments in different languages increases maintenance burden—if the implementation changes, both comments must be updated.
Backend code comments are typically kept in a single language for consistency. Since the rest of this codebase uses English comments, consider removing the Chinese comment and relying on the existing English one.
♻️ Proposed fix
// Constant-time comparison - // 使用固定时间的比较来防止时序攻击 const tokenBuf = Buffer.from(token);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/middleware/auth.ts` at line 33, Remove the duplicate Chinese comment that repeats the English comment about constant-time comparison in the auth middleware; keep the existing English comment and delete the Chinese line so only the single English comment describing the constant-time comparison logic remains (search for the constant-time comparison comment in auth.ts and remove the duplicate Chinese comment immediately following it).server/src/services/proxyService.ts (2)
53-56: Credential check runs after hostname checks — intentional but worth noting.Placing the
username/passwordrejection after the blocked-host and private-IP checks means a credential-bearing URL targeting a blocked host will surface the host error, not the credential error, which can make the rejection reason in logs less precise. Consider moving the credential check to the top ofvalidateUrl(right after the protocol check) so credential leakage is reported first and the function fails fast before any hostname normalization.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/services/proxyService.ts` around lines 53 - 56, The credential rejection currently runs after blocked-host and private-IP checks in validateUrl, causing credential-bearing URLs to be masked by host errors; move the username/password check (the parsed.username || parsed.password throw) up to immediately after the protocol check inside validateUrl so credential-containing URLs fail fast and surface that specific error before any hostname normalization or blocked-host/private-IP checks.
37-48: Redundant block and loose IPv4 regex.Two issues in the IP validation branch:
Lines 41–43 check
BLOCKED_HOSTS.has(hostname)and line 46 immediately repeats the same check—theisIPconditional is dead code. Either remove the inner block or gatePRIVATE_IP_PATTERNSonisIPto clarify that private IP checking applies only to actual IPs (avoiding accidental matches against hostnames like10.example.com).The regex
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/accepts invalid octets (e.g.,999.1.1.1) and misses shorthand IPv4 forms (127.1,2130706433in decimal, etc.) that browsers andfetchmay still resolve as loopback. Use Node's built-innet.isIP(hostname)instead.♻️ Proposed refactor
- const hostname = parsed.hostname.toLowerCase(); - - // 检查是否是IP地址 - const isIP = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname); - if (isIP) { - // IP地址直接检查是否在阻止列表中 - if (BLOCKED_HOSTS.has(hostname)) { - throw new Error(`Blocked proxy request: IP '${hostname}' is not allowed`); - } - } - - if (BLOCKED_HOSTS.has(hostname)) { - throw new Error(`Blocked proxy request: hostname '${hostname}' is not allowed`); - } - if (PRIVATE_IP_PATTERNS.some(p => p.test(hostname))) { - throw new Error(`Blocked proxy request: private IP '${hostname}' is not allowed`); - } + const hostname = parsed.hostname.toLowerCase(); + + if (BLOCKED_HOSTS.has(hostname)) { + throw new Error(`Blocked proxy request: hostname '${hostname}' is not allowed`); + } + const ipKind = net.isIP(hostname); // 0 | 4 | 6 + if (ipKind === 4 && PRIVATE_IP_PATTERNS.some(p => p.test(hostname))) { + throw new Error(`Blocked proxy request: private IP '${hostname}' is not allowed`); + }Requires
import net from 'node:net';at the top.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/services/proxyService.ts` around lines 37 - 48, The code currently uses a loose IPv4 regex and duplicates the BLOCKED_HOSTS check; replace the regex-based isIP logic with Node's net.isIP(hostname) (import net from 'node:net') and only apply IP-specific checks (e.g., PRIVATE_IP_PATTERNS or private-IP gating) when net.isIP(hostname) returns truthy; remove the redundant BLOCKED_HOSTS.has(hostname) inside the old isIP branch so BLOCKED_HOSTS is checked exactly once (and keep the descriptive Error messages using the hostname/IP where appropriate).src/services/backendAdapter.ts (1)
178-185: 2-minute AI timeout is reasonable, but consider making it configurable.120s matches typical LLM latency budgets and is aligned with upstream backend timeouts. For reasoning-heavy models (e.g., long
reasoning_effort), this can still be tight — consider sourcing the value fromAIConfig(or a store setting) rather than a hardcoded constant so users can tune it without a code change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/backendAdapter.ts` around lines 178 - 185, The hardcoded 120000ms timeout in the AI proxy call should be made configurable: replace the literal timeout passed to fetchWithTimeout in the method that posts to `${this._backendUrl}/proxy/ai` (where configId and body are sent) with a value read from a configurable source (e.g., AIConfig or a store setting) and fall back to 120000ms if not set; ensure the new config key is documented/validated and use the same getter (or inject the config) so callers can tune timeouts without changing code.server/src/routes/repositories.ts (1)
94-98:idvalidator accepts non-integer numbers.
typeof 1.5 === 'number' && 1.5 > 0passes, butidis anINTEGERPK. PreferNumber.isInteger(repo.id) && repo.id > 0to reject floats explicitly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/routes/repositories.ts` around lines 94 - 98, The current validator in the repositories loop allows non-integer numbers because it checks typeof repo.id === 'number'; update the condition to explicitly require an integer by using Number.isInteger(repo.id) and keep the positive check (Number.isInteger(repo.id) && repo.id > 0) so floats like 1.5 are rejected; adjust the same validation branch that returns res.status(400).json({ error: ..., code: 'INVALID_REPOSITORY_ID' }) to use this new check referencing repo.id and repositories in the loop.server/src/routes/releases.ts (1)
89-92: Validation looks correct.The combined
!release.id || typeof !== 'number' || <= 0correctly rejects missing, non-numeric, zero, and negative IDs. Error codeRELEASE_ID_REQUIREDis preserved for backward compatibility with existing clients. Just ensure the client side (syncReleasesinbackendAdapter.ts) never sends floats —INTEGERcolumn will silently coerce, but validator allows non-integer numbers like1.5. Minor — addNumber.isInteger(release.id)if strict integer semantics are desired.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/routes/releases.ts` around lines 89 - 92, The validator in releases.ts currently allows non-integer numbers (e.g., 1.5); update the check in the block that tests release.id to include Number.isInteger(release.id) so only integers pass, and also update the client-side syncReleases function in backendAdapter.ts to guarantee it never sends floats (validate/cast ids to integer or reject non-integers before sending) so the API and client stay consistent.server/src/db/connection.ts (1)
26-29: Preserve original error detail when rethrowing.Re-throwing
new Error('Database initialization failed')discards stack and message from the underlyingbetter-sqlite3/fs error. Operators see only the generic message in higher-level handlers (log line helps but the thrown error is what propagates to callers/tests). Prefer attaching the cause.♻️ Suggested change
- } catch (error) { - console.error('Failed to initialize database:', error); - throw new Error('Database initialization failed'); - } + } catch (error) { + console.error('Failed to initialize database:', error); + throw new Error('Database initialization failed', { cause: error as Error }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/db/connection.ts` around lines 26 - 29, The catch currently discards the original error by throwing a new generic Error; preserve the original error details by rethrowing with the original as the cause or by rethrowing the original error object: replace "throw new Error('Database initialization failed')" with either "throw new Error('Database initialization failed', { cause: error })" (Node/Error supports cause) or "throw error" so callers retain the original message and stack; keep the existing console.error log as-is.vite.config.ts (1)
10-14: Remove redundant browser targets and reconsider modernPolyfills setting.
'defaults'already covers Chrome 130+, Firefox 130+, Safari 19+, Edge 130+ (far exceeding your specified versions). The explicit'Chrome >= 60','Firefox >= 60','Safari >= 12','Edge >= 79'entries are no-ops and add noise;'not IE 11'is also redundant since IE 11 isn't in'defaults'anyway. Simplify to just['defaults'].Additionally,
modernPolyfills: truewithout settingmodernTargetsis problematic—the docs explicitly warn this causes core-js to include excessive polyfills in the modern bundle, defeating the purpose of a lean modern build. Either setmodernTargetsexplicitly or removemodernPolyfills.Since this is an Electron app, verify that the legacy plugin is actually needed for your distribution model. If you're only targeting current Electron/Chromium, you can likely remove it entirely along with the regenerator polyfill.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@vite.config.ts` around lines 10 - 14, The legacy plugin configuration is overly verbose and potentially harmful: simplify the targets passed to legacy(...) by removing redundant entries and use just ['defaults'] (or remove the entire legacy(...) plugin if this Electron app only targets current Chromium/Electron), remove 'not IE 11' and the explicit Chrome/Firefox/Safari/Edge entries, and address modernPolyfills by either removing modernPolyfills or explicitly supplying modernTargets to limit core-js injection; also drop additionalLegacyPolyfills if you remove the plugin or if regenerator-runtime isn't required. Locate the legacy(...) call in vite.config.ts and apply one of these fixes depending on whether you need legacy support.src/utils/categoryUtils.ts (1)
62-81: Minor: consider treating falsyaiCategory/defaultCategoryas no-match.If
aiCategoryandcategoryNameare both empty strings,categoryName === ''is caught first (returns''), so this is fine today. But if a caller ever passes a non-emptycategoryNamewithaiCategory === ''anddefaultCategory === '', the strict-equality checks still work because strings differ. Behavior is correct; flagging only as a readability suggestion: guard withaiCategory && categoryName === aiCategoryto make intent explicit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/categoryUtils.ts` around lines 62 - 81, In computeCustomCategory, the comparisons against aiCategory and defaultCategory should explicitly treat falsy AI/default values as “no match”; update the two checks (the ones currently doing categoryName === aiCategory and categoryName === defaultCategory) to first ensure aiCategory/defaultCategory are truthy (e.g., aiCategory && categoryName === aiCategory and defaultCategory && categoryName === defaultCategory) so empty or undefined AI/default categories don’t unintentionally count as matches.src/components/Header.tsx (1)
250-259: Use the existingt()helper for consistency.The file already exposes
t(zh, en)at line 104. Inlininglanguage === 'zh' ? ... : ...here duplicates the pattern and makes future i18n extraction harder (e.g., when adding a third locale). Minor nit.♻️ Proposed refactor
<button onClick={() => { - const confirmed = confirm( - language === 'zh' - ? '确定要退出登录吗?\n\n退出后您的 AI 配置、WebDAV 设置、自定义分类等数据仍会保留。如需完全清除所有数据,请前往「设置 → 数据管理」。' - : 'Are you sure you want to logout?\n\nYour AI configs, WebDAV settings, custom categories and other data will be preserved. To completely clear all data, please go to "Settings → Data Management".' - ); + const confirmed = confirm(t( + '确定要退出登录吗?\n\n退出后您的 AI 配置、WebDAV 设置、自定义分类等数据仍会保留。如需完全清除所有数据,请前往「设置 → 数据管理」。', + 'Are you sure you want to logout?\n\nYour AI configs, WebDAV settings, custom categories and other data will be preserved. To completely clear all data, please go to "Settings → Data Management".' + )); if (confirmed) { logout(); } }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Header.tsx` around lines 250 - 259, Replace the inline language ternary in the logout onClick handler with the existing t(zh, en) helper to keep i18n consistent: find the onClick block that calls confirm(...) and instead pass the Chinese and English message strings as the two arguments to t(...) (preserving the newline/formatting), then use the returned string in confirm(...) and keep the conditional logout() call unchanged; reference the t helper exposed in this module (t) and the logout() call to locate the code.src/services/webdavService.ts (1)
48-73: Preferinstanceof Errorover blindas Errorcasts.
catch (error: unknown)followed byerror as Errorsilently trusts that anything thrown has.name/.message. Non-Errorthrows (strings, plain objects,DOMExceptionedge cases) will produceundefinedwhen accessed, which then passes throughincludes(...)as aTypeError. A narrow guard is safer and only a few lines:♻️ Suggested pattern
const err = error instanceof Error ? error : new Error(String(error));Apply in
retryUpload,handleNetworkError, and the outer catches ofuploadFile/downloadFile/listFiles/testConnection.Also applies to: 85-127
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/webdavService.ts` around lines 48 - 73, Replace blind casts of caught exceptions (e.g., "lastError = error as Error") with a narrow guard that normalizes the caught value to an Error instance (for example: const err = error instanceof Error ? error : new Error(String(error))). Apply this pattern inside retryUpload and handleNetworkError and in the outer catch blocks of uploadFile, downloadFile, listFiles, and testConnection so subsequent uses of err.message/err.name are safe before doing includes(...) or rethrowing; update references from lastError to the normalized err and throw that normalized Error where needed.src/services/autoSync.ts (1)
117-121: LGTM, minor redundancy note.The explicit
_isSyncingFromBackendActive = falsebefore the early return is technically redundant with thefinallyblock at lines 175–184 (which also resets the flag and drains_hasPendingPush). It's harmless but slightly surprising — readers may wonder why it's set twice. Optional: drop the explicit reset and letfinallyhandle it, since_isSyncingFromBackendwas never set totrueon this path either.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/autoSync.ts` around lines 117 - 121, Remove the redundant explicit reset of _isSyncingFromBackendActive inside the early-return branch: if Object.values(changed).some(Boolean) is false, simply return and let the existing finally block (which also clears _isSyncingFromBackendActive and drains _hasPendingPush) handle the flag/reset logic; delete the line "_isSyncingFromBackendActive = false;" in that branch to avoid the duplicate reset while preserving the early return behavior around the changed check.src/components/Modal.tsx (1)
42-53: RedundantstopPropagationon outer container.The outer wrapper (Lines 42–45) already stops propagation for every click that reaches it, which is superfluous because the backdrop and inner panel below already handle their own events. More importantly, stopping propagation at the root modal container can mask click-outside handlers that some parent components may rely on. Consider removing the outer
onClickand keepingstopPropagationonly on the inner panel — the existing backdrop handler already closes on outside clicks.♻️ Suggested simplification
- <div - className="fixed inset-0 z-50 overflow-y-auto" - onClick={(e) => e.stopPropagation()} - > + <div className="fixed inset-0 z-50 overflow-y-auto"> {/* Backdrop */} <div className="fixed inset-0 bg-black bg-opacity-50 transition-opacity" - onClick={(e) => { - e.stopPropagation(); - onClose(); - }} + onClick={onClose} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Modal.tsx` around lines 42 - 53, The outer div in Modal.tsx using onClick={(e) => e.stopPropagation()} is redundant and can block parent click-outside handlers; remove that outer onClick handler and keep stopPropagation only on the inner panel element (leave the backdrop div's onClick that calls onClose() intact) so click-outside still closes via the backdrop and inner clicks don't bubble; update any references to the outer wrapper (className="fixed inset-0 z-50 overflow-y-auto") and ensure onClose is only invoked by the backdrop click handler and not suppressed by the outer container.src/services/aiAnalysisOptimizer.ts (1)
143-185:prefetchReadmesignoresOptimizerConfigand uses hard-coded values.Line 149 caps concurrency at
Math.min(10, repos.length)and line 176 delays100ms between batches, regardless of the injectedmaxConcurrency/batchDelayMs(the defaults happen to match, but any caller overriding the config is silently ignored). This also forks behavior from the rest of the optimizer, which respectscurrentConcurrencyandconfig.batchDelayMselsewhere.♻️ Suggested refactor
- const concurrency = Math.min(10, repos.length); + const concurrency = Math.min(this.config.maxConcurrency, repos.length); @@ - if (i + concurrency < repos.length && !this.aborted) { - await this.delay(100); - } + if (i + concurrency < repos.length && !this.aborted) { + await this.delay(this.config.batchDelayMs); + }Also minor: the intermediate
resultsmap (lines 150, 159-161, 180-182) is redundant — writing directly intoreadmeCacheinsidefetchReadmewould remove a copy step and theerrorfield that is never read.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/aiAnalysisOptimizer.ts` around lines 143 - 185, prefetchReadmes currently uses hard-coded concurrency (Math.min(10,...)) and delay (100ms) and builds a redundant results map; change it to honor the optimizer config by deriving concurrency from this.currentConcurrency || this.config.maxConcurrency (falling back to repos.length) and use this.config.batchDelayMs for the inter-batch delay, replace the temporary results Map with writes directly into readmeCache inside fetchReadme (set '' on error), and keep checks for this.aborted and waitWhilePaused as-is; update references to the function name prefetchReadmes, the fetchReadme inner function, and the variables concurrency and batchDelayMs/currentConcurrency in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/src/routes/repositories.ts`:
- Around line 107-119: The validation in routes/repositories.ts is too strict:
relax checks so owner.avatar_url is optional (only require owner.login is a
string), allow repo.stargazers_count to be missing or non-number (treat as 0
when inserting), and stop aborting the whole batch on the first invalid repo —
instead validate all repos, collect failures and include an identifier (e.g.,
repo.id or repo.full_name) in each error entry before returning a 400 with the
array of failures; update the validation logic around owner.avatar_url,
repo.stargazers_count, and the batch-handling flow (the block using owner,
repo.html_url, repo.stargazers_count and the code that currently returns
immediately) to implement these changes.
In `@server/src/routes/sync.ts`:
- Around line 114-118: Validation for repository entries (and other input
collections) must be performed before entering db.transaction so malformed
client data yields a 400 instead of bubbling out as a 500; move the checks out
of the transaction and extend them to all input arrays (repositories, releases,
categories, asset_filters, etc.), validating required fields (e.g., for
repositories ensure r.id exists and typeof r.id === 'number') and for other
collections validate their required ids/fields, and when validation fails throw
or return a 400-specific error (e.g., BadRequest / createHttpError(400, ...))
rather than plain Error so the outer error handler can respond with HTTP 400.
In `@server/src/services/proxyService.ts`:
- Around line 37-56: The SSRF check is too narrow: update the validation in
proxyService.ts (around isIP, BLOCKED_HOSTS, PRIVATE_IP_PATTERNS and parsed
usage) to reject whole private/loopback/link-local and IPv6 ranges rather than
only literal hosts; either expand PRIVATE_IP_PATTERNS to include regexes for
^127\., ^169\.254\., ^0\., and IPv6 ranges (::1, fc00::/7, fe80::/10,
IPv4-mapped ::ffff:) or, preferably, use a proper IP parser (e.g., net.isIP or
ipaddr.js) to detect address type and perform CIDR membership checks against
127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16,
0.0.0.0/8 and the IPv6 equivalents (loopback, ULA, link-local) before allowing
the proxy request; ensure this validation runs for parsed.hostname (handling
IPv6 without brackets) and keeps the existing BLOCKED_HOSTS literal checks.
In `@src/components/BulkActionToolbar.tsx`:
- Around line 207-319: The bulk-action buttons (those calling handleAction with
actions like
'unstar','categorize','ai-summary','subscribe','unsubscribe','lock-category','unlock-category')
are icon-only and lack accessible names; add a localized aria-label and title to
each button (e.g. aria-label="Unstar selected" / title="Unstar selected") and
mark the Lucide icons (Star, FolderOpen, Bot, Bell, BellOff, Lock, Unlock) and
the Loader2 spinner as aria-hidden="true" so assistive tech reads the button
label only; keep disabled and loading logic intact and ensure the label strings
are localized where your i18n utilities are used.
In `@src/components/LoginScreen.tsx`:
- Line 158: Replace the deprecated onKeyPress usage with onKeyDown on the input
element and ensure the handler (handleKeyPress) is updated to accept a keydown
event (React.KeyboardEvent<HTMLInputElement>) so it correctly detects modifier
keys and non-printable keys; you can keep the handler name (handleKeyPress) or
rename it (e.g., handleKeyDown) but make sure the prop and function signature
match and update any TypeScript types/usages accordingly.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 241-247: The component clamps the displayed page via clampedPage
but leaves currentPage state stale; after computing totalPages (from totalPages,
filteredReleases, repositoryGroups, viewMode) update the state so currentPage is
always 1 <= currentPage <= totalPages (use 1 when totalPages is 0). Add a small
effect or logic that calls the page-state setter (e.g., setCurrentPage) to set
currentPage = Math.min(Math.max(currentPage, 1), Math.max(totalPages, 1))
whenever totalPages, viewMode, filteredReleases, or repositoryGroups change so
the pager UI and button states reflect the real page index.
In `@src/components/SearchBar.tsx`:
- Around line 591-594: Update the Enter key handling in handleKeyDown to ignore
Enter presses while IME composition is active: inside handleKeyDown (which
currently calls handleAISearch on Enter) check e.nativeEvent.isComposing and
return early if true, only invoking handleAISearch when e.key === 'Enter' AND
e.nativeEvent.isComposing is falsy; this prevents IME candidate commits from
triggering handleAISearch.
In `@src/components/SettingsPanel.tsx`:
- Around line 310-322: The panel id/label scheme is inconsistent: update the
panel rendered in SettingsPanel (the div using displayTab, isTransitioning,
content) to use a shared id like settings-tabpanel-${displayTab} and set its
aria-labelledby to the matching tab button id (e.g.,
settings-tab-${displayTab}), then update all tab buttons (mobile: MobileTabNav
and both desktop tab variants / any places that set aria-controls or id such as
the desktop tab buttons currently using
aria-controls={`desktop-tabpanel-${tab.id}`} or mobile-tab ids) to use
id={`settings-tab-${tab.id}`} and aria-controls={`settings-tabpanel-${tab.id}`}
so the aria-controls/aria-labelledby references always resolve regardless of
which nav is mounted.
In `@src/index.css`:
- Around line 8-16: Remove the invalid unprefixed declarations from the
.line-clamp-3 rule: delete "display: box;" and "box-orient: vertical;" and keep
the valid prefixed -webkit properties (e.g., -webkit-box, -webkit-line-clamp,
-webkit-box-orient) along with the modern unprefixed line-clamp property so the
selector .line-clamp-3 only contains supported properties (display:
-webkit-box;, -webkit-line-clamp: 3;, line-clamp: 3;, -webkit-box-orient:
vertical;, overflow: hidden;).
In `@src/polyfills.ts`:
- Line 66: Remove the unconditional console.log in polyfills.ts and either
delete the log or wrap it in a dev-only guard using import.meta.env.DEV;
specifically update the line that prints "[polyfills] Polyfills loaded" so it
only executes when import.meta.env.DEV is true (or remove it entirely) to avoid
noisy production logging.
In `@src/services/aiAnalysisOptimizer.ts`:
- Around line 93-108: Adaptive concurrency never takes effect because
analyzeRepositories and analyzeRepositoriesPipelined snapshot
this.currentConcurrency when spawning workers; update the runtime pool to track
and react to changes from adjustConcurrency by (1) making the worker loop
consult a shared activeWorkers/shouldExit signal so workers can exit if the
desired concurrency shrinks, (2) when adjustConcurrency increases
this.currentConcurrency spawn additional worker() instances to reach the new
target, and (3) stop using the initial snapshot for reporting and prefetch
counts — have onProgress report the live activeWorkers (not the startup
snapshot) and compute readmePrefetchCount from this.currentConcurrency whenever
used; touch adjustConcurrency, analyzeRepositories,
analyzeRepositoriesPipelined, worker loop logic, onProgress reporting, and
readmePrefetchCount usage to implement these runtime-resize semantics.
In `@src/services/aiService.ts`:
- Around line 40-43: The AIReasoningEffort union type (referenced by
getOpenAIReasoningPayload and this.config.reasoningEffort) currently omits
'minimal' which OpenAI's Responses API accepts; either add 'minimal' to the
AIReasoningEffort type definition in the types file (so
getOpenAIReasoningPayload can pass it through safely) or, if intentional, add a
clear comment in the AIReasoningEffort type and implement explicit mapping logic
in getOpenAIReasoningPayload (e.g., map 'minimal' -> 'low') so the code and
types stay consistent. Ensure the chosen approach updates the type declaration
(AIReasoningEffort) and any mapping in getOpenAIReasoningPayload to reflect the
decision.
---
Outside diff comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 214-230: The deletion flow in handleDeleteCategory currently calls
deleteCustomCategory(category.id) before awaiting forceSyncToBackend(), but the
catch only alerts and does not restore the local state; update
handleDeleteCategory to either (A) perform the backend sync first and only call
deleteCustomCategory(category.id) after forceSyncToBackend() succeeds, or (B)
snapshot the affected local state (category list and any repository category
assignments) before calling deleteCustomCategory, then if forceSyncToBackend()
throws restore that snapshot in the catch; reference the existing
functions/identifiers deleteCustomCategory, forceSyncToBackend, and
handleDeleteCategory when making this change.
In `@src/components/SearchBar.tsx`:
- Around line 236-237: The applyFilters function currently aliases the input
array via let filtered = repos and calls filtered.sort(...), which mutates the
caller's array; change it to operate on a shallow copy (e.g., const filtered =
[...repos] or repos.slice()) before sorting or filtering so the original
repositories array is not mutated, and apply the same copy-before-sort fix to
the other sort call in the same file (the second sort around the other sort
block referenced in the review).
In `@src/services/webdavService.ts`:
- Around line 343-353: The catch block in src/services/webdavService.ts contains
a dead branch checking err.message.includes('HTTP 404') that never matches
because 404s are produced as "下载失败,HTTP状态码 ${status}: ${statusText}" earlier;
remove that unreachable includes('HTTP 404') branch (or replace it with a check
that matches the actual message, e.g., matching 'HTTP状态码 404' or inspecting a
numeric status code if available) and keep using this.handleNetworkError(error,
'下载') for other cases; update references in this catch to only handle
authentication/timeout rethrows, the 404 null-return behavior, and the general
network error path, and ensure to use handleNetworkError for remaining errors.
---
Duplicate comments:
In `@src/components/ReleaseTimeline.tsx`:
- Around line 63-80: matchesActiveFilters currently reads preset keywords
directly from PRESET_FILTERS instead of using the persisted assetFilters, so
changes made in AssetFilterManager aren’t reflected; modify matchesActiveFilters
to build the active filter set by looking up each selectedFilters id in
assetFilters first (use the matching assetFilters entry for keywords) and only
fall back to the static PRESET_FILTERS entry when an id is not found in
assetFilters, then run the existing keyword matching logic against that combined
active set (ensure you still reference selectedFilters and assetFilters in the
hook dependencies and keep the function name matchesActiveFilters).
In `@src/components/RepositoryList.tsx`:
- Around line 1127-1129: The toolbar count is derived from selectedRepoIds.size
while bulk actions use selectedRepositories, causing a mismatch after filtering;
update the BulkActionToolbar prop by passing
selectedCount={selectedRepositories.length} instead of selectedRepoIds.size so
the displayed "N selected" matches the set acted on by the BulkActionToolbar and
functions that consume selectedRepositories.
In `@src/index.css`:
- Around line 29-33: The .scrollbar-auto CSS rule uses the non-standard
overflow: overlay; replace that with overflow: auto in the .scrollbar-auto rule
so browsers use the standard behavior while keeping the existing scrollbar-width
and scrollbar-color rules unchanged (i.e., edit the .scrollbar-auto selector to
remove overflow: overlay and set overflow: auto).
---
Nitpick comments:
In `@server/src/db/connection.ts`:
- Around line 26-29: The catch currently discards the original error by throwing
a new generic Error; preserve the original error details by rethrowing with the
original as the cause or by rethrowing the original error object: replace "throw
new Error('Database initialization failed')" with either "throw new
Error('Database initialization failed', { cause: error })" (Node/Error supports
cause) or "throw error" so callers retain the original message and stack; keep
the existing console.error log as-is.
In `@server/src/middleware/auth.ts`:
- Line 33: Remove the duplicate Chinese comment that repeats the English comment
about constant-time comparison in the auth middleware; keep the existing English
comment and delete the Chinese line so only the single English comment
describing the constant-time comparison logic remains (search for the
constant-time comparison comment in auth.ts and remove the duplicate Chinese
comment immediately following it).
In `@server/src/routes/releases.ts`:
- Around line 89-92: The validator in releases.ts currently allows non-integer
numbers (e.g., 1.5); update the check in the block that tests release.id to
include Number.isInteger(release.id) so only integers pass, and also update the
client-side syncReleases function in backendAdapter.ts to guarantee it never
sends floats (validate/cast ids to integer or reject non-integers before
sending) so the API and client stay consistent.
In `@server/src/routes/repositories.ts`:
- Around line 94-98: The current validator in the repositories loop allows
non-integer numbers because it checks typeof repo.id === 'number'; update the
condition to explicitly require an integer by using Number.isInteger(repo.id)
and keep the positive check (Number.isInteger(repo.id) && repo.id > 0) so floats
like 1.5 are rejected; adjust the same validation branch that returns
res.status(400).json({ error: ..., code: 'INVALID_REPOSITORY_ID' }) to use this
new check referencing repo.id and repositories in the loop.
In `@server/src/services/proxyService.ts`:
- Around line 53-56: The credential rejection currently runs after blocked-host
and private-IP checks in validateUrl, causing credential-bearing URLs to be
masked by host errors; move the username/password check (the parsed.username ||
parsed.password throw) up to immediately after the protocol check inside
validateUrl so credential-containing URLs fail fast and surface that specific
error before any hostname normalization or blocked-host/private-IP checks.
- Around line 37-48: The code currently uses a loose IPv4 regex and duplicates
the BLOCKED_HOSTS check; replace the regex-based isIP logic with Node's
net.isIP(hostname) (import net from 'node:net') and only apply IP-specific
checks (e.g., PRIVATE_IP_PATTERNS or private-IP gating) when net.isIP(hostname)
returns truthy; remove the redundant BLOCKED_HOSTS.has(hostname) inside the old
isIP branch so BLOCKED_HOSTS is checked exactly once (and keep the descriptive
Error messages using the hostname/IP where appropriate).
In `@src/components/Header.tsx`:
- Around line 250-259: Replace the inline language ternary in the logout onClick
handler with the existing t(zh, en) helper to keep i18n consistent: find the
onClick block that calls confirm(...) and instead pass the Chinese and English
message strings as the two arguments to t(...) (preserving the
newline/formatting), then use the returned string in confirm(...) and keep the
conditional logout() call unchanged; reference the t helper exposed in this
module (t) and the logout() call to locate the code.
In `@src/components/Modal.tsx`:
- Around line 42-53: The outer div in Modal.tsx using onClick={(e) =>
e.stopPropagation()} is redundant and can block parent click-outside handlers;
remove that outer onClick handler and keep stopPropagation only on the inner
panel element (leave the backdrop div's onClick that calls onClose() intact) so
click-outside still closes via the backdrop and inner clicks don't bubble;
update any references to the outer wrapper (className="fixed inset-0 z-50
overflow-y-auto") and ensure onClose is only invoked by the backdrop click
handler and not suppressed by the outer container.
In `@src/services/aiAnalysisOptimizer.ts`:
- Around line 143-185: prefetchReadmes currently uses hard-coded concurrency
(Math.min(10,...)) and delay (100ms) and builds a redundant results map; change
it to honor the optimizer config by deriving concurrency from
this.currentConcurrency || this.config.maxConcurrency (falling back to
repos.length) and use this.config.batchDelayMs for the inter-batch delay,
replace the temporary results Map with writes directly into readmeCache inside
fetchReadme (set '' on error), and keep checks for this.aborted and
waitWhilePaused as-is; update references to the function name prefetchReadmes,
the fetchReadme inner function, and the variables concurrency and
batchDelayMs/currentConcurrency in your changes.
In `@src/services/autoSync.ts`:
- Around line 117-121: Remove the redundant explicit reset of
_isSyncingFromBackendActive inside the early-return branch: if
Object.values(changed).some(Boolean) is false, simply return and let the
existing finally block (which also clears _isSyncingFromBackendActive and drains
_hasPendingPush) handle the flag/reset logic; delete the line
"_isSyncingFromBackendActive = false;" in that branch to avoid the duplicate
reset while preserving the early return behavior around the changed check.
In `@src/services/backendAdapter.ts`:
- Around line 178-185: The hardcoded 120000ms timeout in the AI proxy call
should be made configurable: replace the literal timeout passed to
fetchWithTimeout in the method that posts to `${this._backendUrl}/proxy/ai`
(where configId and body are sent) with a value read from a configurable source
(e.g., AIConfig or a store setting) and fall back to 120000ms if not set; ensure
the new config key is documented/validated and use the same getter (or inject
the config) so callers can tune timeouts without changing code.
In `@src/services/updateService.ts`:
- Around line 117-120: Remove the redundant runtime assignment that tries to
nullify opener after calling window.open: the variable newWindow from the
window.open(url, '_blank', 'noopener,noreferrer') call can be removed along with
the if (newWindow) { newWindow.opener = null; } block; instead rely on the
feature string 'noopener,noreferrer' passed to window.open (leave the
window.open invocation inline or assign to a throwaway if needed) so the code no
longer contains unreachable dead code related to newWindow/opener.
In `@src/services/webdavService.ts`:
- Around line 48-73: Replace blind casts of caught exceptions (e.g., "lastError
= error as Error") with a narrow guard that normalizes the caught value to an
Error instance (for example: const err = error instanceof Error ? error : new
Error(String(error))). Apply this pattern inside retryUpload and
handleNetworkError and in the outer catch blocks of uploadFile, downloadFile,
listFiles, and testConnection so subsequent uses of err.message/err.name are
safe before doing includes(...) or rethrowing; update references from lastError
to the normalized err and throw that normalized Error where needed.
In `@src/utils/categoryUtils.ts`:
- Around line 62-81: In computeCustomCategory, the comparisons against
aiCategory and defaultCategory should explicitly treat falsy AI/default values
as “no match”; update the two checks (the ones currently doing categoryName ===
aiCategory and categoryName === defaultCategory) to first ensure
aiCategory/defaultCategory are truthy (e.g., aiCategory && categoryName ===
aiCategory and defaultCategory && categoryName === defaultCategory) so empty or
undefined AI/default categories don’t unintentionally count as matches.
In `@vite.config.ts`:
- Around line 10-14: The legacy plugin configuration is overly verbose and
potentially harmful: simplify the targets passed to legacy(...) by removing
redundant entries and use just ['defaults'] (or remove the entire legacy(...)
plugin if this Electron app only targets current Chromium/Electron), remove 'not
IE 11' and the explicit Chrome/Firefox/Safari/Edge entries, and address
modernPolyfills by either removing modernPolyfills or explicitly supplying
modernTargets to limit core-js injection; also drop additionalLegacyPolyfills if
you remove the plugin or if regenerator-runtime isn't required. Locate the
legacy(...) call in vite.config.ts and apply one of these fixes depending on
whether you need legacy support.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e03a2f7f-a28a-4680-bc4b-bc5ea7745c6c
⛔ Files ignored due to path filters (2)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (53)
.github/workflows/build-desktop.ymlpackage.jsonserver/src/db/connection.tsserver/src/middleware/auth.tsserver/src/routes/releases.tsserver/src/routes/repositories.tsserver/src/routes/sync.tsserver/src/services/proxyService.tssrc/App.tsxsrc/components/AssetFilterManager.tsxsrc/components/BackToTop.tsxsrc/components/BulkActionToolbar.tsxsrc/components/BulkCategorizeModal.tsxsrc/components/CategorySidebar.tsxsrc/components/ErrorBoundary.tsxsrc/components/FilterModal.tsxsrc/components/Header.tsxsrc/components/LoginScreen.tsxsrc/components/MarkdownRenderer.tsxsrc/components/Modal.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseCard.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryEditModal.tsxsrc/components/RepositoryList.tsxsrc/components/SearchBar.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.tssrc/constants/presetFilters.tssrc/index.csssrc/main.tsxsrc/polyfills.tssrc/services/aiAnalysisOptimizer.tssrc/services/aiService.tssrc/services/autoSync.tssrc/services/backendAdapter.tssrc/services/githubApi.tssrc/services/indexedDbStorage.tssrc/services/updateService.tssrc/services/webdavService.tssrc/store/useAppStore.tssrc/types/index.tssrc/utils/categoryUtils.tstailwind.config.jsvite.config.ts
|
又发现了一些小问题:
|
refactor(RepositoryCard): 使用浅比较优化性能 fix(SearchBar): 修复输入法组合状态下的回车触发问题 feat(ReleaseCard): 添加源码下载链接支持 perf(CategorySidebar): 优化分类计数计算 fix(WebDAVService): 正确处理网络错误返回 feat(useAppStore): 添加仓库相关发布数据删除功能 style(index.css): 移除冗余样式属性 docs(BackupPanel): 更新备份描述文本 refactor(App): 使用React.memo优化视图渲染 perf(AIAnalysisOptimizer): 改进并发控制逻辑 feat(ReleaseTimeline): 添加刷新状态和分页控制
|
可以把这个PR作为Beta版发布。 |



为订阅和查看GitHub按钮添加中文标题支持,根据当前语言环境显示相应文本
Summary by CodeRabbit
New Features
Refactor
Style
Bug Fixes