|
@@ -1,4 +1,4 @@
|
|
|
-import {randomUUID} from 'node:crypto'
|
|
|
|
|
|
|
+import {randomUUID,createHash} from 'node:crypto'
|
|
|
import {join,dirname,posix} from 'node:path'
|
|
import {join,dirname,posix} from 'node:path'
|
|
|
import {fileURLToPath} from 'node:url'
|
|
import {fileURLToPath} from 'node:url'
|
|
|
import {mkdir,rm} from 'node:fs/promises'
|
|
import {mkdir,rm} from 'node:fs/promises'
|
|
@@ -18,13 +18,16 @@ import {COMPOSE_DESKTOP_IMAGE,type DesktopRelay} from '../../shared/composeDeskt
|
|
|
import {ComputerControls} from './control.js'
|
|
import {ComputerControls} from './control.js'
|
|
|
import {ComputerPublicProxy} from '../network/computerProxy.js'
|
|
import {ComputerPublicProxy} from '../network/computerProxy.js'
|
|
|
import {HermesWorkerProcess,type WorkerModel,type WorkerProxyEnvironment,type WorkerContextConfiguration,type WorkerTool,type WorkerFrame} from './process.js'
|
|
import {HermesWorkerProcess,type WorkerModel,type WorkerProxyEnvironment,type WorkerContextConfiguration,type WorkerTool,type WorkerFrame} from './process.js'
|
|
|
-import {HOST_TOOLS,hostTool,hostPath,readHostFile,writeHostFile} from './hostTools.js'
|
|
|
|
|
|
|
+import {HOST_TOOLS,hostTool,hostPath} from './hostTools.js'
|
|
|
import {ProfileSkillSession,SKILL_TOOLS,SKILL_RULES} from './skills.js'
|
|
import {ProfileSkillSession,SKILL_TOOLS,SKILL_RULES} from './skills.js'
|
|
|
import {homedir,platform} from 'node:os'
|
|
import {homedir,platform} from 'node:os'
|
|
|
|
|
+import {VM_FILE_TRANSFER_SCRIPT} from './fileTransfer.js'
|
|
|
|
|
+import {FileTransferFiles} from '../../shared/fileTransferEndpoint.mjs'
|
|
|
|
|
+import {copyBetweenEndpoints} from '../../server/fileTransfer.js'
|
|
|
|
|
|
|
|
-export interface ComputerTarget {environmentId:string;ownerKey:string;agentId:string;hostAccess?:boolean;profileSession?:boolean;hermesRuntime?:boolean}
|
|
|
|
|
|
|
+export interface ComputerTarget {environmentId:string;ownerKey:string;agentId:string;hostAccess?:boolean;profileSession?:boolean;hermesRuntime?:boolean;fileTransferMaxBytes?:number}
|
|
|
export interface WorkerSession {agentId?:string;id:string;profile:string;ownerKey:string;environmentId:string;cwd:string;configuredCwd:string;history:any[];outcome?:'complete'|'failed'|'uncertain';vmExecution?:'profile';profileSessionId?:string;hermesRuntime?:boolean}
|
|
export interface WorkerSession {agentId?:string;id:string;profile:string;ownerKey:string;environmentId:string;cwd:string;configuredCwd:string;history:any[];outcome?:'complete'|'failed'|'uncertain';vmExecution?:'profile';profileSessionId?:string;hermesRuntime?:boolean}
|
|
|
-interface Live {workMarker?:string;paused?:boolean;desktop?:{width:number;height:number};pauseJob?:Promise<void>;segment?:number;toolCalls:Set<Promise<unknown>>;journal:Map<string,{name:string;result:unknown}>;proxyHandle?:ProxyHandle;session:WorkerSession;model:WorkerModel;contextConfig?:WorkerContextConfiguration;proxyEnv?:WorkerProxyEnvironment;lease?:ComputerLease;acquiring?:Promise<ComputerLease>;controller:AbortController;worker?:HermesWorkerProcess;finishing?:Promise<void>;stopping?:Promise<void>;releasing?:Promise<void>;images:any[];running:boolean;received:boolean}
|
|
|
|
|
|
|
+interface Live {toolsOnly?:boolean;workMarker?:string;paused?:boolean;desktop?:{width:number;height:number};pauseJob?:Promise<void>;segment?:number;toolCalls:Set<Promise<unknown>>;journal:Map<string,{name:string;result:unknown}>;proxyHandle?:ProxyHandle;session:WorkerSession;model:WorkerModel;contextConfig?:WorkerContextConfiguration;proxyEnv?:WorkerProxyEnvironment;lease?:ComputerLease;acquiring?:Promise<ComputerLease>;controller:AbortController;worker?:HermesWorkerProcess;finishing?:Promise<void>;stopping?:Promise<void>;releasing?:Promise<void>;images:any[];running:boolean;received:boolean}
|
|
|
const object=(properties:Record<string,unknown>,required:string[])=>({type:'object',properties,required,additionalProperties:false})
|
|
const object=(properties:Record<string,unknown>,required:string[])=>({type:'object',properties,required,additionalProperties:false})
|
|
|
const text={type:'string'}
|
|
const text={type:'string'}
|
|
|
const TOOLS:WorkerTool[]=[
|
|
const TOOLS:WorkerTool[]=[
|
|
@@ -199,6 +202,24 @@ export class ComputerGateway {
|
|
|
private hostCwd=homedir()
|
|
private hostCwd=homedir()
|
|
|
private profileSession?:ProfileComputerSession
|
|
private profileSession?:ProfileComputerSession
|
|
|
private skillSession?:ProfileSkillSession
|
|
private skillSession?:ProfileSkillSession
|
|
|
|
|
+ private hostTransfers?:FileTransferFiles
|
|
|
|
|
+ private usedVmTransfers=false
|
|
|
|
|
+ private transferNamespace(live:Live){return createHash('sha256').update(`${this.meta.ownerKey}:${this.meta.environmentId}:${this.workId}:${live.session.id}`).digest('hex')}
|
|
|
|
|
+ private async transferFile(live:Live,action:Record<string,unknown>){
|
|
|
|
|
+ const cleanup=action.op==='transfer-abort'
|
|
|
|
|
+ if(live.paused&&!cleanup)throw new HttpError(409,'虚拟机正在等待人工操作','computer_paused')
|
|
|
|
|
+ await this.authorized();this.guard()
|
|
|
|
|
+ if(!cleanup&&['pausing','human','resuming','error'].includes((await this.runtime.controls.status(this.meta)).mode))throw new HttpError(409,'虚拟机正在等待人工操作','computer_paused')
|
|
|
|
|
+ const input=z.object({op:z.enum(['transfer-read-open','transfer-write-open','transfer-read','transfer-append','transfer-finish','transfer-status','transfer-abort']),transferId:z.string().uuid(),path:z.string().min(1).max(4096).optional(),maxBytes:z.number().int().min(1024*1024).max(100*1024*1024).optional(),size:z.number().int().min(0).max(100*1024*1024).optional(),sha256:z.string().regex(/^[a-f0-9]{64}$/).optional(),overwrite:z.boolean().optional(),offset:z.number().int().min(0).max(100*1024*1024).optional(),data:z.string().max(699052).optional()}).strict().parse(action)
|
|
|
|
|
+ const lease=await this.ensureLease(live)
|
|
|
|
|
+ this.usedVmTransfers=true
|
|
|
|
|
+ return this.runtime.pool.use(lease,async context=>{
|
|
|
|
|
+ if(live.paused&&!cleanup)throw new HttpError(409,'虚拟机正在等待人工操作','computer_paused')
|
|
|
|
|
+ const result=await this.runtime.provider.execute(this.spec(live.session),['python3','-c',VM_FILE_TRANSFER_SCRIPT,this.transferNamespace(live)],{...context,input:Buffer.from(JSON.stringify(input))})
|
|
|
|
|
+ context.authorize();this.guard()
|
|
|
|
|
+ return JSON.parse(result.stdout)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
private get hermesManaged(){return this.meta.hermesRuntime===true}
|
|
private get hermesManaged(){return this.meta.hermesRuntime===true}
|
|
|
private get rules(){const sharing=this.meta.environmentId!==this.meta.agentId?'多个机器人共用这台虚拟机:命令和文件操作并行执行,桌面操作交替进行;动手前注意其他机器人可能正在使用同一桌面,避免覆盖彼此的工作。':'';if(this.meta.profileSession)return `${sharing}${PROFILE_COMPUTER_RULES}\n${this.hermesManaged?HERMES_COMPUTER_RULES:SKILL_RULES}`;return `${sharing}${ISOLATED_COMPUTER_RULES}\n${this.hermesManaged?ISOLATED_HERMES_RULES:SKILL_RULES}`}
|
|
private get rules(){const sharing=this.meta.environmentId!==this.meta.agentId?'多个机器人共用这台虚拟机:命令和文件操作并行执行,桌面操作交替进行;动手前注意其他机器人可能正在使用同一桌面,避免覆盖彼此的工作。':'';if(this.meta.profileSession)return `${sharing}${PROFILE_COMPUTER_RULES}\n${this.hermesManaged?HERMES_COMPUTER_RULES:SKILL_RULES}`;return `${sharing}${ISOLATED_COMPUTER_RULES}\n${this.hermesManaged?ISOLATED_HERMES_RULES:SKILL_RULES}`}
|
|
|
private team?:{id:string;catalog:WorkerTool[];call(name:string,args:Record<string,unknown>,callId:string):Promise<unknown>;workspaceMemory:boolean}
|
|
private team?:{id:string;catalog:WorkerTool[];call(name:string,args:Record<string,unknown>,callId:string):Promise<unknown>;workspaceMemory:boolean}
|
|
@@ -225,7 +246,7 @@ export class ComputerGateway {
|
|
|
session.cwd=this.meta.environmentId!==this.meta.agentId?(this.runtime.pool.definition(this.meta.ownerKey,this.meta.environmentId)?.cwd??COMPUTER_WORKSPACE):resolved.cwd;session.configuredCwd=resolved.configuredCwd
|
|
session.cwd=this.meta.environmentId!==this.meta.agentId?(this.runtime.pool.definition(this.meta.ownerKey,this.meta.environmentId)?.cwd??COMPUTER_WORKSPACE):resolved.cwd;session.configuredCwd=resolved.configuredCwd
|
|
|
// Defer pool.configure/acquire until a computer_* (or skill) tool actually needs the VM.
|
|
// Defer pool.configure/acquire until a computer_* (or skill) tool actually needs the VM.
|
|
|
const controller=new AbortController()
|
|
const controller=new AbortController()
|
|
|
- this.live={session,controller,model:('model' in resolved?resolved.model:undefined) as WorkerModel,contextConfig:'contextConfig' in resolved?resolved.contextConfig as WorkerContextConfiguration:undefined,proxyEnv:'proxyEnv' in resolved?resolved.proxyEnv as WorkerProxyEnvironment:undefined,images:[],running:false,received:false,toolCalls:new Set(),journal:new Map()}
|
|
|
|
|
|
|
+ this.live={toolsOnly:params.toolsOnly===true,session,controller,model:('model' in resolved?resolved.model:undefined) as WorkerModel,contextConfig:'contextConfig' in resolved?resolved.contextConfig as WorkerContextConfiguration:undefined,proxyEnv:'proxyEnv' in resolved?resolved.proxyEnv as WorkerProxyEnvironment:undefined,images:[],running:false,received:false,toolCalls:new Set(),journal:new Map()}
|
|
|
this.runtime.save(session);this.runtime.attachGateway(this)
|
|
this.runtime.save(session);this.runtime.attachGateway(this)
|
|
|
if(params.toolsOnly===true)return {session_id:session.id,stored_session_id:session.id,running:false,info:{profile_name:profile,tools_only:true}}
|
|
if(params.toolsOnly===true)return {session_id:session.id,stored_session_id:session.id,running:false,info:{profile_name:profile,tools_only:true}}
|
|
|
if(this.meta.profileSession||this.hermesManaged){
|
|
if(this.meta.profileSession||this.hermesManaged){
|
|
@@ -235,6 +256,12 @@ export class ComputerGateway {
|
|
|
return {session_id:session.id,stored_session_id:session.id,running:false,info:{profile_name:profile}}
|
|
return {session_id:session.id,stored_session_id:session.id,running:false,info:{profile_name:profile}}
|
|
|
}
|
|
}
|
|
|
const live=this.live
|
|
const live=this.live
|
|
|
|
|
+ if(method==='computer.transfer'){
|
|
|
|
|
+ if(!live||params.session_id!==live.session.id)throw new HttpError(403,'文件传输不属于当前电脑会话','computer_session_forbidden')
|
|
|
|
|
+ const call=this.transferFile(live,z.record(z.string(),z.unknown()).parse(params.action))
|
|
|
|
|
+ live.toolCalls.add(call);void call.finally(()=>live.toolCalls.delete(call)).catch(()=>{})
|
|
|
|
|
+ return call
|
|
|
|
|
+ }
|
|
|
if(method==='computer.invoke'){
|
|
if(method==='computer.invoke'){
|
|
|
if(!live||params.session_id!==live.session.id)throw new HttpError(403,'电脑会话不属于当前通道','computer_session_forbidden')
|
|
if(!live||params.session_id!==live.session.id)throw new HttpError(403,'电脑会话不属于当前通道','computer_session_forbidden')
|
|
|
const name=String(params.name??'')
|
|
const name=String(params.name??'')
|
|
@@ -290,7 +317,7 @@ export class ComputerGateway {
|
|
|
if(!this.profileTarget)throw new HttpError(409,'缺少本机 Profile 连接,请更新执行节点','computer_profile_unavailable')
|
|
if(!this.profileTarget)throw new HttpError(409,'缺少本机 Profile 连接,请更新执行节点','computer_profile_unavailable')
|
|
|
if(params.session_id&&!session.profileSessionId)throw new HttpError(409,'本机 Profile 会话尚未建立,请新建会话','computer_profile_session_invalid')
|
|
if(params.session_id&&!session.profileSessionId)throw new HttpError(409,'本机 Profile 会话尚未建立,请新建会话','computer_profile_session_invalid')
|
|
|
const native=new ProfileComputerSession(this.profileTarget,session.profile,this.workId,()=>this.guard(),
|
|
const native=new ProfileComputerSession(this.profileTarget,session.profile,this.workId,()=>this.guard(),
|
|
|
- ()=>[...TOOLS,...(this.hermesManaged?[]:SKILL_TOOLS),...HOST_TOOLS.filter(tool=>tool.name==='computer_copy_file'),...(this.team?.catalog??[])],
|
|
|
|
|
|
|
+ ()=>[...TOOLS,...(this.hermesManaged?[]:SKILL_TOOLS),...HOST_TOOLS.filter(tool=>tool.name==='computer_copy_file').map(tool=>({...tool,description:tool.description.replace('25 MiB',`${(this.meta.fileTransferMaxBytes??25*1024*1024)/1024/1024} MiB`)})),...(this.team?.catalog??[])],
|
|
|
async(name,args)=>{
|
|
async(name,args)=>{
|
|
|
const live=this.live
|
|
const live=this.live
|
|
|
if(!live||!this.active||live.paused)throw new HttpError(410,'本轮电脑工具已结束或暂停','computer_cancelled')
|
|
if(!live||!this.active||live.paused)throw new HttpError(410,'本轮电脑工具已结束或暂停','computer_cancelled')
|
|
@@ -325,7 +352,7 @@ export class ComputerGateway {
|
|
|
live.received=false;live.running=true;live.session.outcome='uncertain';live.session.history.push({role:'user',content:prompt});this.runtime.save(live.session)
|
|
live.received=false;live.running=true;live.session.outcome='uncertain';live.session.history.push({role:'user',content:prompt});this.runtime.save(live.session)
|
|
|
const directory=join(this.runtime.home,'workers',live.session.id,this.workId,String(live.segment=(live.segment??0)+1))
|
|
const directory=join(this.runtime.home,'workers',live.session.id,this.workId,String(live.segment=(live.segment??0)+1))
|
|
|
await mkdir(directory,{recursive:true,mode:0o700})
|
|
await mkdir(directory,{recursive:true,mode:0o700})
|
|
|
- const worker=new HermesWorkerProcess(this.runtime.config.python,this.runtime.script,{mode:'run',home:directory,hermesSource:this.runtime.config.hermesSource,model:live.model,contextConfig:live.contextConfig,proxyEnv:live.proxyEnv,sessionId:live.session.id,taskId:this.workId,cwd:live.session.cwd,...(this.meta.hostAccess?{host:{cwd:this.hostCwd,platform:platform()}}:{}),network:this.runtime.config.network??'none',tools:[...TOOLS,...SKILL_TOOLS,...(this.meta.hostAccess?HOST_TOOLS:[]),...(this.team?.catalog??[])],skillInstructions:SKILL_RULES,prompt:live.images.length?[{type:'text',text:prompt},...live.images]:prompt,history})
|
|
|
|
|
|
|
+ const worker=new HermesWorkerProcess(this.runtime.config.python,this.runtime.script,{mode:'run',home:directory,hermesSource:this.runtime.config.hermesSource,model:live.model,contextConfig:live.contextConfig,proxyEnv:live.proxyEnv,sessionId:live.session.id,taskId:this.workId,cwd:live.session.cwd,...(this.meta.hostAccess?{host:{cwd:this.hostCwd,platform:platform()}}:{}),network:this.runtime.config.network??'none',tools:[...TOOLS,...SKILL_TOOLS,...(this.meta.hostAccess?HOST_TOOLS.map(tool=>tool.name==='computer_copy_file'?{...tool,description:tool.description.replace('25 MiB',`${(this.meta.fileTransferMaxBytes??25*1024*1024)/1024/1024} MiB`)}:tool):[]),...(this.team?.catalog??[])],skillInstructions:SKILL_RULES,prompt:live.images.length?[{type:'text',text:prompt},...live.images]:prompt,history})
|
|
|
live.worker=worker
|
|
live.worker=worker
|
|
|
this.runtime.workers.set(this.workId,{environmentId:this.meta.environmentId,ownerKey:this.meta.ownerKey,process:worker});void worker.exited.then(()=>{if(this.runtime.workers.get(this.workId)?.process===worker)this.runtime.workers.delete(this.workId)})
|
|
this.runtime.workers.set(this.workId,{environmentId:this.meta.environmentId,ownerKey:this.meta.ownerKey,process:worker});void worker.exited.then(()=>{if(this.runtime.workers.get(this.workId)?.process===worker)this.runtime.workers.delete(this.workId)})
|
|
|
worker.onTool=(name,args,id,callId)=>{
|
|
worker.onTool=(name,args,id,callId)=>{
|
|
@@ -372,6 +399,23 @@ export class ComputerGateway {
|
|
|
try{
|
|
try{
|
|
|
const team=this.team?.catalog.find(tool=>tool.name===name)
|
|
const team=this.team?.catalog.find(tool=>tool.name===name)
|
|
|
if(team)return await this.team!.call(team.id??name,z.record(z.string(),z.unknown()).parse(args),id)
|
|
if(team)return await this.team!.call(team.id??name,z.record(z.string(),z.unknown()).parse(args),id)
|
|
|
|
|
+ if(name.startsWith('host_')&&!this.meta.hostAccess||name==='computer_copy_file'&&!this.meta.hostAccess&&!this.hermesManaged)throw new HttpError(403,'本轮未授权该工具','computer_tool_forbidden')
|
|
|
|
|
+ if(this.meta.hostAccess&&name.startsWith('host_'))return await hostTool(name,args,{cwd:this.hostCwd,signal:live.controller.signal,authorize:()=>{this.guard();if(live.paused)throw new HttpError(409,'电脑正在等待人工操作','computer_paused')}})
|
|
|
|
|
+ if((this.meta.hostAccess||this.hermesManaged)&&name==='computer_copy_file'){
|
|
|
|
|
+ const body=z.object({from:z.enum(['host','vm']),source:pathField,destination:pathField}).strict().parse(args)
|
|
|
|
|
+ const limit=this.meta.fileTransferMaxBytes??25*1024*1024
|
|
|
|
|
+ const hostPathValue=body.from==='host'?body.source:body.destination
|
|
|
|
|
+ this.hostTransfers??=new FileTransferFiles(this.hostCwd,{confined:false})
|
|
|
|
|
+ const host={host:'server',name:'执行节点本机',path:hostPathValue,chunks:true,call:async(action:Record<string,unknown>)=>{
|
|
|
|
|
+ await this.authorized();this.guard()
|
|
|
|
|
+ if(this.hermesManaged)return this.profileSession!.transfer(body.from==='host'?'read':'write',hostPathValue,action)
|
|
|
|
|
+ const result=await this.hostTransfers!.call({...action,...(action.path?{path:hostPath(this.hostCwd,String(action.path))}:{})},()=>{this.guard();if(live.paused)throw new HttpError(409,'电脑正在等待人工操作','computer_paused')})
|
|
|
|
|
+ return result.path?{...result,path:hostPathValue}:result
|
|
|
|
|
+ }}
|
|
|
|
|
+ const vm={host:'vm',name:'虚拟机',path:body.from==='host'?body.destination:body.source,chunks:true,call:(action:Record<string,unknown>)=>this.transferFile(live,action)}
|
|
|
|
|
+ await copyBetweenEndpoints(body.from==='host'?host:vm,body.from==='host'?vm:host,limit,true,()=>{this.guard();if(live.paused)throw new HttpError(409,'电脑正在等待人工操作','computer_paused')})
|
|
|
|
|
+ return {ok:true}
|
|
|
|
|
+ }
|
|
|
if(needsVmLease(name,this.team?.catalog.map(tool=>tool.name)??[],SKILL_TOOLS.map(tool=>tool.name))){
|
|
if(needsVmLease(name,this.team?.catalog.map(tool=>tool.name)??[],SKILL_TOOLS.map(tool=>tool.name))){
|
|
|
try{await this.ensureLease(live)}
|
|
try{await this.ensureLease(live)}
|
|
|
catch(error){
|
|
catch(error){
|
|
@@ -397,29 +441,6 @@ export class ComputerGateway {
|
|
|
if(!lease)throw new HttpError(409,'电脑环境尚未就绪','computer_unavailable')
|
|
if(!lease)throw new HttpError(409,'电脑环境尚未就绪','computer_unavailable')
|
|
|
return await this.runtime.pool.use(lease,async context=>{
|
|
return await this.runtime.pool.use(lease,async context=>{
|
|
|
const execute=(argv:string[],input?:Buffer,lane?:'exec'|'gui',user?:'cua'|'root')=>this.runtime.provider.execute(this.spec(live.session),argv,{...context,input,...(lane?{lane}:{}),...(user?{user}:{})})
|
|
const execute=(argv:string[],input?:Buffer,lane?:'exec'|'gui',user?:'cua'|'root')=>this.runtime.provider.execute(this.spec(live.session),argv,{...context,input,...(lane?{lane}:{}),...(user?{user}:{})})
|
|
|
- if(this.meta.hostAccess&&name.startsWith('host_'))return hostTool(name,args,{...context,cwd:this.hostCwd})
|
|
|
|
|
- if((this.meta.hostAccess||this.hermesManaged)&&name==='computer_copy_file'){
|
|
|
|
|
- const body=z.object({from:z.enum(['host','vm']),source:pathField,destination:pathField}).strict().parse(args),host={...context,cwd:this.hostCwd}
|
|
|
|
|
- if(body.from==='host'){
|
|
|
|
|
- const bytes=this.hermesManaged?Buffer.from(String((await this.profileSession!.file('read',body.source)).data),'base64'):await readHostFile(hostPath(this.hostCwd,body.source),25*1024*1024,host)
|
|
|
|
|
- if(bytes.length>25*1024*1024)throw new HttpError(413,'文件超过传输限制','computer_file_limit')
|
|
|
|
|
- await execute(['python3','-c','import pathlib,sys; p=pathlib.Path(sys.argv[1]); p.parent.mkdir(parents=True,exist_ok=True); p.write_bytes(sys.stdin.buffer.read())',body.destination],bytes)
|
|
|
|
|
- }else{
|
|
|
|
|
- const snapshot=`/tmp/yaoyao-copy-${randomUUID()}`,chunks:Buffer[]=[]
|
|
|
|
|
- try{
|
|
|
|
|
- const size=Number((await execute(['python3','-c','import pathlib,sys,shutil; p=pathlib.Path(sys.argv[1]); assert p.is_file() and p.stat().st_size<=25*1024*1024; shutil.copyfile(p,sys.argv[2]); n=pathlib.Path(sys.argv[2]).stat().st_size; assert n<=25*1024*1024; print(n)',body.source,snapshot])).stdout.trim())
|
|
|
|
|
- if(!Number.isSafeInteger(size)||size<0||size>25*1024*1024)throw new Error('文件大小无效')
|
|
|
|
|
- for(let offset=0;offset<size;offset+=524288){
|
|
|
|
|
- const data=(await execute(['python3','-c','import sys,base64; f=open(sys.argv[1],"rb"); f.seek(int(sys.argv[2])); print(base64.b64encode(f.read(524288)).decode())',snapshot,String(offset)])).stdout.trim()
|
|
|
|
|
- chunks.push(Buffer.from(data,'base64'))
|
|
|
|
|
- }
|
|
|
|
|
- const bytes=Buffer.concat(chunks);if(bytes.length!==size)throw new Error('文件复制不完整')
|
|
|
|
|
- if(this.hermesManaged)await this.profileSession!.file('write',body.destination,bytes.toString('base64'))
|
|
|
|
|
- else await writeHostFile(hostPath(this.hostCwd,body.destination),bytes,host)
|
|
|
|
|
- }finally{await execute(['rm','-f','--',snapshot]).catch(()=>{})}
|
|
|
|
|
- }
|
|
|
|
|
- return {ok:true}
|
|
|
|
|
- }
|
|
|
|
|
if(name==='computer_export'){
|
|
if(name==='computer_export'){
|
|
|
const body=z.object({path:pathField,name:z.string().min(1).max(240).optional()}).strict().parse(args)
|
|
const body=z.object({path:pathField,name:z.string().min(1).max(240).optional()}).strict().parse(args)
|
|
|
const snapshot=`/tmp/yaoyao-export-${randomUUID()}`
|
|
const snapshot=`/tmp/yaoyao-export-${randomUUID()}`
|
|
@@ -516,7 +537,16 @@ export class ComputerGateway {
|
|
|
private release(live:Live,keepRunning=false){
|
|
private release(live:Live,keepRunning=false){
|
|
|
clearInterval(this.timer)
|
|
clearInterval(this.timer)
|
|
|
live.releasing??=(async()=>{
|
|
live.releasing??=(async()=>{
|
|
|
|
|
+ await this.hostTransfers?.close();this.hostTransfers=undefined
|
|
|
if(!live.lease){await live.proxyHandle?.release().catch(()=>{});return}
|
|
if(!live.lease){await live.proxyHandle?.release().catch(()=>{});return}
|
|
|
|
|
+ if(this.usedVmTransfers){
|
|
|
|
|
+ // Cleanup remains allowed after cancellation, but can only remove this
|
|
|
|
|
+ // session's staged files in the same VM generation, never a replacement.
|
|
|
|
|
+ const lease=live.lease
|
|
|
|
|
+ const authorize=()=>{const state=this.runtime.pool.status(this.meta.ownerKey).find(item=>item.environmentId===lease.environmentId);if(state?.generation!==lease.generation||!state.holderIds.includes(this.workId))throw new Error('文件传输所属虚拟机已变化')}
|
|
|
|
|
+ await this.runtime.provider.execute(this.spec(live.session),['python3','-c',VM_FILE_TRANSFER_SCRIPT,this.transferNamespace(live)],{authorize,signal:AbortSignal.timeout(5000),timeout:5000,mayFence:()=>false,input:Buffer.from(JSON.stringify({op:'transfer-cleanup'}))}).catch(()=>{})
|
|
|
|
|
+ this.usedVmTransfers=false
|
|
|
|
|
+ }
|
|
|
try{await live.proxyHandle?.release();await this.runtime.pool.release(live.lease,keepRunning)}
|
|
try{await live.proxyHandle?.release();await this.runtime.pool.release(live.lease,keepRunning)}
|
|
|
catch(error){await this.runtime.pool.release(live.lease).catch(()=>{});throw error}
|
|
catch(error){await this.runtime.pool.release(live.lease).catch(()=>{});throw error}
|
|
|
})()
|
|
})()
|
|
@@ -526,7 +556,7 @@ export class ComputerGateway {
|
|
|
if(!live.running||live.paused||this.closed||live.controller.signal.aborted)return
|
|
if(!live.running||live.paused||this.closed||live.controller.signal.aborted)return
|
|
|
live.running=false;await this.interrupt().catch(()=>{});if(!this.closed)this.onDisconnect()
|
|
live.running=false;await this.interrupt().catch(()=>{});if(!this.closed)this.onDisconnect()
|
|
|
}
|
|
}
|
|
|
- get active(){return !!this.live?.running&&!this.live.received&&!this.closed}
|
|
|
|
|
|
|
+ get active(){return !!this.live&&(this.live.running||this.live.toolsOnly===true&&this.live.toolCalls.size>0)&&!this.live.received&&!this.closed}
|
|
|
get paused(){return this.live?.paused===true}
|
|
get paused(){return this.live?.paused===true}
|
|
|
get lease(){return this.live?.lease}
|
|
get lease(){return this.live?.lease}
|
|
|
get specification(){return this.live?this.spec(this.live.session):undefined}
|
|
get specification(){return this.live?this.spec(this.live.session):undefined}
|
|
@@ -560,6 +590,7 @@ export class ComputerGateway {
|
|
|
await live.pauseJob;await this.authorized()
|
|
await live.pauseJob;await this.authorized()
|
|
|
live.paused=false;live.pauseJob=undefined;live.journal.clear()
|
|
live.paused=false;live.pauseJob=undefined;live.journal.clear()
|
|
|
this.event('computer.resumed',{generation:live.lease?.generation,notes})
|
|
this.event('computer.resumed',{generation:live.lease?.generation,notes})
|
|
|
|
|
+ if(live.toolsOnly)return
|
|
|
if(this.profileSession){
|
|
if(this.profileSession){
|
|
|
await this.profileSession.bind();this.guard()
|
|
await this.profileSession.bind();this.guard()
|
|
|
await this.profileSession.rpc('prompt.submit',{text:`${this.rules}\n用户已交还电脑控制权。请先检查当前状态,避免重复已完成的操作。交还说明:${notes||'无额外说明'}\n${live.workMarker??''}`})
|
|
await this.profileSession.rpc('prompt.submit',{text:`${this.rules}\n用户已交还电脑控制权。请先检查当前状态,避免重复已完成的操作。交还说明:${notes||'无额外说明'}\n${live.workMarker??''}`})
|