diff --git a/src/App.vue b/src/App.vue index 7001edc1..5283413e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -901,7 +901,11 @@ const fetchAppFromStore = async ( const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${encodeURIComponent( category, )}/${encodeURIComponent(pkgname)}/app.json`; - const response = await fetch(appJsonUrl); + // 接入 rootAbortController.signal,确保组件卸载/详情关闭时请求可被取消, + // 避免竞态与内存泄漏(onUnmounted 会 abort 该 controller) + const response = await fetch(appJsonUrl, { + signal: rootAbortController.signal, + }); if (!response.ok) return null; const appJson = await response.json(); // img_urls 可能为字符串形式的 JSON,解析失败时安全回退为空数组 @@ -935,6 +939,8 @@ const fetchAppFromStore = async ( currentStatus: "not-installed", }; } catch (e) { + // 组件卸载/详情关闭触发 abort 时静默返回,避免刷 AbortError 日志 + if ((e as Error)?.name === "AbortError") return null; console.warn(`Failed to fetch ${origin} app info for ${pkgname}`, e); return null; } @@ -1398,23 +1404,40 @@ const loadHome = async () => { >[]) : []; for (const l of links) { - const name = (l.Name as string) || (l.name as string) || ""; + // 远程数据不可信,使用 typeof 运行时守卫替代 `as string` 断言, + // 避免非字符串字段(如数字/对象)被注入状态导致下游显示异常。 + const name = + typeof l.Name === "string" + ? l.Name + : typeof l.name === "string" + ? l.name + : ""; if (!name) continue; // 跳过空名称,避免空字符串污染 seenNames 与去重逻辑 if (seenNames.has(name)) continue; // 已由更高优先级来源(spark)占据 // 仅校验 url 必需;远程 homelinks.json 不含 icon 字段(图片由 imgUrl 提供), // 故 icon 不作为硬性校验,缺省为空串以兼容 HomeLink 类型。 - const url = (l.Url as string) || (l.url as string) || ""; + const url = + typeof l.Url === "string" + ? l.Url + : typeof l.url === "string" + ? l.url + : ""; if (!url) continue; - const icon = (l.Icon as string) || (l.icon as string) || ""; + const icon = + typeof l.Icon === "string" + ? l.Icon + : typeof l.icon === "string" + ? l.icon + : ""; seenNames.add(name); // 显式提取已知字段构造,避免通过展开运算符 { ...l } 把远程不可信数据中的未知属性注入响应式状态 const safeLink: HomeLink = { name, url, icon, - more: (l.more as string) || undefined, - imgUrl: (l.imgUrl as string) || undefined, - type: (l.type as string) || undefined, + more: typeof l.more === "string" ? l.more : undefined, + imgUrl: typeof l.imgUrl === "string" ? l.imgUrl : undefined, + type: typeof l.type === "string" ? l.type : undefined, origin: mode, }; homeLinks.value.push(safeLink); diff --git a/src/global/storeConfig.ts b/src/global/storeConfig.ts index f7e23a38..7b1a6a51 100644 --- a/src/global/storeConfig.ts +++ b/src/global/storeConfig.ts @@ -113,9 +113,16 @@ export async function loadPriorityConfig(arch: string): Promise { }, }; } + // 仅打印规则计数而非完整配置,避免潜在敏感信息泄露(诊断用) + const ruleCount = (r: { + pkgnames: string[]; + categories: string[]; + tags: string[]; + }) => r.pkgnames.length + r.categories.length + r.tags.length; console.log( - "[PriorityConfig] 已从服务器加载优先级配置:", - JSON.stringify(dynamicPriorityConfig), + `[PriorityConfig] 已从服务器加载优先级配置: spark ${ruleCount( + dynamicPriorityConfig.sparkPriority, + )} 条, apm ${ruleCount(dynamicPriorityConfig.apmPriority)} 条`, ); } catch (error) { // 获取失败(含 404:服务器无配置文件),默认优先 APM。 diff --git a/src/modules/ranking.ts b/src/modules/ranking.ts index fbac423e..aa71db47 100644 --- a/src/modules/ranking.ts +++ b/src/modules/ranking.ts @@ -83,8 +83,7 @@ export function topUpdatedContributors( recentN: number = RECENT_UPDATE_WINDOW, topN: number = TOP_N, ): ContributorRank[] { - const recent = topByUpdate(apps, origin, recentN).filter( - (a) => a.origin === origin, - ); + // topByUpdate 内部已按 origin 过滤,无需再次 filter + const recent = topByUpdate(apps, origin, recentN); return toRanks(countContributors(recent), topN); }