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

222 lines
28 KiB
JavaScript
Raw 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 }),
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 }),
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'] }, 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')) }),
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'] }, 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'] }, 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 }),
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 }),
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.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', '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 }) },
'/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 }) },
'/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/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 }) },
'/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 }) },
'/audit-logs': { get: operation('audits', 'AuditList', { list: 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', 'DesktopList', { security: 'deviceBearer', list: 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 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')