fix: 处理 PR 审查意见(竞态/类型安全/冗余/日志泄露)

- App.vue fetchAppFromStore 的 fetch 接入 rootAbortController.signal,
  组件卸载时取消请求,避免竞态与内存泄漏;catch 静默处理 AbortError。
- App.vue loadHome 远程字段由 `as string` 断言改为 typeof 运行时守卫,
  避免非字符串数据注入响应式状态(非崩溃,属改进)。
- ranking.ts topUpdatedContributors 移除对 topByUpdate 已过滤结果的冗余 filter。
- storeConfig 加载成功日志改为仅打印规则条数,避免完整配置泄露且保留诊断价值。
- 未采纳:parseAppPayload 默认 origin=spark 维持不变(spark 为历史默认源,
  盲改 apm 有反向误路由风险,且该字符串分支仅为极旧格式兜底)。
This commit is contained in:
xiyidaiwa
2026-08-10 17:19:55 +08:00
parent 9247fe8e63
commit 800a92b419
3 changed files with 41 additions and 12 deletions
+30 -7
View File
@@ -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);
+9 -2
View File
@@ -113,9 +113,16 @@ export async function loadPriorityConfig(arch: string): Promise<void> {
},
};
}
// 仅打印规则计数而非完整配置,避免潜在敏感信息泄露(诊断用)
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。
+2 -3
View File
@@ -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);
}