一、项目背景:xAI 的官方开源编码 Agent
如果说 OpenAI 的 Codex CLI 是编码 Agent 领域最常被引用的 Rust 样本,那么 xAI 的 Grok Build(命令名 grok)就是另一个值得逐行精读的对照组:它由 SpaceXAI 官方出品、以 Apache-2.0 协议开源,是一个运行在终端里的全屏 TUI 编码 Agent——理解代码库、编辑文件、执行 shell 命令、搜索网页、管理长任务,支持交互式使用、headless 脚本/CI 模式,以及通过 Agent Client Protocol(ACP)嵌入编辑器。
这个仓库有几处与众不同的"出身"值得先划重点:
- 它是 monorepo 的定期快照。根目录一个
SOURCE_REV文件记录了源码对应的 SpaceXAI 内部 monorepo commit SHA,仓库由内部主仓周期性同步导出,因此不接受外部贡献(CONTRIBUTING.md明确说明); - 根
Cargo.toml是生成物,首行注释直接写着 “Auto-generated workspace root. Prefer editing per-crate Cargo.toml files."——单一事实来源在各 crate 自己的 manifest 里,这是大型 monorepo 导出时防漂移的典型手段; - 生而 Rust。没有 Codex 那段 TypeScript → Rust 的重写史,90+ 个 crate 的 workspace 从第一天就按 Rust edition 2024 组织,构建产物名为
xai-grok-pager,官方安装包发布为grok。
项目概貌
| 维度 | 数据 |
|---|---|
| 核心语言 | Rust(workspace edition 2024,90+ crates) |
| 进程模型 | Leader-Follower 单例守护进程(Unix domain socket) |
| 接入形态 | TUI / headless CLI(grok -p)/ ACP 编辑器嵌入 / WebSocket serve |
| 模型接口 | xAI API(OpenAI 兼容层,async-openai fork,SSE 流式) |
| 终端 UI | ratatui 0.29 + crossterm + 自维护组件(textarea / inline) |
| 持久化 | SQLite journal + ~/.grok/sessions/ 会话目录 |
| 沙箱 | nono(Landlock/Seatbelt)+ bubblewrap 再执行 + seccomp 子进程断网 |
| 脚本运行时 | Rhai(workflow 引擎) |
| 可观测 | fastrace + OpenTelemetry(OTLP)+ Mixpanel |
| 构建体系 | Cargo workspace + DotSlash 工具链(bin/protoc) |
本文基于当前源码快照分析,涉及文件路径均相对
grok-build/仓库根目录。文中观点仅代表个人技术解读。
本文导读
- 第二节先用一张分层架构图建立全局认知:为什么 Grok 选了 Leader-Follower 守护进程而不是每次冷启动;
- 第三节是主菜:每会话一个 OS 线程的 Actor 模型、五命名空间工具系统、fail-closed 的沙箱纵深、pager 式 TUI 与扩展生态;
- 第四、五节看配置/会话/遥测等基础设施与工程治理实践;
- 适合关注 AI Coding Agent、终端工程、权限治理或 Rust 工程化的读者。
二、架构概览:Leader 单例守护进程
整个 workspace 最有辨识度的决策藏在 xai-grok-shell 的模块注释里——crates/codegen/xai-grok-shell/src/leader/mod.rs 用一幅 ASCII 图开门见山:
//! ┌─────────────────────────────────────────────────────────────┐
//! │ Leader Process │
//! │ Agent (MvpAgent) —— Shared state across all clients │
//! │ IPC Server (Unix socket) —— Namespaces request IDs │
//! └───────────────────────────┼──────────────────────────────────┘
//! │ IPC (Unix socket at ~/.grok/leader.sock)
//! ┌───────────────────┼───────────────────┐
//! ▼ ▼ ▼
//! ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
//! │ TUI Client │ │ IDE Extension │ │ Headless CLI │
//! └───────────────┘ └───────────────┘ └───────────────┘
这与 Codex CLI “每次 CLI 启动即独立进程、状态随进程生灭"的模型形成了鲜明对照:Grok 在一台机器上常驻一个 leader 进程,持有全部 Agent 与会话状态;TUI、IDE 扩展、headless 脚本都是轻客户端,通过 ~/.grok/leader.sock 以 ACP(JSON-RPC)多路复用同一组会话。带来的直接收益:
- 会话跨终端存活:关掉 TUI 重开,会话还在;IDE 和终端可以同时附着到同一个会话;
- 请求 ID 命名空间化:IPC Server 为每个客户端的请求 ID 加命名空间前缀,避免多客户端碰撞,并跟踪"哪个会话归哪个客户端"做定向路由;
- 连接即拉起:组合根里的
connect_or_spawn(xai-grok-pager-bin/src/main.rs:46)先尝试连接既有 socket,连不上再 spawn 一个 leader——用户完全无感。
三个 Agent 入口对应三种进程形态:run_leader(守护进程)、run_stdio_agent(独立 stdio 子进程,供编辑器 spawn)、run_headless(一次性执行)。交互式 TUI 甚至也走 ACP 协议与 agent 通信——agent 可以是进程内线程、leader 共享进程,或独立 stdio/WebSocket 进程,前端完全解耦。
一个安全细节值得一提:沙箱激活时 leader 模式会被禁用(warn_leader_disabled_by_sandbox)。一个被内核级沙箱约束的进程去提供机器级全局 socket 服务显然矛盾,Grok 的选择是 fail-closed 降级为单进程模式。
按依赖方向自上而下,整个 workspace 可归纳为六层:
- 接入层:
xai-grok-pager(TUI)+xai-acp-lib(Agent Client Protocol 封装)+ headless 参数面; - 运行时层:
xai-grok-shell(leader/stdio/headless 入口、会话编排)+xai-grok-agent(Agent 构建)+xai-agent-lifecycle+xai-chat-state; - 能力层:
xai-grok-tools(工具实现与注册表)+xai-tool-runtime/xai-tool-protocol/xai-tool-types(统一工具契约)+xai-grok-mcp+xai-grok-hooks+xai-grok-plugin-marketplace; - 宿主层:
xai-grok-workspace(文件系统、VCS、执行、checkpoint)+xai-grok-sandbox+xai-fast-worktree(按 session/ab/pool/fork 类型管理的 git worktree 池); - 基础层:
xai-grok-config(七层合并配置)+xai-grok-auth+xai-grok-models+xai-grok-telemetry+xai-sqlite-journal+ 30 余个叶子工具 crate; - vendor 层:
third_party/内嵌了完整 Mermaid 渲染栈(dagre/graphlib/mermaid-to-svg 的 Rust 移植),支撑终端内渲染流程图。
三、核心机制解析
3.1 会话模型:每会话一个 OS 线程
Codex 用 SQ/EQ 双队列在单线程事件循环里仲裁一切;Grok 则走了另一条路——三层嵌套的线程隔离:
- 进程级:
MvpAgent(ACP Agent trait 实现)挂在 leader 的LocalSet上,持有SessionRegistry会话句柄表; - 会话级:每个会话是一个
SessionActor——注意,是独立 OS 线程,不是 tokio task。每线程自带 current-thread runtime +LocalSet,因此SessionActor天然!Send,用RefCell/Cell做内部可变性,零跨线程锁; - Turn 级:一个 turn 是
spawn_local出来的AgentTask(持有AbortHandle),turn 内部跑"采样-工具"循环。
// crates/codegen/xai-grok-shell/src/session/acp_session.rs:645
pub(crate) struct SessionActor {
pub(crate) session_info: SessionInfo,
pub(crate) state: TokioMutex<State>, // 任务调度状态
pub(crate) chat_state_handle: xai_chat_state::ChatStateHandle, // 委托 ChatStateActor
pub(crate) current_prompt_id: Arc<Mutex<Option<String>>>,
pub(crate) compaction: CompactionConfig,
pub(crate) pending_interjections: InterjectionBuffer<acp::ImageContent>,
// ...
}
State 结构(acp_session.rs:338)是调度核心:running_task(当前 AgentTask)、pending_inputs(排队输入)、pending_notifications、edit_holds。对话历史与 token 统计则被整体迁移到独立的 ChatStateActor 线程,会话线程只持 handle——读写分离到线程粒度。
为什么用 OS 线程而不是 tokio task? 这是对 tokio 生态默认假设的一次反向操作:既然会话状态本来就不需要跨线程共享,!Send + 线程私有堆反而消灭了锁竞争和状态同步 bug;单个会话若陷入 CPU 密集解析,OS 抢占式调度保证不会饿死其他会话——这是异步任务队列给不了的隔离性。代价是每会话一个线程的内存开销,但对桌面 Agent 而言完全可接受。
Turn 循环本身与 Codex 同构:组装上下文(skills、hooks、AGENTS.md、git/环境信息)→ 模型 SSE 流式请求 → 解析 delta 与工具调用 → ToolDispatch 在沙箱内执行 → 结果回填 → 循环,直到无工具调用发出 TurnCompleted(携带 diff 汇总与 token 用量)并落盘。两个独特设计:
- 插话(interjection):turn 运行中的新输入不是排队等下一轮,而是进入
InterjectionBuffer(xai-interjection-core),配合打断构成完整的中断/转向语义; - 服务端托管工具:
Agent结构持有hosted_tools: Vec<HostedTool>(xai-grok-agent/src/agent.rs:46),像 WebSearch 这类工具以原生 API 类型发给服务端执行——本地工具与服务端工具在同一个注册表里统一声明。
Agent 的构建也颇为讲究:AgentBuilder 从 AgentDefinition + 会话上下文产出构建后不可变的 Agent,工具状态(MCP 注册、完成度跟踪、重试配置)的动态变更全部走 Arc<ToolBridge> 的内部锁——“定义不可变、状态集中管”。
3.2 工具系统:统一契约与移植生态
工具契约收敛在 xai-tool-runtime 一个小 crate 里,核心 trait 用关联类型保住编译期类型安全:
// crates/common/xai-tool-runtime/src/tool.rs:36
pub trait Tool: Send + Sync {
type Args: for<'de> Deserialize<'de> + JsonSchema + Send + 'static;
type Output: Serialize + ToolOutput + Send + 'static;
fn id(&self) -> ToolId;
fn description(&self, _ctx: &ListToolsContext) -> ToolDescription;
fn execute(&self, ctx: ToolCallContext, args: Self::Args)
-> impl Future<Output = ToolStream<Self::Output>> + Send;
// ...
}
执行结果是流式的:ToolStreamItem 只有 Progress(0..n 个,驱动 TUI 实时进度)与 Terminal(恰好 1 个且必须是最后一个)两种变体,长任务的原生进度条由此而来。由于带关联类型的 trait 不是对象安全的,边界处由 blanket impl 擦除为 ToolDyn,再以对象安全的 ToolDispatch 分发——类型安全与动态分发各得其所。
真正有趣的是命名空间体系。每个工具除实现运行时 Tool 外,还实现本 crate 的 ToolMetadata:声明 ToolKind(35 个变体:Read/Edit/Search/Execute/Plan/WebSearch/…)、所属命名空间、以及一份 MiniJinja 描述模板(支持 ${{ tools.by_kind.X }} 占位符——工具描述可以引用"当前还启用了哪些同类工具"来动态措辞)。命名空间枚举如下:
| 命名空间 | 来源 |
|---|---|
GrokBuild / GrokBuildConcise / GrokBuildHashline |
Grok 自研工具的三种表述形态 |
Codex |
移植自 openai/codex 的工具实现 |
OpenCode |
移植自 sst/opencode 的工具实现 |
MCP |
外部 MCP server 动态注册 |
也就是说,Grok 没有闭门造车重写工具集,而是把 Codex 与 opencode 两家开源实现按 Apache-2.0 §4(b) 规范移植进自己的运行时契约(xai-grok-tools/THIRD_PARTY_NOTICES.md 记录了完整许可与变更声明),同一套注册表里五族工具并存,按模型能力与配置组装。register::<T>() 在编译期完成 schema 生成与参数校验的捕获;对外还有一条 protobuf 生成面(xai-grok-tools-api,xai.grok.tools.v1:ExecuteTool/ListTools/SpawnSubagent 等),让宿主服务不必依赖工具实现 crate。
模型路由则收敛在 xai-grok-models:default_models.json 编译期内嵌,运行时按 CLI flag > ENV > config.toml > remote settings > defaults 解析,且分用途建模——主对话、web search 摘要、图片描述、会话标题各有独立模型槽位。
3.3 沙箱:fail-closed 的纵深防御
沙箱 crate 的文档注释第一句就亮明立场:
//! crates/codegen/xai-grok-sandbox/src/lib.rs:8
//! OS-level sandboxing for Grok Build via nono.
//! Applied once at process startup. ... Network is left open at the
//! process level (agent needs LLM API); child network is blocked
//! per-subprocess via seccomp.
第一层是进程级内核沙箱:基于 nono crate 走 Landlock(Linux)/ Seatbelt(macOS),SandboxManager::apply() 在启动时应用一次且不可逆,覆盖进程内 tokio::fs 与全部子进程。注意一个坦诚的权衡:进程级网络保持开放——Agent 自己要访问 LLM API,索性把断网下沉到子进程粒度。
第二层是 Linux bwrap 再执行:当 profile 带 deny 列表时,进程用 bubblewrap 重新 exec 自己——--cap-drop ALL、deny_write 路径 --ro-bind 只读挂载、deny_read 路径用 chmod 000 的占位符 bind-over(读取即 EPERM),并给 hooks 目录准备专门的写保护挂载计划。__GROK_INSIDE_BWRAP 环境变量防止重复再执行。
第三层是子进程网络:已知 Linux 启动路径按子进程安装 seccomp 网络过滤(should_restrict_child_network),Agent 主进程流量不受影响。
第四层是策略与体验:.grok/sandbox.toml 定义 profiles(workspace / strict / devbox / custom,支持 extends 继承);沙箱激活时 bash 自动放行(should_auto_allow_bash)——边界既然硬了,就不必每次打扰用户。
但这个 crate 最值得学习的不是分层本身,而是fail-closed 被贯彻到了判定语义的每个角落:
// crates/codegen/xai-grok-sandbox/src/lib.rs:110
/// This is the configured request, not a report that enforcement succeeded —
/// `is_active()` can be false while the process is still confined (e.g. some
/// Linux bwrap paths), and a requested-but-unapplied profile already warns the
/// user. Keying on the request is the fail-closed choice.
pub fn requested_confinement_profile() -> Option<&'static str> { ... }
判定"是否受限"以请求的 profile 而非"成功应用"为准——因为部分 Linux bwrap 路径下 is_active() 会是 false 而进程实际仍被约束;同理 requires_read_deny 直接看配置里有没有 deny(解析失败返回空集时若据其判定就会静默 fail-open)。每处"看似可以简化"的分支,注释都解释了为什么不简化。
3.4 TUI:pager 隐喻与 Elm 架构
TUI crate 叫 “pager” 不是偶然——整个界面的本质是一个终端分页器:可滚动的对话回滚区(scrollback)+ 底部固定实时区(prompt/状态行),而非传统全屏仪表盘。三种屏幕模式:
// crates/codegen/xai-grok-pager/src/app/mod.rs:312
pub(crate) enum ScreenMode {
Fullscreen, // alternate screen 全屏
Inline, // 不进 alternate screen(tmux control mode / Zellij)
/// Scrollback-native (experimental, `--minimal`): finalized blocks are
/// printed into the terminal's native scrollback via `insert_before` ...
Minimal,
}
Minimal 模式最能体现 pager 语义:已完结的对话块通过 insert_before 直接写进终端原生 scrollback,TUI 只保留一个小 pinned 区——历史归终端管,应用只管增量。/minimal 与 /fullscreen 可运行中原地切换。
渲染架构是教科书级 Elm 单向数据流:actions(Action/Effect/TaskResult 枚举)→ AppView(根组件)→ dispatch(Action → 状态变更 + Vec<Effect>,纯同步、可单测)→ effects(异步任务 spawn)→ event_loop(一条 biased tokio::select! 只做 IO 管道)。输入系统的单一事实来源是动作注册表:同一个注册表同时服务快捷键栏、命令面板 fuzzy 搜索与按键分发,按键按 Pane → Agent → Global 三层冒泡。
两处细节可见打磨深度:resize 防抖 16ms(拖拽窗口每秒几十个 resize 事件,稳定后只做一次全量重排);启动期 type-ahead 过滤(终端能力查询应答 DA2/OSC 会泄漏成 Esc+可打印字节,从第一个 Esc 截断丢弃,防止幽灵文本混进输入框)。
测试基建同样硬核:xai-grok-pager-pty-harness 用真实 PTY + 虚拟终端 + mock 推理服务器做端到端渲染断言。另一个彩蛋是 xai-grok-mermaid:vendored 了完整 Mermaid 渲染栈(dagre 布局 + graphlib + resvg 光栅化),模型输出的 mermaid 代码块可以直接在终端里渲染成图——这也解释了为什么 third_party/ 里有三个 Rust 移植的 JS 图形库。
3.5 扩展生态:Skills、Hooks、Plugins、MCP、ACP
Skills 走兼容优先路线。技能即带 SKILL.md(YAML frontmatter 写 name/description)的目录,发现路径覆盖项目/仓库/用户三级 .grok/skills/,并且默认同时扫描 .claude/、.cursor/、.agents/ 目录——Claude Code 与 Cursor 的技能和斜杠命令开箱即用(可通过 [compat] 配置关闭)。commands/ 目录下的扁平 .md 文件自动变成斜杠命令,与 Claude Code 的 legacy 布局对齐。发现逻辑刻意不走 .gitignore:团队常把 .claude/** 忽略为本地配置,但项目级命令仍需生效——隐藏技能请用 [skills] ignore 显式声明。
其余扩展点各占一个 crate:xai-grok-hooks 覆盖生命周期钩子;xai-grok-plugin-marketplace 管理插件发现与安装;xai-grok-mcp 管理 MCP 客户端连接;xai-acp-lib 封装 Zed 系 agent-client-protocol,让任何实现 ACP 的编辑器(Zed、Neovim 插件等)都能嵌入 Grok。特色功能还有:xai-grok-voice 语音模式、xai-grok-status-line 可定制状态行、xai-workflow(内嵌 Rhai 脚本引擎的可视化工作流)、xai-grok-memory 跨会话记忆、claude_import 一键导入 Claude Code 配置、foreign-sessions 直接读取外部工具的历史会话——对一个后来者,生态兼容本身就是竞争力。
四、基础设施:配置、会话与可观测
配置系统可能是全仓库防御性设计密度最高的地方。TOML 配置按七层优先级合并:
// crates/codegen/xai-grok-config/src/lib.rs:3
//! Merge order (lowest → highest priority):
//! 1. `/etc/grok/managed_config.toml`
//! 2. `$GROK_HOME/managed_config.toml`
//! 3. `$GROK_HOME/config.toml`
//! 4. `$GROK_HOME/requirements.toml` (cloud cache; Ed25519-signed at rest ...)
//! 5. `/etc/grok/requirements.toml`
//! 6. macOS MDM managed preferences (`ai.x.grok`, admin-forced) — macOS only
这条链为企业管控而生:IT 用 managed_config 下发基线,云端用 Ed25519 签名的 requirements.toml 下发硬策略,macOS 上再叠加 MDM 强制偏好。安全边界同样成文:GROK_CONFIG 环境变量 overlay 被限制在软配置白名单(models、features 等),任何能执行代码/改认证/改出口的表(mcp_servers、auth、endpoints、plugins)一律 fail-closed 丢弃;无 HOME 时不降级读 cwd 的 .grok/(防不可信项目目录提权);TOML 报错永不回显源行(可能含密钥)。
功能开关是"注册表驱动"的:16 个 [features] 键每个一行 FeatureSpec(key、TOML path、env 名、默认值、远程设置读取函数),解析优先级唯一化为 requirements pin → env → config → remote → default,且 off_reason() 能输出"为什么关"的人话解释(“off (a requirements.toml pin)")——可解释的配置治理。
会话层:~/.grok/sessions/{按 cwd 编码的目录}/ 全部 0700 权限并自愈;长路径用 {slug}-{blake3 前 16 hex} 编码;SQLite journal 记录事件流支撑 resume、全文会话搜索(xai-grok-session-search)与跨工具会话读取。遥测栈为 fastrace + OTLP + Mixpanel,并配了死循环检测(DoomLoopRecoverySettings:x-grok-doom-loop-check 请求头 + window_tokens + max_retries)——模型陷入重复采样时自动熔断重采样。
五、工程实践亮点
抛开功能,这个仓库有几条值得抄的工程实践:
- 组合根隔离:jemalloc 全局分配器、
malloc_confprofiling、cryptify 字符串混淆、rustls 全部只出现在xai-grok-pager-bin二进制 crate,库 crate 保持纯净可复用——依赖的"脏"与二进制的"加固"分层; - 硬化发布 profile:
release-dist用 thin LTO +codegen-units=1+ 保留符号与行表(CI 先抽 .dSYM/.debug 侧车再 strip),并留有x-prod等按场景分档的 profile 家族; - clippy 治理带故事:
uninlined_format_args = "allow"旁边写着 9 行注释解释"没有 merge queue 之前为什么不能收紧”——lint 决策可追溯; - 工具链即代码:
rust-toolchain.toml锁版本,protoc 走 DotSlash 按需下载,构建可复现; - 仓库即快照:根 Cargo.toml 生成化 +
SOURCE_REV溯源,monorepo 与开源镜像之间不存在双向同步的心智负担。
六、技术栈说明
| 类别 | 选型 | 用途 |
|---|---|---|
| 异步运行时 | tokio(full)+ LocalSet | leader、会话线程、工具并发 |
| 终端 UI | ratatui 0.29 + crossterm + 自维护组件 | TUI 渲染与输入 |
| 模型接口 | async-openai(fork)+ eventsource-stream | OpenAI 兼容 SSE 流式 |
| 序列化 | serde / schemars / prost / ts-rs | 协议、schema、gRPC 工具面 |
| 数据库 | SQLite(journal)+ fs2 | 会话事件流持久化 |
| 沙箱 | nono(Landlock/Seatbelt)+ bubblewrap + seccomp | 内核级纵深防御 |
| MCP | rmcp 系 + agent-client-protocol 0.10 | 扩展与编辑器嵌入 |
| 脚本引擎 | Rhai 1.25 | workflow 工作流 |
| 图形渲染 | vendored mermaid 栈 + resvg + tiny-skia | 终端内渲染流程图 |
| 搜索 | nucleo(helix 同源)+ gix | 模糊文件搜索与 git 状态 |
| 内存 | tikv-jemalloc(profiling) | 分配器与堆分析 |
| 可观测 | fastrace + OpenTelemetry(OTLP)+ Mixpanel | 链路追踪与产品遥测 |
| 测试 | insta / wiremock / serial_test + PTY harness | 快照、mock SSE、端到端终端 |
七、总结与展望
把 Grok Build 的源码通读下来,最能带走的是三条与 Codex 恰成对照的架构判断:
- 进程拓扑是产品决策。Codex 的"每次冷启动独立进程"换来了简单与隔离,Grok 的"Leader 单例守护 + 多客户端复用"换来会话永续与多端同窗——后者更像一个常驻的本地开发服务,而非一次性命令。两种模型没有对错,但 Grok 证明了 ACP + Unix socket 的组合足以把"终端 Agent"演进为"本机会话基础设施”;
- 并发模型可以反 tokio 直觉。每会话一个 OS 线程、
!Send+RefCell,用线程边界替代队列仲裁,是"问题本质是隔离而非调度"时的教科书解法; - fail-closed 是一种代码审美。从沙箱"以请求而非应用判定约束”,到配置"env overlay 白名单"、“报错不回显源行”,Grok 把防御写成了一致的语言——每个放宽的分支都必须先证明自己不会 fail-open。
与 Codex、Claude Code 相比,Grok Build 的差异化打法非常清晰:不重造工具而是移植成熟实现(Codex/opencode 工具按 Apache §4(b) 收编)、不另立技能格式而是兼容 Claude/Cursor 生态、不绑定单一前端而是All-in 开放协议(ACP + MCP + WebSocket serve)。对一个后发的编码 Agent,“把兼容性当功能"可能是最聪明的入场姿势。
展望后续,源码里已埋好几条演进线:xai-grok-workspace-daemon 预示宿主操作将进一步服务化、xai-grok-remote 与 relay 指向远程/云端会话、xai-computer-hub 系列 crate(core/sdk/mcp-adapter)暗示浏览器与计算机控制能力、语音模式与 workflow 引擎则把场景从"写代码"向"通用操作"延展。对想深入 AI Agent 工程化的开发者,xai-grok-shell/src/leader/(进程拓扑)、session/acp_session.rs(会话 Actor)与 xai-grok-sandbox/src/lib.rs(fail-closed 范本)是三块最值得精读的矿区。
参考:x.ai/cli · docs.x.ai/build/overview · 本文图表 SVG 源文件位于本站
/img/grok-build-source-analysis/
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付