BetterOne Console Server 操作手册
更新日期:2026 年 10 月 13 日
面向部署和使用本产品的客户。开发、打包及授权协议细节请见内部文档。
网页“系统”页底部的“文档与法律信息”可打开操作手册及法律文档;登录页底部也可查看用户协议和隐私政策。阅读页在新标签页打开,无需登录或连接外网,可切换中文 / English。
安装与首次登录
将适合 Windows、Linux 或 Raspberry Pi 的发布包部署到主机,启动对应可执行文件,并确认串口驱动及设备访问权限。Linux 应给运行账户必要的串口访问权限;Windows 应确认设备管理器中可见目标 COM 口。
当前发布目标:Windows x64(amd64);Linux amd64、arm64、armv7(32 位 ARM,GOARM=7)。树莓派按操作系统架构选择 arm64 或 armv7,不只按板子型号选择;Windows 不提供 x86 安装包。
默认 Web/API 监听为 0.0.0.0:80,通过 http://<服务器IPv4>/ 访问;实际以 http_host、http_port 配置及启动日志为准。端口占用或权限不足会影响启动。当前仅支持 IPv4。
新数据库只创建待初始化的 admin,没有默认密码,不自动创建 engineer。首次打开 Web 页面,设置并确认至少 8 个字符,不能仅含字母或仅含数字的管理员密码,完成后正常登录。初始化前空密码、Web 登录、SSH 登录及管理员操作不可用;其他用户由管理员创建。初始化接口仅能成功一次,已有数据库中的账户及密码保持原样。
管理员负责账户、API Key、许可证及全量配置操作;工程师可进行日常串口和协议配置。工程师账户需要由管理员自行创建。
API 自动化初始化:先使用 GET /api/auth/initialization 查询 required,再向 POST /api/auth/initialize 提交 password 和 confirm_password。两次密码须一致且至少 8 个字符,不能仅含字母或仅含数字;成功返回 204,已初始化返回 409,不会重置已有密码。随后通过正常登录接口登录。--init-config 仍只生成启动配置及密钥,不设置管理员密码。
选择安装方式
- Windows 服务版:以管理员身份运行
betterone-cs_<版本>_Windows_x86_64_setup.exe。安装器通过 WinSW 注册BetterOneConsoleServer服务并立即启动;安装目录为 Program Files 下的BetterOneConsoleServer,配置、数据和日志在 ProgramData 下。 - Linux deb/rpm:安装对应架构的软件包。安装器使用专用账户
betterone-console-server,启用并重启 systemd 服务;普通用户直接启动无法绑定 80 时可改为 8080。 - 绿色版 ZIP/tar.gz:解压后先运行
betterone-cs --init-config,检查配置,再运行betterone-cs。Windows 使用.exe;Linux 当前目录执行时加./。默认portable模式,配置和数据位于程序旁的config/、data/。
安装版升级会重启服务、断开当前串口会话。安装器不自动搬移其他目录的旧数据;升级前先停止程序并备份。卸载安装版保留配置、数据库、许可证、密钥、安装 ID 及日志,不等于恢复出厂。
服务管理与日志
Windows 管理员 PowerShell:
Get-Service BetterOneConsoleServer
Restart-Service BetterOneConsoleServer
Stop-Service BetterOneConsoleServer
Get-Content "$env:ProgramData\BetterOne\ConsoleServer\logs\betterone-console-service.err.log" -Tail 50
Linux:
sudo systemctl status betterone-console-server
sudo systemctl restart betterone-console-server
sudo systemctl stop betterone-console-server
journalctl -u betterone-console-server.service -n 50
启动日志中的 configuration file: 和 data directory: 是实际使用路径;排查“改了配置却未生效”时先核对这两项,再确认服务已重新启动。
Linux 上不要用 root 直接运行服务程序(例如 sudo betterone-cs -system):它写出的数据库等文件会属于 root,服务账户之后打不开。程序会拒绝这样运行;调试时请用 systemctl 管理服务,或以服务账户运行(sudo -u betterone-console-server betterone-cs -system ...)。
初始化都做什么
“初始化”由四件事组成,发生在不同的时机,都不会覆盖已有的内容。
| 时机 | 做什么 |
|---|---|
配置初始化:--init-config,或任何一次启动的开头 |
创建配置目录和数据目录,并检查数据目录能写(写一个临时文件试探);生成配置文件 settings.toml(权限 0600):默认值(Web 监听 0.0.0.0:80、ssh_host_key、license.txt、license_server、runtime.db),随机的 secret_key(会话加密认证)和 api_key_pepper(API Key 哈希),以及 device_uuid(设备身份里的 generated,同时写入机器级文件,见“设备标识”)。--init-config 只做这一步,然后退出:不创建数据库,也不启动服务。deb、rpm 的安装脚本调用的就是它 |
| 数据库初始化:第一次正常启动 | 打开或新建 runtime.db(SQLite,WAL 模式)并建表;写入 48 个逻辑控制台的默认配置;创建用户 admin,没有密码(待初始化),不创建 engineer,没有默认密码。只有 48 个控制台的配置不完整时才会重写,已有数据不动 |
| SSH 主机密钥:第一次启动 SSH 监听 | ssh_host_key 不存在就生成一把 Ed25519 密钥并保存(权限 0600),已有的不替换 |
| 管理员首次设置密码:第一次打开网页 | 库里只有“没有密码的 admin”时,登录页显示“设置管理员密码”,输入两遍(至少 8 个字符,不能仅含字母或仅含数字);只能成功一次,之后再调用接口返回 409;设置完成后回到正常登录,不会自动登录 |
48 个控制台的默认配置
| 项 | 默认值 |
|---|---|
| 串口参数 | 115200,8 位数据位,1 位停止位,无校验,无流控,没有绑定任何串口 |
| 连接数上限 | 1 |
| 协议 | SSH 和 WebSocket 开启;TCP 和 Telnet 关闭 |
| TCP 监听 | 127.0.0.1:5001–5048 |
| SSH 监听 | 0.0.0.0:6001–6048 |
| Telnet 监听 | 127.0.0.1:7001–7048 |
| WebSocket | 不单独监听,使用 Web 端口,路径 /console/<编号> |
想回到初始状态:用命令行的 --factory-reset(见“恢复出厂设置”),它会删除数据库,再按上面的“数据库初始化”和“管理员首次设置密码”重新开始。
初始化不会做的事
- 不会自动联网:许可证激活和续期都要管理员主动点击。没有许可证时是免费版(2 个端口)。
- 不创建工程师账号、API Key,也不绑定任何串口。
--init-config不动数据库。
新装后的提醒:管理员设置密码之前,任何能访问网页的人都可以抢先设置。deb、rpm 安装后服务会自动启动并监听 80 端口,请在安装后尽快打开网页设置密码。如果设备接在不可信的网络里,先把 http_host 改成 127.0.0.1 或用 --addr 限制监听地址,设好密码再放开。
恢复出厂设置
恢复出厂设置只能用命令行,在设备本机的终端里运行;网页上没有这个功能(网页上的“清除所有串口配置”只清串口配置,见“配置导入、恢复默认与备份”)。
betterone-cs --factory-reset
设备要转给别人时,加上 --new-ssh-host-key,同时换掉 SSH 主机密钥:
betterone-cs --factory-reset --new-ssh-host-key
旧密钥被删除,服务启动时生成新的。这样即使原使用者留有旧密钥的副本(例如在备份里),也无法再冒充这台设备;代价是以前连过的 SSH 客户端会提示“主机密钥已改变”,需要从 known_hosts 里删掉旧条目。只是自己重新开始时不必加。
先停止服务,并先备份。 命令只会在执行期间暂存数据库以便失败时恢复;成功后会删除暂存文件,它不是长期备份。需要保留数据,请先把下面“会删除”的文件复制到别处。
运行时发生什么
- 程序检查服务是否还在运行(有程序在 Web 地址上应答就算运行)。服务运行时不会继续,请先停止服务。
- 显示要删除的文件、要保留的文件,并用大写字母提醒“先备份数据”。
- 让你输入
RESET(全大写,完整输入)。输入别的任何内容都会终止,什么也不改。 - 确认后先检查写入权限和文件类型,把数据库暂存到同目录;写好重置记录、轮换运行时密钥后才清理暂存文件。中途失败会尽量恢复旧数据库;若意外中断而留下暂存目录,下次启动会拒绝创建空数据库,并提示先恢复。
会删除:数据库 runtime.db(以及 runtime.db-wal、runtime.db-shm 等附属文件),也就是:
- 所有用户和密码、所有 API Key;
- 审计记录;
- 全部 48 个逻辑串口的配置(备注、绑定、串口参数、协议开关、监听地址),恢复为默认值(见“初始化都做什么”)。
数据库处理成功后,还会删除下面这些文件(有才删):
| 文件 | 内容 | 删除后 |
|---|---|---|
数据目录的 web-settings.toml |
在网页上改过的 Web 监听地址、端口、HTTPS 开关和证书设置 | 回到 settings.toml 里的设置;完成时会显示恢复后的监听地址和端口 |
web-tls.pem(数据目录,以及旧版本留在配置目录的副本) |
网页上传的 HTTPS 证书和私钥 | 需要 HTTPS 时重新上传 |
web-tls-self-signed.pem |
自签名 HTTPS 证书和私钥 | 重新开启自签名时自动生成新的 |
Windows:ProgramData\BetterOne\ConsoleServer\logs\betterone-console-service*.log |
服务运行日志,含用户名、来源地址和操作记录 | 服务启动后重新记录 |
这些文件删不掉时(例如旧副本在 root 所有的 /etc 下),命令会列出它们,请手动删除;数据库部分已经完成,不受影响。
Linux 的服务日志在系统日志(journal)里,程序无法只删本服务的部分。需要一并清除时运行 sudo journalctl --rotate && sudo journalctl --vacuum-time=1s;注意这会清空所有服务的历史日志。
会保留:
| 保留的内容 | 原因 |
|---|---|
settings.toml |
保留管理员写在里面的 Web 监听地址、许可证服务器地址等基础设置(网页上改过的部分会被清除,见上);恢复出厂会在数据目录的 runtime-identity.toml 中轮换 secret_key 和 api_key_pepper,所以恢复前发出的登录状态和 API Key 全部失效 |
许可证 license.txt |
它属于这台设备,不属于数据;删掉要重新激活,可能多占席位 |
| SSH 主机密钥 | 换掉会让所有 SSH 客户端提示“主机密钥已改变”;加 --new-ssh-host-key 时才换(见上) |
device_uuid(配置文件和机器级文件) |
它是设备身份的一部分,没有硬件标识的机器丢了要重新激活 |
完成后:启动服务;服务会用自己的账户创建新数据库,并写入一条 factory-reset 审计记录(操作者是运行命令的系统用户;通过 sudo 运行时记录执行 sudo 的人,例如 alice (sudo, as betterone-console-server);来源标为“本机”)。然后打开网页,按“首次登录”设置新的管理员密码;管理员在设置密码之前,任何能访问网页的人都可以抢先设置,所以请尽快完成。
其他说明
- 必须在交互式终端里运行;脚本、管道、服务一律拒绝,防止被自动确认。
- 只对已有数据库生效,参数要和服务一致(
-user/-system/-portable、--config-dir、--data-dir,同“忘记管理员密码”里的表),不会在错误的位置新建数据库。 - Linux 的 deb、rpm 安装:先停止服务,再运行
sudo betterone-cs -system --factory-reset。程序发现自己是 root 时,会自动切换成数据目录的属主(服务账户)再动手,所以写出的文件都属于服务账户;新数据库由服务重启后创建。没有 sudo 的系统可以用 root 运行runuser -u betterone-console-server -- betterone-cs -system --factory-reset。 - 普通用户(既不是 root,也不是数据目录的属主)运行会被拒绝,提示里会写明应使用的账户。
- 权限或写入失败时会报错;如果提示遗留
.runtime.db.factory-reset-stage-*暂存目录,不要直接重启或删除它,应先检查其中的旧数据库及previous-runtime-identity.toml,按需恢复两者,再处理该目录。用 root 手动复制或移动文件后,要把属主改回服务账户:sudo chown -R betterone-console-server: /var/lib/betterone-console-server,否则服务打不开这些文件。 - 命令只清理上面列出的文件,不会动其他目录。
忘记管理员密码
还有别的管理员账号:用它登录,在“用户”页给忘记密码的账号重置密码。对方下次登录必须改密码,原有登录会全部失效。
只有一个管理员,而且忘了密码:在安装程序的这台机器上,打开一个终端,用和服务相同的启动参数运行:
betterone-cs --reset-admin-password
- 程序先显示它用的数据库路径(用来确认没有指向别的安装),然后在终端里让你输入新密码、再输入一次。输入时屏幕上不显示字符;至少 8 个字符,不能仅含字母或仅含数字。
- 重置的对象固定是内置的
admin账号。设置完成后,所有已登录的会话都会失效,admin下次登录必须再改一次密码;如果admin被禁用了,会同时重新启用。 - 程序做完就退出,不会启动服务;服务可以不停,登录状态会立即失效。
- 每次重置都会写进审计记录(事件
reset-admin-password,操作者是运行命令的系统用户,通过 sudo 运行时记录执行 sudo 的人,来源标为“本机”),日志里也有一行。记录里没有密码。 - 必须在本机的终端里运行:命令要求交互式终端,管道、脚本、服务都会被拒绝;不提供在命令行或文件里直接写密码的办法,避免密码留在 shell 历史或磁盘上。
- 没有这台机器的操作权限就无法重置。这是有意的:能读写数据库的人本来就拥有这些权限。
按安装方式运行:数据库的位置由运行模式决定,所以参数要和服务一致,否则指到另一个库,程序会提示“找不到数据库”而不是悄悄建一个新的。
| 安装方式 | 命令 |
|---|---|
| Linux 的 deb、rpm(系统服务) | sudo betterone-cs -system --reset-admin-password |
| 绿色版(portable) | 在程序所在目录:./betterone-cs -portable --reset-admin-password |
| 显式 user 模式 | 用平时运行程序的同一个用户:betterone-cs -user --reset-admin-password |
- Linux 上命令以数据目录的属主(通常是服务账户)身份执行:用 sudo 或 root 运行时,程序在加载配置、创建任何文件之前就自动切换到这个账户,不会留下 root 所有的文件。没有 sudo 的系统用 root 运行
runuser -u betterone-console-server -- betterone-cs -system --reset-admin-password。其他普通用户运行会被拒绝。 -system在 Linux 上默认就是/etc/betterone-console-server和/var/lib/betterone-console-server,与安装包一致,不需要再写--config、--config-dir、--data-dir;只有改过路径时才需要。- Windows 服务版:在“以管理员身份运行”的终端里执行
betterone-cs.exe -system --reset-admin-password(-system默认就是 ProgramData 下的安装路径);服务本身使用 LocalService,数据目录权限授予 LocalService、SYSTEM 和 Administrators。 - 没有交互式终端(例如只能通过无终端的远程通道)时无法使用;这时只能先登录到有终端的会话。
删库不是办法:删掉 runtime.db 会丢失全部端口配置、用户、API Key 和审计记录,只有在确实不需要这些数据时才考虑。
部署目录与启动参数
配置文件默认名为 settings.toml,目录按以下模式选择:
忘记配置项名称时,可查看带中文注释的 配置模板,其中列出了配置项、默认值和用途。模板不会自动加载;先使用 --init-config 生成本机配置,再复制需要修改的项目,不要覆盖已有密钥和设备 UUID。未填写的配置项继续使用程序默认值。
发布包会附带 settings.example.toml:压缩包中位于解压后的根目录,Windows 安装版位于程序安装目录,deb/rpm 安装版位于 /usr/share/doc/betterone-console-server/settings.example.toml。它仅供查阅,不会替换实际运行配置。
| 模式 | Linux 配置目录 / DataDir | Windows 配置目录 / DataDir |
|---|---|---|
-user |
$XDG_CONFIG_HOME/betterone-console-server / $XDG_DATA_HOME/betterone-console-server,默认 ~/.config/betterone-console-server / ~/.local/share/betterone-console-server |
KnownFolderLocalAppData\BetterOne\ConsoleServer\config / KnownFolderLocalAppData\BetterOne\ConsoleServer\data |
-system |
/etc/betterone-console-server / /var/lib/betterone-console-server |
KnownFolderProgramData\BetterOne\ConsoleServer\config / KnownFolderProgramData\BetterOne\ConsoleServer\data |
默认 portable,或 -portable |
可执行文件目录的 config/ / data/ |
可执行文件目录的 config\ / data\ |
-user、-system、-portable 只能选择一个,也支持双横线写法。未指定模式参数时,使用 NBC_MODE,未设置则默认 portable。
Linux 忽略相对的 XDG 变量值;Windows 目录通过 Known Folder API 解析。非空 --config、--config-dir、--data-dir、-user / -system / -portable 分别优先于 NBC_CONFIG_FILE、NBC_CONFIG_DIR、NBC_DATA_DIR、NBC_MODE,最后使用模式默认值。指定配置文件时路径独立于配置目录;否则使用 <ConfigDir>/settings.toml。命令行相对路径按启动时 cwd 解析一次。
数据库、SSH 主机密钥及许可证的相对路径按 DataDir 解析,默认为 runtime.db、ssh_host_key、license.txt;绝对路径保留,不按程序基础目录或配置目录解析。修改启动设置后需重启程序。
配置文件是管理员维护的基础值;网页保存的 HTTP/HTTPS 地址及证书设置写入 DataDir/web-settings.toml,启动时覆盖对应基础值,不修改 settings.toml。网页管理的证书、自签名证书和本机授权解除记录也位于 DataDir。升级时保留整个数据目录;要恢复基础 Web 设置,请先停止服务,备份并移走 web-settings.toml,再启动服务。
首次部署可执行 betterone-cs -user --init-config(Windows 使用 .exe;Linux 当前目录运行需加 ./),先检查配置,再正常启动。--init-config 只生成启动配置及缺失的 secret_key、api_key_pepper 并退出,不生成数据库、默认用户或 SSH 主机密钥,不启动监听。已有配置不重写;若缺少自动生成的密钥或设备 UUID,则保存在 DataDir/runtime-identity.toml,不要求服务账户修改只读配置。DataDir 必须可写,初始化也会检查,正常运行需要写入 SQLite WAL 等文件。
Linux 普通用户一般不能绑定 HTTP 80:初始化后将 http_port 改为例如 8080,或由管理员配置 capability。deb/rpm 的 systemd unit 使用专用服务账户并仅授予 CAP_NET_BIND_SERVICE;配置目录为 root:betterone-console-server、0750,配置文件 0640,数据目录归服务账户、0700。安装/升级自动启用并重启服务;请及时设置管理员密码并审查配置。
Windows 服务版通过 WinSW 使用 LocalService 运行,安装后自动启动;ProgramData 配置/数据目录 ACL 仅授予 LocalService、SYSTEM、Administrators。卸载保留配置及数据。客户无需安装 Inno Setup、Go 或 WinSW 开发工具;它们只供构建主机使用。
配置文件 settings.toml
配置文件可以只写需要覆盖的字段:未写的字段使用当前程序内置的默认值;明确写出的值(包括允许的空字符串、0、false)优先,不会被默认值覆盖。例如,不写 license_server 使用内置授权服务器地址;写 license_server = "" 关闭在线授权;写 license_server = "127.0.0.1:8080" 则继续使用这个显式地址。修改配置后需重启程序。
启动设置保存在 settings.toml,位置见上表。修改后需重启程序。文件首次启动时自动生成,已有的项不会被覆盖。
| 配置项 | 默认值 | 作用 |
|---|---|---|
http_host、http_port |
0.0.0.0、80 |
网页界面、REST API 和 WebSocket 终端共用的监听地址。也可以用接口修改,见“修改 Web/API 监听地址” |
websocket_allowed_origins |
空 | 额外允许的 WebSocket 来源(如反向代理的域名),格式 host[:port] |
https_enabled、https_host、https_port |
false、0.0.0.0、443 |
独立的 HTTPS 开关和监听地址;开启后 HTTP 入口跳转到 HTTPS。端口不得与 HTTP 相同 |
tls_cert_file、tls_key_file |
空 | HTTPS 证书和私钥;可不填而使用自签名证书。相对路径按配置目录解析 |
max_connections_per_ip |
128 |
同一个来源地址同时最多保持多少个 SSH、Telnet、Raw TCP 控制台连接(所有控制台合计)。超出的连接被立即关闭。0 表示不限制;经 NAT 或代理访问时,同一出口地址下的所有用户共用这个额度 |
secure_cookies |
false |
由反向代理终结 TLS 时,让会话 Cookie 带 Secure 标志;内置 HTTPS 开启时自动生效 |
ssh_host_key |
ssh_host_key |
SSH 主机密钥文件;相对路径按数据目录解析 |
license_file |
license.txt |
当前唯一授权文件(激活与续期共用);相对路径按数据目录解析 |
license_server |
https://license.betterone.top:8443 |
在线激活和续期检查使用的许可证服务器;写空字符串关闭在线功能。可选项,不写使用程序默认值 |
license_portal_url |
https://license.betterone.top:8443/#devices |
官网设备管理页;用于误激活、名额满或设备释放的引导,不调用管理接口。未填写用默认值,显式空值隐藏链接;修改后重启服务 |
license_offline_url |
https://license.betterone.top:8443/#offline |
官网“离线激活 / 续期”页,按内容识别 .request.txt 激活请求、.renewal.txt 续期请求和 .license.txt 授权文件,下载返回的授权文件。未填写用默认值,显式空值隐藏链接;修改后重启服务。与接口地址 license_server 独立 |
database_dsn |
runtime.db |
SQLite 数据库文件,保存控制台配置、用户、API Key 校验数据和审计记录;相对路径按数据目录解析 |
secret_key |
首次启动自动生成 | 登录会话的加密密钥,见下 |
api_key_pepper |
首次启动自动生成 | API Key 的校验密钥,见下 |
device_uuid |
首次启动自动生成 | 安装身份 UUID v4,同时保存在机器级文件;不要手动修改,详见“设备标识” |
secret_key:登录会话的加密密钥
用户登录后浏览器收到的会话 Cookie,是用这个密钥做 AES-256-GCM 加密并认证的,里面装着用户 ID、会话版本和过期时间。程序靠它判断“这个 Cookie 是不是我签发的、有没有被改动”。网页界面和 WebSocket 终端的登录认证都用它。
- 格式:64 位十六进制字符(32 字节),首次启动自动生成。
- 更换或丢失的后果:所有人当前的登录立即失效,需要重新登录。用户、配置等数据不受影响。
- 泄露的风险:拿到它的人可以伪造登录 Cookie,冒充任何用户(包括管理员)。
api_key_pepper:API Key 的校验密钥
数据库里不保存 API Key 的明文,只保存它的 HMAC-SHA256 哈希,api_key_pepper 就是这个 HMAC 的密钥。即使数据库文件被拿走,没有这个密钥也无法验证或伪造 API Key。创建 API Key 时明文只返回一次,之后无法找回。
- 格式:64 位十六进制字符(32 字节),首次启动自动生成。
- 更换或丢失的后果:所有已有的 API Key 都验证不通过,且无法恢复,必须重新创建并更换使用方的配置。
- 泄露的风险:它本身不能直接登录,但会降低数据库泄露时 API Key 哈希的保护强度。
保管与备份
- 这两项是敏感信息。配置文件应只允许运行程序的用户读写(程序写入时权限为
0600),不要发给他人,不要提交到代码仓库,不要贴到工单或聊天里。系统诊断信息里这两项只显示“已设置”或“未设置”,不含取值。 - 备份和恢复时必须与数据库配套保存。只还原数据库而密钥不同,会出现全部 API Key 失效;只还原密钥而数据库不同,则登录状态与用户数据对不上。
- 升级、迁移时保留原来的值(见“升级旧部署”)。
- 怀疑泄露时:停止程序,检查
settings.toml和DataDir/runtime-identity.toml中对应的密钥,移除所有旧值后再启动;仅删除基础配置中的一行可能仍会使用数据目录中的旧值。代价是全部重新登录(secret_key)或重新创建全部 API Key(api_key_pepper)。操作前先备份,并保护备份中的密钥。 betterone-cs --init-config只补齐缺失的项,已有的值不会改动。
升级旧部署:先停机、备份,再手动迁移
本次文件名切换不兼容旧默认名称,也不自动迁移。新 settings.toml 或 runtime.db 不存在时,即使同目录留有旧 nbc_settings.toml 或 nbc.db,程序也会忽略旧文件并创建新配置或数据库;旧文件不会被删除。若要保留旧部署的数据,升级前停止程序/服务,备份旧配置、数据库及存在的 -wal、-shm、SSH 主机私钥、license.txt 和原程序,再手动迁移。将旧 nbc_settings.toml 改名为新配置目录中的 settings.toml,将旧 nbc.db 改名为数据目录中的 runtime.db;先确认 SQLite 已完整关闭,若仍有 WAL/SHM 文件,应使用一致性备份方式迁移,不能只移动主数据库。自定义 --config、database_dsn 的部署需核对实际路径。
必须保留原 secret_key、api_key_pepper 和 SSH 主机密钥,防止会话/API Key 校验失效或 SSH 主机身份变化。若旧 TOML 显式写了 database_dsn = "nbc.db",也要改为 "runtime.db";否则程序会继续使用显式指定的旧路径。旧相对路径 data/nbc.db、data/ssh_host_key 在新 DataDir 下也须核对。许可证路径同样要核对。不要删除备份;确认启动日志中的配置/数据库路径及原数据可用后再完成升级。
日志输出到 stdout/stderr,systemd 服务由 journal 收集(journalctl -u betterone-console-server.service),绿色版日志写到终端,Windows 服务版由 WinSW 捕获并轮转文件日志。/run 仅为 systemd 运行目录,目前不写 PID 文件。
串口绑定与通信参数
选择逻辑控制台,按设备名(Windows 的 COMx、Linux 的 /dev/tty*)、USB 物理端口位置(槽位)或 USB 设备标识(VID、PID、序列号)三种方式之一进行绑定,三者互斥;各自的使用场景和前提条件见下文“三种绑定方式”。设备名可能随重新枚举改变;槽位绑定适用于保持 USB 连接位置稳定的场景;设备标识绑定适用于适配器固定、位置可能变化的场景。相同设备名、相同槽位或同一个 USB 设备不能重复绑定多个控制台;用不同方式指向同一物理设备仍可能出现打开冲突。
默认通信参数为 115200 8N1、无流控。可配置波特率范围为 300–4,000,000;该范围是软件输入校验范围,实际可用速度取决于串口芯片、驱动和目标设备。设置时与目标设备保持一致,先测试再应用到生产环境。
已打开的串口句柄可继续使用原参数;保存修改后,下次重新打开使用新配置。可通过状态 API 的 active_serial_config 核对当前实际参数。每个控制台的最大连接数单独配置,取值范围为 1–256,默认 1,所有协议共用该上限;不再支持 -1 无上限模式或 0。新提交的越界值会被拒绝,不提供旧数据库或无上限配置的自动迁移。多个客户端共用同一串口流,发送命令前应协调操作。
三种绑定方式:使用场景与前提条件
每个逻辑控制台的串口可以用下面三种方式之一绑定,三者互斥:设置其中一种会自动清除另外两种,同一请求里同时设置两种以上会被拒绝。
| 按设备名 | 按 USB 端口位置 | 按设备标识 | |
|---|---|---|---|
| 配置字段 | device |
slot |
vid、pid、serial_number |
| 示例 | COM11、/dev/ttyUSB0 |
00.1400@2.3 |
0403、6015、D30J42Y6A |
| 绑定的对象 | 终端名本身 | USB 物理端口,找出这个口上现在的串口 | USB 适配器本身,不管它插在哪个口 |
| 终端名变化 | 找不到设备 | 不受影响 | 不受影响 |
| 换适配器(插回同一口) | 取决于终端名是否不变 | 不用改配置 | 必须重新绑定 |
| 换 USB 口或换线 | 取决于终端名是否不变 | 必须重新绑定 | 不受影响 |
| 适用的设备 | 任意串口,包括非 USB 的 | 仅 USB 串口 | 仅有序列号的 USB 串口 |
1. 按设备名(device)
使用场景
- 非 USB 的串口:主板串口、
com0com这类虚拟串口、PTY,它们没有 USB 位置,也没有 USB 序列号,只能这样绑定。 - 一个 USB 设备带多个串口(FT4232H、CP2105、CH348 等多口芯片):各口共用同一个 USB 位置和同一组 VID、PID、序列号,另外两种方式都会报歧义,只能按设备名绑定。
- 现场只有一个适配器、终端名已经固定、也不会换设备的简单场景。
前提条件
- 终端名必须保持不变。Windows 的 COM 号可以在设备管理器里为设备固定,Linux 的
/dev/ttyUSBn由内核按探测顺序分配,重启或重新插拔后可能互换,不能直接依赖;需要时用 udev 规则建立固定名称,或使用系统自带的/dev/serial/by-path/、/dev/serial/by-id/链接,再把这个链接名当作设备名填入。 - 这个终端名没有被其他程序占用。
不适合的情况:Linux 上有多个 USB 串口、又没有用 udev 固定名称时,终端名会漂移,可能绑到错误的设备。
2. 按 USB 端口位置(slot)
使用场景
- 线缆固定接在机架上的某个口,要求“某号控制台就是这个口上的串口”,适配器坏了换一个新的,插回同一个口不用改配置。
- 适配器没有序列号或序列号重复(很多 CH340、部分 PL2303 仿制芯片),无法按设备标识绑定。
- Windows 和 Linux 都支持,格式一致(形如
00.1400@2.3)。
前提条件
- 计算机硬件不改动,尤其是主板。 槽位由主板上的 USB 控制器、接口和 Hub 的物理连接结构决定;更换主板、增减或更换扩展卡、改变设备所接的接口或 Hub,槽位必然改变。
- 带多个串口的设备,槽位不写接口号时绑定第一个口,其他口须写明接口号,见下文“多口设备”。
- 更新 BIOS/固件等变化也可能让槽位改变(PCI 根总线号由固件分配)。出现这类变化后必须重新核对槽位。 详见下文“槽位的稳定性”。
- Linux 需要能读取
/sys:容器、WSL 或权限不足的环境里槽位为空,只能用设备名。 - 不适用于非 USB 串口。
3. 按设备标识(vid、pid、serial_number)
使用场景
- 要求“就是这个适配器”,不依赖它插在哪个口,也不要求不换线:适配器会在不同接口、不同 Hub 之间移动,或线缆位置经常调整。
- 不能满足“硬件不改动”这个前提的现场,例如机器经常更换、扩展卡变动,但适配器本身固定。
前提条件
- 设备必须有序列号,并且在同时在线的设备中唯一。没有序列号的适配器(很多 CH340、部分 PL2303 仿制芯片)不能使用这种方式;个别批次序列号重复时,两个设备同时在线会报歧义。
- VID、PID、序列号三个字段必须同时填写,缺一不可:同型号的多个适配器共用 VID 和 PID,只有序列号能区分。
- 一个设备只有一个串口。 多口芯片的各口共用同一组标识,会报歧义。
- 换了适配器必须重新绑定。
- FTDI 适配器的序列号在 Windows 上带一个通道字母后缀(如
D30J42Y6A),Linux 上是原值(D30J42Y6),所以 FTDI 设备的这种配置在两个平台之间不通用,请填写串口列表中usb_serial实际显示的值。 - VID、PID、序列号由 Go 串口库
go.bug.st/serial提供;该库读取失败时,本次枚举里这三项为空,按设备标识绑定的控制台暂时找不到设备并重试,不影响另外两种方式。
怎么选
- 不是 USB 串口,或设备带多个串口 → 按设备名。
- 线缆位置固定、适配器可能更换,且硬件不改动 → 按 USB 端口位置。
- 适配器固定、位置可能变化,且设备有唯一序列号 → 按设备标识。
- 不确定时:先在串口列表(
GET /api/system/serial-ports)查看设备的slot、usb_vid、usb_pid、usb_serial;有序列号的 USB 适配器两种 USB 方式都可以用,按上面第 2、3 条的场景选择;无序列号的只能选端口位置或设备名。
三种方式找不到设备或有歧义时,控制台按串口故障处理并进入重试(超时后断开客户端),状态里的错误提示可区分“设备不可用”“USB 位置不可用”和“匹配到多个串口”。
槽位(Slot)是什么
槽位识别的是串口接在电脑的什么位置,Windows 和 Linux 写法完全一致:
00.1C00.0000@1.4:2
│ │ │ │ │ └ 接口号(可省略,省略就是第一个)
│ │ │ │ └── 从 @ 开始是 USB 端口链:电脑的口、Hub 的口……
│ └────┴────────── 各级 PCI 设备(4 位十六进制:设备号+功能号)
└────────────────── PCI 根总线号(2 位十六进制)
@之前是 USB 控制器在主板上的位置,@之后是 USB 端口链,最后的:数字是 USB 接口号(只有多口芯片才需要写)。- 十六进制不区分大小写,程序统一显示为大写;端口号是十进制 1–255。
- 例:主板自带 USB 口接 Hub 第 3 口 →
00.1400@2.3;FT4232H 的第 3 个口 →00.1400@2.3:2。 - 板载串口、PCI/PCIe 串口卡没有
@部分,如00.1C00.0000,同一控制器上有多个口时用:序号(按地址从小到大)。 - ARM/树莓派等设备树平台写
dt.平台设备名@…,如dt.3f980000.usb@1.2。
槽位不是串口的序列号,也不是终端名。程序按槽位找到该位置上当前出现的串口,取得它此刻的终端名(COMx、/dev/ttyUSBx),再打开它。因此把适配器换到同一个接口上,槽位不变;终端名只是解析结果,不能作为长期绑定依据。
“位置”是系统报告的位置,与机箱外壳上的接口并不总是一一对应:USB 3.0 接口在系统中对应 2.0 与 3.0 两个逻辑端口,不同速度的设备会落在不同的位置;主板内部设备(蓝牙、无线网卡、前置面板 Hub)会占用端口号;树莓派 3B/3B+ 的所有 USB 口都在板载 Hub 后面,端口链会多一段。
槽位的稳定性
槽位由主板的物理连接结构决定,不使用会随启动顺序变化的 USB 总线号和 Windows 根 Hub 序号。唯一由固件分配的是 PCI 根总线号(@ 前面的第一段),它由 BIOS 决定,同一台机器上固定。
前提:计算机硬件不改动,尤其是主板。 更换主板、增减或更换 PCIe/USB 扩展卡、改变设备所接的接口或 Hub,槽位都会改变。以下情况即使不改硬件也可能让槽位变化,可能导致控制台绑定到另一个口或找不到设备:
- 更新 BIOS/固件、修改 BIOS 中的 PCI/USB 设置(根总线号或设备布局可能变化);
- 速度协商回退:xHCI 主机上 USB 3.0 与 USB 2.0 设备走不同的根端口,线缆或 Hub 不佳时设备降速,位置随之改变;
- 更换 Hub 型号:不同型号的内部端口编号可能不同(USB 3.0 Hub 内部是 2.0 与 3.0 两个子 Hub)。
槽位变化后,原绑定解析不到设备(控制台进入重试,超时后断开客户端)。变更后请逐一核对串口列表(GET /api/system/serial-ports)中的 slot,并做连接验收。
其他限制:
- Linux 读取 sysfs;容器、WSL 或权限不足的环境里槽位为空,只能用设备名。WSL 经
usbipd转发的设备与物理位置无对应关系。 - Windows 从系统的 LocationPaths 和 Hub 查询取得位置,不受 COM 号影响。
- 非 USB、非 PCI 的虚拟串口(com0com、PTY)没有槽位,只能用设备名。
- 请使用正规厂家生产的串口线材/芯片。 盗版、山寨芯片可能无法正确报告接口号、序列号等信息,由此造成的问题由使用者自行承担。
多口设备
一个 USB 设备带多个串口(FT2232H/FT4232H、CP2105、CH348 等)时,各口位置相同,只靠接口号区分:
- 串口列表里每个口的槽位会带上接口号,如
00.1400@2.3:0、00.1400@2.3:1; - 绑定时写不带接口号的
00.1400@2.3等于绑定第一个口(接口号最小的串口);要用其他口,写明接口号; - 接口号来自芯片固件,不随插拔、重启变化;
- 一个接口下再带多个串口的芯片(MCS7840、CH348 等)的口序号是否稳定,需在真实设备上验证;此类设备也可按设备名绑定,并配合 udev 规则(
/dev/serial/by-path/、/dev/serial/by-id/)固定名称。 - 带内置 Hub 的多口卡(Hub 加几个单口芯片):每个芯片接在 Hub 的不同端口上,端口链多一段,各口槽位各不相同。
串口线材和芯片的选择
建议使用正规厂家生产的串口线材、USB 转串口适配器和 USB Hub,并通过正规渠道购买。 常见的正规芯片厂商有 FTDI、Silicon Labs、Prolific(正品)、WCH(正品)等,也可以直接选用知名品牌的成品线缆。多路部署使用带外接电源的 Hub。
盗版、仿冒、山寨或来路不明的芯片和线材,可能出现(但不限于)下面的问题:
- 序列号缺失或多块相同,无法按设备标识区分,必须改用 USB 槽位;
- 官方驱动识别出仿冒芯片后拒绝工作,或改写它的内部配置;
- 数据丢失或乱码、波特率不准、反复断开重连;
- 供电不足、电平不匹配(3.3 V、5 V、RS-232 电平混用)、缺少隔离和保护,严重时损坏所连接的设备。
使用盗版、山寨或来路不明的线材和适配器造成的识别错误、绑定到错误的串口、数据丢失、通信中断和设备损坏,由使用者自行承担风险和责任。 本软件无法保证在这类产品上正常工作。
买到新的线材或适配器后,建议先在“物理串口”页查看它的 USB 标识(VID、PID、序列号):序列号为空,或同一批的几块序列号相同,就不能按设备标识绑定。
通过 Hub 扩展串口:槽位与更换 Hub
用 USB Hub 把几路到几十路串口适配器接到一台电脑上是最常见的部署方式。接了 Hub 以后槽位仍然是同一种格式,只是端口链里每多一级 Hub 就多一段 .端口号:
| 接法 | 槽位(@ 之后的端口链) |
Windows LocationPaths 末尾 |
|---|---|---|
| 电脑的口直接接适配器 | 00.1400@2 |
…#USB(2) |
| 电脑的口接 Hub,适配器接 Hub 第 3 口 | 00.1400@2.3 |
…#USB(2)#USB(3) |
| Hub 再接一级 Hub | 00.1400@2.3.1 |
…#USB(2)#USB(3)#USB(1) |
Hub 内部的端口号是 Hub 芯片自己的硬件编号,插拔、重启、断电都不会变,所以接 Hub 并不增加槽位不稳定的风险;仍然只有"主机控制器那一段"(@ 之前)可能因固件变更而变化,见上文“槽位的稳定性”。
接 Hub 时槽位保持稳定的前提
- Hub 一直接在电脑的同一个口上。把 Hub 换到电脑的另一个口,路径前缀改变,Hub 上所有设备的槽位都会跟着改变。
- Hub 的型号不变,级联顺序不变。端口号是 Hub 芯片内部的编号,不同品牌、不同型号可能不同。
- 不要根据 Hub 外壳上的丝印数字推算槽位。很多 7 口 Hub 内部是两颗 4 口芯片级联的,外壳上的第 5 口对应的槽位可能是
00.1400@2.4.2,不是00.1400@2.5。同一型号上这个对应关系是固定的,但第一次配置时要实际把适配器插上、读出槽位,不要凭丝印猜。 - USB 3.0 Hub 内部是 USB 2.0 和 USB 3.0 两个子 Hub,各有各的端口编号。串口适配器几乎都是 USB 2.0 设备,始终走 USB 2.0 那一半,路径是固定的;但把 USB 3.0 Hub 插到只有 USB 2.0 的接口上,或者插到另一个主机控制器上,路径会和插在 USB 3.0 口上时不同。
- 接较多适配器(建议 4 个以上)的 Hub 使用带外接电源的型号。供电不足时 Hub 会反复断开重新枚举,槽位不变,但串口会时有时无。
更换 Hub 的例子(端口号是示意值,实际以读到的槽位为准)
假设 7 口 Hub A 插在电脑的一个固定口上,路径前缀为 00.1400@2。它由两颗 4 口芯片级联:第一颗的前 3 个口直接引出,第 4 个口接第二颗芯片,第二颗的 4 个口再引出。适配器和绑定如下:
| 外壳上的口 | 槽位 | 绑定的控制台 |
|---|---|---|
| 第 1 口 | 00.1400@2.1 |
控制台 1 |
| 第 2 口 | 00.1400@2.2 |
控制台 2 |
| 第 3 口 | 00.1400@2.3 |
控制台 3 |
| 第 4 口 | 00.1400@2.4.1 |
控制台 4 |
| 第 5 口 | 00.1400@2.4.2 |
控制台 5 |
| 第 6 口 | 00.1400@2.4.3 |
控制台 6 |
| 第 7 口 | 00.1400@2.4.4 |
控制台 7 |
- Hub 坏了,换成同一型号并插回电脑的同一个口:内部结构相同,每个口的槽位完全相同。把适配器按原来的位置插回去,配置不用改,7 路控制台自动恢复。
- 换成另一个品牌的 7 口 Hub B:假设它把级联的那颗芯片接在第 1 个口上,各口的槽位变成:
| 外壳上的口 | 新槽位 |
|---|---|
| 第 1 口 | 00.1400@2.1.1 |
| 第 2 口 | 00.1400@2.1.2 |
| 第 3 口 | 00.1400@2.1.3 |
| 第 4 口 | 00.1400@2.1.4 |
| 第 5 口 | 00.1400@2.2 |
| 第 6 口 | 00.1400@2.3 |
| 第 7 口 | 00.1400@2.4 |
把适配器按外壳上的口位插回去,会出现两种后果:
- 找不到设备:控制台 1 绑定的是
00.1400@2.1,原来插在第 1 口的适配器现在在00.1400@2.1.1,没有对应的绑定,控制台 1 显示找不到设备。 - 接到别的设备上,而且不报错:控制台 2 绑定的是
00.1400@2.2,现在这个槽位对应外壳上的第 5 口。如果这个口上插着另一个设备,控制台 2 就会连到错误的设备。这是最危险的情况。
更换 Hub 的处理步骤
- 尽量换成同一型号,插回电脑的同一个口。
- 如果只能换成不同型号,换完后所有槽位绑定都要重新核对:先不要启用相关控制台,在"物理串口"页把适配器逐个插上,读出每个适配器新的槽位,再更新对应控制台的绑定。
- 配置完成后,向每个控制台各发一条测试数据,确认连到的是预期的设备。
- 适配器带有 USB 序列号的(FTDI、CP210x 等大多数都有),可以改用"按设备标识"绑定,这样更换 Hub、换口都不受影响。没有序列号的廉价芯片(很多 CH340)才需要依赖槽位。
- 在 Hub 上贴标签,写明每个口的槽位和对应的控制台编号,换设备或维修时对照使用。
串口绑定的 API
绑定通过 PATCH /api/consoles/{id}/serial 设置,需要 admin 或 engineer 权限。请求体是部分更新:至少提供一个字段,没提供的字段保持不变,null 和未知字段会被拒绝。
三种绑定方式的字段(互斥,只能设置其中一种):
| 方式 | 请求体字段 | 示例 |
|---|---|---|
| 设备名 | device |
{"device":"COM11"} |
| USB 端口位置 | slot |
{"slot":"00.1400@2.3"} |
| 设备标识 | vid、pid、serial_number |
{"vid":"0403","pid":"6015","serial_number":"D30J42Y6A"} |
设备标识的规则
vid、pid、serial_number三个必须同时提供,缺一不可。 同型号的适配器共用 VID 和 PID,只有序列号能区分,所以序列号不能省;只提供vid和pid会被拒绝。vid、pid各为 4 位十六进制(不区分大小写,保存时转大写),serial_number不能为空白。- 取值直接从串口列表(
GET /api/system/serial-ports)的usb_vid、usb_pid、usb_serial抄用。usb_serial为空的设备没有序列号,不能使用这种方式。 - 更新时三个字段要么都提供,要么都不提供;如果当前已经是设备标识绑定,只想改其中一项,也要把三项一起带上。
互斥与清除
- 设置一种非空绑定,会自动清除另外两种。
- 传空字符串清除对应绑定:清除设备标识时,三个字段都传
"",例如{"vid":"","pid":"","serial_number":""}。 - 同一个请求里同时给出两种以上的非空绑定会被拒绝,不会按先后顺序取舍。
- 只更新串口参数而保留绑定时,不要带
device、slot、vid、pid、serial_number,例如{"baudrate":115200}。
错误响应
| 状态码 | 原因 |
|---|---|
401 |
未登录 |
403 |
角色不足 |
404 |
控制台不存在 |
409 |
同一设备或同一槽位已绑定到另一个控制台(响应里说明是哪个控制台) |
422 |
绑定不合法:缺少 vid、pid、serial_number 中的任何一项、VID 或 PID 不是 4 位十六进制、同时设置了多种绑定方式、参数越界。响应的 detail 说明具体原因,例如 invalid serial configuration: vid, pid and serial_number must all be provided |
相关的只读字段
GET /api/system/serial-ports:每个串口的port_name、slot(槽位)、usb_vid、usb_pid、usb_serial(设备标识,没有序列号时usb_serial为空)。GET /api/consoles和GET /api/consoles/{id}的serial对象:device、slot、vid、pid、serial_number,没有使用的方式为空字符串。GET /api/consoles/{id}/status:configured_device、configured_slot、configured_vid、configured_pid、configured_serial_number是已保存的绑定;resolved_device是此刻实际打开的设备名。
保存后立即替换桥的串口打开方式:已打开的句柄保持原参数,下次连接尝试(包括自动重连)使用新绑定。找不到设备或匹配到多个串口时,控制台按串口故障处理并重试,状态里的错误提示区分“未找到”和“匹配到多个串口”。
按设备标识绑定(VID、PID、序列号)
除按 USB 端口位置绑定外,还可以按设备本身绑定:配置串口时同时填写 vid(厂商 ID)、pid(产品 ID)、serial_number(序列号)三个字段,例如 vid=0403、pid=6015、serial_number=D30J42Y6A。适配器插在哪个 USB 口都能找到,不依赖具体位置,也不要求不换线;代价是换了适配器要重新绑定。串口列表(GET /api/system/serial-ports)的 usb_vid、usb_pid、usb_serial 三个字段给出这些值,可直接抄用。控制台状态里对应的是 configured_vid、configured_pid、configured_serial_number。
规则:
vid、pid各为 4 位十六进制(不区分大小写,保存时转大写),serial_number非空。三个必须同时填写,缺一不可:同型号的多个适配器共用 VID 和 PID,只有序列号能区分。- 与
device、slot三选一。设置其中一种会自动清除另外两种;同一请求里同时设置两种以上会被拒绝。传空字符串可清除。 - 同一设备不能绑定到两个控制台(序列号比较不区分大小写)。
- 数据库中新增
serial_vid、serial_pid、serial_number三列,旧数据库启动时自动补列,原有配置不受影响。
两种位置/身份方式互补,不是替代:
按 USB 端口(slot,如 00.1400@2.3) |
按设备标识(vid、pid、serial_number) |
|
|---|---|---|
| 识别的是 | 这个 USB 口上现在的串口 | 这个适配器 |
| 换适配器 | 不用改配置 | 必须重新绑定 |
| 换接口、换线 | 槽位改变,必须重新绑定 | 不受影响 |
| 没有序列号的适配器 | 可用 | 不能用 |
不能覆盖的情况(这种方式不可能覆盖所有设备):
- FTDI 适配器的序列号带一个字母后缀。 VID、PID、序列号由 Go 串口库
go.bug.st/serial的枚举器提供,按它的值原样使用。Windows 上 FTDI 驱动会在序列号后追加通道字母,该库把它当作序列号的一部分,所以序列号为D30J42Y6的 FT230X 在 Windows 上显示为D30J42Y6A,多通道芯片的各通道依次为…A、…B……;Linux 上读到的是 USB 描述符里的原值D30J42Y6。因此 FTDI 设备的这种绑定配置在 Windows 与 Linux 之间不能通用,请使用串口列表里usb_serial实际显示的值。其他厂家(Prolific、Silicon Labs 等)两边一致。 - 该库读取失败时(它在任一端口读取失败时整体失败):本次枚举里所有设备的
usb_vid、usb_pid、usb_serial为空,按设备标识绑定的控制台暂时找不到设备并重试;按 USB 端口位置或设备名的绑定不受影响。 - 没有序列号的设备(很多 CH340、部分 PL2303 仿制芯片):
usb_serial为空,不能按设备标识绑定,请改用 USB 端口或设备名。只有 VID 和 PID 不够。Windows 上系统为无序列号设备临时生成的实例号(含&)不会被当作序列号。 - 序列号重复的设备:个别批次的适配器序列号相同。两个设备同时在线时,按该标识绑定会拒绝打开并提示 “configured USB slot matches more than one port”,日志列出各候选串口,不会任选其一。
- 一个设备带多个串口(FT4232H 等多口芯片):各口共用同一组 VID、PID、序列号,同样报歧义,请改用设备名。
临时禁用逻辑 Console
管理员或工程师可通过以下接口暂停或恢复已授权的逻辑 Console:
PATCH /api/consoles/1/enabled
Content-Type: application/json
{"enabled": false}
将 enabled 改为 true 即可恢复。该字段必须提供且为布尔值,缺失、null 或未知字段返回 Huma 参数校验错误(422)。
- 禁用立即断开该 Console 已进入串口桥接的会话,并拒绝 TCP、Telnet、SSH、WebSocket 的新转发会话;串口由 owner 结束当前操作后关闭。
- 原串口参数、绑定、连接上限及各协议开关保持不变,网络监听端口仍保留。若需要释放独立监听端口,请关闭对应协议。
- 只保存在内存,服务重启后自动解除临时禁用;配置重载和许可证变更均不会解除暂停。该接口不是持久化禁用配置。
- 列表和详情返回
enabled=false、disabled_reason="manually_disabled";状态返回state="disabled"、message="manually_disabled"。暂停期间仍可编辑配置。 - 重复启用或禁用是幂等操作,且会记录敏感操作审计。未授权 Console 返回 409,不能通过该接口绕过许可证限制。
禁用后仍运行哪些组件?
当前禁用是“暂停串口转发”,不是完全停止或卸载该 Console。 对于已启用的协议,具体行为如下:
| 组件 | 临时禁用后的行为 |
|---|---|
| 物理串口 | 取消串口会话,由 owner 结束当前操作后关闭;暂停期间不会重新打开 |
| 已进入桥接的客户端 | 断开,不再收发串口数据 |
| TCP / Telnet 监听器 | 继续监听并占用端口,新连接无法注册桥接会话并被关闭;Telnet 可能先执行初始协议协商 |
| SSH 监听器 | 继续监听并占用端口;客户端仍可能完成握手、认证或创建 session,但不能进入串口桥接 |
| 共享 WebSocket 服务 | 继续运行以服务其他 Console;拒绝该 Console 的新转发接入 |
| Console owner goroutine | 保留并等待,不执行新的串口打开操作 |
| 配置及管理接口 | 配置保留,仍可查询和修改 |
临时禁用不会关闭尚未进入桥接的 SSH 握手连接或待启动 session,这些连接仍受 SSH 连接上限、握手超时和 session 启动超时约束。因此,禁用不等于停止网络监听或禁止所有协议认证。
重新启用后,不会立即打开串口;新客户端成功进入桥接时才触发打开。临时禁用状态仅存内存,服务重启后解除。
若需要释放该 Console 的 TCP、Telnet 或 SSH 独立监听端口,请通过对应协议开关关闭监听器;恢复时需重新开启协议。关闭某个 Console 的 WebSocket 协议不会停止其他 Console 共用的 WebSocket 服务。
该接口及返回结构已写入 /api/openapi.json 和 /api/docs。可在网页对应页面操作,也可调用该接口。
访问协议与网络保护
| 协议 | 默认配置 | 认证与加密 |
|---|---|---|
| SSH | 启用,端口 6000 + 控制台编号 |
使用启用的本地用户账户认证,SSH 加密 |
| WebSocket | 启用,与 Web 界面共用端口(http_port,默认 80),路径 /console/<编号> |
按接口支持的认证方式接入;默认 ws:// 不加密 |
| Raw TCP | 默认关闭,地址 127.0.0.1,端口 5000 + 控制台编号 |
无应用层身份认证,明文 |
| Telnet | 默认关闭,地址 127.0.0.1,端口 7000 + 控制台编号 |
无应用层身份认证,明文 |
SSH 和 WebSocket 的默认地址为 0.0.0.0,访问时使用服务器的实际 IPv4;0.0.0.0 表示监听所有本地 IPv4 地址。TCP 和 Telnet 默认关闭,预设地址为 127.0.0.1,开启后默认仅允许本机访问;如需远程访问,应显式修改监听地址并配置防火墙。这些默认值用于新建配置及恢复默认,不覆盖已保存的配置。SSH 示例:ssh -p 6001 admin@<服务器IPv4>。
WebSocket 客户端可使用三种认证之一:X-API-Key 请求头、HTTP Basic(本地用户名与密码)或 Web 登录后的会话 Cookie(nbc_session)。Web 界面的终端使用会话 Cookie,不再需要先申请一次性 ticket,也已取消 POST /api/consoles/{id}/websocket-ticket 接口。三种方式按上述顺序判断,先出现的一种决定结果,凭据错误时不会改用其他方式。会话 Cookie 仅限管理员和工程师角色使用,并且只在来源校验通过(见下文)时才会被接受。具体请求以 /api/docs 为准。Web 登录并不为 Raw TCP、Telnet 增加认证。部分配置和状态 GET 接口可匿名读取,也应纳入网络访问控制。
WebSocket 来源校验与保活:
- WebSocket 不再有独立端口:它与 Web 界面共用端口(
http_port,默认80,或--addr指定的端口),地址为ws://<地址>:<Web 端口>/console/<编号>,页面与终端同源,不需要额外端口。是否对某个逻辑控制台开放 WebSocket,由该控制台自己的 WebSocket 开关决定。原先的独立端口(8001)和该端口上的/health已取消,监控请改用GET /api/system/health;接口返回的 WebSocket 监听器地址与端口即 Web 界面的地址与端口,PATCH /api/listeners/{id}不能修改它(返回 409),请改 Web 监听地址。配置库中保存的 WebSocket 主机与端口仅为兼容保留,不再校验,也不会产生端口冲突。 - 浏览器发起的 WebSocket 连接会校验
Origin,用于阻止其他网站的页面借用户浏览器连接串口:来源必须与请求的Host相同(同主机名和端口),即从本服务 Web 界面打开的终端。其他网站的页面会被拒绝。 - 不带
Origin头的非浏览器客户端(脚本、websocat等)不受此校验限制,仍需通过上述认证。 - 通过反向代理访问时,请让代理保留原始
Host头(如 Nginx 的proxy_set_header Host $host;)并转发 WebSocket 升级。若浏览器的Origin仍与Host不一致,可在配置文件settings.toml中设置websocket_allowed_origins,列出允许的来源主机(可含端口,支持*通配),例如:websocket_allowed_origins = ["console.example.com", "console.example.com:8443"]。修改后重启服务生效。 - 服务端每 30 秒向 WebSocket 客户端发送 ping,15 秒内无响应即断开连接并释放连接名额。
连接被拒绝时的提示(控制台连接数已满、已被暂停等):
| 协议 | 客户端看到什么 |
|---|---|
| SSH | 认证成功后打开 shell 时,终端显示 maximum connections reached(其他原因同样显示文字)。握手阶段连接过多时在握手前被关闭,无法提示,仅服务端日志有记录 |
| Telnet | 先显示 [bridge] maximum connections reached 等文字,然后断开 |
| WebSocket | 浏览器收到一条同样的终端文字,并以关闭码 1013(连接数已满)或 1008(其他拒绝)断开,关闭原因里也带有文字 |
| Raw TCP | 不发送任何文字,直接关闭连接:Raw TCP 传输的是串口设备自己的字节协议,插入文字会被上位机当作设备数据。原因请看服务端日志 |
WebSocket 先认证再判断控制台状态:未认证者一律收到 401,不会得知控制台是否存在。认证后若控制台不存在或未开放 WebSocket、超出授权或被手动暂停,浏览器(带 Origin 头)会先完成握手再收到上述文字,因为浏览器读不到握手失败的状态码和内容;脚本等非浏览器客户端仍收到 HTTP 状态码 404、403、409。
资源保护边界:
- SSH 的连接数分两部分单独计算:正在认证(握手阶段)的连接最多 16 条,未认证的连接不会占用控制台配置给正常用户的名额;认证成功的连接数不能超过该控制台的最大连接数(配置的 1–256 值,修改后对新连接立即生效)。认证成功但控制台已满的客户端仍能完成登录,打开 shell 时会看到
maximum connections reached,且该连接约 30 秒后被关闭。握手阶段超过 16 条时,新的 TCP 连接会被直接关闭。两类拒绝都会在日志中记一条记录(每秒最多一条),握手须在 10 秒内完成;每条连接最多 8 个 session,未启动的 session 最多等待 30 秒,PTY 请求不会延长期限。已启动的正常 shell 不受该启动超时限制,仍遵循控制台共享连接上限。 - Telnet 初始协商写入超时为 5 秒;空子协商、非法子协商命令或超过 1024 字节的子协商会终止连接,防止解析越界和缓冲无限增长。
- 网页端口(HTTP/WebSocket)同时最多保持 4096 个连接,超出的连接排队等待;请求头最大 64 KB,连接建立后 5 秒内没有发出完整请求头会被断开。每个浏览器终端占用一个连接。
- 网页接口同时处理的普通请求最多 512 个(不含 WebSocket 终端),超出的请求立即返回
503(Retry-After: 1),不排队。 - 登录和首次设置(
POST /api/auth/login、POST /api/auth/initialize)按来源地址限速:允许连续 30 次,之后每 2 秒恢复一次,超出返回429。这个限速比较宽松,因为经反向代理访问时所有用户共用代理的地址;真正防暴力猜密码的是上面"登录保护"里的账户锁定。程序不信任X-Forwarded-For。 - SSH、Telnet、Raw TCP 三类控制台连接合计最多 4096 个(各控制台自己的连接上限仍然适用),超出的连接排队等待,防止连接洪水耗尽进程的文件描述符。
- 没有为服务设置内存上限(systemd 的
MemoryMax):内存很小的设备上设了上限,正常使用时可能被误杀。需要的话可以在 unit 的 drop-in 里自行添加。 - 网页端口无法打开时(被占用、没有权限、
http_host不是本机地址),启动错误里会附带原因和处理办法。 - WebSocket 握手的资源保护:控制台被手动暂停时返回
409,超出授权时返回403,认证失败返回401,均发生在升级之前。 - 客户端取消或由服务端关闭时,会解除该客户端阻塞的串口入队等待;写队列持续满 30 秒仍无法入队时,也会结束对应客户端,避免在无法及时感知对端断开的情况下永久保留等待 goroutine。这些限制不替代防火墙或部署侧全局连接、内存预算。
限制防火墙和监听范围,优先使用 SSH;HTTP/WS 需要加密时启用内置 HTTPS(见“启用 HTTPS”)或配置 TLS 代理。原始串口数据可能包含目标设备密码等敏感内容,TLS 代理只保护经过该代理的连接。
启用 HTTPS
管理员可在「网络」页的「HTTPS 与证书」区域选择 PEM 格式的证书和对应私钥,点击「启用 HTTPS」。为避免私钥在网络上明文传输,上传只允许通过 HTTPS 或服务器本机的 localhost 页面进行。无图形界面的设备若当前通过远程 HTTP 管理,可先不选择证书文件,点击「启用 HTTPS」并确认使用自签名证书,保存后通过 SSH 或服务管理方式重启;随后用 HTTPS 地址打开同一页面上传正式证书。也可通过 SSH 安全传送证书文件并在服务器上配置。若反向代理已提供 HTTPS,通常无需再启用内置 HTTPS。首次启用或关闭 HTTPS 需重启服务,保存操作不会立即中断现有管理连接。通过页面管理的证书保存在数据目录的 web-tls.pem,应限制该文件的读取权限;续期时在同一页面更换有效证书,新连接会自动使用新证书,无需重启。
没有证书时可直接点击「启用 HTTPS」,确认使用自签名证书后启用;上传的证书或私钥无效时,也会改用自签名证书,并在页面标明。自签名证书保存在数据目录的 web-tls-self-signed.pem,重启后复用;浏览器会提示不受信任,正式对外使用建议换成可信证书。若配置文件指定的证书在启动时不可用,服务仍以自签名证书启动 HTTPS,不会退回明文 HTTP;原证书文件不会被覆盖。
网页界面、REST API 和 WebSocket 终端共用 Web 服务。HTTP 和 HTTPS 分别监听独立端口;开启 HTTPS 后,HTTP 入口会跳转至 HTTPS。在 settings.toml 中可以配置:
http_port = 80
https_enabled = true
https_host = "0.0.0.0"
https_port = 443
tls_cert_file = "server.crt" # 相对路径按配置目录解析,也可以写绝对路径
tls_key_file = "server.key"
https_enabled = false时只提供 HTTP;设为true后,HTTP 和 HTTPS 端口不能相同。未提供证书、只填一个文件、文件不存在或内容无效时,程序改用持久保存的自签名证书启动 HTTPS,不会退回明文 HTTP。- HTTP 与 HTTPS 独立启动:其中一个监听端口绑定失败或运行中退出,控制台会记录错误,另一个继续运行。HTTPS 不可用时,仍在运行的 HTTP 直接提供网页,不会跳转到失效的 HTTPS 地址;两个 Web 端口都无法启动时,服务启动失败。仅 HTTP 可用时请先排除 HTTPS 故障,不要在不可信网络上传输敏感信息。
- 旧配置若没有
https_enabled,但已设置证书和私钥,会继续按原来的单端口 HTTPS 方式运行;在网页重新保存 HTTPS 设置后才切换为独立端口。 - 开启后访问 HTTP 入口会跳转到 HTTPS 端口。最低 TLS 1.2,只使用 HTTP/1.1(WebSocket 终端依赖它)。
- 浏览器会自动改用
wss://连接终端;会话 Cookie 自动带Secure标志。 - 证书续期不需要重启:替换文件后,下一次新连接就会使用新证书。如果新文件无效,日志会记录一次错误,继续使用旧证书。已建立的连接不受影响。
- 正式对外使用建议准备公共可信证书(如 Let's Encrypt)或企业内部 CA 签发的证书。程序生成的自签名证书会触发浏览器警告,需要在客户端手动信任。
- 私钥文件应只允许运行服务的账户读取。
- 只覆盖 Web 端口。SSH 本身已加密;Telnet 和 Raw TCP 是明文协议,不受影响。
- 如果由 Nginx、Caddy 等反向代理终结 TLS,保持本程序使用 HTTP,并设置
secure_cookies = true。 - 「系统诊断信息」会显示 TLS 是否启用和证书文件状态。
登录保护
Web 登录、WebSocket 的 HTTP Basic 认证和 SSH 密码认证都走同一套保护,用来防止暴力猜密码,并避免登录请求占满 CPU 和内存:
- 失败锁定:同一个用户名连续失败 5 次后,第 6 次失败开始被锁定 30 秒,之后每多失败一次锁定时间翻倍,最长 5 分钟。锁定期间即使密码正确也会被拒绝(Web 接口返回
429,WebSocket 和 SSH 当作认证失败)。成功登录会清零;15 分钟内没有新的失败也会清零。 - 不存在的用户名同样计数:不会泄露哪些账户存在,用户名不区分大小写。
- 同时校验的数量有限:密码哈希很耗内存,同时最多校验 2 个,其余排队,等待超过 3 秒就拒绝,返回
429。 - 不会拖慢管理接口:密码校验在配置锁之外进行,登录请求再多也不会卡住配置读写。
- 数据库不可写时仍能登录:登录成功后"最后登录时间"写不进去(磁盘满、数据库只读)只记日志,不影响登录,管理员仍然能进来排查。
- 锁定状态保存在内存里,重启后清空。API Key 不受影响。被误锁时等待锁定结束,或用
--reset-admin-password重置后重启程序。
监听器状态与手动重试
Console TCP/Telnet/SSH、共享 WebSocket 初次绑定失败或 SSH 主机密钥准备失败不会阻止其他监听器启动。运行中 serve 意外退出或可恢复 panic 会将对应监听器标为 failed 并从活动监听器中移除,不退出整个程序。管理 HTTP 是必要服务,初次绑定或运行失败仍为致命错误。
管理员或工程师查询 GET /api/listeners,返回全部 145 项,含协议关闭和未授权的项;不要把“配置存在”当成“正在监听”。原 listener_id/protocol/console_number/host/port 字段保留,增加:
runtime 字段 |
含义 |
|---|---|
state |
running 已绑定并服务;failed 启动或运行失败;stopped 未运行;disabled 协议关闭;license_blocked 授权阻止;unavailable 运行状态能力不可用 |
last_error |
始终为字符串,无错误时为空;重试成功清除旧错误 |
retry_allowed |
是否允许发起重试,布尔值 |
running 不保证串口硬件可用或用户认证成功。手动暂停 Console 后仍运行的监听器保持 running,不是 disabled。WebSocket 是一个共享物理监听器,console_number=null,不能按 Console 分别重试。
# 使用已有 API Key;将示例值替换为实际值
curl -sS -H 'X-API-Key: YOUR_API_KEY' http://127.0.0.1:80/api/listeners
# 从列表取 listener_id;下面假设目标 ID 为 1,无请求体
curl -i -X POST -H 'X-API-Key: YOUR_API_KEY' http://127.0.0.1:80/api/listeners/1/retry
重试使用最新持久化配置,只操作请求的物理监听器,不修改设置。已运行时直接幂等返回,不重启、不断开客户端。未知 ID 返回 404;协议关闭、授权阻止或不可重试返回 409;运行层不支持状态/重试返回 503。绑定尝试失败仍为 HTTP 200,例如(ID、地址和错误内容仅作示意):
{
"listener_id": 1,
"protocol": "tcp",
"console_number": 1,
"host": "127.0.0.1",
"port": 5001,
"runtime": {
"state": "failed",
"last_error": "listen tcp 127.0.0.1:5001: bind: address already in use",
"retry_allowed": true
}
}
客户端应先处理非 200 的请求错误,再检查响应 runtime.state:只有 running 表示监听成功;failed 应展示 last_error,排除端口占用、地址或密钥文件问题后再手动重试。成功返回 HTTP 200、state="running"、last_error="",不能仅以 HTTP 200 判定恢复。
相同 host 的 PATCH /api/listeners/{id} 不触发重试,应调用 POST。配置重载或新许可证应用也可按启用配置再次尝试,但没有自动定时重试。restart_required / service_restart_required 与监听器失败无关。许可证 runtime_error 是最近一次许可证应用的部分失败报告,最新监听器状态以列表为准。
接口及 Schema 已提供于 /api/openapi.json、/api/docs,可用于前端状态与重试按钮;网页“网络”页面提供状态与手动重试入口。重试动作纳入敏感操作审计,仅记录身份、目标和结果等元数据,不记录终端内容。
配置导入、恢复默认与备份
管理员通过 PUT /api/configuration/ 导入完整 48 项控制台配置。提交前检查设备或槽位重复、串口参数及协议监听冲突,并备份原配置。接口结构见 /api/docs,控制台列表可通过 GET /api/consoles 获取。
导入在数据库事务中保存,然后立即尝试重载运行配置;会调整连接上限、下次打开的串口配置及协议监听器,可能中断连接。当前重载不是整体原子切换,重载错误不会通过保存响应返回。因此成功响应只证明配置保存成功,不能证明所有监听器已经应用。导入后检查 GET /api/listeners、启动/运行日志及控制台状态;排除监听故障后可单独 POST 重试,再验证实际连接。当前系统重启标志不能作为导入是否全部生效的依据。
POST /api/configuration/restore-defaults 恢复控制台默认配置并立即尝试重载,同样可能影响连接;不重置用户、API Key 或许可证。
网页上叫“清除所有串口配置”(“系统”页的“危险操作区”,仅管理员),它不是恢复出厂设置:只把全部 48 个逻辑串口的备注、硬件绑定、串口参数、连接数上限、协议开关和监听地址恢复为默认值(见“初始化都做什么”里的默认配置)。不会动的有:用户和密码、API Key、审计记录、许可证和维护令牌、settings.toml(密钥、Web 监听地址、device_uuid)、SSH 主机密钥。执行后需要重启服务才能完全生效,操作无法撤销,建议先导出配置备份。
要把账户、审计记录等一起清除,用命令行的“恢复出厂设置”(见“恢复出厂设置”一节)。
完整备份应在停止服务后复制 SQLite 数据库、settings.toml、SSH 主机密钥和许可证文件;如仍有 SQLite -wal、-shm 文件,应一并保留,或采用可靠的 SQLite 一致性备份方式。仅导出控制台列表不包含账户、API Key、审计或启动密钥。
恢复时停止服务,按原路径还原完整文件集,限制文件权限,再启动验证。丢失 api_key_pepper 会使原 API Key 无法验证;变更 secret_key 会使原登录 Cookie 无法使用;变更 SSH 主机密钥会触发客户端主机身份警告。数据库及备份包含敏感认证数据,不应作为普通配置附件公开发送。
许可证申请与安装
未安装许可证时,免费授权允许运行逻辑控制台 1–2。当前直接售卖的商业授权为 8P、16P、48P 三档,其他档位不直接售卖;实际可用容量以签发的许可证为准:
| 商业授权版本 | 最大逻辑串口使用数量 | 允许使用的逻辑控制台编号 |
|---|---|---|
| 8P | 8 | 1–8 |
| 16P | 16 | 1–16 |
| 48P | 48 | 1–48 |
授权容量是总上限,不是新增数量,也不是从 48 个编号中任选相应数量。例如 8P 只允许使用逻辑控制台 1–8,不是在免费版基础上再增加 8 个;升级到 16P 后,总范围变为 1–16,而不是再增加 16 个。启动时全部 48 路已存储配置均创建 SerialBridge 和常驻 owner;其余未授权控制台保留实例及配置,但拒绝接入、不打开串口、不启动 TCP/Telnet/SSH 独立监听器。共享 WebSocket HTTP 服务继续运行,并逐控制台检查授权。没有客户端时,已授权控制台也不打开硬件串口。容量不是网络监听端口数,也不是客户端连接数。
获取许可证的三种方式
许可证文件由许可证服务器签发,续期也重新签发该文件。本程序只负责向服务器申请、验签和保存。
| 方式 | 适用场景 | 做法 |
|---|---|---|
| 在线激活 | 设备能访问许可证服务器 | 管理员调用 POST /api/system/license/activate,请求体 {"code":"ACT-23456-789AB-CDEFG-HRZQE"}。程序把激活码、本机的设备标识(只有哈希)、主机名和程序版本发给服务器,验签后保存并立即生效。同一台设备重复激活不占新名额 |
| 离线激活 | 设备不能上网 | 管理员生成离线请求,每份请求只能成功使用一次;在联网电脑上传至官网,下载授权文件并导入。设备时钟不影响激活,created_at 原文参与 nonce 校验,生成后不要修改。 |
| 手动导入文件 | 已有许可证文件 | PUT /api/system/license 提交文件内容,或停机后把文件放到 license_file 指定的路径 |
激活确认框会说明:激活会把这台设备绑定到授权上,并上传本机的设备标识哈希和系统架构用于识别;激活错了,72 小时内可在官网撤销,其他换机需要联系卖家。
激活码用完即可丢弃,不要保存在设备上,续期也完全不需要它。
文件格式与大小
许可证文件和离线请求文件都是 UTF-8 纯文本 JSON(不带 BOM)。本机导出的文件统一命名为 cs-YYMMDDHHmmss.license.txt 和 cs-YYMMDDHHmmss.request.txt(生成时的本地时间);激活和续期使用同一种授权文件。程序内部仍按 license_file 配置保存授权,默认是 license.txt。
- 服务端签发的文件请原样保存,不要用编辑器重新排版或另存为其他格式。签名只覆盖其中的
payload,改动payload或sig会导致验签失败。 - 大小上限 8192 字节,与许可证服务器一致。超过的内容不是真正的许可证文件:通过接口导入超限时返回
413或422(按校验路径),放在许可证路径下的会被判为无效,令牌文件超限会被忽略。 - 网页选择
.txt授权文件;旧.lic文件不再支持。按内容提示请求文件类型选错的情况,旧.token也不再接受。后端验签后再核对类型。自动化需要保留原文时使用POST /api/system/license/import-file的file字符串;旧PUT /api/system/license签名信封接口仍兼容。
许可证服务器地址
在线功能连接的服务器由配置文件 settings.toml 中的 license_server 指定,格式是 host:port 或完整的 http(s):// 地址。当前程序默认地址是 https://license.betterone.top:8443。设为空字符串则关闭在线激活和续期检查,只能手动导入文件。旧配置文件没有这一项时使用程序默认值;已有配置中显式写出的地址不会自动替换。修改后需重启程序。
服务器不可达或回复异常时接口返回 502;没有配置服务器时返回 503。
设备标识
许可证绑定一个设备标识 device_id,由三个来源组成。请求、许可证文件里只有它们的 SHA-256 哈希(加产品盐和类型),不含原始值(唯一的例外是管理员主动生成的“系统诊断信息”,其中有明文,见该节):
| 字段 | 来源 | 是否一定有 |
|---|---|---|
hardware |
主板 UUID(Windows 读 SMBIOS,Linux 读 DMI product_uuid);没有时用 CPU 序列号(树莓派等 ARM 板) |
否 |
install |
系统安装 ID(Windows 的 MachineGuid,Linux 的 /etc/machine-id) |
否 |
generated |
软件首次安装时生成的随机 UUID v4,保存在配置文件或数据目录的运行时身份文件,并尽可能另存一份机器级副本(见下) | 是 |
- 激活请求还会带上
platform(操作系统和架构,如linux/arm64),只作参考,不参与匹配。请求里还带device_name(本机主机名,读不到时为空),同样只是方便人工识别设备的标签,不参与匹配。不再使用网卡 MAC。 - 每个来源都严格校验,不合格的视为不存在:
- 主板 UUID 必须是标准 36 位 UUID,不能是全 0、全 F、同一字节重复,或主板厂商常见的默认值(如
03000200-0400-0500-0006-000700080009)。 - CPU 序列号必须是 8 到 32 位十六进制,不能是同一字符重复(很多没有序列号的板子会显示全 0)。
- 安装 ID 去掉连字符和花括号后必须正好是 32 位十六进制,不能是同一字符重复,也不能是
uninitialized。 device_uuid必须是小写的 v4 UUID。它在首次初始化时生成,之后一直沿用;格式不对时程序拒绝启动。只读配置缺少此项时写入数据目录的runtime-identity.toml;机器级副本还在时也可用于恢复。
- 主板 UUID 必须是标准 36 位 UUID,不能是全 0、全 F、同一字节重复,或主板厂商常见的默认值(如
- Linux 上
product_uuid只有 root 能读。安装包的 systemd 服务会在启动前把它复制到/run/betterone-console-server/product_uuid供服务账户读取;手动以普通用户运行时读不到,hardware就缺失。
匹配规则:
- 许可证有
hardware:只比较hardware(主板 UUID 或 CPU 序列号)。重装系统、丢了配置都不影响;换主板、把树莓派的 SD 卡克隆到另一块板子则不匹配;本机读不到硬件标识时也不匹配,不会退到别的来源。 - 许可证没有
hardware(虚拟机、容器等读不到硬件标识的机器):install和generated两项都必须存在并且都与本机一致。本机既没有hardware也没有install时(例如没有machine-id的容器或精简系统),无法识别身份,不能激活;generated只是配置文件里的一个字符串,拷走文件就拷走了身份,所以不能单独作为依据。这样由同一个模板克隆出来的机器(系统安装 ID 相同,但各自首次启动时生成的 UUID 不同)不会共用一份许可证。代价是这类机器重装系统或配置被重建后需要重新激活。
generated 的保存:首次创建配置时 UUID 写入 settings.toml;只读配置缺失或与机器级值不一致时,改写数据目录的 runtime-identity.toml,不修改管理员所有的基础配置。程序也尽可能另存一份独立于运行模式的机器级文件:
| 系统 | 机器级文件 |
|---|---|
| Linux | /var/lib/betterone-console-server/device_uuid(作为系统服务运行时就是数据目录里的文件) |
| Windows | %ProgramData%\BetterOne\ConsoleServer\device_uuid |
- 启动时使用已有 UUID;都不存在才新生成。机器级文件与配置值不一致时以机器级文件为准,结果持久化到数据目录而非改写只读配置。
- 机器级文件写不进去(例如普通用户在 Linux 上直接运行,没有权限写
/var/lib)不算错误,此时由配置或数据目录里的运行时身份文件保留 UUID。 - 机器级文件的内容不是小写 v4 UUID 时程序拒绝启动,与配置文件里的
device_uuid同样处理。 - 可以用环境变量
NBC_DEVICE_UUID_FILE指定别的路径,设成off则关闭这一份;测试和特殊部署才需要。 - 卸载软件时这个文件和配置文件一样不会被删除。要重新来过(接受重新激活),需要把两处都删掉。
- 重装操作系统会丢掉它,这时靠
hardware(有的机器)找回身份,没有hardware的机器需要重新激活。
整机克隆(虚拟机克隆、整盘复制)会复制全部来源,无法在本机识别,需要靠服务器限制重复激活。复制配置文件或机器级文件也会复制 generated。需要换机请联系卖家。
安装与生效
POST /api/system/license/import-file 接受许可证原文(type 必须为 license),降档时先要求确认。安装接口验证并原子保存有效文件后立即协调运行状态,无需重启,响应 restart_required=false。升级只启动新增授权控制台已配置为启用的协议;降级会断开超出授权范围的桥接会话、停止串口并关闭各自的 TCP/Telnet/SSH 监听器,共享 WebSocket HTTP 服务保留并逐控制台检查授权。手动禁用状态不会被许可证变更解除。无效文件或保存失败保持原许可证和运行状态不变。
响应可含管理员专用的可选字符串 runtime_error:表示许可证已成功安装,但监听器应用未全部完成,不是安装失败或必须重启;非管理员不会收到该字段。该字段是最近一次许可证应用的部分失败报告,不随单监听器重试实时更新;最新状态以 GET /api/listeners 为准。管理员应核对授权容量及日志,排除故障后可通过 POST /api/listeners/{id}/retry 单独重试,也可配置重载或重新导入许可证按启用配置再次尝试;没有自动定时重试。
验签用的公钥按文件里的 kid 选择。程序只认内置的公钥表,不会从网络上取公钥;表里没有的 kid 按文件无效处理(原因 unsupported_key),需要升级到内置了新公钥的版本。
维护期与升级
授权是永久的。许可证里的 maintenance_until 只决定能升级到哪些版本,不是软件运行到期日。
有效到期日 = 许可证.maintenance_until
版本可用 = 版本构建日期 <= 有效到期日
比较的是软件的构建日期,不是当前时间,也不联网。构建日期等于到期日的版本可用,晚一天就不可用。到期后原版本照常运行,只是构建日期更晚的新版本不能升级。
安装构建日期晚于有效到期日的新版本并启动后,这个新版本的商业授权不生效,按免费 2P 运行,只允许使用逻辑控制台 1–2。这不是把授权降回去:许可证仍然有效,发布日期不晚于到期日的版本仍然可以完整使用全部端口。 原授权范围内编号 3 及以上的逻辑控制台仍保留 SerialBridge 和常驻 owner,但拒绝接入、不打开物理串口、不启动独立协议监听器,也不能建立 WebSocket 转发会话。已有配置仍保留,不会被删除。这时许可证都还保留着:状态里 state 为 update_not_covered(不是错误,valid 仍为 true),licensed_ports、license_id、maintenance_until 仍然可见;导入或换取一个覆盖该版本的最新授权文件后立即恢复,无需重启。
例如:有效到期日为 2027-10-01,构建日期为 2027-10-01 的版本仍在覆盖范围内;安装构建日期为 2027-10-02 的版本后,即使此前持有 48P 授权,启动后也只能使用编号 1–2,直到续期。
换机、重装、激活错设备
程序本身不提供释放、解绑或停用本机的功能。 客户端在用户手里,把批量解绑的入口做进去,等于交给破解者;而且设备上已有的授权文件释放后仍然有效,释放只是服务器上名额的账面操作,所以必须放在有订单验证、失败锁定和次数上限的官网上。
| 情况 | 谁来操作 | 怎么做 |
|---|---|---|
| 设备激活后 72 小时内激活错了 | 买家自己 | 到官网“查看设备”页,用激活码加订单号(或邮箱)验证后撤销。每个授权的免费撤销次数有限 |
| 超过 72 小时、次数用完、或授权没有登记订单信息 | 卖家 | 联系卖家,由卖家在管理控制台释放 |
撤销之后,买家到新设备上按正常流程激活即可,程序不需要知道撤销这件事。
GET /api/system/license 返回 portal_url(设备管理页地址,任何人可见;地址确定前为空,界面在为空时不显示入口),界面应在“授权”页提供入口,并写一句“更换电脑或激活错了设备,请先在官网撤销旧设备”。激活遇到 seats_full、续期遇到 device_released 时,接口的错误消息会附上说明,设备管理页地址确定后还会附上地址。
维护期按设备计算
每台设备从自己首次激活起算维护期,续期只延长这一台设备的维护期。显示和升级校验以本机授权文件中的日期为准,不假设同一激活码下的设备到期日相同。
许可证状态字段(给界面和脚本)
GET /api/system/license 的 state 已经把所有检查合并成一个值,界面只需要判断它,不用再去比较日期或匹配错误文字:
state |
含义 | 界面建议 |
|---|---|---|
free |
没有安装许可证,正常的免费版(2 个端口) | “当前使用免费版”,按 actions.activate 显示激活入口 |
licensed |
许可证有效,并覆盖此版本 | 显示档位和维护到期日 |
update_not_covered |
许可证有效,但此版本发布于维护期结束之后,此版本按免费版运行。不是错误,授权没有被降回去:发布日期不晚于 maintenance_until 的版本仍然可以完整使用 licensed_ports 个端口 |
提示“此版本(build_date)超出维护期(到 maintenance_until),可续期,或改用维护期内的版本”,按 actions.renew 显示续期按钮 |
invalid |
有许可证但不能使用 | 按 reason(device_mismatch、signature_invalid、unsupported_key、file_too_large、unreadable、license_invalid、device_unavailable)显示对应文案 |
“激活与续期”共用一个输入框,按 ACT / RNW 码类型自动识别操作,已激活后也可输入新激活码更新授权;未配置服务器时在线按钮禁用,可下载对应的离线请求。离线请求下载与许可证导入按钮位于同一输入框下方。替换授权可能改变可用端口范围,请确认后再操作。
其余字段:edition 是显示用的档位名;max_ports 是此版本现在实际可用的端口数;licensed_ports 是许可证授予的端口数;actions 里的 activate、renew 表示对应在线操作是否可用(需要配置了 license_server)。valid 已废弃,只在 invalid 时为 false。reason、licensed_ports、actions 和各种详情只有管理员能看到;匿名请求只有 state、edition、max_ports、build_date 等足够画提示条的内容。
续期:检查最新授权文件
“检查维护续期”使用本机已保存的授权原文与设备身份调用服务端 POST /v1/license/refresh,返回新的 license_file。不需要激活码,且不受重新激活间隔限制;新文件保持授权 ID、设备与端口数不变,只更新维护期、签发时间等。
同一授权只有维护期更晚才替换;日期相同或更早不写入,提示当前日期。被卖家释放的设备不能获取新文件,但原文件仍可继续使用已覆盖版本。
离线续期
设备不能联网时,到能上网的电脑上通过官网“离线激活 / 续期”页完成。官网只收一个文件,按文件内容自动识别,不用在网页上填续期码:
- 准备文件,拷到联网电脑:
- 买了续期码:在“激活与续期”输入 RNW 续期码,点击“生成续期请求文件”(文件名以
.renewal.txt结尾);在线兑换连接失败时,也可按提示确认生成离线请求; - 卖家已经在后台续期:在“激活与续期”展开“设备无法联网?离线续期”,导出本机授权文件(文件名以
.license.txt结尾),用于获取服务器上最新的授权文件,不会兑换续期码。
- 买了续期码:在“激活与续期”输入 RNW 续期码,点击“生成续期请求文件”(文件名以
- 打开官网“离线激活 / 续期”页(
/#offline,旧地址/#renew打开的也是这一页),上传上一步的文件。 - 从官网下载新的
.license.txt授权文件,拷回本机。 - 使用“导入许可证”选择文件或粘贴原文,验证后安装。每台设备分别操作。
导入会验签并匹配本机设备。同一授权的旧文件不倒退日期;不同授权可以替换,但新文件端口数更少时须先确认降档。取消不改原文件。当前端口数取已购买授权,不是免费回退路数。文件最大 8192 字节,选择文件不会自动安装。
管理员 API:GET /api/system/license/export 导出原文(不缓存);POST /api/system/license/import-file 接受 { "file": "原文", "allow_downgrade": false }。只在用户明确确认后改为 true 重发原文件。无新日期不庆祝,续期成功保留烟花,激活成功保留礼花。
输入续期码
激活码为 ACT- 前缀,续期码为 RNW- 前缀,后接 20 个字符(16 位随机部分、4 位校验位)。只接受 23456789ABCDEFGHJKMNPQRSTUVWXYZ;规范化时忽略大小写、连字符、空格和制表符。可以省略前缀,此时按两种前缀分别验证校验位以识别种类。激活与离线请求框拒绝续期码,续期框拒绝激活码;错误码或校验位无效时不会向授权服务端发请求。
授权页提供“兑换续期码”:使用本机授权文件及设备身份向授权服务端 POST /v1/renew 兑换。一个码续一台设备,码的路数不能低于授权路数。兑换后自动验签并导入新的授权文件,无需再点检查续期;兑换不可撤回或转移。
本地接口为 POST /api/system/license/redeem-renewal,请求 {"renewal_code":"RNW-23456-789AB-CDEFG-H3G4C"},仅管理员可用。重复兑换同一码安全,already_redeemed=true 表示不重复增加年数或扣次数。代码只用于此次请求,不保存。
续期码覆盖的路数高于本机授权时,软件会弹出确认框,显示续期码和本机各自的路数,并说明兑换后不能撤回或转给其他设备。确认后才携带 confirm_ports: true 重新兑换原码;取消不会再次请求,续期码未被使用。路数相同直接续期,路数不足则提示规格不符且码未使用。续期只延长维护期,不增加授权路数;路数比授权多的续期码不会带来额外的路数或其他好处,续期时长和路数相符的码一样,请确认买对了续期码。
维护期按设备独立计算,同一激活码下各设备的维护日期可以不同;日期一律以本机签名授权文件为准。续期后的维护期最多到“今天 + 5 年”,超过时会提示维护期已经很长,请过段时间再续,续期码没有被使用。客户端不自行预测续期后的日期;自然月和月末规则由服务端处理。
文件导入使用 POST /api/system/license/import-file,请求 {"file":"<完整文件文本>"},保留原始文本;旧 PUT /api/system/license 信封接口继续可用。
在线接口的错误
| HTTP | 含义 |
|---|---|
| 400、403、409、422、429 | 许可证服务器的拒绝,状态码原样保留,消息里带服务器的错误码,例如 seats_full(设备数已满,消息会提示 72 小时内激活错的设备可在官网撤销)、code_voided(授权已作废)、code_expired(超过激活截止日期)、device_released(设备已被释放)、invalid_license、rate_limited(失败次数过多或请求太频繁,同一激活码或 IP 连续失败 5 次,首次锁定 3 分钟,之后按服务端规则递增) |
| 422 | 激活码格式不对;服务器认为激活码不存在或无效(invalid_code,服务器返回 404,这里统一为 422);文件验签失败、不是本机的许可证等 |
| 409 | 没有安装许可证就检查续期 |
| 502 | 服务器不可达,或回复格式不对 |
| 503 | 没有配置 license_server |
“激活码不存在”和“格式正确但无效”对外都是 invalid_code,这是为了防止被用来试探,请不要想办法区分。
升级前
升级前请核对维护覆盖日期,并停机备份原程序、数据库、配置、SSH 主机密钥和许可证,避免超出授权范围的串口业务中断。要使用维护期之外的新版本,请联系作者续期,然后用“检查续期”换取最新授权文件,立即生效,无需重启。若回退原版本,请先确认数据库和配置兼容性,必要时恢复升级前的备份。
更新维护期结束后,用户选择继续使用旧版本,应自行承担旧版本缺陷、安全漏洞、兼容性问题及其引发的数据损失或业务中断风险。在适用法律允许的最大范围内,作者不对由继续使用旧版本导致的问题及损失承担责任;适用法律规定不得免除的责任除外,另行书面约定的维护或支持义务仍按约定执行。 建议及时评估风险、做好备份,并按需联系作者续期。
设备 UUID 或 Serial 改变时,需联系开发者核实迁移或补发;软件没有自动迁移功能。
串口状态提示
已配置设备绑定后,串口打开失败或运行中断线时,TCP、Telnet、SSH 和 WebSocket 客户端会话会在重试窗口内保持;此期间输入直接丢弃,不会缓存到重连后的串口。未配置设备名或 USB 槽位时,不进入持续重连流程;重试超过 300 秒仍未恢复时关闭客户端会话。
服务端会向所有已连接客户端写入以下状态提示:
| 触发条件 | 客户端收到的原始文本 |
|---|---|
| 首次打开失败或运行中断线 | \r\n[serial] device unavailable; input will be discarded while reconnecting\r\n |
| 持续无法连接 | 同上,最多每 30 秒发送一次 |
| 断线后重新连接成功 | \r\n[serial] device connected\r\n |
| 重连超过 300 秒仍未恢复 | \r\n[serial] reconnect timeout; closing session\r\n,发送后关闭客户端会话 |
首次正常打开成功时不会打印 device connected;该提示只用于发生故障后的恢复。
以上状态提示会直接进入客户端数据流,并不来自物理串口设备。使用 Raw TCP 或 WebSocket 传输纯二进制协议时,客户端必须能够识别并处理这些状态文本。
监听器故障与硬件故障是两个诊断层:GET /api/listeners 检查网络绑定/服务;GET /api/consoles/{conf_id}/status 检查串口打开、读写及重连。监听器为 running 时串口仍可能不可用;POST 监听器重试不会修复驱动、线缆或设备占用,也不会重新启动已运行监听器。下述串口自动重连不等于监听器自动定时重试。
串口打不开:权限与占用
GET /api/consoles/{conf_id}/status 的 last_error 和网页上的串口状态会写明原因:
| 提示 | 含义 | 常见于 |
|---|---|---|
serial device access denied |
运行程序的账户没有读写这个串口设备的权限 | Linux |
serial device is busy |
设备被别的程序占用 | Windows、Linux |
serial device is unavailable |
设备不存在:没插、设备名写错、USB 转串口线被拔掉,或驱动没装好 | Windows、Linux |
Linux
串口设备(/dev/ttyUSB0、/dev/ttyACM0、/dev/ttyS0 等)一般属于 root:dialout、权限 660,只有 root 和 dialout 组能读写。deb/rpm 安装包会把服务账户 betterone-console-server 加进 dialout 组(RPM 系发行版是 uucp),所以按安装包使用时通常不会遇到权限问题。以下几种情况仍会遇到:
- 树莓派板载串口(GPIO 上的
/dev/ttyAMA0、/dev/ttyS0、/dev/serial0):系统默认把它当作登录控制台,设备属于root:tty,而且被登录程序占着。USB 转串口线(/dev/ttyUSB0)不受影响。 - 系统里既没有
dialout组也没有uucp组(少数精简系统),安装脚本无组可加。 - 不用安装包、自己用普通用户直接运行程序,而这个用户不在
dialout组。 - 自定义的 udev 规则把设备权限改得更严。
先确认原因:
ls -l /dev/ttyUSB0 # 设备的属主和组,通常是 root dialout
id betterone-console-server # 服务账户在不在 dialout 组
journalctl -u betterone-console-server -n 50 # 打开串口时的具体报错
sudo fuser /dev/ttyUSB0 # 设备被哪个进程占用
临时处理办法(按上面的原因选):
| 原因 | 处理 |
|---|---|
服务账户不在 dialout 组 |
sudo usermod -aG dialout betterone-console-server,再 sudo systemctl restart betterone-console-server。必须重启服务,组变更才对它生效 |
| 自己用普通用户运行 | sudo usermod -aG dialout <用户名>,然后退出登录再登录(或重启),再启动程序。树莓派默认的 pi 用户已经在这个组里 |
| 树莓派板载串口被当作登录控制台 | sudo raspi-config → Interface Options → Serial Port:“login shell over serial” 选 No,“serial port hardware enabled” 选 Yes,然后重启树莓派。之后设备属于 dialout 组(不同系统版本的菜单名可能略有不同) |
系统没有 dialout/uucp 组 |
看 ls -l 显示的设备所属组,把服务账户加进这个组(sudo usermod -aG <组名> betterone-console-server),再重启服务 |
| udev 规则太严 | 改规则,或加一条规则让设备属于 dialout 组、权限 660,然后 sudo udevadm control --reload && sudo udevadm trigger,重新插拔设备 |
| 设备被其他程序占用 | 用 sudo fuser 找到占用它的进程,关掉它(例如另一个串口工具,或树莓派上的串口登录控制台) |
不要用 chmod 666 /dev/ttyUSB0 这类办法:设备重新插拔或系统重启后就失效,而且会让所有用户都能读写串口。
Windows
Windows 的 COM 口没有 Linux 那样按组划分的权限,默认所有用户都能打开,所以一般不会出现 serial device access denied。打开时系统报“拒绝访问”,实际意思是这个 COM 口已经被别的程序打开了,程序会显示为 serial device is busy。
| 提示 | 处理 |
|---|---|
serial device is busy |
关掉正在使用这个 COM 口的程序,例如 PuTTY、SecureCRT、厂商调试工具、其他串口软件,或另一份正在运行的本程序。同一个 COM 口同一时间只能被一个程序打开 |
serial device is unavailable |
打开“设备管理器” →“端口 (COM 和 LPT)”,确认设备在列表里、COM 编号和配置一致;带黄色感叹号说明驱动没装好,装好厂商驱动(如 FTDI、CH340、CP210x)后重新插拔;也可以用按 USB 物理位置或设备身份绑定,避免 COM 编号变化 |
少数企业电脑通过组策略禁用了串口或 USB 串口设备,这种情况需要请电脑管理员放开,程序这边无法处理。
串口故障边界与硬件验收
串口驱动调用中的可恢复 panic 会转换为错误,服务日志记录堆栈;空句柄(含 typed nil)及非法读写计数会被拒绝。部分写入会继续写剩余数据;读取同时返回数据和错误时,先转发当前有效数据,再处理故障。立即返回的空读取有可取消的短等待,避免忙循环;重试等待不超过剩余窗口,到期后不再发起打开尝试。每次打开使用独立身份,旧读取及旧句柄排队命令不会混入新连接,取消后的剩余写入不会在重连后重放;已交给硬件的字节无法撤回。
Windows/Linux 使用本地原生串口 I/O,写超时默认 5 秒,Bridge 配置读超时 250 ms;正常读超时仅表示暂无输入,不应触发重连。停止时 CancelIO() 永久停止当前句柄的新操作并请求取消,不清空缓冲区,也不表示物理释放。owner 等待在途操作结束再关闭;Windows 必须确认 overlapped 完成后才能释放事件和缓冲区,串口或每次 I/O 的事件句柄关闭失败均保留,由后续 Close 重试;只有所有自有 OS 句柄清理完成才标记 Released。Linux 使用非阻塞 poll/取消信号及 100 ms 取消检查兜底,等待活动操作后先解除独占,再关闭 raw fd 一次,不在 EINTR 后重试数字 fd。
打开失败也可能留下存活句柄,仍须清理。Bridge 在任何下一次 opener 前清理旧句柄;释放未确认时隔离保留,不能盲目反复打开。暂停、许可证控制或非阻塞 Bridge Close() 返回,以及状态显示 idle/disabled,都不是物理释放证明。超时/context/取消不能强制异常驱动完成取消或关闭;可能需要维护设备/驱动、重启服务或系统,不保证绝对限时释放。owner 使用单个定时器按 retryInterval 重试保留句柄的 Close,即使没有客户端、手动禁用或 Bridge 永久关闭也继续清理;确认释放前不打开新句柄,但重试不是驱动强制释放保证。Windows/Linux Drain 轮询等待输出队列为空,不保证硬件移位寄存器发送完成;Break 可取消。旧测试替身的 reset fallback 不是生产取消策略。实现也不保证抵御 OOM、致命运行时错误或操作系统故障。
真实硬件验收尚未完成。 编译后的 Linux internal/serial PTY 套件已在 WSL Ubuntu 实际运行 3 次 PASS;实际 ConfiguredSerialOpener + Bridge 的 Linux PTY 集成测试运行 10 次 PASS,覆盖读写、700ms 空闲无意外重连、每次运行 10 轮 teardown/Released/重开、最后客户端离开/手动禁用/许可证撤销、原生初始化错误清理和描述符泄漏检查。Windows syscall seam mock 测试已实际运行通过。本轮未启用 race;这些 WSL PTY 测试不是物理 USB 驱动测试或原生 Linux 发布验收,后两者仍待完成。可复现 Linux PTY 命令见 internal/serial/README.md;这些测试不替代目标硬件验收。
上线前使用真实设备、独立的正常 Console 和服务日志进行以下人工验收(驱动阻塞/错误可配合故障注入环境):
- 通信中拔掉线缆:检查状态提示、错误及重试;重新插入后验证恢复,断线期间输入和旧排队命令不重放。
- 绑定不存在或被其他程序占用的设备:检查打开失败、重试等待及到期关闭会话,到期后无额外打开尝试;解除占用后重新接入验证恢复。
- 制造串口读/写失败:核对日志与状态;读取带错误的有效尾部数据仍先转发,部分写入正常续写,失败或取消后的剩余命令不重放。
- 持续输出并接入不读取数据的慢客户端:检查正常客户端和其他 Console 仍可通信,确认慢客户端达到 64 KiB/512 帧输出队列上限或单次写入 10 秒超时时被断开并记录原因,而不是静默丢弃数据。SSH 关闭的是该客户端底层连接,该连接上的其他会话也会结束。
- 持续写入时反复禁用、变更许可证或关闭 Bridge,注入取消完成/关闭失败:检查控制 API 返回不被误判为释放、其他 Console 仍正常;旧句柄释放未确认时不得调用任何下一次 opener。Windows CloseHandle 失败后验证保留句柄和清理重试;Linux 验证独占解除后可重开且关闭不会误伤复用 fd。恢复后确认取消的剩余命令不重放;若驱动始终不完成,安排设备/驱动维护或服务/系统重启,不以自动恢复作为验收保证。
逻辑串口状态查询
通过 GET /api/consoles/{conf_id}/status 查询逻辑控制台状态,例如 GET /api/consoles/1/status。需要认证(会话 Cookie 或 X-API-Key),未登录返回 401,conf_id 范围 1–48。
| 字段 | 含义 |
|---|---|
state |
idle、connecting、connected、retry、error;授权限制时为 disabled |
message |
当前状态说明;重试时包含原因及约多少秒后重试 |
serial_release_pending |
始终输出的布尔值;物理 Close 进行中、等待 abort 或释放失败仍保留句柄时为 true;确认释放前不会打开新句柄 |
serial_stalled_operation / serial_stalled_for_seconds |
连续 30 秒未完成的串口操作及已持续秒数;多项阻塞时报告最早一项;无告警为空字符串和 null。表示疑似设备或驱动无响应,不能证明永久卡死。详情页显示处理建议,终端工具栏显示简短告警;操作结束后自动清除,授权限制不隐藏该告警。 |
connection_count |
所有协议合计的当前客户端会话数 |
max_connections |
当前配置的连接上限,范围 1–256,默认 1,无无上限模式;未授权时返回已保存上限 |
configured_device / configured_slot |
已保存设备名或 USB 槽位绑定,无绑定时为空字符串 |
resolved_device |
当前实际打开设备名;未连接或信息不可用时为空字符串 |
active_serial_config |
当前句柄使用的串口配置;未连接或信息不可用时为 null |
connected_for_seconds |
当前串口已连接的整秒数;未连接时为 null |
last_error / last_error_ago_seconds |
最后一次受控错误说明及距该错误的整秒数;新错误覆盖旧错误,连接成功后清除,返回空字符串和 null |
retry_count |
本次客户端会话周期内已安排的重试等待次数;最后一个客户端离开后保留,下次首个客户端接入时清零 |
retry_elapsed_seconds |
当前重试窗口已经过的整秒数,仅重试或重新连接时有值,其他情况为 null |
retry_in_seconds |
约多少秒后尝试打开串口,向上取整;仅 retry 等待期间有值,其他情况为 null |
时长由服务端使用 Go 单调时钟计算,不依赖客户端时间,也不受进程运行期间系统校时影响;已过时长向下取整,剩余等待向上取整,最小为 0。0 表示不足一秒或已到计划时间,null 表示该字段不适用。前端通过轮询更新,剩余秒数仅作提示,不作为实际重试执行依据。状态接口不再返回 connected_at、last_error_at、retry_started_at、next_retry_at。审计日志仍记录绝对时间,按服务端时间解读。
serial_release_pending 已包含在生成的 OpenAPI 中,与 GET /api/listeners 的网络监听器状态独立。禁用/断开连接响应及 idle/disabled 状态不保证资源已释放,应另外检查该字段。
最后一个客户端离开后串口按需关闭。如果最后一次连接未成功,保留最后一次错误;之后连接成功则立即清除错误及错误时长。last_error 不返回驱动原始错误或内部路径;详细驱动信息仍需查看服务日志。
修改串口配置后,已保存配置与当前句柄配置可能不同。configured_device/configured_slot 为最新保存绑定;active_serial_config 为打开当前句柄时的配置,resolved_device 为真实设备名。USB slot 绑定时,active_serial_config.device 可以为空,通过 resolved_device 查看实际 COM/tty。其他已保存参数可通过 GET /api/consoles/{conf_id} 查询;下次自动重连或重新接入使用新配置。
授权限制禁用时,state 为 disabled、message 为 license_limit,连接数和重试次数为 0,当前句柄信息为空,时长字段为 null。serial_release_pending 仍独立反映未完成的释放,禁用不强制将其清零。接口暂未提供按协议连接数、收发字节数和丢帧统计。
用户管理
功能总览
| 功能 | 操作权限 | 说明 |
|---|---|---|
| 查看用户 | admin |
查看账号、角色、启用状态、改密标志和相关时间,不返回密码哈希 |
| 创建用户 | admin |
设置用户名、初始密码、角色和启用状态;密码至少 8 个字符,不能仅含字母或仅含数字,新用户带有首次改密标记,Web 界面要求改密 |
| 编辑用户 | admin |
修改用户名、角色(admin / engineer)或启用状态;修改后该用户的旧登录会话失效 |
| 重置密码 | admin |
无需目标用户的原密码;重置后旧登录会话失效,设置改密标记,Web 界面要求下次登录后改密 |
| 删除用户 | admin |
永久删除账号,使旧登录会话失效 |
| 修改自己的密码 | 已登录的 admin / engineer |
提供当前密码后修改;修改后当前及其他旧登录会话全部失效 |
内置 admin 不能改名或删除,系统必须始终保留至少一个启用的管理员。具体限制和操作要求见下文。
角色权限
用户分为以下两个角色:
| 角色 | 用户管理权限 |
|---|---|
admin |
可以查看、创建、编辑和删除用户,也可以重置所选用户的密码 |
engineer |
不能访问用户管理功能,只能修改自己的密码 |
创建用户
- 用户名不能为空且不能与现有用户名重复。
- 角色只能选择
admin或engineer。 - 初始密码至少为 8 个字符。
- 未明确指定状态时,新用户默认为启用。
- 管理员创建的新用户带有
must_change_password标记,当前 Web 界面要求先修改初始密码。该限制由界面实现,API 和 SSH 未以此标记统一拒绝其他操作。首次初始化的admin已自行设置密码,不会再次要求修改。
编辑用户
管理员可以修改普通用户的用户名、角色和启用状态。修改用户信息后,该用户已有的登录会话立即失效,需要重新登录。
系统必须始终保留至少一个处于启用状态的管理员。因此,当目标用户是最后一个启用的管理员时,不能执行以下操作:
- 将其角色从
admin改为engineer; - 将其状态改为停用;
- 删除该用户。
默认管理员保护
系统内置的 admin 是保底管理账号,适用以下额外规则:
- 不能删除;
- 不能修改用户名;
- 可以修改或重置密码;
- 只有系统中还存在其他启用的管理员时,才能停用或降级该账号。
密码修改与重置
“修改自己的密码”和“管理员重置所选用户密码”是两种不同操作:
| 操作 | 是否需要当前密码 | 操作结果 |
|---|---|---|
| 用户修改自己的密码 | 需要 | 新密码立即生效,当前及其他旧会话全部失效 |
| 管理员重置所选用户密码 | 不需要 | 重置密码立即生效,目标用户的旧会话全部失效,设置改密标记,Web 界面要求其下次登录后改密 |
管理员在用户管理页面为其他用户重置密码时,无需输入目标用户或管理员的当前密码,只需输入并确认至少 8 个字符,不能仅含字母或仅含数字的新密码。
删除用户
- 默认
admin不能删除。 - 最后一个启用的管理员不能删除。
- 其他用户可以由管理员删除。
- 用户删除后,其账号和已有会话立即失效,且该操作不可撤销;如需临时阻止登录,应优先停用用户。
用户管理 API
用户管理接口均要求 admin 权限,完整请求及响应结构可在 /api/docs 或 /api/openapi.json 中查看。
/api/docs 的文档组件资源随程序内嵌,浏览器只需能连接本机服务,无需访问外网即可查看文档并测试本机接口。
| 方法与路径 | 用途 |
|---|---|
GET /api/users |
获取用户列表 |
POST /api/users |
创建用户 |
PATCH /api/users/{id} |
修改用户名、角色或启用状态 |
DELETE /api/users/{id} |
删除用户 |
PUT /api/users/{id}/password |
重置所选用户密码,不需要当前密码 |
SSH 公钥登录
连接 SSH 串口时,除了密码,也可以用 SSH 公钥登录,适合脚本、巡检和自动化工具:不用把密码写进脚本,也不怕被暴力猜测。两种方式同时有效,加了公钥不影响密码登录。
添加自己的公钥:登录网页 → 右上角用户菜单 → “我的 SSH 公钥”,粘贴 .pub 文件的完整一行(例如 id_ed25519.pub 的内容),可另起一个名称,然后“添加公钥”。还没有密钥时,在自己的电脑上运行:
ssh-keygen -t ed25519
管理员替别人管理:“用户”页面 → 该用户一行的“SSH 公钥”,可以查看、添加和删除他的公钥。
连接:和密码登录一样,客户端会自动使用私钥:
ssh -p 6001 alice@<服务器IPv4>
规则
- 支持 ed25519、ECDSA(含硬件安全密钥
sk-类型)和至少 2048 位的 RSA;不支持 DSA、证书和带from=、command=等选项的行。 - 每个用户最多 10 个公钥;同一个公钥不能给同一个用户加两次。
- 只粘贴公钥(
.pub)。私钥(以-----BEGIN ... PRIVATE KEY-----开头)会被拒绝,也不要交给任何人。 - 账号被停用、被删除,或需要修改密码(新建用户、密码刚被重置)时,公钥同样不能登录。新用户请先在网页上登录一次并修改初始密码。
- 删除公钥或停用账号后,新的连接会被拒绝;已经打开的 SSH 会话不会被断开,需要时在端口页面断开。
- 列表显示每个公钥的指纹(与
ssh-keygen -lf id_ed25519.pub的结果一致)和最近使用时间。 - 用公钥登录成功会写一条
ssh-login审计记录,目标里注明所用公钥的指纹;添加、删除公钥也都有审计记录。 - API Key 不属于任何用户,不能有自己的 SSH 公钥。
- 恢复出厂设置会清除所有用户,公钥随之清除。
| 方法与路径 | 用途 |
|---|---|
GET /api/auth/me/ssh-keys |
当前登录用户的公钥列表 |
POST /api/auth/me/ssh-keys |
给自己添加公钥,请求 {"public_key": "...", "comment": "可选名称"} |
DELETE /api/auth/me/ssh-keys/{key_id} |
删除自己的公钥 |
GET /api/users/{id}/ssh-keys |
管理员查看某个用户的公钥 |
POST /api/users/{id}/ssh-keys |
管理员给某个用户添加公钥 |
DELETE /api/users/{id}/ssh-keys/{key_id} |
管理员删除某个用户的公钥 |
API Key 管理
API Key 的查看、创建、修改和删除均要求 admin 权限。
创建与保存
创建时填写名称 client_name 和角色 role(admin 或 engineer)。明文 api_key 只在创建成功时返回一次,请当场保存;之后无法通过列表或其他接口重新获取完整密钥。
创建后修改
创建后可以修改角色和启用状态,不能修改名称或密钥本身。
| 字段 | 是否可修改 | 说明 |
|---|---|---|
role |
可以 | 修改为 admin 或 engineer |
enabled |
可以 | true 启用,false 禁用;禁用后不能用于新的认证请求 |
client_name |
不可以 | 如需更改名称,应创建新的 API Key |
api_key |
不可以 | 如需更换密钥,应创建新的 API Key |
通过 PATCH /api/api-keys/{id} 部分更新,只提交需要修改的字段。例如,将角色改为工程师并禁用:
{"role":"engineer","enabled":false}
更换与删除
需要更换名称、轮换密钥或遗失明文密钥时,创建新的 API Key,保存新密钥并更新使用它的客户端,然后删除旧 Key。名称必须唯一,新建时应使用未被占用的名称。删除不可撤销;如需临时暂停使用,可以先禁用。
API Key 管理接口
| 方法与路径 | 用途 |
|---|---|
GET /api/api-keys |
查看列表,密钥哈希脱敏,不返回明文密钥 |
POST /api/api-keys |
创建,明文密钥仅返回一次 |
PATCH /api/api-keys/{id} |
修改角色或启用状态 |
DELETE /api/api-keys/{id} |
永久删除 |
修改 Web/API 监听地址(重启生效)
网页界面、REST API 和 WebSocket 终端共用一个监听地址,来自配置文件 settings.toml 的 http_host 和 http_port(默认 0.0.0.0:80)。管理员除了手工改配置文件,也可以用接口修改:
| 接口 | 作用 |
|---|---|
GET /api/system/web-address |
查看当前地址 |
PUT /api/system/web-address |
保存新地址,请求体 {"host":"0.0.0.0","port":8080} |
DELETE /api/system/web-settings |
清除网页保存的 HTTP/HTTPS 地址和证书设置,重启后恢复基础配置;需先确认地址可能变化 |
只有管理员可以使用,每次修改都会写入审计记录。
不知道该填哪个地址时:管理员或工程师可以调用 GET /api/system/ipv4-addresses 查看本机已启用网卡上的 IPv4 地址(接口名、地址、前缀长度,以及是否回环地址 127.x.x.x、是否自动分配的 169.254.x.x)。列表里的地址都可以用作监听地址;0.0.0.0 表示所有地址,也可以用,但不在列表中。客户端要连接的地址,一般选 loopback 和 link_local 都为 false 的那个。
只在重启后生效。 保存只会改数据目录的 Web 设置,正在运行的服务不会换地址,所以修改后访问地址不变,直到重启程序。响应里的 running 是现在正在使用的地址,saved 是已保存、下次启动使用的地址,restart_required 为 true 表示需要重启;GET /api/system/status 的 service_restart_required 也会变为 true。把地址改回正在运行的那个,restart_required 恢复为 false。
保存前会做的检查(避免重启后程序连不上):
host必须是本机的 IPv4 地址,或0.0.0.0(所有地址);不是本机的地址返回422。port为 1–65535,并且不能落在控制台监听端口的范围内:5001–5048(Raw TCP)、6001–6048(SSH)、7001–7048(Telnet),这些端口可能现在关着、以后会被启用;落在其中返回422。- 换了端口时,会立刻试着绑定一次,端口被别的程序占用或没有权限(例如 Linux 非 root 使用 80)返回
409。只改host、端口不变时不试绑,因为当前服务正占着这个端口,重启会先释放它。
基础配置文件、注释、密钥和其他设置原样保留;只在数据目录的覆盖文件中修改 http_host、http_port,通过临时文件加重命名的方式原子替换。
用 --addr 启动时:--addr 指定的地址每次启动都优先于配置文件,所以此时保存的地址不会生效,响应里 command_line_override 为 true,restart_required 恒为 false。要让保存的地址生效,请去掉启动参数 --addr。
如果改错了,重启后访问不到:用 --addr 参数启动一次(例如 --addr 127.0.0.1:8000)回到可访问的状态,再用接口修改,或停止服务后编辑/移走 DataDir/web-settings.toml。覆盖文件仍在时,仅修改基础配置不会生效。检查虽然能挡住大多数错误,但不能保证新地址对你的网络环境一定可达(例如防火墙没放行新端口)。
系统诊断信息(联系客服时使用)
遇到程序问题需要联系客服时,管理员可以生成一份诊断信息,复制后发给客服。
怎么用:以管理员身份登录后,进入网页界面的“系统”页,在“支持诊断”一节点击“生成报告”,先弹出隐私风险提示,确认“我已了解,生成报告”后才会真正生成(取消则什么都不会收集),再检查内容,点“复制报告”(或“下载报告(.txt)”),再发给客服。也可以直接调用 POST /api/system/diagnostics,返回的 text 字段就是整份报告。只有管理员可以使用;工程师和未登录用户会被拒绝。
报告正文一律是英文,不随界面语言翻译,方便客服直接阅读;界面上的提示和按钮仍按所选语言显示。
报告编号:报告开头有一行 Report ID,就是这次生成在审计记录里留下的那条记录的编号(响应头 X-Request-ID 也是它)。报告万一外泄,凭这个编号能在审计记录里查到是谁、什么时候、从哪个地址生成的。
不要和许可证文件一起发送:报告里的设备标识不是秘密(本机有权限的人本来就能读到),但同时拿到报告和许可证文件的人,可能在别的机器上使用该授权。给客服发报告时,只发报告,不要附带许可证文件。确认框和报告开头都有这一提示。
请注意(确认框和报告开头都会显示):诊断信息没有做脱敏,包含主机名、运行用户名、文件路径、设备标识、串口与 USB 序列号、网络监听地址、配置摘要和最近的运行日志(日志中可能有客户端 IP 地址、串口设备名和错误详情),仅用于协助排查问题。程序不会自动上传这些内容,是否发送由您决定;请只提供给您联系的客服人员,不要公开发布。报告不包含用户密码、API Key、secret_key、api_key_pepper 和 SSH 主机私钥。
报告内容
| 部分 | 内容 |
|---|---|
| 程序与系统 | 版本、构建日期、平台和架构、Go 版本、操作系统版本、CPU 数、运行时长、主机名 |
| 资源(Resources) | 生成报告那一刻的快照,不是平均值:物理内存总量、可用量和使用率(可用量算上系统可回收的缓存)、交换区总量和已用、本程序占用的内存(Go 运行时从系统取得的总量和堆内使用量,不是进程常驻内存)、系统负载(1、5、15 分钟,只有 Linux 有)。Linux 容器里读到的是整机内存,不是容器限制 |
| 文件与配置 | 可执行文件和工作目录、运行用户;配置文件、数据目录(并检查能否写入)、数据库、许可证、维护令牌、SSH 主机密钥文件的路径和是否存在、大小、修改时间;Web 监听地址、WebSocket 允许的来源、license_server;secret_key、api_key_pepper 只显示“已设置”或“未设置” |
| 许可证 | 档位、license_id、有效维护到期日、本机的设备标识哈希、校验错误 |
| 设备身份(明文) | 明文的主板 UUID、CPU 序列号、操作系统安装 ID 和软件生成的 UUID,每项后面带它的哈希(hashed as …,就是许可证里的值);读不到或被拒绝的来源写明原因(例如“占位 UUID 无效”“Windows 不读取 CPU 序列号”);hardware_source_used 说明主板标识用的是 UUID、CPU 序列号还是都没有。这是整个程序里唯一会显示这些明文的地方,请只发给您联系的客服,不要公开发布 |
| 网络监听器 | 在运行或出错的监听器及最近的错误;已关闭或被授权限制的只统计数量 |
| 控制台 1–48 | 每个控制台的授权和启用状态、串口绑定(设备名/槽位/VID·PID·序列号)、串口参数、各协议的监听地址、连接数、当前串口状态、最近的串口错误 |
| 系统识别到的串口 | 串口名、USB 槽位、VID/PID/序列号、设备标识 |
| 最近的运行日志 | 最近约 300 行日志(只保存在内存中,重启后清空) |
“文件与配置”里的 data_dir_disk 一行是数据目录所在磁盘的剩余空间和总空间(磁盘满了会导致保存失败)。
某一部分无法收集时,报告会在该处写明原因,其他部分照常生成。
每次生成都会写入审计记录(事件 create-system-diagnostics,含操作者和来源 IP,被拒绝的尝试也记录),审计里不保存报告内容。报告只在点击按钮时才收集,页面本身不含任何数据。
连接日志
客户端接入控制台(TCP、Telnet、SSH、WebSocket 四种协议一视同仁)只写入程序日志,不写入数据库,也不进入审计表。审计表只记录管理类操作(见下一节),两者互不混杂;连接记录量大,且 TCP、Telnet 无需认证,被扫描时会产生大量记录。日志输出到标准输出/标准错误,程序本身不写日志文件,也不做轮转,需要保存请由部署者通过 systemd/journald、服务管理器或重定向自行处理。
每次连接有三类日志行
console 3 TCP: client 192.0.2.10:51234 connected (1 of 4 connections)
console 3 SSH: client 192.0.2.10:51240 (user admin) connected (2 of 4 connections)
console 3 SSH: client 192.0.2.10:51240 (user admin) disconnected after 12m3s
console 3 TCP: client 192.0.2.11:40022 refused: maximum connections reached
| 内容 | 说明 |
|---|---|
| 控制台 | console N,N 是逻辑控制台编号 |
| 协议 | TCP、Telnet、SSH、WebSocket |
| 客户端地址 | 对端 IP 和端口 |
| 用户 | 只有经过认证的 SSH、WebSocket 才有((user admin));TCP、Telnet 没有认证,不会有用户 |
| 连接数 | connected (1 of 4 connections):接入后的当前连接数和该控制台的最大连接数 |
| 时长与原因 | 断开时给出持续时间;非正常断开才附原因(如 connection reset by peer、timed out),客户端正常退出不附 |
被拒绝的连接会写明原因:maximum connections reached(超过最大连接数)、console is disabled(被暂停或超出授权)、service is shutting down。SSH 的密码错误写为 login failed for user "xxx";WebSocket 在到达控制台之前被拒绝(未登录、控制台不存在、未开放 WebSocket、授权不足、被暂停)同样会记录。
防刷屏:同一客户端 IP 因同一原因被拒绝,30 秒内只记一行,之后的那一行会注明被省略的次数(3 similar refusals in the last 30s not logged)。SSH 密码错误按“同一 IP”合并,换用户名不会绕过。用户名等由对端提供的文字会去掉换行等控制字符并限制长度,不能伪造日志行。
不记录终端内容:日志只有上述元数据,不含串口收发的任何数据。
与其他功能的关系
- 这些行也会进入“系统诊断信息”最近约 300 行日志里(仅内存,重启清空)。日志中的客户端 IP 和用户名因此可能随诊断信息一起发给客服,诊断页面的告知文字已经提示这一点。
- SSH 密码认证的成功与失败仍会写入审计表(
ssh-login),与这里的日志并行存在。 - 客户端 IP 和用户名属于个人信息,部署者对日志的保存期限、访问权限负责。
敏感操作审计
审计记录自动存入现有 SQLite 数据库的 audit_events 表。覆盖首次管理员初始化、Web 登录成功与失败、退出、修改与重置密码、用户及 API Key 管理、许可证导入、配置修改与恢复默认值、控制台配置修改和强制断开连接。SSH 密码认证成功与失败也会记录;当前 SSH 认证回调不提供客户端地址,因此这类记录的来源 IP 为空。
记录包含时间(UTC)、操作类型、操作者身份与角色、来源 IP、请求 ID、目标路径和结果。Web 操作结果包括 success、failure、denied,HTTP 状态码一并保存。来源 IP 使用实际连接地址,代理部署时为代理地址,不直接信任客户端提交的转发头。
密码、Cookie、API Key 明文、许可证原文及签名、请求和响应正文、终端内容均不写入审计记录。当前版本记录操作及结果,不保存配置字段的前后差异。普通查询、状态轮询和 WebSocket ticket 签发不产生审计写入。
管理员通过 GET /api/audit-events?limit=50 查询,结果按 ID 倒序排列。下一页使用上一页最后一条记录的 ID,例如 GET /api/audit-events?limit=50&before=123。limit 范围为 1–200,默认 50;before 默认 0。使用现有登录 Cookie 或管理员 API Key 认证,可在 /api/docs 查看接口。当前 Nuxt 前端已有日志页面,管理员可查询审计记录;旧的嵌入式前端编译产物可能没有该页面,以实际部署的前端版本为准,API 查询不依赖该页面。
每次敏感操作同步追加一条记录,沿用 SQLite WAL。每小时有新事件时批量清理一次,保留最近 90 天内的最多 100,000 条记录;清理间隔内数量可能暂时超过上限。清理日期依赖服务端系统时间,错误校时可能影响保留范围;前述串口相对时长与此不同。不提供修改或删除审计记录的 API。记录保存在本地数据库,拥有数据库文件访问权限的人仍可修改文件。
审计写入失败时保留原操作结果,在系统日志中输出事件类型及请求 ID,Web 响应额外带有 X-Audit-Status: failed;敏感操作响应带有系统生成的 X-Request-ID。审计写入与业务变更分别提交,并非同一事务;意外终止可能造成业务已完成但审计尚未落盘。高频失败登录仍会产生写入,当前审计功能没有额外引入登录限流。