native-computer.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. """Disposable UI fixture; use distinct ports for concurrent native clients."""
  2. import argparse, base64, json, time, threading, hashlib, struct
  3. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  4. from pathlib import Path
  5. parser=argparse.ArgumentParser();parser.add_argument('--port',type=int,required=True);parser.add_argument('--image',required=True);args=parser.parse_args()
  6. image_bytes=Path(args.image).read_bytes()
  7. image=base64.b64encode(image_bytes).decode()
  8. width,height=struct.unpack('>II',image_bytes[16:24])
  9. state={'mode':'agent','generation':1,'controlId':None,'actions':[],'notes':''}
  10. agents=[dict(id='aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',name='环境助手甲',avatar='',instructions='',nodeId='local',profile='default',archived=False,revision=1,execution='computer'),dict(id='bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',name='环境助手乙',avatar='',instructions='',nodeId='local',profile='default',archived=False,revision=1,execution='computer')]
  11. agents[1].update(computerEnvironmentId='cccccccc-cccc-4ccc-8ccc-cccccccccccc',computerEnvironmentName='协作电脑')
  12. env={'legacy':False,'shared':[],'jobs':[],'image':'sha256:'+'a'*64,'previous':None,'uploaded':0}
  13. lock=threading.Lock()
  14. class Handler(BaseHTTPRequestHandler):
  15. def log_message(self,*args): pass
  16. def respond(self,value):
  17. data=json.dumps(value,ensure_ascii=False).encode();self.send_response(200);self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
  18. def do_GET(self):
  19. with lock:
  20. if self.path=='/fixture/state': return self.respond({**state,**env})
  21. if self.path=='/api/app/computers':
  22. if env['legacy']: self.send_error(404);return
  23. return self.respond({'canManageImages':True,'computers':[{'agent':a,'available':True,'reason':'可以打开电脑面板'} for a in agents]})
  24. if self.path=='/api/app/computers/shared': return self.respond({'computers':env['shared']})
  25. if self.path=='/api/app/admin/runners': return self.respond({'runners':[{'id':'11111111-1111-4111-8111-111111111111','name':'测试执行节点','enabled':True,'online':True}]})
  26. if '/images/jobs/' in self.path and '/chunk' in self.path: return self.respond({'data':base64.b64encode(b'fixture-image').decode(),'offset':0,'size':13,'manifest':{'protocol':1}})
  27. if self.path.endswith('/images'): return self.respond({'configured':True,'busy':False,'activeImageId':env['image'],'previousImageId':env['previous'],'images':[{'imageId':'sha256:'+'a'*64,'architecture':'arm64','driver':'0.20.0'},{'imageId':'sha256:'+'b'*64,'architecture':'arm64','driver':'0.20.0'}],'jobs':env['jobs'],'uploads':[]})
  28. if self.path.endswith('/frame'): return self.respond({'id':'22222222-2222-4222-8222-222222222222','generation':state['generation'],'data':image,'width':width,'height':height,'capturedAt':int(time.time()*1000)})
  29. if self.path.endswith('/computer'): return self.respond({**state,'canResume':True})
  30. return self.respond({'ok':True,'csrfToken':'fixture','features':['computerControl']})
  31. def do_POST(self):
  32. data=json.loads(self.rfile.read(int(self.headers.get('Content-Length','0'))) or b'{}')
  33. with lock:
  34. if self.path=='/fixture/reset':
  35. state.update(mode='agent',generation=1,controlId=None,actions=[],notes='');env.update(legacy=False,shared=[],jobs=[],image='sha256:'+'a'*64,previous=None,uploaded=0)
  36. elif self.path=='/fixture/legacy': env['legacy']=True
  37. elif self.path=='/api/app/computers/shared': env['shared'].append({'id':'cccccccc-cccc-4ccc-8ccc-cccccccccccc','name':data['name'],'memberIds':data['memberIds']})
  38. elif self.path.endswith('/images/jobs'):
  39. if data['operation'] in ['activate','rollback']: env['image'],env['previous']=(data.get('imageId') or env['previous']),env['image']
  40. job={'id':data['requestId'],'operation':data['operation'],'state':'complete','message':'镜像操作完成','createdAt':1,'updatedAt':1}
  41. if data['operation']=='export':job['archiveSize']=13
  42. env['jobs'].insert(0,job)
  43. elif self.path.endswith('/images/uploads'): return self.respond({'id':data['id'],'offset':0})
  44. elif '/images/uploads/' in self.path:
  45. env['uploaded']+=len(base64.b64decode(data['data']));return self.respond({'offset':env['uploaded']})
  46. elif self.path.endswith('/take'):
  47. state.update(mode='human',generation=state['generation']+1,controlId='11111111-1111-4111-8111-111111111111')
  48. return self.respond({**state,'token':'fixture-private-control-token-1234567890','canResume':True})
  49. elif self.path.endswith('/input'): state['actions'].append(data.get('action'))
  50. elif self.path.endswith('/giveback'): state.update(mode='agent',generation=state['generation']+1,controlId=None,notes=data.get('notes',''))
  51. return self.respond({'ok':True})
  52. def do_DELETE(self):
  53. with lock:
  54. if '/computers/shared/' in self.path: env['shared']=[]
  55. return self.respond({'ok':True})
  56. print(f'native computer fixture on 127.0.0.1:{args.port}',flush=True)
  57. ThreadingHTTPServer(('127.0.0.1',args.port),Handler).serve_forever()