Files
journal_organizer/DEVELOPER_GUIDE.md
T
windyboy f7e54692a9 Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern
- Implement Claude API integration for content analysis
- Add Obsidian REST API integration for vault operations
- Create conversational interface (v2.0) with natural language processing
- Add comprehensive configuration management and validation
- Include project documentation and developer guides
- Set up testing framework with unit, integration, and property tests
- Add Kiro specs for Claude API configuration and code quality improvements
- Configure project steering files for development guidelines
2025-12-31 17:55:10 +08:00

964 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发者指南
本指南说明如何扩展和自定义日记整理 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}"
}
]
}
```