mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
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:
+30
-7
@@ -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);
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user