Runbook · 自部署

小智全栈自部署手册

在自己的机器上跑起小智服务端和四个配套服务。Linux / macOS / Windows 都适用—— 差异只在容器底座和「怎么让它一直活着」这两件事上,服务本身完全一样。

适用系统
Linux · macOS · Windows
服务数量
5 个(可按需取舍)
共同底座
Docker + Compose
上次实测
2026-08
00

先选机器

五个服务都是容器,跑在哪个系统上都行。真正决定体验的是两件事: 断电重启后能不能自己起来,以及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 授权
01

装 Docker

LINUXUbuntu / Debian

官方脚本,国内加 --mirror Aliyun
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

MACOS

Docker Desktop 或 OrbStack(更轻、启动快、省电,个人使用免费)。 镜像加速在图形界面的 Settings → Docker Engine 里填同样的 registry-mirrors

WINWindows

需要先开 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
02

国内网络换源

这一节和系统无关,三个平台都会撞上。建议在 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
Debian 换源 —— 新版是 debian.sources 不是 sources.list
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:<端口>

03

五个服务

仓库都是自己的,直接 git clone 即可。建议放同一个父目录下便于统一管理。 下面按建议顺序排列,小智服务端是核心,其余四个是能力扩展,可按需取舍

小智服务端(全模块)

8000 ws 8002 智控台 8003 视觉
来源
xinnan-tech/xiaozhi-esp32-server
官方文档
全模块部署 Deployment_all.md · 精简版 Deployment.md · FAQ
组成
Python 主服务 + Java manager-api + Vue manager-web + MySQL + Redis

启动有先后依赖,不能一把 up

  1. 先起 MySQL + Redis + manager-api + manager-web
  2. 浏览器开 http://<本机IP>:8002注册的第一个账号自动是管理员
  3. 在智控台「参数管理」里取出 server.secret
  4. 填进主服务的 data/.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 对不上开通的产品 —— 最容易卡住的地方

  • HTTP 400 —— resource_id 字符串无效(写错了)。改配置。
  • HTTP 403 —— 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 表——找不到某个音色是这张表里没预置,自己插一行,不是配置写错了。

音乐服务 xiaozhi-music-mcp

2234 8080 备用
来源
git@github.com:tankxu/xiaozhi-music-mcp
文档
仓库内 README.md接入文档.md
音源
网易云 / YouTube / 哔哩哔哩,YTMusic 为主、B站兜底
关键端点
/stream_pcm(固件层)· /mcp · /manage?key= · /admin?key=(设备白名单)

这四个键缺一不可,但它们不是同一类东西

ADMIN_USER / ADMIN_PASS / MCP_TOKEN / MCP_AUDIO_TOKEN 在 compose 里用的是 :? 语法——没设置就直接报错退出,不会取默认值。 代码里的默认值是 admin / changeme / changeme, 这个强制就是为了防止带着默认凭据上线。

.env本质谁在用泄露了能干什么
ADMIN_USER
ADMIN_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 返回的链接连自己都取不到音频。

.env
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 DPAPIyt-dlp #10927),设计上挡死,改配置救不了。

两条可行路径:

  • 浏览器扩展「Get cookies.txt LOCALLY」——已实测有效,Chrome 上就能用。 关键在于扩展跑在浏览器内部,不受 App-Bound Encryption 限制。 登录 YouTube 后在 youtube.com 页面导出 Netscape 格式
  • 换用 Firefox——它的 cookies 是明文 SQLite, yt-dlp --cookies-from-browser firefox 直接能读。

导出的文件放 data/cookies.txt,重启容器生效。代码会自动从 android_vr 切回 web 客户端——两者不兼容,带着 cookies 继续用 android 客户端反而更糟

用小号

拿 cookies 跑自动化下载,有被 YouTube 判定异常的可能。不要用主力账号。

B站转码 stackchan-bili

2233 host 网络
来源
git@github.com:tankxu/stackchan-bili
网络
network_mode: host——不需要也不能再写 ports 映射
机器要求
吃 CPU,弱机器跑不动(见 00 节)

state/ 必须挂载

B站登录凭据 cookies.json 和播放历史都在 ./state。不挂的话容器一重建登录态就没了,每次都要重新扫码。

MACOSWINhost 网络模式的限制

network_mode: hostLinux 上才是真正的宿主机网络。 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。

小宇宙 xiaoyuzhou-server

23020 23021
来源
git@github.com:tankxu/xiaoyuzhou-server
文档
仓库内 README.mddoc/
技术栈
Go —— build 需要 GOPROXY=https://goproxy.cn,direct,约 70 秒

上游 token 很短命,需要自动 refresh 才能长期用。这套 refresh 逻辑原本是在 macOS 上跑通的, 换到 Linux / Windows 上没有实测过

CLIProxyAPI

8317
来源
router-for-me/CLIProxyAPI
官方文档
help.router-for.me · 管理 API

把上游的 Claude Code / Codex / Gemini 等包装成 OpenAI 兼容接口。 端口 8317 是上次部署时定的,不是上游的默认值(README 里没写默认端口)。

添加上游账号走 OAuth 浏览器授权,这一步必须人工完成。

04

让它一直活着

所有 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

固定 IP

设备固件里写的是这个地址,变了就失联。 在路由器上做 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 / PrivateBlock / Public。 后加的 Allow 会被那条 Block 整个压住,与添加顺序无关。 现象很反直觉:本机 curl 正常,同局域网的设备连不上。

Get-NetConnectionProfile
Set-NetConnectionProfile -InterfaceAlias 'WLAN' -NetworkCategory Private
05

设备侧:StackChan 刷机与配置

服务端跑起来之后,设备要刷固件、再把四个服务地址写进去。固件仓库 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_urlhttp://<IP>:8000/xiaozhi/ota/ 要重启才生效
音乐music_urlhttp://<IP>:2234 还要去服务端 /admin 把设备 MAC 加白名单
B站bili_urlhttp://<IP>:2233 指向常开的机器
小宇宙podcast_urlhttp://<IP>:23021 23021 是设备反代口,不是 23020 那个 API 网关

ota_url 之后必须重启设备

另外三个都是立即生效的,ota_url 不是。固件为了开机提速做过一件事: 「NVS 里已有 websocket/mqtt 配置就跳过 CheckVersion」——而新服务端的连接配置恰恰要靠那趟请求才下发。 所以写 ota_url 时固件会连带清掉 websocket/urlmqtt/endpoint 两个缓存键, 然后必须重启才会去新服务端取配置。

不重启的表现是「地址改了但一点反应没有」,设备继续连旧服务端——极容易误判成配置压根没写进去, 于是开始反复检查 JSON、查网络,白费时间。

编译期默认值未必是你的地址

music_url 默认 http://home.tankxu.com:2234bili_url 默认 http://192.168.1.20:2233podcast_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_namequery / kind / mode 都是从固件的工具定义里来的。改了固件的工具签名,这段提示词要跟着改,否则模型会按旧参数名调用而失败。

控制台与文档

06

症状对照表

症状最可能的根因
ASR / TTS 一直 403resource_id 对应的产品没在控制台开通
ASR / TTS 报 400resource_id 字符串写错了
改了模型配置不生效Redis 缓存没清
智控台音色下拉里没有想要的音色ai_tts_voice 表里没预置,自己插一行
音乐容器起来就退出.env 里四个必填键缺了
B站每次都要重新扫码state/ 没挂载
B站画面卡顿 / 掉帧机器 CPU 不够,换机器
YouTube 报 bot 检测需要 cookies,且不能用 Chrome 导
build 卡在 deb.debian.orgDebian 源没换
本机能访问,局域网设备连不上WIN 防火墙 Block 压住 Allow / WIN WSL2 没开 mirrored / MACOS host 网络模式限制
点歌时先问「你要听哪一首」,最后什么也没放提示词里没写音乐规则,模型走了 search_music
改了 ota_url 设备毫无反应没重启——只有它需要重启,另三个立即生效
刷完机 WiFi 和小智绑定全没了用了一体包,它会清空 NVS
设备点歌取不到音频设备 MAC 没加进音乐服务白名单
重启后服务全没了MACOSWIN 没人登录,Docker 没启动
跑一晚上就断MACOS 系统睡眠了
07

这份文档没覆盖到的

帮别人装机的版本在这里

如果是给别人的 Windows 机器远程部署,需要额外处理远程通道、SSH 免密、私有仓库打包传输、 以及一整套 Windows 专有坑——那些都在 帮别人部署版里。

基于 2026-08 的实际部署整理。标注为「未实测」的条目请自行确认后再依赖。