48 KiB
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().
- Wires Shell-mode QR refresh callback to return
- Modify:
packages/napcat-framework/napcat.ts- Wires Framework-mode QR refresh callback to return
loginService.getQRCodePicture().
- Wires Framework-mode QR refresh callback to return
- 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 Loginedfor stale WebUI login state.
- Tests handlers do not return
- 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.Shellinto the image and creates/app/NapCat.Shell.zip.
- Copies staged source-built
- 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
desktopProfileVersiontodesktop-cn-v3.
- Changes default
- 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_IMAGEv3 expectations where existing NapCat runtime text exists.
- Documents
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:
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:
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:
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:
'napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
- Step 2: Write failing runtime tests
Create packages/napcat-test/webuiLoginRuntime.test.ts with:
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:
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:
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:
QQQRCodeRevision: number;
QQQRCodeUpdatedAt: number;
onRefreshQRCode: () => Promise<boolean>;
- Step 2: Update Data.ts imports
Change the import in packages/napcat-webui-backend/src/helper/Data.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:
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<void> {
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:
QQQRCodeRevision: 0,
QQQRCodeUpdatedAt: 0,
Change the default callback:
onRefreshQRCode: async () => {
return false;
},
- Step 5: Replace QR methods and refresh method
In WebUiDataRuntime, replace the QR methods and callback signatures with:
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:
setRefreshQRCodeCallback (func: () => Promise<boolean>): void {
LoginRuntime.onRefreshQRCode = func;
},
getRefreshQRCodeCallback (): () => Promise<boolean> {
return LoginRuntime.onRefreshQRCode;
},
refreshQRCode: async function (
options: QQRefreshQRCodeOptions = {}
): Promise<QQRefreshQRCodeResult> {
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:
__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:
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:
corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' --filter napcat-webui-backend run typecheck
Expected: PASS.
- Step 9: Commit
Run:
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:
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:
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:
/**
* 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:
if (isActuallyLoggedIn()) {
return sendError(res, 'QQ Is Logined');
}
For QQCheckLoginStatusHandler, replace the body with:
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
return sendSuccess(res, buildLoginStatusPayload());
};
For QQRefreshQRcodeHandler, replace the refresh body with:
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:
/**
* 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:
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:
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:
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:
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:
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:
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
loginService.getQRCodePicture();
});
with:
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
return loginService.getQRCodePicture();
});
- Step 4: Update Framework callback
In packages/napcat-framework/napcat.ts, replace:
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
loginService.getQRCodePicture();
});
with:
WebUiDataRuntime.setRefreshQRCodeCallback(async () => {
return loginService.getQRCodePicture();
});
- Step 5: Run source wiring test
Run:
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:
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:
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:
corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' install --frozen-lockfile
Expected: install succeeds.
- Step 2: Run fork tests
Run:
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:
corepack pnpm --dir 'D:\MyFiles\KT\GitHub\NapCatQQ' run typecheck
Expected: PASS.
- Step 4: Build Shell artifact
Run:
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:
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:
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:
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:
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:
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:
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:
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:
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:
#!/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:
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:
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:
desktopProfileVersion: 'desktop-cn-v2',
to:
desktopProfileVersion: 'desktop-cn-v3',
Run:
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:
'desktop-cn-v2',
to:
'desktop-cn-v3',
- Step 3: Update README commands
Replace ci/napcat-desktop-cn/README.md with:
# 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:
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:
$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:
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:
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:
corepack pnpm --dir 'D:\MyFiles\KT\Node\kt-template-online-api' run typecheck
Expected: PASS.
- Step 3: Run diff check
Run:
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:
pnpm --dir 'D:\MyFiles\KT\mcp\ktWorkflow' run global-review
Expected: findings=[].
- Step 5: Commit API repo
Run:
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:
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:
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:
$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:
$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:
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:
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:
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:
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:
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=falseno longer returnsQQ Is Logined. - NapCat emits a new QR event or returns
accepted=true, updated=falsewith no stale QR exposed. - API
scan/statusnever 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.