Compare commits
10 Commits
89f68f6db0
...
d9b3b9751a
| Author | SHA1 | Date | |
|---|---|---|---|
| d9b3b9751a | |||
| af2d5e1cd8 | |||
| d6e8ce848c | |||
| 0d1d29be62 | |||
| e6bc9e056e | |||
| 18a74582f1 | |||
| 992efc3a0c | |||
| 62ddd768b3 | |||
| 76736d9f16 | |||
| dfca035749 |
15
API.md
15
API.md
@ -377,12 +377,25 @@ 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。
|
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 默认值。
|
||||||
|
|
||||||
|
API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,且 `fork-artifact.json` 必须包含完整 marker metadata:upstream 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
15
Jenkinsfile
vendored
@ -100,6 +100,8 @@ 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 {
|
||||||
@ -337,6 +339,8 @@ 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("""
|
||||||
@ -367,6 +371,17 @@ 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')} \\
|
||||||
|
|||||||
13
README.md
13
README.md
@ -77,7 +77,18 @@ 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/MAC,NapCat/OneBot 配置 hash,KT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口,watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network,也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v8 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset,且只有明确二维码过期或扫码确认窗口失效的 QR session failure 才自动换码,避免扫码确认期间被 `ErrType=1/ErrCode=1` 抢刷新打断。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context,生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v8` digest。
|
NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MAC,NapCat/OneBot 配置 hash,KT `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 默认值。
|
||||||
|
|
||||||
|
运行时发布时,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。
|
||||||
|
|
||||||
|
|||||||
@ -10,29 +10,47 @@ 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=$baseImage `
|
--build-arg NAPCAT_BASE_IMAGE=$napcatBaseImageDigest `
|
||||||
-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-v6-verify-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
|
$name = "kt-napcat-v8-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`.
|
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.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,618 @@
|
|||||||
|
# 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 1:ktWorkflow 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 2:ktWorkflow 提示词包与安全读取器
|
||||||
|
|
||||||
|
**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 4:Codex 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 5:MCP 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 6:NAS 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 7:API 运行时 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 context,base 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 8:API 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 4:K8s 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 9:NapCatQQ 发布清单与 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-24:NapCatQQ 上游同步与运行时发布流水线实施
|
||||||
|
|
||||||
|
- 范围: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 review,ktWorkflow/API/NapCatQQ 分仓提交。
|
||||||
|
2. **Inline Execution**:本线程用 `superpowers:executing-plans` 批次执行,每个仓库边界做 review checkpoint。
|
||||||
|
|
||||||
|
推荐选 **1**。这条线跨三个仓库和两个运行环境,用 task 级 subagent 更容易守住边界。
|
||||||
@ -0,0 +1,756 @@
|
|||||||
|
# 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.
|
||||||
@ -0,0 +1,756 @@
|
|||||||
|
# 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_HOME,chmod 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 Desktop,NAS 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 不能解析 digest:build 前失败。
|
||||||
|
- `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 状态,且二维码必须是 fresh,SSE/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 证据都齐全后,才允许宣称发布完成。
|
||||||
@ -136,6 +136,10 @@ 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');
|
||||||
|
|
||||||
@ -163,8 +167,12 @@ 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(
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export function rewriteNapcatTextResponse(input: RewriteTextResponseInput) {
|
|||||||
)}/webui`;
|
)}/webui`;
|
||||||
|
|
||||||
const rewritten = input.body.replace(
|
const rewritten = input.body.replace(
|
||||||
/(^|[\s"'`(=,:])\/(webui|api|File|plugin)(?=\/|[?#"'`)]|$)/g,
|
/(^|[\s"'`(=,:])\/(webui|api|files|plugin)(?=\/|[?#"'`)]|$)/g,
|
||||||
(_match, leader: string, root: string) =>
|
(_match, leader: string, root: string) =>
|
||||||
`${leader}${gatewayWebuiPrefix}/${root}`,
|
`${leader}${gatewayWebuiPrefix}/${root}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -581,7 +581,13 @@ export class ToolsService {
|
|||||||
* @param err - 异常或失败对象;提取状态码、错误体、堆栈或失败原因。
|
* @param err - 异常或失败对象;提取状态码、错误体、堆栈或失败原因。
|
||||||
*/
|
*/
|
||||||
isNapcatAlreadyLoggedInError(err: unknown) {
|
isNapcatAlreadyLoggedInError(err: unknown) {
|
||||||
return this.getErrorMessage(err).includes('QQ Is Logined');
|
const message = this.getErrorMessage(err);
|
||||||
|
return (
|
||||||
|
message.includes('QQ Is Logined') ||
|
||||||
|
(message.includes('当前账号') &&
|
||||||
|
message.includes('已登录') &&
|
||||||
|
message.includes('无法重复登录'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -34,6 +34,10 @@ type PendingQrcodeUpdateOptions = {
|
|||||||
clearStaleQrcode?: boolean;
|
clearStaleQrcode?: boolean;
|
||||||
requireFresh?: boolean;
|
requireFresh?: boolean;
|
||||||
};
|
};
|
||||||
|
type ScanStatusMonitorDeadline = {
|
||||||
|
expiresAt: number;
|
||||||
|
qrcode: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotNapcatLoginService {
|
export class QqbotNapcatLoginService {
|
||||||
@ -45,6 +49,14 @@ 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
|
||||||
@ -53,7 +65,10 @@ export class QqbotNapcatLoginService {
|
|||||||
/**
|
/**
|
||||||
* 清理 NapCat回调状态。
|
* 清理 NapCat回调状态。
|
||||||
*/
|
*/
|
||||||
clear: () => this.loginSessionStore.clear(),
|
clear: () => {
|
||||||
|
this.stopAllScanStatusMonitors();
|
||||||
|
this.loginSessionStore.clear();
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 读取 NapCat回调数据。
|
* 读取 NapCat回调数据。
|
||||||
* @param sessionId - NapCat ID;定位本次读取、更新、删除或关联的NapCat。
|
* @param sessionId - NapCat ID;定位本次读取、更新、删除或关联的NapCat。
|
||||||
@ -1507,6 +1522,9 @@ export class QqbotNapcatLoginService {
|
|||||||
status,
|
status,
|
||||||
step,
|
step,
|
||||||
});
|
});
|
||||||
|
if (this.shouldMonitorScanStatus(session)) {
|
||||||
|
this.startScanStatusMonitor(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1709,10 +1727,130 @@ export class QqbotNapcatLoginService {
|
|||||||
* @param sessionId - NapCat ID;定位本次读取、更新、删除或关联的NapCat。
|
* @param sessionId - NapCat ID;定位本次读取、更新、删除或关联的NapCat。
|
||||||
*/
|
*/
|
||||||
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` 字段生成结果。
|
||||||
|
|||||||
@ -351,6 +351,11 @@ 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/`.
|
||||||
@ -358,6 +363,25 @@ 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');
|
||||||
@ -386,6 +410,7 @@ 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;
|
||||||
@ -446,4 +471,26 @@ 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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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/font/upload/webui";</script>',
|
'<script>const baseURL="/api"; const file="/File/list?path=%2F"; const theme="/files/theme.css";</script>',
|
||||||
'url("/webui/fonts/CustomFont.woff")',
|
'url("/webui/fonts/CustomFont.woff")',
|
||||||
].join(''),
|
].join(''),
|
||||||
sessionId: 'sess_1',
|
sessionId: 'sess_1',
|
||||||
@ -32,8 +32,10 @@ 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(
|
||||||
'file="/napcat-webui/session/sess_1/webui/File/font/upload/webui"',
|
'theme="/napcat-webui/session/sess_1/webui/files/theme.css"',
|
||||||
);
|
);
|
||||||
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")',
|
||||||
|
|||||||
@ -50,6 +50,10 @@ 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');
|
||||||
@ -59,6 +63,21 @@ 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');
|
||||||
|
|||||||
39
test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts
Normal file
39
test/modules/qqbot/napcat/napcat-runtime-promotion.spec.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1054,6 +1054,66 @@ 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/',
|
||||||
@ -1114,6 +1174,200 @@ 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/',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user