diff --git a/README.md b/README.md index 8514f63..5140b72 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。 - 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留;CLI 默认 dry-run,真实清理必须显式传 `--execute`。 - 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。 -- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物、`TASKS.md` 最近记录字段结构和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签,避免历史误报污染上下文;任何文件改动后都要跑一遍。 +- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、NapCat `latest` 镜像漂移风险、根目录生成产物、`TASKS.md` 最近记录字段结构和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签,避免历史误报污染上下文;任何文件改动后都要跑一遍。 - 生成或写入 `TASKS.md` 最近记录:默认 `dryRun=true`,确认后再落盘。 - 生成提交前检查清单:校验 KT commit message 约定。 diff --git a/src/selfTest.ts b/src/selfTest.ts index 2f40819..9f0dc26 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -12,7 +12,7 @@ import { cleanupHistoryArtifacts, parseCliCleanupArgs } from './tools/cleanup.js import { buildWorkflowLoopAudit } from './tools/loop.js'; import { inspectProject } from './tools/inspect.js'; import { prepareTask, readWorkflowContext } from './tools/task.js'; -import { buildGlobalCodeReview, findTaskRecordGovernanceFindings, isBenignCredentialReviewValue } from './tools/review.js'; +import { buildGlobalCodeReview, findNapcatImageGovernanceFindings, findTaskRecordGovernanceFindings, isBenignCredentialReviewValue } from './tools/review.js'; import { buildBusinessTestPlan } from './tools/testing.js'; import { buildVerificationPlan } from './tools/verification.js'; import { buildWorkstreamCloseout } from './tools/closeout.js'; @@ -39,6 +39,17 @@ export async function runSelfTest(): Promise { throw new Error('TASKS record governance self-check failed'); } + const napcatImageFindings = findNapcatImageGovernanceFindings( + ['QQBOT_NAPCAT_IMAGE=mlikiowa/napcat-docker:latest'], + { + file: '.env.example', + project: 'Node/kt-template-online-api', + }, + ); + if (!napcatImageFindings.some((item) => item.category === 'napcat-latest-image')) { + throw new Error('NapCat image governance self-check failed'); + } + const reviewCliParser = { dashedAll: parseGlobalReviewCliArgs(['node', 'server', '--global-review', '--content-scan-all']).contentScanMode, keyValueAll: parseGlobalReviewCliArgs(['node', 'server', '--global-review', '--contentScanMode=all']).contentScanMode, diff --git a/src/tools/review.ts b/src/tools/review.ts index f551601..8eb7dcc 100644 --- a/src/tools/review.ts +++ b/src/tools/review.ts @@ -54,6 +54,8 @@ const frontendEnvProjectAliases = new Set([ const credentialAssignmentPattern = /\b(password|passwd|secret|token|webuiToken|accessToken|privateKey)\b\s*[:=]\s*(["'])([^"']+)\2/i; +const napcatImageAssignmentPattern = + /^\s*QQBOT_NAPCAT_IMAGE\s*=\s*([^#\s]+)\s*(?:#.*)?$/; const taskRecordAllowedFields = new Set(['范围', '关键词', '验证']); const taskRecordRequiredFields = ['范围', '关键词', '验证'] as const; @@ -153,6 +155,37 @@ export function findTaskRecordGovernanceFindings(lines: string[]): ReviewFinding return findings; } +export function findNapcatImageGovernanceFindings( + lines: string[], + context: { + file: string; + project: string; + }, +): ReviewFinding[] { + return lines.flatMap((line, index) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) return []; + + const matched = line.match(napcatImageAssignmentPattern); + if (!matched) return []; + + const image = matched[1].trim(); + if (!image.endsWith(':latest')) return []; + + return [ + { + category: 'napcat-latest-image', + file: context.file, + line: index + 1, + level: 'P3' as const, + message: 'NapCat 运行镜像使用 latest,生产排查会受到镜像漂移影响。', + project: context.project, + suggestion: '改为明确版本 tag 或 digest,并先在单账号容器观察稳定性后推广。', + }, + ]; + }); +} + async function listGitTrackedFiles(projectPath: string): Promise { const tracked = await tryExecFile('git', ['ls-files'], projectPath); if (!tracked.ok) return []; @@ -324,6 +357,15 @@ function collectProjectContentFindings( } }); + if (includeContentScan) { + findings.push( + ...findNapcatImageGovernanceFindings(lines, { + file, + project: project.relativePath, + }), + ); + } + if ( findings.length + credentialHints.length + debugFindings.length >= maxFindings * 3