/**
* 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] 下载ZIP文件: ${zipUrl}`);
// 创建临时ZIP文件路径
const tempZipPath = join(targetDir, `${repo}.zip`);
// 下载ZIP文件
console.log(`[Mindmap] 正在从URL下载ZIP文件: ${zipUrl}`);
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;
}
}
// Executes the 'git clone' command.
async function gitClone(repoUrl: string, targetDir: string, branch?: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
// Use --depth=1 for a shallow clone to speed up the process.
const args = ["clone", "--depth=1"];
if (branch) {
args.push("-b", branch);
}
args.push(repoUrl, targetDir);
// Determine the 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 the 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;
}
// Calls the Coze workflow to generate a mind map URL.
async function runCozeWorkflow(inputText: string): Promise<string> {
// IMPORTANT: These credentials should ideally be stored in environment variables, not hardcoded.
const cozeToken = "pat_E3aM0OdyJ0x8VG8UJIJ5SJq8C2fRdZKU4A5LCjYxxnPWmymFOUgIT2Nq2xknpGq8";
const workflowId = "7535494365978214438";
const appId = "7535445185598308415";
const apiUrl = 'https://api.coze.cn/v1/workflow/stream_run';
console.log(`[Mindmap] 调用Coze工作流处理长度为 ${inputText.length} 的输入...`);
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 },
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[Mindmap] Coze API请求失败: ${response.status}`, errorText);
throw new Error(`Coze API请求失败: ${response.status} ${response.statusText}`);
}
// 处理流式响应
let mindmapUrl: string | undefined;
const reader = response.body?.getReader();
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;
// 处理每个完整的JSON行
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留最后不完整的行
for (const line of lines) {
if (!line.trim()) continue; // 跳过空行
try {
const jsonLine = JSON.parse(line);
// 查找包含思维导图URL的消息
if (jsonLine.event === 'Message' &&
jsonLine.data &&
jsonLine.data.content) {
try {
const contentData = JSON.parse(jsonLine.data.content);
if (contentData.output) {
mindmapUrl = contentData.output;
console.log(`[Mindmap] 找到思维导图URL: ${mindmapUrl}`);
break;
}
} catch (e) {
console.warn(`[Mindmap] 解析内容数据失败:`, e);
}
}
// 检查是否有错误
if (jsonLine.event === 'Error') {
throw new Error(`Coze工作流错误: ${jsonLine.data?.message || '未知错误'}`);
}
// 检查是否完成
if (jsonLine.event === 'Done' && !mindmapUrl) {
console.warn(`[Mindmap] 工作流已完成,但未找到思维导图URL`);
}
} catch (e) {
console.warn(`[Mindmap] 解析JSON行失败: ${line}`, e);
}
}
// 如果已经找到URL,可以提前结束
if (mindmapUrl) {
break;
}
}
}
}
if (!mindmapUrl) {
console.error('[Mindmap] 无法在流式响应中找到思维导图URL');
throw new Error('无法在Coze API响应中找到思维导图URL。请检查输入内容或联系管理员。');
}
if (result.code !== 0) {
console.error('[Mindmap] Coze API返回错误:', result);
throw new Error(`Coze API返回错误: ${result.msg || result.message || '未知错误'}`);
}
let mindmapUrl: string | undefined;
// 流式API可能直接返回数据或包含在不同字段中
if (result.data && typeof result.data === 'string') {
try {
const parsedData = JSON.parse(result.data);
mindmapUrl = parsedData?.output || parsedData?.url || parsedData?.result;
} catch (e) {
console.error('[Mindmap] 解析Coze API响应中的data字符串失败:', e);
}
}
// 也检查其他可能的字段
if (!mindmapUrl) {
mindmapUrl = result.data || result.output || result.url || result.result;
}
if (!mindmapUrl || typeof mindmapUrl !== 'string') {
const fullResponse = JSON.stringify(result, null, 2);
console.error('[Mindmap] 无法在Coze响应中找到思维导图URL。完整响应:', fullResponse);
throw new Error(`Could not find mind map URL in Coze API response. Full response:
${fullResponse}`);
}
console.log(`[Mindmap] 成功生成思维导图URL: ${mindmapUrl}`);
return mindmapUrl;
}
// 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"选项\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."
};
}