diff --git a/.vscode/launch.json b/.vscode/launch.json index c51fa034..a02acbfe 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,7 +30,6 @@ // }, "runtimeArgs": [ "--remote-debugging-port=9229", - "--no-spark", "." ], "envFile": "${workspaceFolder}/.vscode/.debug.env", diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index ecfdf8b5..0feb54f2 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -251,7 +251,8 @@ ipcMain.on("queue-install", async (event, download_json) => { type: "info", title: "APM 安装成功", message: "恭喜您,APM 已成功安装", - detail: "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。", + detail: + "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。", buttons: ["确定"], defaultId: 0, }); @@ -670,7 +671,6 @@ ipcMain.handle("list-upgradable", async () => { return { success: true, apps }; }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any ipcMain.handle("uninstall-installed", async (_event, payload: any) => { const pkgname = typeof payload === "string" ? payload : payload.pkgname; diff --git a/electron/main/backend/telemetry.ts b/electron/main/backend/telemetry.ts new file mode 100644 index 00000000..587e6f9e --- /dev/null +++ b/electron/main/backend/telemetry.ts @@ -0,0 +1,139 @@ +/** + * 启动时遥测:收集系统与商店版本信息并上报至 status.deepinos.org.cn + * 仅在 Linux 下执行一次,不阻塞启动,失败静默记录日志。 + */ +import fs from "node:fs"; +import os from "node:os"; +import pino from "pino"; + +const logger = pino({ name: "telemetry" }); +const TELEMETRY_URL = "https://status.spark-app.store/upload"; + +interface TelemetryPayload { + "Distributor ID": string; + Release: string; + Architecture: string; + Store_Version: string; + UUID: string; + TIME: string; +} + +function readFileSafe(path: string): string { + try { + return fs.readFileSync(path, "utf8").trim(); + } catch { + return ""; + } +} + +/** 解析 /etc/os-release 的 KEY="value" 行 */ +function parseOsRelease(content: string): Record { + const out: Record = {}; + for (const line of content.split("\n")) { + const m = line.match(/^([A-Z_][A-Z0-9_]*)=(?:")?([^"]*)(?:")?$/); + if (m) out[m[1]] = m[2].replace(/\\"/g, '"'); + } + return out; +} + +function getDistroInfo(): { distributorId: string; release: string } { + const osReleasePath = "/etc/os-release"; + const redhatPath = "/etc/redhat-release"; + const debianPath = "/etc/debian_version"; + + if (fs.existsSync(osReleasePath)) { + const content = readFileSafe(osReleasePath); + const parsed = parseOsRelease(content); + const name = parsed.NAME ?? "Unknown"; + const versionId = parsed.VERSION_ID ?? "Unknown"; + return { distributorId: name, release: versionId }; + } + + if (fs.existsSync(redhatPath)) { + const content = readFileSafe(redhatPath); + const distributorId = content.split(/\s+/)[0] ?? "Unknown"; + const releaseMatch = content.match(/release\s+([0-9][0-9.]*)/i); + const release = releaseMatch ? releaseMatch[1] : "Unknown"; + return { distributorId, release }; + } + + if (fs.existsSync(debianPath)) { + const release = readFileSafe(debianPath) || "Unknown"; + return { distributorId: "Debian", release }; + } + + return { distributorId: "Unknown", release: "Unknown" }; +} + +function getUuid(): string { + const content = readFileSafe("/etc/machine-id"); + return content || "unknown"; +} + +/** 架构:与 uname -m 一致,使用 Node 的 os.machine() */ +function getArchitecture(): string { + if (typeof os.machine === "function") { + return os.machine(); + } + const arch = process.arch; + if (arch === "x64") return "x86_64"; + if (arch === "arm64") return "aarch64"; + return arch; +} + +function buildPayload(storeVersion: string): TelemetryPayload { + const { distributorId, release } = getDistroInfo(); + const time = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); + + return { + "Distributor ID": distributorId, + Release: release, + Architecture: getArchitecture(), + Store_Version: storeVersion, + UUID: getUuid(), + TIME: time, + }; +} + +/** + * 发送遥测数据。仅在 Linux 下执行;非 Linux 直接返回。 + * 不抛出异常,错误仅写日志。 + */ +export function sendTelemetryOnce(storeVersion: string): void { + if (process.platform !== "linux") { + logger.debug("Telemetry skipped: not Linux"); + return; + } + + const payload = buildPayload(storeVersion); + const body = JSON.stringify(payload); + + fetch(TELEMETRY_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }) + .then((res) => { + const code = res.status; + if (code === 200) { + logger.debug("Telemetry sent successfully"); + return; + } + if (code === 400) { + logger.warn("Telemetry: 客户端请求错误,请检查 JSON 或接口逻辑"); + return; + } + if (code === 422) { + logger.warn("Telemetry: 请求数据无效,请检查字段值"); + return; + } + if (code === 500) { + logger.warn("Telemetry: 服务器内部错误"); + return; + } + logger.warn(`Telemetry: 未处理的响应码 ${code}`); + }) + .catch((err) => { + logger.warn({ err }, "Telemetry request failed"); + }); +} diff --git a/electron/main/index.ts b/electron/main/index.ts index a6bced11..5de9aab7 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -17,6 +17,7 @@ import pino from "pino"; import { handleCommandLine } from "./deeplink.js"; import { isLoaded } from "../global.js"; import { tasks } from "./backend/install-manager.js"; +import { sendTelemetryOnce } from "./backend/telemetry.js"; // Assure single instance application if (!app.requestSingleInstanceLock()) { @@ -64,13 +65,21 @@ let win: BrowserWindow | null = null; const preload = path.join(__dirname, "../preload/index.mjs"); const indexHtml = path.join(RENDERER_DIST, "index.html"); -// Use app.getVersion() when the app is packaged. +/** 与项目 package.json 一致的版本号:打包用 app.getVersion(),未打包时读 package.json */ +function getAppVersion(): string { + if (app.isPackaged) return app.getVersion(); + const pkgPath = path.join(process.env.APP_ROOT ?? __dirname, "package.json"); + try { + const raw = fs.readFileSync(pkgPath, "utf8"); + const pkg = JSON.parse(raw) as { version?: string }; + return typeof pkg.version === "string" ? pkg.version : "dev"; + } catch { + return "dev"; + } +} + const getUserAgent = (): string => { - const version = - app && app.isPackaged - ? app.getVersion() - : process.env.npm_package_version || "dev"; - return `Spark-Store/${version}`; + return `Spark-Store/${getAppVersion()}`; }; logger.info("User Agent: " + getUserAgent()); @@ -86,9 +95,8 @@ function getStoreFilterFromArgv(): "spark" | "apm" | "both" { return "both"; } -ipcMain.handle( - "get-store-filter", - (): "spark" | "apm" | "both" => getStoreFilterFromArgv(), +ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" => + getStoreFilterFromArgv(), ); async function createWindow() { @@ -204,6 +212,8 @@ app.whenReady().then(() => { }); createWindow(); handleCommandLine(process.argv); + // 启动后执行一次遥测(仅 Linux,不阻塞) + sendTelemetryOnce(getAppVersion()); }); app.on("window-all-closed", () => { @@ -247,9 +257,7 @@ function resolveIconPath(filename: string): string { /** 按优先级返回托盘图标路径:spark-store(.png|.ico) → amber-pm-logo.png。托盘不支持 SVG,故不尝试 spark-store.svg */ function getTrayIconPath(): string | null { const ext = process.platform === "win32" ? ".ico" : ".png"; - const candidates = [ - `spark-store${ext}` - ]; + const candidates = [`spark-store${ext}`]; for (const name of candidates) { const iconPath = resolveIconPath(name); if (fs.existsSync(iconPath)) { @@ -265,7 +273,9 @@ function getTrayIconPath(): string | null { const FALLBACK_TRAY_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg=="; -function getTrayImage(): string | ReturnType { +function getTrayImage(): + | string + | ReturnType { const iconPath = getTrayIconPath(); if (iconPath) return iconPath; return nativeImage.createFromDataURL(FALLBACK_TRAY_PNG); diff --git a/index.html b/index.html index 94d80841..a1631fa7 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,9 @@ + + + 星火应用商店 diff --git a/package.json b/package.json index 04b07729..81ed77d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spark-store", - "version": "4.9.9", + "version": "4.9.9alpha3", "main": "dist-electron/main/index.js", "description": "Client for Spark App Store", "author": "elysia-best ", diff --git a/src/App.vue b/src/App.vue index 0555d3bd..a52dfb82 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,16 +1,21 @@