compose.ts 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import {randomUUID} from 'node:crypto'
  2. import {ContainerComputerProvider,ComputerError,type ComputerSpecification,type ComputerState,type CommandResult,type SkillInstallation} from './container.js'
  3. import type {DesktopRelay} from '../../shared/composeDesktops.js'
  4. /** Uses only the fixed desktops declared by Compose; never calls a container engine. */
  5. export class ComposeComputerProvider extends ContainerComputerProvider {
  6. readonly fixedCapacity=true
  7. private leases=new Map<string,{token:string;instance:string}>()
  8. constructor(runnerId:string,home:string,readonly relay:DesktopRelay){super('docker',runnerId,home)}
  9. async inventory(){return this.relay('','list',{})}
  10. async verifyRuntime(){await this.inventory()}
  11. async inspect(spec:ComputerSpecification):Promise<ComputerState|undefined>{
  12. this.validateSpecification(spec)
  13. const state=await this.relay(spec.id,'health',{ownerKey:spec.ownerKey})
  14. return {id:spec.id,containerId:state.instance,workspace:'/home/cua/workspace',running:state.ready===true,isolation:'container'}
  15. }
  16. async ensure(spec:ComputerSpecification,authorize:()=>void):Promise<ComputerState>{
  17. authorize();const state=await this.inspect(spec);authorize()
  18. if(!state?.running)throw new ComputerError('compose_desktop_offline','Compose 桌面尚未就绪,请检查对应桌面容器')
  19. if(!this.leases.has(spec.id)){
  20. const lease=await this.relay(spec.id,'acquire',{ownerKey:spec.ownerKey,requestId:randomUUID()})
  21. this.leases.set(spec.id,lease)
  22. try{authorize()}catch(error){await this.stop(spec);throw error}
  23. }
  24. return state
  25. }
  26. async releaseFence(spec:ComputerSpecification,keepRunning=false){
  27. const lease=this.leases.get(spec.id);if(!lease)return
  28. try{await this.relay(spec.id,'release',{ownerKey:spec.ownerKey,...lease,cancel:!keepRunning})}
  29. catch(error){const state=await this.relay(spec.id,'health',{ownerKey:spec.ownerKey}).catch(()=>undefined);if(!state||state.instance===lease.instance)throw error}
  30. this.leases.delete(spec.id)
  31. }
  32. async renewFence(spec:ComputerSpecification){const lease=this.leases.get(spec.id);if(lease)await this.relay(spec.id,'renew',{ownerKey:spec.ownerKey,...lease})}
  33. async stop(spec:ComputerSpecification){await this.releaseFence(spec)}
  34. async remove(spec:ComputerSpecification){await this.stop(spec)}
  35. async deleteWorkspace(_spec:ComputerSpecification){} // Compose owns the named volume.
  36. async execute(spec:ComputerSpecification,argv:string[],options:{authorize():void;signal?:AbortSignal;timeout?:number;input?:Buffer;lane?:string;mayFence?():boolean}):Promise<CommandResult>{
  37. options.authorize();if(options.signal?.aborted)throw new ComputerError('computer_cancelled','操作已停止')
  38. const lease=this.leases.get(spec.id);if(!lease)throw new ComputerError('computer_lease_stale','需要当前共享桌面的控制权')
  39. const result=await this.relay(spec.id,'execute',{ownerKey:spec.ownerKey,...lease,argv,cwd:spec.cwd??'/home/cua/workspace',timeout:options.timeout??30000,...(options.input?{input:options.input.toString('base64')}:{})})
  40. options.authorize();if(result.exitCode)throw Object.assign(new ComputerError('computer_command_failed','桌面命令执行失败'),{code:result.exitCode,stdout:result.stdout,stderr:result.stderr})
  41. return result
  42. }
  43. async installSkill(spec:ComputerSpecification,bundle:SkillInstallation,_script:string,options:{authorize():void;signal?:AbortSignal}):Promise<{path:string;revision:string}>{
  44. options.authorize();if(options.signal?.aborted)throw new ComputerError('computer_cancelled','操作已停止')
  45. const lease=this.leases.get(spec.id);if(!lease)throw new ComputerError('computer_lease_stale','需要当前共享桌面的控制权')
  46. const health=await this.relay(spec.id,'health',{ownerKey:spec.ownerKey})
  47. if(!health.features?.includes('skills-v1'))throw new ComputerError('computer_skills_unavailable','请更新 Compose 桌面镜像以使用共享技能')
  48. options.authorize()
  49. const result=await this.relay(spec.id,'skills-install',{ownerKey:spec.ownerKey,...lease,bundle})
  50. options.authorize();if(options.signal?.aborted)throw new ComputerError('computer_cancelled','操作已停止');return result
  51. }
  52. async capture(spec:ComputerSpecification,authorize:()=>void){authorize();const result=await this.relay(spec.id,'frame',{ownerKey:spec.ownerKey});authorize();return result}
  53. async health(spec:ComputerSpecification,authorize:()=>void){authorize();const result=await this.relay(spec.id,'health',{ownerKey:spec.ownerKey});if(!result.ready)throw new ComputerError('compose_desktop_offline','Compose 桌面尚未就绪');return result}
  54. async configureNetwork(_spec:ComputerSpecification,authorize:()=>void){authorize()}
  55. }