From 074bee1d4b351a6b40f0727dd696ff790082c4f0 Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 26 Jun 2026 17:21:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E9=BD=90NapCat=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E6=8B=9F=E7=9C=9F=E6=8E=A2=E9=92=88=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 4 +- ci/napcat-desktop-cn/Dockerfile | 6 +- .../entrypoint-device-profile.patch.sh | 159 ++++++++++++++++++ ci/napcat-desktop-cn/verify.sh | 12 ++ scripts/napcat-desktop-cn-stage-build.mjs | 4 + .../runtime/napcat-config-writer.service.ts | 4 +- .../runtime/napcat-runtime-profile.service.ts | 2 +- .../qqbot-napcat-container.service.ts | 73 +++++++- .../qqbot/napcat/device-identity.spec.ts | 18 ++ .../napcat/napcat-desktop-cn-image.spec.ts | 38 +++++ .../napcat/runtime-protocol-profile.spec.ts | 5 +- .../qqbot-napcat-container.service.spec.ts | 23 +++ 12 files changed, 339 insertions(+), 9 deletions(-) create mode 100644 ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh diff --git a/API.md b/API.md index dc993f0..e379e28 100644 --- a/API.md +++ b/API.md @@ -367,7 +367,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT 同一 QQ 账号只保留一个有效 NapCat 主容器。扫码后如果已有账号绑定到新容器,后端会释放旧绑定和未共享的旧容器,避免同账号多实例互相挤下线。OneBot notice 只有机器人下线、登录失效、`KickedOffLine` 等账号级信号才会记录 QQ 登录态异常并生成 `qqbot.account.offline` 站内信,普通群成员 kick 不属于账号离线信号。下线原因写入 `lastError` 前按 `last_error` 500 字符列宽截断;后续无错误的普通断连只更新 OneBot 连接状态,不清空该原因。账号列表会按近期缓存检查绑定 NapCat 容器的最新登录状态日志,日志检测默认 5 秒超时;`isOnline:false` 属于 QQ 登录态离线信号;心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态。托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像。 -托管 NapCat 容器按账号持久化设备身份,`napcat_device_identity` 保存账号对应的数据目录、hostname、machine-id 路径、MAC 地址、验证状态和最近登录证据。重建同一账号容器时会复用 `pc-<8hex>` hostname、实体 OUI 风格 MAC 和 machine-id,并明确排除 Docker `02:42`、QEMU/KVM `52:54:00`、VMware、Hyper-V 等虚拟化前缀;新增账号创建期在真实 QQ selfId 未知时使用预留容器 id 创建临时设备身份,第一次 Docker run 就注入完整拟真参数,扫码成功后再把该身份和 runtime/protocol profile 归属到真实账号。Docker run 会注入 `--hostname`、`--mac-address` 和只读 `/etc/machine-id` 挂载;后端还会同步写入 QQNT Linux `machine-info`,让 QQNT 计算 GUID 时使用的 MAC 与 Docker 网卡一致。当前策略名为 `qqnt-visible-hostname-v1` / `physical-oui-mac-v1`,绑定关系会回填 `napcat_account_binding.device_identity_id`。 +托管 NapCat 容器按账号持久化设备身份,`napcat_device_identity` 保存账号对应的数据目录、hostname、machine-id 路径、MAC 地址、验证状态和最近登录证据。重建同一账号容器时会复用 `pc-<8hex>` hostname、实体 OUI 风格 MAC 和 machine-id,并明确排除 Docker `02:42`、QEMU/KVM `52:54:00`、VMware、Hyper-V 等虚拟化前缀;新增账号创建期在真实 QQ selfId 未知时使用预留容器 id 创建临时设备身份,第一次 Docker run 就注入完整拟真参数,扫码成功后再把该身份和 runtime/protocol profile 归属到真实账号。Docker run 会注入 `--hostname`、`--mac-address`、只读 `/etc/machine-id` 挂载、`SYS_ADMIN`、`apparmor=unconfined`、`seccomp=unconfined` 和 `NAPCAT_REQUIRE_DEVICE_PROFILE=1`;后端还会同步写入 QQNT Linux `machine-info`,让 QQNT 计算 GUID 时使用的 MAC 与 Docker 网卡一致。派生镜像 entrypoint 会用同一设备 profile 覆盖 QQCore 实际打开的 DMI、boot_id、kernel release/version/proc version、CPU model、uptime、TTY active、mountinfo、`/etc/hosts` 和 `/proc/devices` 等探针;NapCat fork native login 和 core session config 的 `machineId` 与 `systemVersion` 也从该 profile 读取,避免 QQ native 入参和 Docker 可见探针不一致。当前策略名为 `qqnt-visible-hostname-v1` / `physical-oui-mac-v1`,绑定关系会回填 `napcat_account_binding.device_identity_id`。 ### NapCat Runtime Profile @@ -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 v8 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset,且只有明确二维码过期或扫码确认窗口失效的 QR session failure 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context;生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v8` digest。生产 K8s manifest 保留 `kt-napcat-desktop-cn:desktop-cn-v8` / `desktop-cn-v8` 安全默认值;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 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。构建前必须运行 `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-v8` / `desktop-cn-v8` 安全默认值;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 metadata:upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。NapCat base image 在 release evidence 中必须 pin 到 digest。API Jenkins 只做显式参数推广,不负责自动合并上游、自动构建运行时镜像或在 override 为空时隐式改写 NapCat env。 diff --git a/ci/napcat-desktop-cn/Dockerfile b/ci/napcat-desktop-cn/Dockerfile index e85a088..728433f 100644 --- a/ci/napcat-desktop-cn/Dockerfile +++ b/ci/napcat-desktop-cn/Dockerfile @@ -30,7 +30,11 @@ RUN set -eux; \ sha256sum /app/NapCat.Shell.zip | awk '{print $1}' > /ci/napcat-desktop-cn/NapCat.Shell.zip.sha256; \ rm -rf /tmp/NapCat.Shell COPY ci/napcat-desktop-cn/verify.sh /ci/napcat-desktop-cn/verify.sh -RUN sed -i 's/\r$//' /ci/napcat-desktop-cn/verify.sh && chmod +x /ci/napcat-desktop-cn/verify.sh +COPY ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh /ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh +RUN set -eux; \ + sed -i 's/\r$//' /ci/napcat-desktop-cn/verify.sh /ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh; \ + chmod +x /ci/napcat-desktop-cn/verify.sh /ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh; \ + /ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh ENV LANG=zh_CN.UTF-8 \ LC_ALL=zh_CN.UTF-8 \ diff --git a/ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh b/ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh new file mode 100644 index 0000000..b139362 --- /dev/null +++ b/ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh @@ -0,0 +1,159 @@ +#!/bin/sh +set -eu + +ENTRYPOINT=/app/entrypoint.sh + +if grep -q 'KT device profile patch defaults' "$ENTRYPOINT"; then + exit 0 +fi + +grep -q 'mount --bind "$FAKE_CMDLINE" /proc/1/cmdline' "$ENTRYPOINT" + +cat >/tmp/kt-device-profile-defaults.sh <<'EOF' + +# KT device profile patch defaults. +: "${NAPCAT_REQUIRE_DEVICE_PROFILE:=0}" +: "${NAPCAT_DMI_PRODUCT_NAME:=imini Pro}" +: "${NAPCAT_DMI_PRODUCT_UUID:=00000000-0000-0000-0000-000000000000}" +: "${NAPCAT_DMI_SYS_VENDOR:=MECHREVO}" +: "${NAPCAT_DMI_BOARD_VENDOR:=MECHREVO}" +: "${NAPCAT_DMI_BOARD_NAME:=imini Pro}" +: "${NAPCAT_DMI_BIOS_VENDOR:=American Megatrends International, LLC.}" +: "${NAPCAT_DMI_BIOS_VERSION:=imini Pro 1.10}" +: "${NAPCAT_DMI_MODALIAS:=dmi:bvnAmericanMegatrendsInternational,LLC.:bvriminiPro1.10:bd03/31/2024:br1.10:efr1.10:svnMECHREVO:pniminiPro:pvrStandard:rvnMECHREVO:rniminiPro:rvrStandard:cvnMECHREVO:ct3:cvrDefaultstring:skuStandard:}" +: "${NAPCAT_DEVICE_BOOT_ID:=00000000-0000-4000-8000-000000000000}" +: "${NAPCAT_DEVICE_KERNEL_RELEASE:=6.8.0-60-generic}" +: "${NAPCAT_DEVICE_KERNEL_VERSION:=#63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 19:04:15 UTC 2025}" +: "${NAPCAT_DEVICE_PROC_VERSION:=Linux version 6.8.0-60-generic (buildd@lcy02-amd64-001) (x86_64-linux-gnu-gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 19:04:15 UTC 2025}" +: "${NAPCAT_DEVICE_CPU_MODEL:=AMD Ryzen 7 8845H w/ Radeon 780M Graphics}" +: "${NAPCAT_DEVICE_UPTIME:=7200.00 14400.00}" +: "${NAPCAT_DEVICE_TTY_ACTIVE:=tty1}" + +kt_fake_file() { + kt_target="$1" + kt_content="$2" + kt_name="$(echo "$kt_target" | tr '/' '_')" + kt_fake="$FAKE_CGROUP_DIR/kt_device_profile_$kt_name" + printf '%s\n' "$kt_content" > "$kt_fake" + mount --bind "$kt_fake" "$kt_target" 2>/dev/null || true +} +EOF + +cat >/tmp/kt-device-profile-dmi.sh <<'EOF' + +# KT device profile DMI overrides for files QQCore opens. +kt_fake_file /sys/class/dmi/id/product_name "$NAPCAT_DMI_PRODUCT_NAME" +kt_fake_file /sys/class/dmi/id/product_uuid "$NAPCAT_DMI_PRODUCT_UUID" +kt_fake_file /sys/class/dmi/id/sys_vendor "$NAPCAT_DMI_SYS_VENDOR" +kt_fake_file /sys/class/dmi/id/board_vendor "$NAPCAT_DMI_BOARD_VENDOR" +kt_fake_file /sys/class/dmi/id/board_name "$NAPCAT_DMI_BOARD_NAME" +kt_fake_file /sys/class/dmi/id/bios_vendor "$NAPCAT_DMI_BIOS_VENDOR" +kt_fake_file /sys/class/dmi/id/bios_version "$NAPCAT_DMI_BIOS_VERSION" +kt_fake_file /sys/class/dmi/id/modalias "$NAPCAT_DMI_MODALIAS" +EOF + +cat >/tmp/kt-device-profile-proc.sh <<'EOF' + +# KT device profile proc/sys files QQCore opens. +kt_fake_file /proc/sys/kernel/random/boot_id "$NAPCAT_DEVICE_BOOT_ID" +kt_fake_file /proc/sys/kernel/version "$NAPCAT_DEVICE_KERNEL_VERSION" +kt_fake_file /proc/version "$NAPCAT_DEVICE_PROC_VERSION" +kt_fake_file /proc/uptime "$NAPCAT_DEVICE_UPTIME" +if [ -f /proc/cpuinfo ]; then + KT_FAKE_CPUINFO="$FAKE_CGROUP_DIR/kt_device_profile_proc_cpuinfo" + awk -v model="$NAPCAT_DEVICE_CPU_MODEL" ' + /^model name[[:space:]]*:/ { $0 = "model name\t: " model } + /^flags[[:space:]]*:/ { + gsub(/(^| )hypervisor( |$)/, " ") + gsub(/[[:space:]]+/, " ") + } + { print } + ' /proc/cpuinfo > "$KT_FAKE_CPUINFO" 2>/dev/null || true + mount --bind "$KT_FAKE_CPUINFO" /proc/cpuinfo 2>/dev/null || true +fi +if [ -f /proc/devices ]; then + KT_FAKE_DEVICES="$FAKE_CGROUP_DIR/kt_device_profile_proc_devices" + grep -Ev 'trim-trashbin|zvol' /proc/devices > "$KT_FAKE_DEVICES" 2>/dev/null || true + mount --bind "$KT_FAKE_DEVICES" /proc/devices 2>/dev/null || true +fi +if [ -f /sys/devices/virtual/tty/tty0/active ]; then + kt_fake_file /sys/devices/virtual/tty/tty0/active "$NAPCAT_DEVICE_TTY_ACTIVE" +fi +if [ -f /etc/hosts ]; then + KT_FAKE_HOSTS="$FAKE_CGROUP_DIR/kt_device_profile_hosts" + { + printf '127.0.0.1\tlocalhost\n' + printf '::1\tlocalhost ip6-localhost ip6-loopback\n' + printf 'fe00::\tip6-localnet\n' + printf 'ff00::\tip6-mcastprefix\n' + printf 'ff02::1\tip6-allnodes\n' + printf 'ff02::2\tip6-allrouters\n' + } > "$KT_FAKE_HOSTS" + mount --bind "$KT_FAKE_HOSTS" /etc/hosts 2>/dev/null || true +fi +EOF + +cat >/tmp/kt-device-profile-verify.sh <<'EOF' + +if [ "${NAPCAT_REQUIRE_DEVICE_PROFILE:-0}" = "1" ]; then + kt_require_device_profile() { + kt_device_profile_label="$1" + shift + "$@" || { + echo "NapCat device profile check failed: $kt_device_profile_label" >&2 + exit 78 + } + } + + kt_require_device_profile proc1-cmdline sh -c "tr '\\000' ' ' &2 + exit 78 + fi + if [ -f /sys/devices/virtual/tty/tty0/active ]; then + kt_require_device_profile tty-active grep -q "$NAPCAT_DEVICE_TTY_ACTIVE" /sys/devices/virtual/tty/tty0/active + else + echo "NapCat device profile check failed: tty0-active-missing" >&2 + exit 78 + fi + for kt_mountinfo in /proc/self/mountinfo /proc/1/mountinfo; do + if grep -E 'docker|containerd|overlay|\.dockerenv' "$kt_mountinfo" >/dev/null 2>&1; then + echo "NapCat device profile check failed: mountinfo-host-leak:$kt_mountinfo" >&2 + exit 78 + fi + done + if grep -q "$(hostname)" /etc/hosts; then + echo "NapCat device profile check failed: hosts-still-contains-hostname" >&2 + exit 78 + fi + if grep -E 'trim-trashbin|zvol' /proc/devices >/dev/null 2>&1; then + echo "NapCat device profile check failed: proc-devices-host-leak" >&2 + exit 78 + fi +fi +EOF + +sed -i '/FAKE_CGROUP_DIR="\/tmp\/fake_cgroup"/r /tmp/kt-device-profile-defaults.sh' "$ENTRYPOINT" +sed -i '/# Docker Socket/r /tmp/kt-device-profile-dmi.sh' "$ENTRYPOINT" +sed -i '/# \/proc\/1\/sched/r /tmp/kt-device-profile-proc.sh' "$ENTRYPOINT" +sed -i '/: ${NAPCAT_UID:=0}/r /tmp/kt-device-profile-verify.sh' "$ENTRYPOINT" + +rm -f \ + /tmp/kt-device-profile-defaults.sh \ + /tmp/kt-device-profile-dmi.sh \ + /tmp/kt-device-profile-proc.sh \ + /tmp/kt-device-profile-verify.sh diff --git a/ci/napcat-desktop-cn/verify.sh b/ci/napcat-desktop-cn/verify.sh index 511f7f2..8d79b74 100644 --- a/ci/napcat-desktop-cn/verify.sh +++ b/ci/napcat-desktop-cn/verify.sh @@ -44,3 +44,15 @@ grep -R -q 'getQQLoginRuntimeState' "$TMP_DIR" grep -R -q 'qrcodeRevision' "$TMP_DIR" grep -R -q 'needsLoginServiceReset' "$TMP_DIR" grep -R -q '重置已失效登录服务后重新生成二维码' "$TMP_DIR" + +grep -q 'KT device profile patch defaults' /app/entrypoint.sh +grep -q 'NAPCAT_REQUIRE_DEVICE_PROFILE' /app/entrypoint.sh +grep -q '/proc/1/cmdline' /app/entrypoint.sh +grep -q '/proc/1/sched' /app/entrypoint.sh +grep -q '/proc/1/status' /app/entrypoint.sh +grep -q '/proc/1/stat' /app/entrypoint.sh +grep -q '/proc/self/mountinfo' /app/entrypoint.sh +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 diff --git a/scripts/napcat-desktop-cn-stage-build.mjs b/scripts/napcat-desktop-cn-stage-build.mjs index 13d150e..2d8d57c 100644 --- a/scripts/napcat-desktop-cn-stage-build.mjs +++ b/scripts/napcat-desktop-cn-stage-build.mjs @@ -161,6 +161,10 @@ copyIntoContext( resolve(apiRoot, 'ci/napcat-desktop-cn/verify.sh'), resolve(outputRoot, 'ci/napcat-desktop-cn/verify.sh'), ); +copyIntoContext( + resolve(apiRoot, 'ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh'), + resolve(outputRoot, 'ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh'), +); copyIntoContext(shellDist, resolve(outputRoot, 'NapCat.Shell')); const marker = { diff --git a/src/modules/qqbot/napcat/application/runtime/napcat-config-writer.service.ts b/src/modules/qqbot/napcat/application/runtime/napcat-config-writer.service.ts index b263973..e80ce4e 100644 --- a/src/modules/qqbot/napcat/application/runtime/napcat-config-writer.service.ts +++ b/src/modules/qqbot/napcat/application/runtime/napcat-config-writer.service.ts @@ -36,7 +36,7 @@ type NapcatConfig = { process: false; window: false; }; - o3HookMode: 1; + o3HookMode: 0; packetBackend: 'auto'; packetServer: ''; }; @@ -75,7 +75,7 @@ export class NapcatConfigWriterService { process: false, window: false, }, - o3HookMode: 1, + o3HookMode: 0, packetBackend: 'auto', packetServer: '', }; diff --git a/src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts b/src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts index 9dba3bd..d1c2c9c 100644 --- a/src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts +++ b/src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts @@ -162,7 +162,7 @@ export class NapcatRuntimeProfileService { napcatConfigHash: input.protocolProfile.napcatConfigHash || null, napcatConfigJson: input.protocolProfile.napcatConfigJson || null, o3HookGrayEnabled: !!input.protocolProfile.o3HookGrayEnabled, - o3HookMode: input.protocolProfile.o3HookMode ?? 1, + o3HookMode: input.protocolProfile.o3HookMode ?? 0, onebotConfigHash: input.protocolProfile.onebotConfigHash || null, onebotConfigJson: input.protocolProfile.onebotConfigJson || null, packetBackend: input.protocolProfile.packetBackend || 'auto', diff --git a/src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts b/src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts index 87d14d0..86337b3 100644 --- a/src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts +++ b/src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts @@ -1438,6 +1438,31 @@ fi `DEVICE_ENV_PATH=${this.sh(input.deviceIdentity.deviceEnvPath)}`, ].join('\n') + '\n' : ''; + const deviceProfileHeader = [ + `NAPCAT_DMI_PRODUCT_NAME=${this.sh('imini Pro')}`, + `NAPCAT_DMI_SYS_VENDOR=${this.sh('MECHREVO')}`, + `NAPCAT_DMI_BOARD_VENDOR=${this.sh('MECHREVO')}`, + `NAPCAT_DMI_BOARD_NAME=${this.sh('imini Pro')}`, + `NAPCAT_DMI_BIOS_VENDOR=${this.sh( + 'American Megatrends International, LLC.', + )}`, + `NAPCAT_DMI_BIOS_VERSION=${this.sh('imini Pro 1.10')}`, + `NAPCAT_DMI_MODALIAS=${this.sh( + 'dmi:bvnAmericanMegatrendsInternational,LLC.:bvriminiPro1.10:bd03/31/2024:br1.10:efr1.10:svnMECHREVO:pniminiPro:pvrStandard:rvnMECHREVO:rniminiPro:rvrStandard:cvnMECHREVO:ct3:cvrDefaultstring:skuStandard:', + )}`, + `NAPCAT_DEVICE_KERNEL_RELEASE=${this.sh('6.8.0-60-generic')}`, + `NAPCAT_DEVICE_KERNEL_VERSION=${this.sh( + '#63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 19:04:15 UTC 2025', + )}`, + `NAPCAT_DEVICE_PROC_VERSION=${this.sh( + 'Linux version 6.8.0-60-generic (buildd@lcy02-amd64-001) (x86_64-linux-gnu-gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 19:04:15 UTC 2025', + )}`, + `NAPCAT_DEVICE_CPU_MODEL=${this.sh( + 'AMD Ryzen 7 8845H w/ Radeon 780M Graphics', + )}`, + `NAPCAT_DEVICE_UPTIME=${this.sh('7200.00 14400.00')}`, + `NAPCAT_DEVICE_TTY_ACTIVE=${this.sh('tty1')}`, + ].join('\n'); const devicePrepareScript = input.deviceIdentity ? ` mkdir -p "$(dirname "$DEVICE_ENV_PATH")" "$(dirname "$MACHINE_INFO_PATH")" @@ -1464,9 +1489,33 @@ mv "$MACHINE_INFO_TMP" "$MACHINE_INFO_PATH" chmod 644 "$MACHINE_INFO_PATH" ` : ''; + const deviceProfilePrepareScript = ` +if [ -z "\${MACHINE_ID_PATH:-}" ]; then + MACHINE_ID_PATH="$DATA_DIR/machine-id" +fi +mkdir -p "$(dirname "$MACHINE_ID_PATH")" +if [ ! -s "$MACHINE_ID_PATH" ]; then + printf '%s' "$NAME" | sha256sum | cut -c 1-32 > "$MACHINE_ID_PATH" +fi +NAPCAT_DEVICE_MACHINE_ID="$(tr -d '\\r\\n' < "$MACHINE_ID_PATH" | cut -c 1-64)" +format_uuid_from_seed() { + uuid_hash="$(printf '%s' "$1" | sha256sum | awk '{print $1}')" + printf '%s-%s-%s-%s-%s' \\ + "$(printf '%s' "$uuid_hash" | cut -c 1-8)" \\ + "$(printf '%s' "$uuid_hash" | cut -c 9-12)" \\ + "$(printf '%s' "$uuid_hash" | cut -c 13-16)" \\ + "$(printf '%s' "$uuid_hash" | cut -c 17-20)" \\ + "$(printf '%s' "$uuid_hash" | cut -c 21-32)" +} +if [ ! -s "$DATA_DIR/device-boot-id" ]; then + format_uuid_from_seed "$NAPCAT_DEVICE_MACHINE_ID:boot" > "$DATA_DIR/device-boot-id" +fi +NAPCAT_DEVICE_BOOT_ID="$(tr -d '\\r\\n' < "$DATA_DIR/device-boot-id" | cut -c 1-36)" +NAPCAT_DMI_PRODUCT_UUID="$(format_uuid_from_seed "$NAPCAT_DEVICE_MACHINE_ID:dmi")" +`; const deviceRunFlags = input.deviceIdentity ? ' --hostname "$NAPCAT_HOSTNAME" \\\n --mac-address "$NAPCAT_MAC_ADDRESS" \\\n -v "$MACHINE_ID_PATH:/etc/machine-id:ro" \\\n' - : ''; + : ' -v "$MACHINE_ID_PATH:/etc/machine-id:ro" \\\n'; return ` set -eu @@ -1482,10 +1531,12 @@ NAPCAT_SHM_SIZE=${this.sh(runtimeProfile.shmSize)} ${accountHeader} ${passwordHeader} ${deviceHeader} +${deviceProfileHeader} mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs" "$DATA_DIR/cache" "$DATA_DIR/local-share" "$DATA_DIR/runtime" chmod 700 "$DATA_DIR" chmod 700 "$DATA_DIR/runtime" ${devicePrepareScript} +${deviceProfilePrepareScript} ${configWriteScript} @@ -1494,9 +1545,29 @@ docker run -d \\ --name "$NAME" \\ --restart unless-stopped \\ --init \\ + --cap-add SYS_ADMIN \\ + --security-opt apparmor=unconfined \\ + --security-opt seccomp=unconfined \\ --shm-size "$NAPCAT_SHM_SIZE" \\ -e NAPCAT_UID="$NAPCAT_UID" \\ -e NAPCAT_GID="$NAPCAT_GID" \\ + -e NAPCAT_REQUIRE_DEVICE_PROFILE=1 \\ + -e NAPCAT_DEVICE_MACHINE_ID="$NAPCAT_DEVICE_MACHINE_ID" \\ + -e NAPCAT_DEVICE_BOOT_ID="$NAPCAT_DEVICE_BOOT_ID" \\ + -e NAPCAT_DEVICE_KERNEL_RELEASE="$NAPCAT_DEVICE_KERNEL_RELEASE" \\ + -e NAPCAT_DEVICE_KERNEL_VERSION="$NAPCAT_DEVICE_KERNEL_VERSION" \\ + -e NAPCAT_DEVICE_PROC_VERSION="$NAPCAT_DEVICE_PROC_VERSION" \\ + -e NAPCAT_DEVICE_CPU_MODEL="$NAPCAT_DEVICE_CPU_MODEL" \\ + -e NAPCAT_DEVICE_UPTIME="$NAPCAT_DEVICE_UPTIME" \\ + -e NAPCAT_DEVICE_TTY_ACTIVE="$NAPCAT_DEVICE_TTY_ACTIVE" \\ + -e NAPCAT_DMI_PRODUCT_NAME="$NAPCAT_DMI_PRODUCT_NAME" \\ + -e NAPCAT_DMI_PRODUCT_UUID="$NAPCAT_DMI_PRODUCT_UUID" \\ + -e NAPCAT_DMI_SYS_VENDOR="$NAPCAT_DMI_SYS_VENDOR" \\ + -e NAPCAT_DMI_BOARD_VENDOR="$NAPCAT_DMI_BOARD_VENDOR" \\ + -e NAPCAT_DMI_BOARD_NAME="$NAPCAT_DMI_BOARD_NAME" \\ + -e NAPCAT_DMI_BIOS_VENDOR="$NAPCAT_DMI_BIOS_VENDOR" \\ + -e NAPCAT_DMI_BIOS_VERSION="$NAPCAT_DMI_BIOS_VERSION" \\ + -e NAPCAT_DMI_MODALIAS="$NAPCAT_DMI_MODALIAS" \\ -e WEBUI_TOKEN="$WEBUI_TOKEN" \\ -e LANG=${runtimeProfile.locale} \\ -e LC_ALL=${runtimeProfile.locale} \\ diff --git a/test/modules/qqbot/napcat/device-identity.spec.ts b/test/modules/qqbot/napcat/device-identity.spec.ts index c875c92..93d5510 100644 --- a/test/modules/qqbot/napcat/device-identity.spec.ts +++ b/test/modules/qqbot/napcat/device-identity.spec.ts @@ -436,6 +436,15 @@ describe('NapCat device identity persistence', () => { expect(createScript).toContain('--hostname "$NAPCAT_HOSTNAME"'); expect(createScript).toContain('--mac-address "$NAPCAT_MAC_ADDRESS"'); expect(createScript).toContain('-v "$MACHINE_ID_PATH:/etc/machine-id:ro"'); + expect(createScript).toContain('--cap-add SYS_ADMIN \\'); + expect(createScript).toContain('--security-opt apparmor=unconfined \\'); + expect(createScript).toContain('--security-opt seccomp=unconfined \\'); + expect(createScript).toContain( + '-e NAPCAT_DEVICE_MACHINE_ID="$NAPCAT_DEVICE_MACHINE_ID"', + ); + expect(createScript).toContain( + '-e NAPCAT_DEVICE_CPU_MODEL="$NAPCAT_DEVICE_CPU_MODEL"', + ); expect(createScript).toContain( '-v "$DATA_DIR/runtime:/tmp/runtime-napcat"', ); @@ -518,6 +527,15 @@ describe('NapCat device identity persistence', () => { expect(createScript).toContain('--hostname "$NAPCAT_HOSTNAME"'); expect(createScript).toContain('--mac-address "$NAPCAT_MAC_ADDRESS"'); expect(createScript).toContain('-v "$MACHINE_ID_PATH:/etc/machine-id:ro"'); + expect(createScript).toContain('--cap-add SYS_ADMIN \\'); + expect(createScript).toContain('--security-opt apparmor=unconfined \\'); + expect(createScript).toContain('--security-opt seccomp=unconfined \\'); + expect(createScript).toContain( + '-e NAPCAT_DMI_PRODUCT_NAME="$NAPCAT_DMI_PRODUCT_NAME"', + ); + expect(createScript).toContain( + '-e NAPCAT_DEVICE_TTY_ACTIVE="$NAPCAT_DEVICE_TTY_ACTIVE"', + ); expect(createScript).toContain('-e LANG=zh_CN.UTF-8'); expect(createScript).toContain('-e LC_ALL=zh_CN.UTF-8'); expect(createScript).toContain('-e TZ=Asia/Shanghai'); diff --git a/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts b/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts index f40fd0c..0928dc7 100644 --- a/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts +++ b/test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts @@ -40,10 +40,47 @@ describe('NapCat Chinese Desktop Runtime image assets', () => { expect(verify).toContain('/.dockerenv'); expect(verify).toContain('wait_for_absent'); expect(verify).toContain('/proc/1/cgroup'); + expect(verify).toContain('/proc/1/cmdline'); + expect(verify).toContain('/proc/1/sched'); + expect(verify).toContain('/proc/1/status'); + expect(verify).toContain('/proc/1/stat'); + expect(verify).toContain('/proc/self/mountinfo'); + expect(verify).toContain('/sys/class/dmi/id/product_name'); + expect(verify).toContain('/sys/class/dmi/id/bios_vendor'); + expect(verify).toContain('/sys/class/dmi/id/bios_version'); + expect(verify).toContain('/etc/hosts'); expect(verify).toContain('XDG_CONFIG_HOME=/app/.config'); expect(verify).toContain('Asia/Shanghai'); }); + it('patches entrypoint device profile probes that QQCore opens at runtime', () => { + const dockerfile = readSource('ci/napcat-desktop-cn/Dockerfile'); + const entrypointPatch = readSource( + 'ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh', + ); + const source = `${dockerfile}\n${entrypointPatch}`; + + expect(source).toContain('entrypoint-device-profile.patch.sh'); + expect(source).toContain('NAPCAT_REQUIRE_DEVICE_PROFILE'); + expect(source).toContain('NAPCAT_DMI_BIOS_VENDOR'); + expect(source).toContain('NAPCAT_DMI_BIOS_VERSION'); + expect(source).toContain('NAPCAT_DMI_MODALIAS'); + expect(source).toContain('NAPCAT_DEVICE_BOOT_ID'); + expect(source).toContain('NAPCAT_DEVICE_KERNEL_RELEASE'); + expect(source).toContain('NAPCAT_DEVICE_CPU_MODEL'); + expect(source).toContain('NAPCAT_DEVICE_UPTIME'); + expect(source).toContain('NAPCAT_DEVICE_TTY_ACTIVE'); + expect(source).toContain('/proc/uptime'); + expect(source).toContain('/proc/cpuinfo'); + expect(source).toContain('/sys/devices/virtual/tty/tty0/active'); + expect(source).toContain('/proc/self/mountinfo'); + expect(source).toContain('mount --bind "$FAKE_CMDLINE" /proc/1/cmdline'); + expect(source).toContain('kt_require_device_profile'); + expect(source).toContain('exit 78'); + expect(source).toContain('dmi-modalias'); + expect(source).toContain('proc-devices-host-leak'); + }); + it('stages source-built NapCat Shell artifacts for Docker build context', () => { const script = readSource('scripts/napcat-desktop-cn-stage-build.mjs'); @@ -56,6 +93,7 @@ describe('NapCat Chinese Desktop Runtime image assets', () => { expect(script).toContain('jenkinsBuildUrl'); expect(script).toContain('packages/napcat-shell/dist'); expect(script).toContain('fork-artifact.json'); + expect(script).toContain('entrypoint-device-profile.patch.sh'); expect(script).toContain('.kt-workspace/napcat-desktop-cn-build'); expect(script).toContain('assertSafeOutputRoot'); expect(script).toContain('workspaceRoot'); diff --git a/test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts b/test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts index cd68d42..ebb1752 100644 --- a/test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts +++ b/test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts @@ -181,9 +181,9 @@ describe('NapCat runtime profile generation', () => { }, protocolProfile: { napcatConfigHash: 'napcat-hash', - napcatConfigJson: { o3HookMode: 1 }, + napcatConfigJson: { o3HookMode: 0 }, o3HookGrayEnabled: false, - o3HookMode: 1, + o3HookMode: 0, onebotConfigHash: 'onebot-hash', onebotConfigJson: { network: { websocketClients: [] } }, packetBackend: 'auto', @@ -253,6 +253,7 @@ describe('NapCat runtime profile generation', () => { reconnectInterval: 5000, reportSelfMessage: false, }); + expect(result.napcatConfig.o3HookMode).toBe(0); expect( result.files.find((file) => file.path === 'webui.json')?.content, ).toContain(webuiAuthValue); diff --git a/test/qqbot/napcat/qqbot-napcat-container.service.spec.ts b/test/qqbot/napcat/qqbot-napcat-container.service.spec.ts index b3e0c5e..9234a1c 100644 --- a/test/qqbot/napcat/qqbot-napcat-container.service.spec.ts +++ b/test/qqbot/napcat/qqbot-napcat-container.service.spec.ts @@ -505,9 +505,32 @@ describe('QqbotNapcatContainerService', () => { expect(script).toContain("NAPCAT_GID='1101'"); expect(script).toContain("NAPCAT_SHM_SIZE='512m'"); expect(script).toContain('--init \\'); + expect(script).toContain('--cap-add SYS_ADMIN \\'); + expect(script).toContain('--security-opt apparmor=unconfined \\'); + expect(script).toContain('--security-opt seccomp=unconfined \\'); expect(script).toContain('--shm-size "$NAPCAT_SHM_SIZE"'); expect(script).toContain('-e NAPCAT_UID="$NAPCAT_UID"'); expect(script).toContain('-e NAPCAT_GID="$NAPCAT_GID"'); + expect(script).toContain('-e NAPCAT_REQUIRE_DEVICE_PROFILE=1'); + expect(script).toContain('-e NAPCAT_DMI_SYS_VENDOR="$NAPCAT_DMI_SYS_VENDOR"'); + expect(script).toContain( + '-e NAPCAT_DMI_PRODUCT_UUID="$NAPCAT_DMI_PRODUCT_UUID"', + ); + expect(script).toContain( + '-e NAPCAT_DEVICE_BOOT_ID="$NAPCAT_DEVICE_BOOT_ID"', + ); + expect(script).toContain( + '-e NAPCAT_DEVICE_KERNEL_RELEASE="$NAPCAT_DEVICE_KERNEL_RELEASE"', + ); + expect(script).toContain( + '-e NAPCAT_DEVICE_CPU_MODEL="$NAPCAT_DEVICE_CPU_MODEL"', + ); + expect(script).toContain( + '-e NAPCAT_DEVICE_UPTIME="$NAPCAT_DEVICE_UPTIME"', + ); + expect(script).toContain( + '-e NAPCAT_DEVICE_TTY_ACTIVE="$NAPCAT_DEVICE_TTY_ACTIVE"', + ); expect(script).toContain('-e LANG=zh_CN.UTF-8'); expect(script).toContain('-e LC_ALL=zh_CN.UTF-8'); expect(script).toContain('-e LANGUAGE=zh_CN:zh');