Files
spark-store/src/global/storeConfig.ts
T
xiyidaiwa 5e7d88ecc5 fix(审计修复): 缓存穿透TTL共享 + 预刷新网络/超时 + 最大化/APM优先设计注释
- 新增 cacheBusterInterceptor.ts:createCacheBusterInterceptor(ttl=5min) 带 TTL 复用戳,
  解决同会话频繁击穿缓存(B1)并将 App.vue/storeConfig 两处重复逻辑合并为共享工厂(I3)
- storeConfig.ts: priorityConfigAxios 改用共享拦截器;HYBRID_DEFAULT_PRIORITY 加注释
  明确「主推 APM 是既定产品方向」(B2,非缺陷)
- App.vue: axiosInstance 改用共享拦截器(带 TTL)
- index.ts: 预刷新 startSourcePreRefreshOnce 加 net.isOnline() 检查 + 60s 超时兜底,
  避免离线/卡死触发 pkexec 弹窗困惑(I1);createWindow 恢复逻辑加注释明确
  「最大化后恢复重置为默认尺寸是有意设计,非缺陷」(I2)
- vue-tsc + eslint 通过
2026-08-12 21:02:48 +08:00

235 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ref } from "vue";
import axios from "axios";
import type { App, StoreMode } from "./typedefinition";
import { createCacheBusterInterceptor } from "./cacheBusterInterceptor";
export const APM_STORE_BASE_URL: string =
import.meta.env.VITE_APM_STORE_BASE_URL || "";
// 加载优先级配置专用客户端:与渲染进程其余请求共用同一套 HTTP 客户端,
// 避免 Electron 渲染进程中全局 fetch 失败导致配置始终为空、auto 模式全部回退 APM。
const priorityConfigAxios = axios.create({
baseURL: APM_STORE_BASE_URL,
timeout: 10000,
});
// C2priority-config.json 走独立 axios 实例(不经过 axiosInstance),
// 复用共享缓存穿透拦截器(带 TTL 复用戳,避免同会话频繁击穿缓存)。
// 与 C1axiosInstance 的拦截器)为同一工厂生成,确保穿透策略与 TTL 行为一致。
priorityConfigAxios.interceptors.request.use(createCacheBusterInterceptor());
export const APM_STORE_STATS_BASE_URL: string =
import.meta.env.VITE_APM_STORE_STATS_BASE_URL || "";
export const DEFAULT_SPARK_BACKEND_BASE_URL = "http://127.0.0.1:8000";
export const SPARK_BACKEND_BASE_URL: string =
import.meta.env.VITE_SPARK_BACKEND_BASE_URL || DEFAULT_SPARK_BACKEND_BASE_URL;
export const SPARK_ACCOUNT_CENTER_URL: string =
import.meta.env.VITE_SPARK_ACCOUNT_CENTER_URL ||
"https://account.spark-app.store/account";
export const FLARUM_BASE_URL = "https://bbs.spark-app.store";
export const FLARUM_REGISTER_URL = `${FLARUM_BASE_URL}/register`;
export const FLARUM_PROFILE_URL = `${FLARUM_BASE_URL}/u`;
export const FLARUM_SETTINGS_URL = `${FLARUM_BASE_URL}/settings`;
// 下面的变量用于存储当前应用的信息,其实用在多个组件中
export const currentApp = ref<App | null>(null);
export const currentAppSparkInstalled = ref(false);
export const currentAppApmInstalled = ref(false);
export const showApmInstallDialog = ref(false);
export const currentStoreMode = ref<StoreMode>("hybrid");
// 混合模式下默认优先安装的来源(当没有服务器配置或配置获取失败时使用)。
// 设计意图:社区版当前主推 APM 来源(星火 APM 为新架构、deb 为传统来源),
// 故默认优先级设为 "apm"。非临时回归,属既定产品方向;如后续调整需产品确认。
// TODO: 产品确认默认 APM 优先是否长期保持。
export const HYBRID_DEFAULT_PRIORITY: "apm" | "spark" = "apm";
// 优先级规则配置接口
export interface PriorityRules {
// 优先使用 Spark 的规则(例外于默认 APM 优先)
sparkPriority: {
pkgnames: string[]; // 包名列表
categories: string[]; // 分类列表
tags: string[]; // 标签列表
};
// 优先使用 APM 的规则(例外于 sparkPriority,具有更高优先级)
apmPriority: {
pkgnames: string[]; // 包名列表(即使在 sparkPriority 分类中也优先 APM
categories: string[]; // 分类列表
tags: string[]; // 标签列表
};
}
// 动态获取的优先级配置(从服务器加载)
export let dynamicPriorityConfig: PriorityRules = {
sparkPriority: {
pkgnames: [],
categories: [],
tags: [],
},
apmPriority: {
pkgnames: [],
categories: [],
tags: [],
},
};
/**
* 从服务器加载优先级配置
* 配置文件路径: ${arch}-store/priority-config.json (放在 spark 下)
* @param arch 架构,如 "amd64"
*/
export async function loadPriorityConfig(arch: string): Promise<void> {
try {
const configPath = `/${arch}-store/priority-config.json`;
console.log(
`[PriorityConfig] 开始加载优先级配置: ${APM_STORE_BASE_URL}${configPath}`,
);
const response = await priorityConfigAxios.get(configPath);
const config = response.data;
// 顶层结构校验:畸形数据(非对象/为数组/null)直接走失败回退,避免访问属性抛错
if (!config || typeof config !== "object" || Array.isArray(config)) {
throw new Error("Invalid priority-config format: expected object");
}
// 支持新旧两种配置格式
if (config.sparkPriority || config.apmPriority) {
// 新格式:双向配置
dynamicPriorityConfig = {
sparkPriority: {
pkgnames: config.sparkPriority?.pkgnames || [],
categories: config.sparkPriority?.categories || [],
tags: config.sparkPriority?.tags || [],
},
apmPriority: {
pkgnames: config.apmPriority?.pkgnames || [],
categories: config.apmPriority?.categories || [],
tags: config.apmPriority?.tags || [],
},
};
} else {
// 旧格式:只配置 sparkPriority(兼容旧配置)
dynamicPriorityConfig = {
sparkPriority: {
pkgnames: config.pkgnames || [],
categories: config.categories || [],
tags: config.tags || [],
},
apmPriority: {
pkgnames: [],
categories: [],
tags: [],
},
};
}
// 仅打印规则计数而非完整配置,避免潜在敏感信息泄露(诊断用)
const ruleCount = (r: {
pkgnames: string[];
categories: string[];
tags: string[];
}) => r.pkgnames.length + r.categories.length + r.tags.length;
console.log(
`[PriorityConfig] 已从服务器加载优先级配置: spark ${ruleCount(
dynamicPriorityConfig.sparkPriority,
)} 条, apm ${ruleCount(dynamicPriorityConfig.apmPriority)} 条`,
);
} catch (error) {
// 获取失败(含 404:服务器无配置文件),默认优先 APM。
// 注:此处为配置缺失/加载失败,dynamicPriorityConfig 重置为空规则,
// 由 getHybridDefaultOrigin 回退 HYBRID_DEFAULT_PRIORITY(默认 APM)。
console.warn(
`[PriorityConfig] 加载配置失败(${APM_STORE_BASE_URL}/${arch}-store/priority-config.json),使用默认 APM 优先:`,
error,
);
resetPriorityConfig();
}
}
/**
* 重置优先级配置为默认值(配置缺失/加载失败时调用)
*/
function resetPriorityConfig(): void {
dynamicPriorityConfig = {
sparkPriority: {
pkgnames: [],
categories: [],
tags: [],
},
apmPriority: {
pkgnames: [],
categories: [],
tags: [],
},
};
}
/**
* 按优先级配置规则判断单个应用标识是否命中「优先来源」。
* 命中则返回 "apm" / "spark",未命中返回 null。
* 规则优先级(从高到低):apmPriority.pkgnames > sparkPriority.pkgnames >
* apmPriority.categories > sparkPriority.categories > apmPriority.tags > sparkPriority.tags
*/
function matchPriority(app: App): "apm" | "spark" | null {
const { sparkPriority, apmPriority } = dynamicPriorityConfig;
if (apmPriority.pkgnames.includes(app.pkgname)) return "apm";
if (sparkPriority.pkgnames.includes(app.pkgname)) return "spark";
if (apmPriority.categories.includes(app.category)) return "apm";
if (sparkPriority.categories.includes(app.category)) return "spark";
if (app.tags && apmPriority.tags.length > 0) {
const appTags = app.tags.split(";").map((t) => t.trim().toLowerCase());
for (const ruleTag of apmPriority.tags) {
if (appTags.includes(ruleTag.toLowerCase())) return "apm";
}
}
if (app.tags && sparkPriority.tags.length > 0) {
const appTags = app.tags.split(";").map((t) => t.trim().toLowerCase());
for (const ruleTag of sparkPriority.tags) {
if (appTags.includes(ruleTag.toLowerCase())) return "spark";
}
}
return null;
}
/**
* 获取混合模式下应用的默认优先来源(即「应用配置优先级」)。
* 对合并应用,依次用顶层标识、sparkApp、apmApp 去匹配优先级配置,
* 任一来源命中即生效(避免顶层 pkgname 取自某一子版而漏掉另一子版的规则)。
* 若两子版方向相反命中,遵从 apmPriority「例外优先」语义让 apm 胜出。
* 均不命中时回退到 HYBRID_DEFAULT_PRIORITY(默认优先 APM)。
* @param app 应用信息
* @returns "apm" 或 "spark"
*/
export function getHybridDefaultOrigin(app: App): "apm" | "spark" {
const result = matchPriority(app);
if (result) return result;
// 合并应用:两个子版分别匹配,任一命中即采用。
// 若子版自身 category 为空(如部分入口构造的 fallback 应用),
// 回退使用顶层 app.category 参与分类规则匹配,避免漏匹配。
if (app.isMerged) {
const fallbackCategory = app.category;
// 构造 category 回退顶层的候选项,避免子版 category 为空时分类规则漏匹配
const toCandidate = (sub: App): App =>
sub.category ? sub : { ...sub, category: fallbackCategory };
const sparkHit = app.sparkApp
? matchPriority(toCandidate(app.sparkApp))
: null;
const apmHit = app.apmApp ? matchPriority(toCandidate(app.apmApp)) : null;
// apmPriority 是「例外优先于 sparkPriority」:当两子版方向相反命中
//sparkApp 命中 spark 且 apmApp 命中 apm)时,按配置语义让 apm 胜出。
if (sparkHit === "spark" && apmHit === "apm") return "apm";
if (sparkHit) return sparkHit;
if (apmHit) return apmHit;
}
// 默认行为:与 HYBRID_DEFAULT_PRIORITY 保持一致(默认优先 APM)
return HYBRID_DEFAULT_PRIORITY;
}