feat: 升级KT工作流能力
This commit is contained in:
parent
d813dc69ab
commit
ad7ee6d1b0
20
README.md
20
README.md
@ -12,15 +12,15 @@
|
|||||||
- 生成任务收尾包:把状态、验证计划、可选验证执行、全局 review 和历史清理收成一份结果。
|
- 生成任务收尾包:把状态、验证计划、可选验证执行、全局 review 和历史清理收成一份结果。
|
||||||
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
||||||
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
||||||
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、Web/Playground 回跳。
|
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
||||||
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
||||||
- 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、备份和行数校验。
|
- 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、备份和行数校验。
|
||||||
- 生成专项组件工作流:KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger 的防踩坑清单和验证点。
|
- 生成专项组件工作流:KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger 的防踩坑清单和验证点。
|
||||||
- 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。
|
- 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。
|
||||||
- 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留。
|
- 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留。
|
||||||
- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。
|
- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。
|
||||||
- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物和当前变更风险;任何文件改动后都要跑一遍。
|
- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物和当前变更风险;默认只对变更文件做内容扫描,避免历史误报污染上下文;任何文件改动后都要跑一遍。
|
||||||
- 生成或写入 `TASKS.md` 改动记录:默认 `dryRun=true`,确认后再落盘。
|
- 生成或写入 `TASKS.md` 最近记录:默认 `dryRun=true`,确认后再落盘。
|
||||||
- 生成提交前检查清单:校验 KT commit message 约定。
|
- 生成提交前检查清单:校验 KT commit message 约定。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
@ -74,15 +74,15 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
| `kt_cleanup_process_plan` | 生成验证进程清理检查命令 |
|
| `kt_cleanup_process_plan` | 生成验证进程清理检查命令 |
|
||||||
| `kt_env_policy` | 检查 env 文件现状和提交策略 |
|
| `kt_env_policy` | 检查 env 文件现状和提交策略 |
|
||||||
| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 |
|
| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 |
|
||||||
| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描 |
|
| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 |
|
||||||
| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 |
|
| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 |
|
||||||
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
|
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
|
||||||
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
|
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
|
||||||
| `kt_business_test_plan` | 生成固化业务链路测试计划 |
|
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
|
||||||
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
|
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
|
||||||
| `kt_db_sync_plan` | 生成数据库同步安全向导 |
|
| `kt_db_sync_plan` | 生成数据库同步安全向导 |
|
||||||
| `kt_component_workflow` | 输出专项组件/链路防踩坑工作流 |
|
| `kt_component_workflow` | 输出专项组件/链路防踩坑工作流 |
|
||||||
| `kt_append_task_record` | 预览或写入 `TASKS.md` 改动记录 |
|
| `kt_append_task_record` | 预览或写入 `TASKS.md` 最近记录 |
|
||||||
| `kt_commit_checklist` | 生成提交前检查清单并校验 commit message |
|
| `kt_commit_checklist` | 生成提交前检查清单并校验 commit message |
|
||||||
|
|
||||||
## 可复用脚本
|
## 可复用脚本
|
||||||
@ -90,7 +90,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
| 脚本 | 用途 |
|
| 脚本 | 用途 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
|
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
|
||||||
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,输出 JSON 复审报告。 |
|
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告。 |
|
||||||
|
|
||||||
## 项目别名
|
## 项目别名
|
||||||
|
|
||||||
@ -122,10 +122,10 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
- 要收尾时调用 `kt_finish_task`,默认只生成计划和 review;需要执行验证时显式传 `runValidation=true`。
|
- 要收尾时调用 `kt_finish_task`,默认只生成计划和 review;需要执行验证时显式传 `runValidation=true`。
|
||||||
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。
|
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。
|
||||||
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
|
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
|
||||||
- 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply` 等 flow。
|
- 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply`、`qqbot-login-sse`、`fflogs-command`、`system-log-visualization` 等 flow。
|
||||||
- 远程服务排查先调用 `kt_remote_health_check`,默认只生成只读命令;需要执行时显式传 `execute=true`。
|
- 远程服务排查先调用 `kt_remote_health_check`,默认只生成只读命令;需要执行时显式传 `execute=true`。
|
||||||
- 数据库同步前调用 `kt_db_sync_plan`,先确认源库、目标库、备份库和校验点。
|
- 数据库同步前调用 `kt_db_sync_plan`,先确认源库、目标库、备份库和校验点。
|
||||||
- 改 KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger 前调用 `kt_component_workflow`,先看专项禁区。
|
- 改 KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger 前调用 `kt_component_workflow`,先看专项禁区。
|
||||||
- 不确定任务边界时调用 `kt_guardrails`,先拿到“能做什么、不能做什么、怎么验证”。
|
- 不确定任务边界时调用 `kt_guardrails`,先拿到“能做什么、不能做什么、怎么验证”。
|
||||||
- 验证前调用 `kt_suggest_verification`,避免盲跑全量构建。
|
- 验证前调用 `kt_suggest_verification`,避免盲跑全量构建。
|
||||||
- 页面测试前调用 `kt_create_page_test_case`,再执行 Playwright/浏览器测试。
|
- 页面测试前调用 `kt_create_page_test_case`,再执行 Playwright/浏览器测试。
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kt/mcp-kt-workflow",
|
"name": "@kt/mcp-kt-workflow",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Reusable MCP workflow tools for the KT workspace.",
|
"description": "Reusable MCP workflow tools for the KT workspace.",
|
||||||
|
|||||||
@ -205,8 +205,9 @@ export function registerTools(server: McpServer): void {
|
|||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kt_global_code_review',
|
'kt_global_code_review',
|
||||||
{
|
{
|
||||||
description: '对 KT 已知子仓库做只读全局 CodeReview 扫描:Git 状态、tracked env/deploy/secrets、冲突标记、调试输出、疑似凭据字面量和当前变更风险。',
|
description: '对 KT 已知子仓库做只读全局 CodeReview 扫描:Git 状态、tracked env/deploy/secrets、冲突标记、调试输出、疑似凭据字面量和当前变更风险。默认只对变更文件做内容扫描,避免历史误报污染上下文。',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
contentScanMode: z.enum(['changed', 'all']).default('changed'),
|
||||||
includeContentScan: z.boolean().default(true),
|
includeContentScan: z.boolean().default(true),
|
||||||
includeRootScan: z.boolean().default(true),
|
includeRootScan: z.boolean().default(true),
|
||||||
maxFindingsPerProject: z.number().int().min(1).max(200).default(20),
|
maxFindingsPerProject: z.number().int().min(1).max(200).default(20),
|
||||||
@ -224,6 +225,7 @@ export function registerTools(server: McpServer): void {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
changeType: z.enum(taskTypeValues).default('general'),
|
changeType: z.enum(taskTypeValues).default('general'),
|
||||||
cleanupHistory: z.boolean().default(false),
|
cleanupHistory: z.boolean().default(false),
|
||||||
|
contentScanMode: z.enum(['changed', 'all']).default('changed'),
|
||||||
includeRootScan: z.boolean().default(true),
|
includeRootScan: z.boolean().default(true),
|
||||||
keepHistory: z.number().int().min(0).max(50).default(3),
|
keepHistory: z.number().int().min(0).max(50).default(3),
|
||||||
maxFindingsPerProject: z.number().int().min(1).max(200).default(20),
|
maxFindingsPerProject: z.number().int().min(1).max(200).default(20),
|
||||||
@ -274,8 +276,11 @@ export function registerTools(server: McpServer): void {
|
|||||||
.enum([
|
.enum([
|
||||||
'admin-login',
|
'admin-login',
|
||||||
'blog-crud',
|
'blog-crud',
|
||||||
|
'fflogs-command',
|
||||||
'qqbot-account-scan',
|
'qqbot-account-scan',
|
||||||
'qqbot-auto-reply',
|
'qqbot-auto-reply',
|
||||||
|
'qqbot-login-sse',
|
||||||
|
'system-log-visualization',
|
||||||
'web-playground-auth',
|
'web-playground-auth',
|
||||||
'all',
|
'all',
|
||||||
])
|
])
|
||||||
@ -319,10 +324,19 @@ export function registerTools(server: McpServer): void {
|
|||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kt_component_workflow',
|
'kt_component_workflow',
|
||||||
{
|
{
|
||||||
description: '按 KT 专项组件/链路输出防踩坑工作流:KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger。',
|
description: '按 KT 专项组件/链路输出防踩坑工作流:KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger。',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
target: z
|
target: z
|
||||||
.enum(['AdminAuth', 'BlogArgon', 'Knife4jSwagger', 'KtTable', 'QQBot'])
|
.enum([
|
||||||
|
'AdminAuth',
|
||||||
|
'BlogArgon',
|
||||||
|
'FF14Plugin',
|
||||||
|
'Knife4jSwagger',
|
||||||
|
'KtTable',
|
||||||
|
'NapCatLogin',
|
||||||
|
'QQBot',
|
||||||
|
'SystemLog',
|
||||||
|
])
|
||||||
.default('KtTable'),
|
.default('KtTable'),
|
||||||
},
|
},
|
||||||
title: 'KT Component Workflow',
|
title: 'KT Component Workflow',
|
||||||
|
|||||||
@ -38,13 +38,13 @@ export async function runSelfTest(): Promise<void> {
|
|||||||
projects: ['mcp'],
|
projects: ['mcp'],
|
||||||
}),
|
}),
|
||||||
businessTestPlan: buildBusinessTestPlan({
|
businessTestPlan: buildBusinessTestPlan({
|
||||||
flow: 'qqbot-auto-reply',
|
flow: 'system-log-visualization',
|
||||||
}),
|
}),
|
||||||
commitPlan: await buildCommitPlan({
|
commitPlan: await buildCommitPlan({
|
||||||
projects: ['mcp'],
|
projects: ['mcp'],
|
||||||
}),
|
}),
|
||||||
componentWorkflow: buildComponentWorkflow({
|
componentWorkflow: buildComponentWorkflow({
|
||||||
target: 'Knife4jSwagger',
|
target: 'SystemLog',
|
||||||
}),
|
}),
|
||||||
dbSyncPlan: buildDbSyncPlan({}),
|
dbSyncPlan: buildDbSyncPlan({}),
|
||||||
finishTask: await buildFinishTask({
|
finishTask: await buildFinishTask({
|
||||||
|
|||||||
@ -127,6 +127,51 @@ export async function scanTaskRisk(input: RiskScanInput): Promise<Record<string,
|
|||||||
addRisk('medium', file, '后端接口或业务逻辑改动。', '本地启动或复用服务后真实调用对应接口。');
|
addRisk('medium', file, '后端接口或业务逻辑改动。', '本地启动或复用服务后真实调用对应接口。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/src\/common\/decorators\/format-date-time\.decorator\.ts|src\/.*\.(entity|dto)\.ts/.test(normalized)) {
|
||||||
|
addRisk(
|
||||||
|
'medium',
|
||||||
|
file,
|
||||||
|
'后端时间字段或 DTO/Entity 响应结构改动。',
|
||||||
|
'确认数据源经 DTO/Entity 实例返回,FormatDateTime 不做递归响应遍历,时间统一 YYYY-MM-DD HH:mm:ss。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/src\/common\/logger\/|src\/admin\/system-log\/|pino|loki|system-log/i.test(normalized)) {
|
||||||
|
addRisk(
|
||||||
|
'medium',
|
||||||
|
file,
|
||||||
|
'Pino/Loki 或系统日志链路改动。',
|
||||||
|
'验证 API 日志写入、/system/logs 系列接口、Admin 系统日志页面和 Loki ready/query_range。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/src\/qqbot\/plugins\/(ff14Market|fflogs)\//i.test(normalized)) {
|
||||||
|
addRisk(
|
||||||
|
'medium',
|
||||||
|
file,
|
||||||
|
'FF14/FFLogs 命令插件改动。',
|
||||||
|
'确认中文查询和回显走字典映射,命令注册表同步,真实命令输出字段完整。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/src\/qqbot\/(account|napcat|connection)\//i.test(normalized)) {
|
||||||
|
addRisk(
|
||||||
|
'medium',
|
||||||
|
file,
|
||||||
|
'QQBot/NapCat 登录链路改动。',
|
||||||
|
'验证新增账号和更新登录都能生成有效二维码,SSE 进度可见,旧 isLogin 状态不会短路。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/apps\/web-antdv-next\/src\/views\/system\/(dict|log)\//.test(normalized)) {
|
||||||
|
addRisk(
|
||||||
|
'medium',
|
||||||
|
file,
|
||||||
|
'Admin 系统管理页改动。',
|
||||||
|
'验证菜单权限、KtTable 展示、列表筛选和对应 API 响应结构。',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) {
|
if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) {
|
||||||
addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。');
|
addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ interface ReviewFinding {
|
|||||||
|
|
||||||
interface ReviewProjectResult {
|
interface ReviewProjectResult {
|
||||||
changedFiles: string[];
|
changedFiles: string[];
|
||||||
|
contentScannedFiles: string[];
|
||||||
credentialHints: ReviewFinding[];
|
credentialHints: ReviewFinding[];
|
||||||
debugFindings: ReviewFinding[];
|
debugFindings: ReviewFinding[];
|
||||||
findings: ReviewFinding[];
|
findings: ReviewFinding[];
|
||||||
@ -109,11 +110,16 @@ function isBenignCredentialValue(value: string): boolean {
|
|||||||
if (!normalized) return true;
|
if (!normalized) return true;
|
||||||
if (normalized.startsWith('$') || normalized.startsWith('${')) return true;
|
if (normalized.startsWith('$') || normalized.startsWith('${')) return true;
|
||||||
if (normalized.startsWith('<') || normalized.includes('example')) return true;
|
if (normalized.startsWith('<') || normalized.includes('example')) return true;
|
||||||
|
if (/^\*+$/.test(normalized)) return true;
|
||||||
return [
|
return [
|
||||||
'false',
|
'false',
|
||||||
|
'hidden',
|
||||||
|
'masked',
|
||||||
'null',
|
'null',
|
||||||
'password',
|
'password',
|
||||||
'please-replace-me-with-your-own-key',
|
'please-replace-me-with-your-own-key',
|
||||||
|
'redacted',
|
||||||
|
'replace-me',
|
||||||
'secret',
|
'secret',
|
||||||
'token',
|
'token',
|
||||||
'true',
|
'true',
|
||||||
@ -138,6 +144,7 @@ function isCredentialHintAllowed(file: string, line: string): boolean {
|
|||||||
const normalized = file.replaceAll('\\', '/');
|
const normalized = file.replaceAll('\\', '/');
|
||||||
const trimmed = line.trimStart();
|
const trimmed = line.trimStart();
|
||||||
if (trimmed.startsWith('<')) return true;
|
if (trimmed.startsWith('<')) return true;
|
||||||
|
if (/\b(url|uri|dsn)\.password\s*=\s*['"]\*+['"]/.test(trimmed)) return true;
|
||||||
return /(^|\/)views\/(demos|examples)\//.test(normalized);
|
return /(^|\/)views\/(demos|examples)\//.test(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +281,9 @@ function findRootGeneratedArtifacts(): string[] {
|
|||||||
|
|
||||||
async function reviewProject(
|
async function reviewProject(
|
||||||
projectKey: string,
|
projectKey: string,
|
||||||
input: Required<Pick<GlobalCodeReviewInput, 'includeContentScan' | 'maxFindingsPerProject'>>,
|
input: Required<
|
||||||
|
Pick<GlobalCodeReviewInput, 'contentScanMode' | 'includeContentScan' | 'maxFindingsPerProject'>
|
||||||
|
>,
|
||||||
): Promise<ReviewProjectResult> {
|
): Promise<ReviewProjectResult> {
|
||||||
const project = resolveProject(projectKey);
|
const project = resolveProject(projectKey);
|
||||||
const repoType = detectRepoType(project.path);
|
const repoType = detectRepoType(project.path);
|
||||||
@ -284,6 +293,14 @@ async function reviewProject(
|
|||||||
: { ok: false, stderr: '', stdout: '' };
|
: { ok: false, stderr: '', stdout: '' };
|
||||||
const changedFiles = status.ok ? parseGitStatusFiles(status.stdout) : [];
|
const changedFiles = status.ok ? parseGitStatusFiles(status.stdout) : [];
|
||||||
const trackedFiles = repoType === 'git' ? await listGitTrackedFiles(project.path) : [];
|
const trackedFiles = repoType === 'git' ? await listGitTrackedFiles(project.path) : [];
|
||||||
|
const trackedFileSet = new Set(trackedFiles);
|
||||||
|
const contentScannedFiles =
|
||||||
|
input.contentScanMode === 'all'
|
||||||
|
? trackedFiles
|
||||||
|
: changedFiles
|
||||||
|
.map((file) => file.replaceAll('\\', '/'))
|
||||||
|
.filter((file) => shouldReadReviewFile(file))
|
||||||
|
.filter((file) => trackedFileSet.has(file) || existsSync(path.join(project.path, file)));
|
||||||
const trackedSensitiveFiles = trackedFiles.filter(
|
const trackedSensitiveFiles = trackedFiles.filter(
|
||||||
(file) =>
|
(file) =>
|
||||||
sensitiveTrackedFilePattern.test(file) &&
|
sensitiveTrackedFilePattern.test(file) &&
|
||||||
@ -291,7 +308,7 @@ async function reviewProject(
|
|||||||
);
|
);
|
||||||
const contentFindings = collectProjectContentFindings(
|
const contentFindings = collectProjectContentFindings(
|
||||||
project,
|
project,
|
||||||
trackedFiles,
|
contentScannedFiles,
|
||||||
input.includeContentScan,
|
input.includeContentScan,
|
||||||
input.maxFindingsPerProject,
|
input.maxFindingsPerProject,
|
||||||
);
|
);
|
||||||
@ -308,6 +325,7 @@ async function reviewProject(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
changedFiles,
|
changedFiles,
|
||||||
|
contentScannedFiles,
|
||||||
credentialHints: contentFindings.credentialHints,
|
credentialHints: contentFindings.credentialHints,
|
||||||
debugFindings: contentFindings.debugFindings,
|
debugFindings: contentFindings.debugFindings,
|
||||||
findings: [...trackedSensitiveFindings, ...contentFindings.findings],
|
findings: [...trackedSensitiveFindings, ...contentFindings.findings],
|
||||||
@ -330,6 +348,7 @@ export async function buildGlobalCodeReview(
|
|||||||
? input.projects!
|
? input.projects!
|
||||||
: [...defaultReviewProjects];
|
: [...defaultReviewProjects];
|
||||||
const includeContentScan = input.includeContentScan ?? true;
|
const includeContentScan = input.includeContentScan ?? true;
|
||||||
|
const contentScanMode = input.contentScanMode ?? 'changed';
|
||||||
const maxFindingsPerProject = Math.max(
|
const maxFindingsPerProject = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.min(input.maxFindingsPerProject ?? 20, 200),
|
Math.min(input.maxFindingsPerProject ?? 20, 200),
|
||||||
@ -337,6 +356,7 @@ export async function buildGlobalCodeReview(
|
|||||||
const projectResults = await Promise.all(
|
const projectResults = await Promise.all(
|
||||||
projects.map((project) =>
|
projects.map((project) =>
|
||||||
reviewProject(project, {
|
reviewProject(project, {
|
||||||
|
contentScanMode,
|
||||||
includeContentScan,
|
includeContentScan,
|
||||||
maxFindingsPerProject,
|
maxFindingsPerProject,
|
||||||
}),
|
}),
|
||||||
@ -377,11 +397,13 @@ export async function buildGlobalCodeReview(
|
|||||||
notes: [
|
notes: [
|
||||||
'本工具只读扫描,不会修改文件、删除文件或提交代码。',
|
'本工具只读扫描,不会修改文件、删除文件或提交代码。',
|
||||||
'P1 优先处理:冲突标记、被跟踪的真实 env/部署凭据文件。',
|
'P1 优先处理:冲突标记、被跟踪的真实 env/部署凭据文件。',
|
||||||
|
'默认 contentScanMode=changed,只对当前变更文件做 P2/P3 内容扫描,避免历史误报污染上下文;需要全仓库深扫时传 all。',
|
||||||
'P2 需要人工确认:疑似凭据字面量、旧部署文件、调试断点。',
|
'P2 需要人工确认:疑似凭据字面量、旧部署文件、调试断点。',
|
||||||
'P3 为清洁度问题:运行时 console.log 等。',
|
'P3 为清洁度问题:运行时 console.log 等。',
|
||||||
],
|
],
|
||||||
projects: projectResults.map((item) => ({
|
projects: projectResults.map((item) => ({
|
||||||
changedFiles: item.changedFiles,
|
changedFiles: item.changedFiles,
|
||||||
|
contentScannedFileCount: item.contentScannedFiles.length,
|
||||||
credentialHintCount: item.credentialHints.length,
|
credentialHintCount: item.credentialHints.length,
|
||||||
debugFindingCount: item.debugFindings.length,
|
debugFindingCount: item.debugFindings.length,
|
||||||
findingCount: item.findings.length,
|
findingCount: item.findings.length,
|
||||||
@ -394,6 +416,7 @@ export async function buildGlobalCodeReview(
|
|||||||
summary: {
|
summary: {
|
||||||
changedProjectCount: projectResults.filter((item) => item.changedFiles.length > 0)
|
changedProjectCount: projectResults.filter((item) => item.changedFiles.length > 0)
|
||||||
.length,
|
.length,
|
||||||
|
contentScanMode,
|
||||||
findingCount: allFindings.length,
|
findingCount: allFindings.length,
|
||||||
p1: allFindings.filter((item) => item.level === 'P1').length,
|
p1: allFindings.filter((item) => item.level === 'P1').length,
|
||||||
p2: allFindings.filter((item) => item.level === 'P2').length,
|
p2: allFindings.filter((item) => item.level === 'P2').length,
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export async function prepareTask(input: PrepareTaskInput): Promise<Record<strin
|
|||||||
'按最小范围修改。',
|
'按最小范围修改。',
|
||||||
'按 verification.commands 和 notes 做轻量验证。',
|
'按 verification.commands 和 notes 做轻量验证。',
|
||||||
'有文件改动时更新 TASKS.md。',
|
'有文件改动时更新 TASKS.md。',
|
||||||
'最终回复或提交前跑全局 review,并处理全部 findings。',
|
'最终回复或提交前跑全局 review;默认只深扫变更文件,真实 findings 先处理。',
|
||||||
],
|
],
|
||||||
pageTestCase,
|
pageTestCase,
|
||||||
project,
|
project,
|
||||||
@ -66,21 +66,22 @@ export function buildTaskRecord(input: TaskRecordInput): string {
|
|||||||
const date = input.date || formatDateInShanghai();
|
const date = input.date || formatDateInShanghai();
|
||||||
const scope = (input.scope?.length ?? 0) > 0 ? input.scope!.join('、') : '待补充';
|
const scope = (input.scope?.length ?? 0) > 0 ? input.scope!.join('、') : '待补充';
|
||||||
const content = input.content || '待补充';
|
const content = input.content || '待补充';
|
||||||
const testCase = input.testCase || '本次无页面测试或待补充。';
|
|
||||||
const verification = input.verification || '待补充。';
|
const verification = input.verification || '待补充。';
|
||||||
|
|
||||||
return `### ${date}:${input.title}\n\n- 范围:${scope}\n- 内容:${content}\n- 页面测试用例:${testCase}\n- 验证:${verification}\n`;
|
const optionalTestCase = input.testCase ? `- 页面测试用例:${input.testCase}\n` : '';
|
||||||
|
|
||||||
|
return `### ${date}:${input.title}\n\n- 范围:${scope}\n- 关键词:${content}\n${optionalTestCase}- 验证:${verification}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendTaskRecord(input: TaskRecordInput): Record<string, unknown> {
|
export function appendTaskRecord(input: TaskRecordInput): Record<string, unknown> {
|
||||||
const record = buildTaskRecord(input);
|
const record = buildTaskRecord(input);
|
||||||
const tasksPath = resolveInsideRoot('TASKS.md');
|
const tasksPath = resolveInsideRoot('TASKS.md');
|
||||||
const current = readFileSync(tasksPath, 'utf8');
|
const current = readFileSync(tasksPath, 'utf8');
|
||||||
const marker = '## 改动记录';
|
const marker = current.includes('## 最近记录') ? '## 最近记录' : '## 改动记录';
|
||||||
const markerIndex = current.indexOf(marker);
|
const markerIndex = current.indexOf(marker);
|
||||||
|
|
||||||
if (markerIndex === -1) {
|
if (markerIndex === -1) {
|
||||||
throw new Error('TASKS.md does not contain ## 改动记录');
|
throw new Error('TASKS.md does not contain ## 最近记录 or ## 改动记录');
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertAt = current.indexOf('\n', markerIndex);
|
const insertAt = current.indexOf('\n', markerIndex);
|
||||||
|
|||||||
@ -102,6 +102,17 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
|
|||||||
projects: ['api', 'admin', 'blog'],
|
projects: ['api', 'admin', 'blog'],
|
||||||
steps: ['登录 Admin', '新增分类/标签', '新增文章并绑定分类标签', '编辑后查询列表', '清理 KT_TEST_ 数据'],
|
steps: ['登录 Admin', '新增分类/标签', '新增文章并绑定分类标签', '编辑后查询列表', '清理 KT_TEST_ 数据'],
|
||||||
},
|
},
|
||||||
|
'fflogs-command': {
|
||||||
|
assertions: [
|
||||||
|
'命令注册表和在线命令表都能看到 FFLogs 查询命令。',
|
||||||
|
'中文任务名可查询,回显使用本地化任务名和字段名。',
|
||||||
|
'最近 10 次 logs 返回颜色、输出评分、治疗评分、dps、adps、rdps、ndps、hps。',
|
||||||
|
'FF14 查价和 FFLogs 映射优先来自字典表,不在插件代码硬编码。',
|
||||||
|
],
|
||||||
|
preflight: ['API 可达', 'FFLogs client id/secret 由环境变量注入', '字典表存在 FFLogs/FF14Market 映射项', 'QQBot 测试账号在线'],
|
||||||
|
projects: ['api', 'admin'],
|
||||||
|
steps: ['确认字典映射', '执行 /logs 角色 服务器 中文高难任务', '查询命令日志', '确认失败场景返回可读中文错误'],
|
||||||
|
},
|
||||||
'qqbot-account-scan': {
|
'qqbot-account-scan': {
|
||||||
assertions: [
|
assertions: [
|
||||||
'扫码创建返回 qrcode/containerName/webuiPort。',
|
'扫码创建返回 qrcode/containerName/webuiPort。',
|
||||||
@ -112,6 +123,17 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
|
|||||||
projects: ['api', 'admin'],
|
projects: ['api', 'admin'],
|
||||||
steps: ['打开账号连接页', '点击扫码新增账号', '轮询二维码状态', '确认账号回填', '测试删除联动容器'],
|
steps: ['打开账号连接页', '点击扫码新增账号', '轮询二维码状态', '确认账号回填', '测试删除联动容器'],
|
||||||
},
|
},
|
||||||
|
'qqbot-login-sse': {
|
||||||
|
assertions: [
|
||||||
|
'更新登录接口快速返回 pending session,不被前端 HTTP 超时截断。',
|
||||||
|
'scan/events SSE 能看到重置登录态、生成二维码、等待扫码、成功或错误等步骤。',
|
||||||
|
'账号被其他终端踢下线后,更新登录不复用旧二维码或旧 isLogin 状态。',
|
||||||
|
'手动刷新二维码不超时,且不会无意义重启容器。',
|
||||||
|
],
|
||||||
|
preflight: ['API 可达', 'NapCat 容器可达', '账号处于离线或可重置状态', 'Admin QQBot 账号页面可访问'],
|
||||||
|
projects: ['api', 'admin'],
|
||||||
|
steps: ['触发更新登录', '订阅 scan/events', '观察二维码刷新', '扫码或等待过期', '核对账号状态和容器状态'],
|
||||||
|
},
|
||||||
'qqbot-auto-reply': {
|
'qqbot-auto-reply': {
|
||||||
assertions: [
|
assertions: [
|
||||||
'消息日志能看到 OneBot 收到的私聊/群聊/频道消息。',
|
'消息日志能看到 OneBot 收到的私聊/群聊/频道消息。',
|
||||||
@ -122,6 +144,17 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
|
|||||||
projects: ['api', 'admin'],
|
projects: ['api', 'admin'],
|
||||||
steps: ['创建 KT_TEST_ 自动回复规则', '发送私聊测试消息', '查询消息日志', '查询发送日志', '清理测试规则'],
|
steps: ['创建 KT_TEST_ 自动回复规则', '发送私聊测试消息', '查询消息日志', '查询发送日志', '清理测试规则'],
|
||||||
},
|
},
|
||||||
|
'system-log-visualization': {
|
||||||
|
assertions: [
|
||||||
|
'API pino 日志能写入 Loki 或在 Loki 不可用时不阻塞业务。',
|
||||||
|
'/system/logs、/system/logs/summary、/system/logs/status 返回 Vben 兼容结构。',
|
||||||
|
'Admin 系统日志页面能查询、筛选、刷新,控制台无 error。',
|
||||||
|
'时间字段统一显示为 YYYY-MM-DD HH:mm:ss。',
|
||||||
|
],
|
||||||
|
preflight: ['API 可达', 'Loki /ready 返回 200 或降级状态明确', 'Admin 系统日志菜单可见', 'K8s Secret/Jenkins env 已注入 LOKI_*'],
|
||||||
|
projects: ['api', 'admin'],
|
||||||
|
steps: ['触发一条 API 日志', '查询 Loki query_range', '打开 Admin #/system/logs', '执行筛选刷新', '核对 summary/status'],
|
||||||
|
},
|
||||||
'web-playground-auth': {
|
'web-playground-auth': {
|
||||||
assertions: [
|
assertions: [
|
||||||
'无 token 时不主动 refresh,不产生登录循环。',
|
'无 token 时不主动 refresh,不产生登录循环。',
|
||||||
|
|||||||
@ -95,6 +95,7 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
|
|||||||
}
|
}
|
||||||
|
|
||||||
const review = await buildGlobalCodeReview({
|
const review = await buildGlobalCodeReview({
|
||||||
|
contentScanMode: input.contentScanMode ?? 'changed',
|
||||||
includeContentScan: true,
|
includeContentScan: true,
|
||||||
includeRootScan: input.includeRootScan ?? true,
|
includeRootScan: input.includeRootScan ?? true,
|
||||||
maxFindingsPerProject: input.maxFindingsPerProject ?? 20,
|
maxFindingsPerProject: input.maxFindingsPerProject ?? 20,
|
||||||
@ -116,7 +117,7 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
|
|||||||
review,
|
review,
|
||||||
snapshots,
|
snapshots,
|
||||||
stopConditions: [
|
stopConditions: [
|
||||||
'review.findings 非空时先修复,再复跑 kt_global_code_review。',
|
'review.findings 存在真实 P1/P2 时先修复,再复跑 kt_global_code_review。',
|
||||||
'validationRuns 中任一 result.ok=false 时先处理失败命令。',
|
'validationRuns 中任一 result.ok=false 时先处理失败命令。',
|
||||||
'发现真实 env、生产 Secret 或非本次范围文件时停止提交。',
|
'发现真实 env、生产 Secret 或非本次范围文件时停止提交。',
|
||||||
],
|
],
|
||||||
@ -139,7 +140,7 @@ export async function buildCommitPlan(input: CommitPlanInput = {}): Promise<Reco
|
|||||||
'每个独立 Git 仓库单独提交。',
|
'每个独立 Git 仓库单独提交。',
|
||||||
'提交前先跑对应轻量验证和 kt_global_code_review。',
|
'提交前先跑对应轻量验证和 kt_global_code_review。',
|
||||||
'后端真实 .env.development/.env.production 不提交;前端 env 可提交但只能包含客户端公开配置。',
|
'后端真实 .env.development/.env.production 不提交;前端 env 可提交但只能包含客户端公开配置。',
|
||||||
'根目录 AGENTS/SKILLS/TASKS 不属于子仓库,最终回复里单独说明。',
|
'KT 根目录是本地治理仓库;如没有 remote,只本地提交并在最终回复说明。',
|
||||||
],
|
],
|
||||||
includePush: input.includePush ?? false,
|
includePush: input.includePush ?? false,
|
||||||
projects: changedSnapshots.map((snapshot) => {
|
projects: changedSnapshots.map((snapshot) => {
|
||||||
@ -313,10 +314,28 @@ export function buildComponentWorkflow(input: ComponentWorkflowInput = {}): Reco
|
|||||||
verification: ['横向滚动性能', '纵向滚动表头对齐', '列宽/行高 resize', 'summary 固定底部', '宽屏无无意义横向滚动条'],
|
verification: ['横向滚动性能', '纵向滚动表头对齐', '列宽/行高 resize', 'summary 固定底部', '宽屏无无意义横向滚动条'],
|
||||||
},
|
},
|
||||||
QQBot: {
|
QQBot: {
|
||||||
antiPatterns: ['把 NapCat token 写进仓库', '删除账号不清理专属容器', '白名单/黑名单只做前端过滤', '远程服务不可达时伪装成业务成功'],
|
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'],
|
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 过滤由开关控制', '扫码取消/过期/失败清理未绑定容器'],
|
guardrails: ['OneBot 反向 WS token 校验', '权限名单底层先走 QQ号过滤', '群/频道精确 QQ 过滤由开关控制', '扫码取消/过期/失败清理未绑定容器', '更新登录走 SSE 进度,后台重置登录态后再生成二维码'],
|
||||||
verification: ['账号在线', '扫码新增/更新登录', '自动回复规则命中', '权限名单三类过滤', '消息日志和发送日志闭环'],
|
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 查价中文物品名', '字典管理页面可维护映射'],
|
||||||
|
},
|
||||||
|
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 能看到重置/生成二维码/等待扫码/成功或失败', '手动刷新不超时', '过期二维码不再复用'],
|
||||||
|
},
|
||||||
|
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 内容扫描'],
|
||||||
|
verification: ['API typecheck/lint/test', 'Loki /ready 和 query_range', 'Admin #/system/logs 无 console error', 'global-review findings 不含已知脱敏占位符误报'],
|
||||||
},
|
},
|
||||||
} satisfies Record<string, Record<string, unknown>>;
|
} satisfies Record<string, Record<string, unknown>>;
|
||||||
|
|
||||||
|
|||||||
15
src/types.ts
15
src/types.ts
@ -128,6 +128,7 @@ export interface RiskScanInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalCodeReviewInput {
|
export interface GlobalCodeReviewInput {
|
||||||
|
contentScanMode?: 'all' | 'changed';
|
||||||
includeContentScan?: boolean;
|
includeContentScan?: boolean;
|
||||||
includeRootScan?: boolean;
|
includeRootScan?: boolean;
|
||||||
maxFindingsPerProject?: number;
|
maxFindingsPerProject?: number;
|
||||||
@ -137,6 +138,7 @@ export interface GlobalCodeReviewInput {
|
|||||||
export interface FinishTaskInput {
|
export interface FinishTaskInput {
|
||||||
changeType?: TaskType;
|
changeType?: TaskType;
|
||||||
cleanupHistory?: boolean;
|
cleanupHistory?: boolean;
|
||||||
|
contentScanMode?: 'all' | 'changed';
|
||||||
includeRootScan?: boolean;
|
includeRootScan?: boolean;
|
||||||
keepHistory?: number;
|
keepHistory?: number;
|
||||||
maxFindingsPerProject?: number;
|
maxFindingsPerProject?: number;
|
||||||
@ -159,8 +161,11 @@ export interface PushPlanInput {
|
|||||||
export type BusinessFlow =
|
export type BusinessFlow =
|
||||||
| 'admin-login'
|
| 'admin-login'
|
||||||
| 'blog-crud'
|
| 'blog-crud'
|
||||||
|
| 'fflogs-command'
|
||||||
| 'qqbot-account-scan'
|
| 'qqbot-account-scan'
|
||||||
| 'qqbot-auto-reply'
|
| 'qqbot-auto-reply'
|
||||||
|
| 'qqbot-login-sse'
|
||||||
|
| 'system-log-visualization'
|
||||||
| 'web-playground-auth';
|
| 'web-playground-auth';
|
||||||
|
|
||||||
export interface BusinessTestPlanInput {
|
export interface BusinessTestPlanInput {
|
||||||
@ -184,7 +189,15 @@ export interface DbSyncPlanInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentWorkflowInput {
|
export interface ComponentWorkflowInput {
|
||||||
target?: 'AdminAuth' | 'BlogArgon' | 'Knife4jSwagger' | 'KtTable' | 'QQBot';
|
target?:
|
||||||
|
| 'AdminAuth'
|
||||||
|
| 'BlogArgon'
|
||||||
|
| 'FF14Plugin'
|
||||||
|
| 'Knife4jSwagger'
|
||||||
|
| 'KtTable'
|
||||||
|
| 'NapCatLogin'
|
||||||
|
| 'QQBot'
|
||||||
|
| 'SystemLog';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrepareTaskInput extends GuardrailsInput {
|
export interface PrepareTaskInput extends GuardrailsInput {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user