307 lines
12 KiB
Markdown
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.
|