Skip to content

fix: 修复合并冲突、标签渲染问题,添加 Trending 频道,优化本地化#87

Merged
AmintaCCCP merged 2 commits intomainfrom
fix-pr86
Apr 19, 2026
Merged

fix: 修复合并冲突、标签渲染问题,添加 Trending 频道,优化本地化#87
AmintaCCCP merged 2 commits intomainfrom
fix-pr86

Conversation

@AmintaCCCP
Copy link
Copy Markdown
Owner

@AmintaCCCP AmintaCCCP commented Apr 19, 2026

修复说明

本修复解决了 PR #86 与已合并的 #85 之间的冲突,并进行了多项改进:

1. 解决合并冲突

2. 修复代码审计问题

SubscriptionDevCard.tsx: 修复标签渲染逻辑缺陷

原代码问题:当 ai_tags 为空数组时,不会 fallback 到 topics

// 修复前
{(dev.topRepo.ai_tags || dev.topRepo.topics || []).slice(0, 5).map(...)}

// 修复后:长度感知的 fallback
{(() => {
  const tags = (dev.topRepo.ai_tags?.length ? dev.topRepo.ai_tags : dev.topRepo.topics) || [];
  return tags.length > 0 && (...);
})()}

3. 导航栏改名

  • 中文:订阅 → 趋势
  • 英文:Subscribe → Trending

4. 新增 Trending 频道

  • 类型定义:SubscriptionChannelId 添加 'trending'
  • API 方法:githubApi.ts 添加 searchTrending() 方法(近7天创建的 Star 最多的项目)
  • 默认频道配置:useAppStore.ts 添加 trending 频道
  • 刷新逻辑:SubscriptionView.tsx 添加 trending 刷新支持

5. 优化本地化

原名称 中文名称 英文名称
Most Stars 最多 Star Most Stars
Most Forks 最多 Fork Most Forks
Most DEV 热门开发者 Top Developers
Trending (新增) 热门趋势 Trending

修改文件

  • src/store/useAppStore.ts
  • src/components/Header.tsx
  • src/components/SubscriptionDevCard.tsx
  • src/components/SubscriptionView.tsx
  • src/services/githubApi.ts
  • src/types/index.ts

测试

✅ 构建通过 (npm run build)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added "Subscription" view featuring most-starred, most-forked, and trending repositories, plus top developers.
    • Enabled refresh functionality and AI analysis for subscription content.
    • New navigation option to access the Subscription section with dedicated channel selection.
  • Bug Fixes

    • Improved error message display to show more specific details when available.

chan-yuu and others added 2 commits April 19, 2026 22:49
新增 3 个默认订阅频道:
- Most Stars:GitHub Star 数量最多的项目 Top 10
- Most Forks:GitHub Fork 数量最多的项目 Top 10
- Most DEV:GitHub 最受关注的开发者 Top 10 及其最热项目

主要变更:
- types/index.ts: 新增 SubscriptionChannel, SubscriptionRepo, SubscriptionDev 类型
- githubApi.ts: 新增 searchMostStars/searchMostForks/searchDailyDevs 搜索方法
- backendAdapter.ts: 新增 searchRepositories/searchUsers 后端代理方法
- server/proxy.ts: 新增 Search API 代理路由
- useAppStore.ts: 新增 subscription 状态管理、持久化(migrate v5)、channel ID 迁移(daily-dev→most-dev)
- Header.tsx: 三个导航断点(桌面/平板/手机)新增订阅入口(TrendingUp 图标)
- App.tsx: 新增 subscription 视图渲染
- SubscriptionView.tsx: 主视图含频道切换、手动刷新、批量 AI 分析
- SubscriptionSidebar.tsx: 左侧频道光标栏
- SubscriptionRepoCard.tsx: 排行仓库卡片(排名徽章+AI标签+平台图标)
- SubscriptionDevCard.tsx: 开发者排行卡片(头像+bio+最热项目)
- ErrorBoundary.tsx: 错误消息显示防御性处理

频道功能:手动刷新更新数据、批量 AI 分析(复用 AIAnalysisOptimizer)、
数据持久化到 IndexedDB、后端代理支持 Search API
主要修改:
1. 解决与 #85 合并后的冲突(useAppStore.ts 版本号和 defaultCategoryOverrides)
2. 修复 SubscriptionDevCard.tsx 中标签渲染逻辑缺陷(空 ai_tags 数组不 fallback 到 topics)
3. 将顶部导航从订阅改为趋势(中英文)
4. 新增 Trending 频道(模拟近7天创建的 Star 最多的项目)
5. 优化订阅频道的中文本地化:
   - Most Stars -> 最多 Star
   - Most Forks -> 最多 Fork
   - Most DEV -> 热门开发者
   - Trending -> 热门趋势
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete subscription/trending feature enabling users to browse and analyze trending GitHub repositories and developers across four channels: most-starred, most-forked, trending (7-day), and most-developers. Changes span backend proxy routes, new React components for displaying subscription data, store state management with persistence, service methods for GitHub searches, and AI-powered repository/developer analysis capabilities.

Changes

Cohort / File(s) Summary
Backend GitHub Proxy
server/src/routes/proxy.ts
Added two new POST routes /api/proxy/github/search/repositories and /api/proxy/github/search/users that decrypt stored GitHub tokens and proxy search requests to GitHub API with appropriate headers and error handling.
Type Definitions
src/types/index.ts
Extended AppState.currentView to include 'subscription' and added new subscription-related types: SubscriptionChannelId, SubscriptionChannel, SubscriptionRepo, and SubscriptionDev interfaces with metadata for ranking, channels, and optional AI analysis fields.
Store State & Persistence
src/store/useAppStore.ts
Added comprehensive subscription state slice including channels, repos/devs collections, loading flags, last-refresh timestamps; implemented 8 new actions for subscription management; bumped persistence version to 5 with migration logic for channel id remapping (daily-devmost-dev).
Service Methods
src/services/backendAdapter.ts, src/services/githubApi.ts
Added searchRepositories/searchUsers backend adapter methods; added 4 GitHubApiService methods (searchMostStars, searchMostForks, searchTrending, searchDailyDevs) with per-repository/developer mapping and throttled fetching for user details.
Subscription Components
src/components/SubscriptionView.tsx, src/components/SubscriptionSidebar.tsx, src/components/SubscriptionDevCard.tsx, src/components/SubscriptionRepoCard.tsx
Added main subscription view with channel selection, refresh/analysis controls, AI analysis orchestration via AIAnalysisOptimizer, sidebar with per-channel loading indicators, and reusable card components for rendering developer and repository information with optional AI summaries and tags.
App Integration
src/App.tsx, src/components/Header.tsx, src/components/ErrorBoundary.tsx
Added subscription view route rendering; added navigation control in header with TrendingUp icon across desktop/tablet/mobile variants; improved error message specificity in ErrorBoundary.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Header
    participant SubscriptionView
    participant Store
    participant GitHubApiService
    participant BackendAdapter
    participant GitHub API

    User->>Header: Click subscription nav
    Header->>Store: setCurrentView('subscription')
    Store->>SubscriptionView: Render subscription view

    User->>SubscriptionView: Click refresh channel
    SubscriptionView->>Store: setSubscriptionLoading(channel, true)
    SubscriptionView->>GitHubApiService: search[MostStars|Trending|...]()
    GitHubApiService->>BackendAdapter: searchRepositories(query_params)
    BackendAdapter->>GitHub API: GET /search/repositories
    GitHub API-->>BackendAdapter: Results
    BackendAdapter-->>GitHubApiService: Repos mapped to SubscriptionRepo[]
    GitHubApiService-->>SubscriptionView: SubscriptionRepo[] with rank/channel
    SubscriptionView->>Store: setSubscriptionRepos(channel, repos)
    SubscriptionView->>Store: setSubscriptionLastRefresh(channel, timestamp)
    SubscriptionView->>Store: setSubscriptionLoading(channel, false)
    Store-->>SubscriptionView: UI updated with new repos
Loading
sequenceDiagram
    actor User
    participant SubscriptionView
    participant AIAnalysisOptimizer
    participant Store
    participant BackendAdapter
    participant LLM API

    User->>SubscriptionView: Click analyze channel
    SubscriptionView->>AIAnalysisOptimizer: Create with unanalyzed repos
    SubscriptionView->>AIAnalysisOptimizer: prefetchREADMEs()
    SubscriptionView->>AIAnalysisOptimizer: analyzeRepositories(onProgress)
    loop For each repo
        AIAnalysisOptimizer->>BackendAdapter: Fetch README
        AIAnalysisOptimizer->>LLM API: Analyze content (with concurrency control)
        LLM API-->>AIAnalysisOptimizer: Analysis result (tags, summary)
        AIAnalysisOptimizer->>Store: updateSubscriptionRepo(with ai_tags, ai_summary)
        AIAnalysisOptimizer->>SubscriptionView: onProgress callback
    end
    AIAnalysisOptimizer-->>SubscriptionView: Complete
    SubscriptionView->>Store: Mark analyzed_at timestamp
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #43: Extends GitHub proxy endpoints in server/src/routes/proxy.ts with additional POST routes for proxying GitHub Search API requests—directly extends the proxy architecture added here.
  • PR #85: Modifies centralized app state (AppState in src/types/index.ts) and Zustand store implementation with new persisted slices and migration logic—overlaps with subscription state initialization and persistence changes.
  • PR #55: Extends BackendAdapter in src/services/backendAdapter.ts with auth methods and headers—related service layer modifications that may interact with the new search methods.

Poem

🐰 Twitching whiskers with delight!
Subscriptions bloom in trending light,
Repos ranked by stars so bright,
Devs analyzed with AI might,
Hop along, the view's just right!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title describes multiple distinct changes: merge conflict resolution, tag rendering fixes, adding Trending channel, and localization optimization. While these are all present in the changeset, the title conflates several orthogonal improvements without clearly prioritizing the main change. Consider a more focused title highlighting the primary objective (e.g., 'Add Trending subscription channel and fix tag rendering' or 'Add subscription trending channel and resolve merge conflicts'). The current title is somewhat vague by combining unrelated concerns.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-pr86

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
src/store/useAppStore.ts (1)

135-169: ⚠️ Potential issue | 🟠 Major

Add missing subscription fields to PersistedAppState type.

The persist middleware explicitly writes subscriptionChannels, selectedSubscriptionChannel, subscriptionRepos, subscriptionDevs, and subscriptionLastRefresh to storage, but these fields are missing from the PersistedAppState Pick list. This type mismatch is masked by explicit as PersistedAppState casts in the migration code, compromising type safety. Add these fields to maintain consistency between what is persisted and what the type declares.

🐛 Proposed fix
     | 'releaseViewMode'
     | 'releaseSelectedFilters'
     | 'releaseSearchQuery'
+    | 'subscriptionChannels'
+    | 'selectedSubscriptionChannel'
+    | 'subscriptionRepos'
+    | 'subscriptionDevs'
+    | 'subscriptionLastRefresh'
   >
 > & {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 135 - 169, The PersistedAppState type
is missing persisted subscription fields, causing unsafe casts; update the
Pick<> for PersistedAppState (based on AppState) to include
subscriptionChannels, selectedSubscriptionChannel, subscriptionRepos,
subscriptionDevs, and subscriptionLastRefresh with appropriate types (e.g.,
arrays/IDs/Date or the same types as defined on AppState) so the persisted shape
matches what the persist middleware writes and remove the need for unsafe casts
in the migration code that references PersistedAppState.
🧹 Nitpick comments (4)
src/services/backendAdapter.ts (1)

361-399: Consider extracting the shared search-proxy plumbing.

searchRepositories and searchUsers are structurally identical except for the path segment and the item type. A small generic helper would remove the duplication and make it easier to add future search endpoints (e.g., search/code). Non-blocking.

♻️ Sketch
private async searchProxy<T>(kind: 'repositories' | 'users', queryParams: Record<string, string>): Promise<{ items: T[] }> {
  if (!this._backendUrl) throw new Error('Backend not available');
  const res = await this.fetchWithTimeout(`${this._backendUrl}/proxy/github/search/${kind}`, {
    method: 'POST',
    headers: this.getAuthHeaders(),
    body: JSON.stringify({ query_params: queryParams }),
  });
  if (!res.ok) await this.throwTranslatedError(res, `Search ${kind} proxy error`);
  return res.json() as Promise<{ items: T[] }>;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/backendAdapter.ts` around lines 361 - 399, searchRepositories
and searchUsers duplicate the same request/response plumbing; extract a generic
private helper (e.g., searchProxy<T>(kind: string, queryParams:
Record<string,string>)) that performs the backendUrl check, calls
fetchWithTimeout with `${this._backendUrl}/proxy/github/search/${kind}`, uses
this.getAuthHeaders(), checks res.ok and calls this.throwTranslatedError with a
context message, and returns res.json() as Promise<{items: T[]}>; update
searchRepositories to call searchProxy<Repository>('repositories', queryParams)
and searchUsers to call searchProxy<UserType>('users', queryParams) to remove
duplication.
src/components/SubscriptionSidebar.tsx (1)

64-90: Defensive typeof === 'object' casts are obscuring the props contract.

isLoading and lastRefresh are already typed as Record<SubscriptionChannelId, ...>, so the runtime typeof/Record<string, unknown> gymnastics plus repeated channel.id === 'daily-dev' ? 'most-dev' : channel.id ternaries add a lot of noise without adding safety (and the daily-dev id can't match per the type, same as above). If the sidebar trusts its typed props, these accesses collapse to plain isLoading[channel.id] / lastRefresh[channel.id]. Also the indentation at line 67 appears off (extra spaces before const channelLoading).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionSidebar.tsx` around lines 64 - 90, The props
isLoading and lastRefresh are already typed as Record<SubscriptionChannelId,
...>, so remove the defensive typeof === 'object' checks, casts to
Record<string, unknown>, and the channel.id === 'daily-dev' ? 'most-dev' :
channel.id ternaries; replace them with direct indexed accesses like
isLoading[channel.id] and lastRefresh[channel.id] (and compute channelLoading
from isLoading[channel.id] truthiness), and clean up the indentation for the
const channelLoading line so it aligns with the surrounding code inside the
enabledChannels.map callback (keep references to enabledChannels, channel.id,
isLoading, lastRefresh, channelLoading, selectedChannel, and formatLastRefresh
to locate the changes).
src/components/SubscriptionView.tsx (1)

1-12: Remove unused imports.

resolveCategoryAssignment, backend, and Repository are imported but never referenced in this file. Safe to remove.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionView.tsx` around lines 1 - 12, Remove the unused
imports resolveCategoryAssignment, backend, and the type Repository from the top
of SubscriptionView.tsx; update the import block to keep only the actually used
symbols (e.g., React, useState, useCallback, useMemo, RefreshCw, TrendingUp,
Bot, Loader2, useAppStore, GitHubApiService, AIService, AIAnalysisOptimizer,
SubscriptionSidebar, SubscriptionRepoCard, SubscriptionDevCard, and the used
types SubscriptionChannelId, SubscriptionRepo, SubscriptionDev) so the file no
longer imports resolveCategoryAssignment, backend, or Repository.
src/types/index.ts (1)

230-235: Normalize fork count field to forks_count at API boundary.

SubscriptionRepo declares both forks?: number and forks_count?: number, but the GitHub API service only populates forks_count. This makes the fallback chain in SubscriptionRepoCard (repo.forks_count ?? repo.forks ?? 0) include dead code since forks is never assigned. Remove the unused forks field from SubscriptionRepo and update the card fallback to repo.forks_count ?? 0 to align with GitHub's API where forks_count is the standard field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/index.ts` around lines 230 - 235, Remove the unused forks property
from the SubscriptionRepo interface and normalize on forks_count as the
canonical fork count coming from the GitHub API; update any consumer fallback
logic (notably SubscriptionRepoCard) to use repo.forks_count ?? 0 instead of
repo.forks_count ?? repo.forks ?? 0 so the dead-code branch for repo.forks is
removed and the type and UI consistently rely on forks_count.
🤖 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/proxy.ts`:
- Around line 219-295: The two specific routes POST
/api/proxy/github/search/repositories and POST /api/proxy/github/search/users
are never reached because the earlier POST /api/proxy/github/* wildcard handler
intercepts those requests; fix by either moving the specific route registrations
(router.post('/api/proxy/github/search/repositories', ...) and
router.post('/api/proxy/github/search/users', ...)) to be registered before the
wildcard, or refactor the wildcard handler (the POST /api/proxy/github/*
implementation) to detect the alternate body shape { query_params } and handle
it (extract query string from req.body.query_params) and reuse a new helper that
centralizes token retrieval/decryption and header construction (e.g., a
getGithubAuthHeaders() helper) so you remove the duplicated token/decrypt/header
logic and ensure the search endpoints receive the q= parameter instead of being
shadowed.

In `@src/components/Header.tsx`:
- Around line 212-222: The Trending button in Header.tsx becomes an unlabeled
icon when isTextWrapped is true; update the button (the element that calls
setCurrentView('subscription') and renders the <TrendingUp /> icon) to include
an accessible label and tooltip by adding aria-label and title props (e.g.,
using the same localized text from t('趋势', 'Trending')) when isTextWrapped is
true (match the approach used by the Repositories/Releases/Settings buttons) so
screen readers and hover tooltips are present for icon-only mode.

In `@src/components/SubscriptionRepoCard.tsx`:
- Around line 103-114: The tag-rendering fallback uses "repo.ai_tags ||
repo.topics || []" which treats an empty array as truthy and prevents falling
back to topics; update the tag source selection in the SubscriptionRepoCard
component to be length-aware (e.g., choose repo.ai_tags if repo.ai_tags?.length
> 0, otherwise use repo.topics if repo.topics?.length > 0, else []). Keep the
existing outer guard that checks lengths, and then map over the resulting
length-checked array (slice(0,5)) when rendering the <span> chips so topics are
shown when ai_tags is an empty array.

In `@src/components/SubscriptionSidebar.tsx`:
- Around line 41-43: The filter/map that builds enabledChannels uses a
never-possible id check (ch.id === 'daily-dev') and injects hardcoded English
labels/icons, undoing localization and duplicating migration logic; remove the
special-case remap in SubscriptionSidebar (the enabledChannels creation) and
rely on the store's migrate function to normalize legacy ids, letting the
component just use (channels || []).filter(ch => ch.enabled) so it preserves the
store-provided name/icon and matches SubscriptionChannelId types.

In `@src/components/SubscriptionView.tsx`:
- Around line 246-248: isAnalyzingThisChannel currently only checks isAnalyzing
&& analysisProgress.total > 0 and therefore is global; change it to verify the
analysis belongs to the current channel by tracking the analyzing channel id in
state (e.g., add a state variable like analyzingChannelId that you set when
analysis starts and clear when it finishes or errors) and then compute
isAnalyzingThisChannel as isAnalyzing && analysisProgress.total > 0 &&
analyzingChannelId === normalizedChannel; ensure the code paths that start/stop
analysis (the functions that trigger analysis and the completion/error handlers)
set/clear analyzingChannelId accordingly so the UI only shows progress/controls
for the matching normalizedChannel.
- Around line 60-88: The refreshChannel try-block contains leftover
merge-conflict fragments: collapse the duplicated else-if branches into a single
control flow inside the refreshChannel function (use a switch or single if/else
chain checking normalizedId) so that 'most-stars' calls
GitHubApiService.searchMostStars and setSubscriptionRepos, 'most-forks' calls
searchMostForks and setSubscriptionRepos, 'most-dev' calls searchDailyDevs and
then setSubscriptionDevs (declare const devs in the same branch), and 'trending'
calls searchTrending and setSubscriptionRepos; remove the two extra else if
(normalizedId === 'trending') blocks, ensure devs is in scope where
setSubscriptionDevs(devs) is called, call
setSubscriptionLastRefresh(normalizedId, ...) once, and fix indentation and
formatting so the try/catch/finally compiles without TS errors (also re-run tsc
--noEmit and lint).

In `@src/services/githubApi.ts`:
- Around line 280-285: The Trending date calculation in searchTrending
incorrectly wraps the ISO string split in a Set and then indexes it (lastWeek),
which yields undefined; change the extraction to take the date directly from the
split array (e.g., use new Date(...).toISOString().split('T')[0]) so lastWeek is
a proper YYYY-MM-DD string before building the query passed to makeRequest in
searchTrending.
- Around line 318-329: The current logic that fetches a user's top-starred repo
using makeRequest with `/users/${searchUser.login}/repos?sort=stars` should be
changed to use the repository search endpoint that supports sort=stars; replace
the call that sets topRepo (the block that creates topRepo of type
SubscriptionRepo from Repository[] using searchUser.login and channel
'most-dev') with a search-based request (e.g., query
`q=user:${searchUser.login}` and `sort=stars&order=desc&per_page=1`) using the
existing makeRequest helper, then map the first search result into the same
SubscriptionRepo shape (rank: 1, channel: 'most-dev') so subsequent code using
topRepo behaves the same.

In `@src/store/useAppStore.ts`:
- Around line 1006-1017: The migration currently maps legacy IDs on
state.subscriptionChannels but never merges in new defaults like
defaultSubscriptionChannels (so new channels such as "trending" are omitted);
update the branch that handles Array.isArray(state.subscriptionChannels) to
first map/remap legacy entries (referencing state.subscriptionChannels and the
legacy ids 'daily-dev'/'most-dev'), then compute a merged list by taking all
mapped existing channels and appending any items from
defaultSubscriptionChannels whose id is not already present (de-duplicate by id)
so new defaults are added without duplicating existing ones.

---

Outside diff comments:
In `@src/store/useAppStore.ts`:
- Around line 135-169: The PersistedAppState type is missing persisted
subscription fields, causing unsafe casts; update the Pick<> for
PersistedAppState (based on AppState) to include subscriptionChannels,
selectedSubscriptionChannel, subscriptionRepos, subscriptionDevs, and
subscriptionLastRefresh with appropriate types (e.g., arrays/IDs/Date or the
same types as defined on AppState) so the persisted shape matches what the
persist middleware writes and remove the need for unsafe casts in the migration
code that references PersistedAppState.

---

Nitpick comments:
In `@src/components/SubscriptionSidebar.tsx`:
- Around line 64-90: The props isLoading and lastRefresh are already typed as
Record<SubscriptionChannelId, ...>, so remove the defensive typeof === 'object'
checks, casts to Record<string, unknown>, and the channel.id === 'daily-dev' ?
'most-dev' : channel.id ternaries; replace them with direct indexed accesses
like isLoading[channel.id] and lastRefresh[channel.id] (and compute
channelLoading from isLoading[channel.id] truthiness), and clean up the
indentation for the const channelLoading line so it aligns with the surrounding
code inside the enabledChannels.map callback (keep references to
enabledChannels, channel.id, isLoading, lastRefresh, channelLoading,
selectedChannel, and formatLastRefresh to locate the changes).

In `@src/components/SubscriptionView.tsx`:
- Around line 1-12: Remove the unused imports resolveCategoryAssignment,
backend, and the type Repository from the top of SubscriptionView.tsx; update
the import block to keep only the actually used symbols (e.g., React, useState,
useCallback, useMemo, RefreshCw, TrendingUp, Bot, Loader2, useAppStore,
GitHubApiService, AIService, AIAnalysisOptimizer, SubscriptionSidebar,
SubscriptionRepoCard, SubscriptionDevCard, and the used types
SubscriptionChannelId, SubscriptionRepo, SubscriptionDev) so the file no longer
imports resolveCategoryAssignment, backend, or Repository.

In `@src/services/backendAdapter.ts`:
- Around line 361-399: searchRepositories and searchUsers duplicate the same
request/response plumbing; extract a generic private helper (e.g.,
searchProxy<T>(kind: string, queryParams: Record<string,string>)) that performs
the backendUrl check, calls fetchWithTimeout with
`${this._backendUrl}/proxy/github/search/${kind}`, uses this.getAuthHeaders(),
checks res.ok and calls this.throwTranslatedError with a context message, and
returns res.json() as Promise<{items: T[]}>; update searchRepositories to call
searchProxy<Repository>('repositories', queryParams) and searchUsers to call
searchProxy<UserType>('users', queryParams) to remove duplication.

In `@src/types/index.ts`:
- Around line 230-235: Remove the unused forks property from the
SubscriptionRepo interface and normalize on forks_count as the canonical fork
count coming from the GitHub API; update any consumer fallback logic (notably
SubscriptionRepoCard) to use repo.forks_count ?? 0 instead of repo.forks_count
?? repo.forks ?? 0 so the dead-code branch for repo.forks is removed and the
type and UI consistently rely on forks_count.
🪄 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: ec723bc0-3a25-46a5-9d1d-7d85eceabd78

📥 Commits

Reviewing files that changed from the base of the PR and between 75e4cab and 7ececdd.

📒 Files selected for processing (12)
  • server/src/routes/proxy.ts
  • src/App.tsx
  • src/components/ErrorBoundary.tsx
  • src/components/Header.tsx
  • src/components/SubscriptionDevCard.tsx
  • src/components/SubscriptionRepoCard.tsx
  • src/components/SubscriptionSidebar.tsx
  • src/components/SubscriptionView.tsx
  • src/services/backendAdapter.ts
  • src/services/githubApi.ts
  • src/store/useAppStore.ts
  • src/types/index.ts

Comment on lines +219 to +295
// POST /api/proxy/github/search/repositories
router.post('/api/proxy/github/search/repositories', async (req, res) => {
try {
const db = getDb();
const githubPath = 'search/repositories';
const { query_params } = req.body as { query_params?: Record<string, string> };

const tokenRow = db.prepare('SELECT value FROM settings WHERE key = ?').get('github_token') as { value: string } | undefined;
if (!tokenRow?.value) {
res.status(400).json({ error: 'GitHub token not configured', code: 'GITHUB_TOKEN_NOT_CONFIGURED' });
return;
}

let token: string;
try {
token = decrypt(tokenRow.value, config.encryptionKey);
} catch {
res.status(500).json({ error: 'Failed to decrypt GitHub token', code: 'GITHUB_TOKEN_DECRYPT_FAILED' });
return;
}

const queryString = query_params ? '?' + new URLSearchParams(query_params).toString() : '';
const targetUrl = `https://api.github.com/${githubPath}${queryString}`;

const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'GithubStarsManager-Backend',
};

const result = await proxyRequest({ url: targetUrl, method: 'GET', headers });
res.status(result.status).json(result.data);
} catch (err) {
console.error('GitHub search repositories proxy error:', err);
res.status(500).json({ error: 'GitHub search proxy failed', code: 'GITHUB_SEARCH_PROXY_FAILED' });
}
});

// POST /api/proxy/github/search/users
router.post('/api/proxy/github/search/users', async (req, res) => {
try {
const db = getDb();
const githubPath = 'search/users';
const { query_params } = req.body as { query_params?: Record<string, string> };

const tokenRow = db.prepare('SELECT value FROM settings WHERE key = ?').get('github_token') as { value: string } | undefined;
if (!tokenRow?.value) {
res.status(400).json({ error: 'GitHub token not configured', code: 'GITHUB_TOKEN_NOT_CONFIGURED' });
return;
}

let token: string;
try {
token = decrypt(tokenRow.value, config.encryptionKey);
} catch {
res.status(500).json({ error: 'Failed to decrypt GitHub token', code: 'GITHUB_TOKEN_DECRYPT_FAILED' });
return;
}

const queryString = query_params ? '?' + new URLSearchParams(query_params).toString() : '';
const targetUrl = `https://api.github.com/${githubPath}${queryString}`;

const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'GithubStarsManager-Backend',
};

const result = await proxyRequest({ url: targetUrl, method: 'GET', headers });
res.status(result.status).json(result.data);
} catch (err) {
console.error('GitHub search users proxy error:', err);
res.status(500).json({ error: 'GitHub search proxy failed', code: 'GITHUB_SEARCH_PROXY_FAILED' });
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm route registration order in the router file.
rg -nP "router\.(get|post|use)\(" server/src/routes/proxy.ts

Repository: AmintaCCCP/GithubStarsManager

Length of output: 396


🏁 Script executed:

sed -n '46,88p' server/src/routes/proxy.ts

Repository: AmintaCCCP/GithubStarsManager

Length of output: 1720


Critical: These routes are shadowed by the earlier POST /api/proxy/github/* wildcard and are unreachable.

Express matches handlers in registration order. The wildcard at line 46 already matches POST /api/proxy/github/search/repositories and .../search/users, so these new handlers are never invoked. The wildcard expects the body shape { method?, headers? } with query parameters in the URL, but these new routes send { query_params } in the body. When the wildcard intercepts such a request, it reads body.method as undefined, defaults to 'GET', and extracts query parameters from req.url (which is empty), resulting in a request to https://api.github.com/search/repositories without the q= parameter. This produces a 422 API error instead of search results.

Fix by either moving the specific routes above the wildcard, or having the wildcard detect and handle the { query_params } body shape. Since both new handlers duplicate the token/decrypt/header logic from the wildcard, extracting a helper function and branching on body structure would also eliminate ~70 lines of repetition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/routes/proxy.ts` around lines 219 - 295, The two specific routes
POST /api/proxy/github/search/repositories and POST
/api/proxy/github/search/users are never reached because the earlier POST
/api/proxy/github/* wildcard handler intercepts those requests; fix by either
moving the specific route registrations
(router.post('/api/proxy/github/search/repositories', ...) and
router.post('/api/proxy/github/search/users', ...)) to be registered before the
wildcard, or refactor the wildcard handler (the POST /api/proxy/github/*
implementation) to detect the alternate body shape { query_params } and handle
it (extract query string from req.body.query_params) and reuse a new helper that
centralizes token retrieval/decryption and header construction (e.g., a
getGithubAuthHeaders() helper) so you remove the duplicated token/decrypt/header
logic and ensure the search endpoints receive the q= parameter instead of being
shadowed.

Comment thread src/components/Header.tsx
Comment on lines +212 to +222
<button
onClick={() => setCurrentView('subscription')}
className={`${isTextWrapped ? 'p-2.5' : 'px-4 py-2'} rounded-lg font-medium transition-colors ${
currentView === 'subscription'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
<TrendingUp className={`${isTextWrapped ? 'w-5 h-5' : 'w-4 h-4'} ${isTextWrapped ? '' : 'inline mr-2'}`} />
{!isTextWrapped && t('趋势', 'Trending')}
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing aria-label/title on the Trending button for icon-only mode.

Unlike the sibling Repositories/Releases/Settings buttons, the new desktop Trending button omits both aria-label and title, so when isTextWrapped === true it becomes an unlabeled icon — bad for screen readers and tooltips. The tablet/mobile variants are fine.

🛠️ Suggested fix
             <button
               onClick={() => setCurrentView('subscription')}
+              aria-label={isTextWrapped ? t('趋势', 'Trending') : undefined}
+              title={isTextWrapped ? t('趋势', 'Trending') : undefined}
               className={`${isTextWrapped ? 'p-2.5' : 'px-4 py-2'} rounded-lg font-medium transition-colors ${
                 currentView === 'subscription'
                   ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
                   : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
               }`}
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setCurrentView('subscription')}
className={`${isTextWrapped ? 'p-2.5' : 'px-4 py-2'} rounded-lg font-medium transition-colors ${
currentView === 'subscription'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
<TrendingUp className={`${isTextWrapped ? 'w-5 h-5' : 'w-4 h-4'} ${isTextWrapped ? '' : 'inline mr-2'}`} />
{!isTextWrapped && t('趋势', 'Trending')}
</button>
<button
onClick={() => setCurrentView('subscription')}
aria-label={isTextWrapped ? t('趋势', 'Trending') : undefined}
title={isTextWrapped ? t('趋势', 'Trending') : undefined}
className={`${isTextWrapped ? 'p-2.5' : 'px-4 py-2'} rounded-lg font-medium transition-colors ${
currentView === 'subscription'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
<TrendingUp className={`${isTextWrapped ? 'w-5 h-5' : 'w-4 h-4'} ${isTextWrapped ? '' : 'inline mr-2'}`} />
{!isTextWrapped && t('趋势', 'Trending')}
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Header.tsx` around lines 212 - 222, The Trending button in
Header.tsx becomes an unlabeled icon when isTextWrapped is true; update the
button (the element that calls setCurrentView('subscription') and renders the
<TrendingUp /> icon) to include an accessible label and tooltip by adding
aria-label and title props (e.g., using the same localized text from t('趋势',
'Trending')) when isTextWrapped is true (match the approach used by the
Repositories/Releases/Settings buttons) so screen readers and hover tooltips are
present for icon-only mode.

Comment on lines +103 to +114
{((repo.ai_tags && repo.ai_tags.length > 0) || (repo.topics && repo.topics.length > 0)) && (
<div className="flex flex-wrap gap-1.5 mb-3">
{(repo.ai_tags || repo.topics || []).slice(0, 5).map((tag) => (
<span
key={tag}
className="px-2 py-0.5 rounded-md text-xs font-medium bg-blue-50/50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
>
{tag}
</span>
))}
</div>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tag fallback still breaks when ai_tags is an empty array.

The outer guard allows render if either list is non-empty, but repo.ai_tags || repo.topics || [] evaluates [] || topics as [] (empty arrays are truthy in JS). Result: when ai_tags === [] and topics has items, the section renders an empty wrapper with no chips.

Per the PR description, this length-aware fallback was applied to SubscriptionDevCard.tsx — please mirror it here:

🛠️ Suggested fix
-          {((repo.ai_tags && repo.ai_tags.length > 0) || (repo.topics && repo.topics.length > 0)) && (
-            <div className="flex flex-wrap gap-1.5 mb-3">
-              {(repo.ai_tags || repo.topics || []).slice(0, 5).map((tag) => (
+          {(() => {
+            const tags = repo.ai_tags && repo.ai_tags.length > 0
+              ? repo.ai_tags
+              : (repo.topics ?? []);
+            return tags.length > 0 ? (
+              <div className="flex flex-wrap gap-1.5 mb-3">
+                {tags.slice(0, 5).map((tag) => (
                   <span
                     key={tag}
                     className="px-2 py-0.5 rounded-md text-xs font-medium bg-blue-50/50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
                   >
                     {tag}
                   </span>
-              ))}
-            </div>
-          )}
+                ))}
+              </div>
+            ) : null;
+          })()}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{((repo.ai_tags && repo.ai_tags.length > 0) || (repo.topics && repo.topics.length > 0)) && (
<div className="flex flex-wrap gap-1.5 mb-3">
{(repo.ai_tags || repo.topics || []).slice(0, 5).map((tag) => (
<span
key={tag}
className="px-2 py-0.5 rounded-md text-xs font-medium bg-blue-50/50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
>
{tag}
</span>
))}
</div>
)}
{(() => {
const tags = repo.ai_tags && repo.ai_tags.length > 0
? repo.ai_tags
: (repo.topics ?? []);
return tags.length > 0 ? (
<div className="flex flex-wrap gap-1.5 mb-3">
{tags.slice(0, 5).map((tag) => (
<span
key={tag}
className="px-2 py-0.5 rounded-md text-xs font-medium bg-blue-50/50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
>
{tag}
</span>
))}
</div>
) : null;
})()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionRepoCard.tsx` around lines 103 - 114, The
tag-rendering fallback uses "repo.ai_tags || repo.topics || []" which treats an
empty array as truthy and prevents falling back to topics; update the tag source
selection in the SubscriptionRepoCard component to be length-aware (e.g., choose
repo.ai_tags if repo.ai_tags?.length > 0, otherwise use repo.topics if
repo.topics?.length > 0, else []). Keep the existing outer guard that checks
lengths, and then map over the resulting length-checked array (slice(0,5)) when
rendering the <span> chips so topics are shown when ai_tags is an empty array.

Comment on lines +41 to +43
const enabledChannels = (channels || []).filter(ch => ch.enabled).map(ch =>
ch.id === 'daily-dev' ? { ...ch, id: 'most-dev' as const, name: 'Most DEV', nameEn: 'Most DEV', icon: '👤' } : ch
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded English label contradicts the PR's localization goal, and the daily-dev branch is unreachable by type.

  • SubscriptionChannelId is 'most-stars' | 'most-forks' | 'most-dev' | 'trending' (see src/types/index.ts line 219), so ch.id === 'daily-dev' can never be true for a properly-typed SubscriptionChannel[] — TS should be narrowing this to never. If this exists only to handle legacy persisted state from v4, the remap belongs in the store's migrate function (where the PR summary says it already happens), not in every consumer.
  • Even if it did fire, it hardcodes name: 'Most DEV' and icon: '👤', overriding whatever the store configured. The PR explicitly changes the Chinese name to "热门开发者", so this branch would silently undo that localization.

Recommend dropping the map entirely and trusting the migrated store state:

♻️ Suggested simplification
-  const enabledChannels = (channels || []).filter(ch => ch.enabled).map(ch => 
-    ch.id === 'daily-dev' ? { ...ch, id: 'most-dev' as const, name: 'Most DEV', nameEn: 'Most DEV', icon: '👤' } : ch
-  );
+  const enabledChannels = (channels ?? []).filter(ch => ch.enabled);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionSidebar.tsx` around lines 41 - 43, The filter/map
that builds enabledChannels uses a never-possible id check (ch.id ===
'daily-dev') and injects hardcoded English labels/icons, undoing localization
and duplicating migration logic; remove the special-case remap in
SubscriptionSidebar (the enabledChannels creation) and rely on the store's
migrate function to normalize legacy ids, letting the component just use
(channels || []).filter(ch => ch.enabled) so it preserves the store-provided
name/icon and matches SubscriptionChannelId types.

Comment on lines +60 to +88
try {
const githubApi = new GitHubApiService(githubToken);
if (normalizedId === 'most-stars') {
const repos = await githubApi.searchMostStars(10);
setSubscriptionRepos('most-stars', repos);
} else if (normalizedId === 'most-forks') {
const repos = await githubApi.searchMostForks(10);
setSubscriptionRepos('most-forks', repos);
} else if (normalizedId === 'most-dev') {
} else if (normalizedId === 'trending') {
const repos = await githubApi.searchTrending(10);
setSubscriptionRepos('trending', repos);
const devs = await githubApi.searchDailyDevs(10);
} else if (normalizedId === 'trending') {
const repos = await githubApi.searchTrending(10);
setSubscriptionRepos('trending', repos);
setSubscriptionDevs(devs);
} else if (normalizedId === 'trending') {
const repos = await githubApi.searchTrending(10);
setSubscriptionRepos('trending', repos);
}
setSubscriptionLastRefresh(normalizedId, new Date().toISOString());
} catch (err) {
console.error(`Failed to refresh subscription channel ${channelId}:`, err);
alert(t(`刷新失败,请检查网络连接。错误:${err instanceof Error ? err.message : String(err)}`, `Failed to refresh: ${err instanceof Error ? err.message : String(err)}`));
} finally {
setSubscriptionLoading(normalizedId, false);
}
}, [githubToken, t, setSubscriptionLoading, setSubscriptionRepos, setSubscriptionDevs, setSubscriptionLastRefresh]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: refreshChannel has corrupted merge-conflict output — build/runtime will break.

This block has several compounding problems:

  1. The most-dev branch body is empty (lines 68-69): } else if (normalizedId === 'most-dev') { } else if (normalizedId === 'trending') { ... }. So selecting the Most DEV channel never calls searchDailyDevs and never populates subscriptionDevs.
  2. There are three else if (normalizedId === 'trending') branches (lines 69, 73, 77). Only the first is reachable; the latter two are dead code.
  3. const devs = await githubApi.searchDailyDevs(10); is declared inside the second trending branch (line 72) but setSubscriptionDevs(devs); is called inside the third trending branch (line 76) — devs is not in scope there. This will fail type-checking (TS2304: cannot find name 'devs') and at runtime would be a ReferenceError if it ever ran.
  4. Indentation is inconsistent, which is the visual tell of an unresolved 3-way merge from the #85 integration.

This needs to be collapsed back into one clean switch over the four channel ids. For example:

🛠️ Suggested fix
-      if (normalizedId === 'most-stars') {
-        const repos = await githubApi.searchMostStars(10);
-        setSubscriptionRepos('most-stars', repos);
-      } else if (normalizedId === 'most-forks') {
-        const repos = await githubApi.searchMostForks(10);
-        setSubscriptionRepos('most-forks', repos);
-      } else if (normalizedId === 'most-dev') {
-    } else if (normalizedId === 'trending') {
-      const repos = await githubApi.searchTrending(10);
-      setSubscriptionRepos('trending', repos);
-        const devs = await githubApi.searchDailyDevs(10);
-    } else if (normalizedId === 'trending') {
-      const repos = await githubApi.searchTrending(10);
-      setSubscriptionRepos('trending', repos);
-        setSubscriptionDevs(devs);
-    } else if (normalizedId === 'trending') {
-      const repos = await githubApi.searchTrending(10);
-      setSubscriptionRepos('trending', repos);
-      }
+      if (normalizedId === 'most-stars') {
+        const repos = await githubApi.searchMostStars(10);
+        setSubscriptionRepos('most-stars', repos);
+      } else if (normalizedId === 'most-forks') {
+        const repos = await githubApi.searchMostForks(10);
+        setSubscriptionRepos('most-forks', repos);
+      } else if (normalizedId === 'most-dev') {
+        const devs = await githubApi.searchDailyDevs(10);
+        setSubscriptionDevs(devs);
+      } else if (normalizedId === 'trending') {
+        const repos = await githubApi.searchTrending(10);
+        setSubscriptionRepos('trending', repos);
+      }
       setSubscriptionLastRefresh(normalizedId, new Date().toISOString());

The PR notes claim "构建通过 (npm run build)" but this almost certainly fails tsc --noEmit/strict mode because of (3). Please re-run a clean typecheck + lint on this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionView.tsx` around lines 60 - 88, The refreshChannel
try-block contains leftover merge-conflict fragments: collapse the duplicated
else-if branches into a single control flow inside the refreshChannel function
(use a switch or single if/else chain checking normalizedId) so that
'most-stars' calls GitHubApiService.searchMostStars and setSubscriptionRepos,
'most-forks' calls searchMostForks and setSubscriptionRepos, 'most-dev' calls
searchDailyDevs and then setSubscriptionDevs (declare const devs in the same
branch), and 'trending' calls searchTrending and setSubscriptionRepos; remove
the two extra else if (normalizedId === 'trending') blocks, ensure devs is in
scope where setSubscriptionDevs(devs) is called, call
setSubscriptionLastRefresh(normalizedId, ...) once, and fix indentation and
formatting so the try/catch/finally compiles without TS errors (also re-run tsc
--noEmit and lint).

Comment on lines +246 to +248
const isAnalyzingThisChannel = isAnalyzing && (
analysisProgress.total > 0
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

isAnalyzingThisChannel no longer reflects "this channel".

The expression collapses to isAnalyzing && analysisProgress.total > 0, which is globally true/false regardless of which channel is active. If a user switches channels while an analysis is running on another channel, the other channel's progress bar + controls will render. Consider tracking the analyzing channel id in state and comparing against normalizedChannel here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionView.tsx` around lines 246 - 248,
isAnalyzingThisChannel currently only checks isAnalyzing &&
analysisProgress.total > 0 and therefore is global; change it to verify the
analysis belongs to the current channel by tracking the analyzing channel id in
state (e.g., add a state variable like analyzingChannelId that you set when
analysis starts and clear when it finishes or errors) and then compute
isAnalyzingThisChannel as isAnalyzing && analysisProgress.total > 0 &&
analyzingChannelId === normalizedChannel; ensure the code paths that start/stop
analysis (the functions that trigger analysis and the completion/error handlers)
set/clear analyzingChannelId accordingly so the UI only shows progress/controls
for the matching normalizedChannel.

Comment thread src/services/githubApi.ts
Comment on lines +280 to +285
async searchTrending(perPage = 10): Promise<SubscriptionRepo[]> {
// 模拟 Trending: 获取过去 7 天内创建且 Star 较多的项目
const lastWeek = new Set(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T'))[0];
const data = await this.makeRequest<GitHubSearchRepoResponse>(
`/search/repositories?q=created:>${lastWeek}&sort=stars&order=desc&per_page=${perPage}`
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Demonstrate that indexing a Set does not return its first value.
node -e "const value = new Set(new Date().toISOString().split('T'))[0]; console.log(value === undefined ? 'undefined' : value)"

Repository: AmintaCCCP/GithubStarsManager

Length of output: 82


Fix the Set indexing bug in the Trending date calculation.

Line 282 wraps the ISO date string split in a Set, then attempts to index it with [0]. Sets don't support numeric indexing, so this always returns undefined, causing the API query to become created:>undefined. Extract the date directly from the array split instead.

Proposed fix
-    const lastWeek = new Set(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T'))[0];
+    const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
+      .toISOString()
+      .split('T')[0];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async searchTrending(perPage = 10): Promise<SubscriptionRepo[]> {
// 模拟 Trending: 获取过去 7 天内创建且 Star 较多的项目
const lastWeek = new Set(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T'))[0];
const data = await this.makeRequest<GitHubSearchRepoResponse>(
`/search/repositories?q=created:>${lastWeek}&sort=stars&order=desc&per_page=${perPage}`
);
async searchTrending(perPage = 10): Promise<SubscriptionRepo[]> {
// 模拟 Trending: 获取过去 7 天内创建且 Star 较多的项目
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0];
const data = await this.makeRequest<GitHubSearchRepoResponse>(
`/search/repositories?q=created:>${lastWeek}&sort=stars&order=desc&per_page=${perPage}`
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 280 - 285, The Trending date
calculation in searchTrending incorrectly wraps the ISO string split in a Set
and then indexes it (lastWeek), which yields undefined; change the extraction to
take the date directly from the split array (e.g., use new
Date(...).toISOString().split('T')[0]) so lastWeek is a proper YYYY-MM-DD string
before building the query passed to makeRequest in searchTrending.

Comment thread src/services/githubApi.ts
Comment on lines +318 to +329
let topRepo: SubscriptionRepo | null = null;
try {
const reposData = await this.makeRequest<Repository[]>(
`/users/${searchUser.login}/repos?sort=stars&per_page=1`
);
if (reposData && reposData.length > 0) {
topRepo = {
...reposData[0],
rank: 1,
channel: 'most-dev' as const,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub REST API list repositories for a user sort parameter supported values

💡 Result:

The supported values for the 'sort' parameter in the GitHub REST API endpoint GET /users/{username}/repos (List repositories for a user) are: created, updated, pushed, full_name. Default value: full_name

Citations:


🏁 Script executed:

# Check if GitHubSearchRepoResponse is defined in the codebase
rg "GitHubSearchRepoResponse" --type ts --type tsx

Repository: AmintaCCCP/GithubStarsManager

Length of output: 100


🏁 Script executed:

# Check if search/repositories endpoint is used elsewhere in the codebase
rg "search/repositories" --type ts --type tsx

Repository: AmintaCCCP/GithubStarsManager

Length of output: 100


🏁 Script executed:

# Check if GitHubSearchRepoResponse is defined in the codebase
rg "GitHubSearchRepoResponse"

Repository: AmintaCCCP/GithubStarsManager

Length of output: 414


🏁 Script executed:

# Check if search/repositories endpoint is used elsewhere in the codebase
rg "search/repositories"

Repository: AmintaCCCP/GithubStarsManager

Length of output: 795


🏁 Script executed:

# List TypeScript files to understand the structure
fd -e ts | head -20

Repository: AmintaCCCP/GithubStarsManager

Length of output: 652


🏁 Script executed:

# Get the definition of GitHubSearchRepoResponse
rg "interface GitHubSearchRepoResponse" -A 15

Repository: AmintaCCCP/GithubStarsManager

Length of output: 796


🏁 Script executed:

# Look at the actual code at lines 318-329 to see the context
sed -n '310,335p' src/services/githubApi.ts

Repository: AmintaCCCP/GithubStarsManager

Length of output: 788


🏁 Script executed:

# Check how search/repositories is used with similar patterns elsewhere
rg "search/repositories.*user:" -A 3 -B 3

Repository: AmintaCCCP/GithubStarsManager

Length of output: 55


🏁 Script executed:

# Check what types are imported in githubApi.ts to understand Repository type
head -50 src/services/githubApi.ts | grep -E "^import|^type|^interface"

Repository: AmintaCCCP/GithubStarsManager

Length of output: 336


🏁 Script executed:

# Verify the search endpoint is properly typed and functional elsewhere
rg "makeRequest<GitHubSearchRepoResponse>" -B 2 -A 5

Repository: AmintaCCCP/GithubStarsManager

Length of output: 1754


Use repository search for the developer's top-starred repo.

The sort=stars parameter is not supported by the list-user-repositories endpoint (GET /users/{username}/repos). Supported sort values are: created, updated, pushed, full_name. Use the repository search endpoint with sort=stars instead, which is already used elsewhere in the codebase for similar queries.

Fix
-        const reposData = await this.makeRequest<Repository[]>(
-          `/users/${searchUser.login}/repos?sort=stars&per_page=1`
-        );
-        if (reposData && reposData.length > 0) {
+        const reposData = await this.makeRequest<GitHubSearchRepoResponse>(
+          `/search/repositories?q=${encodeURIComponent(`user:${searchUser.login}`)}&sort=stars&order=desc&per_page=1`
+        );
+        if (reposData.items && reposData.items.length > 0) {
           topRepo = {
-            ...reposData[0],
+            ...reposData.items[0],
             rank: 1,
             channel: 'most-dev' as const,
           };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 318 - 329, The current logic that
fetches a user's top-starred repo using makeRequest with
`/users/${searchUser.login}/repos?sort=stars` should be changed to use the
repository search endpoint that supports sort=stars; replace the call that sets
topRepo (the block that creates topRepo of type SubscriptionRepo from
Repository[] using searchUser.login and channel 'most-dev') with a search-based
request (e.g., query `q=user:${searchUser.login}` and
`sort=stars&order=desc&per_page=1`) using the existing makeRequest helper, then
map the first search result into the same SubscriptionRepo shape (rank: 1,
channel: 'most-dev') so subsequent code using topRepo behaves the same.

Comment thread src/store/useAppStore.ts
Comment on lines +1006 to +1017
// 迁移订阅频道(版本 4→5:daily-dev → most-dev,新增 trending)
if (state && !Array.isArray(state.subscriptionChannels)) {
console.log('Migrating: initializing subscription channels');
state.subscriptionChannels = defaultSubscriptionChannels;
} else if (state && Array.isArray(state.subscriptionChannels)) {
state.subscriptionChannels = state.subscriptionChannels.map((ch: Record<string, unknown>) => {
if (ch.id === 'daily-dev' || ch.id === 'most-dev') {
return { ...ch, id: 'most-dev', name: '热门开发者', nameEn: 'Top Developers', icon: '👤' };
}
return ch;
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Merge default subscription channels during migration.

When state.subscriptionChannels already exists, the migration only remaps legacy IDs; it never appends new defaults like trending. Those persisted channels then override defaultSubscriptionChannels during merge, so returning users can miss the new Trending channel.

🐛 Proposed fix
   if (state && !Array.isArray(state.subscriptionChannels)) {
     console.log('Migrating: initializing subscription channels');
     state.subscriptionChannels = defaultSubscriptionChannels;
   } else if (state && Array.isArray(state.subscriptionChannels)) {
-    state.subscriptionChannels = state.subscriptionChannels.map((ch: Record<string, unknown>) => {
+    const migratedChannels = state.subscriptionChannels.map((ch: Record<string, unknown>) => {
       if (ch.id === 'daily-dev' || ch.id === 'most-dev') {
         return { ...ch, id: 'most-dev', name: '热门开发者', nameEn: 'Top Developers', icon: '👤' };
       }
       return ch;
     });
+    const existingIds = new Set(migratedChannels.map((ch: Record<string, unknown>) => ch.id));
+    state.subscriptionChannels = [
+      ...migratedChannels,
+      ...defaultSubscriptionChannels.filter(ch => !existingIds.has(ch.id)),
+    ];
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 1006 - 1017, The migration currently
maps legacy IDs on state.subscriptionChannels but never merges in new defaults
like defaultSubscriptionChannels (so new channels such as "trending" are
omitted); update the branch that handles
Array.isArray(state.subscriptionChannels) to first map/remap legacy entries
(referencing state.subscriptionChannels and the legacy ids
'daily-dev'/'most-dev'), then compute a merged list by taking all mapped
existing channels and appending any items from defaultSubscriptionChannels whose
id is not already present (de-duplicate by id) so new defaults are added without
duplicating existing ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants