Coverage for tools/agent_tools.py: 25%

1"""

2 Agent 管理工具

3"""

4 from pathlib import Path

5 import shutil

6 import yaml

7 from pydantic import BaseModel, Field

8 from qrclaw.tools.registry import register

9 from qrclaw.workspace import AGENTS_ROOT

10 from qrclaw.logger import get_logger

11

12 logger = get_logger("qrclaw.tools.agent_tools")

13

14# 权限配置文件路径

15 PERMISSIONS_FILE = Path.home() / ".qrclaw" / "permissions.yaml"

16

17

18 class CreateAgentArgs(BaseModel):

19 name: str = Field(description="Agent 名称，只能包含字母、数字、中划线和下划线")

20

21

22 class DeleteAgentArgs(BaseModel):

23 name: str = Field(description="要删除的 Agent 名称")

24

25

26 def _is_valid_name(name: str) -> bool:

27 """检查 agent 名称是否合法"""

28 import re

29 return bool(re.match(r'^[a-zA-Z0-9_-]+$', name))

30

31

32 def _remove_agent_permissions(name: str):

33 """从 permissions.yaml 中移除 agent 的权限配置"""

34 if not PERMISSIONS_FILE.exists():

35 return

36

37 try:

38 with open(PERMISSIONS_FILE, "r", encoding="utf-8") as f:

39 config = yaml.safe_load(f) or {}

40

41 if "agents" not in config or name not in config["agents"]:

42 return

43

44 # 移除该 agent 的配置

45 del config["agents"][name]

46

47 with open(PERMISSIONS_FILE, "w", encoding="utf-8") as f:

48 yaml.dump(config, f, default_flow_style=False, allow_unicode=True)

49

50 logger.info(f"已从 permissions.yaml 中移除 agent '{name}' 的权限配置")

51

52 except Exception as e:

53 logger.warning(f"清理权限配置失败: {e}")

54

55

56@register(description="创建一个新的 Agent，会自动创建工作目录结构（sessions/logs/skills）和初始化文件", args_model=CreateAgentArgs)

57 def create_agent(name: str) -> str:

58 """

59 创建一个新的 Agent 工作空间。

60

61 目录结构：

62 ~/.qrclaw/agents/<name>/

63 ├── sessions/ # 会话历史

64 ├── logs/ # 日志文件

65 ├── skills/ # 技能脚本

66 └── MEMORY.md # 中期记忆

67

68 注意：新创建的 Agent 默认为 scoped 权限（只能访问自己的工作目录）。

69 如需访问外部目录，需手动编辑 ~/.qrclaw/permissions.yaml 配置白名单。

70 """

71 logger.info(f"创建 Agent: {name}")

72

73 # 验证名称

74 if not _is_valid_name(name):

75 error_msg = f"错误：Agent 名称不合法，只能包含字母、数字、中划线和下划线: {name}"

76 logger.warning(error_msg)

77 return error_msg

78

79 # 检查是否已存在

80 agent_dir = AGENTS_ROOT / name

81 if agent_dir.exists():

82 error_msg = f"错误：Agent '{name}' 已存在: {agent_dir}"

83 logger.warning(error_msg)

84 return error_msg

85

86 try:

87 # 创建目录结构

88 (agent_dir / "sessions").mkdir(parents=True, exist_ok=True)

89 (agent_dir / "logs").mkdir(parents=True, exist_ok=True)

90 (agent_dir / "skills").mkdir(parents=True, exist_ok=True)

91

92 # 创建 MEMORY.md

93 memory_file = agent_dir / "MEMORY.md"

94 memory_file.write_text(f"# {name} 中期记忆\n\n", encoding="utf-8")

95

96 logger.info(f"Agent '{name}' 创建成功: {agent_dir}")

97 return f"✅ Agent '{name}' 创建成功\n\n工作目录: {agent_dir}\n\n目录结构:\n- sessions/ (会话历史)\n- logs/ (日志文件)\n- skills/ (技能脚本)\n- MEMORY.md (中期记忆)\n\n默认权限: scoped (只能访问自己的工作目录)\n如需访问外部目录，请在 ~/.qrclaw/permissions.yaml 中配置白名单。"

98

99 except Exception as e:

100 error_msg = f"错误：创建 Agent 失败: {e}"

101 logger.error(f"创建 Agent 失败: {name}, 错误: {e}", exc_info=True)

102 return error_msg

103

104

105@register(description="删除一个 Agent 及其工作目录，同时清理权限配置，此操作不可恢复", args_model=DeleteAgentArgs, confirm=True)

106 def delete_agent(name: str) -> str:

107 """

108 删除一个 Agent 及其所有数据。

109

110 警告：此操作会删除会话历史、日志、技能、记忆等所有数据，并清理权限配置，不可恢复！

111 """

112 logger.info(f"删除 Agent: {name}")

113

114 # 不允许删除 default agent

115 if name == "default":

116 error_msg = "错误：不能删除 default agent"

117 logger.warning(error_msg)

118 return error_msg

119

120 # 检查是否存在

121 agent_dir = AGENTS_ROOT / name

122 if not agent_dir.exists():

123 error_msg = f"错误：Agent '{name}' 不存在"

124 logger.warning(error_msg)

125 return error_msg

126

127 try:

128 # 删除整个目录

129 shutil.rmtree(agent_dir)

130

131 # 清理权限配置

132 _remove_agent_permissions(name)

133

134 logger.info(f"Agent '{name}' 已删除: {agent_dir}")

135 return f"✅ Agent '{name}' 已删除\n\n已移除:\n- 工作目录: {agent_dir}\n- 权限配置: ~/.qrclaw/permissions.yaml\n\n⚠️ 所有数据（会话、日志、技能、记忆）已永久删除。"

136

137 except Exception as e:

138 error_msg = f"错误：删除 Agent 失败: {e}"

139 logger.error(f"删除 Agent 失败: {name}, 错误: {e}", exc_info=True)

140 return error_msg