Back to "StyloBot 系列发布丛书 @:侧边车结构"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Architecture ASP.NET Core Go gRPC StyloBot

StyloBot 系列发布丛书 @:侧边车结构

Monday, 01 June 2026

StyloBot'}检测引擎为 ASP.NET Core. 此文章解释该引擎如何连接 Go 网关@ ,NodeMS K4js 应用程序 @,和任何其他通过 gRPC 侧边car},}(一种打字的GO SDKMSsk7)和Cddy插件{,,而没有任何消费者需要了解有关“ MS K9Net 内线” 的任何信息

StyloBot

缩略 github.com/scottgal/stylobot-go SDK , github.com/scottgal/caddy-stylobot 插件@,和 Mostlylucid.BotDetection.Sidecar 容器将很快发布@. 下方所有的东西都描述表面 它们会暴露在地面上

StyloBot 释放系列

  1. 行为@, @% 不是身份为何StyloBot以行为方式模拟客户?
  2. ASP=.@NET 用户界面@ : 服务器@ MS K1 {. NET 应用程序的表面=% 1}
  3. 在LongM-Running .NET服务中查找和固定无约束增长使引擎在生产过程中无趣的可靠性纪律
  4. 檔案型態Script UI: Express, fastifyMS K2 和浏览器组件
  5. 侧车建筑此篇文章 :
  6. 学习加快速度“:”适应性学习系统“, 4 -”记忆和裁决缓存
  7. 静坐不动的测试: 校验纪律 @: 一个 BDF 文件驱动回溯@,load @MS K3和校准
  8. StyloExptracte {-} 本地学习 HTML 到 标记下转换器: HTML→ Markdown 层与探测器对齐 MSSK2 行尸虫清晰识别捕捉了, 和狗食环让它诚实

为什么是副司机?

反向代理是运行 Bot 检测的明显位置@: @ it sit between to everything #,看到每一个请求,}在任何应用程序代码运行前可以屏蔽此逻辑一直持续到您询问您的应用程序应该如何应用为止 不同行为 基于谁提出此请求 .}一个截断区块的网关是门;}大多数应用程序实际需要的是 他们可以以多种方式进行裁决

侧车模式区分了两个关注点 @.+#网关保持快速和无国籍@.Q}侧车维护着会话状态,+Achnestyle holds%,+MSK3漂移 state{,}和判决缓存,使得检测准确的=(+AgromZK6ANCHor 模型MS K7}(per-指印裁决缓存_,Q)和四兆-级学习系统覆盖在内。 学习加快速度@ )._ 本地网络上的两个通讯@({same hostor 或同一个Pod)}(所以圆+-}♪trip是微秒, 而非远端API呼叫的 MS K6#MSKKK7}(因为悬浮预算使得所有-) 请求检测都切实可行

这不是一个新模式 @. @ 代理特使 服务@- @mesh concerns & ( @mTLS#,retries}%,{断路);} 年四月 州和区 开放遥测采集器 用于遥测@. Linkerd, Consultion control, 和 AWS App Mesh 都遵循相同的模式.。 该图案之所以持续出现,是因为它解决了一个真正的问题:你想要复杂的、有特点的行为来跨越语言界限而不在每种语言中重新实施\MS K5

为什么不把它嵌入一个图书馆呢?

选项是直接将检测结果汇编到网关@._For Go, 这意味着纯端端端关-_Go 重新实施或 CGO 与 C 库绑定._BARBAR_对节点它意味着运行在{-}程序中的检测结果与应用程序连接\MS K4}

这种复杂的引擎“.QStyloBot”在四个执行组织上安装了“49Q”探测器,这对这个复杂系统的发动机也是不切实际的。 波浪 @(later 波浪只有在早期信号证明有必要时才会起火, Markov链矢量 在 “129-” 空间中,“MSK0#二维空间”".}“马可夫链条”只是“"*given”的概率模型。 这届会议的最后一件事是:“, ”接下来会发生什么?“?",”和“MS K5 维度捕捉到足够的页面@-过渡形状来分辨人与机器人 =.它运行着 Leiden社区检测 在矢量之上 : 一种图形\ MS K1 组合算法, 即分组会议都表现得一样 MPK2 这是StyloBot如何点出一个机器人网络的。 即使单个会话看起来很精细, StyloBot 在请求@. 之间将所有这一切都持续到 SQLite . 状态需要一个独立的生命周期@. 它无法重新启动节点进程或在网关重新加载其配置时被拆卸 =.}

随身车让每个部件都做它擅长的事 :

graph TD
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px
    classDef store fill:none,stroke:#f59e0b,stroke-width:2px

    GW["Gateway<br/>Caddy / YARP / nginx"]:::input
    SD["StyloBot Sidecar · ASP.NET Core<br/>gRPC :5090 · REST :5091<br/>≤50ms per Detect RPC"]:::async
    APP["Upstream Application<br/>Node / Go / ASP.NET<br/>reads req.stylobot.verdict"]:::good
    DB[("SQLite<br/>sessions · signatures · reputation")]:::store

    GW --> SD
    SD <--> DB
    GW --> APP

网关呼叫侧车@,}输入 HTTP 页头 @,和可选区块_.。 上游应用程序读信头,并按此裁决采取行动*.* 侧车在请求中坚持状态. 检测管道在GRPC的单个电话中运行,其结果传播为九个HTTP页头.

侧车

Mostlylucid.BotDetection.Sidecar 是一个最小的 ASP.NET Core process < . > 它没有为{,服务的任何静态文件,也没有GRPC和REST端点之外的路线@.}它启动全检测引擎并暴露了两个端口_:}

  • :5090: HTTP/2 gRPC 客户端@(gatewayMS K3 Groups , 节点 gRPC client)
  • :5091: HTTP/1.1 用于再生客户 *, re-出口品 /api/v1/* 终点

GRPC 吉普尔

GRPC 吉普尔 是谷歌Google开发的高级- 性能远程程序呼叫框架 协议缓冲 (protobuf) 以其铁丝格式@:为缩略语编码, 比 JSONMS K3gRPC 在 HTTP}/2, 上运行更快和小于JSON 这意味着它在一个 TCP 连接中获得多传@( 多个请求@MS K6 free_.}

界面定义在 .proto {.在该文件中產生程式碼發動器 以任何支援的語言產生已輸入的客戶端與伺服器根管@.StyloBot .proto gRPC 执行中的任何语言文件都可以调用 p侧边车@ : @ Go,}Node @ MSQZQ} Python@ MPK3_ Rust, JavaMS K5和许多其他语种@.

GRPC 界面

该服务有3个RPCs:

service DetectionService {
  rpc Detect(DetectRequest)             returns (DetectResponse);
  rpc DetectBatch(DetectBatchRequest)   returns (DetectBatchResponse);
  rpc RenderWidget(RenderWidgetRequest) returns (RenderWidgetResponse);
}

Detect 请求熱路徑@. @ 通过方法 @ ,_ 路径 @ I,# headers@ MS K5}和可选的 TLS 指纹数据 @.{

DetectBatch 用于对日志重放和离线分析的 ,\ {没有 per-}_请求使用网关@.

RenderWidget 接受一个流动性模板字符串@,; 一个可选的判定@ ,; 和一个关于其他变量的密钥{-=@MS K3然后使模板服务器 @-side 并返回 HTML}{MS_.(这是非-.}NET调用器如何生成bot-}AverHTML, 而没有在下面 <.>}

探查电话里面发生了什么?

图表前的快速词汇表@,}因为这些名字将出现 @:

  • 黑板:a a per- 要求键 -}价值袋 @[email protected]=. request.ip.is_datacenter, detection.useragent.confidence“). 探测器向它发送信号”“; 同一波的后继探测器读到”. “Lives 仅限在一项请求提出期间使用”,“MS K3 Raw PII ‘( IP ,’ UA string ) 在请求中停留,并且从不降落在黑板上$.
  • HttpContext 合成文一个站台 HttpContext gRPC 服务基于原始请求字段=.} 检测引擎是为ASP_.\NET中继软件设计的,预计读取 HttpContext; 合成一个让同一引擎在 gRPC 调用 gRPC 中运行不变
  • 缴款 / 合计证据@:Q 探测器输出@(QSignal写道: @MS K2Q Information deltas*)Q 及其合并结果 @.Q}
sequenceDiagram
    participant GW as Gateway (Caddy)
    participant SD as gRPC Service
    participant ORC as BlackboardOrchestrator
    participant DET as Detectors (up to 49, 4 waves)
    participant DB as SQLite

    GW->>SD: Detect RPC { method, path, headers, remoteIp }
    SD->>ORC: DetectAsync(syntheticHttpContext)
    ORC->>DET: Wave 0 - Identity + ContentSequence
    ORC->>DET: Wave 1 - Fast path <1ms: UA, Header, IP, Heuristic ...
    ORC->>DET: Wave 2 - Session vectors, Behavioural waveform
    ORC->>DET: Wave 3 - Slow path: DNS, advanced fingerprinting
    DET-->>ORC: DetectionContributions (signals, confidence deltas)
    ORC->>DB: update session vector and reputation score
    DB-->>ORC: ok
    ORC-->>SD: AggregatedEvidence { botProbability, riskBand, ... }
    SD-->>GW: DetectResponse { isBot, riskBand, recommendedAction, ... }

整个管道在单一的 GRPC 呼叫@. 响应返回后没有非同步工作 @.\

吴SDK

Go 的网关代码无法导入 ASP.NET 侧面carMS K1} 它能做的是把它称为 gRPC.GOSDK {(}github.com/scottgal/stylobot-go)}提供一个打印界面,将生成的原生虫类型从呼叫者中隐藏起来

为什么SDK隐藏了原药类型

Protobuf-}生成代码是verbose, 且有异常的 APIMS K1}Enums 表示为整数{.字符串作为原始原生昆虫名 < ( }RISK_BAND_HIGH# , # 不是# "High"“). 野外名字是一些发电机中的骆驼卡片, 在另一些发电机中则是蛇”“_ case”, “MS K2 在您的公共API中展出原型”意指你的呼叫者必须理解所有这一切'.

SDK在边界处将“(proto enums”翻译一次,翻译成 " canonical trings", proto structs 到普通的Go structs@)*,打电话的人从不看它 . 。

// the only interface you depend on: no proto imports required
type Client interface {
    Detect(ctx context.Context, req DetectRequest) (*Verdict, error)
    DetectBatch(ctx context.Context, reqs []DetectRequest) ([]*Verdict, error)
    RenderWidget(ctx context.Context, req RenderRequest) (*RenderResponse, error)
    Close() error
}

DetectRequestVerdict 是普通的 Go ructs @ :

type DetectRequest struct {
    Method   string
    Path     string
    Headers  map[string]string
    RemoteIP string
    Protocol string  // "http" or "https"; defaults to "https" if empty
    TLS      *TLSInfo
}

type Verdict struct {
    IsBot             bool
    BotProbability    float32
    Confidence        float32
    BotType           string   // "AiBot", "Scraper", "GoodBot", ...
    BotName           string
    RiskBand          string   // "VeryLow", "Low", "Elevated", "Medium", "High", "VeryHigh"
    RecommendedAction string   // "Allow", "Throttle", "Challenge", "Block"
    ThreatScore       float32
    ThreatBand        string
    ProcessingTimeMs  float32
    DetectorsRun      int32
    Reasons           []Reason
}

创建客户端并运行检测@: @% 1

import (
    stylobot "github.com/scottgal/stylobot-go"
    "context"
    "time"
)

client, err := stylobot.NewClient(
    "localhost:5090",
    stylobot.WithTimeout(50 * time.Millisecond),
    stylobot.WithAPIKey(os.Getenv("SB_API_KEY")),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

verdict, err := client.Detect(ctx, stylobot.DetectRequest{
    Method:   r.Method,
    Path:     r.URL.RequestURI(),
    RemoteIP: r.RemoteAddr,
    Headers:  extractHeaders(r),
    Protocol: "https",
})
if err != nil {
    // fail open: log and continue
    log.Printf("stylobot detect failed: %v", err)
    return next(w, r)
}

if verdict.RecommendedAction == "Block" {
    http.Error(w, "Forbidden", http.StatusForbidden)
    return
}

Lazy连接和启动安全

grpc.NewClient 创建客户端频道, 但并未立即建立 TCP 连接@ . @ 连接发生在第一个 RPC 调用@ MPK1} 这意味着即使侧边车尚未启动, 您的网关进程也成功开始。 启动后的第一个请求可能会失败 {(} @ 并且应该以失败=% K4open), 处理, 但是每次随后的请求都会正常运行, 一旦侧车正在运行\ MS K6}

这与 HTTP 客户端@ , @ 不同, 您通常在创建时连接\ MS K1}The gRPC 更改文档 详细描述整个生命周期 .

超时互动

WithTimeout 日期于 NewClient 设置默认的 per% -} 调用截止时间, 在其中应用 Detect 呼叫. 如果你的调用代码@(或中继器, 如 Caddy 插件 ) 已经从收到的请求中获取一个截止时间\ MPK3} <%SDK4} SDK 应用的第一个截止期限 =. 当使用 cddy 外掛程序时_,该插件拥有“50ms 最后期限 @;您可以省略” WithTimeoutNewClient 让插件控制它@ . @ 独立使用 @ ( @ a handler 直接调用 SDK\ ),} 设置 NewClient 上面显示的@. @%

Caddy 插件

卡迪 是一个基于 Go-的网络服务器, 并使用自动 HTTPSMS K1的反向代理服务器。 它的插件系统是编译@ -time @ :} xcad 时区 以建立自定义的 Caddy 二进制, 该二进制包含您的插件@ , @ 生产一个不依赖共享库运行的自=-} MS K2} 这与 nginx@ MOSK3s动态模块系统@ MPK4}不同.so 在运行时装入的文件 @ ). StyloBot 插件@ MS K1github.com/scottgal/caddy-stylobot@)_ 注册了一个中器械处理器, 它按每个要求调用 GO SDK@.}

Caddyfile 配置@ : @

{
    order stylobot before respond
}

:80 {
    stylobot {
        endpoint localhost:5090   # gRPC host:port of the sidecar
        timeout   50ms            # per-request deadline; fails open on expiry
        # on_block 503            # optional: change the block status code (default: 403)
    }
    reverse_proxy upstream:3000
}

插件将九张裁决信头插入到每一个转发的请求中@: @%

Q 信头@ Q来源字段 @
X-StyloBot-IsBot isBot (BOOL)
X-StyloBot-Probability botProbability (0.0-1.0)
X-StyloBot-Confidence confidence (0.0-1.0)
X-StyloBot-BotType @ @ e @ . @ g. @ AiBot, Scraper, GoodBot
X-StyloBot-BotName @ @ e @ . @ g. @ GPTBot, Googlebot
X-StyloBot-RiskBand VeryLow ... VeryHigh
X-StyloBot-Action Allow / Throttle / Challenge / Block
X-StyloBot-ThreatScore @ @ 数字 @ @
X-StyloBot-ThreatBand None ... Critical

提出请求时 isBot=trueAction=Block 以“403+”停止在入口处,永远不能到达上游。 “.+”所有其它的东西都“(+”包括带有“a”的机器人 ThrottleChallenge 推荐@) 与所有九位信头一起完整转发 @.。 这是预设的分割: 网关处理硬块_; 上游手柄nuance}.

on_block 更改网关块 @ ( default@ MS K1 设置时使用的状态代码 on_block 503 以抑制将 @403作为可重试@.的剪贴机中的逻辑

中间软件对每个要求都做什么

flowchart TD
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px

    A["1. Strip inbound X-StyloBot-* headers"]:::input
    B["2. context.WithTimeout(r.Context(), 50ms)"]:::input
    C["3. sbClient.Detect(ctx, DetectRequest)"]:::async
    D{error?}
    E["log warn - fail open<br/>forward unchanged"]:::good
    F["4. injectHeaders<br/>X-StyloBot-IsBot, Probability, Confidence,<br/>BotType, BotName, RiskBand, Action,<br/>ThreatScore, ThreatBand"]:::input
    I["next.ServeHTTP - forward to upstream<br/>with all verdict headers injected"]:::good

    A --> B --> C --> D
    D -->|yes| E --> I
    D -->|no| F --> I

台阶#1, 脱衣登机头 #.# 客户知道 X-StyloBot-* 标题名称可以自@-}输入一个有利的判决 并让它幸存下来

步骤 @2,上下文截止时间@. 超时是因为 r.Context() 使用 context.WithTimeout 需要相对持续时间的(+;+# context.WithDeadline 需要绝对时间@; @ 它们等同@ )._ 出自 r.Context() 而不是 context.Background() 如果客户端在 gRPC 呼叫完成前断开连接, 取消会通过通讯和侧车停止处理@. 关键点是“ : ”

检测和注射. 九个判决领域变成九个 X-StyloBot-* 标题@. @% 页眉设置在区块检查前@MS K1}#所以上游读取它们 styloBotMiddleware({ mode: 'headers' }) 对于所有非“ MSMK0” 的封结请求 <".}> 请在 isBot=truerecommendedAction=Block 在网关@;}所有其它东西都与随附的判决书标题齐头并进

执行@:

// from sdk/caddy/stylobot.go
func (s *StyloBot) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
    for _, name := range stylobotHeaders {
        r.Header.Del(name)
    }

    ctx, cancel := context.WithTimeout(r.Context(), s.timeout)
    defer cancel()

    verdict, err := s.sbClient.Detect(ctx, sb.DetectRequest{
        Method:   r.Method,
        Path:     r.URL.RequestURI(),
        RemoteIP: ExtractIP(r),
        Protocol: r.Proto,
        Headers:  ExtractHeaders(r),
    })
    if err != nil {
        s.logger.Warn("stylobot detect failed, failing open", zap.Error(err))
        return next.ServeHTTP(w, r)
    }

    injectHeaders(r, verdict)

    if verdict.IsBot && s.OnBlock > 0 && verdict.RecommendedAction == "Block" {
        http.Error(w, "Forbidden", s.OnBlock)
        return nil
    }
    return next.ServeHTTP(w, r)
}

带有xcaddy的大楼

Caddy 插件必须编译为二进制程序 xcad 时区. 集成测试中的 Docker 文件显示模式@:

# from tests/integration/caddy-sidecar/Dockerfile
FROM caddy:2-builder AS builder

WORKDIR /build
COPY sdk/caddy/ caddy-plugin/
COPY sdk/go/    go/

WORKDIR /build/caddy-plugin

RUN xcaddy build \
    --with github.com/scottgal/caddy-stylobot=/build/caddy-plugin \
    --with github.com/scottgal/stylobot-go=/build/go

FROM caddy:2
COPY --from=builder /build/caddy-plugin/caddy /usr/bin/caddy
COPY tests/integration/caddy-sidecar/Caddyfile /etc/caddy/Caddyfile

xcaddy 替换指令

插件@' @%s go.mod 包含@: @%

replace github.com/scottgal/stylobot-go => ../go

这告诉了Go 工具链 @"@当你看到时 stylobot-go, 使用本地目录, 而不是从模块中获取代理@." go buildgo test 插件目录中的“% .”

xcaddy 为构建该模块创建新的临时 Go 模块@ . @% 此模块不继承 replace 插件中的指令@' @%s go.mod"没有第二个" --with 参数@, @% xcaddy 会尝试下载 stylobot-gopkg.go.dev @( 尚未出版@)和失败_.

缩略 --with module=path 参数的本地等量 replace 指令@ :} 它在构建时间绘制本地目录的模块路径 MS K1\ 两个本地模块都必须被明确命名\ MPK2} *

gRPC 上的液体模板

RenderWidget 是一个 GRPC RPC 在侧车上, 它接受一个液体模板字符串@ , @ 使其与检测环境一同返回 HTML ,} 并返回 HHTML=(_ Go 代理@ MPK4 nodeSSR color *, 批量管道) 生成 BotMSSK7ZAWare HTMle 而不运行单独的转换进程@.}

液体模板

流动 是一种由“Smoteify” “MSMKO}”创造的暂时性语言,用于“Shopitize”主题@,Jekyll,GitHub Pages @,和许多其他系统{. 它的关键属性>:它安全地使用用户<-\suppled supplication_(no 任意代码执行MSkQK8简单到非‘-’开发者可以写入{,}并被广泛理解 <.>sptyloBot 流经.@, 高端@- 性能 @.NET 实施液态}, 以建立模板服务器@MS K4side.#

侧车执行@: @

// from src/Mostlylucid.BotDetection.Sidecar/Services/DetectionGrpcService.cs
private static readonly FluidParser Parser = new();  // static, shared, compiled templates cached

public override async Task<Proto.RenderWidgetResponse> RenderWidget(
    Proto.RenderWidgetRequest request, ServerCallContext context)
{
    if (!Parser.TryParse(request.Template, out var template, out var error))
        return new Proto.RenderWidgetResponse { Success = false, Error = error };

    var ctx = new TemplateContext();
    if (request.Verdict is { } v)
    {
        ctx.SetValue("isBot",             v.IsBot);
        ctx.SetValue("botProbability",    (double)v.BotProbability);
        ctx.SetValue("botType",           v.BotType);
        ctx.SetValue("botName",           v.BotName);
        ctx.SetValue("riskBand",          v.RiskBand.ToString());
        ctx.SetValue("recommendedAction", v.RecommendedAction.ToString());
        ctx.SetValue("threatScore",       (double)v.ThreatScore);
        ctx.SetValue("threatBand",        v.ThreatBand.ToString());
    }
    foreach (var kv in request.Vars)
        ctx.SetValue(kv.Key, kv.Value);

    var html = await template.RenderAsync(ctx);
    return new Proto.RenderWidgetResponse { Html = html, Success = true };
}

Llulid.Core 维护一个内部编译的模板缓存@ . 重复翻譯相同的模板字符串 FluidParser 是静态的, 所有 GRPC 调用共享@. @%

节点 StyloBotGrpcClient.renderWidget() 示例和完整模板的变量引用 SDK 类型文章.

从Go:呼叫它

rendered, err := client.RenderWidget(ctx, stylobot.RenderRequest{
    Template: `{% if isBot %}<p class="warning">Bot: {{ botType }}</p>{% endif %}`,
    Verdict:  verdict,
    Vars:     map[string]string{"locale": "en-GB"},
})
if err == nil && rendered.Success {
    fmt.Fprint(w, rendered.HTML)
}

模板模板语法相同,无论您是否调用 RenderWidget 从 Go @ , @ node@ MS K1 @ 或使用 <sb-widget> 在浏览器中@:}相同的液体引擎\ ,}同样的变量名称\ ,}相同的翻譯路徑.}

生产布局

graph LR
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px
    classDef store fill:none,stroke:#f59e0b,stroke-width:2px

    INT([Internet])
    CF["Cloudflare<br/>Tunnel / CDN"]
    CA["Caddy<br/>+ caddy-stylobot"]:::input
    SD["StyloBot Sidecar<br/>:5090 gRPC  ·  :5091 REST"]:::async
    WEB["Upstream App<br/>Node / Go / ASP.NET"]:::good
    DB[("SQLite<br/>sessions · reputation")]:::store

    INT --> CF --> CA
    CA -->|"gRPC Detect<br/>≤50ms"| SD
    SD <-->|"persist"| DB
    CA -->|"X-StyloBot-* headers"| WEB
    WEB -->|"/_stylobot/partials/render<br/>(widget rendering)"| SD

上游应用程序直接呼叫随行车, 以获取部件在连接网关@, @ 绕过“ .}# 元件需要完整的判定环境, 在请求通过网关检测@ MS K2}后发生, 因此没有探测重复=.}

无法在每一层打开

Caddy 插件@, @Node middware @,}與 GOSDK 全部失敗 open:} 一邊車超時或錯誤成為警告日志, 並且可允許的空裁決 *,}不是 5_xx#.{

由于无法检测, 阻止合法交通比在边车停机期间失去的机器人交通更糟糕@. @


更多关于内部检测的插件@,部署模式 @,可观察性},与商业地貌仍待更新

执行源来源 @: GithubMS .comMS K1scottgal/stylobot (星际机器人).+Live 引擎 @,_QTaptro@,#和商用控制 stylobot% . @ net.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.