docs: 补充NapCat WebUI Gateway成熟库方案
This commit is contained in:
parent
571db8ecea
commit
788761698f
@ -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: <https://github.com/chimurai/http-proxy-middleware>
|
||||
- Redis documents `ioredis` as a Node Redis client option, and the package has async Redis primitives suitable for TTL leases: <https://redis.io/tutorials/develop/node/gettingstarted/>
|
||||
- `http-proxy-middleware` WebSocket recipe documents `ws: true`, manual `server.on('upgrade', proxy.upgrade)`, multiple targets, and path rewriting: <https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/websocket.md>
|
||||
- `@nestjs-modules/ioredis` provides Nest `RedisModule.forRoot` and `@InjectRedis()` over `ioredis`: <https://github.com/nest-modules/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: <https://github.com/tj/connect-redis>
|
||||
- VueUse `useIntervalFn` wraps `setInterval` with pause/resume controls and is already available in Admin: <https://vueuse.org/shared/useintervalfn/>
|
||||
|
||||
## 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<string>('NAPCAT_WEBUI_GATEWAY_REDIS_URL') ||
|
||||
`redis://${config.get<string>('NAPCAT_WEBUI_GATEWAY_REDIS_HOST') || '127.0.0.1'}:${config.get<number>('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<QqbotNapcatApi.WebuiGatewaySession>();
|
||||
const state = ref<NapcatWebuiSessionState>('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);
|
||||
|
||||
@ -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:<https://github.com/chimurai/http-proxy-middleware>
|
||||
- Redis 官方 Node 入门文档列出 `ioredis`,适合 Redis TTL 租约:<https://redis.io/tutorials/develop/node/gettingstarted/>
|
||||
- `http-proxy-middleware` WebSocket recipe 说明了 `ws: true`、手动 `server.on('upgrade', proxy.upgrade)`、多 target 和 path rewrite:<https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/websocket.md>
|
||||
- `@nestjs-modules/ioredis` 提供 Nest `RedisModule.forRoot` 和 `@InjectRedis()`,底层使用 `ioredis`:<https://github.com/nest-modules/ioredis>
|
||||
- 明确不使用 `connect-redis`,因为它是 Express session store,而 Gateway 需要 API 预创建 session、一次性 ticket、target 元数据、同账号并发撤销、Credential 缓存和审计事件:<https://github.com/tj/connect-redis>
|
||||
- VueUse `useIntervalFn` 是带 pause/resume 控制的 `setInterval` wrapper,Admin 已有该依赖:<https://vueuse.org/shared/useintervalfn/>
|
||||
|
||||
## 范围检查
|
||||
|
||||
@ -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<string>('NAPCAT_WEBUI_GATEWAY_REDIS_URL') ||
|
||||
`redis://${config.get<string>('NAPCAT_WEBUI_GATEWAY_REDIS_HOST') || '127.0.0.1'}:${config.get<number>('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 <credential>`,删除浏览器传入的 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 页面**
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user