折腾记:在 NAS 上部署属于自己的 AI 助手 Running My Own AI Assistant on a NAS

想法很简单:在飞书或者 Telegram 发一条消息,就能得到 AI 回应。带历史记录,认识我这个人,随时随地能用。

OpenClaw 是最近很火的开源 AI 网关,支持对接各种聊天渠道和大模型。群晖 NAS 24 小时开机、功耗低,是天然的部署平台。于是开始折腾。

第一个坑:权限

拉取镜像、创建目录、启动容器,流程走得很顺。容器一启动,日志里就报错了:

EACCES: permission denied, open '/home/node/.openclaw/openclaw.json'

容器内进程以 uid 1000 运行,我用 root 创建的目录,读不了。一条 chown 命令解决。

但这个问题后来像幽灵一样反复出现——每次用脚本修改配置文件,文件的 owner 都会悄悄变回 root,下次重启容器就又报权限错误。最后变成了一条操作铁律:每次修改配置,必须执行一遍 chown 1000:1000。

本地模型的幻想

一开始想省钱,打算跑本地 Ollama 模型。下载了 qwen2.5:7b,发了第一条消息,等待回应。

等了很久。

DS923+ 的 CPU 是 AMD Ryzen R1600,纯靠 CPU 推理 7B 模型,速度大约 3-5 个 token 每秒。回答一个简单问题需要等一两分钟。

换成 1.5B 的小模型,速度快了,但质量差得离谱。

更关键的是,OpenClaw 有一个隐藏限制:模型的 context window 必须大于等于 32000 tokens,否则它会静默拒绝所有请求,连报错都不给。我排查了很久,翻了无数遍日志,才在某个角落找到这条说明。

本地模型的念头就此打消。改用 DeepSeek API,一切顺了。结论也很简单:NAS 跑 AI 推理,需要一台有独立 GPU 的机器。

Docker 网络的问题

接入 API 之后,消息发出去了,但没有回应。查日志,报的是:

Proxy headers detected from untrusted address

原因在 Docker 网络。每次执行 docker compose down 再 up,Docker 会分配一个新的内部网段——172.19、172.22、172.28,一直往上涨。而 OpenClaw 的 trustedProxies 配置只信任列表里的 IP 段,之前写死的网段在容器重建后失效了,消息被悄悄丢弃。

解决方法是把 trustedProxies 设置为 172.16.0.0/12,一个子网掩码覆盖 Docker 所有可能使用的网段,永远不需要再改。

飞书出乎意料地顺

以为接入飞书会很复杂。结果是整个部署过程里最顺的部分。

飞书的智能机器人支持 WebSocket 长连接——Bot 主动连接飞书服务器,不需要飞书来访问你的机器,不需要公网 IP,不需要端口转发。对于部署在内网 NAS 上的服务来说,几乎是量身定制的方案。

填好 AppID 和 AppSecret,重启容器,日志里出现了一行:

[feishu] WebSocket client started

在飞书里发了第一条消息,它回了。

一个已知 Bug

系统跑起来之后,试着通过 Web UI 把模型从 DeepSeek 切换到 Claude。报错:

model not allowed: anthropic/deepseek-chat

UI 把 deepseek-chat 误判成了 anthropic provider 下面的模型。这是 OpenClaw 的一个已知 Bug,GitHub 上有 issue,但没有真正修复到最新稳定版里。

绕过方法:直接修改配置文件里的 primary 字段,然后重启容器。但更实用的方式是给模型配置别名——比如给 anthropic/claude-sonnet-4-20250514 配置别名 claude,之后在飞书对话框里输入 /claude 就能切换到对应模型,不需要动配置文件。

最终状态

DeepSeek V3 作主力,Claude Sonnet 作备用。飞书能用,Telegram 能用,Web UI 能用,会话历史同步。

折腾下来,这次踩的坑有一个共同规律:表象和根因之间总是隔着好几层。消息发不出去不一定是网络问题,容器起不来不一定是配置写错了,UI 报错不一定是配置不对。遇到问题,直接看错误日志比搜教程更有效。

关于本地模型,我没有放弃这个想法。等以后有一台带 GPU 的机器,再接进来。那时候才算真正不依赖任何外部服务。

但现在这个状态,已经够用了。

The idea was simple: send a message in Lark or Telegram and get an AI response. With conversation history, some awareness of who I am, available anywhere.

OpenClaw is a popular open-source AI gateway that connects to various chat platforms and language models. My Synology NAS runs 24/7 at low power — a natural deployment target. So I started tinkering.

First Problem: Permissions

Pulled the image, created directories, started the container. The logs immediately threw an error:

EACCES: permission denied, open '/home/node/.openclaw/openclaw.json'

The container process runs as uid 1000, but I’d created the config directory as root. A chown command fixed it.

But the problem kept coming back. Every time I modified the config file with a script, the file ownership would silently revert to root, and the next container restart would fail again. Eventually it became an ironclad rule: after every config change, run chown 1000:1000.

The Local Model Illusion

I wanted to save money and run a local Ollama model. Downloaded qwen2.5:7b, sent the first message, waited for a response.

Waited a long time.

The DS923+ has an AMD Ryzen R1600 — dual-core, four-thread. Running a 7B model on CPU alone gives about 3-5 tokens per second. A simple question takes one to two minutes.

Switched to a 1.5B model. Faster, but the quality was terrible.

More critically, OpenClaw has a hidden hard requirement: the model’s context window must be at least 32,000 tokens. If it isn’t, OpenClaw silently rejects all requests — no error message, nothing. I spent a long time digging through logs before finding this mentioned in an obscure corner of the documentation.

The local model idea was dropped. Switched to DeepSeek’s API, and everything smoothed out. The conclusion is simple: running AI inference on a NAS requires a machine with a dedicated GPU.

Docker Networking

After connecting the API, messages were going out but getting no response. The logs showed:

Proxy headers detected from untrusted address

The issue was Docker networking. Every time I ran docker compose down and up again, Docker assigned a new internal subnet — 172.19 today, 172.22 tomorrow, 172.28 the day after. OpenClaw’s trustedProxies config only trusted specific IP ranges, and the new subnet wasn’t on the list. Messages were being silently discarded.

The fix: set trustedProxies to 172.16.0.0/12, a subnet mask that covers every range Docker might ever use. Never needs changing again.

Lark Was Surprisingly Easy

I expected the Lark integration to be painful. It turned out to be the smoothest part of the whole deployment.

Lark’s bot platform supports WebSocket long connections — the bot reaches out to Lark’s servers, not the other way around. No public IP required, no port forwarding. For a service running on a home NAS behind a router, this is almost perfectly designed.

Filled in the AppID and AppSecret, restarted the container, and the logs showed:

[feishu] WebSocket client started

Sent the first message from Lark. It replied.

A Known Bug

Once the system was running, I tried switching the model from DeepSeek to Claude through the Web UI. Error:

model not allowed: anthropic/deepseek-chat

The UI was misidentifying deepseek-chat as a model under the anthropic provider. This is a known OpenClaw bug — there’s an open issue on GitHub, but it hasn’t actually made it into the latest stable release.

The workaround: edit the primary field directly in the config file and restart the container. More practically, you can set up model aliases — give anthropic/claude-sonnet-4-20250514 an alias of claude, and then typing /claude in the Lark chat switches to that model on the fly, no config file changes needed.

Current State

DeepSeek V3 as the primary model, Claude Sonnet as fallback. Works in Lark, Telegram, and the Web UI. Conversation history syncs across channels.

Looking back, the problems I hit all followed the same pattern: the symptom and the root cause are always separated by several layers. Messages not going through doesn’t mean it’s a network problem. Container failing to start doesn’t mean the config is wrong. A UI error doesn’t mean the configuration is bad. When something breaks, reading the error logs directly is more effective than searching for tutorials.

I haven’t given up on local models. When I eventually have a machine with a dedicated GPU, I’ll connect it. That’s when it’ll truly be independent of any external service.

For now, though — it works.

上一篇Previous 我的个人网站是怎么跑起来的 How My Personal Website Actually Runs 下一篇Next 油菜花开的时候 When the Rapeseed Flowers Bloom ← 返回随笔列表← Back to Essays

留言与评论Comments

回到顶部Back to top