From 10b50829ee8f2294c075044b973a21a8f7e13d05 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sun, 30 Aug 2026 18:46:50 +0800 Subject: [PATCH] feat: report Docker image source in system info --- apps/api/src/api-context.ts | 1 + apps/api/src/container-image-source.test.mjs | 18 +++++++++++++++ apps/api/src/container-image-source.ts | 23 +++++++++++++++++++ apps/api/src/index.ts | 8 ++++++- apps/ios/EdgeEver/Data/Models/Models.swift | 1 + .../Features/Workspace/SettingsView.swift | 15 ++++++++++++ .../src/screens/WorkspaceSettingsView.tsx | 14 +++++++++++ apps/site/public/compose.yaml | 1 + .../components/settings/SystemInfoPanel.tsx | 13 ++++++++++- compose.yaml | 1 + docs/openapi.json | 5 ++++ packages/client/src/index.ts | 1 + packages/shared/src/i18n/en-US.ts | 7 ++++++ packages/shared/src/i18n/zh-CN.ts | 7 ++++++ scripts/docker-release.test.mjs | 3 +++ scripts/self-hosted-server.mjs | 1 + 16 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/container-image-source.test.mjs create mode 100644 apps/api/src/container-image-source.ts diff --git a/apps/api/src/api-context.ts b/apps/api/src/api-context.ts index 4410f2ff..a6b50e41 100644 --- a/apps/api/src/api-context.ts +++ b/apps/api/src/api-context.ts @@ -7,6 +7,7 @@ export type Bindings = { storage: StorageAdapter; EDGE_EVER_AUTH_USERNAME?: string; EDGE_EVER_RUNTIME?: string; + EDGE_EVER_CONTAINER_IMAGE?: string; EDGE_EVER_AUTH_PASSWORD?: string; EDGE_EVER_AUTH_PASSWORD_HASH?: string; EDGE_EVER_SESSION_TTL_DAYS?: string; diff --git a/apps/api/src/container-image-source.test.mjs b/apps/api/src/container-image-source.test.mjs new file mode 100644 index 00000000..497b5d1e --- /dev/null +++ b/apps/api/src/container-image-source.test.mjs @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; +import { resolveContainerImageSource } from "./container-image-source"; + +describe("container image source diagnostics", () => { + test("recognizes official image repositories with tags or digests", () => { + expect(resolveContainerImageSource("ghcr.io/tianma-if/edgeever")).toBe("official-ghcr"); + expect(resolveContainerImageSource("ghcr.io/tianma-if/edgeever:v1.2.3")).toBe("official-ghcr"); + expect(resolveContainerImageSource("GHCR.IO/TIANMA-IF/EDGEEVER@sha256:abc")).toBe("official-ghcr"); + expect(resolveContainerImageSource("ccr.ccs.tencentyun.com/edgeever/edgeever:latest")).toBe("official-cn-mirror"); + }); + + test("does not expose custom image references through the classification", () => { + expect(resolveContainerImageSource("registry.internal.example/team/edgeever:latest")).toBe("custom"); + expect(resolveContainerImageSource("ghcr.io/tianma-if/edgeever-fork")).toBe("custom"); + expect(resolveContainerImageSource(" ")).toBe("unknown"); + expect(resolveContainerImageSource(undefined)).toBe("unknown"); + }); +}); diff --git a/apps/api/src/container-image-source.ts b/apps/api/src/container-image-source.ts new file mode 100644 index 00000000..10b5c2e5 --- /dev/null +++ b/apps/api/src/container-image-source.ts @@ -0,0 +1,23 @@ +export type ContainerImageSource = + | "official-ghcr" + | "official-cn-mirror" + | "custom" + | "unknown"; + +const OFFICIAL_GHCR_IMAGE = "ghcr.io/tianma-if/edgeever"; +const OFFICIAL_CN_MIRROR_IMAGE = "ccr.ccs.tencentyun.com/edgeever/edgeever"; + +const matchesImageRepository = (reference: string, repository: string) => + reference === repository + || reference.startsWith(`${repository}:`) + || reference.startsWith(`${repository}@`); + +export const resolveContainerImageSource = ( + imageReference: string | null | undefined, +): ContainerImageSource => { + const normalized = imageReference?.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/+$/, "") ?? ""; + if (!normalized) return "unknown"; + if (matchesImageRepository(normalized, OFFICIAL_GHCR_IMAGE)) return "official-ghcr"; + if (matchesImageRepository(normalized, OFFICIAL_CN_MIRROR_IMAGE)) return "official-cn-mirror"; + return "custom"; +}; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 1806f156..95f40920 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -6,6 +6,7 @@ import { } from "@edgeever/shared"; import { Hono } from "hono"; import { cors } from "hono/cors"; +import { resolveContainerImageSource } from "./container-image-source"; import openApiSpec from "../../../docs/openapi.json"; import releaseSummary from "../../../release-summary.json"; import { @@ -198,10 +199,15 @@ app.get("/api/health", async (c) => { ); } + const runtime = c.env.EDGE_EVER_RUNTIME ?? "cloudflare-workers"; + return c.json({ ok: true, name: "edgeever", - runtime: c.env.EDGE_EVER_RUNTIME ?? "cloudflare-workers", + runtime, + ...(runtime === "self-hosted-bun" + ? { containerImageSource: resolveContainerImageSource(c.env.EDGE_EVER_CONTAINER_IMAGE) } + : {}), authMode, build: INSTANCE_BUILD_ID.slice(0, 12), migration: await getAppliedMigration(c.env), diff --git a/apps/ios/EdgeEver/Data/Models/Models.swift b/apps/ios/EdgeEver/Data/Models/Models.swift index cdc5d000..bc905be3 100644 --- a/apps/ios/EdgeEver/Data/Models/Models.swift +++ b/apps/ios/EdgeEver/Data/Models/Models.swift @@ -38,6 +38,7 @@ struct InstanceHealth: Codable, Equatable, Sendable { var ok: Bool var name: String var runtime: String? + var containerImageSource: String? var authMode: String? var build: String? var migration: String? diff --git a/apps/ios/EdgeEver/Features/Workspace/SettingsView.swift b/apps/ios/EdgeEver/Features/Workspace/SettingsView.swift index e6ad3031..c49e38d0 100644 --- a/apps/ios/EdgeEver/Features/Workspace/SettingsView.swift +++ b/apps/ios/EdgeEver/Features/Workspace/SettingsView.swift @@ -552,6 +552,12 @@ struct SettingsView: View { env.preferences.t("部署平台", en: "Deployment platform"), deploymentPlatformLabel(instanceHealth?.runtime) )) + if instanceHealth?.runtime == "self-hosted-bun" { + items.append(( + env.preferences.t("容器镜像来源", en: "Container image source"), + containerImageSourceLabel(instanceHealth?.containerImageSource) + )) + } return items } @@ -606,6 +612,15 @@ struct SettingsView: View { } } + private func containerImageSourceLabel(_ source: String?) -> String { + switch source { + case "official-ghcr": return "GitHub Container Registry (GHCR)" + case "official-cn-mirror": return env.preferences.t("中国大陆官方镜像", en: "Official mainland China mirror") + case "custom": return env.preferences.t("自定义镜像", en: "Custom image") + default: return unknownSystemInfoValue + } + } + private var systemInfoText: String { [ systemInfoTextSection(env.preferences.t("云端实例", en: "Cloud instance"), items: cloudSystemInfoItems), diff --git a/apps/mobile/src/screens/WorkspaceSettingsView.tsx b/apps/mobile/src/screens/WorkspaceSettingsView.tsx index d591d59b..5556de31 100644 --- a/apps/mobile/src/screens/WorkspaceSettingsView.tsx +++ b/apps/mobile/src/screens/WorkspaceSettingsView.tsx @@ -488,6 +488,7 @@ const getMobileSystemInfoText = (localePreference: MobileLocaleMode) => connectionDescription: "Connection and local sync queue status.", connectionFailed: "Connection failed", connectionSection: "Connection and sync", + containerImageSource: "Container image source", databaseBackend: "Database backend", databaseVersion: "Database version", description: "View cloud instance and current client diagnostics.", @@ -527,6 +528,7 @@ const getMobileSystemInfoText = (localePreference: MobileLocaleMode) => connectionDescription: "实例连接与本地同步队列状态。", connectionFailed: "连接失败", connectionSection: "连接与同步", + containerImageSource: "容器镜像来源", databaseBackend: "数据库后端", databaseVersion: "数据库版本", description: "查看云端实例与当前客户端的诊断信息。", @@ -561,6 +563,15 @@ const getMobileDeploymentPlatform = (runtime: string | null | undefined, english return english ? "Unknown" : "未知"; }; +const getMobileContainerImageSource = (source: string | null | undefined, english: boolean) => { + switch (source) { + case "official-ghcr": return "GitHub Container Registry (GHCR)"; + case "official-cn-mirror": return english ? "Official mainland China mirror" : "中国大陆官方镜像"; + case "custom": return english ? "Custom image" : "自定义镜像"; + default: return english ? "Unknown" : "未知"; + } +}; + const getMobileDatabaseBackend = (backend: string | null | undefined, unknown: string) => { if (backend === "d1") return "D1"; if (backend === "sqlite") return "SQLite"; @@ -627,6 +638,9 @@ const getMobileSystemInfoGroups = ( ? [{ label: copy.existingAttachments, value: copy.existingAttachmentsOriginalStorage }] : []), { label: copy.deploymentPlatform, value: getMobileDeploymentPlatform(instance?.health.runtime, english) }, + ...(instance?.health.runtime === "self-hosted-bun" + ? [{ label: copy.containerImageSource, value: getMobileContainerImageSource(instance.health.containerImageSource, english) }] + : []), ], title: copy.cloudSection, }, diff --git a/apps/site/public/compose.yaml b/apps/site/public/compose.yaml index bdb417a9..830a136d 100644 --- a/apps/site/public/compose.yaml +++ b/apps/site/public/compose.yaml @@ -8,6 +8,7 @@ services: environment: EDGE_EVER_AUTH_USERNAME: "${EDGE_EVER_AUTH_USERNAME:-admin}" EDGE_EVER_AUTH_PASSWORD: "${EDGE_EVER_AUTH_PASSWORD:?Set EDGE_EVER_AUTH_PASSWORD before starting EdgeEver}" + EDGE_EVER_CONTAINER_IMAGE: "${EDGE_EVER_IMAGE:-ghcr.io/tianma-if/edgeever}" volumes: - edgeever-data:/data security_opt: diff --git a/apps/web/src/components/settings/SystemInfoPanel.tsx b/apps/web/src/components/settings/SystemInfoPanel.tsx index 80e8de82..78e6e53e 100644 --- a/apps/web/src/components/settings/SystemInfoPanel.tsx +++ b/apps/web/src/components/settings/SystemInfoPanel.tsx @@ -25,7 +25,7 @@ export type SystemInfoItem = { status?: "connected" | "connecting" | "failed" | "warning" | "error" | "default"; }; -type InstanceSystemDiagnostics = Pick & { +type InstanceSystemDiagnostics = Pick & { runtime?: string | null; }; @@ -62,6 +62,11 @@ const getDeploymentDescription = (t: (key: string) => string) => { return `${trigger} · ${method}`; }; +const getContainerImageSourceTranslationKey = (source: string | null | undefined) => { + if (source === "official-ghcr" || source === "official-cn-mirror" || source === "custom") return source; + return "unknown"; +}; + const getColSpanClass = (colSpan?: SystemInfoItem["colSpan"]) => { if (colSpan === "full") return "sm:col-span-2 lg:col-span-3"; if (colSpan === "two") return "sm:col-span-2 lg:col-span-2"; @@ -129,6 +134,12 @@ const getWebSystemInfoGroups = ( label: t("systemInfo.deploymentPlatform"), value: t(`systemInfo.deploymentPlatforms.${resolveDeploymentPlatform(diagnostics.instance?.runtime)}`), }, + ...(diagnostics.instance?.runtime === "self-hosted-bun" + ? [{ + label: t("systemInfo.containerImageSource"), + value: t(`systemInfo.containerImageSources.${getContainerImageSourceTranslationKey(diagnostics.instance.containerImageSource)}`), + }] + : []), { label: t("systemInfo.deployment"), value: getDeploymentDescription(t), colSpan: "full" }, ], }, diff --git a/compose.yaml b/compose.yaml index bdb417a9..830a136d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,6 +8,7 @@ services: environment: EDGE_EVER_AUTH_USERNAME: "${EDGE_EVER_AUTH_USERNAME:-admin}" EDGE_EVER_AUTH_PASSWORD: "${EDGE_EVER_AUTH_PASSWORD:?Set EDGE_EVER_AUTH_PASSWORD before starting EdgeEver}" + EDGE_EVER_CONTAINER_IMAGE: "${EDGE_EVER_IMAGE:-ghcr.io/tianma-if/edgeever}" volumes: - edgeever-data:/data security_opt: diff --git a/docs/openapi.json b/docs/openapi.json index 3889a42b..d509e106 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -40,6 +40,11 @@ "runtime": { "type": "string" }, + "containerImageSource": { + "type": "string", + "enum": ["official-ghcr", "official-cn-mirror", "custom", "unknown"], + "description": "Current container image repository classification for Docker deployments. Custom repository addresses are not exposed." + }, "authMode": { "type": "string", "enum": ["required", "disabled"] diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index baaa33a9..77bd0fdc 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -57,6 +57,7 @@ export type InstanceHealth = { ok: true; name: string; runtime?: string | null; + containerImageSource?: "official-ghcr" | "official-cn-mirror" | "custom" | "unknown" | string | null; authMode?: string | null; build?: string | null; migration?: string | null; diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index d50e9938..7be428f7 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -668,6 +668,13 @@ export const enUS = { docker: "Docker", unknown: "Unknown", }, + containerImageSource: "Container image source", + containerImageSources: { + "official-ghcr": "GitHub Container Registry (GHCR)", + "official-cn-mirror": "Official mainland China mirror", + custom: "Custom image", + unknown: "Unknown", + }, deployment: "Deployment", deploymentTriggers: { github_release: "GitHub Release trigger", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index e755a357..890e1235 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -668,6 +668,13 @@ export const zhCN = { docker: "Docker", unknown: "未知", }, + containerImageSource: "容器镜像来源", + containerImageSources: { + "official-ghcr": "GitHub Container Registry(GHCR)", + "official-cn-mirror": "中国大陆官方镜像", + custom: "自定义镜像", + unknown: "未知", + }, deployment: "自动部署方式", deploymentTriggers: { github_release: "正式 Release 触发", diff --git a/scripts/docker-release.test.mjs b/scripts/docker-release.test.mjs index e53b2790..b89cc940 100644 --- a/scripts/docker-release.test.mjs +++ b/scripts/docker-release.test.mjs @@ -80,6 +80,9 @@ describe("Docker release contract", () => { expect(compose).toContain( 'EDGE_EVER_AUTH_PASSWORD: "${EDGE_EVER_AUTH_PASSWORD:?', ); + expect(compose).toContain( + 'EDGE_EVER_CONTAINER_IMAGE: "${EDGE_EVER_IMAGE:-ghcr.io/tianma-if/edgeever}"', + ); expect(compose).toContain("edgeever-data:/data"); expect(compose).toContain("no-new-privileges:true"); }); diff --git a/scripts/self-hosted-server.mjs b/scripts/self-hosted-server.mjs index 860d7ed9..6689cb83 100644 --- a/scripts/self-hosted-server.mjs +++ b/scripts/self-hosted-server.mjs @@ -77,6 +77,7 @@ const env = { storage, EDGE_EVER_AUTH_USERNAME: runtimeEnvironment.EDGE_EVER_AUTH_USERNAME ?? "admin", EDGE_EVER_RUNTIME: "self-hosted-bun", + EDGE_EVER_CONTAINER_IMAGE: runtimeEnvironment.EDGE_EVER_CONTAINER_IMAGE, EDGE_EVER_AUTH_PASSWORD: runtimeEnvironment.EDGE_EVER_AUTH_PASSWORD, EDGE_EVER_AUTH_PASSWORD_HASH: runtimeEnvironment.EDGE_EVER_AUTH_PASSWORD_HASH, EDGE_EVER_SESSION_TTL_DAYS: runtimeEnvironment.EDGE_EVER_SESSION_TTL_DAYS ?? "400",