/**
* Repository cloning, document summarization, and Coze mind map generation.
*/
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync, createReadStream } from "node:fs";
import { join, basename } from "node:path";
import { fetch } from "undici";
import { gitcodeRequest } from "./gitcode.js";
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream";
import { promisify } from "node:util";
import { Extract } from "unzipper";
export interface MindmapInput {
repoUrl: string;
branch?: string;
targetDir?: string;
summaryFile: string;
prompt: string;
}
// 检查git是否可用
async function isGitAvailable(): Promise<boolean> {
return new Promise((resolve) => {
const gitProcess = spawn("git", ["--version"], { stdio: "pipe", shell: process.platform === "win32" });
gitProcess.on("close", (code) => {
resolve(code === 0);
});
gitProcess.on("error", () => {
resolve(false);
});
});
}
// 通过API获取仓库信息
async function getRepoInfo(repoUrl: string): Promise<any> {
try {
// 从URL中提取所有者和仓库名
const urlMatch = repoUrl.match(/https?:\/\/(?:www\.)?([^\/]+)\/([^\/]+)\/([^\/\.]+)/);
if (!urlMatch) {
throw new Error("无法解析仓库URL格式");
}
const [, domain, owner, repo] = urlMatch;
// 确定API基础URL
let baseUrl = "https://gitcode.com/api/v5";
if (domain === "gitcode.net") {
baseUrl = "https://gitcode.net/api/v5";
}
// 调用API获取仓库信息
const response = await gitcodeRequest({
token: "", // 公开仓库不需要token
method: "GET",
path: `/repos/${owner}/${repo}`,
baseUrl
});
if (!response.ok) {
throw new Error(`获取仓库信息失败: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(`[Mindmap] 获取仓库信息失败:`, error);
throw error;
}
}
// 下载并解压ZIP文件
async function downloadAndExtractZip(repoUrl: string, targetDir: string, branch?: string): Promise<void> {
try {
// 从URL中提取所有者和仓库名
const urlMatch = repoUrl.match(/https?:\/\/(?:www\.)?([^\/]+)\/([^\/]+)\/([^\/\.]+)/);
if (!urlMatch) {
throw new Error("无法解析仓库URL格式");
}
const [, domain, owner, repo] = urlMatch;
// 确定下载URL
let zipUrl = "";
if (domain.includes("github")) {
branch = branch || "main";
zipUrl = `https://github.com/${owner}/${repo}/archive/refs/heads/${branch}.zip`;
} else if (domain.includes("gitcode")) {
branch = branch || "master";
zipUrl = `https://${domain}/${owner}/${repo}/archive/refs/heads/${branch}.zip`;
} else {
throw new Error(`不支持的Git托管平台: ${domain}`);
}
console.log(`[Mindmap] 正在从URL下载ZIP文件: ${zipUrl}`);
// 创建临时ZIP文件路径
const tempZipPath = join(targetDir, `${repo}.zip`);
// 下载ZIP文件
const response = await fetch(zipUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
}
// 检查响应内容类型
const contentType = response.headers.get('content-type') || '';
console.log(`[Mindmap] 下载文件类型: ${contentType}`);
if (!contentType.includes('zip') && !contentType.includes('octet-stream')) {
console.warn(`[Mindmap] 警告: 下载的文件可能不是ZIP格式,内容类型: ${contentType}`);
}
// 创建写入流并下载
const streamPipeline = promisify(pipeline);
await streamPipeline(response.body!, createWriteStream(tempZipPath));
console.log(`[Mindmap] ZIP文件下载完成,大小: ${statSync(tempZipPath).size} 字节`);
// 验证ZIP文件
try {
const fs = require('fs');
const zipBuffer = fs.readFileSync(tempZipPath);
// 检查ZIP文件头(PK前缀)
if (zipBuffer.length < 4 ||
zipBuffer[0] !== 0x50 ||
zipBuffer[1] !== 0x4B) {
throw new Error('下载的文件不是有效的ZIP格式');
}
console.log(`[Mindmap] ZIP文件验证通过,正在解压...`);
} catch (error) {
throw new Error(`ZIP文件验证失败: ${error instanceof Error ? error.message : String(error)}`);
}
// 解压ZIP文件,添加更多错误处理
await new Promise<void>((resolve, reject) => {
const extractStream = createReadStream(tempZipPath)
.pipe(Extract({ path: targetDir }))
.on('close', () => {
console.log(`[Mindmap] 解压完成`);
resolve();
})
.on('error', (err) => {
console.error(`[Mindmap] 解压错误:`, err);
reject(new Error(`ZIP文件解压失败: ${err instanceof Error ? err.message : String(err)}`));
});
// 添加进度跟踪
extractStream.on('entry', (entry) => {
console.log(`[Mindmap] 正在解压: ${entry.path}`);
});
});
// 删除临时ZIP文件
try {
const fs = require('fs');
fs.unlinkSync(tempZipPath);
console.log(`[Mindmap] 解压完成,已删除临时ZIP文件`);
} catch (error) {
console.warn(`[Mindmap] 警告: 无法删除临时ZIP文件: ${error}`);
}
// 如果解压后的目录是嵌套的(如 repo-master),需要移动内容到目标目录
try {
const entries = readdirSync(targetDir);
const nestedDir = entries.find(entry => {
const fullPath = join(targetDir, entry);
try {
return statSync(fullPath).isDirectory() && entry.includes("-");
} catch (e) {
return false;
}
});
if (nestedDir) {
const nestedPath = join(targetDir, nestedDir);
const nestedFiles = readdirSync(nestedPath);
// 移动所有文件到目标目录
for (const file of nestedFiles) {
const srcPath = join(nestedPath, file);
const destPath = join(targetDir, file);
// 读取文件/目录
const stat = statSync(srcPath);
if (stat.isDirectory()) {
// 如果是目录,递归复制
const fsExtra = require('fs-extra');
fsExtra.copySync(srcPath, destPath);
} else {
// 如果是文件,直接移动
const fs = require('fs');
fs.renameSync(srcPath, destPath);
}
}
// 删除空目录
const fsExtra = require('fs-extra');
fsExtra.removeSync(nestedPath);
console.log(`[Mindmap] 已将文件从嵌套目录移动到目标位置`);
}
} catch (error) {
console.warn(`[Mindmap] 警告: 移动嵌套目录文件失败: ${error}`);
}
} catch (error) {
console.error(`[Mindmap] 下载并解压ZIP文件失败:`, error);
throw error;
}
}
// 调用Coze工作流生成思维导图URL
async function runCozeWorkflow(inputText: string): Promise<string> {
const cozeToken = "pat_E3aM0OdyJ0x8VG8UJIJ5SJq8C2fRdZKU4A5LCjYxxnPWmymFOUgIT2Nq2xknpGq8";
const workflowId = "7535494365978214438";
const appId = "7535445185598308415";
const apiUrl = 'https://api.coze.cn/v1/workflow/stream_run';
console.log(`[Mindmap] 调用Coze工作流处理输入...`);
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${cozeToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
workflow_id: workflowId,
app_id: appId,
parameters: { input: inputText },
}),
});
// 处理流式响应,直接输出完整内容
let mindmapUrl = "";
const reader = response.body?.getReader();
let allResponseContent = ''; // 用于记录所有响应内容
if (reader) {
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (value) {
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
allResponseContent += chunk;
// 处理每个完整的JSON行
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
// 尝试解析JSON行
const jsonLine = JSON.parse(line);
// 根据用户提供的示例格式,检查node_is_finish和content字段
if (jsonLine.node_is_finish === true && jsonLine.content) {
try {
// 尝试解析content字段中的JSON
const contentObj = JSON.parse(jsonLine.content);
if (contentObj.output) {
// 清理URL中的前后空格和反引号
mindmapUrl = contentObj.output.trim().replace(/^`+|`+$/g, '');
console.log(`[Mindmap] 从完成节点的content.output中获取到URL: ${mindmapUrl}`);
}
} catch (parseError) {
// 如果解析content失败,使用正则表达式直接查找URL
const urlRegex = /https?:\/\/[^\s"'>]+/g;
const matches = jsonLine.content.match(urlRegex);
if (matches && matches.length > 0) {
mindmapUrl = matches[0].trim().replace(/^`+|`+$/g, '');
console.log(`[Mindmap] 通过正则从content中获取到URL: ${mindmapUrl}`);
}
}
}
} catch (parseError) {
console.warn(`[Mindmap] 解析JSON行失败:`, parseError);
}
}
}
}
// 处理最后的buffer内容
if (buffer.trim()) {
try {
const jsonLine = JSON.parse(buffer);
if (jsonLine.node_is_finish === true && jsonLine.content) {
try {
const contentObj = JSON.parse(jsonLine.content);
if (contentObj.output) {
mindmapUrl = contentObj.output.trim().replace(/^`+|`+$/g, '');
console.log(`[Mindmap] 从最后的buffer中获取到URL: ${mindmapUrl}`);
}
} catch (parseError) {
const urlRegex = /https?:\/\/[^\s"'>]+/g;
const matches = jsonLine.content.match(urlRegex);
if (matches && matches.length > 0) {
mindmapUrl = matches[0].trim().replace(/^`+|`+$/g, '');
console.log(`[Mindmap] 从最后的buffer中通过正则获取到URL: ${mindmapUrl}`);
}
}
}
} catch (parseError) {
console.warn(`[Mindmap] 解析最后的buffer失败:`, parseError);
}
}
}
// 输出完整的API响应内容,而不是只返回URL
console.log(`[Mindmap] 完整API响应内容:`);
console.log(allResponseContent);
// 返回完整的API响应内容,让MCP客户端自行筛选
return allResponseContent;
} catch (error) {
console.error(`[Mindmap] Coze工作流调用出错:`, error);
throw error;
}
}
// Executes the 'git clone' command.
async function gitClone(repoUrl: string, targetDir: string, branch?: string): Promise<void> {
// 检查并清理目标目录(如果存在且不为空)
try {
const fs = require('fs');
const path = require('path');
if (fs.existsSync(targetDir)) {
// 检查目录是否为空
const files = fs.readdirSync(targetDir);
if (files.length > 0) {
console.log(`[Mindmap] 目标目录 ${targetDir} 已存在且不为空,正在清理...`);
// 删除目录及其所有内容
fs.rmSync(targetDir, { recursive: true, force: true });
console.log(`[Mindmap] 成功清理目标目录 ${targetDir}`);
}
}
} catch (err) {
console.error(`[Mindmap] 清理目标目录时出错:`, err);
}
return new Promise<void>((resolve, reject) => {
// Use --depth=1 for a shallow clone to speed up process.
const args = ["clone", "--depth=1"];
if (branch) {
args.push("-b", branch);
}
args.push(repoUrl, targetDir);
// Determine git command to use
let gitCommand = "git";
// In Windows, try common git installation paths if git is not in PATH
if (process.platform === "win32") {
const possibleGitPaths = [
"C:\\Program Files\\Git\\bin\\git.exe",
"C:\\Program Files (x86)\\Git\\bin\\git.exe",
"C:\\ProgramData\\Git\\bin\\git.exe"
];
// Use first existing git path, or fall back to just "git"
// For now, we'll still use "git" but provide better error messages
gitCommand = "git";
}
console.log(`[Mindmap] 克隆仓库: git ${args.join(' ')}`);
const gitProcess = spawn(gitCommand, args, {
stdio: "pipe",
shell: process.platform === "win32",
env: { ...process.env, LANG: "en_US.UTF-8" } // Ensure consistent error messages
});
let stderr = '';
let stdout = '';
gitProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
gitProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
gitProcess.on("close", (code) => {
if (code === 0) {
console.log(`[Mindmap] 成功克隆到 ${targetDir}`);
resolve();
} else {
console.error(`[Mindmap] git clone 失败,退出码 ${code}:\n${stderr}`);
// Provide a more helpful error message for Windows users
let errorMessage = `git clone 失败,退出码: ${code}\n${stderr}`;
if (stderr.includes("不是内部或外部命令") || stderr.includes("not recognized") || code === 127) {
errorMessage += "\n\n提示: 系统中未找到git命令。请确保:\n1. 已安装Git for Windows\n2. git已添加到系统PATH环境变量\n3. 或者尝试使用完整的git路径,如: C:\\Program Files\\Git\\bin\\git.exe";
}
reject(new Error(errorMessage));
}
});
gitProcess.on("error", (err) => {
console.error('[Mindmap] 启动git进程失败:', err);
// Provide a more helpful error message
let errorMessage = `启动git进程失败: ${err.message}`;
if (err.message.includes("ENOENT")) {
errorMessage += "\n\n提示: 系统中未找到git命令。请确保:\n1. 已安装Git for Windows\n2. git已添加到系统PATH环境变量";
}
reject(new Error(errorMessage));
});
});
}
// Recursively collects Markdown files from a directory.
function collectMarkdownFiles(dir: string, fileList: string[] = []): string[] {
const entries = readdirSync(dir);
for (const entry of entries) {
// Avoid traversing the .git directory for efficiency.
if (entry === '.git') continue;
const fullPath = join(dir, entry);
try {
const stat = statSync(fullPath);
if (stat.isDirectory()) {
collectMarkdownFiles(fullPath, fileList);
} else if (stat.isFile() && (entry.toLowerCase().endsWith(".md") || entry.toLowerCase() === 'readme')) {
fileList.push(fullPath);
}
} catch (error) {
console.warn(`[Mindmap] 无法读取文件 ${fullPath},跳过。`, error);
}
}
return fileList;
}
// Merges collected markdown files into a single summary file.
function buildSummary(mdFiles: string[], outputPath: string): string {
console.log(`[Mindmap] 合并 ${mdFiles.length} 个markdown文件...`);
const summaryContent = mdFiles.map(file => {
try {
const content = readFileSync(file, "utf-8");
// Add a clear header for each file's content.
return `\n\n# 文件: ${basename(file)}\n\n${content}`;
} catch (error) {
console.warn(`[Mindmap] 无法读取文件 ${file},跳过。`, error);
return '';
}
}).join("\n\n---\n");
writeFileSync(outputPath, summaryContent, "utf-8");
console.log(`[Mindmap] 汇总文件已写入 ${outputPath}`);
return summaryContent;
}
// 递归查找所有README.md文件
function findReadmeFiles(dir: string, fileList: string[] = []): string[] {
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
try {
const stat = statSync(fullPath);
if (stat.isDirectory()) {
findReadmeFiles(fullPath, fileList);
} else if (stat.isFile() && entry.toLowerCase() === 'readme.md') {
fileList.push(fullPath);
}
} catch (error) {
console.warn(`[Mindmap] 无法读取路径 ${fullPath},跳过。`, error);
}
}
return fileList;
}
// 合并多个README文件内容
function buildReadmeSummary(readmeFiles: string[]): string {
if (readmeFiles.length === 0) {
throw new Error('在当前目录及其子目录中未找到任何 README.md 文件');
}
console.log(`[Mindmap] 合并 ${readmeFiles.length} 个README.md文件...`);
const summaryContent = readmeFiles.map(file => {
try {
const content = readFileSync(file, "utf-8");
// 添加清晰的标题来标识每个文件的内容
return `\n\n# 文件: ${file}\n\n${content}`;
} catch (error) {
console.warn(`[Mindmap] 无法读取文件 ${file},跳过。`, error);
return '';
}
}).join("\n\n---\n");
return summaryContent;
}
// 生成当前项目README.md的思维导图
export async function generateReadmeMindmap(): Promise<{ mindmapUrl: string; message: string }> {
try {
// 在当前目录及其子目录中查找所有README.md文件
const readmeFiles = findReadmeFiles(process.cwd());
if (readmeFiles.length === 0) {
throw new Error('在当前目录及其子目录中未找到任何 README.md 文件');
}
console.log(`[Mindmap] 找到 ${readmeFiles.length} 个README.md文件`);
readmeFiles.forEach(file => console.log(` - ${file}`));
// 合并所有README文件内容
const readmeContent = buildReadmeSummary(readmeFiles);
// 调用Coze工作流生成思维导图,现在返回完整的API响应
const apiResponse = await runCozeWorkflow(`请根据以下README.md内容生成一个思维导图:\n\n${readmeContent}`);
// 尝试从响应中提取思维导图URL(用于向后兼容)
let extractedUrl = "";
try {
// 检查响应中是否包含URL
const urlRegex = /https?:\/\/[^\s"'>]+/g;
const matches = apiResponse.match(urlRegex);
if (matches && matches.length > 0) {
extractedUrl = matches[0].trim().replace(/^`+|`+$/g, '');
}
} catch (e) {
console.warn(`[Mindmap] 从响应中提取URL失败:`, e);
}
return {
mindmapUrl: apiResponse, // 返回完整的API响应,而不是只返回URL
message: `已成功基于 ${readmeFiles.length} 个README.md文件生成思维导图`
};
} catch (error) {
console.error(`[Mindmap] 生成README思维导图失败:`, error);
throw error instanceof Error ? error : new Error(String(error));
}
}
// Main exported function for the tool.
export async function cloneRepoAndMindmap(input: MindmapInput) {
// 首先检查git是否可用
const gitIsAvailable = await isGitAvailable();
if (!gitIsAvailable) {
try {
console.log(`[Mindmap] 检测到系统未安装Git,尝试通过下载ZIP方式获取项目...`);
// 尝试通过API获取仓库信息
const repoInfo = await getRepoInfo(input.repoUrl);
const repoName = repoInfo.name || basename(input.repoUrl, ".git");
const targetDir = input.targetDir || repoName;
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
// 尝试下载并解压ZIP文件
try {
const defaultBranch = repoInfo.default_branch || "master";
await downloadAndExtractZip(input.repoUrl, targetDir, defaultBranch);
console.log(`[Mindmap] 成功下载并解压项目到 ${targetDir}`);
// 收集Markdown文件并生成思维导图
const mdFiles = collectMarkdownFiles(targetDir);
const summaryPath = join(targetDir, input.summaryFile);
const summaryContent = buildSummary(mdFiles, summaryPath);
const finalInput = input.prompt ? `${input.prompt}\n\n${summaryContent}` : summaryContent;
const outputUrl = await runCozeWorkflow(finalInput);
return {
repoDir: targetDir,
summaryFile: summaryPath,
mindmapUrl: outputUrl,
message: `系统中未安装Git,已成功通过ZIP下载方式获取整个项目。建议安装Git以获得更好的体验:\n1. 从 https://git-scm.com/download/win 下载并安装Git for Windows\n2. 在安装过程中确保选择"Use Git from the Windows Command Prompt"选项`
};
} catch (zipError) {
console.error(`[Mindmap] ZIP下载失败:`, zipError);
// 创建README文件包含仓库信息和下载链接
const readmeContent = `# ${repoInfo.full_name || repoName}\n\n## 仓库信息\n\n**描述**: ${repoInfo.description || "无描述"}\n**所有者**: ${repoInfo.owner?.login || "未知"}\n**默认分支**: ${repoInfo.default_branch || "main"}\n**星标数**: ${repoInfo.stargazers_count || 0}\n**Fork数**: ${repoInfo.forks_count || 0}\n\n## 获取代码方式\n\n系统中未安装Git,且自动下载失败。您可以通过以下方式获取代码:\n\n1. **安装Git后克隆** (推荐):\n - 安装Git for Windows: https://git-scm.com/download/win\n - 安装后执行: git clone ${input.repoUrl}\n\n2. **手动下载ZIP文件**:\n - 访问仓库主页: ${repoInfo.html_url || input.repoUrl}\n - 点击"Download"或"Code"按钮,然后选择"Download ZIP"\n - 下载后解压到当前目录\n\n## 错误信息\n\n自动下载失败原因: ${zipError instanceof Error ? zipError.message : String(zipError)}\n`;
const summaryPath = join(targetDir, input.summaryFile);
writeFileSync(summaryPath, readmeContent, "utf-8");
// 尝试基于仓库信息生成思维导图
const prompt = `请基于以下Git仓库信息生成一个思维导图:\n\n仓库: ${repoInfo.full_name || repoName}\n描述: ${repoInfo.description || "无描述"}\n默认分支: ${repoInfo.default_branch || "main"}\n星标数: ${repoInfo.stargazers_count || 0}\nFork数: ${repoInfo.forks_count || 0}\n主页: ${repoInfo.html_url || input.repoUrl}\n\n${input.prompt || ""}`;
try {
const outputUrl = await runCozeWorkflow(prompt);
return {
repoDir: targetDir,
summaryFile: summaryPath,
mindmapUrl: outputUrl,
message: `系统中未安装Git,自动下载失败,已基于API获取的仓库信息生成思维导图。\n\n安装Git的方法:\n1. 从 https://git-scm.com/download/win 下载并安装Git for Windows\n2. 在安装过程中确保选择"Use Git from the Windows Command Prompt"选项\n\n自动下载失败原因: ${zipError instanceof Error ? zipError.message : String(zipError)}`
};
} catch (workflowError) {
return {
repoDir: targetDir,
summaryFile: summaryPath,
mindmapUrl: "",
message: `系统中未安装Git,自动下载和思维导图生成均失败。\n\n安装Git的方法:\n1. 从 https://git-scm.com/download/win 下载并安装Git for Windows\n2. 在安装过程中确保选择"Use Git from the Windows Command Prompt"选项\n\n自动下载失败原因: ${zipError instanceof Error ? zipError.message : String(zipError)}\n\n思维导图生成失败: ${workflowError instanceof Error ? workflowError.message : String(workflowError)}`
};
}
}
} catch (error) {
throw new Error(`系统中未安装Git,且无法获取仓库信息或下载项目。\n\n请安装Git后重试:\n1. 从 https://git-scm.com/download/win 下载并安装Git for Windows\n2. 在安装过程中确保选择"Use Git from the Windows Command Prompt"选项\n3. 重新启动命令行或IDE\n4. 运行 git --version 验证安装成功\n\n错误详情: ${error instanceof Error ? error.message : String(error)}`);
}
}
// 如果git可用,执行原有的克隆流程
const targetDir = input.targetDir || basename(input.repoUrl, ".git");
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
await gitClone(input.repoUrl, targetDir, input.branch);
const mdFiles = collectMarkdownFiles(targetDir);
const summaryPath = join(targetDir, input.summaryFile);
const summaryContent = buildSummary(mdFiles, summaryPath);
const finalInput = input.prompt ? `${input.prompt}\n\n${summaryContent}` : summaryContent;
const outputUrl = await runCozeWorkflow(finalInput);
return {
repoDir: targetDir,
summaryFile: summaryPath,
mindmapUrl: outputUrl,
message: "Repository cloned, documents summarized, and mind map link generated successfully."
};
}