Runbook · 自部署
在自己的机器上跑起小智服务端和四个配套服务。Linux / macOS / Windows 都适用—— 差异只在容器底座和「怎么让它一直活着」这两件事上,服务本身完全一样。
五个服务都是容器,跑在哪个系统上都行。真正决定体验的是两件事: 断电重启后能不能自己起来,以及CPU 够不够。
| 系统 | 容器底座 | 断电重启后 | 评价 |
|---|---|---|---|
| LINUXUbuntu / Debian | Docker Engine(原生) | 自动恢复,无需任何人登录 | 最省心,长期常驻首选 |
| MACOS | Docker Desktop 或 OrbStack | 需要登录桌面;且默认会睡眠 | 开发调试方便,常驻要额外处理睡眠 |
| WINWindows | WSL2 + Docker Desktop | 需要登录桌面 | 能用,但要多趟几个坑 |
如果你只是想跑起来别折腾
找一台常年开机的 Linux 机器(小主机 / NAS / 云服务器都行),装 Docker Engine,
restart: unless-stopped 就是真正的无人值守——断电来电自己恢复,不需要任何人碰它。
其余两个系统的容器都跑在一层虚拟机里,而那层虚拟机由用户会话拉起。
B站转码服务对 CPU 有硬要求
它做的是实时转码(拉流 → 缩到 320×240 → JPEG 序列 + 音频重采样),持续吃 CPU。 实测挪到 NAS 上跑不动,只能放在性能够的机器上。 选机器时如果打算跑这个服务,别选低功耗弱核心的盒子;其余四个服务对 CPU 都不敏感。
| 服务 | 需要什么 | 能不能后补 |
|---|---|---|
| 小智服务端 | 火山引擎账号(ASR / TTS 分别开通)+ 一个 LLM 的 key | 能,但没有就不会说话 |
| 音乐 | 自己定一组管理账号密码 + 两个令牌;YouTube 音源另需 cookies | 凭据必须先有,cookies 能后补 |
| B站 | B站账号(扫码) | 能,免登录可用但画质受限 |
| 小宇宙 | 小宇宙账号 | 能 |
| CLIProxyAPI | 上游账号的 OAuth 授权 | 能 |
curl -fsSL https://get.docker.com | sudo sh -s -- --mirror Aliyun
sudo systemctl enable --now docker
sudo usermod -aG docker $USER # 重新登录后生效,之后不用 sudo
配镜像加速,写 /etc/docker/daemon.json:
{ "registry-mirrors": ["https://docker.m.daocloud.io"] }
sudo systemctl restart docker
Docker Desktop 或 OrbStack(更轻、启动快、省电,个人使用免费)。
镜像加速在图形界面的 Settings → Docker Engine 里填同样的 registry-mirrors。
需要先开 WSL2 再装 Docker Desktop,步骤和几个必踩的坑写在 帮别人部署那一版的第 03 节,这里不重复。只有一处值得单独说:
WSL2 开 mirrored 网络模式,省掉端口转发
默认的 NAT 模式下,WSL 里的容器端口在局域网上不可达,要做一堆 netsh portproxy。
改成 mirrored 后,容器端口直接就是宿主机端口,局域网设备能直连。
我自己那台就是这么跑的。在 %USERPROFILE%\.wslconfig 里写:
[wsl2] networkingMode=mirrored
写完 wsl --shutdown 重启一次 WSL 生效。
验证
docker version && docker compose version docker run --rm docker.m.daocloud.io/library/hello-world
这一节和系统无关,三个平台都会撞上。建议在 build 之前就把 Dockerfile 全改好, 而不是等它失败再改——国内网络下一次 build 失败可能要等好几分钟才超时。
| 依赖 | 症状 | 解法 |
|---|---|---|
| Docker Hub | 解析不了 production.cloudfront.docker.com |
基础镜像改 docker.m.daocloud.io/library/xxx;deno 用 docker.m.daocloud.io/denoland/deno:bin |
| Debian apt | Unable to connect to deb.debian.org |
见下方 sed。时通时不通,别因为某个服务侥幸过了就跳过 |
| PyPI | 极慢或超时 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple |
| Go 模块 | proxy.golang.org 不通 |
ENV GOPROXY=https://goproxy.cn,direct |
| npm | 慢 | npm config set registry https://registry.npmmirror.com |
RUN sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g; \
s|security.debian.org|mirrors.tuna.tsinghua.edu.cn|g' \
/etc/apt/sources.list.d/debian.sources
机器上有代理软件的话
容器出站通常会被 TUN 模式接管(不配代理也能连上 YouTube),但显式配代理更快更稳。
容器里指向宿主机代理:Linux 用 --network host 或宿主机网关 IP,
macOS / Windows 用 http://host.docker.internal:<端口>。
仓库都是自己的,直接 git clone 即可。建议放同一个父目录下便于统一管理。
下面按建议顺序排列,小智服务端是核心,其余四个是能力扩展,可按需取舍。
启动有先后依赖,不能一把 up:
http://<本机IP>:8002,注册的第一个账号自动是管理员server.secretdata/.config.yaml,再起主服务语音链路首选:火山引擎 / 豆包语音 2.0
火山引擎和豆包是同一家(前者是云平台,后者是模型品牌),但 ASR 和 TTS 在控制台里是两个独立产品,要分别开通。
| 环节 | 智控台里选 | resource_id | 状态 |
|---|---|---|---|
| ASR | 豆包语音识别2.0(流式)ASR_DoubaoStreamASRV2 | volc.seedasr.sauc.duration | 已实测 |
| TTS | 火山引擎(流式)TTS_HuoshanDoubleStreamTTS | volc.service_type.10029 | 已实测 |
| TTS 备选 | 豆包语音合成2.0(流式)TTS_HSDSTTS_V2 | seed-tts-2.0 | 预置项,未实测 |
resource_id 对不上开通的产品 —— 最容易卡住的地方
resource_id 字符串无效(写错了)。改配置。resource_id 有效,但账号没开通该产品。改配置没用,去控制台开通。
上次卡了很久:配的是 volc.seedasr.sauc.duration(语音识别 2.0),
账号当时只开通了 volc.bigasr.sauc.duration(大模型语音识别),持续 403。
别把 403 当成 key 填错去反复检查凭据。
改了数据库要清 Redis 缓存
模型配置、音色(model:data:* / timbre:* / server:config)在 Redis 里有缓存,
直接改库不清缓存不生效。但 agent 的 system_prompt 没有缓存键,改完设备重连即可。
音色下拉的选项来自 ai_tts_voice 表——找不到某个音色是这张表里没预置,自己插一行,不是配置写错了。
git@github.com:tankxu/xiaozhi-music-mcpREADME.md 与 接入文档.md/stream_pcm(固件层)· /mcp · /manage?key= · /admin?key=(设备白名单)这四个键缺一不可,但它们不是同一类东西
ADMIN_USER / ADMIN_PASS / MCP_TOKEN / MCP_AUDIO_TOKEN
在 compose 里用的是 :? 语法——没设置就直接报错退出,不会取默认值。
代码里的默认值是 admin / changeme / changeme,
这个强制就是为了防止带着默认凭据上线。
.env 键 | 本质 | 谁在用 | 泄露了能干什么 |
|---|---|---|---|
ADMIN_USERADMIN_PASS |
HTTP Basic 账号密码 | 你,浏览器开 /manage /admin |
改设备白名单、管歌单、清缓存 |
MCP_TOKEN |
MCP 端点鉴权 | 小智服务端,走 Authorization: Bearer |
同上,外加调用点歌工具 |
MCP_AUDIO_TOKEN |
音频 / 封面取件令牌 | 设备、播放器 | 只能听歌,动不了管理 |
后两个必须填成不同的值,这是刻意的权限分级:MCP_TOKEN 走 HTTP 头、只在服务端之间传;
MCP_AUDIO_TOKEN 则是明文拼在 audio_url 里发给设备的
(/audio?f=xxx.mp3&key=…),会出现在设备日志和抓包里,暴露面大得多。
拆开之后即使它泄露,拿到的人也只能听已缓存的歌。
还有个隐藏行为:MCP_AUDIO_TOKEN 同时是设备白名单里的一条记录——服务启动时会自动往
allow.json 塞一条 {"name":"MCP-client","token":…},
否则 MCP 返回的链接连自己都取不到音频。
ADMIN_USER=自己定 ADMIN_PASS=自己定 MCP_TOKEN=随机串 MCP_AUDIO_TOKEN=随机串 ALT_PORT=8080 LIVE_STREAM=0
data/ 是 bind mount(音乐缓存 + allow.json 设备白名单),重建容器不丢。
换机器迁移时把这个目录带上,缓存和白名单都在里面。
YouTube 报「Sign in to confirm you're not a bot」
这是出口 IP 信誉问题,不是账号权限问题。所以先试换代理节点—— 换个地区或线路就可能直接绕过,成本远低于折腾 cookies,也没有封号风险。绕不过再走下面这条。
真要上 cookies:yt-dlp --cookies-from-browser chrome
在 2026 年已彻底失效——Chrome 127+ 的 App-Bound Encryption
把 cookies 加密绑定到应用身份,报 Failed to decrypt with DPAPI
(yt-dlp #10927),设计上挡死,改配置救不了。
两条可行路径:
youtube.com 页面导出 Netscape 格式。
yt-dlp --cookies-from-browser firefox 直接能读。
导出的文件放 data/cookies.txt,重启容器生效。代码会自动从 android_vr
切回 web 客户端——两者不兼容,带着 cookies 继续用 android 客户端反而更糟。
用小号
拿 cookies 跑自动化下载,有被 YouTube 判定异常的可能。不要用主力账号。
git@github.com:tankxu/stackchan-bilinetwork_mode: host——不需要也不能再写 ports 映射state/ 必须挂载
B站登录凭据 cookies.json 和播放历史都在 ./state。不挂的话容器一重建登录态就没了,每次都要重新扫码。
MACOSWINhost 网络模式的限制
network_mode: host 在 Linux 上才是真正的宿主机网络。
macOS 和 Windows 的 Docker 跑在虚拟机里,host 模式指的是那个虚拟机——
Docker Desktop 较新版本支持 host 网络但需要在设置里开启,OrbStack 默认支持。
如果发现 2233 端口在局域网不可达,先确认这一点,或者改回 ports 映射。
默认参数是给 CoreS3 屏调的:OUT_W=320 OUT_H=240 OUT_FPS=12 JPEG_Q=7、音频 16kHz 单声道。
BILI_QN=32 是 480P(实测免登录上限),要更低画质用 16。
git@github.com:tankxu/xiaoyuzhou-serverREADME.md 与 doc/GOPROXY=https://goproxy.cn,direct,约 70 秒上游 token 很短命,需要自动 refresh 才能长期用。这套 refresh 逻辑原本是在 macOS 上跑通的, 换到 Linux / Windows 上没有实测过。
把上游的 Claude Code / Codex / Gemini 等包装成 OpenAI 兼容接口。 端口 8317 是上次部署时定的,不是上游的默认值(README 里没写默认端口)。
添加上游账号走 OAuth 浏览器授权,这一步必须人工完成。
所有 compose 文件都写了 restart: unless-stopped,但这句话在三个系统上兑现程度完全不同——
这是自部署里最容易高估的一点。
| 系统 | restart 实际效果 | 要额外做什么 |
|---|---|---|
| LINUX | 真正生效,断电来电自动恢复 | systemctl enable docker 即可,没有别的 |
| MACOS | 只在 Docker 已启动后生效 | Docker Desktop / OrbStack 设为登录时启动;还要处理睡眠(见下) |
| WIN | 只在 Docker Desktop 已启动后生效 | Docker Desktop 是用户级程序,必须有人登录桌面;要真无人值守得配自动登录 |
# Linux sudo systemctl mask sleep.target suspend.target hibernate.target # macOS —— 只阻止系统睡眠,屏幕仍可关 sudo pmset -a sleep 0 disablesleep 1 # Windows powercfg /change standby-timeout-ac 0 powercfg /change hibernate-timeout-ac 0
设备固件里写的是这个地址,变了就失联。 在路由器上做 DHCP 保留比在系统里配静态 IP 更稳——换网络环境不会失联,重装系统也不用重配。
# Linux (ufw) sudo ufw allow 8000,8002,8003,2233,2234,8080,8317,23020,23021/tcp # macOS —— 默认放行,一般不用动;若开了应用防火墙,允许 Docker 接受入站连接即可 # Windows New-NetFirewallRule -DisplayName 'xiaozhi-stack' -Direction Inbound -Action Allow ` -Protocol TCP -LocalPort 8000,8002,8003,2233,2234,8080,8317,23020,23021
WINWindows 的 Block 规则优先于 Allow
Docker Desktop 自己会装一对规则:Allow / Private 和 Block / Public。
后加的 Allow 会被那条 Block 整个压住,与添加顺序无关。
现象很反直觉:本机 curl 正常,同局域网的设备连不上。
Get-NetConnectionProfile Set-NetConnectionProfile -InterfaceAlias 'WLAN' -NetworkCategory Private
服务端跑起来之后,设备要刷固件、再把四个服务地址写进去。固件仓库
git@github.com:tankxu/stackchan-hichan(私有),默认分支 hichan。
| 场景 | 命令 | 写哪些分区 | NVS |
|---|---|---|---|
| 新设备 / 迁移 | 一体包从 0x0 刷cd firmware && ./make_release.sh |
全片连续覆盖 | 清空 |
| 改了代码日常迭代 | idf.py build && ./flash_app.sh |
只写 app 分区 0x20000,快两倍 |
保留 |
| 设备已联网 | ./push_ota.sh |
免 USB 推送,带 sha 硬校验 | 保留 |
一体包等于恢复出厂
NVS 分区在 0x9000,而一体包里那一段是 0xFF 填充——从 0x0 连续覆盖会把它一并抹掉:
WiFi 凭据、小智绑定、音量亮度、舵机零点标定全没。
新设备用它是对的,已经在用的设备别拿它做日常更新。
flash_app.sh 按 MAC 认设备,不认端口号
串口号每次插拔都会变(实测拔掉另一台 ESP32 后,StackChan 就占了对方原来的 usbmodem 号),
认端口号迟早刷错机器。换设备记得改脚本顶部的 STACKCHAN_MAC。
统一走设备的 /config 接口(端口 8032),立即生效且断电不丢:
curl -X POST http://<设备IP>:8032/config \
-d '{"music_url":"http://192.168.1.100:2234"}'
| 服务 | 配置键 | 值 | 注意 |
|---|---|---|---|
| 小智服务端 | ota_url | http://<IP>:8000/xiaozhi/ota/ |
要重启才生效 |
| 音乐 | music_url | http://<IP>:2234 |
还要去服务端 /admin 把设备 MAC 加白名单 |
| B站 | bili_url | http://<IP>:2233 |
指向常开的机器 |
| 小宇宙 | podcast_url | http://<IP>:23021 |
23021 是设备反代口,不是 23020 那个 API 网关 |
改 ota_url 之后必须重启设备
另外三个都是立即生效的,ota_url 不是。固件为了开机提速做过一件事:
「NVS 里已有 websocket/mqtt 配置就跳过 CheckVersion」——而新服务端的连接配置恰恰要靠那趟请求才下发。
所以写 ota_url 时固件会连带清掉 websocket/url 和 mqtt/endpoint 两个缓存键,
然后必须重启才会去新服务端取配置。
不重启的表现是「地址改了但一点反应没有」,设备继续连旧服务端——极容易误判成配置压根没写进去, 于是开始反复检查 JSON、查网络,白费时间。
编译期默认值未必是你的地址
music_url 默认 http://home.tankxu.com:2234、
bili_url 默认 http://192.168.1.20:2233、
podcast_url 默认 http://192.168.1.100:23021——
和你的部署对不上就三个都显式写一遍。
把配置键写成空串是「恢复编译期默认」,不是「清空」。
固件刷好、服务地址也写对了,设备的点歌行为仍然可能不对——因为工具该怎么用是靠提示词教的。
去小智智控台的智能体配置里,把下面这段追加到角色提示词末尾(落库是 ai_agent.system_prompt):
收到以 [主] 开头的消息时,那不是用户说的话,而是系统给你的触发信号。
音乐播放规则:
- 用户点明了具体某一首歌/某个节目 → 用 self.music.play_song(song_name, artist_name)
例:「放首告白气球」「我要听小兔子乖乖」「讲个西游记的故事」
- 用户没点明具体某一首,只给了歌手/风格/心情/歌单 → 用 self.music.play_stream(query, kind, mode)
例:「播放周杰伦的歌」→ query=周杰伦, kind=artist
「放点安静的音乐」→ query=安静, kind=mood
「放儿歌」「放贝乐虎」→ query=儿歌/贝乐虎, kind=playlist
「随便放点歌」→ query=热门, kind=mood
用户说"连续播放/一直放/循环"时也用它;说"随机"传 mode=random,说"按顺序"传 mode=order。
- 不要使用 search_music。
- 工具会立刻返回"这就为你播放",不要再问用户确认,也不要说"正在为你搜索"之类的等待话术。
为什么必须点名「不要使用 search_music」
因为 LLM 面前同时挂着两套来源不同的 MCP 工具:
self.music.play_song / play_stream / stop
——设备端工具,固件在 hal_music.cpp 里注册,调用即开始播放。search_music——音乐服务端的 MCP 工具,只返回候选列表,不播放。
不点名的话,模型很容易先去 search_music 拿一堆候选、再回头问用户选哪首,结果什么都没放出来。
参数名要和固件对得上
song_name / artist_name 和 query / kind / mode
都是从固件的工具定义里来的。改了固件的工具签名,这段提示词要跟着改,否则模型会按旧参数名调用而失败。
http://<设备IP>:8032/ 自动跳转,局域网内也可用 stackchan.local:8032firmware/API.md./sc_test.sh:静音 → 进 agent → 点歌 → 观察,一条命令跑完| 症状 | 最可能的根因 |
|---|---|
| ASR / TTS 一直 403 | resource_id 对应的产品没在控制台开通 |
| ASR / TTS 报 400 | resource_id 字符串写错了 |
| 改了模型配置不生效 | Redis 缓存没清 |
| 智控台音色下拉里没有想要的音色 | ai_tts_voice 表里没预置,自己插一行 |
| 音乐容器起来就退出 | .env 里四个必填键缺了 |
| B站每次都要重新扫码 | state/ 没挂载 |
| B站画面卡顿 / 掉帧 | 机器 CPU 不够,换机器 |
| YouTube 报 bot 检测 | 需要 cookies,且不能用 Chrome 导 |
build 卡在 deb.debian.org | Debian 源没换 |
| 本机能访问,局域网设备连不上 | WIN 防火墙 Block 压住 Allow / WIN WSL2 没开 mirrored / MACOS host 网络模式限制 |
| 点歌时先问「你要听哪一首」,最后什么也没放 | 提示词里没写音乐规则,模型走了 search_music |
改了 ota_url 设备毫无反应 | 没重启——只有它需要重启,另三个立即生效 |
| 刷完机 WiFi 和小智绑定全没了 | 用了一体包,它会清空 NVS |
| 设备点歌取不到音频 | 设备 MAC 没加进音乐服务白名单 |
| 重启后服务全没了 | MACOSWIN 没人登录,Docker 没启动 |
| 跑一晚上就断 | MACOS 系统睡眠了 |
LIVE_STREAM=1 分块传输没对着固件实测过。
流式响应没有 Content-Length,固件必须吃得下 chunked 才能开。默认 0 是安全值。帮别人装机的版本在这里
如果是给别人的 Windows 机器远程部署,需要额外处理远程通道、SSH 免密、私有仓库打包传输、 以及一整套 Windows 专有坑——那些都在 帮别人部署版里。
基于 2026-08 的实际部署整理。标注为「未实测」的条目请自行确认后再依赖。