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:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 创建检查点工具
|
||||
*/
|
||||
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getCheckpointManager } from '../../checkpoint/index.js';
|
||||
|
||||
export const checkpointCreateTool: ToolWithMetadata = {
|
||||
name: 'checkpoint_create',
|
||||
description: loadDescription('checkpoint_create'),
|
||||
metadata: {
|
||||
name: 'checkpoint_create',
|
||||
category: 'core',
|
||||
description: '创建一个新的工作区检查点快照',
|
||||
keywords: [
|
||||
'checkpoint',
|
||||
'create',
|
||||
'snapshot',
|
||||
'save',
|
||||
'检查点',
|
||||
'快照',
|
||||
'保存',
|
||||
],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: '检查点名称 (可选)',
|
||||
required: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: '检查点描述 (可选)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const name = params.name as string | undefined;
|
||||
const description = params.description as string | undefined;
|
||||
|
||||
try {
|
||||
const manager = getCheckpointManager();
|
||||
|
||||
if (!manager.isEnabled()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '检查点系统已禁用',
|
||||
};
|
||||
}
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
const checkpoint = await manager.createCheckpoint({
|
||||
name,
|
||||
description,
|
||||
trigger: 'manual',
|
||||
});
|
||||
|
||||
const lines = [
|
||||
`✓ 检查点已创建`,
|
||||
` ID: ${checkpoint.id}`,
|
||||
` Commit: ${checkpoint.commitHash.slice(0, 8)}`,
|
||||
];
|
||||
|
||||
if (checkpoint.name) {
|
||||
lines.push(` 名称: ${checkpoint.name}`);
|
||||
}
|
||||
if (checkpoint.filesChanged > 0) {
|
||||
lines.push(` 文件变更: ${checkpoint.filesChanged} 个`);
|
||||
}
|
||||
lines.push(` 时间: ${new Date(checkpoint.timestamp).toLocaleString()}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 检查点差异工具
|
||||
*/
|
||||
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getCheckpointManager } from '../../checkpoint/index.js';
|
||||
|
||||
export const checkpointDiffTool: ToolWithMetadata = {
|
||||
name: 'checkpoint_diff',
|
||||
description: loadDescription('checkpoint_diff'),
|
||||
metadata: {
|
||||
name: 'checkpoint_diff',
|
||||
category: 'core',
|
||||
description: '显示检查点与当前工作区的差异',
|
||||
keywords: [
|
||||
'checkpoint',
|
||||
'diff',
|
||||
'compare',
|
||||
'changes',
|
||||
'检查点',
|
||||
'差异',
|
||||
'比较',
|
||||
'变更',
|
||||
],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
checkpoint_id: {
|
||||
type: 'string',
|
||||
description: '检查点 ID 或 commit hash (默认为最近的检查点)',
|
||||
required: false,
|
||||
},
|
||||
file: {
|
||||
type: 'string',
|
||||
description: '指定文件路径查看详细差异 (可选)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const checkpointId = params.checkpoint_id as string | undefined;
|
||||
const file = params.file as string | undefined;
|
||||
|
||||
try {
|
||||
const manager = getCheckpointManager();
|
||||
|
||||
if (!manager.isEnabled()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '检查点系统已禁用',
|
||||
};
|
||||
}
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
// 获取目标检查点
|
||||
let targetCheckpoint;
|
||||
if (checkpointId) {
|
||||
targetCheckpoint = await manager.getCheckpoint(checkpointId);
|
||||
if (!targetCheckpoint) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `找不到检查点: ${checkpointId}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
targetCheckpoint = await manager.getLatestCheckpoint();
|
||||
if (!targetCheckpoint) {
|
||||
return {
|
||||
success: true,
|
||||
output: '暂无检查点',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 显示文件详细差异
|
||||
if (file) {
|
||||
const fileDiff = await manager.getFileDiff(targetCheckpoint.id, file);
|
||||
|
||||
const lines = [
|
||||
`文件差异: ${file}`,
|
||||
`检查点: ${targetCheckpoint.commitHash.slice(0, 8)}`,
|
||||
`变更类型: ${fileDiff.type}`,
|
||||
'',
|
||||
];
|
||||
|
||||
if (fileDiff.patch) {
|
||||
lines.push('```diff');
|
||||
lines.push(fileDiff.patch);
|
||||
lines.push('```');
|
||||
} else if (fileDiff.type === 'added') {
|
||||
lines.push('(新文件)');
|
||||
} else if (fileDiff.type === 'deleted') {
|
||||
lines.push('(已删除)');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
// 显示概要差异
|
||||
const diff = await manager.getDiff(targetCheckpoint.id);
|
||||
|
||||
if (diff.files.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: `检查点 ${targetCheckpoint.commitHash.slice(0, 8)} 与当前工作区相同`,
|
||||
};
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`检查点 ${targetCheckpoint.commitHash.slice(0, 8)} 与当前工作区的差异:`,
|
||||
'',
|
||||
` +${diff.totalInsertions} 行添加 -${diff.totalDeletions} 行删除`,
|
||||
'',
|
||||
'变更的文件:',
|
||||
];
|
||||
|
||||
for (const fileChange of diff.files) {
|
||||
const symbol =
|
||||
fileChange.type === 'added'
|
||||
? '+'
|
||||
: fileChange.type === 'deleted'
|
||||
? '-'
|
||||
: fileChange.type === 'renamed'
|
||||
? 'R'
|
||||
: 'M';
|
||||
|
||||
let line = ` ${symbol} ${fileChange.path}`;
|
||||
if (fileChange.oldPath) {
|
||||
line = ` ${symbol} ${fileChange.oldPath} -> ${fileChange.path}`;
|
||||
}
|
||||
|
||||
if (fileChange.insertions || fileChange.deletions) {
|
||||
line += ` (+${fileChange.insertions || 0} -${fileChange.deletions || 0})`;
|
||||
}
|
||||
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 列出检查点工具
|
||||
*/
|
||||
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getCheckpointManager } from '../../checkpoint/index.js';
|
||||
|
||||
export const checkpointListTool: ToolWithMetadata = {
|
||||
name: 'checkpoint_list',
|
||||
description: loadDescription('checkpoint_list'),
|
||||
metadata: {
|
||||
name: 'checkpoint_list',
|
||||
category: 'core',
|
||||
description: '列出所有可用的检查点',
|
||||
keywords: [
|
||||
'checkpoint',
|
||||
'list',
|
||||
'show',
|
||||
'history',
|
||||
'检查点',
|
||||
'列表',
|
||||
'历史',
|
||||
],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: '最多显示的检查点数量 (默认 10)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const limit = (params.limit as number) || 10;
|
||||
|
||||
try {
|
||||
const manager = getCheckpointManager();
|
||||
|
||||
if (!manager.isEnabled()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '检查点系统已禁用',
|
||||
};
|
||||
}
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
const checkpoints = await manager.listCheckpoints();
|
||||
|
||||
if (checkpoints.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: '暂无检查点',
|
||||
};
|
||||
}
|
||||
|
||||
const displayCheckpoints = checkpoints.slice(0, limit);
|
||||
const lines = [`共 ${checkpoints.length} 个检查点:\n`];
|
||||
|
||||
for (const cp of displayCheckpoints) {
|
||||
const date = new Date(cp.timestamp).toLocaleString();
|
||||
const hash = cp.commitHash.slice(0, 8);
|
||||
const name = cp.name ? ` "${cp.name}"` : '';
|
||||
const files = cp.filesChanged > 0 ? ` (${cp.filesChanged} files)` : '';
|
||||
|
||||
lines.push(` ${hash}${name}${files}`);
|
||||
lines.push(` ${cp.description || cp.trigger}`);
|
||||
lines.push(` ${date}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (checkpoints.length > limit) {
|
||||
lines.push(` ... 还有 ${checkpoints.length - limit} 个检查点`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 恢复检查点工具
|
||||
*/
|
||||
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getCheckpointManager } from '../../checkpoint/index.js';
|
||||
|
||||
export const checkpointRestoreTool: ToolWithMetadata = {
|
||||
name: 'checkpoint_restore',
|
||||
description: loadDescription('checkpoint_restore'),
|
||||
metadata: {
|
||||
name: 'checkpoint_restore',
|
||||
category: 'core',
|
||||
description: '恢复到指定的检查点',
|
||||
keywords: [
|
||||
'checkpoint',
|
||||
'restore',
|
||||
'rollback',
|
||||
'undo',
|
||||
'检查点',
|
||||
'恢复',
|
||||
'回滚',
|
||||
'撤销',
|
||||
],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
checkpoint_id: {
|
||||
type: 'string',
|
||||
description: '要恢复的检查点 ID 或 commit hash',
|
||||
required: true,
|
||||
},
|
||||
files: {
|
||||
type: 'string',
|
||||
description: '只恢复指定文件,多个文件用逗号分隔 (可选)',
|
||||
required: false,
|
||||
},
|
||||
dry_run: {
|
||||
type: 'boolean',
|
||||
description: '预览模式,不实际执行恢复',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const checkpointId = params.checkpoint_id as string;
|
||||
const filesStr = params.files as string | undefined;
|
||||
const dryRun = params.dry_run as boolean | undefined;
|
||||
|
||||
if (!checkpointId) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '请指定要恢复的检查点 ID',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const manager = getCheckpointManager();
|
||||
|
||||
if (!manager.isEnabled()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '检查点系统已禁用',
|
||||
};
|
||||
}
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
// 验证检查点存在
|
||||
const checkpoint = await manager.getCheckpoint(checkpointId);
|
||||
if (!checkpoint) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `找不到检查点: ${checkpointId}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 解析文件列表
|
||||
const files = filesStr
|
||||
? filesStr.split(',').map((f) => f.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
// 执行恢复
|
||||
const result = await manager.rollback({
|
||||
target: checkpointId,
|
||||
files,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
if (dryRun) {
|
||||
const lines = [
|
||||
`预览: 恢复到检查点 ${checkpoint.commitHash.slice(0, 8)}`,
|
||||
'',
|
||||
'将恢复以下文件:',
|
||||
];
|
||||
|
||||
for (const file of result.restoredFiles) {
|
||||
lines.push(` - ${file}`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('(使用 dry_run=false 执行实际恢复)');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
const errorLines = ['恢复失败:'];
|
||||
for (const err of result.errors) {
|
||||
errorLines.push(` ${err.file}: ${err.error}`);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: errorLines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`✓ 已恢复到检查点 ${checkpoint.commitHash.slice(0, 8)}`,
|
||||
'',
|
||||
`恢复了 ${result.restoredFiles.length} 个文件:`,
|
||||
];
|
||||
|
||||
for (const file of result.restoredFiles.slice(0, 10)) {
|
||||
lines.push(` - ${file}`);
|
||||
}
|
||||
|
||||
if (result.restoredFiles.length > 10) {
|
||||
lines.push(` ... 还有 ${result.restoredFiles.length - 10} 个文件`);
|
||||
}
|
||||
|
||||
if (result.previousCommit) {
|
||||
lines.push('');
|
||||
lines.push(`提示: 可以使用 checkpoint_restore 恢复到 ${result.previousCommit.slice(0, 8)} 来撤销此操作`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 检查点工具模块
|
||||
*/
|
||||
|
||||
export { checkpointCreateTool } from './checkpoint_create.js';
|
||||
export { checkpointListTool } from './checkpoint_list.js';
|
||||
export { checkpointDiffTool } from './checkpoint_diff.js';
|
||||
export { checkpointRestoreTool } from './checkpoint_restore.js';
|
||||
export { undoTool } from './undo.js';
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 撤销操作工具 (快捷回滚)
|
||||
*/
|
||||
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getCheckpointManager } from '../../checkpoint/index.js';
|
||||
|
||||
export const undoTool: ToolWithMetadata = {
|
||||
name: 'undo',
|
||||
description: loadDescription('undo'),
|
||||
metadata: {
|
||||
name: 'undo',
|
||||
category: 'core',
|
||||
description: '撤销上一次文件操作,回滚到最近的检查点',
|
||||
keywords: ['undo', 'rollback', 'revert', '撤销', '回滚', '恢复'],
|
||||
deferLoading: false, // 常用命令,始终加载
|
||||
},
|
||||
parameters: {
|
||||
confirm: {
|
||||
type: 'boolean',
|
||||
description: '确认执行撤销操作 (默认 true)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const confirm = params.confirm !== false;
|
||||
|
||||
try {
|
||||
const manager = getCheckpointManager();
|
||||
|
||||
if (!manager.isEnabled()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '检查点系统已禁用,无法执行撤销操作',
|
||||
};
|
||||
}
|
||||
|
||||
await manager.initialize();
|
||||
|
||||
// 获取最近两个检查点
|
||||
const checkpoints = await manager.listCheckpoints();
|
||||
|
||||
if (checkpoints.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '没有可用的检查点,无法执行撤销操作',
|
||||
};
|
||||
}
|
||||
|
||||
// 预览模式
|
||||
if (!confirm) {
|
||||
const targetCheckpoint =
|
||||
checkpoints.length > 1 ? checkpoints[1] : checkpoints[0];
|
||||
|
||||
const diff = await manager.getDiff(targetCheckpoint.id);
|
||||
|
||||
const lines = [
|
||||
'预览: 撤销将恢复以下文件变更:',
|
||||
'',
|
||||
`目标检查点: ${targetCheckpoint.commitHash.slice(0, 8)}`,
|
||||
` ${targetCheckpoint.description || targetCheckpoint.trigger}`,
|
||||
` ${new Date(targetCheckpoint.timestamp).toLocaleString()}`,
|
||||
'',
|
||||
];
|
||||
|
||||
if (diff.files.length > 0) {
|
||||
lines.push('将恢复的文件:');
|
||||
for (const file of diff.files.slice(0, 10)) {
|
||||
const symbol =
|
||||
file.type === 'added'
|
||||
? '+'
|
||||
: file.type === 'deleted'
|
||||
? '-'
|
||||
: 'M';
|
||||
lines.push(` ${symbol} ${file.path}`);
|
||||
}
|
||||
if (diff.files.length > 10) {
|
||||
lines.push(` ... 还有 ${diff.files.length - 10} 个文件`);
|
||||
}
|
||||
} else {
|
||||
lines.push('(无文件变更)');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('使用 confirm=true 执行撤销');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
// 执行撤销
|
||||
const result = await manager.undo();
|
||||
|
||||
if (!result.success) {
|
||||
const errorLines = ['撤销失败:'];
|
||||
for (const err of result.errors) {
|
||||
errorLines.push(` ${err.file}: ${err.error}`);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: errorLines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
const lines = [
|
||||
'✓ 撤销成功',
|
||||
'',
|
||||
`恢复了 ${result.restoredFiles.length} 个文件`,
|
||||
];
|
||||
|
||||
if (result.restoredFiles.length > 0 && result.restoredFiles.length <= 5) {
|
||||
for (const file of result.restoredFiles) {
|
||||
lines.push(` - ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.previousCommit) {
|
||||
lines.push('');
|
||||
lines.push(
|
||||
`提示: 使用 checkpoint_restore --checkpoint_id ${result.previousCommit.slice(0, 8)} 可以撤销此操作`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: lines.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
Create a new checkpoint snapshot of the current workspace state.
|
||||
|
||||
Checkpoints are snapshots that can be used to restore the workspace to a previous state. This is useful as a safety net before making changes, or to mark important milestones in your work.
|
||||
|
||||
Parameters:
|
||||
- name: Optional name for the checkpoint (e.g., "before refactoring")
|
||||
- description: Optional description of the checkpoint state
|
||||
|
||||
The checkpoint stores all file changes and can be restored later using checkpoint_restore or undo commands.
|
||||
@@ -0,0 +1,12 @@
|
||||
Show the differences between a checkpoint and the current workspace.
|
||||
|
||||
Displays what has changed since the checkpoint was created, including:
|
||||
- Added, modified, and deleted files
|
||||
- Lines added and removed
|
||||
- Detailed file diff (when file parameter is provided)
|
||||
|
||||
Parameters:
|
||||
- checkpoint_id: The checkpoint ID or commit hash to compare (default: most recent)
|
||||
- file: Specific file path to show detailed diff (optional)
|
||||
|
||||
Use this to preview changes before restoring to a checkpoint.
|
||||
@@ -0,0 +1,13 @@
|
||||
List all available checkpoints in the workspace.
|
||||
|
||||
Shows checkpoint history including:
|
||||
- Checkpoint ID/hash
|
||||
- Name (if provided)
|
||||
- Description or trigger type
|
||||
- Creation timestamp
|
||||
- Number of files changed
|
||||
|
||||
Parameters:
|
||||
- limit: Maximum number of checkpoints to display (default: 10)
|
||||
|
||||
Use this to see available restore points before using checkpoint_restore or undo.
|
||||
@@ -0,0 +1,15 @@
|
||||
Restore the workspace to a specified checkpoint state.
|
||||
|
||||
This will revert all files to their state at the time of the checkpoint. Use with caution as it will overwrite current changes.
|
||||
|
||||
Parameters:
|
||||
- checkpoint_id: Required. The checkpoint ID or commit hash to restore to
|
||||
- files: Optional. Comma-separated list of specific files to restore (partial restore)
|
||||
- dry_run: Optional. If true, shows what would be restored without actually doing it
|
||||
|
||||
Examples:
|
||||
- Full restore: checkpoint_restore checkpoint_id="abc123"
|
||||
- Partial restore: checkpoint_restore checkpoint_id="abc123" files="src/index.ts,src/utils.ts"
|
||||
- Preview: checkpoint_restore checkpoint_id="abc123" dry_run=true
|
||||
|
||||
After restore, the previous state is saved and can be restored if needed.
|
||||
@@ -0,0 +1 @@
|
||||
复制文件或目录。支持递归复制整个目录结构。
|
||||
@@ -0,0 +1 @@
|
||||
创建新目录。支持递归创建父目录。如果目录已存在则不会报错。
|
||||
@@ -0,0 +1 @@
|
||||
删除文件或目录。删除目录时可以选择是否递归删除。需要谨慎使用。
|
||||
@@ -0,0 +1 @@
|
||||
通过字符串替换编辑文件的部分内容。比 write_file 更高效,适合修改文件的一小部分。
|
||||
@@ -0,0 +1 @@
|
||||
获取文件或目录的详细信息,包括大小、权限、创建时间、修改时间等元数据。
|
||||
@@ -0,0 +1 @@
|
||||
在指定目录中搜索文件内容。支持正则表达式,可以指定文件类型过滤。用于查找代码中的特定文本、函数调用、变量引用等。
|
||||
@@ -0,0 +1 @@
|
||||
列出指定目录下的文件和文件夹
|
||||
@@ -0,0 +1 @@
|
||||
移动或重命名文件/目录。可以将文件移动到新位置或更改文件名。
|
||||
@@ -0,0 +1,18 @@
|
||||
对单个文件执行多个搜索替换操作,支持在一次调用中进行多处修改。
|
||||
|
||||
使用场景:
|
||||
- 需要在同一文件中修改多处代码
|
||||
- 重构时需要同时修改多个相关位置
|
||||
- 批量更新变量名、函数名等
|
||||
|
||||
注意事项:
|
||||
1. 每个搜索字符串必须在文件中唯一匹配
|
||||
2. 所有替换按顺序执行,后续替换基于前面替换后的内容
|
||||
3. 如果任何一个搜索字符串无法唯一匹配,整个操作将失败
|
||||
4. 提供足够的上下文确保唯一匹配
|
||||
|
||||
edits 参数格式:
|
||||
[
|
||||
{"search": "要查找的内容1", "replace": "替换后的内容1"},
|
||||
{"search": "要查找的内容2", "replace": "替换后的内容2"}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
读取指定文件的内容
|
||||
@@ -0,0 +1 @@
|
||||
在目录中搜索匹配模式的文件
|
||||
@@ -0,0 +1 @@
|
||||
创建新文件或完全覆盖现有文件。如果只需修改文件的一部分,请使用 edit_file。
|
||||
@@ -0,0 +1,13 @@
|
||||
将文件变更添加到 Git 暂存区,准备提交。
|
||||
|
||||
参数说明:
|
||||
- files: 要暂存的文件列表(数组或单个文件路径)
|
||||
- all: 暂存所有变更(包括新文件和删除)
|
||||
- update: 仅暂存已跟踪文件的变更(不包括新文件)
|
||||
|
||||
使用场景:
|
||||
- 选择性暂存特定文件
|
||||
- 暂存所有变更准备提交
|
||||
- 分批次提交不同功能的代码
|
||||
|
||||
执行后会显示当前暂存状态。
|
||||
@@ -0,0 +1,15 @@
|
||||
管理 Git 分支:列出、创建、删除、重命名分支。
|
||||
|
||||
参数说明:
|
||||
- action: 操作类型
|
||||
- list: 列出分支(默认)
|
||||
- create: 创建新分支
|
||||
- delete: 删除分支
|
||||
- rename: 重命名分支
|
||||
- name: 分支名称(create/delete/rename 时必填)
|
||||
- new_name: 新分支名(rename 时必填)
|
||||
- remote: 显示远程分支
|
||||
- all: 显示所有分支(本地 + 远程)
|
||||
- force: 强制删除未合并的分支
|
||||
|
||||
注意:创建分支后需要使用 git_checkout 切换到新分支。
|
||||
@@ -0,0 +1,14 @@
|
||||
切换 Git 分支或恢复文件到指定状态。
|
||||
|
||||
参数说明:
|
||||
- target: 目标分支名或文件路径(必填)
|
||||
- create: 创建新分支并切换(相当于 git checkout -b)
|
||||
- force: 强制切换(丢弃本地未提交的修改)
|
||||
- file: 指定 target 是文件路径(恢复文件到 HEAD 状态)
|
||||
|
||||
常见场景:
|
||||
- 切换到已有分支: git_checkout with target: "main"
|
||||
- 创建并切换到新分支: git_checkout with target: "feature-xxx", create: true
|
||||
- 恢复单个文件: git_checkout with target: "src/file.ts", file: true
|
||||
|
||||
注意:切换分支前确保没有未提交的变更,或使用 git_stash 暂存。
|
||||
@@ -0,0 +1,14 @@
|
||||
创建 Git 提交,保存暂存区的变更。
|
||||
|
||||
参数说明:
|
||||
- message: 提交信息(必填,除非使用 amend + 无修改)
|
||||
- amend: 修改上次提交(可以修改提交信息或追加变更)
|
||||
- all: 自动暂存所有已跟踪文件的变更后提交
|
||||
|
||||
使用流程:
|
||||
1. 使用 git_add 暂存要提交的文件
|
||||
2. 使用 git_commit 创建提交
|
||||
|
||||
提交信息建议:
|
||||
- 简洁明了地描述本次变更
|
||||
- 使用祈使语气(如 "Add feature" 而非 "Added feature")
|
||||
@@ -0,0 +1,12 @@
|
||||
查看 Git 变更的详细差异,包括文件内容的具体修改。
|
||||
|
||||
参数说明:
|
||||
- path: 指定要查看差异的文件或目录路径
|
||||
- staged: 查看已暂存的变更(默认查看未暂存的变更)
|
||||
- commit: 与指定提交对比(如 HEAD~1, commit-hash)
|
||||
- stat: 仅显示统计信息(修改了多少行)
|
||||
|
||||
使用场景:
|
||||
- 查看修改了哪些内容
|
||||
- 提交前检查变更
|
||||
- 对比不同版本的代码
|
||||
@@ -0,0 +1,14 @@
|
||||
查看 Git 提交历史记录。
|
||||
|
||||
参数说明:
|
||||
- limit: 显示的提交数量(默认 10)
|
||||
- oneline: 每个提交单行显示
|
||||
- file: 查看指定文件的提交历史
|
||||
- author: 筛选指定作者的提交
|
||||
- since: 筛选指定日期之后的提交(如 "2024-01-01" 或 "1 week ago")
|
||||
- graph: 显示分支合并图
|
||||
|
||||
返回内容:
|
||||
- 提交 hash(简短)
|
||||
- 提交信息
|
||||
- 作者和时间
|
||||
@@ -0,0 +1,14 @@
|
||||
从远程仓库拉取更新并合并到本地分支。
|
||||
|
||||
参数说明:
|
||||
- remote: 远程仓库名(默认 origin)
|
||||
- branch: 要拉取的分支名(默认当前分支对应的远程分支)
|
||||
- rebase: 使用 rebase 而非 merge(保持提交历史线性)
|
||||
|
||||
常见场景:
|
||||
- 更新当前分支: git_pull
|
||||
- 使用 rebase 更新: git_pull with rebase: true
|
||||
|
||||
注意事项:
|
||||
- 如果有本地未提交的变更,可能需要先 git_stash 暂存
|
||||
- 如果发生冲突,需要手动解决后提交
|
||||
@@ -0,0 +1,15 @@
|
||||
将本地提交推送到远程仓库。
|
||||
|
||||
参数说明:
|
||||
- remote: 远程仓库名(默认 origin)
|
||||
- branch: 要推送的分支名(默认当前分支)
|
||||
- force: 强制推送(危险!会覆盖远程历史)
|
||||
- set_upstream: 设置上游分支(首次推送新分支时使用 -u)
|
||||
- tags: 推送所有标签
|
||||
|
||||
常见场景:
|
||||
- 推送当前分支: git_push
|
||||
- 首次推送新分支: git_push with set_upstream: true
|
||||
- 推送到特定分支: git_push with branch: "feature-xxx"
|
||||
|
||||
注意:如果推送被拒绝,通常需要先 git_pull 拉取远程更新。
|
||||
@@ -0,0 +1,19 @@
|
||||
临时保存工作区的变更,方便切换分支或拉取更新。
|
||||
|
||||
参数说明:
|
||||
- action: 操作类型
|
||||
- push: 保存当前变更(默认)
|
||||
- pop: 恢复最近的暂存并删除记录
|
||||
- apply: 恢复暂存但保留记录
|
||||
- list: 列出所有暂存
|
||||
- drop: 删除指定暂存
|
||||
- clear: 清除所有暂存
|
||||
- show: 显示暂存的详细内容
|
||||
- message: 暂存说明(push 时可用)
|
||||
- index: 暂存索引(pop/apply/drop 时可用,默认 0 即最近的)
|
||||
- include_untracked: 包含未跟踪的文件
|
||||
|
||||
使用流程:
|
||||
1. git_stash 保存当前工作
|
||||
2. 切换分支或拉取更新
|
||||
3. git_stash with action: "pop" 恢复工作
|
||||
@@ -0,0 +1,14 @@
|
||||
查看 Git 仓库的当前状态,包括已修改、已暂存和未跟踪的文件。
|
||||
|
||||
这是检查仓库状态的首选工具,可以快速了解当前工作区的变更情况。
|
||||
|
||||
参数说明:
|
||||
- short: 简短输出格式(类似 git status -s)
|
||||
- branch: 显示当前分支信息(默认 true)
|
||||
|
||||
返回内容:
|
||||
- 当前分支名
|
||||
- 与远程分支的差异(领先/落后提交数)
|
||||
- 已暂存的变更
|
||||
- 未暂存的变更
|
||||
- 未跟踪的文件
|
||||
@@ -0,0 +1,20 @@
|
||||
Generate a repository map showing the most relevant code symbols (functions, classes, methods) based on AST analysis and PageRank ranking.
|
||||
|
||||
This tool analyzes the codebase structure using Tree-sitter AST parsing and ranks symbols by their relevance using the PageRank algorithm. It helps AI understand the code structure by showing:
|
||||
- Important function and class definitions
|
||||
- Their relationships through references
|
||||
- Prioritized by relevance to the current context
|
||||
|
||||
Parameters:
|
||||
- directory: Directory to analyze (default: current working directory)
|
||||
- chat_files: Files currently being discussed (excluded from map output)
|
||||
- mentioned_files: File names mentioned in conversation (boost relevance)
|
||||
- mentioned_identifiers: Symbol names mentioned (boost relevance)
|
||||
- max_tokens: Maximum output token count (default: 1024)
|
||||
|
||||
The output shows relevant code symbols organized by file, helping to understand:
|
||||
- What functions/classes exist in the codebase
|
||||
- How different parts of the code relate to each other
|
||||
- Which symbols are most important based on usage patterns
|
||||
|
||||
Use this tool when you need to understand the overall structure of a codebase or find relevant code locations for a task.
|
||||
@@ -0,0 +1 @@
|
||||
执行 bash 命令。可以用于运行系统命令、安装包、git 操作等。
|
||||
@@ -0,0 +1,8 @@
|
||||
读取当前会话的待办事项列表。
|
||||
|
||||
返回所有待办事项及其状态:
|
||||
- pending: 待处理
|
||||
- in_progress: 进行中
|
||||
- completed: 已完成
|
||||
|
||||
用于查看任务进度和剩余工作。
|
||||
@@ -0,0 +1,13 @@
|
||||
管理会话的待办事项列表。
|
||||
|
||||
参数说明:
|
||||
- todos: 完整的待办事项列表,每项包含:
|
||||
- content: 任务内容
|
||||
- status: 状态(pending/in_progress/completed)
|
||||
|
||||
使用场景:
|
||||
- 规划复杂任务的步骤
|
||||
- 跟踪工作进度
|
||||
- 标记已完成的任务
|
||||
|
||||
注意:每次调用会替换整个列表,请确保包含所有需要保留的项目。
|
||||
@@ -0,0 +1,14 @@
|
||||
Undo the most recent file operation by restoring to the previous checkpoint.
|
||||
|
||||
This is a quick way to revert the last change. It restores all files to their state at the second-most-recent checkpoint (since the most recent checkpoint captures the current state).
|
||||
|
||||
Parameters:
|
||||
- confirm: Set to false to preview what would be undone without executing (default: true)
|
||||
|
||||
Usage:
|
||||
- Quick undo: undo
|
||||
- Preview first: undo confirm=false
|
||||
|
||||
If you need to undo multiple operations or restore to a specific point, use checkpoint_list and checkpoint_restore instead.
|
||||
|
||||
After undo, you can "redo" by using checkpoint_restore with the previous commit hash (shown in the output).
|
||||
@@ -0,0 +1,19 @@
|
||||
从指定的网页 URL 提取内容。使用 Tavily Extract API 智能解析网页,返回结构化的文本内容。
|
||||
|
||||
适用场景:
|
||||
- 获取网页文章的完整内容
|
||||
- 提取文档页面的详细信息
|
||||
- 抓取多个页面进行对比分析
|
||||
- 获取网页中的图片列表
|
||||
- 深度提取包含表格、嵌入内容的页面
|
||||
|
||||
参数说明:
|
||||
- urls: URL 列表(必填,最多 20 个,也可传单个 URL 字符串)
|
||||
- extract_depth: "basic" 快速提取 / "advanced" 深度提取(含表格等)
|
||||
- format: "markdown" / "text" 输出格式,默认 markdown
|
||||
- include_images: 是否包含图片列表,默认 false
|
||||
|
||||
返回内容:
|
||||
- 每个 URL 的提取内容
|
||||
- 图片列表(如果启用)
|
||||
- 失败的 URL 及错误信息
|
||||
@@ -0,0 +1,21 @@
|
||||
搜索网络获取最新信息。使用 Tavily API 进行智能搜索,返回相关网页内容和 AI 摘要。
|
||||
|
||||
这是进行网络搜索的首选工具,不要使用 curl 或 bash 命令来搜索网络。
|
||||
|
||||
适用场景:
|
||||
- 查询最新新闻、事件、游戏更新
|
||||
- 搜索技术文档、API 参考
|
||||
- 获取实时数据(股价、天气等)
|
||||
- 查找开源项目、库的信息
|
||||
- 了解最新的技术趋势
|
||||
|
||||
参数说明:
|
||||
- query: 搜索关键词(必填)
|
||||
- max_results: 返回结果数量,1-20,默认 5
|
||||
- search_depth: "basic" 快速搜索 / "advanced" 深度搜索
|
||||
- topic: "general" 通用 / "news" 新闻 / "finance" 财经
|
||||
- include_answer: 是否包含 AI 摘要,默认 true
|
||||
|
||||
返回内容:
|
||||
- AI 生成的摘要答案
|
||||
- 相关网页列表(标题、链接、内容摘要)
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
async function copyRecursive(source: string, dest: string): Promise<void> {
|
||||
const stats = await fs.stat(source);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
await fs.mkdir(dest, { recursive: true });
|
||||
const entries = await fs.readdir(source);
|
||||
for (const entry of entries) {
|
||||
await copyRecursive(path.join(source, entry), path.join(dest, entry));
|
||||
}
|
||||
} else {
|
||||
await fs.copyFile(source, dest);
|
||||
}
|
||||
}
|
||||
|
||||
export const copyFileTool: ToolWithMetadata = {
|
||||
name: 'copy_file',
|
||||
description: loadDescription('copy_file'),
|
||||
metadata: {
|
||||
name: 'copy_file',
|
||||
category: 'filesystem',
|
||||
description: '复制文件或目录',
|
||||
keywords: ['copy', 'file', 'cp', 'duplicate', '复制', '文件', '拷贝'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: '源文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
description: '目标路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const source = params.source as string;
|
||||
const destination = params.destination as string;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absoluteSource = path.isAbsolute(source)
|
||||
? source
|
||||
: path.join(cwd, source);
|
||||
|
||||
const absoluteDest = path.isAbsolute(destination)
|
||||
? destination
|
||||
: path.join(cwd, destination);
|
||||
|
||||
// 权限检查 - 源文件需要 read 权限
|
||||
const permissionManager = getPermissionManager();
|
||||
const sourcePermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'read',
|
||||
path: absoluteSource,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!sourcePermResult.allowed) {
|
||||
if (sourcePermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 读取 ${absoluteSource}\n原因: ${sourcePermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${sourcePermResult.reason || '不允许读取此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查 - 目标位置需要 copy 权限
|
||||
const destPermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'copy',
|
||||
path: absoluteDest,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!destPermResult.allowed) {
|
||||
if (destPermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 复制到 ${absoluteDest}\n原因: ${destPermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${destPermResult.reason || '不允许复制到此位置'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查源文件是否存在
|
||||
const sourceStats = await fs.stat(absoluteSource);
|
||||
|
||||
// 检查目标是否是目录
|
||||
let finalDest = absoluteDest;
|
||||
try {
|
||||
const destStats = await fs.stat(absoluteDest);
|
||||
if (destStats.isDirectory()) {
|
||||
// 如果目标是目录,将源文件复制到该目录下
|
||||
finalDest = path.join(absoluteDest, path.basename(absoluteSource));
|
||||
}
|
||||
} catch {
|
||||
// 目标不存在,直接使用目标路径
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
await fs.mkdir(path.dirname(finalDest), { recursive: true });
|
||||
|
||||
// 执行复制
|
||||
if (sourceStats.isDirectory()) {
|
||||
await copyRecursive(absoluteSource, finalDest);
|
||||
} else {
|
||||
await fs.copyFile(absoluteSource, finalDest);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `已复制: ${absoluteSource} -> ${finalDest}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
export const createDirectoryTool: ToolWithMetadata = {
|
||||
name: 'create_directory',
|
||||
description: loadDescription('create_directory'),
|
||||
metadata: {
|
||||
name: 'create_directory',
|
||||
category: 'filesystem',
|
||||
description: '创建目录',
|
||||
keywords: ['create', 'directory', 'mkdir', 'folder', 'new', '创建', '目录', '文件夹', '新建'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要创建的目录路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const dirPath = params.path as string;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absolutePath = path.isAbsolute(dirPath)
|
||||
? dirPath
|
||||
: path.join(cwd, dirPath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'mkdir',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 创建目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许创建此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查目录是否已存在
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
return {
|
||||
success: true,
|
||||
output: `目录已存在: ${absolutePath}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `路径已存在且不是目录: ${absolutePath}`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// 目录不存在,继续创建
|
||||
}
|
||||
|
||||
// 创建目录(递归创建父目录)
|
||||
await fs.mkdir(absolutePath, { recursive: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `已创建目录: ${absolutePath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
export const deleteFileTool: ToolWithMetadata = {
|
||||
name: 'delete_file',
|
||||
description: loadDescription('delete_file'),
|
||||
metadata: {
|
||||
name: 'delete_file',
|
||||
category: 'filesystem',
|
||||
description: '删除文件或目录',
|
||||
keywords: ['delete', 'remove', 'file', 'rm', '删除', '移除', '文件'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要删除的文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
description: '是否递归删除目录(默认 false)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const recursive = (params.recursive as boolean) || false;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'delete',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 删除 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许删除此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
if (!recursive) {
|
||||
// 检查目录是否为空
|
||||
const entries = await fs.readdir(absolutePath);
|
||||
if (entries.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `目录不为空。如需删除非空目录,请设置 recursive: true`,
|
||||
};
|
||||
}
|
||||
await fs.rmdir(absolutePath);
|
||||
} else {
|
||||
await fs.rm(absolutePath, { recursive: true });
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: `已删除目录: ${absolutePath}`,
|
||||
};
|
||||
} else {
|
||||
await fs.unlink(absolutePath);
|
||||
return {
|
||||
success: true,
|
||||
output: `已删除文件: ${absolutePath}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 编辑文件工具
|
||||
*
|
||||
* 使用统一的编辑模式系统
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
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 {
|
||||
createSingleSearchReplaceEdit,
|
||||
applyEdit,
|
||||
validateEdit,
|
||||
} from '../../editors/index.js';
|
||||
|
||||
export const editFileTool: ToolWithMetadata = {
|
||||
name: 'edit_file',
|
||||
description: loadDescription('edit_file'),
|
||||
metadata: {
|
||||
name: 'edit_file',
|
||||
category: 'filesystem',
|
||||
description: '编辑文件内容(查找替换)',
|
||||
keywords: ['edit', 'file', 'replace', 'modify', 'change', 'update', '编辑', '文件', '替换', '修改', '更新'],
|
||||
deferLoading: false, // 核心工具,始终可用
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要编辑的文件路径',
|
||||
required: true,
|
||||
},
|
||||
old_string: {
|
||||
type: 'string',
|
||||
description: '要被替换的原始字符串(必须精确匹配)',
|
||||
required: true,
|
||||
},
|
||||
new_string: {
|
||||
type: 'string',
|
||||
description: '替换后的新字符串',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const oldString = params.old_string as string;
|
||||
const newString = params.new_string as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 创建编辑对象
|
||||
const edit = createSingleSearchReplaceEdit(absolutePath, oldString, newString);
|
||||
|
||||
// 先验证
|
||||
const validation = await validateEdit(edit);
|
||||
if (!validation.valid) {
|
||||
// 格式化错误信息
|
||||
const errorMessages = validation.errors
|
||||
.map((e) => {
|
||||
if (e.type === 'not_found') {
|
||||
return `未找到要替换的字符串。请确保 old_string 与文件中的内容完全匹配(包括空格和换行)。`;
|
||||
}
|
||||
if (e.type === 'ambiguous') {
|
||||
return `找到 ${e.occurrences} 处匹配。old_string 必须唯一,请提供更多上下文使其唯一。`;
|
||||
}
|
||||
return e.message;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: errorMessages,
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'edit',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
oldContent: oldString,
|
||||
newContent: newString,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 编辑 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许编辑此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 应用编辑
|
||||
const result = await applyEdit(edit, {
|
||||
validate: false, // 已经验证过了
|
||||
runDiagnostics: true,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: result.error || '编辑失败',
|
||||
};
|
||||
}
|
||||
|
||||
// 构建输出
|
||||
let output = `文件已编辑: ${absolutePath}`;
|
||||
|
||||
if (result.diagnostics) {
|
||||
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
let size = bytes;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatPermissions(mode: number): string {
|
||||
const types: Record<number, string> = {
|
||||
0o140000: 'socket',
|
||||
0o120000: 'symbolic link',
|
||||
0o100000: 'regular file',
|
||||
0o060000: 'block device',
|
||||
0o040000: 'directory',
|
||||
0o020000: 'character device',
|
||||
0o010000: 'FIFO',
|
||||
};
|
||||
|
||||
const fileType = Object.entries(types).find(([mask]) => (mode & 0o170000) === Number(mask));
|
||||
|
||||
const perms = [
|
||||
(mode & 0o400) ? 'r' : '-',
|
||||
(mode & 0o200) ? 'w' : '-',
|
||||
(mode & 0o100) ? 'x' : '-',
|
||||
(mode & 0o040) ? 'r' : '-',
|
||||
(mode & 0o020) ? 'w' : '-',
|
||||
(mode & 0o010) ? 'x' : '-',
|
||||
(mode & 0o004) ? 'r' : '-',
|
||||
(mode & 0o002) ? 'w' : '-',
|
||||
(mode & 0o001) ? 'x' : '-',
|
||||
].join('');
|
||||
|
||||
return `${fileType?.[1] || 'unknown'} (${perms})`;
|
||||
}
|
||||
|
||||
export const getFileInfoTool: ToolWithMetadata = {
|
||||
name: 'get_file_info',
|
||||
description: loadDescription('get_file_info'),
|
||||
metadata: {
|
||||
name: 'get_file_info',
|
||||
category: 'filesystem',
|
||||
description: '获取文件元信息',
|
||||
keywords: ['file', 'info', 'stat', 'size', 'permission', 'metadata', '文件', '信息', '大小', '权限', '属性'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'info',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 获取文件信息 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许获取此文件信息'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
const info = [
|
||||
`路径: ${absolutePath}`,
|
||||
`类型: ${stats.isDirectory() ? '目录' : stats.isFile() ? '文件' : stats.isSymbolicLink() ? '符号链接' : '其他'}`,
|
||||
`大小: ${formatSize(stats.size)}`,
|
||||
`权限: ${formatPermissions(stats.mode)}`,
|
||||
`创建时间: ${stats.birthtime.toLocaleString()}`,
|
||||
`修改时间: ${stats.mtime.toLocaleString()}`,
|
||||
`访问时间: ${stats.atime.toLocaleString()}`,
|
||||
`inode: ${stats.ino}`,
|
||||
`硬链接数: ${stats.nlink}`,
|
||||
];
|
||||
|
||||
// 如果是符号链接,显示目标
|
||||
if (stats.isSymbolicLink()) {
|
||||
try {
|
||||
const target = await fs.readlink(absolutePath);
|
||||
info.push(`链接目标: ${target}`);
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是目录,统计子项数量
|
||||
if (stats.isDirectory()) {
|
||||
try {
|
||||
const entries = await fs.readdir(absolutePath);
|
||||
info.push(`子项数量: ${entries.length}`);
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: info.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
interface GrepMatch {
|
||||
file: string;
|
||||
line: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const grepContentTool: ToolWithMetadata = {
|
||||
name: 'grep_content',
|
||||
description: loadDescription('grep_content'),
|
||||
metadata: {
|
||||
name: 'grep_content',
|
||||
category: 'filesystem',
|
||||
description: '在文件内容中搜索文本',
|
||||
keywords: ['grep', 'search', 'content', 'text', 'find', 'regex', '搜索', '内容', '文本', '查找', '正则'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
directory: {
|
||||
type: 'string',
|
||||
description: '搜索的起始目录',
|
||||
required: true,
|
||||
},
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: '搜索的文本或正则表达式模式',
|
||||
required: true,
|
||||
},
|
||||
file_pattern: {
|
||||
type: 'string',
|
||||
description: '文件名匹配模式(可选,如 *.ts)',
|
||||
required: false,
|
||||
},
|
||||
max_results: {
|
||||
type: 'number',
|
||||
description: '最大结果数量(可选,默认 100)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const directory = params.directory as string;
|
||||
const pattern = params.pattern as string;
|
||||
const filePattern = params.file_pattern as string | undefined;
|
||||
const maxResults = (params.max_results as number) || 100;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(directory)
|
||||
? directory
|
||||
: path.join(cwd, directory);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'grep',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 搜索目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const matches: GrepMatch[] = [];
|
||||
const searchRegex = new RegExp(pattern, 'gi');
|
||||
const fileRegex = filePattern
|
||||
? new RegExp(filePattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i')
|
||||
: null;
|
||||
|
||||
async function searchFile(filePath: string): Promise<void> {
|
||||
if (matches.length >= maxResults) return;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (matches.length >= maxResults) break;
|
||||
|
||||
if (searchRegex.test(lines[i])) {
|
||||
matches.push({
|
||||
file: filePath,
|
||||
line: i + 1,
|
||||
content: lines[i].trim().substring(0, 200), // 截断过长的行
|
||||
});
|
||||
}
|
||||
// 重置正则表达式的 lastIndex
|
||||
searchRegex.lastIndex = 0;
|
||||
}
|
||||
} catch {
|
||||
// 忽略无法读取的文件(如二进制文件)
|
||||
}
|
||||
}
|
||||
|
||||
async function searchDirectory(dir: string, depth = 0): Promise<void> {
|
||||
if (depth > 10 || matches.length >= maxResults) return;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (matches.length >= maxResults) break;
|
||||
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
// 跳过隐藏文件和 node_modules
|
||||
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await searchDirectory(fullPath, depth + 1);
|
||||
} else if (entry.isFile()) {
|
||||
// 检查文件名是否匹配
|
||||
if (fileRegex && !fileRegex.test(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
await searchFile(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略权限错误
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await searchDirectory(absolutePath);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: '没有找到匹配的内容',
|
||||
};
|
||||
}
|
||||
|
||||
const output = matches
|
||||
.map((m) => `${m.file}:${m.line}: ${m.content}`)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `找到 ${matches.length} 处匹配${matches.length >= maxResults ? '(已达上限)' : ''}:\n\n${output}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// 文件读写
|
||||
export { readFileTool } from './read_file.js';
|
||||
export { writeFileTool } from './write_file.js';
|
||||
export { editFileTool } from './edit_file.js';
|
||||
export { multiEditTool } from './multi_edit.js';
|
||||
|
||||
// 目录操作
|
||||
export { listDirTool } from './list_directory.js';
|
||||
export { createDirectoryTool } from './create_directory.js';
|
||||
|
||||
// 搜索
|
||||
export { searchFilesTool } from './search_files.js';
|
||||
export { grepContentTool } from './grep_content.js';
|
||||
|
||||
// 文件信息
|
||||
export { getFileInfoTool } from './get_file_info.js';
|
||||
|
||||
// 文件管理
|
||||
export { moveFileTool } from './move_file.js';
|
||||
export { copyFileTool } from './copy_file.js';
|
||||
export { deleteFileTool } from './delete_file.js';
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
export const listDirTool: ToolWithMetadata = {
|
||||
name: 'list_directory',
|
||||
description: loadDescription('list_directory'),
|
||||
metadata: {
|
||||
name: 'list_directory',
|
||||
category: 'filesystem',
|
||||
description: '列出目录内容',
|
||||
keywords: ['list', 'directory', 'ls', 'dir', 'folder', '列出', '目录', '文件夹', '查看'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要列出的目录路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const dirPath = params.path as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(dirPath)
|
||||
? dirPath
|
||||
: path.join(cwd, dirPath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'list',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 列出目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许列出此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
|
||||
const result = entries
|
||||
.map((entry) => {
|
||||
const prefix = entry.isDirectory() ? '📁' : '📄';
|
||||
return `${prefix} ${entry.name}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: result || '(空目录)',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
export const moveFileTool: ToolWithMetadata = {
|
||||
name: 'move_file',
|
||||
description: loadDescription('move_file'),
|
||||
metadata: {
|
||||
name: 'move_file',
|
||||
category: 'filesystem',
|
||||
description: '移动或重命名文件/目录',
|
||||
keywords: ['move', 'rename', 'file', 'mv', '移动', '重命名', '文件'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: '源文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
description: '目标路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const source = params.source as string;
|
||||
const destination = params.destination as string;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absoluteSource = path.isAbsolute(source)
|
||||
? source
|
||||
: path.join(cwd, source);
|
||||
|
||||
const absoluteDest = path.isAbsolute(destination)
|
||||
? destination
|
||||
: path.join(cwd, destination);
|
||||
|
||||
// 权限检查 - 源文件需要 move 权限
|
||||
const permissionManager = getPermissionManager();
|
||||
const sourcePermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'move',
|
||||
path: absoluteSource,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!sourcePermResult.allowed) {
|
||||
if (sourcePermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 移动 ${absoluteSource}\n原因: ${sourcePermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${sourcePermResult.reason || '不允许移动此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查 - 目标位置需要 write 权限
|
||||
const destPermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'write',
|
||||
path: absoluteDest,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!destPermResult.allowed) {
|
||||
if (destPermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 写入到 ${absoluteDest}\n原因: ${destPermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${destPermResult.reason || '不允许写入到此位置'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查源文件是否存在
|
||||
await fs.access(absoluteSource);
|
||||
|
||||
// 检查目标是否是目录
|
||||
let finalDest = absoluteDest;
|
||||
try {
|
||||
const destStats = await fs.stat(absoluteDest);
|
||||
if (destStats.isDirectory()) {
|
||||
// 如果目标是目录,将源文件移动到该目录下
|
||||
finalDest = path.join(absoluteDest, path.basename(absoluteSource));
|
||||
}
|
||||
} catch {
|
||||
// 目标不存在,直接使用目标路径
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
await fs.mkdir(path.dirname(finalDest), { recursive: true });
|
||||
|
||||
// 执行移动
|
||||
await fs.rename(absoluteSource, finalDest);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `已移动: ${absoluteSource} -> ${finalDest}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 多块编辑工具
|
||||
*
|
||||
* 在单个文件中执行多个搜索替换操作
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
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 {
|
||||
createSearchReplaceEdit,
|
||||
applyEdit,
|
||||
validateEdit,
|
||||
type SearchReplaceBlock,
|
||||
} from '../../editors/index.js';
|
||||
|
||||
export const multiEditTool: ToolWithMetadata = {
|
||||
name: 'multi_edit',
|
||||
description: loadDescription('multi_edit'),
|
||||
metadata: {
|
||||
name: 'multi_edit',
|
||||
category: 'filesystem',
|
||||
description: '对文件执行多个搜索替换操作',
|
||||
keywords: [
|
||||
'multi',
|
||||
'edit',
|
||||
'batch',
|
||||
'replace',
|
||||
'refactor',
|
||||
'多处',
|
||||
'批量',
|
||||
'编辑',
|
||||
'替换',
|
||||
'重构',
|
||||
],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要编辑的文件路径',
|
||||
required: true,
|
||||
},
|
||||
edits: {
|
||||
type: 'array',
|
||||
description: '编辑操作数组,每个元素包含 search 和 replace 字段',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const editsParam = params.edits as unknown[];
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 解析编辑块
|
||||
const blocks: SearchReplaceBlock[] = [];
|
||||
try {
|
||||
for (const item of editsParam) {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '每个编辑项必须是包含 search 和 replace 字段的对象',
|
||||
};
|
||||
}
|
||||
|
||||
const edit = item as Record<string, unknown>;
|
||||
if (typeof edit.search !== 'string' || typeof edit.replace !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '每个编辑项必须包含 search 和 replace 字符串字段',
|
||||
};
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
search: edit.search,
|
||||
replace: edit.replace,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `解析编辑参数失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '至少需要一个编辑操作',
|
||||
};
|
||||
}
|
||||
|
||||
// 创建编辑对象
|
||||
const edit = createSearchReplaceEdit(absolutePath, blocks);
|
||||
|
||||
// 先验证
|
||||
const validation = await validateEdit(edit);
|
||||
if (!validation.valid) {
|
||||
const errorMessages = validation.errors
|
||||
.map((e) => {
|
||||
if (e.search) {
|
||||
const preview = e.search.length > 50 ? e.search.slice(0, 50) + '...' : e.search;
|
||||
return `${e.message}\n 搜索内容: "${preview}"`;
|
||||
}
|
||||
return e.message;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `验证失败:\n${errorMessages}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
|
||||
// 为每个块创建简要的变更描述
|
||||
const changesSummary = blocks
|
||||
.map((b, i) => {
|
||||
const searchPreview = b.search.length > 30 ? b.search.slice(0, 30) + '...' : b.search;
|
||||
const replacePreview = b.replace.length > 30 ? b.replace.slice(0, 30) + '...' : b.replace;
|
||||
return `${i + 1}. "${searchPreview}" -> "${replacePreview}"`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'edit',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
oldContent: `[多处编辑 - ${blocks.length} 处修改]\n${changesSummary}`,
|
||||
newContent: '[见上方变更摘要]',
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 编辑 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许编辑此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 应用编辑
|
||||
const result = await applyEdit(edit, {
|
||||
validate: false, // 已经验证过了
|
||||
runDiagnostics: true,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: result.error || '编辑失败',
|
||||
};
|
||||
}
|
||||
|
||||
// 构建输出
|
||||
let output = `文件已编辑: ${absolutePath}\n`;
|
||||
output += `应用了 ${result.stats?.blocksApplied || blocks.length} 处修改\n`;
|
||||
output += `+${result.stats?.additions || 0} 行 / -${result.stats?.deletions || 0} 行`;
|
||||
|
||||
if (result.diagnostics) {
|
||||
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
export const readFileTool: ToolWithMetadata = {
|
||||
name: 'read_file',
|
||||
description: loadDescription('read_file'),
|
||||
metadata: {
|
||||
name: 'read_file',
|
||||
category: 'filesystem',
|
||||
description: '读取文件内容',
|
||||
keywords: ['read', 'file', 'content', 'cat', 'view', 'open', '读取', '文件', '内容', '查看', '打开'],
|
||||
deferLoading: false, // 核心工具,始终可用
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要读取的文件路径(相对或绝对路径)',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'read',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 读取 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许读取此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(absolutePath, 'utf-8');
|
||||
return {
|
||||
success: true,
|
||||
output: content,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
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';
|
||||
|
||||
export const searchFilesTool: ToolWithMetadata = {
|
||||
name: 'search_files',
|
||||
description: loadDescription('search_files'),
|
||||
metadata: {
|
||||
name: 'search_files',
|
||||
category: 'filesystem',
|
||||
description: '按文件名搜索文件',
|
||||
keywords: ['search', 'file', 'find', 'glob', 'pattern', '搜索', '文件', '查找', '匹配'],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
directory: {
|
||||
type: 'string',
|
||||
description: '搜索的起始目录',
|
||||
required: true,
|
||||
},
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: '文件名匹配模式(支持 glob 模式,如 *.ts)',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const directory = params.directory as string;
|
||||
const pattern = params.pattern as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(directory)
|
||||
? directory
|
||||
: path.join(cwd, directory);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'search',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 搜索目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const matches: string[] = [];
|
||||
const regex = new RegExp(
|
||||
pattern.replace(/\*/g, '.*').replace(/\?/g, '.'),
|
||||
'i'
|
||||
);
|
||||
|
||||
async function searchRecursive(dir: string, depth = 0): Promise<void> {
|
||||
if (depth > 10) return;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await searchRecursive(fullPath, depth + 1);
|
||||
} else if (regex.test(entry.name)) {
|
||||
matches.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略权限错误
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await searchRecursive(absolutePath);
|
||||
return {
|
||||
success: true,
|
||||
output:
|
||||
matches.length > 0
|
||||
? matches.join('\n')
|
||||
: '没有找到匹配的文件',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 写入文件工具
|
||||
*
|
||||
* 使用统一的编辑模式系统
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
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 {
|
||||
createWholeFileEdit,
|
||||
applyEdit,
|
||||
} from '../../editors/index.js';
|
||||
|
||||
export const writeFileTool: ToolWithMetadata = {
|
||||
name: 'write_file',
|
||||
description: loadDescription('write_file'),
|
||||
metadata: {
|
||||
name: 'write_file',
|
||||
category: 'filesystem',
|
||||
description: '写入文件内容',
|
||||
keywords: ['write', 'file', 'save', 'create', '写入', '文件', '保存', '创建', '新建'],
|
||||
deferLoading: false, // 核心工具,始终可用
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要写入的文件路径',
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: '要写入的内容',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const content = params.content as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'write',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
newContent: content,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 写入 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许写入此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 创建编辑对象
|
||||
const edit = createWholeFileEdit(absolutePath, content);
|
||||
|
||||
// 应用编辑
|
||||
const result = await applyEdit(edit, {
|
||||
validate: true,
|
||||
runDiagnostics: true,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: result.error || '写入失败',
|
||||
};
|
||||
}
|
||||
|
||||
// 构建输出
|
||||
let output = `文件已写入: ${absolutePath}`;
|
||||
|
||||
if (result.diagnostics) {
|
||||
output += `\n\n⚠️ 代码检查发现问题,请修复:${result.diagnostics}`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { ToolWithMetadata } from './types.js';
|
||||
import { toolRegistry } from './registry.js';
|
||||
|
||||
// Shell 工具
|
||||
import { bashTool } from './shell/index.js';
|
||||
|
||||
// 核心工具
|
||||
import { toolSearchTool } from './tool-search.js';
|
||||
import { todoReadTool, todoWriteTool } from './todo/index.js';
|
||||
|
||||
// Task 工具(Agent 子任务)
|
||||
import { taskTool, agentOutputTool } from './task/index.js';
|
||||
|
||||
// Skill 工具
|
||||
import { skillTool, skillSearchTool } from './skill/index.js';
|
||||
|
||||
// 文件系统工具
|
||||
import {
|
||||
readFileTool,
|
||||
writeFileTool,
|
||||
editFileTool,
|
||||
multiEditTool,
|
||||
listDirTool,
|
||||
createDirectoryTool,
|
||||
searchFilesTool,
|
||||
grepContentTool,
|
||||
getFileInfoTool,
|
||||
moveFileTool,
|
||||
copyFileTool,
|
||||
deleteFileTool,
|
||||
} from './filesystem/index.js';
|
||||
|
||||
// Web 工具
|
||||
import { webSearchTool, webExtractTool } from './web/index.js';
|
||||
|
||||
// Git 工具
|
||||
import {
|
||||
gitStatusTool,
|
||||
gitDiffTool,
|
||||
gitLogTool,
|
||||
gitBranchTool,
|
||||
gitAddTool,
|
||||
gitCommitTool,
|
||||
gitPushTool,
|
||||
gitPullTool,
|
||||
gitCheckoutTool,
|
||||
gitStashTool,
|
||||
} from './git/index.js';
|
||||
|
||||
// RepoMap 工具
|
||||
import { repoMapTool } from './repomap/index.js';
|
||||
|
||||
// 检查点工具
|
||||
import {
|
||||
checkpointCreateTool,
|
||||
checkpointListTool,
|
||||
checkpointDiffTool,
|
||||
checkpointRestoreTool,
|
||||
undoTool,
|
||||
} from './checkpoint/index.js';
|
||||
|
||||
// 所有工具列表(用于注册)
|
||||
const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
// 核心工具 (deferLoading: false)
|
||||
toolSearchTool,
|
||||
bashTool,
|
||||
todoReadTool,
|
||||
todoWriteTool,
|
||||
taskTool,
|
||||
agentOutputTool,
|
||||
|
||||
// Skill 工具 (deferLoading: false)
|
||||
skillTool,
|
||||
skillSearchTool,
|
||||
|
||||
// 文件系统工具 (deferLoading: true)
|
||||
readFileTool,
|
||||
writeFileTool,
|
||||
editFileTool,
|
||||
multiEditTool,
|
||||
listDirTool,
|
||||
createDirectoryTool,
|
||||
searchFilesTool,
|
||||
grepContentTool,
|
||||
getFileInfoTool,
|
||||
moveFileTool,
|
||||
copyFileTool,
|
||||
deleteFileTool,
|
||||
|
||||
// Web 工具 (deferLoading: false)
|
||||
webSearchTool,
|
||||
webExtractTool,
|
||||
|
||||
// Git 工具 (deferLoading: false)
|
||||
gitStatusTool,
|
||||
gitDiffTool,
|
||||
gitLogTool,
|
||||
gitBranchTool,
|
||||
gitAddTool,
|
||||
gitCommitTool,
|
||||
gitPushTool,
|
||||
gitPullTool,
|
||||
gitCheckoutTool,
|
||||
gitStashTool,
|
||||
|
||||
// RepoMap 工具 (deferLoading: true)
|
||||
repoMapTool,
|
||||
|
||||
// 检查点工具
|
||||
undoTool, // deferLoading: false - 常用命令
|
||||
checkpointCreateTool,
|
||||
checkpointListTool,
|
||||
checkpointDiffTool,
|
||||
checkpointRestoreTool,
|
||||
];
|
||||
|
||||
// 注册所有工具到 registry
|
||||
toolRegistry.registerAll(allToolsWithMetadata);
|
||||
|
||||
// 导出
|
||||
export { toolRegistry } from './registry.js';
|
||||
export { toolSearchTool } from './tool-search.js';
|
||||
export { todoManager } from './todo/index.js';
|
||||
export { initTaskContext, updateTaskDescription } from './task/index.js';
|
||||
export { updateSkillDescription } from './skill/index.js';
|
||||
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 工具名到子目录的映射
|
||||
const TOOL_CATEGORY_MAP: Record<string, string> = {
|
||||
// shell
|
||||
bash: 'shell',
|
||||
// filesystem
|
||||
read_file: 'filesystem',
|
||||
write_file: 'filesystem',
|
||||
edit_file: 'filesystem',
|
||||
multi_edit: 'filesystem',
|
||||
list_directory: 'filesystem',
|
||||
create_directory: 'filesystem',
|
||||
search_files: 'filesystem',
|
||||
grep_content: 'filesystem',
|
||||
get_file_info: 'filesystem',
|
||||
move_file: 'filesystem',
|
||||
copy_file: 'filesystem',
|
||||
delete_file: 'filesystem',
|
||||
// web
|
||||
web_search: 'web',
|
||||
web_extract: 'web',
|
||||
// git
|
||||
git_status: 'git',
|
||||
git_diff: 'git',
|
||||
git_log: 'git',
|
||||
git_branch: 'git',
|
||||
git_add: 'git',
|
||||
git_commit: 'git',
|
||||
git_push: 'git',
|
||||
git_pull: 'git',
|
||||
git_checkout: 'git',
|
||||
git_stash: 'git',
|
||||
// todo
|
||||
todo_read: 'todo',
|
||||
todo_write: 'todo',
|
||||
};
|
||||
|
||||
export function loadDescription(toolName: string): string {
|
||||
const category = TOOL_CATEGORY_MAP[toolName];
|
||||
const filePath = category
|
||||
? path.join(__dirname, 'descriptions', category, `${toolName}.txt`)
|
||||
: path.join(__dirname, 'descriptions', `${toolName}.txt`);
|
||||
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8').trim();
|
||||
} catch {
|
||||
throw new Error(`无法加载工具描述文件: ${filePath}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Tool } from '../types/index.js';
|
||||
import type { ToolMetadata, ToolWithMetadata, ToolSearchResult } from './types.js';
|
||||
import { searchTools } from './search.js';
|
||||
|
||||
/**
|
||||
* 工具注册中心
|
||||
* 管理所有工具的注册、查询和搜索
|
||||
*/
|
||||
class ToolRegistry {
|
||||
private tools: Map<string, ToolWithMetadata> = new Map();
|
||||
|
||||
/**
|
||||
* 注册单个工具
|
||||
*/
|
||||
register(tool: ToolWithMetadata): void {
|
||||
this.tools.set(tool.name, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册工具
|
||||
*/
|
||||
registerAll(tools: ToolWithMetadata[]): void {
|
||||
for (const tool of tools) {
|
||||
this.register(tool);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取核心工具 (deferLoading: false)
|
||||
* 这些工具在会话开始时就可用
|
||||
*/
|
||||
getCoreTools(): Tool[] {
|
||||
const coreTools: Tool[] = [];
|
||||
for (const tool of this.tools.values()) {
|
||||
if (!tool.metadata.deferLoading) {
|
||||
coreTools.push(this.toBasicTool(tool));
|
||||
}
|
||||
}
|
||||
return coreTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定工具
|
||||
*/
|
||||
getTool(name: string): Tool | undefined {
|
||||
const tool = this.tools.get(name);
|
||||
return tool ? this.toBasicTool(tool) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个工具
|
||||
*/
|
||||
getTools(names: string[]): Tool[] {
|
||||
const result: Tool[] = [];
|
||||
for (const name of names) {
|
||||
const tool = this.getTool(name);
|
||||
if (tool) {
|
||||
result.push(tool);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索工具
|
||||
* @param query 搜索查询
|
||||
* @param limit 返回结果数量限制
|
||||
* @returns 匹配的工具元数据列表
|
||||
*/
|
||||
search(query: string, limit: number = 5): ToolSearchResult[] {
|
||||
const allMetadata = this.getAllMetadata();
|
||||
return searchTools(query, allMetadata, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有工具的元数据
|
||||
*/
|
||||
getAllMetadata(): ToolMetadata[] {
|
||||
return [...this.tools.values()].map((tool) => tool.metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有工具 (用于兼容旧代码)
|
||||
*/
|
||||
getAllTools(): Tool[] {
|
||||
return [...this.tools.values()].map((tool) => this.toBasicTool(tool));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查工具是否存在
|
||||
*/
|
||||
has(name: string): boolean {
|
||||
return this.tools.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具数量
|
||||
*/
|
||||
get size(): number {
|
||||
return this.tools.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ToolWithMetadata 转换为基础 Tool 类型
|
||||
*/
|
||||
private toBasicTool(tool: ToolWithMetadata): Tool {
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
execute: tool.execute,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const toolRegistry = new ToolRegistry();
|
||||
|
||||
// 也导出类,方便测试
|
||||
export { ToolRegistry };
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* RepoMap 工具模块
|
||||
*/
|
||||
|
||||
export { repoMapTool } from './repo_map.js';
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* RepoMap 工具
|
||||
* 生成代码仓库的上下文地图,帮助 AI 理解代码结构
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
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 { createRepoMap, type RepoMapConfig } from '../../repomap/index.js';
|
||||
|
||||
// 缓存 RepoMap 实例
|
||||
const repoMapCache = new Map<string, ReturnType<typeof createRepoMap>>();
|
||||
|
||||
/**
|
||||
* 获取或创建 RepoMap 实例
|
||||
*/
|
||||
function getRepoMap(root: string, config?: Partial<RepoMapConfig>) {
|
||||
const key = root;
|
||||
if (!repoMapCache.has(key)) {
|
||||
repoMapCache.set(key, createRepoMap(root, config));
|
||||
}
|
||||
return repoMapCache.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的文件扩展名
|
||||
*/
|
||||
const SUPPORTED_EXTENSIONS = new Set([
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.mjs',
|
||||
'.cjs',
|
||||
'.py',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 要排除的目录
|
||||
*/
|
||||
const EXCLUDED_DIRS = new Set([
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'.git',
|
||||
'.next',
|
||||
'__pycache__',
|
||||
'.pytest_cache',
|
||||
'coverage',
|
||||
'.nyc_output',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 要排除的文件模式
|
||||
*/
|
||||
function shouldExcludeFile(filename: string): boolean {
|
||||
return (
|
||||
filename.includes('.test.') ||
|
||||
filename.includes('.spec.') ||
|
||||
filename.endsWith('.d.ts')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取目录中的文件
|
||||
*/
|
||||
async function getFilesRecursive(
|
||||
directory: string,
|
||||
maxDepth = 10
|
||||
): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(dir: string, depth: number): Promise<void> {
|
||||
if (depth > maxDepth) return;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
||||
await walk(fullPath, depth + 1);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (SUPPORTED_EXTENSIONS.has(ext) && !shouldExcludeFile(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略无法访问的目录
|
||||
}
|
||||
}
|
||||
|
||||
await walk(directory, 0);
|
||||
return files;
|
||||
}
|
||||
|
||||
export const repoMapTool: ToolWithMetadata = {
|
||||
name: 'repo_map',
|
||||
description: loadDescription('repo_map'),
|
||||
metadata: {
|
||||
name: 'repo_map',
|
||||
category: 'core',
|
||||
description: '生成代码仓库上下文地图,帮助理解代码结构',
|
||||
keywords: [
|
||||
'repomap',
|
||||
'repo',
|
||||
'map',
|
||||
'context',
|
||||
'code',
|
||||
'structure',
|
||||
'ast',
|
||||
'symbol',
|
||||
'definition',
|
||||
'代码地图',
|
||||
'仓库结构',
|
||||
'代码结构',
|
||||
'符号',
|
||||
'定义',
|
||||
],
|
||||
deferLoading: true,
|
||||
},
|
||||
parameters: {
|
||||
directory: {
|
||||
type: 'string',
|
||||
description: '要分析的目录路径(默认为当前工作目录)',
|
||||
required: false,
|
||||
},
|
||||
chat_files: {
|
||||
type: 'string',
|
||||
description:
|
||||
'当前对话中涉及的文件列表,逗号分隔(这些文件会被排除在地图外)',
|
||||
required: false,
|
||||
},
|
||||
mentioned_files: {
|
||||
type: 'string',
|
||||
description: '对话中提到的文件名,逗号分隔(用于提高相关性)',
|
||||
required: false,
|
||||
},
|
||||
mentioned_identifiers: {
|
||||
type: 'string',
|
||||
description: '对话中提到的标识符/符号名,逗号分隔(用于提高相关性)',
|
||||
required: false,
|
||||
},
|
||||
max_tokens: {
|
||||
type: 'number',
|
||||
description: '最大输出 token 数(默认 1024)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const cwd = process.cwd();
|
||||
const directory = (params.directory as string) || cwd;
|
||||
const absolutePath = path.isAbsolute(directory)
|
||||
? directory
|
||||
: path.join(cwd, directory);
|
||||
|
||||
// 解析逗号分隔的字符串为数组
|
||||
const chatFilesStr = (params.chat_files as string) || '';
|
||||
const chatFiles = chatFilesStr
|
||||
? chatFilesStr.split(',').map((s) => s.trim())
|
||||
: [];
|
||||
|
||||
const mentionedFilesStr = (params.mentioned_files as string) || '';
|
||||
const mentionedFiles = new Set(
|
||||
mentionedFilesStr ? mentionedFilesStr.split(',').map((s) => s.trim()) : []
|
||||
);
|
||||
|
||||
const mentionedIdentsStr = (params.mentioned_identifiers as string) || '';
|
||||
const mentionedIdents = new Set(
|
||||
mentionedIdentsStr ? mentionedIdentsStr.split(',').map((s) => s.trim()) : []
|
||||
);
|
||||
|
||||
const maxTokens = (params.max_tokens as number) || 1024;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'search',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 分析目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许分析此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查目录是否存在
|
||||
const stat = await fs.stat(absolutePath);
|
||||
if (!stat.isDirectory()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `${absolutePath} 不是一个目录`,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取 RepoMap 实例
|
||||
const repoMap = getRepoMap(absolutePath, {
|
||||
mapTokens: maxTokens,
|
||||
});
|
||||
|
||||
// 获取文件列表
|
||||
const allFiles = await getFilesRecursive(absolutePath);
|
||||
|
||||
// 分离聊天文件和其他文件
|
||||
const chatFilesAbs = chatFiles.map((f) =>
|
||||
path.isAbsolute(f) ? f : path.join(absolutePath, f)
|
||||
);
|
||||
const otherFiles = allFiles.filter((f) => !chatFilesAbs.includes(f));
|
||||
|
||||
// 生成 repo map
|
||||
const mapContent = await repoMap.getRepoMap(
|
||||
chatFilesAbs,
|
||||
otherFiles,
|
||||
mentionedFiles,
|
||||
mentionedIdents
|
||||
);
|
||||
|
||||
if (!mapContent || mapContent.trim() === '') {
|
||||
return {
|
||||
success: true,
|
||||
output:
|
||||
'未找到相关的代码符号。可能是因为目录中没有支持的代码文件,或者文件过少。',
|
||||
};
|
||||
}
|
||||
|
||||
const header = `# Repository Map\n# Directory: ${absolutePath}\n# Files analyzed: ${allFiles.length}\n\n`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: header + mapContent,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { ToolMetadata, ToolSearchResult } from './types.js';
|
||||
|
||||
/**
|
||||
* 分词函数,支持中英文
|
||||
*/
|
||||
function tokenize(text: string): string[] {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/[\s,,、_\-]+/)
|
||||
.filter((t) => t.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算工具与查询的匹配分数
|
||||
*/
|
||||
function calculateScore(queryTerms: string[], tool: ToolMetadata): number {
|
||||
let score = 0;
|
||||
|
||||
const nameLower = tool.name.toLowerCase();
|
||||
const descLower = tool.description.toLowerCase();
|
||||
const keywordsLower = tool.keywords.map((k) => k.toLowerCase());
|
||||
|
||||
for (const term of queryTerms) {
|
||||
// 名称精确匹配 (最高分)
|
||||
if (nameLower === term) {
|
||||
score += 10;
|
||||
}
|
||||
// 名称包含匹配
|
||||
else if (nameLower.includes(term)) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// 关键词精确匹配
|
||||
if (keywordsLower.includes(term)) {
|
||||
score += 8;
|
||||
}
|
||||
// 关键词包含匹配
|
||||
else if (keywordsLower.some((k) => k.includes(term) || term.includes(k))) {
|
||||
score += 3;
|
||||
}
|
||||
|
||||
// 描述包含匹配
|
||||
if (descLower.includes(term)) {
|
||||
score += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索工具
|
||||
* @param query 搜索查询
|
||||
* @param allTools 所有工具的元数据
|
||||
* @param limit 返回结果数量限制
|
||||
* @returns 匹配的工具列表(按分数排序)
|
||||
*/
|
||||
export function searchTools(
|
||||
query: string,
|
||||
allTools: ToolMetadata[],
|
||||
limit: number = 5
|
||||
): ToolSearchResult[] {
|
||||
const queryTerms = tokenize(query);
|
||||
|
||||
if (queryTerms.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = allTools
|
||||
// 只搜索延迟加载的工具
|
||||
.filter((tool) => tool.deferLoading)
|
||||
.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
category: tool.category,
|
||||
score: calculateScore(queryTerms, tool),
|
||||
}))
|
||||
.filter((r) => r.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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 bashTool: ToolWithMetadata = {
|
||||
name: 'bash',
|
||||
description: loadDescription('bash'),
|
||||
metadata: {
|
||||
name: 'bash',
|
||||
category: 'shell',
|
||||
description: '执行 shell 命令',
|
||||
keywords: ['bash', 'shell', 'command', 'execute', 'run', 'terminal', '命令', '执行', '终端', 'sh', 'cmd'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
parameters: {
|
||||
command: {
|
||||
type: 'string',
|
||||
description: '要执行的 bash 命令',
|
||||
required: true,
|
||||
},
|
||||
cwd: {
|
||||
type: 'string',
|
||||
description: '工作目录(可选,默认为当前目录)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const command = params.command as string;
|
||||
const cwd = (params.cwd as string) || process.cwd();
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkBashPermission({
|
||||
command,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
// 如果需要用户确认但没有设置回调,返回等待确认的状态
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: ${command}\n原因: ${permResult.reason || '需要权限确认'}\n模式: ${permResult.patterns?.join(', ') || ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '命令不被允许执行'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd,
|
||||
timeout: 60000, // 60 秒超时
|
||||
maxBuffer: 1024 * 1024 * 10, // 10MB 输出限制
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: stdout + (stderr ? `\nSTDERR: ${stderr}` : ''),
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: execError.stderr || execError.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { bashTool } from './bash.js';
|
||||
@@ -0,0 +1,2 @@
|
||||
export { skillTool, updateSkillDescription } from './skill.js';
|
||||
export { skillSearchTool } from './skill_search.js';
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Skill 工具
|
||||
*
|
||||
* 允许 Agent 调用预定义的 Skill 来执行特定任务。
|
||||
* Skill 是可复用的提示模板,不同于 Agent(独立执行单元)。
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { getSkillRegistry } from '../../skills/registry.js';
|
||||
|
||||
/**
|
||||
* 获取 Skill 工具动态描述
|
||||
*/
|
||||
function getSkillDescription(): string {
|
||||
const registry = getSkillRegistry();
|
||||
const skills = registry.getEnabled();
|
||||
|
||||
if (skills.length === 0) {
|
||||
return '执行 Skill(当前没有可用的 Skill)';
|
||||
}
|
||||
|
||||
// 按分类分组
|
||||
const byCategory = new Map<string, typeof skills>();
|
||||
const uncategorized: typeof skills = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
if (skill.category) {
|
||||
if (!byCategory.has(skill.category)) {
|
||||
byCategory.set(skill.category, []);
|
||||
}
|
||||
byCategory.get(skill.category)!.push(skill);
|
||||
} else {
|
||||
uncategorized.push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建描述
|
||||
let description = '执行预定义的 Skill 来完成特定任务。\n\n可用的 Skills:\n';
|
||||
|
||||
for (const [category, categorySkills] of byCategory) {
|
||||
description += `\n[${category}]\n`;
|
||||
for (const skill of categorySkills) {
|
||||
description += `- ${skill.name}: ${skill.description}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (uncategorized.length > 0) {
|
||||
description += '\n[其他]\n';
|
||||
for (const skill of uncategorized) {
|
||||
description += `- ${skill.name}: ${skill.description}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
description += `
|
||||
使用示例:
|
||||
- skill({ skill_name: "code-review", params: { code: "..." } })
|
||||
- skill({ skill_name: "generate-tests", params: { code: "...", framework: "vitest" } })`;
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 工具
|
||||
*/
|
||||
export const skillTool: ToolWithMetadata = {
|
||||
name: 'skill',
|
||||
description: getSkillDescription(),
|
||||
parameters: {
|
||||
skill_name: {
|
||||
type: 'string',
|
||||
description: 'Skill 名称',
|
||||
required: true,
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Skill 参数(根据具体 Skill 定义)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'skill',
|
||||
category: 'agent',
|
||||
description: '执行预定义的 Skill',
|
||||
keywords: ['skill', 'template', 'prompt', '技能', '模板', '提示'],
|
||||
deferLoading: false,
|
||||
},
|
||||
async execute(params) {
|
||||
const { skill_name, params: skillParams = {} } = params as {
|
||||
skill_name: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const registry = getSkillRegistry();
|
||||
|
||||
// 检查 Skill 是否存在
|
||||
const skill = registry.get(skill_name);
|
||||
if (!skill) {
|
||||
// 尝试搜索相似的 Skill
|
||||
const suggestions = registry.search(skill_name, 3);
|
||||
let errorMsg = `Skill 不存在: ${skill_name}`;
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
errorMsg += '\n\n你可能想要的 Skill:\n';
|
||||
for (const { skill: s, matchReason } of suggestions) {
|
||||
errorMsg += `- ${s.name}: ${s.description} (${matchReason})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
|
||||
// 执行 Skill(渲染模板)
|
||||
const result = registry.execute(skill_name, skillParams, {
|
||||
workdir: process.cwd(),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: result.error || 'Skill 执行失败',
|
||||
};
|
||||
}
|
||||
|
||||
// 返回渲染后的提示
|
||||
return {
|
||||
success: true,
|
||||
output: result.prompt || '',
|
||||
metadata: {
|
||||
skill: skill_name,
|
||||
category: skill.category,
|
||||
source: skill.source,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新 Skill 工具描述
|
||||
*/
|
||||
export function updateSkillDescription(): void {
|
||||
(skillTool as { description: string }).description = getSkillDescription();
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Skill 搜索工具
|
||||
*
|
||||
* 搜索可用的 Skills,帮助用户发现适合的 Skill。
|
||||
*/
|
||||
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { getSkillRegistry } from '../../skills/registry.js';
|
||||
|
||||
/**
|
||||
* Skill 搜索工具
|
||||
*/
|
||||
export const skillSearchTool: ToolWithMetadata = {
|
||||
name: 'skill_search',
|
||||
description: '搜索可用的 Skills,根据关键词或分类查找合适的 Skill',
|
||||
parameters: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '搜索关键词',
|
||||
required: false,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: '按分类筛选',
|
||||
required: false,
|
||||
},
|
||||
list_all: {
|
||||
type: 'boolean',
|
||||
description: '列出所有可用的 Skills',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'skill_search',
|
||||
category: 'agent',
|
||||
description: '搜索可用的 Skills',
|
||||
keywords: ['skill', 'search', 'find', '技能', '搜索', '查找'],
|
||||
deferLoading: false,
|
||||
},
|
||||
async execute(params) {
|
||||
const { query, category, list_all } = params as {
|
||||
query?: string;
|
||||
category?: string;
|
||||
list_all?: boolean;
|
||||
};
|
||||
|
||||
const registry = getSkillRegistry();
|
||||
|
||||
// 列出所有 Skills
|
||||
if (list_all) {
|
||||
const skills = registry.getEnabled();
|
||||
const stats = registry.getStats();
|
||||
|
||||
if (skills.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: '当前没有可用的 Skills。',
|
||||
};
|
||||
}
|
||||
|
||||
// 按分类分组
|
||||
const byCategory = new Map<string, typeof skills>();
|
||||
const uncategorized: typeof skills = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
if (skill.category) {
|
||||
if (!byCategory.has(skill.category)) {
|
||||
byCategory.set(skill.category, []);
|
||||
}
|
||||
byCategory.get(skill.category)!.push(skill);
|
||||
} else {
|
||||
uncategorized.push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
let output = `📚 可用的 Skills (共 ${stats.total} 个)\n\n`;
|
||||
|
||||
for (const [cat, catSkills] of byCategory) {
|
||||
output += `## ${cat}\n`;
|
||||
for (const skill of catSkills) {
|
||||
output += `- **${skill.name}**: ${skill.description}`;
|
||||
if (skill.source !== 'builtin') {
|
||||
output += ` [${skill.source}]`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
if (uncategorized.length > 0) {
|
||||
output += '## 其他\n';
|
||||
for (const skill of uncategorized) {
|
||||
output += `- **${skill.name}**: ${skill.description}`;
|
||||
if (skill.source !== 'builtin') {
|
||||
output += ` [${skill.source}]`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
output += `\n来源统计: 内置 ${stats.bySource.builtin || 0}, 用户 ${stats.bySource.user || 0}, 项目 ${stats.bySource.project || 0}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { stats },
|
||||
};
|
||||
}
|
||||
|
||||
// 按分类筛选
|
||||
if (category) {
|
||||
const skills = registry.getByCategory(category);
|
||||
|
||||
if (skills.length === 0) {
|
||||
const categories = registry.getCategories();
|
||||
return {
|
||||
success: true,
|
||||
output: `分类 "${category}" 下没有 Skills。\n\n可用分类: ${categories.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
let output = `📁 分类 "${category}" 下的 Skills:\n\n`;
|
||||
for (const skill of skills) {
|
||||
output += `- **${skill.name}**: ${skill.description}\n`;
|
||||
if (skill.parameters) {
|
||||
const paramNames = Object.keys(skill.parameters);
|
||||
output += ` 参数: ${paramNames.join(', ')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { category, count: skills.length },
|
||||
};
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (query) {
|
||||
const results = registry.search(query, 10);
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: `没有找到匹配 "${query}" 的 Skills。\n\n使用 skill_search({ list_all: true }) 查看所有可用的 Skills。`,
|
||||
};
|
||||
}
|
||||
|
||||
let output = `🔍 搜索 "${query}" 的结果:\n\n`;
|
||||
for (const { skill, score, matchReason } of results) {
|
||||
output += `- **${skill.name}** (${score}分): ${skill.description}\n`;
|
||||
output += ` 匹配: ${matchReason}`;
|
||||
if (skill.category) {
|
||||
output += ` | 分类: ${skill.category}`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { query, resultCount: results.length },
|
||||
};
|
||||
}
|
||||
|
||||
// 默认显示统计和分类
|
||||
const stats = registry.getStats();
|
||||
const categories = registry.getCategories();
|
||||
|
||||
let output = `📊 Skills 概览\n\n`;
|
||||
output += `总数: ${stats.total} (启用: ${stats.enabled})\n`;
|
||||
output += `来源: 内置 ${stats.bySource.builtin || 0}, 用户 ${stats.bySource.user || 0}, 项目 ${stats.bySource.project || 0}\n\n`;
|
||||
|
||||
if (categories.length > 0) {
|
||||
output += `可用分类:\n`;
|
||||
for (const cat of categories) {
|
||||
output += `- ${cat} (${stats.byCategory[cat]} 个)\n`;
|
||||
}
|
||||
}
|
||||
|
||||
output += `\n使用方法:\n`;
|
||||
output += `- skill_search({ list_all: true }) - 列出所有 Skills\n`;
|
||||
output += `- skill_search({ query: "关键词" }) - 搜索 Skills\n`;
|
||||
output += `- skill_search({ category: "分类名" }) - 按分类筛选`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
metadata: { stats },
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { getAgentManager } from '../../agent/manager.js';
|
||||
|
||||
/**
|
||||
* Agent Output 工具
|
||||
* 用于获取后台 Agent 的执行结果
|
||||
*/
|
||||
export const agentOutputTool: ToolWithMetadata = {
|
||||
name: 'agent_output',
|
||||
description: `获取后台运行的 Agent 执行结果。
|
||||
|
||||
当使用 task 工具的 run_in_background 参数启动后台 Agent 后,
|
||||
使用此工具查询执行状态和结果。
|
||||
|
||||
使用示例:
|
||||
- 查询结果(阻塞等待): agent_output({ agent_id: "abc123" })
|
||||
- 检查状态(不阻塞): agent_output({ agent_id: "abc123", block: false })
|
||||
- 设置超时: agent_output({ agent_id: "abc123", timeout: 60 })`,
|
||||
parameters: {
|
||||
agent_id: {
|
||||
type: 'string',
|
||||
description: 'Agent ID(由 task 工具返回)',
|
||||
required: true,
|
||||
},
|
||||
block: {
|
||||
type: 'boolean',
|
||||
description: '是否阻塞等待结果完成(默认 true)。设为 false 可立即获取当前状态',
|
||||
required: false,
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: '等待超时时间(秒),默认 150,最大 300',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'agent_output',
|
||||
category: 'agent',
|
||||
description: '获取后台 Agent 执行结果',
|
||||
keywords: ['agent', 'output', 'result', 'background', '结果', '后台', '查询'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
async execute(params) {
|
||||
const {
|
||||
agent_id,
|
||||
block = true,
|
||||
timeout = 150,
|
||||
} = params as {
|
||||
agent_id: string;
|
||||
block?: boolean;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
const agentManager = getAgentManager();
|
||||
|
||||
// 验证超时参数
|
||||
const effectiveTimeout = Math.min(Math.max(timeout, 1), 300);
|
||||
|
||||
// 获取 Agent 输出
|
||||
const agent = await agentManager.getAgentOutput(agent_id, block, effectiveTimeout);
|
||||
|
||||
if (!agent) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${agent_id} 不存在。请检查 agent_id 是否正确。`,
|
||||
};
|
||||
}
|
||||
|
||||
// 根据状态返回不同结果
|
||||
if (agent.status === 'running') {
|
||||
const runningTime = Math.round((Date.now() - agent.startedAt.getTime()) / 1000);
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${agent_id} 仍在运行中...\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 已运行: ${runningTime} 秒\n\n` +
|
||||
`稍后再次调用 agent_output 查询结果,或使用 block: true 等待完成。`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'running',
|
||||
agentName: agent.agentName,
|
||||
runningTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (agent.status === 'failed') {
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${agent_id} 执行失败:\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 耗时: ${duration} 秒\n` +
|
||||
`- 错误: ${agent.error || '未知错误'}`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'failed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// completed
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `## Agent ${agent_id} 执行完成\n\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 耗时: ${duration} 秒\n` +
|
||||
`- 步数: ${agent.steps || 0}\n\n` +
|
||||
`### 结果\n\n${agent.result || '(无输出)'}`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'completed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
steps: agent.steps,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { taskTool, initTaskContext, updateTaskDescription, getTaskContext } from './task.js';
|
||||
export { agentOutputTool } from './agent_output.js';
|
||||
@@ -0,0 +1,284 @@
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { AgentConfig } from '../../types/index.js';
|
||||
import type { ImageData } from '../../agent/types.js';
|
||||
import { agentRegistry, AgentExecutor } from '../../agent/index.js';
|
||||
import { toolRegistry } from '../registry.js';
|
||||
import { SessionManager } from '../../session/index.js';
|
||||
import { getAgentManager } from '../../agent/manager.js';
|
||||
import { loadVisionConfig } from '../../utils/config.js';
|
||||
|
||||
/**
|
||||
* 模型预设映射
|
||||
*/
|
||||
const MODEL_PRESETS: Record<string, string> = {
|
||||
sonnet: 'claude-sonnet-4-20250514',
|
||||
opus: 'claude-opus-4-0-20250514',
|
||||
haiku: 'claude-3-5-haiku-20241022',
|
||||
};
|
||||
|
||||
// Task 工具上下文(运行时注入)
|
||||
let taskContext: {
|
||||
baseConfig: AgentConfig;
|
||||
sessionManager: SessionManager;
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* 初始化 Task 工具上下文
|
||||
*/
|
||||
export function initTaskContext(
|
||||
baseConfig: AgentConfig,
|
||||
sessionManager: SessionManager
|
||||
): void {
|
||||
taskContext = { baseConfig, sessionManager };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Task 工具上下文
|
||||
*/
|
||||
export function getTaskContext(): typeof taskContext {
|
||||
return taskContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Task 工具动态描述
|
||||
*/
|
||||
function getTaskDescription(): string {
|
||||
const subagents = agentRegistry.listSubagents();
|
||||
if (subagents.length === 0) {
|
||||
return '执行子任务(当前没有可用的子 Agent)';
|
||||
}
|
||||
|
||||
const agentList = subagents
|
||||
.map((a) => `- ${a.name}: ${a.description}`)
|
||||
.join('\n');
|
||||
|
||||
return `启动子 Agent 执行复杂任务,支持后台运行和模型选择。
|
||||
|
||||
可用的 Agent:
|
||||
${agentList}
|
||||
|
||||
使用示例:
|
||||
- 同步执行: task({ subagent_type: "explore", prompt: "找到所有 API 路由" })
|
||||
- 后台运行: task({ subagent_type: "code-reviewer", prompt: "审查代码", run_in_background: true })
|
||||
- 指定模型: task({ subagent_type: "general", prompt: "复杂分析", model: "opus" })`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task 工具
|
||||
* 用于创建子任务,委派给指定的 Agent 处理
|
||||
* 支持后台运行和模型选择
|
||||
*/
|
||||
export const taskTool: ToolWithMetadata = {
|
||||
name: 'task',
|
||||
description: getTaskDescription(),
|
||||
parameters: {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: '任务简短描述(3-5 个词,用于标识任务)',
|
||||
required: true,
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
description: '详细的任务说明,包括目标、范围和期望输出',
|
||||
required: true,
|
||||
},
|
||||
subagent_type: {
|
||||
type: 'string',
|
||||
description: '子 Agent 类型,可选: general, explore, code-reviewer',
|
||||
required: true,
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
description: '模型选择: sonnet, opus, haiku(默认继承主 Agent 配置)',
|
||||
required: false,
|
||||
},
|
||||
run_in_background: {
|
||||
type: 'boolean',
|
||||
description: '是否后台运行。后台运行时立即返回 agentId,使用 agent_output 工具获取结果',
|
||||
required: false,
|
||||
},
|
||||
images: {
|
||||
type: 'array',
|
||||
description: '图片数据数组(用于 vision 相关任务),每个图片包含 data(base64)、mimeType、filename(可选)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'task',
|
||||
category: 'agent',
|
||||
description: '执行子任务,委派给专门的 Agent',
|
||||
keywords: ['task', 'agent', 'subagent', '子任务', '委派', '探索', '审查', '后台'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
async execute(params) {
|
||||
const {
|
||||
description,
|
||||
prompt,
|
||||
subagent_type,
|
||||
model,
|
||||
run_in_background,
|
||||
images,
|
||||
} = params as {
|
||||
description: string;
|
||||
prompt: string;
|
||||
subagent_type: string;
|
||||
model?: string;
|
||||
run_in_background?: boolean;
|
||||
images?: ImageData[];
|
||||
};
|
||||
|
||||
// 检查上下文是否已初始化
|
||||
if (!taskContext) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'Task 工具未初始化,请确保正确设置上下文',
|
||||
};
|
||||
}
|
||||
|
||||
const { baseConfig, sessionManager } = taskContext;
|
||||
|
||||
// 1. 获取 Agent 配置
|
||||
const agent = agentRegistry.get(subagent_type);
|
||||
if (!agent) {
|
||||
const availableAgents = agentRegistry.listSubagents().map((a) => a.name);
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `未找到 Agent: ${subagent_type}。可用的 Agent: ${availableAgents.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查是否为 primary 模式
|
||||
if (agent.mode === 'primary') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent "${subagent_type}" 是 primary 模式,不能作为子任务调用`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 处理模型选择
|
||||
let effectiveConfig = baseConfig;
|
||||
|
||||
// Vision Agent 特殊处理:使用 VisionConfig 配置
|
||||
if (subagent_type === 'vision') {
|
||||
const visionConfig = loadVisionConfig();
|
||||
if (!visionConfig) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'Vision Agent 需要配置 Vision 服务。请在配置文件中设置 visionProvider、visionApiKey 等参数。',
|
||||
};
|
||||
}
|
||||
// 使用 Vision 配置覆盖 baseConfig
|
||||
effectiveConfig = {
|
||||
...baseConfig,
|
||||
provider: visionConfig.provider,
|
||||
apiKey: visionConfig.apiKey,
|
||||
model: visionConfig.model,
|
||||
baseUrl: visionConfig.baseUrl,
|
||||
};
|
||||
} else if (model) {
|
||||
const modelName = MODEL_PRESETS[model];
|
||||
if (!modelName) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `无效的模型选择: ${model}。可选: sonnet, opus, haiku`,
|
||||
};
|
||||
}
|
||||
effectiveConfig = {
|
||||
...baseConfig,
|
||||
model: modelName,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 后台运行模式
|
||||
if (run_in_background) {
|
||||
const agentManager = getAgentManager();
|
||||
const parentSessionId = sessionManager.getSessionId() || 'standalone';
|
||||
|
||||
const agentId = await agentManager.runInBackground(
|
||||
agent,
|
||||
description,
|
||||
prompt,
|
||||
effectiveConfig,
|
||||
toolRegistry,
|
||||
{
|
||||
parentSessionId,
|
||||
workdir: process.cwd(),
|
||||
images,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${agentId} 已在后台启动 (@${agent.name})。\n使用 agent_output 工具查询结果: agent_output({ agent_id: "${agentId}" })`,
|
||||
metadata: {
|
||||
agentId,
|
||||
agent: agent.name,
|
||||
mode: 'background',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 同步执行模式
|
||||
const parentSessionId = sessionManager.getSessionId() || 'standalone';
|
||||
const childSession = sessionManager.createChildSession(
|
||||
parentSessionId,
|
||||
agent.name,
|
||||
`${description} (@${agent.name})`
|
||||
);
|
||||
|
||||
// 创建执行器
|
||||
const executor = new AgentExecutor(agent, effectiveConfig, toolRegistry);
|
||||
|
||||
// 执行任务
|
||||
const result = await executor.execute(prompt, {
|
||||
parentSessionId,
|
||||
workdir: process.cwd(),
|
||||
images,
|
||||
onStream: undefined, // 子任务不使用流式输出
|
||||
});
|
||||
|
||||
// 保存子会话
|
||||
childSession.messages = [
|
||||
{ role: 'user', content: prompt },
|
||||
{ role: 'assistant', content: result.text },
|
||||
];
|
||||
await sessionManager.saveChildSession(childSession);
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
output: result.text,
|
||||
metadata: {
|
||||
agent: agent.name,
|
||||
sessionId: childSession.id,
|
||||
steps: result.steps,
|
||||
mode: 'sync',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: result.error || '子任务执行失败',
|
||||
metadata: {
|
||||
agent: agent.name,
|
||||
sessionId: childSession.id,
|
||||
steps: result.steps,
|
||||
mode: 'sync',
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新 Task 工具描述(Agent 注册表初始化后调用)
|
||||
*/
|
||||
export function updateTaskDescription(): void {
|
||||
(taskTool as { description: string }).description = getTaskDescription();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { todoReadTool } from './todoread.js';
|
||||
export { todoWriteTool } from './todowrite.js';
|
||||
export { todoManager } from './todo-manager.js';
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Todo, TodoStatus } from '../../session/types.js';
|
||||
import type { SessionManager } from '../../session/index.js';
|
||||
|
||||
/**
|
||||
* Todo 管理器
|
||||
* 提供对当前会话 todo 列表的操作接口
|
||||
*/
|
||||
class TodoManager {
|
||||
private sessionManager: SessionManager | null = null;
|
||||
|
||||
/**
|
||||
* 设置会话管理器
|
||||
*/
|
||||
setSessionManager(manager: SessionManager): void {
|
||||
this.sessionManager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 todo 列表
|
||||
*/
|
||||
getTodos(): Todo[] {
|
||||
if (!this.sessionManager) {
|
||||
return [];
|
||||
}
|
||||
return this.sessionManager.getTodos();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 todo 列表
|
||||
*/
|
||||
async setTodos(todos: Todo[]): Promise<void> {
|
||||
if (!this.sessionManager) {
|
||||
return;
|
||||
}
|
||||
await this.sessionManager.setTodos(todos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个 todo
|
||||
*/
|
||||
async addTodo(content: string, status: TodoStatus = 'pending'): Promise<Todo> {
|
||||
const todos = this.getTodos();
|
||||
const now = new Date().toISOString();
|
||||
const newTodo: Todo = {
|
||||
id: this.generateId(),
|
||||
content,
|
||||
status,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
todos.push(newTodo);
|
||||
await this.setTodos(todos);
|
||||
return newTodo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 todo 状态
|
||||
*/
|
||||
async updateTodoStatus(id: string, status: TodoStatus): Promise<boolean> {
|
||||
const todos = this.getTodos();
|
||||
const todo = todos.find((t) => t.id === id);
|
||||
if (!todo) return false;
|
||||
|
||||
todo.status = status;
|
||||
todo.updatedAt = new Date().toISOString();
|
||||
await this.setTodos(todos);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 todo
|
||||
*/
|
||||
async deleteTodo(id: string): Promise<boolean> {
|
||||
const todos = this.getTodos();
|
||||
const index = todos.findIndex((t) => t.id === id);
|
||||
if (index === -1) return false;
|
||||
|
||||
todos.splice(index, 1);
|
||||
await this.setTodos(todos);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 todo
|
||||
*/
|
||||
async clearTodos(): Promise<void> {
|
||||
await this.setTodos([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.sessionManager !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const todoManager = new TodoManager();
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { todoManager } from './todo-manager.js';
|
||||
|
||||
export const todoReadTool: ToolWithMetadata = {
|
||||
name: 'todoread',
|
||||
description: `读取当前会话的待办事项列表。
|
||||
|
||||
使用场景:
|
||||
- 在对话开始时查看待处理的任务
|
||||
- 开始新任务前了解当前进度
|
||||
- 用户询问之前的任务或计划时
|
||||
- 不确定下一步做什么时
|
||||
- 完成任务后更新对剩余工作的理解
|
||||
- 每隔几条消息检查一次以确保进度正常
|
||||
|
||||
返回格式:
|
||||
- 返回 JSON 格式的待办事项列表
|
||||
- 每个事项包含 id、content(内容)、status(状态)
|
||||
- 状态:pending(待处理)、in_progress(进行中)、completed(已完成)`,
|
||||
metadata: {
|
||||
name: 'todoread',
|
||||
category: 'core',
|
||||
description: '读取待办事项列表',
|
||||
keywords: ['todo', 'task', 'list', 'read', '待办', '任务', '列表', '进度'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
parameters: {},
|
||||
execute: async (): Promise<ToolResult> => {
|
||||
if (!todoManager.isInitialized()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '会话管理器未初始化,无法读取待办事项',
|
||||
};
|
||||
}
|
||||
|
||||
const todos = todoManager.getTodos();
|
||||
const pendingCount = todos.filter((t) => t.status !== 'completed').length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: JSON.stringify(todos, null, 2),
|
||||
metadata: {
|
||||
todos,
|
||||
pendingCount,
|
||||
totalCount: todos.length,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { Todo, TodoStatus } from '../../session/types.js';
|
||||
import { todoManager } from './todo-manager.js';
|
||||
|
||||
/**
|
||||
* 验证 todo 项
|
||||
*/
|
||||
function validateTodo(item: unknown): item is { content: string; status: TodoStatus } {
|
||||
if (typeof item !== 'object' || item === null) return false;
|
||||
const obj = item as Record<string, unknown>;
|
||||
if (typeof obj.content !== 'string' || obj.content.trim() === '') return false;
|
||||
if (!['pending', 'in_progress', 'completed'].includes(obj.status as string)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export const todoWriteTool: ToolWithMetadata = {
|
||||
name: 'todowrite',
|
||||
description: `创建和管理当前会话的待办事项列表。用于跟踪进度、组织复杂任务,并向用户展示工作进展。
|
||||
|
||||
## 使用场景
|
||||
|
||||
主动使用此工具的情况:
|
||||
1. 复杂多步骤任务 - 任务需要 3 个或更多步骤
|
||||
2. 非平凡的复杂任务 - 需要仔细规划或多个操作的任务
|
||||
3. 用户明确要求使用待办列表
|
||||
4. 用户提供多个任务 - 用户给出编号列表或逗号分隔的任务
|
||||
5. 收到新指令后 - 立即将用户需求记录为待办
|
||||
6. 开始处理任务时 - 将其标记为 in_progress
|
||||
7. 完成任务后 - 标记为 completed 并添加发现的后续任务
|
||||
|
||||
## 不使用此工具的情况
|
||||
|
||||
跳过使用的情况:
|
||||
1. 只有单个简单任务
|
||||
2. 任务太简单,跟踪没有意义
|
||||
3. 任务可以在 3 个简单步骤内完成
|
||||
4. 纯粹的对话或信息性请求
|
||||
|
||||
## 任务状态
|
||||
|
||||
- pending: 待处理,尚未开始
|
||||
- in_progress: 进行中(同一时间只能有一个)
|
||||
- completed: 已完成
|
||||
|
||||
## 任务管理规则
|
||||
|
||||
- 实时更新任务状态
|
||||
- 完成后立即标记(不要批量标记)
|
||||
- 同一时间只有一个任务处于 in_progress
|
||||
- 完成当前任务后再开始新任务`,
|
||||
metadata: {
|
||||
name: 'todowrite',
|
||||
category: 'core',
|
||||
description: '创建和更新待办事项列表',
|
||||
keywords: ['todo', 'task', 'write', 'update', 'create', '待办', '任务', '创建', '更新'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
parameters: {
|
||||
todos: {
|
||||
type: 'array',
|
||||
description:
|
||||
'更新后的待办事项列表。每个事项包含 content(任务内容)和 status(pending/in_progress/completed)',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
if (!todoManager.isInitialized()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '会话管理器未初始化,无法更新待办事项',
|
||||
};
|
||||
}
|
||||
|
||||
const todosInput = params.todos;
|
||||
if (!Array.isArray(todosInput)) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'todos 参数必须是数组',
|
||||
};
|
||||
}
|
||||
|
||||
// 验证并转换输入
|
||||
const now = new Date().toISOString();
|
||||
const existingTodos = todoManager.getTodos();
|
||||
const existingMap = new Map(existingTodos.map((t) => [t.content, t]));
|
||||
|
||||
const newTodos: Todo[] = [];
|
||||
for (let i = 0; i < todosInput.length; i++) {
|
||||
const item = todosInput[i];
|
||||
if (!validateTodo(item)) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `第 ${i + 1} 个待办事项格式无效。需要 { content: string, status: 'pending' | 'in_progress' | 'completed' }`,
|
||||
};
|
||||
}
|
||||
|
||||
// 查找是否已存在(通过内容匹配)
|
||||
const existing = existingMap.get(item.content);
|
||||
if (existing) {
|
||||
// 更新现有项
|
||||
newTodos.push({
|
||||
...existing,
|
||||
status: item.status,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
// 创建新项
|
||||
newTodos.push({
|
||||
id: Math.random().toString(36).substring(2, 10),
|
||||
content: item.content,
|
||||
status: item.status,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await todoManager.setTodos(newTodos);
|
||||
|
||||
const pendingCount = newTodos.filter((t) => t.status !== 'completed').length;
|
||||
const completedCount = newTodos.filter((t) => t.status === 'completed').length;
|
||||
const inProgressCount = newTodos.filter((t) => t.status === 'in_progress').length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `待办事项已更新: ${pendingCount} 待处理, ${inProgressCount} 进行中, ${completedCount} 已完成`,
|
||||
metadata: {
|
||||
todos: newTodos,
|
||||
pendingCount,
|
||||
inProgressCount,
|
||||
completedCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ToolResult } from '../types/index.js';
|
||||
import type { ToolWithMetadata } from './types.js';
|
||||
import { toolRegistry } from './registry.js';
|
||||
|
||||
/**
|
||||
* tool_search 工具
|
||||
* 用于搜索可用的工具,实现动态工具发现
|
||||
*/
|
||||
export const toolSearchTool: ToolWithMetadata = {
|
||||
name: 'tool_search',
|
||||
description: `搜索可用的工具。当你需要执行某项任务但当前没有合适的工具时,使用此工具搜索。
|
||||
|
||||
可搜索的能力类别:
|
||||
- 文件操作: 读取、写入、编辑、复制、移动、删除文件
|
||||
- 目录操作: 列出目录、创建目录、搜索文件
|
||||
- 内容搜索: 在文件中搜索文本、grep
|
||||
- Shell: 执行命令行命令
|
||||
- Git: 版本控制操作 (即将支持)
|
||||
- 网络: HTTP请求、网页抓取 (即将支持)
|
||||
|
||||
搜索后返回的工具将可以直接使用。`,
|
||||
|
||||
parameters: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '描述你需要的功能,如 "读取文件内容"、"搜索代码"、"移动文件"、"执行命令"',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
metadata: {
|
||||
name: 'tool_search',
|
||||
category: 'core',
|
||||
description: '搜索可用工具',
|
||||
keywords: ['search', 'find', 'tool', 'discover', '搜索', '查找', '工具', '发现'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const query = params.query as string;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '请提供搜索关键词',
|
||||
};
|
||||
}
|
||||
|
||||
const results = toolRegistry.search(query, 5);
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: `没有找到与 "${query}" 匹配的工具。请尝试其他关键词,或使用更通用的描述。`,
|
||||
};
|
||||
}
|
||||
|
||||
const toolList = results
|
||||
.map((t) => `- ${t.name}: ${t.description} [${t.category}]`)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `找到 ${results.length} 个相关工具:\n\n${toolList}\n\n重要:这些工具现在已经可以直接调用了。请立即使用合适的工具(如 web_search)来完成任务,不要使用 bash 命令代替。`,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ToolParameter, ToolResult } from '../types/index.js';
|
||||
|
||||
// 工具类别
|
||||
export type ToolCategory = 'core' | 'filesystem' | 'shell' | 'git' | 'web' | 'database' | 'agent';
|
||||
|
||||
// 工具元数据
|
||||
export interface ToolMetadata {
|
||||
name: string;
|
||||
category: ToolCategory;
|
||||
description: string; // 简短描述,用于搜索结果展示
|
||||
keywords: string[]; // 搜索关键词
|
||||
deferLoading: boolean; // true = 延迟加载,false = 始终加载
|
||||
}
|
||||
|
||||
// 扩展后的工具定义(包含元数据)
|
||||
export interface ToolWithMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, ToolParameter>;
|
||||
execute: (params: Record<string, unknown>) => Promise<ToolResult>;
|
||||
metadata: ToolMetadata;
|
||||
}
|
||||
|
||||
// 搜索结果
|
||||
export interface ToolSearchResult {
|
||||
name: string;
|
||||
description: string;
|
||||
category: ToolCategory;
|
||||
score: number;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { webSearchTool } from './web_search.js';
|
||||
export { webExtractTool } from './web_extract.js';
|
||||
@@ -0,0 +1,159 @@
|
||||
import { tavily } from '@tavily/core';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getConfig } from '../../utils/config.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const webExtractTool: ToolWithMetadata = {
|
||||
name: 'web_extract',
|
||||
description: loadDescription('web_extract'),
|
||||
metadata: {
|
||||
name: 'web_extract',
|
||||
category: 'web',
|
||||
description: '从网页URL提取内容',
|
||||
keywords: ['extract', 'url', 'web', 'content', 'scrape', 'fetch', '提取', '网页', '抓取', '内容'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
urls: {
|
||||
type: 'array',
|
||||
description: '要提取内容的 URL 列表(最多 20 个)',
|
||||
required: true,
|
||||
},
|
||||
extract_depth: {
|
||||
type: 'string',
|
||||
description: '提取深度: "basic" 快速提取,"advanced" 深度提取(包含表格、嵌入内容等,默认 basic)',
|
||||
required: false,
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
description: '输出格式: "markdown" 或 "text"(默认 markdown)',
|
||||
required: false,
|
||||
},
|
||||
include_images: {
|
||||
type: 'boolean',
|
||||
description: '是否包含提取的图片列表(默认 false)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
let urls = params.urls as string | string[];
|
||||
// 支持单个 URL 字符串或数组
|
||||
if (typeof urls === 'string') {
|
||||
urls = [urls];
|
||||
}
|
||||
// 限制最多 20 个 URL
|
||||
urls = urls.slice(0, 20);
|
||||
|
||||
const extractDepth = (params.extract_depth as 'basic' | 'advanced') || 'basic';
|
||||
const format = (params.format as 'markdown' | 'text') || 'markdown';
|
||||
const includeImages = (params.include_images as boolean) || false;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkWebPermission({
|
||||
query: `extract: ${urls.join(', ')}`,
|
||||
searchDepth: extractDepth,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认网页提取: ${urls.join(', ')}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `网页提取权限被拒绝: ${permResult.reason || '提取不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取 Tavily API Key
|
||||
const config = getConfig();
|
||||
const apiKey = process.env.TAVILY_API_KEY || config.tavilyApiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY 或在配置文件中添加 tavilyApiKey。',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 Tavily SDK
|
||||
const client = tavily({ apiKey });
|
||||
const response = await client.extract(urls, {
|
||||
extractDepth,
|
||||
includeImages,
|
||||
});
|
||||
|
||||
// 格式化输出
|
||||
let output = `## 网页内容提取\n\n`;
|
||||
|
||||
// 成功提取的结果
|
||||
if (response.results && response.results.length > 0) {
|
||||
output += `### 提取成功 (${response.results.length} 个)\n\n`;
|
||||
|
||||
for (const result of response.results) {
|
||||
output += `---\n`;
|
||||
output += `**URL:** ${result.url}\n\n`;
|
||||
|
||||
// 内容处理
|
||||
let content = result.rawContent || '';
|
||||
if (format === 'text' && content) {
|
||||
// 简单去除 markdown 格式
|
||||
content = content.replace(/[#*`]/g, '');
|
||||
}
|
||||
// 截断过长的内容
|
||||
if (content.length > 5000) {
|
||||
content = content.substring(0, 5000) + '\n\n... (内容已截断)';
|
||||
}
|
||||
output += `${content}\n\n`;
|
||||
|
||||
// 图片列表
|
||||
if (includeImages && result.images && result.images.length > 0) {
|
||||
output += `**图片 (${result.images.length} 张):**\n`;
|
||||
for (const img of result.images.slice(0, 10)) {
|
||||
output += `- ${img}\n`;
|
||||
}
|
||||
if (result.images.length > 10) {
|
||||
output += `- ... 还有 ${result.images.length - 10} 张图片\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 失败的结果
|
||||
if (response.failedResults && response.failedResults.length > 0) {
|
||||
output += `### 提取失败 (${response.failedResults.length} 个)\n\n`;
|
||||
for (const failed of response.failedResults) {
|
||||
output += `- ${failed.url}: ${failed.error || '未知错误'}\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
// 响应时间
|
||||
if (response.responseTime) {
|
||||
output += `_提取耗时: ${response.responseTime.toFixed(2)} 秒_\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `提取失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { tavily } from '@tavily/core';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getConfig } from '../../utils/config.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const webSearchTool: ToolWithMetadata = {
|
||||
name: 'web_search',
|
||||
description: loadDescription('web_search'),
|
||||
metadata: {
|
||||
name: 'web_search',
|
||||
category: 'web',
|
||||
description: '搜索网络获取最新信息',
|
||||
keywords: ['search', 'web', 'internet', 'google', 'query', '搜索', '网络', '查询', '互联网'],
|
||||
deferLoading: false, // 核心工具,始终可用
|
||||
},
|
||||
parameters: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '搜索查询关键词',
|
||||
required: true,
|
||||
},
|
||||
max_results: {
|
||||
type: 'number',
|
||||
description: '返回结果数量(默认 5,最大 20)',
|
||||
required: false,
|
||||
},
|
||||
search_depth: {
|
||||
type: 'string',
|
||||
description: '搜索深度: "basic" 快速搜索,"advanced" 深度搜索(默认 basic)',
|
||||
required: false,
|
||||
},
|
||||
topic: {
|
||||
type: 'string',
|
||||
description: '搜索主题: "general" 通用,"news" 新闻,"finance" 财经(默认 general)',
|
||||
required: false,
|
||||
},
|
||||
include_answer: {
|
||||
type: 'boolean',
|
||||
description: '是否包含 AI 生成的摘要答案(默认 true)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const query = params.query as string;
|
||||
const maxResults = Math.min((params.max_results as number) || 5, 20);
|
||||
const searchDepth = (params.search_depth as 'basic' | 'advanced') || 'basic';
|
||||
const topic = (params.topic as 'general' | 'news' | 'finance') || 'general';
|
||||
const includeAnswer = params.include_answer !== false;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkWebPermission({
|
||||
query,
|
||||
searchDepth,
|
||||
topic,
|
||||
maxResults,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
// 如果需要用户确认但没有设置回调,返回等待确认的状态
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认网络搜索: "${query}"\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `网络搜索权限被拒绝: ${permResult.reason || '搜索不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取 Tavily API Key
|
||||
const config = getConfig();
|
||||
const apiKey = process.env.TAVILY_API_KEY || config.tavilyApiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY 或在配置文件中添加 tavilyApiKey。',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 Tavily SDK
|
||||
const client = tavily({ apiKey });
|
||||
const response = await client.search(query, {
|
||||
searchDepth,
|
||||
topic,
|
||||
maxResults,
|
||||
includeAnswer,
|
||||
});
|
||||
|
||||
// 格式化输出
|
||||
let output = `## 搜索结果: "${query}"\n\n`;
|
||||
|
||||
// 如果有 AI 摘要答案
|
||||
if (response.answer) {
|
||||
output += `### 摘要\n${response.answer}\n\n`;
|
||||
}
|
||||
|
||||
// 搜索结果列表
|
||||
if (response.results && response.results.length > 0) {
|
||||
output += `### 相关链接 (${response.results.length} 条)\n\n`;
|
||||
|
||||
for (let i = 0; i < response.results.length; i++) {
|
||||
const result = response.results[i];
|
||||
output += `**${i + 1}. ${result.title}**\n`;
|
||||
output += `链接: ${result.url}\n`;
|
||||
// 截断过长的内容
|
||||
const content = result.content.length > 300
|
||||
? result.content.substring(0, 300) + '...'
|
||||
: result.content;
|
||||
output += `${content}\n\n`;
|
||||
}
|
||||
} else {
|
||||
output += '未找到相关结果。\n';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `搜索失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user