- 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
423 lines
17 KiB
Python
423 lines
17 KiB
Python
"""
|
||
日记整理命令
|
||
负责协调各个 Skill 完成日记的分析和整理
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Dict, Any, Optional, List
|
||
|
||
# Handle imports with both relative and absolute paths
|
||
try:
|
||
from ..agent_core import Command, SkillResult, CommandContext
|
||
from ..date_validation import validate_date_input
|
||
from ..input_validation import command_input_validator
|
||
from ..config_validation import ClaudeAPIConfig
|
||
from ..skills.claude_skill import ClaudeAnalyzeSkill, ClaudeTransformSkill
|
||
from ..skills.obsidian_skill import (
|
||
ObsidianReadSkill,
|
||
ObsidianWriteSkill,
|
||
ObsidianAppendSkill,
|
||
ObsidianListFilesSkill,
|
||
)
|
||
except ImportError:
|
||
# Fallback to absolute imports when running as script
|
||
from agent_core import Command, SkillResult, CommandContext
|
||
from date_validation import validate_date_input
|
||
from input_validation import command_input_validator
|
||
from config_validation import ClaudeAPIConfig
|
||
from skills.claude_skill import ClaudeAnalyzeSkill, ClaudeTransformSkill
|
||
from skills.obsidian_skill import (
|
||
ObsidianReadSkill,
|
||
ObsidianWriteSkill,
|
||
ObsidianAppendSkill,
|
||
ObsidianListFilesSkill,
|
||
)
|
||
|
||
|
||
class OrganizeCommand(Command):
|
||
"""日记整理命令"""
|
||
|
||
def __init__(self) -> None:
|
||
super().__init__(
|
||
name="organize",
|
||
description="分析和整理日记内容,提取经验和要点",
|
||
aliases=["org", "organize-journal"],
|
||
)
|
||
|
||
# 注册 Skills
|
||
self.register_skill(ObsidianReadSkill())
|
||
self.register_skill(ObsidianWriteSkill())
|
||
self.register_skill(ObsidianAppendSkill())
|
||
self.register_skill(ClaudeAnalyzeSkill())
|
||
self.register_skill(ClaudeTransformSkill())
|
||
|
||
async def execute(self, context: CommandContext) -> SkillResult:
|
||
"""
|
||
执行日记整理命令
|
||
|
||
Args:
|
||
context: 命令执行上下文
|
||
- args:
|
||
- date: 日期(格式: YYYY-MM-DD,默认今天)
|
||
- vault_path: Obsidian vault 路径
|
||
- daily_folder: 日记文件夹(默认: Daily)
|
||
- config: 系统配置
|
||
|
||
Returns:
|
||
SkillResult: 执行结果
|
||
"""
|
||
try:
|
||
# Validate and sanitize input arguments
|
||
validated_args = command_input_validator.validate_organize_command_input(
|
||
context.args
|
||
)
|
||
|
||
# 获取参数
|
||
date_str: Optional[str] = validated_args.get("date")
|
||
vault_path: Optional[str] = validated_args.get("vault_path")
|
||
daily_folder: str = validated_args.get("daily_folder", "Daily")
|
||
|
||
# 从配置中获取信息
|
||
config: Dict[str, Any] = context.config or {}
|
||
obsidian_config: Dict[str, Any] = config.get("obsidian", {})
|
||
claude_config: Dict[str, Any] = config.get("claude", {})
|
||
output_config: Dict[str, Any] = config.get("output", {})
|
||
analysis_config: Dict[str, Any] = config.get("analysis", {})
|
||
|
||
# 如果没有提供日期,使用今天
|
||
if not date_str:
|
||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||
else:
|
||
# Validate the provided date
|
||
validated_date = validate_date_input(
|
||
date_str,
|
||
field_name="date",
|
||
required=True,
|
||
format_hint="iso_date"
|
||
)
|
||
date_str = validated_date.strftime("%Y-%m-%d")
|
||
|
||
# 获取必要的配置
|
||
api_url: str = obsidian_config.get("rest_api", {}).get(
|
||
"url", "https://localhost:27123"
|
||
)
|
||
api_key: Optional[str] = obsidian_config.get("rest_api", {}).get("api_key")
|
||
|
||
# Create enhanced Claude API configuration
|
||
try:
|
||
claude_api_config = ClaudeAPIConfig(**claude_config)
|
||
except Exception as e:
|
||
return SkillResult(
|
||
success=False,
|
||
error=f"Claude API 配置无效: {str(e)}",
|
||
message="请检查 Claude API 配置",
|
||
)
|
||
|
||
if not api_key or not claude_api_config.api_key:
|
||
return SkillResult(
|
||
success=False,
|
||
error="缺少必要的配置",
|
||
message="请配置 Obsidian API 密钥和 Claude API 密钥",
|
||
)
|
||
|
||
self.logger.info(f"开始整理日期 {date_str} 的日记")
|
||
|
||
# 步骤 1: 读取日记
|
||
daily_note_path = str(Path(daily_folder) / f"{date_str}.md")
|
||
|
||
self.logger.info(f"步骤 1: 读取日记 {daily_note_path}")
|
||
read_skill = self.skills["obsidian_read"]
|
||
read_result: SkillResult = await read_skill.execute(
|
||
context,
|
||
file_path=daily_note_path,
|
||
vault_path=vault_path,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
)
|
||
|
||
if not read_result.success:
|
||
return read_result
|
||
|
||
journal_content: str = read_result.data["content"]
|
||
|
||
# 步骤 2: 使用 Claude 分析日记
|
||
self.logger.info("步骤 2: 使用 Claude 分析日记内容")
|
||
analyze_skill = self.skills["claude_analyze"]
|
||
categories: List[str] = analysis_config.get("categories", [])
|
||
|
||
analyze_result: SkillResult = await analyze_skill.execute(
|
||
context,
|
||
journal_content=journal_content,
|
||
claude_config=claude_api_config,
|
||
categories=categories,
|
||
)
|
||
|
||
if not analyze_result.success:
|
||
return analyze_result
|
||
|
||
analysis_data: Dict[str, Any] = analyze_result.data["analysis"]
|
||
|
||
# 步骤 3: 整理分析结果到各个位置
|
||
self.logger.info("步骤 3: 整理分析结果")
|
||
|
||
write_results: Dict[str, SkillResult] = {}
|
||
write_skill = self.skills["obsidian_write"]
|
||
|
||
# 处理经验
|
||
if "experiences" in analysis_data and analysis_data["experiences"]:
|
||
experiences_content: str = self._format_experiences(
|
||
analysis_data["experiences"], date_str
|
||
)
|
||
experiences_path = str(
|
||
Path(
|
||
output_config.get("experiences_folder", "Knowledge/Experiences")
|
||
)
|
||
/ f"{date_str}.md"
|
||
)
|
||
|
||
exp_result: SkillResult = await write_skill.execute(
|
||
context,
|
||
file_path=experiences_path,
|
||
content=experiences_content,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
overwrite=True,
|
||
)
|
||
write_results["experiences"] = exp_result
|
||
|
||
# 处理经验教训
|
||
if "lessons_learned" in analysis_data and analysis_data["lessons_learned"]:
|
||
lessons_content: str = self._format_lessons(
|
||
analysis_data["lessons_learned"], date_str
|
||
)
|
||
lessons_path = str(
|
||
Path(output_config.get("lessons_folder", "Knowledge/Lessons"))
|
||
/ f"{date_str}.md"
|
||
)
|
||
|
||
lessons_result: SkillResult = await write_skill.execute(
|
||
context,
|
||
file_path=lessons_path,
|
||
content=lessons_content,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
overwrite=True,
|
||
)
|
||
write_results["lessons"] = lessons_result
|
||
|
||
# 处理待办事项
|
||
if "action_items" in analysis_data and analysis_data["action_items"]:
|
||
tasks_content: str = self._format_tasks(
|
||
analysis_data["action_items"], date_str
|
||
)
|
||
tasks_path = str(
|
||
Path(output_config.get("tasks_folder", "Tasks/Daily"))
|
||
/ f"{date_str}.md"
|
||
)
|
||
|
||
tasks_result: SkillResult = await write_skill.execute(
|
||
context,
|
||
file_path=tasks_path,
|
||
content=tasks_content,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
overwrite=True,
|
||
)
|
||
write_results["tasks"] = tasks_result
|
||
|
||
# 处理问题
|
||
if "problems" in analysis_data and analysis_data["problems"]:
|
||
problems_content: str = self._format_problems(
|
||
analysis_data["problems"], date_str
|
||
)
|
||
problems_path = str(
|
||
Path(output_config.get("problems_folder", "Knowledge/Problems"))
|
||
/ f"{date_str}.md"
|
||
)
|
||
|
||
problems_result: SkillResult = await write_skill.execute(
|
||
context,
|
||
file_path=problems_path,
|
||
content=problems_content,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
overwrite=True,
|
||
)
|
||
write_results["problems"] = problems_result
|
||
|
||
# 处理成就
|
||
if "achievements" in analysis_data and analysis_data["achievements"]:
|
||
achievements_content: str = self._format_achievements(
|
||
analysis_data["achievements"], date_str
|
||
)
|
||
achievements_path = str(
|
||
Path(
|
||
output_config.get(
|
||
"achievements_folder", "Knowledge/Achievements"
|
||
)
|
||
)
|
||
/ f"{date_str}.md"
|
||
)
|
||
|
||
achievements_result: SkillResult = await write_skill.execute(
|
||
context,
|
||
file_path=achievements_path,
|
||
content=achievements_content,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
overwrite=True,
|
||
)
|
||
write_results["achievements"] = achievements_result
|
||
|
||
# 处理改进建议
|
||
if "improvements" in analysis_data and analysis_data["improvements"]:
|
||
improvements_content: str = self._format_improvements(
|
||
analysis_data["improvements"], date_str
|
||
)
|
||
improvements_path = str(
|
||
Path(
|
||
output_config.get(
|
||
"improvements_folder", "Knowledge/Improvements"
|
||
)
|
||
)
|
||
/ f"{date_str}.md"
|
||
)
|
||
|
||
improvements_result: SkillResult = await write_skill.execute(
|
||
context,
|
||
file_path=improvements_path,
|
||
content=improvements_content,
|
||
api_url=api_url,
|
||
api_key=api_key,
|
||
overwrite=True,
|
||
)
|
||
write_results["improvements"] = improvements_result
|
||
|
||
# 统计结果
|
||
successful_writes: int = sum(1 for r in write_results.values() if r.success)
|
||
|
||
return SkillResult(
|
||
success=True,
|
||
data={
|
||
"date": date_str,
|
||
"journal_file": daily_note_path,
|
||
"analysis": analysis_data,
|
||
"write_results": {k: v.success for k, v in write_results.items()},
|
||
"successful_writes": successful_writes,
|
||
"total_writes": len(write_results),
|
||
"organized_at": datetime.now().isoformat(),
|
||
},
|
||
message=f"成功整理日记 {date_str},已生成 {successful_writes} 个文件",
|
||
)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"执行日记整理命令时出错: {str(e)}")
|
||
return SkillResult(success=False, error=str(e), message="日记整理异常")
|
||
|
||
def _format_experiences(
|
||
self, experiences: List[Dict[str, Any]], date_str: str
|
||
) -> str:
|
||
"""格式化经验内容"""
|
||
content: str = f"# 经验总结 - {date_str}\n\n"
|
||
content += f"*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
|
||
|
||
for i, exp in enumerate(experiences, 1):
|
||
content += f"## {i}. {exp.get('title', '经验')}\n\n"
|
||
content += f"**分类**: {exp.get('category', '未分类')}\n"
|
||
content += f"**优先级**: {exp.get('priority', 'medium')}\n\n"
|
||
content += f"{exp.get('content', '')}\n\n"
|
||
|
||
content += f"\n---\n*来源: [[{date_str}]]*\n"
|
||
return content
|
||
|
||
def _format_lessons(self, lessons: List[Dict[str, Any]], date_str: str) -> str:
|
||
"""格式化经验教训内容"""
|
||
content: str = f"# 经验教训 - {date_str}\n\n"
|
||
content += f"*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
|
||
|
||
for i, lesson in enumerate(lessons, 1):
|
||
content += f"## {i}. {lesson.get('lesson', '教训')}\n\n"
|
||
content += f"**背景**: {lesson.get('context', '')}\n\n"
|
||
content += f"**应用**: {lesson.get('application', '')}\n\n"
|
||
|
||
content += f"\n---\n*来源: [[{date_str}]]*\n"
|
||
return content
|
||
|
||
def _format_tasks(self, tasks: List[Dict[str, Any]], date_str: str) -> str:
|
||
"""格式化待办事项内容"""
|
||
content: str = f"# 待办事项 - {date_str}\n\n"
|
||
content += f"*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
|
||
|
||
# 按优先级分组
|
||
by_priority: Dict[str, List[Dict[str, Any]]] = {
|
||
"high": [],
|
||
"medium": [],
|
||
"low": [],
|
||
}
|
||
for task in tasks:
|
||
priority: str = task.get("priority", "medium")
|
||
by_priority[priority].append(task)
|
||
|
||
for priority in ["high", "medium", "low"]:
|
||
if by_priority[priority]:
|
||
priority_text: Dict[str, str] = {
|
||
"high": "🔴 高",
|
||
"medium": "� 中",
|
||
r"low": "🟢 低",
|
||
}
|
||
content += f"## {priority_text[priority]} 优先级\n\n"
|
||
|
||
for task in by_priority[priority]:
|
||
content += f"- [ ] {task.get('task', '任务')}\n"
|
||
if task.get("deadline"):
|
||
content += f" - 截止: {task.get('deadline')}\n"
|
||
content += "\n"
|
||
|
||
content += f"\n---\n*来源: [[{date_str}]]*\n"
|
||
return content
|
||
|
||
def _format_problems(self, problems: List[Dict[str, Any]], date_str: str) -> str:
|
||
"""格式化问题内容"""
|
||
content: str = f"# 问题记录 - {date_str}\n\n"
|
||
content += f"*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
|
||
|
||
for i, problem in enumerate(problems, 1):
|
||
content += f"## {i}. {problem.get('problem', '问题')}\n\n"
|
||
content += f"**影响**: {problem.get('impact', '')}\n\n"
|
||
content += f"**建议方案**: {problem.get('proposed_solution', '')}\n\n"
|
||
|
||
content += f"\n---\n*来源: [[{date_str}]]*\n"
|
||
return content
|
||
|
||
def _format_achievements(
|
||
self, achievements: List[Dict[str, Any]], date_str: str
|
||
) -> str:
|
||
"""格式化成就内容"""
|
||
content: str = f"# 成就记录 - {date_str}\n\n"
|
||
content += f"*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
|
||
|
||
for i, achievement in enumerate(achievements, 1):
|
||
content += f"## {i}. {achievement.get('achievement', '成就')}\n\n"
|
||
content += f"**重要性**: {achievement.get('significance', '')}\n\n"
|
||
content += f"**证据**: {achievement.get('evidence', '')}\n\n"
|
||
|
||
content += f"\n---\n*来源: [[{date_str}]]*\n"
|
||
return content
|
||
|
||
def _format_improvements(
|
||
self, improvements: List[Dict[str, Any]], date_str: str
|
||
) -> str:
|
||
"""格式化改进建议内容"""
|
||
content: str = f"# 改进建议 - {date_str}\n\n"
|
||
content += f"*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
|
||
|
||
for i, improvement in enumerate(improvements, 1):
|
||
content += f"## {i}. {improvement.get('area', '改进领域')}\n\n"
|
||
content += f"**当前状态**: {improvement.get('current_state', '')}\n\n"
|
||
content += f"**建议改进**: {improvement.get('suggested_change', '')}\n\n"
|
||
content += f"**预期收益**: {improvement.get('expected_benefit', '')}\n\n"
|
||
|
||
content += f"\n---\n*来源: [[{date_str}]]*\n"
|
||
return content
|