|
|
@@ -23,11 +23,19 @@ interface Pending { command:RunnerCommand; resolve(value:any):void; reject(error
|
|
|
interface GatewayConnection { taskEnvironment?:()=>import('../shared/executionEnvironment.js').TaskEnvironment; runnerId:string;computer?:{environmentId:string;ownerKey:string;agentId:string};cleanupOnly?:boolean;publishArtifact?:(name:string,bytes:Buffer)=>Promise<unknown>; valid():boolean; onEvent(frame:GatewayFrame):void; onDisconnect():void }
|
|
|
interface Lease { runnerId:string; input:LeaseInput; calls:Map<string,{fingerprint:string;result:Promise<unknown>}> }
|
|
|
|
|
|
+// Only expose known diagnostics; arbitrary transport errors can contain credentials.
|
|
|
+function networkFailure(error:unknown,fallback:string){
|
|
|
+ if(!(error instanceof HttpError))return fallback
|
|
|
+ if(error.code==='computer_network_upgrade_required')return '缺少虚拟机网络网关,请在 Bot 设置 → 本地虚拟机中重新准备网络镜像;Compose 节点请更新网络网关'
|
|
|
+ if(error.code==='computer_network_cleanup_failed')return '旧网络任务未完成清理,请重试保存代理'
|
|
|
+ return fallback
|
|
|
+}
|
|
|
+
|
|
|
/** Outbound-only machine transport. Browser auth and machine auth remain separate. */
|
|
|
export class RunnerHub {
|
|
|
executionSettings?:import('./executionSettings.js').ExecutionSettings
|
|
|
private networkApplied=new Map<string,{revision:number;error?:string}>()
|
|
|
- networkStatus(){return this.records().filter(r=>r.enabled).map(r=>{const s=this.summary(r),applied=this.networkApplied.get(r.id);return {id:r.id,name:r.name,online:s.online,status:!s.online?'离线':!s.features.includes('vm-egress-v1')?'需要更新 Runner':applied?.error?'连接失败':applied?.revision===this.executionSettings?.proxy().revision?'已同步,启动时校验出口':'待更新'}})}
|
|
|
+ networkStatus(){return this.records().filter(r=>r.enabled).map(r=>{const s=this.summary(r),applied=this.networkApplied.get(r.id);return {id:r.id,name:r.name,online:s.online,status:!s.online?'离线':!s.features.includes('vm-egress-v1')?'需要更新 Runner':applied?.error?`未生效:${applied.error}`:applied?.revision===this.executionSettings?.proxy().revision?'已同步,启动时校验出口':'待更新'}})}
|
|
|
private networkSupported(id:string){return !this.executionSettings?.proxy().enabled||this.online.get(id)?.features.includes('vm-egress-v1')===true}
|
|
|
private networkCommandAllowed(id:string,kind:RunnerCommand['kind'],payload:Record<string,unknown>){return kind!=='computer.control'||this.networkSupported(id)||!(payload.op==='take'||payload.op==='input'||payload.op==='lifecycle'&&!['stop','remove'].includes(String(payload.action)))}
|
|
|
async configureNetworks(){await Promise.all(this.records().filter(r=>r.enabled&&this.summary(r).online).map(async r=>{
|
|
|
@@ -35,9 +43,9 @@ export class RunnerHub {
|
|
|
if(!this.networkSupported(r.id))await Promise.all([...this.connections].filter(([,c])=>c.runnerId===r.id&&c.computer).map(async([id,c])=>{this.connections.delete(id);c.onDisconnect();await this.request(r.id,'gateway.close',{connectionId:id}).catch(()=>{})}))
|
|
|
return
|
|
|
}
|
|
|
- try{const result=await this.request(r.id,'network.configure',{});this.networkApplied.set(r.id,{revision:result.revision})}catch{this.networkApplied.set(r.id,{revision:-1,error:'网络配置未生效'})}
|
|
|
+ try{const result=await this.request(r.id,'network.configure',{});this.networkApplied.set(r.id,{revision:result.revision})}catch(error){this.networkApplied.set(r.id,{revision:-1,error:networkFailure(error,'网络配置未生效,请检查执行节点后重试保存')})}
|
|
|
}))}
|
|
|
- async testNetworks(){const nodes=this.records().filter(r=>r.enabled&&this.summary(r).online&&this.summary(r).features.includes('vm-egress-v1'));if(!nodes.length)throw new HttpError(409,'没有支持代理的在线执行节点','proxy_node_unavailable');return {nodes:await Promise.all(nodes.map(async r=>{try{await this.request(r.id,'network.test',{});return {id:r.id,name:r.name,ok:true}}catch{return {id:r.id,name:r.name,ok:false}}}))}}
|
|
|
+ async testNetworks(){const nodes=this.records().filter(r=>r.enabled&&this.summary(r).online&&this.summary(r).features.includes('vm-egress-v1'));if(!nodes.length)throw new HttpError(409,'没有支持代理的在线执行节点','proxy_node_unavailable');return {nodes:await Promise.all(nodes.map(async r=>{try{await this.request(r.id,'network.test',{});return {id:r.id,name:r.name,ok:true}}catch(error){return {id:r.id,name:r.name,ok:false,error:networkFailure(error,'代理连接失败,请核对地址、认证和执行节点网络')}}}))}}
|
|
|
composeDesktops=new ComposeDesktops([])
|
|
|
get idleForUpdate(): boolean { return this.localVmActivity.size === 0 && [...this.pending.values()].every(commands => commands.size === 0) && [...this.artifacts.values()].every(upload => upload.result !== undefined) }
|
|
|
private localVmActivity = new Map<string, { owner: string; runnerId: string }>()
|