fix: 补齐NapCat设备拟真探针覆盖

This commit is contained in:
sunlei 2026-06-26 17:21:18 +08:00
parent e3ab00c00f
commit 074bee1d4b
12 changed files with 339 additions and 9 deletions

4
API.md
View File

@ -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 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

@ -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 \

View File

@ -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' ' ' </proc/1/cmdline | grep -q '/sbin/init'"
kt_require_device_profile proc1-sched grep -q 'systemd' /proc/1/sched
kt_require_device_profile proc1-status grep -q '^NSpid:[[:space:]]*1' /proc/1/status
kt_require_device_profile proc1-stat grep -q '(systemd)' /proc/1/stat
kt_require_device_profile dmi-product grep -q "$NAPCAT_DMI_PRODUCT_NAME" /sys/class/dmi/id/product_name
kt_require_device_profile dmi-product-uuid grep -q "$NAPCAT_DMI_PRODUCT_UUID" /sys/class/dmi/id/product_uuid
kt_require_device_profile dmi-sys-vendor grep -q "$NAPCAT_DMI_SYS_VENDOR" /sys/class/dmi/id/sys_vendor
kt_require_device_profile dmi-bios-vendor grep -q "$NAPCAT_DMI_BIOS_VENDOR" /sys/class/dmi/id/bios_vendor
kt_require_device_profile dmi-bios-version grep -q "$NAPCAT_DMI_BIOS_VERSION" /sys/class/dmi/id/bios_version
kt_require_device_profile dmi-modalias grep -q "$NAPCAT_DMI_MODALIAS" /sys/class/dmi/id/modalias
kt_require_device_profile boot-id grep -q "$NAPCAT_DEVICE_BOOT_ID" /proc/sys/kernel/random/boot_id
kt_require_device_profile kernel-version grep -q "$NAPCAT_DEVICE_KERNEL_VERSION" /proc/sys/kernel/version
kt_require_device_profile proc-version grep -q "$NAPCAT_DEVICE_KERNEL_RELEASE" /proc/version
kt_require_device_profile proc-uptime grep -q "$NAPCAT_DEVICE_UPTIME" /proc/uptime
kt_require_device_profile proc-cpuinfo grep -q "$NAPCAT_DEVICE_CPU_MODEL" /proc/cpuinfo
if grep -q 'hypervisor' /proc/cpuinfo; then
echo "NapCat device profile check failed: cpuinfo-hypervisor-flag" >&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

View File

@ -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

View File

@ -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 = {

View File

@ -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: '',
};

View File

@ -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',

View File

@ -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} \\

View File

@ -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');

View File

@ -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');

View File

@ -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);

View File

@ -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');