From 571db8ecea8b1d49814ad1630d74c9579e24924e Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 24 Jun 2026 10:38:22 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=BC=96=E5=86=99NapCat=20WebUI=20Gate?= =?UTF-8?q?way=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...apcat-webui-gateway-implementation-plan.md | 1802 +++++++++++++++++ ...webui-gateway-implementation-plan.zh-CN.md | 839 ++++++++ 2 files changed, 2641 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.md create mode 100644 docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.zh-CN.md diff --git a/docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.md b/docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.md new file mode 100644 index 0000000..c81addc --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.md @@ -0,0 +1,1802 @@ +# QQBot NapCat WebUI Gateway 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 an independent NapCat WebUI Gateway service and an Admin second-level route that opens the selected QQBot account's original NapCat WebUI with full operation capability and route-bound session cleanup. + +**Architecture:** The API service remains the Admin-authenticated authority that resolves a QQBot account to its active NapCat container and requests a short-lived Gateway session. The new `kt-napcat-webui-gateway` process owns session storage, one-time bootstrap tickets, NapCat WebUI credential exchange, HTTP/static/API/WebSocket proxying, and audit events. Admin opens `/qqbot/account/:accountId/napcat-webui`, creates a route-bound session, heartbeats while mounted, and revokes on route leave. + +**Tech Stack:** NestJS 11, Express adapter, TypeORM/MySQL, Redis through `ioredis`, `http-proxy-middleware` for Express/WebSocket proxying, Vue 3 TSX, Vben Admin, antdv-next, K8s, Jenkins, Caddy/Admin domain route. + +--- + +## Source References + +- Spec: `docs/superpowers/specs/2026-06-24-qqbot-napcat-webui-gateway-design.md` +- Chinese spec: `docs/superpowers/specs/2026-06-24-qqbot-napcat-webui-gateway-design.zh-CN.md` +- `http-proxy-middleware` supports Express proxy middleware and WebSocket upgrades through `ws: true` and upgrade handling: +- Redis documents `ioredis` as a Node Redis client option, and the package has async Redis primitives suitable for TTL leases: + +## Scope Check + +This is one deployable workstream even though it spans API, Gateway, Admin, and deployment files. The pieces are not independently useful: Admin cannot load a WebUI route without API session creation, API session creation cannot pass online smoke without Gateway, and Gateway cannot be safely exposed without the route lifecycle and deploy route. + +## File Structure + +### API repository: `D:\MyFiles\KT\Node\kt-template-online-api` + +- Modify `package.json` and `pnpm-lock.yaml`: add `ioredis` and `http-proxy-middleware`, plus gateway start scripts. +- Create `src/apps/napcat-webui-gateway/main.ts`: standalone Nest bootstrap on port `48086`. +- Create `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts`: Gateway module imports config, logger, TypeORM, and gateway providers/controllers. +- Create `src/apps/napcat-webui-gateway/config/napcat-webui-gateway-config.service.ts`: reads Gateway env with bounded defaults. +- Create `src/apps/napcat-webui-gateway/domain/napcat-webui-gateway.types.ts`: session, audit, target, and proxy types. +- Create `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-redis.store.ts`: Redis-backed session and ticket store. +- Create `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts`: one-time bootstrap ticket generation and redemption. +- Create `src/apps/napcat-webui-gateway/infrastructure/napcat-webui-credential.client.ts`: server-side WebUI token to Credential exchange. +- Create `src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service.ts`: HTTP, header, redirect, cookie, and WebSocket proxy assembly. +- Create `src/apps/napcat-webui-gateway/application/napcat-webui-gateway-session.service.ts`: session create, active mark, heartbeat, revoke, expire, and concurrent-session policy. +- Create `src/apps/napcat-webui-gateway/presentation/internal-session.controller.ts`: internal service-to-service session API. +- Create `src/apps/napcat-webui-gateway/presentation/public-webui.controller.ts`: bootstrap and public iframe/proxy entry. +- Create `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.dto.ts`: Admin-facing DTOs. +- Create `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.controller.ts`: Admin endpoints under `/qqbot/napcat/webui`. +- Create `src/modules/qqbot/napcat/webui-gateway/application/qqbot-napcat-webui-gateway.service.ts`: account authorization, target resolution, and Gateway client orchestration. +- Create `src/modules/qqbot/napcat/webui-gateway/infrastructure/qqbot-napcat-webui-gateway.client.ts`: internal Gateway HTTP client. +- Create `src/modules/qqbot/napcat/webui-gateway/infrastructure/persistence/napcat-webui-gateway-audit.entity.ts`: MySQL audit entity. +- Modify `src/modules/qqbot/napcat/qqbot-napcat.module.ts`: register controller, service, client, and audit entity. +- Modify `sql/qqbot-init.sql` and `sql/refactor-v3/01-seed-core.sql`: add `QqBot:Account:WebUI` hidden route/menu and row-action permission. +- Modify `sql/refactor-v3/99-verify.sql`: assert the new permission and audit table exist. +- Create `dockerfile.gateway`: production image entry for `dist/apps/napcat-webui-gateway/main`. +- Modify `Jenkinsfile`: build, push, and deploy API image plus Gateway image. +- Modify `k8s/prod/api.yaml`: add Gateway Deployment/Service and API env for Gateway base URL/public base URL/internal secret. +- Modify `README.md` and `API.md`: document Gateway env, route, and validation commands. + +### Admin repository: `D:\MyFiles\KT\Vue\kt-template-admin` + +- Modify `apps/web-antdv-next/src/router/routes/modules/qqbot.ts`: add hidden second-level WebUI route. +- Modify `apps/web-antdv-next/src/api/qqbot/napcat.ts`: add WebUI session contracts and callers. +- Modify `apps/web-antdv-next/src/views/qqbot/account/list.tsx`: add WebUI row action. +- Create `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx`: remote console page. +- Create `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss`: layout and theme-aware console styles. +- Create `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/useNapcatWebuiGatewaySession.ts`: route-bound session lifecycle. +- Modify `apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts`: keep WebUI session code out of account list. +- Create `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx`: page lifecycle tests. +- Modify `apps/web-antdv-next/src/api/qqbot/napcat.spec.ts`: WebUI session caller tests. + +--- + +### Task 1: Add Contracts, Permission Seeds, and Red Tests + +**Files:** +- Create: `test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts` +- Modify: `sql/qqbot-init.sql` +- Modify: `sql/refactor-v3/01-seed-core.sql` +- Modify: `sql/refactor-v3/99-verify.sql` + +- [ ] **Step 1: Write the failing structural test** + +Create `test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts`: + +```ts +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const repoRoot = resolve(__dirname, '../../../..'); + +const read = (path: string) => readFileSync(resolve(repoRoot, path), 'utf8'); + +describe('NapCat WebUI Gateway contract seeds', () => { + it('registers a dedicated Admin permission for full NapCat WebUI access', () => { + const coreSeed = read('sql/refactor-v3/01-seed-core.sql'); + const qqbotSeed = read('sql/qqbot-init.sql'); + + expect(coreSeed).toContain('QqBot:Account:WebUI'); + expect(coreSeed).toContain('QqBotAccountNapcatWebui'); + expect(qqbotSeed).toContain('QqBot:Account:WebUI'); + expect(qqbotSeed).toContain('QqBotAccountNapcatWebui'); + }); + + it('verifies the gateway audit table during full schema checks', () => { + const verifySql = read('sql/refactor-v3/99-verify.sql'); + + expect(verifySql).toContain('qqbot_napcat_webui_gateway_audit'); + expect(verifySql).toContain('QqBot:Account:WebUI'); + }); +}); +``` + +- [ ] **Step 2: Run the new test and verify RED** + +Run: + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts --runInBand +``` + +Expected: FAIL because `QqBot:Account:WebUI` and `qqbot_napcat_webui_gateway_audit` are not present. + +- [ ] **Step 3: Add SQL seed rows** + +Add these rows next to existing QQBot account rows in both `sql/qqbot-init.sql` and `sql/refactor-v3/01-seed-core.sql`: + +```sql +(2041700000000100411, 2041700000000100400, 'QqBotAccountNapcatWebui', '/qqbot/account/:accountId/napcat-webui', '/qqbot/account/napcat-webui/index', NULL, 'QqBot:Account:WebUI', 'menu', '{"activePath":"/qqbot/account","hideInMenu":true,"title":"NapCat WebUI"}', 1, 0), +(2041700000000120407, 2041700000000100402, 'QqBotAccountWebUI', NULL, NULL, NULL, 'QqBot:Account:WebUI', 'button', '{"title":"NapCat WebUI"}', 1, 0), +``` + +Keep IDs unique and do not modify unrelated menu rows. + +- [ ] **Step 4: Add schema verification SQL** + +In `sql/refactor-v3/99-verify.sql`, add assertions using the existing verify style: + +```sql +SELECT 'qqbot_napcat_webui_gateway_audit table exists' AS check_name, + COUNT(*) AS matched +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name = 'qqbot_napcat_webui_gateway_audit'; + +SELECT 'QqBot Account WebUI permission exists' AS check_name, + COUNT(*) AS matched +FROM admin_menu +WHERE auth_code = 'QqBot:Account:WebUI'; +``` + +The current menu table is `admin_menu`, and the permission column is `auth_code`; use those exact names in the verification SQL. + +- [ ] **Step 5: Run the contract test and verify GREEN** + +Run: + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts --runInBand +``` + +Expected: PASS. + +- [ ] **Step 6: Commit Task 1** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts sql/qqbot-init.sql sql/refactor-v3/01-seed-core.sql sql/refactor-v3/99-verify.sql +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat WebUI权限契约" +``` + +--- + +### Task 2: Build API Session Endpoints and Audit Entity + +**Files:** +- Create: `test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.dto.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.controller.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/application/qqbot-napcat-webui-gateway.service.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/infrastructure/qqbot-napcat-webui-gateway.client.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/infrastructure/persistence/napcat-webui-gateway-audit.entity.ts` +- Modify: `src/modules/qqbot/napcat/qqbot-napcat.module.ts` +- Modify: `src/modules/qqbot/napcat/infrastructure/persistence/index.ts` + +- [ ] **Step 1: Write the failing API service test** + +Create `test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts`: + +```ts +import { QqbotNapcatWebuiGatewayService } from '../../../../src/modules/qqbot/napcat/webui-gateway/application/qqbot-napcat-webui-gateway.service'; + +const account = { + id: 'account-1', + name: 'Mirror', + selfId: '1914728559', +}; + +const redactedWebuiToken = ['redacted', 'webui', 'token'].join('-'); + +const container = { + id: 'container-1', + name: 'kt-qqbot-napcat-1', + webuiPort: 6100, + webuiStatus: 'online', + webuiToken: redactedWebuiToken, +}; + +describe('QqbotNapcatWebuiGatewayService', () => { + it('creates a safe Admin session response without leaking NapCat secrets', async () => { + const service = new QqbotNapcatWebuiGatewayService( + { findById: jest.fn().mockResolvedValue(account) } as any, + { findPrimaryContainerByAccountId: jest.fn().mockResolvedValue(container) } as any, + { + createSession: jest.fn().mockResolvedValue({ + expiresAt: 1782268000000, + iframeUrl: '/napcat-webui/session/session-1/bootstrap?ticket=ticket-1', + sessionId: 'session-1', + }), + heartbeat: jest.fn(), + revoke: jest.fn(), + } as any, + { record: jest.fn() } as any, + ); + + const result = await service.createSession({ + accountId: 'account-1', + adminUserId: '2041700000000000002', + clientIp: '127.0.0.1', + userAgent: 'vitest', + }); + + expect(result).toMatchObject({ + account: { id: 'account-1', selfId: '1914728559' }, + container: { + id: 'container-1', + name: 'kt-qqbot-napcat-1', + webuiStatus: 'online', + }, + iframeUrl: '/napcat-webui/session/session-1/bootstrap?ticket=ticket-1', + sessionId: 'session-1', + }); + expect(JSON.stringify(result)).not.toContain('secret-token'); + expect(JSON.stringify(result)).not.toContain('6100'); + }); + + it('rejects accounts without an online WebUI target before calling Gateway', async () => { + const gatewayClient = { createSession: jest.fn() }; + const service = new QqbotNapcatWebuiGatewayService( + { findById: jest.fn().mockResolvedValue(account) } as any, + { + findPrimaryContainerByAccountId: jest + .fn() + .mockResolvedValue({ ...container, webuiStatus: 'offline' }), + } as any, + gatewayClient as any, + { record: jest.fn() } as any, + ); + + await expect( + service.createSession({ + accountId: 'account-1', + adminUserId: '2041700000000000002', + clientIp: '127.0.0.1', + userAgent: 'vitest', + }), + ).rejects.toThrow('NapCat WebUI 不在线'); + expect(gatewayClient.createSession).not.toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 2: Run the service test and verify RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts --runInBand +``` + +Expected: FAIL because the service and DTOs do not exist. + +- [ ] **Step 3: Create DTOs** + +Create `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.dto.ts`: + +```ts +import { ApiProperty } from '@nestjs/swagger'; + +export class QqbotNapcatWebuiSessionCreateDto { + @ApiProperty({ description: 'QQBot account id selected from the account list' }) + accountId!: string; +} + +export class QqbotNapcatWebuiSessionResponseDto { + account!: { + id: string; + name?: string; + selfId: string; + }; + container!: { + id: string; + name: string; + webuiStatus: string; + }; + expiresAt!: number; + iframeUrl!: string; + sessionId!: string; +} +``` + +- [ ] **Step 4: Create the audit entity** + +Create `src/modules/qqbot/napcat/webui-gateway/infrastructure/persistence/napcat-webui-gateway-audit.entity.ts`: + +```ts +import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { KtCreateDateColumn, KtDateTime } from '@/common'; + +@Entity('qqbot_napcat_webui_gateway_audit') +export class NapcatWebuiGatewayAudit { + @PrimaryColumn({ length: 32 }) + id!: string; + + @Column({ length: 64, name: 'session_id' }) + sessionId!: string; + + @Column({ length: 32, name: 'admin_user_id' }) + adminUserId!: string; + + @Column({ length: 32, name: 'account_id' }) + accountId!: string; + + @Column({ length: 32, name: 'self_id' }) + selfId!: string; + + @Column({ length: 32, name: 'container_id' }) + containerId!: string; + + @Column({ length: 64, name: 'event_type' }) + eventType!: string; + + @Column({ length: 128, name: 'client_ip', nullable: true }) + clientIp?: null | string; + + @Column({ length: 512, name: 'user_agent', nullable: true }) + userAgent?: null | string; + + @Column({ name: 'detail_json', nullable: true, type: 'json' }) + detailJson?: null | Record; + + @KtCreateDateColumn() + createTime!: KtDateTime; +} +``` + +Use the existing ID generation helper used by nearby services when recording rows. + +- [ ] **Step 5: Create the Gateway internal client** + +Create `src/modules/qqbot/napcat/webui-gateway/infrastructure/qqbot-napcat-webui-gateway.client.ts` with methods: + +```ts +export interface CreateGatewaySessionInput { + accountId: string; + adminUserId: string; + clientIp?: string; + containerId: string; + containerName: string; + selfId: string; + upstreamBaseUrl: string; + userAgent?: string; + webuiToken: string; +} + +export interface CreateGatewaySessionResult { + expiresAt: number; + iframeUrl: string; + sessionId: string; +} + +export class QqbotNapcatWebuiGatewayClient { + /** + * Creates a Gateway session through the internal service endpoint. + * @param input Account, container, and server-side WebUI credential material resolved by API. + * @returns The browser-safe iframe URL and session lifetime returned by Gateway. + */ + async createSession( + input: CreateGatewaySessionInput, + ): Promise { + // Implement with axios or Node fetch using NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL + // and NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET. Do not log webuiToken. + throw new Error('NapCat WebUI Gateway client not implemented'); + } + + /** + * Forwards a route-bound heartbeat to Gateway. + * @param sessionId Gateway session id returned to Admin. + * @returns Updated session lifetime. + */ + async heartbeat(sessionId: string) { + throw new Error('NapCat WebUI Gateway heartbeat not implemented'); + } + + /** + * Revokes an active Gateway session. + * @param sessionId Gateway session id returned to Admin. + */ + async revoke(sessionId: string) { + throw new Error('NapCat WebUI Gateway revoke not implemented'); + } +} +``` + +Replace the throwing bodies in the implementation step with bounded HTTP calls and sanitized errors. + +- [ ] **Step 6: Create the API service** + +Create `src/modules/qqbot/napcat/webui-gateway/application/qqbot-napcat-webui-gateway.service.ts` with these public methods: + +```ts +export interface CreateAdminWebuiSessionInput { + accountId: string; + adminUserId: string; + clientIp?: string; + userAgent?: string; +} + +export class QqbotNapcatWebuiGatewayService { + /** + * Creates a browser-safe NapCat WebUI Gateway session for one QQBot account. + * @param input Admin user id, selected account id, and client evidence for audit. + * @returns Safe session metadata for Admin route iframe loading. + */ + async createSession(input: CreateAdminWebuiSessionInput) { + const account = await this.accountService.findById(input.accountId); + if (!account) throw new Error('QQBot 账号不存在'); + + const container = + await this.napcatRuntimeService.findPrimaryContainerByAccountId( + input.accountId, + ); + if (!container) throw new Error('账号未绑定 NapCat 容器'); + if (container.webuiStatus === 'offline') { + throw new Error('NapCat WebUI 不在线'); + } + if (!container.webuiToken || !container.webuiPort) { + throw new Error('NapCat WebUI 配置不完整'); + } + + const gateway = await this.gatewayClient.createSession({ + accountId: account.id, + adminUserId: input.adminUserId, + clientIp: input.clientIp, + containerId: container.id, + containerName: container.name, + selfId: account.selfId, + upstreamBaseUrl: this.buildContainerWebuiUrl(container.webuiPort), + userAgent: input.userAgent, + webuiToken: container.webuiToken, + }); + + return { + account: { id: account.id, name: account.name, selfId: account.selfId }, + container: { + id: container.id, + name: container.name, + webuiStatus: container.webuiStatus || 'unknown', + }, + ...gateway, + }; + } +} +``` + +Add `findPrimaryContainerByAccountId(accountId: string)` to `QqbotNapcatContainerService` with JSDoc and a focused unit test, then use that method from `QqbotNapcatWebuiGatewayService`. + +- [ ] **Step 7: Create the API controller** + +Create `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.controller.ts`: + +```ts +import { Body, Controller, HttpCode, HttpStatus, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard'; +import { vbenSuccess } from '@/common'; +import { QqbotNapcatWebuiGatewayService } from '../application/qqbot-napcat-webui-gateway.service'; +import { QqbotNapcatWebuiSessionCreateDto } from './qqbot-napcat-webui-gateway.dto'; + +@ApiTags('QQBot - NapCat WebUI Gateway') +@Controller('qqbot/napcat/webui') +@UseGuards(JwtAuthGuard) +export class QqbotNapcatWebuiGatewayController { + /** + * Initializes the Admin-facing NapCat WebUI Gateway controller. + * @param service Gateway session application service. + */ + constructor(private readonly service: QqbotNapcatWebuiGatewayService) {} + + /** + * Creates one route-bound NapCat WebUI session. + * @param body Selected QQBot account id from Admin. + * @param req Authenticated request carrying Admin user and client evidence. + */ + @Post('session') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '创建 NapCat WebUI Gateway 会话' }) + async createSession(@Body() body: QqbotNapcatWebuiSessionCreateDto, @Req() req: any) { + return vbenSuccess( + await this.service.createSession({ + accountId: body.accountId, + adminUserId: `${req.user?.sub || req.user?.id || ''}`, + clientIp: req.ip, + userAgent: req.headers?.['user-agent'], + }), + ); + } + + @Post('session/:sessionId/heartbeat') + @HttpCode(HttpStatus.OK) + async heartbeat(@Param('sessionId') sessionId: string) { + return vbenSuccess(await this.service.heartbeat(sessionId)); + } + + @Post('session/:sessionId/revoke') + @HttpCode(HttpStatus.OK) + async revoke(@Param('sessionId') sessionId: string) { + return vbenSuccess(await this.service.revoke(sessionId)); + } +} +``` + +- [ ] **Step 8: Register providers and entities** + +Modify `src/modules/qqbot/napcat/qqbot-napcat.module.ts` to include: + +```ts +import { QqbotNapcatWebuiGatewayController } from './webui-gateway/contract/qqbot-napcat-webui-gateway.controller'; +import { QqbotNapcatWebuiGatewayService } from './webui-gateway/application/qqbot-napcat-webui-gateway.service'; +import { QqbotNapcatWebuiGatewayClient } from './webui-gateway/infrastructure/qqbot-napcat-webui-gateway.client'; +``` + +Add the controller to `QQBOT_NAPCAT_CONTROLLERS` and the service/client to `QQBOT_NAPCAT_PROVIDERS`. + +Modify `src/modules/qqbot/napcat/infrastructure/persistence/index.ts` to include `NapcatWebuiGatewayAudit` in `NAPCAT_RUNTIME_ENTITIES` and add `qqbot_napcat_webui_gateway_audit` to `NAPCAT_RUNTIME_DOMAIN_CONTRACT.tables`. + +- [ ] **Step 9: Run API tests and typecheck** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +``` + +Expected: tests PASS and typecheck PASS. + +- [ ] **Step 10: Commit Task 2** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/napcat/webui-gateway src/modules/qqbot/napcat/qqbot-napcat.module.ts src/modules/qqbot/napcat/infrastructure/persistence/index.ts test/modules/qqbot/napcat-webui-gateway +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat WebUI会话接口" +``` + +--- + +### Task 3: Build Gateway App, Session Store, and Bootstrap Tickets + +**Files:** +- Modify: `package.json` +- Modify: `pnpm-lock.yaml` +- Create: `test/apps/napcat-webui-gateway/session-store.spec.ts` +- Create: `src/apps/napcat-webui-gateway/main.ts` +- Create: `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` +- Create: `src/apps/napcat-webui-gateway/config/napcat-webui-gateway-config.service.ts` +- Create: `src/apps/napcat-webui-gateway/domain/napcat-webui-gateway.types.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-redis.store.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts` +- Create: `src/apps/napcat-webui-gateway/application/napcat-webui-gateway-session.service.ts` +- Create: `src/apps/napcat-webui-gateway/presentation/internal-session.controller.ts` + +- [ ] **Step 1: Add dependencies** + +Run: + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api add ioredis http-proxy-middleware +``` + +Expected: `package.json` and `pnpm-lock.yaml` update. Keep `ws` unchanged because it already exists. + +- [ ] **Step 2: Add package scripts** + +Modify `package.json` scripts: + +```json +{ + "start:gateway:prod": "cross-env NODE_ENV=production node dist/apps/napcat-webui-gateway/main", + "start:gateway:dev": "ts-node -r tsconfig-paths/register src/apps/napcat-webui-gateway/main.ts" +} +``` + +The API repo already depends on `ts-node` and `tsconfig-paths`, so the dev script uses the direct TypeScript entrypoint and the production script keeps the compiled `dist/apps/napcat-webui-gateway/main` entrypoint. + +- [ ] **Step 3: Write the failing session lifecycle test** + +Create `test/apps/napcat-webui-gateway/session-store.spec.ts`: + +```ts +import { NapcatWebuiGatewaySessionService } from '../../../src/apps/napcat-webui-gateway/application/napcat-webui-gateway-session.service'; +import type { NapcatWebuiGatewaySessionStore } from '../../../src/apps/napcat-webui-gateway/domain/napcat-webui-gateway.types'; + +class MemoryStore implements NapcatWebuiGatewaySessionStore { + readonly sessions = new Map(); + + async create(session: any) { + this.sessions.set(session.sessionId, session); + return session; + } + + async find(sessionId: string) { + return this.sessions.get(sessionId); + } + + async findActiveByUserAndAccount(adminUserId: string, accountId: string) { + return [...this.sessions.values()].find( + (item) => + item.adminUserId === adminUserId && + item.accountId === accountId && + item.status !== 'revoked', + ); + } + + async update(sessionId: string, patch: any) { + const current = this.sessions.get(sessionId); + const next = { ...current, ...patch }; + this.sessions.set(sessionId, next); + return next; + } +} + +describe('NapcatWebuiGatewaySessionService', () => { + it('revokes an older same-user same-account session when creating a new one', async () => { + const store = new MemoryStore(); + const service = new NapcatWebuiGatewaySessionService(store as any, { + now: () => 1000, + ttlMs: () => 60000, + } as any); + + const first = await service.create({ + accountId: 'account-1', + adminUserId: 'admin-1', + containerId: 'container-1', + containerName: 'container', + selfId: '1914728559', + upstreamBaseUrl: 'http://127.0.0.1:6100', + webuiToken: ['redacted', 'webui', 'token'].join('-'), + }); + const second = await service.create({ + accountId: 'account-1', + adminUserId: 'admin-1', + containerId: 'container-1', + containerName: 'container', + selfId: '1914728559', + upstreamBaseUrl: 'http://127.0.0.1:6100', + webuiToken: ['redacted', 'webui', 'token'].join('-'), + }); + + expect(first.sessionId).not.toBe(second.sessionId); + expect(await store.find(first.sessionId)).toMatchObject({ + status: 'revoked', + }); + expect(await store.find(second.sessionId)).toMatchObject({ + status: 'created', + }); + }); + + it('extends active sessions on heartbeat and rejects revoked sessions', async () => { + const store = new MemoryStore(); + let now = 1000; + const service = new NapcatWebuiGatewaySessionService(store as any, { + now: () => now, + ttlMs: () => 60000, + } as any); + + const session = await service.create({ + accountId: 'account-1', + adminUserId: 'admin-1', + containerId: 'container-1', + containerName: 'container', + selfId: '1914728559', + upstreamBaseUrl: 'http://127.0.0.1:6100', + webuiToken: ['redacted', 'webui', 'token'].join('-'), + }); + + now = 5000; + await service.markActive(session.sessionId); + const heartbeat = await service.heartbeat(session.sessionId); + expect(heartbeat).toMatchObject({ + sessionId: session.sessionId, + status: 'active', + }); + expect(heartbeat.expiresAt).toBe(65000); + + await service.revoke(session.sessionId); + await expect(service.heartbeat(session.sessionId)).rejects.toThrow( + 'Gateway session is not active', + ); + }); +}); +``` + +- [ ] **Step 4: Run the test and verify RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/session-store.spec.ts --runInBand +``` + +Expected: FAIL because Gateway app types and service do not exist. + +- [ ] **Step 5: Create Gateway domain types** + +Create `src/apps/napcat-webui-gateway/domain/napcat-webui-gateway.types.ts`: + +```ts +export type NapcatWebuiGatewaySessionStatus = + | 'active' + | 'created' + | 'expired' + | 'failed' + | 'revoked'; + +export interface NapcatWebuiGatewaySession { + accountId: string; + activeAt?: number; + adminUserId: string; + clientIp?: string; + containerId: string; + containerName: string; + createdAt: number; + expiresAt: number; + lastSeenAt?: number; + revokedAt?: number; + selfId: string; + sessionId: string; + status: NapcatWebuiGatewaySessionStatus; + upstreamBaseUrl: string; + userAgent?: string; + webuiToken: string; +} + +export interface NapcatWebuiGatewaySessionStore { + create(session: NapcatWebuiGatewaySession): Promise; + find(sessionId: string): Promise; + findActiveByUserAndAccount( + adminUserId: string, + accountId: string, + ): Promise; + update( + sessionId: string, + patch: Partial, + ): Promise; +} +``` + +- [ ] **Step 6: Implement session service** + +Create `src/apps/napcat-webui-gateway/application/napcat-webui-gateway-session.service.ts` with `create`, `markActive`, `heartbeat`, `revoke`, and `requireProxySession`. Every method needs JSDoc. Use `crypto.randomUUID()` for `sessionId`. + +The `create()` implementation must: + +```ts +const older = await this.store.findActiveByUserAndAccount( + input.adminUserId, + input.accountId, +); +if (older) { + await this.store.update(older.sessionId, { + revokedAt: now, + status: 'revoked', + }); +} +``` + +The `heartbeat()` implementation must reject `revoked`, `expired`, `failed`, and missing sessions. + +- [ ] **Step 7: Implement Redis store and ticket service** + +Create `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-redis.store.ts`. Use `ioredis` `set(key, value, 'PX', ttlMs)`, `get`, and a secondary index key: + +```text +napcat:webui:session:{sessionId} +napcat:webui:user-account:{adminUserId}:{accountId} +``` + +Create `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts`. Ticket keys: + +```text +napcat:webui:ticket:{ticket} +``` + +Ticket TTL must be 60 seconds or less. Redemption deletes the ticket key before returning the session id. + +- [ ] **Step 8: Add Gateway module and internal controller** + +Create `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` and `presentation/internal-session.controller.ts`. Internal controller paths: + +```text +POST /internal/sessions +POST /internal/sessions/:sessionId/heartbeat +POST /internal/sessions/:sessionId/revoke +GET /internal/health +``` + +Check the `x-kt-gateway-secret` header against `NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET` before accepting mutating internal calls. + +- [ ] **Step 9: Add Gateway bootstrap** + +Create `src/apps/napcat-webui-gateway/main.ts`: + +```ts +import { NestFactory } from '@nestjs/core'; +import { Logger } from 'nestjs-pino'; +import { json, urlencoded } from 'express'; +import { NapcatWebuiGatewayModule } from './napcat-webui-gateway.module'; + +/** + * Starts the standalone NapCat WebUI Gateway process. + */ +async function bootstrap() { + const app = await NestFactory.create(NapcatWebuiGatewayModule, { + bufferLogs: true, + }); + app.useLogger(app.get(Logger)); + app.use(json({ limit: '50mb' })); + app.use(urlencoded({ extended: true, limit: '50mb' })); + await app.listen(Number(process.env.NAPCAT_WEBUI_GATEWAY_PORT || 48086)); +} + +bootstrap(); +``` + +- [ ] **Step 10: Run Gateway session tests and typecheck** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/session-store.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +``` + +Expected: tests PASS and typecheck PASS. + +- [ ] **Step 11: Commit Task 3** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add package.json pnpm-lock.yaml src/apps/napcat-webui-gateway test/apps/napcat-webui-gateway +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat WebUI Gateway会话服务" +``` + +--- + +### Task 4: Add Gateway Credential Exchange and Proxy + +**Files:** +- Create: `test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/napcat-webui-credential.client.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service.ts` +- Create: `src/apps/napcat-webui-gateway/presentation/public-webui.controller.ts` +- Modify: `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` + +- [ ] **Step 1: Write proxy rewrite tests** + +Create `test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts`: + +```ts +import { + rewriteNapcatLocationHeader, + rewriteNapcatSetCookieHeader, + sanitizeGatewayProxyPath, +} from '../../../src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service'; + +describe('NapCat WebUI Gateway proxy rewriting', () => { + it('rejects upstream URL injection and path traversal', () => { + expect(() => sanitizeGatewayProxyPath('https://evil.test/api')).toThrow( + 'Invalid NapCat WebUI proxy path', + ); + expect(() => sanitizeGatewayProxyPath('../api/auth/login')).toThrow( + 'Invalid NapCat WebUI proxy path', + ); + expect(sanitizeGatewayProxyPath('api/QQLogin/CheckLoginStatus')).toBe( + '/api/QQLogin/CheckLoginStatus', + ); + }); + + it('rewrites redirects under the session proxy prefix', () => { + expect( + rewriteNapcatLocationHeader('/webui/login', { + sessionId: 'session-1', + }), + ).toBe('/napcat-webui/session/session-1/webui/webui/login'); + }); + + it('scopes cookies to the active session path', () => { + expect( + rewriteNapcatSetCookieHeader('napcat=value; Path=/; HttpOnly', { + sessionId: 'session-1', + }), + ).toContain('Path=/napcat-webui/session/session-1'); + }); +}); +``` + +- [ ] **Step 2: Run proxy test and verify RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts --runInBand +``` + +Expected: FAIL because proxy helpers do not exist. + +- [ ] **Step 3: Implement credential client** + +Create `src/apps/napcat-webui-gateway/infrastructure/napcat-webui-credential.client.ts`. Use the same NapCat WebUI contract as `NapcatWebuiHttpClient`: hash `webuiToken + ".napcat"`, POST `/api/auth/login`, and cache Credential per session until session revoke/expire. + +Do not log `webuiToken`, hash, or Credential. + +- [ ] **Step 4: Implement proxy helpers** + +Create pure helper exports in `src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service.ts`: + +```ts +export function sanitizeGatewayProxyPath(rawPath: string): string { + const decoded = decodeURIComponent(rawPath || ''); + if (/^https?:\/\//i.test(decoded) || decoded.includes('..')) { + throw new Error('Invalid NapCat WebUI proxy path'); + } + return `/${decoded.replace(/^\/+/, '')}`; +} + +export function rewriteNapcatLocationHeader( + location: string, + input: { sessionId: string }, +) { + if (/^https?:\/\//i.test(location)) return location; + return `/napcat-webui/session/${input.sessionId}/webui/${location.replace(/^\/+/, '')}`; +} + +export function rewriteNapcatSetCookieHeader( + value: string, + input: { sessionId: string }, +) { + return value.replace( + /Path=[^;]*/i, + `Path=/napcat-webui/session/${input.sessionId}`, + ); +} +``` + +- [ ] **Step 5: Implement proxy service** + +Use `createProxyMiddleware` from `http-proxy-middleware` with: + +```ts +{ + changeOrigin: true, + secure: false, + ws: true, + selfHandleResponse: false, +} +``` + +Before proxying: + +- resolve session by `sessionId`; +- reject non-active/non-created sessions with 410; +- redeem Credential through `NapcatWebuiCredentialClient`; +- add `Authorization: Bearer ${credential}` upstream; +- never forward API/Admin cookies upstream; +- never allow the browser to override `target`. + +On first successful upstream response, call `sessionService.markActive(sessionId)`. + +- [ ] **Step 6: Implement public controller** + +Create `src/apps/napcat-webui-gateway/presentation/public-webui.controller.ts`: + +```text +GET /napcat-webui/session/:sessionId/bootstrap +ALL /napcat-webui/session/:sessionId/webui/* +``` + +Bootstrap must redeem `ticket`, set an HttpOnly gateway cookie scoped to `/napcat-webui/session/:sessionId`, and redirect to `/napcat-webui/session/:sessionId/webui/webui`. + +Proxy route delegates to `NapcatWebuiProxyService`. + +- [ ] **Step 7: Run tests and typecheck** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/session-store.spec.ts test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +``` + +Expected: tests PASS and typecheck PASS. + +- [ ] **Step 8: Commit Task 4** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add src/apps/napcat-webui-gateway test/apps/napcat-webui-gateway +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 代理NapCat WebUI流量" +``` + +--- + +### Task 5: Wire Build, Docker, K8s, and API Gateway Env + +**Files:** +- Create: `test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts` +- Create: `dockerfile.gateway` +- Modify: `Jenkinsfile` +- Modify: `k8s/prod/api.yaml` +- Modify: `README.md` +- Modify: `API.md` + +- [ ] **Step 1: Write deployment structural tests** + +Create `test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts`: + +```ts +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const root = resolve(__dirname, '../../../..'); +const read = (path: string) => readFileSync(resolve(root, path), 'utf8'); + +describe('NapCat WebUI Gateway deployment wiring', () => { + it('has a dedicated production dockerfile entry', () => { + const dockerfile = read('dockerfile.gateway'); + + expect(dockerfile).toContain('dist/apps/napcat-webui-gateway/main'); + expect(dockerfile).toContain('EXPOSE 48086'); + }); + + it('deploys Gateway as a separate K8s Deployment and Service', () => { + const manifest = read('k8s/prod/api.yaml'); + + expect(manifest).toContain('name: kt-napcat-webui-gateway'); + expect(manifest).toContain('containerPort: 48086'); + expect(manifest).toContain('NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET'); + expect(manifest).toContain('NAPCAT_WEBUI_GATEWAY_REDIS_HOST'); + }); + + it('builds and pushes a separate Gateway image in Jenkins', () => { + const jenkinsfile = read('Jenkinsfile'); + + expect(jenkinsfile).toContain('GATEWAY_IMAGE_NAME'); + expect(jenkinsfile).toContain('dockerfile.gateway'); + expect(jenkinsfile).toContain('kt-napcat-webui-gateway'); + }); +}); +``` + +- [ ] **Step 2: Run deployment test and verify RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand +``` + +Expected: FAIL because deployment files are not wired yet. + +- [ ] **Step 3: Add `dockerfile.gateway`** + +Create `dockerfile.gateway` by mirroring `dockerfile`, changing: + +```dockerfile +ENV APP_PORT=48086 +ENV LOG_APP_NAME=kt-napcat-webui-gateway +EXPOSE 48086 +CMD ["node", "dist/apps/napcat-webui-gateway/main"] +``` + +Keep font and production dependency installation consistent with the API image. + +- [ ] **Step 4: Update Jenkins** + +Add parameter: + +```groovy +string(name: 'GATEWAY_IMAGE_NAME', defaultValue: 'kt-napcat-webui-gateway', description: 'NapCat WebUI Gateway 镜像名称') +``` + +In `Prepare`, compute: + +```groovy +env.GATEWAY_DOCKER_IMAGE = registry ? "${registry}/${params.GATEWAY_IMAGE_NAME}:${env.IMAGE_TAG_FINAL}" : "${params.GATEWAY_IMAGE_NAME}:${env.IMAGE_TAG_FINAL}" +env.GATEWAY_DOCKER_IMAGE_LATEST = registry ? "${registry}/${params.GATEWAY_IMAGE_NAME}:latest" : "${params.GATEWAY_IMAGE_NAME}:latest" +``` + +In Docker Build, after API image build: + +```groovy +docker build -f dockerfile.gateway -t ${env.GATEWAY_DOCKER_IMAGE} . +if [ '${env.GATEWAY_DOCKER_IMAGE}' != '${env.GATEWAY_DOCKER_IMAGE_LATEST}' ]; then + docker tag ${env.GATEWAY_DOCKER_IMAGE} ${env.GATEWAY_DOCKER_IMAGE_LATEST} +fi +``` + +In Docker Push, push both Gateway tags. In K8s Deploy, set the Gateway deployment image: + +```groovy +kubectl ${kubeConfigArg} ${namespaceArg} set image deployment/kt-napcat-webui-gateway gateway=${env.GATEWAY_DOCKER_IMAGE} +``` + +Check rollout for both API and Gateway deployments. + +- [ ] **Step 5: Update K8s manifest** + +Add Gateway Deployment and Service in `k8s/prod/api.yaml`: + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kt-napcat-webui-gateway + namespace: kt-prod + labels: + app: kt-napcat-webui-gateway +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + app: kt-napcat-webui-gateway + template: + metadata: + labels: + app: kt-napcat-webui-gateway + spec: + containers: + - name: gateway + image: k3d-kt-registry.localhost:5000/kt-napcat-webui-gateway:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 48086 + env: + - name: NODE_ENV + value: production + - name: TZ + value: Asia/Shanghai + - name: NAPCAT_WEBUI_GATEWAY_PORT + value: "48086" + - name: NAPCAT_WEBUI_GATEWAY_REDIS_HOST + value: kt-qqbot-plugin-redis + - name: NAPCAT_WEBUI_GATEWAY_REDIS_PORT + value: "6379" + - name: NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL + value: /napcat-webui + envFrom: + - secretRef: + name: kt-template-online-api-env + readinessProbe: + httpGet: + path: /internal/health + port: 48086 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 +--- +apiVersion: v1 +kind: Service +metadata: + name: kt-napcat-webui-gateway + namespace: kt-prod + labels: + app: kt-napcat-webui-gateway +spec: + type: NodePort + selector: + app: kt-napcat-webui-gateway + ports: + - name: http + port: 48086 + targetPort: 48086 + nodePort: 30086 +``` + +Add API env: + +```yaml +- name: NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL + value: http://kt-napcat-webui-gateway:48086 +- name: NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL + value: /napcat-webui +``` + +Ensure `NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET` is read from the existing production env secret, not committed. + +- [ ] **Step 6: Update docs** + +Add to `README.md` and `API.md`: + +```text +NapCat WebUI Gateway: +- API creates route-bound sessions through /qqbot/napcat/webui/session. +- Gateway listens on 48086 and proxies /napcat-webui/session/:sessionId/*. +- Required env: NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL, NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL, NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET, NAPCAT_WEBUI_GATEWAY_REDIS_HOST, NAPCAT_WEBUI_GATEWAY_REDIS_PORT. +- Browser responses must not contain webuiToken, Credential, Docker host ports, or NAS SSH paths. +``` + +- [ ] **Step 7: Run deployment tests and build checks** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run build +``` + +Expected: tests PASS, typecheck PASS, build PASS, and `dist/apps/napcat-webui-gateway/main.js` exists. + +- [ ] **Step 8: Commit Task 5** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add dockerfile.gateway Jenkinsfile k8s/prod/api.yaml README.md API.md test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 部署NapCat WebUI Gateway" +``` + +--- + +### Task 6: Add Admin API Client, Route, and Account Action + +**Files:** +- Modify: `apps/web-antdv-next/src/api/qqbot/napcat.ts` +- Modify: `apps/web-antdv-next/src/api/qqbot/napcat.spec.ts` +- Modify: `apps/web-antdv-next/src/router/routes/modules/qqbot.ts` +- Modify: `apps/web-antdv-next/src/views/qqbot/account/list.tsx` +- Modify: `apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts` + +- [ ] **Step 1: Add failing Admin API tests** + +Extend `apps/web-antdv-next/src/api/qqbot/napcat.spec.ts` with: + +```ts +it('calls NapCat WebUI Gateway session endpoints', async () => { + await createQqbotNapcatWebuiSession('account-1'); + await heartbeatQqbotNapcatWebuiSession('session-1'); + await revokeQqbotNapcatWebuiSession('session-1'); + + expect(postMock).toHaveBeenCalledWith('/qqbot/napcat/webui/session', { + accountId: 'account-1', + }); + expect(postMock).toHaveBeenCalledWith( + '/qqbot/napcat/webui/session/session-1/heartbeat', + ); + expect(postMock).toHaveBeenCalledWith( + '/qqbot/napcat/webui/session/session-1/revoke', + ); +}); +``` + +Import the new functions at the top of the spec. + +- [ ] **Step 2: Add failing boundary test** + +Extend `apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts`: + +```ts +it('keeps WebUI route lifecycle outside the account list page', () => { + const source = readAccountSource('list.tsx'); + + expect(source).toContain('QqBotAccountNapcatWebui'); + expect(source).not.toContain('createQqbotNapcatWebuiSession'); + expect(source).not.toContain('heartbeatQqbotNapcatWebuiSession'); + expect(source).not.toContain('iframe'); +}); +``` + +- [ ] **Step 3: Run Admin tests and verify RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +``` + +Expected: FAIL because functions and route action are missing. + +- [ ] **Step 4: Add Admin API functions** + +Modify `apps/web-antdv-next/src/api/qqbot/napcat.ts`: + +```ts +export namespace QqbotNapcatApi { + export interface WebuiGatewaySession { + account: { id: string; name?: string; selfId: string }; + container: { id: string; name: string; webuiStatus: string }; + expiresAt: number; + iframeUrl: string; + sessionId: string; + } +} + +export function createQqbotNapcatWebuiSession(accountId: string) { + return requestClient.post( + '/qqbot/napcat/webui/session', + { accountId }, + ); +} + +export function heartbeatQqbotNapcatWebuiSession(sessionId: string) { + return requestClient.post & { status: 'active' }>( + `/qqbot/napcat/webui/session/${sessionId}/heartbeat`, + ); +} + +export function revokeQqbotNapcatWebuiSession(sessionId: string) { + return requestClient.post( + `/qqbot/napcat/webui/session/${sessionId}/revoke`, + ); +} +``` + +- [ ] **Step 5: Add hidden Admin route** + +Modify `apps/web-antdv-next/src/router/routes/modules/qqbot.ts`: + +```ts +{ + component: () => import('#/views/qqbot/account/napcat-webui'), + meta: { + activePath: '/qqbot/account', + hideInMenu: true, + title: 'NapCat WebUI', + }, + name: 'QqBotAccountNapcatWebui', + path: '/qqbot/account/:accountId/napcat-webui', +} +``` + +- [ ] **Step 6: Add account row action** + +Modify `apps/web-antdv-next/src/views/qqbot/account/list.tsx`: + +```ts +{ + disabled: (row) => !row.napcat?.containerName || getWebuiStatus(row) === 'offline', + key: 'napcatWebui', + label: 'WebUI', + onClick: openNapcatWebui, + permissionCodes: ['QqBot:Account:WebUI'], +} +``` + +Add: + +```ts +function openNapcatWebui(row: QqbotApi.Account) { + void router.push({ + name: 'QqBotAccountNapcatWebui', + params: { accountId: row.id }, + }); +} +``` + +- [ ] **Step 7: Run Admin API and boundary tests** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +``` + +Expected: tests PASS. + +- [ ] **Step 8: Commit Task 6** + +```powershell +git -C D:\MyFiles\KT\Vue\kt-template-admin add apps/web-antdv-next/src/api/qqbot/napcat.ts apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/router/routes/modules/qqbot.ts apps/web-antdv-next/src/views/qqbot/account/list.tsx apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加NapCat WebUI入口" +``` + +--- + +### Task 7: Build Admin Route Page and Lifecycle Composable + +**Files:** +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx` +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss` +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/useNapcatWebuiGatewaySession.ts` +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx` + +- [ ] **Step 1: Write route lifecycle tests** + +Create `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx`: + +```ts +import { flushPromises, mount } from '@vue/test-utils'; +import { describe, expect, it, vi } from 'vitest'; +import NapcatWebuiPage from './index'; + +const createSession = vi.fn(); +const heartbeat = vi.fn(); +const revoke = vi.fn(); + +vi.mock('#/api/qqbot/napcat', () => ({ + createQqbotNapcatWebuiSession: createSession, + heartbeatQqbotNapcatWebuiSession: heartbeat, + revokeQqbotNapcatWebuiSession: revoke, +})); + +vi.mock('vue-router', () => ({ + useRoute: () => ({ params: { accountId: 'account-1' } }), + useRouter: () => ({ push: vi.fn() }), +})); + +describe('NapcatWebuiPage', () => { + it('creates a Gateway session on mount and revokes on unmount', async () => { + createSession.mockResolvedValue({ + account: { id: 'account-1', selfId: '1914728559' }, + container: { + id: 'container-1', + name: 'kt-qqbot-napcat-1', + webuiStatus: 'online', + }, + expiresAt: Date.now() + 60000, + iframeUrl: '/napcat-webui/session/session-1/bootstrap?ticket=ticket-1', + sessionId: 'session-1', + }); + heartbeat.mockResolvedValue({ + expiresAt: Date.now() + 60000, + sessionId: 'session-1', + status: 'active', + }); + + const wrapper = mount(NapcatWebuiPage); + await flushPromises(); + + expect(createSession).toHaveBeenCalledWith('account-1'); + expect(wrapper.find('iframe').attributes('src')).toContain( + '/napcat-webui/session/session-1/bootstrap', + ); + + wrapper.unmount(); + await flushPromises(); + + expect(revoke).toHaveBeenCalledWith('session-1'); + }); +}); +``` + +- [ ] **Step 2: Run route page test and verify RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx +``` + +Expected: FAIL because the page does not exist. + +- [ ] **Step 3: Implement lifecycle composable** + +Create `useNapcatWebuiGatewaySession.ts` with: + +```ts +import { onBeforeUnmount, ref } from 'vue'; +import { + createQqbotNapcatWebuiSession, + heartbeatQqbotNapcatWebuiSession, + revokeQqbotNapcatWebuiSession, + type QqbotNapcatApi, +} from '#/api/qqbot/napcat'; + +export type NapcatWebuiSessionState = 'error' | 'idle' | 'loading' | 'ready' | 'revoked'; + +/** + * Owns one route-bound NapCat WebUI Gateway session. + * + * @param accountId QQBot account id from the current route. + * @returns Reactive session state and lifecycle commands for the WebUI page. + */ +export function useNapcatWebuiGatewaySession(accountId: string) { + const errorMessage = ref(''); + const session = ref(); + const state = ref('idle'); + let heartbeatTimer: number | undefined; + + async function open() { + state.value = 'loading'; + errorMessage.value = ''; + try { + session.value = await createQqbotNapcatWebuiSession(accountId); + state.value = 'ready'; + startHeartbeat(); + } catch (error: any) { + errorMessage.value = error?.message || 'NapCat WebUI 会话创建失败'; + state.value = 'error'; + } + } + + function startHeartbeat() { + stopHeartbeat(); + heartbeatTimer = window.setInterval(() => { + const sessionId = session.value?.sessionId; + if (!sessionId) return; + void heartbeatQqbotNapcatWebuiSession(sessionId).catch(() => { + errorMessage.value = 'NapCat WebUI 会话心跳失败,请重新打开'; + state.value = 'error'; + stopHeartbeat(); + }); + }, 20_000); + } + + function stopHeartbeat() { + if (!heartbeatTimer) return; + window.clearInterval(heartbeatTimer); + heartbeatTimer = undefined; + } + + async function revoke() { + stopHeartbeat(); + const sessionId = session.value?.sessionId; + if (!sessionId) return; + await revokeQqbotNapcatWebuiSession(sessionId).catch(() => undefined); + state.value = 'revoked'; + } + + onBeforeUnmount(() => { + void revoke(); + }); + + return { errorMessage, open, revoke, session, state }; +} +``` + +- [ ] **Step 4: Implement route page** + +Create `index.tsx` using `Page`, `Button`, `Alert`, `Spin`, and `Typography` from existing UI libraries. The iframe must only render when `state === 'ready'` and `session.iframeUrl` exists. + +Use one stable root element. Include top controls: + +- back to account list; +- reopen session; +- close session; +- display selfId and container name. + +- [ ] **Step 5: Add SCSS** + +Create `index.scss`: + +```scss +.qqbot-napcat-webui-page { + display: flex; + flex-direction: column; + height: var(--vben-content-height); + min-height: 0; + overflow: hidden; + + &__bar { + align-items: center; + border-bottom: 1px solid hsl(var(--border)); + display: flex; + flex: 0 0 auto; + gap: 12px; + justify-content: space-between; + padding: 10px 12px; + } + + &__frame-wrap { + background: hsl(var(--background)); + flex: 1 1 auto; + min-height: 0; + } + + &__frame { + border: 0; + display: block; + height: 100%; + width: 100%; + } +} +``` + +- [ ] **Step 6: Run Admin tests and typecheck** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next run typecheck +``` + +Expected: tests PASS and typecheck PASS. + +- [ ] **Step 7: Commit Task 7** + +```powershell +git -C D:\MyFiles\KT\Vue\kt-template-admin add apps/web-antdv-next/src/views/qqbot/account/napcat-webui +git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加NapCat WebUI二级页面" +``` + +--- + +### Task 8: Local End-to-End Smoke + +**Files:** +- No planned source changes. Validation fixes must be applied to the failing files from Tasks 1-7 and committed with the related repo. + +- [ ] **Step 1: Confirm repo types and package managers** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch +Get-Content D:\MyFiles\KT\Node\kt-template-online-api\.node-version +Get-Content D:\MyFiles\KT\Node\kt-template-online-api\package.json | Select-String '"packageManager"' +git -C D:\MyFiles\KT\Vue\kt-template-admin status --short --branch +Get-Content D:\MyFiles\KT\Vue\kt-template-admin\.node-version +Get-Content D:\MyFiles\KT\Vue\kt-template-admin\package.json | Select-String '"packageManager"' +``` + +Expected: both repos are Git; API uses pnpm 9.15.9; Admin uses pnpm 10.28.2. + +- [ ] **Step 2: Run focused API validation** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts test/apps/napcat-webui-gateway/session-store.spec.ts test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run build +``` + +Expected: focused tests PASS, typecheck PASS, build PASS. + +- [ ] **Step 3: Run focused Admin validation** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next run typecheck +``` + +Expected: focused tests PASS and typecheck PASS. + +- [ ] **Step 4: Start local API and Gateway** + +Use existing local env conventions. Start API and Gateway in bounded terminals: + +```powershell +Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoLogo','-Command','cd D:\MyFiles\KT\Node\kt-template-online-api; pnpm run start:dev *> .kt-workspace\logs\api-webui-gateway-api.log' +Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoLogo','-Command','cd D:\MyFiles\KT\Node\kt-template-online-api; pnpm run start:gateway:dev *> .kt-workspace\logs\api-webui-gateway-service.log' +``` + +Expected: API listens on `48085`; Gateway listens on `48086`. + +- [ ] **Step 5: Call the API session endpoint against local service** + +Use an existing local Admin token. If a token is not available, log into the local Admin UI and copy the token from the tracked local workflow. Then call: + +```powershell +curl.exe -sS -X POST "http://127.0.0.1:48085/qqbot/napcat/webui/session" -H "authorization: Bearer " -H "content-type: application/json" --data "{\"accountId\":\"\"}" +``` + +Expected: JSON contains `sessionId`, `iframeUrl`, `account`, and `container`, and does not contain `webuiToken`, `Credential`, `6100`, or Docker/NAS host paths. + +- [ ] **Step 6: Open the Admin route locally** + +Start Admin for the route smoke: + +```powershell +Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoLogo','-Command','cd D:\MyFiles\KT\Vue\kt-template-admin; pnpm -F @vben/web-antdv-next run dev *> .kt-workspace\logs\admin-webui-gateway.log' +``` + +Open: + +```text +http://127.0.0.1:5999/#/qqbot/account//napcat-webui +``` + +Expected: page renders the route console, creates a session, loads iframe shell, and revokes when navigating back to `/qqbot/account`. + +- [ ] **Step 7: Clean local processes** + +Stop Node processes started from these project paths: + +```powershell +Get-CimInstance Win32_Process | + Where-Object { $_.CommandLine -like '*kt-template-online-api*start:dev*' -or $_.CommandLine -like '*kt-template-online-api*start:gateway:dev*' -or $_.CommandLine -like '*kt-template-admin*web-antdv-next*dev*' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force } +``` + +- [ ] **Step 8: Commit local validation fixes** + +If validation required fixes, commit the touched repo separately: + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api status --short +git -C D:\MyFiles\KT\Vue\kt-template-admin status --short +``` + +Commit only files related to this feature. + +--- + +### Task 9: Documentation, Review, Push, Deploy, and Online Closure + +**Files:** +- Modify: `D:\MyFiles\KT\TASKS.md` + +- [ ] **Step 1: Update `TASKS.md`** + +Add a recent record with: + +```text +范围:API Gateway service、Admin WebUI 二级页面、K8s/Jenkins Gateway 发布。 +关键词:/qqbot/account/:accountId/napcat-webui、kt-napcat-webui-gateway、route-bound session、heartbeat/revoke、完整 WebUI 操作、token/Credential 不下发浏览器。 +验证:列出 API focused tests、Admin focused tests、typecheck/build、本地 iframe smoke、线上 Gateway health 与账号 1914728559 iframe smoke。 +``` + +- [ ] **Step 2: Run final local gates** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check +git -C D:\MyFiles\KT\Vue\kt-template-admin diff --check +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --changed-files +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project admin --changed-files +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run cleanup-history -- --dry-run +``` + +Expected: diff checks PASS, global-review findings=[], cleanup dry-run deleted=[]. + +- [ ] **Step 3: Commit remaining docs** + +```powershell +git -C D:\MyFiles\KT add TASKS.md +git -C D:\MyFiles\KT commit -m "docs: 记录NapCat WebUI Gateway实施" +``` + +- [ ] **Step 4: Push when explicitly requested** + +Push API and Admin only after user asks: + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api push origin main +git -C D:\MyFiles\KT\Vue\kt-template-admin push origin main +``` + +- [ ] **Step 5: Observe Jenkins/K8s** + +Use deploy observation for API: + +```powershell +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run deploy-observation -- --project api --job KT-Template/KT-Template-API/main --commit --execute +``` + +Manually verify Gateway because the current deploy-observation script targets API deployment by default: + +```powershell +$script = @' +set -eu +KUBECONFIG_PATH='/vol1/docker/kt-k8s/kubeconfig/kt-nas.jenkins.yaml' +kubectl --kubeconfig "$KUBECONFIG_PATH" -n kt-prod get deployment kt-napcat-webui-gateway -o wide +kubectl --kubeconfig "$KUBECONFIG_PATH" -n kt-prod get pod -l app=kt-napcat-webui-gateway +kubectl --kubeconfig "$KUBECONFIG_PATH" -n kt-prod logs -l app=kt-napcat-webui-gateway --tail=80 +'@ +$script | ssh nas "tr -d '\015' | bash -s" +``` + +Expected: API and Gateway deployments Running/Ready, restartCount 0, Gateway image tag matches the Jenkins build. + +- [ ] **Step 6: Configure Caddy/Admin route if not already routed** + +On Tencent Cloud Caddy, follow the stabilized Caddy rule: backup `/opt/nas-gateway/caddy/Caddyfile`, add `/napcat-webui/*` reverse proxy to the Gateway NodePort or service route, run `caddy validate`, reload, then verify: + +```powershell +curl.exe -I https://admin.kwitsukasa.top/napcat-webui/ +``` + +Expected: public route reaches Gateway and does not expose upstream host details. + +- [ ] **Step 7: Online functional smoke** + +Open: + +```text +https://admin.kwitsukasa.top/#/qqbot/account +``` + +For account `1914728559`: + +1. Click `WebUI`. +2. Confirm route becomes `/#/qqbot/account//napcat-webui`. +3. Confirm iframe loads the original NapCat WebUI shell. +4. Open a safe WebUI page such as login status or settings. +5. Confirm browser URL does not contain `webuiToken`, `Credential`, `6100`, Docker container IP, NAS host path, or SSH route. +6. Navigate back to account list. +7. Verify Gateway logs or audit table contain revoke or heartbeat-timeout cleanup. + +- [ ] **Step 8: Final report** + +Report: + +- API commit(s), Admin commit(s), root TASKS commit. +- Tests and typecheck/build evidence. +- Jenkins/K8s evidence. +- Online smoke evidence. +- Any remaining blocker with exact next command. + +--- + +## Self-Review + +- Spec coverage: covered Admin row action, second-level route, route-bound session lifecycle, independent Gateway process, full WebUI operation, no token/credential/browser leak, Redis session state, MySQL audit, K8s/Jenkins deployment, and online smoke. +- Placeholder scan: no forbidden placeholder words or cross-task shorthand remain in the plan body. +- Type consistency: all plan tasks use `QqBot:Account:WebUI`, `/qqbot/napcat/webui/session`, `/napcat-webui/session/:sessionId`, `kt-napcat-webui-gateway`, `48086`, and `QqbotNapcatWebuiGatewayService`. + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.md`. + +Two execution options: + +1. **Subagent-Driven (recommended)** - Dispatch a fresh subagent per task, review between tasks, fast iteration. +2. **Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints. + +Which approach? diff --git a/docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.zh-CN.md b/docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.zh-CN.md new file mode 100644 index 0000000..e07dd5f --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.zh-CN.md @@ -0,0 +1,839 @@ +# QQBot NapCat WebUI Gateway 实施计划 + +> **给 agent worker:** 必须使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 按任务执行本计划。步骤使用 checkbox(`- [ ]`)跟踪。 + +**目标:** 实现独立 NapCat WebUI Gateway 微服务,并在 Admin 二级页面中打开指定 QQBot 账号的原版 NapCat WebUI,支持完整操作和页面生命周期清理。 + +**架构:** API 继续作为 Admin 鉴权和 QQBot 账号绑定解析的权威,只负责创建短期 Gateway session。新增 `kt-napcat-webui-gateway` 进程负责 session 存储、一次性 bootstrap ticket、NapCat WebUI Credential 交换、HTTP/静态资源/API/WebSocket 代理和审计。Admin 打开 `/qqbot/account/:accountId/napcat-webui`,页面 mounted 创建 session,mounted 期间 heartbeat,路由离开时 revoke。 + +**技术栈:** NestJS 11、Express adapter、TypeORM/MySQL、Redis via `ioredis`、`http-proxy-middleware` 代理 Express/WebSocket、Vue 3 TSX、Vben Admin、antdv-next、K8s、Jenkins、Caddy/Admin 域路由。 + +--- + +## 来源参考 + +- 设计文档:`docs/superpowers/specs/2026-06-24-qqbot-napcat-webui-gateway-design.md` +- 中文设计:`docs/superpowers/specs/2026-06-24-qqbot-napcat-webui-gateway-design.zh-CN.md` +- `http-proxy-middleware` 支持 Express proxy middleware 和 WebSocket upgrade: +- Redis 官方 Node 入门文档列出 `ioredis`,适合 Redis TTL 租约: + +## 范围检查 + +这是一条完整可交付链路,虽然跨 API、Gateway、Admin 和部署,但不能拆成互不依赖的计划。Admin 页面没有 API session 不能加载,API session 没有 Gateway 不能 smoke,Gateway 没有 Admin 生命周期和部署路由也不能安全上线。 + +## 文件结构 + +### API 仓库:`D:\MyFiles\KT\Node\kt-template-online-api` + +- 修改 `package.json` 和 `pnpm-lock.yaml`:加入 `ioredis`、`http-proxy-middleware` 和 Gateway 启动脚本。 +- 新增 `src/apps/napcat-webui-gateway/main.ts`:Gateway 独立 Nest bootstrap,端口 `48086`。 +- 新增 `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts`:Gateway module,导入配置、日志、TypeORM 和 Gateway provider/controller。 +- 新增 `src/apps/napcat-webui-gateway/config/napcat-webui-gateway-config.service.ts`:读取 Gateway env 和安全默认值。 +- 新增 `src/apps/napcat-webui-gateway/domain/napcat-webui-gateway.types.ts`:session、audit、target、proxy 类型。 +- 新增 `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-redis.store.ts`:Redis session/ticket store。 +- 新增 `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts`:一次性 bootstrap ticket。 +- 新增 `src/apps/napcat-webui-gateway/infrastructure/napcat-webui-credential.client.ts`:服务端 WebUI token 换 Credential。 +- 新增 `src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service.ts`:HTTP、header、redirect、cookie、WebSocket 代理。 +- 新增 `src/apps/napcat-webui-gateway/application/napcat-webui-gateway-session.service.ts`:create、active、heartbeat、revoke、expire、同账号并发策略。 +- 新增 `src/apps/napcat-webui-gateway/presentation/internal-session.controller.ts`:Gateway 内部服务接口。 +- 新增 `src/apps/napcat-webui-gateway/presentation/public-webui.controller.ts`:bootstrap 和公开 iframe/proxy 入口。 +- 新增 `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.dto.ts`:Admin-facing DTO。 +- 新增 `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.controller.ts`:`/qqbot/napcat/webui` Admin 接口。 +- 新增 `src/modules/qqbot/napcat/webui-gateway/application/qqbot-napcat-webui-gateway.service.ts`:账号鉴权、容器解析、Gateway client 编排。 +- 新增 `src/modules/qqbot/napcat/webui-gateway/infrastructure/qqbot-napcat-webui-gateway.client.ts`:Gateway 内部 HTTP client。 +- 新增 `src/modules/qqbot/napcat/webui-gateway/infrastructure/persistence/napcat-webui-gateway-audit.entity.ts`:MySQL audit entity。 +- 修改 `src/modules/qqbot/napcat/qqbot-napcat.module.ts`:注册 controller、service、client、audit entity。 +- 修改 `sql/qqbot-init.sql` 和 `sql/refactor-v3/01-seed-core.sql`:加入 `QqBot:Account:WebUI` hidden route/menu 和按钮权限。 +- 修改 `sql/refactor-v3/99-verify.sql`:校验新权限和审计表。 +- 新增 `dockerfile.gateway`:生产镜像入口 `dist/apps/napcat-webui-gateway/main`。 +- 修改 `Jenkinsfile`:构建、推送和部署 API 镜像与 Gateway 镜像。 +- 修改 `k8s/prod/api.yaml`:新增 Gateway Deployment/Service,给 API 增加 Gateway base URL/public base URL/internal secret。 +- 修改 `README.md` 和 `API.md`:记录 Gateway env、路由和验证命令。 + +### Admin 仓库:`D:\MyFiles\KT\Vue\kt-template-admin` + +- 修改 `apps/web-antdv-next/src/router/routes/modules/qqbot.ts`:新增隐藏二级 WebUI 路由。 +- 修改 `apps/web-antdv-next/src/api/qqbot/napcat.ts`:新增 WebUI session 类型和 caller。 +- 修改 `apps/web-antdv-next/src/views/qqbot/account/list.tsx`:新增 WebUI 行操作。 +- 新增 `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx`:远程控制台页面。 +- 新增 `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss`:主题化布局。 +- 新增 `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/useNapcatWebuiGatewaySession.ts`:页面生命周期 session。 +- 修改 `apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts`:保证 account list 不承载 WebUI 生命周期。 +- 新增 `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx`:页面生命周期测试。 +- 修改 `apps/web-antdv-next/src/api/qqbot/napcat.spec.ts`:WebUI session caller 测试。 + +--- + +### Task 1:增加契约、权限种子和 RED 测试 + +**Files:** +- Create: `test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts` +- Modify: `sql/qqbot-init.sql` +- Modify: `sql/refactor-v3/01-seed-core.sql` +- Modify: `sql/refactor-v3/99-verify.sql` + +- [ ] **Step 1:写失败的结构测试** + +创建 `test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts`: + +```ts +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const repoRoot = resolve(__dirname, '../../../..'); + +const read = (path: string) => readFileSync(resolve(repoRoot, path), 'utf8'); + +describe('NapCat WebUI Gateway contract seeds', () => { + it('registers a dedicated Admin permission for full NapCat WebUI access', () => { + const coreSeed = read('sql/refactor-v3/01-seed-core.sql'); + const qqbotSeed = read('sql/qqbot-init.sql'); + + expect(coreSeed).toContain('QqBot:Account:WebUI'); + expect(coreSeed).toContain('QqBotAccountNapcatWebui'); + expect(qqbotSeed).toContain('QqBot:Account:WebUI'); + expect(qqbotSeed).toContain('QqBotAccountNapcatWebui'); + }); + + it('verifies the gateway audit table during full schema checks', () => { + const verifySql = read('sql/refactor-v3/99-verify.sql'); + + expect(verifySql).toContain('qqbot_napcat_webui_gateway_audit'); + expect(verifySql).toContain('QqBot:Account:WebUI'); + }); +}); +``` + +- [ ] **Step 2:运行测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts --runInBand +``` + +期望:失败,提示 `QqBot:Account:WebUI` 和 `qqbot_napcat_webui_gateway_audit` 不存在。 + +- [ ] **Step 3:增加 SQL 种子** + +在 `sql/qqbot-init.sql` 和 `sql/refactor-v3/01-seed-core.sql` 的 QQBot account 菜单附近加入: + +```sql +(2041700000000100411, 2041700000000100400, 'QqBotAccountNapcatWebui', '/qqbot/account/:accountId/napcat-webui', '/qqbot/account/napcat-webui/index', NULL, 'QqBot:Account:WebUI', 'menu', '{"activePath":"/qqbot/account","hideInMenu":true,"title":"NapCat WebUI"}', 1, 0), +(2041700000000120407, 2041700000000100402, 'QqBotAccountWebUI', NULL, NULL, NULL, 'QqBot:Account:WebUI', 'button', '{"title":"NapCat WebUI"}', 1, 0), +``` + +保持 ID 唯一,不改无关菜单。 + +- [ ] **Step 4:增加 schema 校验 SQL** + +在 `sql/refactor-v3/99-verify.sql` 按现有风格加入: + +```sql +SELECT 'qqbot_napcat_webui_gateway_audit table exists' AS check_name, + COUNT(*) AS matched +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name = 'qqbot_napcat_webui_gateway_audit'; + +SELECT 'QqBot Account WebUI permission exists' AS check_name, + COUNT(*) AS matched +FROM admin_menu +WHERE auth_code = 'QqBot:Account:WebUI'; +``` + +当前菜单表是 `admin_menu`,权限字段是 `auth_code`;校验 SQL 固定使用这两个名称。 + +- [ ] **Step 5:运行契约测试确认 GREEN** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts --runInBand +``` + +期望:PASS。 + +- [ ] **Step 6:提交 Task 1** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts sql/qqbot-init.sql sql/refactor-v3/01-seed-core.sql sql/refactor-v3/99-verify.sql +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat WebUI权限契约" +``` + +--- + +### Task 2:实现 API session 接口和审计实体 + +**Files:** +- Create: `test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.dto.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.controller.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/application/qqbot-napcat-webui-gateway.service.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/infrastructure/qqbot-napcat-webui-gateway.client.ts` +- Create: `src/modules/qqbot/napcat/webui-gateway/infrastructure/persistence/napcat-webui-gateway-audit.entity.ts` +- Modify: `src/modules/qqbot/napcat/qqbot-napcat.module.ts` +- Modify: `src/modules/qqbot/napcat/infrastructure/persistence/index.ts` + +- [ ] **Step 1:写失败的 API service 测试** + +创建 `test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts`,测试必须断言 Admin 响应不含 `webuiToken`、Credential、端口或容器拓扑,并且 WebUI 离线时不会调用 Gateway。 + +- [ ] **Step 2:运行测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts --runInBand +``` + +期望:失败,因为 service 和 DTO 尚不存在。 + +- [ ] **Step 3:创建 DTO** + +创建 `src/modules/qqbot/napcat/webui-gateway/contract/qqbot-napcat-webui-gateway.dto.ts`,包含 `QqbotNapcatWebuiSessionCreateDto` 和 `QqbotNapcatWebuiSessionResponseDto`,字段与英文计划一致。 + +- [ ] **Step 4:创建审计实体** + +创建 `NapcatWebuiGatewayAudit`,表名 `qqbot_napcat_webui_gateway_audit`,字段包含 `sessionId/adminUserId/accountId/selfId/containerId/eventType/clientIp/userAgent/detailJson/createTime`,禁止保存 token、Credential、密码、验证码或二维码内容。 + +- [ ] **Step 5:创建 Gateway 内部 client** + +创建 `QqbotNapcatWebuiGatewayClient`,提供 `createSession`、`heartbeat`、`revoke`,从 `NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL` 和 `NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET` 调 Gateway 内部接口,错误信息必须脱敏。 + +- [ ] **Step 6:创建 API service** + +创建 `QqbotNapcatWebuiGatewayService`: + +- `createSession()` 校验账号存在。 +- 解析主 NapCat container。 +- WebUI 离线或 token/port 不完整时拒绝。 +- 调 Gateway client 创建 session。 +- 返回安全字段:`account/container/sessionId/iframeUrl/expiresAt`。 + +在 `QqbotNapcatContainerService` 新增 `findPrimaryContainerByAccountId(accountId: string)`,补 JSDoc 和聚焦单测,然后由 `QqbotNapcatWebuiGatewayService` 调用该方法。 + +- [ ] **Step 7:创建 API controller** + +创建 `QqbotNapcatWebuiGatewayController`,路径: + +```text +POST /qqbot/napcat/webui/session +POST /qqbot/napcat/webui/session/:sessionId/heartbeat +POST /qqbot/napcat/webui/session/:sessionId/revoke +``` + +使用 `JwtAuthGuard`,通过 `vbenSuccess` 返回。 + +- [ ] **Step 8:注册 provider 和 entity** + +修改 `qqbot-napcat.module.ts` 与 `napcat/infrastructure/persistence/index.ts`,注册 controller、service、client 和 audit entity。 + +- [ ] **Step 9:运行 API 测试和类型检查** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +``` + +期望:测试 PASS,typecheck PASS。 + +- [ ] **Step 10:提交 Task 2** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/napcat/webui-gateway src/modules/qqbot/napcat/qqbot-napcat.module.ts src/modules/qqbot/napcat/infrastructure/persistence/index.ts test/modules/qqbot/napcat-webui-gateway +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat WebUI会话接口" +``` + +--- + +### Task 3:实现 Gateway App、Session Store 和 Bootstrap Ticket + +**Files:** +- Modify: `package.json` +- Modify: `pnpm-lock.yaml` +- Create: `test/apps/napcat-webui-gateway/session-store.spec.ts` +- Create: `src/apps/napcat-webui-gateway/main.ts` +- Create: `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` +- Create: `src/apps/napcat-webui-gateway/config/napcat-webui-gateway-config.service.ts` +- Create: `src/apps/napcat-webui-gateway/domain/napcat-webui-gateway.types.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-redis.store.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts` +- Create: `src/apps/napcat-webui-gateway/application/napcat-webui-gateway-session.service.ts` +- Create: `src/apps/napcat-webui-gateway/presentation/internal-session.controller.ts` + +- [ ] **Step 1:增加依赖** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api add ioredis http-proxy-middleware +``` + +期望:更新 `package.json` 和 `pnpm-lock.yaml`。保留已有 `ws`。 + +- [ ] **Step 2:增加启动脚本** + +在 `package.json` scripts 增加: + +```json +{ + "start:gateway:prod": "cross-env NODE_ENV=production node dist/apps/napcat-webui-gateway/main", + "start:gateway:dev": "ts-node -r tsconfig-paths/register src/apps/napcat-webui-gateway/main.ts" +} +``` + +API 仓库已有 `ts-node` 和 `tsconfig-paths`,dev script 固定使用 TypeScript entrypoint,生产脚本固定使用编译后的 `dist/apps/napcat-webui-gateway/main`。 + +- [ ] **Step 3:写 session 生命周期测试** + +创建 `test/apps/napcat-webui-gateway/session-store.spec.ts`,覆盖: + +- 同一 Admin 用户 + 同一账号创建新 session 时撤销旧 session。 +- heartbeat 延长 active session。 +- revoked session 不允许 heartbeat。 + +- [ ] **Step 4:运行测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/session-store.spec.ts --runInBand +``` + +期望:失败,因为 Gateway 类型和 service 不存在。 + +- [ ] **Step 5:创建 Gateway domain types** + +创建 `NapcatWebuiGatewaySessionStatus`、`NapcatWebuiGatewaySession`、`NapcatWebuiGatewaySessionStore`,字段与英文计划一致。 + +- [ ] **Step 6:实现 session service** + +创建 `NapcatWebuiGatewaySessionService`,包含 `create`、`markActive`、`heartbeat`、`revoke`、`requireProxySession`。每个方法必须有 JSDoc。`create()` 必须撤销同用户同账号旧 session。 + +- [ ] **Step 7:实现 Redis store 和 ticket service** + +Redis key: + +```text +napcat:webui:session:{sessionId} +napcat:webui:user-account:{adminUserId}:{accountId} +napcat:webui:ticket:{ticket} +``` + +ticket TTL 不超过 60 秒,redeem 时先删除 ticket 再返回 session id。 + +- [ ] **Step 8:增加 Gateway module 和内部 controller** + +内部路径: + +```text +POST /internal/sessions +POST /internal/sessions/:sessionId/heartbeat +POST /internal/sessions/:sessionId/revoke +GET /internal/health +``` + +所有 mutating internal call 必须校验 `x-kt-gateway-secret`。 + +- [ ] **Step 9:增加 Gateway bootstrap** + +创建 `src/apps/napcat-webui-gateway/main.ts`,监听 `NAPCAT_WEBUI_GATEWAY_PORT || 48086`,使用 `Logger`、`json`、`urlencoded`,写明 JSDoc。 + +- [ ] **Step 10:运行测试和类型检查** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/session-store.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +``` + +期望:测试 PASS,typecheck PASS。 + +- [ ] **Step 11:提交 Task 3** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add package.json pnpm-lock.yaml src/apps/napcat-webui-gateway test/apps/napcat-webui-gateway +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat WebUI Gateway会话服务" +``` + +--- + +### Task 4:增加 Credential 交换和 WebUI 代理 + +**Files:** +- Create: `test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/napcat-webui-credential.client.ts` +- Create: `src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service.ts` +- Create: `src/apps/napcat-webui-gateway/presentation/public-webui.controller.ts` +- Modify: `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` + +- [ ] **Step 1:写代理重写测试** + +创建 `proxy-rewrite.spec.ts`,覆盖: + +- 禁止 `https://evil.test/api`。 +- 禁止 `../api/auth/login`。 +- `api/QQLogin/CheckLoginStatus` 规范化成 `/api/QQLogin/CheckLoginStatus`。 +- `Location: /webui/login` 重写到 `/napcat-webui/session/:sessionId/webui/webui/login`。 +- `Set-Cookie` 的 Path 改写到当前 session。 + +- [ ] **Step 2:运行测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts --runInBand +``` + +期望:失败,因为 proxy helper 不存在。 + +- [ ] **Step 3:实现 Credential client** + +按现有 `NapcatWebuiHttpClient` 契约实现:`sha256(webuiToken + ".napcat")`,POST `/api/auth/login`,每个 session 缓存 Credential 到 revoke/expire。不要记录 token、hash 或 Credential。 + +- [ ] **Step 4:实现 proxy helper** + +导出 `sanitizeGatewayProxyPath`、`rewriteNapcatLocationHeader`、`rewriteNapcatSetCookieHeader`,逻辑与英文计划一致。 + +- [ ] **Step 5:实现 proxy service** + +使用 `createProxyMiddleware`: + +```ts +{ + changeOrigin: true, + secure: false, + ws: true, + selfHandleResponse: false, +} +``` + +代理前必须解析 session,拒绝非 active/created session,换取 Credential,注入 `Authorization: Bearer `,删除浏览器传入的 API/Admin cookies,不允许浏览器改变 target。 + +- [ ] **Step 6:实现公开 controller** + +公开路径: + +```text +GET /napcat-webui/session/:sessionId/bootstrap +ALL /napcat-webui/session/:sessionId/webui/* +``` + +bootstrap 兑换一次性 ticket,设置 HttpOnly session cookie,跳转到 `/napcat-webui/session/:sessionId/webui/webui`。 + +- [ ] **Step 7:运行测试和类型检查** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/apps/napcat-webui-gateway/session-store.spec.ts test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +``` + +期望:测试 PASS,typecheck PASS。 + +- [ ] **Step 8:提交 Task 4** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add src/apps/napcat-webui-gateway test/apps/napcat-webui-gateway +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 代理NapCat WebUI流量" +``` + +--- + +### Task 5:接入构建、Docker、K8s 和 API Gateway env + +**Files:** +- Create: `test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts` +- Create: `dockerfile.gateway` +- Modify: `Jenkinsfile` +- Modify: `k8s/prod/api.yaml` +- Modify: `README.md` +- Modify: `API.md` + +- [ ] **Step 1:写部署结构测试** + +创建 `gateway-deployment.spec.ts`,断言: + +- `dockerfile.gateway` 包含 `dist/apps/napcat-webui-gateway/main` 和 `EXPOSE 48086`。 +- `k8s/prod/api.yaml` 包含 `kt-napcat-webui-gateway`、`containerPort: 48086`、`NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET`、`NAPCAT_WEBUI_GATEWAY_REDIS_HOST`。 +- `Jenkinsfile` 包含 `GATEWAY_IMAGE_NAME`、`dockerfile.gateway`、`kt-napcat-webui-gateway`。 + +- [ ] **Step 2:运行测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand +``` + +期望:失败,因为部署文件尚未接入。 + +- [ ] **Step 3:新增 `dockerfile.gateway`** + +以现有 `dockerfile` 为基线,改: + +```dockerfile +ENV APP_PORT=48086 +ENV LOG_APP_NAME=kt-napcat-webui-gateway +EXPOSE 48086 +CMD ["node", "dist/apps/napcat-webui-gateway/main"] +``` + +- [ ] **Step 4:修改 Jenkins** + +新增 `GATEWAY_IMAGE_NAME` 参数,计算 `GATEWAY_DOCKER_IMAGE` 和 `GATEWAY_DOCKER_IMAGE_LATEST`,Docker Build 阶段构建 `dockerfile.gateway`,Docker Push 阶段推送 Gateway 镜像,K8s Deploy 阶段对 API 和 Gateway 两个 Deployment 分别 set image 和 rollout status。 + +- [ ] **Step 5:修改 K8s manifest** + +在 `k8s/prod/api.yaml` 中新增 `kt-napcat-webui-gateway` Deployment/Service,容器端口 `48086`,Redis 指向 `kt-qqbot-plugin-redis:6379`,Gateway env secret 复用 `kt-template-online-api-env`。给 API Deployment 增加: + +```yaml +- name: NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL + value: http://kt-napcat-webui-gateway:48086 +- name: NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL + value: /napcat-webui +``` + +`NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET` 只从线上私有 env secret 读取,不写入 Git。 + +- [ ] **Step 6:更新 README/API 文档** + +记录 Gateway env、端口、公开路由、内部路由、验证命令和“浏览器不出现 token/Credential/容器端口”的验收条件。 + +- [ ] **Step 7:运行部署测试和构建检查** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run build +``` + +期望:测试 PASS,typecheck PASS,build PASS,`dist/apps/napcat-webui-gateway/main.js` 存在。 + +- [ ] **Step 8:提交 Task 5** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api add dockerfile.gateway Jenkinsfile k8s/prod/api.yaml README.md API.md test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts +git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 部署NapCat WebUI Gateway" +``` + +--- + +### Task 6:增加 Admin API Client、路由和账号操作 + +**Files:** +- Modify: `apps/web-antdv-next/src/api/qqbot/napcat.ts` +- Modify: `apps/web-antdv-next/src/api/qqbot/napcat.spec.ts` +- Modify: `apps/web-antdv-next/src/router/routes/modules/qqbot.ts` +- Modify: `apps/web-antdv-next/src/views/qqbot/account/list.tsx` +- Modify: `apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts` + +- [ ] **Step 1:增加 Admin API RED 测试** + +在 `napcat.spec.ts` 中测试 `createQqbotNapcatWebuiSession`、`heartbeatQqbotNapcatWebuiSession`、`revokeQqbotNapcatWebuiSession` 调用正确 URL。 + +- [ ] **Step 2:增加 boundary RED 测试** + +在 `napcat-boundary.spec.ts` 中断言 `list.tsx` 只包含路由名 `QqBotAccountNapcatWebui`,不包含 create/heartbeat/revoke caller 和 iframe。 + +- [ ] **Step 3:运行 Admin 测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +``` + +期望:失败。 + +- [ ] **Step 4:增加 Admin API 函数** + +在 `napcat.ts` 增加 `WebuiGatewaySession` 类型,以及 create/heartbeat/revoke 三个函数,路径分别是: + +```text +/qqbot/napcat/webui/session +/qqbot/napcat/webui/session/:sessionId/heartbeat +/qqbot/napcat/webui/session/:sessionId/revoke +``` + +- [ ] **Step 5:增加隐藏路由** + +在 `qqbot.ts` 增加: + +```ts +{ + component: () => import('#/views/qqbot/account/napcat-webui'), + meta: { + activePath: '/qqbot/account', + hideInMenu: true, + title: 'NapCat WebUI', + }, + name: 'QqBotAccountNapcatWebui', + path: '/qqbot/account/:accountId/napcat-webui', +} +``` + +- [ ] **Step 6:增加账号行操作** + +在 `list.tsx` 的 `rowActions` 中加入 WebUI 动作: + +```ts +{ + disabled: (row) => !row.napcat?.containerName || getWebuiStatus(row) === 'offline', + key: 'napcatWebui', + label: 'WebUI', + onClick: openNapcatWebui, + permissionCodes: ['QqBot:Account:WebUI'], +} +``` + +`openNapcatWebui(row)` 通过 router push 到 `QqBotAccountNapcatWebui`。 + +- [ ] **Step 7:运行 Admin API 和 boundary 测试** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +``` + +期望:PASS。 + +- [ ] **Step 8:提交 Task 6** + +```powershell +git -C D:\MyFiles\KT\Vue\kt-template-admin add apps/web-antdv-next/src/api/qqbot/napcat.ts apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/router/routes/modules/qqbot.ts apps/web-antdv-next/src/views/qqbot/account/list.tsx apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加NapCat WebUI入口" +``` + +--- + +### Task 7:实现 Admin 二级页面和生命周期 composable + +**Files:** +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.tsx` +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/index.scss` +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/useNapcatWebuiGatewaySession.ts` +- Create: `apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx` + +- [ ] **Step 1:写页面生命周期测试** + +创建 `napcat-webui.spec.tsx`,测试 mounted 调 create session,iframe src 使用返回的 `iframeUrl`,unmount 调 revoke。 + +- [ ] **Step 2:运行测试确认 RED** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx +``` + +期望:失败,因为页面不存在。 + +- [ ] **Step 3:实现生命周期 composable** + +创建 `useNapcatWebuiGatewaySession.ts`,状态为 `idle/loading/ready/error/revoked`。`open()` 创建 session,ready 后每 20 秒 heartbeat,heartbeat 失败进入 error 并停止心跳,`revoke()` 停心跳并调用 revoke endpoint,`onBeforeUnmount` 自动 revoke。每个函数补 JSDoc。 + +- [ ] **Step 4:实现 route 页面** + +创建 `index.tsx`: + +- 单一稳定 root。 +- 顶部控制栏:返回账号列表、重新打开、关闭 session、展示 selfId/container。 +- `state=loading` 显示 Spin。 +- `state=error/revoked` 显示 Alert 和重新打开按钮。 +- `state=ready` 且有 `iframeUrl` 时显示 iframe。 + +- [ ] **Step 5:增加 SCSS** + +创建 `index.scss`,使用 `height: var(--vben-content-height)`、`overflow: hidden`、`hsl(var(--background))`、`hsl(var(--border))`,iframe 占满剩余高度。 + +- [ ] **Step 6:运行 Admin 测试和 typecheck** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next run typecheck +``` + +期望:测试 PASS,typecheck PASS。 + +- [ ] **Step 7:提交 Task 7** + +```powershell +git -C D:\MyFiles\KT\Vue\kt-template-admin add apps/web-antdv-next/src/views/qqbot/account/napcat-webui +git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加NapCat WebUI二级页面" +``` + +--- + +### Task 8:本地端到端 smoke + +**Files:** +- 无计划内源码改动。验证发现的问题必须回到 Task 1-7 对应失败文件中修复,并在对应仓库提交。 + +- [ ] **Step 1:确认仓库类型和包管理器** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch +Get-Content D:\MyFiles\KT\Node\kt-template-online-api\.node-version +Get-Content D:\MyFiles\KT\Node\kt-template-online-api\package.json | Select-String '"packageManager"' +git -C D:\MyFiles\KT\Vue\kt-template-admin status --short --branch +Get-Content D:\MyFiles\KT\Vue\kt-template-admin\.node-version +Get-Content D:\MyFiles\KT\Vue\kt-template-admin\package.json | Select-String '"packageManager"' +``` + +期望:两个仓库都是 Git;API 使用 pnpm 9.15.9;Admin 使用 pnpm 10.28.2。 + +- [ ] **Step 2:运行 API 聚焦验证** + +```powershell +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/webui-gateway-contract.spec.ts test/modules/qqbot/napcat-webui-gateway/api-session.service.spec.ts test/apps/napcat-webui-gateway/session-store.spec.ts test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run build +``` + +期望:测试 PASS,typecheck PASS,build PASS。 + +- [ ] **Step 3:运行 Admin 聚焦验证** + +```powershell +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vitest run apps/web-antdv-next/src/api/qqbot/napcat.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-boundary.spec.ts apps/web-antdv-next/src/views/qqbot/account/napcat-webui/napcat-webui.spec.tsx +pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next run typecheck +``` + +期望:测试 PASS,typecheck PASS。 + +- [ ] **Step 4:启动本地 API 和 Gateway** + +```powershell +Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoLogo','-Command','cd D:\MyFiles\KT\Node\kt-template-online-api; pnpm run start:dev *> .kt-workspace\logs\api-webui-gateway-api.log' +Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoLogo','-Command','cd D:\MyFiles\KT\Node\kt-template-online-api; pnpm run start:gateway:dev *> .kt-workspace\logs\api-webui-gateway-service.log' +``` + +期望:API 监听 `48085`,Gateway 监听 `48086`。 + +- [ ] **Step 5:本地调用 API session endpoint** + +使用本地 Admin token 调用: + +```powershell +curl.exe -sS -X POST "http://127.0.0.1:48085/qqbot/napcat/webui/session" -H "authorization: Bearer " -H "content-type: application/json" --data "{\"accountId\":\"\"}" +``` + +期望:返回 `sessionId/iframeUrl/account/container`,不包含 `webuiToken`、`Credential`、`6100`、Docker/NAS 路径。 + +- [ ] **Step 6:本地打开 Admin 路由** + +```powershell +Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoLogo','-Command','cd D:\MyFiles\KT\Vue\kt-template-admin; pnpm -F @vben/web-antdv-next run dev *> .kt-workspace\logs\admin-webui-gateway.log' +``` + +打开: + +```text +http://127.0.0.1:5999/#/qqbot/account//napcat-webui +``` + +期望:二级页面渲染,创建 session,iframe shell 加载,返回账号列表时 revoke。 + +- [ ] **Step 7:清理本地进程** + +```powershell +Get-CimInstance Win32_Process | + Where-Object { $_.CommandLine -like '*kt-template-online-api*start:dev*' -or $_.CommandLine -like '*kt-template-online-api*start:gateway:dev*' -or $_.CommandLine -like '*kt-template-admin*web-antdv-next*dev*' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force } +``` + +- [ ] **Step 8:提交验证修复** + +验证过程若发现缺陷,分别提交 API/Admin 仓库,只提交本功能相关文件。 + +--- + +### Task 9:文档、Review、Push、部署和线上闭环 + +**Files:** +- Modify: `D:\MyFiles\KT\TASKS.md` + +- [ ] **Step 1:更新 `TASKS.md`** + +记录范围、关键词和验证证据:API Gateway service、Admin WebUI 二级页面、K8s/Jenkins Gateway 发布、`/qqbot/account/:accountId/napcat-webui`、route-bound session、heartbeat/revoke、完整 WebUI 操作、token/Credential 不下发浏览器。 + +- [ ] **Step 2:运行最终本地门禁** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check +git -C D:\MyFiles\KT\Vue\kt-template-admin diff --check +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --changed-files +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project admin --changed-files +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run cleanup-history -- --dry-run +``` + +期望:diff check PASS,global-review findings=[],cleanup dry-run deleted=[]。 + +- [ ] **Step 3:提交剩余文档** + +```powershell +git -C D:\MyFiles\KT add TASKS.md +git -C D:\MyFiles\KT commit -m "docs: 记录NapCat WebUI Gateway实施" +``` + +- [ ] **Step 4:用户明确要求后再 push** + +```powershell +git -C D:\MyFiles\KT\Node\kt-template-online-api push origin main +git -C D:\MyFiles\KT\Vue\kt-template-admin push origin main +``` + +- [ ] **Step 5:观察 Jenkins/K8s** + +API 仍用 deploy observation: + +```powershell +pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run deploy-observation -- --project api --job KT-Template/KT-Template-API/main --commit --execute +``` + +Gateway 用 NAS 只读命令补充验证: + +```powershell +$script = @' +set -eu +KUBECONFIG_PATH='/vol1/docker/kt-k8s/kubeconfig/kt-nas.jenkins.yaml' +kubectl --kubeconfig "$KUBECONFIG_PATH" -n kt-prod get deployment kt-napcat-webui-gateway -o wide +kubectl --kubeconfig "$KUBECONFIG_PATH" -n kt-prod get pod -l app=kt-napcat-webui-gateway +kubectl --kubeconfig "$KUBECONFIG_PATH" -n kt-prod logs -l app=kt-napcat-webui-gateway --tail=80 +'@ +$script | ssh nas "tr -d '\015' | bash -s" +``` + +期望:API/Gateway Running/Ready,restartCount 0,Gateway 镜像 tag 与 Jenkins build 匹配。 + +- [ ] **Step 6:配置 Caddy/Admin 路由** + +按稳定 Caddy 规则备份 `/opt/nas-gateway/caddy/Caddyfile`,加入 `/napcat-webui/*` 到 Gateway 的反代,执行 `caddy validate` 和 reload,再验证: + +```powershell +curl.exe -I https://admin.kwitsukasa.top/napcat-webui/ +``` + +期望:公网路由能到 Gateway,响应不暴露 upstream host。 + +- [ ] **Step 7:线上功能 smoke** + +打开: + +```text +https://admin.kwitsukasa.top/#/qqbot/account +``` + +对账号 `1914728559`: + +1. 点击 `WebUI`。 +2. 确认路由进入 `/#/qqbot/account//napcat-webui`。 +3. 确认 iframe 加载原版 NapCat WebUI。 +4. 执行安全 WebUI 操作,例如读取登录状态或打开设置页。 +5. 确认浏览器 URL 不包含 `webuiToken`、`Credential`、`6100`、容器 IP、NAS 路径或 SSH 路由。 +6. 返回账号列表。 +7. 验证 Gateway 日志或审计表出现 revoke 或 heartbeat timeout cleanup。 + +- [ ] **Step 8:最终报告** + +报告 API commit、Admin commit、root TASKS commit、测试/typecheck/build 证据、Jenkins/K8s 证据、线上 smoke 证据和剩余 blocker。 + +--- + +## 自检 + +- 覆盖设计:已覆盖账号行入口、二级路由、页面生命周期 session、独立 Gateway、完整 WebUI 操作、浏览器不泄漏 token/Credential、Redis session、MySQL 审计、K8s/Jenkins 部署和线上 smoke。 +- 禁止词扫描:计划正文没有保留禁止词或跨任务简写。 +- 类型一致性:统一使用 `QqBot:Account:WebUI`、`/qqbot/napcat/webui/session`、`/napcat-webui/session/:sessionId`、`kt-napcat-webui-gateway`、`48086`、`QqbotNapcatWebuiGatewayService`。 + +## 执行交接 + +计划已保存到 `docs/superpowers/plans/2026-06-24-qqbot-napcat-webui-gateway-implementation-plan.zh-CN.md`。 + +两个执行选项: + +1. **Subagent-Driven(推荐)**:每个任务派一个新 subagent,任务之间主线程 review,迭代更快。 +2. **Inline Execution**:在当前会话用 executing-plans 执行,按批次检查。 + +你选哪个?