diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts
index 2fd0abc5..2d45b192 100644
--- a/electron/main/backend/install-manager.ts
+++ b/electron/main/backend/install-manager.ts
@@ -285,10 +285,16 @@ const parseUpgradableList = (output: string) => {
// Listen for download requests from renderer process
ipcMain.on("queue-install", async (event, download_json) => {
- const download =
- typeof download_json === "string"
- ? JSON.parse(download_json)
- : download_json;
+ let download: unknown;
+ try {
+ download =
+ typeof download_json === "string"
+ ? JSON.parse(download_json)
+ : download_json;
+ } catch (err) {
+ logger.error({ err }, "queue-install: invalid JSON payload, ignoring task");
+ return;
+ }
const { id, pkgname, metalinkUrl, filename, origin, upgradeOnly } =
download || {};
diff --git a/src/App.vue b/src/App.vue
index 20e96fdb..362142b7 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1134,7 +1134,10 @@ const openDetail = async (app: App | OpenDetailInput) => {
// 非强制分支:清除可能由「已安装页」入口遗留的 forceViewingOrigin 粘性标志,
// 避免同一应用从其他页面再次打开时被锁死在旧来源(表现为设置切换不生效)。
finalApp.forceViewingOrigin = false;
- if (forceOrigin && (forceOrigin === "spark" ? finalApp.sparkApp : finalApp.apmApp)) {
+ if (
+ forceOrigin &&
+ (forceOrigin === "spark" ? finalApp.sparkApp : finalApp.apmApp)
+ ) {
// 已安装页 + 唯一安装来源 → 强制展示该安装类型,优先级高于用户标签策略
finalApp.viewingOrigin = forceOrigin;
finalApp.forceViewingOrigin = true;
@@ -1544,9 +1547,8 @@ const loadHomeListApps = async (entryId: string) => {
rawApps.map((a) => {
// 首页推荐列表的 jsonUrl 形如 /home/lists/xxx.json;服务端原始数据不含 category 字段,
// 这里提取 URL 首段目录作为分类(无真实分类时回退 "unknown"),避免污染后续优先级匹配。
- const urlCategory = (urls[mode] || "")
- .split("/")
- .filter(Boolean)[0] || "unknown";
+ const urlCategory =
+ (urls[mode] || "").split("/").filter(Boolean)[0] || "unknown";
const category = a.Category || a.category || urlCategory;
let img_urls: string[] = [];
@@ -2176,7 +2178,14 @@ const closeSettingsModal = () => {
};
const openExternalUrl = (url: string) => {
- window.open(url, "_blank", "noopener,noreferrer");
+ try {
+ const parsed = new URL(url);
+ // 仅允许 http/https 协议,防止 javascript:/data: 等造成 XSS
+ if (!["http:", "https:"].includes(parsed.protocol)) return;
+ window.open(url, "_blank", "noopener,noreferrer");
+ } catch {
+ // 非法 URL 直接忽略,不打开
+ }
};
const openReviewUserProfile = (review: AppReview): void => {
@@ -3426,14 +3435,15 @@ onMounted(async () => {
loading.value = true;
homeLoading.value = true;
- // 区域1(links) 与 区域2 板块(homeList) 并行;全量应用也并行加载
- await Promise.all([
- loadHome(),
- preloadHomeListApps(),
- loadApps(() => {
- loading.value = false; // 首屏:已有部分应用即可显示,排行稍后刷新
- }),
- ]);
+ // 首页推荐(区域1 links + 区域2 homeList 板块)轻量且独立,
+ // 不依赖全量应用,进入软件即优先并行加载并显示,不再与 loadApps 耦合。
+ await Promise.all([loadHome(), preloadHomeListApps()]);
+
+ // 全量应用后台加载(不阻塞首页推荐):首个分类成功即关闭首屏遮罩,
+ // 增量渲染,排行稍后由 loadRanking 刷新。
+ void loadApps(() => {
+ loading.value = false; // 首屏:已有部分应用即可显示
+ });
// 全量应用加载完成后再刷新排行榜,确保 spark/apm 应用均已就绪
loadRanking();
diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue
index 5f0ac9c8..0c457b4a 100644
--- a/src/components/AppDetailModal.vue
+++ b/src/components/AppDetailModal.vue
@@ -587,15 +587,16 @@ import type { App, AppReview, ReviewTags } from "../global/typedefinition";
/**
* 净化后端返回的应用描述(v-html 前严格去除所有标签)。
- * 程序先将用户内容中的所有 HTML 标签剥离,再将 \n 转为
。
- *
是纯程序生成,不含任何用户输入,可安全放入 v-html。
+ * 使用 textContent 做 HTML 实体转义,彻底杜绝