Files
journal_organizer/DEVELOPER_GUIDE.md
T

964 lines
23 KiB
Markdown
Raw Normal View History

# 开发者指南
本指南说明如何扩展和自定义日记整理 Agent,包括新的错误处理模式、配置验证功能和故障排除指南。
## 架构概览
Agent 系统基于以下核心概念:
- **Agent**:主控制器,负责管理 Commands 和 Skills
- **Command**:用户可执行的命令,编排多个 Skills
- **Skill**:原子化的功能单元,执行具体任务
- **SkillChain**:多个 Skills 的有序执行链
- **ErrorHandler**:集中式错误处理和日志记录
- **ConfigurationValidator**:配置验证和环境变量扩展
## 核心类
### Agent
```python
from journal_organizer.agent_core import Agent
# 创建 Agent
agent = Agent("MyAgent", config={})
# 注册命令
agent.register_command(my_command)
# 执行命令
result = await agent.execute_command("command_name", args={})
```
### Skill
所有 Skill 都继承自 `Skill` 基类:
```python
from journal_organizer.agent_core import Skill, SkillType, SkillResult, CommandContext
class MySkill(Skill):
def __init__(self):
super().__init__(
name="my_skill",
skill_type=SkillType.ANALYZE,
description="我的自定义 Skill"
)
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
# 实现您的逻辑
try:
result = do_something(**kwargs)
return SkillResult(
success=True,
data=result,
message="执行成功"
)
except Exception as e:
return SkillResult(
success=False,
error=str(e),
message="执行失败"
)
```
### Command
所有 Command 都继承自 `Command` 基类:
```python
from journal_organizer.agent_core import Command, SkillResult, CommandContext
class MyCommand(Command):
def __init__(self):
super().__init__(
name="my_command",
description="我的自定义命令",
aliases=["mc"]
)
# 注册 Skills
self.register_skill(MySkill())
async def execute(self, context: CommandContext) -> SkillResult:
# 获取参数
param1 = context.args.get('param1')
# 执行 Skill
skill = self.skills['my_skill']
result = await skill.execute(context, param1=param1)
return result
```
## 添加新的 Skill
### 步骤 1: 创建 Skill 类
`skills/` 目录下创建一个新文件,例如 `my_skill.py`
```python
from ..agent_core import Skill, SkillType, SkillResult, CommandContext
class MyCustomSkill(Skill):
def __init__(self):
super().__init__(
name="my_custom_skill",
skill_type=SkillType.TRANSFORM,
description="执行自定义转换"
)
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
try:
input_data = kwargs.get('input_data')
# 您的自定义逻辑
output_data = self._process(input_data)
return SkillResult(
success=True,
data=output_data,
message="处理完成"
)
except Exception as e:
return SkillResult(
success=False,
error=str(e),
message="处理失败"
)
def _process(self, data):
# 实现处理逻辑
return data
```
### 步骤 2: 在 Command 中使用 Skill
```python
from ..skills.my_skill import MyCustomSkill
class MyCommand(Command):
def __init__(self):
super().__init__(name="my_command")
self.register_skill(MyCustomSkill())
async def execute(self, context: CommandContext) -> SkillResult:
skill = self.skills['my_custom_skill']
return await skill.execute(context, input_data="test")
```
## 添加新的 Command
### 步骤 1: 创建 Command 类
`commands/` 目录下创建一个新文件,例如 `my_command.py`
```python
from ..agent_core import Command, SkillResult, CommandContext
from ..skills.my_skill import MyCustomSkill
class MyCommand(Command):
def __init__(self):
super().__init__(
name="my_command",
description="我的自定义命令",
aliases=["mc", "my-cmd"]
)
self.register_skill(MyCustomSkill())
async def execute(self, context: CommandContext) -> SkillResult:
# 获取参数
param1 = context.args.get('param1')
param2 = context.args.get('param2', 'default')
# 执行 Skill
skill = self.skills['my_custom_skill']
result = await skill.execute(context, input_data=param1)
return result
```
### 步骤 2: 在 Agent 中注册 Command
编辑 `main.py``_register_commands` 方法:
```python
def _register_commands(self) -> None:
"""注册所有命令"""
self.agent.register_command(OrganizeCommand())
self.agent.register_command(MyCommand()) # 添加新命令
```
### 步骤 3: 测试新命令
```bash
python -m journal_organizer my_command --param1 "value1"
```
## 使用 SkillChain
SkillChain 允许您按顺序执行多个 Skills:
```python
from journal_organizer.agent_core import SkillChain
class MyCommand(Command):
def __init__(self):
super().__init__(name="my_command")
# 创建 Skill 链
chain = SkillChain("my_chain", "执行一系列操作")
chain.add_skill(Skill1(), {"param1": "value1"})
chain.add_skill(Skill2(), {"param2": "value2"})
self.register_skill_chain(chain)
async def execute(self, context: CommandContext) -> SkillResult:
chain = self.skill_chains['my_chain']
return await chain.execute(context)
```
## 异步编程
所有 Skills 和 Commands 都使用异步编程(async/await)。这允许并发执行多个操作。
### 基本示例
```python
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
# 异步调用外部 API
result = await self.call_external_api()
return SkillResult(success=True, data=result)
async def call_external_api(self):
# 使用 aiohttp 进行异步 HTTP 请求
async with aiohttp.ClientSession() as session:
async with session.get('https://api.example.com/data') as resp:
return await resp.json()
```
## 错误处理
### 新的错误处理框架
系统现在使用集中式错误处理框架,提供一致的错误管理和日志记录:
```python
from journal_organizer.error_handling import (
ErrorHandler,
JournalOrganizerError,
ConfigurationError,
APIError,
ValidationError
)
# 创建错误处理器
import logging
logger = logging.getLogger("MySkill")
error_handler = ErrorHandler(logger)
# 处理 API 错误
try:
result = await api_call()
except Exception as e:
error_result = error_handler.handle_api_error(e, "claude", "analyze_text")
return SkillResult(
success=False,
error=error_result["error"],
message=error_result["message"]
)
```
### 自定义异常类型
使用专门的异常类型来处理不同类型的错误:
```python
from journal_organizer.error_handling import (
JournalOrganizerError,
ConfigurationError,
APIError,
ValidationError
)
# 配置错误
if not api_key:
raise ConfigurationError("API key is required", context={"service": "claude"})
# API 错误
if response.status_code != 200:
raise APIError(
"API request failed",
api_name="obsidian",
status_code=response.status_code
)
# 验证错误
if not validate_input(data):
raise ValidationError("Invalid input format", field="date")
```
### Skill 中的错误处理模式
在 Skill 中实现标准化的错误处理:
```python
from journal_organizer.agent_core import Skill, SkillResult, CommandContext
from journal_organizer.error_handling import ErrorHandler, APIError, ValidationError
class MySkill(Skill):
def __init__(self):
super().__init__(name="my_skill")
self.error_handler = ErrorHandler(self.logger)
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
try:
# 输入验证
self._validate_inputs(**kwargs)
# 执行主要逻辑
result = await self._perform_operation(**kwargs)
return SkillResult(success=True, data=result, message="操作成功")
except ValidationError as e:
error_result = self.error_handler.handle_validation_error(e, "input_data")
return SkillResult(
success=False,
error=error_result["error"],
message=error_result["message"]
)
except APIError as e:
error_result = self.error_handler.handle_api_error(e, "external_service", "operation")
return SkillResult(
success=False,
error=error_result["error"],
message=error_result["message"]
)
except Exception as e:
# 处理未预期的错误
self.logger.error(f"Unexpected error in {self.name}: {str(e)}", exc_info=True)
return SkillResult(
success=False,
error="Internal error occurred",
message="操作失败,请检查日志"
)
def _validate_inputs(self, **kwargs):
"""验证输入参数"""
required_params = ['param1', 'param2']
for param in required_params:
if param not in kwargs:
raise ValidationError(f"Missing required parameter: {param}", field=param)
async def _perform_operation(self, **kwargs):
"""执行主要操作"""
# 实现您的逻辑
pass
```
## 日志记录
使用内置的 logger 记录信息:
```python
class MySkill(Skill):
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
self.logger.debug("开始执行")
self.logger.info("处理数据")
self.logger.warning("可能的问题")
self.logger.error("发生错误")
return SkillResult(success=True)
```
## 配置管理
### 新的配置验证系统
系统现在包含强大的配置验证功能,支持类型检查、环境变量扩展和路径验证:
```python
from journal_organizer.config_validation import (
SystemConfig,
ObsidianConfig,
ClaudeConfig,
validate_system_config
)
# 验证配置
try:
config = validate_system_config(raw_config)
print("配置验证成功")
except ValidationError as e:
print(f"配置验证失败: {e}")
```
### 环境变量扩展
配置文件支持环境变量扩展:
```yaml
# config.yaml
claude:
api_key: "${ANTHROPIC_API_KEY}" # 从环境变量读取
model: "${CLAUDE_MODEL:-claude-3-5-sonnet-20241022}" # 带默认值
obsidian:
vault_path: "${OBSIDIAN_VAULT_PATH}"
rest_api:
api_key: "${OBSIDIAN_API_KEY}"
```
### 配置验证示例
```python
from journal_organizer.config_validation import validate_obsidian_config, validate_claude_config
# 验证 Obsidian 配置
obsidian_config = {
"vault_path": "/path/to/vault",
"rest_api": {
"url": "https://localhost:27123",
"api_key": "your-key",
"verify_ssl": False
}
}
try:
validated_config = validate_obsidian_config(obsidian_config)
print("Obsidian 配置有效")
except ValidationError as e:
print(f"Obsidian 配置错误: {e}")
# 验证 Claude 配置
claude_config = {
"api_key": "sk-ant-...",
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 4096
}
try:
validated_config = validate_claude_config(claude_config)
print("Claude 配置有效")
except ValidationError as e:
print(f"Claude 配置错误: {e}")
```
### 在 Skill 中访问配置
```python
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
config = context.config or {}
# 安全地访问配置
claude_config = config.get('claude', {})
api_key = claude_config.get('api_key')
if not api_key:
raise ConfigurationError("Claude API key not configured")
# 使用配置
return SkillResult(success=True)
```
## 测试
### 单元测试示例
```python
import pytest
from journal_organizer.agent_core import CommandContext
@pytest.mark.asyncio
async def test_my_skill():
skill = MySkill()
context = CommandContext(command_name="test")
result = await skill.execute(context, input_data="test")
assert result.success == True
assert result.data is not None
```
### 运行测试
```bash
pytest tests/
```
## 性能优化
### 并发执行
```python
import asyncio
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
# 并发执行多个操作
results = await asyncio.gather(
self.operation1(),
self.operation2(),
self.operation3()
)
return SkillResult(success=True, data=results)
```
### 缓存
```python
from functools import lru_cache
class MySkill(Skill):
@lru_cache(maxsize=128)
def expensive_operation(self, key):
# 缓存昂贵的操作
return process(key)
```
## 最佳实践
### 现代 Python 模式
系统现在遵循现代 Python 最佳实践:
#### 1. 使用 f-strings 进行字符串格式化
```python
# ✅ 推荐:使用 f-strings
name = "用户"
message = f"欢迎 {name},当前时间是 {datetime.now()}"
# ❌ 避免:字符串连接
message = "欢迎 " + name + ",当前时间是 " + str(datetime.now())
```
#### 2. 使用 pathlib 进行文件路径操作
```python
from pathlib import Path
# ✅ 推荐:使用 pathlib
vault_path = Path(config['obsidian']['vault_path'])
daily_folder = vault_path / "Daily"
note_file = daily_folder / f"{date}.md"
# 检查文件是否存在
if note_file.exists():
content = note_file.read_text(encoding='utf-8')
# ❌ 避免:使用 os.path
import os
note_file = os.path.join(vault_path, "Daily", f"{date}.md")
```
#### 3. 使用 dataclasses 定义数据结构
```python
from dataclasses import dataclass, field
from typing import Optional, List
from datetime import datetime
@dataclass
class SkillResult:
success: bool
data: Optional[Dict[str, Any]] = None
error: Optional[str] = None
message: str = ""
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
```
#### 4. 使用类型提示
```python
from typing import Dict, Any, Optional, List, Union
async def execute_skill(
skill_name: str,
context: CommandContext,
**kwargs: Any
) -> SkillResult:
"""
执行指定的 Skill
Args:
skill_name: Skill 名称
context: 命令执行上下文
**kwargs: Skill 参数
Returns:
SkillResult: 执行结果
"""
pass
```
#### 5. 使用异步上下文管理器
```python
from contextlib import asynccontextmanager
import aiohttp
@asynccontextmanager
async def http_client(config: Dict[str, Any]):
"""HTTP 客户端上下文管理器"""
connector = aiohttp.TCPConnector(
ssl=False if not config.get('verify_ssl', True) else None
)
async with aiohttp.ClientSession(connector=connector) as session:
try:
yield session
finally:
await session.close()
# 使用示例
async def call_api():
async with http_client(api_config) as client:
async with client.get(url) as response:
return await response.json()
```
### 通用最佳实践
1. **单一职责**:每个 Skill 只负责一个任务
2. **错误处理**:使用新的错误处理框架
3. **日志记录**:使用 logger 记录重要信息
4. **配置驱动**:使用配置验证系统
5. **异步编程**:充分利用异步特性提高性能
6. **类型安全**:使用类型提示和验证
7. **文档**:为您的 Skills 和 Commands 编写清晰的文档
8. **测试**:编写单元测试和属性测试确保代码质量
9. **版本控制**:使用 git 管理代码版本
10. **代码格式化**:使用 black 和 isort 保持代码风格一致
## 示例:完整的自定义 Skill
```python
"""
自定义 Skill 示例:文本统计
"""
from ..agent_core import Skill, SkillType, SkillResult, CommandContext
class TextStatisticsSkill(Skill):
"""计算文本统计信息的 Skill"""
def __init__(self):
super().__init__(
name="text_statistics",
skill_type=SkillType.ANALYZE,
description="计算文本的字数、词数、句数等统计信息"
)
async def execute(self, context: CommandContext, **kwargs) -> SkillResult:
"""
执行文本统计
Args:
context: 命令执行上下文
**kwargs: 包含 text 参数
Returns:
SkillResult: 包含统计结果的结果
"""
try:
text = kwargs.get('text', '')
if not text:
return SkillResult(
success=False,
error="缺少文本参数",
message="未提供要统计的文本"
)
# 计算统计信息
stats = {
'char_count': len(text),
'word_count': len(text.split()),
'sentence_count': len(text.split('。')),
'line_count': len(text.split('\n')),
'avg_word_length': len(text) / len(text.split()) if text.split() else 0
}
self.logger.info(f"文本统计完成: {stats}")
return SkillResult(
success=True,
data=stats,
message="文本统计完成"
)
except Exception as e:
self.logger.error(f"文本统计失败: {str(e)}")
return SkillResult(
success=False,
error=str(e),
message="文本统计异常"
)
```
## 资源
- [Python 异步编程](https://docs.python.org/3/library/asyncio.html)
- [Anthropic API 文档](https://docs.anthropic.com/)
- [Obsidian API 文档](https://docs.obsidian.md/Obsidian+API)
---
# 故障排除指南
本节提供常见问题的解决方案和调试技巧。
## 常见问题
### 1. 导入错误 (ImportError)
**问题**: `ImportError: attempted relative import with no known parent package`
**解决方案**:
```bash
# 确保以模块方式运行
python -m journal_organizer --help
# 而不是直接运行
python main.py # ❌ 错误方式
```
**原因**: 项目使用相对导入,需要作为包运行。
### 2. 配置文件问题
**问题**: `ConfigurationError: Missing required configuration`
**解决方案**:
1. 检查配置文件是否存在:
```bash
ls -la config.yaml
```
2. 验证配置格式:
```bash
python -c "
import yaml
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
print('配置文件格式正确')
"
```
3. 检查环境变量:
```bash
echo $ANTHROPIC_API_KEY
echo $OBSIDIAN_API_KEY
```
### 3. API 连接问题
**问题**: `APIError: Failed to connect to Claude/Obsidian API`
**解决方案**:
**Claude API**:
```bash
# 测试 API 密钥
curl -H "Authorization: Bearer $ANTHROPIC_API_KEY" \
-H "Content-Type: application/json" \
https://api.anthropic.com/v1/messages
```
**Obsidian API**:
```bash
# 检查 Obsidian Local REST API 插件状态
curl -k -H "Authorization: Bearer $OBSIDIAN_API_KEY" \
https://localhost:27123/
```
### 4. 依赖项问题
**问题**: `ModuleNotFoundError: No module named 'xxx'`
**解决方案**:
```bash
# 检查依赖项状态
python -m journal_organizer check-deps
# 安装缺失的依赖项
pip install -r requirements.txt
# 安装可选依赖项
pip install pyyaml aiohttp anthropic
```
### 5. 权限问题
**问题**: `PermissionError: [Errno 13] Permission denied`
**解决方案**:
```bash
# 检查文件权限
ls -la config.yaml
ls -la /path/to/obsidian/vault
# 修复权限
chmod 644 config.yaml
chmod -R 755 /path/to/obsidian/vault
```
### 6. SSL 证书问题
**问题**: `SSL: CERTIFICATE_VERIFY_FAILED`
**解决方案**:
在配置文件中禁用 SSL 验证(仅用于本地开发):
```yaml
obsidian:
rest_api:
verify_ssl: false
```
## 调试技巧
### 1. 启用详细日志
```bash
# 设置调试级别日志
python -m journal_organizer --log-level DEBUG organize
```
### 2. 使用 Python 调试器
```python
# 在 Skill 中添加断点
import pdb; pdb.set_trace()
# 或使用 ipdb(更友好的界面)
import ipdb; ipdb.set_trace()
```
### 3. 检查配置加载
```python
# 测试配置加载
from journal_organizer.main import JournalOrganizerAgent
agent = JournalOrganizerAgent("config.yaml")
print(f"配置: {agent.config}")
```
### 4. 测试单个 Skill
```python
# 单独测试 Skill
import asyncio
from journal_organizer.skills.claude_skill import ClaudeAnalyzeSkill
from journal_organizer.agent_core import CommandContext
async def test_skill():
skill = ClaudeAnalyzeSkill()
context = CommandContext(command_name="test")
result = await skill.execute(context, text="测试文本")
print(f"结果: {result}")
asyncio.run(test_skill())
```
## 性能问题
### 1. 内存使用过高
**诊断**:
```python
import psutil
import os
process = psutil.Process(os.getpid())
print(f"内存使用: {process.memory_info().rss / 1024 / 1024:.2f} MB")
```
**解决方案**:
- 检查是否有内存泄漏
- 使用 `gc.collect()` 强制垃圾回收
- 限制并发操作数量
### 2. API 调用缓慢
**诊断**:
```python
import time
start_time = time.time()
result = await api_call()
duration = time.time() - start_time
print(f"API 调用耗时: {duration:.2f} 秒")
```
**解决方案**:
- 检查网络连接
- 增加超时设置
- 使用连接池
- 实现重试机制
## 错误代码参考
| 错误代码 | 描述 | 解决方案 |
|---------|------|----------|
| CONFIG_001 | 配置文件不存在 | 创建 config.yaml 文件 |
| CONFIG_002 | 配置格式错误 | 检查 YAML/JSON 语法 |
| CONFIG_003 | 缺少必需配置项 | 添加缺失的配置项 |
| API_001 | API 密钥无效 | 检查并更新 API 密钥 |
| API_002 | API 连接超时 | 检查网络连接和服务状态 |
| API_003 | API 限流 | 减少请求频率或升级 API 计划 |
| SKILL_001 | Skill 执行失败 | 检查 Skill 输入参数和依赖项 |
| SKILL_002 | Skill 超时 | 增加超时设置或优化 Skill 逻辑 |
## 获取帮助
如果问题仍然存在:
1. **检查日志文件**: `logs/journal_organizer.log`
2. **运行诊断命令**: `python -m journal_organizer check-deps`
3. **查看详细错误**: 使用 `--log-level DEBUG`
4. **测试基本功能**: 运行简单的命令如 `list``help`
## 开发环境设置
### 推荐的开发工具
```bash
# 安装开发依赖
pip install pytest pytest-asyncio black isort mypy
# 代码格式化
black .
isort .
# 类型检查
mypy journal_organizer/
# 运行测试
pytest tests/
```
### 调试配置 (VS Code)
创建 `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Journal Organizer",
"type": "python",
"request": "launch",
"module": "journal_organizer",
"args": ["--log-level", "DEBUG", "organize"],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}"
}
]
}
```