实现Issue226阶段1客户端请求统一标记
在fetchClientHttp统一注入X-Genarrative-Client标记 补充Web与Tauri HTTP transport及主站路径定向测试 保留Authorization、响应包络和X-Request-ID等原有Header 同步更新Issue226阶段1实施状态与验收记录
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- `#226 添加客户端特殊标识`:本方案实际实施范围
|
||||
- `#225 添加客户端埋点统计`:主站接收、落库和后台统计,本文只冻结交接契约,不在本次实施
|
||||
|
||||
状态:阶段 0 已完成,待阶段 1 实施<br>
|
||||
状态:阶段 1 已完成,待阶段 2 实施<br>
|
||||
本方案不包含代码修改
|
||||
|
||||
## 1. 一句话交付结果
|
||||
|
||||
@@ -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 工作内容
|
||||
|
||||
Reference in New Issue
Block a user