fix: 保护NapCat预写反检测配置

This commit is contained in:
sunlei 2026-06-29 15:49:43 +08:00
parent 5b0cf2214b
commit 828a321bdb
9 changed files with 66 additions and 14 deletions

2
API.md
View File

@ -377,7 +377,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
该接口只返回脱敏后的运行态证据,供 Admin 排查镜像、locale、shm、配置 hash、漂移状态、风险模式和 watchdog 巡检告警状态;不会返回 WebUI token、reverse WS token、QQ 登录密码、SSH 私钥或运行态密码环境。账号列表只挂载 `napcat.profileStatus`、`napcat.runtimeProfile` 等摘要字段不触发登录、重建或修复动作。watchdog 不执行登录恢复:遇到 QQ 登录态离线只记录离线原因并通知 `super`,登录恢复统一由 Admin 手动「更新登录」触发session behavior profile 只做冷启动、housekeeping、presence 和自动能力分阶段降载,不实现账号级每小时/每日累计发送预算。
NapCat Chinese Desktop Runtime 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset明确二维码过期或扫码确认窗口失效的 QR session failure 会自动换码,其他非自动重试 QR failure 会标记下次 WebUI 登录动作先重置 native login service。v14 起运行时会为 QQ/NapCat/Xvfb 长期进程做 PID 级 `/proc/<pid>/mountinfo` 遮蔽,避免 QQCore 通过 `/proc/self/mountinfo` 看到 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances`、`btrfs`、`/dev/mapper/trim` 等宿主路径v15 在扫码登录成功回调中先写入 `QQLoginInfo` 再写登录态,避免 API 读到 `isLogin=true` 但 QQ 号为空的短暂不一致v16 在 native reset 缺少 `offline()` 时改用 `destroy()` 硬重置半登录服务,并让镜像 verify 等待 mountinfo guard 收敛v17/v18 增加 WebUI 鉴权的 `/api/Debug/RuntimeViewProbe` 同进程诊断并修正 native maps 截断导致的 hook 证据假阴性v19 保留 WebUI `RestartNapCat` 重启 worker 时的 `-q <uin>` 快速登录参数,避免重启后退回无账号扫码。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:*` digest。生产 K8s manifest 保留 `kt-napcat-desktop-cn:desktop-cn-v19` / `desktop-cn-v19` 稳定默认值Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 只有非空时才覆盖 API Deployment env空值会保持 manifest/default env。运行时回滚应重新运行 Jenkins 并填入上一版镜像 digest/profile或清空两个 override 后重新部署回 manifest 默认值。
NapCat Chinese Desktop Runtime 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset明确二维码过期或扫码确认窗口失效的 QR session failure 会自动换码,其他非自动重试 QR failure 会标记下次 WebUI 登录动作先重置 native login service。v14 起运行时会为 QQ/NapCat/Xvfb 长期进程做 PID 级 `/proc/<pid>/mountinfo` 遮蔽,避免 QQCore 通过 `/proc/self/mountinfo` 看到 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances`、`btrfs`、`/dev/mapper/trim` 等宿主路径v15 在扫码登录成功回调中先写入 `QQLoginInfo` 再写登录态,避免 API 读到 `isLogin=true` 但 QQ 号为空的短暂不一致v16 在 native reset 缺少 `offline()` 时改用 `destroy()` 硬重置半登录服务,并让镜像 verify 等待 mountinfo guard 收敛v17/v18 增加 WebUI 鉴权的 `/api/Debug/RuntimeViewProbe` 同进程诊断并修正 native maps 截断导致的 hook 证据假阴性v19 保留 WebUI `RestartNapCat` 重启 worker 时的 `-q <uin>` 快速登录参数,避免重启后退回无账号扫码v20 保护 API 预写的 `/app/napcat/config`,避免上游首次解包 `NapCat.Shell/*` 覆盖 `bypass.*=true``o3HookMode=0`。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:*` digest。生产 K8s manifest 保留 `kt-napcat-desktop-cn:desktop-cn-v20` / `desktop-cn-v20` 稳定默认值Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 只有非空时才覆盖 API Deployment env空值会保持 manifest/default env。运行时回滚应重新运行 Jenkins 并填入上一版镜像 digest/profile或清空两个 override 后重新部署回 manifest 默认值。
API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,且 `fork-artifact.json` 必须包含完整 marker metadataupstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。NapCat base image 在 release evidence 中必须 pin 到 digest。API Jenkins 只做显式参数推广,不负责自动合并上游、自动构建运行时镜像或在 override 为空时隐式改写 NapCat env。

View File

@ -77,7 +77,7 @@ QQBot 插件定时任务由 manifest 的 `tasks` 声明,平台持久化到 `qq
Admin 环境总览面板使用 `ENV_DASHBOARD_*` 只读配置聚合 local-dev、NAS 线上、腾讯云和 r4se 状态。`ENV_DASHBOARD_ADMIN_LOCAL_URL` / `ENV_DASHBOARD_ADMIN_PUBLIC_URL` 只用于展示 Admin 本机与线上入口证据。HTTP 快照提供当前拓扑,后端 local/MQTT 事件总线通过 SSE 推送增量事件给 Admin前端不直连 MQTT也不轮询刷新。Jenkins、K8s、Tencent Cloud、Caddy、WireGuard、Mihomo/OpenClash 未配置时会显示 `unwired` 证据,不能渲染成健康假象;第一版不暴露重启、部署、迁移、容器重建、插件启停或代理切换等写操作。
NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MACNapCat/OneBot 配置 hashKT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v19 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后标记 native login service staleAPI 在源 Docker 容器在线但 WebUI 明确 QQ 离线时会同容器调用 `RestartNapCat` 重启 NapCat worker重建 QQCore login service 后再推进 quick/password/qrcode不做 Docker 重建、补 env 或设备身份迁移,且同一个更新登录 session 只消费一次 worker restart 预算v14 起还会对 QQ/NapCat/Xvfb 长期进程的 `/proc/<pid>/mountinfo` 做 PID 级遮蔽,防止 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances` 等宿主路径泄露v15 修复扫码成功时 `QQLoginInfo` 晚于登录态写入造成的 QQ 号回读空窗v16 在 native reset 缺少 `offline()` 时改用 `destroy()` 硬重置半登录服务,并让镜像 verify 等待 mountinfo guard 收敛v17/v18 增加 WebUI 鉴权的 `/api/Debug/RuntimeViewProbe` 同进程诊断并修正 native maps 截断导致的 hook 证据假阴性v19 保留 WebUI `RestartNapCat` 重启 worker 时的 `-q <uin>` 快速登录参数,避免重启后退回无账号扫码。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v19` digest。`k8s/prod/api.yaml` 保留 `desktop-cn-v19` 稳定默认值Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 仅在填写时通过 `kubectl set env` 推广已验证运行时镜像/profile空值会继续使用 manifest/default env。回滚时重新运行 Jenkins 并填入上一版 digest/profile或清空两个 override 后重新部署 manifest 默认值。
NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MACNapCat/OneBot 配置 hashKT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v20 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后标记 native login service staleAPI 在源 Docker 容器在线但 WebUI 明确 QQ 离线时会同容器调用 `RestartNapCat` 重启 NapCat worker重建 QQCore login service 后再推进 quick/password/qrcode不做 Docker 重建、补 env 或设备身份迁移,且同一个更新登录 session 只消费一次 worker restart 预算v14 起还会对 QQ/NapCat/Xvfb 长期进程的 `/proc/<pid>/mountinfo` 做 PID 级遮蔽,防止 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances` 等宿主路径泄露v15 修复扫码成功时 `QQLoginInfo` 晚于登录态写入造成的 QQ 号回读空窗v16 在 native reset 缺少 `offline()` 时改用 `destroy()` 硬重置半登录服务,并让镜像 verify 等待 mountinfo guard 收敛v17/v18 增加 WebUI 鉴权的 `/api/Debug/RuntimeViewProbe` 同进程诊断并修正 native maps 截断导致的 hook 证据假阴性v19 保留 WebUI `RestartNapCat` 重启 worker 时的 `-q <uin>` 快速登录参数,避免重启后退回无账号扫码v20 保护 API 预写的 `/app/napcat/config`,避免上游首次解包 `NapCat.Shell/*` 覆盖 `bypass.*=true``o3HookMode=0`。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v20` digest。`k8s/prod/api.yaml` 保留 `desktop-cn-v20` 稳定默认值Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 仅在填写时通过 `kubectl set env` 推广已验证运行时镜像/profile空值会继续使用 manifest/default env。回滚时重新运行 Jenkins 并填入上一版 digest/profile或清空两个 override 后重新部署 manifest 默认值。
运行时发布时API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,`fork-artifact.json` 必须带完整 marker metadata包括 upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。release evidence 里的 NapCat base image 必须用 digest pin。API Jenkins 只消费人工确认后的运行时推广参数,不自动合并上游、不自动构建隐藏镜像,也不在 override 为空时覆盖 K8s manifest 中的默认 env。
@ -178,7 +178,7 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow
- QQBot 插件平台统一使用 `plugin.json` manifest 描述插件 key、版本、操作、事件、权限、运行预算和包入口CLI 负责 create/validate/pack/install-local后端只暴露受控 SDK 能力并通过插件维度记录安装、配置、账号绑定和运行事件。
- Bilibili Card 是事件型内置插件:`bilibili-card.message` 只在账号绑定后监听 QQ/NapCat `share/json/xml/lightapp` 卡片或文本里的 Bilibili 链接,`b23.tv` 短链通过平台 `resolveRedirect` 受控 host 能力解析,视频信息从 Bilibili `x/web-interface/view` 获取后回复纯文本摘要。
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表拆开展示 OneBot、容器、WebUI 和 QQ 登录态,心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态;`qqLoginMessage` 只展示 QQ 登录态消息WebUI 配置或请求错误留在 `lastError`
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。`desktop-cn-v19` 镜像从 KT `NapCatQQ` fork 的 source-built `NapCat.Shell` 构建,不再在镜像内对上游 bundle 做字符串 patch并修复非自动重试 QR failure 后下次 WebUI 登录动作不重置、QQCore 通过进程级 mountinfo 探针看到 Docker/宿主路径、扫码登录成功后 API 立即读不到 QQ 号、生产 native reset 缺少 `offline()` 时半登录态无法清理、runtime view native maps 取证假阴性,以及 WebUI `RestartNapCat` 重启 worker 丢失快速登录账号参数的问题。踢下线后的半登录态不能只靠旧 native reset 兜底;源 Docker 容器在线时 API 会先同容器 `RestartNapCat` 重建 NapCat worker再继续登录流程同一个更新登录 session 不能反复重启 worker。
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。`desktop-cn-v20` 镜像从 KT `NapCatQQ` fork 的 source-built `NapCat.Shell` 构建,不再在镜像内对上游 bundle 做字符串 patch并修复非自动重试 QR failure 后下次 WebUI 登录动作不重置、QQCore 通过进程级 mountinfo 探针看到 Docker/宿主路径、扫码登录成功后 API 立即读不到 QQ 号、生产 native reset 缺少 `offline()` 时半登录态无法清理、runtime view native maps 取证假阴性WebUI `RestartNapCat` 重启 worker 丢失快速登录账号参数,以及首次解包覆盖 API 预写 NapCat config 导致 bypass 开关回落默认关闭的问题。踢下线后的半登录态不能只靠旧 native reset 兜底;源 Docker 容器在线时 API 会先同容器 `RestartNapCat` 重建 NapCat worker再继续登录流程同一个更新登录 session 不能反复重启 worker。
- NapCat 账号新增/编辑支持可选 QQ 登录密码Admin 只提交 RSA-OAEP 加密后的 `encryptedLoginPassword`,后端解密后必须用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密保存到 `qqbot_account.napcat_login_password_secret`;空值、`change-me` 和历史公开默认值会被拒绝;列表和详情不回显密码,日志会脱敏密码字段。
- NapCat 容器为已知 `selfId` 创建/重建时会一次性注入 `ACCOUNT` 等必要 env容器重启崩溃/重启策略/宿主重启)可复用持久化会话,但硬踢 `登录已失效` 仍需人工登录。Admin「更新登录」不通过 Docker 重建、重启或补 env 刷新登录态:只要源容器在线,就保持同一 Docker 容器;若 WebUI 明确返回 QQ 离线,会先调用同容器 `/api/QQLogin/RestartNapCat` 重启 NapCat worker随后通过 NapCat WebUI `SetQuickLogin -> PasswordLogin -> RefreshQRcode/GetQQLoginQrcode` 推进原弹窗流程;同一个更新登录 session 只允许一次 worker restart后续状态轮询只能继续 WebUI 登录/二维码刷新,不能反复重启 worker。只有 Docker 容器离线或缺失时,容器准备阶段才创建/重建并一次性注入 env。快速登录失败后如果账号保存了登录密码后端使用解密密码计算 MD5 调 `/api/QQLogin/PasswordLogin`,不会把密码写入运行态 env也没有成功后的 env 清理步骤;密码登录按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询结果。准备中的扫码会话会续期,重复调用更新登录会复用同一 pending `sessionId`,不会再次启动 quick/password/二维码准备;但如果该 pending 会话是在账号维护登录密码前创建、且尚未进入密码/验证码/新设备上下文,后续更新登录必须退役这条无密码会话并新建 refresh session以重新读取账号表中的最新密码。取消扫码会话会在接口返回前把持久化 session 标成 `error/cancelled` 并写完成时间,避免已取消二维码从 DB 恢复为 active pending。若 API Pod 在准备阶段重启,持久化的 `preparingRelogin` 超过 `QQBOT_NAPCAT_RELOGIN_PREPARING_STALE_MS` 后会自动恢复为普通状态检测。pending refresh 会话没有二维码、验证码或新设备挑战时,状态轮询会按 `NAPCAT_LOGIN_QR_AUTO_REFRESH_COOLDOWN_MS` 冷却在同一容器自动重试二维码刷新,避免 SSE 长时间停在生成中。验证码和新设备验证保持同一会话 pending腾讯验证码结果 `ticket`/`randstr`/`sid` 通过 `/qqbot/account/scan/captcha/submit` 回交到同一容器的 `/api/QQLogin/CaptchaLogin`;状态轮询遇到验证码文案但缺少 URL 时会先从当前容器日志恢复 `proofWaterUrl`,没有 URL 也保持验证码处理中而不切到二维码兜底。扫码或密码链路拿到 NapCat 登录阳性但 QQ 号暂不可读时,会按 `NAPCAT_LOGIN_SELF_ID_WAIT_MS` 保持 pending超过窗口才失败。密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,直接通过 WebUI 二维码接口进入扫码兜底,不 reset 登录态。Admin SSE 步骤顺序按实际路径为 `quick-login-*` -> `password-login-*` / `password-login-captcha` / 新设备验证 -> `qrcode/waiting-scan` -> `login-success|login-failed`SSE 事件缓存因 Pod 重启丢失时,新订阅会收到当前会话快照。
- NapCat 设备身份按账号持久化到 `napcat_device_identity`:同一账号重建容器会复用数据目录、`pc-<8hex>` hostname、machine-id 和实体 OUI 风格 MAC明确排除 Docker `02:42`、QEMU/KVM `52:54:00`、VMware、Hyper-V 等虚拟化前缀;新增账号首次扫码会先用预留容器 id 创建临时设备身份并应用到第一次 Docker run扫码成功后归属到真实账号并同步 runtime/protocol profileDocker run 会注入 `--hostname`、`--mac-address`、只读 `/etc/machine-id`,并同步写入 QQNT Linux `machine-info`,使 `/etc/machine-id`、Docker MAC 和 QQNT 本地设备缓存保持一致。当前策略名为 `qqnt-visible-hostname-v1` / `physical-oui-mac-v1`

View File

@ -3,6 +3,29 @@ set -eu
ENTRYPOINT=/app/entrypoint.sh
if grep -q 'KT device profile patch defaults' "$ENTRYPOINT" \
&& grep -q 'KT preserve mounted NapCat config' "$ENTRYPOINT"; then
exit 0
fi
if ! grep -q 'KT preserve mounted NapCat config' "$ENTRYPOINT"; then
grep -Fq 'cp -rf NapCat.Shell/* napcat/' "$ENTRYPOINT"
awk '
/cp -rf NapCat\.Shell\/\* napcat\// {
print " # KT preserve mounted NapCat config."
print " find NapCat.Shell -mindepth 1 -maxdepth 1 ! -name config -exec cp -rf {} napcat/ \\;"
print " if [ ! -f \"napcat/config/napcat.json\" ]; then"
print " mkdir -p napcat/config"
print " cp -rf NapCat.Shell/config/* napcat/config/"
print " fi"
next
}
{ print }
' "$ENTRYPOINT" > "$ENTRYPOINT.tmp"
mv "$ENTRYPOINT.tmp" "$ENTRYPOINT"
chmod +x "$ENTRYPOINT"
fi
if grep -q 'KT device profile patch defaults' "$ENTRYPOINT"; then
exit 0
fi

View File

@ -60,6 +60,9 @@ grep -q '/sys/class/dmi/id/product_name' /app/entrypoint.sh
grep -q '/sys/class/dmi/id/bios_vendor' /app/entrypoint.sh
grep -q '/sys/class/dmi/id/bios_version' /app/entrypoint.sh
grep -q '/etc/hosts' /app/entrypoint.sh
grep -q 'KT preserve mounted NapCat config' /app/entrypoint.sh
grep -q 'find NapCat.Shell -mindepth 1 -maxdepth 1 ! -name config' /app/entrypoint.sh
! grep -q 'cp -rf NapCat.Shell/\* napcat/' /app/entrypoint.sh
MOUNTINFO_HOST_LEAK_PATTERN='overlay|/vol1/docker|docker-init|/docker/containers|napcat-instances|btrfs|/dev/mapper/trim'
MOUNTINFO_GUARD_TIMEOUT_SECONDS="${MOUNTINFO_GUARD_TIMEOUT_SECONDS:-12}"

View File

@ -33,9 +33,9 @@ spec:
- name: DB_TIMEZONE
value: "+08:00"
- name: QQBOT_NAPCAT_IMAGE
value: kt-napcat-desktop-cn:desktop-cn-v19
value: kt-napcat-desktop-cn:desktop-cn-v20
- name: QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION
value: desktop-cn-v19
value: desktop-cn-v20
- name: QQBOT_NAPCAT_SSH_KEY_PATH
value: /app/secrets/napcat-ssh/id_rsa
- name: QQBOT_PLUGIN_TASK_QUEUE_REDIS_PREFIX

View File

@ -71,7 +71,7 @@ export class NapcatRuntimeProfileService {
dataDir: input.dataDir,
desktopProfileVersion: this.getString(
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION',
'desktop-cn-v19',
'desktop-cn-v20',
),
deviceIdentityId: input.deviceIdentityId,
imageRef: this.getString('QQBOT_NAPCAT_IMAGE', ''),

View File

@ -122,6 +122,28 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
expect(source).toContain('proc-devices-host-leak');
});
it('preserves API-written NapCat config during the first shell install', () => {
const entrypointPatch = readSource(
'ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh',
);
const verify = readSource('ci/napcat-desktop-cn/verify.sh');
expect(entrypointPatch).toContain('KT preserve mounted NapCat config');
expect(entrypointPatch).toContain(
"find NapCat.Shell -mindepth 1 -maxdepth 1 ! -name config",
);
expect(entrypointPatch).toContain(
'if [ ! -f \\"napcat/config/napcat.json\\" ]; then',
);
expect(entrypointPatch).toContain(
'cp -rf NapCat.Shell/config/* napcat/config/',
);
expect(verify).toContain('KT preserve mounted NapCat config');
expect(verify).toContain(
"! grep -q 'cp -rf NapCat.Shell/\\* napcat/' /app/entrypoint.sh",
);
});
it('stages source-built NapCat Shell artifacts for Docker build context', () => {
const script = readSource('scripts/napcat-desktop-cn-stage-build.mjs');
@ -173,13 +195,15 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
expect(verify).not.toContain('selfInfo?.online !== false');
});
it('deploys the production API with the verified desktop-cn-v19 runtime profile', () => {
it('deploys the production API with the verified desktop-cn-v20 runtime profile', () => {
const manifest = readSource('k8s/prod/api.yaml');
expect(manifest).toContain('name: QQBOT_NAPCAT_IMAGE');
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v19');
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v20');
expect(manifest).toContain('name: QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION');
expect(manifest).toContain('value: desktop-cn-v19');
expect(manifest).toContain('value: desktop-cn-v20');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v19');
expect(manifest).not.toContain('value: desktop-cn-v19');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v18');
expect(manifest).not.toContain('value: desktop-cn-v18');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v16');
@ -190,6 +214,6 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v7');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v4');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v3');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v2');
expect(manifest).not.toMatch(/kt-napcat-desktop-cn:desktop-cn-v2(?!\d)/);
});
});

View File

@ -30,11 +30,13 @@ describe('NapCat runtime promotion contract', () => {
);
});
it('keeps the verified v19 runtime profile as the K8s manifest default', () => {
it('keeps the verified v20 runtime profile as the K8s manifest default', () => {
const manifest = readSource('k8s/prod/api.yaml');
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v19');
expect(manifest).toContain('value: desktop-cn-v19');
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v20');
expect(manifest).toContain('value: desktop-cn-v20');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v19');
expect(manifest).not.toContain('value: desktop-cn-v19');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v18');
expect(manifest).not.toContain('value: desktop-cn-v18');
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v16');

View File

@ -123,7 +123,7 @@ describe('NapCat runtime profile generation', () => {
});
expect(profile).toMatchObject({
desktopProfileVersion: 'desktop-cn-v19',
desktopProfileVersion: 'desktop-cn-v20',
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
locale: 'zh_CN.UTF-8',
runtimeGid: 1101,