项目快照按渠道分区,后台项目工程支持渠道筛选与游标分页
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m37s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m5s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m40s
Project CI / Repository checks (push) Successful in 5m37s
Project CI / Frontend tests (push) Successful in 6m21s
Project CI / Backend tests (push) Successful in 9m56s
Project CI / Native shell tests (push) Successful in 10m57s
Project CI / AI game creator shell web tests (push) Successful in 5m29s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m37s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m5s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m40s
Project CI / Repository checks (push) Successful in 5m37s
Project CI / Frontend tests (push) Successful in 6m21s
Project CI / Backend tests (push) Successful in 9m56s
Project CI / Native shell tests (push) Successful in 10m57s
Project CI / AI game creator shell web tests (push) Successful in 5m29s
项目快照对象键升级为 agc/project-snapshots/v2/{channel}/{user}/{project}/,渠道取部署配置 GENARRATIVE_AGC_PROJECT_SNAPSHOT_CHANNEL(缺省沿用客户端下载渠道),非法渠道在写入处失败关闭
后台新增渠道列表接口,项目工程列表与下载接受 channel,游标绑定渠道并拒绝跨渠道复用
后台项目工程用户列改为素材查询口径:昵称 + 陶泥号 + 用户详情入口,由 api-server 解析作者信息
后台项目工程改为游标分页:每页 20/50/100、上一页/下一页与当前页提示,翻页失败保留当前页
部署环境示例补充快照渠道配置,并同步运维、技术方案、里程碑与决策记录文档
This commit is contained in:
@@ -34,6 +34,7 @@ import type {
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
AdminOverviewResponse,
|
||||
AdminProjectSnapshotChannelsResponse,
|
||||
AdminProjectSnapshotListQuery,
|
||||
AdminProjectSnapshotListResponse,
|
||||
AdminRechargeOrderListQuery,
|
||||
@@ -214,6 +215,7 @@ export function listAdminProjectSnapshots(
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (query.cursor) params.set('cursor', query.cursor);
|
||||
if (query.channel) params.set('channel', query.channel);
|
||||
params.set('limit', String(query.limit ?? 20));
|
||||
return request<AdminProjectSnapshotListResponse>(
|
||||
`/admin/api/project-snapshots?${params.toString()}`,
|
||||
@@ -221,13 +223,27 @@ export function listAdminProjectSnapshots(
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminProjectSnapshotChannels(
|
||||
token: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return request<AdminProjectSnapshotChannelsResponse>(
|
||||
'/admin/api/project-snapshots/channels',
|
||||
{ token, signal },
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadAdminProjectSnapshot(
|
||||
token: string,
|
||||
channel: string,
|
||||
userId: string,
|
||||
projectId: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const path = `/admin/api/project-snapshots/${encodeURIComponent(userId)}/${encodeURIComponent(projectId)}/download`;
|
||||
const params = new URLSearchParams();
|
||||
if (channel) params.set('channel', channel);
|
||||
const query = params.toString();
|
||||
const path = `/admin/api/project-snapshots/${encodeURIComponent(userId)}/${encodeURIComponent(projectId)}/download${query ? `?${query}` : ''}`;
|
||||
const response = await fetch(buildRequestUrl(path), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.trim()}`,
|
||||
|
||||
@@ -105,11 +105,15 @@ export interface AdminProjectSnapshotEntry {
|
||||
fileCount: number;
|
||||
totalBytes: number;
|
||||
status: 'ready' | 'partial' | 'unverified';
|
||||
channel: string;
|
||||
authorDisplayName?: string | null;
|
||||
authorPublicUserCode?: string | null;
|
||||
}
|
||||
|
||||
export interface AdminProjectSnapshotListQuery {
|
||||
cursor?: string | null;
|
||||
limit?: number;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
||||
export interface AdminProjectSnapshotListResponse {
|
||||
@@ -117,6 +121,11 @@ export interface AdminProjectSnapshotListResponse {
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
export interface AdminProjectSnapshotChannelsResponse {
|
||||
defaultChannel: string;
|
||||
channels: string[];
|
||||
}
|
||||
|
||||
export interface AdminErrorReportEntry {
|
||||
batchId: string;
|
||||
eventCount: number;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
downloadAdminProjectSnapshot,
|
||||
getAdminProjectSnapshotChannels,
|
||||
listAdminProjectSnapshots,
|
||||
} from './adminApiClient';
|
||||
|
||||
@@ -21,12 +22,12 @@ test('项目列表携带分页与后台授权,解析标准响应', async () =>
|
||||
expect(
|
||||
await listAdminProjectSnapshots(
|
||||
'admin-token',
|
||||
{ cursor: 'user/a+项目', limit: 20 },
|
||||
{ cursor: 'user/a+项目', limit: 20, channel: 'release' },
|
||||
controller.signal,
|
||||
),
|
||||
).toEqual(payload);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/admin/api/project-snapshots?cursor=user%2Fa%2B%E9%A1%B9%E7%9B%AE&limit=20',
|
||||
'/admin/api/project-snapshots?cursor=user%2Fa%2B%E9%A1%B9%E7%9B%AE&channel=release&limit=20',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }),
|
||||
signal: controller.signal,
|
||||
@@ -34,6 +35,23 @@ test('项目列表携带分页与后台授权,解析标准响应', async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test('渠道列表按后台授权读取,解析本部署渠道', async () => {
|
||||
const payload = { defaultChannel: 'release', channels: ['dev', 'release'] };
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
new Response(JSON.stringify({ ok: true, data: payload })),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
expect(await getAdminProjectSnapshotChannels('admin-token')).toEqual(payload);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/admin/api/project-snapshots/channels',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('ZIP 下载以授权请求读取并优先保留中文附件名', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response('PK\u0003\u0004', {
|
||||
@@ -48,6 +66,7 @@ test('ZIP 下载以授权请求读取并优先保留中文附件名', async () =
|
||||
const controller = new AbortController();
|
||||
const archive = await downloadAdminProjectSnapshot(
|
||||
'admin-token',
|
||||
'release',
|
||||
'user/a',
|
||||
'project/b',
|
||||
controller.signal,
|
||||
@@ -55,7 +74,7 @@ test('ZIP 下载以授权请求读取并优先保留中文附件名', async () =
|
||||
expect(archive.filename).toBe('三消-r2.zip');
|
||||
expect(archive.blob.type).toBe('application/zip');
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/admin/api/project-snapshots/user%2Fa/project%2Fb/download',
|
||||
'/admin/api/project-snapshots/user%2Fa/project%2Fb/download?channel=release',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer admin-token',
|
||||
@@ -81,7 +100,8 @@ test.each([
|
||||
vi.fn().mockResolvedValue(new Response('PK', { headers })),
|
||||
);
|
||||
expect(
|
||||
(await downloadAdminProjectSnapshot('token', 'user', 'project')).filename,
|
||||
(await downloadAdminProjectSnapshot('token', 'release', 'user', 'project'))
|
||||
.filename,
|
||||
).toBe(expected.replace('工程', 'project'));
|
||||
});
|
||||
|
||||
@@ -104,7 +124,7 @@ test.each([401, 403, 409, 500])(
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
downloadAdminProjectSnapshot('token', 'user', 'project'),
|
||||
downloadAdminProjectSnapshot('token', 'release', 'user', 'project'),
|
||||
).rejects.toMatchObject({
|
||||
status,
|
||||
code: 'SNAPSHOT_FAILURE',
|
||||
@@ -123,6 +143,6 @@ test('200 JSON 或 HTML 不能被保存为成功 ZIP', async () => {
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
downloadAdminProjectSnapshot('token', 'user', 'project'),
|
||||
downloadAdminProjectSnapshot('token', 'release', 'user', 'project'),
|
||||
).rejects.toMatchObject({ code: 'INVALID_PROJECT_ARCHIVE_RESPONSE' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user