abyss-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
abyss/mcp/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ MCP 模块
4
+ 提供 Model Context Protocol 客户端能力,连接外部 MCP Server
5
+ """
6
+ from .manager import MCPClientManager, ServerConfig, MCPServerType, create_default_manager
7
+
8
+ __all__ = [
9
+ "MCPClientManager",
10
+ "ServerConfig",
11
+ "MCPServerType",
12
+ "create_default_manager",
13
+ ]
abyss/mcp/manager.py ADDED
@@ -0,0 +1,189 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ MCP Client Manager 模块
4
+ 管理多个 MCP Server 连接,支持 stdio 和 HTTP 两种传输(SSE 暂未实现)。
5
+ 工具命名规则:mcp__<server_name>__<tool_name>,避免与内置工具冲突。
6
+
7
+ 配置文件格式(~/.abyss/mcp.json):
8
+ {
9
+ "mcp_servers": [
10
+ {"name": "github", "transport": "stdio", "command": "npx", "args": [...], "env": {...}},
11
+ {"name": "db", "transport": "http", "url": "http://...", "headers": {...}}
12
+ ]
13
+ }
14
+
15
+ 注:真实子进程连接(JSON-RPC over stdio)在 start() 中实现,
16
+ 本模块聚焦配置解析、工具注册表、schema 生成等可单测的核心逻辑。
17
+ """
18
+ import json
19
+ import subprocess
20
+ import threading
21
+ from dataclasses import dataclass, field
22
+ from enum import Enum
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Optional, Tuple
25
+
26
+
27
+ class MCPServerType(Enum):
28
+ """MCP Server 传输类型。"""
29
+ STDIO = "stdio"
30
+ HTTP = "http"
31
+ SSE = "sse"
32
+
33
+
34
+ @dataclass
35
+ class ServerConfig:
36
+ """单个 MCP Server 的配置。"""
37
+ name: str
38
+ transport: MCPServerType
39
+ command: Optional[str] = None
40
+ args: List[str] = field(default_factory=list)
41
+ env: Dict[str, str] = field(default_factory=dict)
42
+ url: Optional[str] = None
43
+ headers: Dict[str, str] = field(default_factory=dict)
44
+
45
+ @classmethod
46
+ def from_dict(cls, data: Dict[str, Any]) -> Optional["ServerConfig"]:
47
+ """从配置字典构造 ServerConfig,必填字段缺失返回 None"""
48
+ name = data.get("name", "").strip()
49
+ transport_str = data.get("transport", "").strip().lower()
50
+ if not name or not transport_str:
51
+ return None
52
+ try:
53
+ transport = MCPServerType(transport_str)
54
+ except ValueError:
55
+ return None
56
+
57
+ if transport == MCPServerType.STDIO:
58
+ command = data.get("command", "").strip()
59
+ if not command:
60
+ return None
61
+ return cls(
62
+ name=name, transport=transport,
63
+ command=command,
64
+ args=data.get("args", []) or [],
65
+ env=data.get("env", {}) or {},
66
+ )
67
+ if transport in (MCPServerType.HTTP, MCPServerType.SSE):
68
+ url = data.get("url", "").strip()
69
+ if not url:
70
+ return None
71
+ return cls(
72
+ name=name, transport=transport,
73
+ url=url,
74
+ headers=data.get("headers", {}) or {},
75
+ )
76
+ return None
77
+
78
+
79
+ class MCPClientManager:
80
+ """管理所有已注册的 MCP Server 连接与工具发现。"""
81
+
82
+ def __init__(self, config_path: Optional[Path] = None):
83
+ self.servers: Dict[str, ServerConfig] = {}
84
+ # 工具全名 -> (server_name, original_tool_name, schema)
85
+ self._tools: Dict[str, Tuple[str, str, Dict[str, Any]]] = {}
86
+ # 运行中的子进程(stdio 类型):server_name -> Popen
87
+ self._processes: Dict[str, subprocess.Popen] = {}
88
+ self._lock = threading.Lock()
89
+
90
+ if config_path is not None:
91
+ self._load_config(config_path)
92
+
93
+ def _load_config(self, config_path: Path) -> None:
94
+ """从 JSON 配置文件加载 mcp_servers"""
95
+ if not config_path.exists():
96
+ return
97
+ try:
98
+ with open(config_path, "r", encoding="utf-8") as f:
99
+ data = json.load(f)
100
+ except (json.JSONDecodeError, OSError):
101
+ return
102
+ for server_data in data.get("mcp_servers", []) or []:
103
+ cfg = ServerConfig.from_dict(server_data)
104
+ if cfg is not None:
105
+ self.servers[cfg.name] = cfg
106
+
107
+ @staticmethod
108
+ def make_tool_name(server_name: str, tool_name: str) -> str:
109
+ """生成 MCP 工具的全名:mcp__<server>__<tool>"""
110
+ return f"mcp__{server_name}__{tool_name}"
111
+
112
+ def register_tool(self, server_name: str, tool_schema: Dict[str, Any]) -> None:
113
+ """注册一个已发现的 MCP 工具"""
114
+ original_name = tool_schema.get("name", "")
115
+ if not original_name:
116
+ return
117
+ full_name = self.make_tool_name(server_name, original_name)
118
+ self._tools[full_name] = (server_name, original_name, tool_schema)
119
+
120
+ def get_server_for_tool(self, full_name: str) -> Tuple[Optional[str], Optional[str]]:
121
+ """根据工具全名返回 (server_name, original_tool_name),未注册返回 (None, None)"""
122
+ entry = self._tools.get(full_name)
123
+ if entry is None:
124
+ return (None, None)
125
+ return (entry[0], entry[1])
126
+
127
+ def get_openai_schemas(self) -> List[Dict[str, Any]]:
128
+ """生成所有 MCP 工具的 OpenAI function calling 定义"""
129
+ schemas = []
130
+ for full_name, (_, _, tool_schema) in self._tools.items():
131
+ schemas.append({
132
+ "type": "function",
133
+ "function": {
134
+ "name": full_name,
135
+ "description": tool_schema.get("description", ""),
136
+ "parameters": tool_schema.get("inputSchema", {"type": "object", "properties": {}}),
137
+ }
138
+ })
139
+ return schemas
140
+
141
+ def list_tool_names(self) -> List[str]:
142
+ """返回所有已注册 MCP 工具的全名"""
143
+ return list(self._tools.keys())
144
+
145
+ def start_stdio_server(self, server_name: str) -> bool:
146
+ """启动指定 stdio 类型的 MCP Server 子进程。
147
+ 返回 True 表示启动成功。HTTP/SSE 类型无需启动子进程。
148
+ """
149
+ cfg = self.servers.get(server_name)
150
+ if cfg is None or cfg.transport != MCPServerType.STDIO:
151
+ return False
152
+ with self._lock:
153
+ if server_name in self._processes:
154
+ return True # 已启动
155
+ try:
156
+ env = {**__import__("os").environ, **cfg.env}
157
+ proc = subprocess.Popen(
158
+ [cfg.command] + list(cfg.args),
159
+ stdin=subprocess.PIPE,
160
+ stdout=subprocess.PIPE,
161
+ stderr=subprocess.PIPE,
162
+ env=env,
163
+ text=True,
164
+ encoding="utf-8",
165
+ bufsize=1,
166
+ )
167
+ self._processes[server_name] = proc
168
+ return True
169
+ except Exception:
170
+ return False
171
+
172
+ def stop_all(self) -> None:
173
+ """停止所有运行中的 MCP Server 子进程"""
174
+ with self._lock:
175
+ for name, proc in self._processes.items():
176
+ try:
177
+ proc.terminate()
178
+ proc.wait(timeout=2)
179
+ except Exception:
180
+ try:
181
+ proc.kill()
182
+ except Exception:
183
+ pass
184
+ self._processes.clear()
185
+
186
+
187
+ def create_default_manager() -> MCPClientManager:
188
+ """创建指向 ~/.abyss/mcp.json 的默认 MCPClientManager"""
189
+ return MCPClientManager(config_path=Path.home() / ".abyss" / "mcp.json")
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 提示词加载模块
4
+ 加载系统提示词并注入环境信息
5
+ """
6
+ import os
7
+
8
+
9
+ def load_system_prompt(project_path: str = None) -> str:
10
+ """加载系统提示词,注入当前环境信息。"""
11
+ prompt_path = os.path.join(os.path.dirname(__file__), "system_prompt.md")
12
+ with open(prompt_path, "r", encoding="utf-8") as f:
13
+ prompt = f.read()
14
+
15
+ cwd = project_path or os.getcwd()
16
+ prompt = prompt.replace("{{WORKING_DIR}}", cwd)
17
+
18
+ # 检测项目类型
19
+ if os.path.exists(os.path.join(cwd, "package.json")):
20
+ prompt += "\n\n项目类型: Node.js"
21
+ elif os.path.exists(os.path.join(cwd, "requirements.txt")):
22
+ prompt += "\n\n项目类型: Python"
23
+ elif os.path.exists(os.path.join(cwd, "Cargo.toml")):
24
+ prompt += "\n\n项目类型: Rust"
25
+
26
+ return prompt
abyss/session.py ADDED
@@ -0,0 +1,79 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 会话管理
4
+ 管理一次对话会话的消息历史,支持上下文维护
5
+ """
6
+ from typing import List, Dict, Any
7
+
8
+
9
+ class Session:
10
+ """管理一次对话会话的消息历史,维护完整的对话上下文。"""
11
+
12
+ def __init__(self, system_prompt: str):
13
+ self.messages: List[Dict[str, Any]] = [
14
+ {"role": "system", "content": system_prompt}
15
+ ]
16
+ self._tool_call_counter = 0
17
+
18
+ def add_user(self, content: str):
19
+ """添加用户消息"""
20
+ self.messages.append({"role": "user", "content": content})
21
+
22
+ def add_assistant(self, content: str = None, tool_calls: list = None, reasoning_content: str = None):
23
+ """添加助手消息(含工具调用和思考链)"""
24
+ msg = {"role": "assistant"}
25
+ if content:
26
+ msg["content"] = content
27
+ if reasoning_content:
28
+ msg["reasoning_content"] = reasoning_content
29
+ if tool_calls:
30
+ msg["tool_calls"] = tool_calls
31
+ self.messages.append(msg)
32
+
33
+ def add_tool_result(self, tool_call_id: str, content: str):
34
+ """添加工具执行结果"""
35
+ self.messages.append({
36
+ "role": "tool",
37
+ "tool_call_id": tool_call_id,
38
+ "content": content
39
+ })
40
+ self._tool_call_counter += 1
41
+
42
+ def get_messages(self) -> List[Dict[str, Any]]:
43
+ """获取当前消息列表(用于 API 请求)"""
44
+ return self.messages
45
+
46
+ def get_messages_for_api(self) -> List[Dict[str, Any]]:
47
+ """获取发给 LLM API 的消息列表。剔除 reasoning_content 以节省 token。
48
+
49
+ reasoning_content 是 AI 自己的上次思考,AI 不需要回看旧思考。
50
+ 协议设计参考 OpenAI/Claude 协议,reasoning_content 是单次响应的辅助字段。
51
+ 本地审计 get_messages() 仍保留 reasoning_content。
52
+ """
53
+ result = []
54
+ for m in self.messages:
55
+ if "reasoning_content" in m:
56
+ m = {k: v for k, v in m.items() if k != "reasoning_content"}
57
+ result.append(m)
58
+ return result
59
+
60
+ def get_message_count(self) -> int:
61
+ """获取消息数量(不含 system prompt)"""
62
+ return len(self.messages) - 1
63
+
64
+ def get_tool_call_count(self) -> int:
65
+ """获取工具调用次数"""
66
+ return self._tool_call_counter
67
+
68
+ def clear(self):
69
+ """清空历史,保留 system prompt"""
70
+ system = self.messages[0]
71
+ self.messages = [system]
72
+ self._tool_call_counter = 0
73
+
74
+ def get_last_user_message(self) -> Dict[str, Any]:
75
+ """获取最后一条用户消息"""
76
+ for msg in reversed(self.messages):
77
+ if msg["role"] == "user":
78
+ return msg
79
+ return None
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Skills 模块
4
+ 提供 Agent Skills 加载与注入机制(SKILL.md 格式)
5
+ """
6
+ from .loader import Skill, SkillLoader, create_default_loader
7
+
8
+ __all__ = [
9
+ "Skill",
10
+ "SkillLoader",
11
+ "create_default_loader",
12
+ ]
abyss/skills/loader.py ADDED
@@ -0,0 +1,150 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Skill 加载器模块
4
+ 扫描指定目录下的 SKILL.md 文件,解析 YAML frontmatter + 正文,
5
+ 支持按关键词匹配 Skill 并生成注入 System Prompt 的文本。
6
+
7
+ SKILL.md 格式遵循 agentskills.io 规范:
8
+ - name(必填,kebab-case):唯一标识
9
+ - description(必填):Agent 据此判断何时加载此技能
10
+ - version/author/tags(可选)
11
+
12
+ 加载策略:按需延迟加载——根据用户输入关键词匹配 description。
13
+ """
14
+ import re
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional
18
+
19
+
20
+ @dataclass
21
+ class Skill:
22
+ """单个 Agent Skill:元数据 + 正文内容。"""
23
+ name: str
24
+ description: str
25
+ content: str
26
+ path: Path = None
27
+ version: str = ""
28
+ author: str = ""
29
+ tags: List[str] = field(default_factory=list)
30
+
31
+
32
+ class SkillLoader:
33
+ """扫描并加载 Agent Skills,支持按关键词匹配。"""
34
+
35
+ def __init__(self, skill_dirs: List[Path]):
36
+ self.skill_dirs = skill_dirs
37
+ self._skills: Dict[str, Skill] = {}
38
+
39
+ def discover(self) -> List[Skill]:
40
+ """递归扫描所有 SKILL.md 文件并解析。返回已加载的 Skill 列表"""
41
+ self._skills.clear()
42
+ for skill_dir in self.skill_dirs:
43
+ if not skill_dir.exists() or not skill_dir.is_dir():
44
+ continue
45
+ for md_file in skill_dir.rglob("SKILL.md"):
46
+ skill = self._parse_skill(md_file)
47
+ if skill is not None:
48
+ self._skills[skill.name] = skill
49
+ return list(self._skills.values())
50
+
51
+ def _parse_skill(self, path: Path) -> Optional[Skill]:
52
+ """解析单个 SKILL.md:拆分 frontmatter 和正文,校验必填字段"""
53
+ try:
54
+ raw = path.read_text(encoding="utf-8")
55
+ except Exception:
56
+ return None
57
+
58
+ frontmatter, body = self._split_frontmatter(raw)
59
+ if frontmatter is None:
60
+ return None
61
+
62
+ name = frontmatter.get("name", "").strip()
63
+ description = frontmatter.get("description", "").strip()
64
+ if not name or not description:
65
+ return None # spec 要求 name 和 description 必填
66
+
67
+ return Skill(
68
+ name=name,
69
+ description=description,
70
+ content=body.strip(),
71
+ path=path.parent,
72
+ version=str(frontmatter.get("version", "")),
73
+ author=str(frontmatter.get("author", "")),
74
+ tags=frontmatter.get("tags", []) or [],
75
+ )
76
+
77
+ @staticmethod
78
+ def _split_frontmatter(raw: str):
79
+ """拆分 YAML frontmatter 和正文。返回 (dict, str),无 frontmatter 返回 (None, raw)"""
80
+ if not raw.startswith("---"):
81
+ return None, raw
82
+ # 找第二个 --- 作为 frontmatter 结束
83
+ end_idx = raw.find("\n---", 3)
84
+ if end_idx == -1:
85
+ return None, raw
86
+ fm_text = raw[3:end_idx].strip()
87
+ body = raw[end_idx + 4:].lstrip("\n")
88
+ # 简单 YAML 解析(仅支持 key: value 和列表)
89
+ fm = {}
90
+ for line in fm_text.splitlines():
91
+ line = line.rstrip()
92
+ if not line or line.startswith("#"):
93
+ continue
94
+ if line.startswith(" - "):
95
+ # 列表项,归到最近一个 key
96
+ if fm:
97
+ last_key = list(fm.keys())[-1]
98
+ if not isinstance(fm[last_key], list):
99
+ fm[last_key] = []
100
+ fm[last_key].append(line[4:].strip())
101
+ continue
102
+ if ":" in line:
103
+ key, _, value = line.partition(":")
104
+ key = key.strip()
105
+ value = value.strip()
106
+ if value:
107
+ fm[key] = value
108
+ else:
109
+ fm[key] = [] # 可能是列表开头
110
+ return fm, body
111
+
112
+ def get(self, name: str) -> Optional[Skill]:
113
+ """按名获取已加载的 Skill"""
114
+ return self._skills.get(name)
115
+
116
+ def list_names(self) -> List[str]:
117
+ """返回所有已加载 Skill 名"""
118
+ return list(self._skills.keys())
119
+
120
+ def match_by_keyword(self, user_input: str, limit: int = 3) -> List[Skill]:
121
+ """根据用户输入关键词匹配 Skill 的 description。
122
+ 简单实现:description 中关键词出现在用户输入中即命中。
123
+ 更复杂的语义匹配可后续用 embedding 替换。
124
+ """
125
+ matched = []
126
+ user_lower = user_input.lower()
127
+ for skill in self._skills.values():
128
+ desc_lower = skill.description.lower()
129
+ # 提取 description 中的关键词(中文按字符,英文按单词)
130
+ keywords = re.findall(r"[a-zA-Z]+|[\u4e00-\u9fa5]+", desc_lower)
131
+ hit_count = sum(1 for kw in keywords if len(kw) >= 2 and kw in user_lower)
132
+ if hit_count > 0:
133
+ matched.append((hit_count, skill))
134
+ # 按命中数降序
135
+ matched.sort(key=lambda x: x[0], reverse=True)
136
+ return [s for _, s in matched[:limit]]
137
+
138
+ def build_system_prompt_extension(self, active_skills: List[Skill]) -> str:
139
+ """生成要注入 System Prompt 的 Skill 内容,用 <skill> 标签包裹"""
140
+ if not active_skills:
141
+ return ""
142
+ parts = []
143
+ for skill in active_skills:
144
+ parts.append(f"<skill name='{skill.name}'>\n{skill.content}\n</skill>")
145
+ return "\n".join(parts)
146
+
147
+
148
+ def create_default_loader() -> SkillLoader:
149
+ """创建指向 ~/.abyss/skills/ 的默认 SkillLoader"""
150
+ return SkillLoader([Path.home() / ".abyss" / "skills"])
@@ -0,0 +1,20 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 工具模块
4
+ 提供文件操作、命令执行、联网搜索等工具
5
+ """
6
+ from .base import Tool
7
+ from .file_read import FileReadTool
8
+ from .file_write import FileWriteTool
9
+ from .file_edit import FileEditTool
10
+ from .shell_exec import ShellExecTool
11
+ from .web_search import WebSearchTool
12
+
13
+ __all__ = [
14
+ "Tool",
15
+ "FileReadTool",
16
+ "FileWriteTool",
17
+ "FileEditTool",
18
+ "ShellExecTool",
19
+ "WebSearchTool",
20
+ ]
abyss/tools/base.py ADDED
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 工具基类
4
+ 所有具体工具需继承此类
5
+ """
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict
8
+
9
+
10
+ class Tool(ABC):
11
+ """工具基类,所有具体工具需继承此类。"""
12
+
13
+ @property
14
+ @abstractmethod
15
+ def name(self) -> str:
16
+ """工具名称。"""
17
+ pass
18
+
19
+ @property
20
+ @abstractmethod
21
+ def description(self) -> str:
22
+ """工具功能描述。"""
23
+ pass
24
+
25
+ @property
26
+ @abstractmethod
27
+ def parameters(self) -> Dict[str, Any]:
28
+ """工具参数 JSON Schema。"""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def execute(self, **kwargs) -> Dict[str, Any]:
33
+ """执行工具,返回结果字典。"""
34
+ pass
35
+
36
+ def to_openai_schema(self) -> Dict[str, Any]:
37
+ """转换为 OpenAI Tool 格式。"""
38
+ return {
39
+ "type": "function",
40
+ "function": {
41
+ "name": self.name,
42
+ "description": self.description,
43
+ "parameters": self.parameters
44
+ }
45
+ }
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 文件编辑工具
4
+ 通过搜索替换修改文件内容
5
+ """
6
+ from .base import Tool
7
+
8
+
9
+ class FileEditTool(Tool):
10
+ """通过搜索替换修改文件内容的工具。"""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ return "file_edit"
15
+
16
+ @property
17
+ def description(self) -> str:
18
+ return "修改已有文件的指定内容,通过精确匹配旧字符串替换为新字符串。"
19
+
20
+ @property
21
+ def parameters(self) -> dict:
22
+ return {
23
+ "type": "object",
24
+ "properties": {
25
+ "path": {"type": "string", "description": "文件路径"},
26
+ "old_str": {"type": "string", "description": "要替换的旧内容"},
27
+ "new_str": {"type": "string", "description": "替换后的新内容"}
28
+ },
29
+ "required": ["path", "old_str", "new_str"]
30
+ }
31
+
32
+ def execute(self, path: str, old_str: str, new_str: str) -> dict:
33
+ """编辑文件内容"""
34
+ try:
35
+ with open(path, "r", encoding="utf-8") as f:
36
+ content = f.read()
37
+
38
+ if old_str not in content:
39
+ return {"success": False, "error": "未找到匹配的旧内容"}
40
+
41
+ new_content = content.replace(old_str, new_str, 1)
42
+
43
+ with open(path, "w", encoding="utf-8") as f:
44
+ f.write(new_content)
45
+
46
+ return {"success": True, "path": path}
47
+ except Exception as e:
48
+ return {"success": False, "error": str(e)}
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 文件阅读工具
4
+ 读取指定文件的内容,支持指定行范围
5
+ """
6
+ import os
7
+ from .base import Tool
8
+
9
+
10
+ class FileReadTool(Tool):
11
+ """读取文件内容的工具。"""
12
+
13
+ @property
14
+ def name(self) -> str:
15
+ return "file_read"
16
+
17
+ @property
18
+ def description(self) -> str:
19
+ return "读取指定文件的内容,支持指定行范围。"
20
+
21
+ @property
22
+ def parameters(self) -> dict:
23
+ return {
24
+ "type": "object",
25
+ "properties": {
26
+ "path": {"type": "string", "description": "文件路径"},
27
+ "offset": {"type": "integer", "description": "起始行号,默认1"},
28
+ "limit": {"type": "integer", "description": "最大读取行数"}
29
+ },
30
+ "required": ["path"]
31
+ }
32
+
33
+ def execute(self, path: str, offset: int = 1, limit: int = None) -> dict:
34
+ """读取文件内容"""
35
+ if not os.path.exists(path):
36
+ return {"success": False, "error": f"文件不存在: {path}"}
37
+
38
+ try:
39
+ with open(path, "r", encoding="utf-8") as f:
40
+ lines = f.readlines()
41
+
42
+ start = max(0, offset - 1)
43
+ end = start + limit if limit else len(lines)
44
+ content = "".join(lines[start:end])
45
+
46
+ return {
47
+ "success": True,
48
+ "path": path,
49
+ "content": content,
50
+ "total_lines": len(lines),
51
+ "read_lines": min(end, len(lines)) - start
52
+ }
53
+ except Exception as e:
54
+ return {"success": False, "error": str(e)}
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 文件写入工具
4
+ 创建新文件或覆盖写入已有文件
5
+ """
6
+ import os
7
+ from .base import Tool
8
+
9
+
10
+ class FileWriteTool(Tool):
11
+ """创建或覆盖写入文件的工具。"""
12
+
13
+ @property
14
+ def name(self) -> str:
15
+ return "file_write"
16
+
17
+ @property
18
+ def description(self) -> str:
19
+ return "创建新文件或覆盖写入已有文件。"
20
+
21
+ @property
22
+ def parameters(self) -> dict:
23
+ return {
24
+ "type": "object",
25
+ "properties": {
26
+ "path": {"type": "string", "description": "文件路径"},
27
+ "content": {"type": "string", "description": "文件内容"},
28
+ "append": {"type": "boolean", "description": "是否追加,默认false"}
29
+ },
30
+ "required": ["path", "content"]
31
+ }
32
+
33
+ def execute(self, path: str, content: str, append: bool = False) -> dict:
34
+ """写入文件内容"""
35
+ try:
36
+ mode = "a" if append else "w"
37
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
38
+
39
+ with open(path, mode, encoding="utf-8") as f:
40
+ f.write(content)
41
+
42
+ return {"success": True, "path": path, "mode": "append" if append else "write"}
43
+ except Exception as e:
44
+ return {"success": False, "error": str(e)}