Compare commits
20 Commits
7075d863c2
...
1abfcbf59d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1abfcbf59d | |||
| f9ce9b6015 | |||
| 36da962bb0 | |||
| 4fc7c4a776 | |||
| d53b17f6f1 | |||
| 30222fd85e | |||
| fc750ddcf1 | |||
| 6fc5e65986 | |||
| b767732fd3 | |||
| c16a867c60 | |||
| 3f7ed99aec | |||
| 99408d3cec | |||
| 7619dd5928 | |||
| 1f60506c89 | |||
| 4b18c1bec8 | |||
| d5e9bf5b01 | |||
| e29ce3be86 | |||
| 9052ecbc2a | |||
| efc6f50199 | |||
| 32d02b5a8f |
28
README.md
28
README.md
@ -15,7 +15,8 @@
|
||||
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
||||
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
||||
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
||||
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、稳定 `02:42:*` MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
|
||||
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、实体 OUI 风格 MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
|
||||
- 生成 NapCatQQ 上游同步与 runtime 发布自动化入口:MCP/CLI 默认 dry-run,覆盖 upstream audit、sync candidate review、runtime readiness、remote dev handoff 和 NAS Codex bootstrap plan;upstream audit 在显式 `execute=true` 且 `useCodex=true` 时可运行 Codex,NAS Codex bootstrap 在显式 `execute=true` 时只返回目录准备和 CLI 包检查/安装计划,不复制 secrets、不写 systemd、不触发发布。
|
||||
- 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。
|
||||
- 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。
|
||||
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
||||
@ -40,6 +41,11 @@ pnpm run obsidian-context -- --module ktWorkflow
|
||||
pnpm run obsidian-validate
|
||||
pnpm run obsidian-sync
|
||||
pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --doc-sync "无需文档更新" --cleanup "cleanup-history dry-run deleted=0" --cleanup-final-deleted 0 --review "global-review findings=0" --superpowers-review "Superpowers reviewer completed; no Critical/Important findings" --problem "无新卡点" --solution "无新增稳定解法"
|
||||
pnpm run napcat-upstream-audit -- --artifact-root .kt-workspace/test-artifacts/napcat-upstream-sync
|
||||
pnpm run napcat-sync-candidate-review
|
||||
pnpm run napcat-runtime-release-readiness
|
||||
pnpm run napcat-remote-dev-handoff
|
||||
pnpm run nas-codex-bootstrap
|
||||
pnpm run napcat-device-profile-check
|
||||
pnpm run cleanup-history -- --dry-run
|
||||
pnpm run cleanup-history -- --execute
|
||||
@ -49,6 +55,15 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
|
||||
如果本机 Node/npm/pnpm 版本不对,先执行 `nvm ls` 查看已安装版本,再用 `nvm use <version>` 切换到目标版本;不要先扫盘找 `node.exe` 路径。
|
||||
|
||||
## Jenkins 入口模板
|
||||
|
||||
- `ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile` 是 NapCatQQ 上游审计入口,默认只调用 ktWorkflow 的 `napcat-upstream-audit` dry-run,保留人工审查边界。
|
||||
- Codex 上游审计必须同时设置 `RUN_CODEX_AUDIT=true` 并填写 `CONFIRM_CODEX_WORKSPACE_WRITE=RUN_CODEX_WORKSPACE_WRITE_AUDIT`;这是因为 Codex runner 使用 workspace-write sandbox,不能作为定时任务默认行为。
|
||||
- `ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile` 是运行时发布入口,先调用 `napcat-runtime-release-readiness`,再通过显式参数触发运行时镜像构建和 API promotion;下游 job 使用白名单 choice,避免任意 Jenkins job 名被传入。
|
||||
- 生产 API promotion 除了 `PROMOTE_API_RUNTIME=true` 和明确 image/profile 参数外,还必须填写 `CONFIRM_API_PROMOTION_TARGET=PROMOTE_KT_TEMPLATE_API_MAIN`。
|
||||
- 两个模板默认使用 NAS 工作区 `/vol1/docker/kt-codex/workspace/KT`,运行证据落在 `/vol1/docker/kt-codex/artifacts` 下。
|
||||
- 模板只保存入口和参数形状,不记录 Jenkins credential、Token、SSH key 或其他真实凭据。
|
||||
|
||||
## MCP 客户端配置
|
||||
|
||||
把下面配置加入支持 stdio MCP 的客户端配置里:
|
||||
@ -95,6 +110,11 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
|
||||
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
|
||||
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
|
||||
| `kt_napcat_upstream_audit` | 生成或执行 NapCatQQ upstream release 审计,默认 dry-run,显式 `execute/useCodex` 后才运行 Codex |
|
||||
| `kt_napcat_sync_candidate_review` | 生成 NapCat sync candidate review 的 prompt/context/只读命令骨架,Task 5 不执行 Codex 或 Git 写操作 |
|
||||
| `kt_napcat_runtime_release_readiness` | 生成 NapCat runtime release readiness 的 prompt/context/只读检查骨架,不自动构建或发布 |
|
||||
| `kt_napcat_remote_dev_handoff` | 生成 NapCat 自动化远程开发 handoff 的 prompt/context 骨架,不创建线程或同步仓库 |
|
||||
| `kt_nas_codex_bootstrap_plan` | 生成可信 NAS Codex bootstrap dry-run/execute command plan;execute 只规划目录准备和 CLI 包检查/安装,不复制 secrets、不写 systemd |
|
||||
| `kt_napcat_device_profile_check` | 静态检查 API NapCat 设备身份护栏,覆盖 hostname、MAC、`machine-info`、runtime dir 和 DB timezone |
|
||||
| `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 |
|
||||
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
|
||||
@ -113,6 +133,11 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
|
||||
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移、QQBot 重型服务单测导入风险、QQBot 插件 registry/controller smoke 未 mock 具体插件实现风险、OneBot/QQ 登录态边界、NapCat 密码登录验证码 wait 阶段、日志窗口锚点、验证码状态早于日志 URL 的短轮询、验证码 pending 被状态轮询误清理、API Pod SSH 日志读取超时及清理前 pending 回归,以及普通处理中状态误吃旧验证码日志回归;明显测试占位 token 不报凭据风险,确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
|
||||
| `pnpm run napcat-upstream-audit` | 默认 dry-run 输出 NapCatQQ 上游 release 审计分类、只读 Git 命令和 artifact 路径;显式 `--execute --use-codex` 才运行 Codex。 |
|
||||
| `pnpm run napcat-sync-candidate-review` | 输出 sync candidate review 的 prompt/context/只读命令骨架,不执行 Codex 或 Git 写操作。 |
|
||||
| `pnpm run napcat-runtime-release-readiness` | 输出 runtime release readiness 的 prompt/context/只读检查骨架,不构建镜像、不调用 kubectl、不发布。 |
|
||||
| `pnpm run napcat-remote-dev-handoff` | 输出远程开发 handoff 的 prompt/context 骨架,不创建线程、不同步仓库、不执行 Codex。 |
|
||||
| `pnpm run nas-codex-bootstrap` | 输出可信 NAS Codex bootstrap dry-run plan;传 `--execute` 只生成目录准备和 CLI 包检查/安装命令计划,不真实执行 SSH、不复制 secrets、不写 systemd。 |
|
||||
| `pnpm run napcat-device-profile-check` | 静态检查 API NapCat 设备身份护栏,防止回退到未知设备风险配置。 |
|
||||
| `pnpm run deploy-observation` | 默认 dry-run 输出只读 NAS 观测命令;传 `--execute` 时执行 Jenkins/K8s/health/smoke 观测并写入 `.kt-workspace/test-artifacts/deploy-observation`。 |
|
||||
| `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 |
|
||||
@ -154,6 +179,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
- 大方向结束前调用 `kt_workstream_closeout` 或 `pnpm run workstream-closeout`;必须分别传入 `--review` 的 KT 全局审查证据和 `--superpowers-review` 的 Superpowers code review 证据;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。
|
||||
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。`findings` 先判定真实风险或工具误报,真实风险修业务代码,误报修 `mcp/ktWorkflow/src/tools/review.ts` 并复跑。
|
||||
- QQBot NapCat 登录 review 会检查验证码 pending、Docker 日志窗口、API Pod 读取超时,以及 `needNewDevice/jumpUrl` 是否进入 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` 或兼容的 `deviceVerifyUrl` pending 状态。
|
||||
- QQBot NapCat 二维码刷新卡在生成阶段时,优先计数当前容器近 5-20 分钟日志里的“重置已失效登录服务后重新生成二维码”、`Login Error,ErrType: 7 ErrCode: 4` 和“二维码已保存/二维码解码URL”;如果 reset/error 数远高于二维码产出,按 `docs/qqbot-nas-runtime.md` 的 v5 稳定解法排查 NapCat native login service reset 风暴,先修 fork/镜像,再看 API SSE。
|
||||
- 代码或配置改动后调用 `kt_change_doc_sync`,把需要同步的 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 入口补齐;无需同步时把原因写进收尾证据。
|
||||
- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review 都过门;清理证据必须显式传入,不能依赖缺省值。
|
||||
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
|
||||
|
||||
104
ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile
Normal file
104
ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile
Normal file
@ -0,0 +1,104 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
skipDefaultCheckout(true)
|
||||
timestamps()
|
||||
timeout(time: 60, unit: 'MINUTES')
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'NAPCAT_FORK_REF', defaultValue: 'codex/qr-refresh-login-state', description: 'Reviewed NapCatQQ fork branch, tag, or commit.')
|
||||
string(name: 'UPSTREAM_RELEASE_TAG', defaultValue: '', description: 'Reviewed upstream release tag, when applicable.')
|
||||
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', defaultValue: '', description: 'Explicit runtime image tag to build or promote, for example kt-napcat-desktop-cn:desktop-cn-v9.')
|
||||
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', defaultValue: '', description: 'Explicit API desktop profile version override.')
|
||||
booleanParam(name: 'BUILD_RUNTIME_IMAGE', defaultValue: false, description: 'When true, trigger the reviewed Jenkins runtime image build/publish job.')
|
||||
choice(name: 'RUNTIME_BUILD_JOB', choices: ['KT-NapCatQQ/Runtime-Image-Build'], description: 'Whitelisted Jenkins job that performs the reviewed runtime image build and publish.')
|
||||
booleanParam(name: 'PROMOTE_API_RUNTIME', defaultValue: false, description: 'When true, trigger API promotion with explicit image/profile overrides.')
|
||||
choice(name: 'API_PROMOTION_JOB', choices: ['KT-Template/KT-Template-API/main'], description: 'Whitelisted API Jenkins job that accepts NapCat runtime override parameters.')
|
||||
string(name: 'CONFIRM_API_PROMOTION_TARGET', defaultValue: '', description: 'Type PROMOTE_KT_TEMPLATE_API_MAIN before triggering the production API promotion job.')
|
||||
}
|
||||
|
||||
environment {
|
||||
KT_WORKSPACE_ROOT = '/vol1/docker/kt-codex/workspace/KT'
|
||||
KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow'
|
||||
KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('ktWorkflow readiness') {
|
||||
steps {
|
||||
sh '''
|
||||
set -e
|
||||
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-runtime-release-readiness -- \
|
||||
--artifact-root "$KT_ARTIFACT_ROOT/napcat-runtime-release" \
|
||||
--fork-branch "$NAPCAT_FORK_REF" \
|
||||
--upstream-release-tag "$UPSTREAM_RELEASE_TAG" \
|
||||
--napcat-image-tag "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \
|
||||
--profile "$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Runtime image build and publish') {
|
||||
when {
|
||||
expression { return params.BUILD_RUNTIME_IMAGE }
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
requireRuntimeOverrides(
|
||||
params.QQBOT_NAPCAT_IMAGE_OVERRIDE,
|
||||
params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE
|
||||
)
|
||||
build job: params.RUNTIME_BUILD_JOB,
|
||||
wait: true,
|
||||
parameters: [
|
||||
string(name: 'NAPCAT_FORK_REF', value: params.NAPCAT_FORK_REF),
|
||||
string(name: 'UPSTREAM_RELEASE_TAG', value: params.UPSTREAM_RELEASE_TAG),
|
||||
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE),
|
||||
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE),
|
||||
string(name: 'KT_ARTIFACT_ROOT', value: "${env.KT_ARTIFACT_ROOT}/napcat-runtime-release")
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('API promotion') {
|
||||
when {
|
||||
expression { return params.PROMOTE_API_RUNTIME }
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.CONFIRM_API_PROMOTION_TARGET != 'PROMOTE_KT_TEMPLATE_API_MAIN') {
|
||||
error('CONFIRM_API_PROMOTION_TARGET must equal PROMOTE_KT_TEMPLATE_API_MAIN before API promotion runs.')
|
||||
}
|
||||
requireRuntimeOverrides(
|
||||
params.QQBOT_NAPCAT_IMAGE_OVERRIDE,
|
||||
params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE
|
||||
)
|
||||
build job: params.API_PROMOTION_JOB,
|
||||
wait: true,
|
||||
parameters: [
|
||||
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE),
|
||||
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops runtime build or promotion unless both release override values are explicit.
|
||||
* @param imageTag Reviewed NapCat runtime image tag supplied by the operator.
|
||||
* @param profile Reviewed desktop profile version supplied by the operator.
|
||||
* @return Nothing; aborts this pipeline through `error` when a required value is missing.
|
||||
*/
|
||||
void requireRuntimeOverrides(String imageTag, String profile) {
|
||||
if (!imageTag?.trim()) {
|
||||
error('QQBOT_NAPCAT_IMAGE_OVERRIDE is required before build or promotion.')
|
||||
}
|
||||
if (!profile?.trim()) {
|
||||
error('QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE is required before build or promotion.')
|
||||
}
|
||||
}
|
||||
51
ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile
Normal file
51
ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile
Normal file
@ -0,0 +1,51 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
skipDefaultCheckout(true)
|
||||
timestamps()
|
||||
timeout(time: 30, unit: 'MINUTES')
|
||||
}
|
||||
|
||||
parameters {
|
||||
booleanParam(name: 'RUN_CODEX_AUDIT', defaultValue: false, description: 'When true, allow the optional Codex workspace-write audit after the default dry-run audit.')
|
||||
string(name: 'CONFIRM_CODEX_WORKSPACE_WRITE', defaultValue: '', description: 'Type RUN_CODEX_WORKSPACE_WRITE_AUDIT to confirm Codex may run with a workspace-write sandbox.')
|
||||
}
|
||||
|
||||
environment {
|
||||
KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow'
|
||||
KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('ktWorkflow upstream audit dry-run') {
|
||||
steps {
|
||||
sh '''
|
||||
set -e
|
||||
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \
|
||||
--artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/dry-run"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('ktWorkflow upstream audit with Codex') {
|
||||
when {
|
||||
expression { return params.RUN_CODEX_AUDIT }
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.CONFIRM_CODEX_WORKSPACE_WRITE != 'RUN_CODEX_WORKSPACE_WRITE_AUDIT') {
|
||||
error('CONFIRM_CODEX_WORKSPACE_WRITE must equal RUN_CODEX_WORKSPACE_WRITE_AUDIT before Codex workspace-write audit runs.')
|
||||
}
|
||||
}
|
||||
sh '''
|
||||
set -e
|
||||
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \
|
||||
--execute \
|
||||
--use-codex \
|
||||
--artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/codex"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,11 @@
|
||||
"deploy-observation": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --deploy-observation",
|
||||
"global-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --global-review",
|
||||
"napcat-device-profile-check": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-device-profile-check",
|
||||
"napcat-remote-dev-handoff": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-remote-dev-handoff",
|
||||
"napcat-runtime-release-readiness": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-runtime-release-readiness",
|
||||
"napcat-sync-candidate-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-sync-candidate-review",
|
||||
"napcat-upstream-audit": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-upstream-audit",
|
||||
"nas-codex-bootstrap": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --nas-codex-bootstrap",
|
||||
"obsidian-context": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-context",
|
||||
"obsidian-sync": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-sync",
|
||||
"obsidian-validate": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-validate",
|
||||
|
||||
41
prompts/napcat/remote-dev-handoff.md
Normal file
41
prompts/napcat/remote-dev-handoff.md
Normal file
@ -0,0 +1,41 @@
|
||||
# NapCat Remote Dev Handoff
|
||||
|
||||
You are summarizing a supplied remote development context packet for the next human-controlled step.
|
||||
|
||||
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.
|
||||
|
||||
## Handoff Scope
|
||||
|
||||
Summarize only packet evidence for:
|
||||
|
||||
- repository branches and dirty status
|
||||
- artifact directories and notable generated outputs
|
||||
- Jenkins status, build numbers, commit/image pointers, and unknown deployment state
|
||||
- blockers, missing evidence, or unsafe assumptions
|
||||
- safe next commands that are read-only or dry-run oriented
|
||||
|
||||
Do not ask Codex to modify files. Do not include merge, commit, push, deploy, Docker mutation, or destructive cleanup commands as next commands.
|
||||
|
||||
## Requested Schema
|
||||
|
||||
Return only this JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"repoStates": [
|
||||
{
|
||||
"repo": "Repository path or label.",
|
||||
"branch": "Current branch or unknown.",
|
||||
"dirtyStatus": "clean | dirty | unknown",
|
||||
"head": "Commit hash or unknown."
|
||||
}
|
||||
],
|
||||
"artifactDirectories": ["Artifact directory or missing evidence note."],
|
||||
"jenkinsStatus": "Jenkins status summary or missing evidence note.",
|
||||
"blockers": ["Blocker or missing evidence note."],
|
||||
"safeNextCommands": ["Read-only or dry-run command."]
|
||||
}
|
||||
```
|
||||
34
prompts/napcat/runtime-release-readiness.md
Normal file
34
prompts/napcat/runtime-release-readiness.md
Normal file
@ -0,0 +1,34 @@
|
||||
# NapCat Runtime Release Readiness
|
||||
|
||||
You are reviewing a supplied NapCat runtime release packet before a human promotes an image/profile into the API runtime.
|
||||
|
||||
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.
|
||||
|
||||
## Readiness Checks
|
||||
|
||||
Base every section on packet evidence:
|
||||
|
||||
- source validation: branch, commit, upstream base, fork patch marker, and dirty status
|
||||
- image validation: image tag, image inspect/build evidence, marker evidence, and runtime asset presence
|
||||
- API promotion readiness: explicit image/profile values, configuration alignment, and Jenkins/API promotion prerequisites
|
||||
- online smoke readiness: QQBot/NapCat login, WebUI, command smoke, and rollback smoke expectations
|
||||
- rollback pointer: last known good branch, commit, image tag, profile, and API runtime values
|
||||
|
||||
If any section lacks evidence, state the missing evidence plainly.
|
||||
|
||||
## Requested Schema
|
||||
|
||||
Return only this JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceValidation": "Evidence-backed source validation status.",
|
||||
"imageValidation": "Evidence-backed image validation status.",
|
||||
"apiPromotionReadiness": "Evidence-backed API promotion readiness status.",
|
||||
"onlineSmokeReadiness": "Evidence-backed online smoke readiness status.",
|
||||
"rollbackPointer": "Specific rollback pointer or the missing evidence needed to name one."
|
||||
}
|
||||
```
|
||||
34
prompts/napcat/schemas/runtime-release-readiness.schema.json
Normal file
34
prompts/napcat/schemas/runtime-release-readiness.schema.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"sourceValidation",
|
||||
"imageValidation",
|
||||
"apiPromotionReadiness",
|
||||
"onlineSmokeReadiness",
|
||||
"rollbackPointer"
|
||||
],
|
||||
"properties": {
|
||||
"sourceValidation": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"imageValidation": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"apiPromotionReadiness": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"onlineSmokeReadiness": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"rollbackPointer": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
33
prompts/napcat/schemas/upstream-audit.schema.json
Normal file
33
prompts/napcat/schemas/upstream-audit.schema.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["classification", "reasonCodes", "recommendedAction", "summary"],
|
||||
"properties": {
|
||||
"classification": {
|
||||
"enum": ["safe-candidate", "manual-review", "blocked"]
|
||||
},
|
||||
"reasonCodes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"enum": [
|
||||
"NO_UPSTREAM_CHANGE",
|
||||
"HOT_ZONE_CHANGED",
|
||||
"FORK_PATCH_OVERLAP",
|
||||
"DRY_MERGE_CONFLICT",
|
||||
"BUILD_GRAPH_CHANGED",
|
||||
"METADATA_UNAVAILABLE",
|
||||
"CODEX_SCHEMA_INVALID"
|
||||
]
|
||||
}
|
||||
},
|
||||
"recommendedAction": {
|
||||
"enum": ["no-op", "create-candidate-branch", "request-human-review", "block"]
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
43
prompts/napcat/sync-candidate-review.md
Normal file
43
prompts/napcat/sync-candidate-review.md
Normal file
@ -0,0 +1,43 @@
|
||||
# NapCat Sync Candidate Review
|
||||
|
||||
You are reviewing a supplied NapCat candidate branch context packet before a human decides whether to continue the sync.
|
||||
|
||||
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.
|
||||
|
||||
## Review Scope
|
||||
|
||||
Review only evidence present in the packet. Cover these areas:
|
||||
|
||||
- login service reset behavior and stale native login service state
|
||||
- QR refresh, stale QR prevention, QR expiry, and scan confirmation state
|
||||
- WebUI login runtime state, reconciled QQ online state, and false online signals
|
||||
- captcha and new-device boundaries, including pending state preservation
|
||||
- package, shell, framework, webui build output, and runtime asset layout
|
||||
- test gaps, missing smoke evidence, and unverified online behavior
|
||||
|
||||
## Finding Rules
|
||||
|
||||
Flag only actionable risks grounded in packet evidence. If evidence is missing, report it as a test gap or blocked review item instead of guessing.
|
||||
|
||||
## Requested Schema
|
||||
|
||||
Return only this JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "approve | manual-review | block",
|
||||
"summary": "Short review summary.",
|
||||
"findings": [
|
||||
{
|
||||
"severity": "critical | important | minor",
|
||||
"area": "login-reset | qr-refresh | webui-runtime | captcha-new-device | package-build | test-gap",
|
||||
"evidence": "Packet evidence for the finding.",
|
||||
"requiredAction": "Human-readable action before promotion."
|
||||
}
|
||||
],
|
||||
"testGaps": ["Missing validation evidence."]
|
||||
}
|
||||
```
|
||||
53
prompts/napcat/upstream-audit.md
Normal file
53
prompts/napcat/upstream-audit.md
Normal file
@ -0,0 +1,53 @@
|
||||
# NapCat Upstream Audit
|
||||
|
||||
You are reviewing a supplied NapCat upstream release context packet for KT automation. Decide whether the upstream release can become a candidate branch, needs manual review, or is blocked.
|
||||
|
||||
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.
|
||||
|
||||
## Inputs
|
||||
|
||||
Use the packet sections for upstream release metadata, fork patch range, changed files, dry-merge result, build graph diff, and Codex parser status. Treat missing packet sections as unavailable metadata.
|
||||
|
||||
## Classification Rules
|
||||
|
||||
Choose exactly one `classification`:
|
||||
|
||||
- `safe-candidate`: no upstream change exists, or upstream changes are outside hot zones, do not overlap fork patches, dry merge is clean, build graph is stable, and metadata is available.
|
||||
- `manual-review`: upstream changed login/runtime/build-adjacent areas or fork-adjacent code, but the packet does not prove a hard blocker.
|
||||
- `blocked`: dry merge conflicts, build graph changes that require maintainer action, missing required metadata, or invalid Codex output schema.
|
||||
|
||||
Use one or more `reasonCodes` from this exact list:
|
||||
|
||||
- `NO_UPSTREAM_CHANGE`
|
||||
- `HOT_ZONE_CHANGED`
|
||||
- `FORK_PATCH_OVERLAP`
|
||||
- `DRY_MERGE_CONFLICT`
|
||||
- `BUILD_GRAPH_CHANGED`
|
||||
- `METADATA_UNAVAILABLE`
|
||||
- `CODEX_SCHEMA_INVALID`
|
||||
|
||||
## Hot Zones
|
||||
|
||||
Treat these as NapCat login/runtime hot zones:
|
||||
|
||||
- login service reset and native login service state
|
||||
- QR refresh, stale QR, QR expiry, and scan confirmation state
|
||||
- WebUI login runtime state and reconciled QQ online state
|
||||
- captcha and new-device verification boundaries
|
||||
- package, shell, framework, webui build graph or output layout
|
||||
|
||||
## Requested Schema
|
||||
|
||||
Return only this JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"classification": "safe-candidate | manual-review | blocked",
|
||||
"reasonCodes": ["NO_UPSTREAM_CHANGE"],
|
||||
"recommendedAction": "no-op | create-candidate-branch | request-human-review | block",
|
||||
"summary": "Short evidence-based decision."
|
||||
}
|
||||
```
|
||||
144
src/core/cli.ts
144
src/core/cli.ts
@ -1,6 +1,10 @@
|
||||
import type {
|
||||
DeployObservationInput,
|
||||
GlobalCodeReviewInput,
|
||||
NapcatRemoteDevHandoffInput,
|
||||
NapcatRuntimeReleaseReadinessInput,
|
||||
NapcatSyncCandidateReviewInput,
|
||||
NasCodexBootstrapPlanInput,
|
||||
ObsidianContextInput,
|
||||
ObsidianSyncInput,
|
||||
ObsidianValidateInput,
|
||||
@ -8,6 +12,7 @@ import type {
|
||||
WorkstreamReusablePattern,
|
||||
} from '../types.js';
|
||||
import { workstreamReusablePatternValues } from './constants.js';
|
||||
import type { NapcatUpstreamAuditInput } from '../tools/napcatAutomation.types.js';
|
||||
|
||||
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
|
||||
if (argv.includes('--content-scan-all') || argv.includes('--contentScanMode=all')) {
|
||||
@ -62,6 +67,16 @@ function readOptionList(argv: string[], names: string[]): string[] {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any supported boolean CLI flag is present in the raw argv list.
|
||||
* @param argv - Full process argv including node, script, mode flag, and user options.
|
||||
* @param names - Flag names to accept in dashed or camelCase form.
|
||||
* @returns True when the caller provided one of the named flags.
|
||||
*/
|
||||
function hasFlag(argv: string[], names: string[]): boolean {
|
||||
return names.some((name) => argv.includes(name));
|
||||
}
|
||||
|
||||
export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseoutInput {
|
||||
const patternSet = new Set<string>(workstreamReusablePatternValues);
|
||||
const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern'])
|
||||
@ -123,6 +138,135 @@ export function parseDeployObservationCliArgs(argv: string[]): DeployObservation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the NapCat upstream audit CLI mode into the deterministic audit builder input.
|
||||
* @param argv - Full process argv from `src/server.ts`; `--execute` and `--use-codex` must both be explicit before Codex can run.
|
||||
* @returns Upstream audit input with dry-run defaults for all optional metadata lists.
|
||||
*/
|
||||
export function parseNapcatUpstreamAuditCliArgs(
|
||||
argv: string[],
|
||||
): NapcatUpstreamAuditInput {
|
||||
return {
|
||||
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
|
||||
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
|
||||
createCandidateBranch: hasFlag(argv, ['--create-candidate-branch', '--createCandidateBranch']),
|
||||
dryMergeConflict: hasFlag(argv, ['--dry-merge-conflict', '--dryMergeConflict']),
|
||||
execute: hasFlag(argv, ['--execute']),
|
||||
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
|
||||
forkPatchFiles: readOptionList(argv, [
|
||||
'--fork-patch-file',
|
||||
'--fork-patch-files',
|
||||
'--forkPatchFile',
|
||||
'--forkPatchFiles',
|
||||
]),
|
||||
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
|
||||
lastAcceptedUpstreamBase: readOption(argv, [
|
||||
'--last-accepted-upstream-base',
|
||||
'--lastAcceptedUpstreamBase',
|
||||
]),
|
||||
outputSchemaPath: readOption(argv, ['--output-schema', '--outputSchemaPath']),
|
||||
upstreamChangedFiles: readOptionList(argv, [
|
||||
'--upstream-changed-file',
|
||||
'--upstream-changed-files',
|
||||
'--upstreamChangedFile',
|
||||
'--upstreamChangedFiles',
|
||||
]),
|
||||
upstreamReleaseTag: readOption(argv, [
|
||||
'--upstream-release-tag',
|
||||
'--upstream-release-ref',
|
||||
'--upstreamReleaseTag',
|
||||
]),
|
||||
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
|
||||
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
|
||||
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses common dry-run NapCat prompt tool options shared by candidate review, readiness, and handoff modes.
|
||||
* @param argv - Full process argv from a NapCat automation CLI script.
|
||||
* @returns Prompt skeleton input with `execute` false unless explicitly requested by the caller.
|
||||
*/
|
||||
function parseNapcatPromptToolCliArgs(argv: string[]): NapcatSyncCandidateReviewInput {
|
||||
return {
|
||||
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
|
||||
candidateBranch: readOption(argv, ['--candidate-branch', '--candidateBranch']),
|
||||
execute: hasFlag(argv, ['--execute']),
|
||||
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
|
||||
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
|
||||
lastAcceptedUpstreamBase: readOption(argv, [
|
||||
'--last-accepted-upstream-base',
|
||||
'--lastAcceptedUpstreamBase',
|
||||
]),
|
||||
upstreamReleaseTag: readOption(argv, [
|
||||
'--upstream-release-tag',
|
||||
'--upstream-release-ref',
|
||||
'--upstreamReleaseTag',
|
||||
]),
|
||||
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
|
||||
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
|
||||
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the NapCat sync-candidate review CLI mode.
|
||||
* @param argv - Full process argv from `pnpm run napcat-sync-candidate-review`.
|
||||
* @returns Dry-run prompt skeleton input for human/Codex candidate review handoff.
|
||||
*/
|
||||
export function parseNapcatSyncCandidateReviewCliArgs(
|
||||
argv: string[],
|
||||
): NapcatSyncCandidateReviewInput {
|
||||
return parseNapcatPromptToolCliArgs(argv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the NapCat runtime release readiness CLI mode.
|
||||
* @param argv - Full process argv from `pnpm run napcat-runtime-release-readiness`.
|
||||
* @returns Dry-run prompt skeleton input with image/profile hints and no default mutation.
|
||||
*/
|
||||
export function parseNapcatRuntimeReleaseReadinessCliArgs(
|
||||
argv: string[],
|
||||
): NapcatRuntimeReleaseReadinessInput {
|
||||
return {
|
||||
...parseNapcatPromptToolCliArgs(argv),
|
||||
apiImageTag: readOption(argv, ['--api-image-tag', '--apiImageTag']),
|
||||
napcatImageTag: readOption(argv, ['--napcat-image-tag', '--napcatImageTag']),
|
||||
profile: readOption(argv, ['--profile']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the NapCat remote development handoff CLI mode.
|
||||
* @param argv - Full process argv from `src/server.ts --napcat-remote-dev-handoff`.
|
||||
* @returns Dry-run prompt skeleton input for a remote workspace handoff packet.
|
||||
*/
|
||||
export function parseNapcatRemoteDevHandoffCliArgs(
|
||||
argv: string[],
|
||||
): NapcatRemoteDevHandoffInput {
|
||||
return {
|
||||
...parseNapcatPromptToolCliArgs(argv),
|
||||
targetHost: readOption(argv, ['--target-host', '--targetHost']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the NAS Codex bootstrap planning CLI mode.
|
||||
* @param argv - Full process argv from `pnpm run nas-codex-bootstrap`.
|
||||
* @returns Dry-run bootstrap plan input; `execute` is false unless the caller explicitly opts in.
|
||||
*/
|
||||
export function parseNasCodexBootstrapCliArgs(
|
||||
argv: string[],
|
||||
): NasCodexBootstrapPlanInput {
|
||||
return {
|
||||
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
|
||||
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
|
||||
execute: hasFlag(argv, ['--execute']),
|
||||
serviceName: readOption(argv, ['--service-name', '--serviceName']),
|
||||
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseObsidianCliArgs(argv: string[]): ObsidianValidateInput {
|
||||
return {
|
||||
checkLayout: !argv.includes('--no-layout'),
|
||||
|
||||
@ -69,6 +69,11 @@ export const registeredToolNames = [
|
||||
'kt_commit_plan',
|
||||
'kt_push_plan',
|
||||
'kt_business_test_plan',
|
||||
'kt_napcat_upstream_audit',
|
||||
'kt_napcat_sync_candidate_review',
|
||||
'kt_napcat_runtime_release_readiness',
|
||||
'kt_napcat_remote_dev_handoff',
|
||||
'kt_nas_codex_bootstrap_plan',
|
||||
'kt_napcat_device_profile_check',
|
||||
'kt_blocker_resolution',
|
||||
'kt_remote_health_check',
|
||||
|
||||
@ -22,6 +22,14 @@ import { buildVerificationPlan } from './tools/verification.js';
|
||||
import { buildWorkstreamCloseout } from './tools/closeout.js';
|
||||
import { buildChangeDocSync } from './tools/docSync.js';
|
||||
import { buildDeployObservation } from './tools/deployObservation.js';
|
||||
import {
|
||||
buildNapcatRemoteDevHandoff,
|
||||
buildNapcatRuntimeReleaseReadiness,
|
||||
buildNapcatSyncCandidateReview,
|
||||
buildNapcatUpstreamAudit,
|
||||
buildNasCodexBootstrapPlan,
|
||||
napcatAutomationDefaults,
|
||||
} from './tools/napcatAutomation.js';
|
||||
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
|
||||
import { workstreamReusablePatternValues } from './core/constants.js';
|
||||
export function response(data: unknown): McpTextResponse {
|
||||
@ -35,6 +43,11 @@ export function response(data: unknown): McpTextResponse {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers every ktWorkflow MCP tool against the provided MCP server instance.
|
||||
* @param server - MCP SDK server that owns tool names, schemas, descriptions, and async handlers.
|
||||
* @returns Nothing; handlers are attached to `server` for later stdio transport use.
|
||||
*/
|
||||
export function registerTools(server: McpServer): void {
|
||||
server.registerTool(
|
||||
'kt_read_context',
|
||||
@ -370,11 +383,137 @@ export function registerTools(server: McpServer): void {
|
||||
async (input) => response(buildBusinessTestPlan(input)),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'kt_napcat_upstream_audit',
|
||||
{
|
||||
description:
|
||||
'生成或执行 NapCatQQ 上游 release 审计:默认 dry-run,只读输出 hot zone、fork patch overlap、dry merge 和 Codex 审计入口;execute/useCodex 均需显式开启才会运行 Codex。',
|
||||
inputSchema: {
|
||||
artifactRoot: z
|
||||
.string()
|
||||
.default('.kt-workspace/test-artifacts/napcat-upstream-audit'),
|
||||
codexHome: z.string().optional(),
|
||||
createCandidateBranch: z.boolean().default(false),
|
||||
dryMergeConflict: z.boolean().default(false),
|
||||
execute: z.boolean().default(false),
|
||||
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||
forkPatchFiles: z.array(z.string()).default([]),
|
||||
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||
lastAcceptedUpstreamBase: z
|
||||
.string()
|
||||
.default(napcatAutomationDefaults.lastAcceptedUpstreamBase),
|
||||
outputSchemaPath: z.string().optional(),
|
||||
upstreamChangedFiles: z.array(z.string()).default([]),
|
||||
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||
useCodex: z.boolean().default(false),
|
||||
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||
},
|
||||
title: 'KT NapCat Upstream Audit',
|
||||
},
|
||||
async (input) => response(buildNapcatUpstreamAudit(input)),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'kt_napcat_sync_candidate_review',
|
||||
{
|
||||
description:
|
||||
'生成 NapCatQQ sync candidate 人工/Codex 复审的 dry-run prompt、上下文和只读命令骨架;Task 5 不执行 Codex 或 Git 写操作。',
|
||||
inputSchema: {
|
||||
artifactRoot: z
|
||||
.string()
|
||||
.default('.kt-workspace/test-artifacts/napcat-automation/sync-candidate-review'),
|
||||
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
|
||||
execute: z.boolean().default(false),
|
||||
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||
lastAcceptedUpstreamBase: z
|
||||
.string()
|
||||
.default(napcatAutomationDefaults.lastAcceptedUpstreamBase),
|
||||
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||
useCodex: z.boolean().default(false),
|
||||
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||
},
|
||||
title: 'KT NapCat Sync Candidate Review',
|
||||
},
|
||||
async (input) => response(buildNapcatSyncCandidateReview(input)),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'kt_napcat_runtime_release_readiness',
|
||||
{
|
||||
description:
|
||||
'生成 NapCat runtime release readiness dry-run prompt、上下文和只读检查骨架;不自动构建镜像、不调用 kubectl、不发布。',
|
||||
inputSchema: {
|
||||
apiImageTag: z.string().default(napcatAutomationDefaults.apiImageTag),
|
||||
artifactRoot: z
|
||||
.string()
|
||||
.default('.kt-workspace/test-artifacts/napcat-automation/runtime-release-readiness'),
|
||||
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
|
||||
execute: z.boolean().default(false),
|
||||
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||
napcatImageTag: z.string().default(napcatAutomationDefaults.runtimeImageTag),
|
||||
profile: z.string().default(napcatAutomationDefaults.runtimeProfile),
|
||||
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||
useCodex: z.boolean().default(false),
|
||||
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||
},
|
||||
title: 'KT NapCat Runtime Release Readiness',
|
||||
},
|
||||
async (input) => response(buildNapcatRuntimeReleaseReadiness(input)),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'kt_napcat_remote_dev_handoff',
|
||||
{
|
||||
description:
|
||||
'生成 NapCat 自动化远程开发 handoff 的 dry-run prompt 和上下文骨架;不创建线程、不执行 Codex、不同步仓库。',
|
||||
inputSchema: {
|
||||
artifactRoot: z
|
||||
.string()
|
||||
.default('.kt-workspace/test-artifacts/napcat-automation/remote-dev-handoff'),
|
||||
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
|
||||
execute: z.boolean().default(false),
|
||||
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||
targetHost: z.string().default(napcatAutomationDefaults.targetHost),
|
||||
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||
useCodex: z.boolean().default(false),
|
||||
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||
},
|
||||
title: 'KT NapCat Remote Dev Handoff',
|
||||
},
|
||||
async (input) => response(buildNapcatRemoteDevHandoff(input)),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'kt_nas_codex_bootstrap_plan',
|
||||
{
|
||||
description:
|
||||
'生成可信 NAS Codex 自动化环境 bootstrap 的 dry-run plan;Task 5 仅输出只读检查和占位计划,不安装、不同步、不写 systemd。',
|
||||
inputSchema: {
|
||||
artifactRoot: z
|
||||
.string()
|
||||
.default('.kt-workspace/test-artifacts/napcat-automation/nas-codex-bootstrap'),
|
||||
codexHome: z.string().default(napcatAutomationDefaults.codexHome),
|
||||
execute: z.boolean().default(false),
|
||||
serviceName: z.string().default(napcatAutomationDefaults.bootstrapServiceName),
|
||||
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||
},
|
||||
title: 'KT NAS Codex Bootstrap Plan',
|
||||
},
|
||||
async (input) => response(buildNasCodexBootstrapPlan(input)),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'kt_napcat_device_profile_check',
|
||||
{
|
||||
description:
|
||||
'静态检查 API NapCat 设备身份护栏:QQNT 可见 hostname、02:42 MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
|
||||
'静态检查 API NapCat 设备身份护栏:QQNT 可见 hostname、实体 OUI MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
|
||||
inputSchema: {
|
||||
project: z.string().default('api'),
|
||||
},
|
||||
|
||||
821
src/selfTest.ts
821
src/selfTest.ts
@ -1,9 +1,14 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
parseNasCodexBootstrapCliArgs,
|
||||
parseDeployObservationCliArgs,
|
||||
parseGlobalReviewCliArgs,
|
||||
parseNapcatRemoteDevHandoffCliArgs,
|
||||
parseNapcatRuntimeReleaseReadinessCliArgs,
|
||||
parseNapcatSyncCandidateReviewCliArgs,
|
||||
parseNapcatUpstreamAuditCliArgs,
|
||||
parseWorkstreamCloseoutCliArgs,
|
||||
} from "./core/cli.js";
|
||||
import { registeredToolNames } from "./core/constants.js";
|
||||
@ -35,6 +40,27 @@ import {
|
||||
findTaskRecordGovernanceFindings,
|
||||
isBenignCredentialReviewValue,
|
||||
} from "./tools/review.js";
|
||||
import {
|
||||
napcatAutomationReasonCodes,
|
||||
parseNapcatAutomationClassification,
|
||||
upstreamAuditOutputSchema,
|
||||
} from "./tools/napcatAutomation.types.js";
|
||||
import {
|
||||
buildNapcatCodexExecCommand,
|
||||
buildNapcatRemoteDevHandoff,
|
||||
buildNapcatRuntimeReleaseReadiness,
|
||||
buildNapcatSyncCandidateReview,
|
||||
buildNapcatUpstreamAudit,
|
||||
buildNasCodexBootstrapPlan,
|
||||
classifyNapcatUpstreamAudit,
|
||||
napcatAutomationDefaults,
|
||||
} from "./tools/napcatAutomation.js";
|
||||
import { registerTools } from "./registerTools.js";
|
||||
import {
|
||||
loadNapcatAutomationPrompt,
|
||||
type NapcatAutomationPromptName,
|
||||
napcatAutomationPromptNames,
|
||||
} from "./tools/napcatAutomation.prompts.js";
|
||||
import {
|
||||
buildBusinessTestPlan,
|
||||
buildNapcatDeviceProfileCheck,
|
||||
@ -54,7 +80,788 @@ import {
|
||||
buildPushPlan,
|
||||
buildRemoteHealthCheck,
|
||||
} from "./tools/workflow.js";
|
||||
/**
|
||||
* Runs ktWorkflow guardrail smoke checks from the CLI self-test entry; writes local evidence under the KT workspace and throws on any broken workflow contract.
|
||||
* @returns A promise that resolves after all contract checks and evidence writing complete.
|
||||
*/
|
||||
export async function runSelfTest(): Promise<void> {
|
||||
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");
|
||||
}
|
||||
const napcatAuditExtraPropertySmoke = upstreamAuditOutputSchema.safeParse({
|
||||
classification: "manual-review",
|
||||
reasonCodes: ["HOT_ZONE_CHANGED"],
|
||||
recommendedAction: "request-human-review",
|
||||
summary: "Login hot-zone changed.",
|
||||
unexpectedField: "Codex output schema rejects this.",
|
||||
});
|
||||
if (napcatAuditExtraPropertySmoke.success) {
|
||||
throw new Error("NapCat upstream audit schema strict self-check failed");
|
||||
}
|
||||
if (!napcatAutomationReasonCodes.includes("HOT_ZONE_CHANGED")) {
|
||||
throw new Error("NapCat reason-code list self-check failed");
|
||||
}
|
||||
const napcatCli = parseNapcatUpstreamAuditCliArgs([
|
||||
"node",
|
||||
"server",
|
||||
"--napcat-upstream-audit",
|
||||
"--execute",
|
||||
"--use-codex",
|
||||
"--artifact-root",
|
||||
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync",
|
||||
]);
|
||||
if (!napcatCli.execute || !napcatCli.useCodex) {
|
||||
throw new Error("NapCat upstream audit CLI parser self-check failed");
|
||||
}
|
||||
if (
|
||||
napcatCli.artifactRoot !==
|
||||
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync"
|
||||
) {
|
||||
throw new Error("NapCat upstream audit artifactRoot CLI parser self-check failed");
|
||||
}
|
||||
const napcatCandidateCli = parseNapcatSyncCandidateReviewCliArgs([
|
||||
"node",
|
||||
"server",
|
||||
"--napcat-sync-candidate-review",
|
||||
]);
|
||||
const napcatRuntimeCli = parseNapcatRuntimeReleaseReadinessCliArgs([
|
||||
"node",
|
||||
"server",
|
||||
"--napcat-runtime-release-readiness",
|
||||
]);
|
||||
const napcatRemoteHandoffCli = parseNapcatRemoteDevHandoffCliArgs([
|
||||
"node",
|
||||
"server",
|
||||
"--napcat-remote-dev-handoff",
|
||||
]);
|
||||
const nasBootstrapCli = parseNasCodexBootstrapCliArgs([
|
||||
"node",
|
||||
"server",
|
||||
"--nas-codex-bootstrap",
|
||||
]);
|
||||
if (
|
||||
napcatCandidateCli.execute ||
|
||||
napcatRuntimeCli.execute ||
|
||||
napcatRemoteHandoffCli.execute ||
|
||||
nasBootstrapCli.execute
|
||||
) {
|
||||
throw new Error("NapCat auxiliary CLI dry-run default self-check failed");
|
||||
}
|
||||
const napcatCandidateDefaults = buildNapcatSyncCandidateReview({}).context;
|
||||
const napcatRuntimeDefaults = buildNapcatRuntimeReleaseReadiness({}).context;
|
||||
const napcatRemoteDefaults = buildNapcatRemoteDevHandoff({}).context;
|
||||
const nasBootstrapDefaults = buildNasCodexBootstrapPlan({}).context;
|
||||
if (
|
||||
napcatCandidateDefaults.candidateBranch !==
|
||||
napcatAutomationDefaults.candidateBranch ||
|
||||
napcatCandidateDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||||
napcatCandidateDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||||
napcatCandidateDefaults.lastAcceptedUpstreamBase !==
|
||||
napcatAutomationDefaults.lastAcceptedUpstreamBase ||
|
||||
napcatCandidateDefaults.upstreamReleaseTag !==
|
||||
napcatAutomationDefaults.upstreamReleaseRef ||
|
||||
napcatCandidateDefaults.upstreamRepo !== napcatAutomationDefaults.upstreamRepo ||
|
||||
napcatCandidateDefaults.workspaceRoot !==
|
||||
napcatAutomationDefaults.workspaceRoot
|
||||
) {
|
||||
throw new Error("NapCat candidate review default alignment self-check failed");
|
||||
}
|
||||
if (
|
||||
napcatRuntimeDefaults.apiImageTag !==
|
||||
napcatAutomationDefaults.apiImageTag ||
|
||||
napcatRuntimeDefaults.candidateBranch !==
|
||||
napcatAutomationDefaults.candidateBranch ||
|
||||
napcatRuntimeDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||||
napcatRuntimeDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||||
napcatRuntimeDefaults.napcatImageTag !==
|
||||
napcatAutomationDefaults.runtimeImageTag ||
|
||||
napcatRuntimeDefaults.profile !== napcatAutomationDefaults.runtimeProfile ||
|
||||
napcatRuntimeDefaults.upstreamReleaseTag !==
|
||||
napcatAutomationDefaults.upstreamReleaseRef ||
|
||||
napcatRuntimeDefaults.workspaceRoot !==
|
||||
napcatAutomationDefaults.workspaceRoot
|
||||
) {
|
||||
throw new Error("NapCat runtime readiness default alignment self-check failed");
|
||||
}
|
||||
if (
|
||||
napcatRemoteDefaults.candidateBranch !==
|
||||
napcatAutomationDefaults.candidateBranch ||
|
||||
napcatRemoteDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||||
napcatRemoteDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||||
napcatRemoteDefaults.targetHost !== napcatAutomationDefaults.targetHost ||
|
||||
napcatRemoteDefaults.upstreamReleaseTag !==
|
||||
napcatAutomationDefaults.upstreamReleaseRef ||
|
||||
napcatRemoteDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
|
||||
) {
|
||||
throw new Error("NapCat remote handoff default alignment self-check failed");
|
||||
}
|
||||
if (
|
||||
nasBootstrapDefaults.codexHome !== napcatAutomationDefaults.codexHome ||
|
||||
nasBootstrapDefaults.serviceName !==
|
||||
napcatAutomationDefaults.bootstrapServiceName ||
|
||||
nasBootstrapDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
|
||||
) {
|
||||
throw new Error("NAS Codex bootstrap default alignment self-check failed");
|
||||
}
|
||||
const bootstrap = buildNasCodexBootstrapPlan({ execute: false });
|
||||
const bootstrapCommandText = bootstrap.commands
|
||||
.map((item) => item.command)
|
||||
.join("\n");
|
||||
if (!bootstrapCommandText.includes("/vol1/docker/kt-codex/home/.codex")) {
|
||||
throw new Error("NAS Codex bootstrap CODEX_HOME self-check failed");
|
||||
}
|
||||
if (bootstrapCommandText.includes("C:\\Users")) {
|
||||
throw new Error("NAS Codex bootstrap Windows path guard self-check failed");
|
||||
}
|
||||
for (const expected of [
|
||||
"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",
|
||||
]) {
|
||||
if (!bootstrapCommandText.includes(expected)) {
|
||||
throw new Error(`NAS Codex bootstrap dry-run command missing: ${expected}`);
|
||||
}
|
||||
}
|
||||
for (const command of bootstrap.commands) {
|
||||
if (!command.readOnly) {
|
||||
throw new Error("NAS Codex bootstrap dry-run command must be read-only");
|
||||
}
|
||||
}
|
||||
const executeBootstrap = buildNasCodexBootstrapPlan({ execute: true });
|
||||
const executeBootstrapCommandText = executeBootstrap.commands
|
||||
.map((item) => item.command)
|
||||
.join("\n");
|
||||
if (
|
||||
executeBootstrap.execute !== true ||
|
||||
executeBootstrap.commands.every((command) => command.readOnly)
|
||||
) {
|
||||
throw new Error("NAS Codex bootstrap execute plan self-check failed");
|
||||
}
|
||||
if (
|
||||
!executeBootstrapCommandText.includes("install -d -m 700 /vol1/docker/kt-codex/home/.codex") ||
|
||||
!executeBootstrapCommandText.includes("npm install -g @openai/codex")
|
||||
) {
|
||||
throw new Error("NAS Codex bootstrap execute package plan self-check failed");
|
||||
}
|
||||
for (const forbidden of [
|
||||
"git push",
|
||||
"git merge",
|
||||
"docker build",
|
||||
"docker pull",
|
||||
"kubectl",
|
||||
"systemctl enable",
|
||||
"systemctl start",
|
||||
"cp ~/.codex",
|
||||
"scp ~/.codex",
|
||||
"C:\\Users",
|
||||
]) {
|
||||
if (executeBootstrapCommandText.includes(forbidden)) {
|
||||
throw new Error(`NAS Codex bootstrap forbidden command leaked: ${forbidden}`);
|
||||
}
|
||||
}
|
||||
let windowsBootstrapPathRejected = false;
|
||||
try {
|
||||
buildNasCodexBootstrapPlan({
|
||||
codexHome: "C:\\Users\\example\\.codex",
|
||||
execute: false,
|
||||
});
|
||||
} catch (error) {
|
||||
windowsBootstrapPathRejected = String(error).includes(
|
||||
"Unsafe NAS Codex bootstrap path",
|
||||
);
|
||||
}
|
||||
if (!windowsBootstrapPathRejected) {
|
||||
throw new Error("NAS Codex bootstrap unsafe Windows path self-check failed");
|
||||
}
|
||||
const napcatCandidateOverride = buildNapcatSyncCandidateReview({
|
||||
candidateBranch: "custom-candidate",
|
||||
lastAcceptedUpstreamBase: "custom-base",
|
||||
});
|
||||
const napcatCandidateOverrideCommandText =
|
||||
napcatCandidateOverride.commands.map((item) => item.command).join("\n");
|
||||
if (
|
||||
!napcatCandidateOverrideCommandText.includes("custom-candidate") ||
|
||||
!napcatCandidateOverrideCommandText.includes("custom-base") ||
|
||||
napcatCandidateOverrideCommandText.includes(
|
||||
napcatAutomationDefaults.candidateBranch,
|
||||
) ||
|
||||
napcatCandidateOverrideCommandText.includes(
|
||||
napcatAutomationDefaults.lastAcceptedUpstreamBase,
|
||||
)
|
||||
) {
|
||||
throw new Error("NapCat candidate command override self-check failed");
|
||||
}
|
||||
const napcatRuntimeOverride = buildNapcatRuntimeReleaseReadiness({
|
||||
napcatImageTag: "custom-image",
|
||||
});
|
||||
const napcatRuntimeOverrideCommandText =
|
||||
napcatRuntimeOverride.commands.map((item) => item.command).join("\n");
|
||||
if (
|
||||
!napcatRuntimeOverrideCommandText.includes("custom-image") ||
|
||||
napcatRuntimeOverrideCommandText.includes(
|
||||
napcatAutomationDefaults.runtimeImageTag,
|
||||
)
|
||||
) {
|
||||
throw new Error("NapCat runtime command override self-check failed");
|
||||
}
|
||||
const napcatAuditOverride = buildNapcatUpstreamAudit({
|
||||
dryMergeConflict: false,
|
||||
forkBranch: "custom-fork",
|
||||
forkPatchFiles: [],
|
||||
lastAcceptedUpstreamBase: "custom-base",
|
||||
upstreamChangedFiles: [],
|
||||
upstreamReleaseTag: "custom-upstream",
|
||||
});
|
||||
const napcatAuditOverrideCommandText =
|
||||
napcatAuditOverride.commands.map((item) => item.command).join("\n");
|
||||
if (
|
||||
!napcatAuditOverrideCommandText.includes("custom-fork") ||
|
||||
!napcatAuditOverrideCommandText.includes("custom-base") ||
|
||||
!napcatAuditOverrideCommandText.includes("custom-upstream") ||
|
||||
napcatAuditOverrideCommandText.includes(napcatAutomationDefaults.forkBranch) ||
|
||||
napcatAuditOverrideCommandText.includes(
|
||||
napcatAutomationDefaults.lastAcceptedUpstreamBase,
|
||||
) ||
|
||||
napcatAuditOverrideCommandText.includes(
|
||||
napcatAutomationDefaults.upstreamReleaseRef,
|
||||
)
|
||||
) {
|
||||
throw new Error("NapCat upstream audit command override self-check failed");
|
||||
}
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(resolveInsideRoot("mcp/ktWorkflow/package.json"), "utf8"),
|
||||
) as { scripts?: Record<string, string> };
|
||||
if (!packageJson.scripts?.["napcat-remote-dev-handoff"]) {
|
||||
throw new Error("NapCat remote handoff npm script self-check failed");
|
||||
}
|
||||
const registeredByFakeServer: string[] = [];
|
||||
const fakeMcpServer = {
|
||||
/**
|
||||
* Records registered MCP tool names so self-test proves the actual registerTools surface, not only the constants list.
|
||||
* @param name - MCP tool name passed by `registerTools`.
|
||||
* @returns Nothing; the name is appended to `registeredByFakeServer`.
|
||||
*/
|
||||
registerTool(name: string): void {
|
||||
registeredByFakeServer.push(name);
|
||||
},
|
||||
};
|
||||
registerTools(fakeMcpServer as never);
|
||||
for (const toolName of [
|
||||
"kt_napcat_upstream_audit",
|
||||
"kt_napcat_sync_candidate_review",
|
||||
"kt_napcat_runtime_release_readiness",
|
||||
"kt_napcat_remote_dev_handoff",
|
||||
"kt_nas_codex_bootstrap_plan",
|
||||
] as const) {
|
||||
if (!registeredToolNames.includes(toolName)) {
|
||||
throw new Error(`NapCat MCP tool registry self-check failed: ${toolName}`);
|
||||
}
|
||||
if (!registeredByFakeServer.includes(toolName)) {
|
||||
throw new Error(`NapCat MCP registerTools self-check failed: ${toolName}`);
|
||||
}
|
||||
}
|
||||
if (parseNapcatAutomationClassification("manual-review") !== "manual-review") {
|
||||
throw new Error("NapCat classification parser self-check failed");
|
||||
}
|
||||
const hotZoneAudit = classifyNapcatUpstreamAudit({
|
||||
dryMergeConflict: false,
|
||||
forkPatchFiles: ["packages/napcat-core/login/runtime.ts"],
|
||||
upstreamChangedFiles: ["packages/napcat-core/login/runtime.ts"],
|
||||
});
|
||||
if (hotZoneAudit.classification !== "manual-review") {
|
||||
throw new Error("NapCat hot-zone audit classification self-check failed");
|
||||
}
|
||||
if (!hotZoneAudit.reasonCodes.includes("HOT_ZONE_CHANGED")) {
|
||||
throw new Error("NapCat hot-zone reason-code self-check failed");
|
||||
}
|
||||
const dryRunAudit = buildNapcatUpstreamAudit({
|
||||
dryMergeConflict: false,
|
||||
forkPatchFiles: ["packages/napcat-core/runtime/base.ts"],
|
||||
upstreamChangedFiles: ["packages/napcat-core/runtime/upstream.ts"],
|
||||
});
|
||||
if (dryRunAudit.execute !== false || dryRunAudit.artifacts.length === 0) {
|
||||
throw new Error(
|
||||
"NapCat upstream audit dry-run artifact plan self-check failed",
|
||||
);
|
||||
}
|
||||
const dryRunAuditDefaultCommands = dryRunAudit.commands.map(
|
||||
(item) => item.command,
|
||||
);
|
||||
if (
|
||||
dryRunAuditDefaultCommands.some((command) =>
|
||||
command.includes("FORK_BRANCH:-HEAD"),
|
||||
) ||
|
||||
!dryRunAuditDefaultCommands.some((command) =>
|
||||
command.includes(`FORK_BRANCH:-${napcatAutomationDefaults.forkBranch}`),
|
||||
) ||
|
||||
!dryRunAuditDefaultCommands.some((command) =>
|
||||
command.includes(
|
||||
`UPSTREAM_RELEASE_REF:-${napcatAutomationDefaults.upstreamReleaseRef}`,
|
||||
),
|
||||
)
|
||||
) {
|
||||
throw new Error("NapCat upstream audit command default alignment self-check failed");
|
||||
}
|
||||
const codexCommand = buildNapcatCodexExecCommand({
|
||||
artifactRoot: "/vol1/docker/kt-codex/artifacts/smoke",
|
||||
contextPacketPath:
|
||||
"/vol1/docker/kt-codex/artifacts/smoke/context-packet.md",
|
||||
outputSchemaPath:
|
||||
"/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json",
|
||||
promptName: "upstream-audit",
|
||||
workspaceRoot: "/vol1/docker/kt-codex/workspace/KT",
|
||||
});
|
||||
if (!codexCommand.includes("--ask-for-approval never")) {
|
||||
throw new Error("NapCat Codex runner approval policy self-check failed");
|
||||
}
|
||||
if (!codexCommand.includes("codex --ask-for-approval never exec")) {
|
||||
throw new Error("NapCat Codex runner top-level approval order self-check failed");
|
||||
}
|
||||
if (codexCommand.includes("codex exec") || codexCommand.includes("$(cat")) {
|
||||
throw new Error("NapCat Codex runner stdin safety self-check failed");
|
||||
}
|
||||
for (const expected of [
|
||||
"CODEX_HOME='/vol1/docker/kt-codex/home/.codex'",
|
||||
"--cd '/vol1/docker/kt-codex/workspace/KT'",
|
||||
"--profile-v2 automation",
|
||||
"--sandbox workspace-write",
|
||||
"--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'",
|
||||
]) {
|
||||
if (!codexCommand.includes(expected)) {
|
||||
throw new Error(`NapCat Codex runner command missing: ${expected}`);
|
||||
}
|
||||
}
|
||||
if (codexCommand.includes("git push") || codexCommand.includes("kubectl")) {
|
||||
throw new Error("NapCat Codex runner mutation guard self-check failed");
|
||||
}
|
||||
const upstreamSyncJenkinsTemplate = readFileSync(
|
||||
resolveInsideRoot(
|
||||
"mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const runtimeReleaseJenkinsTemplate = readFileSync(
|
||||
resolveInsideRoot(
|
||||
"mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
for (const expected of [
|
||||
"RUN_CODEX_AUDIT",
|
||||
"CONFIRM_CODEX_WORKSPACE_WRITE",
|
||||
"RUN_CODEX_WORKSPACE_WRITE_AUDIT",
|
||||
]) {
|
||||
if (!upstreamSyncJenkinsTemplate.includes(expected)) {
|
||||
throw new Error(`NapCat upstream Jenkins template missing: ${expected}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Verifies that the default upstream audit Jenkins call stays dry-run before the optional Codex execution stage.
|
||||
* @param template - Full Jenkinsfile text to inspect.
|
||||
* @returns Nothing; throws when the default audit call is unsafe.
|
||||
*/
|
||||
const assertUpstreamJenkinsDefaultAuditDryRun = (template: string): void => {
|
||||
const defaultStageStart = template.indexOf(
|
||||
"stage('ktWorkflow upstream audit dry-run')",
|
||||
);
|
||||
const codexStageStart = template.indexOf(
|
||||
"stage('ktWorkflow upstream audit with Codex')",
|
||||
);
|
||||
if (
|
||||
defaultStageStart === -1 ||
|
||||
codexStageStart === -1 ||
|
||||
defaultStageStart >= codexStageStart
|
||||
) {
|
||||
throw new Error(
|
||||
"NapCat upstream Jenkins template default audit stage order self-check failed",
|
||||
);
|
||||
}
|
||||
const defaultUpstreamAuditStage = template.slice(
|
||||
defaultStageStart,
|
||||
codexStageStart,
|
||||
);
|
||||
if (!defaultUpstreamAuditStage.includes("run napcat-upstream-audit --")) {
|
||||
throw new Error(
|
||||
"NapCat upstream Jenkins template default audit stage missing audit command",
|
||||
);
|
||||
}
|
||||
for (const forbiddenFlag of ["--execute", "--use-codex"]) {
|
||||
if (defaultUpstreamAuditStage.includes(forbiddenFlag)) {
|
||||
throw new Error(
|
||||
`NapCat upstream Jenkins template default audit must not include ${forbiddenFlag}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
assertUpstreamJenkinsDefaultAuditDryRun(upstreamSyncJenkinsTemplate);
|
||||
const unsafeDefaultUpstreamAuditTemplate = [
|
||||
"stage('ktWorkflow upstream audit dry-run') {",
|
||||
" pnpm --dir \"$KT_WORKFLOW_DIR\" run napcat-upstream-audit -- \\",
|
||||
" --execute \\",
|
||||
" --use-codex",
|
||||
"}",
|
||||
"stage('ktWorkflow upstream audit with Codex') {",
|
||||
" pnpm --dir \"$KT_WORKFLOW_DIR\" run napcat-upstream-audit -- \\",
|
||||
" --execute \\",
|
||||
" --use-codex",
|
||||
"}",
|
||||
].join("\n");
|
||||
let unsafeDefaultUpstreamAuditRejected = false;
|
||||
try {
|
||||
assertUpstreamJenkinsDefaultAuditDryRun(
|
||||
unsafeDefaultUpstreamAuditTemplate,
|
||||
);
|
||||
} catch (error) {
|
||||
unsafeDefaultUpstreamAuditRejected = String(error).includes(
|
||||
"NapCat upstream Jenkins template default audit must not include",
|
||||
);
|
||||
}
|
||||
if (!unsafeDefaultUpstreamAuditRejected) {
|
||||
throw new Error(
|
||||
"NapCat upstream Jenkins template unsafe default audit self-check failed",
|
||||
);
|
||||
}
|
||||
for (const forbidden of ["git merge", "git push", "docker build", "kubectl"]) {
|
||||
if (upstreamSyncJenkinsTemplate.includes(forbidden)) {
|
||||
throw new Error(
|
||||
`NapCat upstream Jenkins template forbidden command leaked: ${forbidden}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const expected of [
|
||||
"choice(name: 'RUNTIME_BUILD_JOB'",
|
||||
"choice(name: 'API_PROMOTION_JOB'",
|
||||
"CONFIRM_API_PROMOTION_TARGET",
|
||||
"PROMOTE_KT_TEMPLATE_API_MAIN",
|
||||
]) {
|
||||
if (!runtimeReleaseJenkinsTemplate.includes(expected)) {
|
||||
throw new Error(`NapCat runtime Jenkins template missing: ${expected}`);
|
||||
}
|
||||
}
|
||||
for (const forbidden of [
|
||||
"string(name: 'RUNTIME_BUILD_JOB'",
|
||||
"string(name: 'API_PROMOTION_JOB'",
|
||||
"git merge",
|
||||
"git push",
|
||||
"docker build",
|
||||
"kubectl",
|
||||
]) {
|
||||
if (runtimeReleaseJenkinsTemplate.includes(forbidden)) {
|
||||
throw new Error(
|
||||
`NapCat runtime Jenkins template forbidden text leaked: ${forbidden}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const runtimeImageBuildStageStart = runtimeReleaseJenkinsTemplate.indexOf(
|
||||
"stage('Runtime image build and publish')",
|
||||
);
|
||||
const apiPromotionStageStart = runtimeReleaseJenkinsTemplate.indexOf(
|
||||
"stage('API promotion')",
|
||||
);
|
||||
const pipelineEndBeforeRuntimeHelpers = runtimeReleaseJenkinsTemplate.indexOf(
|
||||
"\n}\n\n/**",
|
||||
apiPromotionStageStart,
|
||||
);
|
||||
if (
|
||||
runtimeImageBuildStageStart === -1 ||
|
||||
apiPromotionStageStart === -1 ||
|
||||
pipelineEndBeforeRuntimeHelpers === -1 ||
|
||||
runtimeImageBuildStageStart > apiPromotionStageStart
|
||||
) {
|
||||
throw new Error("NapCat runtime Jenkins template stage slice self-check failed");
|
||||
}
|
||||
const runtimeImageBuildStage = runtimeReleaseJenkinsTemplate.slice(
|
||||
runtimeImageBuildStageStart,
|
||||
apiPromotionStageStart,
|
||||
);
|
||||
const apiPromotionStage = runtimeReleaseJenkinsTemplate.slice(
|
||||
apiPromotionStageStart,
|
||||
pipelineEndBeforeRuntimeHelpers,
|
||||
);
|
||||
if (runtimeImageBuildStage.includes("CONFIRM_API_PROMOTION_TARGET")) {
|
||||
throw new Error(
|
||||
"NapCat runtime Jenkins runtime image build stage must not check API promotion confirmation",
|
||||
);
|
||||
}
|
||||
if (!apiPromotionStage.includes("CONFIRM_API_PROMOTION_TARGET")) {
|
||||
throw new Error(
|
||||
"NapCat runtime Jenkins API promotion stage missing confirmation target check",
|
||||
);
|
||||
}
|
||||
if (!apiPromotionStage.includes("PROMOTE_KT_TEMPLATE_API_MAIN")) {
|
||||
throw new Error(
|
||||
"NapCat runtime Jenkins API promotion stage missing production confirmation value",
|
||||
);
|
||||
}
|
||||
const apiPromotionConfirmationIndex = apiPromotionStage.indexOf(
|
||||
"CONFIRM_API_PROMOTION_TARGET",
|
||||
);
|
||||
const apiPromotionBuildIndex = apiPromotionStage.indexOf(
|
||||
"build job: params.API_PROMOTION_JOB",
|
||||
);
|
||||
if (
|
||||
apiPromotionConfirmationIndex === -1 ||
|
||||
apiPromotionBuildIndex === -1 ||
|
||||
apiPromotionConfirmationIndex > apiPromotionBuildIndex
|
||||
) {
|
||||
throw new Error(
|
||||
"NapCat runtime Jenkins API promotion confirmation must run before API build job",
|
||||
);
|
||||
}
|
||||
const codexExecutionArtifactRoot = resolveInsideRoot(
|
||||
".kt-workspace/test-artifacts/napcat-codex-runner-self-test",
|
||||
);
|
||||
const codexInvalidSchemaAudit = buildNapcatUpstreamAudit({
|
||||
artifactRoot: codexExecutionArtifactRoot,
|
||||
codexExecutor: () => ({
|
||||
ok: true,
|
||||
stderr: "schema warning",
|
||||
stdout: "{\"classification\":\"surprising\"}\n",
|
||||
}),
|
||||
dryMergeConflict: false,
|
||||
execute: true,
|
||||
forkPatchFiles: [],
|
||||
upstreamChangedFiles: [],
|
||||
useCodex: true,
|
||||
});
|
||||
if (
|
||||
codexInvalidSchemaAudit.classification !== "blocked" ||
|
||||
!codexInvalidSchemaAudit.reasonCodes.includes("CODEX_SCHEMA_INVALID")
|
||||
) {
|
||||
throw new Error("NapCat Codex invalid schema fallback self-check failed");
|
||||
}
|
||||
if (
|
||||
!existsSync(
|
||||
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.jsonl"),
|
||||
) ||
|
||||
!existsSync(path.join(codexExecutionArtifactRoot, "context-packet.md")) ||
|
||||
readFileSync(
|
||||
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.stderr.log"),
|
||||
"utf8",
|
||||
) !== "schema warning"
|
||||
) {
|
||||
throw new Error("NapCat Codex artifact capture self-check failed");
|
||||
}
|
||||
const codexItemTextArtifactRoot = resolveInsideRoot(
|
||||
".kt-workspace/test-artifacts/napcat-codex-runner-success-self-test",
|
||||
);
|
||||
const codexItemTextAudit = buildNapcatUpstreamAudit({
|
||||
artifactRoot: codexItemTextArtifactRoot,
|
||||
codexExecutor: () => ({
|
||||
ok: true,
|
||||
stderr: "",
|
||||
stdout: `${JSON.stringify({
|
||||
item: {
|
||||
text: JSON.stringify({
|
||||
classification: "safe-candidate",
|
||||
reasonCodes: ["NO_UPSTREAM_CHANGE"],
|
||||
recommendedAction: "create-candidate-branch",
|
||||
summary: "ok",
|
||||
}),
|
||||
type: "agent_message",
|
||||
},
|
||||
type: "item.completed",
|
||||
})}\n`,
|
||||
}),
|
||||
dryMergeConflict: false,
|
||||
execute: true,
|
||||
forkPatchFiles: [],
|
||||
upstreamChangedFiles: [],
|
||||
useCodex: true,
|
||||
});
|
||||
if (
|
||||
codexItemTextAudit.classification !== "safe-candidate" ||
|
||||
!codexItemTextAudit.reasonCodes.includes("NO_UPSTREAM_CHANGE") ||
|
||||
codexItemTextAudit.recommendedAction !== "create-candidate-branch"
|
||||
) {
|
||||
throw new Error("NapCat Codex item.text JSONL success self-check failed");
|
||||
}
|
||||
const codexThrowArtifactRoot = resolveInsideRoot(
|
||||
".kt-workspace/test-artifacts/napcat-codex-runner-throw-self-test",
|
||||
);
|
||||
const codexThrowAudit = buildNapcatUpstreamAudit({
|
||||
artifactRoot: codexThrowArtifactRoot,
|
||||
codexExecutor: () => {
|
||||
throw new Error("codex timed out");
|
||||
},
|
||||
dryMergeConflict: false,
|
||||
execute: true,
|
||||
forkPatchFiles: [],
|
||||
upstreamChangedFiles: [],
|
||||
useCodex: true,
|
||||
});
|
||||
if (
|
||||
codexThrowAudit.classification !== "blocked" ||
|
||||
!readFileSync(
|
||||
path.join(codexThrowArtifactRoot, "codex-upstream-audit.stderr.log"),
|
||||
"utf8",
|
||||
).includes("codex timed out")
|
||||
) {
|
||||
throw new Error("NapCat Codex thrown executor stderr self-check failed");
|
||||
}
|
||||
const unsafeArtifactRootPath = path.resolve("/tmp/not-kt");
|
||||
const unsafeArtifactRootExisted = existsSync(unsafeArtifactRootPath);
|
||||
let unsafeArtifactRootRejected = false;
|
||||
let unsafeArtifactExecutorCalled = false;
|
||||
try {
|
||||
buildNapcatUpstreamAudit({
|
||||
artifactRoot: "/tmp/not-kt",
|
||||
codexExecutor: () => {
|
||||
unsafeArtifactExecutorCalled = true;
|
||||
return { ok: true, stderr: "", stdout: "{}\n" };
|
||||
},
|
||||
dryMergeConflict: false,
|
||||
execute: true,
|
||||
forkPatchFiles: [],
|
||||
upstreamChangedFiles: [],
|
||||
useCodex: true,
|
||||
});
|
||||
} catch (error) {
|
||||
unsafeArtifactRootRejected = String(error).includes(
|
||||
"Unsafe NapCat Codex artifactRoot",
|
||||
);
|
||||
} finally {
|
||||
if (!unsafeArtifactRootExisted && existsSync(unsafeArtifactRootPath)) {
|
||||
rmSync(unsafeArtifactRootPath, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
if (!unsafeArtifactRootRejected || unsafeArtifactExecutorCalled) {
|
||||
throw new Error("NapCat Codex unsafe artifact root self-check failed");
|
||||
}
|
||||
const dryRunAuditCommands: string[] = [];
|
||||
for (const command of dryRunAudit.commands) {
|
||||
if (!command.readOnly) {
|
||||
throw new Error("NapCat upstream audit dry-run read-only flag failed");
|
||||
}
|
||||
dryRunAuditCommands.push(command.command);
|
||||
}
|
||||
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
|
||||
for (const expected of [
|
||||
"git status --short --branch",
|
||||
"git fetch --dry-run upstream --tags --prune",
|
||||
"git fetch --dry-run origin --prune",
|
||||
"git diff --name-only",
|
||||
"git merge-tree",
|
||||
]) {
|
||||
if (!dryRunAuditCommandText.includes(expected)) {
|
||||
throw new Error(
|
||||
`NapCat upstream audit dry-run command missing: ${expected}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const command of dryRunAudit.commands) {
|
||||
if (
|
||||
command.readOnly &&
|
||||
/^git\s+fetch(?:\s|$)/.test(command.command) &&
|
||||
!/\s--dry-run(?:\s|$)/.test(command.command)
|
||||
) {
|
||||
throw new Error(
|
||||
"NapCat upstream audit read-only fetch must use --dry-run",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const forbidden of [
|
||||
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
|
||||
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
|
||||
{ label: "docker build", regex: /(^|\n)\s*docker\s+build(?:\s|$)/ },
|
||||
{ label: "kubectl", regex: /(^|\n)\s*kubectl(?:\s|$)/ },
|
||||
{ label: "deploy", regex: /(^|\n)\s*deploy(?:\s|$)/ },
|
||||
]) {
|
||||
if (forbidden.regex.test(dryRunAuditCommandText)) {
|
||||
throw new Error(
|
||||
`NapCat upstream audit dry-run command is not read-only: ${forbidden.label}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const invalidPathCase of [
|
||||
{
|
||||
forkPatchFiles: [],
|
||||
name: "Windows absolute path",
|
||||
upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"],
|
||||
},
|
||||
{
|
||||
forkPatchFiles: [],
|
||||
name: "Unix absolute path",
|
||||
upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"],
|
||||
},
|
||||
{
|
||||
forkPatchFiles: [],
|
||||
name: "UNC path",
|
||||
upstreamChangedFiles: [
|
||||
"\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
forkPatchFiles: [],
|
||||
name: "Windows rooted path",
|
||||
upstreamChangedFiles: [
|
||||
"\\packages\\napcat-core\\login\\runtime.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
forkPatchFiles: [],
|
||||
name: "Windows drive-relative path",
|
||||
upstreamChangedFiles: ["C:packages/napcat-core/login/runtime.ts"],
|
||||
},
|
||||
{
|
||||
forkPatchFiles: [],
|
||||
name: "parent segment path",
|
||||
upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||||
},
|
||||
{
|
||||
forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||||
name: "fork patch parent segment path",
|
||||
upstreamChangedFiles: [],
|
||||
},
|
||||
]) {
|
||||
const invalidPathAudit = classifyNapcatUpstreamAudit({
|
||||
dryMergeConflict: false,
|
||||
forkPatchFiles: invalidPathCase.forkPatchFiles,
|
||||
upstreamChangedFiles: invalidPathCase.upstreamChangedFiles,
|
||||
});
|
||||
if (
|
||||
invalidPathAudit.classification !== "blocked" ||
|
||||
invalidPathAudit.recommendedAction !== "block" ||
|
||||
!invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE")
|
||||
) {
|
||||
throw new Error(
|
||||
`NapCat invalid audit path self-check failed: ${invalidPathCase.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const promptName of napcatAutomationPromptNames) {
|
||||
const promptText = loadNapcatAutomationPrompt(promptName);
|
||||
if (!promptText.includes("Do not edit files")) {
|
||||
throw new Error(`NapCat prompt safety contract missing: ${promptName}`);
|
||||
}
|
||||
}
|
||||
let traversalPromptRejected = false;
|
||||
try {
|
||||
loadNapcatAutomationPrompt("../../README" as NapcatAutomationPromptName);
|
||||
} catch (error) {
|
||||
traversalPromptRejected = String(error).includes(
|
||||
"Unsupported NapCat automation prompt",
|
||||
);
|
||||
}
|
||||
if (!traversalPromptRejected) {
|
||||
throw new Error("NapCat prompt traversal self-check failed");
|
||||
}
|
||||
|
||||
const reviewClassifier = {
|
||||
localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"),
|
||||
realTokenRejected: !isBenignCredentialReviewValue(
|
||||
@ -1026,6 +1833,18 @@ export async function runSelfTest(): Promise<void> {
|
||||
throw new Error("database migration verification self-check failed");
|
||||
}
|
||||
|
||||
const qqbotAccountScanPlan = buildBusinessTestPlan({
|
||||
flow: "qqbot-account-scan",
|
||||
});
|
||||
const qqbotAccountScanText = JSON.stringify(qqbotAccountScanPlan);
|
||||
if (
|
||||
!qqbotAccountScanText.includes("pending sessionId") ||
|
||||
!qqbotAccountScanText.includes("第一轮 Docker run") ||
|
||||
!qqbotAccountScanText.includes("不能造脏数据")
|
||||
) {
|
||||
throw new Error("QQBot account scan non-dirty smoke self-check failed");
|
||||
}
|
||||
|
||||
const closeoutNeedsUpgrade = buildWorkstreamCloseout({
|
||||
cleanupEvidence: ["cleanup-history dry-run deleted=0。"],
|
||||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||||
|
||||
@ -3,8 +3,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
import {
|
||||
parseNasCodexBootstrapCliArgs,
|
||||
parseDeployObservationCliArgs,
|
||||
parseGlobalReviewCliArgs,
|
||||
parseNapcatRemoteDevHandoffCliArgs,
|
||||
parseNapcatRuntimeReleaseReadinessCliArgs,
|
||||
parseNapcatSyncCandidateReviewCliArgs,
|
||||
parseNapcatUpstreamAuditCliArgs,
|
||||
parseObsidianContextCliArgs,
|
||||
parseObsidianCliArgs,
|
||||
parseObsidianSyncCliArgs,
|
||||
@ -18,6 +23,13 @@ import { buildGlobalCodeReview } from './tools/review.js';
|
||||
import { registerTools } from './registerTools.js';
|
||||
import { runSelfTest } from './selfTest.js';
|
||||
import { buildNapcatDeviceProfileCheck } from './tools/testing.js';
|
||||
import {
|
||||
buildNapcatRemoteDevHandoff,
|
||||
buildNapcatRuntimeReleaseReadiness,
|
||||
buildNapcatSyncCandidateReview,
|
||||
buildNapcatUpstreamAudit,
|
||||
buildNasCodexBootstrapPlan,
|
||||
} from './tools/napcatAutomation.js';
|
||||
|
||||
if (process.argv.includes('--cleanup-history')) {
|
||||
console.log(JSON.stringify(cleanupHistoryArtifacts(parseCliCleanupArgs(process.argv)), null, 2));
|
||||
@ -43,6 +55,38 @@ if (process.argv.includes('--cleanup-history')) {
|
||||
2,
|
||||
),
|
||||
);
|
||||
} else if (process.argv.includes('--napcat-upstream-audit')) {
|
||||
console.log(
|
||||
JSON.stringify(buildNapcatUpstreamAudit(parseNapcatUpstreamAuditCliArgs(process.argv)), null, 2),
|
||||
);
|
||||
} else if (process.argv.includes('--napcat-sync-candidate-review')) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
buildNapcatSyncCandidateReview(parseNapcatSyncCandidateReviewCliArgs(process.argv)),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} else if (process.argv.includes('--napcat-runtime-release-readiness')) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
buildNapcatRuntimeReleaseReadiness(parseNapcatRuntimeReleaseReadinessCliArgs(process.argv)),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} else if (process.argv.includes('--napcat-remote-dev-handoff')) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
buildNapcatRemoteDevHandoff(parseNapcatRemoteDevHandoffCliArgs(process.argv)),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} else if (process.argv.includes('--nas-codex-bootstrap')) {
|
||||
console.log(
|
||||
JSON.stringify(buildNasCodexBootstrapPlan(parseNasCodexBootstrapCliArgs(process.argv)), null, 2),
|
||||
);
|
||||
} else if (process.argv.includes('--self-test')) {
|
||||
await runSelfTest();
|
||||
} else if (process.argv.includes('--workstream-closeout')) {
|
||||
|
||||
64
src/tools/napcatAutomation.prompts.ts
Normal file
64
src/tools/napcatAutomation.prompts.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const napcatAutomationPromptNames = [
|
||||
'upstream-audit',
|
||||
'sync-candidate-review',
|
||||
'runtime-release-readiness',
|
||||
'remote-dev-handoff',
|
||||
] as const;
|
||||
|
||||
export type NapcatAutomationPromptName =
|
||||
(typeof napcatAutomationPromptNames)[number];
|
||||
|
||||
const promptsRoot = path.resolve(
|
||||
fileURLToPath(new URL('../..', import.meta.url)),
|
||||
'prompts',
|
||||
'napcat',
|
||||
);
|
||||
const promptsRootBoundary = promptsRoot.endsWith(path.sep)
|
||||
? promptsRoot
|
||||
: `${promptsRoot}${path.sep}`;
|
||||
const napcatAutomationPromptNameSet = new Set<string>(
|
||||
napcatAutomationPromptNames,
|
||||
);
|
||||
const napcatAutomationPromptFileNames = {
|
||||
'upstream-audit': 'upstream-audit.md',
|
||||
'sync-candidate-review': 'sync-candidate-review.md',
|
||||
'runtime-release-readiness': 'runtime-release-readiness.md',
|
||||
'remote-dev-handoff': 'remote-dev-handoff.md',
|
||||
} satisfies Record<NapcatAutomationPromptName, string>;
|
||||
|
||||
/**
|
||||
* Parses an external prompt name into the source-controlled NapCat prompt enum.
|
||||
* @param value - Raw prompt name from CLI, JSON, MCP input, or a typed caller; only exact entries in `napcatAutomationPromptNames` are accepted.
|
||||
* @returns A stable prompt name that can be mapped to a fixed file under `prompts/napcat`.
|
||||
*/
|
||||
export function parseNapcatAutomationPromptName(
|
||||
value: string,
|
||||
): NapcatAutomationPromptName {
|
||||
if (napcatAutomationPromptNameSet.has(value)) {
|
||||
return value as NapcatAutomationPromptName;
|
||||
}
|
||||
throw new Error(`Unsupported NapCat automation prompt: ${value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a source-controlled NapCat automation prompt by stable prompt name.
|
||||
* @param name - Prompt identifier from `napcatAutomationPromptNames`; runtime callers may still pass untyped external strings, so the loader re-validates and resolves only fixed filenames.
|
||||
* @returns UTF-8 prompt text used by Codex CLI automation.
|
||||
*/
|
||||
export function loadNapcatAutomationPrompt(
|
||||
name: NapcatAutomationPromptName,
|
||||
): string {
|
||||
const promptName = parseNapcatAutomationPromptName(String(name));
|
||||
const promptPath = path.resolve(
|
||||
promptsRoot,
|
||||
napcatAutomationPromptFileNames[promptName],
|
||||
);
|
||||
if (!promptPath.startsWith(promptsRootBoundary)) {
|
||||
throw new Error(`NapCat automation prompt escaped prompt root: ${name}`);
|
||||
}
|
||||
return readFileSync(promptPath, 'utf8');
|
||||
}
|
||||
1335
src/tools/napcatAutomation.ts
Normal file
1335
src/tools/napcatAutomation.ts
Normal file
File diff suppressed because it is too large
Load Diff
130
src/tools/napcatAutomation.types.ts
Normal file
130
src/tools/napcatAutomation.types.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const napcatAutomationClassifications = [
|
||||
'safe-candidate',
|
||||
'manual-review',
|
||||
'blocked',
|
||||
] as const;
|
||||
|
||||
export const napcatAutomationReasonCodes = [
|
||||
'NO_UPSTREAM_CHANGE',
|
||||
'HOT_ZONE_CHANGED',
|
||||
'FORK_PATCH_OVERLAP',
|
||||
'DRY_MERGE_CONFLICT',
|
||||
'BUILD_GRAPH_CHANGED',
|
||||
'METADATA_UNAVAILABLE',
|
||||
'CODEX_SCHEMA_INVALID',
|
||||
] as const;
|
||||
|
||||
export const napcatAutomationRecommendedActions = [
|
||||
'no-op',
|
||||
'create-candidate-branch',
|
||||
'request-human-review',
|
||||
'block',
|
||||
] as const;
|
||||
|
||||
export const upstreamAuditOutputSchema = z.object({
|
||||
classification: z.enum(napcatAutomationClassifications),
|
||||
reasonCodes: z.array(z.enum(napcatAutomationReasonCodes)).min(1),
|
||||
recommendedAction: z.enum(napcatAutomationRecommendedActions),
|
||||
summary: z.string().min(1),
|
||||
}).strict();
|
||||
|
||||
export type NapcatAutomationClassification =
|
||||
(typeof napcatAutomationClassifications)[number];
|
||||
export type NapcatAutomationReasonCode =
|
||||
(typeof napcatAutomationReasonCodes)[number];
|
||||
export type NapcatAutomationRecommendedAction =
|
||||
(typeof napcatAutomationRecommendedActions)[number];
|
||||
|
||||
export interface NapcatAuditClassificationInput {
|
||||
dryMergeConflict: boolean;
|
||||
forkPatchFiles: string[];
|
||||
upstreamChangedFiles: string[];
|
||||
}
|
||||
|
||||
export interface NapcatAuditClassificationResult {
|
||||
classification: NapcatAutomationClassification;
|
||||
reasonCodes: NapcatAutomationReasonCode[];
|
||||
recommendedAction: NapcatAutomationRecommendedAction;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface NapcatAuditCommand {
|
||||
command: string;
|
||||
description: string;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export interface NapcatCodexExecCommandInput {
|
||||
artifactRoot: string;
|
||||
codexHome?: string;
|
||||
contextPacketPath: string;
|
||||
outputSchemaPath: string;
|
||||
promptName: 'upstream-audit';
|
||||
workspaceRoot: string;
|
||||
}
|
||||
|
||||
export interface NapcatCodexExecResult {
|
||||
ok: boolean;
|
||||
stderr: string;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
export type NapcatCodexExecutor = (
|
||||
command: string,
|
||||
) => NapcatCodexExecResult;
|
||||
|
||||
export interface NapcatUpstreamAuditInput
|
||||
extends NapcatAuditClassificationInput {
|
||||
artifactRoot?: string;
|
||||
codexExecutor?: NapcatCodexExecutor;
|
||||
codexHome?: string;
|
||||
createCandidateBranch?: boolean;
|
||||
execute?: boolean;
|
||||
forkBranch?: string;
|
||||
forkRepo?: string;
|
||||
lastAcceptedUpstreamBase?: string;
|
||||
outputSchemaPath?: string;
|
||||
promptName?: 'upstream-audit';
|
||||
upstreamReleaseTag?: string;
|
||||
upstreamRepo?: string;
|
||||
useCodex?: boolean;
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
|
||||
export interface NapcatAutomationArtifact {
|
||||
path: string;
|
||||
type: 'context' | 'json' | 'jsonl' | 'log' | 'markdown' | 'script';
|
||||
}
|
||||
|
||||
export interface NapcatAutomationResult {
|
||||
artifacts: NapcatAutomationArtifact[];
|
||||
classification: NapcatAutomationClassification;
|
||||
execute: boolean;
|
||||
recommendedAction: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface NapcatUpstreamAuditResult
|
||||
extends NapcatAuditClassificationResult {
|
||||
artifacts: NapcatAutomationArtifact[];
|
||||
commands: NapcatAuditCommand[];
|
||||
execute: boolean;
|
||||
hotZonePatterns: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the upstream audit classification emitted by deterministic collectors or Codex.
|
||||
* @param value - Raw classification string from deterministic audit metadata or Codex JSON output; must match the shared NapCat automation taxonomy.
|
||||
* @returns A supported NapCat automation classification for downstream workflow decisions.
|
||||
*/
|
||||
export function parseNapcatAutomationClassification(
|
||||
value: string,
|
||||
): NapcatAutomationClassification {
|
||||
const parsed = z.enum(napcatAutomationClassifications).safeParse(value);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Unsupported NapCat audit classification: ${value}`);
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
@ -44,7 +44,7 @@ function createNapcatCheck(
|
||||
/**
|
||||
* Checks that the API project still contains the NapCat device-profile guardrails needed to avoid unknown QQ devices.
|
||||
* @param input - Project alias or path to scan; defaults to the API backend.
|
||||
* @returns Static guardrail report covering hostname, MAC, machine-info, runtime dir, and DB timezone.
|
||||
* @returns Static guardrail report covering hostname, physical-style MAC, machine-info, runtime dir, and DB timezone.
|
||||
*/
|
||||
export function buildNapcatDeviceProfileCheck(
|
||||
input: NapcatDeviceProfileCheckInput = {},
|
||||
@ -76,10 +76,12 @@ export function buildNapcatDeviceProfileCheck(
|
||||
'device identity uses qqnt-visible-hostname-v1 and pc-<8hex>',
|
||||
),
|
||||
createNapcatCheck(
|
||||
'docker-bridge-mac-strategy',
|
||||
deviceIdentity.includes('docker-bridge-mac-v1') &&
|
||||
deviceIdentity.includes('02:42:'),
|
||||
'device identity uses docker-bridge-mac-v1 and stable 02:42:* MACs',
|
||||
'physical-oui-mac-strategy',
|
||||
deviceIdentity.includes('physical-oui-mac-v1') &&
|
||||
deviceIdentity.includes('hasPhysicalOuiMacPrefix') &&
|
||||
deviceIdentity.includes('isRejectedVirtualMacPrefix') &&
|
||||
deviceIdentity.includes('NAPCAT_PHYSICAL_OUI_PREFIXES'),
|
||||
'device identity uses physical-oui-mac-v1 and rejects Docker/QEMU virtual MAC prefixes',
|
||||
),
|
||||
createNapcatCheck(
|
||||
'machine-info-options',
|
||||
@ -230,13 +232,26 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
|
||||
},
|
||||
'qqbot-account-scan': {
|
||||
assertions: [
|
||||
'扫码创建返回 qrcode/containerName/webuiPort。',
|
||||
'扫码创建接口先返回 pending sessionId,再通过 SSE/status 展示容器创建、二维码生成、等待扫码和终态。',
|
||||
'新增账号首次容器创建必须在第一轮 Docker run 注入设备身份和中文桌面 runtime profile,而不是登录后补 env。',
|
||||
'取消/过期/失败会清理未绑定 NapCat 容器。',
|
||||
'若真实接口 smoke 会创建线上 QQBot 账号或 NapCat 容器,必须先确认清理方案;不能造脏数据时用 RED/GREEN、只读线上计数复核和未执行真实造号原因替代。',
|
||||
'删除账号会二次确认并删除专属容器。',
|
||||
],
|
||||
preflight: ['NAS SSH 可达', 'NapCat WebUI 可达', 'API QQBot env 配置完整'],
|
||||
preflight: [
|
||||
'NAS SSH 可达',
|
||||
'NapCat WebUI 可达',
|
||||
'API QQBot env 配置完整',
|
||||
'确认本轮是否允许创建真实 QQBot 账号和 NapCat 容器',
|
||||
],
|
||||
projects: ['api', 'admin'],
|
||||
steps: ['打开账号连接页', '点击扫码新增账号', '轮询二维码状态', '确认账号回填', '测试删除联动容器'],
|
||||
steps: [
|
||||
'打开账号连接页',
|
||||
'点击扫码新增账号并确认接口不等待 Docker/WebUI 长耗时',
|
||||
'订阅 scan/events 或轮询 status 确认中文进度',
|
||||
'确认账号回填和 runtime/profile 归属',
|
||||
'测试删除联动容器或记录未造号的替代验证证据',
|
||||
],
|
||||
},
|
||||
'qqbot-login-sse': {
|
||||
assertions: [
|
||||
|
||||
34
src/types.ts
34
src/types.ts
@ -189,6 +189,40 @@ export interface NapcatDeviceProfileCheckInput {
|
||||
project?: string;
|
||||
}
|
||||
|
||||
export interface NapcatPromptToolInput {
|
||||
artifactRoot?: string;
|
||||
candidateBranch?: string;
|
||||
execute?: boolean;
|
||||
forkBranch?: string;
|
||||
forkRepo?: string;
|
||||
upstreamReleaseTag?: string;
|
||||
upstreamRepo?: string;
|
||||
useCodex?: boolean;
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
|
||||
export interface NapcatSyncCandidateReviewInput extends NapcatPromptToolInput {
|
||||
lastAcceptedUpstreamBase?: string;
|
||||
}
|
||||
|
||||
export interface NapcatRuntimeReleaseReadinessInput extends NapcatPromptToolInput {
|
||||
apiImageTag?: string;
|
||||
napcatImageTag?: string;
|
||||
profile?: string;
|
||||
}
|
||||
|
||||
export interface NapcatRemoteDevHandoffInput extends NapcatPromptToolInput {
|
||||
targetHost?: string;
|
||||
}
|
||||
|
||||
export interface NasCodexBootstrapPlanInput {
|
||||
artifactRoot?: string;
|
||||
codexHome?: string;
|
||||
execute?: boolean;
|
||||
serviceName?: string;
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
|
||||
export interface BlockerResolutionInput {
|
||||
attempts?: number;
|
||||
command?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user