fix: 修复合并冲突、标签渲染问题,添加 Trending 频道,优化本地化#87
Conversation
新增 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 -> 热门趋势
📝 WalkthroughWalkthroughThis 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorAdd missing subscription fields to
PersistedAppStatetype.The persist middleware explicitly writes
subscriptionChannels,selectedSubscriptionChannel,subscriptionRepos,subscriptionDevs, andsubscriptionLastRefreshto storage, but these fields are missing from thePersistedAppStatePick list. This type mismatch is masked by explicitas PersistedAppStatecasts 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.
searchRepositoriesandsearchUsersare 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: Defensivetypeof === 'object'casts are obscuring the props contract.
isLoadingandlastRefreshare already typed asRecord<SubscriptionChannelId, ...>, so the runtimetypeof/Record<string, unknown>gymnastics plus repeatedchannel.id === 'daily-dev' ? 'most-dev' : channel.idternaries add a lot of noise without adding safety (and thedaily-devid can't match per the type, same as above). If the sidebar trusts its typed props, these accesses collapse to plainisLoading[channel.id]/lastRefresh[channel.id]. Also the indentation at line 67 appears off (extra spaces beforeconst 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, andRepositoryare 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 toforks_countat API boundary.
SubscriptionRepodeclares bothforks?: numberandforks_count?: number, but the GitHub API service only populatesforks_count. This makes the fallback chain inSubscriptionRepoCard(repo.forks_count ?? repo.forks ?? 0) include dead code sinceforksis never assigned. Remove the unusedforksfield fromSubscriptionRepoand update the card fallback torepo.forks_count ?? 0to align with GitHub's API whereforks_countis 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
📒 Files selected for processing (12)
server/src/routes/proxy.tssrc/App.tsxsrc/components/ErrorBoundary.tsxsrc/components/Header.tsxsrc/components/SubscriptionDevCard.tsxsrc/components/SubscriptionRepoCard.tsxsrc/components/SubscriptionSidebar.tsxsrc/components/SubscriptionView.tsxsrc/services/backendAdapter.tssrc/services/githubApi.tssrc/store/useAppStore.tssrc/types/index.ts
| // 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' }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm route registration order in the router file.
rg -nP "router\.(get|post|use)\(" server/src/routes/proxy.tsRepository: AmintaCCCP/GithubStarsManager
Length of output: 396
🏁 Script executed:
sed -n '46,88p' server/src/routes/proxy.tsRepository: 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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| {((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> | ||
| )} |
There was a problem hiding this comment.
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.
| {((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.
| 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 | ||
| ); |
There was a problem hiding this comment.
Hardcoded English label contradicts the PR's localization goal, and the daily-dev branch is unreachable by type.
SubscriptionChannelIdis'most-stars' | 'most-forks' | 'most-dev' | 'trending'(seesrc/types/index.tsline 219), soch.id === 'daily-dev'can never be true for a properly-typedSubscriptionChannel[]— TS should be narrowing this tonever. If this exists only to handle legacy persisted state from v4, the remap belongs in the store'smigratefunction (where the PR summary says it already happens), not in every consumer.- Even if it did fire, it hardcodes
name: 'Most DEV'andicon: '👤', 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.
| 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]); |
There was a problem hiding this comment.
Critical: refreshChannel has corrupted merge-conflict output — build/runtime will break.
This block has several compounding problems:
- The
most-devbranch body is empty (lines 68-69):} else if (normalizedId === 'most-dev') { } else if (normalizedId === 'trending') { ... }. So selecting the Most DEV channel never callssearchDailyDevsand never populatessubscriptionDevs. - There are three
else if (normalizedId === 'trending')branches (lines 69, 73, 77). Only the first is reachable; the latter two are dead code. const devs = await githubApi.searchDailyDevs(10);is declared inside the secondtrendingbranch (line 72) butsetSubscriptionDevs(devs);is called inside the thirdtrendingbranch (line 76) —devsis 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.- Indentation is inconsistent, which is the visual tell of an unresolved 3-way merge from the
#85integration.
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).
| const isAnalyzingThisChannel = isAnalyzing && ( | ||
| analysisProgress.total > 0 | ||
| ); |
There was a problem hiding this comment.
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.
| 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}` | ||
| ); |
There was a problem hiding this comment.
🧩 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.
| 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.
| 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://docs.github.com/rest/repos/repos
- 2: https://docs.github.com/en/rest/repos/repos?api=
- 3: https://help.github.com/en/rest/repos/repos
- 4: https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10
🏁 Script executed:
# Check if GitHubSearchRepoResponse is defined in the codebase
rg "GitHubSearchRepoResponse" --type ts --type tsxRepository: 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 tsxRepository: 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 -20Repository: AmintaCCCP/GithubStarsManager
Length of output: 652
🏁 Script executed:
# Get the definition of GitHubSearchRepoResponse
rg "interface GitHubSearchRepoResponse" -A 15Repository: 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.tsRepository: 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 3Repository: 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 5Repository: 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.
| // 迁移订阅频道(版本 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; | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
修复说明
本修复解决了 PR #86 与已合并的 #85 之间的冲突,并进行了多项改进:
1. 解决合并冲突
2. 修复代码审计问题
SubscriptionDevCard.tsx: 修复标签渲染逻辑缺陷
原代码问题:当 ai_tags 为空数组时,不会 fallback 到 topics
3. 导航栏改名
4. 新增 Trending 频道
5. 优化本地化
修改文件
测试
✅ 构建通过 (npm run build)
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes