第 11 章:实战 — 高级特性

本章是 MiniClaw 实战项目的最后一章。我们要给这个小助手装上"安全锁"、"闹钟"、 "团队协作"和"深度思考"四大能力,让它从一个能聊天的玩具变成一个真正靠谱的 AI 助手。

本章目标

经过前三章的努力,MiniClaw 已经有了对话引擎、记忆系统和工具生态。但还差几块拼图:

特性 解决什么问题
安全防护 防止 AI 执行危险命令、泄露敏感信息
定时任务 让 MiniClaw 能定期做一些自动化工作
多 Agent 协作 遇到专业问题交给专业的"子 Agent"处理
扩展思考 面对复杂问题时让 Claude 想得更深

本章结束后,MiniClaw 就是一个功能完整的项目了。


1. 安全防护(钩子实战)

还记得第 6 章学的钩子系统吗?现在把它用起来。安全防护分三层:

  1. PreToolUse 钩子 — 工具执行前拦截危险操作
  2. PostToolUse 钩子 — 工具执行后脱敏敏感信息
  3. 审计日志 — 记录所有工具调用,出了问题可以回溯

1.1 拦截危险命令

最可怕的事情是 AI 跑了一个 rm -rf /。我们必须在命令执行之前就拦住它:

async def block_dangerous_commands(input_data, tool_use_id, context):
    """PreToolUse 钩子:拦截危险的 Bash 命令。"""
    if input_data.get("tool_name") != "Bash":
        return {}  # 不是 Bash,放行

    command = input_data.get("tool_input", {}).get("command", "")

    # 危险命令关键词
    dangerous_patterns = ["rm -rf", "sudo ", "mkfs", "> /dev/", "dd if="]

    for pattern in dangerous_patterns:
        if pattern in command:
            return {
                "decision": "block",
                "reason": f"命令包含危险操作: {pattern}",
            }

    return {}  # 安全,放行

原理:在 Bash 工具执行之前检查命令关键词,发现危险就返回 "decision": "block" 拦掉。

1.2 敏感信息脱敏

AI 的回复里可能不小心包含 API Key、密码。PostToolUse 钩子在返回结果前做清洗:

async def redact_sensitive_info(input_data, tool_use_id, context):
    """PostToolUse 钩子:对工具输出中的敏感信息脱敏。"""
    response = input_data.get("tool_response", "")
    if not isinstance(response, str):
        return {}

    # 检测 API Key / Token / 密码等模式
    patterns = [
        r'(?i)(api[_-]?key|token|secret)\s*[:=]\s*["\']?[\w-]{20,}',
        r'(?i)(password|passwd)\s*[:=]\s*\S+',
    ]

    for pattern in patterns:
        if re.search(pattern, response):
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PostToolUse",
                    "additionalContext": "检测到敏感信息,已提醒注意脱敏。",
                }
            }
    return {}

1.3 审计日志

所有工具调用都记录到 ~/.miniclaw/audit.log,方便事后审计:

async def audit_tool_use(input_data, tool_use_id, context):
    """PostToolUse 钩子:记录工具调用审计日志。"""
    log_file = Path.home() / ".miniclaw" / "audit.log"
    record = {
        "time": datetime.now().isoformat(),
        "tool": input_data.get("tool_name", "unknown"),
        "input": input_data.get("tool_input", {}),
    }
    with open(log_file, "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")
    return {}  # 不影响正常流程

1.4 在引擎中集成钩子

把三个钩子组装到 ClaudeAgentOptions 里:

hooks = {
    "PreToolUse": [
        HookMatcher(matcher="Bash", hooks=[block_dangerous_commands]),
    ],
    "PostToolUse": [
        HookMatcher(matcher=None, hooks=[redact_sensitive_info, audit_tool_use]),
    ],
}

matcher 字段控制钩子针对哪些工具生效:"Bash" 只对 Bash 生效,"Write|Edit" 对 Write 和 Edit 生效,None 对所有工具生效。完整代码在 miniclaw/engine.py


2. 定时任务 (scheduler.py)

有时我们希望 MiniClaw 能定时做事,比如每小时检查系统状态。用 asyncio 做一个轻量调度器。

2.1 核心设计

@dataclass
class ScheduledTask:
    name: str                               # 任务名称
    interval_seconds: float                  # 执行间隔(秒)
    callback: Callable[[], Awaitable[Any]]  # 异步回调函数
    enabled: bool = True                     # 是否启用

调度器对每个任务启动一个 asyncio Task,循环执行回调然后睡一段时间。

2.2 使用示例

scheduler = Scheduler()

async def check_status():
    print(f"[{datetime.now()}] 系统状态正常")

scheduler.add_task(ScheduledTask(
    name="状态检查",
    interval_seconds=3600,  # 每小时一次
    callback=check_status,
))

await scheduler.start()
# ... 程序运行中 ...
await scheduler.stop()

在 CLI 中输入 /schedule 查看任务列表,/schedule toggle 心跳检测 切换任务开关。


3. 多 Agent 协作 (agents.py)

Claude Agent SDK 支持定义多个"子 Agent",每个有自己的专长。主 Agent 遇到特定任务时会自动把任务交给合适的子 Agent。

3.1 AgentDefinition

from claude_agent_sdk import AgentDefinition

code_reviewer = AgentDefinition(
    description="代码审查专家,检查代码质量和潜在问题",
    prompt="你是一个代码审查专家。仔细分析代码,找出 bug、性能问题和安全漏洞。",
    tools=["Read", "Grep", "Glob"],  # 只给读取权限
    model="sonnet",                   # 用 Sonnet,快且便宜
)
字段 含义
description 子 Agent 的自我介绍,主 Agent 据此判断何时调用
prompt 子 Agent 的系统提示词
tools 可使用的工具列表
model 模型:"sonnet"、"opus"、"haiku" 或 "inherit"

3.2 AgentManager

管理类统一注册所有子 Agent,预定义了三个:代码审查、文档写作、数据分析。

class AgentManager:
    def __init__(self):
        self._agents = {}
        self._register_defaults()  # 注册预定义 Agent

    def get_all(self):
        """返回所有 Agent,传给 ClaudeAgentOptions.agents"""
        return dict(self._agents)

3.3 注入到引擎

agent_manager = AgentManager()
options = ClaudeAgentOptions(
    agents=agent_manager.get_all(),
)

就这么简单!主 Agent 会根据对话内容自动决定是否调用子 Agent。输入 /agents 查看所有可用的子 Agent:

MiniClaw> /agents
可用的子 Agent:
  code-reviewer  代码审查专家,检查代码质量和潜在问题
  writer         文档写作专家,擅长写技术文档和说明
  analyst        数据分析专家,擅长分析数据和生成报告

4. 扩展思考模式

有些问题需要 Claude "多想一会儿"——复杂的数学推理、多步骤的逻辑分析。SDK 提供了扩展思考(Extended Thinking)功能。

4.1 三种思考模式

# 自适应 - Claude 自己决定要不要深度思考
thinking = {"type": "adaptive"}

# 启用 - 强制开启,指定思考预算(token 数量)
thinking = {"type": "enabled", "budget_tokens": 10000}

# 禁用 - 关闭思考
thinking = {"type": "disabled"}

4.2 effort 参数

不想操心 token 预算?用更简单的 effort

options = ClaudeAgentOptions(effort="high")
# 可选值:low / medium / high / max

thinkingeffort 同时设置时,thinking 优先级更高。一般用 effort 就够了。

4.3 什么时候用

场景 推荐设置
日常闲聊 不设置或 effort="low"
代码编写 effort="medium"
复杂 bug 排查 effort="high"
数学证明、逻辑推理 effort="max"

4.4 CLI 命令

MiniClaw> /think on       # 开启(默认 effort=high)
MiniClaw> /think off      # 关闭
MiniClaw> /think max      # 设置强度
MiniClaw> /think status   # 查看当前状态

5. 完整项目收尾

5.1 最终目录结构

miniclaw/
  __init__.py       # 包入口
  __main__.py       # 启动入口 (python -m miniclaw)
  auth.py           # 认证管理
  config.py         # 配置中心
  engine.py         # 对话引擎(集成钩子、Agent、思考模式)
  memory.py         # 记忆系统
  cli.py            # 命令行界面(全部命令)
  scheduler.py      # 定时任务调度器
  agents.py         # 多 Agent 管理
  tools/
    __init__.py     # 工具包入口
    registry.py     # 工具注册表
    builtin.py      # 内置工具

5.2 启动方式

export ANTHROPIC_API_KEY="你的key"
python -m miniclaw

5.3 命令一览

命令 功能
/help 显示帮助信息
/clear 清空对话历史
/save 保存当前对话
/load 加载历史对话
/tools 查看可用工具
/agents 查看子 Agent 列表
/think on\|off\|max 切换思考模式
/schedule 查看定时任务
/quit 退出

5.4 核心代码串讲

引擎启动时,一次性把所有高级特性组装好:

# engine.py 核心流程
async def connect(self):
    hooks = self._build_hooks()              # 1. 安全防护
    agents = self.agent_manager.get_all()    # 2. 多 Agent
    thinking = self._build_thinking_config() # 3. 思考模式

    options = ClaudeAgentOptions(
        system_prompt=self.config.system_prompt,
        hooks=hooks,
        agents=agents,
        thinking=thinking,
        permission_mode="bypassPermissions",
    )

    self._client = ClaudeSDKClient(options)
    await self._client.connect()
    await self.scheduler.start()             # 4. 定时任务

6. 回顾与展望

整个教程学了什么

章节 内容 核心概念
第 0 章 环境准备 SDK 安装、API Key
第 1 章 第一次对话 query() 函数
第 2 章 消息系统 Message 类型体系
第 3 章 配置选项 ClaudeAgentOptions
第 4 章 多轮对话 ClaudeSDKClient
第 5 章 自定义工具 MCP 工具
第 6 章 钩子系统 HookMatcher
第 7 章 权限控制 PermissionMode
第 8 章 项目架构 MiniClaw 设计
第 9 章 核心引擎 对话 + 记忆
第 10 章 工具生态 工具注册表
第 11 章 高级特性 安全、调度、Agent、思考

扩展方向

MiniClaw 还有很多可以玩的方向:

  1. Web 界面 — 用 FastAPI + WebSocket 做一个 Web 版
  2. 消息平台集成 — 接入飞书、钉钉、Slack
  3. 更多工具 — 数据库查询、HTTP 请求、文件上传
  4. RAG 增强 — 接入向量数据库,让记忆系统更强大
  5. 多用户支持 — 每个用户独立的对话和配置
  6. 监控面板 — 可视化审计日志和使用统计

MiniClaw 的核心功能到这里就完成了!接下来,我们将探索 Claude Code 的 Skill 扩展体系 — 学会用 Slash Commands、Skills 和 Plugins 为你的 Agent 注入更多能力。


完整代码

以下是本章所有文件的完整可运行代码。在 miniclaw/ 目录的上级目录执行 python -m miniclaw 即可运行。

miniclaw/__init__.py

"""MiniClaw - 基于 Claude Agent SDK 的智能命令行助手(完整版)。

这是教程第 11 章的最终版本,集成了全部功能:
- 对话引擎 + 流式输出
- 记忆系统
- 工具注册 + 内置工具
- 安全防护(钩子)
- 定时任务
- 多 Agent 协作
- 扩展思考模式
"""

__version__ = "0.4.0"
__app_name__ = "MiniClaw"

miniclaw/auth.py

"""认证管理 - 管理 API Key 和认证状态。

从环境变量或配置文件中加载 API Key。
"""

import os
from pathlib import Path


class AuthManager:
    """管理 Claude API 认证。"""

    # API Key 环境变量名
    ENV_KEY = "ANTHROPIC_API_KEY"

    def __init__(self):
        self._api_key: str | None = None

    def load_api_key(self) -> str | None:
        """加载 API Key,优先级:环境变量 > 配置文件。"""
        # 1. 先查环境变量
        key = os.environ.get(self.ENV_KEY)
        if key:
            self._api_key = key
            return key

        # 2. 再查配置文件 ~/.miniclaw/api_key
        key_file = Path.home() / ".miniclaw" / "api_key"
        if key_file.exists():
            key = key_file.read_text(encoding="utf-8").strip()
            if key:
                self._api_key = key
                # 设置到环境变量,让 SDK 能读到
                os.environ[self.ENV_KEY] = key
                return key

        return None

    def save_api_key(self, key: str) -> None:
        """保存 API Key 到配置文件。"""
        key_dir = Path.home() / ".miniclaw"
        key_dir.mkdir(exist_ok=True)

        key_file = key_dir / "api_key"
        key_file.write_text(key, encoding="utf-8")

        # 设置文件权限为仅所有者可读写
        key_file.chmod(0o600)

        self._api_key = key
        os.environ[self.ENV_KEY] = key

    @property
    def is_authenticated(self) -> bool:
        """是否已认证。"""
        return self._api_key is not None

    @property
    def api_key_preview(self) -> str:
        """显示 API Key 的前几位(脱敏)。"""
        if not self._api_key:
            return "(未设置)"
        return self._api_key[:12] + "..." + self._api_key[-4:]

miniclaw/config.py

"""配置中心 - 管理 MiniClaw 的全局配置。

所有配置项集中在这里管理,方便修改和扩展。
"""

from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class MiniClawConfig:
    """MiniClaw 全局配置。"""

    # 应用信息
    app_name: str = "MiniClaw"

    # 数据目录
    data_dir: Path = field(default_factory=lambda: Path.home() / ".miniclaw")

    # 系统提示词
    system_prompt: str = (
        "你是 MiniClaw,一个友好、专业的命令行智能助手。"
        "你擅长编程、系统管理和解答技术问题。"
        "回答要简洁明了,代码要附上中文注释。"
        "如果用户的问题不够明确,主动追问。"
    )

    # 对话设置
    max_turns: int | None = None  # None 表示不限制
    max_budget_usd: float | None = None  # None 表示不限制

    # 记忆系统
    memory_file: str = "memory.json"
    max_memory_entries: int = 100

    # 审计日志
    audit_log_file: str = "audit.log"

    # 思考模式(默认关闭)
    thinking_enabled: bool = False
    thinking_effort: str = "high"  # low / medium / high / max

    # 权限模式
    permission_mode: str = "bypassPermissions"

    def ensure_data_dir(self) -> None:
        """确保数据目录存在。"""
        self.data_dir.mkdir(parents=True, exist_ok=True)

    @property
    def memory_path(self) -> Path:
        """记忆文件完整路径。"""
        return self.data_dir / self.memory_file

    @property
    def audit_log_path(self) -> Path:
        """审计日志完整路径。"""
        return self.data_dir / self.audit_log_file

miniclaw/engine.py

"""对话引擎 - MiniClaw 的核心(完整版)。

集成全部功能:
- 流式对话
- 钩子(安全防护:危险命令拦截、敏感信息脱敏、审计日志)
- 多 Agent 协作
- 扩展思考模式
- MCP 工具
"""

import json
import re
from datetime import datetime
from pathlib import Path
from typing import Any

from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    HookMatcher,
    AssistantMessage,
    ResultMessage,
    TextBlock,
    ThinkingBlock,
    create_sdk_mcp_server,
)

from miniclaw.config import MiniClawConfig
from miniclaw.memory import MemoryManager
from miniclaw.agents import AgentManager
from miniclaw.scheduler import Scheduler, ScheduledTask
from miniclaw.tools import ToolRegistry, register_builtin_tools


# ========== 安全钩子 ==========


async def block_dangerous_commands(
    input_data: dict[str, Any],
    tool_use_id: str | None,
    context: dict[str, Any],
) -> dict[str, Any]:
    """PreToolUse 钩子:拦截危险的 Bash 命令。

    在 Bash 工具执行之前检查命令内容,发现危险操作直接拦截。
    """
    # 只检查 Bash 工具
    if input_data.get("tool_name") != "Bash":
        return {}

    command = input_data.get("tool_input", {}).get("command", "")

    # 危险命令关键词列表
    dangerous_patterns = [
        "rm -rf /",       # 删除根目录
        "rm -rf ~",       # 删除用户目录
        "sudo rm",        # sudo 删除
        "mkfs",           # 格式化磁盘
        "> /dev/sd",      # 覆盖磁盘设备
        "dd if=/dev/zero",  # 磁盘写零
        ":(){:|:&};:",    # Fork 炸弹
        "chmod -R 777 /", # 危险权限修改
    ]

    for pattern in dangerous_patterns:
        if pattern in command:
            return {
                "decision": "block",
                "reason": f"[安全防护] 命令包含危险操作 '{pattern}',已拦截。",
                "systemMessage": f"危险操作被阻止: {pattern}",
            }

    return {}  # 安全,放行


async def redact_sensitive_info(
    input_data: dict[str, Any],
    tool_use_id: str | None,
    context: dict[str, Any],
) -> dict[str, Any]:
    """PostToolUse 钩子:对工具输出中的敏感信息进行脱敏。

    检查工具的返回内容,把 API Key、密码之类的敏感信息替换成 ***。
    """
    response = input_data.get("tool_response", "")
    if not isinstance(response, str):
        return {}

    # 敏感信息正则模式
    sensitive_patterns = [
        # API Key / Token / Secret
        (r'(?i)(api[_-]?key|token|secret|access[_-]?key)\s*[:=]\s*["\']?[\w\-]{16,}', "***REDACTED***"),
        # 密码
        (r'(?i)(password|passwd|pwd)\s*[:=]\s*\S+', "password=***REDACTED***"),
        # AWS 风格的 Key
        (r'(?:AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}', "***AWS_KEY_REDACTED***"),
    ]

    found_sensitive = False
    for pattern, _ in sensitive_patterns:
        if re.search(pattern, response):
            found_sensitive = True
            break

    if found_sensitive:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PostToolUse",
                "additionalContext": "[安全防护] 检测到敏感信息,已提醒 Claude 注意脱敏。",
            }
        }

    return {}


async def audit_tool_use(
    input_data: dict[str, Any],
    tool_use_id: str | None,
    context: dict[str, Any],
) -> dict[str, Any]:
    """PostToolUse 钩子:记录工具调用审计日志。

    所有工具调用都会被记录到 ~/.miniclaw/audit.log。
    """
    log_dir = Path.home() / ".miniclaw"
    log_dir.mkdir(exist_ok=True)
    log_file = log_dir / "audit.log"

    record = {
        "time": datetime.now().isoformat(),
        "tool": input_data.get("tool_name", "unknown"),
        "input_summary": _summarize_input(input_data.get("tool_input", {})),
        "tool_use_id": tool_use_id,
    }

    try:
        with open(log_file, "a", encoding="utf-8") as f:
            f.write(json.dumps(record, ensure_ascii=False) + "\n")
    except OSError:
        pass  # 写日志失败不应影响正常流程

    return {}


def _summarize_input(tool_input: dict[str, Any]) -> str:
    """生成工具输入的摘要(避免日志太大)。"""
    summary = {}
    for key, value in tool_input.items():
        if isinstance(value, str) and len(value) > 200:
            summary[key] = value[:200] + "...(截断)"
        else:
            summary[key] = value
    return json.dumps(summary, ensure_ascii=False)


# ========== 默认定时任务 ==========


async def heartbeat_task() -> None:
    """心跳任务:定期打印系统状态(演示用)。"""
    timestamp = datetime.now().strftime("%H:%M:%S")
    print(f"\n[{timestamp}] [心跳] MiniClaw 运行正常")


# ========== 对话引擎 ==========


class MiniClawEngine:
    """MiniClaw 对话引擎(完整版)。

    这是 MiniClaw 的核心,负责:
    1. 管理 ClaudeSDKClient 的生命周期
    2. 组装所有配置(钩子、Agent、工具、思考模式)
    3. 处理对话的发送和接收
    """

    def __init__(self, config: MiniClawConfig | None = None) -> None:
        self.config = config or MiniClawConfig()
        self.config.ensure_data_dir()

        # 记忆系统
        self.memory = MemoryManager(self.config.memory_path)
        self.memory.load()

        # Agent 管理
        self.agent_manager = AgentManager()

        # 工具注册表
        self.tool_registry = ToolRegistry()
        register_builtin_tools(self.tool_registry)

        # 定时任务调度器
        self.scheduler = Scheduler()

        # 客户端实例(connect 后创建)
        self._client: ClaudeSDKClient | None = None

        # 思考模式状态
        self._thinking_enabled = self.config.thinking_enabled
        self._thinking_effort = self.config.thinking_effort

    def _build_hooks(self) -> dict[str, list[HookMatcher]]:
        """构建钩子配置。"""
        return {
            "PreToolUse": [
                HookMatcher(
                    matcher="Bash",  # 只拦截 Bash 工具的危险命令
                    hooks=[block_dangerous_commands],
                ),
            ],
            "PostToolUse": [
                HookMatcher(
                    matcher=None,  # 对所有工具生效
                    hooks=[redact_sensitive_info, audit_tool_use],
                ),
            ],
        }

    def _build_thinking_config(self) -> dict[str, Any] | None:
        """构建思考模式配置。"""
        if not self._thinking_enabled:
            return None
        # 使用 enabled 模式,预算 10000 token
        return {"type": "enabled", "budget_tokens": 10000}

    def _build_effort(self) -> str | None:
        """构建 effort 参数。"""
        if self._thinking_enabled:
            return self._thinking_effort
        return None

    async def connect(self) -> None:
        """启动引擎:创建客户端并连接。"""
        # 创建 MCP 工具服务器
        mcp_tools = self.tool_registry.get_mcp_tools()
        mcp_servers = {}
        if mcp_tools:
            mcp_servers["miniclaw-tools"] = create_sdk_mcp_server(
                name="miniclaw-tools",
                version="1.0.0",
                tools=mcp_tools,
            )

        # 组装选项
        options = ClaudeAgentOptions(
            system_prompt=self.config.system_prompt,
            permission_mode=self.config.permission_mode,  # type: ignore[arg-type]
            max_turns=self.config.max_turns,
            max_budget_usd=self.config.max_budget_usd,
            hooks=self._build_hooks(),  # type: ignore[arg-type]
            agents=self.agent_manager.get_all(),
            thinking=self._build_thinking_config(),  # type: ignore[arg-type]
            effort=self._build_effort(),  # type: ignore[arg-type]
            mcp_servers=mcp_servers,
        )

        # 创建客户端并连接
        self._client = ClaudeSDKClient(options)
        await self._client.connect()

        # 注册默认定时任务(演示:1 小时心跳)
        self.scheduler.add_task(ScheduledTask(
            name="心跳检测",
            interval_seconds=3600,
            callback=heartbeat_task,
            enabled=False,  # 默认不启用,用户通过 /schedule 开启
        ))

        # 启动调度器
        await self.scheduler.start()

    async def send_message(self, message: str) -> str:
        """发送消息并收集完整回复。

        Args:
            message: 用户输入的消息。

        Returns:
            Claude 的完整文本回复。
        """
        if not self._client:
            raise RuntimeError("引擎未连接,请先调用 connect()")

        # 记录用户消息
        self.memory.add_entry("user", message)

        # 发送消息
        await self._client.query(message)

        # 收集回复
        full_response = ""
        async for msg in self._client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        full_response += block.text
                    elif isinstance(block, ThinkingBlock):
                        # 思考内容可以选择性展示
                        pass

            elif isinstance(msg, ResultMessage):
                # 对话结束,可以获取费用信息
                if msg.total_cost_usd:
                    cost_info = f"\n[费用: ${msg.total_cost_usd:.4f}]"
                    full_response += cost_info

        # 记录助手回复
        self.memory.add_entry("assistant", full_response)

        return full_response

    async def send_message_stream(self, message: str):
        """流式发送消息,逐步 yield 回复片段。

        Args:
            message: 用户输入的消息。

        Yields:
            (type, content) 元组:
            - ("text", str): 文本片段
            - ("thinking", str): 思考内容
            - ("result", ResultMessage): 结果消息
        """
        if not self._client:
            raise RuntimeError("引擎未连接,请先调用 connect()")

        self.memory.add_entry("user", message)
        await self._client.query(message)

        full_response = ""
        async for msg in self._client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        full_response += block.text
                        yield ("text", block.text)
                    elif isinstance(block, ThinkingBlock):
                        yield ("thinking", block.thinking)

            elif isinstance(msg, ResultMessage):
                self.memory.add_entry("assistant", full_response)
                yield ("result", msg)

    def set_thinking(self, enabled: bool, effort: str = "high") -> None:
        """设置思考模式。

        注意:修改后需要重新连接才能生效(因为 options 在连接时就固定了)。
        对于 MiniClaw 这种长连接的场景,这个设置在下次重启时生效。
        """
        self._thinking_enabled = enabled
        self._thinking_effort = effort

    @property
    def thinking_enabled(self) -> bool:
        """当前是否开启了扩展思考。"""
        return self._thinking_enabled

    @property
    def thinking_effort(self) -> str:
        """当前的思考强度。"""
        return self._thinking_effort

    async def disconnect(self) -> None:
        """断开连接,释放资源。"""
        # 停止定时任务
        await self.scheduler.stop()

        # 保存记忆
        self.memory.save()

        # 断开客户端
        if self._client:
            await self._client.disconnect()
            self._client = None

miniclaw/memory.py

"""记忆系统 - 管理对话历史的持久化存储。

把对话记录保存到本地文件,下次启动时可以加载回来。
"""

import json
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path


@dataclass
class MemoryEntry:
    """一条记忆记录。"""

    role: str  # "user" 或 "assistant"
    content: str
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())


@dataclass
class ConversationMemory:
    """一次对话的完整记忆。"""

    session_id: str
    title: str  # 对话标题(取自第一条用户消息)
    entries: list[MemoryEntry] = field(default_factory=list)
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())
    updated_at: str = field(default_factory=lambda: datetime.now().isoformat())


class MemoryManager:
    """记忆管理器 - 负责对话历史的增删查改。"""

    def __init__(self, storage_path: Path):
        self._path = storage_path
        self._conversations: dict[str, ConversationMemory] = {}
        self._current_session: str | None = None

    def load(self) -> None:
        """从文件加载记忆数据。"""
        if not self._path.exists():
            return

        try:
            data = json.loads(self._path.read_text(encoding="utf-8"))
            for session_id, conv_data in data.items():
                entries = [
                    MemoryEntry(**entry) for entry in conv_data.get("entries", [])
                ]
                self._conversations[session_id] = ConversationMemory(
                    session_id=session_id,
                    title=conv_data.get("title", "未命名对话"),
                    entries=entries,
                    created_at=conv_data.get("created_at", ""),
                    updated_at=conv_data.get("updated_at", ""),
                )
        except (json.JSONDecodeError, KeyError) as e:
            print(f"[记忆系统] 加载记忆数据失败: {e}")

    def save(self) -> None:
        """保存记忆数据到文件。"""
        self._path.parent.mkdir(parents=True, exist_ok=True)

        data = {}
        for session_id, conv in self._conversations.items():
            data[session_id] = asdict(conv)

        self._path.write_text(
            json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
        )

    def new_session(self, session_id: str) -> None:
        """开始一个新的对话会话。"""
        self._current_session = session_id
        self._conversations[session_id] = ConversationMemory(
            session_id=session_id,
            title="新对话",
        )

    def add_entry(self, role: str, content: str) -> None:
        """添加一条记忆。"""
        if not self._current_session:
            return

        conv = self._conversations.get(self._current_session)
        if not conv:
            return

        entry = MemoryEntry(role=role, content=content)
        conv.entries.append(entry)
        conv.updated_at = datetime.now().isoformat()

        # 如果是第一条用户消息,用它做对话标题
        if role == "user" and len(conv.entries) == 1:
            conv.title = content[:50] + ("..." if len(content) > 50 else "")

    def get_current_entries(self) -> list[MemoryEntry]:
        """获取当前会话的所有记忆。"""
        if not self._current_session:
            return []
        conv = self._conversations.get(self._current_session)
        return conv.entries if conv else []

    def list_sessions(self) -> list[tuple[str, str, str]]:
        """列出所有会话:(session_id, title, updated_at)。"""
        return [
            (sid, conv.title, conv.updated_at)
            for sid, conv in self._conversations.items()
        ]

    def clear_current(self) -> None:
        """清空当前会话的记忆。"""
        if self._current_session and self._current_session in self._conversations:
            self._conversations[self._current_session].entries.clear()

miniclaw/cli.py

"""命令行界面 - MiniClaw 的用户交互层(完整版)。

支持的命令:
    /help       显示帮助信息
    /clear      清空对话历史
    /save       保存当前对话
    /load       加载历史对话
    /tools      查看可用工具
    /agents     查看子 Agent 列表
    /think      切换思考模式
    /schedule   查看和管理定时任务
    /quit       退出
"""

import uuid
from datetime import datetime

from miniclaw import __app_name__, __version__
from miniclaw.auth import AuthManager
from miniclaw.config import MiniClawConfig
from miniclaw.engine import MiniClawEngine


class MiniClawCLI:
    """MiniClaw 命令行界面。

    负责用户输入处理、命令分发、输出格式化。
    对话逻辑全部交给 MiniClawEngine 处理。
    """

    def __init__(self) -> None:
        self.config = MiniClawConfig()
        self.auth = AuthManager()
        self.engine = MiniClawEngine(self.config)
        self._running = False

    async def run(self) -> None:
        """主运行循环。"""
        # 1. 检查认证
        if not self._check_auth():
            return

        # 2. 连接引擎
        print(f"正在启动 {__app_name__}...")
        try:
            await self.engine.connect()
        except Exception as e:
            print(f"启动失败: {e}")
            print("请检查 API Key 是否正确,以及网络连接是否正常。")
            return

        # 3. 创建新的对话会话
        session_id = str(uuid.uuid4())[:8]
        self.engine.memory.new_session(session_id)
        print(f"就绪!会话 ID: {session_id}")
        print("输入 /help 查看帮助,/quit 退出\n")

        # 4. 主循环
        self._running = True
        try:
            while self._running:
                try:
                    user_input = input(f"{__app_name__}> ").strip()
                except EOFError:
                    break

                if not user_input:
                    continue

                # 判断是命令还是对话
                if user_input.startswith("/"):
                    await self._handle_command(user_input)
                else:
                    await self._handle_chat(user_input)

        finally:
            # 5. 清理
            print("\n正在保存数据并断开连接...")
            await self.engine.disconnect()
            print("已退出。")

    def _check_auth(self) -> bool:
        """检查认证状态。"""
        key = self.auth.load_api_key()
        if key:
            print(f"API Key: {self.auth.api_key_preview}")
            return True

        print("未找到 API Key!")
        print("请通过以下方式之一设置:")
        print("  1. 设置环境变量: export ANTHROPIC_API_KEY='你的key'")
        print("  2. 直接输入(将保存到 ~/.miniclaw/api_key):")

        try:
            key_input = input("API Key: ").strip()
        except (EOFError, KeyboardInterrupt):
            return False

        if key_input:
            self.auth.save_api_key(key_input)
            print("API Key 已保存!")
            return True

        print("未输入 API Key,退出。")
        return False

    async def _handle_chat(self, message: str) -> None:
        """处理对话消息。"""
        try:
            print()  # 空行分隔

            # 使用流式模式,逐步输出
            has_thinking = False
            async for msg_type, content in self.engine.send_message_stream(message):
                if msg_type == "text":
                    print(content, end="", flush=True)
                elif msg_type == "thinking":
                    if not has_thinking:
                        print("[思考中...]", flush=True)
                        has_thinking = True
                elif msg_type == "result":
                    # result 是 ResultMessage 对象
                    if hasattr(content, "total_cost_usd") and content.total_cost_usd:
                        print(f"\n[费用: ${content.total_cost_usd:.4f}]", end="")

            print("\n")  # 回复结束后换行

        except Exception as e:
            print(f"\n对话出错: {e}\n")

    async def _handle_command(self, command: str) -> None:
        """处理斜杠命令。"""
        parts = command.split(maxsplit=1)
        cmd = parts[0].lower()
        args = parts[1] if len(parts) > 1 else ""

        handlers = {
            "/help": self._cmd_help,
            "/clear": self._cmd_clear,
            "/save": self._cmd_save,
            "/load": self._cmd_load,
            "/tools": self._cmd_tools,
            "/agents": self._cmd_agents,
            "/think": self._cmd_think,
            "/schedule": self._cmd_schedule,
            "/quit": self._cmd_quit,
            "/exit": self._cmd_quit,
        }

        handler = handlers.get(cmd)
        if handler:
            await handler(args)
        else:
            print(f"未知命令: {cmd},输入 /help 查看帮助\n")

    # ========== 命令处理器 ==========

    async def _cmd_help(self, args: str) -> None:
        """显示帮助信息。"""
        help_text = f"""
{__app_name__} v{__version__} - 命令帮助

对话命令:
  /clear          清空当前对话历史
  /save           保存当前对话到记忆系统
  /load           查看和加载历史对话

工具与 Agent:
  /tools          查看可用的自定义工具
  /agents         查看可用的子 Agent

高级功能:
  /think on       开启扩展思考模式
  /think off      关闭扩展思考模式
  /think <level>  设置思考强度 (low/medium/high/max)
  /schedule       查看定时任务列表

其他:
  /help           显示本帮助
  /quit           退出 {__app_name__}
"""
        print(help_text)

    async def _cmd_clear(self, args: str) -> None:
        """清空对话历史。"""
        self.engine.memory.clear_current()
        print("对话历史已清空\n")

    async def _cmd_save(self, args: str) -> None:
        """保存对话。"""
        self.engine.memory.save()
        entries = self.engine.memory.get_current_entries()
        print(f"已保存!当前对话包含 {len(entries)} 条消息\n")

    async def _cmd_load(self, args: str) -> None:
        """列出并加载历史对话。"""
        sessions = self.engine.memory.list_sessions()
        if not sessions:
            print("没有历史对话\n")
            return

        print("历史对话:")
        for i, (sid, title, updated) in enumerate(sessions, 1):
            # 格式化时间
            try:
                dt = datetime.fromisoformat(updated)
                time_str = dt.strftime("%m-%d %H:%M")
            except ValueError:
                time_str = updated[:16]
            print(f"  {i}. [{sid}] {title}  ({time_str})")
        print()

    async def _cmd_tools(self, args: str) -> None:
        """显示可用工具。"""
        categories = self.engine.tool_registry.get_tools_by_category()

        if not categories:
            print("没有已注册的自定义工具\n")
            return

        print("可用的自定义工具:\n")
        for category, tools in categories.items():
            print(f"  [{category}]")
            for tool_info in tools:
                print(f"    {tool_info.name:20s} {tool_info.description}")
            print()

    async def _cmd_agents(self, args: str) -> None:
        """显示可用的子 Agent。"""
        agents = self.engine.agent_manager.list_agents()

        if not agents:
            print("没有可用的子 Agent\n")
            return

        print("可用的子 Agent:\n")
        # 计算名称最大宽度,方便对齐
        max_name_len = max(len(name) for name, _ in agents)
        for name, description in agents:
            print(f"  {name:<{max_name_len + 2}} {description}")
        print()
        print("提示:在对话中提到 Agent 名称,主 Agent 会自动调用它。\n")

    async def _cmd_think(self, args: str) -> None:
        """切换思考模式。"""
        args = args.strip().lower()

        if not args or args == "on":
            self.engine.set_thinking(True, "high")
            print("已开启扩展思考模式 (effort=high)")
            print("注意:修改将在下次重新连接时生效\n")

        elif args == "off":
            self.engine.set_thinking(False)
            print("已关闭扩展思考模式")
            print("注意:修改将在下次重新连接时生效\n")

        elif args in ("low", "medium", "high", "max"):
            self.engine.set_thinking(True, args)
            print(f"已设置思考强度为 {args}")
            print("注意:修改将在下次重新连接时生效\n")

        elif args == "status":
            status = "开启" if self.engine.thinking_enabled else "关闭"
            print(f"扩展思考: {status}")
            if self.engine.thinking_enabled:
                print(f"思考强度: {self.engine.thinking_effort}")
            print()

        else:
            print("用法: /think on|off|low|medium|high|max|status\n")

    async def _cmd_schedule(self, args: str) -> None:
        """查看和管理定时任务。"""
        args = args.strip()

        if not args or args == "list":
            # 列出所有任务
            tasks = self.engine.scheduler.list_tasks()
            if not tasks:
                print("没有定时任务\n")
                return

            status_str = "运行中" if self.engine.scheduler.is_running else "已停止"
            print(f"定时任务(调度器: {status_str}):\n")
            for name, interval, enabled in tasks:
                status = "开" if enabled else "关"
                # 友好的时间显示
                if interval >= 3600:
                    interval_str = f"{interval / 3600:.0f} 小时"
                elif interval >= 60:
                    interval_str = f"{interval / 60:.0f} 分钟"
                else:
                    interval_str = f"{interval:.0f} 秒"
                print(f"  [{status}] {name} - 每 {interval_str} 执行一次")
            print()

        elif args.startswith("toggle "):
            # 切换任务状态
            task_name = args[7:].strip()
            result = self.engine.scheduler.toggle_task(task_name)
            if result is None:
                print(f"找不到任务: {task_name}\n")
            else:
                status = "开启" if result else "关闭"
                print(f"任务 '{task_name}' 已{status}\n")

        else:
            print("用法:")
            print("  /schedule          查看所有定时任务")
            print("  /schedule toggle <名称>  切换任务开关\n")

    async def _cmd_quit(self, args: str) -> None:
        """退出程序。"""
        self._running = False
        print("正在退出...\n")

miniclaw/scheduler.py

"""定时任务调度器 - 基于 asyncio 的轻量调度。

用法:
    scheduler = Scheduler()
    scheduler.add_task(ScheduledTask(
        name="状态检查",
        interval_seconds=3600,
        callback=check_status,
    ))
    await scheduler.start()
    # ... 程序运行中 ...
    await scheduler.stop()
"""

import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any


@dataclass
class ScheduledTask:
    """一个定时任务。

    Attributes:
        name: 任务名称,用于显示和管理。
        interval_seconds: 执行间隔,单位秒。
        callback: 异步回调函数,每次触发时执行。
        enabled: 是否启用,禁用的任务不会被调度。
    """

    name: str
    interval_seconds: float
    callback: Callable[[], Awaitable[Any]]
    enabled: bool = True


class Scheduler:
    """轻量级异步任务调度器。

    基于 asyncio.create_task 实现,每个定时任务对应一个后台协程。
    协程在循环中执行回调、等待间隔时间,如此反复。
    """

    def __init__(self) -> None:
        self._tasks: list[ScheduledTask] = []
        self._running_tasks: dict[str, asyncio.Task[None]] = {}
        self._running = False

    def add_task(self, task: ScheduledTask) -> None:
        """添加一个定时任务。

        如果调度器已经在运行,新任务会立即启动。
        """
        self._tasks.append(task)
        # 如果调度器已经在跑了,立即启动新任务
        if self._running and task.enabled:
            self._running_tasks[task.name] = asyncio.create_task(
                self._run_task(task)
            )

    def remove_task(self, name: str) -> bool:
        """按名称移除一个定时任务。返回是否成功。"""
        # 先取消正在运行的协程
        if name in self._running_tasks:
            self._running_tasks[name].cancel()
            del self._running_tasks[name]

        # 从列表中移除
        for i, task in enumerate(self._tasks):
            if task.name == name:
                self._tasks.pop(i)
                return True
        return False

    def toggle_task(self, name: str) -> bool | None:
        """切换任务的启用/禁用状态。返回新状态,None 表示没找到。"""
        for task in self._tasks:
            if task.name == name:
                task.enabled = not task.enabled
                # 如果正在运行,需要相应地启动或停止
                if self._running:
                    if task.enabled and task.name not in self._running_tasks:
                        self._running_tasks[task.name] = asyncio.create_task(
                            self._run_task(task)
                        )
                    elif not task.enabled and task.name in self._running_tasks:
                        self._running_tasks[task.name].cancel()
                        del self._running_tasks[task.name]
                return task.enabled
        return None

    async def _run_task(self, task: ScheduledTask) -> None:
        """执行单个定时任务的循环。"""
        # 首次执行前先等一个间隔(避免启动时立即触发)
        await asyncio.sleep(task.interval_seconds)

        while self._running and task.enabled:
            try:
                await task.callback()
            except asyncio.CancelledError:
                # 被取消了,安静退出
                break
            except Exception as e:
                # 任务出错不影响调度器,打印错误继续
                timestamp = datetime.now().strftime("%H:%M:%S")
                print(f"[{timestamp}] [调度器] 任务 '{task.name}' 出错: {e}")

            await asyncio.sleep(task.interval_seconds)

    async def start(self) -> None:
        """启动调度器,开始执行所有已启用的任务。"""
        if self._running:
            return  # 避免重复启动

        self._running = True
        for task in self._tasks:
            if task.enabled:
                self._running_tasks[task.name] = asyncio.create_task(
                    self._run_task(task)
                )

    async def stop(self) -> None:
        """停止调度器,取消所有正在运行的任务。"""
        self._running = False
        for name, t in self._running_tasks.items():
            t.cancel()
            # 等待任务真正结束,忽略 CancelledError
            try:
                await t
            except asyncio.CancelledError:
                pass
        self._running_tasks.clear()

    def list_tasks(self) -> list[tuple[str, float, bool]]:
        """列出所有任务:(name, interval_seconds, enabled)。"""
        return [
            (task.name, task.interval_seconds, task.enabled)
            for task in self._tasks
        ]

    @property
    def is_running(self) -> bool:
        """调度器是否正在运行。"""
        return self._running

miniclaw/agents.py

"""多 Agent 管理 - 定义和管理专业子 Agent。

通过 AgentDefinition 定义不同专长的子 Agent,
主 Agent 会根据对话内容自动决定是否交给子 Agent 处理。

用法:
    manager = AgentManager()
    agents = manager.get_all()  # 传给 ClaudeAgentOptions.agents
"""

from claude_agent_sdk import AgentDefinition


class AgentManager:
    """管理多个 Agent 定义。

    内置三个预定义的 Agent:
    - code-reviewer: 代码审查专家
    - writer: 文档写作专家
    - analyst: 数据分析专家

    也可以通过 register() 方法注册自定义 Agent。
    """

    def __init__(self) -> None:
        self._agents: dict[str, AgentDefinition] = {}
        self._register_defaults()

    def _register_defaults(self) -> None:
        """注册默认的专业 Agent。"""

        # 代码审查专家:只有读取权限,不能修改代码
        self._agents["code-reviewer"] = AgentDefinition(
            description="代码审查专家,检查代码质量和潜在问题",
            prompt=(
                "你是一个代码审查专家。仔细分析代码,找出以下问题:\n"
                "1. Bug 和逻辑错误\n"
                "2. 性能问题和内存泄漏\n"
                "3. 安全漏洞\n"
                "4. 代码风格和可读性\n"
                "给出建设性的反馈,说明问题在哪里以及如何修复。"
            ),
            tools=["Read", "Grep", "Glob"],
            model="sonnet",
        )

        # 文档写作专家:可以读写文件
        self._agents["writer"] = AgentDefinition(
            description="文档写作专家,擅长写技术文档和说明",
            prompt=(
                "你是一个技术文档专家。你的职责:\n"
                "1. 写出清晰、准确、易懂的文档\n"
                "2. 使用恰当的 Markdown 格式\n"
                "3. 包含代码示例和使用说明\n"
                "4. 保持结构化的章节组织\n"
                "用中文写作,语气专业但不失亲切。"
            ),
            tools=["Read", "Write"],
            model="sonnet",
        )

        # 数据分析专家:可以读取文件和执行命令
        self._agents["analyst"] = AgentDefinition(
            description="数据分析专家,擅长分析数据和生成报告",
            prompt=(
                "你是一个数据分析专家。你擅长:\n"
                "1. 读取和解析各种数据格式(CSV, JSON, 日志等)\n"
                "2. 进行统计分析和趋势发现\n"
                "3. 生成清晰的分析报告\n"
                "4. 用简单的语言解释复杂的数据发现\n"
                "分析时要注意数据质量问题,给出可操作的建议。"
            ),
            tools=["Read", "Grep", "Glob", "Bash"],
            model="sonnet",
        )

    def register(self, name: str, agent: AgentDefinition) -> None:
        """注册一个自定义 Agent。

        Args:
            name: Agent 的唯一标识符。
            agent: Agent 的定义。
        """
        self._agents[name] = agent

    def unregister(self, name: str) -> bool:
        """取消注册一个 Agent。返回是否成功。"""
        if name in self._agents:
            del self._agents[name]
            return True
        return False

    def get(self, name: str) -> AgentDefinition | None:
        """按名称获取 Agent 定义。"""
        return self._agents.get(name)

    def get_all(self) -> dict[str, AgentDefinition]:
        """获取所有 Agent 定义,直接传给 ClaudeAgentOptions.agents。"""
        return dict(self._agents)

    def list_agents(self) -> list[tuple[str, str]]:
        """列出所有 Agent:(name, description)。"""
        return [
            (name, agent.description) for name, agent in self._agents.items()
        ]

miniclaw/tools/__init__.py

"""MiniClaw 工具包 - 提供可扩展的工具系统。

通过 ToolRegistry 注册和管理工具,
通过 builtin 模块提供内置工具。
"""

from miniclaw.tools.registry import ToolRegistry
from miniclaw.tools.builtin import register_builtin_tools

__all__ = ["ToolRegistry", "register_builtin_tools"]

miniclaw/tools/registry.py

"""工具注册表 - 管理 MiniClaw 的工具生态。

工具通过 register() 方法注册到注册表中,
引擎在初始化时读取注册表来配置 MCP 工具。
"""

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any

from claude_agent_sdk import SdkMcpTool


@dataclass
class ToolInfo:
    """工具的描述信息。"""

    name: str
    description: str
    category: str = "通用"  # 工具分类


class ToolRegistry:
    """工具注册表。

    用法:
        registry = ToolRegistry()
        registry.register(my_tool, category="开发")
        mcp_tools = registry.get_mcp_tools()  # 传给 create_sdk_mcp_server
    """

    def __init__(self) -> None:
        self._tools: dict[str, SdkMcpTool[Any]] = {}
        self._info: dict[str, ToolInfo] = {}

    def register(
        self,
        tool: SdkMcpTool[Any],
        category: str = "通用",
    ) -> None:
        """注册一个 MCP 工具。

        Args:
            tool: 通过 @tool 装饰器创建的 SdkMcpTool。
            category: 工具分类,用于展示。
        """
        self._tools[tool.name] = tool
        self._info[tool.name] = ToolInfo(
            name=tool.name,
            description=tool.description,
            category=category,
        )

    def unregister(self, name: str) -> bool:
        """取消注册一个工具。"""
        if name in self._tools:
            del self._tools[name]
            del self._info[name]
            return True
        return False

    def get_mcp_tools(self) -> list[SdkMcpTool[Any]]:
        """获取所有已注册的 MCP 工具列表。"""
        return list(self._tools.values())

    def get_tool_names(self) -> list[str]:
        """获取所有工具名称。"""
        return list(self._tools.keys())

    def list_tools(self) -> list[ToolInfo]:
        """列出所有工具的描述信息。"""
        return list(self._info.values())

    def get_tools_by_category(self) -> dict[str, list[ToolInfo]]:
        """按分类返回工具信息。"""
        categories: dict[str, list[ToolInfo]] = {}
        for info in self._info.values():
            if info.category not in categories:
                categories[info.category] = []
            categories[info.category].append(info)
        return categories

miniclaw/tools/builtin.py

"""内置工具 - MiniClaw 自带的工具集。

提供一些常用的自定义工具,通过 SDK MCP Server 暴露给 Claude。
"""

import platform
from datetime import datetime
from typing import Any

from claude_agent_sdk import tool

from miniclaw.tools.registry import ToolRegistry


# ========== 工具定义 ==========


@tool(
    name="get_current_time",
    description="获取当前时间和日期",
    input_schema={
        "type": "object",
        "properties": {
            "format": {
                "type": "string",
                "description": "时间格式,如 '%Y-%m-%d %H:%M:%S'(可选)",
            }
        },
        "required": [],
    },
)
async def get_current_time(args: dict[str, Any]) -> dict[str, Any]:
    """返回当前时间。"""
    fmt = args.get("format", "%Y-%m-%d %H:%M:%S")
    now = datetime.now().strftime(fmt)
    return {"content": [{"type": "text", "text": f"当前时间: {now}"}]}


@tool(
    name="get_system_info",
    description="获取系统信息(操作系统、Python版本等)",
    input_schema={
        "type": "object",
        "properties": {},
        "required": [],
    },
)
async def get_system_info(args: dict[str, Any]) -> dict[str, Any]:
    """返回系统基本信息。"""
    import sys

    info_lines = [
        f"操作系统: {platform.system()} {platform.release()}",
        f"主机名: {platform.node()}",
        f"架构: {platform.machine()}",
        f"Python: {sys.version}",
        f"平台: {platform.platform()}",
    ]
    return {"content": [{"type": "text", "text": "\n".join(info_lines)}]}


@tool(
    name="note_pad",
    description="简易记事本,可以保存和读取笔记",
    input_schema={
        "type": "object",
        "properties": {
            "action": {
                "type": "string",
                "description": "操作类型: 'write' 写入, 'read' 读取, 'list' 列表",
            },
            "title": {
                "type": "string",
                "description": "笔记标题",
            },
            "content": {
                "type": "string",
                "description": "笔记内容(写入时必填)",
            },
        },
        "required": ["action"],
    },
)
async def note_pad(args: dict[str, Any]) -> dict[str, Any]:
    """简易记事本工具。"""
    import json
    from pathlib import Path

    notes_file = Path.home() / ".miniclaw" / "notes.json"
    notes_file.parent.mkdir(parents=True, exist_ok=True)

    # 加载已有笔记
    notes: dict[str, str] = {}
    if notes_file.exists():
        try:
            notes = json.loads(notes_file.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            notes = {}

    action = args.get("action", "list")

    if action == "write":
        title = args.get("title", "未命名")
        content = args.get("content", "")
        notes[title] = content
        notes_file.write_text(
            json.dumps(notes, ensure_ascii=False, indent=2), encoding="utf-8"
        )
        return {"content": [{"type": "text", "text": f"笔记 '{title}' 已保存"}]}

    elif action == "read":
        title = args.get("title", "")
        if title in notes:
            return {
                "content": [
                    {"type": "text", "text": f"# {title}\n\n{notes[title]}"}
                ]
            }
        return {
            "content": [{"type": "text", "text": f"找不到笔记: {title}"}],
            "is_error": True,
        }

    else:  # list
        if not notes:
            return {"content": [{"type": "text", "text": "还没有任何笔记"}]}
        titles = "\n".join(f"  - {t}" for t in notes.keys())
        return {
            "content": [{"type": "text", "text": f"现有笔记:\n{titles}"}]
        }


# ========== 注册函数 ==========


def register_builtin_tools(registry: ToolRegistry) -> None:
    """把所有内置工具注册到注册表。"""
    registry.register(get_current_time, category="实用工具")
    registry.register(get_system_info, category="系统信息")
    registry.register(note_pad, category="实用工具")

miniclaw/__main__.py

"""MiniClaw 启动入口 - python -m miniclaw 即可运行。

这是完整版入口文件,集成全部功能:
安全防护、定时任务、多 Agent 协作、扩展思考。
"""

import asyncio
import sys

from miniclaw import __app_name__, __version__
from miniclaw.cli import MiniClawCLI


def print_banner():
    """打印启动横幅。"""
    banner = f"""
    ╔══════════════════════════════════════╗
    ║   {__app_name__} v{__version__}                      ║
    ║   基于 Claude Agent SDK 的智能助手   ║
    ╚══════════════════════════════════════╝

    功能清单:
      - 多轮对话 + 流式输出
      - 记忆系统(对话持久化)
      - 工具生态(可扩展)
      - 安全防护(危险命令拦截 + 敏感信息脱敏)
      - 定时任务调度
      - 多 Agent 协作
      - 扩展思考模式

    输入 /help 查看所有命令
    """
    print(banner)


def main():
    """主入口函数。"""
    print_banner()

    cli = MiniClawCLI()

    try:
        asyncio.run(cli.run())
    except KeyboardInterrupt:
        print("\n再见!")
        sys.exit(0)


if __name__ == "__main__":
    main()

下一章: 第 12 章:Skill 与扩展体系