feat: 重构为 Monorepo 架构并实现 HTTP Server

架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
This commit is contained in:
2025-12-12 10:42:20 +08:00
parent 59dbed926e
commit 5e32375f0e
301 changed files with 3281 additions and 43 deletions
+119
View File
@@ -0,0 +1,119 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitAddTool: ToolWithMetadata = {
name: 'git_add',
description: loadDescription('git_add'),
metadata: {
name: 'git_add',
category: 'git',
description: '暂存文件到 Git',
keywords: ['git', 'add', 'stage', '暂存', '添加', 'staging'],
deferLoading: false,
},
parameters: {
files: {
type: 'array',
description: '要暂存的文件列表',
required: false,
},
all: {
type: 'boolean',
description: '暂存所有变更(-A',
required: false,
},
update: {
type: 'boolean',
description: '仅暂存已跟踪文件的变更(-u)',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
let files = params.files as string[] | string | undefined;
const all = params.all as boolean;
const update = params.update as boolean;
// 转换单个文件为数组
if (typeof files === 'string') {
files = [files];
}
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'add',
target: all ? '所有文件' : files?.join(', '),
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git add\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
let command: string;
if (all) {
command = 'git add -A';
} else if (update) {
command = 'git add -u';
} else if (files && files.length > 0) {
// 转义文件名中的特殊字符
const escapedFiles = files.map(f => `"${f}"`).join(' ');
command = `git add ${escapedFiles}`;
} else {
return {
success: false,
output: '',
error: '请指定要暂存的文件(files 参数)或使用 all: true 暂存所有变更',
};
}
const { stdout, stderr } = await execAsync(command, {
cwd: process.cwd(),
timeout: 30000,
});
// git add 成功时通常没有输出,显示暂存的文件
let output = stdout || '文件已暂存';
// 获取暂存状态
const { stdout: statusOut } = await execAsync('git status -s', {
cwd: process.cwd(),
timeout: 10000,
});
if (statusOut) {
output += '\n\n当前暂存状态:\n' + statusOut;
}
return {
success: true,
output: output + (stderr ? `\n${stderr}` : ''),
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
return {
success: false,
output: execError.stdout || '',
error: execError.stderr || execError.message,
};
}
},
};
+173
View File
@@ -0,0 +1,173 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
import type { GitOperation } from '../../permission/types.js';
const execAsync = promisify(exec);
export const gitBranchTool: ToolWithMetadata = {
name: 'git_branch',
description: loadDescription('git_branch'),
metadata: {
name: 'git_branch',
category: 'git',
description: '管理 Git 分支',
keywords: ['git', 'branch', '分支', 'branches', 'create', 'delete', '创建', '删除'],
deferLoading: false,
},
parameters: {
action: {
type: 'string',
description: '操作类型: list(默认), create, delete, rename',
required: false,
},
name: {
type: 'string',
description: '分支名称(create/delete/rename 时必填)',
required: false,
},
new_name: {
type: 'string',
description: '新分支名(rename 时必填)',
required: false,
},
remote: {
type: 'boolean',
description: '显示远程分支(-r',
required: false,
},
all: {
type: 'boolean',
description: '显示所有分支(-a',
required: false,
},
force: {
type: 'boolean',
description: '强制删除未合并的分支(-D)',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const action = (params.action as string) || 'list';
const name = params.name as string | undefined;
const newName = params.new_name as string | undefined;
const remote = params.remote as boolean;
const all = params.all as boolean;
const force = params.force as boolean;
// 确定权限操作类型
let operation: GitOperation = 'branch_list';
if (action === 'create') {
operation = 'branch_create';
} else if (action === 'delete') {
operation = 'branch_delete';
} else if (action === 'rename') {
operation = 'branch_create'; // rename 视为创建操作
}
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation,
target: name,
force,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git branch ${action}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
let command: string;
switch (action) {
case 'list':
const listArgs = ['branch'];
if (all) {
listArgs.push('-a');
} else if (remote) {
listArgs.push('-r');
}
listArgs.push('-v'); // 显示最后提交信息
command = `git ${listArgs.join(' ')}`;
break;
case 'create':
if (!name) {
return {
success: false,
output: '',
error: '创建分支需要提供分支名称(name 参数)',
};
}
command = `git branch ${name}`;
break;
case 'delete':
if (!name) {
return {
success: false,
output: '',
error: '删除分支需要提供分支名称(name 参数)',
};
}
command = `git branch ${force ? '-D' : '-d'} ${name}`;
break;
case 'rename':
if (!name || !newName) {
return {
success: false,
output: '',
error: '重命名分支需要提供原名称(name)和新名称(new_name',
};
}
command = `git branch -m ${name} ${newName}`;
break;
default:
return {
success: false,
output: '',
error: `未知操作: ${action}。支持的操作: list, create, delete, rename`,
};
}
const { stdout, stderr } = await execAsync(command, {
cwd: process.cwd(),
timeout: 30000,
});
let output = stdout;
if (action !== 'list' && !stdout) {
output = `分支操作成功: ${action} ${name}${newName ? ` -> ${newName}` : ''}`;
}
return {
success: true,
output: output + (stderr ? `\n${stderr}` : ''),
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
return {
success: false,
output: execError.stdout || '',
error: execError.stderr || execError.message,
};
}
},
};
+136
View File
@@ -0,0 +1,136 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitCheckoutTool: ToolWithMetadata = {
name: 'git_checkout',
description: loadDescription('git_checkout'),
metadata: {
name: 'git_checkout',
category: 'git',
description: '切换分支或恢复文件',
keywords: ['git', 'checkout', 'switch', '切换', '分支', 'restore', '恢复'],
deferLoading: false,
},
parameters: {
target: {
type: 'string',
description: '目标分支名或文件路径',
required: true,
},
create: {
type: 'boolean',
description: '创建新分支并切换(-b',
required: false,
},
force: {
type: 'boolean',
description: '强制切换(丢弃本地修改)',
required: false,
},
file: {
type: 'boolean',
description: '目标是文件路径(恢复文件到 HEAD 状态)',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const target = params.target as string;
const create = params.create as boolean;
const force = params.force as boolean;
const isFile = params.file as boolean;
if (!target) {
return {
success: false,
output: '',
error: '请指定目标分支名或文件路径(target 参数)',
};
}
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'checkout',
target,
force,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git checkout ${target}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
const args: string[] = ['checkout'];
if (create) {
args.push('-b');
}
if (force) {
args.push('-f');
}
if (isFile) {
args.push('--', target);
} else {
args.push(target);
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 30000,
});
// 切换分支成功的提示通常在 stderr
const output = stdout || stderr || `已切换到 ${target}`;
return {
success: true,
output,
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
const errorMsg = execError.stderr || execError.message;
// 检查常见错误
if (errorMsg.includes('local changes') || errorMsg.includes('would be overwritten')) {
return {
success: false,
output: execError.stdout || '',
error: `本地有未提交的变更会被覆盖。请先提交或暂存(git_stash)变更,或使用 force: true 强制切换(会丢失本地修改)。\n${errorMsg}`,
};
}
if (errorMsg.includes('did not match') || errorMsg.includes('pathspec')) {
return {
success: false,
output: execError.stdout || '',
error: `找不到分支或文件: ${target}\n${errorMsg}`,
};
}
return {
success: false,
output: execError.stdout || '',
error: errorMsg,
};
}
},
};
+119
View File
@@ -0,0 +1,119 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitCommitTool: ToolWithMetadata = {
name: 'git_commit',
description: loadDescription('git_commit'),
metadata: {
name: 'git_commit',
category: 'git',
description: '提交 Git 变更',
keywords: ['git', 'commit', '提交', 'save', '保存', 'snapshot'],
deferLoading: false,
},
parameters: {
message: {
type: 'string',
description: '提交信息(必填)',
required: true,
},
amend: {
type: 'boolean',
description: '修改上次提交(--amend',
required: false,
},
all: {
type: 'boolean',
description: '自动暂存所有已跟踪文件的变更(-a)',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const message = params.message as string;
const amend = params.amend as boolean;
const all = params.all as boolean;
if (!message && !amend) {
return {
success: false,
output: '',
error: '提交信息是必填的(message 参数)',
};
}
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'commit',
message,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git commit\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
const args: string[] = ['commit'];
if (all) {
args.push('-a');
}
if (amend) {
args.push('--amend');
if (message) {
args.push('-m', `"${message.replace(/"/g, '\\"')}"`);
} else {
args.push('--no-edit');
}
} else {
args.push('-m', `"${message.replace(/"/g, '\\"')}"`);
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 30000,
});
return {
success: true,
output: stdout + (stderr ? `\n${stderr}` : ''),
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
// 检查是否是没有变更可提交
if (execError.message?.includes('nothing to commit') ||
execError.stderr?.includes('nothing to commit')) {
return {
success: false,
output: '',
error: '没有变更需要提交。请先使用 git_add 暂存文件。',
};
}
return {
success: false,
output: execError.stdout || '',
error: execError.stderr || execError.message,
};
}
},
};
+110
View File
@@ -0,0 +1,110 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitDiffTool: ToolWithMetadata = {
name: 'git_diff',
description: loadDescription('git_diff'),
metadata: {
name: 'git_diff',
category: 'git',
description: '查看 Git 变更差异',
keywords: ['git', 'diff', '差异', '对比', 'changes', '变更', 'compare'],
deferLoading: false,
},
parameters: {
path: {
type: 'string',
description: '指定文件或目录路径',
required: false,
},
staged: {
type: 'boolean',
description: '显示已暂存的变更(--staged',
required: false,
},
commit: {
type: 'string',
description: '与指定提交对比(如 HEAD~1, commit-hash',
required: false,
},
stat: {
type: 'boolean',
description: '仅显示统计信息(--stat',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const path = params.path as string | undefined;
const staged = params.staged as boolean;
const commit = params.commit as string | undefined;
const stat = params.stat as boolean;
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'diff',
target: path,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git diff\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
const args: string[] = ['diff'];
if (staged) {
args.push('--staged');
}
if (stat) {
args.push('--stat');
}
if (commit) {
args.push(commit);
}
if (path) {
args.push('--', path);
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 60000,
maxBuffer: 1024 * 1024 * 10,
});
const output = stdout || '(无差异)';
return {
success: true,
output: output + (stderr ? `\n${stderr}` : ''),
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
return {
success: false,
output: execError.stdout || '',
error: execError.stderr || execError.message,
};
}
},
};
+126
View File
@@ -0,0 +1,126 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitLogTool: ToolWithMetadata = {
name: 'git_log',
description: loadDescription('git_log'),
metadata: {
name: 'git_log',
category: 'git',
description: '查看 Git 提交历史',
keywords: ['git', 'log', 'history', '历史', '提交', 'commit', 'commits'],
deferLoading: false,
},
parameters: {
limit: {
type: 'number',
description: '显示的提交数量(默认 10',
required: false,
},
oneline: {
type: 'boolean',
description: '单行显示(--oneline',
required: false,
},
file: {
type: 'string',
description: '查看指定文件的提交历史',
required: false,
},
author: {
type: 'string',
description: '筛选指定作者的提交',
required: false,
},
since: {
type: 'string',
description: '起始日期(如 "2024-01-01" 或 "1 week ago"',
required: false,
},
graph: {
type: 'boolean',
description: '显示分支图(--graph',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const limit = (params.limit as number) || 10;
const oneline = params.oneline as boolean;
const file = params.file as string | undefined;
const author = params.author as string | undefined;
const since = params.since as string | undefined;
const graph = params.graph as boolean;
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'log',
target: file,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git log\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
const args: string[] = ['log', `-${limit}`];
if (oneline) {
args.push('--oneline');
} else {
// 默认格式:更易读
args.push('--format=%h %s (%an, %ar)');
}
if (graph) {
args.push('--graph');
}
if (author) {
args.push(`--author=${author}`);
}
if (since) {
args.push(`--since="${since}"`);
}
if (file) {
args.push('--', file);
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 30000,
});
return {
success: true,
output: stdout || '(无提交记录)',
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
return {
success: false,
output: execError.stdout || '',
error: execError.stderr || execError.message,
};
}
},
};
+118
View File
@@ -0,0 +1,118 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitPullTool: ToolWithMetadata = {
name: 'git_pull',
description: loadDescription('git_pull'),
metadata: {
name: 'git_pull',
category: 'git',
description: '从远程仓库拉取更新',
keywords: ['git', 'pull', '拉取', 'fetch', 'download', '下载', 'update', '更新'],
deferLoading: false,
},
parameters: {
remote: {
type: 'string',
description: '远程仓库名(默认 origin',
required: false,
},
branch: {
type: 'string',
description: '分支名(默认当前分支)',
required: false,
},
rebase: {
type: 'boolean',
description: '使用 rebase 而非 merge--rebase',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const remote = (params.remote as string) || 'origin';
const branch = params.branch as string | undefined;
const rebase = params.rebase as boolean;
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'pull',
remote,
target: branch,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git pull\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
const args: string[] = ['pull'];
if (rebase) {
args.push('--rebase');
}
args.push(remote);
if (branch) {
args.push(branch);
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 120000,
});
const output = stdout || stderr || '已是最新';
return {
success: true,
output,
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
const errorMsg = execError.stderr || execError.message;
// 检查冲突
if (errorMsg.includes('CONFLICT') || errorMsg.includes('conflict')) {
return {
success: false,
output: execError.stdout || '',
error: `拉取时发生合并冲突,请手动解决冲突后使用 git_add 和 git_commit 提交。\n${errorMsg}`,
};
}
// 检查本地变更
if (errorMsg.includes('local changes') || errorMsg.includes('uncommitted changes')) {
return {
success: false,
output: execError.stdout || '',
error: `本地有未提交的变更。请先提交或暂存(git_stash)本地变更。\n${errorMsg}`,
};
}
return {
success: false,
output: execError.stdout || '',
error: errorMsg,
};
}
},
};
+145
View File
@@ -0,0 +1,145 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitPushTool: ToolWithMetadata = {
name: 'git_push',
description: loadDescription('git_push'),
metadata: {
name: 'git_push',
category: 'git',
description: '推送 Git 变更到远程仓库',
keywords: ['git', 'push', '推送', 'upload', '上传', 'remote'],
deferLoading: false,
},
parameters: {
remote: {
type: 'string',
description: '远程仓库名(默认 origin',
required: false,
},
branch: {
type: 'string',
description: '分支名(默认当前分支)',
required: false,
},
force: {
type: 'boolean',
description: '强制推送(--force,危险操作)',
required: false,
},
set_upstream: {
type: 'boolean',
description: '设置上游分支(-u',
required: false,
},
tags: {
type: 'boolean',
description: '推送所有标签(--tags',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const remote = (params.remote as string) || 'origin';
const branch = params.branch as string | undefined;
const force = params.force as boolean;
const setUpstream = params.set_upstream as boolean;
const tags = params.tags as boolean;
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'push',
remote,
target: branch,
force,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git push${force ? ' --force' : ''}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
// 强制推送警告
if (force) {
// 这里权限系统已经处理了确认,但可以添加额外的输出
}
try {
const args: string[] = ['push'];
if (setUpstream) {
args.push('-u');
}
if (force) {
args.push('--force');
}
if (tags) {
args.push('--tags');
}
args.push(remote);
if (branch) {
args.push(branch);
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 120000, // 推送可能需要更长时间
});
// git push 的输出通常在 stderr
const output = stdout || stderr || '推送成功';
return {
success: true,
output,
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
// 检查常见错误
const errorMsg = execError.stderr || execError.message;
if (errorMsg.includes('rejected')) {
return {
success: false,
output: execError.stdout || '',
error: `推送被拒绝。远程分支有新的提交,请先执行 git_pull 拉取更新。\n${errorMsg}`,
};
}
if (errorMsg.includes('no upstream')) {
return {
success: false,
output: execError.stdout || '',
error: `当前分支没有设置上游分支。请使用 set_upstream: true 参数。\n${errorMsg}`,
};
}
return {
success: false,
output: execError.stdout || '',
error: errorMsg,
};
}
},
};
+191
View File
@@ -0,0 +1,191 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
import type { GitOperation } from '../../permission/types.js';
const execAsync = promisify(exec);
export const gitStashTool: ToolWithMetadata = {
name: 'git_stash',
description: loadDescription('git_stash'),
metadata: {
name: 'git_stash',
category: 'git',
description: '暂存工作区变更',
keywords: ['git', 'stash', '暂存', '保存', 'save', 'temporary', '临时'],
deferLoading: false,
},
parameters: {
action: {
type: 'string',
description: '操作类型: push(默认), pop, list, apply, drop, clear',
required: false,
},
message: {
type: 'string',
description: '暂存说明(push 时可用)',
required: false,
},
index: {
type: 'number',
description: '暂存索引(pop/apply/drop 时可用,默认 0',
required: false,
},
include_untracked: {
type: 'boolean',
description: '包含未跟踪的文件(-u',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const action = (params.action as string) || 'push';
const message = params.message as string | undefined;
const index = params.index as number | undefined;
const includeUntracked = params.include_untracked as boolean;
// 确定权限操作类型
let operation: GitOperation = 'stash';
if (action === 'pop' || action === 'apply') {
operation = 'stash_pop';
}
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation,
message,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git stash ${action}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
let command: string;
const stashRef = index !== undefined ? `stash@{${index}}` : '';
switch (action) {
case 'push':
case 'save':
const pushArgs = ['stash', 'push'];
if (includeUntracked) {
pushArgs.push('-u');
}
if (message) {
pushArgs.push('-m', `"${message.replace(/"/g, '\\"')}"`);
}
command = `git ${pushArgs.join(' ')}`;
break;
case 'pop':
command = `git stash pop ${stashRef}`.trim();
break;
case 'apply':
command = `git stash apply ${stashRef}`.trim();
break;
case 'drop':
command = `git stash drop ${stashRef}`.trim();
break;
case 'list':
command = 'git stash list';
break;
case 'clear':
command = 'git stash clear';
break;
case 'show':
command = `git stash show -p ${stashRef}`.trim();
break;
default:
return {
success: false,
output: '',
error: `未知操作: ${action}。支持的操作: push, pop, apply, drop, list, clear, show`,
};
}
const { stdout, stderr } = await execAsync(command, {
cwd: process.cwd(),
timeout: 30000,
maxBuffer: 1024 * 1024 * 10,
});
let output = stdout;
// 针对不同操作给出友好输出
if (!output) {
switch (action) {
case 'push':
case 'save':
output = '工作区变更已暂存';
break;
case 'pop':
output = '暂存已恢复并删除';
break;
case 'apply':
output = '暂存已恢复(暂存记录保留)';
break;
case 'drop':
output = '暂存已删除';
break;
case 'clear':
output = '所有暂存已清除';
break;
case 'list':
output = '(无暂存记录)';
break;
}
}
return {
success: true,
output: output + (stderr ? `\n${stderr}` : ''),
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
const errorMsg = execError.stderr || execError.message;
// 检查常见错误
if (errorMsg.includes('No local changes') || errorMsg.includes('no changes')) {
return {
success: false,
output: '',
error: '没有需要暂存的变更',
};
}
if (errorMsg.includes('CONFLICT') || errorMsg.includes('conflict')) {
return {
success: false,
output: execError.stdout || '',
error: `恢复暂存时发生冲突,请手动解决。\n${errorMsg}`,
};
}
return {
success: false,
output: execError.stdout || '',
error: errorMsg,
};
}
},
};
+86
View File
@@ -0,0 +1,86 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
const execAsync = promisify(exec);
export const gitStatusTool: ToolWithMetadata = {
name: 'git_status',
description: loadDescription('git_status'),
metadata: {
name: 'git_status',
category: 'git',
description: '查看 Git 仓库状态',
keywords: ['git', 'status', '状态', '仓库', 'repository', 'changes', '变更'],
deferLoading: false,
},
parameters: {
short: {
type: 'boolean',
description: '简短输出格式(-s',
required: false,
},
branch: {
type: 'boolean',
description: '显示分支信息(-b',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const short = params.short as boolean;
const branch = params.branch !== false; // 默认显示分支
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkGitPermission({
operation: 'status',
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: git status\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
};
}
try {
const args: string[] = ['status'];
if (short) {
args.push('-s');
}
if (branch) {
args.push('-b');
}
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
cwd: process.cwd(),
timeout: 30000,
});
return {
success: true,
output: stdout + (stderr ? `\n${stderr}` : ''),
};
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
return {
success: false,
output: execError.stdout || '',
error: execError.stderr || execError.message,
};
}
},
};
+10
View File
@@ -0,0 +1,10 @@
export { gitStatusTool } from './git_status.js';
export { gitDiffTool } from './git_diff.js';
export { gitLogTool } from './git_log.js';
export { gitBranchTool } from './git_branch.js';
export { gitAddTool } from './git_add.js';
export { gitCommitTool } from './git_commit.js';
export { gitPushTool } from './git_push.js';
export { gitPullTool } from './git_pull.js';
export { gitCheckoutTool } from './git_checkout.js';
export { gitStashTool } from './git_stash.js';