Merge branch 'Erotica' of gitee.com:xiyidaiwa/spark-store into Erotica

# Conflicts:
#	src/App.vue
This commit is contained in:
xiyidaiwa
2026-08-07 18:23:35 +08:00
5 changed files with 308 additions and 229 deletions
+26 -27
View File
@@ -1269,7 +1269,6 @@ ipcMain.handle("show-apm-install-dialog", async (event) => {
return { success: false, cancelled: true }; return { success: false, cancelled: true };
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle( ipcMain.handle(
"uninstall-installed", "uninstall-installed",
async ( async (
@@ -1283,42 +1282,42 @@ ipcMain.handle(
} }
const { pkgname, origin } = parsed; const { pkgname, origin } = parsed;
const superUserCmd = await checkSuperUserCommand(); const superUserCmd = await checkSuperUserCommand();
const execCommand = superUserCmd || SHELL_CALLER_PATH; const execCommand = superUserCmd || SHELL_CALLER_PATH;
const execParams = superUserCmd ? [SHELL_CALLER_PATH] : []; const execParams = superUserCmd ? [SHELL_CALLER_PATH] : [];
if (origin === "apm") { if (origin === "apm") {
execParams.push("apm", "remove", "-y", pkgname); execParams.push("apm", "remove", "-y", pkgname);
} else { } else {
execParams.push("aptss", "remove", "-y", pkgname); execParams.push("aptss", "remove", "-y", pkgname);
} }
const { code, stdout, stderr } = await runCommandCapture( const { code, stdout, stderr } = await runCommandCapture(
execCommand, execCommand,
execParams, execParams,
); );
const success = code === 0; const success = code === 0;
if (success) { if (success) {
logger.info(`卸载完成: ${pkgname}`); logger.info(`卸载完成: ${pkgname}`);
} else { } else {
logger.error(`卸载失败: ${pkgname} ${stderr || stdout}`); logger.error(`卸载失败: ${pkgname} ${stderr || stdout}`);
} }
return { return {
success, success,
message: success message: success
? "卸载完成" ? "卸载完成"
: stderr || stdout || `卸载失败,退出码 ${code}`, : stderr || stdout || `卸载失败,退出码 ${code}`,
}; };
}); },
);
interface LaunchAppPayload { interface LaunchAppPayload {
pkgname: string; pkgname: string;
origin?: "spark" | "apm"; origin?: "spark" | "apm";
} }
ipcMain.handle( ipcMain.handle(
"launch-app", "launch-app",
async ( async (
+15
View File
@@ -357,6 +357,16 @@ function saveWindowState(state: WindowState): void {
} }
let saveBoundsTimer: NodeJS.Timeout | null = null; let saveBoundsTimer: NodeJS.Timeout | null = null;
function flushSaveBounds(): void {
if (saveBoundsTimer) {
clearTimeout(saveBoundsTimer);
saveBoundsTimer = null;
}
if (win && !win.isDestroyed()) {
const { width, height, x, y } = win.getBounds();
saveWindowState({ width, height, x, y, maximized: win.isMaximized() });
}
}
function scheduleSaveBounds(winInstance: BrowserWindow): void { function scheduleSaveBounds(winInstance: BrowserWindow): void {
if (saveBoundsTimer) clearTimeout(saveBoundsTimer); if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
saveBoundsTimer = setTimeout(() => { saveBoundsTimer = setTimeout(() => {
@@ -372,6 +382,11 @@ function scheduleSaveBounds(winInstance: BrowserWindow): void {
}, 400); }, 400);
} }
// 应用退出前立即持久化(防抖 400ms 可能在快速关闭时丢失最后一次状态)
app.on("before-quit", () => {
flushSaveBounds();
});
async function createWindow() { async function createWindow() {
const saved = loadWindowState(); const saved = loadWindowState();
// 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中, // 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中,
+262 -198
View File
@@ -897,10 +897,24 @@ const fetchAppFromStore = async (
try { try {
const arch = window.apm_store.arch || "amd64"; const arch = window.apm_store.arch || "amd64";
const finalArch = origin === "spark" ? `${arch}-store` : `${arch}-apm`; const finalArch = origin === "spark" ? `${arch}-store` : `${arch}-apm`;
const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${category}/${pkgname}/app.json`; // 路径参数需编码,避免特殊字符破坏请求路径或造成路径穿越
const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${encodeURIComponent(
category,
)}/${encodeURIComponent(pkgname)}/app.json`;
const response = await fetch(appJsonUrl); const response = await fetch(appJsonUrl);
if (!response.ok) return null; if (!response.ok) return null;
const appJson = await response.json(); const appJson = await response.json();
// img_urls 可能为字符串形式的 JSON,解析失败时安全回退为空数组
const parsedImgUrls = (() => {
if (typeof appJson.img_urls === "string") {
try {
return JSON.parse(appJson.img_urls) as string[];
} catch {
return [];
}
}
return (appJson.img_urls as string[]) || [];
})();
return { return {
name: appJson.Name || "", name: appJson.Name || "",
pkgname: appJson.Pkgname || pkgname, pkgname: appJson.Pkgname || pkgname,
@@ -914,10 +928,7 @@ const fetchAppFromStore = async (
size: appJson.Size || "", size: appJson.Size || "",
more: appJson.More || "", more: appJson.More || "",
tags: appJson.Tags || "", tags: appJson.Tags || "",
img_urls: img_urls: parsedImgUrls,
typeof appJson.img_urls === "string"
? (JSON.parse(appJson.img_urls) as string[])
: appJson.img_urls || [],
icons: appJson.icons || "", icons: appJson.icons || "",
category: category, category: category,
origin: origin, origin: origin,
@@ -944,6 +955,31 @@ interface OpenDetailInput extends Partial<App> {
origin?: "spark" | "apm"; origin?: "spark" | "apm";
} }
// 提取远程/本地均无匹配时的回退 App 构造(两处兜底分支共用,避免重复字段映射)
const createFallbackApp = (
raw: Record<string, unknown>,
pkgname: string,
category: string,
): App => ({
name: (raw.name as string) || "",
pkgname,
version: (raw.version as string) || "",
filename: (raw.filename as string) || "",
category,
torrent_address: "",
author: "",
contributor: "",
website: "",
update: "",
size: "",
more: (raw.more as string) || "",
tags: "",
img_urls: [],
icons: "",
origin: (raw.origin as "spark" | "apm") || "apm",
currentStatus: "not-installed",
});
const openDetail = async (app: App | OpenDetailInput) => { const openDetail = async (app: App | OpenDetailInput) => {
// 提取 pkgname 和 category(必须存在) // 提取 pkgname 和 category(必须存在)
const pkgname = app?.pkgname; const pkgname = app?.pkgname;
@@ -1029,26 +1065,11 @@ const openDetail = async (app: App | OpenDetailInput) => {
finalApp = fullApp; finalApp = fullApp;
} else { } else {
// 两个仓库都没有找到,且本地也没有,构造一个最小可用的 App 对象 // 两个仓库都没有找到,且本地也没有,构造一个最小可用的 App 对象
finalApp = { finalApp = createFallbackApp(
name: ((app as Record<string, unknown>).name as string) || "", app as Record<string, unknown>,
pkgname: pkgname, pkgname,
version: ((app as Record<string, unknown>).version as string) || "", category,
filename: ((app as Record<string, unknown>).filename as string) || "", );
category: category,
torrent_address: "",
author: "",
contributor: "",
website: "",
update: "",
size: "",
more: ((app as Record<string, unknown>).more as string) || "",
tags: "",
img_urls: [],
icons: "",
origin:
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
currentStatus: "not-installed",
} as App;
} }
} else { } else {
// 非 HomeView 来源,使用原来的逻辑 // 非 HomeView 来源,使用原来的逻辑
@@ -1056,26 +1077,11 @@ const openDetail = async (app: App | OpenDetailInput) => {
finalApp = fullApp; finalApp = fullApp;
} else { } else {
// 构造一个最小可用的 App 对象 // 构造一个最小可用的 App 对象
finalApp = { finalApp = createFallbackApp(
name: ((app as Record<string, unknown>).name as string) || "", app as Record<string, unknown>,
pkgname: pkgname, pkgname,
version: ((app as Record<string, unknown>).version as string) || "", category,
filename: ((app as Record<string, unknown>).filename as string) || "", );
category: category,
torrent_address: "",
author: "",
contributor: "",
website: "",
update: "",
size: "",
more: ((app as Record<string, unknown>).more as string) || "",
tags: "",
img_urls: [],
icons: "",
origin:
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
currentStatus: "not-installed",
} as App;
} }
} }
@@ -1324,7 +1330,7 @@ const loadRanking = async () => {
const gen = ++rankingGeneration; const gen = ++rankingGeneration;
rankingLoading.value = true; rankingLoading.value = true;
const all = apps.value.slice(); const all = apps.value.slice();
const CONCURRENCY = 64; const CONCURRENCY = 15;
const results: App[] = []; const results: App[] = [];
for (let i = 0; i < all.length; i += CONCURRENCY) { for (let i = 0; i < all.length; i += CONCURRENCY) {
if (gen !== rankingGeneration) { if (gen !== rankingGeneration) {
@@ -1367,42 +1373,51 @@ const loadHome = async () => {
// 按名称去重,spark 优先:同名链接 spark 覆盖 apm // 按名称去重,spark 优先:同名链接 spark 覆盖 apm
const seenNames = new Set<string>(); const seenNames = new Set<string>();
for (const mode of modes) { // 并行请求各来源的 homelinks.json,缩短首页加载耗时
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`; const modeResults = await Promise.all(
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`; modes.map(async (mode) => {
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
// homelinks.json const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
try { try {
const res = await fetch(`${base}/homelinks.json`); const res = await fetch(`${base}/homelinks.json`);
if (res.ok) { if (res.ok) return { mode, raw: (await res.json()) as unknown };
const raw = await res.json(); } catch (e) {
// 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误) console.warn(`Failed to load ${mode} homelinks.json`, e);
const links = Array.isArray(raw) ? (raw.filter((x) => x && typeof x === "object") as Record<string, unknown>[]) : [];
for (const l of links) {
const name = (l.Name as string) || (l.name as string) || "";
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) || "";
if (!url) continue;
const icon = (l.Icon as string) || (l.icon as string) || "";
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,
origin: mode,
};
homeLinks.value.push(safeLink);
}
} }
} catch (e) { return { mode, raw: undefined };
console.warn(`Failed to load ${mode} homelinks.json`, e); }),
);
for (const { mode, raw } of modeResults) {
if (!raw) continue;
// 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误)
const links = Array.isArray(raw)
? (raw.filter((x) => x && typeof x === "object") as Record<
string,
unknown
>[])
: [];
for (const l of links) {
const name = (l.Name as string) || (l.name as string) || "";
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) || "";
if (!url) continue;
const icon = (l.Icon as string) || (l.icon as string) || "";
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,
origin: mode,
};
homeLinks.value.push(safeLink);
} }
} }
} catch (error: unknown) { } catch (error: unknown) {
@@ -1554,7 +1569,10 @@ const loadHomeListApps = async (entryId: string) => {
try { try {
const path = `/${finalArch}${jsonUrl}`; const path = `/${finalArch}${jsonUrl}`;
const rawApps = const rawApps =
(await fetchWithRetry<Record<string, string>[]>(path, rootAbortController.signal)) || []; (await fetchWithRetry<Record<string, string>[]>(
path,
rootAbortController.signal,
)) || [];
const apps = parseAppList(rawApps, mode); const apps = parseAppList(rawApps, mode);
for (const app of apps) { for (const app of apps) {
if (!app.pkgname || seenPkgnames.has(app.pkgname)) continue; if (!app.pkgname || seenPkgnames.has(app.pkgname)) continue;
@@ -2967,10 +2985,16 @@ const normalizeAppJson = (
size: appJson.Size, size: appJson.Size,
more: appJson.More, more: appJson.More,
tags: appJson.Tags, tags: appJson.Tags,
img_urls: img_urls: (() => {
typeof appJson.img_urls === "string" if (typeof appJson.img_urls === "string") {
? (JSON.parse(appJson.img_urls) as string[]) try {
: appJson.img_urls, return JSON.parse(appJson.img_urls) as string[];
} catch {
return [];
}
}
return (appJson.img_urls as string[]) || [];
})(),
icons: appJson.icons, icons: appJson.icons,
category: category, category: category,
origin: origin, origin: origin,
@@ -3144,7 +3168,10 @@ const loadApps = async (onFirstBatch?: () => void) => {
const path = `/${finalArch}/${category}/applist.json`; const path = `/${finalArch}/${category}/applist.json`;
logger.info(`加载分类: ${category} (来源: ${mode})`); logger.info(`加载分类: ${category} (来源: ${mode})`);
const categoryApps = await fetchWithRetry<AppJson[]>(path, rootAbortController.signal); const categoryApps = await fetchWithRetry<AppJson[]>(
path,
rootAbortController.signal,
);
const normalizedApps = (categoryApps || []).map((appJson) => const normalizedApps = (categoryApps || []).map((appJson) =>
normalizeAppJson(appJson, category, mode as "spark" | "apm"), normalizeAppJson(appJson, category, mode as "spark" | "apm"),
@@ -3209,15 +3236,135 @@ const handleWindowResize = () => {
}, 400); }, 400);
}; };
// —— 以下为可复用的事件/IPC 监听处理器(命名函数,便于 onUnmounted 统一移除)——
// 收集"等待 loading 完成后执行一次"的 watcher,便于卸载时统一停止,避免泄漏
const pendingWatchers: Array<() => void> = [];
const registerOnceWatcher = (stop: () => void): void => {
pendingWatchers.push(stop);
};
const handleHashChange = () => {
isSubmitterView.value = window.location.hash === "#submitter";
};
const handleKeydown = (e: KeyboardEvent) => {
if (showPreview.value) {
if (e.key === "Escape") closeScreenPreview();
if (e.key === "ArrowLeft") prevScreen();
if (e.key === "ArrowRight") nextScreen();
}
if (showModal.value && e.key === "Escape") {
closeDetail();
}
};
const handleDeepLinkUpdate = () => {
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
openUpdateModal();
stop();
const idx = pendingWatchers.indexOf(stop);
if (idx >= 0) pendingWatchers.splice(idx, 1);
}
});
registerOnceWatcher(stop);
} else {
openUpdateModal();
}
};
const handleDeepLinkInstalled = () => {
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
openInstalledModal();
stop();
const idx = pendingWatchers.indexOf(stop);
if (idx >= 0) pendingWatchers.splice(idx, 1);
}
});
registerOnceWatcher(stop);
} else {
openInstalledModal();
}
};
const handleTriggerApmInstallDialog = () => {
showApmInstallDialog.value = true;
};
const handleDeepLinkInstall = (_event: IpcRendererEvent, pkgname: string) => {
const tryOpen = () => {
const target = apps.value.find((a) => a.pkgname === pkgname);
if (target) {
openDetail(target);
} else {
logger.warn(`Deep link: app ${pkgname} not found`);
}
};
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
tryOpen();
stop();
const idx = pendingWatchers.indexOf(stop);
if (idx >= 0) pendingWatchers.splice(idx, 1);
}
});
registerOnceWatcher(stop);
} else {
tryOpen();
}
};
const handleDeepLinkSearch = (
_event: IpcRendererEvent,
data: { pkgname: string },
) => {
const tryOpen = () => {
currentView.value = "default";
activeTab.value = "all";
const target = apps.value.find((a) => a.pkgname === data.pkgname);
if (target) {
openDetail({ ...target, _fromDeepLink: true });
} else {
searchQuery.value = data.pkgname;
logger.warn(
`Deep link: app ${data.pkgname} not found, fallback to search`,
);
}
};
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
tryOpen();
stop();
const idx = pendingWatchers.indexOf(stop);
if (idx >= 0) pendingWatchers.splice(idx, 1);
}
});
registerOnceWatcher(stop);
} else {
tryOpen();
}
};
const handleRemoveComplete = (
_event: IpcRendererEvent,
payload: ChannelPayload,
) => {
const pkgname = currentApp.value?.pkgname;
if (payload.success && pkgname) {
removeDownloadItem(pkgname);
}
};
// 生命周期钩子 // 生命周期钩子
onMounted(async () => { onMounted(async () => {
initTheme(); initTheme();
updateCenterStore.bind(); updateCenterStore.bind();
const handleHashChange = () => {
isSubmitterView.value = window.location.hash === "#submitter";
};
handleHashChange(); handleHashChange();
window.addEventListener("hashchange", handleHashChange); window.addEventListener("hashchange", handleHashChange);
@@ -3277,121 +3424,23 @@ onMounted(async () => {
logger.info("所有应用数据加载完成"); logger.info("所有应用数据加载完成");
// 设置键盘导航 // 设置键盘导航
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", handleKeydown);
if (showPreview.value) {
if (e.key === "Escape") closeScreenPreview();
if (e.key === "ArrowLeft") prevScreen();
if (e.key === "ArrowRight") nextScreen();
}
if (showModal.value && e.key === "Escape") {
closeDetail();
}
});
// Deep link Handlers // Deep link Handlers
window.ipcRenderer.on("deep-link-update", () => { window.ipcRenderer.on("deep-link-update", handleDeepLinkUpdate);
if (loading.value) { window.ipcRenderer.on("deep-link-installed", handleDeepLinkInstalled);
const stop = watch(loading, (val) => {
if (!val) {
openUpdateModal();
stop();
}
});
} else {
openUpdateModal();
}
});
window.ipcRenderer.on("deep-link-installed", () => {
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
openInstalledModal();
stop();
}
});
} else {
openInstalledModal();
}
});
window.ipcRenderer.on("trigger-apm-install-dialog", () => {
showApmInstallDialog.value = true;
});
window.ipcRenderer.on( window.ipcRenderer.on(
"deep-link-install", "trigger-apm-install-dialog",
(_event: IpcRendererEvent, pkgname: string) => { handleTriggerApmInstallDialog,
const tryOpen = () => {
const target = apps.value.find((a) => a.pkgname === pkgname);
if (target) {
openDetail(target);
} else {
logger.warn(`Deep link: app ${pkgname} not found`);
}
};
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
tryOpen();
stop();
}
});
} else {
tryOpen();
}
},
);
window.ipcRenderer.on(
"deep-link-search",
(_event: IpcRendererEvent, data: { pkgname: string }) => {
// 根据包名直接打开应用详情
const tryOpen = () => {
// 先切换到"全部应用"分类
currentView.value = "default";
activeTab.value = "all";
// 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息
const target = apps.value.find((a) => a.pkgname === data.pkgname);
if (target) {
openDetail({ ...target, _fromDeepLink: true });
} else {
// 如果找不到应用,回退到搜索模式
searchQuery.value = data.pkgname;
logger.warn(
`Deep link: app ${data.pkgname} not found, fallback to search`,
);
}
};
if (loading.value) {
const stop = watch(loading, (val) => {
if (!val) {
tryOpen();
stop();
}
});
} else {
tryOpen();
}
},
); );
window.ipcRenderer.on("deep-link-install", handleDeepLinkInstall);
window.ipcRenderer.on("deep-link-search", handleDeepLinkSearch);
window.ipcRenderer.on( window.ipcRenderer.on(
"install-complete", "install-complete",
handleInstallCompleteForDownloadRecord, handleInstallCompleteForDownloadRecord,
); );
window.ipcRenderer.on("remove-complete", handleRemoveComplete);
window.ipcRenderer.on(
"remove-complete",
(_event: IpcRendererEvent, payload: ChannelPayload) => {
const pkgname = currentApp.value?.pkgname;
if (payload.success && pkgname) {
removeDownloadItem(pkgname);
}
},
);
window.ipcRenderer.send("renderer-ready", { status: true }); window.ipcRenderer.send("renderer-ready", { status: true });
logger.info("Renderer process is ready!"); logger.info("Renderer process is ready!");
@@ -3400,11 +3449,26 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
rootAbortController.abort(); rootAbortController.abort();
updateCenterStore.unbind(); updateCenterStore.unbind();
// 停止仍挂起的"等待 loading 完成后执行一次"的 watcher,避免泄漏
for (const stop of pendingWatchers.splice(0)) stop();
// 移除 window / document 监听
window.removeEventListener("hashchange", handleHashChange);
window.removeEventListener("resize", handleWindowResize);
document.removeEventListener("keydown", handleKeydown);
// 移除所有 IPC 监听
window.ipcRenderer.off("deep-link-update", handleDeepLinkUpdate);
window.ipcRenderer.off("deep-link-installed", handleDeepLinkInstalled);
window.ipcRenderer.off(
"trigger-apm-install-dialog",
handleTriggerApmInstallDialog,
);
window.ipcRenderer.off("deep-link-install", handleDeepLinkInstall);
window.ipcRenderer.off("deep-link-search", handleDeepLinkSearch);
window.ipcRenderer.off( window.ipcRenderer.off(
"install-complete", "install-complete",
handleInstallCompleteForDownloadRecord, handleInstallCompleteForDownloadRecord,
); );
window.removeEventListener("resize", handleWindowResize); window.ipcRenderer.off("remove-complete", handleRemoveComplete);
}); });
// 观察器 // 观察器
+2 -2
View File
@@ -226,7 +226,8 @@ export interface CategoryInfo {
zh: string; zh: string;
origins?: string[]; origins?: string[];
origin?: "spark" | "apm"; origin?: "spark" | "apm";
[k: string]: unknown; // 保留扩展点,避免使用宽泛的索引签名削弱类型安全
extra?: Record<string, unknown>;
} }
export interface HomeLink { export interface HomeLink {
@@ -238,7 +239,6 @@ export interface HomeLink {
imgUrl?: string; imgUrl?: string;
type?: string; type?: string;
origin?: "spark" | "apm"; origin?: "spark" | "apm";
[k: string]: unknown;
} }
export interface SidebarEntry { export interface SidebarEntry {
+3 -2
View File
@@ -24,13 +24,14 @@ export interface ContributorRank {
* 聚合与展示统一去除 `<...>` 取显示名。 * 聚合与展示统一去除 `<...>` 取显示名。
*/ */
export function parseContributors(raw: string | undefined): string[] { export function parseContributors(raw: string | undefined): string[] {
if (!raw) return []; if (!raw || typeof raw !== "string") return [];
const names: string[] = []; const names: string[] = [];
for (const part of raw.split(/[;]/)) { for (const part of raw.split(/[;]/)) {
const trimmed = part.trim(); const trimmed = part.trim();
if (!trimmed) continue; if (!trimmed) continue;
// 去除 <...> 包裹的邮箱,取显示名;长度 1~50 过滤空字符串/异常超长
const name = trimmed.replace(/<[^>]*>/g, "").trim(); const name = trimmed.replace(/<[^>]*>/g, "").trim();
if (name) names.push(name); if (name.length > 0 && name.length <= 50) names.push(name);
} }
return names; return names;
} }