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 # PS 5.1 默认不用 TLS 1.2,Invoke-WebRequest 连 GitHub 会直接失败 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-WebRequest -UseBasicParsing -OutFile "$env:TEMP\OpenSSH-Win64.zip" ` -Uri 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/OpenSSH-Win64.zip' # 先验货再往下走:下载失败时它可能留一个 0 字节文件,后面所有命令连环报错 Get-FileHash "$env:TEMP\OpenSSH-Win64.zip" -Algorithm SHA256 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 的 DefaultShell 改成 PowerShell
网上教程很爱这么写,但那会破坏 scp——而三个私有服务全靠它传过去。
保持默认的 cmd.exe。需要 PowerShell 时用
ssh host 'powershell -NoProfile -ExecutionPolicy Bypass -File C:\Windows\Temp\x.ps1',
把脚本先 scp 过去再执行——这样也顺便躲开了在 SSH 命令行里拼引号的地狱。
这条通道重启后依然可用
sshd 和 tailscaled 都是系统服务,开机自启,不需要有人登录 Windows 桌面。
但下一节的 Docker Desktop 是用户级程序——机器重启后必须有人登录桌面,容器才会起来。这个区别后面会反复咬人。
这一节放在装东西之前,因为它们是整个部署里最耗时间的部分。上次五个服务里,每一个都在其中某一条上卡过。 先读完再动手,能省掉大半天。
如果 DISM 报「错误 2 找不到指定的文件」,直接跳到坑四
那一条比其余三条都靠前、都致命:WSL2 根本装不上,整条 Docker 路线归零。 它和功能名、Windows 版本、payload 都没关系,五秒钟就能确认是不是它。
任何碰 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。
2026-08-24 复测:计划任务这条解法也可能救不了
另一台机器(Win11 25H2 26200 / Docker Desktop 4.87 / 用户是 administrator)上,上面那个模板
不起作用:任务确实执行了——build.log 里 Pulling 都开始打印了——
然后照样报同一句 logon session does not exist。
那台机器的 config.json 里 credsStore 是 desktop 而
auths 是 空的:它根本没登录过任何 registry,拉的又全是公开镜像,
凭据助手纯属累赘——但把 credsStore 删掉依然无效,
和本页上面列的那三种办法一样。
WSL 那条路是死的:wsl -d docker-desktop -e docker pull 会被明确拒绝
(「This is not supported,请从 Windows 侧调用;要用第三方 WSL2 发行版请先启用 Docker Desktop 的 WSL 集成」)。
走通它得先装一个 Ubuntu 发行版再开集成,不比找人点一下划算。
真正可行的是这两条:
restart: always 会自己活着,之后不用再麻烦他。脚本里每步打印耗时和日志路径,
失败时你能直接从 SSH 读日志定位。docker compose up -d --no-build 在 SSH 里是能用的——它不碰 registry。
所以改完 .env 重建容器、改配置重启、exec、logs、restart
这些事,agent 自己全能做完,只有「拉新镜像」和「build」需要借人手。用 Tee-Object 包 docker 输出会看起来像卡死
让人在桌面跑脚本时别用 | Tee-Object 收集 docker 的输出:PowerShell 5.1 对 docker 的
ANSI 进度条处理很差,会一路憋到命令结束才吐出来。对方看到的是标题打完就没动静,
十有八九来问你「是不是卡住了」。要么直接让它输出到终端,要么在脚本里自己打阶段性进度。
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 被当独立元素 |
生成的字符串结构错乱 |
远端 .ps1 脚本文件本身 |
按 ANSI 读脚本源码 | 脚本里只要有中文(注释、字符串、正则),解析就被破坏:$(...) 子表达式静默不求值、
ParserError: 字符串缺少终止符。传给远端执行的 ps1 一律写纯 ASCII |
$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`""
这一条排在最后,但它发生得最早、卡得最死——WSL2 装不上,后面整条 Docker 路线归零。
症状:DISM 连自我修复都做不到
dism /online /enable-feature /featurename:VirtualMachinePlatform /all 报
错误: 2 系统找不到指定的文件;换 Enable-WindowsOptionalFeature 一样;
Get-WindowsOptionalFeature -Online 直接抛 COMException;
连 dism /online /cleanup-image /scanhealth 都在 4.9% 崩掉。
去掉 /all、显式指定 /packagename、sfc /scannow、
RestoreHealth——全都没用,因为 DISM 自己就死在那个损坏项上。
不要在功能名、payload、Windows 版本上找原因。答案在 C:\Windows\Logs\CBS\CBS.log 里,
长这样:
on:[27]'\SystemRoot\WinSxS\Catalogs' → STATUS_OBJECT_NAME_NOT_FOUND Failed to get CSI system store [HRESULT = 0x80070002] CSI store consistency check fails Failed to load component store
C:\Windows\WinSxS\Catalogs 整个目录不见了。
CBS 每次加载组件存储都要先打开它,打不开就整个 CSI store 加载失败,于是所有功能安装动作
统一报「找不到指定的文件」。上次那台机器上装着 360——它的「系统瘦身 / 深度清理」正是会动 WinSxS 的那类工具,
而 .cat 签名文件体积大,恰好是这类工具的首选目标。
五秒钟确认是不是这一条
dism /online /get-packages 能正常列完(packages 表是好的),
/get-featureinfo 配合 /packagename:Microsoft-Windows-Foundation-Package~...
也能查出功能状态——只有真正要写入的动作才崩。
「查得动、装不了」就是它。再看一眼 Test-Path 'C:\Windows\WinSxS\Catalogs' 即可定案。
$cat = 'C:\Windows\WinSxS\Catalogs' New-Item -ItemType Directory -Path $cat -Force | Out-Null # 连 owner 一起抄(owner 必须是 NT SERVICE\TrustedInstaller) $sddl = (Get-Acl 'C:\Windows\WinSxS\Manifests').Sddl $acl = Get-Acl $cat $acl.SetSecurityDescriptorSddlForm($sddl) Set-Acl -Path $cat -AclObject $acl # 立刻验证:这条不再报 Error 2 就是修好了 dism /online /enable-feature /featurename:VirtualMachinePlatform /norestart /english
上次几秒钟就修好了,enable-feature 直接跑到 100%。
Set-Acl 用 SDDL 已经把 owner 一并设成 TrustedInstaller,
后面若再补一句 icacls /setowner 报「拒绝访问」可以无视。
修完 Manifests(4.6 万个文件)和 FileMaps 都在,说明组件存储本身是健康的,
只是丢了一个目录。启用完两个功能要真重启,vmcompute 服务是重启后才被创建出来的。
这不是我们造成的,必须写进交付说明
丢掉的 .cat 文件补不回来,重建空目录只是让功能安装恢复。那台机器在我们接手之前,
Windows 功能安装和部分累积更新就已经是坏的,只是没人装过功能所以没暴露。
要彻底修得用同版本 ISO 做就地修复安装——那是机主自己的决定,别替他做,但要告诉他。
顺带一条:sfc /scannow 在 SSH 里是空转的
它返回 exit 0,但 CBS.log 里连一条 [SR] 记录都没有——
等于什么都没做。要跑它得推回交互式会话(同坑一)。别拿它的 exit 0 当「系统没问题」的证据。
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', `
'--installation-dir=D:\StackChan\DockerDesktop', `
'--wsl-default-data-root=D:\StackChan\_docker'
C 盘紧张时这两个参数是必须的
--wsl-default-data-root 才是关键的那个:所有镜像和容器数据都落在这里
(ext4.vhdx)。默认位置是 %LOCALAPPDATA%\Docker\wsl,在 C 盘。
五个服务的镜像加起来不小——上次小智主服务那个镜像单独就 10.6GB,
mysql 1.3GB、音乐服务 1.04GB、B站 851MB。
上次那台 C 盘只剩 39GB,装完 Docker + 拉完全部镜像,C 盘一点没少(40.1GB),
D 盘承担了全部。验证方法:docker info 里的 DockerRootDir 是容器内路径,
看不出宿主位置;直接看那个目录的大小增长最实在。
写 %USERPROFILE%\.docker\daemon.json,注意用上一节的 .NET 写法避免 UTF-16 问题:
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://docker.1ms.run",
"https://dockerproxy.net"
]
}
验证
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)——排查局域网连不通时不要错怪代理, 先按坑二查防火墙。
2026-08 那台是 clash-verge / verge-mihomo,系统代理 127.0.0.1:7897。
一个必须知道的前提:WSL 是 NAT 模式,容器里的 127.0.0.1 到不了宿主的代理
(wsl 自己会提示「检测到 localhost 代理配置,但未镜像到 WSL」)。
容器里要用代理只能写 http://host.docker.internal:7897。
往对方机器传大文件:别走明文 HTTP 跨境,走他自己的代理
私有仓库的包只有几 MB,scp 走 Tailscale 就够。但固件、安装包这类几十 MB 到几百 MB 的东西,
Tailscale 那条链路实测只有 ~5KB/s(3MB 传了十分钟),完全不能用。
换成「自己起 HTTP + UPnP 映射 + 对方机器直连公网 IP 下载」快得多,但如果你在境外、
对方在国内,明文 HTTP 传大文件会在中途被 reset:
第一次 WebClient 传到一半断(它还有 100 秒默认超时),
换 curl 直连则是 Recv failure: Connection was reset、downloaded=0。
解法是让对方机器走它自己的代理来下——加密隧道不受干扰。同一个文件、同一条链路,
curl.exe -x http://127.0.0.1:7897 一次就传完(14MB / 63 秒 / 226KB/s;3.9MB 只要 8 秒)。
另外服务端要支持 Range(curl -C - 才能续传),文件名用随机串、传完立刻关掉端口映射——
固件里编译着机主的 WiFi 密码,别让它在公网上多待一秒。
目录统一放 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:
model.pt(见下方红框),不然主服务起不来server.secret(智控台「参数管理」里有,但直接查库更快,见下)data/.config.yaml,再起主服务server.websocket / server.ota / server.fronted_url
三个参数(默认全是 null,见下方红框),清 Redishttp://<局域网IP>:8002,注册的第一个账号自动是管理员(密码让机主自己定)必须先下 model.pt(约 893MB),否则主服务起不来
compose 里主服务挂了 ./models/SenseVoiceSmall/model.pt。这个文件不存在的话
Docker 会把挂载点建成一个目录,主服务随即失败。就算你打算用云端 ASR(火山引擎),
这个挂载点也得是个真文件。
# modelscope 会拒绝 curl 的默认 UA,而且是【静默返回 0 字节】不报错 # 另外 curl 不会自己建目录 —— 目录不存在时同样得到 0 字节 New-Item -ItemType Directory -Force -Path 'D:\...\models\SenseVoiceSmall' curl.exe -L -A 'Mozilla/5.0' -o '...\model.pt' ` 'https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt' # 验货:头两字节必须是 PK(pytorch 是 zip 容器)
上次这一步失败过两次都是 0 字节,因为它不报错:第一次是 UA,第二次是目录不存在。 一定要检查大小和头两字节,不要只看 curl 的退出码。国内直连约 30 秒下完。
docker exec xiaozhi-esp32-server-db mysql -uroot -p123456 \ -D xiaozhi_esp32_server -N -B \ -e "select param_code,param_value from sys_params where param_code like '%secret%';"
三个默认 null 的参数:不配,设备拿到的是 docker 内网 IP
sys_params 里这三条初始值都是 null:
server.websocket、server.ota、server.fronted_url
(后者默认是 http://xiaozhi.server.com)。为空时主服务就用自己探测到的地址——
而它跑在容器里,探到的必然是 172.18.0.x 这种内网 IP:
视觉分析接口是 http://172.18.0.4:8003/mcp/vision/explain Websocket地址是 ws://172.18.0.4:8000/xiaozhi/v1/ ← 设备永远连不上
主服务启动时会 POST /config/server-base 试着把自己的地址上报给 manager-api,
这一步失败了也只是重试两次然后继续(日志里一闪而过),参数保持 null。
所以要手动配:
update sys_params set param_value='ws://<局域网IP>:8000/xiaozhi/v1/' where param_code='server.websocket';
update sys_params set param_value='http://<局域网IP>:8002/xiaozhi/ota/' where param_code='server.ota';
update sys_params set param_value='http://<局域网IP>:8002' where param_code='server.fronted_url';
# 改完必须清缓存,否则不生效
docker exec xiaozhi-esp32-server-redis redis-cli FLUSHALL
注意 websocket 是 8000,OTA 是 8002,两个端口不一样。
fronted_url 会印在设备屏幕上让人去绑定,留着默认的
xiaozhi.server.com 机主会一脸茫然。
设备要走激活绑定,OTA 返回里就有激活码
OTA 接口的响应里带一段 activation:
"activation": { "code": "158552", "message": "http://<fronted_url>\n158552",
"challenge": "<设备MAC>" }
设备重启后屏幕上显示这个码,机主要在智控台里输入它完成绑定,绑定后才能连 websocket 对话。 所以「注册管理员账号」这一步不能省,也不能替机主做。
改了数据库要清 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,但 Windows 上必须改成
ports: ["2233:2233"](见下方红框)Windows 上 network_mode: host 是无效的,必须改 bridge + ports
Docker Desktop 的 host 网络绑的是 WSL 虚拟机的网络栈,不是 Windows 主机。
结果是端口到不了局域网——你在宿主机上 curl 127.0.0.1:2233 可能还通,
设备却永远连不上,而且这个现象和坑二(防火墙)长得一模一样,很容易查错方向。
# 删掉 network_mode: host,换成:
ports:
- "2233:2233"
改了不影响可用性:仓库里用 host 只是性能优化(省掉 docker-proxy 那一跳用户态 NAT), 而 README 的实测表格里 NUC 用 bridge 的结论就是「流畅」。 只要机器 CPU 不比那台 i7-8559U 差,这一跳的开销无所谓——上次那台是 i5-13600KF,实测正常。
另一条出路是把 WSL 改成 mirrored 网络模式(%USERPROFILE%\.wslconfig 里
networkingMode=mirrored),那样 host 网络就正常了。
但那是给自己机器用的做法——它会改变机主整台机器的 WSL 网络行为,
给别人装机时改 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。
远程刷别人的机器时这条更要命:上次客户那台 Windows 上有 5 个 COM 口——两个蓝牙串口、
一个摄像头的 CDC 接口、一个 CH340 板子(VID_1A86)、真正的 StackChan 在
VID_303A&PID_1001(303A 是 Espressif 的 VID,1001 = ESP32-S3 内置 USB)。
[System.IO.Ports.SerialPort]::GetPortNames() 第一个返回的恰好是那块 CH340。
先用 esptool --port COMx --no-stub read-mac 认准芯片型号和 MAC 再刷,别信端口顺序。
远程刷机顺序:先刷 app,再擦 NVS —— 反了就白刷
在别人机器上没法用 flash_app.sh(那边没有 IDF),流程是「本地构建 → 传 bin → esptool 刷」。
如果既要换 app 又要清 NVS(比如改了编译期 WiFi 凭据),顺序必须是先刷 app 后擦 NVS。
反过来做会这样:esptool erase-region 擦完会自动 Hard resetting,
设备立刻用还没换掉的旧 app 启动,发现凭据表为空,
于是把旧凭据又种回了 NVS;等你刷完新 app 再复位,新 app 看到凭据表非空
(固件的规则是「非空就不种入」),编译期的新凭据永远进不去。
表现是「明明改了 SSID,设备还在扫旧的那个」,而 sdkconfig.h 里查得到新值——
上次就在这里绕了一圈。判断依据很简单:进设备配网页面看凭据表里是哪个 SSID。
sdkconfig.defaults.local 改了不生效:它不覆盖已存在的 sdkconfig
文件头上写着「改完需要 idf.py reconfigure 才生效」——那句话只对全新的构建目录成立。
IDF 的规则是 sdkconfig.defaults* 仅在 sdkconfig 不存在时用来生成初始配置,
既有的 sdkconfig 优先,reconfigure 也不会去覆盖它。
所以给别人装机改 WiFi / OTA 地址时,直接改 sdkconfig 本身(或者删掉它重新生成)。
改完一定要验证产物,别信输入:
grep -E 'DEFAULT_WIFI_SSID|DEFAULT_WIFI_PASSWORD|CONFIG_OTA_URL' build/config/sdkconfig.h
上次就是漏了这一步:改了 sdkconfig.defaults.local、跑了 reconfigure、构建成功、
一体包也打好了,烧进设备的却还是我自己家的 WiFi,设备一直报 No AP found。
设备只能连 2.4GHz:先确认目标 SSID 有 2.4G 在广播
ESP32-S3 没有 5GHz 射频。而现在很多路由器是「双频同名」,机主嘴里的「就这一个 WiFi」
在设备眼里可能只有 5G 那一半。上次客户的 205 扫出来只有一个 BSSID、
波段: 5 GHz / 频道: 40,设备自然永远 No AP found。
# 中文系统上 netsh 的字段名是中文,别在远端解析,把原始输出取回本地看
netsh wlan show networks mode=bssid
看目标 SSID 下的 波段 / 频道字段:频道 1-13 是 2.4G,36 以上是 5G。 诊断时注意两件事:① 机主的电脑连在 5G 上时,它扫到的邻居 2.4G AP 可以作为 「网卡 2.4G 扫描功能正常」的旁证;② 隐藏 SSID 也不会出现在扫描列表里, 而固件走的是「扫描后按信号强度匹配凭据表」,扫不到就等于不存在—— 这种情况下用设备自己的 AP 配网页面手动输入 SSID 可以绕过(配网走直连,不依赖扫描)。
统一走设备的 /config 接口(端口 8032),立即生效且断电不丢:
curl -X POST http://<设备IP>:8032/config \
-d '{"music_url":"http://192.168.1.100:2234"}'
| 服务 | 配置键 | 值 | 注意 |
|---|---|---|---|
| 小智服务端 | ota_url | http://<IP>:8002/xiaozhi/ota/ |
全模块部署是 8002,不是 8000;要重启才生效 |
| 音乐 | music_url | http://<IP>:2234 |
还要去服务端 /admin 把设备 MAC 加白名单 |
| B站 | bili_url | http://<IP>:2233 |
指向常开的机器 |
| 小宇宙 | podcast_url | http://<IP>:23021 |
23021 是设备反代口,不是 23020 那个 API 网关 |
OTA 端点在 8002,写 8000 会得到一个查不出原因的失败
本文档早先版本把 ota_url 写成 :8000,那是错的(至少对 v0.9.6 全模块部署)。
8000 是纯 websocket 端口:GET / 会回一句 Server is running
(websocket 库的默认行为,看着像服务正常),但 POST /xiaozhi/ota/ 得到的是
Empty reply from server——连接被收下、数据被读走、然后直接关闭,
主服务日志里一条记录都不留。这个组合极其难查:端口通、GET 有响应、服务端无日志。
全模块部署里 OTA 端点由 manager-api 提供,也就是 8002。三条一起测一遍就能定案:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://<IP>:8003/xiaozhi/ota/ # 404
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://<IP>:8002/xiaozhi/ota/ # 200 ← 就是它
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://<IP>:8000/xiaozhi/ota/ # 000 空回复
设备侧的表现是屏幕/串口报 checkNewVersion failed to connect <IP>:8000,
很容易误判成「主服务没起来」而去反复重启容器。先确认端口,再怀疑服务。
改 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 → 点歌 → 观察,一条命令跑完| 症状 | 最可能的根因 | 去哪一节 |
|---|---|---|
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 |
| 点歌时先问「你要听哪一首」,最后什么也没放 | 提示词里没写音乐规则,模型走了 search_music | 07 |
改了 ota_url 设备毫无反应 | 没重启——只有它需要重启,另三个立即生效 | 07 |
| DISM 报「错误 2 找不到指定的文件」,ScanHealth 也在 4.9% 崩 | WinSxS\Catalogs 目录被清理软件删了 | 坑四 |
功能查得动(get-packages 正常)但装不了 | 同上,只有写入动作走 CSI store | 坑四 |
| 计划任务也报 logon session 错误 | 这条解法并非万能,改用「桌面跑一次」 | 坑一 |
| 桌面脚本打完标题就没动静,像卡死 | Tee-Object 憋住了 docker 的进度输出 | 坑一 |
远端 ps1 报 ParserError / $(...) 不求值 | 脚本里有中文,PS 5.1 按 ANSI 读源码 | 坑三 |
设备报 checkNewVersion failed to connect :8000 | OTA 在 8002,8000 是纯 websocket 口 | 07 |
| OTA 的 POST 得到空回复且服务端无日志 | 同上,POST 打到了 websocket 端口 | 07 |
设备连不上 ws,日志里地址是 172.18.0.x | 三个 server.* 参数默认 null,主服务只能报容器内网 IP | 服务卡 1 |
设备屏幕上让你去 xiaozhi.server.com 绑定 | server.fronted_url 没改 | 服务卡 1 |
主服务起不来,model.pt 挂载点变成了目录 | SenseVoice 模型没下(约 893MB) | 服务卡 1 |
| 模型下下来是 0 字节但 curl 不报错 | modelscope 拒绝默认 UA;或目录不存在(curl 不建目录) | 服务卡 1 |
| B站服务宿主能访问、局域网设备连不上 | Windows 上 network_mode: host 绑的是 WSL 虚拟机 | 服务卡 3 |
| 改了 SSID 但设备还在扫旧的那个 | 擦 NVS 早于刷 app,旧 app 又把旧凭据种回去了 | 07 |
sdkconfig.defaults.local 改了不生效 | 它不覆盖已存在的 sdkconfig,要直接改后者 | 07 |
设备一直 No AP found,机主说这个 WiFi 能用 | 目标 SSID 只有 5G 在广播,或 SSID 隐藏(扫描匹配对隐藏 AP 无效) | 07 |
| 公网传大文件传一半被 reset | 跨境明文 HTTP,改走对方自己的代理 | 04 |
sfc /scannow 返回 0 但什么都没修 | SSH 非交互会话里它是空转的 | 坑四 |
| 刷完机 WiFi 和小智绑定全没了 | 用了一体包,它会清空 NVS | 07 |
| 设备点歌取不到音频 | 设备 MAC 没加进音乐服务白名单 | 07 |
| 重启后所有服务都没了 | 没人登录桌面,Docker Desktop 没启动 | 06-4 |
诚实标注,避免下一个 agent 把没验证的当成已验证的:
LIVE_STREAM=1 分块传输没有对着固件实测过。
流式响应没有 Content-Length,固件必须吃得下 chunked 才能开。默认给的是 0,是安全值。
.cat 文件没有补回来。
坑四的修法只是重建空目录让功能安装恢复;组件存储的签名目录仍是不完整的,
就地修复安装(同版本 ISO)没做过。
TTS_HSDSTTS_V2(豆包语音合成2.0)仍未实测。
上次部署的耗时分布,供估算
远程通道(Tailscale + OpenSSH)约 1 小时,WSL/Docker 底座约 2 小时(大半耗在那个 16MB 的错误安装包上), 五个服务约 4 小时(大半耗在 Docker 凭据和各种换源上)。 照着这份文档走,底座和换源的时间应该能压掉大半。
基于两次真实远程部署整理:2026-08-18(Windows 10 专业版 22H2 / 16 核 / 32G)与
2026-08-24(Windows 11 专业版 25H2 build 26200 / i5-13600KF 20 线程 / 32G / 服务装在 D 盘)。
第二次新增的部分:坑四(组件存储被清理软件破坏)、坑一的失效复测、OTA 端口纠错(8000 → 8002)、
三个默认 null 的 server.* 参数、Windows 上 host 网络不可用、以及固件侧三个坑。
文档里的「上次」按上下文指这两次之一,标注为「没有验证」的条目请自行确认后再依赖。