Compare commits

..

No commits in common. "d9b3b9751a3ce34f853b9d98d59e62cdf2797ae4" and "89f68f6db0827731c798f5e50269df90bdb208c4" have entirely different histories.

17 changed files with 13 additions and 3749 deletions

15
API.md
View File

@ -377,25 +377,12 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
该接口只返回脱敏后的运行态证据,供 Admin 排查镜像、locale、shm、配置 hash、漂移状态、风险模式和 watchdog 巡检告警状态;不会返回 WebUI token、reverse WS token、QQ 登录密码、SSH 私钥或运行态密码环境。账号列表只挂载 `napcat.profileStatus`、`napcat.runtimeProfile` 等摘要字段不触发登录、重建或修复动作。watchdog 不执行登录恢复:遇到 QQ 登录态离线只记录离线原因并通知 `super`,登录恢复统一由 Admin 手动「更新登录」触发session behavior profile 只做冷启动、housekeeping、presence 和自动能力分阶段降载,不实现账号级每小时/每日累计发送预算。 该接口只返回脱敏后的运行态证据,供 Admin 排查镜像、locale、shm、配置 hash、漂移状态、风险模式和 watchdog 巡检告警状态;不会返回 WebUI token、reverse WS token、QQ 登录密码、SSH 私钥或运行态密码环境。账号列表只挂载 `napcat.profileStatus`、`napcat.runtimeProfile` 等摘要字段不触发登录、重建或修复动作。watchdog 不执行登录恢复:遇到 QQ 登录态离线只记录离线原因并通知 `super`,登录恢复统一由 Admin 手动「更新登录」触发session behavior profile 只做冷启动、housekeeping、presence 和自动能力分阶段降载,不实现账号级每小时/每日累计发送预算。
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。生产 K8s manifest 保留 `kt-napcat-desktop-cn:desktop-cn-v8` / `desktop-cn-v8` 安全默认值Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 只有非空时才覆盖 API Deployment env空值会保持 manifest/default env。运行时回滚应重新运行 Jenkins 并填入上一版镜像 digest/profile或清空两个 override 后重新部署回 manifest 默认值。 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。
API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,且 `fork-artifact.json` 必须包含完整 marker metadataupstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。NapCat base image 在 release evidence 中必须 pin 到 digest。API Jenkins 只做显式参数推广,不负责自动合并上游、自动构建运行时镜像或在 override 为空时隐式改写 NapCat env。
```powershell
node scripts/napcat-desktop-cn-stage-build.mjs `
--napcat-root D:\MyFiles\KT\GitHub\NapCatQQ `
--upstream-release-tag v4.8.0 `
--upstream-release-commit 0000000000000000000000000000000000000000 `
--napcat-base-image-digest mlikiowa/napcat-docker@sha256:0000000000000000000000000000000000000000000000000000000000000000 `
--jenkins-build-url https://jenkins.kwitsukasa.top/job/KT-NapCatQQ-Runtime-Release/1/
```
### NapCat WebUI Gateway ### 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 是独立部署的内部代理服务,生产镜像由 `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。
Gateway 只改写 NapCat HTML/JS/CSS 中需要浏览器直连的绝对根路径:`/webui/*`、`/api/*`、`/files/*` 和 `/plugin/*`。NapCat 文件管理的 `File` 路由属于 axios `baseURL="/api"` 下的 API 子路径,页面源码里的 `"/File/list"` 必须保持原样,由浏览器最终请求 `/api/File/list`;不能把 `/File/*` 当作独立静态根路径改写到 Gateway session 前缀,否则会形成 `/webui/api/napcat-webui/session/.../File/list` 并让文件管理拿到 HTML。
必需环境变量:`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 字面量。 必需环境变量:`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。 部署验收使用:`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。

15
Jenkinsfile vendored
View File

@ -100,8 +100,6 @@ pipeline {
string(name: 'K8S_CONTAINER', defaultValue: 'api', description: 'Deployment 内业务容器名称') string(name: 'K8S_CONTAINER', defaultValue: 'api', description: 'Deployment 内业务容器名称')
string(name: 'K8S_ENV_SECRET', defaultValue: 'kt-template-online-api-env', description: '由 .env.production 生成的 K8s Secret 名称') string(name: 'K8S_ENV_SECRET', defaultValue: 'kt-template-online-api-env', description: '由 .env.production 生成的 K8s Secret 名称')
string(name: 'K8S_ROLLOUT_TIMEOUT', defaultValue: '180s', description: 'kubectl rollout status 超时时间') string(name: 'K8S_ROLLOUT_TIMEOUT', defaultValue: '180s', description: 'kubectl rollout status 超时时间')
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', defaultValue: '', description: 'Verified NapCat runtime image to inject into API deployment; empty keeps manifest/default env')
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', defaultValue: '', description: 'Verified NapCat runtime profile version to inject into API deployment; empty keeps manifest/default env')
} }
environment { environment {
@ -339,8 +337,6 @@ pipeline {
def kubeConfigArg = "--kubeconfig ${shellQuote(kubeConfigFile)}" def kubeConfigArg = "--kubeconfig ${shellQuote(kubeConfigFile)}"
def namespaceArg = "-n ${shellQuote(namespace)}" def namespaceArg = "-n ${shellQuote(namespace)}"
def changeCause = "Jenkins ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.GIT_COMMIT ?: 'unknown'}" def changeCause = "Jenkins ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.GIT_COMMIT ?: 'unknown'}"
def napcatImageOverride = params.QQBOT_NAPCAT_IMAGE_OVERRIDE?.trim()
def napcatProfileOverride = params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE?.trim()
// 每次发布都从 Agent 私有 env 文件重建 Secret避免真实配置进入 Git。 // 每次发布都从 Agent 私有 env 文件重建 Secret避免真实配置进入 Git。
runCmd(""" runCmd("""
@ -371,17 +367,6 @@ 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} set image ${shellQuote('deployment/kt-napcat-webui-gateway')} ${shellQuote("gateway=${env.GATEWAY_DOCKER_IMAGE}")}
""".stripIndent())
if (napcatImageOverride) {
runCmd("kubectl ${kubeConfigArg} ${namespaceArg} set env ${shellQuote("deployment/${deploymentName}")} ${shellQuote("QQBOT_NAPCAT_IMAGE=${napcatImageOverride}")}")
}
if (napcatProfileOverride) {
runCmd("kubectl ${kubeConfigArg} ${namespaceArg} set env ${shellQuote("deployment/${deploymentName}")} ${shellQuote("QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION=${napcatProfileOverride}")}")
}
runCmd("""
set -e
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')} \\ kubectl ${kubeConfigArg} ${namespaceArg} annotate ${shellQuote('deployment/kt-napcat-webui-gateway')} \\

View File

@ -77,18 +77,7 @@ QQBot 插件定时任务由 manifest 的 `tasks` 声明,平台持久化到 `qq
Admin 环境总览面板使用 `ENV_DASHBOARD_*` 只读配置聚合 local-dev、NAS 线上、腾讯云和 r4se 状态。`ENV_DASHBOARD_ADMIN_LOCAL_URL` / `ENV_DASHBOARD_ADMIN_PUBLIC_URL` 只用于展示 Admin 本机与线上入口证据。HTTP 快照提供当前拓扑,后端 local/MQTT 事件总线通过 SSE 推送增量事件给 Admin前端不直连 MQTT也不轮询刷新。Jenkins、K8s、Tencent Cloud、Caddy、WireGuard、Mihomo/OpenClash 未配置时会显示 `unwired` 证据,不能渲染成健康假象;第一版不暴露重启、部署、迁移、容器重建、插件启停或代理切换等写操作。 Admin 环境总览面板使用 `ENV_DASHBOARD_*` 只读配置聚合 local-dev、NAS 线上、腾讯云和 r4se 状态。`ENV_DASHBOARD_ADMIN_LOCAL_URL` / `ENV_DASHBOARD_ADMIN_PUBLIC_URL` 只用于展示 Admin 本机与线上入口证据。HTTP 快照提供当前拓扑,后端 local/MQTT 事件总线通过 SSE 推送增量事件给 Admin前端不直连 MQTT也不轮询刷新。Jenkins、K8s、Tencent Cloud、Caddy、WireGuard、Mihomo/OpenClash 未配置时会显示 `unwired` 证据,不能渲染成健康假象;第一版不暴露重启、部署、迁移、容器重建、插件启停或代理切换等写操作。
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。`k8s/prod/api.yaml` 保留 `desktop-cn-v8` 安全默认值Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 仅在填写时通过 `kubectl set env` 推广已验证运行时镜像/profile空值会继续使用 manifest/default env。回滚时重新运行 Jenkins 并填入上一版 digest/profile或清空两个 override 后重新部署 manifest 默认值。 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。
运行时发布时API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,`fork-artifact.json` 必须带完整 marker metadata包括 upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。release evidence 里的 NapCat base image 必须用 digest pin。API Jenkins 只消费人工确认后的运行时推广参数,不自动合并上游、不自动构建隐藏镜像,也不在 override 为空时覆盖 K8s manifest 中的默认 env。
```powershell
node scripts/napcat-desktop-cn-stage-build.mjs `
--napcat-root D:\MyFiles\KT\GitHub\NapCatQQ `
--upstream-release-tag v4.8.0 `
--upstream-release-commit 0000000000000000000000000000000000000000 `
--napcat-base-image-digest mlikiowa/napcat-docker@sha256:0000000000000000000000000000000000000000000000000000000000000000 `
--jenkins-build-url https://jenkins.kwitsukasa.top/job/KT-NapCatQQ-Runtime-Release/1/
```
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。 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。

View File

@ -10,47 +10,29 @@ corepack pnpm --dir D:\MyFiles\KT\GitHub\NapCatQQ --filter napcat-webui-frontend
corepack pnpm --dir D:\MyFiles\KT\GitHub\NapCatQQ run build:shell corepack pnpm --dir D:\MyFiles\KT\GitHub\NapCatQQ run build:shell
``` ```
Resolve release evidence before staging. Production and release builds must use an immutable upstream base image digest, not an unpinned tag:
```powershell
docker pull mlikiowa/napcat-docker:latest
$napcatBaseImageDigest = docker image inspect mlikiowa/napcat-docker:latest --format '{{index .RepoDigests 0}}'
if (-not $napcatBaseImageDigest -or $napcatBaseImageDigest -notmatch '@sha256:') {
throw 'NapCat upstream image digest not found; release builds must use repo@sha256:... evidence.'
}
$upstreamReleaseTag = 'v4.8.100'
$upstreamReleaseCommit = '0123456789abcdef0123456789abcdef01234567'
$jenkinsBuildUrl = 'https://jenkins.example/job/NapCatQQ/123/'
```
Stage Docker build context: Stage Docker build context:
```powershell ```powershell
node scripts/napcat-desktop-cn-stage-build.mjs ` node scripts/napcat-desktop-cn-stage-build.mjs `
--napcat-root D:\MyFiles\KT\GitHub\NapCatQQ ` --napcat-root D:\MyFiles\KT\GitHub\NapCatQQ `
--out .kt-workspace\napcat-desktop-cn-build ` --out .kt-workspace\napcat-desktop-cn-build
--upstream-release-tag $upstreamReleaseTag `
--upstream-release-commit $upstreamReleaseCommit `
--napcat-base-image-digest $napcatBaseImageDigest `
--jenkins-build-url $jenkinsBuildUrl
``` ```
The staged context must contain `NapCat.Shell` and `ci/napcat-desktop-cn/fork-artifact.json`. The API repo does not commit `NapCat.Shell.zip`; it is rebuilt from the staged `NapCat.Shell` directory inside the Docker image. `fork-artifact.json` must carry complete marker metadata for the fork commit, upstream base commit, upstream release tag, upstream release commit, Jenkins build URL, `napcatMjsSha256`, dist SHA256, and `napcatBaseImageDigest`.
Build and verify: Build and verify:
```powershell ```powershell
$baseImage = docker image inspect mlikiowa/napcat-docker:latest --format '{{index .RepoDigests 0}}'
if (-not $baseImage) { throw 'NapCat upstream image digest not found; pull and inspect the image before building.' }
docker build ` docker build `
--build-arg NAPCAT_BASE_IMAGE=$napcatBaseImageDigest ` --build-arg NAPCAT_BASE_IMAGE=$baseImage `
-t kt-napcat-desktop-cn:desktop-cn-v8 ` -t kt-napcat-desktop-cn:desktop-cn-v8 `
-f .kt-workspace/napcat-desktop-cn-build/ci/napcat-desktop-cn/Dockerfile ` -f .kt-workspace/napcat-desktop-cn-build/ci/napcat-desktop-cn/Dockerfile `
.kt-workspace/napcat-desktop-cn-build .kt-workspace/napcat-desktop-cn-build
$name = "kt-napcat-v8-verify-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" $name = "kt-napcat-v6-verify-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
docker run -d --name $name kt-napcat-desktop-cn:desktop-cn-v8 docker run -d --name $name kt-napcat-desktop-cn:desktop-cn-v8
docker exec $name sh /ci/napcat-desktop-cn/verify.sh docker exec $name sh /ci/napcat-desktop-cn/verify.sh
docker rm -f $name docker rm -f $name
``` ```
Record the final image digest in `QQBOT_NAPCAT_IMAGE`, and keep the matching `fork-artifact.json` with the release evidence. For local Windows rehearsal, placeholder `$upstreamReleaseTag`, `$upstreamReleaseCommit`, and `$jenkinsBuildUrl` values are acceptable only if the image is not promoted. Record the final image digest in `QQBOT_NAPCAT_IMAGE`.

View File

@ -1,618 +0,0 @@
# NapCatQQ 上游同步与运行时发布流水线实施计划
> **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:** 把 NapCatQQ 上游 release 审计、Codex CLI 辅助审查、KT 派生运行时镜像构建、API 显式参数推广全部收敛到可验证的 ktWorkflow 自动化链路。
**Architecture:** `mcp/ktWorkflow` 作为自动化控制面负责确定性采集器、提示词、schema 校验、Codex CLI runner、artifact 归档、MCP tools 和 Jenkins/systemd 命令入口。API 仓库只负责运行时镜像构建契约和部署参数消费Jenkins/NAS 定时器只调用 ktWorkflow不复制审计算法。
**Tech Stack:** TypeScript ESM、MCP SDK、zod、Node `child_process`、Codex CLI `exec`、Jenkins Pipeline、Docker、K8s、NestJS API runtime config、Jest。
---
## 文件结构
### `D:\MyFiles\KT\mcp\ktWorkflow`
- 新增 `prompts/napcat/upstream-audit.md`:上游 release 风险分类提示词。
- 新增 `prompts/napcat/sync-candidate-review.md`:候选分支审查提示词。
- 新增 `prompts/napcat/runtime-release-readiness.md`:运行时镜像发布就绪审查提示词。
- 新增 `prompts/napcat/remote-dev-handoff.md`NAS 远程开发交接提示词。
- 新增 `prompts/napcat/schemas/upstream-audit.schema.json``codex exec --output-schema` 使用的 JSON Schema。
- 新增 `prompts/napcat/schemas/runtime-release-readiness.schema.json`:运行时发布就绪报告 schema。
- 新增 `src/tools/napcatAutomation.types.ts`zod schema、输入输出类型、分类和 reason code 常量。
- 新增 `src/tools/napcatAutomation.prompts.ts`:提示词路径解析和读取。
- 新增 `src/tools/napcatAutomation.ts`确定性采集器、Codex runner、artifact writer、MCP/CLI builder。
- 修改 `src/types.ts`、`src/core/cli.ts`、`src/registerTools.ts`、`src/server.ts`、`src/core/constants.ts`、`src/selfTest.ts`、`package.json`、`README.md`。
### `D:\MyFiles\KT\Node\kt-template-online-api`
- 修改 `scripts/napcat-desktop-cn-stage-build.mjs`:写入 upstream release、base image digest、Jenkins URL 等 marker。
- 修改 `test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts`:覆盖 marker 与默认镜像契约。
- 新增 `test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts`:覆盖 Jenkins/K8s runtime override。
- 修改 `Jenkinsfile`:增加 `QQBOT_NAPCAT_IMAGE_OVERRIDE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE`
- 修改 `k8s/prod/api.yaml`:保留安全默认值,由 Jenkins 在发布时显式覆盖。
- 修改 `README.md`、`API.md`:记录发布和回滚契约。
### `D:\MyFiles\KT\GitHub\NapCatQQ`
- 新增 `ci/kt-runtime-release.md`KT 运行时发布清单。
- 本计划不改 NapCat 登录源码;如果上游同步撞到登录 hot zone另起 NapCatQQ 专项计划。
### `D:\MyFiles\KT`
- 修改 `TASKS.md`:记录实施结果和验证证据。
---
### Task 1ktWorkflow NapCat 自动化契约
**Files:**
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\src\tools\napcatAutomation.types.ts`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\prompts\napcat\schemas\upstream-audit.schema.json`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\prompts\napcat\schemas\runtime-release-readiness.schema.json`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\selfTest.ts`
- [ ] **Step 1先写 RED self-test**
覆盖三件事:`manual-review` 分类能被解析、`HOT_ZONE_CHANGED` reason code 存在、`upstreamAuditOutputSchema` 能校验最小输出。
```ts
import {
napcatAutomationReasonCodes,
parseNapcatAutomationClassification,
upstreamAuditOutputSchema,
} from './tools/napcatAutomation.types.js';
const napcatAuditSchemaSmoke = upstreamAuditOutputSchema.safeParse({
classification: 'manual-review',
reasonCodes: ['HOT_ZONE_CHANGED'],
recommendedAction: 'request-human-review',
summary: 'Login hot-zone changed.',
});
if (!napcatAuditSchemaSmoke.success) {
throw new Error('NapCat upstream audit schema self-check failed');
}
if (!napcatAutomationReasonCodes.includes('HOT_ZONE_CHANGED')) {
throw new Error('NapCat reason-code list self-check failed');
}
if (parseNapcatAutomationClassification('manual-review') !== 'manual-review') {
throw new Error('NapCat classification parser self-check failed');
}
```
- [ ] **Step 2确认 RED**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
```
预期:因为 `napcatAutomation.types.js` 不存在而失败。
- [ ] **Step 3实现类型与 zod schema**
实现 `safe-candidate`、`manual-review`、`blocked` 三类分类,以及 `NO_UPSTREAM_CHANGE`、`HOT_ZONE_CHANGED`、`FORK_PATCH_OVERLAP`、`DRY_MERGE_CONFLICT`、`BUILD_GRAPH_CHANGED`、`METADATA_UNAVAILABLE`、`CODEX_SCHEMA_INVALID` 七个 reason code。新增函数必须写 JSDoc参数说明要解释来源和约束。
- [ ] **Step 4写 Codex CLI JSON Schema**
`upstream-audit.schema.json` 必须要求 `classification`、`reasonCodes`、`recommendedAction`、`summary`,并禁止额外字段;`runtime-release-readiness.schema.json` 必须要求 `sourceValidation`、`imageValidation`、`apiPromotionReadiness`、`onlineSmokeReadiness`、`rollbackPointer`。
- [ ] **Step 5确认 GREEN**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add prompts/napcat src/tools/napcatAutomation.types.ts src/selfTest.ts
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "feat: 定义NapCat自动化审计契约"
```
---
### Task 2ktWorkflow 提示词包与安全读取器
**Files:**
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\prompts\napcat\upstream-audit.md`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\prompts\napcat\sync-candidate-review.md`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\prompts\napcat\runtime-release-readiness.md`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\prompts\napcat\remote-dev-handoff.md`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\src\tools\napcatAutomation.prompts.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\selfTest.ts`
- [ ] **Step 1先写 RED self-test**
self-test 遍历所有提示词,要求每个 prompt 都包含安全边界:
```md
Do not edit files.
Do not run merge, commit, push, deploy, or Docker mutation commands.
Use only the supplied context packet.
Return JSON that matches the requested schema.
```
- [ ] **Step 2确认 RED**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
```
- [ ] **Step 3实现 prompt loader**
`loadNapcatAutomationPrompt(name)` 只允许读取 `prompts/napcat/${name}.md` 中的固定 prompt 名称,不能接受任意路径。函数必须有 JSDoc说明 `name` 来自稳定枚举,不是用户文件路径。
- [ ] **Step 4写四份提示词**
四份 prompt 分别服务:
- `upstream-audit.md`:只基于 context packet 做 release 风险分类。
- `sync-candidate-review.md`:审查示例候选分支 `kt/sync/v4.8.0`重点看登录态、二维码、WebUI auth、captcha/new-device。
- `runtime-release-readiness.md`拆开源码验证、镜像验证、API 推广、线上 smoke、回滚点。
- `remote-dev-handoff.md`:输出 NAS 远程开发交接,不让 Codex 自动改代码。
- [ ] **Step 5确认 GREEN**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add prompts/napcat src/tools/napcatAutomation.prompts.ts src/selfTest.ts
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "feat: 增加NapCat自动化提示词包"
```
---
### Task 3确定性上游审计采集器
**Files:**
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\src\tools\napcatAutomation.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\tools\napcatAutomation.types.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\selfTest.ts`
- [ ] **Step 1先写 hot-zone RED self-test**
构造上游和 KT patch 同时命中 `packages/napcat-core/login/runtime.ts` 的输入,要求分类为 `manual-review`reasonCodes 包含 `HOT_ZONE_CHANGED`
- [ ] **Step 2确认 RED**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
```
- [ ] **Step 3实现 hot-zone 分类器**
分类规则:
- dry merge 冲突 -> `blocked` + `DRY_MERGE_CONFLICT`
- 上游命中登录/二维码/WebUI/auth/OneBot/build hot zone -> `manual-review`
- 上游文件与 KT fork patch 文件重叠 -> `manual-review`
- 无 hot-zone、无重叠、无冲突 -> `safe-candidate`
- [ ] **Step 4生成只读审计命令**
`buildNapcatUpstreamAudit(execute=false)` 只能生成只读命令:`git fetch`、`git diff --name-only`、`git merge-tree`、`git range-diff`。不能包含 `git merge`、`git push`、`docker build`、`kubectl`。
- [ ] **Step 5确认 GREEN**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add src/tools/napcatAutomation.ts src/tools/napcatAutomation.types.ts src/selfTest.ts
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "feat: 增加NapCat上游审计采集器"
```
---
### Task 4Codex CLI Runner 与 Artifact 捕获
**Files:**
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\tools\napcatAutomation.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\selfTest.ts`
- [ ] **Step 1先写 Codex 命令安全 RED self-test**
测试 `buildNapcatCodexExecCommand()` 必须包含 `--ask-for-approval never`、`--json`、`--ephemeral`、`--output-schema`,且不能包含 `git push`、`kubectl`、`docker build`。
- [ ] **Step 2确认 RED**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
```
- [ ] **Step 3实现 Codex 命令封装**
命令固定走 NAS 形态:
```bash
CODEX_HOME=/vol1/docker/kt-codex/home/.codex \
codex exec \
--cd /vol1/docker/kt-codex/workspace/KT \
--profile automation \
--sandbox workspace-write \
--ask-for-approval never \
--json \
--ephemeral \
--output-schema /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json \
--output-last-message "$ARTIFACT_DIR/codex-upstream-audit.md" \
- < "$CONTEXT_PACKET"
```
- [ ] **Step 4实现 execute 模式**
`execute=true && useCodex=true` 时写入 context packet运行 Codex捕获 JSONL校验 zod schema。schema 失败时返回 `blocked` + `CODEX_SCHEMA_INVALID`,并保留原始输出 artifact。
- [ ] **Step 5确认 GREEN**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add src/tools/napcatAutomation.ts src/selfTest.ts
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "feat: 封装NapCat Codex审计执行器"
```
---
### Task 5MCP Tools 与 CLI Scripts
**Files:**
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\types.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\core\cli.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\registerTools.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\server.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\core\constants.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\package.json`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\README.md`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\selfTest.ts`
- [ ] **Step 1先写注册和 CLI parser RED self-test**
要求 `registeredToolNames` 包含 `kt_napcat_upstream_audit``parseNapcatUpstreamAuditCliArgs()` 能解析 `--execute`、`--use-codex`、`--artifact-root`。
- [ ] **Step 2确认 RED**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
```
- [ ] **Step 3注册五个 MCP tools**
- `kt_napcat_upstream_audit`
- `kt_napcat_sync_candidate_review`
- `kt_napcat_runtime_release_readiness`
- `kt_napcat_remote_dev_handoff`
- `kt_nas_codex_bootstrap_plan`
所有 input schema 都要有默认值,且 `execute` 默认 `false`
- [ ] **Step 4增加 package scripts**
```json
{
"napcat-upstream-audit": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-upstream-audit",
"napcat-sync-candidate-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-sync-candidate-review",
"napcat-runtime-release-readiness": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-runtime-release-readiness",
"nas-codex-bootstrap": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --nas-codex-bootstrap"
}
```
- [ ] **Step 5确认 GREEN**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run napcat-upstream-audit -- --artifact-root .kt-workspace/test-artifacts/napcat-upstream-sync
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add package.json README.md src
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "feat: 接入NapCat自动化MCP工具和CLI"
```
---
### Task 6NAS Codex Bootstrap 工具
**Files:**
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\tools\napcatAutomation.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\src\selfTest.ts`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\README.md`
- [ ] **Step 1先写 Linux 路径 RED self-test**
测试 dry-run 输出必须包含 `/vol1/docker/kt-codex/home/.codex`,且不能包含 `C:\Users`
- [ ] **Step 2确认 RED**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
```
- [ ] **Step 3实现 dry-run**
dry-run 只输出检查命令:
```bash
node --version
corepack --version
pnpm --version
git --version
docker --version
codex --version
test -d /vol1/docker/kt-codex/workspace/KT
test -d /vol1/docker/kt-codex/home/.codex
```
- [ ] **Step 4实现 execute 模式**
`execute=true` 可以创建目录和安装运行环境,但不能 push、merge、deploy也不能改业务仓库。敏感 Codex auth 只允许可信直连同步,不进入 Git、Jenkins artifact、Docker context 或日志。
- [ ] **Step 5确认 GREEN**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run nas-codex-bootstrap -- --dry-run
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add README.md src
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "feat: 增加NAS Codex环境引导工具"
```
---
### Task 7API 运行时 Artifact 元数据
**Files:**
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\napcat-desktop-cn-stage-build.mjs`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-desktop-cn-image.spec.ts`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\README.md`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\API.md`
- [ ] **Step 1先写 marker RED test**
`napcat-desktop-cn-image.spec.ts` 要断言 stage-build 脚本包含:
```ts
expect(script).toContain('upstreamReleaseTag');
expect(script).toContain('upstreamReleaseCommit');
expect(script).toContain('napcatBaseImageDigest');
expect(script).toContain('jenkinsBuildUrl');
```
- [ ] **Step 2确认 RED**
```powershell
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand
```
- [ ] **Step 3扩展 stage-build 参数和 marker**
新增参数:
- `--upstream-release-tag`
- `--upstream-release-commit`
- `--napcat-base-image-digest`
- `--jenkins-build-url`
写入 `fork-artifact.json`,和现有 `forkCommit`、`distSha256`、`napcatMjsSha256` 一起归档。
- [ ] **Step 4同步 README/API**
文档必须写清楚API 仓库不提交 `NapCat.Shell.zip`,生产镜像必须来自带完整 marker 的 staged contextbase image 必须是 digest。
- [ ] **Step 5确认 GREEN**
```powershell
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add scripts/napcat-desktop-cn-stage-build.mjs test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts README.md API.md
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 扩展NapCat运行时构建元数据"
```
---
### Task 8API Jenkins 运行时推广 Override
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-runtime-promotion.spec.ts`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\Jenkinsfile`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\k8s\prod\api.yaml`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\README.md`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\API.md`
- [ ] **Step 1先写 Jenkins/K8s RED test**
测试 Jenkinsfile 必须有两个参数:
- `QQBOT_NAPCAT_IMAGE_OVERRIDE`
- `QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE`
并且 K8s deploy 阶段必须通过 `kubectl set env` 注入 `QQBOT_NAPCAT_IMAGE``QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION`
- [ ] **Step 2确认 RED**
```powershell
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts --runTestsByPath --runInBand
```
- [ ] **Step 3增加 Jenkins 参数**
两个参数默认空字符串。为空时继续使用 manifest 默认值;非空时才覆盖运行时 env。
- [ ] **Step 4K8s deploy 阶段注入 env**
`kubectl set image` 之后执行:
```groovy
def napcatImageOverride = params.QQBOT_NAPCAT_IMAGE_OVERRIDE?.trim()
def napcatProfileOverride = params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE?.trim()
if (napcatImageOverride) {
runCmd("kubectl ${kubeConfigArg} ${namespaceArg} set env ${shellQuote("deployment/${deploymentName}")} ${shellQuote("QQBOT_NAPCAT_IMAGE=${napcatImageOverride}")}")
}
if (napcatProfileOverride) {
runCmd("kubectl ${kubeConfigArg} ${namespaceArg} set env ${shellQuote("deployment/${deploymentName}")} ${shellQuote("QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION=${napcatProfileOverride}")}")
}
```
- [ ] **Step 5确认 GREEN**
```powershell
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts --runTestsByPath --runInBand
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
```
- [ ] **Step 6提交**
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add Jenkinsfile k8s/prod/api.yaml README.md API.md test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 支持NapCat运行时镜像参数推广"
```
---
### Task 9NapCatQQ 发布清单与 Jenkins 入口模板
**Files:**
- Create: `D:\MyFiles\KT\GitHub\NapCatQQ\ci\kt-runtime-release.md`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\ci\jenkins\KT-NapCatQQ-Upstream-Sync.Jenkinsfile`
- Create: `D:\MyFiles\KT\mcp\ktWorkflow\ci\jenkins\KT-NapCatQQ-Runtime-Release.Jenkinsfile`
- Modify: `D:\MyFiles\KT\mcp\ktWorkflow\README.md`
- [ ] **Step 1写 NapCatQQ 发布清单**
清单必须包含origin 指向 KT 可写 fork/mirror、upstream 指向 `https://github.com/NapNeko/NapCatQQ.git`、运行 `pnpm install --frozen-lockfile`、聚焦登录测试、typecheck、build:webui/build:shell/build:framework、Docker 构建只走 ktWorkflow/Jenkins、不自动合并上游。
- [ ] **Step 2写上游审计 Jenkins 模板**
模板只调用:
```groovy
sh '''
set -e
pnpm --dir /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow run napcat-upstream-audit -- \
--execute \
--use-codex \
--artifact-root /vol1/docker/kt-codex/artifacts/napcat-upstream-sync
'''
```
- [ ] **Step 3写运行时发布 Jenkins 模板**
模板调用 ktWorkflow readiness 和 API promotion 命令,不内联审计算法,不自动 merge不自动 push。
- [ ] **Step 4验证模板**
```powershell
rg -n "napcat-upstream-audit|napcat-runtime-release-readiness|git merge|git push" D:\MyFiles\KT\mcp\ktWorkflow\ci\jenkins
git -C D:\MyFiles\KT\mcp\ktWorkflow diff --check
git -C D:\MyFiles\KT\GitHub\NapCatQQ diff --check
```
预期:能看到 ktWorkflow 调用,看不到自动 `git merge``git push`
- [ ] **Step 5提交**
```powershell
git -C D:\MyFiles\KT\mcp\ktWorkflow add README.md ci/jenkins
git -C D:\MyFiles\KT\mcp\ktWorkflow commit -m "docs: 增加NapCatQQ自动化Jenkins入口"
git -C D:\MyFiles\KT\GitHub\NapCatQQ add ci/kt-runtime-release.md
git -C D:\MyFiles\KT\GitHub\NapCatQQ commit -m "docs: 增加KT运行时发布清单"
```
---
### Task 10本地验证、审查和计划收口
**Files:**
- Modify: `D:\MyFiles\KT\TASKS.md`
- [ ] **Step 1跑本地验证**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run napcat-upstream-audit -- --artifact-root .kt-workspace/test-artifacts/napcat-upstream-sync
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run nas-codex-bootstrap -- --dry-run
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run typecheck
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts --runTestsByPath --runInBand
corepack pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
git -C D:\MyFiles\KT\mcp\ktWorkflow diff --check
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
git -C D:\MyFiles\KT\GitHub\NapCatQQ diff --check
```
- [ ] **Step 2跑 KT global-review**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --content-scan-mode changed --max-findings-per-project 20
```
预期:`findings=[]`;如果是工具误报,修 ktWorkflow review 规则后复跑。
- [ ] **Step 3更新 TASKS**
记录范围、关键词、验证证据:
```md
### 2026-06-24NapCatQQ 上游同步与运行时发布流水线实施
- 范围mcp/ktWorkflow、Node/kt-template-online-api、GitHub/NapCatQQ。
- 关键词ktWorkflow NapCat 自动化、Codex CLI exec、上游 release 审计、NAS bootstrap、API runtime image/profile override。
- 验证self-test/typecheck/Jest/global-review/diff-check 通过;线上 Jenkins/NAS dry-run 等待下一阶段。
```
- [ ] **Step 4清理历史产物预览**
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run cleanup-history -- --dry-run
```
如果 `deleted` 非空,执行 `--execute` 后复跑 dry-run直到 `deleted=[]`
- [ ] **Step 5提交 TASKS**
```powershell
git -C D:\MyFiles\KT add TASKS.md
git -C D:\MyFiles\KT commit -m "docs: 记录NapCatQQ自动化实施结果"
```
---
## 执行交接
计划已保存到 `docs/superpowers/plans/2026-06-24-napcatqq-upstream-sync-runtime-release-pipeline-implementation-plan.zh-CN.md`
两种执行方式:
1. **Subagent-Driven推荐**:每个 task 派一个新 subagent父线程逐 task reviewktWorkflow/API/NapCatQQ 分仓提交。
2. **Inline Execution**:本线程用 `superpowers:executing-plans` 批次执行,每个仓库边界做 review checkpoint。
推荐选 **1**。这条线跨三个仓库和两个运行环境,用 task 级 subagent 更容易守住边界。

View File

@ -1,756 +0,0 @@
# NapCatQQ Upstream Sync and Runtime Release Pipeline Design
## Background
KT now carries a maintained `NapCatQQ` fork because QQ login, QR refresh, duplicate-login reset, and WebUI runtime state need source-level fixes. The production runtime image is a KT-derived Chinese desktop image:
```text
kt-napcat-desktop-cn:<profile>
```
The current release loop still has a manual gap:
1. Build the `NapCatQQ` fork locally.
2. Stage the `NapCat.Shell` artifact with the API repository script.
3. Copy or build the Docker context on the NAS.
4. Verify the image.
5. Update API runtime image/profile values.
6. Push API/Admin and observe Jenkins/K8s.
That is too easy to drift. It also does not answer a bigger operational question: when upstream `NapNeko/NapCatQQ` ships a new latest release, which upstream changes can KT safely sync, and which changes conflict with KT's forked login/runtime fixes?
This design makes `NapCatQQ` its own release unit and adds a separate upstream release audit loop. The audit loop is read-only by default. It never merges upstream into KT automatically.
The scheduled audit must not depend on the laptop. The laptop moves between networks and cannot be treated as a reliable scheduler or runner. The NAS becomes the resident execution node for scheduled checks, release artifacts, Docker image builds, and future remote Codex development sessions.
Primary upstream metadata sources:
- GitHub latest release REST API: `GET /repos/{owner}/{repo}/releases/latest`.
- GitHub compare REST API: `GET /repos/{owner}/{repo}/compare/{basehead}`.
- Local Git history checks: `git diff`, `git range-diff`, `git merge-tree`, and file-level hot-zone scans.
## Goals
1. Create an independent `NapCatQQ` Jenkins pipeline for fork validation and runtime image release.
2. Add a scheduled upstream latest-release audit that detects new upstream releases without automatically merging them.
3. Classify upstream changes as safe candidate, manual-review required, or blocked.
4. Produce auditable reports showing upstream deltas, KT fork patches, overlap files, hot-zone hits, and recommended action.
5. Build verified `kt-napcat-desktop-cn` images from selected KT fork refs.
6. Promote verified runtime images into API deployment through explicit parameters or promotion metadata, not ad hoc manifest edits.
7. Keep production release completion tied to online smoke evidence, not Jenkins/K8s success alone.
8. Install and configure a NAS-resident Codex CLI environment with a full KT workspace so remote development can continue when the laptop is unavailable.
9. Run scheduled upstream audits on the NAS through Jenkins or a NAS service timer, never from the laptop.
10. Put Codex CLI automation orchestration in `mcp/ktWorkflow`, including prompt templates, schemas, context collectors, and Jenkins/systemd command entry points.
## Non-Goals
- Do not bypass QQ/Tencent captcha, new-device verification, or account safety flows.
- Do not auto-merge upstream `release-latest` into KT's maintained branch.
- Do not push to the upstream `NapNeko/NapCatQQ` repository.
- Do not treat OneBot heartbeat as QQ account login success.
- Do not make the API repository own NapCat source patches.
- Do not store GitHub tokens, Jenkins credentials, SSH keys, WebUI tokens, or Docker registry credentials in Git.
- Do not automatically migrate existing production accounts to a new runtime image without explicit release confirmation.
- Do not store copied Codex secrets, sessions, logs, SQLite state, browser state, or auth files in Git or release artifacts.
- Do not run unattended Codex agent sessions that modify code or merge upstream without explicit human approval.
- Do not let Codex CLI automation read live credentials unless the prompt task explicitly needs them. Upstream release auditing should run from Git metadata and generated context packets.
## Repositories and Ownership
### `D:\MyFiles\KT\GitHub\NapCatQQ`
Owns KT's NapCat source fork and source-level tests.
Required remotes:
```text
upstream = https://github.com/NapNeko/NapCatQQ.git
origin = KT writable mirror or fork repository
```
The current local `origin` may point at upstream. The implementation must correct that before any Jenkins push path is enabled. Jenkins must refuse to push if `origin` resolves to `NapNeko/NapCatQQ`.
Recommended long-lived branches:
```text
kt/runtime-maintenance
kt/sync/<upstream-release-tag>
kt/release/<runtime-profile>
```
### `D:\MyFiles\KT\Node\kt-template-online-api`
Owns:
- `scripts/napcat-desktop-cn-stage-build.mjs`
- `ci/napcat-desktop-cn/Dockerfile`
- `ci/napcat-desktop-cn/verify.sh`
- API runtime image/profile parameters and deployment contract
- API-side login/SSE safety guards
The API repository should not commit `NapCat.Shell.zip` artifacts.
### Jenkins and NAS Docker
Own:
- Scheduled upstream release audit execution.
- NapCat fork build/test execution.
- NAS-local Docker image build and verification.
- Runtime promotion metadata and deployment observation artifacts.
### `D:\MyFiles\KT\mcp\ktWorkflow`
Owns the automation control plane. Jenkins and NAS timers should call ktWorkflow instead of embedding bespoke shell logic.
Required capabilities:
- Generate upstream audit context packets from Git/GitHub facts.
- Run Codex CLI automation prompts with schema validation and artifact capture.
- Provide MCP tools for interactive use by Codex Desktop and CLI.
- Provide npm scripts for Jenkins/systemd, such as `napcat-upstream-audit`, `napcat-sync-candidate-review`, `napcat-runtime-release-readiness`, and `nas-codex-bootstrap`.
- Keep deterministic collectors, prompt templates, schemas, output normalization, and artifact writing in one reusable implementation.
The API repository remains the owner of runtime Docker integration and API deployment contract. It should not own the general automation runner.
### NAS Codex Worker and Full KT Workspace
The NAS must host a stable remote development and scheduled-audit environment. Current discovery shows:
```text
host: Tsukasa-NAS
os: Debian GNU/Linux 12 (bookworm), x86_64
available: git 2.43.0, Docker 28.5.2
missing: node, npm, pnpm, corepack, codex
laptop surface: Codex Desktop; local CLI wrapper currently reports codex-cli 0.131.0
```
Recommended resident layout:
```text
/vol1/docker/kt-codex/
home/.codex/ # CODEX_HOME for NAS, chmod 700
workspace/KT/ # Full KT workspace checkout
artifacts/ # Audit and release artifacts
logs/ # Service logs, rotated
jenkins-workspaces/ # Clean job-specific worktrees/checkouts
```
The NAS setup must install Node and Codex before enabling scheduled jobs:
1. Install Node on the NAS with a pinned version that satisfies KT engines, currently at least Node `>=20.19.0` for `mcp/ktWorkflow`.
2. Enable `corepack` and install `pnpm` according to each repository's `packageManager`.
3. Install the latest stable Codex CLI on the NAS. The laptop runs Codex Desktop, so NAS CLI does not need to match the Desktop wrapper version.
4. Verify `node --version`, `corepack --version`, `pnpm --version`, `git --version`, `docker --version`, and `codex --version`.
The KT workspace on NAS must be a real full checkout, not a copy of temporary laptop state. Implementation must create a workspace manifest that lists every KT subrepo path, remote, default branch, and writable/push policy. Repositories without a reliable remote must be called out explicitly before the first bootstrap. Ignored runtime folders such as `.kt-workspace`, build outputs, logs, sessions, and DB sync drafts must not be mirrored as source of truth.
Codex configuration parity is logical parity, not a byte-for-byte copy of the Windows profile:
- Copy or generate non-secret configuration for trusted KT projects, model defaults, approval policy, sandbox policy, Superpowers skills, custom KT skills, and `ktWorkflow` MCP.
- Convert Windows paths in `config.toml` to NAS Linux paths.
- Keep GUI-only integrations such as local browser, computer-use, and Windows Figma local context disabled unless a NAS-compatible backend is explicitly installed.
- Because the NAS is fully trusted, sensitive Codex files may be copied from the laptop when needed. The sync must still use a direct trusted channel, keep owner-only permissions, and exclude those files from Git, Jenkins artifacts, Docker build contexts, and public logs.
- Bootstrap Codex auth either by interactive `codex login` on NAS or by directly copying the required auth material into `CODEX_HOME` with `chmod 600`.
- Sessions, logs, SQLite state, browser state, and generated images may be copied only if they are useful for remote continuity; they remain local runtime state and must not become source-control or release artifacts.
- Keep `CODEX_HOME=/vol1/docker/kt-codex/home/.codex` for NAS services so scheduled jobs do not write into root's default home by accident.
The scheduled upstream audit should be deterministic shell/Jenkins work by default. Codex CLI is installed for remote development, review, and manual takeover, not for silent unattended code modification. If a future job invokes Codex non-interactively, it must run in a throwaway branch/worktree, emit a full artifact report, and require human review before commit, push, merge, or deployment.
The intended automation split is:
```text
ktWorkflow deterministic collector
-> context packet with git/GitHub/build facts
-> ktWorkflow Codex CLI prompt runner
-> structured JSON/Markdown report
-> human-approved follow-up action
```
This gives Codex CLI the parts it is good at: reading diffs, explaining risk, checking hot-zone interactions, drafting candidate actions, and writing handoff reports. It does not make Codex the authority that silently mutates production state.
## Pipeline Overview
```mermaid
flowchart TD
Upstream["NapNeko/NapCatQQ latest release"] --> Audit["KT-NapCatQQ-Upstream-Sync"]
Nas["NAS resident runner"] --> Audit
Fork["KT NapCatQQ fork"] --> Audit
Audit --> Report["Audit report artifact"]
Audit --> Candidate["Optional kt/sync/<tag> candidate branch"]
Candidate --> Human["Human review and approval"]
Fork --> Release["KT-NapCatQQ-Runtime-Release"]
Human --> Release
Release --> Image["kt-napcat-desktop-cn:<profile>"]
Image --> ApiDeploy["API Jenkins deploy with runtime image/profile params"]
ApiDeploy --> Observe["deploy-observation + online smoke"]
```
Two Jenkins jobs are required:
```text
KT-NapCatQQ-Upstream-Sync
KT-NapCatQQ-Runtime-Release
```
The upstream sync job is scheduled and read-only by default. The runtime release job is manually triggered, or triggered by an approved candidate branch.
All scheduled executions run on the NAS. Jenkins is the preferred scheduler because it already owns build logs and artifacts. If Jenkins is unavailable for this job, use a NAS service timer such as `systemd` with the same script and artifact directory. The laptop may trigger jobs manually, but it must not be the scheduler.
## Upstream Sync Audit Job
### Trigger
- Scheduled on the NAS, for example once per day.
- Manual trigger with `UPSTREAM_RELEASE_TAG` override.
### Inputs
```text
UPSTREAM_REPO=NapNeko/NapCatQQ
FORK_BRANCH=kt/runtime-maintenance
LAST_ACCEPTED_UPSTREAM_BASE=<commit recorded from previous release marker>
UPSTREAM_RELEASE_TAG=<optional manual override>
CREATE_CANDIDATE_BRANCH=false by default
```
### Steps
1. Fetch upstream metadata.
- If `UPSTREAM_RELEASE_TAG` is empty, call GitHub latest release API.
- Resolve the release tag to a peeled commit with `git ls-remote --tags upstream <tag>^{}` or equivalent.
- Record release name, tag, commit, published time, and release URL.
2. Fetch fork and upstream history.
- Fetch `upstream`.
- Fetch KT writable `origin`.
- Checkout `FORK_BRANCH`.
- Confirm working tree is clean.
3. Compute upstream delta.
- `upstreamDelta = LAST_ACCEPTED_UPSTREAM_BASE..UPSTREAM_RELEASE_COMMIT`
- Capture commits, changed files, rename/delete status, package lock changes, and hot-zone hits.
4. Compute KT fork patch set.
- `forkPatch = LAST_ACCEPTED_UPSTREAM_BASE..FORK_BRANCH`
- Capture KT-only commits and files.
5. Detect overlap and risk.
- `overlapFiles = upstreamDelta.files intersect forkPatch.files`
- `hotZoneFiles = upstreamDelta.files matching login/runtime/build patterns`
- Run `git merge-tree` or an equivalent dry merge against the release commit.
- Run `git range-diff` when a candidate rebase can be generated.
6. Classify the release.
- `safe-candidate`: no hot-zone hit, no overlap with KT patch files, lockfile/build changes pass static checks.
- `manual-review`: hot-zone touched, overlap touched, package/build graph changed, or range-diff is non-trivial.
- `blocked`: dry merge conflicts, tests cannot install/build, artifact structure changed, or required upstream metadata cannot be verified.
7. Write artifacts.
- Markdown human report.
- JSON machine report.
- File lists for upstream delta, fork patch, overlap, and hot zones.
- Suggested next action.
8. Optionally create candidate branch.
- Only allowed when classification is `safe-candidate` or manually requested.
- Branch name: `kt/sync/<release-tag>`.
- Candidate creation applies KT patches on top of upstream release without merging back to `FORK_BRANCH`.
- Candidate branch must be pushed only to KT writable remote.
- Candidate branch must never auto-merge.
9. Archive NAS-local artifacts.
- Write reports under `/vol1/docker/kt-codex/artifacts/napcat-upstream-sync/<timestamp>`.
- Jenkins must archive the same reports.
- Reports must include the NAS runner hostname, Codex CLI version when available, Git version, and workspace manifest revision.
- If Codex CLI automation is enabled, archive the exact prompt, context packet, JSONL event log, final response, and output schema validation result.
### Hot Zones
The audit treats these as high-risk areas:
```text
packages/napcat-core/**/login*
packages/napcat-core/**/qrcode*
packages/napcat-shell/**
packages/napcat-framework/**
packages/napcat-webui-backend/**/QQLogin*
packages/napcat-webui-backend/**/Data*
packages/napcat-webui-backend/**/auth*
packages/napcat-webui-frontend/**
packages/napcat-adapter/**
packages/napcat-onebot/**
packages/napcat-vite/**
package.json
pnpm-lock.yaml
tsconfig*.json
vite*.ts
```
This list is intentionally conservative. If upstream changes login state, QR generation, WebUI auth, OneBot startup, or build packaging, the sync must go through human review.
### Audit Report Contract
JSON artifact:
```json
{
"upstream": {
"repo": "NapNeko/NapCatQQ",
"releaseTag": "v0.0.0",
"releaseCommit": "0000000000000000000000000000000000000000",
"publishedAt": "2026-06-24T00:00:00Z",
"releaseUrl": "https://github.com/NapNeko/NapCatQQ/releases/tag/v0.0.0"
},
"fork": {
"branch": "kt/runtime-maintenance",
"headCommit": "0000000000000000000000000000000000000000",
"lastAcceptedUpstreamBase": "0000000000000000000000000000000000000000"
},
"classification": "manual-review",
"reasonCodes": ["HOT_ZONE_CHANGED", "FORK_PATCH_OVERLAP"],
"upstreamChangedFiles": [],
"forkPatchFiles": [],
"overlapFiles": [],
"hotZoneFiles": [],
"candidateBranch": null,
"recommendedAction": "Review hot-zone changes before creating a candidate branch."
}
```
The report must be safe to archive and share. It must not include tokens, secrets, private env files, QQ passwords, captcha tickets, or WebUI credentials.
## Runtime Release Job
### Trigger
- Manual trigger with an approved source ref.
- Optional upstream-sync trigger after a candidate branch is reviewed and approved.
### Inputs
```text
NAPCAT_SOURCE_REF=kt/runtime-maintenance or kt/sync/<tag>
UPSTREAM_RELEASE_TAG=<tag recorded in audit>
UPSTREAM_RELEASE_COMMIT=<commit recorded in audit>
RUNTIME_PROFILE=desktop-cn-vN
NAPCAT_BASE_IMAGE=mlikiowa/napcat-docker@sha256:<digest>
API_REF=main
PROMOTE_TO_API=false by default
CANARY_ACCOUNT_ID=<optional>
```
`NAPCAT_BASE_IMAGE` must resolve to a digest before Docker build. If the user chooses to base on `mlikiowa/napcat-docker:latest`, the pipeline must first pull it, resolve its `RepoDigest`, and use the digest in the actual Docker build and release marker.
### Steps
1. Checkout `NapCatQQ`.
- Use KT writable fork remote.
- Checkout `NAPCAT_SOURCE_REF`.
- Confirm clean tree.
- Record fork commit.
2. Install and verify source.
- `pnpm install --frozen-lockfile`
- Focused login/runtime tests.
- `pnpm run typecheck`
- `pnpm run build:webui`
- `pnpm run build:shell`
- `pnpm run build:framework`
3. Checkout API repository as a build integration dependency.
- Use `API_REF`.
- Run the stage-build script with `--napcat-root`.
- Ensure the staged `fork-artifact.json` contains:
- upstream release tag
- upstream release commit
- last accepted upstream base
- fork commit
- dist sha256
- `napcat.mjs` sha256
- base image digest
- Jenkins build URL
4. Build NAS-local Docker image.
- Build from staged context.
- Tag immutable image:
```text
kt-napcat-desktop-cn:<upstream-tag>-kt.<jenkins-build>
```
- Tag promotion alias only after verify:
```text
kt-napcat-desktop-cn:<runtime-profile>
```
5. Verify image.
- Run a temporary container.
- Execute `/ci/napcat-desktop-cn/verify.sh`.
- Verify locale, timezone, fonts, XDG paths, hidden Docker markers, fork marker, artifact hash, and critical runtime symbols.
- Remove temporary container.
- Inspect image ID and digest-like local image ID.
6. Archive release metadata.
- `napcat-runtime-release.json`
- Docker image inspect output.
- `fork-artifact.json`
- test summaries.
7. Optional API promotion.
- If `PROMOTE_TO_API=true`, trigger API Jenkins with runtime image/profile parameters.
- API deploy must update K8s runtime env through a controlled deployment parameter or generated manifest overlay.
- API deploy must not require hand-editing `k8s/prod/api.yaml` for every runtime image.
8. Deployment observation and smoke.
- Run API `deploy-observation`.
- Verify API `/health/runtime`.
- Verify K8s deployment generation, pod image, ready replicas, restart count, and logs.
- For QQ login behavior, complete only after a real account smoke or a clearly documented manual-scan wait state.
The runtime release job must use a clean NAS job workspace rather than the long-lived remote development workspace. This prevents a remote Codex session and a Jenkins release from modifying the same checkout at the same time.
## ktWorkflow Codex CLI Automation
Implementation should add a NapCat automation module inside `mcp/ktWorkflow`, for example:
```text
mcp/ktWorkflow/
prompts/napcat/
upstream-audit.md
sync-candidate-review.md
runtime-release-readiness.md
remote-dev-handoff.md
schemas/
upstream-audit.schema.json
runtime-release-readiness.schema.json
src/tools/napcatAutomation.ts
src/tools/napcatAutomation.types.ts
src/tools/napcatAutomation.prompts.ts
```
Prompts and schemas are source-controlled because they are part of the automation contract. Secrets are never embedded in prompts. Each ktWorkflow tool must accept explicit inputs, generate a context packet, optionally call Codex CLI, validate the output schema, and write artifacts under `.kt-workspace` locally or `/vol1/docker/kt-codex/artifacts` on NAS.
MCP tools should include:
```text
kt_napcat_upstream_audit
kt_napcat_sync_candidate_review
kt_napcat_runtime_release_readiness
kt_napcat_remote_dev_handoff
kt_nas_codex_bootstrap_plan
```
CLI scripts should include:
```text
pnpm --dir mcp/ktWorkflow run napcat-upstream-audit -- --execute --artifact-root /vol1/docker/kt-codex/artifacts/napcat-upstream-sync
pnpm --dir mcp/ktWorkflow run napcat-sync-candidate-review -- --source-ref kt/sync/<tag>
pnpm --dir mcp/ktWorkflow run napcat-runtime-release-readiness -- --release-artifact <path>
pnpm --dir mcp/ktWorkflow run nas-codex-bootstrap -- --dry-run
```
The scripts are the only supported scheduler entry points. Jenkins and `systemd` should call these scripts and should not duplicate the audit algorithm.
### `upstream-audit.md`
Purpose: classify a new upstream latest release from already-collected facts.
Prompt contract:
```text
You are auditing whether KT's NapCatQQ fork can safely sync an upstream NapNeko/NapCatQQ release.
Inputs:
- Upstream release metadata.
- Last accepted upstream base.
- KT fork branch and head commit.
- Upstream changed file list and commit list.
- KT fork patch file list and commit list.
- Hot-zone file list.
- Dry merge or range-diff output.
Rules:
- Do not run merge, commit, push, deploy, or edit files.
- Do not infer success from missing data.
- Classify exactly one of: safe-candidate, manual-review, blocked.
- Explain every reasonCode with file-level evidence.
- Recommend next action: no-op, create candidate branch, request human review, or block.
Output:
- JSON matching mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json.
- A concise Markdown summary safe for Jenkins artifacts.
```
Recommended command shape on NAS:
```bash
CODEX_HOME=/vol1/docker/kt-codex/home/.codex \
codex exec \
--cd /vol1/docker/kt-codex/workspace/KT \
--profile automation \
--sandbox workspace-write \
--ask-for-approval never \
--json \
--ephemeral \
--output-schema /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json \
--output-last-message "$ARTIFACT_DIR/codex-upstream-audit.md" \
- < "$CONTEXT_PACKET"
```
ktWorkflow owns this command invocation and must capture the JSONL event stream. `workspace-write` is used so Codex can write only the requested report files under the job artifact directory. The prompt still forbids source edits and push/merge/deploy actions.
### `sync-candidate-review.md`
Purpose: review a generated `kt/sync/<release-tag>` candidate branch before any merge.
Prompt contract:
```text
Review the candidate branch against KT's NapCat login/runtime requirements.
Must inspect:
- login service reset behavior
- QR refresh and stale QR handling
- WebUI login runtime state
- captcha and new-device flow boundaries
- package/build output structure
- test delta and missing coverage
Must not:
- edit files
- commit
- push
- mark production ready
Output:
- Critical/Important findings first.
- Required tests before merge.
- Whether this candidate can proceed to manual code review.
```
### `runtime-release-readiness.md`
Purpose: decide whether a built runtime image can be promoted to API.
Inputs:
- NapCatQQ test/typecheck/build summaries.
- `fork-artifact.json`.
- Docker image inspect output.
- `verify.sh` output.
- API integration test output.
- planned runtime image/profile values.
Output must separate:
- source validation
- image validation
- API promotion readiness
- online smoke readiness
- rollback pointer
### `remote-dev-handoff.md`
Purpose: create a safe handoff when a human opens NAS Codex CLI for remote development.
It should summarize:
- active branch and dirty status for each repo
- relevant artifact directories
- latest Jenkins job state
- current blockers
- exact commands that are safe to run next
It must not ask Codex to modify files automatically.
## API Promotion Contract
The API Jenkinsfile should gain optional parameters:
```text
QQBOT_NAPCAT_IMAGE_OVERRIDE=
QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE=
```
When set, the K8s deploy stage must apply these as runtime env values for the API deployment. The implementation may use a generated manifest overlay or `kubectl set env`, but it must leave secrets out of Git and record the effective values in deployment evidence.
API tests must enforce:
- Production no longer depends on `latest`.
- The runtime image override path is explicit.
- The default profile value is still a known safe fallback.
- The API code does not silently downgrade to older runtime profiles.
## Data Flow
```mermaid
sequenceDiagram
participant Upstream as GitHub Upstream
participant Nas as NAS Runner
participant Sync as Upstream Sync Jenkins
participant Fork as KT NapCatQQ Fork
participant Release as Runtime Release Jenkins
participant ApiRepo as API Repo
participant Docker as NAS Docker
participant ApiDeploy as API Jenkins
Nas->>Sync: scheduled trigger
Sync->>Upstream: read latest release metadata
Sync->>Fork: fetch maintained branch
Sync->>Sync: diff upstream delta vs KT fork patch
Sync-->>Fork: optional kt/sync/<tag> branch, never auto-merge
Sync-->>Sync: archive audit report
Release->>Fork: checkout approved source ref
Release->>Release: test, typecheck, build webui/shell/framework
Release->>ApiRepo: run stage-build script
Release->>Docker: build and verify kt-napcat-desktop-cn image
Release->>ApiDeploy: optional promote with image/profile params
ApiDeploy-->>Release: rollout and smoke evidence
```
## Error Handling
- GitHub API rate limit or outage: mark audit as `blocked` with retry advice; do not infer latest release from stale data unless explicitly allowed.
- NAS runner lacks Node, pnpm, Docker, Git, Codex, or KT workspace manifest: mark setup incomplete and do not enable the scheduled trigger.
- NAS Codex auth is missing: remote development is unavailable until interactive login or trusted auth-file sync completes; deterministic Jenkins audits may still run if they do not need Codex auth.
- NAS Codex config drifts from the generated template: fail the remote-development smoke and require config regeneration.
- ktWorkflow NapCat automation tool, prompt, or schema is missing: deterministic audit may still run, but AI-assisted classification is marked unavailable.
- Codex CLI output does not match the required schema: mark the audit `blocked` and archive the raw output for review.
- Laptop is offline or away from the LAN: scheduled audits and runtime release jobs must continue on NAS.
- Upstream release has no resolvable tag commit: mark `blocked`.
- Fork writable remote points to upstream: fail before push.
- Dirty workspace: fail before audit candidate or release.
- Hot-zone overlap: mark `manual-review`; do not create or merge a candidate unless explicitly requested.
- Dry merge conflict: mark `blocked`.
- `pnpm install`, tests, typecheck, shell/framework build failure: mark runtime release failed; do not build or promote an image.
- Docker base image cannot resolve to digest: fail before build.
- `verify.sh` fails: remove verify container, keep artifacts, do not tag promotion alias.
- API promotion deploy succeeds but online smoke fails: keep deployment evidence separate from functional completion and provide rollback steps.
## Rollback
Runtime rollback is controlled by the API runtime image/profile values:
1. Identify the last verified runtime image/profile from release artifacts.
2. Trigger API Jenkins with previous `QQBOT_NAPCAT_IMAGE_OVERRIDE` and profile.
3. Observe K8s rollout.
4. Existing online NapCat containers are not automatically rebuilt. Account-level migration remains explicit because container rebuilds affect QQ device/login risk.
## Validation Strategy
### Upstream Sync Job
Local/job validation:
```powershell
pnpm --dir mcp/ktWorkflow run self-test
git diff --check
```
Jenkins dry run must show:
- Latest release metadata resolved.
- Last accepted upstream base resolved.
- Upstream delta file list.
- Fork patch file list.
- Overlap/hot-zone classification.
- Report artifact paths.
- NAS runner hostname and artifact directory.
- Workspace manifest revision.
### NAS Codex and Workspace
NAS setup validation:
```bash
node --version
corepack --version
pnpm --version
git --version
docker --version
codex --version
cd /vol1/docker/kt-codex/workspace/KT
git status --short --branch
pnpm --dir mcp/ktWorkflow run self-test
```
Remote development smoke:
- `codex --version` reports the latest installed NAS CLI version.
- `CODEX_HOME` points at `/vol1/docker/kt-codex/home/.codex`.
- `ktWorkflow` MCP can start from Linux paths.
- A dry-run upstream audit can write reports into `/vol1/docker/kt-codex/artifacts`.
- No laptop-only Windows path remains in the NAS `config.toml`.
Codex automation prompt smoke:
```bash
CODEX_HOME=/vol1/docker/kt-codex/home/.codex \
codex exec --cd /vol1/docker/kt-codex/workspace/KT \
--profile automation \
--sandbox workspace-write \
--ask-for-approval never \
--json \
--ephemeral \
--output-schema /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json \
--output-last-message /vol1/docker/kt-codex/artifacts/smoke/codex-upstream-audit.md \
- < /vol1/docker/kt-codex/artifacts/smoke/context-packet.md
```
Expected result: schema-valid JSON, no source changes, and a Markdown report in the artifact directory. The preferred smoke is through ktWorkflow:
```bash
pnpm --dir /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow run napcat-upstream-audit -- --dry-run --use-codex
```
### Runtime Release Job
NapCatQQ:
```powershell
corepack pnpm install --frozen-lockfile
corepack pnpm --filter napcat-test run test -- loginQrcodeRefresh webuiLoginSourceWiring webuiQQLoginHandlers webuiLoginRuntime
corepack pnpm run typecheck
corepack pnpm run build:webui
corepack pnpm run build:shell
corepack pnpm run build:framework
```
API integration:
```powershell
corepack pnpm exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runTestsByPath --runInBand
corepack pnpm run typecheck
git diff --check
```
NAS image:
```bash
docker build --build-arg NAPCAT_BASE_IMAGE="$NAPCAT_BASE_IMAGE_DIGEST" -t "$IMMUTABLE_TAG" -f "$STAGED_CONTEXT/ci/napcat-desktop-cn/Dockerfile" "$STAGED_CONTEXT"
docker run -d --name "$VERIFY_CONTAINER" "$IMMUTABLE_TAG"
docker exec "$VERIFY_CONTAINER" sh /ci/napcat-desktop-cn/verify.sh
docker rm -f "$VERIFY_CONTAINER"
docker tag "$IMMUTABLE_TAG" "$PROMOTION_TAG"
```
Online:
- API `deploy-observation` passes.
- API `/health/runtime` passes.
- For login-runtime releases, a canary account either logs in successfully or reaches a clear captcha/new-device/manual-scan pending state with a fresh QR and correct SSE/Admin status.
## Completion Criteria
- `NapCatQQ` has a standalone Jenkins release path.
- NAS has a resident Codex CLI and full KT workspace environment for scheduled audits and remote development.
- A scheduled audit detects upstream latest releases and writes safe reports.
- The scheduled audit runs from NAS service/Jenkins, not from the laptop.
- Codex CLI automation runs through ktWorkflow; prompts are versioned, schema-validated, and archived with every AI-assisted audit.
- Upstream sync never auto-merges into KT maintenance branches.
- Hot-zone conflicts are blocked or marked for manual review.
- Runtime images are built from approved fork refs and verified inside containers.
- API deployment consumes runtime image/profile through an explicit promotion contract.
- Jenkins/K8s deployment evidence and online QQBot/NapCat smoke evidence are both captured before claiming a release complete.

View File

@ -1,756 +0,0 @@
# NapCatQQ 上游同步与运行时发布流水线设计
## 背景
KT 现在已经维护了自己的 `NapCatQQ` fork因为 QQ 登录、二维码刷新、重复登录状态重置、WebUI 登录运行态这些问题必须在源码层修。生产运行时镜像是 KT 派生的中文桌面镜像:
```text
kt-napcat-desktop-cn:<profile>
```
目前发布链路还有一段人工操作:
1. 本地构建 `NapCatQQ` fork。
2. 用 API 仓库脚本 staging `NapCat.Shell` artifact。
3. 在 NAS 上构建 Docker context。
4. 验证镜像。
5. 修改 API 的运行时镜像/profile。
6. 推送 API/Admin 并观察 Jenkins/K8s。
这段人工链路容易漂移。更重要的是,当上游 `NapNeko/NapCatQQ` 发布新的 latest release 时,我们必须知道“哪些上游改动能同步,哪些会撞上 KT fork 的登录/二维码补丁”,不能无脑合并上游。
本设计把 `NapCatQQ` fork 变成独立发布单元,并新增一个上游 release 审计循环。审计循环默认只读,只产报告和候选分支,绝不自动合并到 KT 维护分支。
定时审计不能依赖笔记本。笔记本需要通勤和切换网络,不能作为稳定 scheduler 或 runner。NAS 必须成为常驻执行节点负责定时检查、release artifact、Docker 镜像构建,以及后续远程 Codex 开发会话。
主要上游元数据来源:
- GitHub latest release REST API`GET /repos/{owner}/{repo}/releases/latest`。
- GitHub compare REST API`GET /repos/{owner}/{repo}/compare/{basehead}`。
- 本地 Git 检查:`git diff`、`git range-diff`、`git merge-tree`、文件级 hot zone 扫描。
## 目标
1. 给 `NapCatQQ` 建独立 Jenkins 流水线,负责 fork 验证和运行时镜像发布。
2. 增加定时上游 latest release 审计,发现新 release 但不自动合并。
3. 把上游改动分成:可生成候选、必须人工审查、阻断。
4. 生成可审计报告:上游 delta、KT fork patch、重叠文件、hot zone 命中、推荐动作。
5. 从已确认的 KT fork ref 构建并验证 `kt-napcat-desktop-cn` 镜像。
6. 通过显式参数或发布元数据把已验证镜像推广到 API 部署,不再手工临时改 manifest。
7. 线上完成标准必须包含真实 smoke不把 Jenkins/K8s 成功当成功能闭环。
8. 在 NAS 上安装并配置常驻 Codex CLI 与 KT 全量 workspace保证笔记本不在线时仍可远程开发。
9. 上游定时审计必须跑在 NAS 的 Jenkins 或 NAS 服务定时器中,不能跑在笔记本。
10. 把 Codex CLI 自动化编排放进 `mcp/ktWorkflow`,由 ktWorkflow 统一管理提示词模板、schema、context 采集器和 Jenkins/systemd 命令入口。
## 非目标
- 不绕过 QQ/Tencent 验证码、新设备验证或账号安全流程。
- 不自动把 upstream `release-latest` 合并到 KT 维护分支。
- 不向上游 `NapNeko/NapCatQQ` 仓库推送。
- 不把 OneBot 心跳当作 QQ 账号登录成功。
- 不让 API 仓库持有 NapCat 源码补丁。
- 不把 GitHub token、Jenkins 凭证、SSH key、WebUI token、Docker registry 凭证写进 Git。
- 不在没有明确确认时自动迁移线上账号到新运行时镜像。
- 不把同步到 NAS 的 Codex secrets、sessions、logs、SQLite 状态、浏览器状态或 auth 文件写入 Git 或 release artifact。
- 不运行会在无人值守状态下修改代码或合并 upstream 的 Codex agent。
- 不让 Codex CLI 自动化读取实时凭据,除非该提示词任务明确需要。上游 release 审计应只基于 Git 元数据和生成的 context packet。
## 仓库职责
### `D:\MyFiles\KT\GitHub\NapCatQQ`
负责 KT 的 NapCat 源码 fork 和源码级测试。
需要的 remote
```text
upstream = https://github.com/NapNeko/NapCatQQ.git
origin = KT 可写 mirror 或 fork 仓库
```
当前本地 `origin` 可能指向上游。实现阶段必须先修正这一点。Jenkins 如果发现 `origin` 指向 `NapNeko/NapCatQQ`,必须拒绝 push。
建议长期分支:
```text
kt/runtime-maintenance
kt/sync/<upstream-release-tag>
kt/release/<runtime-profile>
```
### `D:\MyFiles\KT\Node\kt-template-online-api`
负责:
- `scripts/napcat-desktop-cn-stage-build.mjs`
- `ci/napcat-desktop-cn/Dockerfile`
- `ci/napcat-desktop-cn/verify.sh`
- API 运行时镜像/profile 参数与部署契约
- API 侧登录/SSE 防旧码护栏
API 仓库不提交 `NapCat.Shell.zip` 二进制 artifact。
### Jenkins 与 NAS Docker
负责:
- 定时执行上游 release 审计。
- 执行 NapCat fork 构建、测试和类型检查。
- 在 NAS 本地构建并验证 Docker 镜像。
- 保存运行时发布元数据和部署观测 artifact。
### `D:\MyFiles\KT\mcp\ktWorkflow`
负责自动化控制面。Jenkins 和 NAS 定时器应该调用 ktWorkflow而不是各自复制一份临时 shell 逻辑。
必备能力:
- 基于 Git/GitHub 事实生成上游审计 context packet。
- 调用 Codex CLI 自动化提示词,并完成 schema 校验和 artifact 捕获。
- 给 Codex Desktop 和 Codex CLI 都提供 MCP tools。
- 给 Jenkins/systemd 提供 npm scripts例如 `napcat-upstream-audit`、`napcat-sync-candidate-review`、`napcat-runtime-release-readiness`、`nas-codex-bootstrap`。
- 把确定性采集器、提示词模板、schema、输出规范化、artifact 写入统一收口在一个可复用实现里。
API 仓库继续负责运行时 Docker 集成和 API 部署契约,不负责通用自动化 runner。
### NAS Codex Worker 与 KT 全量 Workspace
NAS 必须承载稳定的远程开发和定时审计环境。当前只读探测结果:
```text
host: Tsukasa-NAS
os: Debian GNU/Linux 12 (bookworm), x86_64
available: git 2.43.0, Docker 28.5.2
missing: node, npm, pnpm, corepack, codex
laptop surface: Codex Desktop本机 CLI wrapper 当前显示 codex-cli 0.131.0
```
推荐常驻目录:
```text
/vol1/docker/kt-codex/
home/.codex/ # NAS 的 CODEX_HOMEchmod 700
workspace/KT/ # KT 全量 workspace checkout
artifacts/ # 审计和发布 artifact
logs/ # 服务日志,按周期轮转
jenkins-workspaces/ # Jenkins 干净 job workspace/worktree
```
启用定时任务前NAS 必须先安装 Node 和 Codex
1. 安装满足 KT engines 的 Node 版本,目前 `mcp/ktWorkflow` 至少需要 Node `>=20.19.0`
2. 启用 `corepack`,并按各仓库 `packageManager` 使用对应 `pnpm`
3. NAS 安装最新稳定版 Codex CLI。笔记本使用的是 Codex DesktopNAS CLI 不需要和 Desktop wrapper 版本锁死一致。
4. 验证 `node --version`、`corepack --version`、`pnpm --version`、`git --version`、`docker --version`、`codex --version`。
NAS 上的 KT workspace 必须是真实全量 checkout不是笔记本临时状态复制。实现阶段要创建 workspace manifest列出每个 KT 子仓库的路径、remote、默认分支和是否允许 push。没有稳定 remote 的仓库必须在首次 bootstrap 前明确列出。`.kt-workspace`、构建输出、日志、session、DB sync 草稿等 ignored runtime 目录不能作为源码真相源同步。
Codex 配置一致指“逻辑一致”,不是把 Windows profile 原样复制到 Linux
- 同步或生成非敏感配置KT trusted projects、model 默认值、approval policy、sandbox policy、Superpowers skills、自定义 KT skills、`ktWorkflow` MCP。
- 把 `config.toml` 里的 Windows 路径转换成 NAS Linux 路径。
- 本地浏览器、computer-use、Windows Figma local context 这类 GUI-only 能力默认禁用,除非后续安装了 NAS 可用 backend。
- NAS 是完全可信环境,需要时可以从笔记本复制敏感 Codex 文件。同步仍必须走可信直连通道,文件 owner-only 权限,并排除在 Git、Jenkins artifact、Docker build context 和公开日志之外。
- Codex auth 可通过 NAS 上交互式 `codex login` 完成,也可以直接把所需 auth material 同步到 `CODEX_HOME` 并设置 `chmod 600`
- sessions、logs、SQLite state、browser state、generated images 只有在远程连续性有价值时才复制;它们仍是本地运行态,不是源码或发布 artifact。
- NAS 服务统一使用 `CODEX_HOME=/vol1/docker/kt-codex/home/.codex`,避免定时任务意外写入 root 默认 home。
上游定时审计默认只运行确定性的 shell/Jenkins 逻辑。Codex CLI 安装的目的,是远程开发、人工审查和接管,不是默默无人值守改代码。未来如果需要非交互调用 Codex也必须在一次性分支/worktree 中运行,产出完整 artifact并在 commit、push、merge 或 deployment 前要求人工审查。
自动化分工如下:
```text
ktWorkflow 确定性采集器
-> 包含 git/GitHub/build 事实的 context packet
-> ktWorkflow Codex CLI 提示词 runner
-> 结构化 JSON/Markdown 报告
-> 人工确认后的后续动作
```
这样 Codex CLI 做它擅长的部分:读 diff、解释风险、检查 hot-zone 交互、草拟候选动作、写交接报告。它不成为无人值守修改生产状态的最终权威。
## 流水线总览
```mermaid
flowchart TD
Upstream["NapNeko/NapCatQQ latest release"] --> Audit["KT-NapCatQQ-Upstream-Sync"]
Nas["NAS 常驻 runner"] --> Audit
Fork["KT NapCatQQ fork"] --> Audit
Audit --> Report["审计报告 artifact"]
Audit --> Candidate["可选 kt/sync/<tag> 候选分支"]
Candidate --> Human["人工审查与确认"]
Fork --> Release["KT-NapCatQQ-Runtime-Release"]
Human --> Release
Release --> Image["kt-napcat-desktop-cn:<profile>"]
Image --> ApiDeploy["API Jenkins 带运行时参数发布"]
ApiDeploy --> Observe["deploy-observation + 线上 smoke"]
```
需要两个 Jenkins job
```text
KT-NapCatQQ-Upstream-Sync
KT-NapCatQQ-Runtime-Release
```
上游同步 job 定时执行,默认只读。运行时发布 job 手动触发,或者由已经人工确认的候选分支触发。
所有定时执行都必须跑在 NAS 上。优先使用 Jenkins因为它已经负责构建日志和 artifact如果这个 job 暂时不适合放 Jenkins就用 NAS 服务定时器,例如 `systemd` timer执行同一套脚本和 artifact 目录。笔记本可以手动触发任务,但不能作为 scheduler。
## 上游同步审计 Job
### 触发方式
- 在 NAS 上定时触发,例如每天一次。
- 手动触发,可指定 `UPSTREAM_RELEASE_TAG`
### 输入
```text
UPSTREAM_REPO=NapNeko/NapCatQQ
FORK_BRANCH=kt/runtime-maintenance
LAST_ACCEPTED_UPSTREAM_BASE=<上一次已接入 release marker 里的 commit>
UPSTREAM_RELEASE_TAG=<可选手动指定>
CREATE_CANDIDATE_BRANCH=false
```
### 步骤
1. 获取上游元数据。
- 如果 `UPSTREAM_RELEASE_TAG` 为空,调用 GitHub latest release API。
- 把 release tag 解析成 peeled commit。
- 记录 release 名称、tag、commit、发布时间和 release URL。
2. 拉取 fork 与上游历史。
- fetch `upstream`
- fetch KT 可写 `origin`
- checkout `FORK_BRANCH`
- 确认工作区干净。
3. 计算上游 delta。
- `upstreamDelta = LAST_ACCEPTED_UPSTREAM_BASE..UPSTREAM_RELEASE_COMMIT`
- 记录 commit、变更文件、rename/delete、lockfile 变化、hot zone 命中。
4. 计算 KT fork patch。
- `forkPatch = LAST_ACCEPTED_UPSTREAM_BASE..FORK_BRANCH`
- 记录 KT-only commit 和文件。
5. 检测重叠和风险。
- `overlapFiles = 上游 delta 文件 ∩ KT patch 文件`
- `hotZoneFiles = 上游 delta 中命中登录/运行时/构建模式的文件`
- 运行 `git merge-tree` 或等价 dry merge。
- 能生成候选 rebase 时运行 `git range-diff`
6. 分类 release。
- `safe-candidate`:没有 hot zone、没有 KT patch 文件重叠、lockfile/build 变化通过静态检查。
- `manual-review`:命中 hot zone、命中重叠、package/build 图变化、或 range-diff 不平凡。
- `blocked`dry merge 冲突、依赖安装/构建失败、artifact 结构变化、上游元数据无法验证。
7. 输出 artifact。
- Markdown 人类报告。
- JSON 机器报告。
- 上游 delta、KT patch、重叠文件、hot zone 文件列表。
- 推荐下一步动作。
8. 可选创建候选分支。
- 只有 `safe-candidate` 或人工显式要求时允许。
- 分支名:`kt/sync/<release-tag>`。
- 候选分支是在上游 release 上应用 KT patch不合回 `FORK_BRANCH`
- 候选分支只能推到 KT 可写 remote。
- 候选分支永远不能自动 merge。
9. 归档 NAS 本地 artifact。
- 报告写入 `/vol1/docker/kt-codex/artifacts/napcat-upstream-sync/<timestamp>`
- Jenkins 归档同一份报告。
- 报告必须包含 NAS runner hostname、可用时的 Codex CLI version、Git version、workspace manifest revision。
- 启用 Codex CLI 自动化时,必须归档精确 prompt、context packet、JSONL event log、final response 和 output schema 校验结果。
### Hot Zone
这些路径视为高风险区域:
```text
packages/napcat-core/**/login*
packages/napcat-core/**/qrcode*
packages/napcat-shell/**
packages/napcat-framework/**
packages/napcat-webui-backend/**/QQLogin*
packages/napcat-webui-backend/**/Data*
packages/napcat-webui-backend/**/auth*
packages/napcat-webui-frontend/**
packages/napcat-adapter/**
packages/napcat-onebot/**
packages/napcat-vite/**
package.json
pnpm-lock.yaml
tsconfig*.json
vite*.ts
```
这份列表故意保守。只要上游改到登录状态、二维码生成、WebUI auth、OneBot 启动或构建打包,就必须人工审查。
### 审计报告契约
JSON artifact
```json
{
"upstream": {
"repo": "NapNeko/NapCatQQ",
"releaseTag": "v0.0.0",
"releaseCommit": "0000000000000000000000000000000000000000",
"publishedAt": "2026-06-24T00:00:00Z",
"releaseUrl": "https://github.com/NapNeko/NapCatQQ/releases/tag/v0.0.0"
},
"fork": {
"branch": "kt/runtime-maintenance",
"headCommit": "0000000000000000000000000000000000000000",
"lastAcceptedUpstreamBase": "0000000000000000000000000000000000000000"
},
"classification": "manual-review",
"reasonCodes": ["HOT_ZONE_CHANGED", "FORK_PATCH_OVERLAP"],
"upstreamChangedFiles": [],
"forkPatchFiles": [],
"overlapFiles": [],
"hotZoneFiles": [],
"candidateBranch": null,
"recommendedAction": "Review hot-zone changes before creating a candidate branch."
}
```
报告必须可归档、可分享,不包含 token、secret、私有 env、QQ 密码、验证码 ticket 或 WebUI credential。
## 运行时发布 Job
### 触发方式
- 手动指定已确认的 source ref。
- 候选分支审查通过后可触发。
### 输入
```text
NAPCAT_SOURCE_REF=kt/runtime-maintenance or kt/sync/<tag>
UPSTREAM_RELEASE_TAG=<审计报告里的 tag>
UPSTREAM_RELEASE_COMMIT=<审计报告里的 commit>
RUNTIME_PROFILE=desktop-cn-vN
NAPCAT_BASE_IMAGE=mlikiowa/napcat-docker@sha256:<digest>
API_REF=main
PROMOTE_TO_API=false
CANARY_ACCOUNT_ID=<可选>
```
`NAPCAT_BASE_IMAGE` 必须在 Docker build 前解析为 digest。如果选择基于 `mlikiowa/napcat-docker:latest`,流水线必须先 pull再解析 `RepoDigest`,实际构建和 release marker 都使用 digest。
### 步骤
1. Checkout `NapCatQQ`
- 使用 KT 可写 fork remote。
- checkout `NAPCAT_SOURCE_REF`
- 确认工作区干净。
- 记录 fork commit。
2. 安装和验证源码。
- `pnpm install --frozen-lockfile`
- 登录/运行态聚焦测试。
- `pnpm run typecheck`
- `pnpm run build:webui`
- `pnpm run build:shell`
- `pnpm run build:framework`
3. Checkout API 仓库作为构建集成依赖。
- 使用 `API_REF`
- 执行 stage-build 脚本并传入 `--napcat-root`
- staged `fork-artifact.json` 必须包含:
- upstream release tag
- upstream release commit
- last accepted upstream base
- fork commit
- dist sha256
- `napcat.mjs` sha256
- base image digest
- Jenkins build URL
4. 构建 NAS 本地 Docker 镜像。
- 从 staged context 构建。
- 先打不可变 tag
```text
kt-napcat-desktop-cn:<upstream-tag>-kt.<jenkins-build>
```
- verify 通过后再打推广别名:
```text
kt-napcat-desktop-cn:<runtime-profile>
```
5. 验证镜像。
- 起临时容器。
- 执行 `/ci/napcat-desktop-cn/verify.sh`
- 验证 locale、时区、字体、XDG、容器隐藏标记、fork marker、artifact hash、关键 runtime symbol。
- 删除临时容器。
- 记录 image ID。
6. 归档 release 元数据。
- `napcat-runtime-release.json`
- Docker image inspect 输出
- `fork-artifact.json`
- 测试摘要
7. 可选推广到 API。
- `PROMOTE_TO_API=true` 时,触发 API Jenkins并传入 runtime image/profile 参数。
- API 发布阶段通过受控参数或生成 overlay 更新 K8s runtime env。
- 不要求每次手工编辑 `k8s/prod/api.yaml`
8. 部署观测和 smoke。
- 执行 API `deploy-observation`
- 验证 API `/health/runtime`
- 验证 K8s deployment generation、pod image、ready replicas、restart count 和日志。
- 涉及 QQ 登录行为时,必须等真实账号 smoke 或明确记录“等待人工扫码”的状态,不能只凭 Jenkins/K8s 完成。
运行时发布 job 必须使用干净的 NAS job workspace而不是长期远程开发 workspace避免远程 Codex 会话和 Jenkins 发布同时修改同一个 checkout。
## ktWorkflow Codex CLI 自动化
实现阶段应在 `mcp/ktWorkflow` 增加 NapCat 自动化模块,例如:
```text
mcp/ktWorkflow/
prompts/napcat/
upstream-audit.md
sync-candidate-review.md
runtime-release-readiness.md
remote-dev-handoff.md
schemas/
upstream-audit.schema.json
runtime-release-readiness.schema.json
src/tools/napcatAutomation.ts
src/tools/napcatAutomation.types.ts
src/tools/napcatAutomation.prompts.ts
```
提示词和 schema 属于自动化契约,所以需要进 Git。secret 绝不写进 prompt。每个 ktWorkflow tool 接收显式输入、生成 context packet、可选调用 Codex CLI、校验输出 schema并把 artifact 写到本地 `.kt-workspace` 或 NAS `/vol1/docker/kt-codex/artifacts`
MCP tools 应包含:
```text
kt_napcat_upstream_audit
kt_napcat_sync_candidate_review
kt_napcat_runtime_release_readiness
kt_napcat_remote_dev_handoff
kt_nas_codex_bootstrap_plan
```
CLI scripts 应包含:
```text
pnpm --dir mcp/ktWorkflow run napcat-upstream-audit -- --execute --artifact-root /vol1/docker/kt-codex/artifacts/napcat-upstream-sync
pnpm --dir mcp/ktWorkflow run napcat-sync-candidate-review -- --source-ref kt/sync/<tag>
pnpm --dir mcp/ktWorkflow run napcat-runtime-release-readiness -- --release-artifact <path>
pnpm --dir mcp/ktWorkflow run nas-codex-bootstrap -- --dry-run
```
这些 scripts 是唯一支持的 scheduler 入口。Jenkins 和 `systemd` 只调用这些 scripts不复制审计算法。
### `upstream-audit.md`
用途:根据已采集事实,对上游 latest release 做同步风险分类。
提示词契约:
```text
You are auditing whether KT's NapCatQQ fork can safely sync an upstream NapNeko/NapCatQQ release.
Inputs:
- Upstream release metadata.
- Last accepted upstream base.
- KT fork branch and head commit.
- Upstream changed file list and commit list.
- KT fork patch file list and commit list.
- Hot-zone file list.
- Dry merge or range-diff output.
Rules:
- Do not run merge, commit, push, deploy, or edit files.
- Do not infer success from missing data.
- Classify exactly one of: safe-candidate, manual-review, blocked.
- Explain every reasonCode with file-level evidence.
- Recommend next action: no-op, create candidate branch, request human review, or block.
Output:
- JSON matching mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json.
- A concise Markdown summary safe for Jenkins artifacts.
```
NAS 推荐命令形态:
```bash
CODEX_HOME=/vol1/docker/kt-codex/home/.codex \
codex exec \
--cd /vol1/docker/kt-codex/workspace/KT \
--profile automation \
--sandbox workspace-write \
--ask-for-approval never \
--json \
--ephemeral \
--output-schema /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json \
--output-last-message "$ARTIFACT_DIR/codex-upstream-audit.md" \
- < "$CONTEXT_PACKET"
```
这个命令由 ktWorkflow 封装并捕获 JSONL event stream。这里用 `workspace-write` 是为了允许 Codex 只写 job artifact 目录下的报告文件。prompt 仍禁止源码编辑、push、merge 和 deployment。
### `sync-candidate-review.md`
用途:在任何 merge 之前审查生成的 `kt/sync/<release-tag>` 候选分支。
提示词契约:
```text
Review the candidate branch against KT's NapCat login/runtime requirements.
Must inspect:
- login service reset behavior
- QR refresh and stale QR handling
- WebUI login runtime state
- captcha and new-device flow boundaries
- package/build output structure
- test delta and missing coverage
Must not:
- edit files
- commit
- push
- mark production ready
Output:
- Critical/Important findings first.
- Required tests before merge.
- Whether this candidate can proceed to manual code review.
```
### `runtime-release-readiness.md`
用途:判断已构建运行时镜像是否可以推广到 API。
输入:
- NapCatQQ test/typecheck/build 摘要。
- `fork-artifact.json`
- Docker image inspect 输出。
- `verify.sh` 输出。
- API integration test 输出。
- 计划发布的 runtime image/profile 值。
输出必须拆开:
- source validation
- image validation
- API promotion readiness
- online smoke readiness
- rollback pointer
### `remote-dev-handoff.md`
用途:人工打开 NAS Codex CLI 远程开发前,生成安全交接。
需要总结:
- 每个仓库的当前分支和 dirty 状态。
- 相关 artifact 目录。
- 最新 Jenkins job 状态。
- 当前 blocker。
- 下一步可安全执行的精确命令。
它不能要求 Codex 自动修改文件。
## API 推广契约
API Jenkinsfile 增加可选参数:
```text
QQBOT_NAPCAT_IMAGE_OVERRIDE=
QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE=
```
设置后K8s deploy 阶段必须把它们作为 API deployment 的运行时 env。实现可以用 generated manifest overlay也可以用 `kubectl set env`,但不能把 secret 写进 Git并且要在部署证据里记录最终生效值。
API 测试需要保证:
- 生产不依赖 `latest`
- runtime image override 路径显式存在。
- 默认 profile 仍是已知安全 fallback。
- API 不会静默降级到旧 runtime profile。
## 数据流
```mermaid
sequenceDiagram
participant Upstream as GitHub 上游
participant Nas as NAS Runner
participant Sync as 上游审计 Jenkins
participant Fork as KT NapCatQQ Fork
participant Release as 运行时发布 Jenkins
participant ApiRepo as API 仓库
participant Docker as NAS Docker
participant ApiDeploy as API Jenkins
Nas->>Sync: 定时触发
Sync->>Upstream: 读取 latest release 元数据
Sync->>Fork: fetch 维护分支
Sync->>Sync: 对比上游 delta 与 KT fork patch
Sync-->>Fork: 可选 kt/sync/<tag>,不自动合并
Sync-->>Sync: 归档审计报告
Release->>Fork: checkout 已确认 source ref
Release->>Release: test/typecheck/build webui/shell/framework
Release->>ApiRepo: 执行 stage-build 脚本
Release->>Docker: 构建并验证 kt-napcat-desktop-cn 镜像
Release->>ApiDeploy: 可选带 image/profile 参数推广
ApiDeploy-->>Release: rollout 和 smoke 证据
```
## 错误处理
- GitHub API 限流或不可用:审计标记 `blocked`,给出重试建议;除非明确允许,不用陈旧数据推断 latest。
- NAS runner 缺 Node、pnpm、Docker、Git、Codex 或 KT workspace manifest标记 setup incomplete不启用定时触发。
- NAS Codex auth 缺失:远程开发不可用,直到交互式登录或可信 auth 文件同步完成;如果确定性 Jenkins 审计不需要 Codex auth则审计仍可运行。
- NAS Codex config 与生成模板漂移remote-development smoke 失败,要求重新生成配置。
- ktWorkflow NapCat automation tool、prompt 或 schema 缺失:确定性审计仍可运行,但 AI 辅助分类标记为 unavailable。
- Codex CLI 输出不符合 schema审计标记 `blocked`,并归档原始输出供人工查看。
- 笔记本离线或不在局域网:定时审计和运行时发布仍必须在 NAS 继续运行。
- 上游 release tag 无法解析 commit标记 `blocked`
- fork 可写 remote 指向上游push 前失败。
- 工作区不干净:候选或发布前失败。
- 命中 hot-zone overlap标记 `manual-review`,除非人工明确要求,否则不创建、不合并候选。
- dry merge 冲突:标记 `blocked`
- `pnpm install`、测试、typecheck、shell/framework 构建失败:发布失败,不构建、不推广镜像。
- Docker base image 不能解析 digestbuild 前失败。
- `verify.sh` 失败:删除验证容器,保留 artifact不打推广 tag。
- API 推广部署成功但线上 smoke 失败:部署证据和功能闭环分开记录,并给出回滚步骤。
## 回滚
运行时回滚由 API runtime image/profile 控制:
1. 从 release artifact 找到上一个已验证 runtime image/profile。
2. 触发 API Jenkins传入旧的 `QQBOT_NAPCAT_IMAGE_OVERRIDE` 和 profile。
3. 观察 K8s rollout。
4. 已存在的线上 NapCat 容器不自动重建。账号级迁移必须显式执行,因为容器重建会影响 QQ 设备/登录风控。
## 验证策略
### 上游同步 Job
本地/job 验证:
```powershell
pnpm --dir mcp/ktWorkflow run self-test
git diff --check
```
Jenkins dry run 必须看到:
- latest release 元数据已解析。
- last accepted upstream base 已解析。
- 上游 delta 文件列表。
- KT fork patch 文件列表。
- overlap/hot-zone 分类。
- 报告 artifact 路径。
- NAS runner hostname 和 artifact 目录。
- workspace manifest revision。
### NAS Codex 与 Workspace
NAS setup 验证:
```bash
node --version
corepack --version
pnpm --version
git --version
docker --version
codex --version
cd /vol1/docker/kt-codex/workspace/KT
git status --short --branch
pnpm --dir mcp/ktWorkflow run self-test
```
远程开发 smoke
- `codex --version` 返回 NAS 已安装的最新 CLI 版本。
- `CODEX_HOME` 指向 `/vol1/docker/kt-codex/home/.codex`
- `ktWorkflow` MCP 能从 Linux 路径启动。
- 上游审计 dry-run 能把报告写入 `/vol1/docker/kt-codex/artifacts`
- NAS `config.toml` 中没有笔记本 Windows-only 路径。
Codex 自动化提示词 smoke
```bash
CODEX_HOME=/vol1/docker/kt-codex/home/.codex \
codex exec --cd /vol1/docker/kt-codex/workspace/KT \
--profile automation \
--sandbox workspace-write \
--ask-for-approval never \
--json \
--ephemeral \
--output-schema /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json \
--output-last-message /vol1/docker/kt-codex/artifacts/smoke/codex-upstream-audit.md \
- < /vol1/docker/kt-codex/artifacts/smoke/context-packet.md
```
预期结果JSON 符合 schema、源码无改动、Markdown 报告写入 artifact 目录。首选 smoke 通过 ktWorkflow 执行:
```bash
pnpm --dir /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow run napcat-upstream-audit -- --dry-run --use-codex
```
### 运行时发布 Job
NapCatQQ
```powershell
corepack pnpm install --frozen-lockfile
corepack pnpm --filter napcat-test run test -- loginQrcodeRefresh webuiLoginSourceWiring webuiQQLoginHandlers webuiLoginRuntime
corepack pnpm run typecheck
corepack pnpm run build:webui
corepack pnpm run build:shell
corepack pnpm run build:framework
```
API 集成:
```powershell
corepack pnpm exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runTestsByPath --runInBand
corepack pnpm run typecheck
git diff --check
```
NAS 镜像:
```bash
docker build --build-arg NAPCAT_BASE_IMAGE="$NAPCAT_BASE_IMAGE_DIGEST" -t "$IMMUTABLE_TAG" -f "$STAGED_CONTEXT/ci/napcat-desktop-cn/Dockerfile" "$STAGED_CONTEXT"
docker run -d --name "$VERIFY_CONTAINER" "$IMMUTABLE_TAG"
docker exec "$VERIFY_CONTAINER" sh /ci/napcat-desktop-cn/verify.sh
docker rm -f "$VERIFY_CONTAINER"
docker tag "$IMMUTABLE_TAG" "$PROMOTION_TAG"
```
线上:
- API `deploy-observation` 通过。
- API `/health/runtime` 通过。
- 登录运行时发布必须用 canary 账号验证:要么真实登录成功,要么进入清晰的验证码/新设备/人工扫码 pending 状态,且二维码必须是 freshSSE/Admin 状态必须正确。
## 完成标准
- `NapCatQQ` 有独立 Jenkins 发布链路。
- NAS 有常驻 Codex CLI 和 KT 全量 workspace 环境,可支撑定时审计和远程开发。
- 定时审计能发现上游 latest release 并生成安全报告。
- 定时审计从 NAS 服务/Jenkins 运行,不从笔记本运行。
- Codex CLI 自动化通过 ktWorkflow 执行提示词已版本化、schema 校验,并随每次 AI 辅助审计归档。
- 上游同步不会自动合并进 KT 维护分支。
- hot-zone 冲突会阻断或进入人工审查。
- 运行时镜像只从已确认 fork ref 构建,并经过容器内 verify。
- API 部署通过显式 image/profile 推广契约消费运行时镜像。
- Jenkins/K8s 部署证据和线上 QQBot/NapCat smoke 证据都齐全后,才允许宣称发布完成。

View File

@ -136,10 +136,6 @@ const napcatRootArg = readArg('napcat-root');
const napcatRoot = resolve(napcatRootArg); const napcatRoot = resolve(napcatRootArg);
const outputRoot = resolve(apiRoot, readArg('out', DEFAULT_OUTPUT)); const outputRoot = resolve(apiRoot, readArg('out', DEFAULT_OUTPUT));
const upstreamBaseCommit = readArg('upstream-base-commit', DEFAULT_UPSTREAM_BASE); const upstreamBaseCommit = readArg('upstream-base-commit', DEFAULT_UPSTREAM_BASE);
const upstreamReleaseTag = readArg('upstream-release-tag', 'unknown');
const upstreamReleaseCommit = readArg('upstream-release-commit', upstreamBaseCommit);
const napcatBaseImageDigest = readArg('napcat-base-image-digest', '');
const jenkinsBuildUrl = readArg('jenkins-build-url', '');
const shellDist = resolve(napcatRoot, 'packages/napcat-shell/dist'); const shellDist = resolve(napcatRoot, 'packages/napcat-shell/dist');
const napcatMjs = resolve(shellDist, 'napcat.mjs'); const napcatMjs = resolve(shellDist, 'napcat.mjs');
@ -167,12 +163,8 @@ const marker = {
builtAt: new Date().toISOString(), builtAt: new Date().toISOString(),
distSha256: sha256Directory(shellDist), distSha256: sha256Directory(shellDist),
forkCommit: gitCommit(napcatRoot), forkCommit: gitCommit(napcatRoot),
jenkinsBuildUrl,
napcatBaseImageDigest,
napcatMjsSha256: sha256File(napcatMjs), napcatMjsSha256: sha256File(napcatMjs),
upstreamBaseCommit, upstreamBaseCommit,
upstreamReleaseCommit,
upstreamReleaseTag,
}; };
writeFileSync( writeFileSync(

View File

@ -141,7 +141,7 @@ export function rewriteNapcatTextResponse(input: RewriteTextResponseInput) {
)}/webui`; )}/webui`;
const rewritten = input.body.replace( const rewritten = input.body.replace(
/(^|[\s"'`(=,:])\/(webui|api|files|plugin)(?=\/|[?#"'`)]|$)/g, /(^|[\s"'`(=,:])\/(webui|api|File|plugin)(?=\/|[?#"'`)]|$)/g,
(_match, leader: string, root: string) => (_match, leader: string, root: string) =>
`${leader}${gatewayWebuiPrefix}/${root}`, `${leader}${gatewayWebuiPrefix}/${root}`,
); );

View File

@ -581,13 +581,7 @@ export class ToolsService {
* @param err - * @param err -
*/ */
isNapcatAlreadyLoggedInError(err: unknown) { isNapcatAlreadyLoggedInError(err: unknown) {
const message = this.getErrorMessage(err); return this.getErrorMessage(err).includes('QQ Is Logined');
return (
message.includes('QQ Is Logined') ||
(message.includes('当前账号') &&
message.includes('已登录') &&
message.includes('无法重复登录'))
);
} }
/** /**

View File

@ -34,10 +34,6 @@ type PendingQrcodeUpdateOptions = {
clearStaleQrcode?: boolean; clearStaleQrcode?: boolean;
requireFresh?: boolean; requireFresh?: boolean;
}; };
type ScanStatusMonitorDeadline = {
expiresAt: number;
qrcode: string;
};
@Injectable() @Injectable()
export class QqbotNapcatLoginService { export class QqbotNapcatLoginService {
@ -49,14 +45,6 @@ export class QqbotNapcatLoginService {
string, string,
Set<(event: QqbotLoginScanEvent) => void> Set<(event: QqbotLoginScanEvent) => void>
> = {}; > = {};
private readonly scanStatusMonitorTimers: Record<
string,
NodeJS.Timeout | undefined
> = {};
private readonly scanStatusMonitorDeadlines: Record<
string,
ScanStatusMonitorDeadline | undefined
> = {};
private readonly refreshStartTasks: Record< private readonly refreshStartTasks: Record<
string, string,
Promise<QqbotLoginScanResult> | undefined Promise<QqbotLoginScanResult> | undefined
@ -65,10 +53,7 @@ export class QqbotNapcatLoginService {
/** /**
* NapCat回调状态 * NapCat回调状态
*/ */
clear: () => { clear: () => this.loginSessionStore.clear(),
this.stopAllScanStatusMonitors();
this.loginSessionStore.clear();
},
/** /**
* NapCat回调数据 * NapCat回调数据
* @param sessionId - NapCat IDNapCat * @param sessionId - NapCat IDNapCat
@ -1522,9 +1507,6 @@ export class QqbotNapcatLoginService {
status, status,
step, step,
}); });
if (this.shouldMonitorScanStatus(session)) {
this.startScanStatusMonitor(session);
}
} }
/** /**
@ -1727,130 +1709,10 @@ export class QqbotNapcatLoginService {
* @param sessionId - NapCat IDNapCat * @param sessionId - NapCat IDNapCat
*/ */
private cleanupSessionEvents(sessionId: string) { private cleanupSessionEvents(sessionId: string) {
this.stopScanStatusMonitor(sessionId);
delete this.sessionEventLogCache[sessionId]; delete this.sessionEventLogCache[sessionId];
delete this.sessionEventListenerCache[sessionId]; delete this.sessionEventListenerCache[sessionId];
} }
/**
* Determines whether a pending QR session needs server-side polling so SSE can progress without browser polling.
* @param session - Login session whose QR, preparation flags, and terminal status decide monitor ownership.
* @returns True when the backend should keep reconciling NapCat status for this session.
*/
private shouldMonitorScanStatus(session: QqbotLoginScanSession) {
return (
session.status === 'pending' &&
!!session.qrcode &&
!session.preparingContainer &&
!session.preparingRelogin
);
}
/**
* Starts one bounded status monitor for a QR session and lets the timer avoid holding the Node process open.
* @param session - Pending QR session whose id is used to drive later status reconciliation.
*/
private startScanStatusMonitor(session: QqbotLoginScanSession) {
this.ensureScanStatusMonitorDeadline(session);
if (this.hasScanStatusMonitorDeadlinePassed(session)) {
void this.expireSession(session);
return;
}
if (this.scanStatusMonitorTimers[session.id]) return;
const timer = setTimeout(() => {
this.scanStatusMonitorTimers[session.id] = undefined;
void this.runScanStatusMonitor(session.id);
}, this.getLoginPollIntervalMs());
timer.unref?.();
this.scanStatusMonitorTimers[session.id] = timer;
}
/**
* Stops any server-side status monitor for a login session.
* @param sessionId - Session key whose timer should no longer reconcile NapCat state.
*/
private stopScanStatusMonitor(sessionId: string) {
const timer = this.scanStatusMonitorTimers[sessionId];
if (timer) clearTimeout(timer);
delete this.scanStatusMonitorTimers[sessionId];
delete this.scanStatusMonitorDeadlines[sessionId];
}
/**
* Stops every server-side QR status monitor before the backing session store is cleared.
*/
private stopAllScanStatusMonitors() {
Object.keys(this.scanStatusMonitorTimers).forEach((sessionId) => {
this.stopScanStatusMonitor(sessionId);
});
}
/**
* Captures the active QR code deadline so monitor polling cannot extend the same QR session forever.
* @param session - Pending QR session whose current QR URL and expiry form the monitor deadline snapshot.
*/
private ensureScanStatusMonitorDeadline(session: QqbotLoginScanSession) {
if (!session.qrcode) return;
const current = this.scanStatusMonitorDeadlines[session.id];
if (current?.qrcode === session.qrcode) return;
this.scanStatusMonitorDeadlines[session.id] = {
expiresAt: session.expiresAt,
qrcode: session.qrcode,
};
}
/**
* Checks the monitor-owned QR deadline instead of the session expiry that status polling may renew.
* @param session - Pending QR session whose monitor snapshot decides terminal expiry.
* @returns True when the monitored QR should be expired by the backend monitor.
*/
private hasScanStatusMonitorDeadlinePassed(
session: QqbotLoginScanSession,
) {
const deadline = this.scanStatusMonitorDeadlines[session.id];
if (deadline && session.qrcode && deadline.qrcode !== session.qrcode) {
this.ensureScanStatusMonitorDeadline(session);
return false;
}
return !!deadline && Date.now() > deadline.expiresAt;
}
/**
* Polls the same status path used by Admin so SSE can emit success/expired events after QR generation.
* @param sessionId - Pending QR session id to reload from the session store before each poll.
*/
private async runScanStatusMonitor(sessionId: string) {
try {
const session = await this.loginSessionStore.get(sessionId);
if (!session || !this.shouldMonitorScanStatus(session)) return;
if (this.hasScanStatusMonitorDeadlinePassed(session)) {
await this.expireSession(session);
return;
}
const result = await this.status(sessionId);
if (result.status !== 'pending') return;
const current =
this.loginSessionStore.getCached(sessionId) ||
(await this.loginSessionStore.get(sessionId));
if (current && this.shouldMonitorScanStatus(current)) {
if (this.hasScanStatusMonitorDeadlinePassed(current)) {
await this.expireSession(current);
return;
}
this.startScanStatusMonitor(current);
}
} catch {
const current = this.loginSessionStore.getCached(sessionId);
if (current && this.shouldMonitorScanStatus(current)) {
if (this.hasScanStatusMonitorDeadlinePassed(current)) {
await this.expireSession(current);
return;
}
this.startScanStatusMonitor(current);
}
}
}
/** /**
* NapCat * NapCat
* @param session - session 使 `status``errorMessage``expiresAt``qrcode` * @param session - session 使 `status``errorMessage``expiresAt``qrcode`

View File

@ -351,11 +351,6 @@ describe('NapcatWebuiProxyService redirect rewriting', () => {
let credentialClient: { let credentialClient: {
getCredential: jest.Mock; getCredential: jest.Mock;
}; };
let upstreamRequests: Array<{
authorization?: string;
method?: string;
url?: string;
}>;
/** /**
* Starts a tiny NapCat-like upstream that redirects `/webui` to `/webui/`. * Starts a tiny NapCat-like upstream that redirects `/webui` to `/webui/`.
@ -363,25 +358,6 @@ describe('NapcatWebuiProxyService redirect rewriting', () => {
*/ */
async function startRedirectUpstream() { async function startRedirectUpstream() {
const server = createServer((req, res) => { const server = createServer((req, res) => {
upstreamRequests.push({
authorization: req.headers.authorization,
method: req.method,
url: req.url,
});
if (req.url === '/api/File/list?path=%2F') {
res.statusCode = HttpStatus.OK;
res.setHeader('content-type', 'application/json; charset=UTF-8');
res.end(
JSON.stringify({
code: 0,
data: [{ isDirectory: true, name: 'app', size: 0 }],
message: 'success',
}),
);
return;
}
if (req.url === '/webui') { if (req.url === '/webui') {
res.statusCode = HttpStatus.MOVED_PERMANENTLY; res.statusCode = HttpStatus.MOVED_PERMANENTLY;
res.setHeader('content-type', 'text/html; charset=UTF-8'); res.setHeader('content-type', 'text/html; charset=UTF-8');
@ -410,7 +386,6 @@ describe('NapcatWebuiProxyService redirect rewriting', () => {
} }
beforeEach(async () => { beforeEach(async () => {
upstreamRequests = [];
const started = await startRedirectUpstream(); const started = await startRedirectUpstream();
upstream = started.server; upstream = started.server;
upstreamBaseUrl = started.baseUrl; upstreamBaseUrl = started.baseUrl;
@ -471,26 +446,4 @@ describe('NapcatWebuiProxyService redirect rewriting', () => {
`/napcat-webui/session/${SESSION_ID}/webui/webui/`, `/napcat-webui/session/${SESSION_ID}/webui/webui/`,
); );
}); });
it('forwards NapCat File API requests under /api without duplicating the Gateway session prefix', async () => {
const response = await request(app.getHttpServer())
.get(`/napcat-webui/session/${SESSION_ID}/webui/api/File/list?path=%2F`)
.expect(HttpStatus.OK);
expect(response.body).toEqual({
code: 0,
data: [{ isDirectory: true, name: 'app', size: 0 }],
message: 'success',
});
expect(upstreamRequests).toContainEqual({
authorization: 'Bearer credential-1',
method: 'GET',
url: '/api/File/list?path=%2F',
});
expect(
upstreamRequests.some((item) =>
item.url?.includes('/api/napcat-webui/session/'),
),
).toBe(false);
});
}); });

View File

@ -17,7 +17,7 @@ describe('NapcatWebuiProxyService response rewriting', () => {
body: [ body: [
'<script type="module" src="/webui/assets/index.js"></script>', '<script type="module" src="/webui/assets/index.js"></script>',
'<link rel="stylesheet" href="/webui/assets/index.css">', '<link rel="stylesheet" href="/webui/assets/index.css">',
'<script>const baseURL="/api"; const file="/File/list?path=%2F"; const theme="/files/theme.css";</script>', '<script>const baseURL="/api"; const file="/File/font/upload/webui";</script>',
'url("/webui/fonts/CustomFont.woff")', 'url("/webui/fonts/CustomFont.woff")',
].join(''), ].join(''),
sessionId: 'sess_1', sessionId: 'sess_1',
@ -32,10 +32,8 @@ describe('NapcatWebuiProxyService response rewriting', () => {
expect(rewritten).toContain( expect(rewritten).toContain(
'baseURL="/napcat-webui/session/sess_1/webui/api"', 'baseURL="/napcat-webui/session/sess_1/webui/api"',
); );
expect(rewritten).toContain('file="/File/list?path=%2F"');
expect(rewritten).not.toContain('/webui/api/napcat-webui/session');
expect(rewritten).toContain( expect(rewritten).toContain(
'theme="/napcat-webui/session/sess_1/webui/files/theme.css"', 'file="/napcat-webui/session/sess_1/webui/File/font/upload/webui"',
); );
expect(rewritten).toContain( expect(rewritten).toContain(
'url("/napcat-webui/session/sess_1/webui/webui/fonts/CustomFont.woff")', 'url("/napcat-webui/session/sess_1/webui/webui/fonts/CustomFont.woff")',

View File

@ -50,10 +50,6 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
expect(script).toContain('napcatMjsSha256'); expect(script).toContain('napcatMjsSha256');
expect(script).toContain('forkCommit'); expect(script).toContain('forkCommit');
expect(script).toContain('upstreamBaseCommit'); expect(script).toContain('upstreamBaseCommit');
expect(script).toContain('upstreamReleaseTag');
expect(script).toContain('upstreamReleaseCommit');
expect(script).toContain('napcatBaseImageDigest');
expect(script).toContain('jenkinsBuildUrl');
expect(script).toContain('packages/napcat-shell/dist'); expect(script).toContain('packages/napcat-shell/dist');
expect(script).toContain('fork-artifact.json'); expect(script).toContain('fork-artifact.json');
expect(script).toContain('.kt-workspace/napcat-desktop-cn-build'); expect(script).toContain('.kt-workspace/napcat-desktop-cn-build');
@ -63,21 +59,6 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
expect(script).toContain('Refusing to delete unsafe output root'); expect(script).toContain('Refusing to delete unsafe output root');
}); });
/**
* Verifies the checked-in operator guide documents the full release evidence contract.
*/
it('documents complete release metadata for staging NapCat Shell artifacts', () => {
const readme = readSource('ci/napcat-desktop-cn/README.md');
expect(readme).toContain('--upstream-release-tag');
expect(readme).toContain('--upstream-release-commit');
expect(readme).toContain('--napcat-base-image-digest');
expect(readme).toContain('--jenkins-build-url');
expect(readme).toContain('NapCat.Shell.zip');
expect(readme).toContain('fork-artifact.json');
expect(readme).toContain('sha256:');
});
it('uses source-built NapCat Shell artifact instead of bundled JS patching', () => { it('uses source-built NapCat Shell artifact instead of bundled JS patching', () => {
const dockerfile = readSource('ci/napcat-desktop-cn/Dockerfile'); const dockerfile = readSource('ci/napcat-desktop-cn/Dockerfile');
const verify = readSource('ci/napcat-desktop-cn/verify.sh'); const verify = readSource('ci/napcat-desktop-cn/verify.sh');

View File

@ -1,39 +0,0 @@
import { readFileSync } from 'fs';
import { join } from 'path';
const repoRoot = join(__dirname, '../../../..');
/**
* Reads a repo file as UTF-8 text for static runtime promotion contract assertions.
* @param relativePath - Repository-relative path under `Node/kt-template-online-api`.
* @returns The file contents as UTF-8 text.
*/
const readSource = (relativePath: string) =>
readFileSync(join(repoRoot, relativePath), 'utf8');
describe('NapCat runtime promotion contract', () => {
it('exposes Jenkins parameters for runtime image and profile overrides', () => {
const jenkinsfile = readSource('Jenkinsfile');
expect(jenkinsfile).toContain("string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE'");
expect(jenkinsfile).toContain(
"string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE'",
);
expect(jenkinsfile).toContain(
'kubectl ${kubeConfigArg} ${namespaceArg} set env',
);
expect(jenkinsfile).toContain(
'QQBOT_NAPCAT_IMAGE=${napcatImageOverride}',
);
expect(jenkinsfile).toContain(
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION=${napcatProfileOverride}',
);
});
it('keeps a safe default runtime profile in the K8s manifest', () => {
const manifest = readSource('k8s/prod/api.yaml');
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v8');
expect(manifest).toContain('value: desktop-cn-v8');
});
});

View File

@ -1054,66 +1054,6 @@ describe('QqbotNapcatLoginService', () => {
); );
}); });
it('confirms real login status when quick login reports already logged in using the NapCat Chinese message', async () => {
const container = {
baseUrl: 'http://127.0.0.1:6103/',
id: 'container-quick-already-online-cn',
name: 'napcat-1914728559',
};
const accountService = {
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
};
const containerService = {
bindAccount: jest.fn().mockResolvedValue(undefined),
resetRuntimeLoginState: jest.fn(),
restartRuntimeContainer: jest.fn(),
};
const refreshService = new QqbotNapcatLoginService(
{ get: jest.fn() } as unknown as ConfigService,
accountService as unknown as QqbotAccountService,
containerService as unknown as QqbotNapcatContainerService,
new ToolsService(),
);
const session = (refreshService as any).createSession({
accountId: 'account-1',
container,
expectedSelfId: '1914728559',
mode: 'refresh',
preparingRelogin: true,
status: 'pending',
});
(refreshService as any).sessions.set(session.id, session);
mockWebuiLoginPost(refreshService, {
quickError: '当前账号(1914728559)已登录,无法重复登录',
});
const getLoginStatus = jest
.spyOn(refreshService as any, 'getLoginStatus')
.mockResolvedValue({ isLogin: true });
jest.spyOn(refreshService as any, 'getLoginInfo').mockResolvedValue({
nickname: 'Mirror',
online: true,
uin: '1914728559',
});
const refreshQrcode = jest
.spyOn(refreshService as any, 'refreshOrGetQrcode')
.mockResolvedValue('fallback-qrcode');
await (refreshService as any).prepareReloginQrcode(session, container);
expect(getLoginStatus).toHaveBeenCalledWith(container, true);
expect(refreshQrcode).not.toHaveBeenCalled();
expect(session.status).toBe('success');
const events = (refreshService as any).sessionEventLogs.get(session.id);
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
message: 'NapCat 已登录,已确认真实在线状态',
step: 'login-success',
}),
]),
);
});
it('keeps binding errors terminal after already-logged quick status is confirmed online', async () => { it('keeps binding errors terminal after already-logged quick status is confirmed online', async () => {
const container = { const container = {
baseUrl: 'http://127.0.0.1:6103/', baseUrl: 'http://127.0.0.1:6103/',
@ -1174,200 +1114,6 @@ describe('QqbotNapcatLoginService', () => {
expect(steps).toContain('relogin-error'); expect(steps).toContain('relogin-error');
}); });
it('monitors a ready qrcode session and publishes login success without frontend polling', async () => {
const container = {
baseUrl: 'http://127.0.0.1:6103/',
id: 'container-monitor-qrcode',
name: 'napcat-monitor-qrcode',
};
const accountService = {
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
};
const containerService = {
bindAccount: jest.fn().mockResolvedValue(undefined),
};
const monitorService = new QqbotNapcatLoginService(
{
get: jest.fn((key: string) =>
key === 'QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS' ? 10 : undefined,
),
} as unknown as ConfigService,
accountService as unknown as QqbotAccountService,
containerService as unknown as QqbotNapcatContainerService,
new ToolsService(),
);
const session = (monitorService as any).createSession({
accountId: 'account-1',
container,
expectedSelfId: '1914728559',
mode: 'refresh',
qrcode: 'https://txz.qq.com/p?k=ready&f=1600001615',
status: 'pending',
});
(monitorService as any).sessions.set(session.id, session);
jest
.spyOn(monitorService as any, 'getSessionContainer')
.mockResolvedValue(container);
const getLoginStatus = jest
.spyOn(monitorService as any, 'getLoginStatus')
.mockResolvedValue({ isLogin: true });
jest.spyOn(monitorService as any, 'getLoginInfo').mockResolvedValue({
nick: 'Mirror',
online: true,
uin: '1914728559',
});
(monitorService as any).publishScanResultEvent(
session,
'qrcode-ready',
'success',
'登录二维码已生成',
);
await new Promise((resolve) => setTimeout(resolve, 30));
expect(getLoginStatus).toHaveBeenCalledWith(container);
expect(session.status).toBe('success');
expect((monitorService as any).sessionEventLogs.get(session.id)).toEqual(
expect.arrayContaining([
expect.objectContaining({ step: 'login-success' }),
]),
);
});
it('does not let the server-side qrcode monitor extend the original qrcode deadline on temporary errors', async () => {
const container = {
baseUrl: 'http://127.0.0.1:6103/',
id: 'container-monitor-temporary-error',
name: 'napcat-monitor-temporary-error',
};
const monitorService = new QqbotNapcatLoginService(
{
get: jest.fn((key: string) => {
if (key === 'QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS') return 10;
if (key === 'NAPCAT_LOGIN_QR_EXPIRE_MS') return 25;
return undefined;
}),
} as unknown as ConfigService,
{
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
} as unknown as QqbotAccountService,
{
bindAccount: jest.fn().mockResolvedValue(undefined),
} as unknown as QqbotNapcatContainerService,
new ToolsService(),
);
const session = (monitorService as any).createSession({
accountId: 'account-1',
container,
expectedSelfId: '1914728559',
mode: 'refresh',
qrcode: 'https://txz.qq.com/p?k=temporary&f=1600001615',
status: 'pending',
});
(monitorService as any).sessions.set(session.id, session);
jest
.spyOn(monitorService as any, 'getSessionContainer')
.mockResolvedValue(container);
jest
.spyOn(monitorService as any, 'getLoginStatus')
.mockRejectedValue(new Error('NapCat 请求超时'));
const publishResult = jest.spyOn(
monitorService as any,
'publishScanResultEvent',
);
(monitorService as any).publishScanResultEvent(
session,
'qrcode-ready',
'success',
'登录二维码已生成',
);
await new Promise((resolve) => setTimeout(resolve, 80));
expect(session.status).toBe('expired');
expect(publishResult).toHaveBeenCalledWith(
session,
'session-expired',
'error',
expect.any(String),
);
});
it('refreshes the monitor deadline when a new qrcode is published while a timer is already active', async () => {
const container = {
baseUrl: 'http://127.0.0.1:6103/',
id: 'container-monitor-new-qrcode',
name: 'napcat-monitor-new-qrcode',
};
let qrTtlReads = 0;
const monitorService = new QqbotNapcatLoginService(
{
get: jest.fn((key: string) => {
if (key === 'QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS') return 60;
if (key === 'NAPCAT_LOGIN_QR_EXPIRE_MS') {
qrTtlReads += 1;
return qrTtlReads <= 2 ? 25 : 120;
}
return undefined;
}),
} as unknown as ConfigService,
{
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
} as unknown as QqbotAccountService,
{
bindAccount: jest.fn().mockResolvedValue(undefined),
} as unknown as QqbotNapcatContainerService,
new ToolsService(),
);
const firstQrcode = 'https://txz.qq.com/p?k=first&f=1600001615';
const nextQrcode = 'https://txz.qq.com/p?k=next&f=1600001615';
const session = (monitorService as any).createSession({
accountId: 'account-1',
container,
expectedSelfId: '1914728559',
mode: 'refresh',
qrcode: firstQrcode,
status: 'pending',
});
(monitorService as any).sessions.set(session.id, session);
jest
.spyOn(monitorService as any, 'getSessionContainer')
.mockResolvedValue(container);
jest.spyOn(monitorService as any, 'getLoginStatus').mockResolvedValue({
isLogin: false,
qrcodeurl: nextQrcode,
});
const publishResult = jest.spyOn(
monitorService as any,
'publishScanResultEvent',
);
(monitorService as any).publishScanResultEvent(
session,
'qrcode-ready',
'success',
'登录二维码已生成',
);
await new Promise((resolve) => setTimeout(resolve, 10));
session.qrcode = nextQrcode;
(monitorService as any).publishScanResultEvent(
session,
'qrcode-ready',
'success',
'登录二维码已刷新',
);
await new Promise((resolve) => setTimeout(resolve, 80));
expect(session.status).toBe('pending');
expect(
(monitorService as any).scanStatusMonitorDeadlines[session.id]?.qrcode,
).toBe(nextQrcode);
expect(
publishResult.mock.calls.some((call) => call[1] === 'session-expired'),
).toBe(false);
(monitorService as any).sessions.clear();
});
it('does not rebuild even when the refresh session already has a rebuild count', async () => { it('does not rebuild even when the refresh session already has a rebuild count', async () => {
const container = { const container = {
baseUrl: 'http://127.0.0.1:6103/', baseUrl: 'http://127.0.0.1:6103/',