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:
windyboy
2025-12-31 17:55:10 +08:00
parent 3200ad3dd5
commit f7e54692a9
67 changed files with 23088 additions and 0 deletions
+23
View File
@@ -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",
]
+467
View File
@@ -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} 格式",
)
+517
View File
@@ -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,
)