diff --git a/scripts/windows/Install-Moonsea-Windows.ps1 b/scripts/windows/Install-Moonsea-Windows.ps1 index 3e5fae4..5b367c4 100644 --- a/scripts/windows/Install-Moonsea-Windows.ps1 +++ b/scripts/windows/Install-Moonsea-Windows.ps1 @@ -62,6 +62,8 @@ $draftSourcePath = Join-Path $projectRoot "assets\admin-drafts" $managerExtension = [System.IO.Path]::GetExtension($ManagerPath) $managerFileName = if ($managerExtension -eq ".mjs") { "MoonseaManager.mjs" } else { "MoonseaManager.exe" } $managerPidPath = Join-Path $InstallRoot "manager.pid" +$adminMarkerPath = Join-Path $InstallRoot "admin-access.enabled" +$legacyAdminMarkerPath = Join-Path (Join-Path $env:LOCALAPPDATA "MoonseaCodex") "admin-access.enabled" $packageMetadataPath = Join-Path $projectRoot "package.json" $updaterSourcePath = Join-Path $scriptRoot "Update-Moonsea-Windows.ps1" @@ -192,6 +194,11 @@ New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null New-Item -ItemType Directory -Path $buildsRoot -Force | Out-Null New-Item -ItemType Directory -Path $profilePath -Force | Out-Null New-Item -ItemType Directory -Path $releasesRoot -Force | Out-Null +if (-not (Test-Path -LiteralPath $adminMarkerPath -PathType Leaf) -and + -not $adminMarkerPath.Equals($legacyAdminMarkerPath, [System.StringComparison]::OrdinalIgnoreCase) -and + (Test-Path -LiteralPath $legacyAdminMarkerPath -PathType Leaf)) { + Copy-Item -LiteralPath $legacyAdminMarkerPath -Destination $adminMarkerPath +} $needsBuild = $true if (Test-Path -LiteralPath $activeBuild -PathType Container) { diff --git a/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 b/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 index 4a02fe9..054c3eb 100644 --- a/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 +++ b/scripts/windows/Install-Moonsea-WorkBuddy-Windows.ps1 @@ -62,6 +62,11 @@ $draftSourcePath = Join-Path $projectRoot "assets\admin-drafts" $managerExtension = [System.IO.Path]::GetExtension($ManagerPath) $managerFileName = if ($managerExtension -eq ".mjs") { "MoonseaWorkBuddyManager.mjs" } else { "MoonseaWorkBuddyManager.exe" } $managerPidPath = Join-Path $InstallRoot "manager.pid" +$adminMarkerPath = Join-Path $InstallRoot "admin-access.enabled" +$legacyAdminMarkerPaths = @( + (Join-Path (Join-Path $env:LOCALAPPDATA "MoonseaWorkBuddy") "admin-access.enabled"), + (Join-Path (Join-Path $env:LOCALAPPDATA "MoonseaCodex") "admin-access.enabled") +) $packageMetadataPath = Join-Path $projectRoot "package.json" $updaterSourcePath = Join-Path $scriptRoot "Update-Moonsea-WorkBuddy-Windows.ps1" @@ -227,6 +232,15 @@ New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null New-Item -ItemType Directory -Path $buildsRoot -Force | Out-Null New-Item -ItemType Directory -Path $profilePath -Force | Out-Null New-Item -ItemType Directory -Path $releasesRoot -Force | Out-Null +if (-not (Test-Path -LiteralPath $adminMarkerPath -PathType Leaf)) { + $legacyAdminMarkerPath = $legacyAdminMarkerPaths | Where-Object { + -not $adminMarkerPath.Equals($_, [System.StringComparison]::OrdinalIgnoreCase) -and + (Test-Path -LiteralPath $_ -PathType Leaf) + } | Select-Object -First 1 + if ($legacyAdminMarkerPath) { + Copy-Item -LiteralPath $legacyAdminMarkerPath -Destination $adminMarkerPath + } +} $needsBuild = $true if (Test-Path -LiteralPath $activeBuild -PathType Container) { diff --git a/src/manager-core.mjs b/src/manager-core.mjs index 4559858..14df76b 100644 --- a/src/manager-core.mjs +++ b/src/manager-core.mjs @@ -375,7 +375,7 @@ export function createRequestHandler({ sendJson(response, 200, { ok: true, appVersion, - adminAccess, + adminAccess: typeof adminAccess === "function" ? adminAccess() : adminAccess, catalogVersion: 3, themeDeliveryVersion, ...(await status(profilePath)), diff --git a/src/manager.mjs b/src/manager.mjs index 6a9a293..41fe973 100644 --- a/src/manager.mjs +++ b/src/manager.mjs @@ -57,7 +57,7 @@ if (appPidArgument !== null && (!Number.isInteger(appPid) || appPid < 1)) { } const projectRoot = findProjectRoot(); const pidPath = path.join(installRoot, "manager.pid"); -const adminAccess = fs.existsSync(path.join(installRoot, "admin-access.enabled")); +const hasAdminAccess = () => fs.existsSync(path.join(installRoot, "admin-access.enabled")); const updaterPath = process.platform === "win32" ? path.join( projectRoot, @@ -215,7 +215,7 @@ const server = http.createServer(createRequestHandler({ adminRoot: path.join(projectRoot, "admin"), draftRoot: path.join(projectRoot, "assets", "admin-drafts"), appVersion: APP_VERSION, - adminAccess, + adminAccess: hasAdminAccess, updateService, resolveTheme: (themeId) => themeDeliveryService.resolve(themeId), })); diff --git a/tests/manager.test.mjs b/tests/manager.test.mjs index 42f893d..386bd0b 100644 --- a/tests/manager.test.mjs +++ b/tests/manager.test.mjs @@ -142,6 +142,19 @@ test("管理员入口仅在本机授权标记存在时向官网公开", async () const owner = await requestLocalPage(ownerHandler, "/api/status", PUBLIC_SITE_ORIGIN); assert.equal(owner.statusCode, 200); assert.equal(JSON.parse(owner.body).adminAccess, true); + + let dynamicAccess = false; + const dynamicHandler = createRequestHandler({ + profilePath: "fixture", + siteRoot: path.join(projectRoot, "site"), + adminAccess: () => dynamicAccess, + status: async () => ({ connected: false, message: "fixture" }), + }); + const beforeMarker = await requestLocalPage(dynamicHandler, "/api/status", PUBLIC_SITE_ORIGIN); + assert.equal(JSON.parse(beforeMarker.body).adminAccess, false); + dynamicAccess = true; + const afterMarker = await requestLocalPage(dynamicHandler, "/api/status", PUBLIC_SITE_ORIGIN); + assert.equal(JSON.parse(afterMarker.body).adminAccess, true); }); test("主题创作台只由本机助手提供且实验壁纸不进入公开目录", async () => { @@ -625,6 +638,21 @@ test("Windows 发布脚本兼容非 UTF-8 系统区域的 PowerShell 5.1", () => assert.match(installer, /\$output \| ForEach-Object \{ Write-Host \$_ \}/); }); +test("Windows 自定义安装目录会迁移已有的管理员入口标记", () => { + const scriptsRoot = path.join(projectRoot, "scripts", "windows"); + const codexInstaller = fs.readFileSync( + path.join(scriptsRoot, "Install-Moonsea-Windows.ps1"), + "ascii", + ); + const workBuddyInstaller = fs.readFileSync( + path.join(scriptsRoot, "Install-Moonsea-WorkBuddy-Windows.ps1"), + "ascii", + ); + + assert.match(codexInstaller, /\$legacyAdminMarkerPath[\s\S]*Copy-Item -LiteralPath \$legacyAdminMarkerPath -Destination \$adminMarkerPath/); + assert.match(workBuddyInstaller, /\$legacyAdminMarkerPaths[\s\S]*"MoonseaCodex"[\s\S]*Copy-Item -LiteralPath \$legacyAdminMarkerPath -Destination \$adminMarkerPath/); +}); + test("WorkBuddy Windows 链路使用独立客户端协议与安装器", () => { const scriptsRoot = path.join(projectRoot, "scripts", "windows"); const installer = fs.readFileSync( diff --git a/web/app/globals.css b/web/app/globals.css index 64c8b24..56dd885 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -53,6 +53,8 @@ main, footer { width: min(var(--site-width), calc(100% - 40px)); margin-inline: .eyebrow { margin: 0 0 20px; color: #8fc8cf; font-size: 11px; font-weight: 780; letter-spacing: .16em; } .section-kicker { margin: 0 0 14px; color: #8fc8cf; font-size: 14px; font-weight: 720; letter-spacing: .02em; } +.landing-shell { --line: rgba(167, 204, 202, .18); --muted: #8ea7a9; min-height: 100vh; color: #edf5f0; background: #061823; } +.landing-shell footer { color: #8ea7a9; } .landing-main { position: relative; width: 100%; min-height: calc(100dvh - 72px); overflow: clip; isolation: isolate; background: #061823; } .landing-main > section { position: relative; z-index: 1; } .moonsea-backdrop { position: fixed; z-index: 0; inset: 72px 0 0; overflow: hidden; pointer-events: none; background: @@ -310,6 +312,38 @@ a.footer-owner-entry:hover, a.footer-owner-entry:focus-visible { color: #dce9e5; .themes-shell .theme-card__footer button:disabled { color: #71898b; background: rgba(5, 27, 37, .64); } .themes-shell footer { color: #8ea7a9; } +.updates-page { width: min(1050px, calc(100% - 40px)); padding: clamp(86px, 9vw, 132px) 0 132px; } +.updates-hero { display: grid; max-width: 780px; margin: 0 0 clamp(92px, 11vw, 148px); } +.updates-hero .eyebrow { margin-bottom: 25px; color: #82bfc2; } +.updates-hero h1 { margin: 0; color: #edf5f0; font-size: clamp(62px, 8vw, 108px); font-weight: 710; line-height: .88; letter-spacing: -.075em; text-wrap: balance; } +.updates-hero h1 span { display: block; color: #b9cfcb; } +.updates-hero > p:last-child { max-width: 580px; margin: 34px 0 0; color: #9eb6b6; font-size: clamp(16px, 1.6vw, 19px); line-height: 1.85; } +.updates-timeline { position: relative; display: grid; gap: 0; padding: 0; margin: 0; list-style: none; } +.updates-timeline::before { position: absolute; z-index: 0; top: 11px; bottom: 0; left: 171px; width: 1px; content: ""; background: linear-gradient(180deg, rgba(119, 199, 198, .72), rgba(117, 176, 178, .22) 78%, transparent); } +.update-entry { position: relative; display: grid; grid-template-columns: 140px 62px minmax(0, 1fr); align-items: start; } +.update-entry + .update-entry { padding-top: clamp(86px, 10vw, 132px); } +.update-entry__meta { display: grid; justify-items: end; gap: 8px; padding-top: 5px; color: #86a4a5; text-align: right; } +.update-entry__meta time { color: #c6d7d2; font-size: 13px; font-weight: 710; letter-spacing: .02em; } +.update-entry__meta span { font-size: 10px; font-weight: 740; letter-spacing: .12em; } +.update-entry__rail { position: relative; z-index: 1; display: grid; min-height: 28px; place-items: start center; padding-top: 1px; } +.update-entry__node { position: relative; display: block; width: 22px; height: 22px; border: 1px solid rgba(140, 207, 203, .60); border-radius: 50%; background: #082833; box-shadow: 0 0 0 6px rgba(6, 31, 42, .92), 0 0 26px rgba(90, 187, 187, .18); } +.update-entry__node::before { position: absolute; inset: 6px; border-radius: 50%; content: ""; background: #9fd3cc; box-shadow: 0 0 11px rgba(146, 217, 209, .68); } +.update-entry__node::after { position: absolute; top: 10px; left: 20px; width: 29px; height: 1px; content: ""; background: linear-gradient(90deg, rgba(144, 207, 203, .66), transparent); } +.update-entry__content { min-width: 0; padding-top: 1px; } +.update-entry__version { display: flex; min-height: 24px; align-items: center; gap: 9px; color: #8fc2c1; font-size: 12px; font-weight: 760; letter-spacing: .07em; } +.update-entry__version em { padding: 4px 7px; border: 1px solid rgba(245, 223, 157, .38); border-radius: 999px; color: #f0dda4; background: rgba(180, 145, 59, .10); font-size: 9px; font-style: normal; letter-spacing: .1em; } +.update-entry__content h2 { max-width: 760px; margin: 18px 0 0; color: #edf5f0; font-size: clamp(34px, 4.3vw, 58px); font-weight: 690; line-height: 1.04; letter-spacing: -.052em; text-wrap: balance; } +.update-entry__summary { max-width: 700px; margin: 23px 0 0; color: #aec3c1; font-size: clamp(16px, 1.5vw, 18px); line-height: 1.8; } +.update-entry__media { display: grid; gap: 16px; margin-top: 35px; } +.update-entry__media img { display: block; width: 100%; height: auto; border: 1px solid rgba(174, 214, 210, .22); border-radius: 15px; background: #03151f; box-shadow: 0 30px 78px rgba(0, 8, 14, .42); } +.update-entry__content ul { display: grid; gap: 12px; max-width: 690px; padding: 0; margin: 29px 0 0; color: #98b0b0; list-style: none; } +.update-entry__content li { position: relative; padding-left: 18px; font-size: 14px; line-height: 1.72; } +.update-entry__content li::before { position: absolute; top: .7em; left: 0; width: 7px; height: 1px; content: ""; background: #70aaa9; } +.update-entry__release { display: inline-flex; margin-top: 30px; align-items: center; gap: 7px; color: #c9dbd6; font-size: 13px; font-weight: 700; text-decoration-color: rgba(164, 207, 204, .38); text-underline-offset: 6px; } +.update-entry__release:hover { color: #f0f5ef; text-decoration-color: currentColor; } +.update-entry__release span { font-size: 15px; transition: transform .18s ease; } +.update-entry__release:hover span { transform: translate(2px, -2px); } + .theme-detail { display: grid; min-height: calc(100vh - 178px); align-items: center; grid-template-columns: minmax(0, 1.18fr) minmax(340px, .82fr); gap: clamp(44px, 7vw, 104px); padding-block: clamp(72px, 9vw, 128px); } .theme-detail__preview { aspect-ratio: 16 / 11; padding: clamp(24px, 4vw, 52px); overflow: hidden; border: 1px solid rgba(163, 204, 203, .22); border-radius: 16px; box-shadow: 0 34px 90px rgba(0, 8, 15, .44); } .theme-detail__preview.is-pro { padding: clamp(18px, 2.5vw, 34px); } @@ -394,6 +428,9 @@ a.footer-owner-entry:hover, a.footer-owner-entry:focus-visible { color: #dce9e5; .theme-gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); } .metric-grid--usage { grid-template-columns: repeat(3, 1fr); } .data-grid--three { grid-template-columns: 1fr 1fr; } + .updates-page { width: min(920px, calc(100% - 40px)); } + .updates-timeline::before { left: 151px; } + .update-entry { grid-template-columns: 120px 62px minmax(0, 1fr); } } @media (max-width: 680px) { @@ -446,6 +483,24 @@ a.footer-owner-entry:hover, a.footer-owner-entry:focus-visible { color: #dce9e5; .theme-preview-dialog__window.pro-codex-window .pro-codex-composer { min-height: 40px; } .theme-preview-dialog__footer { align-items: stretch; flex-direction: column; gap: 12px; padding: 14px 16px 16px; } .theme-preview-dialog__footer > div { display: grid; grid-template-columns: 1fr 1fr; } + .updates-page { width: min(100% - 28px, var(--site-width)); padding: 64px 0 92px; } + .updates-hero { margin-bottom: 82px; } + .updates-hero h1 { font-size: clamp(54px, 17vw, 78px); } + .updates-hero > p:last-child { margin-top: 26px; font-size: 15px; } + .updates-timeline::before { left: 11px; } + .update-entry { grid-template-columns: 34px minmax(0, 1fr); } + .update-entry + .update-entry { padding-top: 84px; } + .update-entry__meta { grid-column: 2; grid-row: 1; display: flex; justify-items: start; gap: 10px; padding: 0 0 12px; text-align: left; } + .update-entry__meta time, .update-entry__meta span { font-size: 10px; } + .update-entry__meta span::before { margin-right: 10px; content: "·"; } + .update-entry__rail { grid-column: 1; grid-row: 1 / span 2; justify-items: start; padding-top: 0; } + .update-entry__node { width: 22px; height: 22px; } + .update-entry__node::after { width: 17px; } + .update-entry__content { grid-column: 2; grid-row: 2; } + .update-entry__content h2 { margin-top: 14px; font-size: clamp(32px, 10vw, 46px); } + .update-entry__summary { margin-top: 19px; font-size: 15px; } + .update-entry__media { margin-top: 28px; } + .update-entry__media img { border-radius: 11px; } footer { align-items: flex-start; flex-direction: column; justify-content: center; gap: 12px; padding-block: 26px; } .footer-links { flex-wrap: wrap; gap: 12px 18px; } .activity-chart { gap: 4px; } diff --git a/web/app/page.tsx b/web/app/page.tsx index e95da6d..07cc3c1 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; import { THEMES } from "../lib/theme-catalog"; import { FeaturedThemeSwitcher } from "./featured-theme-switcher"; import { MoonseaRipple } from "./moonsea-ripple"; -import { SiteHeader } from "./site-chrome"; +import { SiteFooter, SiteHeader } from "./site-chrome"; import { ThemeBrowserLink } from "./theme-browser-link"; export const dynamic = "force-static"; @@ -23,7 +23,7 @@ export default function Home() { const featuredThemes = [...latestWallpapers, ...latestGradients].slice(0, 6); return ( - <> +
@@ -51,6 +51,7 @@ export default function Home() {
- + + ); } diff --git a/web/app/robots.ts b/web/app/robots.ts index feeffbe..989f89a 100644 --- a/web/app/robots.ts +++ b/web/app/robots.ts @@ -5,7 +5,7 @@ export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "*", - allow: ["/", "/themes"], + allow: ["/", "/themes", "/updates"], disallow: ["/admin", "/api", "/download"], }, sitemap: new URL("/sitemap.xml", SITE_URL).toString(), diff --git a/web/app/site-chrome.tsx b/web/app/site-chrome.tsx index 904d456..f472cd9 100644 --- a/web/app/site-chrome.tsx +++ b/web/app/site-chrome.tsx @@ -65,6 +65,7 @@ export function SiteFooter() {

主题 + 更新日志 隐私说明 使用帮助
diff --git a/web/app/sitemap.ts b/web/app/sitemap.ts index 9a8abd0..b167014 100644 --- a/web/app/sitemap.ts +++ b/web/app/sitemap.ts @@ -14,6 +14,11 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: "weekly", priority: 0.9, }, + { + url: new URL("/updates", SITE_URL).toString(), + changeFrequency: "weekly", + priority: 0.8, + }, ...THEMES.map((theme) => ({ url: new URL(`/themes/${theme.id}`, SITE_URL).toString(), changeFrequency: "weekly" as const, diff --git a/web/app/updates/page.tsx b/web/app/updates/page.tsx new file mode 100644 index 0000000..e7baee2 --- /dev/null +++ b/web/app/updates/page.tsx @@ -0,0 +1,98 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import { SITE_UPDATES } from "../../lib/site-updates"; +import { SiteFooter, SiteHeader } from "../site-chrome"; + +export const dynamic = "force-static"; + +export const metadata: Metadata = { + title: "更新日志", + description: "沿着月海的发布线,查看主题、壁纸、助手与网站体验的每一次更新。", + alternates: { canonical: "/updates" }, + openGraph: { + title: "月海更新日志", + description: "主题、壁纸、助手与网站体验的持续更新。", + url: "/updates", + }, +}; + +export default function UpdatesPage() { + return ( +
+ +
+
+

MOONSEA RELEASE CURRENT

+

+ 月海, + 持续发生。 +

+

+ 从一张新壁纸,到一次更自然的应用体验。 +
+ 这里记录月海正在变好的每一步。 +

+
+ +
    + {SITE_UPDATES.map((update, updateIndex) => ( +
  1. +
    + + {update.kind} +
    + + + +
    +
    + {update.version} + {update.current ? 当前 : null} +
    +

    {update.title}

    +

    {update.summary}

    + + {update.images?.length ? ( +
    + {update.images.map((image, imageIndex) => ( + {image.alt} + ))} +
    + ) : null} + +
      + {update.details.map((detail) => ( +
    • {detail}
    • + ))} +
    + + {update.releaseUrl ? ( + + 查看完整发布说明 + + ) : null} +
    +
  2. + ))} +
+
+ +
+ ); +} diff --git a/web/lib/site-updates.ts b/web/lib/site-updates.ts new file mode 100644 index 0000000..07fccc0 --- /dev/null +++ b/web/lib/site-updates.ts @@ -0,0 +1,131 @@ +export type SiteUpdateImage = { + src: string; + alt: string; + width: number; + height: number; +}; + +export type SiteUpdate = { + id: string; + date: string; + displayDate: string; + kind: "站点更新" | "版本"; + version: string; + title: string; + summary: string; + details: string[]; + images?: SiteUpdateImage[]; + releaseUrl?: string; + current?: boolean; +}; + +export const SITE_UPDATES: SiteUpdate[] = [ + { + id: "unified-theme-wall", + date: "2026-07-31", + displayDate: "7 月 31 日", + kind: "站点更新", + version: "现在", + title: "一个主题墙,两个工作台", + summary: + "Codex 与 WorkBuddy 现在共享同一套月海主题。切换应用对象时,页面保持原位,只改变连接状态、应用目标和下载内容。", + details: [ + "主题墙不再拆成两个重复页面,浏览路径更短。", + "未安装月海助手也能打开模拟窗口预览;当天上新的主题会显示 NEW。", + "首页、导航与品牌标记完成统一,管理员入口收进页脚品牌名。", + ], + images: [ + { + src: "/og.png", + alt: "月海主题站首页与主题预览", + width: 1731, + height: 909, + }, + ], + current: true, + }, + { + id: "v1-5-9", + date: "2026-07-31", + displayDate: "7 月 31 日", + kind: "版本", + version: "v1.5.9", + title: "WorkBuddy 正式加入月海", + summary: + "月海助手扩展到 WorkBuddy:同一套主题可在两个工作台之间切换,完整的壁纸与主题桥已经接通。", + details: [ + "新增 WorkBuddy 月海版安装、更新与卸载链路。", + "壁纸、透明层、阅读对比度与月海助手控制面板保持一致。", + "主题上传与远程分发进入统一流程,为持续上新做好准备。", + ], + images: [ + { + src: "/theme-assets/tide-dragon-realm.png", + alt: "潮汐龙境主题在工作台中的预览", + width: 1055, + height: 588, + }, + ], + releaseUrl: + "https://github.com/413162826/moonsea-codex-theme/releases/tag/v1.5.9", + }, + { + id: "v1-5-8", + date: "2026-07-30", + displayDate: "7 月 30 日", + kind: "版本", + version: "v1.5.8", + title: "新壁纸,不再要求升级整个助手", + summary: + "主题清单改为远程更新。月海上新壁纸后,用户点击应用,助手会自动获取、校验、缓存并生效。", + details: [ + "不需要手动选择下载目录,也不需要搬运图片文件。", + "下载前校验文件完整性,失败不会覆盖当前正在使用的主题。", + "官网预览、主题清单与助手应用使用同一份主题信息。", + ], + images: [ + { + src: "/theme-assets/moonlit-silent.png", + alt: "月海无声主题在工作台中的预览", + width: 1664, + height: 936, + }, + ], + releaseUrl: + "https://github.com/413162826/moonsea-codex-theme/releases/tag/v1.5.8", + }, + { + id: "v1-5-7", + date: "2026-07-30", + displayDate: "7 月 30 日", + kind: "版本", + version: "v1.5.7", + title: "让下载与发布更可信", + summary: + "我们开始区分访问、下载、设备与自动化流量,并把 Windows 发布流程改造成可追踪的生命周期。", + details: [ + "匿名统计按设备随机标识去重,不采集硬件指纹。", + "机器人、自测与无法确认的流量可以单列,不再混入真实成果。", + "Windows 候选包、正式发布与下载入口之间有了明确状态。", + ], + releaseUrl: + "https://github.com/413162826/moonsea-codex-theme/releases/tag/v1.5.7", + }, + { + id: "v1-5-6", + date: "2026-07-27", + displayDate: "7 月 27 日", + kind: "版本", + version: "v1.5.6", + title: "更新链路开始可验证、可恢复", + summary: + "版本发布不再只是上传一个安装包:候选构建、验收、放行与更新恢复开始使用同一套可验证证据。", + details: [ + "安装包与更新包来自同一批构建产物。", + "发布前必须通过真实安装与更新验证。", + "中断的更新可以恢复,不把半完成状态留给用户。", + ], + releaseUrl: + "https://github.com/413162826/moonsea-codex-theme/releases/tag/v1.5.6", + }, +]; diff --git a/web/tests/rendered-html.test.mjs b/web/tests/rendered-html.test.mjs index a32ebce..903b3cf 100644 --- a/web/tests/rendered-html.test.mjs +++ b/web/tests/rendered-html.test.mjs @@ -361,10 +361,28 @@ test("公开页面提供固定 canonical、robots 与 sitemap", async () => { /rel="canonical" href="https:\/\/moonsea-codex-theme\.suguowen5\.chatgpt\.site\/themes"/, ); + const updates = await fetch(`${origin}/updates`); + const updatesHtml = await updates.text(); + assert.equal(updates.status, 200); + assert.match( + updatesHtml, + /rel="canonical" href="https:\/\/moonsea-codex-theme\.suguowen5\.chatgpt\.site\/updates"/, + ); + assert.match(updatesHtml, /月海,/); + assert.match(updatesHtml, /持续发生。/); + assert.match(updatesHtml, /一个主题墙,两个工作台/); + assert.match(updatesHtml, /WorkBuddy 正式加入月海/); + assert.match(updatesHtml, /新壁纸,不再要求升级整个助手/); + assert.match(updatesHtml, /class="updates-timeline"/); + assert.match(updatesHtml, /dateTime="2026-07-31"/); + assert.match(updatesHtml, /theme-assets\/moonlit-silent\.png/); + assert.match(updatesHtml, /releases\/tag\/v1\.5\.9/); + const robots = await fetch(`${origin}/robots.txt`); assert.equal(robots.status, 200); const robotsText = await robots.text(); assert.match(robotsText, /Disallow: \/admin/); + assert.match(robotsText, /Allow: \/updates/); assert.match( robotsText, /Sitemap: https:\/\/moonsea-codex-theme\.suguowen5\.chatgpt\.site\/sitemap\.xml/, @@ -374,6 +392,7 @@ test("公开页面提供固定 canonical、robots 与 sitemap", async () => { assert.equal(sitemap.status, 200); const sitemapText = await sitemap.text(); assert.match(sitemapText, /https:\/\/moonsea-codex-theme\.suguowen5\.chatgpt\.site\/themes<\/loc>/); + assert.match(sitemapText, /https:\/\/moonsea-codex-theme\.suguowen5\.chatgpt\.site\/updates<\/loc>/); assert.match(sitemapText, /https:\/\/moonsea-codex-theme\.suguowen5\.chatgpt\.site\/themes\/moon-white<\/loc>/); assert.match( sitemapText, @@ -535,6 +554,7 @@ test("月海品牌标识使用月牙与潮汐组合图形", async () => { }); test("管理员入口藏在页脚品牌文字中且普通访客不可点击", async () => { + const homepage = await readFile(new URL("../app/page.tsx", import.meta.url), "utf8"); const chrome = await readFile(new URL("../app/site-chrome.tsx", import.meta.url), "utf8"); const ownerLink = await readFile( new URL("../app/owner-admin-link.tsx", import.meta.url), @@ -542,6 +562,7 @@ test("管理员入口藏在页脚品牌文字中且普通访客不可点击", as ); assert.doesNotMatch(chrome, //); + assert.match(homepage, //); assert.match( chrome, /\s*月海 · 主题实验室\s*<\/OwnerAdminLink>/,