"""Endor concept topology: explicit meshes, storage dependencies and recovery paths.""" from pathlib import Path import drawsvg as d INK = '#233b38' GREEN = '#2d8b77' BLUE = '#467fb4' PURPLE = '#8a6ab4' AMBER = '#b58b47' MUTED = '#6d817c' LINE = '#dce8e3' FONT = 'ui-sans-serif, system-ui, -apple-system, PingFang SC, Microsoft YaHei, sans-serif' def build_architecture(output: Path) -> None: svg = d.Drawing(2000, 1860) svg.append_title('Endor 架构:全球入口、区域网关、独立存储分片与统一备份') svg.append(d.Rectangle(0, 0, 2000, 1860, fill='#fcfdfb')) def rect(x, y, w, h, fill='white', stroke=LINE, radius=12, dash=None): svg.append(d.Rectangle(x, y, w, h, rx=radius, fill=fill, stroke=stroke, stroke_width=1.2, stroke_dasharray=dash)) def text(s, x, y, size=16, color=INK, weight='400', anchor='start'): svg.append(d.Text(s, size, x, y, fill=color, font_family=FONT, font_weight=weight, text_anchor=anchor)) def path(points, color=GREEN, dash=None, width=1.6, arrow=False, opacity=1): p = d.Path(fill='none', stroke=color, stroke_width=width, stroke_dasharray=dash, stroke_linecap='round', stroke_linejoin='round', opacity=opacity) p.M(*points[0]) for pt in points[1:]: p.L(*pt) svg.append(p) if arrow: x, y = points[-1] px, py = points[-2] import math angle = math.atan2(y-py, x-px) a = d.Path(fill='none', stroke=color, stroke_width=width, stroke_linecap='round', opacity=opacity) for sign in [-1, 1]: a.M(x-7*math.cos(angle+sign*.48), y-7*math.sin(angle+sign*.48)).L(x, y) svg.append(a) def dot(x, y, color=GREEN, r=3): svg.append(d.Circle(x, y, r, fill='#fcfdfb', stroke=color, stroke_width=1.5)) def icon(kind, x, y, color=GREEN, scale=1): # Icons are vector geometry rather than font glyphs or downloaded artwork. g = d.Group(transform=f'translate({x},{y}) scale({scale})', stroke=color, stroke_width=1.8, fill='none', stroke_linecap='round', stroke_linejoin='round') if kind == 'globe': g.append(d.Circle(0, 0, 23)); g.append(d.Ellipse(0, 0, 10, 23)) g.append(d.Line(-22, -8, 22, -8)); g.append(d.Line(-22, 8, 22, 8)) elif kind == 'client': g.append(d.Rectangle(-26,-20,52,33,rx=4)); g.append(d.Line(-32,21,32,21)) g.append(d.Lines(-26,13,-32,21,32,21,26,13)) g.append(d.Line(-6,17,6,17)) elif kind == 'edge': g.append(d.Circle(0,0,24)); g.append(d.Lines(-10,-6,0,-14,11,-6,0,2,-10,-6)) g.append(d.Lines(-10,3,0,11,11,3)); g.append(d.Lines(-10,11,0,19,11,11)) elif kind == 'shield': g.append(d.Path().M(0,-25).L(22,-16).L(20,5).Q(17,19,0,27).Q(-17,19,-20,5).L(-22,-16).Z()) g.append(d.Lines(-9,0,-2,7,11,-7)) elif kind == 'lb': g.append(d.Circle(0,-17,7)); g.append(d.Circle(-22,19,7)); g.append(d.Circle(22,19,7)) g.append(d.Lines(0,-10,0,1,-22,1,-22,12)); g.append(d.Lines(0,1,22,1,22,12)) elif kind == 'gateway': # Routing diamond: deliberately distinct from a Runtime server. g.append(d.Lines(0,-23,23,0,0,23,-23,0,0,-23)) g.append(d.Line(-34,0,-12,0)); g.append(d.Lines(-17,-5,-12,0,-17,5)) for yy in [-12,0,12]: g.append(d.Lines(0,0,9,yy,34,yy)) g.append(d.Lines(29,yy-4,34,yy,29,yy+4)) elif kind == 'runtime': for dy in [-14,2]: g.append(d.Rectangle(-25,dy,50,12,rx=3)) g.append(d.Circle(-16,dy+6,1.3,fill=color)); g.append(d.Line(-8,dy+6,16,dy+6)) if kind == 'runtime': g.append(d.Line(-13,20,13,20)); g.append(d.Line(0,14,0,20)) elif kind == 'vm': g.append(d.Rectangle(-17,-13,34,26,rx=4)) for a in [-10,0,10]: g.append(d.Line(a,-18,a,-13)); g.append(d.Line(a,13,a,18)) for a in [-6,6]: g.append(d.Line(-22,a,-17,a)); g.append(d.Line(17,a,22,a)) g.append(d.Path().M(-6,-5).L(0,0).L(-6,5)); g.append(d.Line(4,6,10,6)) elif kind == 'db': g.append(d.Path().M(-21,-12).L(-21,15).C(-21,27,21,27,21,15).L(21,-12)) g.append(d.Ellipse(0,-12,21,8)); g.append(d.Path().M(-21,1).C(-21,12,21,12,21,1)) elif kind == 'pool': for dx,dy in [(-12,-9),(12,-9),(0,12)]: g.append(d.Rectangle(dx-11,dy-10,22,20,rx=5)) g.append(d.Circle(dx,dy,3,fill=color,stroke='none')) elif kind == 'bucket': g.append(d.Ellipse(0,-15,23,7)); g.append(d.Lines(-23,-15,-18,21,18,21,23,-15)) g.append(d.Path().M(-17,6).Q(0,17,17,6)) elif kind == 'backup': g.append(d.Rectangle(-25,-15,50,36,rx=4)); g.append(d.Line(-29,-15,29,-15)) g.append(d.Lines(-6,5,0,-2,6,5)); g.append(d.Line(0,-2,0,14)) svg.append(g) def badge(s, x, y, color=GREEN, bg='#edf5ee', w=118): rect(x, y-17, w, 25, bg, 'none', 5) text(s, x+w/2, y, 12, color, '500', 'middle') text('ENDOR / SYSTEM ATLAS', 40, 42, 14, GREEN, '600') text('01 全球接入', 40, 83, 13, MUTED, '600') path([(160,143),(550,143)], arrow=True) text('动态 API / 对象访问',280,131,13,GREEN) path([(110,113),(110,100),(1300,100),(1300,114)],BLUE,arrow=True) text('静态资源 / 可缓存内容',1000,91,13,BLUE) path([(110,179),(110,214),(1750,214),(1750,179)],PURPLE,'5 6',arrow=True) text('登录 / 换取凭证',1470,204,12,PURPLE) icon('client',110,143) text('全球客户端',110,191,17,INK,'600','middle') icon('globe',600,143) text('endorphin API',654,138,24,INK,'600') text('全球流量入口 · 按健康与策略选区',654,165,14,MUTED) icon('edge',1300,143,BLUE) text('CDN',1340,137,21,INK,'600') text('命中直接返回',1340,162,13,MUTED) icon('shield',1750,143,PURPLE) text('身份服务',1790,138,18,INK,'600') text('登录 / 签发凭证',1790,162,12,MUTED) # A real fan-out includes an open-ended third branch, not just two regions. path([(835,180),(835,243),(1900,243),(1900,306)],GREEN,arrow=True) rect(1855,315,110,132,'#f8faf7',LINE,12,'3 5') text('… / etc',1910,354,23,GREEN,'500','middle') text('其他区域',1910,388,15,INK,'600','middle') text('按需扩展',1910,418,12,MUTED,'400','middle') regions = [('北美','NA',40,GREEN),('亚洲','AS',960,BLUE)] # Each storage family owns its own shard set. No PG/Aerarium/S3 triplets. families = [('PostgreSQL','PG','db',150), ('Aerarium','AO','pool',435), ('S3','S3','bucket',720)] backup_ports = [] for name,prefix,ox,accent in regions: center = ox+435 rect(ox,275,870,1025,'#ffffff',LINE,18) text(f'{name}区域',ox+28,314,24,INK,'600') text(f'{prefix} / 独立故障域',ox+28,340,12,MUTED) badge('区域入口',ox+708,316,accent,'#eff5f0',132) path([(835,180),(835,243),(center,243),(center,352)],GREEN,arrow=True) icon('lb',center,377,accent,.72) text('区域负载均衡',center+42,372,16,INK,'600') text('健康检查 / TLS 路由',center+42,396,12,MUTED) gateways = [ox+265,ox+605] runtime_x = [ox+105+i*165 for i in range(5)] runtime_y = [605,619,605,619,605] for gx in gateways: path([(center,403),(center,423),(gx,423),(gx,444)],accent,arrow=True) text('TRAEFIK',ox+26,467,12,MUTED,'600') text('生产冗余 · 可选',ox+26,491,12,MUTED) for i,gx in enumerate(gateways): icon('gateway',gx,467,accent,.85) text(f'Traefik {i+1}',gx,510,14,INK,'500','middle') if i == 1: badge('HA 副本',gx+49,469,accent,'#edf5f0',85) for nx,ny in zip(runtime_x,runtime_y): path([(gx,523),(nx,ny-30)],accent,width=1.35,opacity=.65) dot(gx,523,accent) text('K₂,₅ 全连接',ox+23,556,11,accent) for i,(nx,ny) in enumerate(zip(runtime_x,runtime_y)): dot(nx,ny-30,accent) icon('runtime',nx,ny,accent) text(f'Nexus Runtime {i+1:02}',nx,ny+39,13,INK,'600','middle') # VM deliberately offset to the lower right of its host. path([(nx+26,ny+5),(nx+64,ny+5),(nx+64,ny+70),(nx+50,ny+70)],accent,'2 4',width=1) icon('vm',nx+35,ny+70,accent,.55) text('Aster VM',nx+35,ny+100,11,INK,'400','middle') # Route storage traffic outside the label/VM, not through them. path([(nx-26,ny),(nx-61,ny),(nx-61,747)],accent,width=1.1,opacity=.75) path([(runtime_x[0]-61,747),(runtime_x[-1]-61,747)],accent,width=1.2) path([(center,747),(center,793)],accent,arrow=True) text('分片定位 / 对象访问',center+16,775,12,MUTED) text('02 独立存储分片群',ox+26,775,13,INK,'600') ys = [880,1020,1160] for label,code,kind,dx in families: cx=ox+dx rect(cx-95,802,190,468,'#f8faf7','#e3ebe2',10) text(label,cx,829,17,INK,'600','middle') text('Object data pool' if code=='AO' else '独立分片群',cx,850,11,MUTED,'400','middle') for i,sy in enumerate(ys): icon(kind,cx,sy,accent,.8) text(f'{prefix}-{code}-{i+1:02}',cx,sy+36,12,INK,'500','middle') path([(cx,sy+43),(cx,sy+54)],accent,'2 4',width=1) icon(kind,cx,sy+69,accent,.32) text('区域 HA',cx+23,sy+73,10,MUTED) dot(cx,1270,AMBER) backup_ports.append((prefix,code,cx)) # K3,3 on each side: every Aerarium shard can address every PG/S3 shard. # The two storage families do not gain a fictitious direct PG-to-S3 edge. pg,ao,s3 = ox+150,ox+435,ox+720 for ay in ys: path([(ao-25,ay),(ao-75,ay)],accent,width=1.1) path([(ao+25,ay),(ao+75,ay)],accent,width=1.1) for by in ys: path([(ao-75,ay),(pg+75,by)],accent,width=1.15,opacity=.55) path([(ao+75,ay),(s3-75,by)],accent,width=1.15,opacity=.55) dot(ao-75,ay,accent,2.5); dot(ao+75,ay,accent,2.5) for sy in ys: path([(pg+75,sy),(pg+25,sy)],accent,width=1.1,arrow=True) path([(s3-75,sy),(s3-25,sy)],accent,width=1.1,arrow=True) dot(pg+75,sy,accent,2.5); dot(s3-75,sy,accent,2.5) text('Aerarium ↔ PG / S3:各分片群全连接,按归属与映射定位数据',center,1290,12,MUTED,'400','middle') # Identity is a control-plane relation; representative endpoints are labeled. path([(1750,180),(1750,227),(935,227),(935,653)],PURPLE,'5 6') path([(935,653),(900,653),(900,605),(871,605)],PURPLE,'5 6',arrow=True) path([(935,653),(985,653),(985,605),(1038,605)],PURPLE,'5 6',arrow=True) rect(870,667,130,49,'#fcfdfb','none',4) text('JWKS / 凭证验证',935,686,11,PURPLE,anchor='middle') text('Nexus 本地验证',935,705,10,PURPLE,anchor='middle') # CDN fills from an S3 shard, without crossing other storage labels. path([(1300,180),(1300,207),(1975,207),(1975,885),(1808,885),(1808,880),(1705,880)],BLUE,'2 6',arrow=True) rect(1948,559,49,51,'#fcfdfb','none',3) text('CDN',1972,580,11,BLUE,anchor='middle') text('回源',1972,601,11,BLUE,anchor='middle') # One system, not a North America / Asia pair of backup silos. for i,(prefix,code,cx) in enumerate(backup_ports): dest={'PG':370,'AO':980,'S3':1580}[code] lane=1325+i*18 path([(cx,1270),(cx,lane),(dest,lane),(dest,1460)],AMBER,'6 6',arrow=True) rect(40,1460,1790,280,'#fbf8f1','#e9dec8',16) text('03 统一备份系统',68,1502,23,INK,'600') text('全区域 / 全部分片 · 统一编排、持久化、校验与恢复',68,1531,14,MUTED) for kind,cx,label,line1,line2 in [ ('db',370,'PostgreSQL 备份','基础备份 + WAL / 恢复点','各区域 PostgreSQL 分片的事务恢复材料'), ('pool',980,'Aerarium 备份','对象池状态 + 索引 / 版本映射','关联对象、PG 恢复点与 S3 对象版本'), ('bucket',1580,'S3 备份','对象版本 + Blob / 快照','各区域 S3 分片的实际对象与文件数据'), ]: icon(kind,cx-145,1596,AMBER,1.05) text(label,cx-95,1587,18,INK,'600') text(line1,cx-95,1615,14,MUTED) text(line2,cx-95,1641,12,MUTED) path([(90,1670),(1780,1670)],'#e3d5b9',width=1) text('统一恢复目录:区域 / 存储类型 / 分片 ID / 恢复清单 / 校验和 / 重试状态',935,1709,14,AMBER,'500','middle') path([(50,1790),(105,1790)],GREEN,arrow=True) text('请求 / 存储依赖',118,1795,12,MUTED) path([(365,1790),(420,1790)],PURPLE,'5 6') text('身份控制 / 公钥',433,1795,12,MUTED) path([(680,1790),(735,1790)],BLUE,'2 6') text('缓存回源',748,1795,12,MUTED) path([(965,1790),(1020,1790)],AMBER,'6 6') text('统一备份采集',1033,1795,12,MUTED) text('实线为逻辑可达关系;全连接不表示广播,也不表示任意分片拥有相同数据。',40,1837,12,MUTED) text('备份保证以已持久化且已校验的恢复点为边界。',1830,1837,12,AMBER,anchor='end') output.parent.mkdir(parents=True, exist_ok=True) svg.save_svg(str(output))