feat: v3.0 Tool Calling — AI 本地文件操作系统

新增文件:
- src/main/tool-security.ts: 路径白名单/黑名单、命令安全检查
- src/main/tool-handlers.ts: 7个工具实现(read/write/list/search/create/delete/run)
- src/renderer/services/tool-registry.ts: 工具注册调度中心
- src/renderer/services/agent-engine.ts: Agent Loop 流式多轮工具调用引擎
- src/renderer/components/tool-confirm-modal.ts: 高风险操作确认对话框

修改文件:
- types.d.ts: 新增 ToolCall/ToolResult/ToolCallRecord 等类型
- ollama.ts: chatStream 支持 tools 参数
- ipc.ts: 新增 tool:execute/getConfig/setAllowedDirs IPC
- preload.ts: 暴露 tool API
- chat-area.ts: 渲染工具调用卡片
- input-area.ts: 集成 Agent Loop 引擎
- settings-modal.ts: 工具调用开关设置
- index.html: 工具调用设置面板 + 确认对话框 HTML
- style.css: 工具调用卡片、确认对话框样式
- main.ts: 初始化工具调用配置
This commit is contained in:
thzxx
2026-04-06 13:29:43 +08:00
parent 0a1397771e
commit 5bfb137a8a
15 changed files with 1609 additions and 6 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* Tool Security - 安全检查模块
* 路径白名单/黑名单、命令过滤、路径遍历检测
*/
import * as path from 'path';
import * as os from 'os';
const HOME = os.homedir();
/** 默认允许的目录(可通过设置覆盖) */
let allowedDirs: string[] = [
HOME,
path.join(HOME, 'Desktop'),
path.join(HOME, 'Documents'),
path.join(HOME, 'Downloads'),
path.join(HOME, 'Projects'),
path.join(HOME, 'projects'),
'/tmp',
];
/** 永久禁止的目录 */
const BLOCKED_DIRS: string[] = [
'/etc', '/sys', '/proc', '/dev', '/boot', '/root',
'C:\\Windows', 'C:\\Program Files', 'C:\\ProgramData',
path.join(HOME, '.ssh'),
path.join(HOME, '.gnupg'),
path.join(HOME, '.aws'),
];
/** 命令黑名单 */
const BLOCKED_COMMANDS: string[] = [
'rm -rf /', 'rm -rf /*', ':(){ :|:& };:',
'mkfs', 'dd if=', 'wipefs', 'shred',
'shutdown', 'reboot', 'poweroff', 'halt',
'useradd', 'usermod', 'userdel', 'passwd',
'chmod 777', 'chown root',
'crontab -e',
'systemctl enable', 'systemctl disable',
'curl | sh', 'wget | sh', 'curl | bash', 'wget | bash',
'eval', 'exec >',
];
export interface CheckResult {
ok: boolean;
reason?: string;
}
export function checkPathAllowed(targetPath: string, operation: 'read' | 'write'): CheckResult {
const resolved = path.resolve(targetPath);
if (targetPath.split(path.sep).filter(s => s === '..').length > 5) {
return { ok: false, reason: '路径遍历深度过大' };
}
for (const blocked of BLOCKED_DIRS) {
if (resolved === blocked || resolved.startsWith(blocked + path.sep)) {
return { ok: false, reason: `禁止访问受保护路径: ${blocked}` };
}
}
if (operation === 'write') {
const inAllowedDir = allowedDirs.some(dir =>
resolved === dir || resolved.startsWith(dir + path.sep)
);
if (!inAllowedDir) {
return { ok: false, reason: `写操作被限制在允许的目录内。当前路径: ${resolved}` };
}
}
return { ok: true };
}
export function checkCommandAllowed(command: string): CheckResult {
const lowerCmd = command.toLowerCase().trim();
for (const blocked of BLOCKED_COMMANDS) {
if (lowerCmd.includes(blocked.toLowerCase())) {
return { ok: false, reason: `命令包含被禁止的操作: ${blocked}` };
}
}
if (/(\||>|<)\s*(sh|bash|zsh|powershell|cmd)/i.test(lowerCmd)) {
return { ok: false, reason: '禁止通过管道执行 shell 命令' };
}
if (/\/dev\/tcp\//i.test(lowerCmd) || /bash\s+-i\s+>&/i.test(lowerCmd)) {
return { ok: false, reason: '检测到疑似反弹 shell 操作' };
}
return { ok: true };
}
export function setAllowedDirs(dirs: string[]): void {
allowedDirs = dirs.map(d => path.resolve(d));
}
export function getAllowedDirs(): string[] {
return [...allowedDirs];
}
export function getBlockedDirs(): string[] {
return [...BLOCKED_DIRS];
}