index.ts•19 kB
import { ToolDefinition, ToolResponse } from '../types.js';
import { Config } from '../../config/config.js';
import { HttpsProxyAgent } from 'https-proxy-agent';
import fetch from 'node-fetch';
import * as fs from 'fs';
import * as path from 'path';
// GitHub API 类型定义
interface GitHubContentItem {
type: 'file' | 'dir';
name: string;
path: string;
content?: string;
}
interface GitHubTreeItem {
type: 'tree' | 'blob';
path: string;
}
interface GitHubTreeResponse {
tree: GitHubTreeItem[];
}
interface GitHubRepoSearchItem {
full_name: string;
stargazers_count: number;
description: string | null;
}
interface GitHubRepoSearchResponse {
items: GitHubRepoSearchItem[];
}
interface GitHubCodeSearchItem {
repository: {
full_name: string;
};
path: string;
score: number;
}
interface GitHubCodeSearchResponse {
items: GitHubCodeSearchItem[];
}
interface GitHubRepo {
name: string;
description: string | null;
private: boolean;
default_branch: string;
}
interface CreateRepoParams {
name: string;
description?: string;
private?: boolean;
auto_init?: boolean;
}
interface UpdateRepoParams {
name: string;
description?: string;
private?: boolean;
default_branch?: string;
}
// GitHub 管理器类
class GitHubManager {
private baseUrl: string = "https://api.github.com";
private downloadDir: string;
constructor(
private readonly config: Config
) {
// 使用当前目录下的 github 目录存储下载的文件
this.downloadDir = path.join(process.cwd(), 'github');
// 确保下载目录存在
if (!fs.existsSync(this.downloadDir)) {
fs.mkdirSync(this.downloadDir, { recursive: true });
}
}
private getHeaders() {
return {
'Authorization': `token ${this.config.github.token}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'project-tools'
};
}
private async fetchWithProxy<T>(url: string, options: any = {}): Promise<T> {
const proxy = this.config.network.proxy;
const fetchOptions = {
...options,
headers: this.getHeaders(),
};
if (proxy) {
fetchOptions.agent = new HttpsProxyAgent(proxy);
}
const response = await fetch(url, fetchOptions);
if (!response.ok) {
const error = await response.text();
throw new Error(`GitHub API error: ${response.status} - ${error}`);
}
return await response.json() as Promise<T>;
}
private validateToken(): void {
if (!this.config.github.token) {
throw new Error('请先在配置文件中设置有效的 GitHub Token');
}
}
// 列出仓库内容
async listContents(args: { owner: string; repo: string; path?: string; branch?: string }): Promise<ToolResponse> {
try {
this.validateToken();
const branch = args.branch || 'main';
let url = `${this.baseUrl}/repos/${args.owner}/${args.repo}/contents`;
if (args.path) {
url += `/${args.path}`;
}
url += `?ref=${branch}`;
const data = await this.fetchWithProxy<GitHubContentItem | GitHubContentItem[]>(url);
const items = Array.isArray(data) ? data : [data];
const formattedText = items.map(item =>
`${item.type === 'dir' ? '[目录]' : '[文件]'} ${item.path}`
).join('\n');
return {
content: [{
type: 'text',
text: formattedText || '仓库为空'
}]
};
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return {
content: [{
type: 'text',
text: `仓库 ${args.owner}/${args.repo} 不存在或无访问权限`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 获取仓库树结构
async getTree(args: { owner: string; repo: string; branch?: string }): Promise<ToolResponse> {
try {
this.validateToken();
const branch = args.branch || "main";
const url = `${this.baseUrl}/repos/${args.owner}/${args.repo}/git/trees/${branch}?recursive=1`;
const data = await this.fetchWithProxy<GitHubTreeResponse>(url);
const formattedText = data.tree.map(item =>
`${item.type === 'tree' ? '[目录]' : '[文件]'} ${item.path}`
).join('\n');
return {
content: [{
type: 'text',
text: formattedText || '仓库为空'
}]
};
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return {
content: [{
type: 'text',
text: `仓库 ${args.owner}/${args.repo} 不存在或无访问权限`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 搜索仓库
async searchRepos(args: { query: string }): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/search/repositories?q=${encodeURIComponent(args.query)}`;
const data = await this.fetchWithProxy<GitHubRepoSearchResponse>(url);
if (data.items.length === 0) {
return {
content: [{
type: 'text',
text: '未找到匹配的仓库'
}]
};
}
const formattedText = data.items.slice(0, 10).map(repo =>
`${repo.full_name} (★${repo.stargazers_count})\n${repo.description || '无描述'}`
).join('\n\n');
return {
content: [{
type: 'text',
text: formattedText
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 搜索代码
async searchCode(args: { query: string }): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/search/code?q=${encodeURIComponent(args.query)}`;
const data = await this.fetchWithProxy<GitHubCodeSearchResponse>(url);
if (data.items.length === 0) {
return {
content: [{
type: 'text',
text: '未找到匹配的代码'
}]
};
}
const formattedText = data.items.slice(0, 10).map(item =>
`[${item.repository.full_name}] ${item.path} (相关度: ${item.score.toFixed(2)})`
).join('\n');
return {
content: [{
type: 'text',
text: formattedText
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 获取文件内容
async getFileContent(args: { owner: string; repo: string; path: string; save?: boolean }): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/repos/${args.owner}/${args.repo}/contents/${args.path}`;
const data = await this.fetchWithProxy<GitHubContentItem>(url);
if (data.type !== 'file' || !data.content) {
throw new Error('不是一个文件或文件为空');
}
const content = Buffer.from(data.content, 'base64').toString('utf8');
// 如果需要保存文件
if (args.save) {
// 确保下载目录存在
if (!fs.existsSync(this.downloadDir)) {
await fs.promises.mkdir(this.downloadDir, { recursive: true });
}
const savePath = path.join(this.downloadDir, `${args.owner}_${args.repo}_${path.basename(args.path)}`);
await fs.promises.writeFile(savePath, content, 'utf8');
return {
content: [{
type: 'text',
text: `文件已保存到: ${savePath}\n\n${content}`
}]
};
}
return {
content: [{
type: 'text',
text: content
}]
};
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return {
content: [{
type: 'text',
text: `文件 ${args.path} 在仓库 ${args.owner}/${args.repo} 中不存在`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 列出用户的仓库
async listRepos(): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/user/repos?type=owner`;
const data = await this.fetchWithProxy<GitHubRepo[]>(url);
if (data.length === 0) {
return {
content: [{
type: 'text',
text: '没有找到任何仓库'
}]
};
}
const formattedText = data.map(repo =>
`[${repo.private ? '私有' : '公开'}] ${repo.name}\n${repo.description || '无描述'}`
).join('\n\n');
return {
content: [{
type: 'text',
text: formattedText
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 创建新仓库
async createRepo(args: CreateRepoParams): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/user/repos`;
const response = await fetch(url, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(args)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`创建仓库失败: ${response.status} - ${error}`);
}
const data = await response.json() as GitHubRepo;
return {
content: [{
type: 'text',
text: `仓库创建成功: ${data.name}\n${data.description || '无描述'}`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 更新仓库设置
async updateRepo(args: { owner: string; repo: string; settings: UpdateRepoParams }): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/repos/${args.owner}/${args.repo}`;
const response = await fetch(url, {
method: 'PATCH',
headers: this.getHeaders(),
body: JSON.stringify(args.settings)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`更新仓库失败: ${response.status} - ${error}`);
}
const data = await response.json() as GitHubRepo;
return {
content: [{
type: 'text',
text: `仓库更新成功: ${data.name}\n${data.description || '无描述'}`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 删除仓库
async deleteRepo(args: { owner: string; repo: string }): Promise<ToolResponse> {
try {
this.validateToken();
const url = `${this.baseUrl}/repos/${args.owner}/${args.repo}`;
const response = await fetch(url, {
method: 'DELETE',
headers: this.getHeaders()
});
if (!response.ok) {
const error = await response.text();
throw new Error(`删除仓库失败: ${response.status} - ${error}`);
}
return {
content: [{
type: 'text',
text: `仓库 ${args.owner}/${args.repo} 已成功删除`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: error instanceof Error ? error.message : String(error)
}],
isError: true
};
}
}
// 清理资源
dispose(): void {
// 不需要清理资源
}
}
// 创建 GitHub 工具
export function createGithubTools(
config: Config
): ToolDefinition[] {
const manager = new GitHubManager(config);
return [
{
name: 'github_ls',
description: '列出 GitHub 仓库中的文件和目录。当需要浏览 GitHub 仓库内容时使用此工具。我会自动从配置文件读取 token 和代理设置。',
inputSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
description: '仓库所有者'
},
repo: {
type: 'string',
description: '仓库名称'
},
path: {
type: 'string',
description: '目录路径(可选)'
},
branch: {
type: 'string',
description: '分支名称(可选)'
}
},
required: ['owner', 'repo']
},
handler: args => manager.listContents(args)
},
{
name: 'github_tree',
description: '递归显示 GitHub 仓库的完整目录树结构。当需要了解仓库的整体文件组织时使用此工具,它提供比 github_ls 更全面的视图。',
inputSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
description: '仓库所有者(用户名或组织名)'
},
repo: {
type: 'string',
description: '仓库名称'
},
branch: {
type: 'string',
description: '分支名称,默认为仓库的默认分支'
}
},
required: ['owner', 'repo']
},
handler: args => manager.getTree(args)
},
{
name: 'github_search_repo',
description: '搜索 GitHub 仓库。当我需要查找特定主题、功能或示例代码的仓库时,我会使用此工具。我会根据上下文自动构建合适的搜索关键词。',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索关键词。支持 GitHub 高级搜索语法,如: "language:javascript stars:>1000"'
}
},
required: ['query']
},
handler: args => manager.searchRepos(args)
},
{
name: 'github_search_code',
description: '在 GitHub 上搜索代码。当我需要查找特定实现方式、解决方案或代码示例时,我会使用此工具。我会根据上下文自动构建精确的搜索关键词。',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索关键词。支持文件名、代码内容、语言等搜索,如: "filename:webpack.config.js language:javascript"'
}
},
required: ['query']
},
handler: args => manager.searchCode(args)
},
{
name: 'github_cat',
description: '查看 GitHub 仓库中的文件内容。当我需要分析或参考特定文件的实现时使用此工具。',
inputSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
description: '仓库所有者(用户名或组织名)'
},
repo: {
type: 'string',
description: '仓库名称'
},
path: {
type: 'string',
description: '文件路径,例如: "README.md" 或 "src/index.js"'
},
save: {
type: 'boolean',
description: '是否保存文件到本地(可选)'
}
},
required: ['owner', 'repo', 'path']
},
handler: args => manager.getFileContent(args)
},
{
name: 'github_list_repos',
description: '列出当前用户的所有仓库。用于查看和管理你的GitHub仓库。',
inputSchema: {
type: 'object',
properties: {},
required: []
},
handler: () => manager.listRepos()
},
{
name: 'github_create_repo',
description: '创建一个新的GitHub仓库。',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '仓库名称'
},
description: {
type: 'string',
description: '仓库描述(可选)'
},
private: {
type: 'boolean',
description: '是否为私有仓库(可选,默认为false)'
},
auto_init: {
type: 'boolean',
description: '是否使用README初始化仓库(可选,默认为false)'
}
},
required: ['name']
},
handler: args => manager.createRepo(args)
},
{
name: 'github_update_repo',
description: '更新GitHub仓库的设置。',
inputSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
description: '仓库所有者'
},
repo: {
type: 'string',
description: '仓库名称'
},
settings: {
type: 'object',
description: '要更新的设置',
properties: {
name: {
type: 'string',
description: '新的仓库名称'
},
description: {
type: 'string',
description: '新的仓库描述'
},
private: {
type: 'boolean',
description: '是否为私有仓库'
},
default_branch: {
type: 'string',
description: '默认分支'
}
}
}
},
required: ['owner', 'repo', 'settings']
},
handler: args => manager.updateRepo(args)
},
{
name: 'github_delete_repo',
description: '删除GitHub仓库。请谨慎使用此功能,删除后无法恢复!',
inputSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
description: '仓库所有者'
},
repo: {
type: 'string',
description: '要删除的仓库名称'
}
},
required: ['owner', 'repo']
},
handler: args => manager.deleteRepo(args)
}
];
}