feat: 增加 NapCat WebUI Gateway 部署配置

This commit is contained in:
sunlei 2026-06-24 15:13:02 +08:00
parent d0fbc13763
commit eb162ddba6
6 changed files with 234 additions and 3 deletions

8
API.md
View File

@ -379,6 +379,14 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
NapCat Chinese Desktop Runtime v8 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset且只有明确二维码过期或扫码确认窗口失效的 QR session failure 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v8` digest。 NapCat Chinese Desktop Runtime v8 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset且只有明确二维码过期或扫码确认窗口失效的 QR session failure 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v8` digest。
### NapCat WebUI Gateway
NapCat WebUI Gateway 是独立部署的内部代理服务,生产镜像由 `dockerfile.gateway` 打包 `dist/apps/napcat-webui-gateway/main.js`K8s 服务名为 `kt-napcat-webui-gateway`,端口 `48086`。API 侧只通过内部路由 `NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL` 创建、续期、撤销会话和交换一次性 ticket浏览器只访问公开前缀 `NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL` 下的代理页面、静态资源和 WebSocket 转发,不能直连 NapCat 容器 WebUI。
必需环境变量:`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`、`NAPCAT_WEBUI_GATEWAY_SESSION_TTL_MS`、`NAPCAT_WEBUI_GATEWAY_TICKET_TTL_MS`、`NAPCAT_WEBUI_GATEWAY_UPSTREAM_TIMEOUT_MS`。生产 `NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET` 只来自 Jenkins 私有 `.env.production` 生成的 `kt-template-online-api-env` Secret不写入 Git 或 manifest 字面量。
部署验收使用:`pnpm exec jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand`、`pnpm run typecheck`、`pnpm run build`、`Test-Path .\dist\apps\napcat-webui-gateway\main.js`、`git diff --check`。安全验收要求浏览器永远不接收 WebUI token、Credential、上游 URL/端口、Docker 拓扑、Redis 地址或内部 secret。
`napcat_login_event` 实体和表仅作为历史 schema 兼容保留watchdog 不再写入 quick/password 恢复事件,也不再依赖该表判断是否恢复登录。 `napcat_login_event` 实体和表仅作为历史 schema 兼容保留watchdog 不再写入 quick/password 恢复事件,也不再依赖该表判断是否恢复登录。
外发消息不直接抢发:后端会按 `QQBOT_SEND_GLOBAL_INTERVAL_MS`、`QQBOT_SEND_TARGET_INTERVAL_MS` 和 `QQBOT_SEND_JITTER_MS` 预约发送窗口,默认全局 2500ms、同会话 8000ms、抖动 0-800ms如果等待超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS`,本次发送会在下发前被拒绝。在线命令和自动回复规则会叠加运行时保底冷却,默认命令 5000ms、规则 30000ms复读机默认连续 4 次相同普通文本才触发,同一会话默认 10 分钟内只复读一次,并限制普通文本长度,减少自动行为被风控识别的概率。 外发消息不直接抢发:后端会按 `QQBOT_SEND_GLOBAL_INTERVAL_MS`、`QQBOT_SEND_TARGET_INTERVAL_MS` 和 `QQBOT_SEND_JITTER_MS` 预约发送窗口,默认全局 2500ms、同会话 8000ms、抖动 0-800ms如果等待超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS`,本次发送会在下发前被拒绝。在线命令和自动回复规则会叠加运行时保底冷却,默认命令 5000ms、规则 30000ms复读机默认连续 4 次相同普通文本才触发,同一会话默认 10 分钟内只复读一次,并限制普通文本长度,减少自动行为被风控识别的概率。

30
Jenkinsfile vendored
View File

@ -38,6 +38,7 @@ def requiredRuntimeEnvKeys() {
'FFLOGS_CLIENT_SECRET', 'FFLOGS_CLIENT_SECRET',
'QQBOT_PLUGIN_QUEUE_REDIS_HOST', 'QQBOT_PLUGIN_QUEUE_REDIS_HOST',
'QQBOT_PLUGIN_QUEUE_REDIS_PORT', 'QQBOT_PLUGIN_QUEUE_REDIS_PORT',
'NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET',
] ]
} }
@ -85,6 +86,7 @@ pipeline {
string(name: 'PUBLISH_BRANCH_PATTERN', defaultValue: '^(main|master|release/.+)$', description: '允许推送镜像的分支正则') string(name: 'PUBLISH_BRANCH_PATTERN', defaultValue: '^(main|master|release/.+)$', description: '允许推送镜像的分支正则')
string(name: 'DOCKER_REGISTRY', defaultValue: 'k3d-kt-registry.localhost:5000', description: '镜像仓库地址K8s 发布默认使用 fnOS NAS 上的 k3d 本地 registry') string(name: 'DOCKER_REGISTRY', defaultValue: 'k3d-kt-registry.localhost:5000', description: '镜像仓库地址K8s 发布默认使用 fnOS NAS 上的 k3d 本地 registry')
string(name: 'IMAGE_NAME', defaultValue: 'kt-template-online-api', description: 'Docker 镜像名称') string(name: 'IMAGE_NAME', defaultValue: 'kt-template-online-api', description: 'Docker 镜像名称')
string(name: 'GATEWAY_IMAGE_NAME', defaultValue: 'kt-napcat-webui-gateway', description: 'NapCat WebUI Gateway Docker 镜像名称')
string(name: 'IMAGE_TAG', defaultValue: '', description: '镜像标签,为空时使用 分支名-BUILD_NUMBERPR 使用源分支名') string(name: 'IMAGE_TAG', defaultValue: '', description: '镜像标签,为空时使用 分支名-BUILD_NUMBERPR 使用源分支名')
string(name: 'CONTAINER_NAME', defaultValue: 'kt-template-online-api', description: '业务容器名称') string(name: 'CONTAINER_NAME', defaultValue: 'kt-template-online-api', description: '业务容器名称')
string(name: 'CONTAINER_PORT', defaultValue: '48085', description: '宿主机映射端口,容器内固定使用 48085') string(name: 'CONTAINER_PORT', defaultValue: '48085', description: '宿主机映射端口,容器内固定使用 48085')
@ -133,6 +135,8 @@ pipeline {
env.DOCKER_REGISTRY_EFFECTIVE = registry ?: '' env.DOCKER_REGISTRY_EFFECTIVE = registry ?: ''
env.DOCKER_IMAGE = registry ? "${registry}/${params.IMAGE_NAME}:${env.IMAGE_TAG_FINAL}" : "${params.IMAGE_NAME}:${env.IMAGE_TAG_FINAL}" env.DOCKER_IMAGE = registry ? "${registry}/${params.IMAGE_NAME}:${env.IMAGE_TAG_FINAL}" : "${params.IMAGE_NAME}:${env.IMAGE_TAG_FINAL}"
env.DOCKER_IMAGE_LATEST = registry ? "${registry}/${params.IMAGE_NAME}:latest" : "${params.IMAGE_NAME}:latest" env.DOCKER_IMAGE_LATEST = registry ? "${registry}/${params.IMAGE_NAME}:latest" : "${params.IMAGE_NAME}:latest"
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"
// Agent 由 NAS 侧预先创建;这里仅确认 CI 所需的 Node/pnpm 环境可用。 // Agent 由 NAS 侧预先创建;这里仅确认 CI 所需的 Node/pnpm 环境可用。
if (isUnix()) { if (isUnix()) {
@ -183,6 +187,8 @@ pipeline {
Docker registry: ${env.DOCKER_REGISTRY_EFFECTIVE ?: '-'} Docker registry: ${env.DOCKER_REGISTRY_EFFECTIVE ?: '-'}
Docker image: ${env.DOCKER_IMAGE} Docker image: ${env.DOCKER_IMAGE}
Docker latest: ${env.DOCKER_IMAGE_LATEST} Docker latest: ${env.DOCKER_IMAGE_LATEST}
Gateway image: ${env.GATEWAY_DOCKER_IMAGE}
Gateway latest: ${env.GATEWAY_DOCKER_IMAGE_LATEST}
Deploy target: ${params.DEPLOY_TARGET} Deploy target: ${params.DEPLOY_TARGET}
Publish branch: ${env.IS_PUBLISH_BRANCH} Publish branch: ${env.IS_PUBLISH_BRANCH}
Run container: ${params.RUN_DOCKER_CONTAINER} Run container: ${params.RUN_DOCKER_CONTAINER}
@ -237,16 +243,24 @@ pipeline {
if (isUnix()) { if (isUnix()) {
runCmd(""" runCmd("""
test -f dist/main.js test -f dist/main.js
test -f dist/apps/napcat-webui-gateway/main.js
docker build -f dockerfile -t ${env.DOCKER_IMAGE} . docker build -f dockerfile -t ${env.DOCKER_IMAGE} .
docker build -f dockerfile.gateway -t ${env.GATEWAY_DOCKER_IMAGE} .
if [ '${env.DOCKER_IMAGE}' != '${env.DOCKER_IMAGE_LATEST}' ]; then if [ '${env.DOCKER_IMAGE}' != '${env.DOCKER_IMAGE_LATEST}' ]; then
docker tag ${env.DOCKER_IMAGE} ${env.DOCKER_IMAGE_LATEST} docker tag ${env.DOCKER_IMAGE} ${env.DOCKER_IMAGE_LATEST}
fi fi
if [ '${env.GATEWAY_DOCKER_IMAGE}' != '${env.GATEWAY_DOCKER_IMAGE_LATEST}' ]; then
docker tag ${env.GATEWAY_DOCKER_IMAGE} ${env.GATEWAY_DOCKER_IMAGE_LATEST}
fi
""".stripIndent()) """.stripIndent())
} else { } else {
runCmd('', """ runCmd('', """
if not exist dist\\main.js exit /b 1 if not exist dist\\main.js exit /b 1
if not exist dist\\apps\\napcat-webui-gateway\\main.js exit /b 1
docker build -f dockerfile -t ${env.DOCKER_IMAGE} . docker build -f dockerfile -t ${env.DOCKER_IMAGE} .
docker build -f dockerfile.gateway -t ${env.GATEWAY_DOCKER_IMAGE} .
if not "${env.DOCKER_IMAGE}"=="${env.DOCKER_IMAGE_LATEST}" docker tag ${env.DOCKER_IMAGE} ${env.DOCKER_IMAGE_LATEST} if not "${env.DOCKER_IMAGE}"=="${env.DOCKER_IMAGE_LATEST}" docker tag ${env.DOCKER_IMAGE} ${env.DOCKER_IMAGE_LATEST}
if not "${env.GATEWAY_DOCKER_IMAGE}"=="${env.GATEWAY_DOCKER_IMAGE_LATEST}" docker tag ${env.GATEWAY_DOCKER_IMAGE} ${env.GATEWAY_DOCKER_IMAGE_LATEST}
""".stripIndent()) """.stripIndent())
} }
} }
@ -267,9 +281,14 @@ pipeline {
runCmd(""" runCmd("""
docker push ${env.DOCKER_IMAGE} docker push ${env.DOCKER_IMAGE}
docker push ${env.DOCKER_IMAGE_LATEST} docker push ${env.DOCKER_IMAGE_LATEST}
docker push ${env.GATEWAY_DOCKER_IMAGE}
docker push ${env.GATEWAY_DOCKER_IMAGE_LATEST}
""".stripIndent()) """.stripIndent())
} else { } else {
runCmd("docker push ${env.DOCKER_IMAGE}") runCmd("""
docker push ${env.DOCKER_IMAGE}
docker push ${env.GATEWAY_DOCKER_IMAGE}
""".stripIndent())
} }
} }
} }
@ -295,6 +314,7 @@ pipeline {
def namespace = params.K8S_NAMESPACE?.trim() ?: 'kt-prod' def namespace = params.K8S_NAMESPACE?.trim() ?: 'kt-prod'
def deploymentName = params.K8S_DEPLOYMENT?.trim() ?: 'kt-template-online-api' def deploymentName = params.K8S_DEPLOYMENT?.trim() ?: 'kt-template-online-api'
def containerName = params.K8S_CONTAINER?.trim() ?: 'api' def containerName = params.K8S_CONTAINER?.trim() ?: 'api'
def gatewayDeploymentName = 'kt-napcat-webui-gateway'
def envSecret = params.K8S_ENV_SECRET?.trim() ?: 'kt-template-online-api-env' def envSecret = params.K8S_ENV_SECRET?.trim() ?: 'kt-template-online-api-env'
def rolloutTimeout = params.K8S_ROLLOUT_TIMEOUT?.trim() ?: '180s' def rolloutTimeout = params.K8S_ROLLOUT_TIMEOUT?.trim() ?: '180s'
def containerEnvFile = params.CONTAINER_ENV_FILE?.trim() def containerEnvFile = params.CONTAINER_ENV_FILE?.trim()
@ -338,10 +358,14 @@ pipeline {
kubectl ${kubeConfigArg} apply -f ${shellQuote(manifestFile)} kubectl ${kubeConfigArg} apply -f ${shellQuote(manifestFile)}
kubectl ${kubeConfigArg} ${namespaceArg} set image ${shellQuote("deployment/${deploymentName}")} ${shellQuote("${containerName}=${env.DOCKER_IMAGE}")} kubectl ${kubeConfigArg} ${namespaceArg} set image ${shellQuote("deployment/${deploymentName}")} ${shellQuote("${containerName}=${env.DOCKER_IMAGE}")}
kubectl ${kubeConfigArg} ${namespaceArg} set image ${shellQuote('deployment/kt-napcat-webui-gateway')} ${shellQuote("gateway=${env.GATEWAY_DOCKER_IMAGE}")}
kubectl ${kubeConfigArg} ${namespaceArg} annotate ${shellQuote("deployment/${deploymentName}")} \\ kubectl ${kubeConfigArg} ${namespaceArg} annotate ${shellQuote("deployment/${deploymentName}")} \\
${shellQuote("kubernetes.io/change-cause=${changeCause}")} --overwrite ${shellQuote("kubernetes.io/change-cause=${changeCause}")} --overwrite
kubectl ${kubeConfigArg} ${namespaceArg} annotate ${shellQuote('deployment/kt-napcat-webui-gateway')} \\
${shellQuote("kubernetes.io/change-cause=${changeCause}")} --overwrite
kubectl ${kubeConfigArg} ${namespaceArg} rollout status ${shellQuote("deployment/${deploymentName}")} --timeout=${shellQuote(rolloutTimeout)} kubectl ${kubeConfigArg} ${namespaceArg} rollout status ${shellQuote("deployment/${deploymentName}")} --timeout=${shellQuote(rolloutTimeout)}
kubectl ${kubeConfigArg} ${namespaceArg} get pod,svc -l app=${shellQuote(deploymentName)} kubectl ${kubeConfigArg} ${namespaceArg} rollout status ${shellQuote('deployment/kt-napcat-webui-gateway')} --timeout=${shellQuote(rolloutTimeout)}
kubectl ${kubeConfigArg} ${namespaceArg} get pod,svc -l ${shellQuote("app in (${deploymentName},${gatewayDeploymentName})")}
""".stripIndent()) """.stripIndent())
} }
} }
@ -407,7 +431,7 @@ pipeline {
post { post {
success { success {
archiveArtifacts artifacts: 'dist/**,package.json,pnpm-lock.yaml,dockerfile,k8s/**,ci/fnos-k8s/**', fingerprint: true, allowEmptyArchive: true archiveArtifacts artifacts: 'dist/**,package.json,pnpm-lock.yaml,dockerfile,dockerfile.gateway,k8s/**,ci/fnos-k8s/**', fingerprint: true, allowEmptyArchive: true
} }
} }
} }

View File

@ -79,6 +79,8 @@ Admin 环境总览面板使用 `ENV_DASHBOARD_*` 只读配置聚合 local-dev、
NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MACNapCat/OneBot 配置 hashKT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v8 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset且只有明确二维码过期或扫码确认窗口失效的 QR session failure 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v8` digest。 NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MACNapCat/OneBot 配置 hashKT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v8 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset且只有明确二维码过期或扫码确认窗口失效的 QR session failure 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v8` digest。
NapCat WebUI Gateway 是独立运行的 NestJS 入口,生产镜像使用 `dockerfile.gateway` 打包 `dist/apps/napcat-webui-gateway/main.js` 并监听 `48086`。API 通过内部地址 `NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL=http://kt-napcat-webui-gateway:48086` 创建/续期/撤销 WebUI 会话Admin 浏览器只访问公开前缀 `NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL=/napcat-webui`。Gateway 运行时需要 `NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET`、`NAPCAT_WEBUI_GATEWAY_REDIS_HOST`、`NAPCAT_WEBUI_GATEWAY_REDIS_PORT`、`NAPCAT_WEBUI_GATEWAY_SESSION_TTL_MS`、`NAPCAT_WEBUI_GATEWAY_TICKET_TTL_MS`、`NAPCAT_WEBUI_GATEWAY_UPSTREAM_TIMEOUT_MS`;生产 secret 由 Jenkins 从私有 `.env.production` 重建到 `kt-template-online-api-env`,不得写入 Git。验收命令`pnpm exec jest --runTestsByPath test/modules/qqbot/napcat-webui-gateway/gateway-deployment.spec.ts --runInBand`、`pnpm run typecheck`、`pnpm run build`、`Test-Path .\dist\apps\napcat-webui-gateway\main.js`、`git diff --check`。安全边界:浏览器不得收到 WebUI token、Credential、上游 URL/端口、Docker 拓扑、Redis 地址或内部 secret。
## 启动 ## 启动
```bash ```bash

46
dockerfile.gateway Normal file
View File

@ -0,0 +1,46 @@
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
ENV TZ=Asia/Shanghai
ENV APP_PORT=48086
ENV LOG_LEVEL=info
ENV LOG_APP_NAME=kt-napcat-webui-gateway
ENV LOG_PRETTY=false
ENV LOKI_ENV=production
ENV LOKI_PUSH_ENDPOINT=/loki/api/v1/push
ENV LOKI_QUERY_ENDPOINT=/loki/api/v1/query_range
ENV LOKI_PUSH_TIMEOUT_MS=30000
ENV LOKI_QUERY_TIMEOUT_MS=10000
ENV LOKI_BATCH_INTERVAL_SECONDS=5
ENV LOKI_BATCH_MAX_BUFFER_SIZE=10000
ENV LOKI_QUERY_MAX_LIMIT=1000
ENV LOKI_SILENCE_ERRORS=true
ENV FFLOGS_BASE_URL=https://cn.fflogs.com
ENV FFLOGS_WEB_BASE_URL=https://cn.fflogs.com
ENV FFLOGS_GRAPHQL_URL=https://cn.fflogs.com/api/v2/client
ENV FFLOGS_TOKEN_URL=https://cn.fflogs.com/oauth/token
ENV FFLOGS_DEFAULT_SERVER_REGION=CN
ENV FFLOGS_REQUEST_TIMEOUT_MS=10000
COPY package.json pnpm-lock.yaml ./
RUN apt-get update \
&& apt-get install -y --no-install-recommends fontconfig fonts-noto-cjk openssh-client \
&& fc-cache -f \
&& rm -rf /var/lib/apt/lists/*
# 生产镜像只安装运行依赖dist 由 Jenkins Build stage 提前产出。
# 跳过安装阶段脚本,避免 NODE_ENV=production 时 devDependency 中的 husky 不存在导致 prepare 失败。
RUN npm install -g pnpm@9.15.9 --registry=https://registry.npmmirror.com \
&& pnpm config set registry https://registry.npmmirror.com \
&& pnpm install --prod --frozen-lockfile --ignore-scripts \
&& pnpm rebuild skia-canvas
# dist 由 Jenkins 的 Build stage 生成,这里只打包运行产物。
COPY dist ./dist
EXPOSE 48086
CMD ["node", "dist/apps/napcat-webui-gateway/main"]

View File

@ -42,6 +42,10 @@ spec:
value: kt:qqbot:plugin-task value: kt:qqbot:plugin-task
- name: BANGDREAM_TSUGU_CACHE_ROOT - name: BANGDREAM_TSUGU_CACHE_ROOT
value: /data/qqbot/plugins/bangdream/cache value: /data/qqbot/plugins/bangdream/cache
- name: NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL
value: http://kt-napcat-webui-gateway:48086
- name: NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL
value: /napcat-webui
# Jenkins 每次发布会从 Agent 私有 .env.production 重建这个 Secret。 # Jenkins 每次发布会从 Agent 私有 .env.production 重建这个 Secret。
envFrom: envFrom:
- secretRef: - secretRef:
@ -162,6 +166,87 @@ spec:
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata:
name: kt-napcat-webui-gateway
namespace: kt-prod
labels:
app: kt-napcat-webui-gateway
spec:
type: ClusterIP
selector:
app: kt-napcat-webui-gateway
ports:
- name: http
port: 48086
targetPort: 48086
---
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
strategy:
type: Recreate
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_INTERNAL_SECRET
valueFrom:
secretKeyRef:
name: kt-template-online-api-env
key: NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET
readinessProbe:
tcpSocket:
port: 48086
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
tcpSocket:
port: 48086
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata: metadata:
name: kt-qqbot-plugin-redis name: kt-qqbot-plugin-redis
namespace: kt-prod namespace: kt-prod

View File

@ -0,0 +1,66 @@
import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';
const REPO_ROOT = resolve(__dirname, '../../../..');
/**
*
* @param relativePath -
* @returns
*/
function readRepoFile(relativePath: string): string {
return readFileSync(resolve(REPO_ROOT, relativePath), 'utf8');
}
/**
*
* @param relativePath -
* @returns
*/
function expectRepoFile(relativePath: string): string {
const filePath = resolve(REPO_ROOT, relativePath);
expect(existsSync(filePath)).toBe(true);
return filePath;
}
describe('NapCat WebUI Gateway deployment configuration', () => {
it('packages the standalone gateway app in a dedicated runtime image', () => {
expectRepoFile('dockerfile.gateway');
const dockerfile = readRepoFile('dockerfile.gateway');
expect(dockerfile).toContain('dist/apps/napcat-webui-gateway/main');
expect(dockerfile).toContain('EXPOSE 48086');
expect(dockerfile).toContain('LOG_APP_NAME=kt-napcat-webui-gateway');
});
it('declares gateway API linkage and cluster runtime without literal secrets', () => {
const manifest = readRepoFile('k8s/prod/api.yaml');
expect(manifest).toContain('kt-napcat-webui-gateway');
expect(manifest).toContain('containerPort: 48086');
expect(manifest).toContain('NAPCAT_WEBUI_GATEWAY_REDIS_HOST');
expect(manifest).toContain('NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET');
expect(manifest).toContain('NAPCAT_WEBUI_GATEWAY_INTERNAL_BASE_URL');
expect(manifest).toContain('NAPCAT_WEBUI_GATEWAY_PUBLIC_BASE_URL');
expect(manifest).toMatch(
/name:\s*NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET[\s\S]*?valueFrom:[\s\S]*?secretKeyRef:/,
);
expect(manifest).not.toMatch(
/name:\s*NAPCAT_WEBUI_GATEWAY_INTERNAL_SECRET[\s\S]*?value:\s*["']?[^"'\s]+/,
);
});
it('builds, pushes and rolls out the gateway image through Jenkins', () => {
const jenkinsfile = readRepoFile('Jenkinsfile');
expect(jenkinsfile).toContain('GATEWAY_IMAGE_NAME');
expect(jenkinsfile).toContain('dockerfile.gateway');
expect(jenkinsfile).toContain('kt-napcat-webui-gateway');
expect(jenkinsfile).toContain('GATEWAY_DOCKER_IMAGE');
expect(jenkinsfile).toMatch(/docker push \$\{env\.GATEWAY_DOCKER_IMAGE\}/);
expect(jenkinsfile).toMatch(
/rollout status [\s\S]*deployment\/kt-napcat-webui-gateway/,
);
});
});