SSD Nodes Learn 🎉 VPS $4.99/月起
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-07

如何在 VPS 上自托管 OpenTag 处理 @提及

了解 OpenTag v0.9.0 的 VPS 部署方法,涵盖 TLS 入口、GitHub 与 Slack Webhook 签名校验、Token 权限范围,以及避免误触发的安全默认配置。

提及代理时 OpenTag 的处理流程

OpenTag 会将 Slack 线程或 GitHub issue 中的 @提及转换为在您拥有的机器上运行的编码代理任务。有人在 issue 中评论 @opentag investigate this。监听器接收平台事件,验证其签名,将该提及匹配到已绑定的项目,针对本地检出代码启动编码代理,然后将结果发布回同一线程。

该项目采用 MIT 许可证,代码位于 amplifthq/opentag。截至 August 2026,最新的标记版本是 v0.9.0,于 28 July 2026 发布,并以 npm 软件包形式提供。项目没有官方容器镜像,因此需要固定的是 npm 版本。下面的每条命令都会固定该版本。

由于 GitHub 集成,这会成为 VPS 项目,而不是笔记本电脑项目。GitHub 通过向您注册的 URL 发起 HTTP 请求来发送仓库事件,因此该 URL 明天仍必须在同一地址响应。

四个组成部分

监听器接收平台事件,每个平台都有自己的监听器。GitHub 监听器是一个 HTTP 端点,使用 3050 端口,路径为 /github/webhooks。Slack Events API 监听器使用 3040 端口,路径为 /slack/events。Slack 也可以运行在 Socket Mode 下。此时应用会主动建立出站 WebSocket 连接,完全不需要入站端口。

调度器负责协调。它默认监听 3030 端口,将运行状态保存到由 OPENTAG_DATABASE_PATH 指定的本地数据库文件中,并为每次运行记录审计轨迹。任何外部系统都不应访问此端口。

运行器是本地 daemon。它会轮询任务、认领运行、为运行持有租约,并在运行期间默认每 15 秒发送一次心跳。如果已认领运行的项目目标不存在,或不在其自身配置中的允许列表内,运行器会拒绝该运行。正是这项检查,阻止 GitHub 事件将您的 agent 指向一个从未绑定的仓库。

执行器就是编码 agent。OpenTag 通过 ACP(agent client protocol)启动它。ACP 是一种通过标准输入和标准输出通信的 JSON-RPC 协议,因此 agent 会作为子进程,在 OpenTag 为其提供的工作目录中运行。内置名称包括 echocodexclaude-codecursoropencodehermesopenclaw。请从 echo 开始。示例配置随附的 executor 使用它,因为在模型修改代码前,它可以验证整条链路均能正常工作。

顺序始终不变:平台事件、签名检查、运行记录、认领、agent、在线程中回复。

为什么笔记本电脑和隧道还不够

GitHub 设置指南要求您运行 ngrok http 3050,然后将隧道主机名粘贴到仓库 Webhook 中。这样只能在最初的十分钟内正常工作。免费隧道主机名会在进程每次重启时变化,笔记本电脑进入睡眠状态后,隧道也会停止。GitHub 会保留旧的负载 URL 并持续尝试访问,因此 Webhook 设置中的 Recent Deliveries 标签页会不断出现失败记录,而相关讨论串仍然没有响应。通常一周都不会有人注意到,因为一个没有任何动作的 Webhook,看起来和一个没人提到的机器人完全一样。

VPS 可以解决这两个问题。DNS 名称不会变化,因此您只需粘贴一次的负载 URL 会一直有效。服务器不会进入睡眠状态,因此即使在 02:00 收到评论,也能得到响应。请先正确配置服务器:新 VPS 上的前十分钟介绍了本指南所需的登录用户和防火墙设置。

Slack 是例外。在 Socket Mode 下,Slack 会主动向外建立连接,不需要公网 URL,因此仅部署 Slack 时可以保持服务不对公网开放。GitHub 没有等效方式。仓库 Webhook 使用入站 HTTP,这意味着必须提供公网端点,也意味着必须使用 TLS(传输层安全)并验证签名。

在 Ubuntu 上基于固定版本发布自行托管 OpenTag

OpenTag v0.9.0 要求 Node.js 22 或更高版本。Ubuntu 24.04 自带的软件仓库提供 Node 18,因此请从 NodeSource 安装。

curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -v

node -v 必须输出 v22 或更高版本。在 Node 20 上,安装过程会输出 EBADENGINE 警告,CLI 启动后可能会失败。

为服务创建专用账户。代理以该用户的权限运行,因此不应使用您的登录账户,也不应使用 root。VPS 上的最小权限用户介绍了为什么这一步值得执行。

sudo adduser --disabled-password --gecos "" opentag
sudo loginctl enable-linger opentag
sudo npm install -g @opentag/cli@0.9.0
command -v opentag

command -v opentag 应输出类似 /usr/bin/opentag 的路径。在 Linux 上,linger 设置很重要:OpenTag 通过 systemd 安装后台服务;用户服务未启用 linger 时,SSH 会话关闭后服务会立即停止。

以该用户运行设置程序。

sudo -iu opentag opentag setup

设置程序会询问 6 项内容:CLI 语言、本地监听地址、编码代理、要处理的本地项目、要保存的平台凭据,以及运行方式。将监听地址保留为 127.0.0.1,因为 nginx 会终止 TLS 并将请求转发到该地址,因此监听器无需从外部访问。对于 GitHub,设置程序还会询问 owner/repo 格式的仓库地址、是否允许创建拉取请求、webhook 端口(默认为 3050)以及令牌。最后选择后台服务模式。如果已有配置,并希望在不显示提示的情况下安装服务,使用 opentag setup --service

配置文件位于 /home/opentag/.config/opentag/config.json,运行时状态位于 /home/opentag/.local/state/opentag。设置程序写入文件后,建议手动检查这些键。

{
  "runnerId": "runner_local",
  "dispatcherUrl": "http://localhost:3030",
  "runnerToken": "...",
  "approvalMode": "ask",
  "repositories": []
}

优先使用 runnerToken,即作用域限定于运行器的不记名令牌,而不是较旧的共享 pairingToken。除非将凭据替换为机密引用,否则配置文件会以纯文本保存凭据。机密引用会在启动时从环境变量或磁盘文件读取值。无论采用哪种方式,该文件都是此主机上最敏感的内容:权限应为 600,所有者应为 opentag,并且绝不能放在 git 仓库中。更完整的说明请参阅不要将机密信息交给 AI 代理

在对外提供服务前检查安装结果。

sudo -iu opentag opentag doctor
sudo -iu opentag opentag status

opentag doctor 会检查调度器、绑定、检出内容和执行器。opentag status 会输出配置和运行时状态;存在运行记录后,还可以将检查范围限定为单次运行。在将平台指向此主机前,修复 doctor 报告的所有问题。

仅开放两个路径并在前端处理 TLS

nginx 终止 TLS,并且只转发两个路径。其他所有请求都返回 404,因此扫描器即使发现主机,也无法获知其后运行的服务。

/etc/nginx/sites-available/opentag 中编写一个普通的 80 端口 server 块,并加入下面的两个 location,然后让 Certbot 添加 TLS 部分。

sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/opentag /etc/nginx/sites-enabled/opentag
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d opentag.example.com

nginx -t 会输出 syntax is oktest is successful。它是防止配置拼写错误导致网站因 reload 中断的唯一保护措施。在 Ubuntu 24.04 上使用 nginx 配置 Certbot介绍续期流程,以及 ACME(自动证书管理环境)挑战失败的原因。完成后的配置块如下所示。

server {
    listen 443 ssl;
    server_name opentag.example.com;

    ssl_certificate     /etc/letsencrypt/live/opentag.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/opentag.example.com/privkey.pem;

    client_max_body_size 2m;

    location = /github/webhooks {
        proxy_pass http://127.0.0.1:3050;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    location = /slack/events {
        proxy_pass http://127.0.0.1:3040;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    location / {
        return 404;
    }
}

location = /github/webhooks 中的 = 是精确匹配;proxy_pass 后面不带任何内容时,会原样传递原始 URI。删除 = 后,/github/webhooks/ 下的所有路径也会被转发,暴露面超出了监听器的实际需求。

防火墙配置保持严格限制。

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

绝不开放 3030、3040 和 3050 端口。确认这些端口绑定到 loopback,而不是绑定到所有网络接口。

sudo ss -tlnp

每一行 OpenTag 都应显示为 127.0.0.1:3030 或类似内容。若某行显示为 0.0.0.0:3050,则表示监听器向整个互联网提供服务,目前只是由 ufw 阻止访问;防火墙中的一个错误就可能导致 agent trigger 暴露。ufw 防火墙基础说明了这个默认拒绝规则的实际作用。

两个检查即可验证前端入口。curl -I https://opentag.example.com/ 会从 nginx 返回 404,这表明证书有效,且 catch-all 已关闭。向 /slack/events/github/webhooks 发送不带签名的请求时,绝不能返回 200。

验证每个签名,因为 URL 是公开的

任何人都可以找到负载 URL。它位于仓库设置、浏览器历史记录,或粘贴到工单中的屏幕截图里。签名是区分真实 GitHub 交付请求和他人手动构造请求的唯一依据。

GitHub 使用 webhook secret 为每个交付请求签名,并将结果发送到 x-hub-signature-256 标头中。OpenTag 会使用 platforms.github.webhookSecret 验证该标头。项目加固说明直接规定:不要在 /github/webhooks 上接受未签名的源事件。Slack 使用 SLACK_SIGNING_SECRET 为每个请求签名,并加入时间戳,因此捕获的请求体无法在数小时后重放。

跳过验证并不是小风险。未验证的端点会接受手动构造的 issue_comment 负载,其中包含 @opentag;随后,OpenTag 会使用您的令牌,在您的检出目录中,根据陌生人的指令运行编码代理。回复会发送到伪造负载指定的线程。

OpenTag 在此基础上增加了两层保护。源交付请求会按交付 ID 跟踪,因此重新交付同一事件不会启动第二次运行。Runner 调用接受幂等键,因此重放某个调用时只会返回成功,不会追加另一条审计事件。

速率限制可配置,并且应启用。OPENTAG_RATE_LIMIT_WINDOW_MSOPENTAG_RATE_LIMIT_MAX_REQUESTS 限制请求速率,OPENTAG_MAX_REQUEST_BODY_BYTES 限制请求体大小,超大的负载会被拒绝并返回 413 request_body_too_largeOPENTAG_RATE_LIMIT_DISABLED=true 仅用于本地开发,不应出现在公网服务器上。同一份说明还规定:公开中继 URL 必须使用 HTTPS;CLI 仅允许 localhost 使用普通 HTTP。

机器人实际需要哪些令牌权限范围?

在 GitHub 上,OpenTag 使用细粒度个人访问令牌,而不是 GitHub App。文档说明,App 方案正在规划中,目前不是 CLI 的默认配置。这会带来一个容易被忽略的后果:机器人会以创建令牌的用户身份发表评论。请在一个您愿意看到其名称出现在每条分类回复中的账户下创建令牌。

请按照设置指南,将权限范围限制到最低。选择 Only select repositories,然后选择一个仓库。授予 Issues: Read and writePull requests: Read and write。这些权限足以读取提及并在线程中回复。

请注意缺少的权限:代码写入权限。除非将 preparePullRequestBranch 设置为 true,否则 OpenTag 不会推送分支;此外还有单独的 githubApplyToken,用于确保写入代码的令牌与发表评论的令牌不同。请将两者分开,并在读写评论路径稳定运行几周之前,保持写入令牌停用。

应避免使用以下配置:对 All repositories 中所有仓库授予 Contents: Read and write 的令牌。现在,任何能在这些仓库中发表评论的人,都可以引导一个拥有提交权限的代理,而审计记录显示的执行者是令牌所有者。请在代理证明其可靠性后,一次只扩大到一个仓库。

在 Slack 上,机器人的权限范围是 app_mentions:readchat:writereactions:writechannels:history。私有频道还需要 groups:history,并订阅 message.groups 事件。Socket Mode 需要包含 connections:write 的应用级令牌,即以 xapp- 开头的令牌。channels:history 可读取机器人已加入的公开频道中的消息历史,因此请将机器人添加到需要它的频道,而不是添加到所有频道。

端到端验证一个问题

先配置 webhook。在仓库中依次打开 Settings、Webhooks 和 Add webhook。Payload URL 为 https://opentag.example.com/github/webhooks,Content type 为 application/json,Secret 使用安装程序生成的密钥。仅订阅 Issue commentsPull request review comments,不要订阅其他事件。

保存后,GitHub 会立即发送一次 ping delivery。打开 Recent Deliveries,确认请求至少到达了服务器。如果这里返回 502,表示 nginx 无法连接监听端口。这是本地问题,不是 GitHub 的问题。

现在进行实际测试。打开一个描述 bug 的 issue,并发表评论:

@opentag triage this. Reproduce the report against the current main branch, then reply with the file and function most likely responsible, plus the test you would write first.

按以下顺序确认结果。Recent Deliveries 记录了返回 2xx 响应的 issue_comment delivery。Dispatcher 记录一次运行。Runner 获取该运行并开始发送心跳。Executor 打开 checkout 并执行任务。答案以评论形式出现在同一个 issue 线程中。sudo -iu opentag opentag status 会显示正在执行的运行,因此可以直接监控,而不必猜测状态。

在第一次实际运行前,将 approvalMode 设置为 ask。在 ask 模式下,运行会暂停,并等待人员确认后才执行任何会改变状态的操作。autoautonomous 模式也可用。稍后可以在已经查看一个月运行记录的仓库中使用这些模式。

在 Slack 侧,同一次运行会先在频道中发送 /bind owner/repo,然后发送 mention。Bot 还会响应 /help/status/doctor/stop/unbind confirm。使用 OPENTAG_SLACK_BINDING_ADMIN_USER_IDS 限制可以修改绑定的用户。该设置是以逗号分隔的 Slack 用户 ID 列表,因为绑定表示从公共频道到服务器上某个 checkout 的映射关系。

Triage 适合作为第一条验证路径,因为它只读取数据,不写入数据,而且结果容易检查。下一步是 Review:Agent 不再评论 issue,而是针对 diff 发表评论。自托管的 pull request review agent 使用的就是同一架构,只是目标改为 pull request。如果希望 Agent 在执行任务时访问自有系统,可以使用 VPS 上的 MCP 服务器

当代理在所有人面前出错时会怎样?

它会出错。问题在于这会造成什么代价。

公开 issue 中的错误回复,会以团队认识的用户名发布为评论;评论发布后,GitHub 会立即将邮件发送给所有订阅者。删除评论并不能撤回邮件。Slack 通知也是如此。应按答案会公开出错来规划,而不是假设它只会在私下正确。

以下 4 项选择可以控制损失,其重要性超过你编写的任何提示词。

  • 使用 ask 模式,让代理提出方案,由人员批准;这样错误方案只需一次点击即可停止。
  • 保持 preparePullRequestBranch 的默认值 false,这样错误运行最坏只会发布错误评论,而不会修改错误分支。
  • 开始时只绑定一个仓库和一个频道。运行器会拒绝任何目标项目不在本地允许列表中的运行,因此未绑定的仓库无法自行调用代理。
  • 将评论令牌与任何应用令牌分开,这样撤销写入权限不会同时中断问题分流。

对于方向错误的运行,Slack 提供 /stop 命令。每次运行还会留下审计记录,其中包含启动运行的提及内容以及代理执行的操作。之后可以通过这些记录判断错误发生的位置。

社交层面的安排与配置同样重要。将机器人放在一个大家预期会有机器人的频道中,并让大家知道它可能出错。在一个有 40 人的频道中,如果大家误以为回复经过人工审核,那么一个自信但错误的答案造成的代价会高于它节省的问题分流成本。在频道描述中写明机器人由谁负责,以及由谁检查其输出。

备份、升级和版本固定

所有内容都存放在两个路径中:/home/opentag/.config/opentag/config.json/home/opentag/.local/state/opentag。前者包含凭据,后者包含运行历史和数据库文件。请将两者都以 mode 600 备份,并存放在服务器之外。丢失这些内容意味着需要重新创建令牌和绑定关系,而不是重建服务器。

升级包括提升版本并重启。

sudo npm install -g @opentag/cli@0.9.0
sudo -iu opentag opentag service stop
sudo -iu opentag opentag service start
sudo -iu opentag opentag doctor

不要跟踪 @latest,而应固定版本。该软件会使用有效令牌在您的代码仓库上运行编码代理,因此,夜间发布的新版本会未经审核地改变这一组件。安全策略不会回移植修复,修复只会进入最新版本。因此,固定版本意味着先阅读变更日志,再有意进行升级。这不意味着永远停留在 v0.9.0。到 July 2026 为止的版本历史显示,每月会发布多个版本,因此在每次升级前阅读发行说明是合理的做法。

FAQ

运行 OpenTag 必须使用 VPS 吗?笔记本电脑够用吗?

仅运行 Slack 时,笔记本电脑就够用,因为 Socket Mode 会建立出站 WebSocket 连接,不需要入站端口。GitHub 则不同。仓库 Webhook 会通过入站 HTTP 发送到您注册的一次性 URL,因此该地址必须保持不变,并且在您休息时也能响应。免费账户提供的隧道主机每次重启都会更换地址,GitHub 仍会向旧地址发送请求。此时,仓库的 Recent Deliveries 选项卡中会出现失败记录,线程中也不会有任何响应。使用具有固定 DNS 名称和证书的 VPS 可以同时解决这两个问题。

OpenTag 需要哪些 GitHub 权限?

使用细粒度个人访问令牌,并将仓库范围限制为 Only select repositories,同时授予 Issues: Read and writePull requests: Read and write 权限。这样即可读取提及并在线程中回复。除非将 preparePullRequestBranch 设置为 true,使 OpenTag 推送分支,否则不需要代码写入权限。另有单独的 githubApplyToken,可将用于写入代码的令牌与用于评论的令牌分开。不要使用对所有仓库都有效且具有 contents write 权限的令牌,因为任何能够在这些仓库中发表评论的人,都可能控制一个可以提交代码的代理。

如何停止正在出问题的运行?

Slack 提供了专门用于此目的的 /stop 命令。在服务器上,opentag status 可显示当前正在运行的内容,opentag service stop 可停止 daemon;这会结束整个流水线,而不是只停止某次运行。若要避免使用这两个命令,请将 approvalMode 设置为 ask,让运行在修改任何内容前暂停并等待人工确认;同时保持 preparePullRequestBranch 为 false,使异常运行生成评论,而不是创建分支。

为什么 Webhook 返回 502,而线程中没有任何响应?

502 由 nginx 返回,而不是由 OpenTag 返回。这表示代理无法连接到监听程序。/var/log/nginx/error.log 会显示 connect() failed (111: Connection refused) while connecting to upstream。监听程序可能已停止,也可能监听的端口与 proxy_pass 行中指定的端口不同。运行 sudo ss -tlnp,确认 GitHub 使用 127.0.0.1:3050、Slack 使用 127.0.0.1:3040,并确认这两个端口上确实有程序在监听;然后运行 opentag doctor 查看绑定和执行器。

#opentag#ai-agents#slack#github#webhooks#自托管