12 KiB
QQBot NapCat Device Name And Timezone Hardening Implementation Plan
Execution note: Execute this plan task-by-task with the KT-local workflow and use the checkboxes to track plan state.
Goal: Restore a visible QQ security-page device name while keeping NapCat device identity persistent, fix mixed UTC/Asia-Shanghai timestamps, and make runtime/profile evidence persist online.
Architecture: Use upstream NapCat evidence as the source of truth: QQNT Linux reads os.hostname() for hostName/devName, new-device OIDB sends str_dev_name: os.hostname(), and Linux GUID is based on /etc/machine-id + machine-info MAC. The online regression evidence shows the new container exposes ubuntu-pc-*, but Docker MAC was changed to a physical OUI while QQNT machine-info still stored the old 02:42:* MAC, creating inconsistent device identity. Generate a short stable QQNT-safe hostname that the upstream Docker entrypoint will not rewrite, return MAC strategy to upstream-compatible stable Docker bridge 02:42:*, write matching machine-info, persist profile rows after container creation, and set MySQL connection timezone explicitly.
Tech Stack: NestJS, TypeORM, MySQL, NapCat-Docker, Jest, PowerShell/NAS SSH smoke.
File Map
- Modify
src/modules/qqbot/napcat/infrastructure/integration/device/napcat-device-identity.service.ts- Generate short stable hostname
pc-<8 hex>to avoid NapCat-Docker entrypoint reset and avoid long desktop names that regress to未知设备. - Generate stable upstream-compatible Docker bridge MAC
02:42:*. - Migrate existing
desktop-hostname-v1/physical-oui-v1identities back to QQNT-safe defaults with evidence.
- Generate short stable hostname
- Modify
src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts- Carry
machineInfoPathand hyphen MAC for QQNT Linuxmachine-info.
- Carry
- Modify
src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts- Write
device.env,/etc/machine-id, and QQNTmachine-infoconsistently. - Create and mount a persistent
XDG_RUNTIME_DIR. - Update
napcat_account_binding.device_identity_idafter create/rebuild. - Persist runtime/protocol profile after successful container creation.
- Write
- Modify
src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts- Persist planned runtime and protocol profile rows when repositories are injected.
- Modify
src/app.module.ts- Set TypeORM MySQL timezone from
DB_TIMEZONE, default+08:00.
- Set TypeORM MySQL timezone from
- Modify
src/runtime/config/runtime-config.types.tsandsrc/runtime/config/runtime-config.service.ts- Expose DB timezone in runtime health evidence.
- Modify
ci/napcat-desktop-cn/Dockerfile- Ensure the derived image keeps
zh_CN.UTF-8available and does not get shadowed by upstreamC.UTF-8.
- Ensure the derived image keeps
- Modify
.env.example,README.md,API.md,sql/qqbot-init.sql,sql/refactor-v3/00-full-schema.sql,sql/refactor-v3/99-verify.sql- Document/align defaults and schema verification.
- Modify tests:
test/modules/qqbot/napcat/device-identity.spec.tstest/modules/qqbot/napcat/runtime-protocol-profile.spec.tstest/runtime/runtime-config.service.spec.ts- Add
test/app/typeorm-timezone-config.spec.ts
Task 1: Device Identity Regression RED/GREEN
Files:
-
Modify:
test/modules/qqbot/napcat/device-identity.spec.ts -
Modify:
src/modules/qqbot/napcat/infrastructure/integration/device/napcat-device-identity.service.ts -
Modify:
src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts -
Step 1: Write failing tests
Add expectations that new identities use a QQNT-safe visible hostname and upstream-compatible MAC:
expect(identity.hostname).toMatch(/^pc-[a-f0-9]{8}$/);
expect(identity.hostname).not.toMatch(/^[a-f0-9]{12,}$/);
expect(identity.macAddress).toMatch(/^02:42:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}$/);
expect(identity.hostnameStrategy).toBe('qqnt-visible-hostname-v1');
expect(identity.macStrategy).toBe('docker-bridge-mac-v1');
Also update the migration test so a legacy ubuntu-pc-* / physical OUI identity migrates to the new strategies and records trigger: 'qqnt-device-name-regression-repair'.
- Step 2: Run RED
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts --runInBand
Expected: FAIL because current code emits ubuntu-pc-* and physical OUI MAC.
- Step 3: Implement identity generation
Change the service to:
private buildQqntVisibleHostname(seed: string) {
const hash = createHash('sha256').update(seed).digest('hex');
return `pc-${hash.slice(0, 8)}`;
}
private buildDockerBridgeMacAddress(accountId: string, containerName: string) {
const hash = createHash('sha256')
.update(`${accountId}:${containerName}:docker-bridge-mac-v1`)
.digest('hex');
return `02:42:${hash.slice(0, 2)}:${hash.slice(2, 4)}:${hash.slice(4, 6)}:${hash.slice(6, 8)}`.toLowerCase();
}
Update migration predicates to migrate too-long hostnames, pure 12+ hex hostnames, desktop-hostname-v1, physical-oui-v1, rejected virtual prefixes outside 02:42, and values containing qq|bot|napcat|docker|container|lxc.
- Step 4: Run GREEN
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts --runInBand
Expected: PASS.
Task 2: Docker Script Machine-Info And Binding RED/GREEN
Files:
-
Modify:
test/modules/qqbot/napcat/device-identity.spec.ts -
Modify:
src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts -
Modify:
src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts -
Step 1: Write failing tests
Assert the generated script writes QQNT machine-info with a backup, mounts persistent runtime dir, and updates binding deviceIdentityId:
expect(createScript).toContain('MACHINE_INFO_PATH=');
expect(createScript).toContain('MAC_HYPHEN=');
expect(createScript).toContain("printf '\\\\000\\\\000\\\\000\\\\021'");
expect(createScript).toContain("tr 'A-Za-z' 'N-ZA-Mn-za-m'");
expect(createScript).toContain('-v "$DATA_DIR/runtime:/tmp/runtime-napcat"');
expect(bindingRepository.update).toHaveBeenCalledWith(
expect.objectContaining({ accountId: 'account-10001', containerId: 'container-created' }),
expect.objectContaining({ deviceIdentityId: expect.any(String) }),
);
- Step 2: Run RED
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts --runInBand
Expected: FAIL because machine-info and binding updates are not implemented.
- Step 3: Implement script and binding update
Add machineInfoPath and macAddressHyphen to NapcatDockerDeviceOptions. In buildRemoteCreateScript, write:
MACHINE_INFO_PATH="$DATA_DIR/QQ/nt_qq/global/nt_data/msf/machine-info"
NAPCAT_MAC_HYPHEN="$(printf '%s' "$NAPCAT_MAC_ADDRESS" | tr ':' '-')"
mkdir -p "$(dirname "$MACHINE_INFO_PATH")"
if [ -s "$MACHINE_INFO_PATH" ]; then
CURRENT_MACHINE_INFO="$(mktemp)"
cp "$MACHINE_INFO_PATH" "$CURRENT_MACHINE_INFO"
fi
{
printf '\000\000\000\021'
printf '%s' "$MAC_HYPHEN" | tr 'A-Za-z' 'N-ZA-Mn-za-m'
} > "$MACHINE_INFO_PATH"
After successful create/rebuild, update napcat_account_binding.device_identity_id for the account/container pair.
- Step 4: Run GREEN
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts --runInBand
Expected: PASS.
Task 3: Runtime/Profile Persistence RED/GREEN
Files:
-
Modify:
test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts -
Modify:
src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts -
Modify:
src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts -
Step 1: Write failing tests
Add a test that calls recordPlannedProfiles() and expects both repositories to save rows:
expect(runtimeProfileRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
accountId: 'account-1',
deviceIdentityId: 'identity-1',
hostnameStrategy: 'qqnt-visible-hostname-v1',
macStrategy: 'docker-bridge-mac-v1',
profileStatus: 'pending',
}),
);
expect(protocolProfileRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
accountId: 'account-1',
o3HookMode: 1,
packetBackend: 'auto',
profileStatus: 'pending',
}),
);
- Step 2: Run RED
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runInBand
Expected: FAIL because the service currently does not persist generated profiles.
- Step 3: Implement persistence
Add optional injected repositories to NapcatRuntimeProfileService, implement recordPlannedProfiles(input), and call it after successful Docker create/rebuild with the runtime snapshot and config bundle hashes.
- Step 4: Run GREEN
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runInBand
Expected: PASS.
Task 4: MySQL Timezone RED/GREEN
Files:
-
Add:
test/app/typeorm-timezone-config.spec.ts -
Modify:
src/app.module.ts -
Modify:
src/runtime/config/runtime-config.types.ts -
Modify:
src/runtime/config/runtime-config.service.ts -
Step 1: Write failing tests
Extract a small exported helper from app.module.ts or test the factory through metadata so the returned TypeORM options include:
expect(options.timezone).toBe('+08:00');
Also assert RuntimeConfigService.readDatabaseProfile() reports timezone: '+08:00'.
- Step 2: Run RED
Run:
pnpm exec jest --runTestsByPath test/app/typeorm-timezone-config.spec.ts test/runtime/runtime-config.service.spec.ts --runInBand
Expected: FAIL because no DB timezone is configured or exposed.
- Step 3: Implement timezone config
Set TypeORM MySQL timezone to configService.get('DB_TIMEZONE') || '+08:00', and expose the same field from runtime health config.
- Step 4: Run GREEN
Run:
pnpm exec jest --runTestsByPath test/app/typeorm-timezone-config.spec.ts test/runtime/runtime-config.service.spec.ts --runInBand
Expected: PASS.
Task 5: Image, SQL, Docs, And Verification
Files:
-
Modify:
ci/napcat-desktop-cn/Dockerfile -
Modify:
.env.example -
Modify:
README.md -
Modify:
API.md -
Modify:
sql/qqbot-init.sql -
Modify:
sql/refactor-v3/00-full-schema.sql -
Modify:
sql/refactor-v3/99-verify.sql -
Modify:
TASKS.md -
Step 1: Keep derived image actually Chinese-locale capable
Ensure the Dockerfile installs locales, generates zh_CN.UTF-8, and sets LC_ALL=zh_CN.UTF-8. The verification script must fail if locale -a lacks zh_CN.utf8.
- Step 2: Align SQL defaults and docs
Document DB_TIMEZONE=+08:00, QQBOT_NAPCAT_IMAGE derived image usage, and device identity strategy names. Update SQL verify checks for device_identity_id, strategy columns, and profile tables.
- Step 3: Run focused validation
Run:
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts test/app/typeorm-timezone-config.spec.ts test/runtime/runtime-config.service.spec.ts --runInBand
pnpm run typecheck
git diff --check
Expected: all pass.
- Step 4: Online controlled repair after deploy
After commit/push/deploy, run a scoped online repair:
- Backup
napcat_device_identity,napcat_account_binding,napcat_runtime_profile,napcat_protocol_profile,napcat_login_session. - Restart/recreate NapCat containers through the managed API path so code writes DB binding/profile rows and Docker machine-info.
- Verify:
napcat_account_binding.device_identity_idis not null for enabled accounts.- profile tables have rows for enabled accounts.
- each running container has hostname
pc-<8 hex>, Docker MAC02:42:*,/etc/machine-id, decodedmachine-infoMAC equal to Docker MAC,TZ=Asia/Shanghai, writable XDG runtime dir, andlocale -aincludeszh_CN.utf8. - MySQL
NOW()offset or API session rows no longer mix UTC and CST fields.
- Ask user to scan one login QR if QQ requires new-device verification; then confirm QQ mobile page shows a non-unknown device name.