feat: 规范KT工作流上下文治理

This commit is contained in:
sunlei 2026-06-03 18:37:43 +08:00
parent e715cca304
commit d813dc69ab
15 changed files with 249 additions and 43 deletions

View File

@ -1,10 +1,10 @@
# KT Workflow MCP
`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用 TypeScript 封装工具入参、项目别名和返回结构,用来把根目录 `AGENTS.md`、`SKILLS.md`、`TASKS.md` 中的协作流程变成可调用工具。
`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用 TypeScript 封装工具入参、项目别名和返回结构,用来把根目录 `AGENTS.md` 的硬规则、`SKILLS.md` 的工作流索引、`TASKS.md` 的上下文记录和 `docs/` 的详细流程变成可调用工具。
## 能力边界
- 读取 KT 工作区上下文:项目清单、硬性规则、协作技能、最近任务记录
- 读取 KT 工作区紧凑上下文项目清单、硬性规则、skill registry、标准 skill 包索引、历史关键词、最近任务记录和详细文档入口;需要全量文档时显式开启
- 检查子项目环境Git/SVN、包管理器、Node 版本、env 文件、Git 状态。
- 生成防偏差工作包:把开工前检查、禁止项、风险扫描、验证计划汇总成一份可执行清单。
- 生成验证建议按后端、前端、样式、页面、部署、MCP 等变更类型给出轻量验证命令。
@ -15,11 +15,11 @@
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、Web/Playground 回跳。
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
- 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、备份和行数校验。
- 生成专项组件工作流KtTable、BlogArgon、AdminAuth、QQBot 的防踩坑清单和验证点。
- 生成专项组件工作流KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger 的防踩坑清单和验证点。
- 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。
- 清理历史产物:按目录最近修改时间只保留最近 3 轮测试/验证产物,模板目录永久保留。
- 检查 env 策略和变更风险:提醒真实配置、锁文件、核心表格组件、部署链路等高风险改动。
- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量和当前变更风险;任何文件改动后都要跑一遍。
- 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留。
- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。
- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物和当前变更风险;任何文件改动后都要跑一遍。
- 生成或写入 `TASKS.md` 改动记录:默认 `dryRun=true`,确认后再落盘。
- 生成提交前检查清单:校验 KT commit message 约定。
@ -62,7 +62,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| 工具 | 用途 |
| --- | --- |
| `kt_read_context` | 读取 KT 根目录上下文和最近任务记录 |
| `kt_read_context` | 读取 KT 根目录紧凑上下文和最近任务记录,支持显式读取完整文档 |
| `kt_inspect_project` | 检查子项目仓库、包管理器、Node、env 和 Git 状态 |
| `kt_inspect_all_projects` | 一次性检查所有 KT 项目,适合多仓库联动任务 |
| `kt_guardrails` | 按任务类型生成开工、改动、禁止项和验证约束 |
@ -70,7 +70,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `kt_suggest_verification` | 生成轻量验证命令和注意事项 |
| `kt_create_page_test_case` | 生成页面级可视化测试用例 |
| `kt_api_test_plan` | 生成接口真实调用测试计划 |
| `kt_cleanup_history` | 清理历史测试/验证产物,默认预览,执行时只保留最近 3 轮 |
| `kt_cleanup_history` | 清理 `.kt-workspace` 历史测试/验证产物,默认预览,执行时只保留最近 3 轮 |
| `kt_cleanup_process_plan` | 生成验证进程清理检查命令 |
| `kt_env_policy` | 检查 env 文件现状和提交策略 |
| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 |
@ -101,6 +101,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `api` | `Node/kt-template-online-api` |
| `admin` | `Vue/kt-template-admin` |
| `blog` | `Vue/kt-blog-web` |
| `knife4j` | `Plugins/knife4j-swagger-vue3` |
| `web` | `Vue/kt-template-online-web` |
| `playground` | `Vue/kt-template-online-playground` |
@ -110,12 +111,13 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
- 工具入参类型集中在 `src/types.ts`MCP 注册集中在 `src/registerTools.ts`
- `src/core/*` 放项目别名、工作区路径、命令执行、仓库/包管理器识别等基础能力。
- `src/tools/*` 按功能拆分:检查、验证、测试、清理、风险、复审、任务记录和工作流计划。
- 根目录上下文分层遵守 `docs/kt-context-semantics.md``AGENTS.md` 是硬规则,`SKILLS.md` 是 registry标准 skill 正文放在 `skills/*/SKILL.md`
- MCP 客户端直接通过 `node --import tsx loader` 启动 TS 入口,不再保留旧的 `src/server.mjs`
- Node/npm/pnpm 版本异常时优先走 `nvm ls` + `nvm use`,不要用扫盘路径作为第一选择。
## 使用建议
- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context``kt_inspect_project`
- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context``kt_inspect_project``kt_read_context` 默认紧凑输出,只有确实需要完整 `TASKS.md` 时再传 `includeFullDocs=true`
- 多项目联动时先调用 `kt_inspect_all_projects`
- 要收尾时调用 `kt_finish_task`,默认只生成计划和 review需要执行验证时显式传 `runValidation=true`
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。
@ -123,13 +125,13 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
- 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply` 等 flow。
- 远程服务排查先调用 `kt_remote_health_check`,默认只生成只读命令;需要执行时显式传 `execute=true`
- 数据库同步前调用 `kt_db_sync_plan`,先确认源库、目标库、备份库和校验点。
- 改 KtTable、BlogArgon、AdminAuth、QQBot 前调用 `kt_component_workflow`,先看专项禁区。
- 改 KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger 前调用 `kt_component_workflow`,先看专项禁区。
- 不确定任务边界时调用 `kt_guardrails`,先拿到“能做什么、不能做什么、怎么验证”。
- 验证前调用 `kt_suggest_verification`,避免盲跑全量构建。
- 页面测试前调用 `kt_create_page_test_case`,再执行 Playwright/浏览器测试。
- Admin 页面测试前可先执行 `pnpm run admin-login -- --url <Admin登录页>` 固化登录态,输出的 `storageState` 可作为后续 Playwright 用例前置状态。
- 接口改动后调用 `kt_api_test_plan`,并真实请求一次接口。
- 验证启动过本地服务后调用 `kt_cleanup_process_plan`,清掉本次进程。
- 每轮测试/验证结束后调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --keep=3`,让历史产物只保留最近 3 轮。
- 每轮测试/验证结束后调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --keep=3`,让 `.kt-workspace` 历史产物只保留最近 3 轮。
- 改完文件后用 `kt_append_task_record``dryRun` 预览记录,再决定是否写入。
- 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。

View File

@ -1,6 +1,6 @@
{
"name": "@kt/mcp-kt-workflow",
"version": "0.4.0",
"version": "0.5.0",
"private": true,
"type": "module",
"description": "Reusable MCP workflow tools for the KT workspace.",

View File

@ -20,7 +20,7 @@ const workspaceRoot = path.resolve(
process.env.KT_WORKSPACE_ROOT || path.join(dirname, '../../..'),
);
const adminRoot = path.join(workspaceRoot, 'Vue/kt-template-admin');
const defaultArtifactDir = path.join(workspaceRoot, '.codex-verify/admin-login');
const defaultArtifactDir = path.join(workspaceRoot, '.kt-workspace/verify/admin-login');
/**
* @param value

View File

@ -3,6 +3,7 @@ export const projectAliases = {
admin: 'Vue/kt-template-admin',
api: 'Node/kt-template-online-api',
blog: 'Vue/kt-blog-web',
knife4j: 'Plugins/knife4j-swagger-vue3',
mcp: 'mcp/ktWorkflow',
playground: 'Vue/kt-template-online-playground',
root: '.',
@ -13,6 +14,7 @@ export const projectLabels = {
admin: 'Vue Admin 后台',
api: 'NestJS API 后端',
blog: 'Blog Web 博客前台',
knife4j: 'Knife4j Swagger Vue3 插件',
mcp: 'KT Workflow MCP',
playground: 'Playground 编辑器',
root: 'KT 协作根目录',
@ -59,10 +61,10 @@ export const registeredToolNames = [
] as const;
export const defaultHistoryRoots = [
'test-artifacts',
'.codex-test-logs',
'codex-test-logs',
'.codex-verify',
'.playwright-mcp',
'.codex-db-sync',
'.kt-workspace/test-artifacts',
'.kt-workspace/test-logs',
'.kt-workspace/verify',
'.kt-workspace/playwright-mcp',
'.kt-workspace/db-sync',
'.kt-workspace/tmp',
] as const;

View File

@ -85,6 +85,38 @@ export function getRecentTaskRecords(count = 5): string {
return records.join('\n\n').trim();
}
export function getTaskContextIndex(): string {
const tasks = readText('TASKS.md');
const recentMarker = '\n## 最近记录';
const historyMarker = '\n## 历史索引';
const recentIndex = tasks.indexOf(recentMarker);
const historyIndex = tasks.indexOf(historyMarker);
const head = recentIndex === -1 ? tasks : tasks.slice(0, recentIndex);
const history = historyIndex === -1 ? '' : tasks.slice(historyIndex);
return [head.trim(), history.trim()].filter(Boolean).join('\n\n');
}
export function listWorkspaceSkills(): Array<{ description: string | null; name: string; path: string }> {
const skillsRoot = resolveInsideRoot('skills');
if (!existsSync(skillsRoot)) return [];
return readdirSync(skillsRoot, { withFileTypes: true })
.filter((item) => item.isDirectory())
.map((item) => {
const skillPath = path.join(skillsRoot, item.name, 'SKILL.md');
const content = existsSync(skillPath) ? readFileSync(skillPath, 'utf8') : '';
const name = content.match(/^name:\s*(.+)$/m)?.[1]?.trim() || item.name;
const description = content.match(/^description:\s*(.+)$/m)?.[1]?.trim() || null;
return {
description,
name,
path: toPosix(path.relative(workspaceRoot, skillPath)),
};
});
}
export function formatDateInShanghai(date = new Date()): string {
const parts = new Intl.DateTimeFormat('en-CA', {
day: '2-digit',

View File

@ -27,9 +27,10 @@ export function registerTools(server: McpServer): void {
server.registerTool(
'kt_read_context',
{
description: '读取 KT 根目录 AGENTS/SKILLS/TASKS并返回项目清单和最近任务记录。',
description: '读取 KT 根目录紧凑上下文,并返回项目清单和最近任务记录;需要完整文档时显式 includeFullDocs=true。',
inputSchema: {
taskRecordCount: z.number().int().min(0).max(20).default(5),
includeFullDocs: z.boolean().default(false),
taskRecordCount: z.number().int().min(0).max(20).default(3),
},
title: 'Read KT Workflow Context',
},
@ -152,7 +153,7 @@ export function registerTools(server: McpServer): void {
server.registerTool(
'kt_cleanup_history',
{
description: '清理 KT 根目录历史产物。默认 dryRun=true只预览执行时按 LastWriteTime 每个历史目录仅保留最近 3 轮test-artifacts/_templates 永久保留。',
description: '清理 KT 运行态历史产物。默认 dryRun=true只预览执行时按 LastWriteTime 每个 .kt-workspace 历史目录仅保留最近 3 轮,.kt-workspace/test-artifacts/_templates 永久保留。',
inputSchema: {
dryRun: z.boolean().default(true),
keep: z.number().int().min(0).max(50).default(3),
@ -318,9 +319,11 @@ export function registerTools(server: McpServer): void {
server.registerTool(
'kt_component_workflow',
{
description: '按 KT 专项组件/链路输出防踩坑工作流KtTable、BlogArgon、AdminAuth、QQBot。',
description: '按 KT 专项组件/链路输出防踩坑工作流KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger。',
inputSchema: {
target: z.enum(['AdminAuth', 'BlogArgon', 'KtTable', 'QQBot']).default('KtTable'),
target: z
.enum(['AdminAuth', 'BlogArgon', 'Knife4jSwagger', 'KtTable', 'QQBot'])
.default('KtTable'),
},
title: 'KT Component Workflow',
},

View File

@ -24,6 +24,7 @@ export async function runSelfTest(): Promise<void> {
keep: 3,
}),
inspectAdmin: await inspectProject({ includeGit: false, project: 'admin' }),
inspectKnife4j: await inspectProject({ includeGit: false, project: 'knife4j' }),
prepareTask: await prepareTask({
includeGit: false,
project: 'mcp',
@ -43,7 +44,7 @@ export async function runSelfTest(): Promise<void> {
projects: ['mcp'],
}),
componentWorkflow: buildComponentWorkflow({
target: 'KtTable',
target: 'Knife4jSwagger',
}),
dbSyncPlan: buildDbSyncPlan({}),
finishTask: await buildFinishTask({
@ -63,7 +64,7 @@ export async function runSelfTest(): Promise<void> {
project: 'mcp',
}),
};
const outputDir = resolveInsideRoot('.codex-verify/ktWorkflow');
const outputDir = resolveInsideRoot('.kt-workspace/verify/ktWorkflow');
mkdirSync(outputDir, { recursive: true });
writeFileSync(
path.join(outputDir, 'self-test.json'),

View File

@ -91,7 +91,9 @@ export function cleanupHistoryArtifacts(input: CleanupHistoryInput = {}): Record
continue;
}
const preservedNames = new Set(rootRelativePath === 'test-artifacts' ? ['_templates'] : []);
const preservedNames = new Set(
rootRelativePath.endsWith('test-artifacts') ? ['_templates'] : [],
);
const entries = listHistoryEntries(rootPath, preservedNames);
const preserved = entries.slice(0, keep);
const deleting = entries.slice(keep);

View File

@ -1,12 +1,38 @@
import { existsSync } from 'node:fs';
import { existsSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';
import type { ProjectAlias } from '../types.js';
import type { EnvPolicyInput, RiskScanInput } from '../types.js';
import { tryExecFile } from '../core/exec.js';
import { detectRepoType, parseGitStatusFiles } from '../core/project.js';
import { readTextIfExists, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js';
const frontendEnvProjectAliases = new Set<ProjectAlias>([
'admin',
'blog',
'playground',
'web',
]);
function allowsTrackedFrontendEnv(projectAlias: ProjectAlias | null): boolean {
return Boolean(projectAlias && frontendEnvProjectAliases.has(projectAlias));
}
function readSmallFileIfExists(filePath: string): string {
if (!existsSync(filePath)) return '';
try {
const stats = statSync(filePath);
if (!stats.isFile() || stats.size > 512 * 1024) return '';
return readFileSync(filePath, 'utf8');
} catch {
return '';
}
}
export function buildEnvPolicy(input: EnvPolicyInput): Record<string, unknown> {
const project = resolveProject(input.project);
const frontendEnvAllowed = allowsTrackedFrontendEnv(project.alias);
const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => {
const absolutePath = path.join(project.path, file);
return {
@ -24,14 +50,20 @@ export function buildEnvPolicy(input: EnvPolicyInput): Record<string, unknown> {
return {
envFiles,
ignoredRules: ignoredRules.filter((line) => line.includes('.env')),
policy: [
'真实开发和生产配置只保留在本地,不提交。',
'.env.example 用于维护变量名和示例值。',
'接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。',
],
policy: frontendEnvAllowed
? [
'前端项目的 .env.development/.env.production 可以跟随仓库,用于客户端构建变量。',
'前端 env 仍不得写入服务端密钥、数据库密码、WordPress 管理员密码、MinIO Secret 或 token。',
'.env.example 继续维护变量名和示例值。',
]
: [
'真实开发和生产配置只保留在本地,不提交。',
'.env.example 用于维护变量名和示例值。',
'接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。',
],
project,
warnings: envFiles
.filter((file) => file.file !== '.env.example' && file.exists)
.filter((file) => file.file !== '.env.example' && file.exists && !frontendEnvAllowed)
.map((file) => `${file.file} 存在,提交前确认已被 .gitignore 忽略。`),
};
}
@ -65,8 +97,17 @@ export async function scanTaskRisk(input: RiskScanInput): Promise<Record<string,
for (const file of changedFiles) {
const normalized = file.replaceAll('\\', '/');
if (/\.env\.(development|production)$/.test(normalized)) {
addRisk('high', file, '真实环境配置文件有改动。', '确认不会提交真实密钥和密码。');
if (/(^|\/)\.env($|\..+)/.test(normalized)) {
if (allowsTrackedFrontendEnv(project.alias)) {
addRisk(
'medium',
file,
'前端构建环境文件有改动。',
'确认只包含客户端公开配置,不包含服务端密钥、数据库密码或 token。',
);
} else {
addRisk('high', file, '真实环境配置文件有改动。', '确认不会提交真实密钥和密码。');
}
}
if (/package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(normalized)) {
@ -89,6 +130,42 @@ export async function scanTaskRisk(input: RiskScanInput): Promise<Record<string,
if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) {
addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。');
}
if (/\.(tsx|jsx|vue)$/.test(normalized)) {
const source = readSmallFileIfExists(path.join(project.path, normalized));
if (/\bfunction\s+render[A-Z0-9_]/.test(source)) {
addRisk(
'medium',
file,
'Vue TSX render helper 使用 function 声明,容易在插槽渲染时踩 this/上下文问题。',
'改为箭头函数,并在嵌套组件插槽中显式传 default。',
);
}
if (/<(?:A?TabPane|Tabs\.TabPane)\b[\s\S]*?>\s*\{\s*(?!\{[\s\S]*?\bdefault\s*:)/.test(source)) {
addRisk(
'medium',
file,
'TabPane 插槽疑似未显式声明 default。',
'Vue TSX 嵌套组件插槽使用 {{ default: () => ... }} 或等价显式 default 写法。',
);
}
if (/\bonUpdate:[A-Za-z_$][\w$]*/.test(source)) {
addRisk(
'medium',
file,
'Vue TSX 中存在手写 onUpdate:* 绑定。',
'优先使用原生 v-model:value / v-model:activeKey 等写法,减少事件名偏差。',
);
}
if (/\bdestroyOnClose\b/.test(source)) {
addRisk(
'medium',
file,
'Antdv Modal destroyOnClose 已废弃。',
'改用 destroyOnHidden或优先复用 Vben Modal/Form 封装。',
);
}
}
}
return {

View File

@ -16,7 +16,7 @@ export function buildGuardrails(input: GuardrailsInput): GuardrailsResult {
duringEdit: [
'改动范围收敛到本次需求相关文件。',
'复杂度高的地方加简短注释,避免空泛解释。',
'真实环境配置不写入提交内容,优先只维护 .env.example。',
'后端真实环境配置不写入提交内容;前端 env 只允许客户端公开变量。',
'前端实现遵循现有 antdv-next/Vben 约定,不引入无关抽象。',
],
doNot: [

View File

@ -13,6 +13,7 @@ export const defaultReviewProjects = [
'web',
'playground',
'mcp',
'knife4j',
] as const;
const reviewSkipDirectoryNames = new Set([
@ -29,11 +30,27 @@ const reviewSkipDirectoryNames = new Set([
const reviewSkipFilePattern =
/(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|.*\.map|.*\.png|.*\.jpg|.*\.jpeg|.*\.gif|.*\.webp|.*\.zip|.*\.tar|.*\.gz)$/i;
const rootGeneratedArtifactNames = new Set([
'.codex-db-sync',
'.codex-test-logs',
'.codex-verify',
'.playwright-mcp',
'codex-test-logs',
'test-artifacts',
]);
const sensitiveTrackedFilePattern =
/(^|\/)(\.env($|\..+)|deploy\.json|secrets?\.(env|json|ya?ml)|id_rsa|[^/]+\.(pem|key))$/i;
const allowedSensitiveTrackedFilePattern = /(^|\/)\.env\.example$/i;
const frontendEnvProjectAliases = new Set<ProjectAlias>([
'admin',
'blog',
'playground',
'web',
]);
const credentialAssignmentPattern =
/\b(password|passwd|secret|token|webuiToken|accessToken|privateKey)\b\s*[:=]\s*(["'])([^"']+)\2/i;
@ -124,6 +141,14 @@ function isCredentialHintAllowed(file: string, line: string): boolean {
return /(^|\/)views\/(demos|examples)\//.test(normalized);
}
function isAllowedSensitiveTrackedFile(projectAlias: ProjectAlias | null, file: string): boolean {
if (allowedSensitiveTrackedFilePattern.test(file)) return true;
if (projectAlias && frontendEnvProjectAliases.has(projectAlias)) {
return /(^|\/)\.env($|\..+)/i.test(file);
}
return false;
}
function collectProjectContentFindings(
project: ResolvedProject,
trackedFiles: string[],
@ -168,6 +193,18 @@ function collectProjectContentFindings(
}
if (includeContentScan) {
if (/\.(tsx|jsx|vue)$/.test(file) && /\bfunction\s+render[A-Z0-9_]/.test(line)) {
findings.push({
category: 'vue-tsx-render-helper',
file,
line: index + 1,
level: 'P2',
message: 'Vue TSX render helper 使用 function 声明。',
project: project.relativePath,
suggestion: '改为箭头函数,并在嵌套组件插槽中显式传 default。',
});
}
const credentialMatch = line.match(credentialAssignmentPattern);
if (
credentialMatch &&
@ -227,6 +264,14 @@ function findWorkspaceFilesByName(names: Set<string>, maxResults: number): strin
return results;
}
function findRootGeneratedArtifacts(): string[] {
if (!existsSync(workspaceRoot)) return [];
return readdirSync(workspaceRoot, { withFileTypes: true })
.filter((entry) => rootGeneratedArtifactNames.has(entry.name))
.map((entry) => entry.name);
}
async function reviewProject(
projectKey: string,
input: Required<Pick<GlobalCodeReviewInput, 'includeContentScan' | 'maxFindingsPerProject'>>,
@ -242,7 +287,7 @@ async function reviewProject(
const trackedSensitiveFiles = trackedFiles.filter(
(file) =>
sensitiveTrackedFilePattern.test(file) &&
!allowedSensitiveTrackedFilePattern.test(file),
!isAllowedSensitiveTrackedFile(project.alias, file),
);
const contentFindings = collectProjectContentFindings(
project,
@ -301,6 +346,8 @@ export async function buildGlobalCodeReview(
input.includeRootScan === false
? []
: findWorkspaceFilesByName(new Set(['deploy.js', 'deploy.json']), 50);
const rootGeneratedArtifacts =
input.includeRootScan === false ? [] : findRootGeneratedArtifacts();
const findings = projectResults.flatMap((item) => [
...item.findings,
...item.debugFindings,
@ -314,7 +361,15 @@ export async function buildGlobalCodeReview(
project: 'root',
suggestion: '确认是否仍被脚本引用;无用则删除并从 Git 索引移除。',
}));
const allFindings = [...findings, ...rootFindings];
const rootGeneratedFindings = rootGeneratedArtifacts.map<ReviewFinding>((file) => ({
category: 'root-generated-artifact',
file,
level: 'P2',
message: '根目录存在生成态临时产物。',
project: 'root',
suggestion: '统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。',
}));
const allFindings = [...findings, ...rootFindings, ...rootGeneratedFindings];
return {
findings: allFindings,
@ -344,6 +399,7 @@ export async function buildGlobalCodeReview(
p2: allFindings.filter((item) => item.level === 'P2').length,
p3: allFindings.filter((item) => item.level === 'P3').length,
reviewedProjects: projectResults.length,
rootGeneratedArtifacts,
rootLegacyFiles,
},
workspaceRoot,

View File

@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import type { CommitChecklistInput, PrepareTaskInput, TaskRecordInput, WorkflowContextInput } from '../types.js';
import { projectAliases } from '../core/constants.js';
import { detectRepoType } from '../core/project.js';
import { formatDateInShanghai, getRecentTaskRecords, readText, resolveInsideRoot, resolveProject, workspaceRoot } from '../core/workspace.js';
import { formatDateInShanghai, getRecentTaskRecords, getTaskContextIndex, listWorkspaceSkills, readText, resolveInsideRoot, resolveProject, workspaceRoot } from '../core/workspace.js';
import { buildGuardrails } from './guardrails.js';
import { inspectProject } from './inspect.js';
import { scanTaskRisk } from './envRisk.js';
@ -125,6 +125,8 @@ export async function createCommitChecklist(input: CommitChecklistInput): Promis
}
export function readWorkflowContext(input: WorkflowContextInput): Record<string, unknown> {
const includeFullDocs = input.includeFullDocs === true;
const taskRecordCount = input.taskRecordCount ?? 3;
const projects = Object.keys(projectAliases).map((key) => {
const project = resolveProject(key);
return {
@ -137,9 +139,18 @@ export function readWorkflowContext(input: WorkflowContextInput): Record<string,
return {
docs: {
agents: readText('AGENTS.md'),
recentTasks: getRecentTaskRecords(input.taskRecordCount),
detailDocs: [
'docs/kt-context-semantics.md',
'docs/kt-workspace-structure.md',
'docs/kt-testing-workflows.md',
],
recentTasks: getRecentTaskRecords(taskRecordCount),
skills: readText('SKILLS.md'),
skillPackages: listWorkspaceSkills(),
taskIndex: getTaskContextIndex(),
tasks: includeFullDocs ? readText('TASKS.md') : undefined,
},
includeFullDocs,
projects,
workspaceRoot,
};

View File

@ -57,6 +57,13 @@ export function buildVerificationPlan(input: VerificationPlanInput): Verificatio
notes.push('MCP 变更需要用真实 SDK Client 做 listTools/callTool smoke test。');
}
if (project.alias === 'knife4j') {
notes.push('Knife4j 插件常规改动先跑 typecheck发布、提交钩子或 UI 复刻改动再跑 verify:commit/build。');
if (scripts['verify:commit'] && ['commit', 'deploy'].includes(changeType)) {
commands.push(`${packageManager} run verify:commit`);
}
}
if (input.includePageTest || changeType === 'page') {
notes.push(
'页面级测试必须先写测试用例,使用可见浏览器或截图记录关键步骤,同一用例最多三轮闭环。',

View File

@ -60,6 +60,9 @@ function suggestCommitMessage(snapshot: RepoSnapshot): string {
if (snapshot.project.alias === 'blog') {
return 'fix(blog): 优化博客前台体验';
}
if (snapshot.project.alias === 'knife4j') {
return 'feat(knife4j): 升级接口文档插件能力';
}
return `chore(${snapshot.project.alias || 'repo'}): 更新项目改动`;
}
@ -135,7 +138,7 @@ export async function buildCommitPlan(input: CommitPlanInput = {}): Promise<Reco
checks: [
'每个独立 Git 仓库单独提交。',
'提交前先跑对应轻量验证和 kt_global_code_review。',
'确认 .env.development/.env.production 等真实配置只从索引移除,不提交真实值。',
'后端真实 .env.development/.env.production 不提交;前端 env 可提交但只能包含客户端公开配置。',
'根目录 AGENTS/SKILLS/TASKS 不属于子仓库,最终回复里单独说明。',
],
includePush: input.includePush ?? false,
@ -256,6 +259,7 @@ export function buildDbSyncPlan(input: DbSyncPlanInput = {}): Record<string, unk
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。',
'mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers <source-conn> kt_template > .codex-db-sync/kt_template.sql',
`mysql <target-conn> -e "CREATE DATABASE IF NOT EXISTS ${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}`,
@ -266,6 +270,7 @@ export function buildDbSyncPlan(input: DbSyncPlanInput = {}): Record<string, unk
guardrails: [
'默认只生成计划不执行覆盖、drop、truncate。',
'同步前必须确认 source/target 方向,不能靠变量名猜。',
'3306/3307 端口方向先用只读 SELECT 预检确认,避免把远程库误当成本地库。',
'目标库备份校验通过前不得删除备份库。',
'涉及生产库时优先只读验证,批量删除必须二次确认。',
],
@ -295,6 +300,12 @@ export function buildComponentWorkflow(input: ComponentWorkflowInput = {}): Reco
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 与左侧调试栏固定'],
},
KtTable: {
antiPatterns: ['自定义纵向滚动条覆盖 Antdv 原生滚动', 'scroll/resize 高频写响应式状态', '表格高度参与展开收起动画', '固定列透明导致透视'],
files: ['Vue/kt-template-admin/apps/web-antdv-next/src/components/ktTable'],

View File

@ -2,6 +2,7 @@ export type ProjectAlias =
| 'admin'
| 'api'
| 'blog'
| 'knife4j'
| 'mcp'
| 'playground'
| 'root'
@ -183,7 +184,7 @@ export interface DbSyncPlanInput {
}
export interface ComponentWorkflowInput {
target?: 'AdminAuth' | 'BlogArgon' | 'KtTable' | 'QQBot';
target?: 'AdminAuth' | 'BlogArgon' | 'Knife4jSwagger' | 'KtTable' | 'QQBot';
}
export interface PrepareTaskInput extends GuardrailsInput {
@ -209,6 +210,7 @@ export interface CommitChecklistInput {
}
export interface WorkflowContextInput {
includeFullDocs?: boolean;
taskRecordCount?: number;
}