| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- import {setTimeout as delay} from 'node:timers/promises'
- import {HttpError} from '../../server/errors.js'
- import {WorkspaceGateway,type GatewayFrame,type GatewayTarget} from '../../server/workspaceGateway.js'
- import {createWorkspaceToolLease,requireTeamToolBridge,type WorkspaceToolLease} from '../../server/workspaceToolLease.js'
- import type {WorkerTool} from './process.js'
- /** A normal Hermes Profile session, with the same native tool bridge as cloud computers. */
- export class ProfileComputerSession {
- private readonly gateway:WorkspaceGateway
- private toolLease?:WorkspaceToolLease
- private runtimeId=''
- storedId=''
- private closed=false
- private readonly controller=new AbortController()
- onEvent:(frame:GatewayFrame)=>void=()=>{}
- onDisconnect:()=>void=()=>{}
- onStoredId:(id:string)=>void=()=>{}
- constructor(readonly target:GatewayTarget,readonly profile:string,readonly workId:string,
- readonly authorize:()=>void,readonly catalog:()=>WorkerTool[],
- readonly call:(name:string,args:unknown,id:string)=>Promise<unknown>,
- readonly computerPolicy?: {mode:'isolated'|'profile';hostAccess:boolean},readonly workspaceMemory:()=>boolean=()=>false){
- this.gateway=new WorkspaceGateway(target)
- this.gateway.onDisconnect=()=>{if(!this.closed)this.onDisconnect()}
- this.gateway.onEvent=frame=>{
- if(this.closed||!this.runtimeId||frame.session_id!==this.runtimeId)return
- if(frame.type==='session.info'){
- const id=frame.payload?.stored_session_id??frame.payload?.session_key
- if(typeof id==='string'&&id){this.storedId=id;this.onStoredId(id)}
- }
- this.onEvent(frame)
- }
- }
- async open(storedId:string|undefined,params:Record<string,unknown>){
- if(this.computerPolicy){
- const response=await this.target.session.request('/api/plugins/yaoyao-bot-bridge/capabilities',{search:new URLSearchParams({profile:this.profile}),cache:'reload'})
- let capability:any;try{capability=JSON.parse(response.body.toString())}catch{}
- if(response.status!==200||capability?.ready!==true||capability?.computer_runtime_version!==2)
- throw new HttpError(409,'请更新夭夭工具桥并重启 Hermes Dashboard 服务,当前服务尚未加载托管虚拟机会话能力。','computer_bridge_upgrade_required')
- }
- await this.gateway.connect()
- const {session_id:_virtual,recoverOnly:_recover,...options}=params
- const opened=await this.gateway.rpc(storedId?'session.resume':'session.create',{
- ...options,profile:this.profile,close_on_disconnect:false,...(storedId?{session_id:storedId}:{}),
- })
- this.runtimeId=String(opened.session_id??'')
- this.storedId=String(opened.stored_session_id??opened.session_key??storedId??'')
- if(!this.runtimeId||!this.storedId)throw new HttpError(502,'本机 Profile 会话身份不完整','computer_profile_session_invalid')
- this.onStoredId(this.storedId)
- return opened
- }
- async bind(){
- this.authorize()
- if(!this.toolLease){
- await requireTeamToolBridge(this.target,this.profile)
- this.toolLease=await createWorkspaceToolLease({target:this.target,profile:this.profile,workId:this.workId,
- session:()=>({runtimeId:this.runtimeId,storedId:this.storedId}),signal:this.controller.signal,computerPolicy:this.computerPolicy,workspaceMemory:this.workspaceMemory(),
- assertActive:()=>{this.controller.signal.throwIfAborted();this.authorize()},
- catalog:()=>this.catalog().map(tool=>({...tool,id:tool.id??tool.name})),
- call:async(name,args,id)=>{
- const value=await this.call(name,args,id!)
- // Hermes Worker uses OpenAI image blocks; the Profile bridge speaks MCP.
- if(value&&typeof value==='object'&&(value as any)._multimodal){
- const result=value as any
- return {content:[{type:'text',text:result.text_summary??''},...result.content.map((item:any)=>{
- const match=/^data:(image\/[a-z]+);base64,(.+)$/.exec(item.image_url?.url??'')
- if(!match)throw new Error('虚拟机截图格式无效')
- return {type:'image',mimeType:match[1],data:match[2]}
- })]}
- }
- return value
- },onFailure:()=>{void this.stop().finally(()=>this.onDisconnect()).catch(()=>{})},
- })
- }
- await this.toolLease.bind()
- }
- rpc(method:string,params:Record<string,unknown>={}){
- const {session_id:_virtual,workMarker:_marker,...options}=params
- // session.active_list is manager-scoped: Hermes' contract is extra=forbid and rejects session_id.
- if(method==='session.active_list')return this.gateway.rpc(method,{...options,profile:this.profile})
- return this.gateway.rpc(method,{...options,session_id:this.runtimeId})
- }
- async file(action:'read'|'write',path:string,data?:string){
- this.authorize()
- if(!this.toolLease?.profileRequest)throw new HttpError(403,'Hermes 文件授权尚未建立','computer_file_forbidden')
- return this.toolLease.profileRequest('/computer-file',{action,path,...(data!==undefined?{data}:{})})
- }
- async stop(){
- if(!this.runtimeId||this.closed)return
- await this.rpc('session.interrupt')
- // An interrupt acknowledgement precedes actual termination of native tools.
- const deadline=Date.now()+20000
- for(;;){
- const state=await this.rpc('session.active_list')
- const session=state.sessions?.find((item:{id:string})=>item.id===this.runtimeId)
- if(session?.status==='idle')return
- if(Date.now()>=deadline)throw new HttpError(409,'本机任务尚未停止,暂不能交出电脑控制权','computer_profile_stopping')
- await delay(100,undefined,{signal:this.controller.signal})
- }
- }
- async close(){
- if(this.closed)return
- this.closed=true;this.controller.abort()
- await this.toolLease?.dispose()
- this.gateway.close()
- }
- }
|