跳至正文
Workers — workerd:运行原理、源码架构与自部署流程

workerd:运行原理、源码架构与自部署流程

cloudflare/workerd 项目

AI 参与说明(Agent:Codex):本文依据官方仓库、固定发布版源码和官方文档辅助研究、撰写与校验,执行入口为 Codex Desktop。完整模型标识与 reasoning effort 未取得任务级运行记录。工程建议与上游能力分开说明;最小示例的实际验证范围见文末。

workerd 是 Cloudflare Workers 使用的 JavaScript / Wasm 服务端运行时。本文的自部署,是指在自己的 Linux 服务器上安装 workerd,部署自己的 Worker 应用,并由自己维护进程、域名、HTTPS 和数据。对于无状态 HTTP 服务,这与部署普通应用服务器接近,不需要先建设多租户平台。项目定位

阅读路线是:先理解运行原理和源码架构,再跑通两个 Worker 的最小示例,最后把同一份应用接到 systemd 与 Nginx。默认使用官方二进制,无需从源码编译,也无需为了运行 workerd 注册 Cloudflare 账号;调用其他托管产品时,仍需满足对应产品的接入条件。

本文整理于 2026-10-30,源码和示例固定在 v1.20261029.1,提交为 c82276613b30e6f1a9b5b28067318c5201dc7956。这是本次查询时官方 latest release 指向的版本;后续版本不能直接套用文中的路径和限制。发布记录

英文术语中文名称简要解释
Runtime运行时加载代码、提供 API 并执行事件的程序
V8 isolateV8 隔离实例拥有独立 JavaScript 堆和执行状态的引擎实例
Service服务配置图中的节点,可以是 Worker、外部服务、网络或磁盘目录
Capability binding能力绑定将可访问的具体资源作为对象交给代码
Nanoservices纳米服务官方对同进程内独立 Worker 组件组合方式的称呼
IoContext输入输出上下文管理一次事件相关异步操作和生命周期的内部对象
Durable Objects持久对象按身份寻址、带状态和存储接口的执行单元

1. workerd 提供哪一层能力

workerd runs Worker code; a hosting platform manages its deployment and operation.

可以把它放在应用服务器的位置:接收请求,执行 Worker,再返回响应。它也能作为可编程代理,或作为本地开发的底层运行时。Wrangler 是开发与部署工具,Miniflare 负责本地环境及平台能力模拟;它们和 workerd 的职责不同。官方介绍、本地开发入口

目标workerd 能直接承担什么还要建设或接入什么
HTTP 应用、请求改写、反向代理加载 JS / Wasm、监听 socket、处理请求和流TLS、域名、进程管理、监控
多 Worker 组件Service bindings、独立执行环境、配置连接关系代码构建、配置生成、版本发布
外部资源访问网络、指定后端和磁盘服务等能力入口数据库或对象存储本身、访问策略
带状态应用Durable Objects 的执行与存储接口持久化配置、备份、恢复和状态路由

不能因为某个 API 的客户端实现位于运行时中,就推断其背后的托管服务也随二进制提供。KV、R2、D1、Queues 等都应逐项核对后端和 binding 的实现方式;本地模拟通过,也不能证明已获得对应云产品的生产保障。

2. 核心原理:复用运行时,用能力连接组件

V8 isolate 让请求不必各自启动一个进程

V8 执行 JavaScript 与 Wasm,workerd 在其上提供 Request、Response、流、网络等服务端 API。Worker 的执行环境可以处理多次事件,请求之间无需重复启动完整操作系统环境。不同 Worker 的 JavaScript 堆隔离,但它们可以共享宿主进程;isolate 不等于虚拟机。Workers 执行模型、Worker 实现

运行中的同步 JavaScript 要持有执行锁;等待异步 I/O 时,运行时可以调度其他工作。因此“支持很多并发连接”不等于“一个 Worker 的同步计算会自动并行”。长时间计算仍会占用执行资源。多个副本和 CPU 核的使用方式,需要通过外部进程与部署方案规划。

进程中的全局变量可以暂存缓存,但不适合作为可靠业务数据库:进程重启、版本切换和多个副本都会改变其可见性。

Nanoservices 减少组件之间的网络边界

例如入口 Worker 做鉴权,业务 Worker 处理订单。将二者配置为同一进程内的服务后,入口通过 env.ORDERS.fetch(request) 调用业务 Worker,不需要先经过一个独立部署的 HTTP 服务地址。

官方将这种模式称为 Nanoservices,并建议可将同一组服务部署到每台机器,形成 Homogeneous deployment。它降低了组件之间的网络通信成本,也让一组组件共享进程故障域。仍然存在调度、对象包装和业务计算,不能把“接近本地调用”的设计目标理解成所有调用都零开销。设计原则

Capability bindings 决定代码能访问什么

配置把一个指定服务交给 env.ORDERS;代码拿到的是通往该服务的能力对象。目标由配置决定,无需让代码自行拼出真实地址。这样的边界也便于把生产后端替换成测试后端。

需要特别检查两条路径:显式 binding 与 全局 fetch()。固定版的 globalOutbound 默认是 internet,缺省的同名网络服务允许公共网络。给代码几个受控 binding,并不会顺带关闭全局出网。需要限制网络访问时,应显式配置 globalOutbound 和网络服务的 allow / deny 规则,再从实际进程网络边界验证。配置定义

即使固定了后端,也要校验路径、方法、重定向与业务权限;不能把 capability 模式理解成业务层自动获得完整的 SSRF 防护。

3. 架构与源码:配置先形成服务图,再接入执行层

这个版本已经包含 Rust 服务端模块。旧文章中指向 server/server.c++ 的阅读路径,不适合直接用于本版本。服务图、连接和 CLI 的一部分在 Rust,执行与 Web API 的大量实现仍在 C++,通过桥接层协作。

flowchart TB
  A["CLI 与 Cap'n Proto 配置"] --> B["Rust Server:建立并连接 Service 图"]
  B --> C["Socket 与 HTTP listener"]
  C --> D["WorkerService:选择入口与 I/O channels"]
  D --> E["C++ WorkerEntrypoint 与 IoContext"]
  E --> F["Worker 执行环境与 V8 isolate"]
  F --> G["应用 fetch handler"]
  G --> H["Service binding 或 global fetch"]
  H --> I["本地 Worker / 指定后端 / 网络服务"]
  I --> J["Response 与响应流"]
  G --> J

图展示普通 HTTP 请求的主路径;应用不一定调用外部服务,Durable Objects 的寻址和存储也不在这张图中展开。

模块与依赖关系

以下链接均固定到本文版本,可以顺着入口继续阅读。

模块代码入口职责
CLIserver/cli/解析 serve、配置路径和运行参数
配置与服务图server/server/config.rs创建服务,再解析服务之间的引用
HTTP 接入server/server/listen/mod.rssocket、连接、请求改写与分发
Worker 服务server/server/worker.rs编译 Worker、连接 channels、创建事件入口
执行工厂server/factory/桥接服务端与底层 Worker 执行对象
事件生命周期io/worker-entrypoint.c++事件交付、进入 JS、响应和异常收尾
异步上下文io/io-context.hI/O 所属关系、异步任务与生命周期
Web APIapi/global-scope.c++、api/http.c++handler 分发、Request / Response 和 fetch
JS 与宿主桥接jsg/C++ 对象与 V8 值、异常和生命周期的转换
状态存储io/actor-sqlite.c++Durable Objects 的 SQLite 存储实现

服务端依赖执行工厂提供的接口,不需要自己实现 JavaScript 引擎;Web API 经 JSG 接入 V8,I/O 经 channels 返回服务图。这也是配置能更换后端、而业务代码继续使用同一个 binding 的原因。JSG 说明

核心数据模型

模型关键内容为什么需要它
Configservices、sockets描述一个实例暴露什么,以及包含哪些资源
ServiceWorker、ExternalServer、Network、DiskDirectory 等分支用统一节点表达不同能力
Worker 配置modules、compatibilityDate、bindings、globalOutbound定义代码、API 行为与权限连接
ServiceDesignatorname、entrypoint、props指向服务及具体入口,不只是一个 URL
PendingLink尚未解析的服务引用允许先创建节点,再连接互相引用的服务
LinkedChannels解析后的 channel 表让 binding 中的 channel 编号找到实际服务
IoContext / IncomingRequest事件、异步操作和交付状态将请求生命周期与复用的 Worker 环境区分开

config.rs 的构建过程分为三轮:收集各 Worker 的 actor 配置、创建服务、连接引用。WorkerService::new 经 factory 编译代码并保留 PendingLink,连接完成后才开始处理请求。这不是每个 HTTP 请求都重新编译一次代码。服务图构建、连接与 channel 表

4. 一次 HTTP 请求实际经过什么

以 GET /hello?name=Alice 为例,下面按源码说明主路径,不是逐帧堆栈;省略了追踪、代理和 Durable Objects 的分支。

  1. 选择配置的服务。 Rust 的 HttpConnection::request 根据 socket 配置处理 URL / header 改写,再通过 channel 创建本次请求接口。非法请求可能在这里返回 400,应用代码尚未执行。
  2. 创建 Worker 事件入口。 目标为 Worker 时,WorkerService::start_request 将 entrypoint、metadata 与 ChannelFactory 交给 ffi::worker_start_request。
  3. 进入事件上下文。 C++ 的 WorkerEntrypoint::request 调用 requestImpl,取出 IncomingRequest,标记交付,再通过 context.run(...) 在对应执行锁下运行代码。
  4. 调用应用 handler。 lock.getExportedHandler(...) 取得模块导出的 handler;ServiceWorkerGlobalScope::request 把原生请求转换成 JS Request,再进入模块的 fetch(request, env, ctx) 或旧式事件处理路径。
  5. 通过 channel 访问依赖。 若应用调用 env.BACKEND.fetch(...),目标由已经连接的 binding 决定。它可以是同进程 Worker,也可以是指定外部服务;不会仅因 URL 中出现某个主机名就自动改成该主机。
  6. 发送响应并收尾。 handler 的返回值或 Promise 进入响应处理;请求体和响应体可以流式传递。执行入口负责取消、异常及后台任务的生命周期。响应头发出后再出现流错误,也不能保证还能改写状态码。

证据入口:HTTP listener、start_request、requestImpl、全局作用域请求分发。

这里有三个不同时间点:事件送达、HTTP 响应完成、waitUntil() 任务完成。waitUntil() 能延长事件相关工作的生命周期,但进程崩溃仍会丢失未完成任务;需要可靠重试的业务应使用持久化任务系统。

5. 自部署复杂度:相当于维护一个应用服务器

对自己的无状态 HTTP 应用,主要工作是安装一个运行时、写一份配置、托管一个进程、接入 HTTPS。不需要自行实现 V8、Worker 调度器或发布平台。已有服务器和运维配置时,部署链路较短。

下面是工程规划估计,不是官方工期;前提是应用已经能在 workerd 运行,已有 Linux 基础。

部署范围复杂度粗略投入主要工作
本机或服务器上跑通 HTTP低数十分钟到半天固定二进制、配置、启动和请求测试
单机长期运行自己的无状态应用低到中半天到数天systemd、域名、证书、日志和更新回滚
迁移依赖 KV / D1 / R2 等产品的既有应用取决于依赖逐项评估找到后端或改写数据访问,核对行为差异
单机持久状态或多副本部署中到高取决于恢复要求数据目录、备份恢复、状态归属和流量管理

服务器配置不按 Worker 数量直接推算。 可先用现有小型 Linux 主机做业务压测,根据 CPU、RSS、请求延迟和并发连接数决定容量。本文没有提供最低内存或吞吐承诺;源码编译所需的机器资源,也不能作为运行官方二进制的最低要求。

不可信代码执行是另一类需求。官方明确说明 workerd 本身不是强化安全沙箱;若以后开放用户上传代码,再单独设计外层隔离。本版本默认工厂的 NullLimitEnforcer 与 NullIsolateLimitEnforcer 也不等于托管平台配额;运行自己的应用时,仍可在宿主层设置进程资源上限。安全边界、默认限制器

Durable Objects 是自建的主要难点之一

固定版提供 inMemory 和 localDisk:前者进程退出就丢失存储;后者指向可写磁盘服务,持久化到本地 SQLite 等文件,但明确标为实验性。该版 schema 还写明对象限于单个运行时实例,集群分发是待办。固定版存储定义

因此,两个副本各用一块本地盘,不会自动形成共享状态系统。必须回答同一个对象由谁处理、节点失效后如何恢复、如何避免两个副本同时写、备份能否一致恢复等问题。仅挂载共享目录不足以证明这些条件成立。

**版本差异:**本次检索的 main 已出现需要 --experimental 的 ClusterConfig,涉及 S3 兼容 bucket、租约和所有权记录。它不在本文固定发布版中,也不能据此宣称已获得托管 Durable Objects 的生产保障。评估时应另锁包含该功能的提交,测试故障转移、网络分区与升级路径。main 的实验性定义

6. 最小自建流程:两个 Worker,通过 binding 调用

以下示例只运行自己信任的代码。环境要求为 Node.js / npm,以及可运行官方预编译二进制的平台。固定版 README 对 Linux 要求 glibc 2.35+,并列出 CPU 指令集条件;Alpine 的 musl 环境不能直接按 glibc 包处理。生产部署应事先下载和锁定制品,不在启动时临时使用 npx latest。平台要求

安装固定版

sh
mkdir workerd-demo
cd workerd-demo
npm init -y
npm install --save-exact workerd@1.20261029.1
./node_modules/.bin/workerd --version

预期版本输出为 workerd 2026-10-29。保存 package-lock.json,后续用 npm ci 复现依赖。

创建三个文件

config.capnp 明确暴露一个 HTTP socket,入口 Worker 通过 BACKEND 访问第二个 Worker:

capnp
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "front", worker = (
      modules = [(name = "front.js", esModule = embed "front.js")],
      compatibilityDate = "2026-10-29",
      bindings = [(name = "BACKEND", service = "backend")]
    )),
    (name = "backend", worker = (
      modules = [(name = "backend.js", esModule = embed "backend.js")],
      compatibilityDate = "2026-10-29"
    ))
  ],
  sockets = [
    (name = "http", address = "127.0.0.1:8080", http = (), service = "front")
  ]
);

front.js:

javascript
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === "/healthz") return new Response("ok");
    if (url.pathname !== "/hello") {
      return new Response("Not Found", { status: 404 });
    }
    return env.BACKEND.fetch(request);
  }
};

backend.js:

javascript
export default {
  fetch(request) {
    const name = new URL(request.url).searchParams.get("name") || "world";
    return new Response(`Hello, ${name}!`, {
      headers: { "content-type": "text/plain; charset=utf-8" }
    });
  }
};

启动与验证

sh
./node_modules/.bin/workerd serve config.capnp

另一个终端执行:

sh
curl -i http://127.0.0.1:8080/healthz
curl -i 'http://127.0.0.1:8080/hello?name=Alice'
curl -i http://127.0.0.1:8080/missing

依次应得到 200 / ok、200 / Hello, Alice!、404 / Not Found。只有 front 暴露到 socket,backend 无需另开端口。修改 binding 指向会改变调用目标;把 BACKEND 名称改错则不能继续完成这条调用。这比只看到 Hello World 更能验证服务图的连接关系。

配置中没有声明 module import 的外部 npm 包;对于带依赖或 TypeScript 的项目,应先构建成可加载模块,再把产物交给运行时。该版也支持 workerd serve wrangler.jsonc,但只接受 README 列出的字段子集,不能把整个 Wrangler 部署配置原样当作兼容承诺。配置入口

compatibilityDate 用来选择 API 兼容行为,不锁定二进制;它也不替代安全更新。二进制版本、兼容日期和业务代码需要分别记录。Compatibility dates

7. 部署到自己的 Linux 服务器

这一节给出单机、可信代码、普通 HTTP 应用的部署模板。假设使用带 systemd 的 Ubuntu / Debian,已安装兼容的 Node.js、npm 和 Nginx,并有可用域名与证书。示例中的 app.example.com 和证书路径要替换为实际值;命令在目标服务器执行,本文没有替读者操作服务器。

推荐拓扑是:浏览器通过 HTTPS 连接 Nginx,Nginx 把请求转发到 127.0.0.1:8080 的 workerd,workerd 再执行本文的 front 和 backend。公网不必暴露 8080。workerd 本身也有 TLS 配置能力,这里选择让已有反向代理统一处理证书。

7.1 放置应用和固定二进制

沿用前面的 workerd-demo 目录,确认包含 config.capnp、两个 JS 文件、package.json 和锁文件。在目标 Linux 服务器执行依赖安装,避免把 macOS 的原生包复制过去:

sh
cd workerd-demo
npm ci
./node_modules/.bin/workerd --version
sudo useradd --system --no-create-home --shell /usr/sbin/nologin workerd-app
sudo install -d -m 0755 /opt/workerd-app/releases/r1
sudo install -m 0755 node_modules/workerd/bin/workerd /opt/workerd-app/releases/r1/workerd
sudo install -m 0644 config.capnp front.js backend.js /opt/workerd-app/releases/r1/
sudo ln -s /opt/workerd-app/releases/r1 /opt/workerd-app/current

这些是新部署命令:若服务账号或 current 已存在,应检查并复用,不要重复覆盖。固定版 npm 安装完成后,bin/workerd 在本文验证的 Linux 环境是原生可执行文件。这里将它和应用一起放入版本目录;本例不需要在启动时执行 npm,也不依赖 Node.js 提供 HTTP 服务。源码、配置及二进制由管理员维护,运行用户只需读取和执行。

7.2 使用 systemd 托管进程

创建 /etc/systemd/system/workerd-app.service:

ini
[Unit]
Description=Self-deployed workerd application
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=workerd-app
Group=workerd-app
WorkingDirectory=/opt/workerd-app/current
ExecStart=/opt/workerd-app/current/workerd serve /opt/workerd-app/current/config.capnp
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target

启动并检查:

sh
sudo systemd-analyze verify /etc/systemd/system/workerd-app.service
sudo systemctl daemon-reload
sudo systemctl enable --now workerd-app
sudo systemctl status workerd-app --no-pager
curl --fail http://127.0.0.1:8080/healthz
curl --fail 'http://127.0.0.1:8080/hello?name=Alice'
sudo journalctl -u workerd-app -n 100 --no-pager

预期服务处于 active,两个 HTTP 请求分别返回 ok 和 Hello, Alice!。systemd 的启动成功只证明进程启动,不能代替 HTTP 就绪检查。服务配置

这个 unit 针对无状态示例,ProtectSystem=strict 限制文件系统写入。如果应用使用本地持久存储,应单独配置可写数据目录,例如 StateDirectory=workerd-app,再让磁盘服务指向 /var/lib/workerd-app。不要把数据存放到随版本替换的 current 目录;路径、权限和备份要一起验证。MemoryDenyWriteExecute 等更强限制可能与 V8 的 JIT 行为冲突,不能不经测试就叠加。执行环境选项

7.3 接入域名与 HTTPS

先把域名的 A / AAAA 记录指向服务器,并取得匹配该域名的证书及自动续期机制。AAAA 只在服务器 IPv6 确实可达时配置。以下 Nginx 配置假定证书已存在,放入被 http 上下文加载的站点配置文件:

nginx
server {
    listen 80;
    server_name app.example.com;
    return 301 https://app.example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name app.example.com;
    ssl_certificate /etc/ssl/workerd-app/fullchain.pem;
    ssl_certificate_key /etc/ssl/workerd-app/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_buffering off;
    }
}

Nginx 在这里是直接面向客户端的入口;若前面另有 CDN 或负载均衡器,需要另行定义可信代理和真实客户端地址。proxy_buffering off 便于传递流式响应,普通业务也可按需求调整。WebSocket 还需要 Upgrade / Connection 转发设置,本例不覆盖。代理配置、HTTPS 配置

为了让应用看到原始的 https scheme,将 config.capnp 中 socket 的 http = () 改成:

capnp
http = (forwardedProtoHeader = "X-Forwarded-Proto")

这会信任该 header,因此必须保持 workerd 仅监听 loopback,并由可信 Nginx 覆写该值;不能直接把同一入口开放到公网任人传入。随后执行:

sh
sudo systemctl restart workerd-app
sudo nginx -t
sudo systemctl reload nginx
curl --fail https://app.example.com/healthz
curl --fail 'https://app.example.com/hello?name=Alice'

服务器防火墙与云安全组只放行所需入口,例如 80、443 和受限管理端口;不要额外放行 8080、Inspector 或 debug port。证书续期后需要让 Nginx 重新加载证书。系统自启、证书续期和外网请求都应实际验收,不能只检查本机 curl。

7.4 更新和回滚

把新版本二进制与应用放到 /opt/workerd-app/releases/r2/,先在不同端口验证:

sh
/opt/workerd-app/releases/r2/workerd serve /opt/workerd-app/releases/r2/config.capnp --socket-addr http=127.0.0.1:8081

另一个终端调用 http://127.0.0.1:8081/healthz 与 /hello。通过后停止该测试进程,用临时链接加 rename 切换版本:

sh
sudo ln -s /opt/workerd-app/releases/r2 /opt/workerd-app/current.next
sudo mv -Tf /opt/workerd-app/current.next /opt/workerd-app/current
sudo systemctl restart workerd-app
curl --fail 'http://127.0.0.1:8080/hello?name=Alice'

失败时按相同方式把链接切回 r1 并重启。current.next 应在每次操作前确认不存在;保留旧制品以便恢复。本流程的单实例重启会造成短暂中断。需要无中断升级时,可同时运行两个端口的独立服务,检查新实例就绪后切换 Nginx upstream,再排空旧连接。

compatibilityDate 保持原值可以减少 API 行为变化,但仍需对新二进制回归测试。升级涉及数据库格式或业务迁移时,切回二进制不一定能恢复数据;应先安排备份和兼容策略。生产不建议用 --watch 自动替换文件充当发布流程。

7.5 上线后维护什么

最少关注错误率、请求延迟、CPU、RSS、连接数、进程重启、磁盘空间和证书到期。日志可先用 journald 管理,按业务需求再接集中观测;/healthz 是进程探针,关键依赖应有单独检查。对于 waitUntil() 任务、流式响应和 WebSocket,另验收服务重启时的行为。

什么时候才需要从源码构建

只部署 JS 应用时,优先使用官方预编译包。修改底层 API、追踪原生崩溃、适配特殊平台或准备上游贡献时,再承担 V8 与原生工具链构建成本。

sh
git clone --branch v1.20261029.1 --depth 1 https://github.com/cloudflare/workerd.git
cd workerd
# 先按该版本 README 安装 Bazelisk。
bazel build //src/workerd/server:workerd --config=release
./bazel-bin/src/workerd/server/workerd --version

固定版 Linux 构建会下载 LLVM 工具链与 sysroot;首次构建的网络、磁盘与耗时需求明显高于安装 npm 二进制。本文未执行完整源码编译,不给出机器规格和构建时间承诺。构建说明

8. 自部署前的判断

对于自己编写、主要使用 Web API 的 HTTP Worker,可以从一台 Linux 服务器、一个 workerd 进程、一套 Nginx / systemd 配置开始。是否增加容器、多个进程或负载均衡,应由现有运维体系与压测决定;容器不是运行 workerd 的前提。

迁移已有 Cloudflare Workers 应用时,先清点它依赖的 KV、D1、R2、Durable Objects 等能力。纯执行逻辑通常更容易迁移;数据后端与托管能力需要分别安排。这部分兼容工作,才是不同应用自部署复杂度差异的主要来源。

验证范围

本文最小示例在 Linux、Node.js 24 和 npm 固定安装的 workerd@1.20261029.1 下验证 HTTP、查询参数、404 与同进程 service binding。另验证了复制出的原生二进制、绝对配置路径与备用端口覆盖。systemd 与 Nginx 部署配置是按官方说明整理的目标服务器模板,未在真实 systemd 主机上安装运行,也未签发证书。验证不涵盖生产 TLS、Durable Objects 持久化与故障转移,也不证明某个完整 Workers 应用可以不改代码迁移。源码构建命令按固定版本官方说明核对,未实际编译。

关联阅读

参考资料

博客助手

正在打开博客助手…