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 工作内容