feat: git 工具升级为全操作支持(16 个子操作)
git 工具支持的操作: - status: 查看状态(分支/修改/暂存/未追踪/删除/重命名) - log: 提交历史 - diff: 差异查看(支持 staged) - add: 暂存文件 - commit: 提交 - push/pull: 推送/拉取 - branch: 列出/创建/删除分支 - checkout: 切换分支 - merge: 合并分支 - stash: 暂存更改 - remote: 查看/添加远程仓库 - clone: 克隆仓库 - init: 初始化仓库 - reset: 重置 - tag: 标签管理 全部自动执行,无需用户确认
This commit is contained in:
+2
-2
@@ -32,7 +32,7 @@ import {
|
||||
handleDiffFiles,
|
||||
handleReplaceInFiles,
|
||||
handleReadMultipleFiles,
|
||||
handleGitStatus,
|
||||
handleGit,
|
||||
handleCompress
|
||||
} from './tool-handlers.js';
|
||||
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
|
||||
@@ -132,7 +132,7 @@ export function setupIPC(): void {
|
||||
case 'diff_files': return await handleDiffFiles(args as { file1: string; file2: string; context_lines?: number });
|
||||
case 'replace_in_files': return await handleReplaceInFiles(args as { path: string; glob: string; old_text: string; new_text: string });
|
||||
case 'read_multiple_files':return await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number });
|
||||
case 'git_status': return await handleGitStatus(args as { path?: string });
|
||||
case 'git': return await handleGit(args as any);
|
||||
case 'compress': return await handleCompress(args as { action: string; path: string; destination?: string; format?: string });
|
||||
default: return { success: false, error: `未知工具: ${toolName}` };
|
||||
}
|
||||
|
||||
+182
-22
@@ -799,31 +799,37 @@ export async function handleReadMultipleFiles(params: { paths: string[]; max_cha
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleGitStatus(params: { path?: string }): Promise<ToolResult> {
|
||||
export async function handleGit(params: { action: string; path?: string; files?: string[]; message?: string; branch?: string; remote?: string; remote_url?: string; count?: number; all?: boolean; staged?: boolean; new_branch?: boolean; delete_branch?: boolean; force?: boolean; url?: string }): Promise<ToolResult> {
|
||||
try {
|
||||
const cwd = params.path ? path.resolve(params.path) : getWorkspaceDir();
|
||||
|
||||
async function runGit(args: string[]): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
return new Promise((resolve) => {
|
||||
const git = spawn('git', ['status', '--porcelain=v1', '-b'], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
const git = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, LANG: 'en_US.UTF-8' } });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
git.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
|
||||
git.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
||||
|
||||
git.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
resolve({ success: false, error: stderr || '不是 git 仓库' });
|
||||
return;
|
||||
git.on('close', (code) => resolve({ stdout, stderr, code: code ?? 1 }));
|
||||
git.on('error', (err) => resolve({ stdout, stderr: err.message, code: 1 }));
|
||||
});
|
||||
}
|
||||
|
||||
const lines = stdout.split('\n').filter(l => l.trim());
|
||||
switch (params.action) {
|
||||
case 'status': {
|
||||
const r = await runGit(['status', '--porcelain=v1', '-b']);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr || '不是 git 仓库' };
|
||||
|
||||
const lines = r.stdout.split('\n').filter(l => l.trim());
|
||||
const branchLine = lines.find(l => l.startsWith('##')) || '';
|
||||
const branch = branchLine.replace('## ', '').split('...')[0] || 'unknown';
|
||||
const tracking = branchLine.includes('...') ? branchLine.split('...')[1] : '';
|
||||
|
||||
const modified: string[] = [];
|
||||
const staged: string[] = [];
|
||||
const untracked: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
const renamed: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('##')) continue;
|
||||
@@ -831,25 +837,179 @@ export async function handleGitStatus(params: { path?: string }): Promise<ToolRe
|
||||
const file = line.slice(3);
|
||||
if (status === '??') untracked.push(file);
|
||||
else {
|
||||
if (status[0] !== ' ' && status[0] !== '?') staged.push(file);
|
||||
if (status[1] !== ' ' && status[1] !== '?') modified.push(file);
|
||||
if (status[0] === 'D') deleted.push(file);
|
||||
else if (status[0] === 'R') renamed.push(file);
|
||||
else if (status[0] !== ' ' && status[0] !== '?') staged.push(file);
|
||||
if (status[1] === 'M') modified.push(file);
|
||||
else if (status[1] === 'D') deleted.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
branch,
|
||||
modified,
|
||||
staged,
|
||||
untracked,
|
||||
isClean: modified.length === 0 && staged.length === 0 && untracked.length === 0
|
||||
});
|
||||
});
|
||||
return {
|
||||
success: true, branch, tracking, modified, staged, untracked, deleted, renamed,
|
||||
isClean: modified.length === 0 && staged.length === 0 && untracked.length === 0 && deleted.length === 0
|
||||
};
|
||||
}
|
||||
|
||||
git.on('error', (err) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
case 'log': {
|
||||
const count = params.count || 20;
|
||||
const format = '--oneline';
|
||||
const r = await runGit(['log', format, `--max-count=${count}`, ...(params.all ? ['--all'] : [])]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
const commits = r.stdout.split('\n').filter(l => l.trim()).map(l => {
|
||||
const spaceIdx = l.indexOf(' ');
|
||||
return { hash: l.slice(0, spaceIdx), message: l.slice(spaceIdx + 1) };
|
||||
});
|
||||
return { success: true, commits, total: commits.length };
|
||||
}
|
||||
|
||||
case 'diff': {
|
||||
const args = ['diff'];
|
||||
if (params.staged) args.push('--cached');
|
||||
if (params.files?.length) args.push('--', ...params.files);
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
const hasChanges = r.stdout.trim().length > 0;
|
||||
return { success: true, diff: r.stdout, hasChanges, staged: !!params.staged };
|
||||
}
|
||||
|
||||
case 'add': {
|
||||
if (!params.files?.length) return { success: false, error: '请指定要暂存的文件' };
|
||||
const r = await runGit(['add', ...params.files]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'add', files: params.files };
|
||||
}
|
||||
|
||||
case 'commit': {
|
||||
if (!params.message) return { success: false, error: '请提供提交信息' };
|
||||
const r = await runGit(['commit', '-m', params.message]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr || '提交失败(可能没有暂存的更改)' };
|
||||
return { success: true, action: 'commit', message: params.message, output: r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'push': {
|
||||
const args = ['push'];
|
||||
if (params.remote) args.push(params.remote);
|
||||
if (params.branch) args.push(params.branch);
|
||||
if (params.force) args.push('--force');
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'push', output: r.stderr.trim() || r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'pull': {
|
||||
const args = ['pull'];
|
||||
if (params.remote) args.push(params.remote);
|
||||
if (params.branch) args.push(params.branch);
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'pull', output: r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'branch': {
|
||||
if (params.new_branch && params.branch) {
|
||||
const r = await runGit(['checkout', '-b', params.branch]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'branch', created: params.branch };
|
||||
}
|
||||
if (params.delete_branch && params.branch) {
|
||||
const r = await runGit(['branch', '-d', params.branch]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'branch', deleted: params.branch };
|
||||
}
|
||||
const r = await runGit(['branch', ...(params.all ? ['-a'] : [])]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
const branches = r.stdout.split('\n').filter(l => l.trim()).map(l => ({
|
||||
name: l.replace(/^\*?\s*/, '').trim(),
|
||||
current: l.startsWith('*')
|
||||
}));
|
||||
return { success: true, action: 'branch', branches };
|
||||
}
|
||||
|
||||
case 'checkout': {
|
||||
if (!params.branch) return { success: false, error: '请指定分支名' };
|
||||
const args = ['checkout'];
|
||||
if (params.force) args.push('-f');
|
||||
args.push(params.branch);
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'checkout', branch: params.branch };
|
||||
}
|
||||
|
||||
case 'merge': {
|
||||
if (!params.branch) return { success: false, error: '请指定要合并的分支' };
|
||||
const r = await runGit(['merge', params.branch]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr || '合并冲突' };
|
||||
return { success: true, action: 'merge', branch: params.branch, output: r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'stash': {
|
||||
const subAction = params.message || 'push';
|
||||
const args = ['stash', subAction];
|
||||
if (subAction === 'push' && params.message) args.push('-m', params.message);
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'stash', subAction, output: r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'remote': {
|
||||
if (params.remote_url && params.remote) {
|
||||
const r = await runGit(['remote', 'add', params.remote, params.remote_url]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'remote', added: params.remote, url: params.remote_url };
|
||||
}
|
||||
const args = ['remote', '-v'];
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
const remotes = r.stdout.split('\n').filter(l => l.trim()).map(l => {
|
||||
const [name, url] = l.split(/\s+/);
|
||||
return { name, url };
|
||||
});
|
||||
return { success: true, action: 'remote', remotes };
|
||||
}
|
||||
|
||||
case 'clone': {
|
||||
if (!params.url) return { success: false, error: '请提供仓库 URL' };
|
||||
const destDir = params.path ? path.resolve(params.path) : getWorkspaceDir();
|
||||
const r = await runGit(['clone', params.url, destDir]);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'clone', url: params.url, destination: destDir };
|
||||
}
|
||||
|
||||
case 'init': {
|
||||
const r = await runGit(['init']);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'init', output: r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'reset': {
|
||||
const args = ['reset'];
|
||||
if (params.staged) args.push('--cached');
|
||||
if (params.force) args.push('--hard');
|
||||
if (params.files?.length) args.push('--', ...params.files);
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'reset', output: r.stdout.trim() };
|
||||
}
|
||||
|
||||
case 'tag': {
|
||||
if (params.branch) {
|
||||
// branch field reused as tag name
|
||||
const args = ['tag', params.branch];
|
||||
if (params.message) args.push('-m', params.message);
|
||||
const r = await runGit(args);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
return { success: true, action: 'tag', created: params.branch };
|
||||
}
|
||||
const r = await runGit(['tag', '-l']);
|
||||
if (r.code !== 0) return { success: false, error: r.stderr };
|
||||
const tags = r.stdout.split('\n').filter(l => l.trim());
|
||||
return { success: true, action: 'tag', tags };
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, error: `未知 git 操作: ${params.action}` };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
|
||||
@@ -461,8 +461,8 @@
|
||||
<div class="tool-card-desc">一次读取多个文件内容</div>
|
||||
</div>
|
||||
<div class="tool-card">
|
||||
<div class="tool-card-header"><span class="tool-card-icon">🔖</span><span class="tool-card-name">git_status</span><span class="tool-card-badge auto">自动</span></div>
|
||||
<div class="tool-card-desc">查看 Git 仓库状态:分支、修改、暂存</div>
|
||||
<div class="tool-card-header"><span class="tool-card-icon">🔖</span><span class="tool-card-name">git</span><span class="tool-card-badge auto">自动</span></div>
|
||||
<div class="tool-card-desc">Git 全操作:status/log/diff/add/commit/push/pull/branch/checkout/merge/stash/remote/clone/init/reset/tag</div>
|
||||
</div>
|
||||
<div class="tool-card">
|
||||
<div class="tool-card-header"><span class="tool-card-icon">🗜️</span><span class="tool-card-name">compress</span><span class="tool-card-badge confirm">需确认</span></div>
|
||||
|
||||
@@ -295,13 +295,26 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git_status',
|
||||
description: 'Show git repository status: branch, modified/staged/untracked files.',
|
||||
name: 'git',
|
||||
description: 'Git operations: status, log, diff, add, commit, push, pull, branch, checkout, merge, stash, remote, clone, init. All operations are automatic (no confirmation).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: [],
|
||||
required: ['action'],
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Repository path. Defaults to workspace.' }
|
||||
action: { type: 'string', enum: ['status', 'log', 'diff', 'add', 'commit', 'push', 'pull', 'branch', 'checkout', 'merge', 'stash', 'remote', 'clone', 'init', 'reset', 'tag'], description: 'Git action to perform.' },
|
||||
path: { type: 'string', description: 'Repository path. Defaults to workspace.' },
|
||||
files: { type: 'array', items: { type: 'string' }, description: 'Files for add/diff/checkout.' },
|
||||
message: { type: 'string', description: 'Commit message for commit action.' },
|
||||
branch: { type: 'string', description: 'Branch name for branch/checkout/merge.' },
|
||||
remote: { type: 'string', description: 'Remote name for push/pull/remote.' },
|
||||
remote_url: { type: 'string', description: 'Remote URL for remote add.' },
|
||||
count: { type: 'integer', description: 'Number of log entries. Default: 20.' },
|
||||
all: { type: 'boolean', description: 'Show all (branches, stashes, etc). Default: false.' },
|
||||
staged: { type: 'boolean', description: 'Show staged changes in diff. Default: false.' },
|
||||
new_branch: { type: 'boolean', description: 'Create new branch in branch action. Default: false.' },
|
||||
delete_branch: { type: 'boolean', description: 'Delete branch in branch action. Default: false.' },
|
||||
force: { type: 'boolean', description: 'Force push/checkout. Default: false.' },
|
||||
url: { type: 'string', description: 'Repository URL for clone.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +350,7 @@ let enabledTools: Set<string> = new Set([
|
||||
'run_command',
|
||||
'move_file', 'copy_file', 'web_fetch', 'append_file', 'edit_file',
|
||||
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
||||
'read_multiple_files', 'git_status', 'compress'
|
||||
'read_multiple_files', 'git', 'compress'
|
||||
]);
|
||||
|
||||
export function setToolEnabled(toolName: string, enabled: boolean): void {
|
||||
@@ -422,7 +435,7 @@ export function getToolIcon(name: string): string {
|
||||
diff_files: '🔀',
|
||||
replace_in_files: '🔄',
|
||||
read_multiple_files: '📚',
|
||||
git_status: '🔖',
|
||||
git: '🔖',
|
||||
compress: '🗜️'
|
||||
};
|
||||
return icons[name] || '🔧';
|
||||
@@ -448,7 +461,7 @@ export function formatToolName(name: string): string {
|
||||
diff_files: '文件对比',
|
||||
replace_in_files: '批量替换',
|
||||
read_multiple_files: '批量读取',
|
||||
git_status: 'Git 状态',
|
||||
git: 'Git 操作',
|
||||
compress: '压缩/解压'
|
||||
};
|
||||
return names[name] || name;
|
||||
|
||||
Reference in New Issue
Block a user