ktworkflow-mcp/src/tools/envRisk.ts

108 lines
3.7 KiB
TypeScript

import { existsSync } from 'node:fs';
import path from 'node:path';
import type { EnvPolicyInput, RiskScanInput } from '../types.js';
import { tryExecFile } from '../core/exec.js';
import { detectRepoType } from '../core/project.js';
import { readTextIfExists, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js';
export function buildEnvPolicy(input: EnvPolicyInput): Record<string, unknown> {
const project = resolveProject(input.project);
const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => {
const absolutePath = path.join(project.path, file);
return {
exists: existsSync(absolutePath),
file,
relativePath: toPosix(path.relative(workspaceRoot, absolutePath)),
};
});
const gitignore = readTextIfExists(path.join(project.path, '.gitignore'));
const ignoredRules = gitignore
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
return {
envFiles,
ignoredRules: ignoredRules.filter((line) => line.includes('.env')),
policy: [
'真实开发和生产配置只保留在本地,不提交。',
'.env.example 用于维护变量名和示例值。',
'接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。',
],
project,
warnings: envFiles
.filter((file) => file.file !== '.env.example' && file.exists)
.map((file) => `${file.file} 存在,提交前确认已被 .gitignore 忽略。`),
};
}
export async function scanTaskRisk(input: RiskScanInput): Promise<Record<string, unknown>> {
const project = resolveProject(input.project);
let changedFiles = input.changedFiles || [];
if (changedFiles.length === 0 && detectRepoType(project.path) === 'git') {
const status = await tryExecFile('git', ['status', '--short'], project.path);
if (status.ok) {
changedFiles = status.stdout
.split(/\r?\n/)
.map((line) => line.slice(3).trim())
.filter(Boolean);
}
}
const risks: Array<{
file: string;
level: 'high' | 'medium';
reason: string;
suggestion: string;
}> = [];
const addRisk = (
level: 'high' | 'medium',
file: string,
reason: string,
suggestion: string,
) => {
risks.push({ file, level, reason, suggestion });
};
for (const file of changedFiles) {
const normalized = file.replaceAll('\\', '/');
if (/\.env\.(development|production)$/.test(normalized)) {
addRisk('high', file, '真实环境配置文件有改动。', '确认不会提交真实密钥和密码。');
}
if (/package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(normalized)) {
addRisk('medium', file, '依赖或脚本发生变化。', '确认锁文件与 package.json 同步,并运行对应轻量验证。');
}
if (normalized.includes('/components/ktTable/')) {
addRisk(
'medium',
file,
'KtTable 是高频核心组件,布局和滚动性能容易回归。',
'保持原生 antdv-next 表格能力,避免监听 scroll/resize 做高频状态更新。',
);
}
if (/src\/admin\/|src\/.*controller|src\/.*service|src\/.*dto/.test(normalized)) {
addRisk('medium', file, '后端接口或业务逻辑改动。', '本地启动或复用服务后真实调用对应接口。');
}
if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) {
addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。');
}
}
return {
changedFiles,
project,
risks,
safeDefaults: [
'不覆盖用户已有改动。',
'只运行必要验证。',
'提交前重新检查 status 和 diff。',
],
};
}