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
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Skills 模块
|
||||
"""
|
||||
|
||||
from .obsidian_skill import (
|
||||
ObsidianReadSkill,
|
||||
ObsidianWriteSkill,
|
||||
ObsidianAppendSkill,
|
||||
ObsidianListFilesSkill,
|
||||
)
|
||||
from .claude_skill import (
|
||||
ClaudeAnalyzeSkill,
|
||||
ClaudeTransformSkill,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ObsidianReadSkill",
|
||||
"ObsidianWriteSkill",
|
||||
"ObsidianAppendSkill",
|
||||
"ObsidianListFilesSkill",
|
||||
"ClaudeAnalyzeSkill",
|
||||
"ClaudeTransformSkill",
|
||||
]
|
||||
@@ -0,0 +1,467 @@
|
||||
"""
|
||||
Claude AI 集成 Skill
|
||||
负责与 Claude API 的交互和内容分析
|
||||
Enhanced with configurable API URL support
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
|
||||
try:
|
||||
from ..dependency_manager import get_dependency_manager
|
||||
from ..claude_api_client import ClaudeAPIClient
|
||||
from ..config_validation import ClaudeAPIConfig
|
||||
|
||||
# Try to import anthropic with graceful degradation
|
||||
dependency_manager = get_dependency_manager()
|
||||
Anthropic = dependency_manager.get_class_from_module('anthropic', 'Anthropic')
|
||||
AsyncAnthropic = dependency_manager.get_class_from_module('anthropic', 'AsyncAnthropic')
|
||||
except ImportError:
|
||||
# Fallback for backward compatibility
|
||||
try:
|
||||
from anthropic import Anthropic, AsyncAnthropic
|
||||
from claude_api_client import ClaudeAPIClient
|
||||
from config_validation import ClaudeAPIConfig
|
||||
except ImportError:
|
||||
AsyncAnthropic = None
|
||||
Anthropic = None
|
||||
ClaudeAPIClient = None
|
||||
ClaudeAPIConfig = None
|
||||
|
||||
from ..agent_core import Skill, SkillType, SkillResult, CommandContext
|
||||
from ..api_response_validation import validate_api_response
|
||||
from ..error_handling import (
|
||||
ErrorHandler,
|
||||
APIError,
|
||||
ConfigurationError,
|
||||
ValidationError,
|
||||
ErrorContext,
|
||||
get_error_handler,
|
||||
)
|
||||
from ..input_validation import command_input_validator
|
||||
|
||||
|
||||
class ClaudeAnalyzeSkill(Skill):
|
||||
"""Claude 日记分析 Skill"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
name="claude_analyze",
|
||||
skill_type=SkillType.ANALYZE,
|
||||
description="使用 Claude 分析日记内容并提取关键信息",
|
||||
)
|
||||
self.client: Optional[ClaudeAPIClient] = None
|
||||
self.error_handler = get_error_handler()
|
||||
|
||||
def _get_analysis_prompt(self, categories: List[str]) -> str:
|
||||
"""
|
||||
获取分析 prompt
|
||||
|
||||
Args:
|
||||
categories: 分类列表
|
||||
|
||||
Returns:
|
||||
prompt 字符串
|
||||
"""
|
||||
categories_str: str = "、".join(categories) if categories else "技术学习、项目管理、个人成长"
|
||||
|
||||
return f"""你是一个专业的日记分析助手。请分析以下日记内容,并按照指定的格式提取关键信息。
|
||||
|
||||
分析要求:
|
||||
1. 提取经验和见解(Experiences):日记中提到的重要经验、发现或见解
|
||||
2. 提取学到的知识(Lessons Learned):具体学到的知识点、最佳实践或原则
|
||||
3. 提取待办事项(Action Items):需要采取行动的任务或改进项
|
||||
4. 提取问题和挑战(Problems):遇到的问题、挑战或障碍
|
||||
5. 提取成就和进展(Achievements):完成的工作、达成的目标或进展
|
||||
6. 提取改进建议(Improvements):可以改进的方向或优化建议
|
||||
|
||||
分类类别:{categories_str}
|
||||
|
||||
请以 JSON 格式返回结果,结构如下:
|
||||
{{
|
||||
"experiences": [
|
||||
{{
|
||||
"title": "标题",
|
||||
"content": "详细内容",
|
||||
"category": "分类",
|
||||
"priority": "high/medium/low"
|
||||
}}
|
||||
],
|
||||
"lessons_learned": [
|
||||
{{
|
||||
"lesson": "学到的内容",
|
||||
"context": "背景信息",
|
||||
"application": "如何应用"
|
||||
}}
|
||||
],
|
||||
"action_items": [
|
||||
{{
|
||||
"task": "任务描述",
|
||||
"priority": "high/medium/low",
|
||||
"deadline": "建议截止日期",
|
||||
"status": "new"
|
||||
}}
|
||||
],
|
||||
"problems": [
|
||||
{{
|
||||
"problem": "问题描述",
|
||||
"impact": "影响程度",
|
||||
"proposed_solution": "建议方案"
|
||||
}}
|
||||
],
|
||||
"achievements": [
|
||||
{{
|
||||
"achievement": "成就描述",
|
||||
"significance": "重要性",
|
||||
"evidence": "证据或细节"
|
||||
}}
|
||||
],
|
||||
"improvements": [
|
||||
{{
|
||||
"area": "改进领域",
|
||||
"current_state": "当前状态",
|
||||
"suggested_change": "建议改进",
|
||||
"expected_benefit": "预期收益"
|
||||
}}
|
||||
],
|
||||
"summary": "日记的总体总结"
|
||||
}}
|
||||
|
||||
日记内容:
|
||||
"""
|
||||
|
||||
async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult:
|
||||
"""
|
||||
分析日记内容
|
||||
|
||||
Args:
|
||||
context: 命令执行上下文
|
||||
**kwargs: 包含以下参数
|
||||
- journal_content: 日记内容
|
||||
- api_key: Claude API 密钥
|
||||
- model: 模型名称(默认 claude-3-5-sonnet-20241022)
|
||||
- categories: 分类列表(可选)
|
||||
|
||||
Returns:
|
||||
SkillResult: 包含分析结果的结果
|
||||
"""
|
||||
try:
|
||||
return await self._execute_analyze(context, **kwargs)
|
||||
except (ValidationError, ConfigurationError, APIError) as e:
|
||||
error_context = ErrorContext(
|
||||
component="claude_analyze",
|
||||
operation="analyze_journal",
|
||||
user_message="Failed to analyze journal content with Claude AI",
|
||||
technical_details=kwargs,
|
||||
)
|
||||
error_response = self.error_handler.handle_error(e, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
except Exception as e:
|
||||
api_error = APIError(
|
||||
message=f"Unexpected error analyzing journal: {str(e)}",
|
||||
api_name="claude",
|
||||
cause=e,
|
||||
)
|
||||
error_context = ErrorContext(
|
||||
component="claude_analyze",
|
||||
operation="analyze_journal",
|
||||
user_message="An unexpected error occurred while analyzing the journal",
|
||||
)
|
||||
error_response = self.error_handler.handle_error(api_error, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
|
||||
async def _execute_analyze(
|
||||
self, context: CommandContext, **kwargs: Any
|
||||
) -> SkillResult:
|
||||
"""Internal method that performs the actual analysis"""
|
||||
# Validate input parameters
|
||||
validated_kwargs = command_input_validator.validate_skill_input(
|
||||
'claude_analyze', kwargs
|
||||
)
|
||||
|
||||
journal_content: str = validated_kwargs["journal_content"]
|
||||
|
||||
# Handle both legacy and new configuration formats
|
||||
if 'claude_config' in validated_kwargs:
|
||||
# New format: ClaudeAPIConfig object
|
||||
claude_config = validated_kwargs['claude_config']
|
||||
if not isinstance(claude_config, ClaudeAPIConfig):
|
||||
raise ConfigurationError(
|
||||
message="claude_config must be a ClaudeAPIConfig instance",
|
||||
config_key="claude_config"
|
||||
)
|
||||
else:
|
||||
# Legacy format: individual parameters
|
||||
api_key: str = validated_kwargs["api_key"]
|
||||
model: str = validated_kwargs.get("model", "claude-3-5-sonnet-20241022")
|
||||
api_url: str = validated_kwargs.get("api_url", "https://api.anthropic.com")
|
||||
max_tokens: int = validated_kwargs.get("max_tokens", 4096)
|
||||
temperature: float = validated_kwargs.get("temperature", 0.7)
|
||||
|
||||
# Create ClaudeAPIConfig from legacy parameters
|
||||
claude_config = ClaudeAPIConfig(
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
api_url=api_url,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
categories: List[str] = validated_kwargs.get("categories", [])
|
||||
|
||||
if not ClaudeAPIClient:
|
||||
raise ConfigurationError(
|
||||
message="ClaudeAPIClient is not available. Please check your installation.",
|
||||
config_key="claude_api_client",
|
||||
)
|
||||
|
||||
# Initialize enhanced client
|
||||
try:
|
||||
client = ClaudeAPIClient(claude_config)
|
||||
except Exception as e:
|
||||
raise ConfigurationError(
|
||||
message=f"Failed to initialize Claude API client: {str(e)}",
|
||||
config_key="claude_client_init",
|
||||
cause=e
|
||||
)
|
||||
|
||||
# Construct prompt
|
||||
system_prompt: str = self._get_analysis_prompt(categories)
|
||||
user_message: str = journal_content
|
||||
|
||||
self.logger.info(f"开始分析日记,模型: {claude_config.model}, API URL: {claude_config.api_url}")
|
||||
|
||||
try:
|
||||
# Call Claude API using enhanced client
|
||||
message = await client.create_message(
|
||||
messages=[
|
||||
{"role": "user", "content": f"{system_prompt}{user_message}"}
|
||||
]
|
||||
)
|
||||
except APIError:
|
||||
# Re-raise APIError as-is (already properly formatted)
|
||||
raise
|
||||
except Exception as e:
|
||||
raise APIError(
|
||||
message=f"Claude API call failed: {str(e)}",
|
||||
api_name="claude",
|
||||
cause=e
|
||||
)
|
||||
|
||||
# Validate API response
|
||||
validated_response = validate_api_response(
|
||||
message.model_dump() if hasattr(message, 'model_dump') else message.__dict__,
|
||||
"claude",
|
||||
"analyze"
|
||||
)
|
||||
|
||||
# Parse response
|
||||
response_text: str = message.content[0].text
|
||||
|
||||
# Try to extract JSON
|
||||
try:
|
||||
# Find JSON block
|
||||
json_start: int = response_text.find("{")
|
||||
json_end: int = response_text.rfind("}") + 1
|
||||
|
||||
if json_start >= 0 and json_end > json_start:
|
||||
json_str: str = response_text[json_start:json_end]
|
||||
analysis_result: Dict[str, Any] = json.loads(json_str)
|
||||
else:
|
||||
# If no JSON found, return raw text
|
||||
analysis_result = {
|
||||
"raw_response": response_text,
|
||||
"parse_error": "无法解析 JSON 格式",
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.warning(f"JSON 解析失败: {str(e)}")
|
||||
analysis_result = {"raw_response": response_text, "parse_error": str(e)}
|
||||
|
||||
return SkillResult(
|
||||
success=True,
|
||||
data={
|
||||
"analysis": analysis_result,
|
||||
"model": claude_config.model,
|
||||
"api_url": claude_config.api_url,
|
||||
"analyzed_at": datetime.now().isoformat(),
|
||||
"journal_length": len(journal_content),
|
||||
"api_response": validated_response,
|
||||
"client_info": client.get_client_info(),
|
||||
},
|
||||
message="成功分析日记内容",
|
||||
)
|
||||
|
||||
|
||||
class ClaudeTransformSkill(Skill):
|
||||
"""Claude 内容转换 Skill"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
name="claude_transform",
|
||||
skill_type=SkillType.TRANSFORM,
|
||||
description="使用 Claude 转换和格式化内容",
|
||||
)
|
||||
self.error_handler = get_error_handler()
|
||||
|
||||
async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult:
|
||||
"""
|
||||
转换内容格式
|
||||
|
||||
Args:
|
||||
context: 命令执行上下文
|
||||
**kwargs: 包含以下参数
|
||||
- content: 要转换的内容
|
||||
- transform_type: 转换类型(markdown, html, summary 等)
|
||||
- api_key: Claude API 密钥
|
||||
- model: 模型名称(可选)
|
||||
|
||||
Returns:
|
||||
SkillResult: 包含转换结果的结果
|
||||
"""
|
||||
try:
|
||||
return await self._execute_transform(context, **kwargs)
|
||||
except (ValidationError, ConfigurationError, APIError) as e:
|
||||
error_context = ErrorContext(
|
||||
component="claude_transform",
|
||||
operation="transform_content",
|
||||
user_message="Failed to transform content with Claude AI",
|
||||
technical_details=kwargs,
|
||||
)
|
||||
error_response = self.error_handler.handle_error(e, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
except Exception as e:
|
||||
api_error = APIError(
|
||||
message=f"Unexpected error transforming content: {str(e)}",
|
||||
api_name="claude",
|
||||
cause=e,
|
||||
)
|
||||
error_context = ErrorContext(
|
||||
component="claude_transform",
|
||||
operation="transform_content",
|
||||
user_message="An unexpected error occurred while transforming content",
|
||||
)
|
||||
error_response = self.error_handler.handle_error(api_error, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
|
||||
async def _execute_transform(
|
||||
self, context: CommandContext, **kwargs: Any
|
||||
) -> SkillResult:
|
||||
"""Internal method that performs the actual transformation"""
|
||||
content: Optional[str] = kwargs.get("content")
|
||||
transform_type: str = kwargs.get("transform_type", "markdown")
|
||||
|
||||
if not content:
|
||||
raise ValidationError(
|
||||
message="Content is required for transformation",
|
||||
field_name="content",
|
||||
validation_rule="non_empty",
|
||||
)
|
||||
|
||||
# Handle both legacy and new configuration formats
|
||||
if 'claude_config' in kwargs:
|
||||
# New format: ClaudeAPIConfig object
|
||||
claude_config = kwargs['claude_config']
|
||||
if not isinstance(claude_config, ClaudeAPIConfig):
|
||||
raise ConfigurationError(
|
||||
message="claude_config must be a ClaudeAPIConfig instance",
|
||||
config_key="claude_config"
|
||||
)
|
||||
else:
|
||||
# Legacy format: individual parameters
|
||||
api_key: Optional[str] = kwargs.get("api_key")
|
||||
model: str = kwargs.get("model", "claude-3-5-sonnet-20241022")
|
||||
api_url: str = kwargs.get("api_url", "https://api.anthropic.com")
|
||||
max_tokens: int = kwargs.get("max_tokens", 4096)
|
||||
temperature: float = kwargs.get("temperature", 0.7)
|
||||
|
||||
if not api_key:
|
||||
raise ConfigurationError(
|
||||
message="Claude API key is required", config_key="api_key"
|
||||
)
|
||||
|
||||
# Create ClaudeAPIConfig from legacy parameters
|
||||
claude_config = ClaudeAPIConfig(
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
api_url=api_url,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
if not ClaudeAPIClient:
|
||||
raise ConfigurationError(
|
||||
message="ClaudeAPIClient is not available. Please check your installation.",
|
||||
config_key="claude_api_client",
|
||||
)
|
||||
|
||||
# Initialize enhanced client
|
||||
try:
|
||||
client = ClaudeAPIClient(claude_config)
|
||||
except Exception as e:
|
||||
raise ConfigurationError(
|
||||
message=f"Failed to initialize Claude API client: {str(e)}",
|
||||
config_key="claude_client_init",
|
||||
cause=e
|
||||
)
|
||||
|
||||
# Build prompt based on transformation type
|
||||
prompts: Dict[str, str] = {
|
||||
"markdown": "请将以下内容转换为格式良好的 Markdown 格式:",
|
||||
"html": "请将以下内容转换为 HTML 格式:",
|
||||
"summary": "请为以下内容生成一个简洁的总结:",
|
||||
"outline": "请为以下内容生成一个结构化的大纲:",
|
||||
"checklist": "请将以下内容转换为检查清单格式:",
|
||||
}
|
||||
|
||||
system_prompt: str = prompts.get(transform_type, "请转换以下内容:")
|
||||
|
||||
self.logger.info(f"转换内容,类型: {transform_type}, 模型: {claude_config.model}")
|
||||
|
||||
try:
|
||||
message = await client.create_message(
|
||||
messages=[{"role": "user", "content": f"{system_prompt}\n\n{content}"}]
|
||||
)
|
||||
except APIError:
|
||||
# Re-raise APIError as-is (already properly formatted)
|
||||
raise
|
||||
except Exception as e:
|
||||
raise APIError(
|
||||
message=f"Claude API call failed: {str(e)}",
|
||||
api_name="claude",
|
||||
cause=e
|
||||
)
|
||||
|
||||
transformed_content: str = message.content[0].text
|
||||
|
||||
return SkillResult(
|
||||
success=True,
|
||||
data={
|
||||
"original_length": len(content),
|
||||
"transformed_length": len(transformed_content),
|
||||
"transform_type": transform_type,
|
||||
"transformed_content": transformed_content,
|
||||
"transformed_at": datetime.now().isoformat(),
|
||||
"model": claude_config.model,
|
||||
"api_url": claude_config.api_url,
|
||||
"client_info": client.get_client_info(),
|
||||
},
|
||||
message=f"成功转换内容为 {transform_type} 格式",
|
||||
)
|
||||
@@ -0,0 +1,517 @@
|
||||
"""
|
||||
Obsidian 集成 Skill
|
||||
负责与 Obsidian Local REST API 的交互
|
||||
"""
|
||||
|
||||
import ssl
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List, Union, AsyncGenerator
|
||||
|
||||
from ..agent_core import Skill, SkillType, SkillResult, CommandContext
|
||||
from ..api_response_validation import validate_api_response
|
||||
from ..dependency_manager import get_dependency_manager
|
||||
from ..error_handling import (
|
||||
APIError,
|
||||
ConfigurationError,
|
||||
ValidationError,
|
||||
ErrorContext,
|
||||
get_error_handler,
|
||||
)
|
||||
from ..input_validation import command_input_validator
|
||||
|
||||
# Try to import aiohttp with graceful degradation
|
||||
dependency_manager = get_dependency_manager()
|
||||
aiohttp = dependency_manager.get_module('aiohttp')
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def obsidian_api_client(
|
||||
api_url: str, api_key: str
|
||||
) -> AsyncGenerator[Any, None]:
|
||||
"""
|
||||
Async context manager for Obsidian API client
|
||||
|
||||
Args:
|
||||
api_url: Obsidian API URL
|
||||
api_key: API key for authentication
|
||||
|
||||
Yields:
|
||||
Configured aiohttp ClientSession
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If aiohttp is not available
|
||||
"""
|
||||
if aiohttp is None:
|
||||
raise ConfigurationError(
|
||||
message="aiohttp library is not installed. Please install it with: pip install aiohttp>=3.9.0",
|
||||
config_key="aiohttp_dependency",
|
||||
)
|
||||
|
||||
# Create SSL context (skip certificate verification for local development)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
# Configure headers
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Create session with proper configuration
|
||||
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
||||
async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
|
||||
try:
|
||||
yield session
|
||||
except Exception as e:
|
||||
# Log error but let it propagate
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("obsidian_api_client")
|
||||
logger.error(f"Error in Obsidian API client: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
class ObsidianReadSkill(Skill):
|
||||
"""读取 Obsidian 笔记 Skill"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
name="obsidian_read",
|
||||
skill_type=SkillType.READ,
|
||||
description="从 Obsidian 读取笔记内容",
|
||||
)
|
||||
self.error_handler = get_error_handler()
|
||||
|
||||
async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult:
|
||||
"""
|
||||
读取 Obsidian 笔记
|
||||
|
||||
Args:
|
||||
context: 命令执行上下文
|
||||
**kwargs: 包含以下参数
|
||||
- file_path: 笔记文件路径(相对于 vault)
|
||||
- vault_path: vault 路径
|
||||
- api_url: API URL
|
||||
- api_key: API 密钥
|
||||
|
||||
Returns:
|
||||
SkillResult: 包含笔记内容的结果
|
||||
"""
|
||||
try:
|
||||
return await self._execute_read(context, **kwargs)
|
||||
except (ValidationError, ConfigurationError, APIError) as e:
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_read",
|
||||
operation="read_note",
|
||||
user_message="Failed to read note from Obsidian",
|
||||
technical_details=kwargs,
|
||||
)
|
||||
error_response = self.error_handler.handle_error(e, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
except Exception as e:
|
||||
# Handle any unexpected errors
|
||||
api_error = APIError(
|
||||
message=f"Unexpected error reading note: {str(e)}",
|
||||
api_name="obsidian",
|
||||
cause=e,
|
||||
)
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_read",
|
||||
operation="read_note",
|
||||
user_message="An unexpected error occurred while reading the note",
|
||||
)
|
||||
error_response = self.error_handler.handle_error(api_error, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
|
||||
async def _execute_read(
|
||||
self, context: CommandContext, **kwargs: Any
|
||||
) -> SkillResult:
|
||||
"""Internal method that performs the actual read operation"""
|
||||
# Validate input parameters
|
||||
validated_kwargs = command_input_validator.validate_skill_input(
|
||||
'obsidian_read', kwargs
|
||||
)
|
||||
|
||||
file_path: str = validated_kwargs["file_path"]
|
||||
api_url: str = validated_kwargs.get("api_url", "https://localhost:27123")
|
||||
api_key: str = validated_kwargs["api_key"]
|
||||
|
||||
# Use async context manager for API client
|
||||
async with obsidian_api_client(api_url, api_key) as session:
|
||||
# Build API URL
|
||||
api_endpoint: str = f"{api_url}/vault/{file_path}"
|
||||
|
||||
self.logger.info(f"读取笔记: {file_path}")
|
||||
|
||||
async with session.get(api_endpoint) as response:
|
||||
if response.status == 200:
|
||||
content: str = await response.text()
|
||||
|
||||
# Validate API response
|
||||
validated_response = validate_api_response(
|
||||
content, "obsidian", "read"
|
||||
)
|
||||
|
||||
return SkillResult(
|
||||
success=True,
|
||||
data={
|
||||
"file_path": file_path,
|
||||
"content": validated_response["content"],
|
||||
"size": validated_response["length"],
|
||||
"read_at": datetime.now().isoformat(),
|
||||
},
|
||||
message=f"成功读取笔记: {file_path}",
|
||||
)
|
||||
elif response.status == 404:
|
||||
raise APIError(
|
||||
message=f"Note file not found: {file_path}",
|
||||
api_name="obsidian",
|
||||
status_code=response.status,
|
||||
)
|
||||
else:
|
||||
error_text: str = await response.text()
|
||||
raise APIError(
|
||||
message=f"Failed to read note: {error_text}",
|
||||
api_name="obsidian",
|
||||
status_code=response.status,
|
||||
response_data=error_text,
|
||||
)
|
||||
|
||||
|
||||
class ObsidianWriteSkill(Skill):
|
||||
"""写入 Obsidian 笔记 Skill"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
name="obsidian_write",
|
||||
skill_type=SkillType.WRITE,
|
||||
description="向 Obsidian 写入或更新笔记",
|
||||
)
|
||||
self.error_handler = get_error_handler()
|
||||
|
||||
async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult:
|
||||
"""
|
||||
写入或创建 Obsidian 笔记
|
||||
|
||||
Args:
|
||||
context: 命令执行上下文
|
||||
**kwargs: 包含以下参数
|
||||
- file_path: 笔记文件路径(相对于 vault)
|
||||
- content: 要写入的内容
|
||||
- overwrite: 是否覆盖现有内容(默认 False)
|
||||
- api_url: API URL
|
||||
- api_key: API 密钥
|
||||
|
||||
Returns:
|
||||
SkillResult: 执行结果
|
||||
"""
|
||||
try:
|
||||
return await self._execute_write(context, **kwargs)
|
||||
except (ValidationError, ConfigurationError, APIError) as e:
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_write",
|
||||
operation="write_note",
|
||||
user_message="Failed to write note to Obsidian",
|
||||
technical_details=kwargs,
|
||||
)
|
||||
error_response = self.error_handler.handle_error(e, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
except Exception as e:
|
||||
api_error = APIError(
|
||||
message=f"Unexpected error writing note: {str(e)}",
|
||||
api_name="obsidian",
|
||||
cause=e,
|
||||
)
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_write",
|
||||
operation="write_note",
|
||||
user_message="An unexpected error occurred while writing the note",
|
||||
)
|
||||
error_response = self.error_handler.handle_error(api_error, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
|
||||
async def _execute_write(
|
||||
self, context: CommandContext, **kwargs: Any
|
||||
) -> SkillResult:
|
||||
"""Internal method that performs the actual write operation"""
|
||||
# Validate input parameters
|
||||
validated_kwargs = command_input_validator.validate_skill_input(
|
||||
'obsidian_write', kwargs
|
||||
)
|
||||
|
||||
file_path: str = validated_kwargs["file_path"]
|
||||
content: str = validated_kwargs["content"]
|
||||
overwrite: bool = validated_kwargs.get("overwrite", False)
|
||||
api_url: str = validated_kwargs.get("api_url", "https://localhost:27123")
|
||||
api_key: str = validated_kwargs["api_key"]
|
||||
|
||||
async with obsidian_api_client(api_url, api_key) as session:
|
||||
api_endpoint: str = f"{api_url}/vault/{file_path}"
|
||||
|
||||
payload: Dict[str, Union[str, bool]] = {
|
||||
"content": content,
|
||||
"overwrite": overwrite,
|
||||
}
|
||||
|
||||
self.logger.info(f"写入笔记: {file_path}")
|
||||
|
||||
async with session.post(api_endpoint, json=payload) as response:
|
||||
if response.status in [200, 201]:
|
||||
# Validate API response
|
||||
response_text = await response.text()
|
||||
validated_response = validate_api_response(
|
||||
response_text, "obsidian", "write"
|
||||
)
|
||||
|
||||
return SkillResult(
|
||||
success=True,
|
||||
data={
|
||||
"file_path": file_path,
|
||||
"size": len(content) if content else 0,
|
||||
"written_at": datetime.now().isoformat(),
|
||||
"response": validated_response,
|
||||
},
|
||||
message=f"成功写入笔记: {file_path}",
|
||||
)
|
||||
else:
|
||||
error_text: str = await response.text()
|
||||
raise APIError(
|
||||
message=f"Failed to write note: {error_text}",
|
||||
api_name="obsidian",
|
||||
status_code=response.status,
|
||||
response_data=error_text,
|
||||
)
|
||||
|
||||
|
||||
class ObsidianAppendSkill(Skill):
|
||||
"""追加内容到 Obsidian 笔记 Skill"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
name="obsidian_append",
|
||||
skill_type=SkillType.WRITE,
|
||||
description="向 Obsidian 笔记追加内容",
|
||||
)
|
||||
self.error_handler = get_error_handler()
|
||||
|
||||
async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult:
|
||||
"""
|
||||
向笔记追加内容
|
||||
|
||||
Args:
|
||||
context: 命令执行上下文
|
||||
**kwargs: 包含以下参数
|
||||
- file_path: 笔记文件路径
|
||||
- content: 要追加的内容
|
||||
- api_url: API URL
|
||||
- api_key: API 密钥
|
||||
|
||||
Returns:
|
||||
SkillResult: 执行结果
|
||||
"""
|
||||
try:
|
||||
return await self._execute_append(context, **kwargs)
|
||||
except (ValidationError, ConfigurationError, APIError) as e:
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_append",
|
||||
operation="append_note",
|
||||
user_message="Failed to append content to Obsidian note",
|
||||
technical_details=kwargs,
|
||||
)
|
||||
error_response = self.error_handler.handle_error(e, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
except Exception as e:
|
||||
api_error = APIError(
|
||||
message=f"Unexpected error appending to note: {str(e)}",
|
||||
api_name="obsidian",
|
||||
cause=e,
|
||||
)
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_append",
|
||||
operation="append_note",
|
||||
user_message="An unexpected error occurred while appending to the note",
|
||||
)
|
||||
error_response = self.error_handler.handle_error(api_error, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
|
||||
async def _execute_append(
|
||||
self, context: CommandContext, **kwargs: Any
|
||||
) -> SkillResult:
|
||||
"""Internal method that performs the actual append operation"""
|
||||
file_path: Optional[str] = kwargs.get("file_path")
|
||||
content: Optional[str] = kwargs.get("content")
|
||||
api_url: str = kwargs.get("api_url", "https://localhost:27123")
|
||||
api_key: Optional[str] = kwargs.get("api_key")
|
||||
|
||||
if not file_path:
|
||||
raise ValidationError(
|
||||
message="File path is required for appending to notes",
|
||||
field_name="file_path",
|
||||
validation_rule="non_empty",
|
||||
)
|
||||
|
||||
if content is None:
|
||||
raise ValidationError(
|
||||
message="Content is required for appending to notes",
|
||||
field_name="content",
|
||||
validation_rule="not_none",
|
||||
)
|
||||
|
||||
if not api_key:
|
||||
raise ConfigurationError(
|
||||
message="Obsidian API key is required", config_key="api_key"
|
||||
)
|
||||
|
||||
async with obsidian_api_client(api_url, api_key) as session:
|
||||
api_endpoint: str = f"{api_url}/vault/{file_path}"
|
||||
|
||||
payload: Dict[str, Union[str, bool]] = {"content": content, "append": True}
|
||||
|
||||
self.logger.info(f"追加内容到笔记: {file_path}")
|
||||
|
||||
async with session.post(api_endpoint, json=payload) as response:
|
||||
if response.status in [200, 201]:
|
||||
return SkillResult(
|
||||
success=True,
|
||||
data={
|
||||
"file_path": file_path,
|
||||
"appended_size": len(content) if content else 0,
|
||||
"appended_at": datetime.now().isoformat(),
|
||||
},
|
||||
message=f"成功追加内容到笔记: {file_path}",
|
||||
)
|
||||
else:
|
||||
error_text: str = await response.text()
|
||||
raise APIError(
|
||||
message=f"Failed to append to note: {error_text}",
|
||||
api_name="obsidian",
|
||||
status_code=response.status,
|
||||
response_data=error_text,
|
||||
)
|
||||
|
||||
|
||||
class ObsidianListFilesSkill(Skill):
|
||||
"""列出 Obsidian 文件 Skill"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
name="obsidian_list_files",
|
||||
skill_type=SkillType.READ,
|
||||
description="列出 Obsidian vault 中的文件",
|
||||
)
|
||||
self.error_handler = get_error_handler()
|
||||
|
||||
async def execute(self, context: CommandContext, **kwargs: Any) -> SkillResult:
|
||||
"""
|
||||
列出指定文件夹中的文件
|
||||
|
||||
Args:
|
||||
context: 命令执行上下文
|
||||
**kwargs: 包含以下参数
|
||||
- folder_path: 文件夹路径(可选)
|
||||
- api_url: API URL
|
||||
- api_key: API 密钥
|
||||
|
||||
Returns:
|
||||
SkillResult: 包含文件列表的结果
|
||||
"""
|
||||
try:
|
||||
return await self._execute_list(context, **kwargs)
|
||||
except (ValidationError, ConfigurationError, APIError) as e:
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_list_files",
|
||||
operation="list_files",
|
||||
user_message="Failed to list files from Obsidian vault",
|
||||
technical_details=kwargs,
|
||||
)
|
||||
error_response = self.error_handler.handle_error(e, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
except Exception as e:
|
||||
api_error = APIError(
|
||||
message=f"Unexpected error listing files: {str(e)}",
|
||||
api_name="obsidian",
|
||||
cause=e,
|
||||
)
|
||||
error_context = ErrorContext(
|
||||
component="obsidian_list_files",
|
||||
operation="list_files",
|
||||
user_message="An unexpected error occurred while listing files",
|
||||
)
|
||||
error_response = self.error_handler.handle_error(api_error, error_context)
|
||||
return SkillResult(
|
||||
success=error_response["success"],
|
||||
error=error_response["error"],
|
||||
message=error_response["message"],
|
||||
)
|
||||
|
||||
async def _execute_list(
|
||||
self, context: CommandContext, **kwargs: Any
|
||||
) -> SkillResult:
|
||||
"""Internal method that performs the actual list operation"""
|
||||
folder_path: str = kwargs.get("folder_path", "")
|
||||
api_url: str = kwargs.get("api_url", "https://localhost:27123")
|
||||
api_key: Optional[str] = kwargs.get("api_key")
|
||||
|
||||
if not api_key:
|
||||
raise ConfigurationError(
|
||||
message="Obsidian API key is required", config_key="api_key"
|
||||
)
|
||||
|
||||
async with obsidian_api_client(api_url, api_key) as session:
|
||||
api_endpoint: str = f"{api_url}/vault/list"
|
||||
params: Dict[str, str] = {}
|
||||
if folder_path:
|
||||
params["path"] = folder_path
|
||||
|
||||
self.logger.info(f"列出文件: {folder_path or 'root'}")
|
||||
|
||||
async with session.get(api_endpoint, params=params) as response:
|
||||
if response.status == 200:
|
||||
files: Union[List[Any], Dict[str, Any]] = await response.json()
|
||||
return SkillResult(
|
||||
success=True,
|
||||
data={
|
||||
"folder_path": folder_path,
|
||||
"files": files,
|
||||
"count": len(files) if isinstance(files, list) else 0,
|
||||
},
|
||||
message=f"成功列出文件",
|
||||
)
|
||||
else:
|
||||
error_text: str = await response.text()
|
||||
raise APIError(
|
||||
message=f"Failed to list files: {error_text}",
|
||||
api_name="obsidian",
|
||||
status_code=response.status,
|
||||
response_data=error_text,
|
||||
)
|
||||
Reference in New Issue
Block a user