本页目录
  1. 安装与首次登录
  2. 串口绑定与通信参数
  3. 临时禁用逻辑 Console
  4. 访问协议与网络保护
  5. 启用 HTTPS
  6. 登录保护
  7. 监听器状态与手动重试
  8. 配置导入、恢复默认与备份
  9. 许可证申请与安装
  10. 串口状态提示
  11. 逻辑串口状态查询
  12. 用户管理
  13. API Key 管理
  14. 修改 Web/API 监听地址(重启生效)
  15. 系统诊断信息(联系客服时使用)
  16. 连接日志
  17. 敏感操作审计

BetterOne Console Server 操作手册

English edition

更新日期: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 里删掉旧条目。只是自己重新开始时不必加。

先停止服务,并先备份。 命令只会在执行期间暂存数据库以便失败时恢复;成功后会删除暂存文件,它不是长期备份。需要保留数据,请先把下面“会删除”的文件复制到别处。

运行时发生什么

  1. 程序检查服务是否还在运行(有程序在 Web 地址上应答就算运行)。服务运行时不会继续,请先停止服务。
  2. 显示要删除的文件、要保留的文件,并用大写字母提醒“先备份数据”。
  3. 让你输入 RESET(全大写,完整输入)。输入别的任何内容都会终止,什么也不改。
  4. 确认后先检查写入权限和文件类型,把数据库暂存到同目录;写好重置记录、轮换运行时密钥后才清理暂存文件。中途失败会尽量恢复旧数据库;若意外中断而留下暂存目录,下次启动会拒绝创建空数据库,并提示先恢复。

会删除:数据库 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 提供;该库读取失败时,本次枚举里这三项为空,按设备标识绑定的控制台暂时找不到设备并重试,不影响另外两种方式。

怎么选

  1. 不是 USB 串口,或设备带多个串口 → 按设备名。
  2. 线缆位置固定、适配器可能更换,且硬件不改动 → 按 USB 端口位置。
  3. 适配器固定、位置可能变化,且设备有唯一序列号 → 按设备标识。
  4. 不确定时:先在串口列表(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. 找不到设备:控制台 1 绑定的是 00.1400@2.1,原来插在第 1 口的适配器现在在 00.1400@2.1.1,没有对应的绑定,控制台 1 显示找不到设备。
  2. 接到别的设备上,而且不报错:控制台 2 绑定的是 00.1400@2.2,现在这个槽位对应外壳上的第 5 口。如果这个口上插着另一个设备,控制台 2 就会连到错误的设备。这是最危险的情况。

更换 Hub 的处理步骤

  1. 尽量换成同一型号,插回电脑的同一个口。
  2. 如果只能换成不同型号,换完后所有槽位绑定都要重新核对:先不要启用相关控制台,在"物理串口"页把适配器逐个插上,读出每个适配器新的槽位,再更新对应控制台的绑定。
  3. 配置完成后,向每个控制台各发一条测试数据,确认连到的是预期的设备。
  4. 适配器带有 USB 序列号的(FTDI、CP210x 等大多数都有),可以改用"按设备标识"绑定,这样更换 Hub、换口都不受影响。没有序列号的廉价芯片(很多 CH340)才需要依赖槽位。
  5. 在 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;机器级副本还在时也可用于恢复。
  • 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、设备与端口数不变,只更新维护期、签发时间等。

同一授权只有维护期更晚才替换;日期相同或更早不写入,提示当前日期。被卖家释放的设备不能获取新文件,但原文件仍可继续使用已覆盖版本。

离线续期

设备不能联网时,到能上网的电脑上通过官网“离线激活 / 续期”页完成。官网只收一个文件,按文件内容自动识别,不用在网页上填续期码:

  1. 准备文件,拷到联网电脑:
    • 买了续期码:在“激活与续期”输入 RNW 续期码,点击“生成续期请求文件”(文件名以 .renewal.txt 结尾);在线兑换连接失败时,也可按提示确认生成离线请求;
    • 卖家已经在后台续期:在“激活与续期”展开“设备无法联网?离线续期”,导出本机授权文件(文件名以 .license.txt 结尾),用于获取服务器上最新的授权文件,不会兑换续期码。
  2. 打开官网“离线激活 / 续期”页(/#offline,旧地址 /#renew 打开的也是这一页),上传上一步的文件。
  3. 从官网下载新的 .license.txt 授权文件,拷回本机。
  4. 使用“导入许可证”选择文件或粘贴原文,验证后安装。每台设备分别操作。

导入会验签并匹配本机设备。同一授权的旧文件不倒退日期;不同授权可以替换,但新文件端口数更少时须先确认降档。取消不改原文件。当前端口数取已购买授权,不是免费回退路数。文件最大 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),所以按安装包使用时通常不会遇到权限问题。以下几种情况仍会遇到:

  1. 树莓派板载串口(GPIO 上的 /dev/ttyAMA0、/dev/ttyS0、/dev/serial0):系统默认把它当作登录控制台,设备属于 root:tty,而且被登录程序占着。USB 转串口线(/dev/ttyUSB0)不受影响。
  2. 系统里既没有 dialout 组也没有 uucp 组(少数精简系统),安装脚本无组可加。
  3. 不用安装包、自己用普通用户直接运行程序,而这个用户不在 dialout 组。
  4. 自定义的 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。审计写入与业务变更分别提交,并非同一事务;意外终止可能造成业务已完成但审计尚未落盘。高频失败登录仍会产生写入,当前审计功能没有额外引入登录限流。