本章介紹如何為 .claude/lib/ 共用程式庫添加新功能。這是維護和擴展 Hook 系統的關鍵技能。

前置知識

建議先閱讀:

共用模組架構

目前的 .claude/lib/ 結構:

1.claude/lib/
2├── __init__.py          # 模組初始化與匯出
3├── git_utils.py         # Git 操作工具
4├── hook_logging.py      # 日誌系統
5├── hook_io.py           # 輸入輸出處理
6├── config_loader.py     # 配置載入器
7├── hook_validator.py    # Hook 驗證器
8└── markdown_link_checker.py  # Markdown 連結檢查

步驟 1:規劃新模組

決定放置位置

情況建議
新的獨立功能建立新檔案
擴展現有功能修改現有檔案
工具函式加入相關現有模組

設計介面

在寫程式碼之前,先規劃公開介面:

 1# 思考要提供什麼功能給使用者
 2
 3# 函式:簡單操作
 4def validate_yaml(content: str) -> bool:
 5    """驗證 YAML 格式"""
 6    pass
 7
 8# 類別:複雜操作或需要狀態
 9class YamlValidator:
10    """YAML 驗證器"""
11
12    def __init__(self, strict: bool = True):
13        """初始化"""
14        pass
15
16    def validate(self, content: str) -> ValidationResult:
17        """驗證內容"""
18        pass

步驟 2:實作新模組

範例:建立 YAML 工具模組

  1# .claude/lib/yaml_utils.py
  2"""
  3YAML 工具模組
  4
  5提供 YAML 檔案的讀取、驗證和處理功能。
  6"""
  7
  8from pathlib import Path
  9from typing import Optional, Any
 10from dataclasses import dataclass
 11
 12# 嘗試導入 yaml
 13try:
 14    import yaml
 15    HAS_YAML = True
 16except ImportError:
 17    HAS_YAML = False
 18
 19
 20@dataclass
 21class YamlResult:
 22    """YAML 處理結果"""
 23    success: bool
 24    data: Optional[dict] = None
 25    error: Optional[str] = None
 26
 27
 28def load_yaml(path: str, encoding: str = "utf-8") -> YamlResult:
 29    """
 30    載入 YAML 檔案
 31
 32    Args:
 33        path: 檔案路徑
 34        encoding: 檔案編碼
 35
 36    Returns:
 37        YamlResult: 載入結果
 38
 39    Example:
 40        result = load_yaml("config.yaml")
 41        if result.success:
 42            print(result.data)
 43        else:
 44            print(f"錯誤: {result.error}")
 45    """
 46    if not HAS_YAML:
 47        return YamlResult(
 48            success=False,
 49            error="PyYAML 未安裝。請執行: pip install pyyaml"
 50        )
 51
 52    file_path = Path(path)
 53
 54    if not file_path.exists():
 55        return YamlResult(
 56            success=False,
 57            error=f"檔案不存在: {path}"
 58        )
 59
 60    try:
 61        content = file_path.read_text(encoding=encoding)
 62        data = yaml.safe_load(content)
 63        return YamlResult(success=True, data=data or {})
 64    except yaml.YAMLError as e:
 65        return YamlResult(success=False, error=f"YAML 解析錯誤: {e}")
 66
 67
 68def validate_yaml(content: str) -> bool:
 69    """
 70    驗證 YAML 格式
 71
 72    Args:
 73        content: YAML 內容字串
 74
 75    Returns:
 76        bool: 格式正確返回 True
 77    """
 78    if not HAS_YAML:
 79        return False
 80
 81    try:
 82        yaml.safe_load(content)
 83        return True
 84    except yaml.YAMLError:
 85        return False
 86
 87
 88def merge_yaml_configs(*configs: dict) -> dict:
 89    """
 90    合併多個 YAML 配置
 91
 92    後面的配置會覆蓋前面的。
 93
 94    Args:
 95        *configs: 要合併的配置字典
 96
 97    Returns:
 98        dict: 合併後的配置
 99
100    Example:
101        base = {"a": 1, "b": 2}
102        override = {"b": 3, "c": 4}
103        result = merge_yaml_configs(base, override)
104        # result = {"a": 1, "b": 3, "c": 4}
105    """
106    result = {}
107    for config in configs:
108        if config:
109            _deep_merge(result, config)
110    return result
111
112
113def _deep_merge(base: dict, override: dict) -> None:
114    """深度合併字典(就地修改 base)"""
115    for key, value in override.items():
116        if (
117            key in base
118            and isinstance(base[key], dict)
119            and isinstance(value, dict)
120        ):
121            _deep_merge(base[key], value)
122        else:
123            base[key] = value

步驟 3:更新 __init__.py

__init__.py 中註冊新模組:

 1# .claude/lib/__init__.py
 2"""
 3Claude Hooks 共用程式庫
 4"""
 5
 6# 現有匯入
 7from .git_utils import (
 8    run_git_command,
 9    get_current_branch,
10    # ...
11)
12
13from .hook_logging import setup_hook_logging
14from .hook_io import read_hook_input, write_hook_output
15
16# 新增:YAML 工具
17from .yaml_utils import (
18    load_yaml,
19    validate_yaml,
20    merge_yaml_configs,
21    YamlResult,
22)
23
24__all__ = [
25    # 現有匯出
26    "run_git_command",
27    "get_current_branch",
28    "setup_hook_logging",
29    "read_hook_input",
30    "write_hook_output",
31    # 新增
32    "load_yaml",
33    "validate_yaml",
34    "merge_yaml_configs",
35    "YamlResult",
36]
37
38__version__ = "0.29.0"  # 更新版本

步驟 4:撰寫測試

  1# tests/lib/test_yaml_utils.py
  2"""
  3YAML 工具模組測試
  4"""
  5
  6import unittest
  7from unittest.mock import patch, mock_open
  8import sys
  9from pathlib import Path
 10
 11# 添加 lib 目錄到路徑
 12sys.path.insert(0, str(Path(__file__).parent.parent.parent / ".claude" / "lib"))
 13
 14from yaml_utils import load_yaml, validate_yaml, merge_yaml_configs, YamlResult
 15
 16
 17class TestLoadYaml(unittest.TestCase):
 18    """測試 load_yaml 函式"""
 19
 20    def test_load_valid_yaml(self):
 21        """測試載入有效的 YAML 檔案"""
 22        yaml_content = "key: value\nlist:\n  - item1\n  - item2"
 23
 24        with patch("pathlib.Path.exists", return_value=True):
 25            with patch("pathlib.Path.read_text", return_value=yaml_content):
 26                result = load_yaml("test.yaml")
 27
 28        self.assertTrue(result.success)
 29        self.assertEqual(result.data["key"], "value")
 30        self.assertEqual(len(result.data["list"]), 2)
 31
 32    def test_load_nonexistent_file(self):
 33        """測試載入不存在的檔案"""
 34        with patch("pathlib.Path.exists", return_value=False):
 35            result = load_yaml("nonexistent.yaml")
 36
 37        self.assertFalse(result.success)
 38        self.assertIn("不存在", result.error)
 39
 40    def test_load_invalid_yaml(self):
 41        """測試載入無效的 YAML"""
 42        invalid_content = "key: [invalid yaml"
 43
 44        with patch("pathlib.Path.exists", return_value=True):
 45            with patch("pathlib.Path.read_text", return_value=invalid_content):
 46                result = load_yaml("invalid.yaml")
 47
 48        self.assertFalse(result.success)
 49        self.assertIn("解析錯誤", result.error)
 50
 51
 52class TestValidateYaml(unittest.TestCase):
 53    """測試 validate_yaml 函式"""
 54
 55    def test_valid_yaml(self):
 56        """測試有效的 YAML"""
 57        self.assertTrue(validate_yaml("key: value"))
 58        self.assertTrue(validate_yaml("list:\n  - a\n  - b"))
 59
 60    def test_invalid_yaml(self):
 61        """測試無效的 YAML"""
 62        self.assertFalse(validate_yaml("key: [unclosed"))
 63        self.assertFalse(validate_yaml("  bad indent\nkey: value"))
 64
 65
 66class TestMergeYamlConfigs(unittest.TestCase):
 67    """測試 merge_yaml_configs 函式"""
 68
 69    def test_simple_merge(self):
 70        """測試簡單合併"""
 71        base = {"a": 1, "b": 2}
 72        override = {"b": 3, "c": 4}
 73
 74        result = merge_yaml_configs(base, override)
 75
 76        self.assertEqual(result["a"], 1)
 77        self.assertEqual(result["b"], 3)  # 被覆蓋
 78        self.assertEqual(result["c"], 4)
 79
 80    def test_deep_merge(self):
 81        """測試深度合併"""
 82        base = {
 83            "database": {
 84                "host": "localhost",
 85                "port": 5432
 86            }
 87        }
 88        override = {
 89            "database": {
 90                "port": 3306,
 91                "name": "mydb"
 92            }
 93        }
 94
 95        result = merge_yaml_configs(base, override)
 96
 97        self.assertEqual(result["database"]["host"], "localhost")
 98        self.assertEqual(result["database"]["port"], 3306)
 99        self.assertEqual(result["database"]["name"], "mydb")
100
101    def test_multiple_configs(self):
102        """測試多個配置合併"""
103        config1 = {"a": 1}
104        config2 = {"b": 2}
105        config3 = {"c": 3}
106
107        result = merge_yaml_configs(config1, config2, config3)
108
109        self.assertEqual(result, {"a": 1, "b": 2, "c": 3})
110
111
112if __name__ == "__main__":
113    unittest.main()

步驟 5:更新文件

在模組文檔中說明

在模組開頭加入詳細的 docstring:

 1"""
 2YAML 工具模組
 3
 4提供 YAML 檔案的讀取、驗證和處理功能。
 5
 6主要功能:
 7- load_yaml: 載入 YAML 檔案
 8- validate_yaml: 驗證 YAML 格式
 9- merge_yaml_configs: 合併配置
10
11依賴:
12- PyYAML (可選,但建議安裝)
13
14使用方式:
15    from lib.yaml_utils import load_yaml, validate_yaml
16
17    # 載入配置
18    result = load_yaml("config.yaml")
19    if result.success:
20        config = result.data
21
22    # 驗證格式
23    is_valid = validate_yaml(content)
24
25版本: 0.29.0
26"""

擴展現有模組

範例:為 git_utils 添加新功能

 1# 在 .claude/lib/git_utils.py 中添加
 2
 3def get_uncommitted_changes() -> list[str]:
 4    """
 5    取得未提交的變更檔案列表
 6
 7    Returns:
 8        list[str]: 變更檔案的路徑列表
 9
10    Example:
11        changes = get_uncommitted_changes()
12        if changes:
13            print(f"有 {len(changes)} 個未提交的變更")
14    """
15    success, output = run_git_command(["status", "--porcelain"])
16
17    if not success:
18        return []
19
20    files = []
21    for line in output.strip().split("\n"):
22        if line.strip():
23            # 格式: "XY filename" 或 "XY filename -> newname"
24            parts = line[3:].split(" -> ")
25            files.append(parts[-1])
26
27    return files
28
29
30def has_staged_changes() -> bool:
31    """
32    檢查是否有已暫存的變更
33
34    Returns:
35        bool: 有暫存變更返回 True
36    """
37    success, output = run_git_command(["diff", "--cached", "--name-only"])
38    return success and bool(output.strip())

然後在 __init__.py 中更新匯出:

 1from .git_utils import (
 2    run_git_command,
 3    get_current_branch,
 4    get_project_root,
 5    get_worktree_list,
 6    is_protected_branch,
 7    is_allowed_branch,
 8    # 新增
 9    get_uncommitted_changes,
10    has_staged_changes,
11)
12
13__all__ = [
14    # ... 現有匯出 ...
15    "get_uncommitted_changes",
16    "has_staged_changes",
17]

設計原則

1. 單一職責

每個模組專注一個領域:

 1# 好:專注於 Git 操作
 2# git_utils.py
 3def get_current_branch(): ...
 4def run_git_command(): ...
 5def is_protected_branch(): ...
 6
 7# 不好:混合不同功能
 8# utils.py
 9def get_current_branch(): ...
10def validate_yaml(): ...
11def send_notification(): ...

2. 統一的返回值模式

使用一致的返回值設計:

 1# 簡單操作:返回 (bool, str)
 2def run_command(cmd: str) -> tuple[bool, str]:
 3    """返回 (成功與否, 輸出或錯誤訊息)"""
 4    pass
 5
 6# 複雜操作:返回 dataclass
 7@dataclass
 8class OperationResult:
 9    success: bool
10    data: Optional[Any] = None
11    error: Optional[str] = None
12
13def complex_operation() -> OperationResult:
14    pass

3. 優雅的依賴處理

 1# 處理可選依賴
 2try:
 3    import yaml
 4    HAS_YAML = True
 5except ImportError:
 6    HAS_YAML = False
 7
 8def validate_yaml(content: str) -> bool:
 9    if not HAS_YAML:
10        raise ImportError("需要安裝 PyYAML: pip install pyyaml")
11    # ...

4. 完整的文檔字串

 1def load_config(name: str) -> dict:
 2    """
 3    載入配置檔案
 4
 5    Args:
 6        name: 配置名稱(不含副檔名)
 7
 8    Returns:
 9        dict: 配置內容
10
11    Raises:
12        FileNotFoundError: 配置檔案不存在
13
14    Example:
15        config = load_config("agents")
16        print(config["known_agents"])
17    """

完整檢查清單

擴展共用模組時的檢查項目:

  • 設計清晰的公開介面
  • 使用 Type Hints
  • 撰寫完整的 docstring
  • 處理可選依賴
  • 統一返回值模式
  • 更新 __init__.py
  • 更新 __all__ 匯出
  • 更新版本號
  • 撰寫單元測試
  • 測試與現有 Hook 的整合

思考題

  1. 什麼時候應該建立新模組,什麼時候應該擴展現有模組?
  2. 如何設計 API 使其易於測試?
  3. __all__ 的作用是什麼?為什麼要維護它?

上一章:如何新增一個 Hook 下一章:如何新增語言解析器 回到首頁:Python 維護工程師實戰指南