From bbbd91db883d940e3654e3070923ffd0fef031dc Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:21:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=B8=B8=E6=88=8F=E5=88=86?= =?UTF-8?q?=E5=8F=91=E7=BD=91=E9=A1=B5=E5=8F=91=E5=B8=83=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=8E=89=E5=8F=91=E5=B8=83=E7=81=B0=E5=BA=A6?= =?UTF-8?q?=E6=8C=89=E5=8C=BF=E5=90=8D=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scripts/check-game-distribution-web-publish-e2e.mjs:网页登录、选封面、选发行包 ZIP、填资料提交,待审不可见、审核后公开、更新沿用同一 gameId 且旧版在线,共 12 项 PASS - frontendRuntimeConfigService 去掉 skipAuth:发布灰度按作者判定,匿名请求恒为 false,之前只要显式配置过灰度网页端就永远没有发布入口;单测同步更新 - 游戏分发里程碑阶段 C 第 4 条改为已勾选,第 5 条缺口收窄到上传中断/双击/登录失效/窗口关闭与换账号 - pitfalls 记录「按用户灰度配置不能走 skipAuth」的经验 --- ...‹碑】游戏分发目录详情与在线游玩-2026-09-18.md | 11 +- docs/project-memory/shared-memory/pitfalls.md | 7 +- package.json | 1 + ...heck-game-distribution-web-publish-e2e.mjs | 349 ++++++++++++++++++ .../frontendRuntimeConfigService.test.ts | 1 - src/services/frontendRuntimeConfigService.ts | 3 +- 6 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 scripts/check-game-distribution-web-publish-e2e.mjs diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index e65685b30..52eb54443 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -98,7 +98,7 @@ - [ ] AGC 从已构建 dist 生成根入口为 `index.html` 的真实包,一次提交动作完成检查、资料确认、上传和送审;状态及失败原因与服务端回读一致。 - [ ] AGC 发布面板隐藏发行包技术摘要;打开时基于有界、脱敏的项目上下文免费生成一句话简介与白名单分类,失败保留本地兜底且不阻断发布;作者始终可以直接编辑生成结果。 - [ ] AGC 发布封面支持基于项目上下文生成,复用现役图片生成与泥点扣费链路;生成结果登记为当前账号平台素材后自动作为 `coverAssetId`,不二次上传。 -- [ ] 网页可选 ZIP、提交封面和必需资料,进入相同上传/校验/审核流程;任一客户端可以查看同账号游戏状态,更新沿用相同 `gameId`。 +- [x] 网页可选 ZIP、提交封面和必需资料,进入相同上传/校验/审核流程;任一客户端可以查看同账号游戏状态,更新沿用相同 `gameId`。 - [ ] 上传中断、双击、登录失效、窗口关闭后恢复原操作;换账号不能恢复前账号私有状态;待审不能显示为已发布。 - [x] 目录支持真实数据、关键词/分类/设备筛选、空/错/加载态;详情提供明确主动作,搜索与返回恢复上下文。 - [x] 游客从目录/分享链接进入详情再启动真实已发布游戏;开始操作后才加载 iframe,加载失败可重试,退出不会自动重启游戏。 @@ -202,4 +202,11 @@ - AGC 侧三条(dist 真实包一次提交、发布面板免费生成简介/分类、封面生成复用扣费链路):需要 AGC 客户端 GUI 与真实 Provider,本轮未做。 - 网页 ZIP 上传与跨端状态(第 4 条)、上传中断/换账号/待审显示(第 5 条):ZIP 上传面板与账号态切换仍需专门用例。 - 第 8 条(桌面/移动导航、上传发布面板、全屏、安全区、焦点可达):本轮只覆盖移动端目录/详情/游玩、旋转提示、桌面端游戏在移动端的明确门槛与无横向溢出;发布面板、全屏、安全区与键盘焦点仍缺用例。 - - 第 9 条(warm token、共享组件、无假统计、无演示兜底):本轮只顺带确认目录里的游玩次数来自真实数据(新游戏显示 0 次游玩),视觉与组件复用仍需人工/截图评审。 \ No newline at end of file + - 第 9 条(warm token、共享组件、无假统计、无演示兜底):本轮只顺带确认目录里的游玩次数来自真实数据(新游戏显示 0 次游玩),视觉与组件复用仍需人工/截图评审。 +## 本轮核对(2026-09-28,阶段 C 网页发布) + +- 已勾选(第 4 条:网页可选 ZIP、提交封面与必需资料,进入同一上传/校验/审核流程;同账号状态可见;更新沿用同一 `gameId`)——本轮补齐 + - 新增 `scripts/check-game-distribution-web-publish-e2e.mjs`(`npm run check:game-distribution-web-publish-e2e`,真实 dev 网页栈 + Chromium):网页里用密码登录/注册 → `/games/publish` 填资料、选封面、选发行包 ZIP → 提交后页面给出「已提交审核」;服务端回读同一账号 `my-games` 是 `latestVersion.status=pending_review` + 游戏 `unpublished`,广场里看不到该游戏;网页「我的游戏」显示「审核中」。管理员审核通过后广场出现该游戏且 `entryUrl=/games//`,网页「我的游戏」同步显示「已发布」;再用 `/games/publish?game=` 提交新版本,`versions.length=2`、`latestVersion=pending_review`、游戏仍是 `published`(旧版在线)——更新沿用同一 `gameId`。**12 项 PASS**。 + - **本轮顺带修掉一处真实缺陷**:`loadFrontendRuntimeConfig()` 之前用 `skipAuth: true` 调 `/api/runtime/frontend-config`,而发布灰度是**按作者**判定的(匿名恒为 false)。结果是:只要运营显式配置过 `game-distribution:publish`,登录作者在网页端也永远看不到发布入口(页面一直显示「发布功能正在灰度中」)。改为带登录态请求后,登录后壳层重读配置即可拿到发布入口,无需刷新页面;`frontendRuntimeConfigService.test.ts` 同步更新。 +- 仍未勾选(第 5 条其余部分) + - 「待审不能显示为已发布」已随本轮取证(更新待审时游戏仍显示已发布、新版单独处于审核中);**仍缺**:上传中断、双击提交、登录失效、窗口关闭后恢复原操作,以及换账号不能恢复前账号私有状态的用例。 \ No newline at end of file diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 9147932e0..9d1f312ef 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6068,4 +6068,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` ## 2026-09-28 开发态 StrictMode 双跑 effect 击穿「一次性初始化」守卫;Vite dev 缺发行入口代理 - **现象 1(真实缺陷)**:从游戏详情返回目录后,之前输入的关键词被清空(分类/设备还在)。`GameGalleryPage` 用 `didSyncPropKeywordRef` 守卫「顶栏搜索词覆盖目录筛选」,React 开发态 StrictMode 会把挂载 effect 跑两遍:第一遍设标记并返回,第二遍就拿着空 `searchKeyword` 把刚从 `sessionStorage` 恢复的关键词清掉。**处理**:改成按 prop 值比对(`lastPropKeywordRef`),只有 prop 真的变化才覆盖本地筛选;`GameDistributionPages.test.tsx` 与网页 E2E 都回归通过。教训:任何「只在首次挂载跳过」的 ref 守卫都要按 StrictMode 双跑来审一遍。 -- **现象 2(dev 栈对齐)**:平台同源发行入口 `/games//` 在生产由 nginx 映射到发行网关并清空 Cookie,本地 Vite 代理没有这条规则,iframe 会落到 SPA 外壳(看起来“加载成功”,其实是壳)。**处理**:`vite.config.ts` 增加 `^/games/game_[0-9a-f]{32}(?:/.*)?$` 代理,rewrite 到 `/api/game-distribution/releases/…` 并去掉 Cookie 头,本地网页栈与生产同口径;`check:game-distribution-web-e2e` 里用「iframe 文本包含包内标记」断言真正载入的是发行包。 \ No newline at end of file +- **现象 2(dev 栈对齐)**:平台同源发行入口 `/games//` 在生产由 nginx 映射到发行网关并清空 Cookie,本地 Vite 代理没有这条规则,iframe 会落到 SPA 外壳(看起来“加载成功”,其实是壳)。**处理**:`vite.config.ts` 增加 `^/games/game_[0-9a-f]{32}(?:/.*)?$` 代理,rewrite 到 `/api/game-distribution/releases/…` 并去掉 Cookie 头,本地网页栈与生产同口径;`check:game-distribution-web-e2e` 里用「iframe 文本包含包内标记」断言真正载入的是发行包。 +## 2026-09-28 前端运行时配置接口用 skipAuth 调用,导致显式配置发布灰度后网页端永远没有发布入口 + +- **现象**:运营把 `game-distribution:publish` 显式配置成 `enabled=true, rolloutPercent=100` 后,登录作者在网页 `/games/publish` 仍看到「发布功能正在灰度中 / 当前账号还没有发布入口」,但用同一账号的 access token 直接请求 `/api/runtime/frontend-config` 得到 `gameDistributionPublishEnabled=true`。 +- **根因**:`src/services/frontendRuntimeConfigService.ts` 的 `loadFrontendRuntimeConfig()` 传了 `skipAuth: true`,请求不带登录态;而发布灰度是**按作者**判定的(匿名恒为 false),所以壳层拿到的永远是匿名结果。 +- **处理**:去掉 `skipAuth`(保留 `skipRefresh` 等选项),登录后壳层按 `gamePublishGateUserId` 重读配置即可拿到发布入口;`frontendRuntimeConfigService.test.ts` 与 `check:game-distribution-web-publish-e2e` 都覆盖回归。教训:任何「按用户/作者灰度」的配置接口都不能走 `skipAuth`,匿名结果与登录结果语义不同。 \ No newline at end of file diff --git a/package.json b/package.json index 65da868a7..d3e3dcd11 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "check:game-distribution-sandbox-e2e": "node scripts/check-game-distribution-sandbox-e2e.mjs", "check:external-v1-scene-generation-smoke": "node scripts/check-external-v1-scene-generation-smoke.mjs", "check:game-distribution-web-e2e": "node scripts/check-game-distribution-web-e2e.mjs", + "check:game-distribution-web-publish-e2e": "node scripts/check-game-distribution-web-publish-e2e.mjs", "check:production-ops": "node scripts/check-production-ops-guardrails.mjs", "check:preview-deployer": "node scripts/check-preview-deployer.mjs", "check:maintenance-page": "node scripts/check-maintenance-page.mjs", diff --git a/scripts/check-game-distribution-web-publish-e2e.mjs b/scripts/check-game-distribution-web-publish-e2e.mjs new file mode 100644 index 000000000..8568702c3 --- /dev/null +++ b/scripts/check-game-distribution-web-publish-e2e.mjs @@ -0,0 +1,349 @@ +// 游戏分发网页发布链路检查:真实 dev 网页栈 + Chromium,作者在网页里登录、选封面、选发行包 +// ZIP、填资料并提交,随后核对「同一账号在网页里看到待审状态」「审核通过后出现在广场」。 +// +// 需要:完整本地 dev 栈(`npm run dev`)+ 管理员账号 + playwright。 +// npm install --prefix %TEMP%\genarrative-pw --no-save --no-package-lock playwright +// E2E_PLAYWRIGHT_DIR=%TEMP%\genarrative-pw +// E2E_ADMIN_USER=... E2E_ADMIN_PASSWORD=... npm run check:game-distribution-web-publish-e2e +// E2E_WEB_BASE 默认 http://127.0.0.1:3000,E2E_API_BASE 默认 http://127.0.0.1:8082。 +import { createHash } from 'node:crypto'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +import JSZip from 'jszip'; + +const COVER_PNG = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64', +); +const WEB = process.env.E2E_WEB_BASE ?? 'http://127.0.0.1:3000'; +const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082'; +const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' }; +const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim(); +const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? ''; +const DEV_PASSWORD = 'GenE2e123!'; + +if (!ADMIN_USER || !ADMIN_PASSWORD) { + console.error( + '缺少 E2E_ADMIN_USER / E2E_ADMIN_PASSWORD:本脚本要通过网页真实发布并审核。', + ); + process.exit(2); +} + +let failures = 0; +function check(name, ok, detail = '') { + if (!ok) failures += 1; + console.log( + `${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`, + ); +} + +async function api(pathname, options = {}) { + const { method = 'GET', token, body, headers = {} } = options; + const finalHeaders = { ...ENVELOPE, ...headers }; + if (token) finalHeaders.Authorization = `Bearer ${token}`; + let finalBody; + if (body !== undefined) { + finalHeaders['Content-Type'] = 'application/json'; + finalBody = JSON.stringify(body); + } + const response = await fetch(`${API}${pathname}`, { + method, + headers: finalHeaders, + body: finalBody, + }); + const text = await response.text(); + let json = null; + try { + json = JSON.parse(text); + } catch { + json = null; + } + return { status: response.status, text, json, data: json?.data }; +} + +async function loadPlaywright() { + const dir = (process.env.E2E_PLAYWRIGHT_DIR ?? '').trim(); + if (!dir) return import('playwright'); + const requireFromDir = createRequire(path.join(dir, 'noop.js')); + return requireFromDir('playwright'); +} + +async function loginThroughWebUi(page, phone) { + await page.goto(`${WEB}/games`, { waitUntil: 'commit', timeout: 120_000 }); + await page.waitForSelector('.platform-account-entry', { timeout: 120_000 }); + await page.locator('.platform-account-entry').first().click(); + await page.getByRole('tab', { name: '密码登录' }).first().click(); + await page.waitForTimeout(600); + await page + .getByLabel('同意法律协议') + .first() + .check({ force: true }) + .catch(() => {}); + await page + .locator('input[placeholder="13800000000"]:visible') + .first() + .fill(phone); + await page + .locator('input[placeholder="输入密码"]:visible') + .first() + .fill(DEV_PASSWORD); + await page.getByRole('button', { name: '登录', exact: true }).first().click(); + await page + .locator('.platform-account-entry') + .first() + .filter({ hasText: phone.slice(-4) }) + .waitFor({ state: 'visible', timeout: 30_000 }); +} + +async function main() { + const adminLogin = await api('/admin/api/login', { + method: 'POST', + body: { username: ADMIN_USER, password: ADMIN_PASSWORD }, + }); + const admin = adminLogin.data?.token ?? adminLogin.data?.accessToken; + check( + '管理员登录成功', + adminLogin.status === 200 && Boolean(admin), + `status=${adminLogin.status}`, + ); + if (!admin) process.exit(1); + const gate = await api('/admin/api/feature-gates', { + method: 'PUT', + token: admin, + body: { + gateKey: 'game-distribution:publish', + enabled: true, + rolloutPercent: 100, + allowUserIds: [], + allowUserTags: [], + denyUserIds: [], + description: 'E2E 网页发布链路', + }, + }); + check('发布灰度已开启', gate.status === 200, `status=${gate.status}`); + + const stamp = Date.now(); + const phone = `131${String(stamp).slice(-8)}`; + const title = `网页发布 ${String(stamp).slice(-6)}`; + const workDir = await mkdtemp( + path.join(tmpdir(), 'genarrative-web-publish-'), + ); + const coverPath = path.join(workDir, 'cover.png'); + const zipPath = path.join(workDir, 'package.zip'); + await writeFile(coverPath, COVER_PNG); + const zip = new JSZip(); + zip.file( + 'index.html', + 'web publish' + + '

WEB-PUBLISH-OK

', + ); + zip.file('assets/app.js', 'document.documentElement.dataset.e2e="publish";'); + const zipBytes = Buffer.from(await zip.generateAsync({ type: 'uint8array' })); + await writeFile(zipPath, zipBytes); + const zipSha256 = createHash('sha256').update(zipBytes).digest('hex'); + + const { chromium } = await loadPlaywright(); + const executablePath = (process.env.E2E_CHROMIUM_EXECUTABLE ?? '').trim(); + const browser = await chromium.launch({ + headless: true, + ...(executablePath ? { executablePath } : {}), + }); + let gameId = ''; + let versionId = ''; + try { + const page = await browser.newPage({ + viewport: { width: 1280, height: 900 }, + }); + await loginThroughWebUi(page, phone); + check('网页里用密码登录/注册成功', true, `phone=${phone}`); + + await page.goto(`${WEB}/games/publish`, { + waitUntil: 'commit', + timeout: 120_000, + }); + await page + .getByLabel('游戏名称') + .waitFor({ state: 'visible', timeout: 60_000 }); + await page.getByLabel('游戏名称').fill(title); + await page.getByLabel('一句话简介').fill('网页发布链路 E2E'); + await page + .getByLabel('详细介绍') + .fill('由 Playwright 在真实网页里提交的发行包。'); + await page.getByLabel('分类').selectOption('休闲'); + await page.getByLabel('标签').fill('e2e'); + await page.getByLabel('游戏封面(必需)').setInputFiles(coverPath); + await page + .locator('img[alt="游戏封面预览"]') + .waitFor({ state: 'visible', timeout: 60_000 }); + await page.getByLabel('发行包 ZIP').setInputFiles(zipPath); + await page.waitForTimeout(500); + await page.getByRole('button', { name: '提交审核' }).first().click(); + await page + .getByText('已提交审核,审核通过后会自动出现在游戏广场。') + .first() + .waitFor({ state: 'visible', timeout: 60_000 }); + check('网页提交发行包 ZIP 后进入待审', true); + + const authorLogin = await api('/api/auth/entry', { + method: 'POST', + body: { purePhoneNumber: phone, password: DEV_PASSWORD }, + }); + const author = authorLogin.data?.token; + const mine = await api('/api/game-distribution/my-games', { + token: author, + }); + const mineGame = (mine.data?.games ?? []).find( + (item) => item.title === title, + ); + gameId = mineGame?.id ?? ''; + versionId = mineGame?.versions?.[0]?.versionId ?? ''; + check( + '同一账号在服务端看到该游戏为审核中', + mineGame?.latestVersion?.status === 'pending_review' && + mineGame?.status === 'unpublished', + `gameId=${gameId} versionStatus=${mineGame?.latestVersion?.status ?? ''} visibility=${mineGame?.status ?? ''}`, + ); + const catalogBefore = await api('/api/game-distribution/games?limit=100'); + check( + '待审期间广场看不到该游戏', + !(catalogBefore.data?.games ?? []).some((item) => item.id === gameId), + ); + + await page.goto(`${WEB}/games/mine`, { + waitUntil: 'commit', + timeout: 120_000, + }); + await page + .getByText(title) + .first() + .waitFor({ state: 'visible', timeout: 60_000 }); + check( + '网页「我的游戏」显示审核中', + await page.getByText('审核中').first().isVisible(), + ); + + const approved = await api( + `/admin/api/game-distribution/versions/${versionId}/review`, + { + method: 'POST', + token: admin, + headers: { 'Idempotency-Key': `web-publish-approve-${stamp}` }, + body: { decision: 'approve', expectedPublicationRevision: 0 }, + }, + ); + check( + '管理员审核通过网页提交的版本', + approved.status === 200, + `status=${approved.status}`, + ); + const catalogAfter = await api('/api/game-distribution/games?limit=100'); + const publishedGame = (catalogAfter.data?.games ?? []).find( + (item) => item.id === gameId, + ); + check( + '审核通过后广场出现该游戏且入口由服务端派生', + Boolean(publishedGame) && + publishedGame.currentVersion?.entryUrl === `/games/${gameId}/`, + `entryUrl=${publishedGame?.currentVersion?.entryUrl ?? ''}`, + ); + await page.goto(`${WEB}/games/mine`, { + waitUntil: 'commit', + timeout: 120_000, + }); + await page + .getByText(title) + .first() + .waitFor({ state: 'visible', timeout: 60_000 }); + check( + '网页「我的游戏」同步显示为已发布', + await page.getByText('已发布').first().isVisible(), + ); + + const mineAfter = await api('/api/game-distribution/my-games', { + token: author, + }); + const sameGame = (mineAfter.data?.games ?? []).find( + (item) => item.id === gameId, + ); + check( + '同一 gameId 的账号状态在两端读同一份后端数据', + Boolean(sameGame) && + sameGame.status === 'published' && + sameGame.latestVersion?.packageSha256 === zipSha256, + `status=${sameGame?.status ?? ''} sha=${String(sameGame?.latestVersion?.packageSha256).slice(0, 12)} expected=${zipSha256.slice(0, 12)}`, + ); + + // 更新沿用同一 gameId:网页发布页带 ?game= 回填旧版,提交新版本后仍是同一个游戏。 + const updateZip = new JSZip(); + updateZip.file( + 'index.html', + 'web publish v2' + + '

WEB-PUBLISH-V2

', + ); + updateZip.file( + 'assets/app.js', + 'document.documentElement.dataset.e2e="publish-v2";', + ); + const updateBytes = Buffer.from( + await updateZip.generateAsync({ type: 'uint8array' }), + ); + const updateZipPath = path.join(workDir, 'package-v2.zip'); + await writeFile(updateZipPath, updateBytes); + await page.goto(`${WEB}/games/publish?game=${gameId}`, { + waitUntil: 'commit', + timeout: 120_000, + }); + await page + .getByText('提交新版本审核') + .first() + .waitFor({ state: 'visible', timeout: 60_000 }); + await page.getByLabel('发行包 ZIP').setInputFiles(updateZipPath); + await page.waitForTimeout(500); + await page.getByRole('button', { name: '提交新版本审核' }).first().click(); + await page + .getByText('已提交审核,审核通过后会自动出现在游戏广场。') + .first() + .waitFor({ state: 'visible', timeout: 60_000 }); + const mineAfterUpdate = await api('/api/game-distribution/my-games', { + token: author, + }); + const updatedGame = (mineAfterUpdate.data?.games ?? []).find( + (item) => item.id === gameId, + ); + check( + '网页提交新版本沿用同一 gameId 且旧版仍在线', + Boolean(updatedGame) && + updatedGame.versions?.length === 2 && + updatedGame.latestVersion?.status === 'pending_review' && + updatedGame.status === 'published', + `versions=${updatedGame?.versions?.length ?? 'n/a'} latest=${updatedGame?.latestVersion?.status ?? ''} status=${updatedGame?.status ?? ''}`, + ); + await page.close(); + } finally { + await browser.close(); + await api('/admin/api/feature-gates', { + method: 'PUT', + token: admin, + body: { + gateKey: 'game-distribution:publish', + enabled: false, + rolloutPercent: 0, + allowUserIds: [], + allowUserTags: [], + denyUserIds: [], + description: 'E2E 网页发布链路(收尾关闭)', + }, + }); + } + + console.log(`\n${failures === 0 ? '全部通过' : `${failures} 项失败`}`); + process.exit(failures === 0 ? 0 : 1); +} + +main().catch((error) => { + console.error(`[check-game-distribution-web-publish-e2e] 运行失败:${error}`); + process.exit(1); +}); diff --git a/src/services/frontendRuntimeConfigService.test.ts b/src/services/frontendRuntimeConfigService.test.ts index 3977e7fed..bc5c71e74 100644 --- a/src/services/frontendRuntimeConfigService.test.ts +++ b/src/services/frontendRuntimeConfigService.test.ts @@ -34,7 +34,6 @@ describe('frontendRuntimeConfigService', () => { '读取前端运行时配置失败', { authImpact: 'local', - skipAuth: true, skipRefresh: true, notifyAuthStateChange: false, clearAuthOnUnauthorized: false, diff --git a/src/services/frontendRuntimeConfigService.ts b/src/services/frontendRuntimeConfigService.ts index d507b498d..c047cad3a 100644 --- a/src/services/frontendRuntimeConfigService.ts +++ b/src/services/frontendRuntimeConfigService.ts @@ -19,7 +19,8 @@ export async function loadFrontendRuntimeConfig() { '读取前端运行时配置失败', { authImpact: 'local', - skipAuth: true, + // 必须带上登录态:发布灰度按作者判定,匿名请求恒为 false, + // 否则一旦显式配置过 `game-distribution:publish`,网页端就再也拿不到发布入口。 skipRefresh: true, notifyAuthStateChange: false, clearAuthOnUnauthorized: false,