vdi-cloud/tools/generate-platform-openapi.mjs

282 lines
37 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { mkdirSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
const string = { type: 'string' }, integer = { type: 'integer' }, number = { type: 'number' }, boolean = { type: 'boolean' }
const date = { type: 'string', format: 'date-time' }
const ref = name => ({ $ref: `#/components/schemas/${name}` })
const array = items => ({ type: 'array', items })
const object = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false })
const schemas = {
Error: object({ code: string, message: string, requestId: string }),
Administrator: object({ id: string, username: string, displayName: string, role: { type: 'string', enum: ['super_admin', 'operator', 'viewer'] } }),
LoginInput: object({ username: string, password: string, code: { ...string, maxLength: 80 } }, ['username', 'password']),
MFAStatus: object({ enabled: boolean, recoveryRemaining: integer }),
MFASetupInput: object({ password: { ...string, format: 'password', maxLength: 200 } }),
MFASetup: object({ secret: string, provisioningUri: string, expiresAt: date }),
MFAConfirm: object({ code: { ...string, minLength: 6, maxLength: 6 } }),
MFACredential: object({ password: { ...string, format: 'password', maxLength: 200 }, code: { ...string, maxLength: 80 } }),
MFARecovery: object({ recoveryCodes: array(string) }),
LoginResult: object({ accessToken: string, expiresIn: integer, user: ref('Administrator') }),
Desktop: object({ id: string, name: string, os: { type: 'string', enum: ['Windows', 'Ubuntu', '麒麟'] }, node: string, vmid: integer, pool: string, powerState: string, serviceState: string, provider: { type: 'string', enum: ['demo', 'proxmox'] }, endpoint: string, app: string, width: integer, height: integer, fps: integer, updatedAt: date }),
Terminal: object({ id: string, sn: string, name: string, group: string, status: string, agentVersion: string, lastSeen: { ...date, nullable: true }, sessionDesktopId: { ...string, nullable: true }, configVersion: integer, fps: { ...number, nullable: true }, rtt: { ...number, nullable: true } }),
User: object({ id: string, username: string, displayName: string, enabled: boolean, terminalId: { ...string, nullable: true }, desktopIds: array(string) }),
Resource: object({ id: string, name: string, status: string, cpu: number, memory: number, storage: number, provider: string, updatedAt: date }),
GPUReference: object({ vmid: integer, vmName: string, vmStatus: string, template: boolean, desktopId: string, desktopName: string, slot: string, phase: { type: 'string', enum: ['current', 'pending'] }, source: { type: 'string', enum: ['direct', 'mapping'] }, mapping: string, mediated: boolean, pendingRemoval: boolean }),
GPUHealth: object({ status: string, source: string, observedAt: date, temperatureC: { ...number, nullable: true }, utilizationPercent: { ...number, nullable: true }, memoryUsedBytes: { ...integer, nullable: true }, memoryTotalBytes: { ...integer, nullable: true }, driver: string, driverBinding: { type: 'string', enum: ['bound', 'unknown'] } }),
GPUItem: object({ id: string, address: string, name: string, vendor: string, vendorId: string, deviceId: string, class: string, iommuGroup: { ...integer, nullable: true }, state: { type: 'string', enum: ['unreferenced', 'unknown', 'referenced', 'multiple'] }, health: ref('GPUHealth'), references: array(ref('GPUReference')) }),
GPUIssue: object({ code: string, vmid: integer, message: string }),
GPUInventory: object({ node: string, observedAt: date, complete: boolean, visibleVmCount: integer, scannedVmCount: integer, items: array(ref('GPUItem')), issues: array(ref('GPUIssue')), health: object({ status: string, source: string, temperature: string, utilization: string, memory: string, driverBinding: string }) }),
Task: object({ id: string, kind: string, action: string, targetId: string, targetName: string, actor: string, status: { type: 'string', enum: ['queued', 'running', 'success', 'failed', 'timeout'] }, message: string, providerRef: string, createdAt: date, updatedAt: date }),
Audit: object({ id: string, actor: string, action: string, target: string, result: string, requestId: string, createdAt: date }),
Overview: object({ terminalCount: integer, onlineTerminals: integer, desktopCount: integer, runningDesktops: integer, activeSessions: integer, failedTasks: integer, mode: string }),
Settings: object({ mode: { type: 'string', enum: ['demo', 'production'] }, computeProvider: string, pveConfigured: boolean, terminalTransport: string, capabilities: array(string) }),
ImportInput: object({ name: string, os: { type: 'string', enum: ['Windows', 'Ubuntu', '麒麟'] }, node: string, vmid: integer, endpoint: string, width: integer, height: integer, fps: integer }, ['name', 'os', 'node', 'vmid']),
DiscoveredVM: object({ name: string, node: string, vmid: integer, status: string }),
EnrollmentInput: object({ sn: string, name: string, group: string }),
Enrollment: object({ id: string, activationSecret: string }),
TerminalUpdate: object({ name: string, group: string }),
UserUpdate: object({ displayName: string, enabled: boolean }),
BatchBindings: object({ userIds: { ...array(string), minItems: 1, maxItems: 100 }, desktopIds: { ...array(string), minItems: 1, maxItems: 100 }, action: { type: 'string', enum: ['grant', 'revoke'] } }),
BatchBindingsResult: object({ accepted: boolean, userCount: integer }),
CreateUserInput: object({ username: string, displayName: string }),
BindingInput: object({ terminalId: { ...string, nullable: true }, desktopIds: array(string), enabled: boolean }),
DesktopAction: object({ action: { type: 'string', enum: ['start', 'shutdown', 'reboot'] } }),
TerminalAction: object({ action: { type: 'string', enum: ['reboot', 'reset-session', 'collect-logs'] } }),
RegisterInput: object({ sn: string, activationSecret: string, agentVersion: string }),
RegisterResult: object({ terminalId: string, accessToken: string, heartbeatInterval: integer }),
HeartbeatInput: object({ agentVersion: string, fps: { ...number, nullable: true }, rtt: { ...number, nullable: true } }, ['agentVersion']),
TerminalSystemMetrics: object({ cpuPercent: { ...number, nullable: true, minimum: 0, maximum: 100 }, memoryPercent: { ...number, nullable: true, minimum: 0, maximum: 100 }, socTemperatureC: { ...number, nullable: true, minimum: -40, maximum: 150 }, uptimeSeconds: { ...number, nullable: true, minimum: 0, maximum: 3155760000 } }, []),
TerminalMetricLatest: object({ cpuPercent: { ...number, nullable: true }, memoryPercent: { ...number, nullable: true }, socTemperatureC: { ...number, nullable: true }, uptimeSeconds: { ...number, nullable: true }, sampledAt: { ...date, nullable: true }, supported: boolean, online: boolean, fresh: boolean }),
TerminalMetricPoint: object({ time: date, cpuPercent: { ...number, nullable: true }, memoryPercent: { ...number, nullable: true }, socTemperatureC: { ...number, nullable: true }, uptimeSeconds: { ...number, nullable: true }, samples: integer }),
TerminalMetrics: object({ latest: ref('TerminalMetricLatest'), items: array(ref('TerminalMetricPoint')), from: date, to: date, bucketSeconds: integer, retentionDays: integer }),
Accepted: object({ accepted: boolean }),
HeartbeatResult: object({ accepted: boolean, serverTime: date }),
DeviceConfig: object({ configVersion: integer, identityMode: string, commandTransport: string, heartbeatInterval: integer }),
TicketInput: object({ desktopId: string }),
Ticket: object({ ticket: string, desktopId: string, expiresAt: date, credentialMode: string }),
ConsumeInput: object({ ticket: string }),
Connection: object({ desktopId: string, sessionId: string, protocol: string, endpoint: string, app: string, width: integer, height: integer, fps: integer, credentialMode: string }),
DeviceDiagnostics: object({ agentVersion: string, bootId: string, uptimeSeconds: number, kioskState: string, logTail: string }),
SessionInput: object({ sessionId: string, state: { type: 'string', enum: ['connected', 'ended'] } }),
AckInput: object({ status: { type: 'string', enum: ['success', 'failed'] }, message: string, diagnostics: ref('DeviceDiagnostics') }, ['status']),
}
Object.assign(schemas, {
PveConnectionInput: object({ url: string, tokenId: string, tokenSecret: { ...string, format: 'password', writeOnly: true }, caPem: string, revision: string }, ['url', 'tokenId', 'revision']),
PveConnection: object({ url: string, tokenId: string, caPem: string, revision: string, tokenConfigured: boolean, source: { type: 'string', enum: ['environment', 'database'] } }),
PveConnectionCheck: object({ available: boolean, nodes: integer, vms: integer, message: string }),
VMDisk: object({ key: string, storage: string, sizeGiB: number }),
VMHardware: object({ cores: integer, sockets: integer, memoryMiB: integer, digest: string, disks: array(ref('VMDisk')), powerState: string, blockers: array(string) }),
HardwareInput: object({ kind: { type: 'string', enum: ['compute', 'disk'] }, digest: string, cores: integer, sockets: integer, memoryMiB: integer, disk: string, sizeGiB: integer }, ['kind', 'digest']),
PowerBatchInput: object({ action: { type: 'string', enum: ['start', 'shutdown', 'reboot'] }, desktopIds: { ...array(string), minItems: 1, maxItems: 50 }, previewToken: string }, ['action', 'desktopIds']),
PowerBatchCheckItem: object({ desktopId: string, name: string, vmid: integer, powerState: string, available: boolean, code: string, message: string }),
PowerBatchPreview: object({ items: array(ref('PowerBatchCheckItem')), eligibleCount: integer, skippedCount: integer, previewToken: string, expiresAt: date }),
PowerBatch: object({ id: string, action: { type: 'string', enum: ['start', 'shutdown', 'reboot'] }, actor: string, createdAt: date, totalCount: integer, submittedCount: integer, skippedCount: integer, activeCount: integer, successCount: integer, failedCount: integer, uncertainCount: integer }),
PowerBatchItem: object({ desktopId: string, name: string, vmid: integer, taskId: string, status: string, code: string, message: string }),
PowerBatchDetail: object({ batch: ref('PowerBatch'), items: array(ref('PowerBatchItem')) }),
SnapshotItem: object({ name: string, description: string, createdAt: integer, parent: string, includesMemory: boolean, state: string, revision: string }),
SnapshotView: object({ items: array(ref('SnapshotItem')), digest: string, powerState: string, blockers: array(string), disks: array(string), canCreate: boolean, canDelete: boolean, canRestore: boolean }),
SnapshotInput: object({ action: { type: 'string', enum: ['create', 'delete', 'rollback'] }, name: { ...string, maxLength: 40 }, description: { ...string, maxLength: 500 }, digest: string, revision: string, confirmVmid: integer }, ['action', 'name', 'digest']),
HardwareCheck: object({ available: boolean, message: string }),
CsvImportInput: object({ kind: { type: 'string', enum: ['users', 'terminals', 'bindings'] }, csv: { ...string, maxLength: 524288 }, previewToken: string }, ['kind', 'csv']),
CsvImportRow: object({ line: integer, key: string, values: { type: 'object', additionalProperties: string }, errors: array(string) }),
CsvImportPreview: object({ kind: string, totalRows: integer, validRows: integer, errorRows: integer, previewToken: string, rows: array(ref('CsvImportRow')) }),
CsvImportResult: object({ id: string, kind: string, count: integer, items: array(object({ line: integer, key: string, id: string })), activationSecrets: array(object({ sn: string, activationSecret: string })), replayed: boolean }),
SessionRecord: object({ id: string, sessionId: string, terminalId: string, terminalName: string, desktopId: string, desktopName: string, username: string, displayName: string, state: { type: 'string', enum: ['connecting', 'connected', 'ended', 'interrupted'] }, source: { type: 'string', enum: ['ticket', 'observed'] }, reservedAt: date, connectedAt: { ...date, nullable: true }, lastReportedAt: date, endedAt: { ...date, nullable: true }, endReason: string }),
AlertRuleInput: object({ name: string, metric: { type: 'string', enum: ['node_cpu', 'node_memory', 'node_storage', 'node_unavailable', 'terminal_offline', 'terminal_cpu', 'terminal_memory', 'terminal_temperature'] }, threshold: number, holdSeconds: integer, enabled: boolean }),
AlertRule: object({ id: string, name: string, metric: { type: 'string', enum: ['node_cpu', 'node_memory', 'node_storage', 'node_unavailable', 'terminal_offline', 'terminal_cpu', 'terminal_memory', 'terminal_temperature'] }, threshold: number, holdSeconds: integer, enabled: boolean }),
AlertRecord: object({ id: string, ruleId: string, ruleName: string, metric: string, targetId: string, targetName: string, value: number, threshold: number, status: { type: 'string', enum: ['firing', 'resolved'] }, startedAt: date, openedAt: date, resolvedAt: { ...date, nullable: true }, resolveReason: string, acknowledgedAt: { ...date, nullable: true }, acknowledgedBy: string, dataFresh: { ...boolean, nullable: true } }),
ResourcePoint: object({ time: date, cpu: { ...number, nullable: true }, memory: { ...number, nullable: true }, storage: { ...number, nullable: true }, samples: integer }),
ResourceSeries: object({ items: array(ref('ResourcePoint')), bucketSeconds: integer, retentionDays: integer }),
TerminalPolicyInput: object({ name: string, version: string, heartbeatInterval: { ...integer, minimum: 15, maximum: 60 }, commandPollInterval: { ...integer, minimum: 3, maximum: 30 }, desktopRefreshInterval: { ...integer, minimum: 5, maximum: 120 } }),
TerminalPolicy: object({ id: string, name: string, version: string, heartbeatInterval: integer, commandPollInterval: integer, desktopRefreshInterval: integer, createdAt: date }),
PolicyDeployment: object({ id: string, templateId: string, templateName: string, templateVersion: string, actor: string, targetCount: integer, createdAt: date }),
PolicyDeploymentInput: object({ templateId: string, targetMode: { type: 'string', enum: ['devices', 'group'] }, terminalIds: { ...array(string), maxItems: 200 }, group: string }, ['templateId', 'targetMode']),
PolicyResult: object({ id: string, terminalId: string, terminalName: string, group: string, protocol: integer, deploymentId: string, templateName: string, templateVersion: string, configVersion: integer, status: { type: 'string', enum: ['unconfigured', 'pending', 'applied', 'failed', 'superseded'] }, errorCode: string, acknowledgedAt: { ...date, nullable: true }, online: boolean, enabled: boolean, registered: boolean }),
PolicyAck: object({ deploymentId: string, configVersion: integer, status: { type: 'string', enum: ['applied', 'failed'] }, errorCode: { type: 'string', enum: ['invalid_policy', 'persist_failed', 'runtime_unavailable'] } }, ['deploymentId', 'configVersion', 'status']),
AdministratorAccount: object({ id: string, username: string, displayName: string, role: { type: 'string', enum: ['super_admin', 'operator', 'viewer'] }, enabled: boolean }),
AdministratorSession: object({ key: string, createdAt: { ...date, nullable: true }, expiresAt: date, clientIp: string, userAgent: string, current: boolean }),
CreateAdministrator: object({ username: { type: 'string', minLength: 3, maxLength: 80 }, displayName: string, role: { type: 'string', enum: ['super_admin', 'operator', 'viewer'] }, password: { type: 'string', format: 'password', minLength: 12, maxLength: 72 } }),
UpdateAdministrator: object({ displayName: string, role: { type: 'string', enum: ['super_admin', 'operator', 'viewer'] }, enabled: boolean }),
ResetAdministratorPassword: object({ password: { type: 'string', format: 'password', minLength: 12, maxLength: 72 } }),
ChangePassword: object({ currentPassword: { type: 'string', format: 'password' }, newPassword: { type: 'string', format: 'password', minLength: 12, maxLength: 72 } }),
ImageTemplate: object({ id: string, name: string, version: string, os: { type: 'string', enum: ['Windows', 'Ubuntu', '麒麟'] }, node: string, vmid: integer, enabled: boolean, validatedAt: date }),
TemplateInput: object({ name: string, version: string, os: { type: 'string', enum: ['Windows', 'Ubuntu', '麒麟'] }, node: string, vmid: integer }),
TemplateUpdate: object({ enabled: boolean }),
DesktopPool: object({ id: string, name: string, type: { type: 'string', enum: ['dedicated', 'shared'] }, maxCount: integer, templateId: { ...string, nullable: true }, enabled: boolean, createdAt: date, desktopCount: integer, reservedCount: integer, assignedCount: integer, activeCount: integer }),
PoolInput: object({ name: string, type: { type: 'string', enum: ['dedicated', 'shared'] }, maxCount: integer, templateId: { ...string, nullable: true }, enabled: boolean }),
PoolMembers: object({ desktopIds: array(string) }),
CloneInput: object({ templateId: string, poolId: { ...string, nullable: true }, name: string, vmid: integer, full: boolean, storage: string }),
CloneCheck: object({ available: boolean, message: string }),
Storage: object({ storage: string, avail: number, active: integer, enabled: integer, content: string }),
ConnectionInput: object({ endpoint: string, width: integer, height: integer, fps: integer, initialized: boolean }),
})
schemas.Desktop.properties.poolId = { ...string, nullable: true }
schemas.Desktop.properties.setupRequired = boolean
schemas.DeviceDesktop = object({ ...schemas.Desktop.properties, powerOnConnect: boolean })
schemas.DeviceDesktopList = object({ items: array(ref('DeviceDesktop')), total: integer, page: integer, pageSize: integer })
schemas.GPUPrepareInput = object({ desktopId: { ...string, maxLength: 100 } })
schemas.GPUPrepareResult = object({ taskId: string, status: { type: 'string', enum: ['queued', 'running', 'success', 'failed', 'timeout'] }, message: string })
schemas.GPUReconcileResult = object({ reconciled: boolean, vmid: string })
schemas.DiagnosticSubject = object({ kind: { type: 'string', enum: ['terminal', 'desktop'] }, id: string, name: string, state: string, node: string, vmid: integer, agentVersion: string, lastSeen: { ...date, nullable: true } })
schemas.DiagnosticEvent = object({ id: string, source: { type: 'string', enum: ['tasks', 'audits', 'sessions', 'alerts', 'logs'] }, time: date, action: string, status: string, targetId: string, targetName: string, actor: string, message: string, details: string })
schemas.DiagnosticReport = object({ subject: ref('DiagnosticSubject'), from: date, to: date, observedAt: date, items: array(ref('DiagnosticEvent')), counts: object({ tasks: integer, audits: integer, sessions: integer, alerts: integer, logs: integer }), total: integer, page: integer, pageSize: integer, source: string, notes: array(string) })
schemas.TerminalLogArchive = object({ id: string, taskId: string, terminalId: string, terminalName: string, capturedAt: date, bootId: string, agentVersion: string, kioskState: string, logText: string })
schemas.TerminalLogArchivePage = object({ items: array(ref('TerminalLogArchive')), page: integer, pageSize: integer, total: integer, observedAt: date, retentionDays: integer })
schemas.GPUExclusiveMember = object({ desktopId: string, name: string, vmid: integer, powerState: string, powerObserved: boolean, activeSessions: integer, activeTasks: integer, conflicts: array(string), eligible: boolean })
schemas.GPUExclusiveStatus = object({ enabled: boolean, node: string, vmids: array(integer), observedAt: date, lockAvailable: boolean, groupComplete: boolean, members: array(ref('GPUExclusiveMember')), conflicts: array(string), allocationReady: boolean, releaseReady: boolean, releasePolicy: string, activeTasks: array(ref('Task')) })
schemas.Administrator.properties.enabled = boolean
schemas.HeartbeatInput.properties.policyProtocol = { ...integer, minimum: 0, maximum: 1 }
schemas.HeartbeatInput.properties.systemMetrics = { ...ref('TerminalSystemMetrics'), nullable: true }
Object.assign(schemas.DeviceConfig.properties, { deploymentId: string, policyProtocol: integer, commandPollInterval: integer, desktopRefreshInterval: integer })
schemas.DeviceConfig.required = Object.keys(schemas.DeviceConfig.properties)
// Older frontends can omit this display field; the API always supplies it.
schemas.Terminal.properties.sessionState = string
schemas.Terminal.properties.enabled = boolean
for (const name of ['Desktop', 'Terminal', 'User', 'Resource', 'Task', 'Audit', 'DiscoveredVM', 'ImageTemplate', 'DesktopPool', 'Storage', 'AdministratorAccount', 'AdministratorSession', 'TerminalPolicy', 'PolicyDeployment', 'PolicyResult', 'SessionRecord', 'AlertRule', 'AlertRecord', 'PowerBatch']) {
schemas[`${name}List`] = object({ items: array(ref(name)), page: integer, pageSize: integer, total: integer, requestId: string })
}
Object.assign(schemas.AlertRecordList.properties, { firingCount: integer, unacknowledgedCount: integer })
schemas.AlertRecordList.required.push('firingCount', 'unacknowledgedCount')
const json = schema => ({ 'application/json': { schema } })
const operation = (id, output, { input, status = 200, list = false, param = false, command = false, node = false, period = false, resource = false, security = 'adminBearer' } = {}) => ({
operationId: id,
security: security ? [{ [security]: [] }] : [],
...(input ? { requestBody: { required: true, content: json(ref(input)) } } : {}),
parameters: [
...(period ? ['from', 'to'].map(name => ({ name, in: 'query', schema: date })) : []),
...(resource ? [{ name: 'resourceId', in: 'query', required: true, schema: string }] : []),
...(node ? [{ name: 'node', in: 'query', required: true, schema: string }] : []),
...(param ? [{ name: 'id', in: 'path', required: true, schema: string }] : []),
...(list ? ['page', 'pageSize', 'keyword', 'status'].map(name => ({ name, in: 'query', schema: name.startsWith('page') ? integer : string })) : []),
...(command ? [{ name: 'Idempotency-Key', in: 'header', required: true, schema: { type: 'string', maxLength: 100 } }] : []),
],
responses: {
[status]: { description: 'Successful response', ...(output ? { content: json(ref(output)) } : {}) },
...Object.fromEntries([400, 401, 403, 404, 409, 429, 500, 502, 503].map(code => [code, { description: 'Request failed', content: json(ref('Error')) }])),
},
})
const paths = {
'/settings/pve': { get: operation('pveConnection', 'PveConnection'), put: operation('savePveConnection', 'Accepted', { input: 'PveConnectionInput' }) },
'/settings/pve/check': { post: operation('checkPveConnection', 'PveConnectionCheck', { input: 'PveConnectionInput' }) },
'/desktops/{id}/hardware': { get: operation('desktopHardware', 'VMHardware', { param: true }), post: operation('changeDesktopHardware', 'Task', { param: true, input: 'HardwareInput', command: true, status: 202 }) },
'/desktops/{id}/hardware/check': { post: operation('checkDesktopHardware', 'HardwareCheck', { param: true, input: 'HardwareInput' }) },
'/desktop-batches': { get: operation('desktopPowerBatches', 'PowerBatchList', { list: true }), post: operation('submitDesktopPowerBatch', 'PowerBatchDetail', { input: 'PowerBatchInput', command: true, status: 202 }) },
'/desktop-batches/check': { post: operation('checkDesktopPowerBatch', 'PowerBatchPreview', { input: 'PowerBatchInput' }) },
'/desktop-batches/{id}': { get: operation('desktopPowerBatch', 'PowerBatchDetail', { param: true }) },
'/tasks/{id}/batch-power-reconcile': { post: operation('reconcileBatchPower', 'Accepted', { param: true }) },
'/desktops/{id}/snapshots': { get: operation('desktopSnapshots', 'SnapshotView', { param: true }) },
'/desktops/{id}/snapshots/check': { post: operation('checkDesktopSnapshot', 'HardwareCheck', { param: true, input: 'SnapshotInput' }) },
'/desktops/{id}/snapshots/actions': { post: operation('changeDesktopSnapshot', 'Task', { param: true, input: 'SnapshotInput', command: true, status: 202 }) },
'/tasks/{id}/snapshot-reconcile': { post: operation('reconcileSnapshot', 'Accepted', { param: true }) },
'/tasks/{id}/hardware-reconcile': { post: operation('reconcileHardware', 'Accepted', { param: true }) },
'/imports/preview': { post: operation('previewCsvImport', 'CsvImportPreview', { input: 'CsvImportInput' }) },
'/imports/commit': { post: operation('commitCsvImport', 'CsvImportResult', { input: 'CsvImportInput', command: true }) },
'/sessions/history': { get: operation('sessionHistory', 'SessionRecordList', { list: true, period: true }) },
'/monitor/series': { get: operation('resourceSeries', 'ResourceSeries', { period: true, resource: true }) },
'/monitor/rules': { get: operation('alertRules', 'AlertRuleList', { list: true }), post: operation('createAlertRule', 'AlertRule', { input: 'AlertRuleInput' }) },
'/monitor/rules/{id}': { put: operation('updateAlertRule', 'AlertRule', { input: 'AlertRuleInput', param: true }) },
'/monitor/alerts': { get: operation('alerts', 'AlertRecordList', { list: true }) },
'/monitor/alerts/{id}/acknowledge': { post: operation('acknowledgeAlert', 'Accepted', { param: true }) },
'/terminal-policy-templates': { get: operation('terminalPolicyTemplates', 'TerminalPolicyList', { list: true }), post: operation('createTerminalPolicy', 'TerminalPolicy', { input: 'TerminalPolicyInput', status: 201 }) },
'/terminal-policy-states': { get: operation('terminalPolicyStates', 'PolicyResultList', { list: true }) },
'/terminal-policy-deployments': { get: operation('policyDeployments', 'PolicyDeploymentList', { list: true }), post: operation('deployPolicy', 'PolicyDeployment', { input: 'PolicyDeploymentInput', command: true, status: 202 }) },
'/terminal-policy-deployments/{id}/results': { get: operation('policyResults', 'PolicyResultList', { list: true, param: true }) },
'/terminal/config/ack': { post: operation('policyAck', 'Accepted', { input: 'PolicyAck', security: 'deviceBearer' }) },
'/auth/login': { post: operation('login', 'LoginResult', { input: 'LoginInput', security: null }) },
'/auth/me': { get: operation('me', 'Administrator') },
'/auth/logout': { post: operation('logout', null, { status: 204 }) },
'/auth/password': { post: operation('changeOwnPassword', null, { input: 'ChangePassword', status: 204 }) },
'/auth/mfa': { get: operation('mfaStatus', 'MFAStatus'), delete: operation('disableMFA', null, { input: 'MFACredential', status: 204 }) },
'/auth/mfa/setup': { post: operation('beginMFA', 'MFASetup', { input: 'MFASetupInput' }) },
'/auth/mfa/confirm': { post: operation('confirmMFA', 'MFARecovery', { input: 'MFAConfirm' }) },
'/auth/mfa/recovery': { post: operation('rotateMFARecovery', 'MFARecovery', { input: 'MFACredential' }) },
'/administrators': { get: operation('administrators', 'AdministratorAccountList', { list: true }), post: operation('createAdministrator', 'AdministratorAccount', { input: 'CreateAdministrator', status: 201 }) },
'/administrators/{id}': { put: operation('updateAdministrator', 'AdministratorAccount', { input: 'UpdateAdministrator', param: true }) },
'/administrators/{id}/password': { post: operation('resetAdministratorPassword', null, { input: 'ResetAdministratorPassword', param: true, status: 204 }) },
'/administrators/{id}/sessions': { get: operation('administratorSessions', 'AdministratorSessionList', { list: true, param: true }) },
'/administrators/{id}/sessions/{sessionKey}': { delete: { ...operation('revokeAdministratorSession', null, { param: true, status: 204 }), parameters: [{ name: 'id', in: 'path', required: true, schema: string }, { name: 'sessionKey', in: 'path', required: true, schema: { ...string, minLength: 64, maxLength: 64 } }] } },
'/overview': { get: operation('overview', 'Overview') },
'/settings': { get: operation('settings', 'Settings') },
'/resources': { get: operation('resources', 'ResourceList', { list: true }) },
'/resources/gpus': { get: operation('gpuInventory', 'GPUInventory', { node: true }) },
'/resources/gpu-exclusive': { get: operation('gpuExclusiveStatus', 'GPUExclusiveStatus', { node: true }) },
'/resources/vms': { get: operation('discoverVMs', 'DiscoveredVMList', { list: true }) },
'/resources/templates': { get: operation('discoverTemplates', 'DiscoveredVMList', { list: true }) },
'/resources/storage': { get: operation('storages', 'StorageList', { list: true, node: true }) },
'/templates': { get: operation('templates', 'ImageTemplateList', { list: true }), post: operation('registerTemplate', 'ImageTemplate', { input: 'TemplateInput', status: 201 }) },
'/templates/{id}': { patch: operation('updateTemplate', 'ImageTemplate', { param: true, input: 'TemplateUpdate' }) },
'/templates/{id}/validate': { post: operation('validateTemplate', 'ImageTemplate', { param: true }) },
'/pools': { get: operation('pools', 'DesktopPoolList', { list: true }), post: operation('createPool', 'DesktopPool', { input: 'PoolInput' }) },
'/pools/{id}': { put: operation('updatePool', 'DesktopPool', { param: true, input: 'PoolInput' }) },
'/pools/{id}/members': { put: operation('poolMembers', 'Accepted', { param: true, input: 'PoolMembers' }) },
'/desktops/clone/check': { post: operation('checkClone', 'CloneCheck', { input: 'CloneInput' }) },
'/desktops/clone': { post: operation('cloneDesktop', 'Task', { input: 'CloneInput', command: true, status: 202 }) },
'/desktops/{id}/connection': { put: operation('configureDesktop', 'Desktop', { param: true, input: 'ConnectionInput' }) },
'/resources/sync': { post: operation('syncResources', 'Task', { status: 202, command: true }) },
'/desktops': { get: operation('desktops', 'DesktopList', { list: true }) },
'/desktops/import': { post: operation('importDesktop', 'Desktop', { input: 'ImportInput' }) },
'/desktops/{id}': { get: operation('desktop', 'Desktop', { param: true }) },
'/desktops/{id}/actions': { post: operation('desktopAction', 'Task', { input: 'DesktopAction', param: true, status: 202, command: true }) },
'/terminals': { get: operation('terminals', 'TerminalList', { list: true }), post: operation('enrollTerminal', 'Enrollment', { input: 'EnrollmentInput', status: 201 }) },
'/terminals/{id}': { put: operation('updateTerminal', 'Accepted', { input: 'TerminalUpdate', param: true }) },
'/terminals/{id}/metrics': { get: operation('terminalMetrics', 'TerminalMetrics', { param: true, period: true }) },
'/terminals/{id}/disable': { post: operation('disableTerminal', 'Accepted', { param: true }) },
'/terminals/{id}/reactivate': { post: operation('reactivateTerminal', 'Enrollment', { param: true }) },
'/terminals/{id}/commands': { post: operation('terminalCommand', 'Task', { input: 'TerminalAction', param: true, status: 202, command: true }) },
'/users': { get: operation('users', 'UserList', { list: true }), post: operation('createUser', 'User', { input: 'CreateUserInput', status: 201 }) },
'/users/{id}': { put: operation('updateUser', 'User', { input: 'UserUpdate', param: true }) },
'/users/batch-bindings': { post: operation('batchBindings', 'BatchBindingsResult', { input: 'BatchBindings' }) },
'/users/{id}/bindings': { put: operation('updateBindings', 'User', { input: 'BindingInput', param: true }) },
'/tasks': { get: operation('tasks', 'TaskList', { list: true, period: true }) },
'/tasks/export': { get: operation('exportTasks', null, { period: true }) },
'/tasks/{id}': { get: operation('task', 'Task', { param: true }) },
'/tasks/{id}/reconcile': { post: operation('reconcileProvision', 'Task', { param: true, status: 202 }) },
'/tasks/{id}/diagnostics': { get: operation('taskDiagnostics', 'DeviceDiagnostics', { param: true }) },
'/diagnostics': { get: operation('diagnostics', 'DiagnosticReport') },
'/diagnostics/export': { get: operation('exportDiagnostics', null) },
'/terminal-logs': { get: operation('terminalLogArchives', 'TerminalLogArchivePage') },
'/audit-logs': { get: operation('audits', 'AuditList', { list: true, period: true }) },
'/audit-logs/export': { get: operation('exportAudits', null, { period: true }) },
'/terminal/register': { post: operation('registerDevice', 'RegisterResult', { input: 'RegisterInput', security: null }) },
'/terminal/heartbeat': { post: operation('heartbeat', 'HeartbeatResult', { input: 'HeartbeatInput', security: 'deviceBearer' }) },
'/terminal/config': { get: operation('deviceConfig', 'DeviceConfig', { security: 'deviceBearer' }) },
'/terminal/desktops': { get: operation('deviceDesktops', 'DeviceDesktopList', { security: 'deviceBearer', list: true }) },
'/terminal/desktop-prepare': { post: operation('prepareGPUDesktop', 'GPUPrepareResult', { input: 'GPUPrepareInput', status: 202, security: 'deviceBearer' }) },
'/terminal/desktop-prepare/{id}': { get: operation('gpuPrepareStatus', 'GPUPrepareResult', { param: true, security: 'deviceBearer' }) },
'/tasks/{id}/gpu-reconcile': { post: operation('reconcileGPUPrepare', 'GPUReconcileResult', { param: true }) },
'/terminal/commands': { get: operation('deviceCommands', 'TaskList', { security: 'deviceBearer', list: true }) },
'/terminal/commands/{id}/ack': { post: operation('deviceAck', 'Accepted', { input: 'AckInput', param: true, security: 'deviceBearer' }) },
'/terminal/connect-ticket': { post: operation('createTicket', 'Ticket', { input: 'TicketInput', status: 201, security: 'deviceBearer' }) },
'/terminal/connect-ticket/consume': { post: operation('consumeTicket', 'Connection', { input: 'ConsumeInput', security: 'deviceBearer' }) },
'/terminal/session': { post: operation('deviceSession', 'Accepted', { input: 'SessionInput', security: 'deviceBearer' }) },
}
const taskFilters = ['actor', 'action', 'kind', 'targetId'].map(name => ({ name, in: 'query', schema: { ...string, maxLength: 200 } }))
const diagnosticFilters = [
{ name: 'targetType', in: 'query', required: true, schema: { type: 'string', enum: ['terminal', 'desktop'] } },
{ name: 'targetId', in: 'query', required: true, schema: { ...string, minLength: 1, maxLength: 100 } },
...['from', 'to'].map(name => ({ name, in: 'query', schema: date })),
{ name: 'source', in: 'query', schema: { type: 'string', enum: ['', 'tasks', 'audits', 'sessions', 'alerts', 'logs'] } },
]
paths['/diagnostics'].get.parameters.push(...diagnosticFilters, { name: 'page', in: 'query', schema: { ...integer, minimum: 1 } }, { name: 'pageSize', in: 'query', schema: { ...integer, minimum: 1, maximum: 100 } })
paths['/diagnostics/export'].get.parameters.push(...diagnosticFilters)
paths['/terminal-logs'].get.parameters.push({ name: 'terminalId', in: 'query', schema: string }, { name: 'keyword', in: 'query', schema: { ...string, maxLength: 200 } }, { name: 'from', in: 'query', schema: date }, { name: 'to', in: 'query', schema: date }, { name: 'page', in: 'query', schema: { ...integer, minimum: 1 } }, { name: 'pageSize', in: 'query', schema: { ...integer, minimum: 1, maximum: 100 } })
paths['/diagnostics'].get.description = 'Super administrators and operators only. Exact ID correlation, merged lifecycle events, maximum seven days. Redacts common credential formats. No collection or VM changes.'
paths['/diagnostics/export'].get.description = 'Super administrators and operators only. ZIP of all matching events, up to 2000, with diagnostic JSON and scope notes. Uses the same object/time/source filters, ignores pagination, and records the export audit.'
paths['/diagnostics/export'].get.responses[200] = { description: 'Scoped diagnostic archive', content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } }
paths['/diagnostics/export'].get.responses[413] = { description: 'More than 2000 matching events; narrow time range or source', content: json(ref('Error')) }
taskFilters.push({ name: 'active', in: 'query', schema: boolean, description: 'true includes queued, running and unknown outcomes (timeout)' })
paths['/tasks'].get.parameters.push(...taskFilters)
paths['/tasks/export'].get.parameters.push(...taskFilters, ...['keyword', 'status'].map(name => ({ name, in: 'query', schema: { ...string, maxLength: 200 } })))
paths['/tasks/export'].get.responses[200] = { description: 'Complete filtered CSV; maximum 10000 rows. UTC times, raw action/status codes, no provider payload or idempotency keys.', headers: { 'X-Export-Count': { schema: integer } }, content: { 'text/csv': { schema: { type: 'string', format: 'binary' } } } }
paths['/tasks/export'].get.responses[413] = { description: 'Over 10000 matching tasks', content: json(ref('Error')) }
const auditFilters = ['actor', 'action', 'result'].map(name => ({ name, in: 'query', schema: { ...string, maxLength: 200 } }))
paths['/audit-logs'].get.parameters.push(...auditFilters)
paths['/audit-logs/export'].get.parameters.push(...auditFilters, { name: 'keyword', in: 'query', schema: { ...string, maxLength: 200 } })
paths['/audit-logs/export'].get.description = 'Authenticated administrators of all roles can export matching audit fields (maximum 10000 rows). No pagination or silent truncation. CSV uses UTF-8 BOM and formula-safe cells. Successful preparation is audited.'
paths['/audit-logs/export'].get.responses[200] = { description: 'Complete filtered CSV, with UTC timestamps and raw action/result codes', headers: { 'X-Export-Count': { schema: integer } }, content: { 'text/csv': { schema: { type: 'string', format: 'binary' } } } }
paths['/audit-logs/export'].get.responses[413] = { description: 'Over 10000 matching records; narrow the filter', content: json(ref('Error')) }
const output = { openapi: '3.0.3', info: { title: 'VDI 管理平台 API', version: '0.1.0', description: '第一阶段接口。平台票据不替代 Sunshine 配对;HTTP 202 只代表已提交。' }, servers: [{ url: '/api/v1' }], paths, components: { schemas, securitySchemes: { adminBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, deviceBearer: { type: 'http', scheme: 'bearer', description: '设备激活后获得的独立令牌,不是管理员 JWT' } } } }
const directory = fileURLToPath(new URL('../docs/openapi/', import.meta.url))
mkdirSync(directory, { recursive: true })
writeFileSync(`${directory}/platform.v1.json`, `${JSON.stringify(output, null, 2)}\n`)
console.log('Updated docs/openapi/platform.v1.json')