初始提交:OCI 面板前端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:29 +08:00
commit db45a669e3
135 changed files with 25220 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
import { request } from './request'
import type { AiCallLog, AiChannel, AiContentLog, AiKey, AiModel, AiModelCacheItem } from '@/types/api'
// ---- 网关密钥 ----
export function listAiKeys(): Promise<AiKey[]> {
return request<{ items: AiKey[] }>('/ai-keys').then((r) => r.items)
}
export interface CreateAiKeyRequest {
name: string
value?: string
group?: string
}
/** 创建密钥;key 为明文,仅本次响应返回 */
export function createAiKey(body: CreateAiKeyRequest): Promise<{ key: string; item: AiKey }> {
return request('/ai-keys', { method: 'POST', body })
}
export function updateAiKey(
id: number,
body: { name?: string; enabled?: boolean; group?: string },
): Promise<void> {
return request(`/ai-keys/${id}`, { method: 'PUT', body })
}
export function deleteAiKey(id: number): Promise<void> {
return request(`/ai-keys/${id}`, { method: 'DELETE' })
}
/** 设置密钥内容日志窗口:0 关闭,>0 从现在起开启 N 小时(上限 168 = 7 天),返回最新密钥 */
export function updateAiKeyContentLog(id: number, hours: number): Promise<AiKey> {
return request(`/ai-keys/${id}/content-log`, { method: 'PUT', body: { hours } })
}
// ---- 渠道(号池)----
export function listAiChannels(): Promise<AiChannel[]> {
return request<{ items: AiChannel[] }>('/ai-channels').then((r) => r.items)
}
export interface AiChannelInput {
ociConfigId?: number
region?: string
name?: string
group?: string
enabled?: boolean
priority?: number
weight?: number
}
export function createAiChannel(body: AiChannelInput): Promise<AiChannel> {
return request('/ai-channels', { method: 'POST', body })
}
export function updateAiChannel(id: number, body: AiChannelInput): Promise<void> {
return request(`/ai-channels/${id}`, { method: 'PUT', body })
}
export function deleteAiChannel(id: number): Promise<void> {
return request(`/ai-channels/${id}`, { method: 'DELETE' })
}
export function probeAiChannel(id: number): Promise<AiChannel> {
return request(`/ai-channels/${id}/probe`, { method: 'POST' })
}
export function syncAiChannelModels(id: number): Promise<AiModelCacheItem[]> {
return request<{ items: AiModelCacheItem[] }>(`/ai-channels/${id}/sync-models`, {
method: 'POST',
}).then((r) => r.items)
}
// ---- 聚合模型与调用日志 ----
export function listAiModels(): Promise<AiModel[]> {
return request<{ items: AiModel[] }>('/ai-models').then((r) => r.items)
}
export function listAiCallLogs(params: {
page: number
size: number
}): Promise<{ items: AiCallLog[]; total: number }> {
return request('/ai-logs', { query: params })
}
/** 内容日志(红线例外);keyId / callLogId 可选过滤 */
export function listAiContentLogs(params: {
page: number
size: number
keyId?: number
callLogId?: number
}): Promise<{ items: AiContentLog[]; total: number }> {
return request('/ai-content-logs', { query: params })
}
+28
View File
@@ -0,0 +1,28 @@
import { mockCosts, mockTraffic } from './mock'
import { mockOn, mocked, request } from './request'
import type { CostItem, InstanceTraffic } from '@/types/api'
export function getInstanceTraffic(
cfgId: number,
instanceId: string,
days = 30,
region?: string,
): Promise<InstanceTraffic> {
if (mockOn) return mocked(mockTraffic, 800)
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/traffic`, {
query: { days, region },
})
}
export interface CostQuery {
startTime?: string
endTime?: string
granularity?: 'DAILY' | 'MONTHLY'
queryType?: 'COST' | 'USAGE'
groupBy?: 'service' | 'skuName' | 'region' | 'compartmentName'
}
export function getCosts(cfgId: number, query: CostQuery = {}): Promise<CostItem[]> {
if (mockOn) return mocked(mockCosts)
return request(`/oci-configs/${cfgId}/costs`, { query: { ...query } })
}
+151
View File
@@ -0,0 +1,151 @@
import { mockOn, mocked, request } from './request'
import type {
CredentialsInfo,
OauthProviderInfo,
LoginRequest,
LoginResponse,
OAuthSettings,
TotpSetup,
TotpStatus,
UpdateCredentialsRequest,
UpdateOAuthRequest,
UserIdentityInfo,
} from '@/types/api'
export function login(body: LoginRequest): Promise<LoginResponse> {
if (mockOn) {
if (body.password === 'wrong') return Promise.reject(new Error('用户名或密码错误'))
return mocked({ token: 'mock-token', expiresAt: '2099-01-01T00:00:00Z' }, 400)
}
return request('/auth/login', { method: 'POST', body })
}
/** 服务端登出:当前令牌拉黑至自然过期;调用方无论成败都应清本地会话 */
export function logout(): Promise<void> {
if (mockOn) return mocked(undefined, 100)
return request('/auth/logout', { method: 'POST' })
}
// ---- 登录凭据 ----
export function getCredentials(): Promise<CredentialsInfo> {
if (mockOn) return mocked({ username: 'admin', passwordLoginDisabled: false })
return request('/auth/credentials')
}
/** 修改用户名 / 密码;成功后旧 token 随即失效,调用方应登出重登 */
export function updateCredentials(body: UpdateCredentialsRequest): Promise<void> {
if (mockOn) return mocked(undefined, 400)
return request('/auth/credentials', { method: 'PUT', body })
}
/** 保存密码登录禁用开关;开启要求至少绑定一个外部身份(后端 409) */
export function updatePasswordLogin(disabled: boolean): Promise<void> {
if (mockOn) return mocked(undefined, 300)
return request('/auth/password-login', { method: 'PUT', body: { disabled } })
}
// ---- 两步验证(TOTP) ----
export function getTotpStatus(): Promise<TotpStatus> {
if (mockOn) return mocked({ enabled: false })
return request('/auth/totp')
}
/** 生成待激活密钥;10 分钟内输入验证码激活,重复调用覆盖旧暂存 */
export function setupTotp(): Promise<TotpSetup> {
if (mockOn)
return mocked({
secret: 'JBSWY3DPEHPK3PXP',
otpauthUri: 'otpauth://totp/oci-portal:admin?secret=JBSWY3DPEHPK3PXP&issuer=oci-portal',
})
return request('/auth/totp/setup', { method: 'POST' })
}
export function activateTotp(code: string): Promise<void> {
if (mockOn) return mocked(undefined, 300)
return request('/auth/totp/activate', { method: 'POST', body: { code } })
}
/** 停用两步验证;密码或当前验证码任一确认 */
export function disableTotp(body: { password?: string; code?: string }): Promise<void> {
if (mockOn) return mocked(undefined, 300)
return request('/auth/totp/disable', { method: 'POST', body })
}
// ---- 外部身份(OAuth) ----
/** 可登录的 provider 列表(登录页公开接口;已禁用的不返回);
* passwordLoginDisabled 为 true 且有可用 provider 时,登录页隐藏密码表单 */
export function getOauthProviders(): Promise<{
providers: OauthProviderInfo[]
passwordLoginDisabled: boolean
}> {
if (mockOn)
return mocked({
providers: [
{ provider: 'github', displayName: 'GitHub' },
{ provider: 'oidc', displayName: 'OIDC SSO' },
],
passwordLoginDisabled: false,
})
return request('/auth/oauth/providers')
}
/** 获取授权跳转 URL;bind 模式要求已登录(带 JWT) */
export function getOauthAuthorizeUrl(
provider: string,
mode: 'login' | 'bind',
): Promise<{ url: string }> {
if (mockOn) return mocked({ url: 'https://example.com/oauth/authorize?mock=1' })
return request(`/auth/oauth/${provider}/authorize`, { query: { mode } })
}
export function listIdentities(): Promise<{ items: UserIdentityInfo[] }> {
if (mockOn)
return mocked({
items: [
{ id: 1, provider: 'github', display: 'octocat', createdAt: '2026-07-07T10:00:00+08:00' },
],
})
return request('/auth/identities')
}
export function unbindIdentity(id: number): Promise<void> {
if (mockOn) return mocked(undefined, 300)
return request(`/auth/identities/${id}`, { method: 'DELETE' })
}
// ---- OAuth provider 配置(设置页) ----
export function getOAuthSettings(): Promise<OAuthSettings> {
if (mockOn)
return mocked({
oidcIssuer: '',
oidcClientId: '',
oidcSecretSet: false,
oidcDisplayName: '',
oidcDisabled: false,
githubClientId: '',
githubSecretSet: false,
githubDisplayName: '',
githubDisabled: false,
})
return request('/settings/oauth')
}
export function updateOAuthSettings(body: UpdateOAuthRequest): Promise<OAuthSettings> {
if (mockOn)
return mocked({
oidcIssuer: body.oidcIssuer,
oidcClientId: body.oidcClientId,
oidcSecretSet: !!body.oidcClientSecret,
oidcDisplayName: body.oidcDisplayName,
oidcDisabled: body.oidcDisabled,
githubClientId: body.githubClientId,
githubSecretSet: !!body.githubClientSecret,
githubDisplayName: body.githubDisplayName,
githubDisabled: body.githubDisabled,
})
return request('/settings/oauth', { method: 'PUT', body })
}
+59
View File
@@ -0,0 +1,59 @@
import { mockBootVolumes } from './mock'
import { mockOn, mocked, request } from './request'
import type { BootVolume, UpdateBootVolumeRequest } from '@/types/api'
export function listBootVolumes(
cfgId: number,
availabilityDomain?: string,
compartmentId?: string,
region?: string,
): Promise<BootVolume[]> {
if (mockOn) return mocked(compartmentId ? [] : (mockBootVolumes[cfgId] ?? []))
return request(`/oci-configs/${cfgId}/boot-volumes`, {
query: { availabilityDomain, compartmentId, region },
})
}
export function getBootVolume(
cfgId: number,
bootVolumeId: string,
region?: string,
): Promise<BootVolume> {
if (mockOn) {
const found = Object.values(mockBootVolumes)
.flat()
.find((b) => b.id === bootVolumeId)
return found ? mocked(found) : Promise.reject(new Error('引导卷不存在'))
}
return request(`/oci-configs/${cfgId}/boot-volumes/${encodeURIComponent(bootVolumeId)}`, {
query: { region },
})
}
export function updateBootVolume(
cfgId: number,
bootVolumeId: string,
body: UpdateBootVolumeRequest,
region?: string,
): Promise<BootVolume> {
if (mockOn) {
const base = Object.values(mockBootVolumes).flat()[0]
return mocked({ ...base, ...body }, 700)
}
return request(`/oci-configs/${cfgId}/boot-volumes/${encodeURIComponent(bootVolumeId)}`, {
method: 'PUT',
body: { region: region ?? '', ...body },
})
}
export function deleteBootVolume(
cfgId: number,
bootVolumeId: string,
region?: string,
): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/boot-volumes/${encodeURIComponent(bootVolumeId)}`, {
method: 'DELETE',
query: { region },
})
}
+121
View File
@@ -0,0 +1,121 @@
import { mockCompartments, mockConfigs, mockOverview, mockRegionSubs, mockRegions } from './mock'
import { mockOn, mocked, request } from './request'
import type {
Compartment,
ImportConfigRequest,
OciConfig,
OciConfigSummary,
Overview,
RegionInfo,
RegionSubscription,
UpdateConfigRequest,
VerifyResult,
} from '@/types/api'
export function getOverview(): Promise<Overview> {
if (mockOn) return mocked(mockOverview)
return request('/overview')
}
export function listCompartments(id: number): Promise<Compartment[]> {
if (mockOn) return mocked(mockCompartments[id] ?? [])
return request(`/oci-configs/${id}/compartments`)
}
/** 列表返回瘦身摘要,订阅 / 促销等详情字段走 getConfig 单查 */
export function listConfigs(): Promise<OciConfigSummary[]> {
if (mockOn) return mocked(mockConfigs)
return request('/oci-configs')
}
export function getConfig(id: number): Promise<OciConfig> {
if (mockOn) {
const found = mockConfigs.find((c) => c.id === id)
return found ? mocked(found) : Promise.reject(new Error('配置不存在'))
}
return request(`/oci-configs/${id}`)
}
export function importConfig(body: ImportConfigRequest): Promise<OciConfig> {
if (mockOn) {
const id = Math.max(...mockConfigs.map((c) => c.id)) + 1
const created = { ...mockConfigs[0], id, alias: body.alias, accountType: 'unknown' as const }
mockConfigs.push(created)
return mocked(created, 1200)
}
return request('/oci-configs', { method: 'POST', body })
}
export function updateConfig(id: number, body: UpdateConfigRequest): Promise<VerifyResult> {
if (mockOn) {
const found = mockConfigs.find((c) => c.id === id)
if (!found) return Promise.reject(new Error('配置不存在'))
if (body.alias) found.alias = body.alias
if (body.group !== undefined) found.group = body.group
if (body.region) found.region = body.region
if (body.multiRegion !== undefined) found.multiRegion = body.multiRegion
if (body.multiCompartment !== undefined) found.multiCompartment = body.multiCompartment
return mocked({ config: found, changes: {} }, 900)
}
return request(`/oci-configs/${id}`, { method: 'PUT', body })
}
export function verifyConfig(id: number): Promise<VerifyResult> {
if (mockOn) return mocked({ config: mockConfigs.find((c) => c.id === id) ?? mockConfigs[0], changes: {} }, 800)
return request(`/oci-configs/${id}/verify`, { method: 'POST' })
}
export function deleteConfig(id: number): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}`, { method: 'DELETE' })
}
export function listRegions(): Promise<RegionInfo[]> {
if (mockOn) return mocked(mockRegions)
return request('/regions')
}
export function listRegionSubscriptions(id: number): Promise<RegionSubscription[]> {
if (mockOn) return mocked(mockRegionSubs)
return request(`/oci-configs/${id}/region-subscriptions`)
}
export function subscribeRegion(id: number, regionKey: string): Promise<RegionSubscription[]> {
if (mockOn)
return mocked([
...mockRegionSubs,
{ key: regionKey, name: '', status: 'IN_PROGRESS', isHomeRegion: false, alias: '' },
])
return request(`/oci-configs/${id}/region-subscriptions`, { method: 'POST', body: { regionKey } })
}
/** 筛选器用区域列表(缓存):未开多区域支持只含默认区域;有非 READY 时服务端实时刷新 */
export function listCachedRegions(id: number): Promise<RegionSubscription[]> {
if (mockOn) {
const cfg = mockConfigs.find((c) => c.id === id)
if (!cfg) return mocked([])
if (!cfg.multiRegion) {
const info = mockRegions.find((r) => r.name === cfg.region)
return mocked([
{
key: info?.key ?? cfg.homeRegionKey,
name: cfg.region,
status: 'READY',
isHomeRegion: true,
alias: info?.alias ?? '',
},
])
}
return mocked(mockRegionSubs)
}
return request(`/oci-configs/${id}/cached-regions`)
}
/** 筛选器用区间列表(缓存):未开多区间支持返回空数组(前端锁定根) */
export function listCachedCompartments(id: number): Promise<Compartment[]> {
if (mockOn) {
const cfg = mockConfigs.find((c) => c.id === id)
return mocked(cfg?.multiCompartment ? (mockCompartments[id] ?? []) : [])
}
return request(`/oci-configs/${id}/cached-compartments`)
}
+380
View File
@@ -0,0 +1,380 @@
import {
mockAds,
mockBvAttachments,
mockConsoleConns,
mockImages,
mockInstances,
mockShapes,
mockVolAttachments,
mockVolumes,
} from './mock'
import { mockOn, mocked, request } from './request'
import type {
AttachVnicRequest,
BootVolumeAttachment,
ComputeShape,
ConsoleConnection,
CreateInstanceRequest,
CreateInstancesResponse,
ImageInfo,
Instance,
PowerAction,
UpdateInstanceRequest,
Vnic,
Volume,
VolumeAttachment,
} from '@/types/api'
// ---- 创建实例前置查询 ----
export interface ImagesQuery {
region?: string
operatingSystem?: string
shape?: string
}
export function listImages(cfgId: number, query: ImagesQuery = {}): Promise<ImageInfo[]> {
if (mockOn) return mocked(mockImages)
return request(`/oci-configs/${cfgId}/images`, { query: { ...query } })
}
export function getImage(cfgId: number, imageId: string, region?: string): Promise<ImageInfo> {
if (mockOn) {
const found = mockImages.find((i) => i.id === imageId) ?? {
...mockImages[0],
id: imageId,
displayName: 'Canonical-Ubuntu-24.04-aarch64-2026.05.20-0',
}
return mocked(found)
}
return request(`/oci-configs/${cfgId}/images/${encodeURIComponent(imageId)}`, {
query: { region },
})
}
export function listAvailabilityDomains(cfgId: number, region?: string): Promise<string[]> {
if (mockOn) return mocked(mockAds)
return request(`/oci-configs/${cfgId}/availability-domains`, { query: { region } })
}
export function listShapes(
cfgId: number,
region?: string,
availabilityDomain?: string,
): Promise<ComputeShape[]> {
if (mockOn) return mocked(mockShapes)
return request(`/oci-configs/${cfgId}/shapes`, { query: { region, availabilityDomain } })
}
export function listVolumes(
cfgId: number,
availabilityDomain?: string,
compartmentId?: string,
region?: string,
): Promise<Volume[]> {
if (mockOn) return mocked(mockVolumes[cfgId] ?? [])
return request(`/oci-configs/${cfgId}/volumes`, {
query: { availabilityDomain, compartmentId, region },
})
}
export function listInstances(
cfgId: number,
region?: string,
compartmentId?: string,
): Promise<Instance[]> {
if (mockOn) return mocked(compartmentId ? [] : (mockInstances[cfgId] ?? []))
return request(`/oci-configs/${cfgId}/instances`, { query: { region, compartmentId } })
}
export function getInstance(cfgId: number, instanceId: string, region?: string): Promise<Instance> {
if (mockOn) {
const all = Object.values(mockInstances).flat()
const found = all.find((i) => i.id === instanceId)
return found ? mocked(found) : Promise.reject(new Error('实例不存在'))
}
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, {
query: { region },
})
}
export function createInstances(
cfgId: number,
body: CreateInstanceRequest,
): Promise<CreateInstancesResponse> {
if (mockOn) return mocked({ instances: [mockInstances[1][0]], errors: [] }, 1200)
return request(`/oci-configs/${cfgId}/instances`, { method: 'POST', body })
}
export function updateInstance(
cfgId: number,
instanceId: string,
body: UpdateInstanceRequest,
region?: string,
): Promise<Instance> {
if (mockOn) return mocked({ ...mockInstances[1][0], ...body })
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, {
method: 'PUT',
body: { region: region ?? '', ...body },
})
}
export function terminateInstance(
cfgId: number,
instanceId: string,
preserveBootVolume: boolean,
region?: string,
): Promise<void> {
if (mockOn) return mocked(undefined, 600)
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, {
method: 'DELETE',
query: { preserveBootVolume, region },
})
}
export function instanceAction(
cfgId: number,
instanceId: string,
action: PowerAction,
region?: string,
): Promise<Instance> {
if (mockOn) {
const next = action === 'STOP' || action === 'SOFTSTOP' ? 'STOPPING' : 'STARTING'
return mocked({ ...mockInstances[1][0], lifecycleState: next }, 600)
}
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/action`, {
method: 'POST',
body: { region: region ?? '', action },
})
}
export function changePublicIp(
cfgId: number,
instanceId: string,
region?: string,
): Promise<{ publicIp: string }> {
if (mockOn) return mocked({ publicIp: '130.61.99.201' }, 900)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/change-public-ip`,
{ method: 'POST', body: { region: region ?? '' } },
)
}
export function addIpv6(
cfgId: number,
instanceId: string,
address = '',
region?: string,
): Promise<{ ipv6Address: string }> {
if (mockOn) return mocked({ ipv6Address: address || '2603:c020:400d:aa01::2' }, 700)
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/ipv6-addresses`, {
method: 'POST',
body: { region: region ?? '', address },
})
}
export function removeIpv6(
cfgId: number,
instanceId: string,
address: string,
region?: string,
): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/ipv6-addresses`, {
method: 'DELETE',
query: { address, region },
})
}
// ---- VNIC 管理 ----
export function listVnics(cfgId: number, instanceId: string, region?: string): Promise<Vnic[]> {
if (mockOn) return mocked([])
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, {
query: { region },
})
}
export function attachVnic(
cfgId: number,
instanceId: string,
body: AttachVnicRequest,
): Promise<Vnic> {
if (mockOn) return Promise.reject(new Error('mock 环境不支持附加 VNIC'))
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, {
method: 'POST',
body: { ...body, region: body.region ?? '' },
})
}
export function detachVnic(cfgId: number, attachmentId: string, region?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/vnic-attachments/${encodeURIComponent(attachmentId)}`, {
method: 'DELETE',
query: { region },
})
}
/** 为指定 VNIC 添加 IPv6address 留空自动分配。删除复用实例级 removeIpv6(按地址遍历全部网卡) */
export function addVnicIpv6(
cfgId: number,
vnicId: string,
address = '',
region?: string,
): Promise<{ address: string }> {
if (mockOn) return mocked({ address: address || '2603:c020:400d:aa01::3' }, 700)
return request(`/oci-configs/${cfgId}/vnics/${encodeURIComponent(vnicId)}/ipv6-addresses`, {
method: 'POST',
body: { region: region ?? '', address },
})
}
// ---- 网页控制台(串行 / VNC----
export type ConsoleSessionType = 'serial' | 'vnc'
export function createConsoleSession(
cfgId: number,
instanceId: string,
type: ConsoleSessionType,
region?: string,
): Promise<{ sessionId: string; type: ConsoleSessionType }> {
if (mockOn) return Promise.reject(new Error('mock 环境不支持网页控制台'))
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-sessions`,
{ method: 'POST', body: { type, region: region ?? '' } },
)
}
export function deleteConsoleSession(sessionId: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/console-sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' })
}
/** 控制台数据面 WS 地址:浏览器 WS 无法带自定义头,token 走 query */
export function consoleSessionWsUrl(sessionId: string, token: string, extra?: string): string {
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
const q = extra ? `&${extra}` : ''
return `${proto}://${location.host}/api/v1/console-sessions/${encodeURIComponent(sessionId)}/ws?token=${encodeURIComponent(token)}${q}`
}
// ---- 控制台连接 ----
export function listConsoleConnections(
cfgId: number,
instanceId: string,
region?: string,
): Promise<ConsoleConnection[]> {
if (mockOn) return mocked(mockConsoleConns)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-connections`,
{ query: { region } },
)
}
export function createConsoleConnection(
cfgId: number,
instanceId: string,
sshPublicKey: string,
region?: string,
): Promise<ConsoleConnection> {
if (mockOn) return mocked(mockConsoleConns[0], 1500)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-connections`,
{ method: 'POST', body: { region: region ?? '', sshPublicKey } },
)
}
export function deleteConsoleConnection(
cfgId: number,
connectionId: string,
region?: string,
): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/console-connections/${encodeURIComponent(connectionId)}`, {
method: 'DELETE',
query: { region },
})
}
// ---- 引导卷挂载 ----
export function listBootVolumeAttachments(
cfgId: number,
instanceId: string,
region?: string,
): Promise<BootVolumeAttachment[]> {
if (mockOn) return mocked(mockBvAttachments)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/boot-volume-attachments`,
{ query: { region } },
)
}
export function attachBootVolume(
cfgId: number,
instanceId: string,
bootVolumeId: string,
region?: string,
): Promise<BootVolumeAttachment> {
if (mockOn) return mocked(mockBvAttachments[0], 1200)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/boot-volume-attachments`,
{ method: 'POST', body: { region: region ?? '', bootVolumeId } },
)
}
export function replaceBootVolume(
cfgId: number,
instanceId: string,
bootVolumeId: string,
region?: string,
): Promise<BootVolumeAttachment> {
if (mockOn) return mocked(mockBvAttachments[0], 1500)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/replace-boot-volume`,
{ method: 'POST', body: { region: region ?? '', bootVolumeId } },
)
}
export function detachBootVolume(
cfgId: number,
attachmentId: string,
region?: string,
): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/boot-volume-attachments/${encodeURIComponent(attachmentId)}`, {
method: 'DELETE',
query: { region },
})
}
// ---- 块卷挂载 ----
export function listVolumeAttachments(
cfgId: number,
instanceId: string,
region?: string,
): Promise<VolumeAttachment[]> {
if (mockOn) return mocked(mockVolAttachments)
return request(
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/volume-attachments`,
{ query: { region } },
)
}
export function attachVolume(
cfgId: number,
instanceId: string,
volumeId: string,
readOnly = false,
region?: string,
): Promise<VolumeAttachment> {
if (mockOn) return mocked(mockVolAttachments[0], 1000)
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/volume-attachments`, {
method: 'POST',
body: { region: region ?? '', volumeId, readOnly },
})
}
export function detachVolume(cfgId: number, attachmentId: string, region?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/volume-attachments/${encodeURIComponent(attachmentId)}`, {
method: 'DELETE',
query: { region },
})
}
+104
View File
@@ -0,0 +1,104 @@
import { mockLogEvents, mockLogRelays, mockLogWebhooks } from './mock'
import { mockOn, mocked, request } from './request'
import type { LogEventPage, LogRelayView, LogWebhookInfo, LogWebhookState } from '@/types/api'
export interface LogEventParams {
page: number
pageSize: number
/** 按租户过滤,缺省为全部 */
cfgId?: number
}
export function listLogEvents(params: LogEventParams): Promise<LogEventPage> {
if (mockOn) {
const filtered = params.cfgId
? mockLogEvents.filter((e) => e.ociConfigId === params.cfgId)
: mockLogEvents
const start = (params.page - 1) * params.pageSize
return mocked({
items: filtered.slice(start, start + params.pageSize),
total: filtered.length,
})
}
return request('/log-events', { query: { ...params } })
}
export function getLogWebhook(cfgId: number): Promise<LogWebhookState> {
if (mockOn) {
const info = mockLogWebhooks.get(cfgId)
return mocked(info ? { exists: true, webhook: { ...info } } : { exists: false })
}
return request(`/oci-configs/${cfgId}/log-webhook`)
}
/** 生成(或幂等返回)回传回调地址 */
export function ensureLogWebhook(cfgId: number): Promise<LogWebhookInfo> {
if (mockOn) {
let info = mockLogWebhooks.get(cfgId)
if (!info) {
const secret = `mock${cfgId}`.padEnd(64, '0')
info = {
path: `/api/v1/webhooks/oci-logs/${secret}`,
secret,
createdAt: new Date().toISOString(),
}
mockLogWebhooks.set(cfgId, info)
}
return mocked({ ...info }, 400)
}
return request(`/oci-configs/${cfgId}/log-webhook`, { method: 'POST' })
}
/** 撤销回调地址,旧 URL 随即 404 */
export function revokeLogWebhook(cfgId: number): Promise<void> {
if (mockOn) {
mockLogWebhooks.delete(cfgId)
return mocked(undefined, 300)
}
return request(`/oci-configs/${cfgId}/log-webhook`, { method: 'DELETE' })
}
/** 查询 OCI 侧链路状态与关键事件清单 */
export function getLogRelay(cfgId: number): Promise<LogRelayView> {
if (mockOn) return mocked(structuredCloneRelay(cfgId))
return request(`/oci-configs/${cfgId}/log-relay`)
}
/** 一键建立回传链路(P1):同步执行,含订阅确认与 Connector 就绪等待,可达 2 分钟 */
export function setupLogRelay(cfgId: number): Promise<LogRelayView> {
if (mockOn) {
mockLogRelays.set(cfgId, true)
return mocked(structuredCloneRelay(cfgId), 1500)
}
return request(`/oci-configs/${cfgId}/log-relay`, { method: 'POST' })
}
/** 销毁 OCI 侧链路并撤销回调地址 */
export function teardownLogRelay(cfgId: number): Promise<void> {
if (mockOn) {
mockLogRelays.delete(cfgId)
mockLogWebhooks.delete(cfgId)
return mocked(undefined, 800)
}
return request(`/oci-configs/${cfgId}/log-relay`, { method: 'DELETE' })
}
/** mock 模式下按已建状态拼一份链路视图 */
function structuredCloneRelay(cfgId: number): LogRelayView {
const built = mockLogRelays.get(cfgId) === true
const res = (suffix: string) => (built ? { id: `ocid1.mock.${suffix}`, state: 'ACTIVE' } : { id: '', state: '' })
return {
endpoint: built ? `https://demo.example.com/api/v1/webhooks/oci-logs/mock${cfgId}` : undefined,
topic: res('topic'),
subscription: res('sub'),
connector: res('conn'),
policy: res('policy'),
ready: built,
events: [
'LaunchInstance', 'TerminateInstance', 'InstanceAction',
'CreateUser', 'DeleteUser', 'UpdateUser',
'CreateApiKey', 'DeleteApiKey', 'UpdateUserCapabilities',
'CreateRegionSubscription', 'CreatePolicy', 'UpdatePolicy', 'DeletePolicy',
],
}
}
+1716
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
import { mockIpv6Report, mockSecurityLists, mockSubnets, mockVcns } from './mock'
import { mockOn, mocked, request } from './request'
import type { CreateVcnRequest, Ipv6Report, SecurityList, Subnet, Vcn } from '@/types/api'
export function listVcns(cfgId: number, region?: string, compartmentId?: string): Promise<Vcn[]> {
if (mockOn) return mocked(compartmentId ? [] : (mockVcns[cfgId] ?? []))
return request(`/oci-configs/${cfgId}/vcns`, { query: { region, compartmentId } })
}
export function getVcn(cfgId: number, vcnId: string, region?: string): Promise<Vcn> {
if (mockOn) {
const found = Object.values(mockVcns)
.flat()
.find((v) => v.id === vcnId)
return found ? mocked(found) : Promise.reject(new Error('VCN 不存在'))
}
return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}`, { query: { region } })
}
export function createVcn(cfgId: number, body: CreateVcnRequest): Promise<Vcn> {
if (mockOn) return mocked({ ...mockVcns[1][0], id: 'ocid1.vcn..new', displayName: body.displayName }, 900)
return request(`/oci-configs/${cfgId}/vcns`, { method: 'POST', body })
}
export function renameVcn(
cfgId: number,
vcnId: string,
displayName: string,
region?: string,
): Promise<Vcn> {
if (mockOn) return mocked({ ...mockVcns[1][0], displayName })
return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}`, {
method: 'PUT',
body: { region: region ?? '', displayName },
})
}
export function deleteVcn(cfgId: number, vcnId: string, region?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}`, {
method: 'DELETE',
query: { region },
})
}
export function enableVcnIpv6(cfgId: number, vcnId: string, region?: string): Promise<Ipv6Report> {
if (mockOn) return mocked(mockIpv6Report, 1500)
return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}/enable-ipv6`, {
method: 'POST',
body: { region: region ?? '' },
})
}
export function listSubnets(cfgId: number, vcnId: string, region?: string): Promise<Subnet[]> {
if (mockOn) return mocked(mockSubnets.filter((s) => s.vcnId === vcnId || vcnId === ''))
return request(`/oci-configs/${cfgId}/subnets`, { query: { vcnId, region } })
}
export function listSecurityLists(
cfgId: number,
vcnId: string,
region?: string,
): Promise<SecurityList[]> {
if (mockOn) return mocked(mockSecurityLists.filter((s) => s.vcnId === vcnId || vcnId === ''))
return request(`/oci-configs/${cfgId}/security-lists`, { query: { vcnId, region } })
}
+90
View File
@@ -0,0 +1,90 @@
import { mockProxies } from './mock'
import { mockOn, mocked, request } from './request'
import type { ProxyImportResult, ProxyInfo, ProxyInput } from '@/types/api'
export function listProxies(): Promise<{ items: ProxyInfo[] }> {
if (mockOn) return mocked({ items: mockProxies })
return request('/proxies')
}
/** mock 端复刻后端自动命名:留空生成 {type}-{host}:{port},冲突加序号 */
function mockAutoName(body: ProxyInput): string {
const base = `${body.type}-${body.host}:${body.port}`
let name = base
for (let i = 2; mockProxies.some((p) => p.name === name); i++) name = `${base}-${i}`
return name
}
function mockCreate(body: ProxyInput): ProxyInfo {
const created: ProxyInfo = {
id: Math.max(0, ...mockProxies.map((p) => p.id)) + 1,
name: body.name?.trim() || mockAutoName(body),
type: body.type,
host: body.host,
port: body.port,
username: body.username ?? '',
passwordSet: !!body.password,
country: '',
city: '',
geoAt: '',
usedBy: 0,
createdAt: new Date().toISOString(),
}
mockProxies.push(created)
return created
}
export function createProxy(body: ProxyInput): Promise<ProxyInfo> {
if (mockOn) return mocked(mockCreate(body))
return request('/proxies', { method: 'POST', body })
}
export function updateProxy(id: number, body: ProxyInput): Promise<ProxyInfo> {
if (mockOn) {
const found = mockProxies.find((p) => p.id === id)
if (!found) return Promise.reject(new Error('代理不存在'))
Object.assign(found, body, {
name: body.name?.trim() || found.name,
passwordSet: body.password ? true : found.passwordSet,
})
return mocked({ ...found })
}
return request(`/proxies/${id}`, { method: 'PUT', body })
}
export function deleteProxy(id: number): Promise<void> {
if (mockOn) {
const i = mockProxies.findIndex((p) => p.id === id)
if (i >= 0) mockProxies.splice(i, 1)
return mocked(undefined)
}
return request(`/proxies/${id}`, { method: 'DELETE' })
}
/** 批量导入:每行 scheme://[user:pass@]host:port 或 host:port[:user:pass] */
export function importProxies(text: string): Promise<ProxyImportResult> {
if (mockOn) {
const created = text
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('#'))
.map((l) => {
const m = /^(?:(socks5|http|https):\/\/)?(?:[^@]+@)?([^:@/]+):(\d+)/.exec(l)
return m ? mockCreate({ type: m[1] ?? 'socks5', host: m[2], port: Number(m[3]) }) : null
})
.filter((p): p is ProxyInfo => !!p)
return mocked({ created, failed: [] })
}
return request('/proxies/import', { method: 'POST', body: { text } })
}
/** 手动重测出口地区,同步返回最新视图 */
export function probeProxy(id: number): Promise<ProxyInfo> {
if (mockOn) {
const found = mockProxies.find((p) => p.id === id)
if (!found) return Promise.reject(new Error('代理不存在'))
Object.assign(found, { country: '日本', city: '大阪市', geoAt: new Date().toISOString() })
return mocked({ ...found })
}
return request(`/proxies/${id}/probe`, { method: 'POST' })
}
+66
View File
@@ -0,0 +1,66 @@
import { useAuthStore } from '@/stores/auth'
const BASE = '/api/v1'
export class ApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.status = status
}
}
interface RequestOptions {
method?: string
body?: unknown
query?: Record<string, string | number | boolean | undefined>
}
function buildUrl(path: string, query?: RequestOptions['query']): string {
if (!query) return BASE + path
const qs = new URLSearchParams()
for (const [k, v] of Object.entries(query)) {
if (v !== undefined && v !== '') qs.set(k, String(v))
}
const s = qs.toString()
return s ? `${BASE}${path}?${s}` : BASE + path
}
async function parseError(resp: Response): Promise<never> {
let message = `请求失败(${resp.status}`
try {
const body = (await resp.json()) as { error?: string; hint?: string }
if (body.error) message = body.hint ? `${body.hint}${body.error}` : body.error
} catch {
/* 非 JSON 响应体,保留默认消息 */
}
if (resp.status === 401) useAuthStore().logout()
// 全局限速(429)整页拦截;登录接口的 429 是账号锁定,留在表单内提示
if (resp.status === 429 && !resp.url.includes('/auth/login') && location.pathname !== '/blocked') {
location.assign('/blocked')
}
throw new ApiError(resp.status, message)
}
/** 统一请求封装:注入 JWT、401 登出、错误转 ApiError */
export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
const auth = useAuthStore()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (auth.token) headers.Authorization = `Bearer ${auth.token}`
const resp = await fetch(buildUrl(path, opts.query), {
method: opts.method ?? 'GET',
headers,
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
})
if (!resp.ok) await parseError(resp)
if (resp.status === 204) return undefined as T
return (await resp.json()) as T
}
export const mockOn = import.meta.env.VITE_MOCK === '1'
/** mock 数据延时返回,模拟网络延迟 */
export function mocked<T>(data: T, ms = 200): Promise<T> {
return new Promise((resolve) => setTimeout(() => resolve(structuredClone(data)), ms))
}
+152
View File
@@ -0,0 +1,152 @@
import {
mockNotifyEvents,
mockSecuritySetting,
mockSystemLogs,
mockTaskSetting,
mockTelegramSetting,
} from './mock'
import { mockOn, mocked, request } from './request'
import type {
AboutInfo,
NotifyEventsSetting,
NotifyTemplateItem,
SecuritySetting,
SystemLogPage,
TaskSetting,
TelegramSetting,
} from '@/types/api'
export function getTelegramSetting(): Promise<TelegramSetting> {
if (mockOn) return mocked({ ...mockTelegramSetting })
return request('/settings/telegram')
}
export interface UpdateTelegramBody {
enabled: boolean
/** 缺省表示沿用已保存的 token,空串表示清除 */
botToken?: string
chatId: string
}
export function updateTelegramSetting(body: UpdateTelegramBody): Promise<TelegramSetting> {
if (mockOn) {
mockTelegramSetting.enabled = body.enabled
mockTelegramSetting.chatId = body.chatId
if (body.botToken !== undefined) {
mockTelegramSetting.tokenSet = body.botToken !== ''
mockTelegramSetting.tokenTail = body.botToken.length >= 8 ? body.botToken.slice(-4) : ''
}
return mocked({ ...mockTelegramSetting }, 400)
}
return request('/settings/telegram', { method: 'PUT', body })
}
export function testTelegram(): Promise<void> {
if (mockOn) return mocked(undefined, 800)
return request('/settings/telegram/test', { method: 'POST' })
}
export function getNotifyEvents(): Promise<NotifyEventsSetting> {
if (mockOn) return mocked({ ...mockNotifyEvents })
return request('/settings/notify-events')
}
/** 全量保存五个事件开关,返回最新值 */
export function updateNotifyEvents(body: NotifyEventsSetting): Promise<NotifyEventsSetting> {
if (mockOn) {
Object.assign(mockNotifyEvents, body)
return mocked({ ...mockNotifyEvents }, 400)
}
return request('/settings/notify-events', { method: 'PUT', body })
}
export function listNotifyTemplates(): Promise<NotifyTemplateItem[]> {
if (mockOn)
return mocked([
{
kind: 'task_fail',
label: '任务失败',
vars: ['task_name', 'error'],
defaultTemplate: '❌ 任务失败:{{task_name}}\n{{error}}',
template: '',
},
])
return request<{ items: NotifyTemplateItem[] }>('/settings/notify-templates').then((r) => r.items)
}
/** 保存单个通知模板;空串清除自定义(恢复默认) */
export function updateNotifyTemplate(kind: string, template: string): Promise<void> {
if (mockOn) return mocked(undefined, 300)
return request(`/settings/notify-templates/${kind}`, { method: 'PUT', body: { template } })
}
/** 用示例变量渲染并发送测试消息;template 非空时按编辑中内容渲染(不落库) */
export function testNotifyTemplate(kind: string, template: string): Promise<void> {
if (mockOn) return mocked(undefined, 600)
return request(`/settings/notify-templates/${kind}/test`, { method: 'POST', body: { template } })
}
export function getTaskSetting(): Promise<TaskSetting> {
if (mockOn) return mocked({ ...mockTaskSetting })
return request('/settings/task')
}
/** 保存任务行为设置,返回最新值;阈值越界(1-10 之外)后端返回 400 */
export function updateTaskSetting(body: TaskSetting): Promise<TaskSetting> {
if (mockOn) {
Object.assign(mockTaskSetting, body)
return mocked({ ...mockTaskSetting }, 400)
}
return request('/settings/task', { method: 'PUT', body })
}
export function getSecuritySetting(): Promise<SecuritySetting> {
if (mockOn) return mocked({ ...mockSecuritySetting })
return request('/settings/security')
}
/** 保存安全设置,返回最新值;越界或非法后端返回 400,保存后立即生效 */
export function updateSecuritySetting(body: SecuritySetting): Promise<SecuritySetting> {
if (mockOn) {
Object.assign(mockSecuritySetting, body)
return mocked({ ...mockSecuritySetting }, 400)
}
return request('/settings/security', { method: 'PUT', body })
}
export interface SystemLogParams {
page: number
pageSize: number
/** 关键字,对路径与用户名模糊匹配 */
keyword?: string
}
export function listSystemLogs(params: SystemLogParams): Promise<SystemLogPage> {
if (mockOn) {
const kw = (params.keyword ?? '').toLowerCase()
const filtered = kw
? mockSystemLogs.filter(
(l) => l.path.toLowerCase().includes(kw) || l.username.toLowerCase().includes(kw),
)
: mockSystemLogs
const start = (params.page - 1) * params.pageSize
return mocked({
items: filtered.slice(start, start + params.pageSize),
total: filtered.length,
})
}
return request('/system-logs', { query: { ...params } })
}
// ---- 关于 ----
/** 构建与运行环境信息(设置 · 关于) */
export function getAbout(): Promise<AboutInfo> {
if (mockOn)
return mocked({
version: 'v1.0.0',
buildTime: '2026-07-09T12:00Z',
goVersion: 'go1.26.4',
platform: 'linux/amd64',
})
return request('/about')
}
+75
View File
@@ -0,0 +1,75 @@
import { mockTaskLogs, mockTasks } from './mock'
import { mockOn, mocked, request } from './request'
import type { Task, TaskLog, TaskStatus } from '@/types/api'
export function listTasks(): Promise<Task[]> {
if (mockOn) return mocked(mockTasks)
return request('/tasks')
}
export function getTask(id: number): Promise<Task> {
if (mockOn) {
const found = mockTasks.find((t) => t.id === id)
return found ? mocked(found) : Promise.reject(new Error('任务不存在'))
}
return request(`/tasks/${id}`)
}
export interface CreateTaskRequest {
name: string
type: string
cronExpr: string
payload: Record<string, unknown>
}
export function createTask(body: CreateTaskRequest): Promise<Task> {
if (mockOn) {
const created: Task = {
...mockTasks[0],
id: Math.max(...mockTasks.map((t) => t.id)) + 1,
name: body.name,
type: body.type as Task['type'],
cronExpr: body.cronExpr,
payload: JSON.stringify(body.payload),
status: 'active',
lastRunAt: null,
runCount: 0,
}
mockTasks.push(created)
return mocked(created, 600)
}
return request('/tasks', { method: 'POST', body })
}
export interface UpdateTaskRequest {
name?: string
cronExpr?: string
payload?: Record<string, unknown>
status?: TaskStatus
}
export function updateTask(id: number, body: UpdateTaskRequest): Promise<Task> {
if (mockOn) {
const base = mockTasks.find((t) => t.id === id) ?? mockTasks[0]
return mocked({ ...base, status: body.status ?? base.status })
}
return request(`/tasks/${id}`, { method: 'PUT', body })
}
export function deleteTask(id: number): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/tasks/${id}`, { method: 'DELETE' })
}
export function listTaskLogs(id: number, limit = 50): Promise<TaskLog[]> {
if (mockOn) return mocked(mockTaskLogs[id] ?? [])
return request(`/tasks/${id}/logs`, { query: { limit } })
}
export function runTask(id: number): Promise<TaskLog> {
if (mockOn) {
const logs = mockTaskLogs[id]
return mocked(logs?.[0] ?? { id: 0, taskId: id, success: true, message: 'ok', durationMs: 100, createdAt: '' }, 1000)
}
return request(`/tasks/${id}/run`, { method: 'POST' })
}
+344
View File
@@ -0,0 +1,344 @@
import {
mockAuditEvents,
mockIdentitySetting,
mockIdps,
mockLimits,
mockPolicies,
mockRecipients,
mockSignOnRules,
mockSubscriptions,
mockUserDetails,
mockUsers,
} from './mock'
import { mockOn, mocked, request } from './request'
import type {
AuditEventsResult,
IamUser,
IamUserDetail,
IdentityProvider,
IdentitySetting,
LimitItem,
LimitService,
NotificationRecipients,
PasswordPolicy,
SignOnRule,
SubscriptionDetail,
SubscriptionSummary,
} from '@/types/api'
// ---- 配额 ----
export interface LimitsQuery {
service?: string
region?: string
name?: string
scopeType?: 'GLOBAL' | 'REGION' | 'AD'
availabilityDomain?: string
/** 服务端过滤上限为 0 的配额项,查用量时不占并发名额 */
nonZero?: boolean
withAvailability?: boolean
}
export function listLimits(id: number, query: LimitsQuery = {}): Promise<LimitItem[]> {
if (mockOn) {
if (query.service === 'regions')
return mocked([{ name: 'subscribed-region-count', scopeType: 'GLOBAL', value: 3 }])
const needle = (query.name ?? '').toLowerCase()
return mocked(
mockLimits
.filter((l) => !needle || l.name.toLowerCase().includes(needle))
.filter((l) => !query.scopeType || l.scopeType === query.scopeType)
.filter((l) => !query.nonZero || l.value > 0),
)
}
return request(`/oci-configs/${id}/limits`, { query: { ...query } })
}
export function listLimitServices(id: number): Promise<LimitService[]> {
if (mockOn)
return mocked([
{ name: 'compute', description: 'Compute' },
{ name: 'vcn', description: 'Virtual Cloud Network' },
{ name: 'block-storage', description: 'Block Volume' },
])
return request(`/oci-configs/${id}/limits/services`)
}
// ---- 订阅 ----
export function listSubscriptions(id: number): Promise<SubscriptionSummary[]> {
if (mockOn)
return mocked(
mockSubscriptions.map((d) => ({
id: d.id,
serviceName: d.serviceName,
timeCreated: d.timeCreated,
timeUpdated: d.timeUpdated,
})),
)
return request(`/oci-configs/${id}/subscriptions`)
}
export function getSubscription(id: number, subscriptionId: string): Promise<SubscriptionDetail> {
if (mockOn)
return mocked(mockSubscriptions.find((d) => d.id === subscriptionId) ?? mockSubscriptions[0])
return request(`/oci-configs/${id}/subscriptions/${subscriptionId}`)
}
// ---- 用户 ----
export function listUsers(id: number): Promise<IamUser[]> {
if (mockOn) return mocked(mockUsers)
return request(`/oci-configs/${id}/users`)
}
// 编辑表单预填充用;不在身份域的经典用户返回 inDomain: false
export function getUserDetail(id: number, userId: string): Promise<IamUserDetail> {
if (mockOn)
return mocked(
mockUserDetails[userId] ?? {
inDomain: false,
givenName: '',
familyName: '',
isDomainAdmin: false,
inAdminGroup: false,
},
)
return request(`/oci-configs/${id}/users/${userId}`)
}
export interface CreateUserBody {
name: string
givenName: string
familyName: string
description?: string
email?: string
/** 同时勾选时后端先授身份域管理员,再加入管理员组 */
grantDomainAdmin?: boolean
addToAdminGroup?: boolean
}
export function createUser(id: number, body: CreateUserBody): Promise<IamUser> {
if (mockOn)
return mocked({ ...mockUsers[2], id: 'ocid1.user..new', name: body.name, email: body.email ?? '' })
return request(`/oci-configs/${id}/users`, { method: 'POST', body })
}
export interface UpdateUserBody {
description?: string
email?: string
givenName?: string
familyName?: string
/** 三态:true 授予/加入,false 撤销/移出,缺省不动 */
grantDomainAdmin?: boolean
addToAdminGroup?: boolean
}
export function updateUser(id: number, userId: string, body: UpdateUserBody): Promise<IamUser> {
if (mockOn) {
const u = mockUsers.find((x) => x.id === userId) ?? mockUsers[2]
if (body.description !== undefined) u.description = body.description
if (body.email !== undefined) u.email = body.email
return mocked({ ...u })
}
return request(`/oci-configs/${id}/users/${userId}`, { method: 'PUT', body })
}
export function deleteUser(id: number, userId: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/users/${userId}`, { method: 'DELETE' })
}
export function resetUserPassword(id: number, userId: string): Promise<{ password: string }> {
if (mockOn) return mocked({ password: 'Xy9#kQ2m$Lp8' })
return request(`/oci-configs/${id}/users/${userId}/reset-password`, { method: 'POST' })
}
export function clearUserMfa(id: number, userId: string): Promise<{ deletedTotpDevices: number }> {
if (mockOn) return mocked({ deletedTotpDevices: 1 })
return request(`/oci-configs/${id}/users/${userId}/mfa-devices`, { method: 'DELETE' })
}
export function deleteUserApiKeys(
id: number,
userId: string,
includeCurrent = false,
): Promise<{ deletedApiKeys: number }> {
if (mockOn) return mocked({ deletedApiKeys: 1 })
return request(`/oci-configs/${id}/users/${userId}/api-keys`, {
method: 'DELETE',
query: { includeCurrent },
})
}
// ---- 审计日志 ----
/** 实时查询租户 OCI 审计事件:hours ∈ [1,72] 首查;
* 截断续查传上次响应的 start/end 与 nextPage(绝对窗+游标断点续翻) */
export function listAuditEvents(
id: number,
opts: { hours?: number; region?: string; start?: string; end?: string; page?: string } = {},
): Promise<AuditEventsResult> {
// mock 下选 1 小时窗演示截断提示
if (mockOn)
return mocked(
{
items: mockAuditEvents,
truncated: opts.hours === 1,
start: '2026-07-06T10:00:00Z',
end: '2026-07-07T10:00:00Z',
},
300,
)
const { hours = 24, region, start, end, page } = opts
const query = start && end ? { region, start, end, page } : { hours, region }
return request(`/oci-configs/${id}/audit-events`, { query })
}
/** 取回单条审计事件的原始 JSON;404 表示缓存过期且重查未找回(前端降级展示精简字段) */
export function getAuditEventDetail(
id: number,
q: { eventId: string; eventTime: string; region?: string },
): Promise<{ raw: unknown }> {
if (mockOn) return mocked({ raw: { eventId: q.eventId, mock: true, note: '演示原始事件' } }, 200)
return request(`/oci-configs/${id}/audit-events/detail`, { query: q })
}
// ---- 域设置 ----
export function getNotificationRecipients(id: number): Promise<NotificationRecipients> {
if (mockOn) return mocked(mockRecipients)
return request(`/oci-configs/${id}/notification-recipients`)
}
export function updateNotificationRecipients(
id: number,
recipients: string[],
): Promise<NotificationRecipients> {
if (mockOn) return mocked({ recipients, testModeEnabled: recipients.length > 0 })
return request(`/oci-configs/${id}/notification-recipients`, {
method: 'PUT',
body: { recipients },
})
}
export function listPasswordPolicies(id: number): Promise<PasswordPolicy[]> {
if (mockOn) return mocked(mockPolicies)
return request(`/oci-configs/${id}/password-policies`)
}
export function updatePasswordPolicy(
id: number,
policyId: string,
body: { passwordExpiresAfter?: number; minLength?: number; numPasswordsInHistory?: number },
): Promise<PasswordPolicy> {
if (mockOn) return mocked({ ...mockPolicies[0], ...body })
return request(`/oci-configs/${id}/password-policies/${policyId}`, { method: 'PUT', body })
}
// ---- 身份设置 ----
export function getIdentitySetting(id: number): Promise<IdentitySetting> {
if (mockOn) return mocked({ ...mockIdentitySetting })
return request(`/oci-configs/${id}/identity-settings`)
}
export function updateIdentitySetting(
id: number,
primaryEmailRequired: boolean,
): Promise<IdentitySetting> {
if (mockOn) {
mockIdentitySetting.primaryEmailRequired = primaryEmailRequired
return mocked({ ...mockIdentitySetting })
}
return request(`/oci-configs/${id}/identity-settings`, {
method: 'PUT',
body: { primaryEmailRequired },
})
}
export async function downloadSamlMetadata(id: number): Promise<void> {
if (mockOn) {
const blob = new Blob(['<mock-saml-metadata/>'], { type: 'application/xml' })
triggerDownload(blob, 'oci-domain-saml-metadata.xml')
return
}
const { useAuthStore } = await import('@/stores/auth')
const auth = useAuthStore()
const resp = await fetch(`/api/v1/oci-configs/${id}/saml-metadata`, {
headers: auth.token ? { Authorization: `Bearer ${auth.token}` } : {},
})
if (!resp.ok) throw new Error(`下载失败(${resp.status}`)
triggerDownload(await resp.blob(), 'oci-domain-saml-metadata.xml')
}
function triggerDownload(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
// ---- Federation ----
export interface CreateIdpBody {
name: string
metadata: string
description?: string
/** logodata URI 或外链 URL */
iconUrl?: string
nameIdFormat?: string
/** 非空表示按断言属性映射;空则按 SAML NameID 映射 */
assertionAttribute?: string
userStoreAttribute?: string
jitEnabled?: boolean
jitCreate?: boolean
jitUpdate?: boolean
jitAssignAdminGroup?: boolean
jitMapEmail?: boolean
}
export function createIdp(id: number, body: CreateIdpBody): Promise<IdentityProvider> {
if (mockOn) {
const created = {
...mockIdps[1],
id: `idp-${mockIdps.length + 1}`,
name: body.name,
enabled: false,
}
mockIdps.push(created)
return mocked(created, 900)
}
return request(`/oci-configs/${id}/identity-providers`, { method: 'POST', body })
}
export function listIdps(id: number): Promise<IdentityProvider[]> {
if (mockOn) return mocked(mockIdps)
return request(`/oci-configs/${id}/identity-providers`)
}
export function activateIdp(id: number, idpId: string, enabled: boolean): Promise<IdentityProvider> {
if (mockOn) return mocked({ ...mockIdps[0], enabled })
return request(`/oci-configs/${id}/identity-providers/${idpId}/activate`, {
method: 'POST',
body: { enabled },
})
}
export function deleteIdp(id: number, idpId: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/identity-providers/${idpId}`, { method: 'DELETE' })
}
export function listSignOnRules(id: number): Promise<SignOnRule[]> {
if (mockOn) return mocked(mockSignOnRules)
return request(`/oci-configs/${id}/sign-on-rules`)
}
export function createSignOnExemption(id: number, identityProviderId: string): Promise<SignOnRule> {
if (mockOn) return mocked(mockSignOnRules[0])
return request(`/oci-configs/${id}/sign-on-exemptions`, {
method: 'POST',
body: { identityProviderId },
})
}
export function deleteSignOnExemption(id: number, ruleId: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/sign-on-exemptions/${ruleId}`, { method: 'DELETE' })
}