diff --git a/apps/docs/public/figures/endor-deployment.svg b/apps/docs/public/figures/endor-deployment.svg new file mode 100644 index 0000000..4acabda --- /dev/null +++ b/apps/docs/public/figures/endor-deployment.svg @@ -0,0 +1,701 @@ + + + + +Endor 部署与对象挂载:本地端、自部署与平台集群 + +ENDOR / DEPLOYMENT ATLAS +本地、自部署与平台,连接到同一个对象世界 +部署位置可以不同;Tessera 通过本地运行时组织对象,并按授权挂载远端对象池。 + +自部署 A +独立服务 / 独立数据 + + + + + + + + + + +Nginx / Traefik +可选网关 · 云上可用托管入口替代 + + + + + + + + + + + + + +Nexus Runtime 01 + + + + + + + + + + + + + + + + + + +Aster VM + + + + + + + + + +Aerarium +Object data pool + + + + + + + + +PostgreSQL + + + + + + + + +S3 + + + + + + + + + +自部署 B +独立服务 / 独立数据 + + + + + + + + + + +Nginx / Traefik +可选网关 · 云上可用托管入口替代 + + + + + + + + + + + + + +Nexus Runtime 01 + + + + + + + + + + + + + + + + + + +Aster VM + + + + + + + + + + + + + + +Nexus Runtime 02 + + + + + + + + + + + + + + + + + + +Aster VM + + + + + + + + + +Aerarium +Object data pool + + + + + + + + +PostgreSQL + + + + + + + + +S3 + + + + + + + + + + + +Aerarium +Object data pool + + + + + + + + +PostgreSQL + + + + + + + + +S3 + + + + + + + + +… / etc · 更多独立自部署 + +Endor 平台集群 +平台托管 · 多区域 · 按需扩展 + + + + + + + + + + +区域入口 + + + + + + + + + + + + + + +区域入口 + + + + + + + + + + + + + + +Nexus 01 + + + + + + + + + + + + + + + + + + +Aster VM + + + + + + + + + + + +Nexus 02 + + + + + + + + + + + + + + + + + + +Aster VM + + + + + + + + + + + +Nexus 03 + + + + + + + + + + + + + + + + + + +Aster VM + +… / etc + + + + + + + + + +Aerarium 对象池 +平台分片群 · 远端挂载接入 + + + + + + + + +PostgreSQL 分片群 + + + + + + + + +S3 分片群 + + + + + +统一备份 / 区域 HA:沿用系统架构图,此处省略展开 + +自部署 Aerarium → 平台:经授权挂载对象池,保留源端的数据归属 + + +远端挂载 · 可选择自部署或 Endor 平台 + + + +本地端 + + + + + + + + + + +Windows +Tessera 前端 + + + +本地对象 + + + +挂载 + + + + + + + + + + +Nexus Runtime + + + + + + + + + + + + + + + + + + +Aster VM + + + +本机运行 / 远端对象访问 + + + + + + + + + + +Linux +Tessera 前端 + + + +本地对象 + + + +挂载 + + + + + + + + + + +Nexus Runtime + + + + + + + + + + + + + + + + + + +Aster VM + + + +本机运行 / 远端对象访问 + + + + + + + + + + +macOS +Tessera 前端 + + + +本地对象 + + + +挂载 + + + + + + + + + + +Nexus Runtime + + + + + + + + + + + + + + + + + + +Aster VM + + + +本机运行 / 远端对象访问 + + + + + + + + + + +iOS +Tessera 前端 + + + +本地对象 + + + +挂载 + + + + + + + + + + +Nexus Runtime + + + + + + + + + + + + + + + + + + +Aster VM + + + +本机运行 / 远端对象访问 + + + + + + + + + + +Android +Tessera 前端 + + + +本地对象 + + + +挂载 + + + + + + + + + + +Nexus Runtime + + + + + + + + + + + + + + + + + + +Aster VM + + + +本机运行 / 远端对象访问 + + + +本地对象 / 存储依赖 + + + +本地 Nexus 挂载远端 + + + +自部署 Aerarium 挂载到平台 +目标架构示意 · 挂载不等于复制;鉴权、命名与写入策略需另行定义。 + \ No newline at end of file diff --git a/apps/docs/src/components/ArchitectureFigure.tsx b/apps/docs/src/components/ArchitectureFigure.tsx index f717a27..9232837 100644 --- a/apps/docs/src/components/ArchitectureFigure.tsx +++ b/apps/docs/src/components/ArchitectureFigure.tsx @@ -1,8 +1,6 @@ import { useRef, useState } from "react"; -const source = "/figures/endor-architecture.svg"; - -export function ArchitectureFigure({ alt }: { alt: string }) { +export function ArchitectureFigure({ source, alt }: { source: string; alt: string }) { const dialog = useRef(null); const trigger = useRef(null); const stage = useRef(null); @@ -19,7 +17,7 @@ export function ArchitectureFigure({ alt }: { alt: string }) { ref={trigger} className="architecture-preview" onClick={open} - aria-label="放大 Endor 架构图" + aria-label={`放大:${alt}`} > {/* eslint-disable-next-line @next/next/no-img-element */} {alt} @@ -27,7 +25,7 @@ export function ArchitectureFigure({ alt }: { alt: string }) { { if ( event.target === dialog.current || diff --git a/apps/docs/src/pages/docs/[slug].tsx b/apps/docs/src/pages/docs/[slug].tsx index 786a753..7e296e5 100644 --- a/apps/docs/src/pages/docs/[slug].tsx +++ b/apps/docs/src/pages/docs/[slug].tsx @@ -113,8 +113,8 @@ export default function DocPage({

{children}

), img: ({ src, alt }) => - src === "/figures/endor-architecture.svg" ? ( - + typeof src === "string" && src.startsWith("/figures/endor-") ? ( + ) : ( // eslint-disable-next-line @next/next/no-img-element {alt diff --git a/content/docs/00-endor-concepts-draft.md b/content/docs/00-endor-concepts-draft.md index 0598dc4..b61998d 100644 --- a/content/docs/00-endor-concepts-draft.md +++ b/content/docs/00-endor-concepts-draft.md @@ -10,6 +10,18 @@ Endor 的概念模型把全球访问、区域运行时和对象数据连接起 图中绿色/蓝色实线表示请求路径或区域内数据依赖,紫色虚线表示身份控制与公钥分发,蓝色点线表示 CDN 回源,金色虚线表示向统一备份系统采集数据。连线表示可达或依赖关系,**不表示请求向所有节点广播**。 +## 本地、自部署与平台挂载 + +![Endor 部署与对象挂载:两组自部署通过 Nginx 或 Traefik 接入,各自包含一到两个 Nexus Runtime、配套 Aster VM 和 Aerarium、PostgreSQL、S3;自部署对象池可挂载到平台,Windows、Linux、macOS、iOS、Android 上的 Tessera 通过本地 Nexus 挂载本地和远端对象](/figures/endor-deployment.svg) + +这张图将部署规模收起来,强调对象可以在哪里运行、从哪里挂载。两组自部署示例分别使用一个和两个 Nexus Runtime,每个 Nexus 都带一个 Aster VM;**每个 Nexus 在示例中配一套简单的 Aerarium、PostgreSQL 和 S3**;这用于表达小规模自部署的完整组成。Aerarium 依赖 PostgreSQL 和 S3,右侧平台则将多区域、运行时和存储分片简化为小型集群。 + +自部署入口可以选择 Nginx 或 Traefik。如果云服务已经提供合适的托管入口、TLS 和路由能力,就可以省去自行部署网关这一环;仍需配置服务暴露和访问权限。 + +紫色虚线表示**自部署 Aerarium 可以经授权挂载到 Endor 平台端**:平台获得访问源对象池的逻辑入口,源端仍保留数据归属。它不是将整个 PostgreSQL 或 S3 直接公开,也不隐含自动上传、镜像、备份或数据合并。 + +下方五种系统都运行 Tessera 前端,并带有本地 Nexus Runtime 和对应 Aster VM。本地对象先挂载到自带的 Nexus;本地 Nexus 也可按需挂载自部署或平台端的远程对象池,让 Tessera 通过本地运行时访问它们。蓝色线路表示可选择的远端连接,不要求所有设备同时挂载所有服务。该图延续本文的目标架构约定,不代表五个平台的运行时集成都已完成;远端身份映射、连接协议、写入权限和断线行为仍需继续定义。 + ## 全球入口与区域入口 客户端包括浏览器、Tessera、SDK 和 CLI。`endorphin API` 是逻辑上的全球流量入口,尚未在本文规定一个真实的域名。全球调度根据健康、延迟、项目归属和数据放置策略选择区域;不能只选择网络最近的区域而忽略对象在哪个分片。 diff --git a/design/verification/deployment/expanded.png b/design/verification/deployment/expanded.png new file mode 100644 index 0000000..ca87e82 Binary files /dev/null and b/design/verification/deployment/expanded.png differ diff --git a/tools/figures/src/endor_figures/cli.py b/tools/figures/src/endor_figures/cli.py index 83e63db..dc22dc2 100644 --- a/tools/figures/src/endor_figures/cli.py +++ b/tools/figures/src/endor_figures/cli.py @@ -2,6 +2,7 @@ from pathlib import Path import drawsvg as draw from .architecture import build_architecture +from .deployment import build_deployment def main() -> None: @@ -36,3 +37,7 @@ def main() -> None: topology = output.parent / "endor-architecture.svg" build_architecture(topology) print(f"Generated {topology.relative_to(root)}") + + deployment = output.parent / "endor-deployment.svg" + build_deployment(deployment) + print(f"Generated {deployment.relative_to(root)}") diff --git a/tools/figures/src/endor_figures/deployment.py b/tools/figures/src/endor_figures/deployment.py new file mode 100644 index 0000000..f7436af --- /dev/null +++ b/tools/figures/src/endor_figures/deployment.py @@ -0,0 +1,189 @@ +"""Deployment choices and explicit remote object-pool mounts; no replication implied.""" +from pathlib import Path +import math +import drawsvg as d +from .architecture import INK, GREEN, BLUE, PURPLE, MUTED, LINE, FONT + + +def build_deployment(output: Path) -> None: + svg = d.Drawing(1800, 1550) + svg.append_title('Endor 部署与对象挂载:本地端、自部署与平台集群') + svg.append(d.Rectangle(0, 0, 1800, 1550, fill='#fcfdfb')) + + 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 box(x, y, w, h, fill='white', dash=None): + svg.append(d.Rectangle(x, y, w, h, rx=16, fill=fill, stroke=LINE, + stroke_width=1.3, stroke_dasharray=dash)) + + def wire(points, color=GREEN, dash=None, arrow=False, width=1.6): + p = d.Path(fill='none', stroke=color, stroke_width=width, + stroke_dasharray=dash, stroke_linejoin='round', stroke_linecap='round') + p.M(*points[0]) + for point in points[1:]: + p.L(*point) + svg.append(p) + if arrow: + x, y = points[-1] + px, py = points[-2] + angle = math.atan2(y-py, x-px) + for sign in [-1, 1]: + svg.append(d.Line(x-7*math.cos(angle+sign*.48), + y-7*math.sin(angle+sign*.48), x, y, + stroke=color, stroke_width=width)) + + def dot(x, y, color=GREEN): + svg.append(d.Circle(x, y, 3.5, fill='#fcfdfb', stroke=color, stroke_width=1.5)) + + def icon(kind, x, y, color=GREEN, scale=1): + g = d.Group(transform=f'translate({x},{y}) scale({scale})', fill='none', + stroke=color, stroke_width=1.8, stroke_linejoin='round', stroke_linecap='round') + if kind == 'runtime': + for yy in [-17, 0]: + g.append(d.Rectangle(-26, yy, 52, 12, rx=3)) + g.append(d.Circle(-18, yy+6, 1.5, fill=color)) + g.append(d.Line(-10, yy+6, 17, yy+6)) + g.append(d.Lines(-13,20,13,20)); g.append(d.Line(0,12,0,20)) + elif kind == 'vm': + g.append(d.Rectangle(-16,-12,32,24,rx=4)) + for a in [-9,0,9]: + g.append(d.Line(a,-17,a,-12)); g.append(d.Line(a,12,a,17)) + for a in [-6,6]: + g.append(d.Line(-21,a,-16,a)); g.append(d.Line(16,a,21,a)) + g.append(d.Lines(-6,-5,0,0,-6,5)); g.append(d.Line(5,5,10,5)) + elif kind == 'pool': + for dx,dy in [(-12,-10),(12,-10),(0,13)]: + g.append(d.Rectangle(dx-10,dy-10,20,20,rx=5)) + g.append(d.Circle(dx,dy,2.5,fill=color)) + elif kind == 'db': + g.append(d.Ellipse(0,-14,22,8)) + g.append(d.Path().M(-22,-14).L(-22,18).C(-22,30,22,30,22,18).L(22,-14)) + g.append(d.Path().M(-22,1).C(-22,13,22,13,22,1)) + elif kind == 's3': + g.append(d.Ellipse(0,-16,23,7)) + g.append(d.Lines(-23,-16,-18,23,18,23,23,-16)) + g.append(d.Path().M(-17,6).Q(0,17,17,6)) + elif kind == 'gateway': + g.append(d.Lines(0,-24,24,0,0,24,-24,0,0,-24)) + g.append(d.Line(-34,0,-12,0)) + for a in [-13,0,13]: + g.append(d.Lines(-8,0,8,a,34,a,29,a-4)) + g.append(d.Line(34,a,29,a+4)) + elif kind == 'laptop': + g.append(d.Rectangle(-37,-27,74,46,rx=5)) + g.append(d.Lines(-37,19,-46,30,46,30,37,19)) + g.append(d.Line(-8,25,8,25)) + elif kind == 'phone': + g.append(d.Rectangle(-21,-35,42,72,rx=8)) + g.append(d.Line(-7,-28,7,-28)); g.append(d.Line(-5,30,5,30)) + svg.append(g) + + def runtime(x, y, label, color=GREEN, compact=False): + icon('runtime', x, y, color, .85 if compact else 1) + text(label,x,y+43,14,color=INK,weight='600',anchor='middle') + wire([(x+26,y+5),(x+58,y+5),(x+58,y+68)],color,'2 4',arrow=True,width=1.1) + icon('vm',x+58,y+83,color,.62) + text('Aster VM',x+58,y+112,12,MUTED,anchor='middle') + + text('ENDOR / DEPLOYMENT ATLAS',40,43,14,GREEN,'600') + text('本地、自部署与平台,连接到同一个对象世界',40,91,29,INK,'600') + text('部署位置可以不同;Tessera 通过本地运行时组织对象,并按授权挂载远端对象池。',40,123,16,MUTED) + + # Two independent self-hosted examples, each with its own simple storage stack. + for x, name, count in [(40,'自部署 A',1),(565,'自部署 B',2)]: + cx=x+240 + box(x,165,480,700) + text(name,x+24,204,23,INK,'600') + text('独立服务 / 独立数据',x+24,231,13,MUTED) + icon('gateway',cx,295) + text('Nginx / Traefik',cx,345,17,INK,'600','middle') + text('可选网关 · 云上可用托管入口替代',cx,371,13,MUTED,anchor='middle') + xs=[cx] if count==1 else [x+125,x+325] + for i,nx in enumerate(xs): + wire([(cx,385),(nx,419)],arrow=True) + runtime(nx,445,f'Nexus Runtime {i+1:02}') + wire([(nx-28,445),(nx-50,445),(nx-50,594),(nx,594),(nx,611)],width=1.2) + for nx in xs: + # Small deployments deliberately show a simple stack per Runtime. + icon('pool',nx,642,scale=.8) + text('Aerarium',nx,688,17,INK,'600','middle') + text('Object data pool',nx,711,11,MUTED,anchor='middle') + for target,kind,label in [(nx-65,'db','PostgreSQL'),(nx+65,'s3','S3')]: + wire([(nx-25 if kind=='db' else nx+25,650),(target,748)],arrow=True) + icon(kind,target,777,scale=.7) + text(label,target,823,13,INK,'500','middle') + # Every pool has an explicit route onto the platform mount lane. + wire([(nx+24,628),(nx+50,628),(nx+50,590),(x+450,590), + (x+450,925 if count==1 else 955), + (1100 if count==1 else 1080,925 if count==1 else 955), + (1100 if count==1 else 1080,610 if count==1 else 640), + (1195,610 if count==1 else 640)],PURPLE,'6 6',arrow=True) + wire([(cx,865),(x+16,865),(x+16,295),(cx-35,295)],BLUE,'2 5',arrow=True,width=1.1) + dot(cx,865,BLUE) + wire([(cx,865),(cx,901)],BLUE) + text('… / etc · 更多独立自部署',565,897,14,MUTED) + + # Simplified platform: two gateways, a complete K2,3 mesh, grouped storage. + box(1120,165,640,700) + text('Endor 平台集群',1144,204,23,INK,'600') + text('平台托管 · 多区域 · 按需扩展',1144,231,13,MUTED) + for gx in [1300,1580]: + icon('gateway',gx,295,BLUE,.8) + text('区域入口',gx,337,14,INK,'500','middle') + for nx in [1220,1430,1640]: + wire([(gx,350),(nx,406)],BLUE,width=1.1) + dot(gx,350,BLUE) + for i,nx in enumerate([1220,1430,1640]): + runtime(nx,432,f'Nexus {i+1:02}',BLUE,True) + wire([(nx-25,432),(nx-48,432),(nx-48,575),(1440,575),(1440,600)],BLUE,width=1) + text('… / etc',1708,426,13,BLUE,anchor='middle') + box(1195,600,490,99,'#f5f8fb') + icon('pool',1240,642,BLUE,.8) + text('Aerarium 对象池',1282,637,20,INK,'600') + text('平台分片群 · 远端挂载接入',1282,669,13,MUTED) + for target,kind,label in [(1280,'db','PostgreSQL 分片群'),(1600,'s3','S3 分片群')]: + wire([(1440,699),(target,744)],BLUE,arrow=True) + icon(kind,target,773,BLUE,.85) + text(label,target,824,15,INK,'500','middle') + wire([(1440,865),(1740,865),(1740,295),(1608,295)],BLUE,'2 5',arrow=True,width=1.1) + dot(1440,865,BLUE) + wire([(1440,865),(1440,1015)],BLUE) + text('统一备份 / 区域 HA:沿用系统架构图,此处省略展开',1440,851,12,MUTED,anchor='middle') + + # Dedicated labelled lanes keep federation separate from local remote-access mounts. + box(500,969,780,31,'#fcfdfb') + text('自部署 Aerarium → 平台:经授权挂载对象池,保留源端的数据归属',890,990,15,PURPLE,anchor='middle') + wire([(280,901),(280,1015),(1640,1015)],BLUE) + wire([(805,901),(805,1015)],BLUE) + text('远端挂载 · 可选择自部署或 Endor 平台',40,990,15,BLUE,'500') + for x in [280,805,1440]: + dot(x,1015,BLUE) + + text('本地端',40,1071,23,INK,'600') + for i,(name,kind) in enumerate([('Windows','laptop'),('Linux','laptop'),('macOS','laptop'),('iOS','phone'),('Android','phone')]): + x=40+i*350 + cx=x+160 + box(x,1100,320,340,'#f9fbf8') + wire([(cx,1100),(cx,1015)],BLUE,arrow=True) + dot(cx,1015,BLUE) + icon(kind,x+68,1160,GREEN,.7) + text(name,x+118,1147,20,INK,'600') + text('Tessera 前端',x+118,1178,16,GREEN,'500') + wire([(x+145,1190),(x+145,1200),(x+65,1200),(x+65,1208)],arrow=True,width=1.1) + text('本地对象',x+38,1226,14,INK,'500') + wire([(x+105,1221),(x+155,1221),(x+155,1255)],arrow=True) + text('挂载',x+166,1240,11,MUTED) + runtime(x+155,1284,'Nexus Runtime',compact=True) + wire([(x+181,1284),(x+293,1284),(x+293,1115),(cx,1115),(cx,1100)],BLUE,'2 5',arrow=True,width=1.2) + text('本机运行 / 远端对象访问',cx,1420,12,MUTED,anchor='middle') + + wire([(40,1482),(100,1482)],GREEN,arrow=True) + text('本地对象 / 存储依赖',114,1487,13,MUTED) + wire([(435,1482),(495,1482)],BLUE,'2 5',arrow=True) + text('本地 Nexus 挂载远端',509,1487,13,MUTED) + wire([(850,1482),(910,1482)],PURPLE,'6 6',arrow=True) + text('自部署 Aerarium 挂载到平台',924,1487,13,MUTED) + text('目标架构示意 · 挂载不等于复制;鉴权、命名与写入策略需另行定义。',40,1526,13,MUTED) + svg.save_svg(str(output))