import { mockAds, mockBvAttachments, mockConsoleConns, mockImages, mockInstances, mockShapes, mockVolAttachments, mockVolumes, } from './mock' import { reactive } from 'vue' import { ApiError, 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 { if (mockOn) return mocked(mockImages) return request(`/oci-configs/${cfgId}/images`, { query: { ...query } }) } export function getImage(cfgId: number, imageId: string, region?: string): Promise { 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 { if (mockOn) return mocked(mockAds) return request(`/oci-configs/${cfgId}/availability-domains`, { query: { region } }) } export function listShapes( cfgId: number, region?: string, availabilityDomain?: string, ): Promise { 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 { 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 { 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 { 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 { 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 { if (mockOn) return mocked({ ...mockInstances[1][0], ...body }) return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, { method: 'PUT', body: { region: region ?? '', ...body }, }) } /** 终止请求飞行中的实例(instance OCID):模块级响应式集合,列表页、 * 详情页与组件重挂载共用一把锁,防「关闭重开对话框后用相反的 * preserveBootVolume 并发提交」;跨浏览器页签由服务端同键 guard 兜底 */ export const terminatingInstances = reactive(new Set()) /** Instance OCID 唯一标识物理实例;cfgId 不得拆成两把终止锁 */ export function terminatingKey(_cfgId: number, instanceId: string): string { return instanceId } export async function terminateInstance( cfgId: number, instanceId: string, preserveBootVolume: boolean, region?: string, ): Promise { const key = terminatingKey(cfgId, instanceId) if (terminatingInstances.has(key)) { throw new ApiError(409, '该实例的终止请求正在处理中') } terminatingInstances.add(key) try { if (mockOn) return await mocked(undefined, 600) return await requestTerminate(cfgId, instanceId, preserveBootVolume, region) } finally { terminatingInstances.delete(key) } } function requestTerminate( cfgId: number, instanceId: string, preserveBootVolume: boolean, region?: string, ): Promise { 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 { 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 { 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 { if (mockOn) return mocked([ { attachmentId: 'ocid1.vnicattachment..a1', vnicId: 'ocid1.vnic..v1', displayName: instanceId.slice(-12), isPrimary: true, privateIp: '10.0.0.11', publicIp: '141.147.60.10', ipv6Addresses: ['2603:c020:400d:aa01::1'], subnetId: 'ocid1.subnet..pub', subnetName: 'public-subnet', lifecycleState: 'ATTACHED', timeCreated: '2026-05-20T14:32:00Z', }, { attachmentId: 'ocid1.vnicattachment..a2', vnicId: 'ocid1.vnic..v2', displayName: 'vnic-secondary', isPrimary: false, privateIp: '10.0.0.12', publicIp: '', ipv6Addresses: [], subnetId: 'ocid1.subnet..pub', subnetName: 'public-subnet', lifecycleState: 'ATTACHED', timeCreated: '2026-07-20T08:00:00Z', }, ]) return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, { query: { region }, }) } export function attachVnic( cfgId: number, instanceId: string, body: AttachVnicRequest, ): Promise { 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 { if (mockOn) return mocked(undefined) return request(`/oci-configs/${cfgId}/vnic-attachments/${encodeURIComponent(attachmentId)}`, { method: 'DELETE', query: { region }, }) } /** 更换指定 VNIC 的临时公网 IP,返回新地址;绑定保留 IP 的网卡会被拒绝 */ export function changeVnicPublicIp( cfgId: number, vnicId: string, region?: string, ): Promise<{ publicIp: string }> { if (mockOn) return mocked({ publicIp: '130.61.99.7' }, 800) return request(`/oci-configs/${cfgId}/vnics/${encodeURIComponent(vnicId)}/change-public-ip`, { method: 'POST', body: { region: region ?? '' }, }) } /** 为指定 VNIC 添加 IPv6;address 留空自动分配。删除复用实例级 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { if (mockOn) return mocked(undefined) return request(`/oci-configs/${cfgId}/volume-attachments/${encodeURIComponent(attachmentId)}`, { method: 'DELETE', query: { region }, }) }