fix: 固化 Superpowers 审查闭环
This commit is contained in:
parent
7e79b38600
commit
1d899734b4
16
README.md
16
README.md
@ -9,9 +9,9 @@
|
||||
- 检查子项目环境:Git/SVN、包管理器、Node 版本、env 文件、Git 状态。
|
||||
- 生成防偏差工作包:把开工前检查、禁止项、风险扫描、验证计划汇总成一份可执行清单。
|
||||
- 生成验证建议:按后端、前端、样式、页面、部署、数据库、MCP 等变更类型给出轻量验证命令和迁移核验要点。
|
||||
- 固化改动后 review:验证计划和提交清单都会提醒执行 `kt_global_code_review` / `pnpm run global-review`。
|
||||
- 固化改动后 review:验证计划和提交清单都会提醒执行 `kt_global_code_review` / `pnpm run global-review`;大方向收尾必须同时提供 Superpowers code review 证据。
|
||||
- 生成任务收尾包:把状态、验证计划、可选验证执行、全局 review 和历史清理收成一份结果。
|
||||
- 生成大方向完结闭环审计:确认开发目标、测试证据、问题记录、稳定解法和 ktWorkflow 升级是否齐备。
|
||||
- 生成大方向完结闭环审计:确认开发目标、测试证据、KT 全局 review、Superpowers code review、问题记录、稳定解法和 ktWorkflow 升级是否齐备。
|
||||
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
||||
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
||||
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
||||
@ -37,7 +37,7 @@ pnpm run self-test
|
||||
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" --problem "无新卡点" --solution "无新增稳定解法"
|
||||
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 cleanup-history -- --dry-run
|
||||
pnpm run cleanup-history -- --execute
|
||||
pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
@ -85,8 +85,8 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 |
|
||||
| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 |
|
||||
| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 |
|
||||
| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级 |
|
||||
| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级和全局 review |
|
||||
| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、文档同步、历史清理、KT 全局 review、Superpowers code review、问题记录、稳定解法和 ktWorkflow 升级 |
|
||||
| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review |
|
||||
| `kt_change_doc_sync` | 根据改动路径生成 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 文档同步清单 |
|
||||
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
|
||||
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
|
||||
@ -111,7 +111,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
| `pnpm run obsidian-validate` | 校验 KT Obsidian vault 结构和链接;默认 warning 不让脚本失败,需要严格模式时传 `--fail-on-warnings`。 |
|
||||
| `pnpm run obsidian-sync` | 审计 Obsidian 工作流入口是否连通,并联动执行 validate。 |
|
||||
| `pnpm run cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮并保留模板目录。 |
|
||||
| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级缺口。 |
|
||||
| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、文档同步、历史清理、KT 全局 review、Superpowers code review、问题记录、稳定解法和 ktWorkflow 升级缺口。 |
|
||||
|
||||
## 项目别名
|
||||
|
||||
@ -143,10 +143,10 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
- 需要按知识图谱定位上下文时先调用 `kt_obsidian_context`,例如 `module=Admin` 或 `query=BangDream`;Obsidian 只做文档索引,返回的 `ruleAnchors` 用来读取相关规则文档,`codeAnchors` 必须继续搭配 `kt_inspect_project`、`kt_risk_scan`、`kt_suggest_verification` 锚定真实代码上下文。
|
||||
- 多项目联动时先调用 `kt_inspect_all_projects`。
|
||||
- 要收尾时调用 `kt_finish_task`,默认只生成计划和 review;需要执行验证时显式传 `runValidation=true`。
|
||||
- 大方向结束前调用 `kt_workstream_closeout` 或 `pnpm run workstream-closeout`;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。
|
||||
- 大方向结束前调用 `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` 并复跑。
|
||||
- 代码或配置改动后调用 `kt_change_doc_sync`,把需要同步的 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 入口补齐;无需同步时把原因写进收尾证据。
|
||||
- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级和全局 review 都过门;清理证据必须显式传入,不能依赖缺省值。
|
||||
- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review 都过门;清理证据必须显式传入,不能依赖缺省值。
|
||||
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
|
||||
- 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply`、`qqbot-login-sse`、`fflogs-command`、`system-log-visualization` 等 flow。
|
||||
- 遇到同一命令或同一远程步骤重复卡住时调用 `kt_blocker_resolution`;第二次仍失败时先写入 `TASKS.md` 或补成脚本,再继续。
|
||||
|
||||
@ -79,6 +79,11 @@ export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseo
|
||||
reviewEvidence: readOptionList(argv, ['--review', '--review-evidence']),
|
||||
reusablePatterns: patterns,
|
||||
stableSolutions: readOptionList(argv, ['--solution', '--stable-solution']),
|
||||
superpowersReviewEvidence: readOptionList(argv, [
|
||||
'--superpowers-review',
|
||||
'--superpowers-review-evidence',
|
||||
'--sp-review',
|
||||
]),
|
||||
title: readOption(argv, ['--title']),
|
||||
upgradeNotes: readOption(argv, ['--upgrade-notes']),
|
||||
verificationEvidence: readOptionList(argv, ['--verification', '--verification-evidence']),
|
||||
|
||||
@ -246,7 +246,7 @@ export function registerTools(server: McpServer): void {
|
||||
server.registerTool(
|
||||
'kt_workstream_closeout',
|
||||
{
|
||||
description: '生成大方向完结闭环审计:开发、测试证据、问题记录、稳定解法和 ktWorkflow 升级是否齐备。',
|
||||
description: '生成大方向完结闭环审计:开发、测试证据、KT 全局 review、Superpowers code review、问题记录、稳定解法和 ktWorkflow 升级是否齐备。',
|
||||
inputSchema: {
|
||||
changedProjects: z.array(z.string()).default([]),
|
||||
cleanupEvidence: z.array(z.string()).default([]),
|
||||
@ -257,6 +257,7 @@ export function registerTools(server: McpServer): void {
|
||||
reviewEvidence: z.array(z.string()).default([]),
|
||||
reusablePatterns: z.array(z.enum(workstreamReusablePatternValues)).default(['none']),
|
||||
stableSolutions: z.array(z.string()).default([]),
|
||||
superpowersReviewEvidence: z.array(z.string()).default([]),
|
||||
title: z.string().optional(),
|
||||
upgradeNotes: z.string().optional(),
|
||||
verificationEvidence: z.array(z.string()).default([]),
|
||||
@ -269,7 +270,7 @@ export function registerTools(server: McpServer): void {
|
||||
server.registerTool(
|
||||
'kt_workflow_loop_audit',
|
||||
{
|
||||
description: '审计 KT 自动化闭环:上下文锚定、测试用例、验证证据、历史清理、问题固化、ktWorkflow 升级和全局 review 是否齐备。',
|
||||
description: '审计 KT 自动化闭环:上下文锚定、测试用例、验证证据、历史清理、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review 是否齐备。',
|
||||
inputSchema: {
|
||||
changedProjects: z.array(z.string()).default([]),
|
||||
cleanupExecuted: z.boolean().default(false),
|
||||
@ -285,6 +286,7 @@ export function registerTools(server: McpServer): void {
|
||||
reviewEvidence: z.array(z.string()).default([]),
|
||||
stableSolutions: z.array(z.string()).default([]),
|
||||
stage: z.enum(['failure', 'finish', 'start', 'test']).default('finish'),
|
||||
superpowersReviewEvidence: z.array(z.string()).default([]),
|
||||
taskTitle: z.string().optional(),
|
||||
testCases: z.array(z.string()).default([]),
|
||||
verificationEvidence: z.array(z.string()).default([]),
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { parseGlobalReviewCliArgs } from "./core/cli.js";
|
||||
import {
|
||||
parseGlobalReviewCliArgs,
|
||||
parseWorkstreamCloseoutCliArgs,
|
||||
} from "./core/cli.js";
|
||||
import { registeredToolNames } from "./core/constants.js";
|
||||
import { buildBlockerResolution } from "./tools/blocker.js";
|
||||
import { buildChangeDocSync } from "./tools/docSync.js";
|
||||
@ -135,6 +138,23 @@ export async function runSelfTest(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const closeoutCliParser = parseWorkstreamCloseoutCliArgs([
|
||||
"node",
|
||||
"server",
|
||||
"--workstream-closeout",
|
||||
"--review",
|
||||
"global-review findings=0",
|
||||
"--superpowers-review",
|
||||
"Superpowers reviewer completed",
|
||||
]);
|
||||
if (
|
||||
closeoutCliParser.reviewEvidence?.[0] !== "global-review findings=0" ||
|
||||
closeoutCliParser.superpowersReviewEvidence?.[0] !==
|
||||
"Superpowers reviewer completed"
|
||||
) {
|
||||
throw new Error("workstream closeout review CLI parser self-check failed");
|
||||
}
|
||||
|
||||
const refactorGuardrails = buildGuardrails({
|
||||
project: "mcp",
|
||||
taskType: "refactor",
|
||||
@ -197,6 +217,7 @@ export async function runSelfTest(): Promise<void> {
|
||||
stableSolutions: [
|
||||
"收尾时固定观测 build、commit、镜像、Deployment、Pod 和 smoke。",
|
||||
],
|
||||
superpowersReviewEvidence: ["Superpowers reviewer completed; Important findings handled。"],
|
||||
title: "发布闭环",
|
||||
verificationEvidence: ["Jenkins #132 SUCCESS,K8s Pod Running。"],
|
||||
});
|
||||
@ -212,6 +233,7 @@ export async function runSelfTest(): Promise<void> {
|
||||
reviewEvidence: ["global-review findings=0。"],
|
||||
reusablePatterns: ["none"],
|
||||
stableSolutions: ["无新增稳定解法。"],
|
||||
superpowersReviewEvidence: ["Superpowers reviewer completed; no Critical/Important findings。"],
|
||||
title: "文档收尾",
|
||||
verificationEvidence: ["typecheck/self-test/global-review 通过。"],
|
||||
});
|
||||
@ -226,6 +248,7 @@ export async function runSelfTest(): Promise<void> {
|
||||
reviewEvidence: ["global-review findings=0。"],
|
||||
reusablePatterns: ["none"],
|
||||
stableSolutions: ["无新增稳定解法。"],
|
||||
superpowersReviewEvidence: ["Superpowers reviewer completed。"],
|
||||
title: "无效清理证据",
|
||||
verificationEvidence: ["typecheck 通过。"],
|
||||
});
|
||||
@ -233,6 +256,20 @@ export async function runSelfTest(): Promise<void> {
|
||||
throw new Error("workstream closeout cleanup evidence self-check failed");
|
||||
}
|
||||
|
||||
const closeoutMissingSuperpowersReview = buildWorkstreamCloseout({
|
||||
cleanupFinalDeleted: 0,
|
||||
docSyncEvidence: ["无需文档更新,已记录原因。"],
|
||||
problemRecords: ["无新卡点。"],
|
||||
reviewEvidence: ["global-review findings=0。"],
|
||||
reusablePatterns: ["none"],
|
||||
stableSolutions: ["无新增稳定解法。"],
|
||||
title: "缺 Superpowers 审查证据",
|
||||
verificationEvidence: ["typecheck 通过。"],
|
||||
});
|
||||
if ((closeoutMissingSuperpowersReview.canReportComplete as boolean) !== false) {
|
||||
throw new Error("workstream closeout Superpowers review gate self-check failed");
|
||||
}
|
||||
|
||||
const obsidianContext = readObsidianContext({
|
||||
maxDocuments: 6,
|
||||
module: "ktWorkflow",
|
||||
@ -319,6 +356,7 @@ export async function runSelfTest(): Promise<void> {
|
||||
stableSolutions: [
|
||||
"cleanup-history 默认 dry-run,显式 --execute 后复验 deleted=0。",
|
||||
],
|
||||
superpowersReviewEvidence: ["Superpowers reviewer completed; no Critical/Important findings"],
|
||||
taskTitle: "自动化闭环审计",
|
||||
testCases: ["cleanup-history dry-run/execute 回归测试"],
|
||||
verificationEvidence: ["typecheck/self-test/cleanup dry-run 通过"],
|
||||
@ -338,6 +376,7 @@ export async function runSelfTest(): Promise<void> {
|
||||
const loopAuditMissingCleanup = buildWorkflowLoopAudit({
|
||||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||||
reviewEvidence: ["global-review findings=0。"],
|
||||
superpowersReviewEvidence: ["Superpowers reviewer completed。"],
|
||||
taskTitle: "缺清理证据闭环",
|
||||
testCases: ["self-test"],
|
||||
verificationEvidence: ["typecheck 通过。"],
|
||||
|
||||
@ -82,6 +82,7 @@ export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Re
|
||||
const problemRecords = normalizeList(input.problemRecords);
|
||||
const reviewEvidence = normalizeList(input.reviewEvidence);
|
||||
const stableSolutions = normalizeList(input.stableSolutions);
|
||||
const superpowersReviewEvidence = normalizeList(input.superpowersReviewEvidence);
|
||||
const patterns = normalizePatterns(input.reusablePatterns);
|
||||
const upgradeNeeded = patterns.some((pattern) => pattern !== 'none');
|
||||
const ktWorkflowUpdated = input.ktWorkflowUpdated === true;
|
||||
@ -90,7 +91,8 @@ export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Re
|
||||
verificationEvidence.length > 0 ? '' : '补充测试或 smoke 验证证据;只有计划不算闭环完成。',
|
||||
docSyncEvidence.length > 0 ? '' : '补充文档同步证据;无需更新时也要显式写明原因。',
|
||||
cleanupOk ? '' : '补充历史产物清理证据;至少包含 cleanup-history dry-run deleted=0 或结构化 cleanupFinalDeleted=0。',
|
||||
reviewEvidence.length > 0 ? '' : '补充全局 review 证据。',
|
||||
reviewEvidence.length > 0 ? '' : '补充 KT 全局 review 证据。',
|
||||
superpowersReviewEvidence.length > 0 ? '' : '补充 Superpowers code review 证据。',
|
||||
problemRecords.length > 0 ? '' : '补充问题记录;如无新卡点,显式写“无新卡点”。',
|
||||
stableSolutions.length > 0 ? '' : '补充稳定解法;如无新解法,显式写“无新增稳定解法”。',
|
||||
upgradeNeeded && !ktWorkflowUpdated
|
||||
@ -122,7 +124,8 @@ export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Re
|
||||
? undefined
|
||||
: '补充 cleanup-history dry-run deleted=0 证据或传入 cleanupFinalDeleted=0。',
|
||||
},
|
||||
buildPhase('global review', reviewEvidence, '补充 kt_global_code_review 或 global-review 证据。'),
|
||||
buildPhase('KT global review', reviewEvidence, '补充 kt_global_code_review 或 global-review 证据。'),
|
||||
buildPhase('Superpowers code review', superpowersReviewEvidence, '补充 Superpowers reviewer 创建/完成/关闭和处理结论证据。'),
|
||||
buildPhase('problem record', problemRecords, '记录问题点;没有问题也要显式写明。'),
|
||||
buildPhase('stable solution', stableSolutions, '记录稳定解法;没有新增解法也要显式写明。'),
|
||||
{
|
||||
@ -142,6 +145,7 @@ export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Re
|
||||
'pnpm run typecheck',
|
||||
'pnpm run self-test',
|
||||
'pnpm run global-review',
|
||||
'pnpm run workstream-closeout -- --superpowers-review "Superpowers reviewer completed; no Critical/Important findings"',
|
||||
'git diff --check',
|
||||
],
|
||||
missingItems,
|
||||
|
||||
@ -44,6 +44,7 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco
|
||||
const problemRecords = normalizeList(input.problemRecords);
|
||||
const stableSolutions = normalizeList(input.stableSolutions);
|
||||
const reviewEvidence = normalizeList(input.reviewEvidence);
|
||||
const superpowersReviewEvidence = normalizeList(input.superpowersReviewEvidence);
|
||||
const cleanupEvidence = normalizeList(input.cleanupEvidence);
|
||||
const cleanupPreviewDeleted = normalizeCleanupDeletedCount(
|
||||
input.cleanupPreviewDeleted,
|
||||
@ -86,7 +87,8 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco
|
||||
'执行 kt_cleanup_history dryRun=false 或 pnpm run cleanup-history -- --execute,并复跑 dry-run 确认 deleted=0。',
|
||||
),
|
||||
buildRequiredGate('documentation sync evidence', docSyncEvidence, '运行 kt_change_doc_sync 并同步 README/API/AGENTS/docs/Obsidian/skills/ktWorkflow 中需要更新的入口;无需更新时写明原因。'),
|
||||
buildRequiredGate('global review evidence', reviewEvidence, '文件改动后跑 kt_global_code_review 或 pnpm run global-review。'),
|
||||
buildRequiredGate('KT global review evidence', reviewEvidence, '文件改动后跑 kt_global_code_review 或 pnpm run global-review。'),
|
||||
buildRequiredGate('Superpowers code review evidence', superpowersReviewEvidence, '非平凡改动后按 Superpowers requesting-code-review 创建 reviewer,并记录完成/处理/关闭证据。'),
|
||||
];
|
||||
|
||||
if (needsSolidification) {
|
||||
@ -123,7 +125,7 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco
|
||||
'pnpm --dir mcp/ktWorkflow run cleanup-history -- --dry-run',
|
||||
'pnpm --dir mcp/ktWorkflow run cleanup-history -- --execute',
|
||||
'pnpm --dir mcp/ktWorkflow run global-review',
|
||||
'pnpm --dir mcp/ktWorkflow run workstream-closeout -- --title "<目标>" --verification "<证据>" --problem "<问题记录>" --solution "<稳定解法>"',
|
||||
'pnpm --dir mcp/ktWorkflow run workstream-closeout -- --title "<目标>" --verification "<证据>" --review "<KT全局审查证据>" --superpowers-review "<Superpowers审查证据>" --problem "<问题记录>" --solution "<稳定解法>"',
|
||||
],
|
||||
gates,
|
||||
loops: {
|
||||
@ -164,6 +166,7 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco
|
||||
reusablePattern,
|
||||
reviewEvidenceCount: reviewEvidence.length,
|
||||
stableSolutionCount: stableSolutions.length,
|
||||
superpowersReviewEvidenceCount: superpowersReviewEvidence.length,
|
||||
testCaseCount: testCases.length,
|
||||
verificationEvidenceCount: verificationEvidence.length,
|
||||
},
|
||||
|
||||
@ -1,12 +1,31 @@
|
||||
import type { CommitPlanInput, ComponentWorkflowInput, DbSyncPlanInput, ExecResult, FinishTaskInput, PushPlanInput, RemoteHealthCheckInput, RepoType, ResolvedProject } from '../types.js';
|
||||
import { tryExecFile, tryPowerShell } from '../core/exec.js';
|
||||
import { detectRepoType, parseGitStatusFiles } from '../core/project.js';
|
||||
import { getGlobalReviewCommand, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js';
|
||||
import { cleanupHistoryArtifacts } from './cleanup.js';
|
||||
import { karpathyChangedFileChecklistItem, karpathyFinishNote, karpathyStopCondition } from './karpathy.js';
|
||||
import { buildGlobalCodeReview, defaultReviewProjects } from './review.js';
|
||||
import { buildVerificationPlan } from './verification.js';
|
||||
import { buildWorkstreamCloseout } from './closeout.js';
|
||||
import type {
|
||||
CommitPlanInput,
|
||||
ComponentWorkflowInput,
|
||||
DbSyncPlanInput,
|
||||
ExecResult,
|
||||
FinishTaskInput,
|
||||
PushPlanInput,
|
||||
RemoteHealthCheckInput,
|
||||
RepoType,
|
||||
ResolvedProject,
|
||||
} from "../types.js";
|
||||
import { tryExecFile, tryPowerShell } from "../core/exec.js";
|
||||
import { detectRepoType, parseGitStatusFiles } from "../core/project.js";
|
||||
import {
|
||||
getGlobalReviewCommand,
|
||||
resolveProject,
|
||||
toPosix,
|
||||
workspaceRoot,
|
||||
} from "../core/workspace.js";
|
||||
import { cleanupHistoryArtifacts } from "./cleanup.js";
|
||||
import {
|
||||
karpathyChangedFileChecklistItem,
|
||||
karpathyFinishNote,
|
||||
karpathyStopCondition,
|
||||
} from "./karpathy.js";
|
||||
import { buildGlobalCodeReview, defaultReviewProjects } from "./review.js";
|
||||
import { buildVerificationPlan } from "./verification.js";
|
||||
import { buildWorkstreamCloseout } from "./closeout.js";
|
||||
interface RepoSnapshot {
|
||||
branch: string;
|
||||
changedFiles: string[];
|
||||
@ -17,83 +36,100 @@ interface RepoSnapshot {
|
||||
}
|
||||
|
||||
function normalizeProjectKeys(projects?: string[]): string[] {
|
||||
const selected = (projects?.length ?? 0) > 0 ? projects! : [...defaultReviewProjects];
|
||||
return Array.from(new Set(selected.filter((project) => project !== 'root')));
|
||||
const selected =
|
||||
(projects?.length ?? 0) > 0 ? projects! : [...defaultReviewProjects];
|
||||
return Array.from(new Set(selected.filter((project) => project !== "root")));
|
||||
}
|
||||
|
||||
function formatShanghaiTimestamp(date = new Date()): string {
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
hour12: false,
|
||||
minute: '2-digit',
|
||||
month: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
minute: "2-digit",
|
||||
month: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "Asia/Shanghai",
|
||||
year: "numeric",
|
||||
}).formatToParts(date);
|
||||
const map = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
||||
return `${map.year}${map.month}${map.day}_${map.hour}${map.minute}${map.second}`;
|
||||
}
|
||||
|
||||
function normalizeDbIdentifier(value: string): string {
|
||||
return value.replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').slice(0, 64);
|
||||
return value
|
||||
.replace(/[^a-zA-Z0-9_]/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.slice(0, 64);
|
||||
}
|
||||
|
||||
async function collectRepoSnapshot(projectKey: string): Promise<RepoSnapshot> {
|
||||
const project = resolveProject(projectKey);
|
||||
const repoType = detectRepoType(project.path);
|
||||
const status = repoType === 'git'
|
||||
? await tryExecFile('git', ['status', '--short'], project.path)
|
||||
: null;
|
||||
const branch = repoType === 'git'
|
||||
? await tryExecFile('git', ['branch', '--show-current'], project.path)
|
||||
: null;
|
||||
const remote = repoType === 'git'
|
||||
? await tryExecFile('git', ['remote', '-v'], project.path)
|
||||
: null;
|
||||
const status =
|
||||
repoType === "git"
|
||||
? await tryExecFile("git", ["status", "--short"], project.path)
|
||||
: null;
|
||||
const branch =
|
||||
repoType === "git"
|
||||
? await tryExecFile("git", ["branch", "--show-current"], project.path)
|
||||
: null;
|
||||
const remote =
|
||||
repoType === "git"
|
||||
? await tryExecFile("git", ["remote", "-v"], project.path)
|
||||
: null;
|
||||
|
||||
return {
|
||||
branch: branch?.ok ? branch.stdout : '',
|
||||
branch: branch?.ok ? branch.stdout : "",
|
||||
changedFiles: status?.ok ? parseGitStatusFiles(status.stdout) : [],
|
||||
project,
|
||||
remote: remote?.ok ? remote.stdout : '',
|
||||
remote: remote?.ok ? remote.stdout : "",
|
||||
repoType,
|
||||
status: status?.ok ? status.stdout : status?.stderr || '',
|
||||
status: status?.ok ? status.stdout : status?.stderr || "",
|
||||
};
|
||||
}
|
||||
|
||||
function suggestCommitMessage(snapshot: RepoSnapshot): string {
|
||||
const files = snapshot.changedFiles.join('\n');
|
||||
const files = snapshot.changedFiles.join("\n");
|
||||
|
||||
if (snapshot.project.alias === 'mcp') {
|
||||
return 'feat(workflow): 扩展KT工作流能力';
|
||||
if (snapshot.project.alias === "mcp") {
|
||||
return "feat(workflow): 扩展KT工作流能力";
|
||||
}
|
||||
if (snapshot.project.alias === 'api' && /qqbot/i.test(files)) {
|
||||
return 'fix(qqbot): 修复QQBot运行链路';
|
||||
if (snapshot.project.alias === "api" && /qqbot/i.test(files)) {
|
||||
return "fix(qqbot): 修复QQBot运行链路";
|
||||
}
|
||||
if (snapshot.project.alias === 'admin' && /\.env|authentication/.test(files)) {
|
||||
return 'fix(admin): 收敛环境配置和登录页';
|
||||
if (
|
||||
snapshot.project.alias === "admin" &&
|
||||
/\.env|authentication/.test(files)
|
||||
) {
|
||||
return "fix(admin): 收敛环境配置和登录页";
|
||||
}
|
||||
if (snapshot.project.alias === 'web' && /deploy\.json|\.gitignore/.test(files)) {
|
||||
return 'chore(web): 清理旧部署配置';
|
||||
if (
|
||||
snapshot.project.alias === "web" &&
|
||||
/deploy\.json|\.gitignore/.test(files)
|
||||
) {
|
||||
return "chore(web): 清理旧部署配置";
|
||||
}
|
||||
if (snapshot.project.alias === 'blog') {
|
||||
return 'fix(blog): 优化博客前台体验';
|
||||
if (snapshot.project.alias === "blog") {
|
||||
return "fix(blog): 优化博客前台体验";
|
||||
}
|
||||
if (snapshot.project.alias === 'knife4j') {
|
||||
return 'feat(knife4j): 升级接口文档插件能力';
|
||||
if (snapshot.project.alias === "knife4j") {
|
||||
return "feat(knife4j): 升级接口文档插件能力";
|
||||
}
|
||||
return `chore(${snapshot.project.alias || 'repo'}): 更新项目改动`;
|
||||
return `chore(${snapshot.project.alias || "repo"}): 更新项目改动`;
|
||||
}
|
||||
|
||||
export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Record<string, unknown>> {
|
||||
export async function buildFinishTask(
|
||||
input: FinishTaskInput = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const projectKeys = normalizeProjectKeys(input.projects);
|
||||
const snapshots = await Promise.all(projectKeys.map(collectRepoSnapshot));
|
||||
const changedSnapshots = snapshots.filter((snapshot) => snapshot.changedFiles.length > 0);
|
||||
const changedSnapshots = snapshots.filter(
|
||||
(snapshot) => snapshot.changedFiles.length > 0,
|
||||
);
|
||||
const verificationPlans = changedSnapshots.map((snapshot) =>
|
||||
buildVerificationPlan({
|
||||
changeType: input.changeType || 'general',
|
||||
changeType: input.changeType || "general",
|
||||
project: snapshot.project.alias || snapshot.project.relativePath,
|
||||
}),
|
||||
);
|
||||
@ -105,7 +141,9 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
|
||||
|
||||
if (input.runValidation) {
|
||||
for (const plan of verificationPlans) {
|
||||
for (const command of plan.commands.filter((item) => !item.includes('global-review'))) {
|
||||
for (const command of plan.commands.filter(
|
||||
(item) => !item.includes("global-review"),
|
||||
)) {
|
||||
validationRuns.push({
|
||||
command,
|
||||
project: plan.project,
|
||||
@ -116,7 +154,7 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
|
||||
}
|
||||
|
||||
const review = await buildGlobalCodeReview({
|
||||
contentScanMode: input.contentScanMode ?? 'changed',
|
||||
contentScanMode: input.contentScanMode ?? "changed",
|
||||
includeContentScan: true,
|
||||
includeRootScan: input.includeRootScan ?? true,
|
||||
maxFindingsPerProject: input.maxFindingsPerProject ?? 20,
|
||||
@ -132,34 +170,38 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
|
||||
return {
|
||||
cleanup,
|
||||
closeout: buildWorkstreamCloseout({
|
||||
changedProjects: changedSnapshots.map((snapshot) => snapshot.project.relativePath),
|
||||
changedProjects: changedSnapshots.map(
|
||||
(snapshot) => snapshot.project.relativePath,
|
||||
),
|
||||
problemRecords: [],
|
||||
reusablePatterns: ['none'],
|
||||
reusablePatterns: ["none"],
|
||||
stableSolutions: [],
|
||||
title: 'kt_finish_task 收尾审计',
|
||||
title: "kt_finish_task 收尾审计",
|
||||
verificationEvidence: validationRuns
|
||||
.filter((item) => item.result.ok)
|
||||
.map((item) => `${item.project.relativePath}: ${item.command}`),
|
||||
}),
|
||||
mode: input.runValidation ? 'validation_executed' : 'plan_only',
|
||||
mode: input.runValidation ? "validation_executed" : "plan_only",
|
||||
notes: [
|
||||
'默认只生成收尾结果;runValidation=true 时才执行验证命令。',
|
||||
'cleanupHistory=true 时才真实清理历史产物,否则仅预览;预览中 deleted>0 说明测试/验证历史还未满足最近 3 轮规则。',
|
||||
'提交和推送仍需用户明确要求后执行。',
|
||||
"默认只生成收尾结果;runValidation=true 时才执行验证命令。",
|
||||
"cleanupHistory=true 时才真实清理历史产物,否则仅预览;预览中 deleted>0 说明测试/验证历史还未满足最近 3 轮规则。",
|
||||
"提交和推送仍需用户明确要求后执行。",
|
||||
karpathyFinishNote,
|
||||
],
|
||||
review,
|
||||
snapshots,
|
||||
stopConditions: [
|
||||
'review.findings 存在 P1/P2 时先判定真实风险或工具误报;真实风险修代码,工具误报修 ktWorkflow,再复跑 kt_global_code_review。',
|
||||
'validationRuns 中任一 result.ok=false 时先处理失败命令。',
|
||||
'cleanup.dryRun=true 且 cleanup.deleted 非空时,先执行 kt_cleanup_history dryRun=false 或 pnpm run cleanup-history -- --execute,再报告测试/验证历史已合规。',
|
||||
'发现真实 env、生产 Secret 或非本次范围文件时停止提交。',
|
||||
"review.findings 存在 P1/P2 时先判定真实风险或工具误报;真实风险修代码,工具误报修 ktWorkflow,再复跑 kt_global_code_review。",
|
||||
"validationRuns 中任一 result.ok=false 时先处理失败命令。",
|
||||
"cleanup.dryRun=true 且 cleanup.deleted 非空时,先执行 kt_cleanup_history dryRun=false 或 pnpm run cleanup-history -- --execute,再报告测试/验证历史已合规。",
|
||||
"发现真实 env、生产 Secret 或非本次范围文件时停止提交。",
|
||||
karpathyStopCondition,
|
||||
],
|
||||
summary: {
|
||||
changedProjectCount: changedSnapshots.length,
|
||||
changedProjects: changedSnapshots.map((snapshot) => snapshot.project.relativePath),
|
||||
changedProjects: changedSnapshots.map(
|
||||
(snapshot) => snapshot.project.relativePath,
|
||||
),
|
||||
cleanupPendingCount,
|
||||
validationRunCount: validationRuns.length,
|
||||
},
|
||||
@ -168,18 +210,24 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildCommitPlan(input: CommitPlanInput = {}): Promise<Record<string, unknown>> {
|
||||
const snapshots = await Promise.all(normalizeProjectKeys(input.projects).map(collectRepoSnapshot));
|
||||
const changedSnapshots = snapshots.filter((snapshot) => snapshot.changedFiles.length > 0);
|
||||
export async function buildCommitPlan(
|
||||
input: CommitPlanInput = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const snapshots = await Promise.all(
|
||||
normalizeProjectKeys(input.projects).map(collectRepoSnapshot),
|
||||
);
|
||||
const changedSnapshots = snapshots.filter(
|
||||
(snapshot) => snapshot.changedFiles.length > 0,
|
||||
);
|
||||
|
||||
return {
|
||||
checks: [
|
||||
'每个独立 Git 仓库单独提交。',
|
||||
"每个独立 Git 仓库单独提交。",
|
||||
karpathyChangedFileChecklistItem,
|
||||
'提交前先跑对应轻量验证和 kt_global_code_review。',
|
||||
'kt_global_code_review findings 必须为 0;确认误报时先修 ktWorkflow 规则,不把误报当作长期上下文。',
|
||||
'后端真实 .env.development/.env.production 不提交;前端 env 可提交但只能包含客户端公开配置。',
|
||||
'KT 根目录是本地治理仓库;如没有 remote,只本地提交并在最终回复说明。',
|
||||
"提交前先跑对应轻量验证和 kt_global_code_review。",
|
||||
"kt_global_code_review findings 必须为 0;确认误报时先修 ktWorkflow 规则,不把误报当作长期上下文。",
|
||||
"后端真实 .env.development/.env.production 不提交;前端 env 可提交但只能包含客户端公开配置。",
|
||||
"KT 根目录是本地治理仓库;如没有 remote,只本地提交并在最终回复说明。",
|
||||
],
|
||||
includePush: input.includePush ?? false,
|
||||
projects: changedSnapshots.map((snapshot) => {
|
||||
@ -188,10 +236,12 @@ export async function buildCommitPlan(input: CommitPlanInput = {}): Promise<Reco
|
||||
branch: snapshot.branch,
|
||||
changedFiles: snapshot.changedFiles,
|
||||
commands: [
|
||||
'git status --short',
|
||||
'git add -A',
|
||||
"git status --short",
|
||||
"git add -A",
|
||||
`git commit -m "${message}"`,
|
||||
...(input.includePush ? [`git push origin ${snapshot.branch || 'main'}`] : []),
|
||||
...(input.includePush
|
||||
? [`git push origin ${snapshot.branch || "main"}`]
|
||||
: []),
|
||||
],
|
||||
message,
|
||||
project: snapshot.project,
|
||||
@ -209,24 +259,28 @@ export async function buildCommitPlan(input: CommitPlanInput = {}): Promise<Reco
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildPushPlan(input: PushPlanInput = {}): Promise<Record<string, unknown>> {
|
||||
const snapshots = await Promise.all(normalizeProjectKeys(input.projects).map(collectRepoSnapshot));
|
||||
const remote = input.remote || 'origin';
|
||||
export async function buildPushPlan(
|
||||
input: PushPlanInput = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const snapshots = await Promise.all(
|
||||
normalizeProjectKeys(input.projects).map(collectRepoSnapshot),
|
||||
);
|
||||
const remote = input.remote || "origin";
|
||||
|
||||
return {
|
||||
checks: [
|
||||
'推送前工作区应干净;如仍有改动,先提交或说明为什么暂不提交。',
|
||||
'推送前复跑 kt_global_code_review,findings 必须为 0;如出现误报先升级 ktWorkflow 规则。',
|
||||
'如果 Jenkins/Gitea host key、mirror、hooks 异常,先修远程仓库状态再重试。',
|
||||
"推送前工作区应干净;如仍有改动,先提交或说明为什么暂不提交。",
|
||||
"推送前复跑 kt_global_code_review,findings 必须为 0;如出现误报先升级 ktWorkflow 规则。",
|
||||
"如果 Jenkins/Gitea host key、mirror、hooks 异常,先修远程仓库状态再重试。",
|
||||
],
|
||||
projects: snapshots
|
||||
.filter((snapshot) => snapshot.repoType === 'git')
|
||||
.filter((snapshot) => snapshot.repoType === "git")
|
||||
.map((snapshot) => {
|
||||
const branch = input.branch || snapshot.branch || 'main';
|
||||
const branch = input.branch || snapshot.branch || "main";
|
||||
return {
|
||||
branch,
|
||||
commands: [
|
||||
'git status --short',
|
||||
"git status --short",
|
||||
`git log --oneline ${remote}/${branch}..${branch}`,
|
||||
`git push ${remote} ${branch}`,
|
||||
],
|
||||
@ -243,21 +297,43 @@ function buildRemoteHealthCommands(input: RemoteHealthCheckInput = {}): Array<{
|
||||
command: string;
|
||||
service: string;
|
||||
}> {
|
||||
const host = input.sshTarget || input.host || process.env.KT_NAS_SSH_TARGET || 'nas.kwitsukasa.top';
|
||||
const host =
|
||||
input.sshTarget ||
|
||||
input.host ||
|
||||
process.env.KT_NAS_SSH_TARGET ||
|
||||
"nas.kwitsukasa.top";
|
||||
const port = input.port ?? Number(process.env.KT_NAS_SSH_PORT || 2202);
|
||||
const ssh = (remoteCommand: string) => `ssh -p ${port} ${host} "${remoteCommand}"`;
|
||||
const ssh = (remoteCommand: string) =>
|
||||
`ssh -p ${port} ${host} "${remoteCommand}"`;
|
||||
const allCommands: Record<string, string> = {
|
||||
docker: ssh("docker ps --format '{{.Names}} {{.Status}} {{.Ports}}'"),
|
||||
gitea: ssh("docker ps --format '{{.Names}} {{.Status}}' | grep -i gitea || true"),
|
||||
jenkins: ssh("docker ps --format '{{.Names}} {{.Status}}' | grep -i jenkins || true"),
|
||||
k3d: ssh('kubectl get pods -A -o wide 2>/dev/null || k3d cluster list 2>/dev/null || true'),
|
||||
minio: ssh("docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -i minio || true"),
|
||||
mosquitto: ssh("docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -i mosquitto || true"),
|
||||
mysql: ssh("docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -Ei 'mysql|mariadb' || true"),
|
||||
napcat: ssh("docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -Ei 'napcat|qqbot' || true"),
|
||||
ssh: ssh('hostname && date'),
|
||||
gitea: ssh(
|
||||
"docker ps --format '{{.Names}} {{.Status}}' | grep -i gitea || true",
|
||||
),
|
||||
jenkins: ssh(
|
||||
"docker ps --format '{{.Names}} {{.Status}}' | grep -i jenkins || true",
|
||||
),
|
||||
k3d: ssh(
|
||||
"kubectl get pods -A -o wide 2>/dev/null || k3d cluster list 2>/dev/null || true",
|
||||
),
|
||||
minio: ssh(
|
||||
"docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -i minio || true",
|
||||
),
|
||||
mosquitto: ssh(
|
||||
"docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -i mosquitto || true",
|
||||
),
|
||||
mysql: ssh(
|
||||
"docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -Ei 'mysql|mariadb' || true",
|
||||
),
|
||||
napcat: ssh(
|
||||
"docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep -Ei 'napcat|qqbot' || true",
|
||||
),
|
||||
ssh: ssh("hostname && date"),
|
||||
};
|
||||
const services = (input.services?.length ?? 0) > 0 ? input.services! : Object.keys(allCommands);
|
||||
const services =
|
||||
(input.services?.length ?? 0) > 0
|
||||
? input.services!
|
||||
: Object.keys(allCommands);
|
||||
|
||||
return services
|
||||
.filter((service) => allCommands[service])
|
||||
@ -267,7 +343,9 @@ function buildRemoteHealthCommands(input: RemoteHealthCheckInput = {}): Array<{
|
||||
}));
|
||||
}
|
||||
|
||||
export async function buildRemoteHealthCheck(input: RemoteHealthCheckInput = {}): Promise<Record<string, unknown>> {
|
||||
export async function buildRemoteHealthCheck(
|
||||
input: RemoteHealthCheckInput = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const commands = buildRemoteHealthCommands(input);
|
||||
const results = input.execute
|
||||
? await Promise.all(
|
||||
@ -282,17 +360,19 @@ export async function buildRemoteHealthCheck(input: RemoteHealthCheckInput = {})
|
||||
commands,
|
||||
execute: input.execute ?? false,
|
||||
notes: [
|
||||
'默认 execute=false,只生成只读探测命令。',
|
||||
'远程命令只做 hostname/date/docker ps/kubectl get 等只读检查。',
|
||||
'真实密码、token、Secret 不从仓库读取,也不写入输出。',
|
||||
"默认 execute=false,只生成只读探测命令。",
|
||||
"远程命令只做 hostname/date/docker ps/kubectl get 等只读检查。",
|
||||
"真实密码、token、Secret 不从仓库读取,也不写入输出。",
|
||||
],
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDbSyncPlan(input: DbSyncPlanInput = {}): Record<string, unknown> {
|
||||
const source = input.source || 'local';
|
||||
const target = input.target || 'remote';
|
||||
export function buildDbSyncPlan(
|
||||
input: DbSyncPlanInput = {},
|
||||
): Record<string, unknown> {
|
||||
const source = input.source || "local";
|
||||
const target = input.target || "remote";
|
||||
const backupName = normalizeDbIdentifier(
|
||||
input.backupName ||
|
||||
`kt_template_backup_${formatShanghaiTimestamp()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
@ -302,90 +382,281 @@ export function buildDbSyncPlan(input: DbSyncPlanInput = {}): Record<string, unk
|
||||
return {
|
||||
backupName,
|
||||
commands: [
|
||||
'从 Node/kt-template-online-api/.env.development 和 .env.production 读取连接信息,只在本机 shell 环境使用,不写入 Git。',
|
||||
'KT 当前约定:127.0.0.1:3306 是本地 MySQL,127.0.0.1:3307 是远程 NAS 打洞连接;3307 不通时先建立 ssh -N -L 127.0.0.1:3307:127.0.0.1:3306 nas。',
|
||||
"从 Node/kt-template-online-api/.env.development 和 .env.production 读取连接信息,只在本机 shell 环境使用,不写入 Git。",
|
||||
"KT 当前约定:127.0.0.1:3306 是本地 MySQL,127.0.0.1:3307 是远程 NAS 打洞连接;3307 不通时先建立 ssh -N -L 127.0.0.1:3307:127.0.0.1:3306 nas。",
|
||||
`mkdir -p .kt-workspace/db-sync/${backupName}`,
|
||||
`mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers <source-conn> kt_template > ${dumpPath}`,
|
||||
`mysql <target-conn> -e "CREATE DATABASE ${backupName} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"`,
|
||||
`mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers <target-conn> kt_template | mysql <target-conn> ${backupName}`,
|
||||
`mysql <target-conn> kt_template < ${dumpPath}`,
|
||||
'对比源库/目标库表数量、视图/函数/过程数量和关键表行数。',
|
||||
"对比源库/目标库表数量、视图/函数/过程数量和关键表行数。",
|
||||
`验证通过后再执行:mysql <target-conn> -e "DROP DATABASE ${backupName};"`,
|
||||
],
|
||||
guardrails: [
|
||||
'默认只生成计划,不执行覆盖、drop、truncate。',
|
||||
'同步前必须确认 source/target 方向,不能靠变量名猜。',
|
||||
'3306/3307 端口方向先用只读 SELECT 预检确认,避免把远程库误当成本地库。',
|
||||
'目标库备份校验通过前不得删除备份库。',
|
||||
'涉及生产库时优先只读验证,批量删除必须二次确认。',
|
||||
"默认只生成计划,不执行覆盖、drop、truncate。",
|
||||
"同步前必须确认 source/target 方向,不能靠变量名猜。",
|
||||
"3306/3307 端口方向先用只读 SELECT 预检确认,避免把远程库误当成本地库。",
|
||||
"目标库备份校验通过前不得删除备份库。",
|
||||
"涉及生产库时优先只读验证,批量删除必须二次确认。",
|
||||
],
|
||||
source,
|
||||
target,
|
||||
verification: [
|
||||
'SHOW TABLES 数量一致。',
|
||||
'admin_user、admin_menu、qqbot_account、qqbot_message_log 等关键表行数符合预期。',
|
||||
'应用登录和核心接口 smoke 通过。',
|
||||
'全局 review findings=0。',
|
||||
"SHOW TABLES 数量一致。",
|
||||
"admin_user、admin_menu、qqbot_account、qqbot_message_log 等关键表行数符合预期。",
|
||||
"应用登录和核心接口 smoke 通过。",
|
||||
"全局 review findings=0。",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildComponentWorkflow(input: ComponentWorkflowInput = {}): Record<string, unknown> {
|
||||
const target = input.target || 'KtTable';
|
||||
export function buildComponentWorkflow(
|
||||
input: ComponentWorkflowInput = {},
|
||||
): Record<string, unknown> {
|
||||
const target = input.target || "KtTable";
|
||||
const workflows = {
|
||||
AdminAuth: {
|
||||
antiPatterns: ['无 token 时主动 refresh', '跨端回跳时清空 Admin 已有授权态', '把 redirect 写丢或重复编码'],
|
||||
files: ['Vue/kt-template-admin/apps/web-antdv-next/src/store/auth.ts', 'Vue/kt-template-online-web/src/api/auth.ts', 'Vue/kt-template-online-playground/src/api/auth.ts'],
|
||||
guardrails: ['先画清楚 Admin/API/Web/Playground token 链路', '401 时先判断旧 token 是否存在', 'Admin 已登录时直接带回授权态'],
|
||||
verification: ['清空 token 测无循环', 'Admin 已登录测直接回跳', '刷新页面测持久化'],
|
||||
antiPatterns: [
|
||||
"无 token 时主动 refresh",
|
||||
"跨端回跳时清空 Admin 已有授权态",
|
||||
"把 redirect 写丢或重复编码",
|
||||
],
|
||||
files: [
|
||||
"Vue/kt-template-admin/apps/web-antdv-next/src/store/auth.ts",
|
||||
"Vue/kt-template-online-web/src/api/auth.ts",
|
||||
"Vue/kt-template-online-playground/src/api/auth.ts",
|
||||
],
|
||||
guardrails: [
|
||||
"先画清楚 Admin/API/Web/Playground token 链路",
|
||||
"401 时先判断旧 token 是否存在",
|
||||
"Admin 已登录时直接带回授权态",
|
||||
],
|
||||
verification: [
|
||||
"清空 token 测无循环",
|
||||
"Admin 已登录测直接回跳",
|
||||
"刷新页面测持久化",
|
||||
],
|
||||
},
|
||||
BlogArgon: {
|
||||
antiPatterns: ['使用具名 id 控制 DOM', 'SCSS 类名逃出 kt-blog 命名空间', '主题变量散落在 scss 文件', '亮色模式仍套暗色遮罩'],
|
||||
files: ['Vue/kt-blog-web/src/hooks/useBlogTheme.ts', 'Vue/kt-blog-web/src/styles', 'Vue/kt-blog-web/src/components/blog'],
|
||||
guardrails: ['样式统一 kt-blog BEM 命名空间', '主题变量收敛到 useBlogTheme', '动画用 Vue Transition', '和线上 Argon DOM/视觉逐项对照'],
|
||||
verification: ['亮/暗模式切换', '搜索 Enter 和动画', '文章切换动画', '无感知滚动加载', '背景图滚动到底部不断层'],
|
||||
antiPatterns: [
|
||||
"使用具名 id 控制 DOM",
|
||||
"SCSS 类名逃出 kt-blog 命名空间",
|
||||
"主题变量散落在 scss 文件",
|
||||
"亮色模式仍套暗色遮罩",
|
||||
],
|
||||
files: [
|
||||
"Vue/kt-blog-web/src/hooks/useBlogTheme.ts",
|
||||
"Vue/kt-blog-web/src/styles",
|
||||
"Vue/kt-blog-web/src/components/blog",
|
||||
],
|
||||
guardrails: [
|
||||
"样式统一 kt-blog BEM 命名空间",
|
||||
"主题变量收敛到 useBlogTheme",
|
||||
"动画用 Vue Transition",
|
||||
"和线上 Argon DOM/视觉逐项对照",
|
||||
],
|
||||
verification: [
|
||||
"亮/暗模式切换",
|
||||
"搜索 Enter 和动画",
|
||||
"文章切换动画",
|
||||
"无感知滚动加载",
|
||||
"背景图滚动到底部不断层",
|
||||
],
|
||||
},
|
||||
Knife4jSwagger: {
|
||||
antiPatterns: ['视觉只大致相似', 'TSX 里用 function render helper', 'TabPane 默认插槽不显式传 default', '把请求入参编辑拆成无法复用的零散状态'],
|
||||
files: ['Plugins/knife4j-swagger-vue3/packages/knife4j-vue3-ui', 'Plugins/knife4j-swagger-vue3/packages/openapi-parser', 'Plugins/knife4j-swagger-vue3/apps/playground'],
|
||||
guardrails: ['以 Knife4j 原始 UI 为 1:1 视觉源', '全部 Vue 视图使用 TSX', '嵌套组件 slot 显式 default', '路由使用 hash path 参数,不用 query 兼容旧逻辑', '调试台按 OpenAPI 标准合并 path/query/header/body/auth/global 参数'],
|
||||
verification: ['pnpm run typecheck', '真实后端 48085 文档页面', '控制台 warning/error 检查', 'Body/Headers/Auth/全局参数发送链路', '小宽度 @media 与左侧调试栏固定'],
|
||||
antiPatterns: [
|
||||
"视觉只大致相似",
|
||||
"TSX 里用 function render helper",
|
||||
"TabPane 默认插槽不显式传 default",
|
||||
"把请求入参编辑拆成无法复用的零散状态",
|
||||
],
|
||||
files: [
|
||||
"Plugins/knife4j-swagger-vue3/packages/knife4j-vue3-ui",
|
||||
"Plugins/knife4j-swagger-vue3/packages/openapi-parser",
|
||||
"Plugins/knife4j-swagger-vue3/apps/playground",
|
||||
],
|
||||
guardrails: [
|
||||
"以 Knife4j 原始 UI 为 1:1 视觉源",
|
||||
"全部 Vue 视图使用 TSX",
|
||||
"嵌套组件 slot 显式 default",
|
||||
"路由使用 hash path 参数,不用 query 兼容旧逻辑",
|
||||
"调试台按 OpenAPI 标准合并 path/query/header/body/auth/global 参数",
|
||||
],
|
||||
verification: [
|
||||
"pnpm run typecheck",
|
||||
"真实后端 48085 文档页面",
|
||||
"控制台 warning/error 检查",
|
||||
"Body/Headers/Auth/全局参数发送链路",
|
||||
"小宽度 @media 与左侧调试栏固定",
|
||||
],
|
||||
},
|
||||
KtTable: {
|
||||
antiPatterns: ['自定义纵向滚动条覆盖 Antdv 原生滚动', 'scroll/resize 高频写响应式状态', '表格高度参与展开收起动画', '固定列透明导致透视'],
|
||||
files: ['Vue/kt-template-admin/apps/web-antdv-next/src/components/ktTable'],
|
||||
guardrails: ['优先使用 antdv-next 原生 table 能力', '统计列固定到底部但不参与表单动画', '序号列和操作列固定最小/最大宽', 'resize 用 RAF 节流和非深度监听'],
|
||||
verification: ['横向滚动性能', '纵向滚动表头对齐', '列宽/行高 resize', 'summary 固定底部', '宽屏无无意义横向滚动条'],
|
||||
antiPatterns: [
|
||||
"自定义纵向滚动条覆盖 Antdv 原生滚动",
|
||||
"scroll/resize 高频写响应式状态",
|
||||
"表格高度参与展开收起动画",
|
||||
"固定列透明导致透视",
|
||||
],
|
||||
files: [
|
||||
"Vue/kt-template-admin/apps/web-antdv-next/src/components/ktTable",
|
||||
],
|
||||
guardrails: [
|
||||
"优先使用 antdv-next 原生 table 能力",
|
||||
"统计列固定到底部但不参与表单动画",
|
||||
"序号列和操作列固定最小/最大宽",
|
||||
"resize 用 RAF 节流和非深度监听",
|
||||
],
|
||||
verification: [
|
||||
"横向滚动性能",
|
||||
"纵向滚动表头对齐",
|
||||
"列宽/行高 resize",
|
||||
"summary 固定底部",
|
||||
"宽屏无无意义横向滚动条",
|
||||
],
|
||||
},
|
||||
QQBot: {
|
||||
antiPatterns: ['把 NapCat token 写进仓库', '删除账号不清理专属容器', '白名单/黑名单只做前端过滤', '远程服务不可达时伪装成业务成功', '更新登录时用旧 isLogin 状态短路'],
|
||||
files: ['Node/kt-template-online-api/src/qqbot', 'Vue/kt-template-admin/apps/web-antdv-next/src/views/qqbot', 'ci/fnos-k8s/deploy-qqbot-runtime.ps1'],
|
||||
guardrails: ['OneBot 反向 WS token 校验', '权限名单底层先走 QQ号过滤', '群/频道精确 QQ 过滤由开关控制', '扫码取消/过期/失败清理未绑定容器', '更新登录走 SSE 进度,后台重置登录态后再生成二维码'],
|
||||
verification: ['账号在线', '扫码新增/更新登录', 'scan/events 每步进度', '自动回复规则命中', '权限名单三类过滤', '消息日志和发送日志闭环'],
|
||||
antiPatterns: [
|
||||
"把 NapCat token 写进仓库",
|
||||
"删除账号不清理专属容器",
|
||||
"白名单/黑名单只做前端过滤",
|
||||
"远程服务不可达时伪装成业务成功",
|
||||
"更新登录时用旧 isLogin 状态短路",
|
||||
],
|
||||
files: [
|
||||
"Node/kt-template-online-api/src/qqbot",
|
||||
"Vue/kt-template-admin/apps/web-antdv-next/src/views/qqbot",
|
||||
"ci/fnos-k8s/deploy-qqbot-runtime.ps1",
|
||||
],
|
||||
guardrails: [
|
||||
"OneBot 反向 WS token 校验",
|
||||
"权限名单底层先走 QQ号过滤",
|
||||
"群/频道精确 QQ 过滤由开关控制",
|
||||
"扫码取消/过期/失败清理未绑定容器",
|
||||
"更新登录走 SSE 进度,后台重置登录态后再生成二维码",
|
||||
],
|
||||
verification: [
|
||||
"账号在线",
|
||||
"扫码新增/更新登录",
|
||||
"scan/events 每步进度",
|
||||
"自动回复规则命中",
|
||||
"权限名单三类过滤",
|
||||
"消息日志和发送日志闭环",
|
||||
],
|
||||
},
|
||||
FF14Plugin: {
|
||||
antiPatterns: ['把 boss/服务器/职业映射硬编码到插件里', 'FFLogs 英文任务名直接回显给中文用户', '外部 API 失败时吞掉错误上下文', '字典缺失时返回空白结果'],
|
||||
files: ['Node/kt-template-online-api/src/qqbot/plugins/ff14Market', 'Node/kt-template-online-api/src/qqbot/plugins/fflogs', 'Node/kt-template-online-api/src/admin/dict', 'Vue/kt-template-admin/apps/web-antdv-next/src/views/system/dict'],
|
||||
guardrails: ['FF14 查价和 FFLogs 映射优先走字典表维护', '中文任务名查询和回显都要经过字典', '命令注册表和在线命令列表同步验证', '保留最近 logs 的颜色/评分/dps/adps/rdps/ndps/hps 字段语义'],
|
||||
verification: ['/logs 角色 服务器 高难任务中文名', '/logs 角色 服务器 最近10次', 'FF14 查价中文物品名', '字典管理页面可维护映射'],
|
||||
antiPatterns: [
|
||||
"把 boss/服务器/职业映射硬编码到插件里",
|
||||
"FFLogs 英文任务名直接回显给中文用户",
|
||||
"外部 API 失败时吞掉错误上下文",
|
||||
"字典缺失时返回空白结果",
|
||||
],
|
||||
files: [
|
||||
"Node/kt-template-online-api/src/qqbot/plugins/ff14Market",
|
||||
"Node/kt-template-online-api/src/qqbot/plugins/fflogs",
|
||||
"Node/kt-template-online-api/src/admin/dict",
|
||||
"Vue/kt-template-admin/apps/web-antdv-next/src/views/system/dict",
|
||||
],
|
||||
guardrails: [
|
||||
"FF14 查价和 FFLogs 映射优先走字典表维护",
|
||||
"中文任务名查询和回显都要经过字典",
|
||||
"命令注册表和在线命令列表同步验证",
|
||||
"保留最近 logs 的颜色/评分/dps/adps/rdps/ndps/hps 字段语义",
|
||||
],
|
||||
verification: [
|
||||
"/logs 角色 服务器 高难任务中文名",
|
||||
"/logs 角色 服务器 最近10次",
|
||||
"FF14 查价中文物品名",
|
||||
"字典管理页面可维护映射",
|
||||
],
|
||||
},
|
||||
FnosK8s: {
|
||||
antiPatterns: ['把 fnOS 原生面板改成 Docker 应用', '在 ui/config 显式配置公网端口', '提交 _build/_dist 打包产物', '只测桌面视口不测 fnOS 内嵌窗口和窄屏', '用非官方或旧缓存图标替代 Kubernetes 官方图标'],
|
||||
files: ['Plugins/fnos-k8s-dashboard-fpk/src/server.ts', 'Plugins/fnos-k8s-dashboard-fpk/src/ui', 'Plugins/fnos-k8s-dashboard-fpk/apps/kt-k8s-dashboard/fnos', 'Plugins/fnos-k8s-dashboard-fpk/scripts'],
|
||||
guardrails: ['保持非 Docker .fpk 形态', 'Web 面板源码只放 src/ui,由打包脚本复制进 app.tgz', 'fnOS 桌面入口使用 ui/config iframe 和 /kt-k8s-dashboard/ 代理路径', '应用标题与图标保持 Kubernetes 官方语义', '集群配置文件只作为运行时数据,不提交真实 kubeconfig'],
|
||||
verification: ['bun run typecheck', 'bun run build:fpk', 'bun run verify:fpk', '本地 /api/health 和首页请求', 'Playwright 桌面与 390px 窄屏截图', '线上应用中心/桌面入口只在用户允许远程测试时验证'],
|
||||
antiPatterns: [
|
||||
"把 fnOS 原生面板改成 Docker 应用",
|
||||
"在 ui/config 显式配置公网端口",
|
||||
"提交 _build/_dist 打包产物",
|
||||
"只测桌面视口不测 fnOS 内嵌窗口和窄屏",
|
||||
"用非官方或旧缓存图标替代 Kubernetes 官方图标",
|
||||
],
|
||||
files: [
|
||||
"Plugins/fnos-k8s-dashboard-fpk/src/server.ts",
|
||||
"Plugins/fnos-k8s-dashboard-fpk/src/ui",
|
||||
"Plugins/fnos-k8s-dashboard-fpk/apps/kt-k8s-dashboard/fnos",
|
||||
"Plugins/fnos-k8s-dashboard-fpk/scripts",
|
||||
],
|
||||
guardrails: [
|
||||
"保持非 Docker .fpk 形态",
|
||||
"Web 面板源码只放 src/ui,由打包脚本复制进 app.tgz",
|
||||
"fnOS 桌面入口使用 ui/config iframe 和 /kt-k8s-dashboard/ 代理路径",
|
||||
"应用标题与图标保持 Kubernetes 官方语义",
|
||||
"集群配置文件只作为运行时数据,不提交真实 kubeconfig",
|
||||
],
|
||||
verification: [
|
||||
"bun run typecheck",
|
||||
"bun run build:fpk",
|
||||
"bun run verify:fpk",
|
||||
"本地 /api/health 和首页请求",
|
||||
"Playwright 桌面与 390px 窄屏截图",
|
||||
"线上应用中心/桌面入口只在用户允许远程测试时验证",
|
||||
],
|
||||
},
|
||||
NapCatLogin: {
|
||||
antiPatterns: ['更新登录复用已过期二维码 URL', '前端等待长接口导致超时', '手动刷新每次重启容器', '后端拿旧 login state 判断成功'],
|
||||
files: ['Node/kt-template-online-api/src/qqbot/account', 'Node/kt-template-online-api/src/qqbot/napcat', 'Vue/kt-template-admin/apps/web-antdv-next/src/views/qqbot'],
|
||||
guardrails: ['创建账号和更新登录共用同一套二维码生成事实源', '更新登录先 reset runtime login state', 'SSE 返回 step/status/qrcode/error', '容器重启只在必要时触发,优先直接通信 NapCat API'],
|
||||
verification: ['账号被踢下线后点击更新登录', 'scan/events 能看到重置/生成二维码/等待扫码/成功或失败', '手动刷新不超时', '过期二维码不再复用'],
|
||||
antiPatterns: [
|
||||
"更新登录复用已过期二维码 URL",
|
||||
"前端等待长接口导致超时",
|
||||
"手动刷新每次重启容器",
|
||||
"后端拿旧 login state 判断成功",
|
||||
"把 ACCOUNT 当成登录凭据,忽略 QQ 历史登录态、账号密码或扫码兜底",
|
||||
"在请求日志、Loki 或列表接口里暴露 QQ 登录密码",
|
||||
],
|
||||
files: [
|
||||
"Node/kt-template-online-api/src/qqbot/account",
|
||||
"Node/kt-template-online-api/src/qqbot/napcat",
|
||||
"Vue/kt-template-admin/apps/web-antdv-next/src/views/qqbot",
|
||||
],
|
||||
guardrails: [
|
||||
"创建账号和更新登录共用同一套二维码生成事实源",
|
||||
"更新登录顺序固定为 ACCOUNT/-q 快速登录 -> 账号保存的密码登录 -> reset runtime login state 并生成二维码",
|
||||
"看门狗自动登录只跑 quick -> password,扫码必须由管理员手动触发",
|
||||
"账号登录密码前端只传 RSA-OAEP 加密字段 encryptedLoginPassword,后端用 QQBOT_ACCOUNT_SECRET_KEY 二次加密落库,列表不回显",
|
||||
"ACCOUNT 只指定目标账号;免扫码优先依赖数据卷历史登录态,密码登录失败或触发强制扫码时再进二维码",
|
||||
"SSE 返回 step/status/qrcode/error",
|
||||
"容器重启只在必要时触发,优先直接通信 NapCat API",
|
||||
],
|
||||
verification: [
|
||||
"账号被踢下线后点击更新登录",
|
||||
"docker inspect 确认已绑定容器存在 ACCOUNT=<selfId>",
|
||||
"docker restart 后 WebUI CheckLoginStatus/GetQQLoginInfo 验证 isLogin/online/uin;失败时检查 NapCat 日志是否为无历史登录记录、密码登录失败或需扫码验证",
|
||||
"scan/events 能看到 quick-login-start/quick-login-fallback/password-login-start/password-login-fallback/重置/生成二维码/等待扫码/成功或失败",
|
||||
"目标 Jest 覆盖 quick 成功、password 成功、password 失败进二维码、密文保存、容器 env 注入",
|
||||
"手动刷新不超时",
|
||||
"过期二维码不再复用",
|
||||
],
|
||||
},
|
||||
SystemLog: {
|
||||
antiPatterns: ['把真实 Loki 凭据写入仓库', '只改 Dockerfile 不校验线上 env/Secret', '日志时间格式前后端不一致', 'global-review 把历史脱敏占位符或中文显示标签当作真实凭据输出'],
|
||||
files: ['Node/kt-template-online-api/src/common/logger', 'Node/kt-template-online-api/src/admin/system-log', 'Vue/kt-template-admin/apps/web-antdv-next/src/views/system/log', 'mcp/ktWorkflow/src/tools/review.ts'],
|
||||
guardrails: ['pino + Loki env 走 Jenkins 私有 env 或 K8s Secret', '后端时间字段经 DTO/Entity 装饰器统一 YYYY-MM-DD HH:mm:ss', '系统日志页面走 /system/logs 系列接口', 'review 默认只对变更文件做 P2/P3 内容扫描,确认误报要沉淀到 review.ts'],
|
||||
verification: ['API typecheck/lint/test', 'Loki /ready 和 query_range', 'Admin #/system/logs 无 console error', 'global-review findings 不含已知脱敏占位符或中文显示标签误报'],
|
||||
antiPatterns: [
|
||||
"把真实 Loki 凭据写入仓库",
|
||||
"只改 Dockerfile 不校验线上 env/Secret",
|
||||
"日志时间格式前后端不一致",
|
||||
"global-review 把历史脱敏占位符或中文显示标签当作真实凭据输出",
|
||||
],
|
||||
files: [
|
||||
"Node/kt-template-online-api/src/common/logger",
|
||||
"Node/kt-template-online-api/src/admin/system-log",
|
||||
"Vue/kt-template-admin/apps/web-antdv-next/src/views/system/log",
|
||||
"mcp/ktWorkflow/src/tools/review.ts",
|
||||
],
|
||||
guardrails: [
|
||||
"pino + Loki env 走 Jenkins 私有 env 或 K8s Secret",
|
||||
"后端时间字段经 DTO/Entity 装饰器统一 YYYY-MM-DD HH:mm:ss",
|
||||
"系统日志页面走 /system/logs 系列接口",
|
||||
"review 默认只对变更文件做 P2/P3 内容扫描,确认误报要沉淀到 review.ts",
|
||||
],
|
||||
verification: [
|
||||
"API typecheck/lint/test",
|
||||
"Loki /ready 和 query_range",
|
||||
"Admin #/system/logs 无 console error",
|
||||
"global-review findings 不含已知脱敏占位符或中文显示标签误报",
|
||||
],
|
||||
},
|
||||
} satisfies Record<string, Record<string, unknown>>;
|
||||
|
||||
|
||||
@ -261,6 +261,7 @@ export interface WorkstreamCloseoutInput {
|
||||
reviewEvidence?: string[];
|
||||
reusablePatterns?: WorkstreamReusablePattern[];
|
||||
stableSolutions?: string[];
|
||||
superpowersReviewEvidence?: string[];
|
||||
title?: string;
|
||||
upgradeNotes?: string;
|
||||
verificationEvidence?: string[];
|
||||
@ -300,6 +301,7 @@ export interface WorkflowLoopAuditInput {
|
||||
reviewEvidence?: string[];
|
||||
stableSolutions?: string[];
|
||||
stage?: 'failure' | 'finish' | 'start' | 'test';
|
||||
superpowersReviewEvidence?: string[];
|
||||
taskTitle?: string;
|
||||
testCases?: string[];
|
||||
verificationEvidence?: string[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user