kt-template-online-api/docs/superpowers/plans/2026-06-18-qqbot-napcat-device-name-timezone-hardening-plan.md

12 KiB

QQBot NapCat Device Name And Timezone Hardening Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

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-v1 identities back to QQNT-safe defaults with evidence.
  • Modify src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts
    • Carry machineInfoPath and hyphen MAC for QQNT Linux machine-info.
  • Modify src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service.ts
    • Write device.env, /etc/machine-id, and QQNT machine-info consistently.
    • Create and mount a persistent XDG_RUNTIME_DIR.
    • Update napcat_account_binding.device_identity_id after create/rebuild.
    • Persist runtime/protocol profile after successful container creation.
  • 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.
  • Modify src/runtime/config/runtime-config.types.ts and src/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-8 available and does not get shadowed by upstream C.UTF-8.
  • 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.ts
    • test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts
    • test/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:

  1. Backup napcat_device_identity, napcat_account_binding, napcat_runtime_profile, napcat_protocol_profile, napcat_login_session.
  2. Restart/recreate NapCat containers through the managed API path so code writes DB binding/profile rows and Docker machine-info.
  3. Verify:
    • napcat_account_binding.device_identity_id is not null for enabled accounts.
    • profile tables have rows for enabled accounts.
    • each running container has hostname pc-<8 hex>, Docker MAC 02:42:*, /etc/machine-id, decoded machine-info MAC equal to Docker MAC, TZ=Asia/Shanghai, writable XDG runtime dir, and locale -a includes zh_CN.utf8.
    • MySQL NOW() offset or API session rows no longer mix UTC and CST fields.
  4. Ask user to scan one login QR if QQ requires new-device verification; then confirm QQ mobile page shows a non-unknown device name.