#!/usr/bin/env python3 from __future__ import annotations import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse from http import cookies from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1])) DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3')) HOST = os.getenv('AIOS_WEBOPS_HOST','0.0.0.0') PORT = int(os.getenv('AIOS_WEBOPS_PORT','18081')) PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top') WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top') API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top') WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top') SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production') ROLES = ['owner','admin','user'] NODES = ['server-hw','server-local'] PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')] def now(): return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) def uid(p='id'): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}' def esc(x): return html.escape('' if x is None else str(x)) def jdump(x): return json.dumps(x, ensure_ascii=False) def jload(x): try: return json.loads(x or '{}') except Exception: return {} def db(): DB_PATH.parent.mkdir(parents=True, exist_ok=True) con=sqlite3.connect(DB_PATH); con.row_factory=sqlite3.Row; return con def exec_sql(sql,args=()): with db() as con: con.execute(sql,args); con.commit() def rows(sql,args=()): with db() as con: return [dict(r) for r in con.execute(sql,args).fetchall()] def one(sql,args=()): r=rows(sql,args); return r[0] if r else None def init_db(): (ROOT/'data').mkdir(parents=True,exist_ok=True) for f in sorted((ROOT/'migrations').glob('*.sql')): with db() as con: con.executescript(f.read_text(encoding='utf-8')); con.commit() def audit(actor, action, target='', payload=None): exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now())) def notify(event_type,payload): exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now())) def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest() def check_pw(stored,p): return stored in (p, hash_pw(p)) def token(username,role): data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig def parse_session(header): if not header: return None c=cookies.SimpleCookie(); c.load(header) if 'aios_session' not in c: return None parts=c['aios_session'].value.split('|') if len(parts)!=4: return None data='|'.join(parts[:3]); sig=parts[3] if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved')) def require(user, roles): return bool(user and user['role'] in roles) def table(data): if not data: return '
No records.
' cols=list(data[0].keys()); head=''.join(f''+esc(json.dumps({'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'},'roles':ROLES,'nodes':NODES},ensure_ascii=False,indent=2))+''
if path=='/uat': return table(rows('SELECT * FROM uat_results ORDER BY created_at DESC LIMIT 200')) + ('' if require(user,['owner','admin']) else '')
return 'Not found
' def do_GET(self): init_db(); path=urllib.parse.urlparse(self.path).path; user=self.user() if path=='/': self.redirect('/dashboard'); return if path=='/health/ready': self.json({'status':'ok','roles':ROLES,'nodes':NODES,'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'}}); return if path=='/login': self.send_body(200,layout('Login','',None)); return if path=='/logout': self.redirect('/login',["aios_session=; Path=/; Max-Age=0"]); return if path=='/register': self.send_body(200,layout('Register','',None)); return if not user: self.redirect('/login'); return known=[p for p,t in PAGES] if path in known: self.send_body(200,layout(path.strip('/').title(),self.page(path,user),user)); return self.send_body(404,layout('Not Found','not found
',user)) def do_POST(self): init_db(); path=urllib.parse.urlparse(self.path).path if path=='/login': n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode()) u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved')) if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return self.send_body(403,layout('Login Failed','登录失败。
',None)); return if path=='/register': n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode()) username=data.get('username',[''])[0].strip(); pw=data.get('password',[''])[0]; display=data.get('display_name',[''])[0] if not username or not pw: self.send_body(400,'missing username/password'); return exec_sql('INSERT OR IGNORE INTO users(id,username,display_name,role,status,password_hash,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',(uid('user'),username,display,'user','pending',hash_pw(pw),now(),now())) approval(username,'user_registration',{'username':username},'low'); self.send_body(200,layout('Register Submitted','注册已提交,等待审核。
',None)); return user=self.user() if not user: self.json({'error':'not_authenticated'},401); return data={}; n=int(self.headers.get('Content-Length','0') or 0) if n: try: data=json.loads(self.rfile.read(n).decode() or '{}') except Exception: data={} actor=user['username']; role=user['role'] try: if path=='/api/nodes/health' and require(user,['owner','admin']): exec_sql('UPDATE node_status SET health=?,last_seen_at=?,note=? WHERE node_id=?',(data.get('health'),now(),f'updated_by={actor}',data.get('node_id'))); audit(actor,'node_health_update',data.get('node_id'),data); self.json({'status':'ok'}); return if path=='/api/routing/request' and require(user,['owner','admin']): if data.get('role') not in ROLES or data.get('active_node') not in NODES: raise ValueError('bad routing') if role=='owner': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(data['active_node'],now(),data['role'])); audit(actor,'routing_changed','routing_policy',data); self.json({'status':'executed'}); return self.json(approval(actor,'routing_change',data,'high')); return if path=='/api/failover/dry-run' and require(user,['owner','admin']): self.json({'status':'dry_run','migration_order':rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')}); return if path in ['/api/failover/request','/api/failover/request-failback'] and require(user,['owner','admin']): self.json(approval(actor,'failover_execute' if path.endswith('request') else 'failback_execute',data,'critical')); return if path=='/api/approvals/approve' and require(user,['owner']): ap=one('SELECT * FROM approvals WHERE id=?',(data.get('approval_id'),)); payload=jload(ap['payload']) if ap else {} if not ap: raise ValueError('approval not found') result={'approved':True} if ap['action']=='routing_change': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(payload.get('active_node'),now(),payload.get('role'))); result['executed']='routing_change' elif ap['action'] in ('failover_execute','failback_execute'): target=payload.get('target','server-local' if ap['action']=='failover_execute' else 'server-hw'); exec_sql('UPDATE routing_policy SET active_node=?,updated_at=?',(target,now())); result={'executed':ap['action'],'target':target} elif ap['action']=='user_registration': exec_sql('UPDATE users SET status=?,updated_at=? WHERE username=?',('approved',now(),payload.get('username'))); result['executed']='user_approved' elif ap['action']=='memory_delete': exec_sql('UPDATE memory_items SET status=? WHERE username=?',('soft_deleted',payload.get('username'))); result['executed']='memory_soft_deleted' exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('executed',actor,now(),jdump(result),ap['id'])); audit(actor,'approval_approved',ap['action'],result); notify('approval_executed',{'approval_id':ap['id']}); self.json({'status':'executed','result':result}); return if path=='/api/approvals/reject' and require(user,['owner']): exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('rejected',actor,now(),jdump({'rejected':True}),data.get('approval_id'))); audit(actor,'approval_rejected','approvals',data); self.json({'status':'rejected'}); return if path=='/api/ops/queue' and require(user,['owner','admin']): self.json(queue_op(actor,data.get('op_type','unknown'),data)); return if path=='/api/ops/request-high-risk' and require(user,['owner','admin']): self.json(approval(actor,data.get('op_type','unknown'),data,'critical')); return if path=='/api/users/update' and require(user,['owner','admin']): if data.get('role')=='owner' and role!='owner': raise PermissionError('only owner can grant owner') if data.get('role') not in ROLES: raise ValueError('bad role') exec_sql('UPDATE users SET role=?,status=?,updated_at=? WHERE username=?',(data.get('role'),data.get('status'),now(),data.get('username'))); audit(actor,'user_update',data.get('username'),data); self.json({'status':'updated'}); return if path=='/api/quota/set' and require(user,['owner','admin']): exec_sql('INSERT INTO quota_policy(username,daily_limit,used_today,updated_at) VALUES(?,?,0,?) ON CONFLICT(username) DO UPDATE SET daily_limit=excluded.daily_limit,updated_at=excluded.updated_at',(data.get('username'),int(data.get('daily_limit',0)),now())); audit(actor,'quota_set',data.get('username'),data); self.json({'status':'updated'}); return if path=='/api/models/request' and require(user,['owner','admin']): self.json(approval(actor,'model_policy_change',data,'high')); return if path=='/api/memory/export' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_export',data)); return if path=='/api/memory/compress' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_compress',data)); return if path=='/api/memory/request-delete' and require(user,['owner','admin']): self.json(approval(actor,'memory_delete',data,'high')); return if path=='/api/simulation/wechat' and require(user,['owner','admin']): self.json(simulate_wechat(data.get('username','sample-user'),data.get('message_type','text'),data.get('content',''))); return if path=='/api/simulation/run-suite' and require(user,['owner','admin']): self.json(run_uat()); return if path=='/api/life/diagnose' and require(user,['owner','admin']): self.json(life_diagnose_v5(actor)); return if path=='/api/life/plan' and require(user,['owner','admin']): self.json(life_plan_v5(actor)); return if path=='/api/life/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'apply_life_plan',data,'high')); return if path=='/api/patches/submit' and require(user,['owner','admin']): self.json(submit_patch_item(actor,data.get('patch_type','doc'),data.get('title','untitled'),data.get('target_area','docs'),data.get('content',''),data.get('risk','medium'))); return if path=='/api/patches/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'patch_apply',data,'high')); return if path=='/api/versions/create' and require(user,['owner','admin']): vid=uid('ver'); exec_sql('INSERT INTO baseline_versions(id,version_name,package_name,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(vid,data.get('version_name','v5-draft'),data.get('package_name','current'),data.get('summary','created from WebOps'),'draft',actor,now())); audit(actor,'version_created','baseline_versions',data); self.json({'version_id':vid}); return if path=='/api/versions/request-publish' and require(user,['owner','admin']): self.json(approval(actor,'baseline_publish',data,'critical')); return if path=='/api/optimization/diagnose' and require(user,['owner','admin']): self.json(diagnose()); return if path=='/api/optimization/apply-request' and require(user,['owner','admin']): self.json(approval(actor,'apply_self_optimization_fix',data,'high')); return if path=='/api/uat/run' and require(user,['owner','admin']): self.json(run_uat()); return self.json({'error':'forbidden_or_unknown_endpoint','path':path},403) except Exception as e: audit(actor,'api_error',path,{'error':str(e),'data':data}); self.json({'error':str(e)},500) def main(): init_db(); print(f'AIOS WebOps listening on {HOST}:{PORT} for {WEBOPS_DOMAIN}', flush=True); ThreadingHTTPServer((HOST,PORT),H).serve_forever() if __name__=='__main__': main()