Update code
This commit is contained in:
140
dev-assistant-mcp/src/tools/autoFix.ts
Normal file
140
dev-assistant-mcp/src/tools/autoFix.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const autoFixTool = {
|
||||
name: "auto_fix",
|
||||
description:
|
||||
"自动修复代码问题。运行 Prettier 格式化、ESLint --fix 自动修复、以及其他自动修复工具。返回修复前后的变更摘要。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
files: {
|
||||
type: "string",
|
||||
description: "指定修复的文件或 glob(可选,默认修复整个项目)",
|
||||
},
|
||||
tools: {
|
||||
type: "string",
|
||||
description: "指定修复工具,逗号分隔(可选):prettier, eslint, autopep8。默认自动检测。",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
};
|
||||
|
||||
async function runFix(cmd: string, cwd: string): Promise<{ success: boolean; output: string }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(cmd, {
|
||||
cwd,
|
||||
timeout: 30000,
|
||||
maxBuffer: 1024 * 1024 * 5,
|
||||
shell: process.platform === "win32" ? "powershell.exe" : "/bin/bash",
|
||||
});
|
||||
return { success: true, output: stdout || stderr || "✅ 修复完成" };
|
||||
} catch (error: any) {
|
||||
return { success: false, output: error.stdout || error.stderr || error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeAutoFix(args: {
|
||||
project_path: string;
|
||||
files?: string;
|
||||
tools?: string;
|
||||
}): Promise<string> {
|
||||
const { project_path, files, tools } = args;
|
||||
const hasFile = (name: string) => existsSync(join(project_path, name));
|
||||
|
||||
const fixResults: { tool: string; success: boolean; output: string }[] = [];
|
||||
|
||||
// 先获取 git diff 作为修复前基线
|
||||
let diffBefore = "";
|
||||
try {
|
||||
const { stdout } = await execAsync("git diff --stat", { cwd: project_path, timeout: 5000 });
|
||||
diffBefore = stdout;
|
||||
} catch {}
|
||||
|
||||
const requestedTools = tools ? tools.split(",").map((t) => t.trim().toLowerCase()) : [];
|
||||
const autoDetect = requestedTools.length === 0;
|
||||
|
||||
// Prettier
|
||||
if (autoDetect ? (hasFile(".prettierrc") || hasFile(".prettierrc.json") || hasFile("prettier.config.js")) : requestedTools.includes("prettier")) {
|
||||
const target = files || ".";
|
||||
const result = await runFix(`npx prettier --write "${target}"`, project_path);
|
||||
fixResults.push({ tool: "Prettier", ...result });
|
||||
}
|
||||
|
||||
// ESLint --fix
|
||||
if (autoDetect ? (hasFile(".eslintrc.js") || hasFile(".eslintrc.json") || hasFile("eslint.config.js") || hasFile("eslint.config.mjs")) : requestedTools.includes("eslint")) {
|
||||
const target = files || "src/";
|
||||
const result = await runFix(`npx eslint "${target}" --fix`, project_path);
|
||||
fixResults.push({ tool: "ESLint --fix", ...result });
|
||||
}
|
||||
|
||||
// Python autopep8
|
||||
if (autoDetect ? (hasFile("requirements.txt") || hasFile("pyproject.toml")) : requestedTools.includes("autopep8")) {
|
||||
const target = files || ".";
|
||||
const result = await runFix(`python -m autopep8 --in-place --recursive "${target}"`, project_path);
|
||||
if (!result.output.includes("No module named")) {
|
||||
fixResults.push({ tool: "autopep8", ...result });
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有检测到工具配置,尝试 package.json 中的 format 脚本
|
||||
if (fixResults.length === 0 && hasFile("package.json")) {
|
||||
const result = await runFix("npm run format 2>&1 || npm run lint:fix 2>&1 || echo NO_FIX_SCRIPT", project_path);
|
||||
if (!result.output.includes("NO_FIX_SCRIPT") && !result.output.includes("Missing script")) {
|
||||
fixResults.push({ tool: "npm script (format/lint:fix)", ...result });
|
||||
}
|
||||
}
|
||||
|
||||
// 获取修复后的 diff
|
||||
let diffAfter = "";
|
||||
try {
|
||||
const { stdout } = await execAsync("git diff --stat", { cwd: project_path, timeout: 5000 });
|
||||
diffAfter = stdout;
|
||||
} catch {}
|
||||
|
||||
// 组装报告
|
||||
const output: string[] = [
|
||||
`# 自动修复报告`,
|
||||
``,
|
||||
`📂 项目: ${project_path}`,
|
||||
``,
|
||||
];
|
||||
|
||||
if (fixResults.length === 0) {
|
||||
output.push(
|
||||
"⚠️ 未检测到格式化/修复工具",
|
||||
"",
|
||||
"建议安装:",
|
||||
"- `npm install -D prettier` — 代码格式化",
|
||||
"- `npm install -D eslint` — 代码质量检查和自动修复",
|
||||
"- `pip install autopep8` — Python 代码格式化",
|
||||
);
|
||||
} else {
|
||||
for (const r of fixResults) {
|
||||
output.push(
|
||||
`## ${r.success ? "✅" : "⚠️"} ${r.tool}`,
|
||||
"```",
|
||||
r.output.slice(0, 2000),
|
||||
"```",
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (diffAfter && diffAfter !== diffBefore) {
|
||||
output.push("## 变更摘要 (git diff --stat)", "```", diffAfter, "```");
|
||||
} else if (fixResults.some((r) => r.success)) {
|
||||
output.push("📝 修复完成,代码已更新");
|
||||
}
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
144
dev-assistant-mcp/src/tools/buildProject.ts
Normal file
144
dev-assistant-mcp/src/tools/buildProject.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const buildProjectTool = {
|
||||
name: "build_project",
|
||||
description:
|
||||
"构建项目。自动检测构建方式(npm run build、tsc、vite build、webpack 等),执行构建并返回结果。如果构建失败,返回错误详情供自动修复。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
command: {
|
||||
type: "string",
|
||||
description: "自定义构建命令(可选,默认自动检测)",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
};
|
||||
|
||||
async function runBuild(cmd: string, cwd: string): Promise<{ stdout: string; stderr: string; code: number; duration: number }> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(cmd, {
|
||||
cwd,
|
||||
timeout: 300000, // 5分钟
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
shell: process.platform === "win32" ? "powershell.exe" : "/bin/bash",
|
||||
});
|
||||
return { stdout, stderr, code: 0, duration: Date.now() - start };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout || "",
|
||||
stderr: error.stderr || "",
|
||||
code: error.code ?? 1,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeBuildProject(args: {
|
||||
project_path: string;
|
||||
command?: string;
|
||||
}): Promise<string> {
|
||||
const { project_path, command } = args;
|
||||
const hasFile = (name: string) => existsSync(join(project_path, name));
|
||||
|
||||
let buildCmd = command || "";
|
||||
let buildTool = "自定义";
|
||||
|
||||
if (!buildCmd) {
|
||||
// 自动检测构建方式
|
||||
if (hasFile("package.json")) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(project_path, "package.json"), "utf-8"));
|
||||
if (pkg.scripts?.build) {
|
||||
buildCmd = "npm run build";
|
||||
buildTool = `npm (${pkg.scripts.build})`;
|
||||
} else if (hasFile("tsconfig.json")) {
|
||||
buildCmd = "npx tsc";
|
||||
buildTool = "TypeScript (tsc)";
|
||||
}
|
||||
} catch {}
|
||||
} else if (hasFile("Makefile")) {
|
||||
buildCmd = "make";
|
||||
buildTool = "Make";
|
||||
} else if (hasFile("setup.py") || hasFile("pyproject.toml")) {
|
||||
buildCmd = "python -m build 2>&1 || python setup.py build 2>&1";
|
||||
buildTool = "Python";
|
||||
}
|
||||
}
|
||||
|
||||
if (!buildCmd) {
|
||||
return `# 构建结果\n\n⚠️ 未检测到构建配置\n项目路径: ${project_path}\n\n建议手动指定 command 参数`;
|
||||
}
|
||||
|
||||
// 检查依赖是否安装
|
||||
if (hasFile("package.json") && !hasFile("node_modules")) {
|
||||
const installResult = await runBuild("npm install", project_path);
|
||||
if (installResult.code !== 0) {
|
||||
return `# 构建失败\n\n❌ 依赖安装失败 (npm install)\n\n\`\`\`\n${installResult.stderr || installResult.stdout}\n\`\`\``;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await runBuild(buildCmd, project_path);
|
||||
const fullOutput = (result.stdout + "\n" + result.stderr).trim();
|
||||
|
||||
// 提取错误信息
|
||||
const errors: string[] = [];
|
||||
const lines = fullOutput.split("\n");
|
||||
for (const line of lines) {
|
||||
if (/error\s+TS\d+/i.test(line) || /Error:/i.test(line) || /ERROR/i.test(line) || /Failed/i.test(line)) {
|
||||
errors.push(line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
const output: string[] = [
|
||||
`# 构建报告`,
|
||||
``,
|
||||
`📂 项目: ${project_path}`,
|
||||
`🔧 工具: ${buildTool}`,
|
||||
`⏱️ 耗时: ${(result.duration / 1000).toFixed(1)}s`,
|
||||
`${result.code === 0 ? "✅ **构建成功**" : "❌ **构建失败**"}`,
|
||||
``,
|
||||
`## 命令`,
|
||||
`\`${buildCmd}\``,
|
||||
``,
|
||||
];
|
||||
|
||||
if (errors.length > 0 && result.code !== 0) {
|
||||
output.push(
|
||||
`## 错误摘要(${errors.length} 项)`,
|
||||
...errors.slice(0, 20).map((e) => `- ${e}`),
|
||||
errors.length > 20 ? `\n... 还有 ${errors.length - 20} 项错误` : "",
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
output.push(
|
||||
`## 完整输出`,
|
||||
"```",
|
||||
fullOutput.slice(0, 5000),
|
||||
"```",
|
||||
);
|
||||
|
||||
if (result.code !== 0) {
|
||||
output.push(
|
||||
``,
|
||||
`## 自动修复建议`,
|
||||
`1. 使用 code_debug 分析上方错误`,
|
||||
`2. 修复代码后重新运行 build_project`,
|
||||
`3. 或使用 lint_check 先检查代码质量`,
|
||||
);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
218
dev-assistant-mcp/src/tools/codeDebug.ts
Normal file
218
dev-assistant-mcp/src/tools/codeDebug.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
export const codeDebugTool = {
|
||||
name: "code_debug",
|
||||
description:
|
||||
"分析代码错误,返回结构化的调试信息:错误分类、常见原因、排查步骤和修复模式。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
description: "有问题的代码",
|
||||
},
|
||||
error_message: {
|
||||
type: "string",
|
||||
description: "错误信息或堆栈跟踪",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "编程语言",
|
||||
},
|
||||
expected_behavior: {
|
||||
type: "string",
|
||||
description: "期望的正确行为(可选)",
|
||||
},
|
||||
},
|
||||
required: ["code", "error_message", "language"],
|
||||
},
|
||||
};
|
||||
|
||||
interface ErrorPattern {
|
||||
pattern: RegExp;
|
||||
category: string;
|
||||
commonCauses: string[];
|
||||
fixStrategies: string[];
|
||||
}
|
||||
|
||||
const errorPatterns: ErrorPattern[] = [
|
||||
{
|
||||
pattern: /TypeError.*(?:undefined|null|is not a function|Cannot read prop)/i,
|
||||
category: "类型错误 (TypeError)",
|
||||
commonCauses: [
|
||||
"访问了 undefined/null 的属性",
|
||||
"函数调用目标不是函数",
|
||||
"异步操作返回值未正确 await",
|
||||
"解构赋值时对象为空",
|
||||
],
|
||||
fixStrategies: [
|
||||
"使用可选链 ?. 和空值合并 ??",
|
||||
"添加前置空值检查",
|
||||
"确认异步操作是否正确 await",
|
||||
"检查 import 路径和导出方式是否匹配",
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: /ReferenceError.*is not defined/i,
|
||||
category: "引用错误 (ReferenceError)",
|
||||
commonCauses: [
|
||||
"变量/函数未声明就使用",
|
||||
"拼写错误",
|
||||
"作用域问题(块级作用域、闭包)",
|
||||
"缺少 import 语句",
|
||||
],
|
||||
fixStrategies: [
|
||||
"检查变量名拼写",
|
||||
"确认 import/require 语句",
|
||||
"检查变量声明的作用域",
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: /SyntaxError/i,
|
||||
category: "语法错误 (SyntaxError)",
|
||||
commonCauses: [
|
||||
"括号/引号不匹配",
|
||||
"缺少分号或逗号",
|
||||
"关键字拼写错误",
|
||||
"ESM/CJS 混用",
|
||||
],
|
||||
fixStrategies: [
|
||||
"检查报错行及前几行的括号/引号匹配",
|
||||
"确认模块系统一致性(import vs require)",
|
||||
"使用格式化工具自动修复",
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: /ECONNREFUSED|ETIMEDOUT|ENOTFOUND|fetch failed|network/i,
|
||||
category: "网络错误",
|
||||
commonCauses: [
|
||||
"目标服务未启动",
|
||||
"URL/端口配置错误",
|
||||
"DNS 解析失败",
|
||||
"防火墙/代理阻断",
|
||||
"SSL 证书问题",
|
||||
],
|
||||
fixStrategies: [
|
||||
"确认目标服务运行状态和端口",
|
||||
"检查 URL 配置(协议、域名、端口)",
|
||||
"添加重试机制和超时控制",
|
||||
"检查网络连通性",
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: /ENOENT|no such file|Module not found|Cannot find module/i,
|
||||
category: "文件/模块未找到",
|
||||
commonCauses: [
|
||||
"文件路径错误",
|
||||
"依赖未安装(缺少 npm install)",
|
||||
"相对路径 vs 绝对路径搞混",
|
||||
"文件扩展名不匹配",
|
||||
],
|
||||
fixStrategies: [
|
||||
"检查文件路径和大小写",
|
||||
"运行 npm install 安装依赖",
|
||||
"检查 tsconfig/webpack 的路径别名配置",
|
||||
"确认文件扩展名(.js/.ts/.mjs)",
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: /Permission denied|EACCES|403|401|Unauthorized/i,
|
||||
category: "权限错误",
|
||||
commonCauses: [
|
||||
"文件权限不足",
|
||||
"API 认证失败(Token 过期/错误)",
|
||||
"CORS 跨域限制",
|
||||
],
|
||||
fixStrategies: [
|
||||
"检查文件/目录权限(chmod)",
|
||||
"检查 API Key / Token 是否有效",
|
||||
"配置正确的 CORS 头",
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: /out of memory|heap|stack overflow|Maximum call stack/i,
|
||||
category: "内存/栈溢出",
|
||||
commonCauses: [
|
||||
"无限递归",
|
||||
"处理超大数据集未分页",
|
||||
"内存泄漏(未清理的引用)",
|
||||
],
|
||||
fixStrategies: [
|
||||
"检查递归终止条件",
|
||||
"对大数据使用流式处理或分页",
|
||||
"使用 --max-old-space-size 调整堆内存",
|
||||
"排查事件监听器和定时器泄漏",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export async function executeCodeDebug(args: {
|
||||
code: string;
|
||||
error_message: string;
|
||||
language: string;
|
||||
expected_behavior?: string;
|
||||
}): Promise<string> {
|
||||
const { code, error_message, language, expected_behavior } = args;
|
||||
|
||||
// 匹配错误模式
|
||||
const matched = errorPatterns.filter((ep) => ep.pattern.test(error_message));
|
||||
const categories = matched.length > 0 ? matched : [{
|
||||
category: "未分类错误",
|
||||
commonCauses: ["请仔细阅读完整错误信息和堆栈"],
|
||||
fixStrategies: ["根据错误信息定位问题代码行", "添加日志逐步排查"],
|
||||
}];
|
||||
|
||||
// 从堆栈中提取行号
|
||||
const lineRefs: string[] = [];
|
||||
const lineRegex = /(?:at\s+.+?|File\s+.+?):(\d+)(?::(\d+))?/g;
|
||||
let match;
|
||||
while ((match = lineRegex.exec(error_message)) !== null) {
|
||||
lineRefs.push(`行 ${match[1]}${match[2] ? `:${match[2]}` : ""}`);
|
||||
}
|
||||
|
||||
const output: string[] = [
|
||||
`# 调试分析报告`,
|
||||
``,
|
||||
`**语言**: ${language}`,
|
||||
``,
|
||||
`## 错误信息`,
|
||||
"```",
|
||||
error_message,
|
||||
"```",
|
||||
``,
|
||||
];
|
||||
|
||||
if (lineRefs.length > 0) {
|
||||
output.push(`## 堆栈中的关键位置`, ...lineRefs.map((l) => `- ${l}`), ``);
|
||||
}
|
||||
|
||||
for (const cat of categories) {
|
||||
output.push(
|
||||
`## 错误类型: ${cat.category}`,
|
||||
``,
|
||||
`### 常见原因`,
|
||||
...cat.commonCauses.map((c) => `- ${c}`),
|
||||
``,
|
||||
`### 修复策略`,
|
||||
...cat.fixStrategies.map((s) => `- ${s}`),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (expected_behavior) {
|
||||
output.push(`## 期望行为`, expected_behavior, ``);
|
||||
}
|
||||
|
||||
output.push(
|
||||
`## 问题代码`,
|
||||
"```" + language,
|
||||
code,
|
||||
"```",
|
||||
``,
|
||||
`## 排查步骤`,
|
||||
`1. 根据以上错误分类和常见原因,定位问题代码行`,
|
||||
`2. 在可疑位置添加日志输出,验证变量值`,
|
||||
`3. 根据修复策略实施修改`,
|
||||
`4. 编写测试用例确认修复有效`,
|
||||
);
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
163
dev-assistant-mcp/src/tools/codeReview.ts
Normal file
163
dev-assistant-mcp/src/tools/codeReview.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
export const codeReviewTool = {
|
||||
name: "code_review",
|
||||
description:
|
||||
"审查代码质量。根据审查重点返回结构化的检查清单和分析结果,涵盖 bug、安全、性能、风格等维度。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
description: "要审查的代码",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "编程语言(如 typescript, python, java 等)",
|
||||
},
|
||||
focus: {
|
||||
type: "string",
|
||||
description:
|
||||
"审查重点(可选):security=安全性, performance=性能, style=代码风格, all=全面审查",
|
||||
enum: ["security", "performance", "style", "all"],
|
||||
},
|
||||
},
|
||||
required: ["code", "language"],
|
||||
},
|
||||
};
|
||||
|
||||
const reviewChecklists: Record<string, string[]> = {
|
||||
security: [
|
||||
"检查是否存在 SQL 注入、XSS、CSRF 等注入攻击风险",
|
||||
"检查是否硬编码了密钥、密码、Token 等敏感信息",
|
||||
"检查用户输入是否经过验证和清洗",
|
||||
"检查权限控制是否完善,是否有越权风险",
|
||||
"检查是否使用了不安全的加密算法或随机数生成",
|
||||
"检查第三方依赖是否有已知安全漏洞",
|
||||
],
|
||||
performance: [
|
||||
"检查是否存在不必要的循环嵌套或 O(n²) 以上复杂度",
|
||||
"检查是否有内存泄漏(未清理的定时器、事件监听、闭包)",
|
||||
"检查是否有不必要的重复计算,是否需要缓存/记忆化",
|
||||
"检查异步操作是否可以并行化(Promise.all)",
|
||||
"检查是否有阻塞主线程的同步操作",
|
||||
"检查数据库查询是否有 N+1 问题",
|
||||
],
|
||||
style: [
|
||||
"检查命名是否清晰表达意图(变量、函数、类)",
|
||||
"检查函数长度是否超过 50 行,是否需要拆分",
|
||||
"检查嵌套层级是否过深(>3 层),是否可用 guard clause",
|
||||
"检查是否有重复代码(DRY 原则)",
|
||||
"检查注释是否准确且必要,而非冗余",
|
||||
"检查是否遵循语言社区的标准代码风格",
|
||||
],
|
||||
};
|
||||
|
||||
export async function executeCodeReview(args: {
|
||||
code: string;
|
||||
language: string;
|
||||
focus?: string;
|
||||
}): Promise<string> {
|
||||
const { code, language, focus = "all" } = args;
|
||||
|
||||
const lines = code.split("\n");
|
||||
const lineCount = lines.length;
|
||||
|
||||
// 基础代码统计
|
||||
const stats = {
|
||||
totalLines: lineCount,
|
||||
blankLines: lines.filter((l) => l.trim() === "").length,
|
||||
commentLines: lines.filter((l) => {
|
||||
const t = l.trim();
|
||||
return t.startsWith("//") || t.startsWith("#") || t.startsWith("/*") || t.startsWith("*");
|
||||
}).length,
|
||||
codeLines: 0,
|
||||
maxLineLength: Math.max(...lines.map((l) => l.length)),
|
||||
maxNestingDepth: 0,
|
||||
};
|
||||
stats.codeLines = stats.totalLines - stats.blankLines - stats.commentLines;
|
||||
|
||||
// 计算最大嵌套深度
|
||||
let depth = 0;
|
||||
for (const line of lines) {
|
||||
const opens = (line.match(/{/g) || []).length;
|
||||
const closes = (line.match(/}/g) || []).length;
|
||||
depth += opens - closes;
|
||||
if (depth > stats.maxNestingDepth) stats.maxNestingDepth = depth;
|
||||
}
|
||||
|
||||
// 检测潜在问题
|
||||
const issues: string[] = [];
|
||||
|
||||
// 通用检查
|
||||
if (stats.maxLineLength > 120) issues.push(`⚠️ 存在超长行(最长 ${stats.maxLineLength} 字符),建议不超过 120`);
|
||||
if (stats.maxNestingDepth > 4) issues.push(`⚠️ 嵌套层级过深(${stats.maxNestingDepth} 层),建议重构`);
|
||||
if (stats.codeLines > 300) issues.push(`⚠️ 文件过长(${stats.codeLines} 行代码),建议拆分`);
|
||||
if (stats.commentLines === 0 && stats.codeLines > 30) issues.push("🔵 缺少注释,建议为关键逻辑添加说明");
|
||||
|
||||
// 安全检查
|
||||
if (focus === "security" || focus === "all") {
|
||||
if (/eval\s*\(/.test(code)) issues.push("🔴 使用了 eval(),存在代码注入风险");
|
||||
if (/innerHTML\s*=/.test(code)) issues.push("🔴 使用了 innerHTML,存在 XSS 风险");
|
||||
if (/(password|secret|api_?key|token)\s*=\s*["'][^"']+["']/i.test(code))
|
||||
issues.push("🔴 可能硬编码了敏感信息");
|
||||
if (/\bhttp:\/\//.test(code)) issues.push("🟡 使用了 HTTP 而非 HTTPS");
|
||||
if (/exec\s*\(|spawn\s*\(/.test(code) && !/sanitize|escape|validate/.test(code))
|
||||
issues.push("🟡 执行了外部命令,确认输入已清洗");
|
||||
}
|
||||
|
||||
// 性能检查
|
||||
if (focus === "performance" || focus === "all") {
|
||||
if (/for\s*\(.*for\s*\(/s.test(code)) issues.push("🟡 存在嵌套循环,注意时间复杂度");
|
||||
if (/setTimeout|setInterval/.test(code) && !/clearTimeout|clearInterval/.test(code))
|
||||
issues.push("<22> 设置了定时器但未见清理,可能内存泄漏");
|
||||
if (/\.forEach\(.*await/.test(code)) issues.push("🟡 在 forEach 中使用 await,不会等待完成,建议用 for...of");
|
||||
if (/new RegExp\(/.test(code) && /for|while|map|forEach/.test(code))
|
||||
issues.push("🔵 循环中创建正则表达式,建议提取到循环外");
|
||||
}
|
||||
|
||||
// 风格检查
|
||||
if (focus === "style" || focus === "all") {
|
||||
if (/var\s+/.test(code) && (language === "typescript" || language === "javascript"))
|
||||
issues.push("🔵 使用了 var,建议改用 const/let");
|
||||
if (/console\.log/.test(code)) issues.push("🔵 包含 console.log,确认是否需要在生产环境移除");
|
||||
if (/any/.test(code) && language === "typescript") issues.push("🔵 使用了 any 类型,建议使用具体类型");
|
||||
if (/TODO|FIXME|HACK|XXX/.test(code)) issues.push("🔵 存在 TODO/FIXME 标记,建议处理");
|
||||
}
|
||||
|
||||
// 获取对应的检查清单
|
||||
let checklist: string[];
|
||||
if (focus === "all") {
|
||||
checklist = [
|
||||
...reviewChecklists.security,
|
||||
...reviewChecklists.performance,
|
||||
...reviewChecklists.style,
|
||||
];
|
||||
} else {
|
||||
checklist = reviewChecklists[focus] || reviewChecklists.style;
|
||||
}
|
||||
|
||||
// 组装结果
|
||||
const output = [
|
||||
`# 代码审查报告`,
|
||||
``,
|
||||
`**语言**: ${language} | **审查重点**: ${focus}`,
|
||||
``,
|
||||
`## 代码统计`,
|
||||
`- 总行数: ${stats.totalLines}(代码 ${stats.codeLines} / 注释 ${stats.commentLines} / 空行 ${stats.blankLines})`,
|
||||
`- 最长行: ${stats.maxLineLength} 字符`,
|
||||
`- 最大嵌套深度: ${stats.maxNestingDepth} 层`,
|
||||
``,
|
||||
`## 自动检测到的问题(${issues.length} 项)`,
|
||||
issues.length > 0 ? issues.map((i) => `- ${i}`).join("\n") : "- ✅ 未检测到明显问题",
|
||||
``,
|
||||
`## 人工审查清单`,
|
||||
`请逐项检查以下内容:`,
|
||||
...checklist.map((item, idx) => `${idx + 1}. ${item}`),
|
||||
``,
|
||||
`## 待审查代码`,
|
||||
"```" + language,
|
||||
code,
|
||||
"```",
|
||||
];
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
200
dev-assistant-mcp/src/tools/codeWrite.ts
Normal file
200
dev-assistant-mcp/src/tools/codeWrite.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
export const codeWriteTool = {
|
||||
name: "code_write",
|
||||
description:
|
||||
"根据需求描述生成代码编写指引。返回结构化的需求分析、技术方案、代码骨架和最佳实践建议。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
requirement: {
|
||||
type: "string",
|
||||
description: "功能需求描述",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "编程语言(如 typescript, python, java 等)",
|
||||
},
|
||||
framework: {
|
||||
type: "string",
|
||||
description: "框架(可选,如 react, express, fastapi 等)",
|
||||
},
|
||||
context: {
|
||||
type: "string",
|
||||
description: "额外上下文(可选,如现有代码片段、接口定义等)",
|
||||
},
|
||||
},
|
||||
required: ["requirement", "language"],
|
||||
},
|
||||
};
|
||||
|
||||
const languageBestPractices: Record<string, string[]> = {
|
||||
typescript: [
|
||||
"使用严格类型,避免 any",
|
||||
"用 interface/type 定义数据结构",
|
||||
"async/await 处理异步",
|
||||
"使用 const 优先,必要时 let",
|
||||
"错误处理使用 try-catch + 自定义 Error 类",
|
||||
],
|
||||
javascript: [
|
||||
"使用 const/let,禁用 var",
|
||||
"使用 JSDoc 注释函数签名",
|
||||
"async/await 处理异步",
|
||||
"使用解构赋值简化代码",
|
||||
"模块化:每个文件单一职责",
|
||||
],
|
||||
python: [
|
||||
"遵循 PEP 8 风格规范",
|
||||
"使用 type hints 标注类型",
|
||||
"使用 dataclass/Pydantic 定义数据模型",
|
||||
"用 with 语句管理资源",
|
||||
"异常处理:捕获具体异常而非 Exception",
|
||||
],
|
||||
java: [
|
||||
"遵循 Java 命名约定",
|
||||
"使用 Optional 避免 NullPointerException",
|
||||
"用 Stream API 处理集合",
|
||||
"接口优先于实现",
|
||||
"使用 Lombok 减少样板代码",
|
||||
],
|
||||
};
|
||||
|
||||
const frameworkTemplates: Record<string, string> = {
|
||||
react: `// React 组件骨架
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
// TODO: 定义 props
|
||||
}
|
||||
|
||||
export function ComponentName({ }: Props) {
|
||||
const [state, setState] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: 副作用逻辑
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* TODO: JSX 结构 */}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
express: `// Express 路由骨架
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
// TODO: 业务逻辑
|
||||
res.json({ success: true, data: {} });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;`,
|
||||
fastapi: `# FastAPI 路由骨架
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class RequestModel(BaseModel):
|
||||
# TODO: 定义请求模型
|
||||
pass
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
success: bool
|
||||
data: dict = {}
|
||||
|
||||
@router.get("/", response_model=ResponseModel)
|
||||
async def handler():
|
||||
try:
|
||||
# TODO: 业务逻辑
|
||||
return ResponseModel(success=True, data={})
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))`,
|
||||
vue: `<!-- Vue 组件骨架 -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- TODO: 模板结构 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
// TODO: 定义响应式状态
|
||||
const state = ref();
|
||||
|
||||
onMounted(() => {
|
||||
// TODO: 初始化逻辑
|
||||
});
|
||||
</script>`,
|
||||
};
|
||||
|
||||
export async function executeCodeWrite(args: {
|
||||
requirement: string;
|
||||
language: string;
|
||||
framework?: string;
|
||||
context?: string;
|
||||
}): Promise<string> {
|
||||
const { requirement, language, framework, context } = args;
|
||||
|
||||
const practices = languageBestPractices[language.toLowerCase()] || [
|
||||
"遵循语言社区标准",
|
||||
"添加类型标注",
|
||||
"做好错误处理",
|
||||
"保持函数单一职责",
|
||||
];
|
||||
|
||||
const output: string[] = [
|
||||
`# 代码编写指引`,
|
||||
``,
|
||||
`## 需求`,
|
||||
requirement,
|
||||
``,
|
||||
`## 技术规格`,
|
||||
`- **语言**: ${language}`,
|
||||
];
|
||||
|
||||
if (framework) {
|
||||
output.push(`- **框架**: ${framework}`);
|
||||
}
|
||||
|
||||
output.push(
|
||||
``,
|
||||
`## ${language} 最佳实践`,
|
||||
...practices.map((p) => `- ${p}`),
|
||||
``
|
||||
);
|
||||
|
||||
if (framework && frameworkTemplates[framework.toLowerCase()]) {
|
||||
output.push(
|
||||
`## 代码骨架模板`,
|
||||
"```" + language,
|
||||
frameworkTemplates[framework.toLowerCase()],
|
||||
"```",
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
output.push(
|
||||
`## 编写要求`,
|
||||
`1. 代码必须完整可运行,包含所有必要的 import`,
|
||||
`2. 遵循 ${language} 最佳实践和命名规范`,
|
||||
`3. 关键逻辑添加简洁注释`,
|
||||
`4. 包含必要的错误处理和边界检查`,
|
||||
`5. 函数保持单一职责,不超过 50 行`,
|
||||
``
|
||||
);
|
||||
|
||||
if (context) {
|
||||
output.push(`## 参考上下文`, "```", context, "```", ``);
|
||||
}
|
||||
|
||||
output.push(
|
||||
`## 请根据以上指引生成完整代码`
|
||||
);
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
183
dev-assistant-mcp/src/tools/depManage.ts
Normal file
183
dev-assistant-mcp/src/tools/depManage.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const depManageTool = {
|
||||
name: "dep_manage",
|
||||
description:
|
||||
"依赖管理。安装/更新/删除依赖、检查过时包、安全漏洞审计、分析 bundle 大小。支持 npm 和 pip。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
action: {
|
||||
type: "string",
|
||||
description: "操作类型",
|
||||
enum: ["install", "add", "remove", "update", "outdated", "audit", "list", "why"],
|
||||
},
|
||||
packages: {
|
||||
type: "string",
|
||||
description: "包名(多个用空格分隔),add/remove/why 时必填",
|
||||
},
|
||||
dev: {
|
||||
type: "boolean",
|
||||
description: "是否为开发依赖(默认 false)",
|
||||
},
|
||||
},
|
||||
required: ["project_path", "action"],
|
||||
},
|
||||
};
|
||||
|
||||
async function run(cmd: string, cwd: string, timeout = 120000): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(cmd, {
|
||||
cwd, timeout,
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
shell: process.platform === "win32" ? "powershell.exe" : "/bin/bash",
|
||||
});
|
||||
return { stdout, stderr, code: 0 };
|
||||
} catch (error: any) {
|
||||
return { stdout: error.stdout || "", stderr: error.stderr || "", code: error.code ?? 1 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDepManage(args: {
|
||||
project_path: string;
|
||||
action: string;
|
||||
packages?: string;
|
||||
dev?: boolean;
|
||||
}): Promise<string> {
|
||||
const { project_path, action, packages, dev = false } = args;
|
||||
const hasFile = (name: string) => existsSync(join(project_path, name));
|
||||
const isNode = hasFile("package.json");
|
||||
const isPython = hasFile("requirements.txt") || hasFile("pyproject.toml");
|
||||
|
||||
if (!isNode && !isPython) {
|
||||
return "❌ 未检测到 package.json 或 requirements.txt";
|
||||
}
|
||||
|
||||
let cmd = "";
|
||||
let title = "";
|
||||
|
||||
if (isNode) {
|
||||
switch (action) {
|
||||
case "install":
|
||||
cmd = "npm install";
|
||||
title = "npm install";
|
||||
break;
|
||||
case "add":
|
||||
if (!packages) return "❌ add 需要 packages 参数";
|
||||
cmd = `npm install ${packages}${dev ? " --save-dev" : ""}`;
|
||||
title = `npm install ${packages}${dev ? " (dev)" : ""}`;
|
||||
break;
|
||||
case "remove":
|
||||
if (!packages) return "❌ remove 需要 packages 参数";
|
||||
cmd = `npm uninstall ${packages}`;
|
||||
title = `npm uninstall ${packages}`;
|
||||
break;
|
||||
case "update":
|
||||
cmd = packages ? `npm update ${packages}` : "npm update";
|
||||
title = `npm update${packages ? ` ${packages}` : ""}`;
|
||||
break;
|
||||
case "outdated":
|
||||
cmd = "npm outdated --long 2>&1 || true";
|
||||
title = "npm outdated(过时依赖检查)";
|
||||
break;
|
||||
case "audit":
|
||||
cmd = "npm audit 2>&1 || true";
|
||||
title = "npm audit(安全漏洞审计)";
|
||||
break;
|
||||
case "list":
|
||||
cmd = "npm list --depth=0 2>&1";
|
||||
title = "npm list(已安装依赖)";
|
||||
break;
|
||||
case "why":
|
||||
if (!packages) return "❌ why 需要 packages 参数";
|
||||
cmd = `npm why ${packages} 2>&1`;
|
||||
title = `npm why ${packages}`;
|
||||
break;
|
||||
}
|
||||
} else if (isPython) {
|
||||
switch (action) {
|
||||
case "install":
|
||||
cmd = hasFile("requirements.txt") ? "pip install -r requirements.txt" : "pip install -e .";
|
||||
title = "pip install";
|
||||
break;
|
||||
case "add":
|
||||
if (!packages) return "❌ add 需要 packages 参数";
|
||||
cmd = `pip install ${packages}`;
|
||||
title = `pip install ${packages}`;
|
||||
break;
|
||||
case "remove":
|
||||
if (!packages) return "❌ remove 需要 packages 参数";
|
||||
cmd = `pip uninstall -y ${packages}`;
|
||||
title = `pip uninstall ${packages}`;
|
||||
break;
|
||||
case "update":
|
||||
cmd = packages ? `pip install --upgrade ${packages}` : "pip install --upgrade -r requirements.txt";
|
||||
title = `pip upgrade${packages ? ` ${packages}` : ""}`;
|
||||
break;
|
||||
case "outdated":
|
||||
cmd = "pip list --outdated 2>&1";
|
||||
title = "pip outdated";
|
||||
break;
|
||||
case "audit":
|
||||
cmd = "pip-audit 2>&1 || pip check 2>&1";
|
||||
title = "pip audit / check";
|
||||
break;
|
||||
case "list":
|
||||
cmd = "pip list 2>&1";
|
||||
title = "pip list";
|
||||
break;
|
||||
case "why":
|
||||
if (!packages) return "❌ why 需要 packages 参数";
|
||||
cmd = `pip show ${packages} 2>&1`;
|
||||
title = `pip show ${packages}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cmd) return `❌ 未知操作: ${action}`;
|
||||
|
||||
const result = await run(cmd, project_path);
|
||||
const fullOutput = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
const icon = result.code === 0 ? "✅" : "⚠️";
|
||||
|
||||
const output: string[] = [
|
||||
`# ${icon} ${title}`,
|
||||
``,
|
||||
`📂 ${project_path}`,
|
||||
`📦 ${isNode ? "npm" : "pip"}`,
|
||||
``,
|
||||
"```",
|
||||
fullOutput.slice(0, 6000) || "(无输出)",
|
||||
"```",
|
||||
];
|
||||
|
||||
// audit 额外解析
|
||||
if (action === "audit" && isNode) {
|
||||
const criticalMatch = fullOutput.match(/(\d+)\s+(critical|high)/gi);
|
||||
if (criticalMatch && criticalMatch.length > 0) {
|
||||
output.push(``, `⚠️ **发现高危漏洞!** 建议运行 \`npm audit fix\` 或 \`npm audit fix --force\``);
|
||||
} else if (result.code === 0) {
|
||||
output.push(``, `✅ 未发现已知安全漏洞`);
|
||||
}
|
||||
}
|
||||
|
||||
// outdated 额外解析
|
||||
if (action === "outdated" && fullOutput.trim()) {
|
||||
const lines = fullOutput.trim().split("\n").filter((l) => l.trim());
|
||||
if (lines.length > 1) {
|
||||
output.push(``, `📊 发现 ${lines.length - 1} 个可更新的包`);
|
||||
output.push(`💡 运行 \`dep_manage action=update\` 更新所有包`);
|
||||
}
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
271
dev-assistant-mcp/src/tools/devServer.ts
Normal file
271
dev-assistant-mcp/src/tools/devServer.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { exec, spawn, ChildProcess } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// 进程管理器 - 跟踪所有启动的开发服务器
|
||||
const managedProcesses: Map<string, {
|
||||
process: ChildProcess;
|
||||
command: string;
|
||||
cwd: string;
|
||||
startTime: number;
|
||||
logs: string[];
|
||||
}> = new Map();
|
||||
|
||||
export const devServerTool = {
|
||||
name: "dev_server",
|
||||
description:
|
||||
"开发服务器管理。启动/停止/重启开发服务器,查看运行状态和实时日志。支持自动检测项目的 dev 命令。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
action: {
|
||||
type: "string",
|
||||
description: "操作类型",
|
||||
enum: ["start", "stop", "restart", "status", "logs", "list"],
|
||||
},
|
||||
command: {
|
||||
type: "string",
|
||||
description: "自定义启动命令(可选,默认自动检测 npm run dev 等)",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
description: "服务器名称/标识(可选,默认使用目录名)",
|
||||
},
|
||||
tail: {
|
||||
type: "number",
|
||||
description: "显示最近几行日志(默认 30)",
|
||||
},
|
||||
},
|
||||
required: ["action"],
|
||||
},
|
||||
};
|
||||
|
||||
function getServerName(projectPath?: string, name?: string): string {
|
||||
if (name) return name;
|
||||
if (projectPath) return projectPath.split(/[/\\]/).pop() || "server";
|
||||
return "default";
|
||||
}
|
||||
|
||||
function detectDevCommand(projectPath: string): string | null {
|
||||
const pkgPath = join(projectPath, "package.json");
|
||||
if (existsSync(pkgPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
const scripts = pkg.scripts || {};
|
||||
// 按优先级检测
|
||||
for (const name of ["dev", "start:dev", "serve", "start"]) {
|
||||
if (scripts[name]) return `npm run ${name}`;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (existsSync(join(projectPath, "manage.py"))) {
|
||||
return "python manage.py runserver";
|
||||
}
|
||||
if (existsSync(join(projectPath, "main.py"))) {
|
||||
return "python main.py";
|
||||
}
|
||||
if (existsSync(join(projectPath, "app.py"))) {
|
||||
return "python app.py";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function executeDevServer(args: {
|
||||
project_path?: string;
|
||||
action: string;
|
||||
command?: string;
|
||||
name?: string;
|
||||
tail?: number;
|
||||
}): Promise<string> {
|
||||
const { project_path, action, command, name, tail = 30 } = args;
|
||||
|
||||
switch (action) {
|
||||
case "list": {
|
||||
if (managedProcesses.size === 0) {
|
||||
return "# 开发服务器\n\n_没有正在运行的服务器_";
|
||||
}
|
||||
|
||||
const output: string[] = ["# 运行中的开发服务器", ""];
|
||||
for (const [key, info] of managedProcesses) {
|
||||
const running = !info.process.killed && info.process.exitCode === null;
|
||||
const uptime = Math.round((Date.now() - info.startTime) / 1000);
|
||||
output.push(
|
||||
`## ${running ? "🟢" : "🔴"} ${key}`,
|
||||
`- 命令: \`${info.command}\``,
|
||||
`- 目录: ${info.cwd}`,
|
||||
`- PID: ${info.process.pid}`,
|
||||
`- 运行时间: ${uptime}s`,
|
||||
`- 状态: ${running ? "运行中" : "已停止"}`,
|
||||
``
|
||||
);
|
||||
}
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
case "start": {
|
||||
if (!project_path) return "❌ start 需要 project_path 参数";
|
||||
|
||||
const serverName = getServerName(project_path, name);
|
||||
const existing = managedProcesses.get(serverName);
|
||||
if (existing && !existing.process.killed && existing.process.exitCode === null) {
|
||||
return `⚠️ 服务器 "${serverName}" 已在运行中 (PID: ${existing.process.pid})\n\n使用 action=restart 重启,或 action=stop 先停止`;
|
||||
}
|
||||
|
||||
const startCmd = command || detectDevCommand(project_path);
|
||||
if (!startCmd) {
|
||||
return `❌ 未检测到启动命令,请手动指定 command 参数\n\n常见命令:\n- npm run dev\n- npm start\n- python app.py`;
|
||||
}
|
||||
|
||||
// 解析命令
|
||||
const isWin = process.platform === "win32";
|
||||
const child = spawn(isWin ? "cmd" : "sh", [isWin ? "/c" : "-c", startCmd], {
|
||||
cwd: project_path,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: false,
|
||||
});
|
||||
|
||||
const logs: string[] = [];
|
||||
const maxLogs = 200;
|
||||
|
||||
const addLog = (data: Buffer, stream: string) => {
|
||||
const lines = data.toString().split("\n").filter(Boolean);
|
||||
for (const line of lines) {
|
||||
logs.push(`[${stream}] ${line}`);
|
||||
if (logs.length > maxLogs) logs.shift();
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout?.on("data", (data) => addLog(data, "out"));
|
||||
child.stderr?.on("data", (data) => addLog(data, "err"));
|
||||
|
||||
managedProcesses.set(serverName, {
|
||||
process: child,
|
||||
command: startCmd,
|
||||
cwd: project_path,
|
||||
startTime: Date.now(),
|
||||
logs,
|
||||
});
|
||||
|
||||
// 等待一会检查是否立即崩溃
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const crashed = child.exitCode !== null;
|
||||
if (crashed) {
|
||||
const output = logs.join("\n");
|
||||
managedProcesses.delete(serverName);
|
||||
return `# ❌ 服务器启动失败\n\n命令: \`${startCmd}\`\n退出码: ${child.exitCode}\n\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\``;
|
||||
}
|
||||
|
||||
return [
|
||||
`# ✅ 服务器已启动`,
|
||||
``,
|
||||
`- 名称: ${serverName}`,
|
||||
`- 命令: \`${startCmd}\``,
|
||||
`- PID: ${child.pid}`,
|
||||
`- 目录: ${project_path}`,
|
||||
``,
|
||||
`最近日志:`,
|
||||
"```",
|
||||
logs.slice(-10).join("\n") || "(等待输出...)",
|
||||
"```",
|
||||
``,
|
||||
`💡 使用 \`dev_server action=logs\` 查看实时日志`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
case "stop": {
|
||||
const serverName = getServerName(project_path, name);
|
||||
const info = managedProcesses.get(serverName);
|
||||
if (!info) {
|
||||
return `❌ 未找到服务器 "${serverName}"\n\n使用 action=list 查看所有运行中的服务器`;
|
||||
}
|
||||
|
||||
const pid = info.process.pid;
|
||||
try {
|
||||
// Windows 需要 taskkill 杀进程树
|
||||
if (process.platform === "win32" && pid) {
|
||||
await execAsync(`taskkill /PID ${pid} /T /F`).catch(() => {});
|
||||
} else {
|
||||
info.process.kill("SIGTERM");
|
||||
}
|
||||
} catch {}
|
||||
|
||||
managedProcesses.delete(serverName);
|
||||
return `# ✅ 服务器已停止\n\n- 名称: ${serverName}\n- PID: ${pid}`;
|
||||
}
|
||||
|
||||
case "restart": {
|
||||
const serverName = getServerName(project_path, name);
|
||||
const info = managedProcesses.get(serverName);
|
||||
if (info) {
|
||||
try {
|
||||
if (process.platform === "win32" && info.process.pid) {
|
||||
await execAsync(`taskkill /PID ${info.process.pid} /T /F`).catch(() => {});
|
||||
} else {
|
||||
info.process.kill("SIGTERM");
|
||||
}
|
||||
} catch {}
|
||||
managedProcesses.delete(serverName);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
|
||||
// 重新启动
|
||||
return executeDevServer({
|
||||
project_path: info?.cwd || project_path,
|
||||
action: "start",
|
||||
command: command || info?.command,
|
||||
name: serverName,
|
||||
});
|
||||
}
|
||||
|
||||
case "status": {
|
||||
const serverName = getServerName(project_path, name);
|
||||
const info = managedProcesses.get(serverName);
|
||||
if (!info) {
|
||||
return `❌ 未找到服务器 "${serverName}"`;
|
||||
}
|
||||
|
||||
const running = !info.process.killed && info.process.exitCode === null;
|
||||
const uptime = Math.round((Date.now() - info.startTime) / 1000);
|
||||
|
||||
return [
|
||||
`# ${running ? "🟢" : "🔴"} ${serverName}`,
|
||||
``,
|
||||
`- 状态: ${running ? "运行中" : `已停止 (退出码: ${info.process.exitCode})`}`,
|
||||
`- 命令: \`${info.command}\``,
|
||||
`- PID: ${info.process.pid}`,
|
||||
`- 运行时间: ${uptime}s`,
|
||||
`- 目录: ${info.cwd}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
case "logs": {
|
||||
const serverName = getServerName(project_path, name);
|
||||
const info = managedProcesses.get(serverName);
|
||||
if (!info) {
|
||||
return `❌ 未找到服务器 "${serverName}"`;
|
||||
}
|
||||
|
||||
const recentLogs = info.logs.slice(-tail);
|
||||
return [
|
||||
`# 📋 ${serverName} 日志(最近 ${tail} 行)`,
|
||||
``,
|
||||
"```",
|
||||
recentLogs.join("\n") || "(无日志)",
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
default:
|
||||
return `❌ 未知操作: ${action}`;
|
||||
}
|
||||
}
|
||||
209
dev-assistant-mcp/src/tools/docWrite.ts
Normal file
209
dev-assistant-mcp/src/tools/docWrite.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
export const docWriteTool = {
|
||||
name: "doc_write",
|
||||
description:
|
||||
"为代码生成文档指引。分析代码结构,提取函数/类/接口信息,返回结构化的文档框架和编写指引。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
description: "需要生成文档的代码",
|
||||
},
|
||||
doc_type: {
|
||||
type: "string",
|
||||
description: "文档类型",
|
||||
enum: ["readme", "api", "inline", "changelog", "jsdoc"],
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "编程语言",
|
||||
},
|
||||
project_name: {
|
||||
type: "string",
|
||||
description: "项目名称(可选,用于 README)",
|
||||
},
|
||||
extra_info: {
|
||||
type: "string",
|
||||
description: "额外信息(可选,如版本号、变更内容等)",
|
||||
},
|
||||
},
|
||||
required: ["code", "doc_type", "language"],
|
||||
},
|
||||
};
|
||||
|
||||
interface CodeSymbol {
|
||||
type: "function" | "class" | "interface" | "variable" | "route" | "export";
|
||||
name: string;
|
||||
line: number;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
function extractSymbols(code: string, language: string): CodeSymbol[] {
|
||||
const symbols: CodeSymbol[] = [];
|
||||
const lines = code.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
const lineNum = i + 1;
|
||||
|
||||
// 函数
|
||||
let m: RegExpMatchArray | null;
|
||||
if ((m = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/))) {
|
||||
symbols.push({ type: "function", name: m[1], line: lineNum, signature: m[0] });
|
||||
}
|
||||
// 箭头函数 / const 赋值
|
||||
else if ((m = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(?([^)]*)\)?\s*=>/))) {
|
||||
symbols.push({ type: "function", name: m[1], line: lineNum, signature: m[0] });
|
||||
}
|
||||
// 类
|
||||
else if ((m = line.match(/(?:export\s+)?class\s+(\w+)/))) {
|
||||
symbols.push({ type: "class", name: m[1], line: lineNum });
|
||||
}
|
||||
// 接口 (TS)
|
||||
else if ((m = line.match(/(?:export\s+)?interface\s+(\w+)/))) {
|
||||
symbols.push({ type: "interface", name: m[1], line: lineNum });
|
||||
}
|
||||
// 路由 (Express/FastAPI)
|
||||
else if ((m = line.match(/(?:router|app)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/))) {
|
||||
symbols.push({ type: "route", name: `${m[1].toUpperCase()} ${m[2]}`, line: lineNum });
|
||||
}
|
||||
// Python 函数
|
||||
else if ((m = line.match(/(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/))) {
|
||||
symbols.push({ type: "function", name: m[1], line: lineNum, signature: m[0] });
|
||||
}
|
||||
// Python 类
|
||||
else if ((m = line.match(/class\s+(\w+)(?:\(([^)]*)\))?:/))) {
|
||||
symbols.push({ type: "class", name: m[1], line: lineNum });
|
||||
}
|
||||
// export default
|
||||
else if ((m = line.match(/export\s+default\s+(\w+)/))) {
|
||||
symbols.push({ type: "export", name: m[1], line: lineNum });
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
const docTypeInstructions: Record<string, string> = {
|
||||
readme: `请生成 README.md,包含以下章节:
|
||||
# 项目名称
|
||||
## 简介(一句话描述)
|
||||
## 功能特性
|
||||
## 快速开始(安装、配置、运行)
|
||||
## API / 使用说明
|
||||
## 项目结构
|
||||
## License`,
|
||||
|
||||
api: `请生成 API 文档,每个端点/函数包含:
|
||||
- 描述
|
||||
- 请求方法和路径(如适用)
|
||||
- 参数(名称、类型、是否必填、描述)
|
||||
- 返回值(类型、描述)
|
||||
- 示例代码
|
||||
- 错误码(如适用)`,
|
||||
|
||||
inline: `请为代码添加内联注释:
|
||||
- 每个函数/方法添加文档注释(JSDoc / docstring)
|
||||
- 关键逻辑添加简洁的解释性注释
|
||||
- 不要过度注释显而易见的代码
|
||||
- 返回带注释的完整代码`,
|
||||
|
||||
changelog: `请生成 CHANGELOG.md,遵循 Keep a Changelog 格式:
|
||||
## [版本号] - 日期
|
||||
### Added(新增)
|
||||
### Changed(变更)
|
||||
### Fixed(修复)
|
||||
### Removed(移除)`,
|
||||
|
||||
jsdoc: `请为所有函数/类/接口生成文档注释:
|
||||
- @param 参数(类型、描述)
|
||||
- @returns 返回值(类型、描述)
|
||||
- @throws 可能的异常
|
||||
- @example 使用示例
|
||||
- 返回带完整文档注释的代码`,
|
||||
};
|
||||
|
||||
export async function executeDocWrite(args: {
|
||||
code: string;
|
||||
doc_type: string;
|
||||
language: string;
|
||||
project_name?: string;
|
||||
extra_info?: string;
|
||||
}): Promise<string> {
|
||||
const { code, doc_type, language, project_name, extra_info } = args;
|
||||
|
||||
const symbols = extractSymbols(code, language);
|
||||
const lines = code.split("\n");
|
||||
|
||||
const output: string[] = [
|
||||
`# 文档生成指引`,
|
||||
``,
|
||||
`**文档类型**: ${doc_type} | **语言**: ${language}${project_name ? ` | **项目**: ${project_name}` : ""}`,
|
||||
``,
|
||||
];
|
||||
|
||||
// 代码结构分析
|
||||
output.push(`## 代码结构分析`, ``);
|
||||
|
||||
const functions = symbols.filter((s) => s.type === "function");
|
||||
const classes = symbols.filter((s) => s.type === "class");
|
||||
const interfaces = symbols.filter((s) => s.type === "interface");
|
||||
const routes = symbols.filter((s) => s.type === "route");
|
||||
|
||||
if (functions.length > 0) {
|
||||
output.push(`### 函数 (${functions.length})`);
|
||||
for (const f of functions) {
|
||||
output.push(`- **${f.name}** (行 ${f.line})${f.signature ? `: \`${f.signature}\`` : ""}`);
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
if (classes.length > 0) {
|
||||
output.push(`### 类 (${classes.length})`);
|
||||
for (const c of classes) {
|
||||
output.push(`- **${c.name}** (行 ${c.line})`);
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
if (interfaces.length > 0) {
|
||||
output.push(`### 接口 (${interfaces.length})`);
|
||||
for (const i of interfaces) {
|
||||
output.push(`- **${i.name}** (行 ${i.line})`);
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
if (routes.length > 0) {
|
||||
output.push(`### API 路由 (${routes.length})`);
|
||||
for (const r of routes) {
|
||||
output.push(`- **${r.name}** (行 ${r.line})`);
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
if (symbols.length === 0) {
|
||||
output.push(`_未检测到标准的函数/类/接口/路由定义_`, ``);
|
||||
}
|
||||
|
||||
// 统计
|
||||
output.push(
|
||||
`### 文件统计`,
|
||||
`- 总行数: ${lines.length}`,
|
||||
`- 代码符号: ${symbols.length} 个`,
|
||||
``
|
||||
);
|
||||
|
||||
// 文档编写指引
|
||||
const instruction = docTypeInstructions[doc_type] || docTypeInstructions.readme;
|
||||
output.push(`## 文档编写指引`, ``, instruction, ``);
|
||||
|
||||
if (extra_info) {
|
||||
output.push(`## 补充信息`, extra_info, ``);
|
||||
}
|
||||
|
||||
// 附上源代码
|
||||
output.push(`## 源代码`, "```" + language, code, "```");
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
211
dev-assistant-mcp/src/tools/envCheck.ts
Normal file
211
dev-assistant-mcp/src/tools/envCheck.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { platform, totalmem, freemem, cpus, hostname, userInfo } from "os";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const envCheckTool = {
|
||||
name: "env_check",
|
||||
description:
|
||||
"环境检测。检查 Node.js、Python、Git 等工具版本,检测端口占用,查看系统资源(CPU/内存/磁盘),验证开发环境是否就绪。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
checks: {
|
||||
type: "string",
|
||||
description: "要检查的项目,逗号分隔(可选):all, node, python, git, ports, system, docker。默认 all。",
|
||||
},
|
||||
ports: {
|
||||
type: "string",
|
||||
description: "要检测的端口号,逗号分隔(如 3000,8080,5173)",
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
};
|
||||
|
||||
async function getVersion(cmd: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||
return stdout.trim().split("\n")[0];
|
||||
} catch {
|
||||
return "❌ 未安装";
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPort(port: number): Promise<boolean> {
|
||||
try {
|
||||
const cmd = process.platform === "win32"
|
||||
? `netstat -ano | findstr :${port}`
|
||||
: `lsof -i :${port} 2>/dev/null || ss -tlnp | grep :${port}`;
|
||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||
return stdout.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getDiskSpace(): Promise<string> {
|
||||
try {
|
||||
const cmd = process.platform === "win32"
|
||||
? 'powershell -command "Get-PSDrive -PSProvider FileSystem | Select-Object Name, @{N=\'Used(GB)\';E={[math]::Round($_.Used/1GB,1)}}, @{N=\'Free(GB)\';E={[math]::Round($_.Free/1GB,1)}} | Format-Table -AutoSize"'
|
||||
: "df -h / /home 2>/dev/null";
|
||||
const { stdout } = await execAsync(cmd, { timeout: 10000 });
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return "无法获取";
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeEnvCheck(args: {
|
||||
checks?: string;
|
||||
ports?: string;
|
||||
}): Promise<string> {
|
||||
const { checks = "all", ports } = args;
|
||||
const checkList = checks.split(",").map((c) => c.trim().toLowerCase());
|
||||
const doAll = checkList.includes("all");
|
||||
|
||||
const output: string[] = [
|
||||
`# 环境检测报告`,
|
||||
``,
|
||||
`🖥️ ${hostname()} | ${platform()} | ${userInfo().username}`,
|
||||
`📅 ${new Date().toLocaleString("zh-CN")}`,
|
||||
``,
|
||||
];
|
||||
|
||||
// 系统资源
|
||||
if (doAll || checkList.includes("system")) {
|
||||
const totalMem = (totalmem() / 1024 / 1024 / 1024).toFixed(1);
|
||||
const freeMem = (freemem() / 1024 / 1024 / 1024).toFixed(1);
|
||||
const usedMem = ((totalmem() - freemem()) / 1024 / 1024 / 1024).toFixed(1);
|
||||
const memPercent = ((1 - freemem() / totalmem()) * 100).toFixed(0);
|
||||
const cpuInfo = cpus();
|
||||
const cpuModel = cpuInfo[0]?.model || "未知";
|
||||
|
||||
output.push(
|
||||
`## 💻 系统资源`,
|
||||
``,
|
||||
`| 项目 | 值 |`,
|
||||
`|------|------|`,
|
||||
`| CPU | ${cpuModel} |`,
|
||||
`| 核心数 | ${cpuInfo.length} |`,
|
||||
`| 内存 | ${usedMem} / ${totalMem} GB (${memPercent}%) |`,
|
||||
`| 空闲内存 | ${freeMem} GB |`,
|
||||
``
|
||||
);
|
||||
|
||||
const disk = await getDiskSpace();
|
||||
if (disk !== "无法获取") {
|
||||
output.push(`### 磁盘空间`, "```", disk, "```", ``);
|
||||
}
|
||||
}
|
||||
|
||||
// Node.js
|
||||
if (doAll || checkList.includes("node")) {
|
||||
const [nodeVer, npmVer, npxVer, yarnVer, pnpmVer] = await Promise.all([
|
||||
getVersion("node --version"),
|
||||
getVersion("npm --version"),
|
||||
getVersion("npx --version"),
|
||||
getVersion("yarn --version"),
|
||||
getVersion("pnpm --version"),
|
||||
]);
|
||||
|
||||
output.push(
|
||||
`## 📦 Node.js 生态`,
|
||||
``,
|
||||
`| 工具 | 版本 |`,
|
||||
`|------|------|`,
|
||||
`| Node.js | ${nodeVer} |`,
|
||||
`| npm | ${npmVer} |`,
|
||||
`| npx | ${npxVer} |`,
|
||||
`| yarn | ${yarnVer} |`,
|
||||
`| pnpm | ${pnpmVer} |`,
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
// Python
|
||||
if (doAll || checkList.includes("python")) {
|
||||
const [pyVer, py3Ver, pipVer] = await Promise.all([
|
||||
getVersion("python --version"),
|
||||
getVersion("python3 --version"),
|
||||
getVersion("pip --version"),
|
||||
]);
|
||||
|
||||
output.push(
|
||||
`## 🐍 Python 生态`,
|
||||
``,
|
||||
`| 工具 | 版本 |`,
|
||||
`|------|------|`,
|
||||
`| Python | ${pyVer} |`,
|
||||
`| Python3 | ${py3Ver} |`,
|
||||
`| pip | ${pipVer} |`,
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
// Git
|
||||
if (doAll || checkList.includes("git")) {
|
||||
const gitVer = await getVersion("git --version");
|
||||
const gitUser = await getVersion("git config --global user.name");
|
||||
const gitEmail = await getVersion("git config --global user.email");
|
||||
|
||||
output.push(
|
||||
`## 🔧 Git`,
|
||||
``,
|
||||
`| 项目 | 值 |`,
|
||||
`|------|------|`,
|
||||
`| 版本 | ${gitVer} |`,
|
||||
`| 用户名 | ${gitUser} |`,
|
||||
`| 邮箱 | ${gitEmail} |`,
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
// Docker
|
||||
if (doAll || checkList.includes("docker")) {
|
||||
const [dockerVer, composeVer] = await Promise.all([
|
||||
getVersion("docker --version"),
|
||||
getVersion("docker compose version 2>&1 || docker-compose --version 2>&1"),
|
||||
]);
|
||||
|
||||
output.push(
|
||||
`## 🐳 Docker`,
|
||||
``,
|
||||
`| 工具 | 版本 |`,
|
||||
`|------|------|`,
|
||||
`| Docker | ${dockerVer} |`,
|
||||
`| Compose | ${composeVer} |`,
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
// 端口检测
|
||||
if (doAll || checkList.includes("ports") || ports) {
|
||||
const portsToCheck = ports
|
||||
? ports.split(",").map((p) => parseInt(p.trim())).filter(Boolean)
|
||||
: [3000, 3001, 5173, 5174, 8080, 8081, 4000, 4173, 5000, 5500];
|
||||
|
||||
const portResults = await Promise.all(
|
||||
portsToCheck.map(async (port) => ({
|
||||
port,
|
||||
occupied: await checkPort(port),
|
||||
}))
|
||||
);
|
||||
|
||||
output.push(
|
||||
`## 🔌 端口状态`,
|
||||
``,
|
||||
`| 端口 | 状态 |`,
|
||||
`|------|------|`,
|
||||
...portResults.map((r) => `| ${r.port} | ${r.occupied ? "🔴 已占用" : "🟢 空闲"} |`),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
// 总结
|
||||
output.push(`---`, `_检测完成_`);
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
67
dev-assistant-mcp/src/tools/execCommand.ts
Normal file
67
dev-assistant-mcp/src/tools/execCommand.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { exec, spawn } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const execCommandTool = {
|
||||
name: "exec_command",
|
||||
description:
|
||||
"在本地电脑上执行 Shell 命令。用于运行编译、测试、构建、安装依赖等任何命令。返回 stdout、stderr 和退出码。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
command: {
|
||||
type: "string",
|
||||
description: "要执行的命令(如 npm install, tsc --noEmit, python script.py)",
|
||||
},
|
||||
cwd: {
|
||||
type: "string",
|
||||
description: "工作目录(绝对路径)",
|
||||
},
|
||||
timeout: {
|
||||
type: "number",
|
||||
description: "超时时间(毫秒),默认 60000",
|
||||
},
|
||||
},
|
||||
required: ["command"],
|
||||
},
|
||||
};
|
||||
|
||||
export async function executeExecCommand(args: {
|
||||
command: string;
|
||||
cwd?: string;
|
||||
timeout?: number;
|
||||
}): Promise<string> {
|
||||
const { command, cwd, timeout = 60000 } = args;
|
||||
const workDir = cwd || process.cwd();
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd: workDir,
|
||||
timeout,
|
||||
maxBuffer: 1024 * 1024 * 10, // 10MB
|
||||
shell: process.platform === "win32" ? "powershell.exe" : "/bin/bash",
|
||||
});
|
||||
|
||||
const output: string[] = [
|
||||
`$ ${command}`,
|
||||
`📂 ${workDir}`,
|
||||
`✅ 退出码: 0`,
|
||||
];
|
||||
if (stdout.trim()) output.push(`\n--- stdout ---\n${stdout.trim()}`);
|
||||
if (stderr.trim()) output.push(`\n--- stderr ---\n${stderr.trim()}`);
|
||||
|
||||
return output.join("\n");
|
||||
} catch (error: any) {
|
||||
const output: string[] = [
|
||||
`$ ${command}`,
|
||||
`📂 ${workDir}`,
|
||||
`❌ 退出码: ${error.code ?? "unknown"}`,
|
||||
];
|
||||
if (error.stdout?.trim()) output.push(`\n--- stdout ---\n${error.stdout.trim()}`);
|
||||
if (error.stderr?.trim()) output.push(`\n--- stderr ---\n${error.stderr.trim()}`);
|
||||
if (error.killed) output.push(`\n⏰ 命令超时(${timeout}ms)被终止`);
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
}
|
||||
191
dev-assistant-mcp/src/tools/fileOps.ts
Normal file
191
dev-assistant-mcp/src/tools/fileOps.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, watchFile, unwatchFile } from "fs";
|
||||
import { dirname, join, extname } from "path";
|
||||
|
||||
export const readFileTool = {
|
||||
name: "read_local_file",
|
||||
description:
|
||||
"读取本地文件内容。支持任意文本文件,返回带行号的内容。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "文件绝对路径",
|
||||
},
|
||||
start_line: {
|
||||
type: "number",
|
||||
description: "起始行号(可选,从 1 开始)",
|
||||
},
|
||||
end_line: {
|
||||
type: "number",
|
||||
description: "结束行号(可选)",
|
||||
},
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
};
|
||||
|
||||
export const writeFileTool = {
|
||||
name: "write_local_file",
|
||||
description:
|
||||
"写入内容到本地文件。如果文件不存在会自动创建(含父目录)。支持完整写入或按行替换。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "文件绝对路径",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "要写入的完整文件内容",
|
||||
},
|
||||
create_dirs: {
|
||||
type: "boolean",
|
||||
description: "是否自动创建父目录(默认 true)",
|
||||
},
|
||||
},
|
||||
required: ["path", "content"],
|
||||
},
|
||||
};
|
||||
|
||||
export const patchFileTool = {
|
||||
name: "patch_file",
|
||||
description:
|
||||
"精确修改本地文件。查找指定文本并替换为新文本,支持多次替换。适合自动纠错和代码修改。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "文件绝对路径",
|
||||
},
|
||||
old_text: {
|
||||
type: "string",
|
||||
description: "要查找并替换的原文本(必须精确匹配)",
|
||||
},
|
||||
new_text: {
|
||||
type: "string",
|
||||
description: "替换后的新文本",
|
||||
},
|
||||
replace_all: {
|
||||
type: "boolean",
|
||||
description: "是否替换所有匹配项(默认 false,只替换第一个)",
|
||||
},
|
||||
},
|
||||
required: ["path", "old_text", "new_text"],
|
||||
},
|
||||
};
|
||||
|
||||
export async function executeReadFile(args: {
|
||||
path: string;
|
||||
start_line?: number;
|
||||
end_line?: number;
|
||||
}): Promise<string> {
|
||||
const { path: filePath, start_line, end_line } = args;
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return `❌ 文件不存在: ${filePath}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const start = (start_line || 1) - 1;
|
||||
const end = end_line || lines.length;
|
||||
const selected = lines.slice(start, end);
|
||||
|
||||
const numbered = selected.map((line, i) => `${String(start + i + 1).padStart(4)} | ${line}`).join("\n");
|
||||
|
||||
return [
|
||||
`📄 ${filePath}`,
|
||||
`行数: ${lines.length} | 显示: ${start + 1}-${Math.min(end, lines.length)}`,
|
||||
`类型: ${extname(filePath) || "unknown"}`,
|
||||
``,
|
||||
numbered,
|
||||
].join("\n");
|
||||
} catch (error: any) {
|
||||
return `❌ 读取失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeWriteFile(args: {
|
||||
path: string;
|
||||
content: string;
|
||||
create_dirs?: boolean;
|
||||
}): Promise<string> {
|
||||
const { path: filePath, content, create_dirs = true } = args;
|
||||
|
||||
try {
|
||||
if (create_dirs) {
|
||||
const dir = dirname(filePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const existed = existsSync(filePath);
|
||||
const oldLines = existed ? readFileSync(filePath, "utf-8").split("\n").length : 0;
|
||||
|
||||
writeFileSync(filePath, content, "utf-8");
|
||||
const newLines = content.split("\n").length;
|
||||
|
||||
return [
|
||||
`✅ 文件已${existed ? "更新" : "创建"}: ${filePath}`,
|
||||
existed ? `行数变化: ${oldLines} → ${newLines}` : `行数: ${newLines}`,
|
||||
].join("\n");
|
||||
} catch (error: any) {
|
||||
return `❌ 写入失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executePatchFile(args: {
|
||||
path: string;
|
||||
old_text: string;
|
||||
new_text: string;
|
||||
replace_all?: boolean;
|
||||
}): Promise<string> {
|
||||
const { path: filePath, old_text, new_text, replace_all = false } = args;
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return `❌ 文件不存在: ${filePath}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
|
||||
if (!content.includes(old_text)) {
|
||||
return `❌ 未找到匹配文本。\n\n搜索内容:\n\`\`\`\n${old_text.slice(0, 200)}\n\`\`\`\n\n提示: old_text 必须精确匹配文件中的内容(包括空格和换行)`;
|
||||
}
|
||||
|
||||
const occurrences = content.split(old_text).length - 1;
|
||||
let newContent: string;
|
||||
|
||||
if (replace_all) {
|
||||
newContent = content.split(old_text).join(new_text);
|
||||
} else {
|
||||
const idx = content.indexOf(old_text);
|
||||
newContent = content.substring(0, idx) + new_text + content.substring(idx + old_text.length);
|
||||
}
|
||||
|
||||
writeFileSync(filePath, newContent, "utf-8");
|
||||
|
||||
const replacedCount = replace_all ? occurrences : 1;
|
||||
return [
|
||||
`✅ 文件已修改: ${filePath}`,
|
||||
`替换: ${replacedCount} 处${occurrences > 1 && !replace_all ? `(共找到 ${occurrences} 处匹配,仅替换第 1 处)` : ""}`,
|
||||
``,
|
||||
`--- 旧代码 ---`,
|
||||
"```",
|
||||
old_text.slice(0, 500),
|
||||
"```",
|
||||
``,
|
||||
`+++ 新代码 +++`,
|
||||
"```",
|
||||
new_text.slice(0, 500),
|
||||
"```",
|
||||
].join("\n");
|
||||
} catch (error: any) {
|
||||
return `❌ 修改失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
198
dev-assistant-mcp/src/tools/gitOps.ts
Normal file
198
dev-assistant-mcp/src/tools/gitOps.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitOpsTool = {
|
||||
name: "git_ops",
|
||||
description:
|
||||
"Git 全套操作。支持 status、diff、add、commit、push、pull、log、branch、checkout、stash、reset 等所有常用 Git 命令。自动解析输出为结构化报告。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
action: {
|
||||
type: "string",
|
||||
description: "Git 操作",
|
||||
enum: [
|
||||
"status", "diff", "diff_staged", "add", "add_all", "commit",
|
||||
"push", "pull", "log", "branch", "branch_create", "checkout",
|
||||
"stash", "stash_pop", "reset_soft", "reset_hard", "remote", "init",
|
||||
],
|
||||
},
|
||||
message: {
|
||||
type: "string",
|
||||
description: "commit 消息(commit 时必填)",
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
description: "目标参数(文件路径/分支名/commit hash 等)",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "log 显示条数(默认 10)",
|
||||
},
|
||||
},
|
||||
required: ["project_path", "action"],
|
||||
},
|
||||
};
|
||||
|
||||
async function git(args: string, cwd: string): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(`git ${args}`, {
|
||||
cwd,
|
||||
timeout: 30000,
|
||||
maxBuffer: 1024 * 1024 * 5,
|
||||
});
|
||||
return { stdout, stderr, code: 0 };
|
||||
} catch (error: any) {
|
||||
return { stdout: error.stdout || "", stderr: error.stderr || "", code: error.code ?? 1 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeGitOps(args: {
|
||||
project_path: string;
|
||||
action: string;
|
||||
message?: string;
|
||||
target?: string;
|
||||
count?: number;
|
||||
}): Promise<string> {
|
||||
const { project_path, action, message, target, count = 10 } = args;
|
||||
|
||||
let result: { stdout: string; stderr: string; code: number };
|
||||
let title = "";
|
||||
|
||||
switch (action) {
|
||||
case "status":
|
||||
title = "Git Status";
|
||||
result = await git("status --short --branch", project_path);
|
||||
if (result.code === 0) {
|
||||
const lines = result.stdout.trim().split("\n");
|
||||
const branch = lines[0] || "";
|
||||
const changes = lines.slice(1);
|
||||
const staged = changes.filter((l) => /^[MADRC]/.test(l));
|
||||
const unstaged = changes.filter((l) => /^.[MADRC]/.test(l));
|
||||
const untracked = changes.filter((l) => l.startsWith("??"));
|
||||
|
||||
return [
|
||||
`# ${title}`, `📂 ${project_path}`, `🌿 ${branch}`, ``,
|
||||
`| 状态 | 数量 |`, `|------|------|`,
|
||||
`| ✅ 已暂存 | ${staged.length} |`,
|
||||
`| 📝 已修改 | ${unstaged.length} |`,
|
||||
`| ❓ 未跟踪 | ${untracked.length} |`, ``,
|
||||
changes.length > 0 ? "```\n" + result.stdout.trim() + "\n```" : "✨ 工作区干净",
|
||||
].join("\n");
|
||||
}
|
||||
break;
|
||||
|
||||
case "diff":
|
||||
title = "Git Diff (工作区)";
|
||||
result = await git(`diff ${target ? `-- "${target}"` : ""} --stat`, project_path);
|
||||
if (result.stdout.trim()) {
|
||||
const detail = await git(`diff ${target ? `-- "${target}"` : ""} --no-color`, project_path);
|
||||
return `# ${title}\n\n## 摘要\n\`\`\`\n${result.stdout.trim()}\n\`\`\`\n\n## 详细\n\`\`\`diff\n${detail.stdout.slice(0, 8000)}\n\`\`\``;
|
||||
}
|
||||
return `# ${title}\n\n✨ 无差异`;
|
||||
|
||||
case "diff_staged":
|
||||
title = "Git Diff (暂存区)";
|
||||
result = await git("diff --cached --stat", project_path);
|
||||
if (result.stdout.trim()) {
|
||||
const detail = await git("diff --cached --no-color", project_path);
|
||||
return `# ${title}\n\n## 摘要\n\`\`\`\n${result.stdout.trim()}\n\`\`\`\n\n## 详细\n\`\`\`diff\n${detail.stdout.slice(0, 8000)}\n\`\`\``;
|
||||
}
|
||||
return `# ${title}\n\n✨ 暂存区无内容`;
|
||||
|
||||
case "add":
|
||||
title = "Git Add";
|
||||
result = await git(`add "${target || "."}"`, project_path);
|
||||
break;
|
||||
|
||||
case "add_all":
|
||||
title = "Git Add All";
|
||||
result = await git("add -A", project_path);
|
||||
break;
|
||||
|
||||
case "commit":
|
||||
if (!message) return "❌ commit 需要提供 message 参数";
|
||||
title = "Git Commit";
|
||||
result = await git(`commit -m "${message.replace(/"/g, '\\"')}"`, project_path);
|
||||
break;
|
||||
|
||||
case "push":
|
||||
title = "Git Push";
|
||||
result = await git(`push ${target || ""}`, project_path);
|
||||
break;
|
||||
|
||||
case "pull":
|
||||
title = "Git Pull";
|
||||
result = await git(`pull ${target || ""}`, project_path);
|
||||
break;
|
||||
|
||||
case "log":
|
||||
title = "Git Log";
|
||||
result = await git(`log --oneline --graph --decorate -n ${count}`, project_path);
|
||||
if (result.code === 0) {
|
||||
return `# ${title} (最近 ${count} 条)\n\n\`\`\`\n${result.stdout.trim()}\n\`\`\``;
|
||||
}
|
||||
break;
|
||||
|
||||
case "branch":
|
||||
title = "Git Branch";
|
||||
result = await git("branch -a -v", project_path);
|
||||
break;
|
||||
|
||||
case "branch_create":
|
||||
if (!target) return "❌ 创建分支需要提供 target(分支名)";
|
||||
title = `Git Branch Create: ${target}`;
|
||||
result = await git(`checkout -b "${target}"`, project_path);
|
||||
break;
|
||||
|
||||
case "checkout":
|
||||
if (!target) return "❌ checkout 需要提供 target(分支名或文件)";
|
||||
title = `Git Checkout: ${target}`;
|
||||
result = await git(`checkout "${target}"`, project_path);
|
||||
break;
|
||||
|
||||
case "stash":
|
||||
title = "Git Stash";
|
||||
result = await git(`stash${message ? ` push -m "${message}"` : ""}`, project_path);
|
||||
break;
|
||||
|
||||
case "stash_pop":
|
||||
title = "Git Stash Pop";
|
||||
result = await git("stash pop", project_path);
|
||||
break;
|
||||
|
||||
case "reset_soft":
|
||||
title = "Git Reset (soft)";
|
||||
result = await git(`reset --soft ${target || "HEAD~1"}`, project_path);
|
||||
break;
|
||||
|
||||
case "reset_hard":
|
||||
title = "Git Reset (hard) ⚠️";
|
||||
result = await git(`reset --hard ${target || "HEAD"}`, project_path);
|
||||
break;
|
||||
|
||||
case "remote":
|
||||
title = "Git Remote";
|
||||
result = await git("remote -v", project_path);
|
||||
break;
|
||||
|
||||
case "init":
|
||||
title = "Git Init";
|
||||
result = await git("init", project_path);
|
||||
break;
|
||||
|
||||
default:
|
||||
return `❌ 未知 Git 操作: ${action}`;
|
||||
}
|
||||
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
||||
const icon = result.code === 0 ? "✅" : "❌";
|
||||
|
||||
return `# ${icon} ${title}\n\n📂 ${project_path}\n\n\`\`\`\n${output || "(无输出)"}\n\`\`\``;
|
||||
}
|
||||
165
dev-assistant-mcp/src/tools/lintCheck.ts
Normal file
165
dev-assistant-mcp/src/tools/lintCheck.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const lintCheckTool = {
|
||||
name: "lint_check",
|
||||
description:
|
||||
"对项目执行代码检查。自动检测项目类型并运行对应的 lint 工具(ESLint、TypeScript 编译检查、Pylint 等),返回结构化的错误列表。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
fix: {
|
||||
type: "boolean",
|
||||
description: "是否自动修复可修复的问题(默认 false)",
|
||||
},
|
||||
files: {
|
||||
type: "string",
|
||||
description: "指定检查的文件或 glob(可选,默认检查整个项目)",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
};
|
||||
|
||||
interface LintResult {
|
||||
tool: string;
|
||||
success: boolean;
|
||||
errorCount: number;
|
||||
warningCount: number;
|
||||
output: string;
|
||||
}
|
||||
|
||||
async function runCommand(cmd: string, cwd: string, timeout = 30000): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(cmd, {
|
||||
cwd,
|
||||
timeout,
|
||||
maxBuffer: 1024 * 1024 * 5,
|
||||
shell: process.platform === "win32" ? "powershell.exe" : "/bin/bash",
|
||||
});
|
||||
return { stdout, stderr, code: 0 };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout || "",
|
||||
stderr: error.stderr || "",
|
||||
code: error.code ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeLintCheck(args: {
|
||||
project_path: string;
|
||||
fix?: boolean;
|
||||
files?: string;
|
||||
}): Promise<string> {
|
||||
const { project_path, fix = false, files } = args;
|
||||
const results: LintResult[] = [];
|
||||
|
||||
const hasFile = (name: string) => existsSync(join(project_path, name));
|
||||
const hasNodeModules = hasFile("node_modules");
|
||||
|
||||
// TypeScript 编译检查
|
||||
if (hasFile("tsconfig.json")) {
|
||||
const result = await runCommand("npx tsc --noEmit --pretty", project_path);
|
||||
const errorCount = (result.stdout.match(/error TS/g) || []).length;
|
||||
results.push({
|
||||
tool: "TypeScript (tsc --noEmit)",
|
||||
success: result.code === 0,
|
||||
errorCount,
|
||||
warningCount: 0,
|
||||
output: result.stdout || result.stderr || "(无输出)",
|
||||
});
|
||||
}
|
||||
|
||||
// ESLint
|
||||
if (hasFile(".eslintrc.js") || hasFile(".eslintrc.json") || hasFile(".eslintrc.yml") || hasFile("eslint.config.js") || hasFile("eslint.config.mjs")) {
|
||||
const target = files || "src/";
|
||||
const fixFlag = fix ? " --fix" : "";
|
||||
const result = await runCommand(`npx eslint ${target}${fixFlag} --format stylish`, project_path);
|
||||
const errorMatch = result.stdout.match(/(\d+) errors?/);
|
||||
const warnMatch = result.stdout.match(/(\d+) warnings?/);
|
||||
results.push({
|
||||
tool: `ESLint${fix ? " (--fix)" : ""}`,
|
||||
success: result.code === 0,
|
||||
errorCount: errorMatch ? parseInt(errorMatch[1]) : 0,
|
||||
warningCount: warnMatch ? parseInt(warnMatch[1]) : 0,
|
||||
output: result.stdout || result.stderr || "✅ 无问题",
|
||||
});
|
||||
}
|
||||
|
||||
// Python: pylint / flake8
|
||||
if (hasFile("requirements.txt") || hasFile("setup.py") || hasFile("pyproject.toml")) {
|
||||
const target = files || ".";
|
||||
// 优先 flake8
|
||||
const result = await runCommand(`python -m flake8 ${target} --max-line-length=120 --count`, project_path);
|
||||
if (result.code !== 127) { // 127 = command not found
|
||||
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
||||
results.push({
|
||||
tool: "Flake8",
|
||||
success: result.code === 0,
|
||||
errorCount: lines.length,
|
||||
warningCount: 0,
|
||||
output: result.stdout || result.stderr || "✅ 无问题",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果什么检查工具都没找到
|
||||
if (results.length === 0) {
|
||||
// 尝试通用的 package.json lint 脚本
|
||||
if (hasFile("package.json")) {
|
||||
const result = await runCommand("npm run lint 2>&1 || echo LINT_SCRIPT_NOT_FOUND", project_path);
|
||||
if (!result.stdout.includes("LINT_SCRIPT_NOT_FOUND") && !result.stdout.includes("Missing script")) {
|
||||
results.push({
|
||||
tool: "npm run lint",
|
||||
success: result.code === 0,
|
||||
errorCount: result.code === 0 ? 0 : 1,
|
||||
warningCount: 0,
|
||||
output: result.stdout || result.stderr,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return `# Lint 检查结果\n\n⚠️ 未检测到 lint 工具配置(ESLint、tsconfig、pylint 等)\n项目路径: ${project_path}\n\n建议:\n- TypeScript 项目:添加 tsconfig.json\n- JS 项目:npm init @eslint/config\n- Python 项目:pip install flake8`;
|
||||
}
|
||||
|
||||
// 组装报告
|
||||
const totalErrors = results.reduce((sum, r) => sum + r.errorCount, 0);
|
||||
const totalWarnings = results.reduce((sum, r) => sum + r.warningCount, 0);
|
||||
const allPassed = results.every((r) => r.success);
|
||||
|
||||
const output: string[] = [
|
||||
`# Lint 检查报告`,
|
||||
``,
|
||||
`📂 项目: ${project_path}`,
|
||||
`${allPassed ? "✅ 全部通过" : "❌ 发现问题"} | 错误: ${totalErrors} | 警告: ${totalWarnings}`,
|
||||
``,
|
||||
];
|
||||
|
||||
for (const r of results) {
|
||||
output.push(
|
||||
`## ${r.success ? "✅" : "❌"} ${r.tool}`,
|
||||
`错误: ${r.errorCount} | 警告: ${r.warningCount}`,
|
||||
"```",
|
||||
r.output.slice(0, 3000), // 限制输出长度
|
||||
"```",
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (!allPassed && !fix) {
|
||||
output.push(`💡 提示: 可以设置 fix=true 自动修复可修复的问题`);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
184
dev-assistant-mcp/src/tools/projectScan.ts
Normal file
184
dev-assistant-mcp/src/tools/projectScan.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
||||
import { join, extname, basename } from "path";
|
||||
|
||||
export const projectScanTool = {
|
||||
name: "project_scan",
|
||||
description:
|
||||
"扫描分析项目结构。返回项目类型、技术栈、文件结构、依赖列表、可用脚本、配置文件等全局信息,帮助快速理解项目。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
max_depth: {
|
||||
type: "number",
|
||||
description: "目录扫描最大深度(默认 3)",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
};
|
||||
|
||||
function scanDir(dir: string, depth: number, maxDepth: number, prefix: string = ""): string[] {
|
||||
if (depth > maxDepth) return [];
|
||||
const lines: string[] = [];
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir).filter(
|
||||
(e) => !e.startsWith(".") && e !== "node_modules" && e !== "__pycache__" && e !== "dist" && e !== "build" && e !== ".git"
|
||||
);
|
||||
|
||||
for (const entry of entries.slice(0, 30)) { // 限制每层最多30项
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
lines.push(`${prefix}📂 ${entry}/`);
|
||||
lines.push(...scanDir(fullPath, depth + 1, maxDepth, prefix + " "));
|
||||
} else {
|
||||
const size = stat.size;
|
||||
const sizeStr = size < 1024 ? `${size}B` : size < 1024 * 1024 ? `${(size / 1024).toFixed(0)}KB` : `${(size / 1024 / 1024).toFixed(1)}MB`;
|
||||
lines.push(`${prefix}📄 ${entry} (${sizeStr})`);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (entries.length > 30) {
|
||||
lines.push(`${prefix}... 还有 ${entries.length - 30} 个条目`);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function countFiles(dir: string, ext: string, depth = 0): number {
|
||||
if (depth > 5) return 0;
|
||||
let count = 0;
|
||||
try {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (entry.startsWith(".") || entry === "node_modules" || entry === "__pycache__" || entry === "dist") continue;
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) count += countFiles(fullPath, ext, depth + 1);
|
||||
else if (extname(entry) === ext) count++;
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
return count;
|
||||
}
|
||||
|
||||
export async function executeProjectScan(args: {
|
||||
project_path: string;
|
||||
max_depth?: number;
|
||||
}): Promise<string> {
|
||||
const { project_path, max_depth = 3 } = args;
|
||||
const hasFile = (name: string) => existsSync(join(project_path, name));
|
||||
const readJson = (name: string) => {
|
||||
try { return JSON.parse(readFileSync(join(project_path, name), "utf-8")); } catch { return null; }
|
||||
};
|
||||
|
||||
const output: string[] = [
|
||||
`# 项目分析报告`,
|
||||
``,
|
||||
`📂 路径: ${project_path}`,
|
||||
``,
|
||||
];
|
||||
|
||||
// 项目类型和技术栈检测
|
||||
const techStack: string[] = [];
|
||||
const configs: string[] = [];
|
||||
|
||||
if (hasFile("package.json")) techStack.push("Node.js");
|
||||
if (hasFile("tsconfig.json")) techStack.push("TypeScript");
|
||||
if (hasFile("next.config.js") || hasFile("next.config.mjs") || hasFile("next.config.ts")) techStack.push("Next.js");
|
||||
if (hasFile("vite.config.ts") || hasFile("vite.config.js")) techStack.push("Vite");
|
||||
if (hasFile("webpack.config.js")) techStack.push("Webpack");
|
||||
if (hasFile("requirements.txt") || hasFile("pyproject.toml")) techStack.push("Python");
|
||||
if (hasFile("Cargo.toml")) techStack.push("Rust");
|
||||
if (hasFile("go.mod")) techStack.push("Go");
|
||||
if (hasFile("pom.xml") || hasFile("build.gradle")) techStack.push("Java");
|
||||
if (hasFile("docker-compose.yml") || hasFile("Dockerfile")) techStack.push("Docker");
|
||||
if (hasFile(".eslintrc.js") || hasFile("eslint.config.js")) configs.push("ESLint");
|
||||
if (hasFile(".prettierrc") || hasFile(".prettierrc.json")) configs.push("Prettier");
|
||||
if (hasFile("jest.config.js") || hasFile("jest.config.ts")) configs.push("Jest");
|
||||
if (hasFile("vitest.config.ts")) configs.push("Vitest");
|
||||
if (hasFile(".env") || hasFile(".env.example")) configs.push(".env");
|
||||
|
||||
output.push(`## 技术栈`, techStack.length > 0 ? techStack.join(" + ") : "未检测到", ``);
|
||||
|
||||
if (configs.length > 0) {
|
||||
output.push(`## 工具配置`, configs.join(", "), ``);
|
||||
}
|
||||
|
||||
// package.json 分析
|
||||
const pkg = readJson("package.json");
|
||||
if (pkg) {
|
||||
output.push(`## package.json`);
|
||||
if (pkg.name) output.push(`- **名称**: ${pkg.name}`);
|
||||
if (pkg.version) output.push(`- **版本**: ${pkg.version}`);
|
||||
if (pkg.description) output.push(`- **描述**: ${pkg.description}`);
|
||||
|
||||
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
||||
output.push(``, `### 可用脚本`);
|
||||
for (const [name, cmd] of Object.entries(pkg.scripts)) {
|
||||
output.push(`- \`npm run ${name}\` → ${cmd}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (pkg.dependencies) {
|
||||
const deps = Object.entries(pkg.dependencies);
|
||||
output.push(``, `### 依赖 (${deps.length})`);
|
||||
for (const [name, ver] of deps.slice(0, 20)) {
|
||||
output.push(`- ${name}: ${ver}`);
|
||||
}
|
||||
if (deps.length > 20) output.push(`... 还有 ${deps.length - 20} 个`);
|
||||
}
|
||||
|
||||
if (pkg.devDependencies) {
|
||||
const devDeps = Object.entries(pkg.devDependencies);
|
||||
output.push(``, `### 开发依赖 (${devDeps.length})`);
|
||||
for (const [name, ver] of devDeps.slice(0, 15)) {
|
||||
output.push(`- ${name}: ${ver}`);
|
||||
}
|
||||
if (devDeps.length > 15) output.push(`... 还有 ${devDeps.length - 15} 个`);
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
// Python 依赖
|
||||
if (hasFile("requirements.txt")) {
|
||||
try {
|
||||
const reqs = readFileSync(join(project_path, "requirements.txt"), "utf-8")
|
||||
.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
||||
output.push(`## Python 依赖 (${reqs.length})`);
|
||||
for (const r of reqs.slice(0, 20)) output.push(`- ${r.trim()}`);
|
||||
if (reqs.length > 20) output.push(`... 还有 ${reqs.length - 20} 个`);
|
||||
output.push(``);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 文件统计
|
||||
const fileCounts: Record<string, number> = {};
|
||||
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".py", ".css", ".html", ".json", ".md"]) {
|
||||
const count = countFiles(project_path, ext);
|
||||
if (count > 0) fileCounts[ext] = count;
|
||||
}
|
||||
|
||||
if (Object.keys(fileCounts).length > 0) {
|
||||
output.push(`## 文件统计`);
|
||||
for (const [ext, count] of Object.entries(fileCounts).sort((a, b) => b[1] - a[1])) {
|
||||
output.push(`- ${ext}: ${count} 个文件`);
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
// 目录树
|
||||
output.push(`## 目录结构`);
|
||||
const tree = scanDir(project_path, 0, max_depth);
|
||||
output.push(...tree);
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
151
dev-assistant-mcp/src/tools/runTests.ts
Normal file
151
dev-assistant-mcp/src/tools/runTests.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const runTestsTool = {
|
||||
name: "run_tests",
|
||||
description:
|
||||
"运行项目测试。自动检测测试框架(Jest、Mocha、Pytest 等),执行测试并返回结构化的结果(通过/失败/跳过数量及失败详情)。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
test_file: {
|
||||
type: "string",
|
||||
description: "指定测试文件(可选,默认运行全部测试)",
|
||||
},
|
||||
test_name: {
|
||||
type: "string",
|
||||
description: "指定测试名称或模式(可选)",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
};
|
||||
|
||||
async function runCommand(cmd: string, cwd: string): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(cmd, {
|
||||
cwd,
|
||||
timeout: 120000,
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
shell: process.platform === "win32" ? "powershell.exe" : "/bin/bash",
|
||||
});
|
||||
return { stdout, stderr, code: 0 };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout || "",
|
||||
stderr: error.stderr || "",
|
||||
code: error.code ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeRunTests(args: {
|
||||
project_path: string;
|
||||
test_file?: string;
|
||||
test_name?: string;
|
||||
}): Promise<string> {
|
||||
const { project_path, test_file, test_name } = args;
|
||||
const hasFile = (name: string) => existsSync(join(project_path, name));
|
||||
|
||||
let testCmd = "";
|
||||
let framework = "";
|
||||
|
||||
// 检测测试框架
|
||||
if (hasFile("jest.config.js") || hasFile("jest.config.ts") || hasFile("jest.config.mjs")) {
|
||||
framework = "Jest";
|
||||
testCmd = "npx jest --verbose --no-coverage";
|
||||
if (test_file) testCmd += ` "${test_file}"`;
|
||||
if (test_name) testCmd += ` -t "${test_name}"`;
|
||||
} else if (hasFile("vitest.config.ts") || hasFile("vitest.config.js")) {
|
||||
framework = "Vitest";
|
||||
testCmd = "npx vitest run --reporter verbose";
|
||||
if (test_file) testCmd += ` "${test_file}"`;
|
||||
} else if (hasFile("pytest.ini") || hasFile("pyproject.toml") || hasFile("setup.cfg")) {
|
||||
framework = "Pytest";
|
||||
testCmd = "python -m pytest -v";
|
||||
if (test_file) testCmd += ` "${test_file}"`;
|
||||
if (test_name) testCmd += ` -k "${test_name}"`;
|
||||
} else if (hasFile("package.json")) {
|
||||
// 尝试 package.json 中的 test 脚本
|
||||
framework = "npm test";
|
||||
testCmd = "npm test 2>&1";
|
||||
} else if (hasFile("requirements.txt")) {
|
||||
framework = "Pytest (default)";
|
||||
testCmd = "python -m pytest -v";
|
||||
if (test_file) testCmd += ` "${test_file}"`;
|
||||
}
|
||||
|
||||
if (!testCmd) {
|
||||
return `# 测试结果\n\n⚠️ 未检测到测试框架\n项目路径: ${project_path}\n\n建议:\n- JS/TS: npm install -D jest 或 vitest\n- Python: pip install pytest`;
|
||||
}
|
||||
|
||||
const result = await runCommand(testCmd, project_path);
|
||||
const fullOutput = (result.stdout + "\n" + result.stderr).trim();
|
||||
|
||||
// 解析测试结果
|
||||
let passed = 0, failed = 0, skipped = 0;
|
||||
|
||||
// Jest / Vitest 格式
|
||||
const jestMatch = fullOutput.match(/Tests:\s+(?:(\d+) failed,?\s*)?(?:(\d+) skipped,?\s*)?(?:(\d+) passed)?/);
|
||||
if (jestMatch) {
|
||||
failed = parseInt(jestMatch[1] || "0");
|
||||
skipped = parseInt(jestMatch[2] || "0");
|
||||
passed = parseInt(jestMatch[3] || "0");
|
||||
}
|
||||
|
||||
// Pytest 格式
|
||||
const pytestMatch = fullOutput.match(/(\d+) passed(?:.*?(\d+) failed)?(?:.*?(\d+) skipped)?/);
|
||||
if (pytestMatch) {
|
||||
passed = parseInt(pytestMatch[1] || "0");
|
||||
failed = parseInt(pytestMatch[2] || "0");
|
||||
skipped = parseInt(pytestMatch[3] || "0");
|
||||
}
|
||||
|
||||
const total = passed + failed + skipped;
|
||||
const allPassed = failed === 0 && result.code === 0;
|
||||
|
||||
const output: string[] = [
|
||||
`# 测试报告`,
|
||||
``,
|
||||
`📂 项目: ${project_path}`,
|
||||
`🔧 框架: ${framework}`,
|
||||
``,
|
||||
`## 结果`,
|
||||
allPassed ? "✅ **全部通过**" : "❌ **存在失败**",
|
||||
``,
|
||||
`| 状态 | 数量 |`,
|
||||
`|------|------|`,
|
||||
`| ✅ 通过 | ${passed} |`,
|
||||
`| ❌ 失败 | ${failed} |`,
|
||||
`| ⏭️ 跳过 | ${skipped} |`,
|
||||
`| 📊 总计 | ${total} |`,
|
||||
``,
|
||||
`## 命令`,
|
||||
`\`${testCmd}\``,
|
||||
``,
|
||||
`## 完整输出`,
|
||||
"```",
|
||||
fullOutput.slice(0, 5000),
|
||||
"```",
|
||||
];
|
||||
|
||||
if (failed > 0) {
|
||||
output.push(
|
||||
``,
|
||||
`## 建议`,
|
||||
`1. 查看上方失败的测试用例详情`,
|
||||
`2. 使用 code_debug 工具分析失败原因`,
|
||||
`3. 修复后重新运行 run_tests 验证`,
|
||||
);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
206
dev-assistant-mcp/src/tools/searchCode.ts
Normal file
206
dev-assistant-mcp/src/tools/searchCode.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { readdirSync, readFileSync, statSync } from "fs";
|
||||
import { join, extname, relative } from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const searchCodeTool = {
|
||||
name: "search_code",
|
||||
description:
|
||||
"在项目中搜索代码。支持正则表达式、文本搜索、文件名搜索。返回匹配的文件、行号和上下文。类似 grep/ripgrep。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
query: {
|
||||
type: "string",
|
||||
description: "搜索内容(支持正则表达式)",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
description: "搜索模式:content=代码内容(默认)、filename=文件名、symbol=函数/类名",
|
||||
enum: ["content", "filename", "symbol"],
|
||||
},
|
||||
includes: {
|
||||
type: "string",
|
||||
description: "文件过滤 glob(如 *.ts, *.py)",
|
||||
},
|
||||
case_sensitive: {
|
||||
type: "boolean",
|
||||
description: "是否区分大小写(默认 false)",
|
||||
},
|
||||
max_results: {
|
||||
type: "number",
|
||||
description: "最大结果数(默认 50)",
|
||||
},
|
||||
},
|
||||
required: ["project_path", "query"],
|
||||
},
|
||||
};
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
"node_modules", ".git", "__pycache__", "dist", "build", ".next",
|
||||
".nuxt", "coverage", ".cache", ".tsbuildinfo", "vendor",
|
||||
]);
|
||||
|
||||
const TEXT_EXTS = new Set([
|
||||
".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs", ".c", ".cpp", ".h",
|
||||
".css", ".scss", ".less", ".html", ".vue", ".svelte", ".json", ".yaml", ".yml",
|
||||
".toml", ".md", ".txt", ".sh", ".bat", ".ps1", ".sql", ".graphql", ".prisma",
|
||||
".env", ".gitignore", ".eslintrc", ".prettierrc",
|
||||
]);
|
||||
|
||||
interface SearchMatch {
|
||||
file: string;
|
||||
line: number;
|
||||
content: string;
|
||||
context_before?: string;
|
||||
context_after?: string;
|
||||
}
|
||||
|
||||
function searchInDir(
|
||||
dir: string,
|
||||
rootDir: string,
|
||||
regex: RegExp,
|
||||
mode: string,
|
||||
includeExt: string | null,
|
||||
results: SearchMatch[],
|
||||
maxResults: number,
|
||||
depth = 0
|
||||
): void {
|
||||
if (depth > 10 || results.length >= maxResults) return;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
if (results.length >= maxResults) break;
|
||||
if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
|
||||
|
||||
const fullPath = join(dir, entry);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
searchInDir(fullPath, rootDir, regex, mode, includeExt, results, maxResults, depth + 1);
|
||||
} else if (stat.isFile()) {
|
||||
const ext = extname(entry).toLowerCase();
|
||||
|
||||
// 文件名搜索
|
||||
if (mode === "filename") {
|
||||
if (regex.test(entry)) {
|
||||
results.push({ file: relative(rootDir, fullPath), line: 0, content: entry });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 内容搜索 - 只搜索文本文件
|
||||
if (!TEXT_EXTS.has(ext) && ext !== "") continue;
|
||||
if (includeExt && !entry.endsWith(includeExt.replace("*", ""))) continue;
|
||||
if (stat.size > 1024 * 512) continue; // 跳过 >512KB 的文件
|
||||
|
||||
const content = readFileSync(fullPath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (results.length >= maxResults) break;
|
||||
|
||||
if (mode === "symbol") {
|
||||
// 只匹配函数/类/接口定义
|
||||
const line = lines[i];
|
||||
if (/(?:function|class|interface|def|const|let|var|export)\s/.test(line) && regex.test(line)) {
|
||||
results.push({
|
||||
file: relative(rootDir, fullPath),
|
||||
line: i + 1,
|
||||
content: line.trim(),
|
||||
context_before: i > 0 ? lines[i - 1].trim() : undefined,
|
||||
context_after: i < lines.length - 1 ? lines[i + 1].trim() : undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (regex.test(lines[i])) {
|
||||
results.push({
|
||||
file: relative(rootDir, fullPath),
|
||||
line: i + 1,
|
||||
content: lines[i].trim(),
|
||||
context_before: i > 0 ? lines[i - 1].trim() : undefined,
|
||||
context_after: i < lines.length - 1 ? lines[i + 1].trim() : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function executeSearchCode(args: {
|
||||
project_path: string;
|
||||
query: string;
|
||||
mode?: string;
|
||||
includes?: string;
|
||||
case_sensitive?: boolean;
|
||||
max_results?: number;
|
||||
}): Promise<string> {
|
||||
const { project_path, query, mode = "content", includes, case_sensitive = false, max_results = 50 } = args;
|
||||
|
||||
const flags = case_sensitive ? "" : "i";
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(query, flags);
|
||||
} catch {
|
||||
regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
|
||||
}
|
||||
|
||||
const results: SearchMatch[] = [];
|
||||
const includeExt = includes ? includes.replace("*", "") : null;
|
||||
|
||||
searchInDir(project_path, project_path, regex, mode, includeExt, results, max_results);
|
||||
|
||||
if (results.length === 0) {
|
||||
return `# 搜索结果\n\n🔍 "${query}" (${mode})\n📂 ${project_path}\n\n_未找到匹配结果_`;
|
||||
}
|
||||
|
||||
// 按文件分组
|
||||
const grouped: Record<string, SearchMatch[]> = {};
|
||||
for (const r of results) {
|
||||
(grouped[r.file] || (grouped[r.file] = [])).push(r);
|
||||
}
|
||||
|
||||
const fileCount = Object.keys(grouped).length;
|
||||
const output: string[] = [
|
||||
`# 搜索结果`,
|
||||
``,
|
||||
`🔍 "${query}" | 模式: ${mode}${includes ? ` | 过滤: ${includes}` : ""}`,
|
||||
`📂 ${project_path}`,
|
||||
`📊 ${results.length} 个匹配,${fileCount} 个文件`,
|
||||
``,
|
||||
];
|
||||
|
||||
for (const [file, matches] of Object.entries(grouped)) {
|
||||
output.push(`## 📄 ${file} (${matches.length} 处)`);
|
||||
|
||||
for (const m of matches) {
|
||||
if (mode === "filename") {
|
||||
output.push(`- ${m.content}`);
|
||||
} else {
|
||||
output.push(`**行 ${m.line}:**`);
|
||||
if (m.context_before) output.push(`\`\`\`\n ${m.context_before}`);
|
||||
else output.push("```");
|
||||
output.push(`→ ${m.content}`);
|
||||
if (m.context_after) output.push(` ${m.context_after}\n\`\`\``);
|
||||
else output.push("```");
|
||||
}
|
||||
}
|
||||
output.push(``);
|
||||
}
|
||||
|
||||
if (results.length >= max_results) {
|
||||
output.push(`\n⚠️ 结果已截断(最大 ${max_results} 条),可增大 max_results 或缩小搜索范围`);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
241
dev-assistant-mcp/src/tools/workflow.ts
Normal file
241
dev-assistant-mcp/src/tools/workflow.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { executeProjectScan } from "./projectScan.js";
|
||||
import { executeLintCheck } from "./lintCheck.js";
|
||||
import { executeAutoFix } from "./autoFix.js";
|
||||
import { executeBuildProject } from "./buildProject.js";
|
||||
import { executeRunTests } from "./runTests.js";
|
||||
import { executeGitOps } from "./gitOps.js";
|
||||
import { executeDepManage } from "./depManage.js";
|
||||
|
||||
export const workflowTool = {
|
||||
name: "workflow",
|
||||
description:
|
||||
"自动化工作流编排。串联多个工具形成流水线,一键执行完整开发流程。内置多种预设流程(全量检查、CI 模拟、项目初始化等),也支持自定义步骤。",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "项目根目录(绝对路径)",
|
||||
},
|
||||
preset: {
|
||||
type: "string",
|
||||
description: "预设工作流",
|
||||
enum: [
|
||||
"full_check", // 全量检查:scan → lint → build → test
|
||||
"fix_and_verify", // 修复验证:auto_fix → lint → build → test
|
||||
"pre_commit", // 提交前检查:lint → build → test → git status
|
||||
"ci_simulate", // CI 模拟:install → lint → build → test → audit
|
||||
"quick_scan", // 快速扫描:scan → lint
|
||||
"deploy_prep", // 部署准备:lint → build → test → git status
|
||||
],
|
||||
},
|
||||
steps: {
|
||||
type: "string",
|
||||
description: "自定义步骤,逗号分隔(如 scan,lint,fix,build,test,git_status,audit)。与 preset 二选一。",
|
||||
},
|
||||
stop_on_error: {
|
||||
type: "boolean",
|
||||
description: "遇到错误是否停止(默认 true)",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
};
|
||||
|
||||
interface StepResult {
|
||||
name: string;
|
||||
success: boolean;
|
||||
duration: number;
|
||||
output: string;
|
||||
}
|
||||
|
||||
type StepFn = (projectPath: string) => Promise<string>;
|
||||
|
||||
const STEP_MAP: Record<string, { name: string; fn: StepFn }> = {
|
||||
scan: {
|
||||
name: "📋 项目扫描",
|
||||
fn: (p) => executeProjectScan({ project_path: p }),
|
||||
},
|
||||
lint: {
|
||||
name: "🔍 代码检查",
|
||||
fn: (p) => executeLintCheck({ project_path: p }),
|
||||
},
|
||||
fix: {
|
||||
name: "🔧 自动修复",
|
||||
fn: (p) => executeAutoFix({ project_path: p }),
|
||||
},
|
||||
build: {
|
||||
name: "🏗️ 构建项目",
|
||||
fn: (p) => executeBuildProject({ project_path: p }),
|
||||
},
|
||||
test: {
|
||||
name: "🧪 运行测试",
|
||||
fn: (p) => executeRunTests({ project_path: p }),
|
||||
},
|
||||
git_status: {
|
||||
name: "📊 Git 状态",
|
||||
fn: (p) => executeGitOps({ project_path: p, action: "status" }),
|
||||
},
|
||||
git_diff: {
|
||||
name: "📝 Git Diff",
|
||||
fn: (p) => executeGitOps({ project_path: p, action: "diff" }),
|
||||
},
|
||||
audit: {
|
||||
name: "🛡️ 安全审计",
|
||||
fn: (p) => executeDepManage({ project_path: p, action: "audit" }),
|
||||
},
|
||||
outdated: {
|
||||
name: "📦 过时依赖检查",
|
||||
fn: (p) => executeDepManage({ project_path: p, action: "outdated" }),
|
||||
},
|
||||
install: {
|
||||
name: "📥 安装依赖",
|
||||
fn: (p) => executeDepManage({ project_path: p, action: "install" }),
|
||||
},
|
||||
};
|
||||
|
||||
const PRESETS: Record<string, string[]> = {
|
||||
full_check: ["scan", "lint", "build", "test"],
|
||||
fix_and_verify: ["fix", "lint", "build", "test"],
|
||||
pre_commit: ["lint", "build", "test", "git_status"],
|
||||
ci_simulate: ["install", "lint", "build", "test", "audit"],
|
||||
quick_scan: ["scan", "lint"],
|
||||
deploy_prep: ["lint", "build", "test", "git_status"],
|
||||
};
|
||||
|
||||
export async function executeWorkflow(args: {
|
||||
project_path: string;
|
||||
preset?: string;
|
||||
steps?: string;
|
||||
stop_on_error?: boolean;
|
||||
}): Promise<string> {
|
||||
const { project_path, preset, steps, stop_on_error = true } = args;
|
||||
|
||||
// 确定步骤列表
|
||||
let stepKeys: string[];
|
||||
let workflowName: string;
|
||||
|
||||
if (steps) {
|
||||
stepKeys = steps.split(",").map((s) => s.trim());
|
||||
workflowName = "自定义工作流";
|
||||
} else if (preset && PRESETS[preset]) {
|
||||
stepKeys = PRESETS[preset];
|
||||
workflowName = `预设: ${preset}`;
|
||||
} else {
|
||||
// 默认全量检查
|
||||
stepKeys = PRESETS.full_check;
|
||||
workflowName = "预设: full_check(全量检查)";
|
||||
}
|
||||
|
||||
// 验证步骤
|
||||
const invalidSteps = stepKeys.filter((k) => !STEP_MAP[k]);
|
||||
if (invalidSteps.length > 0) {
|
||||
return `❌ 未知步骤: ${invalidSteps.join(", ")}\n\n可用步骤: ${Object.keys(STEP_MAP).join(", ")}`;
|
||||
}
|
||||
|
||||
const totalSteps = stepKeys.length;
|
||||
const startTime = Date.now();
|
||||
const results: StepResult[] = [];
|
||||
|
||||
const output: string[] = [
|
||||
`# 🚀 工作流执行`,
|
||||
``,
|
||||
`📂 ${project_path}`,
|
||||
`📋 ${workflowName}`,
|
||||
`📊 共 ${totalSteps} 个步骤`,
|
||||
`${stop_on_error ? "⛔ 遇错停止" : "⏭️ 遇错继续"}`,
|
||||
``,
|
||||
`---`,
|
||||
``,
|
||||
];
|
||||
|
||||
let hasError = false;
|
||||
|
||||
for (let i = 0; i < stepKeys.length; i++) {
|
||||
const key = stepKeys[i];
|
||||
const step = STEP_MAP[key];
|
||||
|
||||
output.push(`## [${i + 1}/${totalSteps}] ${step.name}`);
|
||||
|
||||
const stepStart = Date.now();
|
||||
try {
|
||||
const result = await step.fn(project_path);
|
||||
const duration = Date.now() - stepStart;
|
||||
|
||||
// 判断步骤是否成功(检查输出中的错误标志)
|
||||
const isError = result.includes("❌") && !result.includes("✅");
|
||||
const stepSuccess = !isError;
|
||||
|
||||
results.push({
|
||||
name: step.name,
|
||||
success: stepSuccess,
|
||||
duration,
|
||||
output: result,
|
||||
});
|
||||
|
||||
output.push(
|
||||
`${stepSuccess ? "✅ 通过" : "❌ 失败"} (${(duration / 1000).toFixed(1)}s)`,
|
||||
``,
|
||||
`<details>`,
|
||||
`<summary>查看详情</summary>`,
|
||||
``,
|
||||
result.slice(0, 3000),
|
||||
``,
|
||||
`</details>`,
|
||||
``
|
||||
);
|
||||
|
||||
if (!stepSuccess) {
|
||||
hasError = true;
|
||||
if (stop_on_error) {
|
||||
output.push(`\n⛔ **遇到错误,工作流停止。** 后续步骤: ${stepKeys.slice(i + 1).join(" → ")}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
const duration = Date.now() - stepStart;
|
||||
results.push({ name: step.name, success: false, duration, output: error.message });
|
||||
output.push(`❌ 异常 (${(duration / 1000).toFixed(1)}s): ${error.message}`, ``);
|
||||
hasError = true;
|
||||
if (stop_on_error) {
|
||||
output.push(`\n⛔ **工作流中断**`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 总结报告
|
||||
const totalDuration = Date.now() - startTime;
|
||||
const passed = results.filter((r) => r.success).length;
|
||||
const failed = results.filter((r) => !r.success).length;
|
||||
|
||||
output.push(
|
||||
``,
|
||||
`---`,
|
||||
``,
|
||||
`# 📊 工作流总结`,
|
||||
``,
|
||||
`| 项目 | 值 |`,
|
||||
`|------|------|`,
|
||||
`| 总耗时 | ${(totalDuration / 1000).toFixed(1)}s |`,
|
||||
`| 已执行 | ${results.length}/${totalSteps} |`,
|
||||
`| ✅ 通过 | ${passed} |`,
|
||||
`| ❌ 失败 | ${failed} |`,
|
||||
`| 最终结果 | ${hasError ? "❌ 有问题需要处理" : "✅ 全部通过"} |`,
|
||||
``
|
||||
);
|
||||
|
||||
if (hasError) {
|
||||
output.push(
|
||||
`## 💡 建议`,
|
||||
`1. 查看失败步骤的详情`,
|
||||
`2. 使用 \`auto_fix\` 自动修复格式问题`,
|
||||
`3. 使用 \`code_debug\` 分析编译错误`,
|
||||
`4. 修复后运行 \`workflow preset=fix_and_verify\` 验证`,
|
||||
);
|
||||
} else {
|
||||
output.push(`✨ **所有检查通过,代码质量良好!**`);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user