From 8c0af3d8b8b0941656470e250de5595cc2733d6e Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 1 Sep 2026 13:06:51 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0Issue226=E9=98=B6=E6=AE=B51?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E8=AF=B7=E6=B1=82=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在fetchClientHttp统一注入X-Genarrative-Client标记 补充Web与Tauri HTTP transport及主站路径定向测试 保留Authorization、响应包络和X-Request-ID等原有Header 同步更新Issue226阶段1实施状态与验收记录 --- .../src/services/clientHttp.ts | 13 ++- .../tests/clientHttp.test.ts | 110 +++++++++++++++++- ...Issue226-AGC客户端主站请求统一标记-2026-09-01.md | 2 +- ...26-AGC客户端主站请求标记分阶段验收-2026-09-01.md | 10 +- 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/apps/ai-game-creator-shell/src/services/clientHttp.ts b/apps/ai-game-creator-shell/src/services/clientHttp.ts index d8f2a9902..5bc5740b9 100644 --- a/apps/ai-game-creator-shell/src/services/clientHttp.ts +++ b/apps/ai-game-creator-shell/src/services/clientHttp.ts @@ -2,6 +2,8 @@ import { fetch as tauriHttpFetch } from '@tauri-apps/plugin-http'; export const AGC_DEVELOPMENT_API_BASE_URL = 'https://dev.genarrative.world'; export const AGC_RELEASE_API_BASE_URL = 'https://www.genarrative.world'; +export const AGC_CLIENT_MARKER_HEADER = 'X-Genarrative-Client'; +export const AGC_CLIENT_MARKER_VALUE = 'agc'; export type ClientServerPreset = 'release' | 'dev' | 'custom'; @@ -125,6 +127,12 @@ type ClientHttpTarget = { url: string; }; +function withAgcClientMarker(init: RequestInit): RequestInit { + const headers = new Headers(init.headers); + headers.set(AGC_CLIENT_MARKER_HEADER, AGC_CLIENT_MARKER_VALUE); + return { ...init, headers }; +} + function currentClientHttpContext(): ClientHttpContext { return { isDevelopment: import.meta.env.DEV, @@ -177,8 +185,9 @@ export async function fetchClientHttp( ? currentContext : { ...currentContext, serverBaseUrl }, ); + const markedInit = withAgcClientMarker(init); if (target.transport === 'tauri-http') { - return tauriHttpFetch(target.url, init); + return tauriHttpFetch(target.url, markedInit); } - return fetch(target.url, init); + return fetch(target.url, markedInit); } diff --git a/apps/ai-game-creator-shell/tests/clientHttp.test.ts b/apps/ai-game-creator-shell/tests/clientHttp.test.ts index 8b6114d3a..5c0f8d235 100644 --- a/apps/ai-game-creator-shell/tests/clientHttp.test.ts +++ b/apps/ai-game-creator-shell/tests/clientHttp.test.ts @@ -1,8 +1,16 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { fetch as tauriHttpFetch } from '@tauri-apps/plugin-http'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { + API_RESPONSE_ENVELOPE_HEADER, + API_RESPONSE_ENVELOPE_VERSION, +} from '../../../packages/shared/src/http'; +import { + AGC_CLIENT_MARKER_HEADER, + AGC_CLIENT_MARKER_VALUE, AGC_DEVELOPMENT_API_BASE_URL, AGC_RELEASE_API_BASE_URL, + fetchClientHttp, getClientServerBaseUrl, getClientServerSelection, normalizeClientServerBaseUrl, @@ -11,8 +19,106 @@ import { setClientServerSelection, } from '../src/services/clientHttp'; +vi.mock('@tauri-apps/plugin-http', () => ({ + fetch: vi.fn(), +})); + describe('AGC client HTTP transport', () => { - afterEach(() => resetClientServerSelectionForTests()); + afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + resetClientServerSelectionForTests(); + }); + + it('adds the AGC marker while preserving and overriding request headers', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue(new Response(null, { status: 204 })); + vi.stubGlobal('fetch', fetchMock); + const inputHeaders = new Headers({ + Authorization: 'Bearer fixture-token', + 'X-Request-ID': 'request-123', + [API_RESPONSE_ENVELOPE_HEADER]: API_RESPONSE_ENVELOPE_VERSION, + [AGC_CLIENT_MARKER_HEADER]: 'caller-value', + }); + const init: RequestInit = { + method: 'POST', + headers: inputHeaders, + body: '{}', + credentials: 'same-origin', + }; + + await fetchClientHttp('/api/auth/me', init); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [target, forwardedInit] = fetchMock.mock.calls[0] as [ + string, + RequestInit, + ]; + const forwardedHeaders = new Headers(forwardedInit.headers); + expect(target).toBe('/api/auth/me'); + expect(forwardedHeaders.get(AGC_CLIENT_MARKER_HEADER)).toBe( + AGC_CLIENT_MARKER_VALUE, + ); + expect(forwardedHeaders.get('Authorization')).toBe('Bearer fixture-token'); + expect(forwardedHeaders.get('X-Request-ID')).toBe('request-123'); + expect(forwardedHeaders.get(API_RESPONSE_ENVELOPE_HEADER)).toBe( + API_RESPONSE_ENVELOPE_VERSION, + ); + expect(forwardedInit.method).toBe('POST'); + expect(forwardedInit.body).toBe('{}'); + expect(forwardedInit.credentials).toBe('same-origin'); + expect(inputHeaders.get(AGC_CLIENT_MARKER_HEADER)).toBe('caller-value'); + }); + + it.each([ + '/api/auth/me', + '/api/profile/dashboard', + '/api/editor/projects', + '/api/assets/read-bytes?objectKey=fixture', + ])('marks %s through the shared Web transport', async (url) => { + const fetchMock = vi + .fn() + .mockResolvedValue(new Response(null, { status: 204 })); + vi.stubGlobal('fetch', fetchMock); + + await fetchClientHttp(url, {}); + + const [, forwardedInit] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect( + new Headers(forwardedInit.headers).get(AGC_CLIENT_MARKER_HEADER), + ).toBe(AGC_CLIENT_MARKER_VALUE); + }); + + it('adds the AGC marker to the Tauri HTTP transport', async () => { + const tauriFetchMock = vi.mocked(tauriHttpFetch); + tauriFetchMock.mockResolvedValue(new Response(null, { status: 204 })); + vi.stubEnv('MODE', 'production'); + vi.stubEnv('DEV', false); + vi.stubGlobal('window', { + __TAURI__: {}, + location: { protocol: 'tauri:' }, + }); + + await fetchClientHttp( + '/api/auth/me', + { headers: { Authorization: 'Bearer fixture-token' } }, + { serverBaseUrl: AGC_DEVELOPMENT_API_BASE_URL }, + ); + + expect(tauriFetchMock).toHaveBeenCalledTimes(1); + const [target, forwardedInit] = tauriFetchMock.mock.calls[0] as [ + string, + RequestInit, + ]; + const forwardedHeaders = new Headers(forwardedInit.headers); + expect(target).toBe(`${AGC_DEVELOPMENT_API_BASE_URL}/api/auth/me`); + expect(forwardedHeaders.get(AGC_CLIENT_MARKER_HEADER)).toBe( + AGC_CLIENT_MARKER_VALUE, + ); + expect(forwardedHeaders.get('Authorization')).toBe('Bearer fixture-token'); + }); it('keeps local development requests on the Vite API proxy', () => { expect( diff --git a/local-docs/【实施方案】Issue226-AGC客户端主站请求统一标记-2026-09-01.md b/local-docs/【实施方案】Issue226-AGC客户端主站请求统一标记-2026-09-01.md index 14d8ef7d9..cc2327c85 100644 --- a/local-docs/【实施方案】Issue226-AGC客户端主站请求统一标记-2026-09-01.md +++ b/local-docs/【实施方案】Issue226-AGC客户端主站请求统一标记-2026-09-01.md @@ -6,7 +6,7 @@ - `#226 添加客户端特殊标识`:本方案实际实施范围 - `#225 添加客户端埋点统计`:主站接收、落库和后台统计,本文只冻结交接契约,不在本次实施 -状态:阶段 0 已完成,待阶段 1 实施
+状态:阶段 1 已完成,待阶段 2 实施
本方案不包含代码修改 ## 1. 一句话交付结果 diff --git a/local-docs/【实施计划】Issue226-AGC客户端主站请求标记分阶段验收-2026-09-01.md b/local-docs/【实施计划】Issue226-AGC客户端主站请求标记分阶段验收-2026-09-01.md index 4b8744cb9..a8e0d9588 100644 --- a/local-docs/【实施计划】Issue226-AGC客户端主站请求标记分阶段验收-2026-09-01.md +++ b/local-docs/【实施计划】Issue226-AGC客户端主站请求标记分阶段验收-2026-09-01.md @@ -6,7 +6,7 @@ - `#226 添加客户端特殊标识`:本计划全部实施范围 - `#225 添加客户端埋点统计`:只接收交接契约,不在本计划实现 -当前状态:阶段 0 已完成;阶段 1 尚未开始。阶段 0 的证据与验收记录见 +当前状态:阶段 1 已完成;阶段 2 尚未开始。阶段 0 的证据与验收记录见 [【阶段验收】Issue226阶段0现状基线与契约冻结-2026-09-01.md](C:/projects/narrative/Genarrative/local-docs/【阶段验收】Issue226阶段0现状基线与契约冻结-2026-09-01.md)。 ## 1. 交付目标 @@ -125,6 +125,14 @@ TS 定向测试必须证明: - 没有在业务 API 文件中出现重复 Header 注入。 - 相关 TS 定向测试通过。 +### 5.4 阶段 1 实施记录 + +- `clientHttp.ts` 新增固定 Header 常量,并在 `fetchClientHttp` 内通过 `Headers` 合并并覆盖同名调用方 Header。 +- Web `fetch` 与 Tauri HTTP plugin 都接收同一份带标记的 `RequestInit`;原始 `RequestInit.headers` 不被修改。 +- `clientHttp.test.ts` 已覆盖 `/api/auth/*`、`/api/profile/*`、`/api/editor/*`、`/api/assets/*`,并分别验证 Web/Tauri transport、Authorization、envelope Header、`X-Request-ID` 和同名 Header 覆盖。 +- 阶段 1 验证命令:`npm exec vitest run apps/ai-game-creator-shell/tests/clientHttp.test.ts`(14 tests passed);`npm run ai-game-creator-shell:typecheck` 通过。 +- `appUpdate.ts` 未接入 `fetchClientHttp`,更新下载边界保持不变。 + ## 6. 阶段 2:Rust 主站 Client Builder factory ### 6.1 工作内容