fix: 固化 Superpowers 审查闭环

This commit is contained in:
sunlei 2026-06-12 19:53:59 +08:00
parent 7e79b38600
commit 1d899734b4
8 changed files with 492 additions and 166 deletions

View File

@ -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` 或补成脚本,再继续。

View File

@ -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']),

View File

@ -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([]),

View File

@ -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 SUCCESSK8s 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 通过。"],

View File

@ -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,

View File

@ -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,
},

View File

@ -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_reviewfindings 必须为 0如出现误报先升级 ktWorkflow 规则。',
'如果 Jenkins/Gitea host key、mirror、hooks 异常,先修远程仓库状态再重试。',
"推送前工作区应干净;如仍有改动,先提交或说明为什么暂不提交。",
"推送前复跑 kt_global_code_reviewfindings 必须为 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 是本地 MySQL127.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 是本地 MySQL127.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>>;

View File

@ -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[];