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

307 lines
12 KiB
Markdown

# 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-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`
- [x] **Step 1: Write failing tests**
Add expectations that new identities use a QQNT-safe visible hostname and upstream-compatible MAC:
```ts
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'`.
- [x] **Step 2: Run RED**
Run:
```powershell
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.
- [x] **Step 3: Implement identity generation**
Change the service to:
```ts
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`.
- [x] **Step 4: Run GREEN**
Run:
```powershell
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`
- [x] **Step 1: Write failing tests**
Assert the generated script writes QQNT machine-info with a backup, mounts persistent runtime dir, and updates binding `deviceIdentityId`:
```ts
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) }),
);
```
- [x] **Step 2: Run RED**
Run:
```powershell
pnpm exec jest --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts --runInBand
```
Expected: FAIL because `machine-info` and binding updates are not implemented.
- [x] **Step 3: Implement script and binding update**
Add `machineInfoPath` and `macAddressHyphen` to `NapcatDockerDeviceOptions`. In `buildRemoteCreateScript`, write:
```sh
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.
- [x] **Step 4: Run GREEN**
Run:
```powershell
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`
- [x] **Step 1: Write failing tests**
Add a test that calls `recordPlannedProfiles()` and expects both repositories to save rows:
```ts
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',
}),
);
```
- [x] **Step 2: Run RED**
Run:
```powershell
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.
- [x] **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.
- [x] **Step 4: Run GREEN**
Run:
```powershell
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`
- [x] **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:
```ts
expect(options.timezone).toBe('+08:00');
```
Also assert `RuntimeConfigService.readDatabaseProfile()` reports `timezone: '+08:00'`.
- [x] **Step 2: Run RED**
Run:
```powershell
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.
- [x] **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.
- [x] **Step 4: Run GREEN**
Run:
```powershell
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`
- [x] **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`.
- [x] **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.
- [x] **Step 3: Run focused validation**
Run:
```powershell
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.