Skip to content

Commit 0973fc4

Browse files
fix(aether-console): improve asset management workflow
1 parent 165f3da commit 0973fc4

10 files changed

Lines changed: 461 additions & 33 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
vi.mock('@/api/http', () => ({
4+
http: {
5+
get: vi.fn(),
6+
post: vi.fn(),
7+
put: vi.fn(),
8+
patch: vi.fn(),
9+
},
10+
}))
11+
12+
import { http } from '@/api/http'
13+
import { disableAsset, enableAsset, getAsset, registerAsset, reviseAsset } from './asset.api'
14+
15+
const mockedGet = vi.mocked(http.get)
16+
const mockedPost = vi.mocked(http.post)
17+
const mockedPut = vi.mocked(http.put)
18+
const mockedPatch = vi.mocked(http.patch)
19+
20+
describe('asset api', () => {
21+
beforeEach(() => {
22+
mockedGet.mockReset()
23+
mockedPost.mockReset()
24+
mockedPut.mockReset()
25+
mockedPatch.mockReset()
26+
})
27+
28+
it('maps backend asset detail fields into frontend asset shape', async () => {
29+
mockedGet.mockResolvedValueOnce({
30+
data: {
31+
apiCode: 'weather-api',
32+
assetName: 'Weather API',
33+
assetType: 'STANDARD_API',
34+
categoryCode: 'tools',
35+
status: 'ENABLED',
36+
requestMethod: 'GET',
37+
upstreamUrl: 'https://upstream.example.com/weather',
38+
authScheme: 'NONE',
39+
aiCapabilityProfile: {
40+
provider: 'OpenAI',
41+
model: 'gpt-4.1',
42+
streamingSupported: true,
43+
capabilityTags: ['chat', 'tools'],
44+
},
45+
},
46+
})
47+
48+
const result = await getAsset('weather-api')
49+
50+
expect(result).toEqual(
51+
expect.objectContaining({
52+
apiCode: 'weather-api',
53+
displayName: 'Weather API',
54+
requestMethod: 'GET',
55+
upstreamUrl: 'https://upstream.example.com/weather',
56+
authScheme: 'NONE',
57+
aiProfile: {
58+
provider: 'OpenAI',
59+
model: 'gpt-4.1',
60+
streaming: true,
61+
tags: ['chat', 'tools'],
62+
},
63+
}),
64+
)
65+
})
66+
67+
it('maps frontend register payload to backend assetName contract', async () => {
68+
mockedPost.mockResolvedValueOnce({
69+
data: {
70+
apiCode: 'weather-api',
71+
assetName: 'Weather API',
72+
assetType: 'STANDARD_API',
73+
categoryCode: 'tools',
74+
status: 'DRAFT',
75+
},
76+
})
77+
78+
await registerAsset({
79+
apiCode: 'weather-api',
80+
displayName: 'Weather API',
81+
assetType: 'STANDARD_API',
82+
categoryCode: 'tools',
83+
})
84+
85+
expect(mockedPost).toHaveBeenCalledWith('v1/assets', {
86+
apiCode: 'weather-api',
87+
assetName: 'Weather API',
88+
assetType: 'STANDARD_API',
89+
categoryCode: 'tools',
90+
})
91+
})
92+
93+
it('uses backend put contract when revising asset config', async () => {
94+
mockedPut.mockResolvedValueOnce({
95+
data: {
96+
apiCode: 'weather-api',
97+
assetName: 'Weather API',
98+
assetType: 'STANDARD_API',
99+
categoryCode: 'tools',
100+
status: 'DRAFT',
101+
},
102+
})
103+
104+
await reviseAsset('weather-api', {
105+
displayName: 'Weather API',
106+
categoryCode: 'tools',
107+
requestMethod: 'GET',
108+
upstreamUrl: 'https://upstream.example.com/weather',
109+
})
110+
111+
expect(mockedPut).toHaveBeenCalledWith('v1/assets/weather-api', {
112+
assetName: 'Weather API',
113+
categoryCode: 'tools',
114+
requestMethod: 'GET',
115+
upstreamUrl: 'https://upstream.example.com/weather',
116+
})
117+
})
118+
119+
it('uses backend patch contract for enable and disable actions', async () => {
120+
mockedPatch.mockResolvedValue({
121+
data: {
122+
apiCode: 'weather-api',
123+
assetName: 'Weather API',
124+
assetType: 'STANDARD_API',
125+
categoryCode: 'tools',
126+
status: 'ENABLED',
127+
},
128+
})
129+
130+
await enableAsset('weather-api')
131+
await disableAsset('weather-api')
132+
133+
expect(mockedPatch).toHaveBeenNthCalledWith(1, 'v1/assets/weather-api/enable')
134+
expect(mockedPatch).toHaveBeenNthCalledWith(2, 'v1/assets/weather-api/disable')
135+
})
136+
})

aether-ui/aether-console/src/api/catalog/asset.api.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,36 @@ import type {
1010
} from './catalog.dto'
1111
import type { ApiAsset, ApiAssetSummary, PageResult } from './catalog.types'
1212

13+
function mapAiProfile(dto: AssetDto): ApiAsset['aiProfile'] {
14+
const profile = dto.aiCapabilityProfile ?? dto.aiProfile
15+
if (!profile) {
16+
return undefined
17+
}
18+
19+
return {
20+
provider: profile.provider,
21+
model: profile.model,
22+
streaming: 'streamingSupported' in profile ? profile.streamingSupported : profile.streaming,
23+
tags: 'capabilityTags' in profile ? profile.capabilityTags : profile.tags,
24+
}
25+
}
26+
1327
function mapAsset(dto: AssetDto): ApiAsset {
1428
return {
1529
apiCode: dto.apiCode,
16-
displayName: dto.displayName,
30+
displayName: dto.assetName ?? dto.displayName ?? dto.apiCode,
1731
assetType: dto.assetType,
1832
categoryCode: dto.categoryCode,
1933
status: dto.status,
2034
description: dto.description,
35+
requestMethod: dto.requestMethod ?? undefined,
36+
upstreamUrl: dto.upstreamUrl ?? undefined,
2137
authScheme: dto.authScheme,
22-
aiProfile: dto.aiProfile,
38+
authConfig: dto.authConfig ?? undefined,
39+
requestTemplate: dto.requestTemplate ?? undefined,
40+
requestExample: dto.requestExample ?? undefined,
41+
responseExample: dto.responseExample ?? undefined,
42+
aiProfile: mapAiProfile(dto),
2343
}
2444
}
2545

@@ -36,7 +56,14 @@ function mapAssetSummary(dto: AssetSummaryDto): ApiAssetSummary {
3656
}
3757

3858
export async function registerAsset(body: RegisterAssetBody): Promise<ApiAsset> {
39-
const { data } = await http.post<AssetDto>('v1/assets', body)
59+
const { data } = await http.post<AssetDto>('v1/assets', {
60+
apiCode: body.apiCode,
61+
assetName: body.displayName,
62+
assetType: body.assetType,
63+
categoryCode: body.categoryCode,
64+
description: body.description,
65+
authScheme: body.authScheme,
66+
})
4067
return mapAsset(data)
4168
}
4269

@@ -56,17 +83,27 @@ export async function getAsset(apiCode: string): Promise<ApiAsset> {
5683
}
5784

5885
export async function reviseAsset(apiCode: string, body: ReviseAssetBody): Promise<ApiAsset> {
59-
const { data } = await http.patch<AssetDto>(`v1/assets/${apiCode}`, body)
86+
const { data } = await http.put<AssetDto>(`v1/assets/${apiCode}`, {
87+
assetName: body.displayName,
88+
categoryCode: body.categoryCode,
89+
description: body.description,
90+
requestMethod: body.requestMethod,
91+
upstreamUrl: body.upstreamUrl,
92+
authScheme: body.authScheme,
93+
requestTemplate: body.requestTemplate,
94+
requestExample: body.requestExample,
95+
responseExample: body.responseExample,
96+
})
6097
return mapAsset(data)
6198
}
6299

63100
export async function enableAsset(apiCode: string): Promise<ApiAsset> {
64-
const { data } = await http.post<AssetDto>(`v1/assets/${apiCode}/enable`)
101+
const { data } = await http.patch<AssetDto>(`v1/assets/${apiCode}/enable`)
65102
return mapAsset(data)
66103
}
67104

68105
export async function disableAsset(apiCode: string): Promise<ApiAsset> {
69-
const { data } = await http.post<AssetDto>(`v1/assets/${apiCode}/disable`)
106+
const { data } = await http.patch<AssetDto>(`v1/assets/${apiCode}/disable`)
70107
return mapAsset(data)
71108
}
72109

aether-ui/aether-console/src/api/catalog/catalog.dto.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,26 @@ export interface CategoryDto {
3232

3333
export interface AssetDto {
3434
apiCode: string
35-
displayName: string
35+
assetName?: string | null
36+
displayName?: string | null
3637
assetType: 'AI_API' | 'STANDARD_API'
37-
categoryCode: string
38+
categoryCode: string | null
3839
status: 'DRAFT' | 'ENABLED' | 'DISABLED'
3940
description?: string
41+
requestMethod?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | null
42+
upstreamUrl?: string | null
4043
authScheme?: string
44+
authConfig?: string | null
45+
requestTemplate?: string | null
46+
requestExample?: string | null
47+
responseExample?: string | null
4148
aiProfile?: AiProfileDto
49+
aiCapabilityProfile?: {
50+
provider: string
51+
model: string
52+
streamingSupported: boolean
53+
capabilityTags: string[]
54+
} | null
4255
}
4356

4457
export interface PageDto<T> {
@@ -58,10 +71,15 @@ export interface RegisterAssetBody {
5871
}
5972

6073
export interface ReviseAssetBody {
61-
displayName?: string
62-
categoryCode?: string
63-
description?: string
64-
authScheme?: string
74+
displayName?: string | null
75+
categoryCode?: string | null
76+
description?: string | null
77+
requestMethod?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | null
78+
upstreamUrl?: string | null
79+
authScheme?: string | null
80+
requestTemplate?: string | null
81+
requestExample?: string | null
82+
responseExample?: string | null
6583
}
6684

6785
export interface BindAiProfileBody {

aether-ui/aether-console/src/api/catalog/catalog.mock.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ const categories: CategoryDto[] = [
3838
const assets: AssetDto[] = [
3939
{
4040
apiCode: 'deepseek-v3',
41+
assetName: 'DeepSeek V3',
4142
displayName: 'DeepSeek V3',
4243
assetType: 'AI_API',
4344
categoryCode: 'cat-ai',
4445
status: 'ENABLED',
46+
requestMethod: 'POST',
47+
upstreamUrl: 'https://upstream.example.com/deepseek/chat',
4548
description: '通用推理与工具调用旗舰模型。',
46-
authScheme: 'Bearer Token',
49+
authScheme: 'HEADER_TOKEN',
4750
aiProfile: {
4851
provider: 'DeepSeek',
4952
model: 'deepseek-v3',
@@ -53,39 +56,48 @@ const assets: AssetDto[] = [
5356
},
5457
{
5558
apiCode: 'kimi-k2',
59+
assetName: 'Kimi K2',
5660
displayName: 'Kimi K2',
5761
assetType: 'AI_API',
5862
categoryCode: 'cat-ai',
5963
status: 'ENABLED',
64+
requestMethod: 'POST',
65+
upstreamUrl: 'https://upstream.example.com/kimi/chat',
6066
description: '长上下文与 Agent 场景旗舰模型。',
61-
authScheme: 'Bearer Token',
67+
authScheme: 'HEADER_TOKEN',
6268
aiProfile: { provider: 'Moonshot', model: 'kimi-k2', streaming: true, tags: ['reasoning'] },
6369
},
6470
{
6571
apiCode: 'baidu-search',
72+
assetName: 'Baidu Search API',
6673
displayName: 'Baidu Search API',
6774
assetType: 'STANDARD_API',
6875
categoryCode: 'cat-search',
6976
status: 'ENABLED',
77+
requestMethod: 'GET',
78+
upstreamUrl: 'https://upstream.example.com/search',
7079
description: '联网检索与引用增强接口。',
71-
authScheme: 'API Key',
80+
authScheme: 'QUERY_TOKEN',
7281
},
7382
{
7483
apiCode: 'weather-api',
84+
assetName: 'Weather Data API',
7585
displayName: 'Weather Data API',
7686
assetType: 'STANDARD_API',
7787
categoryCode: 'cat-data',
7888
status: 'DISABLED',
89+
requestMethod: 'GET',
90+
upstreamUrl: 'https://upstream.example.com/weather',
7991
description: '实时天气与预报数据服务。',
80-
authScheme: 'API Key',
92+
authScheme: 'QUERY_TOKEN',
8193
},
8294
]
8395

8496
// ── Helpers ──────────────────────────────────────────────────
8597

8698
function page<T>(items: T[], params: Record<string, string>): PageDto<T> {
8799
const p = Number(params.page ?? 1)
88-
const size = Number(params.pageSize ?? 20)
100+
const size = Number(params.size ?? params.pageSize ?? 20)
89101
const keyword = (params.keyword ?? '').toLowerCase()
90102
const filtered = keyword
91103
? ((items as Record<string, unknown>[]).filter((item) =>
@@ -135,9 +147,9 @@ const routes: { method: string; pattern: RegExp; handler: MockHandler }[] = [
135147
.filter((a) => a.status === 'ENABLED')
136148
.map((a) => ({
137149
apiCode: a.apiCode,
138-
displayName: a.displayName,
150+
displayName: a.assetName ?? a.displayName ?? a.apiCode,
139151
assetType: a.assetType,
140-
categoryCode: a.categoryCode,
152+
categoryCode: a.categoryCode ?? '',
141153
categoryName: categories.find((c) => c.categoryCode === a.categoryCode)?.name,
142154
}))
143155
return ok(page(list, params))
@@ -151,9 +163,9 @@ const routes: { method: string; pattern: RegExp; handler: MockHandler }[] = [
151163
if (!asset) notFound()
152164
const detail: DiscoveryAssetDetailDto = {
153165
apiCode: asset.apiCode,
154-
displayName: asset.displayName,
166+
displayName: asset.assetName ?? asset.displayName ?? asset.apiCode,
155167
assetType: asset.assetType,
156-
categoryCode: asset.categoryCode,
168+
categoryCode: asset.categoryCode ?? '',
157169
categoryName: categories.find((c) => c.categoryCode === asset.categoryCode)?.name,
158170
description: asset.description,
159171
authScheme: asset.authScheme,
@@ -244,7 +256,7 @@ const routes: { method: string; pattern: RegExp; handler: MockHandler }[] = [
244256
},
245257
},
246258
{
247-
method: 'PATCH',
259+
method: 'PUT',
248260
pattern: /^\/api\/v1\/assets\/(.+)$/,
249261
handler: (_, body, match) => {
250262
const asset = assets.find((a) => a.apiCode === match![1])
@@ -254,7 +266,7 @@ const routes: { method: string; pattern: RegExp; handler: MockHandler }[] = [
254266
},
255267
},
256268
{
257-
method: 'POST',
269+
method: 'PATCH',
258270
pattern: /^\/api\/v1\/assets\/(.+)\/enable$/,
259271
handler: (_, __, match) => {
260272
const asset = assets.find((a) => a.apiCode === match![1])
@@ -264,7 +276,7 @@ const routes: { method: string; pattern: RegExp; handler: MockHandler }[] = [
264276
},
265277
},
266278
{
267-
method: 'POST',
279+
method: 'PATCH',
268280
pattern: /^\/api\/v1\/assets\/(.+)\/disable$/,
269281
handler: (_, __, match) => {
270282
const asset = assets.find((a) => a.apiCode === match![1])

0 commit comments

Comments
 (0)