这台 DS918+ 上跑着七块硬盘、一个 WordPress 博客和十几个容器。以前它一闹脾气,我就得开电脑、连 DSM、翻控制面板,一圈点下来半小时没了。现在我可以直接说一句"博客打不开了帮我查查",AI 会自己去测证书、看 CDN 回源、翻容器日志,甚至把文章写完直接存进后台。
这篇文章记录这套方案是怎么搭起来的,以及我踩进去的五个坑——其中第四个差点把整条通道弄断,而且我自己当时还判断错了方向。
适合谁:手上有群晖或任意 Linux 服务器,希望让 AI 助手参与日常运维的人。 前提:需要一台能装 Tailscale 的服务器、一台电脑,以及一个愿意读文档的下午。 不适合:想找"一键脚本"的人。这套方案的价值在于每一层你都看得懂,否则出问题时无法自救。
一、这套方案能做到什么
先把能力边界说清楚,再谈怎么搭。
能做的:
| 场景 | 具体表现 |
|---|---|
| 排障 | 网站打不开、证书快过期、CDN 和源站返回不一致,AI 自己逐层测 |
| 巡检 | 报 CPU / 内存 / 温度 / 运行时长,列容器和重启次数,算 Docker 占了多少空间 |
| 容器运维 | 看日志、看实时资源占用、启停重启指定容器 |
| 内容发布 | 写文章、选分类标签、上传配图,直接存进 WordPress(默认草稿) |
| 文件操作 | 列目录、读内容、搜索、新建重命名移动复制 |
做不到的(刻意的):
- 改 DSM 本身的设置(网络、防火墙、计划任务、用户密码)——这需要群晖管理员权限,而给 AI 开管理员等于把整台机器交出去
- 删文章。这套工具集没有任何删除能力,只能新建和修改
- 读磁盘 SMART 和卷容量。普通账号调不动存储相关的 API
最后一条有点反直觉:磁盘写满是"网站突然打不开"最常见的原因之一,而我恰恰看不到容量。这是权限设计的直接代价,后面第八节会讲怎么妥协。
二、前置条件
| 项目 | 要求 | 说明 |
|---|---|---|
| 服务器 | 群晖 DSM 7.x,或任意能跑 Docker 的 Linux | 本文以 DS918+ / DSM 7.2.2 实测 |
| 电脑 | Windows / macOS / Linux 均可 | 本文在 Windows 上实测 |
| 组网 | Tailscale 账号(免费版够用) | 两台设备都装 |
| 服务器账号 | 一个非管理员账号 | 千万别用管理员,见第七节 |
| AI 客户端 | 支持 MCP 协议的 | 本文用 WorkBuddy 实测 |
| 博客(可选) | WordPress 5.6+ | 应用密码是 5.6 引入的 |
关于"非管理员账号"这一条,值得单独说一句:它会让一部分工具直接不可用(比如读磁盘健康度),但这是划算的。这套方案的所有通道都建立在"谁在私有网络里谁就能调用"的基础上,账号权限就是最后一道闸。
三、整体思路

整套东西分三层:
第一层是网络。服务器的 MCP 端口只在 Tailscale 这个私有网络里监听——局域网不通、公网不通,路由器上也没有端口映射。AI 客户端连进来的唯一途径是那条加密隧道。
第二层是服务。服务器上跑两个容器化的小服务(分别管文件和系统状态、管容器),加上一个本机进程(管博客)。它们把"服务器能做的事"翻译成 AI 能调用的工具。
第三层是工具。每个服务暴露一组带参数的工具描述,AI 读到描述后就能自己决定该调哪个。
为什么不做成一个大服务?因为故障隔离。管容器的那个服务一旦挂了,文件和系统状态那条通道还活着,我至少还能从日志里看出问题。把所有能力塞进一个进程,它一挂就全瞎。
四、第一步:把服务器和电脑放进同一个私有网络
这一步解决什么问题:让 AI 能安全地访问服务器,而不是把管理端口暴露到公网。
最省事的做法当然是端口映射,但那样等于把管理接口挂到互联网上——扫描器几小时内就会找上门。Tailscale 的做法是给每台设备分配一个 100.x 段的私有地址,设备之间直连(打洞失败时走中继),流量端到端加密。
安装过程很直白:群晖在套件中心装 Tailscale,电脑装对应客户端,登录同一个账号,两台设备就出现在同一个虚拟网络里。
验证这一步成功的信号:在电脑上访问服务器的 Tailscale 地址,能打开 DSM 登录页:
# 把 <NAS的Tailscale IP> 换成你自己的
curl -k -I https://<NAS的Tailscale IP>:5001
返回 HTTP/1.1 200 OK 或者跳转到登录页,说明隧道通了。
顺手做一件重要的事:确认这些管理端口从局域网和公网都访问不到。在另一台普通设备上试:
# 应该超时或拒绝连接,而不是返回页面
curl -m 5 -I http://192.168.1.10:8765
如果这里能通,说明只用 Tailscale 还不够——要么是防火墙没配,要么是服务监听了 0.0.0.0 而防火墙没拦。
五、第二步:给 AI 装上手脚
这一步解决什么问题:让 AI 知道"这台服务器能做什么、怎么调"。
这里用的是 MCP(Model Context Protocol)。简单说,它是一套约定:服务端声明自己有哪些工具、每个工具要什么参数,AI 客户端读到这个清单,就能自己决定什么时候调哪个。
我搭了三条通道,分别对应不同风险等级:
| 通道 | 承载方式 | 工具数 | 覆盖范围 |
|---|---|---|---|
| 文件与系统状态 | 服务器上的容器,监听 8765 | 17 | 读文件、列目录、看 CPU 内存温度、查套件状态 |
| 容器运维 | 服务器上的容器,监听 8766 | 12 | 容器列表、日志、实时资源、启停重启 |
| 博客发布 | 电脑上的本地进程 | 10 | 站点体检、文章读写、媒体上传 |
为什么第三条放在电脑上而不是服务器上? 因为它在电脑上跑意味着服务器侧零部署——不用装插件、不用开端口、不用给文件写权限。它通过 WordPress 官方的 REST API 和网站对话,凭据是一个可以随时撤销的应用程序密码。
三条通道都需要三样东西才能真正可用:能连上、能认证、有权限。前两样配好后,最容易翻车的是第三样——后面第七、八节讲的坑基本都在这一层。
六、第三步:打通博客发布
这一步解决什么问题:让 AI 写好的文章能真的进到博客后台。

WordPress 从 5.6 开始提供"应用程序密码":在用户资料页生成一串专用密码,专门给程序调用 REST API 用,不经过登录表单,也可以随时单独撤销。理论上配好就能用。
我按文档配好之后,得到的却是一句:
wp-config.json 里缺少 username 或 app_password(发布类工具需要)
问题不是密码错了,而是程序根本看不到密码。顺着查下去,原因分两层:
6.1 表层:某个插件把功能关掉了
WordPress 有个过滤器叫 wp_is_application_passwords_available,任何插件都能改它的返回值。安全插件的逻辑是"这个功能可能被社工利用,默认关掉",于是它写了这么一段:
// 来自安全插件源码
if ( wfConfig::get('loginSec_disableApplicationPasswords') ) {
add_filter( 'wp_is_application_passwords_available', '__return_false' );
}
注意这里有个关键细节:它没有指定优先级,也就是用默认的 10。
6.2 解法:用一个优先级更高的过滤器盖回去
WordPress 的过滤器按优先级从小到大执行,大的后执行、返回值最终生效。所以我加了一个只做一件事的 mu-plugin(mu-plugins 目录下的文件会被自动加载,不需要在后台启用):
<?php
// /wp-content/mu-plugins/enable-app-passwords.php
add_filter( 'wp_is_application_passwords_available', '__return_true', 99 );
优先级 99 > 10,所以最终结果是"可用"。

验证这一步成功的信号:访问站点的 /wp-json/,在返回的 JSON 里找 authentication 段,应该能看到 application-passwords 字样出现了。
这个解法有两个副作用,得知道:
- 插件后台里那个开关从此失效——你在界面上关掉它,功能依然是开的,因为 99 比 10 大。真正的开关变成了这个文件。
- 用户资料页上仍会显示一行「Application password is disabled by Wordfence」的提示。那是假提示,功能实际是通的,忽略即可。
想回到插件原本的行为也很简单,但顺序不能反:先在插件后台关掉那个选项,再删掉这个 mu-plugin。反过来先删文件的话,功能会立刻失效。
6.3 顺便确认一件事:CDN 不会吃认证头
排查过程中我一度怀疑是 CDN 把 Authorization 请求头吃掉了。实测下来是干净的:源站收到的确实是 HTTPS,CDN 会正常转发 Authorization 头。现在发文章走的就是公网域名,没有绕路。
如果你的站点也遇到认证失败,这一层值得验证一下,别跳过:
# -I 只看响应头,不下载正文
curl -I -u "用户名:应用密码" https://你的域名/wp-json/wp/v2/posts
返回 200 说明认证链路完整。返回 401 是凭据问题,403 是权限问题(账号角色不够),两者要分开查。
七、安全边界:那条不能碰的红线
这一步解决什么问题:避免 AI 把自己赖以工作的通道掐断。
这条教训值得单开一节。
管容器的那条通道,本身也是跑在服务器上的一个容器。也就是说,它能管理包括它自己在内的所有容器。如果 AI 某天判断"这个服务好像不太对,重启一下",它就可能把自己重启掉。
后果比听起来严重。实测发现:Docker 会把这次重启当成人工停止,unless-stopped 重启策略不会介入。而这台服务器上既没开 Docker API、也没开 SSH,于是此后没有任何通道能把它重新启动起来,只能人工到 DSM 容器管理器里点。

所以这类服务必须内建一道护栏:在调用 Docker API 之前先判断"我这次操作的目标是不是我自己"。识别方式不能只靠一个来源——实测发现某些环境下容器里根本没有 HOSTNAME 环境变量,只靠它就会漏判。可靠的做法是多路并用,任一路命中就拒绝:
| 识别来源 | 说明 |
|---|---|
| 显式声明 | 配置里写死"我是谁",确定性最高 |
| 主机名类 | 容器的 /etc/hostname、/proc/sys/kernel/hostname |
| ID 类 | /proc/self/cgroup、/proc/self/mountinfo 里的 64 位十六进制串 |
| 反查名字 | 用已知 ID 反问 Docker"这个 ID 叫什么",把容器名也纳入比对 |
最后一条容易被忽略:实际误操作往往是用容器名去调用的,只按 ID 比对拦不住。
护栏还必须可见。一个无法确认是否生效的安全护栏等于没有护栏,所以这个服务会通过一个只读接口报告自己的护栏状态(是否就绪、识别出了哪些标识),启动时也会往日志里打一行。
八、踩过的坑
按"现象 / 原因 / 解法"列出来,都是我实际撞过的:
| 现象 | 真正的原因 | 解法 |
|---|---|---|
| 服务报"读不到配置文件",但文件权限看着正常 | 文件属主是 root,而服务以普通用户身份运行。权限位没错,属主不匹配 |
改属主,而不是加权限位 |
客户端报 421 Invalid Host header |
白名单校验要求带端口号,而且配置项会整体替换默认值 | 白名单写成 IP:端口 的形式 |
| 服务在自己容器内连不上 DSM API | 配置里写的是局域网地址,而防火墙只放行了私有网络来源 | 配置改成私有网络地址 |
| 重启容器后整条通道永久失联 | 重启的是承载服务自己的容器;Docker 判定为人工停止,重启策略不介入 | 见第七节,内建自指护栏 |
| 应用密码配好了却报"缺少凭据" | 安全插件用过滤器关掉了该功能 | 见 6.2,用更高优先级的过滤器覆盖 |
还有两个我自己判断错的,比上面的坑更值得记:
误判一:某次发现服务器在局域网里访问不到,我第一反应是"服务器网络出问题了"。实际情况是我自己电脑的网络切换了(从无线换到了有线),观测条件变了。后来做对照才发现服务器一直是好的。
误判二:读取某个受保护目录下的文件时报 502 Bad Gateway,我一度怀疑是通道在多个地址之间摇摆。做了对照组实验才推翻——同一时段列目录的操作全部成功,说明通道没坏,是那个文件本身(属主和权限)读不到。后来才知道,这类权限失败在这个接口上就是表现为 502,同一个接口以前还见过 105 和 407 两种错误码。
两次错误的共同点:把权限问题当成了环境故障。这也是我给这套方案加"每次报故障前必须先做对照实验"这条规矩的原因——先排除自己这一侧的变化,再下结论。
九、自检清单
搭完之后照着过一遍,全绿才算真的能用:
- ☐ 电脑能通过私有网络地址打开 DSM 登录页
- ☐ 管理端口从局域网和公网都访问不到(这是好事)
- ☐ AI 客户端里能看到三条通道的工具清单
- ☐ 让 AI"列一下现在跑着哪些容器",能返回正确结果
- ☐ 让 AI"给服务器做个体检",能报出 CPU / 内存 / 温度
- ☐
curl -I -u "用户名:应用密码" https://你的域名/wp-json/wp/v2/posts返回 200 - ☐ 让 AI"列一下我最近发布的 5 篇文章",能列出来
- ☐ 让 AI 试着发一篇测试草稿,能在后台看到
最后一条别省。能读不代表能写,读操作的权限门槛通常比写低。
十、出问题了怎么回滚
这套方案改动面很小,回滚点也就几个:
| 想撤掉什么 | 怎么撤 |
|---|---|
| 应用密码功能 | 先在安全插件后台关掉对应选项,再删 mu-plugin 文件 |
| 博客发布通道 | 删掉本地那个进程的配置目录即可,服务器侧本就是零部署 |
| 两条服务器通道 | 在 DSM 容器管理器里停掉对应项目并删除 |
| 整个方案 | 卸载 Tailscale,删除两个容器 |
唯一的不可逆风险是那个自指操作——所以第七节的护栏不是可选项。
写在最后
这套方案最反直觉的一点是:最难的部分不是让 AI 连上来,而是让它别把自己弄下线。
技术上的东西——组网、协议、认证——都有现成的文档可查,照着做基本能通。真正会让人卡住的是那些"看起来像环境故障、实际是权限问题"的错觉,以及"这个操作指向它自己会怎样"这种没人会提前想到的场景。
我在搭的过程中犯的两个判断错误,都是因为跳过了对照实验就直接下结论。所以现在这套流程里最有用的一条规矩是:报故障之前,先确认自己这一侧没变。
下一步我打算把这套东西的说明书整理成一份可直接交给新会话的上下文包——毕竟 AI 每次开新对话都是零记忆的,环境事实得写在文件里,它才能立刻接手。

Comments NOTHING