feat: 规范KT工作流上下文治理
This commit is contained in:
parent
e715cca304
commit
d813dc69ab
24
README.md
24
README.md
@ -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`,确认文件范围和提交信息。
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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 原始命令行布尔值。
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -16,7 +16,7 @@ export function buildGuardrails(input: GuardrailsInput): GuardrailsResult {
|
||||
duringEdit: [
|
||||
'改动范围收敛到本次需求相关文件。',
|
||||
'复杂度高的地方加简短注释,避免空泛解释。',
|
||||
'真实环境配置不写入提交内容,优先只维护 .env.example。',
|
||||
'后端真实环境配置不写入提交内容;前端 env 只允许客户端公开变量。',
|
||||
'前端实现遵循现有 antdv-next/Vben 约定,不引入无关抽象。',
|
||||
],
|
||||
doNot: [
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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(
|
||||
'页面级测试必须先写测试用例,使用可见浏览器或截图记录关键步骤,同一用例最多三轮闭环。',
|
||||
|
||||
@ -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 是本地 MySQL,127.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'],
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user