Runbook · 2026-08 实测
把小智服务端和四个配套服务,远程部署到一台国内网络环境下的 Windows 机器。 这份文档写给执行部署的 agent——每一步都给到可直接执行的命令、验证方式,以及上一次实际踩到的坑。
你要操作的是别人的电脑。机主已经授权,但这不代表可以随便动——下面三条是硬规矩,上一次部署时每一条都付出过代价。
规矩一 · 不要把委托人的账号发过去
音乐服务的 API key、小智服务端的 LLM/TTS 凭据、B站和小宇宙的登录态,一律让机主自己注册、自己扫码。 你的角色是把服务跑起来并留好填凭据的入口,不是把自己这边的账号复制一份过去。
规矩二 · 打包前必须读 .gitignore,不要凭记忆列排除项
三个私有服务要从委托人的机器打包传过去。上次我凭记忆写 --exclude,
漏掉了 app/allow.json(设备 MAC 白名单),把它一起打进包传到了对方机器。
正确做法:tar 前先 cat .gitignore,按文件里写的来;打完包先列一遍包内容确认没有 state/cookies/凭据,再传。
规矩三 · 私有仓库 clone 不了,只能打包传
下面五个服务里有三个是私有仓库(git@github.com:tankxu/…),对方机器没有权限。
唯一的路径是在委托人的机器上 tar → scp → 对方机器解压。
公开的那两个(小智服务端、CLIProxyAPI)可以直接在对方机器上拉。
| 要什么 | 用途 | 拿不到的后果 |
|---|---|---|
| Tailscale 上的 IP | 建立 SSH 通道 | 只能靠 ToDesk 手动点,效率差一个数量级 |
| Windows 用户名 | 配置计划任务的 -UserId | Docker 拉镜像那一步会卡死 |
| 局域网 IP(建议固定) | 设备要连的地址 | DHCP 一换设备就失联 |
| 是否装了代理软件 | 排查网络问题的前提 | 会把代理造成的现象错怪到别的地方 |
| 大模型 / TTS 的 key | 小智服务端 | 服务能起但不出声 |
机器硬指标
上次那台是 Win10 专业版 22H2 / 16 核 / 32G / C 盘剩 125G,跑满五个服务无压力。 家庭版装不了 Hyper-V,但 WSL2 可以,所以家庭版也能跑——只是 Docker Desktop 只能走 WSL2 后端。 磁盘至少留 40G。
先把 SSH 打通,后面所有操作都在 SSH 里做。ToDesk 只在两个场景用:装 Tailscale 之前的引导,和后面需要人在桌面上点确认的时刻(Docker Desktop 首次启动、B站扫码登录)。
这一步必须机主自己在桌面上做(要登录账号)。装完让他把 tailscale ip -4 的结果发给你。
Add-WindowsCapability
官方那条命令在国内网络下会报成功但文件根本没落盘:它返回 RestartNeeded: True,
重启后 sshd 服务依然不存在。原因是组件要从 Windows Update 拉,而那个源在国内不通。
直接装 GitHub 的离线包:
# 从 github.com/PowerShell/Win32-OpenSSH/releases 下载 OpenSSH-Win64.zip Expand-Archive -Path .\OpenSSH-Win64.zip -DestinationPath 'C:\Program Files' -Force cd 'C:\Program Files\OpenSSH-Win64' # 执行策略会挡住安装脚本,这样绕过(只影响当前进程) Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force .\install-sshd.ps1 Start-Service sshd Set-Service sshd -StartupType Automatic New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' ` -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
administrators_authorized_keys
Windows 的 OpenSSH 对管理员账号不读 ~/.ssh/authorized_keys,
读的是 C:\ProgramData\ssh\administrators_authorized_keys。放错位置的表现是密钥认证静默失败、继续问密码。
权限收紧必须用 SID 而不是组名——中文版 Windows 上写 Administrators 会失败,
因为本地化后组名是「管理员」。
$k = 'C:\ProgramData\ssh\administrators_authorized_keys' Set-Content -Path $k -Value 'ssh-ed25519 AAAA... your-key' -Encoding ascii icacls $k /inheritance:r icacls $k /grant '*S-1-5-32-544:F' # Administrators icacls $k /grant '*S-1-5-18:F' # SYSTEM Restart-Service sshd
验证
从你这边连过去,应当不问密码直接进,并且 IsAdmin 为 True:
ssh w@100.x.x.x "powershell -c \"([Security.Principal.WindowsPrincipal] \
[Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators')\""
这条通道重启后依然可用
sshd 和 tailscaled 都是系统服务,开机自启,不需要有人登录 Windows 桌面。
但下一节的 Docker Desktop 是用户级程序——机器重启后必须有人登录桌面,容器才会起来。这个区别后面会反复咬人。
这一节放在装东西之前,因为它们是整个部署里最耗时间的部分。上次五个服务里,每一个都在其中某一条上卡过。 先读完再动手,能省掉大半天。
任何碰 registry 的命令(docker pull、docker compose build)在 SSH 会话里都会报:
error getting credentials - err: exit status 1, out: `A specified logon session does not exist.`
根因是 Docker Desktop 的凭据助手依赖 Windows 的交互式登录会话,
而 SSH 是非交互式会话,拿不到那个 token。这是设计限制,不是配置问题——
DOCKER_CONFIG、--config 指向干净目录、删掉 credsStore 字段,这三种办法我都试了,全部无效。
唯一可行的解法:注册一个 LogonType Interactive 的计划任务,把命令推回用户会话执行。
$dir = 'C:\xiaozhi-music-mcp'
$log = "$dir\build.log"
$act = New-ScheduledTaskAction -Execute 'cmd.exe' `
-Argument "/c cd /d $dir && docker compose up -d --build > $log 2>&1"
$prin = New-ScheduledTaskPrincipal -UserId 'DESKTOP-XXXX\w' -LogonType Interactive
Register-ScheduledTask -TaskName 'dc-build' -Action $act -Principal $prin -Force
Start-ScheduledTask -TaskName 'dc-build'
计划任务是异步的,你拿不到退出码
Start-ScheduledTask 立刻返回,命令在后台跑。判断进度要靠轮询副作用:
docker images 里镜像有没有出现、build.log 的尾部、docker ps 的状态。
写轮询脚本时注意不要用宽泛的 grep 判成功——上次我用 grep ota 结果匹配到了日志里的 Total,
用 grep TTS 匹配到的其实是一条 TTS 失败日志,两次都误报了成功。
匹配要够具体(POST /xiaozhi/ota/ 这种),并且收集一个时间窗口的完整日志再判断,不要一命中就 break。
Docker Desktop 安装时会自己写入一对规则:Allow / Private 和 Block / Public。
你后加的「Profile=Any 放行 8000」会被那条 Block 整个压住——因为 Windows 防火墙里 Block 永远赢 Allow,与添加顺序无关。
现象极其反直觉
你从 Tailscale 连得上,同一个局域网里的设备反而连不上。 因为 Tailscale 虚拟网卡被识别成 Private(走 Allow),而 WLAN 被识别成 Public(撞上 Block)。 排查时很容易怀疑到端口、容器、路由上去,实际上是网络配置文件的问题。
Get-NetConnectionProfile # 先看当前是 Public 还是 Private
Set-NetConnectionProfile -InterfaceAlias 'WLAN' -NetworkCategory Private
这比删掉 Block 规则更安全:笔记本带出门连公共 WiFi 时,那条 Public 的 Block 会继续保护。 另外仍然要为服务端口加放行规则:
New-NetFirewallRule -DisplayName 'xiaozhi-stack' -Direction Inbound -Action Allow ` -Protocol TCP -LocalPort 8000,8002,8003,2233,2234,8080,8317,23020,23021
| 操作 | 默认行为 | 后果 |
|---|---|---|
Get-Content -Raw |
按 ANSI 读 | UTF-8 中文注释读成乱码,并且吃掉换行——compose 文件 88 行变 85 行,YAML 直接塌掉 |
> 重定向 |
写 UTF-16LE | JSON 文件首字节是 0xFF,任何解析器都读不了 |
| 数组里拼引号 | [char]34 被当独立元素 |
生成的字符串结构错乱 |
$enc = New-Object System.Text.UTF8Encoding $false # $false = 不带 BOM $txt = [System.IO.File]::ReadAllText($path, $enc) [System.IO.File]::WriteAllText($path, $txt, $enc) # 拼引号用反引号转义,不要用 [char]34 $s = "key=`"value`""
wsl --install 在国内会卡在连 Microsoft Store,不要用它。
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
必须真重启,不能跳过。SSH 会自己恢复(sshd 是系统服务)。
ssh w@100.x.x.x "shutdown /r /t 5"
这里我卡了很久
网上到处都是 wsl_update_x64.msi(约 16MB)。它只更新内核,补不上 WSL 主体版本。
装完的表现是:wsl --version 报「不支持的命令」(说明还是系统自带的旧 inbox WSL),
Docker Desktop 29.x 认不出来,引擎起不来、界面一直转圈。
正确的是从 github.com/microsoft/WSL/releases
下 wsl.<版本>.x64.msi,约 250MB。上次装的是 2.7.11.0,装完内核到 6.18.33.2,Docker 引擎 15 秒就绪。
msiexec /i wsl.2.7.11.0.x64.msi /quiet /norestart
wsl --version # 必须能打印出版本号,否则就是装错了包
wsl --set-default-version 2
安装包从官网下(这个域名国内通)。装完需要机主在桌面上点一次同意条款并保持登录,
之后你才能在 SSH 里用 docker。
Start-Process -Wait -FilePath .\'Docker Desktop Installer.exe' ` -ArgumentList 'install','--quiet','--accept-license','--backend=wsl-2'
写 %USERPROFILE%\.docker\daemon.json,注意用上一节的 .NET 写法避免 UTF-16 问题:
{
"registry-mirrors": ["https://docker.m.daocloud.io"]
}
验证
ssh w@100.x.x.x "docker version && docker run --rm docker.m.daocloud.io/library/hello-world"
若 hello-world 那条报 logon session 错误,说明必须走计划任务(坑一),这是预期内的,不是装错了。
每一个服务都在这张表里的某一行卡过。建议在 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。时通时不通——上次 B站服务侥幸过了,音乐服务就卡死,所以不要因为一个服务通了就跳过这步 |
| PyPI | 极慢或超时 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple |
| Go 模块 | proxy.golang.org 不通 |
Dockerfile 里加 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
如果机器上装了代理软件
上次那台装了 0dcloud(Meta Tunnel,mihomo 内核)。TUN 模式下走 fake-ip,
所有域名解析到 198.18.x.x,HTTP 代理口在 127.0.0.1:17891。
容器出站也被它接管(不配代理也能连上 YouTube),但显式配 http://host.docker.internal:17891 更快更稳。
它的路由表是干净的(192.168.1.0/24 正常走 WLAN)——排查局域网连不通时不要错怪代理, 先按坑二查防火墙。
目录统一放 C 盘根下,凭据各自存在同目录的 KEYS.txt 里,方便机主自己查改。
下面按建议的部署顺序排列——小智服务端是核心,其余四个是它的能力扩展,可按需取舍。
C:\xiaozhi-server # 小智服务端(全模块) C:\xiaozhi-music-mcp # 音乐 C:\stackchan-bili # B站转码 C:\xiaoyuzhou-server # 小宇宙播客 C:\cli-proxy-api # CLIProxyAPI
# 1. 先看清楚要排除什么 —— 不要凭记忆写 cat .gitignore # 2. 打包(--exclude 按 .gitignore 来,state/data/cookies 一律不带) tar --exclude='.git' --exclude='state' --exclude='data' \ --exclude='__pycache__' --exclude='.env' \ -czf /tmp/svc.tgz -C /path/to/repo . # 3. 传之前先列一遍包内容,确认没有凭据混进去 tar -tzf /tmp/svc.tgz | grep -iE 'cookie|allow|token|secret|\.env|state/' # 4. 传过去解压 scp /tmp/svc.tgz w@100.x.x.x:/C:/tmp/ ssh w@100.x.x.x "tar -xzf C:/tmp/svc.tgz -C C:/xiaozhi-music-mcp"
C:\xiaozhi-server启动顺序有依赖,不能一把 up:
http://<局域网IP>:8002,注册的第一个账号自动是管理员server.secretdata/.config.yaml,再起主服务改了数据库要清 Redis 缓存
模型配置、音色(model:data:* / timbre:* / server:config)都在 Redis 里有缓存,
直接改库不清缓存不生效。但 agent 的 system_prompt 没有缓存键,改完设备重连即可。
语音链路首选:火山引擎 / 豆包语音 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 |
系统预置项,未实测 |
两者都填 appid + access_token,TTS 的 ws_url 保持预置值
wss://openspeech.bytedance.com/api/v3/tts/bidirection 即可。
LLM 与语音链路互相独立,上次配的是 DeepSeek。
最容易卡住的地方:resource_id 和实际开通的产品对不上
resource_id 决定你调的是哪个产品,它必须和控制台里真正开通了的那个对应。
两种报错方向完全相反,先分清再动手:
resource_id 字符串本身无效(写错了)。改配置。resource_id 有效,但这个账号没开通该产品。改配置没有用,去控制台开通。
上次整整卡在这里:配的是 volc.seedasr.sauc.duration(语音识别 2.0),
而账号当时只开通了 volc.bigasr.sauc.duration(大模型语音识别),持续 403。
开通对应产品后立刻就通了。不要把 403 当成 key 填错,去反复检查凭据——那是白费时间。
音色
智控台音色下拉的选项来自数据库 ai_tts_voice 表,火山这一路预置了几十个
(TTS_HuoshanDoubleStreamTTS_0001 爽快思思、_0002 温暖阿虎……),
底层是 speaker 字符串如 zh_female_shuangkuaisisi_moon_bigtts。
下拉里找不到某个音色,是这张表里没有预置,不是配置写错了——自己往
ai_tts_voice 插一行即可(讯飞的 x6_pro 就属于这种情况)。
插完记得清 Redis 缓存。
备选链路
讯飞(ASR_XunfeiStream + TTS_XunFeiStreamTTS)也实测跑通过,可以作为火山出问题时的退路。
另外系统预置了 ASR_FunASR 等本地方案,不依赖云服务但吃 CPU,
机器性能够、又不想为语音付费时可以考虑。
git@github.com:tankxu/xiaozhi-music-mcp(私有,需打包传)C:\xiaozhi-music-mcp/stream_pcm(固件层)、/mcp(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 判定异常的可能。不要用主力账号。
在目标机器上导出,不要用你自己机器导的。cookies 与请求出口 IP 不一致时更容易触发风控。
git@github.com:tankxu/stackchan-bili(私有)C:\stackchan-bilinetwork_mode: host —— 不写也不能写 ports 映射state/ 必须挂载
B站登录凭据 cookies.json 和播放历史都在 ./state。
不挂的话容器一重建登录态就没了,每次都要重新扫码。这个目录也不要从委托人那边带过来。
登录要在对方的桌面上完成:用 ToDesk 弹出扫码页,让机主用自己的B站 App 扫。
服务会轮询扫码状态(最长 3 分钟),成功后 state/cookies.json 落盘。
默认参数是给 CoreS3 屏调的
OUT_W=320 OUT_H=240 OUT_FPS=12 JPEG_Q=7、音频 16kHz 单声道。
BILI_QN=32 是 480P(实测免登录上限),要更低画质用 16。
git@github.com:tankxu/xiaoyuzhou-server(私有)C:\xiaoyuzhou-serverGOPROXY=https://goproxy.cn,direct)Go 编译约 70 秒。上游 token 很短命,Mac 侧原本有自动 refresh 的机制; 如果对方机器上要长期用,需要确认 refresh 逻辑在 Windows 上也跑得起来——这一条上次没有验证。
C:\cli-proxy-api最后一步只能由人来做
添加上游账号走的是 OAuth 浏览器授权,agent 代劳不了。 把服务跑起来、端口通了之后,剩下的登录动作留给机主,并把操作步骤写清楚交给他。
| 服务 | 仓库 | 官方文档 |
|---|---|---|
| 小智服务端 | xinnan-tech/xiaozhi-esp32-server 公开 |
Deployment_all.md(全模块,本手册对应) Deployment.md(只跑 server 的精简版) FAQ.md · xiaozhi.biz |
| CLIProxyAPI | router-for-me/CLIProxyAPI 公开 |
help.router-for.me 管理 API |
| 音乐服务 | git@github.com:tankxu/xiaozhi-music-mcp私有,打包传 |
仓库内 README.md 与 接入文档.md |
| B站转码 | git@github.com:tankxu/stackchan-bili私有,打包传 |
仓库内 README.md |
| 小宇宙 | git@github.com:tankxu/xiaoyuzhou-server私有,打包传 |
仓库内 README.md 与 doc/ |
上游文档和这份手册的分工
上游文档讲的是标准环境下怎么装;这份手册讲的是国内网络 + Windows + 远程操作下额外要处理什么。 两者是叠加关系——先按上游文档理解服务本身的结构,再回来看这里的坑。 上游文档不会告诉你 Docker 凭据在 SSH 会话里拿不到,也不会告诉你防火墙 Block 优先于 Allow。
见坑二。这一步不做的话,局域网里的 ESP32 设备连不上,但你从 Tailscale 测一切正常,非常容易漏。
在路由器上做 DHCP 保留,比在 Windows 里配静态 IP 更稳(换网络环境不会失联)。设备固件里写的是这个地址。
powercfg /change standby-timeout-ac 0
powercfg /change hibernate-timeout-ac 0
powercfg /change monitor-timeout-ac 15 # 屏幕可以关,机器不能睡
这件事必须写进交付说明
Docker Desktop 是用户级程序:机器重启后,必须有人登录 Windows 桌面,容器才会起来。 勾选「开机启动 Docker Desktop」只是在登录后自动拉起,不能替代登录。
所以:断电恢复后,SSH 和 Tailscale 会自己回来(系统服务),但五个服务全都不在, 直到有人登录桌面。如果机器要真正无人值守,需要配置自动登录,或者改用 WSL 里裸装 Docker Engine(不经 Docker Desktop)。
docker ps 全部 UpKEYS.txt 已生成,凭据都是机主自己的
服务端跑起来之后,设备要刷固件、再把四个服务地址写进去。固件仓库
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。
给别人装机时这三个必须全部显式改掉,否则设备会去连不属于他的服务。
把配置键写成空串是「恢复编译期默认」,不是「清空」——别拿它来解绑。
http://<设备IP>:8032/ 自动跳转,局域网内也可用 stackchan.local:8032firmware/API.md./sc_test.sh:静音 → 进 agent → 点歌 → 观察,一条命令跑完| 症状 | 最可能的根因 | 去哪一节 |
|---|---|---|
A specified logon session does not exist | SSH 非交互会话拿不到 Docker 凭据 | 坑一 |
| Tailscale 能连,局域网设备连不上 | 防火墙 Block/Public 压住了 Allow | 坑二 |
| compose 文件被改后 YAML 报错 | Get-Content -Raw 吃掉了换行 | 坑三 |
JSON 文件读不了,首字节 0xFF | > 重定向写成了 UTF-16LE | 坑三 |
| Docker 引擎一直转圈起不来 | 装了 16MB 的内核包而不是 250MB 完整包 | 03-3 |
sshd 服务不存在但装的时候说成功了 | Add-WindowsCapability 拉不到组件 | 01-2 |
| 密钥认证失败、继续问密码 | 公钥放了 ~/.ssh/ 而不是 administrators_authorized_keys | 01-3 |
icacls 报找不到组 | 中文版 Windows,要用 SID 不能用组名 | 01-3 |
build 卡在 deb.debian.org | Debian 源没换 | 04 |
| 音乐容器起来就退出 | .env 里四个必填键缺了 | 服务卡 2 |
| B站每次都要重新扫码 | state/ 没挂载 | 服务卡 3 |
| YouTube 报 bot 检测 | 需要 cookies,且不能用 Chrome 导 | 服务卡 2 |
| 改了模型配置不生效 | Redis 缓存没清 | 服务卡 1 |
改了 ota_url 设备毫无反应 | 没重启——只有它需要重启,另三个立即生效 | 07 |
| 刷完机 WiFi 和小智绑定全没了 | 用了一体包,它会清空 NVS | 07 |
| 设备点歌取不到音频 | 设备 MAC 没加进音乐服务白名单 | 07 |
| 重启后所有服务都没了 | 没人登录桌面,Docker Desktop 没启动 | 06-4 |
诚实标注,避免下一个 agent 把没验证的当成已验证的:
LIVE_STREAM=1 分块传输没有对着固件实测过。
流式响应没有 Content-Length,固件必须吃得下 chunked 才能开。默认给的是 0,是安全值。
上次部署的耗时分布,供估算
远程通道(Tailscale + OpenSSH)约 1 小时,WSL/Docker 底座约 2 小时(大半耗在那个 16MB 的错误安装包上), 五个服务约 4 小时(大半耗在 Docker 凭据和各种换源上)。 照着这份文档走,底座和换源的时间应该能压掉大半。
基于 2026-08-18 一次真实远程部署整理。目标机 Windows 10 专业版 22H2 / 16 核 / 32G。
文档里所有「上次」指的都是那一次,标注为「没有验证」的条目请自行确认后再依赖。