diff --git a/docs/superpowers/plans/2026-06-23-qqbot-napcat-source-fork-qr-refresh-implementation-plan.md b/docs/superpowers/plans/2026-06-23-qqbot-napcat-source-fork-qr-refresh-implementation-plan.md new file mode 100644 index 0000000..8574e45 --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-qqbot-napcat-source-fork-qr-refresh-implementation-plan.md @@ -0,0 +1,1580 @@ +# QQBot NapCat Source Fork QR Refresh 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:** Build and ship a source-level NapCatQQ fork fix so stale WebUI login state no longer blocks fresh QR generation, then consume that fork through the KT `desktop-cn-v3` NapCat image. + +**Architecture:** Fix the root state model in NapCatQQ first, then replace the KT image's bundled-JS patch with a staged source-built Shell artifact. Keep API stale-QR guards in place as defense-in-depth, and prove the result with fork tests, image static tests, image verify, and one production canary account. + +**Tech Stack:** NapCatQQ monorepo, TypeScript, Vitest, Vite shell build, NestJS API repo, Jest, Docker, NAS SSH, Jenkins/K8s. + +--- + +## File Structure + +### NapCatQQ Fork + +Target repo: `D:\MyFiles\KT\GitHub\NapCatQQ` + +- Modify: `packages/napcat-webui-backend/src/types/index.ts` + - Owns the runtime state and QR refresh result types. +- Modify: `packages/napcat-webui-backend/src/helper/Data.ts` + - Owns WebUI login runtime state, QR revision, stale login-state reconciliation, and refresh result semantics. +- Modify: `packages/napcat-webui-backend/src/api/QQLogin.ts` + - Owns WebUI login HTTP handler behavior. +- Modify: `packages/napcat-shell/base.ts` + - Wires Shell-mode QR refresh callback to return `loginService.getQRCodePicture()`. +- Modify: `packages/napcat-framework/napcat.ts` + - Wires Framework-mode QR refresh callback to return `loginService.getQRCodePicture()`. +- Modify: `packages/napcat-test/vitest.config.ts` + - Adds aliases needed by the new tests. +- Create: `packages/napcat-test/webuiLoginRuntime.test.ts` + - Tests stale login reconciliation, QR revision, and refresh observability. +- Create: `packages/napcat-test/webuiQQLoginHandlers.test.ts` + - Tests handlers do not return `QQ Is Logined` for stale WebUI login state. +- Create: `packages/napcat-test/webuiLoginSourceWiring.test.ts` + - Static safety test for Shell/Framework callback wiring. + +### KT API Repo + +Target repo: `D:\MyFiles\KT\Node\kt-template-online-api` + +- Create: `scripts/napcat-desktop-cn-stage-build.mjs` + - Stages Docker build context from a source-built NapCatQQ Shell dist. +- Modify: `ci/napcat-desktop-cn/Dockerfile` + - Copies staged source-built `NapCat.Shell` into the image and creates `/app/NapCat.Shell.zip`. +- Modify: `ci/napcat-desktop-cn/verify.sh` + - Verifies fork marker, artifact hash, locale, timezone, XDG, fontconfig, and source-fix markers. +- Delete: `ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh` + - Removes bundled-JS patching. +- Modify: `ci/napcat-desktop-cn/README.md` + - Documents v3 staging/build/verify commands. +- Modify: `src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts` + - Changes default `desktopProfileVersion` to `desktop-cn-v3`. +- Modify: `test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts` + - Replaces patch assertions with source-built artifact assertions. +- Modify: `test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts` + - Updates expected default desktop profile version. +- Modify: `README.md`, `API.md` + - Documents `QQBOT_NAPCAT_IMAGE` v3 expectations where existing NapCat runtime text exists. + +### KT Root Docs + +Target repo: `D:\MyFiles\KT` + +- Modify: `docs/qqbot-nas-runtime.md` + - Records the new v3 source-fork image path and canary evidence requirements. +- Modify: `TASKS.md` + - Adds a short current-context record after implementation files change. + +## Execution Rules + +- Do not use `.worktree`; use normal dev branches. +- Do not mutate production containers until the canary task. +- Do not remove API stale-QR protections. +- Do not push until the user asks. +- Every new or touched function in KT-owned files must have JSDoc with parameter purpose and return semantics. +- Every task that edits code must run its targeted RED/GREEN check before commit. + +## Task 1: Prepare NapCatQQ Fork Branch + +**Files:** +- Create or update repo: `D:\MyFiles\KT\GitHub\NapCatQQ` + +- [ ] **Step 1: Create or update the fork checkout** + +Run: + +```powershell +if (Test-Path -LiteralPath 'D:\MyFiles\KT\GitHub\NapCatQQ\.git') { + git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' fetch origin main --prune +} else { + git clone https://github.com/NapNeko/NapCatQQ.git 'D:\MyFiles\KT\GitHub\NapCatQQ' +} +``` + +Expected: clone or fetch succeeds. + +- [ ] **Step 2: Create the implementation branch** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' checkout main +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' pull --ff-only origin main +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' switch -c codex/qr-refresh-login-state +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' rev-parse HEAD +``` + +Expected: HEAD is `5c18a62530d87dbadf53d267002894faa6ca7e90`. If it differs, record the actual commit in the task summary and continue from that commit. + +- [ ] **Step 3: Confirm package manager** + +Run: + +```powershell +corepack pnpm --version +Get-Content -Raw -LiteralPath 'D:\MyFiles\KT\GitHub\NapCatQQ\package.json' | Select-String -Pattern '"typecheck"|"test"|"build:shell"' +``` + +Expected: scripts include `typecheck`, `test`, and `build:shell`. + +## Task 2: Add Failing NapCat Runtime Tests + +**Files:** +- Modify: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-test\vitest.config.ts` +- Create: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-test\webuiLoginRuntime.test.ts` + +- [ ] **Step 1: Add the WebUI backend alias** + +In `packages/napcat-test/vitest.config.ts`, add this alias inside `resolve.alias`: + +```ts +'napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'), +``` + +- [ ] **Step 2: Write failing runtime tests** + +Create `packages/napcat-test/webuiLoginRuntime.test.ts` with: + +```ts +import { beforeEach, describe, expect, test } from 'vitest'; +import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data'; + +/** + * Injects the minimal OneBot adapter shape used by WebUI login-state reconciliation. + * @param online - Explicit QQ online state; `undefined` represents adapter/core not ready. + */ +function setCoreOnlineState (online: boolean | undefined): void { + WebUiDataRuntime.setOneBotContext( + online === undefined + ? null + : { + core: { + selfInfo: { + online, + }, + }, + } + ); +} + +describe('WebUI login runtime state', () => { + beforeEach(() => { + WebUiDataRuntime.__resetForTest(); + }); + + test('reconciles stale WebUI login status when core selfInfo is offline', () => { + WebUiDataRuntime.setQQLoginStatus(true); + WebUiDataRuntime.setQQLoginQrcodeURL('old-qrcode-url'); + setCoreOnlineState(false); + + const state = WebUiDataRuntime.getQQLoginRuntimeState({ + clearStaleQRCode: true, + reconcile: true, + }); + + expect(state).toMatchObject({ + canStartLoginFlow: true, + isActuallyLogin: false, + isStaleLoginStatus: true, + online: false, + qrcodeurl: '', + webuiLoginStatus: false, + }); + expect(WebUiDataRuntime.getQQLoginStatus()).toBe(false); + expect(WebUiDataRuntime.getQQLoginQrcodeURL()).toBe(''); + }); + + test('keeps login status when core online state is still unknown', () => { + WebUiDataRuntime.setQQLoginStatus(true); + WebUiDataRuntime.setQQLoginQrcodeURL('existing-qrcode-url'); + setCoreOnlineState(undefined); + + const state = WebUiDataRuntime.getQQLoginRuntimeState({ + clearStaleQRCode: true, + reconcile: true, + }); + + expect(state).toMatchObject({ + canStartLoginFlow: false, + isActuallyLogin: true, + isStaleLoginStatus: false, + online: undefined, + qrcodeurl: 'existing-qrcode-url', + webuiLoginStatus: true, + }); + }); + + test('increments QR revision only when QR URL changes or clears', () => { + expect(WebUiDataRuntime.getQQLoginRuntimeState().qrcodeRevision).toBe(0); + + WebUiDataRuntime.setQQLoginQrcodeURL('first-url'); + const firstState = WebUiDataRuntime.getQQLoginRuntimeState(); + + WebUiDataRuntime.setQQLoginQrcodeURL('first-url'); + const repeatedState = WebUiDataRuntime.getQQLoginRuntimeState(); + + WebUiDataRuntime.clearQQLoginQrcodeURL(); + const clearedState = WebUiDataRuntime.getQQLoginRuntimeState(); + + expect(firstState.qrcodeRevision).toBe(1); + expect(repeatedState.qrcodeRevision).toBe(1); + expect(clearedState.qrcodeRevision).toBe(2); + expect(clearedState.qrcodeurl).toBe(''); + }); + + test('reports refresh as accepted and updated when QR callback changes revision', async () => { + WebUiDataRuntime.setRefreshQRCodeCallback(async () => { + setTimeout(() => { + WebUiDataRuntime.setQQLoginQrcodeURL('fresh-qrcode-url'); + }, 10); + return true; + }); + + const result = await WebUiDataRuntime.refreshQRCode({ + pollIntervalMs: 10, + waitForUpdateMs: 300, + }); + + expect(result).toMatchObject({ + accepted: true, + qrcodeurl: 'fresh-qrcode-url', + updated: true, + }); + expect(result.qrcodeRevision).toBe(1); + }); + + test('reports refresh as rejected when kernel callback returns false', async () => { + WebUiDataRuntime.setRefreshQRCodeCallback(async () => false); + + const result = await WebUiDataRuntime.refreshQRCode({ + pollIntervalMs: 10, + waitForUpdateMs: 50, + }); + + expect(result).toMatchObject({ + accepted: false, + error: 'QR refresh request was rejected by login service', + updated: false, + }); + }); +}); +``` + +- [ ] **Step 3: Run the failing runtime tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiLoginRuntime.test.ts +``` + +Expected: FAIL because `__resetForTest`, `getQQLoginRuntimeState`, `clearQQLoginQrcodeURL`, and typed `refreshQRCode` options do not exist yet. + +## Task 3: Implement NapCat Runtime State Model + +**Files:** +- Modify: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-webui-backend\src\types\index.ts` +- Modify: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-webui-backend\src\helper\Data.ts` +- Test: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-test\webuiLoginRuntime.test.ts` + +- [ ] **Step 1: Extend runtime types** + +In `packages/napcat-webui-backend/src/types/index.ts`, add: + +```ts +export interface QQLoginRuntimeStateOptions { + clearStaleQRCode?: boolean; + reconcile?: boolean; +} + +export interface QQLoginRuntimeState { + canStartLoginFlow: boolean; + isActuallyLogin: boolean; + isStaleLoginStatus: boolean; + online?: boolean; + qrcodeRevision: number; + qrcodeUpdatedAt: number; + qrcodeurl: string; + webuiLoginStatus: boolean; +} + +export interface QQRefreshQRCodeOptions { + pollIntervalMs?: number; + waitForUpdateMs?: number; +} + +export interface QQRefreshQRCodeResult { + accepted: boolean; + error?: string; + qrcodeRevision: number; + qrcodeUpdatedAt: number; + qrcodeurl: string; + updated: boolean; +} +``` + +Then update `LoginRuntimeType`: + +```ts + QQQRCodeRevision: number; + QQQRCodeUpdatedAt: number; + onRefreshQRCode: () => Promise; +``` + +- [ ] **Step 2: Update Data.ts imports** + +Change the import in `packages/napcat-webui-backend/src/helper/Data.ts`: + +```ts +import { + NapCatCoreWorkingEnv, + type LoginRuntimeType, + type QQLoginRuntimeState, + type QQLoginRuntimeStateOptions, + type QQRefreshQRCodeOptions, + type QQRefreshQRCodeResult, +} from '../types'; +``` + +- [ ] **Step 3: Add focused helper functions with JSDoc** + +Add these helpers above `export const WebUiDataRuntime`: + +```ts +const DEFAULT_QR_REFRESH_WAIT_MS = 1200; +const DEFAULT_QR_REFRESH_POLL_MS = 50; + +/** + * Sleeps between short QR revision checks without blocking the event loop. + * @param ms - Milliseconds to wait before checking whether the QR listener updated runtime state. + */ +function delay (ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Reads the best available core-side QQ online state from the registered OneBot adapter. + * @returns `true` or `false` when core has explicit selfInfo, otherwise `undefined` during early startup. + */ +function getCoreOnlineState (): boolean | undefined { + return LoginRuntime.OneBotContext?.core?.selfInfo?.online; +} + +/** + * Builds a public login runtime snapshot and optionally reconciles stale WebUI state. + * @param options - Controls whether stale login status mutates runtime state and whether old QR data is cleared. + */ +function buildQQLoginRuntimeState ( + options: QQLoginRuntimeStateOptions = {} +): QQLoginRuntimeState { + const reconcile = options.reconcile ?? false; + const online = getCoreOnlineState(); + const staleBeforeReconcile = LoginRuntime.QQLoginStatus && online === false; + + if (staleBeforeReconcile && reconcile) { + LoginRuntime.QQLoginStatus = false; + if (!LoginRuntime.QQLoginError) { + LoginRuntime.QQLoginError = 'QQ 登录态已失效,请重新登录'; + } + if (options.clearStaleQRCode) { + clearQQLoginQrcodeURL(); + } + } + + const isUnknownOnline = online === undefined; + const isActuallyLogin = LoginRuntime.QQLoginStatus && (isUnknownOnline || online === true); + + return { + canStartLoginFlow: !isActuallyLogin, + isActuallyLogin, + isStaleLoginStatus: staleBeforeReconcile, + online, + qrcodeRevision: LoginRuntime.QQQRCodeRevision, + qrcodeUpdatedAt: LoginRuntime.QQQRCodeUpdatedAt, + qrcodeurl: LoginRuntime.QQQRCodeURL, + webuiLoginStatus: LoginRuntime.QQLoginStatus, + }; +} + +/** + * Clears the cached QR URL and increments revision when the value actually changes. + */ +function clearQQLoginQrcodeURL (): void { + if (!LoginRuntime.QQQRCodeURL) return; + LoginRuntime.QQQRCodeURL = ''; + LoginRuntime.QQQRCodeRevision += 1; + LoginRuntime.QQQRCodeUpdatedAt = Date.now(); +} +``` + +- [ ] **Step 4: Update the initial runtime object** + +In `LoginRuntime`, add the QR fields and boolean refresh callback: + +```ts + QQQRCodeRevision: 0, + QQQRCodeUpdatedAt: 0, +``` + +Change the default callback: + +```ts + onRefreshQRCode: async () => { + return false; + }, +``` + +- [ ] **Step 5: Replace QR methods and refresh method** + +In `WebUiDataRuntime`, replace the QR methods and callback signatures with: + +```ts + setQQLoginQrcodeURL (url: LoginRuntimeType['QQQRCodeURL']): void { + if (LoginRuntime.QQQRCodeURL === url) return; + LoginRuntime.QQQRCodeURL = url; + LoginRuntime.QQQRCodeRevision += 1; + LoginRuntime.QQQRCodeUpdatedAt = Date.now(); + }, + + clearQQLoginQrcodeURL (): void { + clearQQLoginQrcodeURL(); + }, + + getQQLoginQrcodeURL (): LoginRuntimeType['QQQRCodeURL'] { + return LoginRuntime.QQQRCodeURL; + }, + + getQQLoginRuntimeState (options?: QQLoginRuntimeStateOptions): QQLoginRuntimeState { + return buildQQLoginRuntimeState(options); + }, +``` + +Replace refresh callback methods with: + +```ts + setRefreshQRCodeCallback (func: () => Promise): void { + LoginRuntime.onRefreshQRCode = func; + }, + + getRefreshQRCodeCallback (): () => Promise { + return LoginRuntime.onRefreshQRCode; + }, + + refreshQRCode: async function ( + options: QQRefreshQRCodeOptions = {} + ): Promise { + LoginRuntime.QQLoginError = ''; + const beforeRevision = LoginRuntime.QQQRCodeRevision; + const accepted = await LoginRuntime.onRefreshQRCode(); + const waitForUpdateMs = options.waitForUpdateMs ?? DEFAULT_QR_REFRESH_WAIT_MS; + const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_QR_REFRESH_POLL_MS; + + if (!accepted) { + return { + accepted: false, + error: 'QR refresh request was rejected by login service', + qrcodeRevision: LoginRuntime.QQQRCodeRevision, + qrcodeUpdatedAt: LoginRuntime.QQQRCodeUpdatedAt, + qrcodeurl: LoginRuntime.QQQRCodeURL, + updated: false, + }; + } + + const deadline = Date.now() + waitForUpdateMs; + while (Date.now() < deadline && LoginRuntime.QQQRCodeRevision === beforeRevision) { + await delay(pollIntervalMs); + } + + return { + accepted: true, + qrcodeRevision: LoginRuntime.QQQRCodeRevision, + qrcodeUpdatedAt: LoginRuntime.QQQRCodeUpdatedAt, + qrcodeurl: LoginRuntime.QQQRCodeURL, + updated: LoginRuntime.QQQRCodeRevision !== beforeRevision, + }; + }, +``` + +- [ ] **Step 6: Add the test reset helper** + +Add this method at the end of `WebUiDataRuntime` before the closing brace: + +```ts + __resetForTest (): void { + LoginRuntime.LoginCurrentTime = Date.now(); + LoginRuntime.LoginCurrentRate = 0; + LoginRuntime.QQLoginStatus = false; + LoginRuntime.QQQRCodeURL = ''; + LoginRuntime.QQQRCodeRevision = 0; + LoginRuntime.QQQRCodeUpdatedAt = 0; + LoginRuntime.QQLoginUin = ''; + LoginRuntime.QQLoginInfo = { + nick: '', + uid: '', + uin: '', + }; + LoginRuntime.QQLoginError = ''; + LoginRuntime.OneBotContext = null; + LoginRuntime.onRefreshQRCode = async () => false; + }, +``` + +- [ ] **Step 7: Run runtime tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiLoginRuntime.test.ts +``` + +Expected: PASS. + +- [ ] **Step 8: Run focused typecheck** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-webui-backend run typecheck +``` + +Expected: PASS. + +- [ ] **Step 9: Commit** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' add packages/napcat-webui-backend/src/types/index.ts packages/napcat-webui-backend/src/helper/Data.ts packages/napcat-test/vitest.config.ts packages/napcat-test/webuiLoginRuntime.test.ts +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' commit -m "fix: 修复WebUI登录态与二维码刷新状态" +``` + +## Task 4: Fix QQLogin Handler Guards + +**Files:** +- Create: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-test\webuiQQLoginHandlers.test.ts` +- Modify: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-webui-backend\src\api\QQLogin.ts` + +- [ ] **Step 1: Write failing handler tests** + +Create `packages/napcat-test/webuiQQLoginHandlers.test.ts`: + +```ts +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { + QQCheckLoginStatusHandler, + QQGetQRcodeHandler, + QQRefreshQRcodeHandler, + QQSetQuickLoginHandler, +} from 'napcat-webui-backend/src/api/QQLogin'; +import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data'; + +type MockResponseState = { + body?: unknown; + statusCode?: number; +}; + +/** + * Builds the minimal Express response shape used by NapCat WebUI response helpers. + * @returns The mock response object plus mutable state for assertions. + */ +function createMockResponse (): { res: any; state: MockResponseState } { + const state: MockResponseState = {}; + const res = { + json: vi.fn((body: unknown) => { + state.body = body; + return res; + }), + send: vi.fn((body: unknown) => { + state.body = body; + return res; + }), + status: vi.fn((statusCode: number) => { + state.statusCode = statusCode; + return res; + }), + }; + return { res, state }; +} + +/** + * Injects a stale or healthy OneBot core state for handler tests. + * @param online - Core selfInfo online value exposed through the registered adapter. + */ +function setCoreOnlineState (online: boolean): void { + WebUiDataRuntime.setOneBotContext({ + core: { + selfInfo: { + online, + }, + }, + }); +} + +describe('QQLogin WebUI handlers', () => { + beforeEach(() => { + WebUiDataRuntime.__resetForTest(); + }); + + test('CheckLoginStatus clears stale login state and does not return stale QR', async () => { + WebUiDataRuntime.setQQLoginStatus(true); + WebUiDataRuntime.setQQLoginQrcodeURL('old-qrcode-url'); + setCoreOnlineState(false); + const { res, state } = createMockResponse(); + + await QQCheckLoginStatusHandler({} as any, res, vi.fn()); + + expect(state.body).toMatchObject({ + code: 0, + data: { + isLogin: false, + isOffline: true, + qrcodeRevision: 2, + qrcodeurl: '', + }, + }); + }); + + test('RefreshQRcode is allowed for stale WebUI login state', async () => { + WebUiDataRuntime.setQQLoginStatus(true); + setCoreOnlineState(false); + WebUiDataRuntime.setRefreshQRCodeCallback(async () => { + WebUiDataRuntime.setQQLoginQrcodeURL('fresh-qrcode-url'); + return true; + }); + const { res, state } = createMockResponse(); + + await QQRefreshQRcodeHandler({} as any, res, vi.fn()); + + expect(state.body).toMatchObject({ + code: 0, + data: { + accepted: true, + qrcodeurl: 'fresh-qrcode-url', + updated: true, + }, + }); + }); + + test('Quick login remains blocked when core confirms actual online state', async () => { + WebUiDataRuntime.setQQLoginStatus(true); + setCoreOnlineState(true); + const { res, state } = createMockResponse(); + + await QQSetQuickLoginHandler({ body: { uin: '10001' } } as any, res, vi.fn()); + + expect(state.body).toMatchObject({ + code: 1, + message: 'QQ Is Logined', + }); + }); + + test('GetQRcode rejects stale empty QR after reconciliation', async () => { + WebUiDataRuntime.setQQLoginStatus(true); + WebUiDataRuntime.setQQLoginQrcodeURL('old-qrcode-url'); + setCoreOnlineState(false); + const { res, state } = createMockResponse(); + + await QQGetQRcodeHandler({} as any, res, vi.fn()); + + expect(state.body).toMatchObject({ + code: 1, + message: 'QRCode Get Error', + }); + }); +}); +``` + +- [ ] **Step 2: Run the failing handler tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiQQLoginHandlers.test.ts +``` + +Expected: FAIL because handlers still use direct `getQQLoginStatus()` guards and `RefreshQRcode` does not return result data. + +- [ ] **Step 3: Add handler helper with JSDoc** + +In `QQLogin.ts`, add below helper functions: + +```ts +/** + * Reconciles WebUI login state and returns true only when the QQ account is actually online. + * @param clearStaleQRCode - Whether stale QR cache should be cleared while reconciling offline state. + */ +function isActuallyLoggedIn (clearStaleQRCode = true): boolean { + return WebUiDataRuntime.getQQLoginRuntimeState({ + clearStaleQRCode, + reconcile: true, + }).isActuallyLogin; +} + +/** + * Builds the login status payload from the shared runtime state helper. + * @returns Handler response payload used by CheckLoginStatus. + */ +function buildLoginStatusPayload () { + const state = WebUiDataRuntime.getQQLoginRuntimeState({ + clearStaleQRCode: true, + reconcile: true, + }); + return { + isLogin: state.isActuallyLogin, + isOffline: state.isStaleLoginStatus, + loginError: WebUiDataRuntime.getQQLoginError(), + qrcodeRevision: state.qrcodeRevision, + qrcodeUpdatedAt: state.qrcodeUpdatedAt, + qrcodeurl: state.qrcodeurl, + }; +} +``` + +- [ ] **Step 4: Replace direct login guards** + +For `QQGetQRcodeHandler`, `QQSetQuickLoginHandler`, `QQRefreshQRcodeHandler`, `QQPasswordLoginHandler`, `QQCaptchaLoginHandler`, and `QQNewDeviceLoginHandler`, replace direct `WebUiDataRuntime.getQQLoginStatus()` checks with: + +```ts + if (isActuallyLoggedIn()) { + return sendError(res, 'QQ Is Logined'); + } +``` + +For `QQCheckLoginStatusHandler`, replace the body with: + +```ts +export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => { + return sendSuccess(res, buildLoginStatusPayload()); +}; +``` + +For `QQRefreshQRcodeHandler`, replace the refresh body with: + +```ts + const result = await WebUiDataRuntime.refreshQRCode(); + if (!result.accepted) { + return sendError(res, result.error || 'QRCode Refresh Rejected'); + } + return sendSuccess(res, result); +``` + +- [ ] **Step 5: Add JSDoc to touched exported handlers** + +Add one-sentence JSDoc above every touched exported handler. Example for refresh: + +```ts +/** + * Refreshes the manual QQ login QR code when the account is not actually online. + */ +export const QQRefreshQRcodeHandler: RequestHandler = async (_, res) => { +``` + +Repeat this style for the other touched handlers. + +- [ ] **Step 6: Run handler tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiQQLoginHandlers.test.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Run combined WebUI tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiLoginRuntime.test.ts webuiQQLoginHandlers.test.ts +``` + +Expected: PASS. + +- [ ] **Step 8: Commit** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' add packages/napcat-webui-backend/src/api/QQLogin.ts packages/napcat-test/webuiQQLoginHandlers.test.ts +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' commit -m "fix: 收敛QQ登录接口真实在线态判断" +``` + +## Task 5: Wire Shell and Framework QR Refresh Callback + +**Files:** +- Create: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-test\webuiLoginSourceWiring.test.ts` +- Modify: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-shell\base.ts` +- Modify: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-framework\napcat.ts` + +- [ ] **Step 1: Write failing source wiring test** + +Create `packages/napcat-test/webuiLoginSourceWiring.test.ts`: + +```ts +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, test } from 'vitest'; + +const repoRoot = resolve(__dirname, '../..'); + +/** + * Reads a NapCatQQ source file from the repository root. + * @param relativePath - Repo-relative source path to inspect. + */ +function readSource (relativePath: string): string { + return readFileSync(resolve(repoRoot, relativePath), 'utf8'); +} + +describe('WebUI QR refresh source wiring', () => { + test('shell returns loginService.getQRCodePicture accepted state', () => { + const source = readSource('packages/napcat-shell/base.ts'); + + expect(source).toContain('WebUiDataRuntime.setRefreshQRCodeCallback(async () => {'); + expect(source).toContain('return loginService.getQRCodePicture();'); + }); + + test('framework returns loginService.getQRCodePicture accepted state', () => { + const source = readSource('packages/napcat-framework/napcat.ts'); + + expect(source).toContain('WebUiDataRuntime.setRefreshQRCodeCallback(async () => {'); + expect(source).toContain('return loginService.getQRCodePicture();'); + }); +}); +``` + +- [ ] **Step 2: Run the failing source wiring test** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiLoginSourceWiring.test.ts +``` + +Expected: FAIL because both callbacks call `loginService.getQRCodePicture()` without returning it. + +- [ ] **Step 3: Update Shell callback** + +In `packages/napcat-shell/base.ts`, replace: + +```ts + WebUiDataRuntime.setRefreshQRCodeCallback(async () => { + loginService.getQRCodePicture(); + }); +``` + +with: + +```ts + WebUiDataRuntime.setRefreshQRCodeCallback(async () => { + return loginService.getQRCodePicture(); + }); +``` + +- [ ] **Step 4: Update Framework callback** + +In `packages/napcat-framework/napcat.ts`, replace: + +```ts + WebUiDataRuntime.setRefreshQRCodeCallback(async () => { + loginService.getQRCodePicture(); + }); +``` + +with: + +```ts + WebUiDataRuntime.setRefreshQRCodeCallback(async () => { + return loginService.getQRCodePicture(); + }); +``` + +- [ ] **Step 5: Run source wiring test** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiLoginSourceWiring.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Run shell and framework typecheck** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-shell run typecheck +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-framework run typecheck +``` + +Expected: both commands PASS. + +- [ ] **Step 7: Commit** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' add packages/napcat-shell/base.ts packages/napcat-framework/napcat.ts packages/napcat-test/webuiLoginSourceWiring.test.ts +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' commit -m "fix: 返回二维码刷新请求接收状态" +``` + +## Task 6: Build and Verify NapCatQQ Fork Artifact + +**Files:** +- No new source files unless verification reveals a targeted compile or test issue. + +- [ ] **Step 1: Install dependencies** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' install --frozen-lockfile +``` + +Expected: install succeeds. + +- [ ] **Step 2: Run fork tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run webuiLoginRuntime.test.ts webuiQQLoginHandlers.test.ts webuiLoginSourceWiring.test.ts +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-test exec vitest run +``` + +Expected: all tests PASS. + +- [ ] **Step 3: Run typecheck** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' run typecheck +``` + +Expected: PASS. + +- [ ] **Step 4: Build Shell artifact** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-webui-frontend run build +corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' run build:shell +``` + +Expected: `D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-shell\dist\napcat.mjs` exists. + +- [ ] **Step 5: Record artifact evidence** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' log -1 --oneline +Get-FileHash -Algorithm SHA256 'D:\MyFiles\KT\GitHub\NapCatQQ\packages\napcat-shell\dist\napcat.mjs' +``` + +Expected: output includes fork commit and a SHA256 for `napcat.mjs`. + +## Task 7: Add API Build-Context Staging Script + +**Files:** +- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\napcat-desktop-cn-stage-build.mjs` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-desktop-cn-image.spec.ts` + +- [ ] **Step 1: Add failing static test for staging script** + +Append this test to `napcat-desktop-cn-image.spec.ts`: + +```ts + it('stages source-built NapCat Shell artifacts for Docker build context', () => { + const script = readSource('scripts/napcat-desktop-cn-stage-build.mjs'); + + expect(script).toContain('napcatMjsSha256'); + expect(script).toContain('forkCommit'); + expect(script).toContain('upstreamBaseCommit'); + expect(script).toContain('packages/napcat-shell/dist'); + expect(script).toContain('fork-artifact.json'); + expect(script).toContain('.kt-workspace/napcat-desktop-cn-build'); + }); +``` + +- [ ] **Step 2: Run the failing API image test** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand +``` + +Expected: FAIL because the staging script does not exist. + +- [ ] **Step 3: Create the staging script** + +Create `scripts/napcat-desktop-cn-stage-build.mjs`: + +```js +import { createHash } from 'node:crypto'; +import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { join, relative, resolve, sep } from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const DEFAULT_OUTPUT = '.kt-workspace/napcat-desktop-cn-build'; +const DEFAULT_UPSTREAM_BASE = '5c18a62530d87dbadf53d267002894faa6ca7e90'; + +/** + * Reads a named CLI argument in `--key value` form. + * @param {string} name - Argument name without the leading dashes. + * @param {string} fallback - Value used when the argument is absent. + * @returns {string} Parsed argument value. + */ +function readArg (name, fallback = '') { + const index = process.argv.indexOf(`--${name}`); + return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback; +} + +/** + * Computes a SHA256 digest for a file. + * @param {string} filePath - Absolute path to the file being fingerprinted. + * @returns {string} Lowercase hex SHA256 digest. + */ +function sha256File (filePath) { + return createHash('sha256').update(readFileSync(filePath)).digest('hex'); +} + +/** + * Recursively computes a stable digest for a directory from relative paths and file contents. + * @param {string} directory - Absolute directory path. + * @returns {string} Lowercase hex SHA256 digest. + */ +function sha256Directory (directory) { + const hash = createHash('sha256'); + const stack = [directory]; + const files = []; + while (stack.length > 0) { + const current = stack.pop(); + for (const entry of listDirectory(current)) { + const absolute = join(current, entry); + if (statSync(absolute).isDirectory()) { + stack.push(absolute); + } else { + files.push(absolute); + } + } + } + files.sort(); + for (const file of files) { + hash.update(relative(directory, file).split(sep).join('/')); + hash.update('\0'); + hash.update(readFileSync(file)); + hash.update('\0'); + } + return hash.digest('hex'); +} + +/** + * Lists directory entries in stable order. + * @param {string} directory - Directory to list. + * @returns {string[]} Sorted child names. + */ +function listDirectory (directory) { + return readdirSync(directory).sort(); +} + +/** + * Reads the current git commit for the NapCat fork. + * @param {string} repoRoot - Absolute repository path. + * @returns {string} Current commit hash. + */ +function gitCommit (repoRoot) { + return execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); +} + +/** + * Copies one file or directory into the staged Docker context. + * @param {string} source - Source path. + * @param {string} target - Target path. + */ +function copyIntoContext (source, target) { + cpSync(source, target, { recursive: true }); +} + +const apiRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); +const napcatRoot = resolve(readArg('napcat-root')); +const outputRoot = resolve(apiRoot, readArg('out', DEFAULT_OUTPUT)); +const upstreamBaseCommit = readArg('upstream-base-commit', DEFAULT_UPSTREAM_BASE); +const shellDist = resolve(napcatRoot, 'packages/napcat-shell/dist'); +const napcatMjs = resolve(shellDist, 'napcat.mjs'); + +if (!napcatRoot || !existsSync(napcatRoot)) { + throw new Error('--napcat-root must point to the NapCatQQ fork repository'); +} +if (!existsSync(napcatMjs)) { + throw new Error(`NapCat shell build output is missing: ${napcatMjs}`); +} + +rmSync(outputRoot, { force: true, recursive: true }); +mkdirSync(resolve(outputRoot, 'ci/napcat-desktop-cn'), { recursive: true }); +copyIntoContext(resolve(apiRoot, 'ci/napcat-desktop-cn/Dockerfile'), resolve(outputRoot, 'ci/napcat-desktop-cn/Dockerfile')); +copyIntoContext(resolve(apiRoot, 'ci/napcat-desktop-cn/verify.sh'), resolve(outputRoot, 'ci/napcat-desktop-cn/verify.sh')); +copyIntoContext(shellDist, resolve(outputRoot, 'NapCat.Shell')); + +const marker = { + builtAt: new Date().toISOString(), + distSha256: sha256Directory(shellDist), + forkCommit: gitCommit(napcatRoot), + napcatMjsSha256: sha256File(napcatMjs), + upstreamBaseCommit, +}; + +writeFileSync( + resolve(outputRoot, 'ci/napcat-desktop-cn/fork-artifact.json'), + `${JSON.stringify(marker, null, 2)}\n`, + 'utf8' +); + +console.log(JSON.stringify({ + marker, + outputRoot, +}, null, 2)); +``` + +- [ ] **Step 4: Run the API image test** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand +``` + +Expected: this new test passes, while old bundled-patch tests still fail until Task 8 updates them. + +## Task 8: Replace Bundled-JS Patch With Source Artifact Image + +**Files:** +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\ci\napcat-desktop-cn\Dockerfile` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\ci\napcat-desktop-cn\verify.sh` +- Delete: `D:\MyFiles\KT\Node\kt-template-online-api\ci\napcat-desktop-cn\patches\qq-login-real-online-guard.sh` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-desktop-cn-image.spec.ts` + +- [ ] **Step 1: Replace the old patch assertions** + +In `napcat-desktop-cn-image.spec.ts`, replace the test named `patches NapCat WebUI login guards to allow qrcode refresh after real QQ offline` with: + +```ts + it('uses source-built NapCat Shell artifact instead of bundled JS patching', () => { + const dockerfile = readSource('ci/napcat-desktop-cn/Dockerfile'); + const verify = readSource('ci/napcat-desktop-cn/verify.sh'); + + expect(dockerfile).toContain('COPY NapCat.Shell /tmp/NapCat.Shell'); + expect(dockerfile).toContain('fork-artifact.json'); + expect(dockerfile).toContain('zip -qr /app/NapCat.Shell.zip .'); + expect(dockerfile).not.toContain('qq-login-real-online-guard.sh'); + expect(dockerfile).not.toContain('NAPCAT_PATCH_ROOT'); + expect(verify).toContain('napcatMjsSha256'); + expect(verify).toContain('getQQLoginRuntimeState'); + expect(verify).toContain('qrcodeRevision'); + expect(verify).not.toContain('selfInfo?.online !== false'); + }); +``` + +Also remove expectations that read `ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh`. + +- [ ] **Step 2: Run the failing image test** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand +``` + +Expected: FAIL because Dockerfile and verify still reference the old patch. + +- [ ] **Step 3: Replace Dockerfile patch block** + +Replace the patch block in `ci/napcat-desktop-cn/Dockerfile` with: + +```dockerfile +COPY NapCat.Shell /tmp/NapCat.Shell +COPY ci/napcat-desktop-cn/fork-artifact.json /ci/napcat-desktop-cn/fork-artifact.json +RUN set -eux; \ + cd /tmp/NapCat.Shell; \ + zip -qr /app/NapCat.Shell.zip .; \ + 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 +``` + +- [ ] **Step 4: Replace verify.sh** + +Replace `ci/napcat-desktop-cn/verify.sh` with: + +```sh +#!/bin/sh +set -eu + +MARKER=/ci/napcat-desktop-cn/fork-artifact.json +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +locale -a | grep -i '^zh_CN.utf8$' +locale | grep 'LANG=zh_CN.UTF-8' +test "$(cat /etc/timezone)" = "Asia/Shanghai" +fc-match "Noto Sans CJK SC" | grep -E 'Noto|WenQuanYi|wqy' +test "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-}" = "XDG_CONFIG_HOME=/app/.config" +test "XDG_CACHE_HOME=${XDG_CACHE_HOME:-}" = "XDG_CACHE_HOME=/app/.cache" +test "XDG_DATA_HOME=${XDG_DATA_HOME:-}" = "XDG_DATA_HOME=/app/.local/share" +test ! -e /.dockerenv +grep -q '^0::/$' /proc/1/cgroup + +test -s "$MARKER" +grep -q '"upstreamBaseCommit"' "$MARKER" +grep -q '"forkCommit"' "$MARKER" +grep -q '"napcatMjsSha256"' "$MARKER" +test -s /app/NapCat.Shell.zip +test -s /ci/napcat-desktop-cn/NapCat.Shell.zip.sha256 + +unzip -q /app/NapCat.Shell.zip -d "$TMP_DIR" +test -s "$TMP_DIR/napcat.mjs" + +EXPECTED_MJS_SHA="$(sed -n 's/.*"napcatMjsSha256"[[:space:]]*:[[:space:]]*"\([a-f0-9]\{64\}\)".*/\1/p' "$MARKER")" +ACTUAL_MJS_SHA="$(sha256sum "$TMP_DIR/napcat.mjs" | awk '{print $1}')" +test "$EXPECTED_MJS_SHA" = "$ACTUAL_MJS_SHA" + +grep -R -q 'getQQLoginRuntimeState' "$TMP_DIR" +grep -R -q 'qrcodeRevision' "$TMP_DIR" +``` + +- [ ] **Step 5: Delete old patch script** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' rm -- 'ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh' +``` + +Expected: file is staged for deletion. + +- [ ] **Step 6: Run image tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand +``` + +Expected: PASS. + +## Task 9: Update Runtime Profile Version and Docs + +**Files:** +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\application\runtime\napcat-runtime-profile.service.ts` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\runtime-protocol-profile.spec.ts` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\ci\napcat-desktop-cn\README.md` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\README.md` +- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\API.md` +- Modify: `D:\MyFiles\KT\docs\qqbot-nas-runtime.md` + +- [ ] **Step 1: Write failing profile-version expectation** + +In `runtime-protocol-profile.spec.ts`, change: + +```ts +desktopProfileVersion: 'desktop-cn-v2', +``` + +to: + +```ts +desktopProfileVersion: 'desktop-cn-v3', +``` + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runTestsByPath --runInBand --testNamePattern "resolves Chinese Desktop Runtime defaults" +``` + +Expected: FAIL because service default is still `desktop-cn-v2`. + +- [ ] **Step 2: Update service default** + +In `napcat-runtime-profile.service.ts`, change: + +```ts +'desktop-cn-v2', +``` + +to: + +```ts +'desktop-cn-v3', +``` + +- [ ] **Step 3: Update README commands** + +Replace `ci/napcat-desktop-cn/README.md` with: + +```md +# NapCat Chinese Desktop Runtime Image + +This image consumes a source-built NapCatQQ Shell artifact staged from `D:\MyFiles\KT\GitHub\NapCatQQ`. + +Build NapCatQQ first: + +```powershell +corepack pnpm --dir D:\MyFiles\KT\GitHub\NapCatQQ install --frozen-lockfile +corepack pnpm --dir D:\MyFiles\KT\GitHub\NapCatQQ --filter napcat-webui-frontend run build +corepack pnpm --dir D:\MyFiles\KT\GitHub\NapCatQQ run build:shell +``` + +Stage Docker build context: + +```powershell +node scripts/napcat-desktop-cn-stage-build.mjs ` + --napcat-root D:\MyFiles\KT\GitHub\NapCatQQ ` + --out .kt-workspace\napcat-desktop-cn-build +``` + +Build and verify: + +```powershell +$baseImage = docker image inspect mlikiowa/napcat-docker:latest --format '{{index .RepoDigests 0}}' +if (-not $baseImage) { throw 'NapCat upstream image digest not found; pull and inspect the image before building.' } +docker build ` + --build-arg NAPCAT_BASE_IMAGE=$baseImage ` + -t kt-napcat-desktop-cn:desktop-cn-v3 ` + -f .kt-workspace/napcat-desktop-cn-build/ci/napcat-desktop-cn/Dockerfile ` + .kt-workspace/napcat-desktop-cn-build + +$name = "kt-napcat-v3-verify-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" +docker run -d --name $name kt-napcat-desktop-cn:desktop-cn-v3 +docker exec $name sh /ci/napcat-desktop-cn/verify.sh +docker rm -f $name +``` + +Record the final image digest in `QQBOT_NAPCAT_IMAGE`. +``` + +- [ ] **Step 4: Update project docs** + +In `README.md`, `API.md`, and `D:\MyFiles\KT\docs\qqbot-nas-runtime.md`, add a short note under the existing QQBot/NapCat runtime sections: + +```md +NapCat Chinese Desktop Runtime v3 uses a source-built `NapCat.Shell` artifact from the KT `NapCatQQ` fork. The image must be staged with `scripts/napcat-desktop-cn-stage-build.mjs`; production should point `QQBOT_NAPCAT_IMAGE` to the verified `kt-napcat-desktop-cn:desktop-cn-v3` digest. +``` + +- [ ] **Step 5: Run profile tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runTestsByPath --runInBand +``` + +Expected: PASS. + +## Task 10: Validate API Image Integration and Commit + +**Files:** +- All API files changed in Tasks 7 to 9. +- Root docs changed in Task 9. + +- [ ] **Step 1: Run focused API tests** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runTestsByPath --runInBand +``` + +Expected: PASS. + +- [ ] **Step 2: Run API typecheck** + +Run: + +```powershell +corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' run typecheck +``` + +Expected: PASS. + +- [ ] **Step 3: Run diff check** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' diff --check +git -C 'D:\MyFiles\KT' diff --check -- TASKS.md docs/qqbot-nas-runtime.md +``` + +Expected: no whitespace errors. + +- [ ] **Step 4: Run global review** + +Run: + +```powershell +pnpm --dir 'D:\MyFiles\KT\mcp\ktWorkflow' run global-review +``` + +Expected: `findings=[]`. + +- [ ] **Step 5: Commit API repo** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' status --short +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' add ci/napcat-desktop-cn scripts/napcat-desktop-cn-stage-build.mjs src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts test/modules/qqbot/napcat README.md API.md +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' commit -m "feat: 接入NapCat源码构建镜像" +``` + +- [ ] **Step 6: Commit root docs** + +Run: + +```powershell +git -C 'D:\MyFiles\KT' status --short +git -C 'D:\MyFiles\KT' add docs/qqbot-nas-runtime.md TASKS.md +git -C 'D:\MyFiles\KT' commit -m "docs: 记录NapCat源码构建运行态" +``` + +If the root repo has unrelated dirty files, stage only `docs/qqbot-nas-runtime.md` and `TASKS.md`. + +## Task 11: Build Image and Run Local or NAS Verify + +**Files:** +- No source files unless verification exposes a targeted bug. + +- [ ] **Step 1: Stage build context** + +Run from `D:\MyFiles\KT\Node\kt-template-online-api`: + +```powershell +node scripts/napcat-desktop-cn-stage-build.mjs ` + --napcat-root D:\MyFiles\KT\GitHub\NapCatQQ ` + --out .kt-workspace\napcat-desktop-cn-build +``` + +Expected: output contains `forkCommit`, `upstreamBaseCommit`, `napcatMjsSha256`, and `outputRoot`. + +- [ ] **Step 2: Build image** + +Run: + +```powershell +$baseImage = docker image inspect mlikiowa/napcat-docker:latest --format '{{index .RepoDigests 0}}' +if (-not $baseImage) { docker pull mlikiowa/napcat-docker:latest; $baseImage = docker image inspect mlikiowa/napcat-docker:latest --format '{{index .RepoDigests 0}}' } +docker build ` + --build-arg NAPCAT_BASE_IMAGE=$baseImage ` + -t kt-napcat-desktop-cn:desktop-cn-v3 ` + -f .kt-workspace/napcat-desktop-cn-build/ci/napcat-desktop-cn/Dockerfile ` + .kt-workspace/napcat-desktop-cn-build +``` + +Expected: image builds successfully. + +- [ ] **Step 3: Run image verify** + +Run: + +```powershell +$name = "kt-napcat-v3-verify-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" +docker run -d --name $name kt-napcat-desktop-cn:desktop-cn-v3 +docker exec $name sh /ci/napcat-desktop-cn/verify.sh +docker rm -f $name +docker image inspect kt-napcat-desktop-cn:desktop-cn-v3 --format '{{.Id}}' +``` + +Expected: verify exits 0 and image id is printed. + +## Task 12: Deploy and Canary Online + +**Files:** +- No source files unless online verification exposes a targeted bug. + +- [ ] **Step 1: Push only after user approval** + +Run only when the user asks to publish: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' status --short --branch +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' status --short --branch +git -C 'D:\MyFiles\KT' status --short --branch +``` + +Expected: only intentional commits are ahead. + +- [ ] **Step 2: Push API and fork branches** + +Run: + +```powershell +git -C 'D:\MyFiles\KT\GitHub\NapCatQQ' push -u origin codex/qr-refresh-login-state +git -C 'D:\MyFiles\KT\Node\kt-template-online-api' push origin main +``` + +Expected: both pushes succeed. + +- [ ] **Step 3: Observe API deployment** + +Run: + +```powershell +pnpm --dir 'D:\MyFiles\KT\mcp\ktWorkflow' run deploy-observation -- --project api --job KT-Template/KT-Template-API/main --execute +``` + +Expected: Jenkins succeeds, K8s Deployment updated, new Pod Running/Ready, restartCount stable. + +- [ ] **Step 4: Configure production image** + +Using the existing NAS/K8s env update workflow, set: + +```text +QQBOT_NAPCAT_IMAGE=kt-napcat-desktop-cn:desktop-cn-v3 +QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION=desktop-cn-v3 +``` + +Expected: API Pod sees the new env after rollout. + +- [ ] **Step 5: Canary one account** + +Use the current Admin update-login flow for one user-approved account. Evidence to capture: + +```text +NapCat fork commit: +NapCat image id: +scan session id: +old qrcode sha prefix: +new qrcode sha prefix: +/app/napcat/cache/qrcode.png old mtime: +/app/napcat/cache/qrcode.png new mtime: +RefreshQRcode accepted: +RefreshQRcode updated: +SSE final observed state: +``` + +Expected: + +- stale `QQLoginStatus=true + online=false` no longer returns `QQ Is Logined`. +- NapCat emits a new QR event or returns `accepted=true, updated=false` with no stale QR exposed. +- API `scan/status` never returns the old QR hash. +- Admin/SSE shows either a new QR or a precise pending reason. + +## Self-Review Checklist + +- Spec goal 1, source fork: Tasks 1 to 6. +- Spec goal 2, stale login state: Tasks 2 to 4. +- Spec goal 3, refresh observability: Tasks 2, 3, 5, 12. +- Spec goal 4, no stale QR in WebUI handlers: Task 4. +- Spec goal 5, API stale-QR guard retained: Tasks 7 to 10 do not edit API login service. +- Spec goal 6, source artifact image: Tasks 7 to 11. +- Spec goal 7, local and online verification: Tasks 6, 10, 11, 12. +- No placeholder markers remain in this plan.