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 index c81addc..8af4a28 100644 --- 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 @@ -6,7 +6,7 @@ **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. +**Tech Stack:** NestJS 11, Express adapter, TypeORM/MySQL, Redis through `@nestjs-modules/ioredis` + `ioredis`, `http-proxy-middleware` for Express/WebSocket proxying, Vue 3 TSX, VueUse `useIntervalFn`, Vben Admin, antdv-next, K8s, Jenkins, Caddy/Admin domain route. --- @@ -15,7 +15,10 @@ - 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: +- `http-proxy-middleware` WebSocket recipe documents `ws: true`, manual `server.on('upgrade', proxy.upgrade)`, multiple targets, and path rewriting: +- `@nestjs-modules/ioredis` provides Nest `RedisModule.forRoot` and `@InjectRedis()` over `ioredis`: +- `connect-redis` is intentionally not used because it is an Express session store, while this Gateway needs API-created sessions, one-time tickets, target metadata, concurrent-session revocation, Credential cache, and audit events: +- VueUse `useIntervalFn` wraps `setInterval` with pause/resume controls and is already available in Admin: ## Scope Check @@ -25,9 +28,9 @@ This is one deployable workstream even though it spans API, Gateway, Admin, and ### 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. +- Modify `package.json` and `pnpm-lock.yaml`: add `@nestjs-modules/ioredis`, `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/napcat-webui-gateway.module.ts`: Gateway module imports config, logger, TypeORM, `RedisModule`, 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. @@ -572,10 +575,10 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat W Run: ```powershell -pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api add ioredis http-proxy-middleware +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api add @nestjs-modules/ioredis ioredis http-proxy-middleware ``` -Expected: `package.json` and `pnpm-lock.yaml` update. Keep `ws` unchanged because it already exists. +Expected: `package.json` and `pnpm-lock.yaml` update. Keep `ws` unchanged because it already exists. Do not add `connect-redis` because Gateway sessions are domain sessions, not Express login sessions. - [ ] **Step 2: Add package scripts** @@ -774,7 +777,26 @@ The `heartbeat()` implementation must reject `revoked`, `expired`, `failed`, and - [ ] **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: +Create `src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-redis.store.ts`. Inject Redis through `@nestjs-modules/ioredis`: + +```ts +import { Injectable } from '@nestjs/common'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import type Redis from 'ioredis'; + +@Injectable() +export class NapcatWebuiGatewayRedisStore + implements NapcatWebuiGatewaySessionStore +{ + /** + * Initializes the Redis-backed Gateway session store. + * @param redis Shared Gateway Redis client managed by Nest RedisModule. + */ + constructor(@InjectRedis() private readonly redis: Redis) {} +} +``` + +Use Redis `set(key, value, 'PX', ttlMs)`, `get`, and a secondary index key: ```text napcat:webui:session:{sessionId} @@ -791,7 +813,31 @@ Ticket TTL must be 60 seconds or less. Redemption deletes the ticket key before - [ ] **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: +Create `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` and `presentation/internal-session.controller.ts`. The module must use the community Redis module rather than a custom Redis provider: + +```ts +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RedisModule } from '@nestjs-modules/ioredis'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + RedisModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'single', + url: + config.get('NAPCAT_WEBUI_GATEWAY_REDIS_URL') || + `redis://${config.get('NAPCAT_WEBUI_GATEWAY_REDIS_HOST') || '127.0.0.1'}:${config.get('NAPCAT_WEBUI_GATEWAY_REDIS_PORT') || 6379}`, + }), + }), + ], +}) +export class NapcatWebuiGatewayModule {} +``` + +Internal controller paths: ```text POST /internal/sessions @@ -853,6 +899,7 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat W - 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/main.ts` - Modify: `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` - [ ] **Step 1: Write proxy rewrite tests** @@ -861,8 +908,8 @@ Create `test/apps/napcat-webui-gateway/proxy-rewrite.spec.ts`: ```ts import { + buildGatewayCookiePathRewrite, rewriteNapcatLocationHeader, - rewriteNapcatSetCookieHeader, sanitizeGatewayProxyPath, } from '../../../src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service'; @@ -887,12 +934,10 @@ describe('NapCat WebUI Gateway proxy rewriting', () => { ).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'); + it('delegates cookie path rewriting to http-proxy-middleware options', () => { + expect(buildGatewayCookiePathRewrite({ sessionId: 'session-1' })).toEqual({ + '*': '/napcat-webui/session/session-1', + }); }); }); ``` @@ -913,7 +958,7 @@ 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`: +Create pure helper exports in `src/apps/napcat-webui-gateway/infrastructure/proxy/napcat-webui-proxy.service.ts`. Keep cookie rewriting as a `http-proxy-middleware` option instead of rewriting `Set-Cookie` by hand: ```ts export function sanitizeGatewayProxyPath(rawPath: string): string { @@ -932,14 +977,10 @@ export function rewriteNapcatLocationHeader( 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}`, - ); +export function buildGatewayCookiePathRewrite(input: { sessionId: string }) { + return { + '*': `/napcat-webui/session/${input.sessionId}`, + }; } ``` @@ -950,6 +991,13 @@ Use `createProxyMiddleware` from `http-proxy-middleware` with: ```ts { changeOrigin: true, + cookiePathRewrite: buildGatewayCookiePathRewrite({ sessionId }), + on: { + proxyReq: handleProxyReq, + proxyReqWs: handleProxyReqWs, + proxyRes: handleProxyRes, + }, + pathRewrite: (_path, req) => sanitizeGatewayProxyPath(req.params[0] || ''), secure: false, ws: true, selfHandleResponse: false, @@ -967,6 +1015,28 @@ Before proxying: On first successful upstream response, call `sessionService.markActive(sessionId)`. +For WebSocket upgrade, do not tunnel frames through MQTT and do not hand-roll a WebSocket bridge. Expose a method on `NapcatWebuiProxyService` and bind HPM's upgrade handler from `main.ts`: + +```ts +/** + * Binds NapCat WebUI WebSocket upgrades to the same proxy middleware. + * @param server HTTP server created by Nest's Express adapter. + */ +bindWebSocketUpgrade(server: import('http').Server) { + server.on('upgrade', (req, socket, head) => { + if (!req.url?.startsWith('/napcat-webui/session/')) return; + this.proxy.upgrade(req, socket, head); + }); +} +``` + +Modify `src/apps/napcat-webui-gateway/main.ts` after `app.listen()`: + +```ts + const server = app.getHttpServer(); + app.get(NapcatWebuiProxyService).bindWebSocketUpgrade(server); +``` + - [ ] **Step 6: Implement public controller** Create `src/apps/napcat-webui-gateway/presentation/public-webui.controller.ts`: @@ -978,7 +1048,7 @@ 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`. +Proxy route delegates to `NapcatWebuiProxyService`. The proxy service remains responsible for path sanitization, session validation, Credential injection, HPM `cookiePathRewrite`, HPM HTTP proxying, and HPM WebSocket upgrade handling. - [ ] **Step 7: Run tests and typecheck** @@ -1445,6 +1515,7 @@ Expected: FAIL because the page does not exist. Create `useNapcatWebuiGatewaySession.ts` with: ```ts +import { useIntervalFn } from '@vueuse/core'; import { onBeforeUnmount, ref } from 'vue'; import { createQqbotNapcatWebuiSession, @@ -1465,42 +1536,43 @@ export function useNapcatWebuiGatewaySession(accountId: string) { const errorMessage = ref(''); const session = ref(); const state = ref('idle'); - let heartbeatTimer: number | undefined; + const { pause: pauseHeartbeat, resume: resumeHeartbeat } = useIntervalFn( + () => { + const sessionId = session.value?.sessionId; + if (!sessionId) return; + void heartbeatQqbotNapcatWebuiSession(sessionId).catch(() => { + errorMessage.value = 'NapCat WebUI 会话心跳失败,请重新打开'; + state.value = 'error'; + pauseHeartbeat(); + }); + }, + 20_000, + { immediate: false }, + ); + + /** + * Creates a Gateway session and starts the route-bound heartbeat. + */ async function open() { state.value = 'loading'; errorMessage.value = ''; try { session.value = await createQqbotNapcatWebuiSession(accountId); state.value = 'ready'; - startHeartbeat(); + resumeHeartbeat(); } catch (error: any) { errorMessage.value = error?.message || 'NapCat WebUI 会话创建失败'; state.value = 'error'; + pauseHeartbeat(); } } - 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; - } - + /** + * Revokes the current Gateway session and stops heartbeat traffic. + */ async function revoke() { - stopHeartbeat(); + pauseHeartbeat(); const sessionId = session.value?.sessionId; if (!sessionId) return; await revokeQqbotNapcatWebuiSession(sessionId).catch(() => undefined); 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 index e07dd5f..5b50c70 100644 --- 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 @@ -6,7 +6,7 @@ **架构:** 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 域路由。 +**技术栈:** NestJS 11、Express adapter、TypeORM/MySQL、Redis via `@nestjs-modules/ioredis` + `ioredis`、`http-proxy-middleware` 代理 Express/WebSocket、Vue 3 TSX、VueUse `useIntervalFn`、Vben Admin、antdv-next、K8s、Jenkins、Caddy/Admin 域路由。 --- @@ -15,7 +15,10 @@ - 设计文档:`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 租约: +- `http-proxy-middleware` WebSocket recipe 说明了 `ws: true`、手动 `server.on('upgrade', proxy.upgrade)`、多 target 和 path rewrite: +- `@nestjs-modules/ioredis` 提供 Nest `RedisModule.forRoot` 和 `@InjectRedis()`,底层使用 `ioredis`: +- 明确不使用 `connect-redis`,因为它是 Express session store,而 Gateway 需要 API 预创建 session、一次性 ticket、target 元数据、同账号并发撤销、Credential 缓存和审计事件: +- VueUse `useIntervalFn` 是带 pause/resume 控制的 `setInterval` wrapper,Admin 已有该依赖: ## 范围检查 @@ -25,9 +28,9 @@ ### API 仓库:`D:\MyFiles\KT\Node\kt-template-online-api` -- 修改 `package.json` 和 `pnpm-lock.yaml`:加入 `ioredis`、`http-proxy-middleware` 和 Gateway 启动脚本。 +- 修改 `package.json` 和 `pnpm-lock.yaml`:加入 `@nestjs-modules/ioredis`、`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/napcat-webui-gateway.module.ts`:Gateway module,导入配置、日志、TypeORM、`RedisModule` 和 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。 @@ -259,10 +262,10 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat W - [ ] **Step 1:增加依赖** ```powershell -pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api add ioredis http-proxy-middleware +pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api add @nestjs-modules/ioredis ioredis http-proxy-middleware ``` -期望:更新 `package.json` 和 `pnpm-lock.yaml`。保留已有 `ws`。 +期望:更新 `package.json` 和 `pnpm-lock.yaml`。保留已有 `ws`。不要加 `connect-redis`,因为 Gateway session 是领域 session,不是 Express 登录 session。 - [ ] **Step 2:增加启动脚本** @@ -303,7 +306,7 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/ - [ ] **Step 7:实现 Redis store 和 ticket service** -Redis key: +使用 `@nestjs-modules/ioredis` 的 `@InjectRedis()` 注入 Redis client,不自写 Redis provider。Redis key: ```text napcat:webui:session:{sessionId} @@ -315,6 +318,30 @@ ticket TTL 不超过 60 秒,redeem 时先删除 ticket 再返回 session id。 - [ ] **Step 8:增加 Gateway module 和内部 controller** +`napcat-webui-gateway.module.ts` 必须通过 `RedisModule.forRootAsync` 接入 Redis: + +```ts +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RedisModule } from '@nestjs-modules/ioredis'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + RedisModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'single', + url: + config.get('NAPCAT_WEBUI_GATEWAY_REDIS_URL') || + `redis://${config.get('NAPCAT_WEBUI_GATEWAY_REDIS_HOST') || '127.0.0.1'}:${config.get('NAPCAT_WEBUI_GATEWAY_REDIS_PORT') || 6379}`, + }), + }), + ], +}) +export class NapcatWebuiGatewayModule {} +``` + 内部路径: ```text @@ -355,6 +382,7 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat W - 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/main.ts` - Modify: `src/apps/napcat-webui-gateway/napcat-webui-gateway.module.ts` - [ ] **Step 1:写代理重写测试** @@ -365,7 +393,7 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 增加NapCat W - 禁止 `../api/auth/login`。 - `api/QQLogin/CheckLoginStatus` 规范化成 `/api/QQLogin/CheckLoginStatus`。 - `Location: /webui/login` 重写到 `/napcat-webui/session/:sessionId/webui/webui/login`。 -- `Set-Cookie` 的 Path 改写到当前 session。 +- `buildGatewayCookiePathRewrite({ sessionId })` 返回 `http-proxy-middleware` 的 `cookiePathRewrite` 配置,把 cookie path 限定到当前 session。 - [ ] **Step 2:运行测试确认 RED** @@ -381,7 +409,7 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/ - [ ] **Step 4:实现 proxy helper** -导出 `sanitizeGatewayProxyPath`、`rewriteNapcatLocationHeader`、`rewriteNapcatSetCookieHeader`,逻辑与英文计划一致。 +导出 `sanitizeGatewayProxyPath`、`rewriteNapcatLocationHeader`、`buildGatewayCookiePathRewrite`,逻辑与英文计划一致。Cookie path 改写交给 `http-proxy-middleware` 的 `cookiePathRewrite`,不要手写 `Set-Cookie` 字符串替换。 - [ ] **Step 5:实现 proxy service** @@ -390,6 +418,13 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/ ```ts { changeOrigin: true, + cookiePathRewrite: buildGatewayCookiePathRewrite({ sessionId }), + on: { + proxyReq: handleProxyReq, + proxyReqWs: handleProxyReqWs, + proxyRes: handleProxyRes, + }, + pathRewrite: (_path, req) => sanitizeGatewayProxyPath(req.params[0] || ''), secure: false, ws: true, selfHandleResponse: false, @@ -398,6 +433,13 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/ 代理前必须解析 session,拒绝非 active/created session,换取 Credential,注入 `Authorization: Bearer `,删除浏览器传入的 API/Admin cookies,不允许浏览器改变 target。 +WebSocket upgrade 必须仍走 `http-proxy-middleware`,不能通过 MQTT 搬运 WebUI 数据帧,也不要手写 WebSocket tunnel。`NapcatWebuiProxyService` 暴露 `bindWebSocketUpgrade(server)`,内部用 HPM 的 `proxy.upgrade(req, socket, head)`;`main.ts` 在 `app.listen()` 后调用: + +```ts +const server = app.getHttpServer(); +app.get(NapcatWebuiProxyService).bindWebSocketUpgrade(server); +``` + - [ ] **Step 6:实现公开 controller** 公开路径: @@ -407,7 +449,7 @@ GET /napcat-webui/session/:sessionId/bootstrap ALL /napcat-webui/session/:sessionId/webui/* ``` -bootstrap 兑换一次性 ticket,设置 HttpOnly session cookie,跳转到 `/napcat-webui/session/:sessionId/webui/webui`。 +bootstrap 兑换一次性 ticket,设置 HttpOnly session cookie,跳转到 `/napcat-webui/session/:sessionId/webui/webui`。Proxy route 委托给 `NapcatWebuiProxyService`,由该服务统一负责 path sanitize、session 校验、Credential 注入、HPM `cookiePathRewrite`、HTTP proxy 和 WebSocket upgrade。 - [ ] **Step 7:运行测试和类型检查** @@ -611,7 +653,7 @@ pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin --filter @vben/web-antdv-next vit - [ ] **Step 3:实现生命周期 composable** -创建 `useNapcatWebuiGatewaySession.ts`,状态为 `idle/loading/ready/error/revoked`。`open()` 创建 session,ready 后每 20 秒 heartbeat,heartbeat 失败进入 error 并停止心跳,`revoke()` 停心跳并调用 revoke endpoint,`onBeforeUnmount` 自动 revoke。每个函数补 JSDoc。 +创建 `useNapcatWebuiGatewaySession.ts`,状态为 `idle/loading/ready/error/revoked`。使用 Admin 已有的 `@vueuse/core` `useIntervalFn(callback, 20_000, { immediate: false })` 管理 heartbeat,不手写原生定时器。`open()` 创建 session,ready 后 `resumeHeartbeat()`;heartbeat 失败进入 error 并 `pauseHeartbeat()`;`revoke()` 先 `pauseHeartbeat()` 再调 revoke endpoint;`onBeforeUnmount` 自动 revoke。每个函数补 JSDoc。 - [ ] **Step 4:实现 route 页面**