Skill: dyby99-gif/xinqing-journal (version 1.0.0)

--- name: xinqing-journal version: 1.0.0 description: > 心情日记 — 智能情绪追踪器，通过自然语言记录中文日记、自动识别7种情绪、 1-10分评分、智能标签提取，生成日/周/月报告和趋势分析。 数据纯本地JSON存储，零网络请求，保护隐私。 emoji: "\U0001F308" category: health tags: - health - journal - mood-tracker - chinese - mental-health - privacy requires: python: ">=3.9" os: - macos - linux - windows author: dyby99 license: MIT --- # Xinqing Journal - 心情日记 用自然语言记录每天的心情，自动分析情绪变化趋势。 ## 功能特性 - 📝 **自然语言日记**: "今天工作很顺利，心情不错" - 🎭 **情绪自动识别**: 开心、平静、兴奋、焦虑、难过、愤怒、疲惫 - ⭐ **情绪评分**: 1-10分自动或手动评分 - 🏷️ **智能标签提取**: 自动提取人、事、物、地点、天气标签 - 📊 **多维度报告**: 日报、周报、月报、趋势分析 - 🔒 **本地数据存储**: JSON文件存储，保护隐私 - 📅 **心情日历**: 可视化月历查看情绪分布 - 💡 **智能建议**: 根据情绪数据给出健康建议 ## 快速开始 ### 添加日记 ```bash # 简单日记 python scripts/journal.py add "今天工作很顺利，心情不错" # 指定情绪评分 python scripts/journal.py add "今天下雨了有点郁闷，心情4分" # 详细日记 python scripts/journal.py add "今天和朋友去了咖啡店，聊得很开心，心情9分" # 昨天日记 python scripts/journal.py add "昨天加班到很晚很累" ``` ### 查看日记 ```bash # 列出最近7天 python scripts/journal.py list # 列出最近30天 python scripts/journal.py list 30 # 按情绪筛选 python scripts/journal.py list 30 开心 ``` ### 心情日历 ```bash # 本月日历 python scripts/journal.py calendar # 指定年月 python scripts/journal.py calendar 2024 1 ``` ### 生成情绪报告 ```bash # 日报 python scripts/mood-report.py daily python scripts/mood-report.py daily 2024-01-15 # 周报 python scripts/mood-report.py weekly python scripts/mood-report.py weekly 1 # 上周 # 月报 python scripts/mood-report.py monthly python scripts/mood-report.py monthly 1 # 上月 # 趋势分析 python scripts/mood-report.py trend 30 ``` ### 查看统计 ```bash # 情绪摘要 python scripts/journal.py summary python scripts/journal.py summary 90 # 情绪类型列表 python scripts/journal.py moods ``` ### 编辑和删除 ```bash # 更新日记 python scripts/journal.py update abc123 "修改后的内容" # 删除日记 python scripts/journal.py delete abc123 ``` ## 项目结构 ``` xinqing-journal/ ├── SKILL.md # 本文件 ├── .clawhubignore # 发布忽略配置 ├── scripts/ │ ├── journal.py # 核心日记模块 │ └── mood-report.py # 情绪报告生成器 └── assets/ └── moods.json # 情绪配置 ``` ## 数据存储 - **路径**: `~/.openclaw/workspace/data/journal/entries.json` - **格式**: JSON - **隐私**: 纯本地存储，不上传云端 - **原子写入**: 使用临时文件+原子替换，防止数据损坏 数据结构: ```json { "entries": [ { "id": "a1b2c3d4", "date": "2024-01-15", "content": "今天工作很顺利", "mood": "开心", "score": 8, "tags": ["工作"], "raw_text": "今天工作很顺利，心情不错", "created_at": "2024-01-15T12:30:00", "updated_at": "2024-01-15T12:30:00" } ], "version": "1.0" } ``` ## 自然语言支持 ### 时间表达 - "今天..." - 今天 - "昨天..." - 昨天 - "前天..." - 前天 - "2024-01-15..." - 指定日期 ### 情绪评分 - "心情8分" - "评分9" - "mood 7" - "8/10" ### 情绪识别关键词 | 情绪 | 触发关键词 | |------|-----------| | 开心 | 开心、高兴、快乐、愉快、欢喜、喜悦、美滋滋、哈哈、嘿嘿 | | 平静 | 平静、平和、安宁、淡定、从容、安稳、宁静、祥和 | | 兴奋 | 兴奋、激动、亢奋、狂喜、太棒了、绝了、燃、起飞 | | 焦虑 | 焦虑、担心、紧张、不安、忐忑、发愁、压力大、迷茫 | | 难过 | 难过、伤心、悲伤、失落、沮丧、郁闷、委屈、想哭、emo | | 愤怒 | 愤怒、生气、恼火、气愤、不爽、烦躁、火大、爆炸 | | 疲惫 | 疲惫、累、疲倦、困、乏力、没精神 | ## 标签自动提取 自动识别以下类别标签: - **人**: 朋友、家人、同事、老板、同学、对象 - **事**: 工作、学习、考试、项目、会议、面试 - **物**: 咖啡、茶、书、电影、音乐、游戏 - **地点**: 公司、家、学校、咖啡店、公园 - **天气**: 晴天、阴天、下雨、下雪、热、冷 ## 报告类型 ### 日报 - 当日情绪评分 - 情绪类型分布 - 日记内容列表 ### 周报 - 本周情绪总览 - 日均情绪评分 - 每日情绪变化 - 主导情绪分析 - 健康建议 ### 月报 - 月度情绪总览 - 每周情绪概况 - 情绪分布统计 - 主导情绪变化 - 月度建议 ### 趋势分析 - 情绪趋势（上升/下降/稳定） - 每周情绪评分变化 - 情绪波动异常检测 - 长期健康建议 ## 命令行参考 ### journal.py 命令 | 命令 | 用法 | 说明 | |------|------|------| | add | `add <内容>` | 添加日记 | | list | `list [天数] [情绪]` | 列出日记 | | calendar | `calendar [年] [月]` | 心情日历 | | summary | `summary [天数]` | 情绪摘要 | | delete | `delete <ID>` | 删除日记 | | moods | `moods` | 情绪类型列表 | | update | `update <ID> <内容>` | 更新日记 | ### mood-report.py 命令 | 命令 | 用法 | 说明 | |------|------|------| | daily | `daily [日期]` | 日报 | | weekly | `weekly [偏移]` | 周报 | | monthly | `monthly [偏移]` | 月报 | | trend | `trend [天数]` | 趋势分析 | ## 配置文件 情绪配置存储在 `assets/moods.json`: ```json { "moods": { "开心": { "score_range": [7, 9], "keywords": ["开心", "高兴", "快乐"], "emoji": "😊", "color": "#FFD93D" } }, "tag_patterns": { "人": ["朋友", "家人", "同事"], "事": ["工作", "学习", "考试"] } } ``` 可自定义: - 情绪类型和关键词 - 评分范围 - Emoji和颜色 - 标签提取规则 ## 使用建议 1. **每日记录**: 养成每天写日记的习惯，可以在睡前花3-5分钟回顾一天 2. **诚实表达**: 记录真实的情绪，不必掩饰负面情绪 3. **具体描述**: 尝试描述产生情绪的具体原因，而不仅是情绪本身 4. **定期复盘**: 每周查看周报，了解自己的情绪周期 5. **关注趋势**: 注意长期趋势，而非单一天的评分 ## 情绪健康提示 - 评分持续低于3分超过一周，建议寻求专业帮助 - 情绪波动过大（忽高忽低）可能提示压力过大 - 长期焦虑或难过情绪需要关注心理健康 - 保持记录本身就是情绪调节的好方法 ## 故障排除 ### 无法识别情绪 - 在日记中使用更明确的情绪词汇 - 直接指定评分："心情5分" ### 数据文件损坏 - 使用原子写入机制，极少出现损坏 - 如有问题可手动编辑 JSON 文件修复 - 建议定期备份 `entries.json` ### 标签提取不准确 - 在 `moods.json` 中添加自定义标签关键词 - 在日记中使用更具体的词汇 ## 扩展开发 ### 添加新情绪类型 编辑 `assets/moods.json`: ```json { "moods": { "新情绪": { "score_range": [4, 6], "keywords": ["关键词1", "关键词2"], "emoji": "🆕", "color": "#FFFFFF" } } } ``` ### 作为模块使用 ```python from scripts.journal import JournalTracker tracker = JournalTracker() entry = tracker.add("今天心情不错") print(entry) # 获取统计 summary = tracker.get_summary(days=30) print(summary) ``` ## 技术规格 - **Python版本**: 3.9+ - **依赖**: 无外部依赖，仅使用标准库 - **编码**: UTF-8 - **数据格式**: JSON - **ID生成**: UUID短ID（8位） - **文件操作**: 原子写入 ## 隐私说明 - 所有数据仅存储在本地 - 无网络传输 - 无云端同步 - 用户完全拥有数据 ## 许可证 MIT License

path: assets/moods.json
content: {
  "moods": {
    "开心": {
      "score_range": [7, 9],
      "keywords": ["开心", "高兴", "快乐", "愉快", "欢喜", "喜悦", "满足", "舒畅", "美滋滋", "不错", "挺好", "很好", "真好", "好开心", "哈哈", "嘿嘿", "嘻嘻", "棒", "赞", "nice", "happy", "joy"],
      "color": "#FFD93D",
      "emoji": "😊"
    },
    "平静": {
      "score_range": [5, 7],
      "keywords": ["平静", "平和", "安宁", "淡定", "从容", "安稳", "宁静", "祥和", "无事", "一般", "还行", "ordinary", "calm", "peaceful"],
      "color": "#6BCB77",
      "emoji": "😌"
    },
    "兴奋": {
      "score_range": [8, 10],
      "keywords": ["兴奋", "激动", "亢奋", "狂喜", "太棒了", "绝了", "燃", "冲", "起飞", "哇塞", "excited", "thrilled", "awesome"],
      "color": "#FF6B6B",
      "emoji": "🤩"
    },
    "焦虑": {
      "score_range": [3, 5],
      "keywords": ["焦虑", "担心", "紧张", "不安", "忐忑", "发愁", "压力大", "着急", "慌", "迷茫", "anxious", "worried", "nervous"],
      "color": "#9B59B6",
      "emoji": "😰"
    },
    "难过": {
      "score_range": [2, 4],
      "keywords": ["难过", "伤心", "悲伤", "失落", "沮丧", "郁闷", "委屈", "想哭", "emo", "blue", "sad", "upset", "depressed"],
      "color": "#3498DB",
      "emoji": "😢"
    },
    "愤怒": {
      "score_range": [1, 3],
      "keywords": ["愤怒", "生气", "恼火", "气愤", "不爽", "烦躁", "火大", "爆炸", "怒", "angry", "mad", "furious", "annoyed"],
      "color": "#E74C3C",
      "emoji": "😠"
    },
    "疲惫": {
      "score_range": [3, 5],
      "keywords": ["疲惫", "累", "疲倦", "困", "乏力", "没精神", " exhaustion", "tired", "sleepy", "exhausted", "weary"],
      "color": "#95A5A6",
      "emoji": "😴"
    }
  },
  "tag_patterns": {
    "社交": ["朋友", "家人", "同事", "老板", "同学", "对象", "聚会", "聚餐"],
    "工作": ["工作", "加班", "项目", "会议", "面试", "出差", "上班", "开会"],
    "学习": ["学习", "考试", "课程", "看书", "培训", "读书"],
    "运动": ["运动", "跑步", "健身", "游泳", "瑜伽", "散步", "打球"],
    "饮食": ["吃饭", "午饭", "晚饭", "早餐", "外卖", "火锅", "烧烤", "奶茶"],
    "娱乐": ["电影", "音乐", "游戏", "KTV", "唱歌", "旅游", "旅行"],
    "健康": ["医院", "看病", "头疼", "感冒", "失眠", "锻炼", "体检"],
    "天气": ["晴天", "阴天", "下雨", "下雪", "刮风", "热", "冷"]
  },
  "settings": {
    "default_mood": "平静",
    "default_score": 5,
    "max_entry_length": 2000,
    "auto_save": true
  }
}


path: scripts/journal.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
心情日记 - 核心模块
自然语言日记记录、情绪识别、标签提取、CRUD操作
"""

import json
import os
import re
import sys
import tempfile
import uuid
from calendar import monthcalendar
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Tuple

# 数据存储路径
DATA_DIR = Path(os.path.expanduser("~/.openclaw/workspace/data/journal"))
DATA_FILE = DATA_DIR / "entries.json"

# 隐私保护：确保数据文件权限仅限当前用户
_PRIVATE_PERMS = 0o600  # rw-------
_PRIVATE_DIR_PERMS = 0o700  # rwx------

# 加载情绪配置
def load_mood_config() -> Dict:
    """加载情绪配置文件"""
    config_path = Path(__file__).parent.parent / "assets" / "moods.json"
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        # 默认配置
        return {
            "moods": {
                "开心": {"score_range": [7, 9], "keywords": ["开心", "高兴"], "emoji": "😊"},
                "平静": {"score_range": [5, 7], "keywords": ["平静", "平和"], "emoji": "😌"},
                "兴奋": {"score_range": [8, 10], "keywords": ["兴奋", "激动"], "emoji": "🤩"},
                "焦虑": {"score_range": [3, 5], "keywords": ["焦虑", "担心"], "emoji": "😰"},
                "难过": {"score_range": [2, 4], "keywords": ["难过", "伤心"], "emoji": "😢"},
                "愤怒": {"score_range": [1, 3], "keywords": ["愤怒", "生气"], "emoji": "😠"},
                "疲惫": {"score_range": [3, 5], "keywords": ["疲惫", "累"], "emoji": "😴"}
            },
            "tag_patterns": {},
            "settings": {"default_mood": "平静", "default_score": 5}
        }


class JournalTracker:
    """心情日记追踪器"""
    
    def __init__(self):
        self.config = load_mood_config()
        self._ensure_data_dir()
        self.data = self._load_data()
    
    def _ensure_data_dir(self):
        """确保数据目录存在，并设置严格权限（仅当前用户可访问）"""
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        try:
            os.chmod(str(DATA_DIR), _PRIVATE_DIR_PERMS)
        except OSError:
            pass  # Windows 等不支持 chmod 的系统跳过
    
    def _load_data(self) -> Dict:
        """加载数据文件"""
        if DATA_FILE.exists():
            try:
                with open(DATA_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except (json.JSONDecodeError, IOError) as e:
                print(f"⚠️ 数据文件读取失败: {e}，创建新文件")
        return {"entries": [], "version": "1.0"}
    
    def _atomic_save(self, data: Dict):
        """原子保存数据（先写临时文件再rename，防止中断导致数据丢失）"""
        try:
            fd, tmp_path = tempfile.mkstemp(
                dir=str(DATA_DIR), suffix='.tmp', prefix='journal_'
            )
            try:
                with os.fdopen(fd, 'w', encoding='utf-8') as f:
                    json.dump(data, f, ensure_ascii=False, indent=2)
                os.replace(tmp_path, str(DATA_FILE))
                # 隐私保护：确保数据文件仅当前用户可读写
                try:
                    os.chmod(str(DATA_FILE), _PRIVATE_PERMS)
                except OSError:
                    pass
            except Exception:
                if os.path.exists(tmp_path):
                    os.unlink(tmp_path)
                raise
        except IOError as e:
            print(f"❌ 数据保存失败: {e}")
            sys.exit(1)
    
    def _generate_short_id(self) -> str:
        """生成短UUID（8位）"""
        return uuid.uuid4().hex[:8]
    
    def _detect_mood(self, text: str) -> Tuple[str, int]:
        """
        自动识别情绪类型和评分
        
        Returns:
            (情绪名称, 情绪评分)
        """
        text_lower = text.lower()

        # 构建所有 (关键词, 情绪名, 评分) 的列表，按关键词长度降序排列
        # 这样"太棒了"会优先于"棒"匹配
        all_matches: List[Tuple[str, str, int]] = []
        for mood_name, mood_data in self.config["moods"].items():
            score_range = mood_data.get("score_range", [5, 5])
            score = (score_range[0] + score_range[1]) // 2
            for keyword in mood_data.get("keywords", []):
                if keyword in text_lower:
                    all_matches.append((keyword, mood_name, score))

        if all_matches:
            # 最长关键词优先匹配（更精确）
            all_matches.sort(key=lambda x: len(x[0]), reverse=True)
            _, mood_name, score = all_matches[0]
            return mood_name, score

        # 如果没有匹配到，返回默认
        default_mood = self.config["settings"].get("default_mood", "平静")
        default_score = self.config["settings"].get("default_score", 5)
        return default_mood, default_score
    
    def _extract_score(self, text: str) -> Optional[int]:
        """从文本中提取情绪评分（1-10）"""
        # 匹配格式：心情8分、评分9、 mood 7/10
        patterns = [
            r'心情[是为]?\s*(\d+)[分]?',
            r'评分\s*(\d+)',
            r'mood\s*(\d+)',
            r'(\d+)[/]?10',
        ]
        for pattern in patterns:
            match = re.search(pattern, text)
            if match:
                try:
                    score = int(match.group(1))
                    if 1 <= score <= 10:
                        return score
                except ValueError:
                    continue
        return None
    
    def _extract_tags(self, text: str) -> List[str]:
        """自动提取标签（返回分类名如"工作""社交"，而非具体关键词）"""
        tags = []
        tag_patterns = self.config.get("tag_patterns", {})

        for category, keywords in tag_patterns.items():
            for keyword in keywords:
                if keyword in text:
                    tags.append(category)
                    break

        # 去重并限制数量
        return list(dict.fromkeys(tags))[:5]
    
    def _parse_date(self, text: str) -> str:
        """解析日期，支持中文时间词和标准日期格式"""
        today = datetime.now()

        if '昨天' in text:
            date = today - timedelta(days=1)
        elif '前天' in text:
            date = today - timedelta(days=2)
        elif '今天' in text or '刚刚' in text or '刚才' in text:
            date = today
        else:
            # 优先匹配 YYYY-MM-DD
            match = re.search(r'(\d{4})-(\d{1,2})-(\d{1,2})', text)
            if match:
                try:
                    date = datetime(int(match.group(1)), int(match.group(2)), int(match.group(3)))
                    return date.strftime("%Y-%m-%d")
                except ValueError:
                    pass

            # 尝试匹配 MM-DD（无年份）
            match = re.search(r'(?<!\d)(\d{1,2})-(\d{1,2})(?!\d)', text)
            if match:
                try:
                    date = datetime(today.year, int(match.group(1)), int(match.group(2)))
                    return date.strftime("%Y-%m-%d")
                except ValueError:
                    pass

            date = today

        return date.strftime("%Y-%m-%d")
    
    def _clean_content(self, text: str) -> str:
        """清理日记内容，移除元数据标记"""
        # 移除情绪评分标记
        text = re.sub(r'心情[是为]?\s*\d+[分]?', '', text)
        text = re.sub(r'评分\s*\d+', '', text)
        # 移除时间词
        text = re.sub(r'(今天|昨天|前天|刚刚|刚才)', '', text)
        # 清理多余空格
        text = text.strip()
        return text[:2000]  # 限制长度
    
    def add(self, text: str) -> Dict:
        """
        添加一篇日记
        
        Args:
            text: 自然语言日记内容
            
        Returns:
            日记记录字典
        """
        # 解析日期
        date = self._parse_date(text)
        
        # 情绪识别
        detected_mood, detected_score = self._detect_mood(text)
        
        # 提取评分（如果有明确指定）
        explicit_score = self._extract_score(text)
        if explicit_score:
            score = explicit_score
        else:
            score = detected_score
        
        # 提取标签
        tags = self._extract_tags(text)
        
        # 清理内容（保留有意义的文字）
        content = self._clean_content(text)

        # 如果清理后太短，使用原始文本
        if not content or len(content) < 2:
            content = text[:500]

        if not content.strip():
            raise ValueError("日记内容不能为空，请写点什么吧")
        
        # 创建记录
        entry = {
            "id": self._generate_short_id(),
            "date": date,
            "content": content,
            "mood": detected_mood,
            "score": score,
            "tags": tags,
            "raw_text": text,
            "created_at": datetime.now().isoformat(),
            "updated_at": datetime.now().isoformat()
        }
        
        # 添加到数据
        self.data["entries"].append(entry)
        self._atomic_save(self.data)
        
        return entry
    
    def list(self, days: int = 7, mood: Optional[str] = None) -> List[Dict]:
        """
        列出日记记录
        
        Args:
            days: 最近多少天
            mood: 按情绪筛选
        """
        cutoff_date = datetime.now() - timedelta(days=days)
        cutoff_str = cutoff_date.strftime("%Y-%m-%d")
        
        records = []
        for e in self.data["entries"]:
            if e["date"] >= cutoff_str:
                if mood is None or e["mood"] == mood:
                    records.append(e)
        
        # 按日期倒序
        records.sort(key=lambda x: (x["date"], x["created_at"]), reverse=True)
        return records
    
    def get_by_id(self, entry_id: str) -> Optional[Dict]:
        """通过ID获取日记"""
        for e in self.data["entries"]:
            if e["id"] == entry_id:
                return e
        return None
    
    def update(self, entry_id: str, new_text: str) -> Optional[Dict]:
        """更新日记"""
        for i, e in enumerate(self.data["entries"]):
            if e["id"] == entry_id:
                # 重新解析
                date = self._parse_date(new_text)
                detected_mood, detected_score = self._detect_mood(new_text)
                explicit_score = self._extract_score(new_text)
                score = explicit_score if explicit_score else detected_score
                tags = self._extract_tags(new_text)
                content = self._clean_content(new_text)
                
                # 更新记录
                self.data["entries"][i].update({
                    "date": date,
                    "content": content,
                    "mood": detected_mood,
                    "score": score,
                    "tags": tags,
                    "raw_text": new_text,
                    "updated_at": datetime.now().isoformat()
                })
                
                self._atomic_save(self.data)
                return self.data["entries"][i]
        return None
    
    def delete(self, entry_id: str) -> bool:
        """删除日记"""
        for i, e in enumerate(self.data["entries"]):
            if e["id"] == entry_id:
                del self.data["entries"][i]
                self._atomic_save(self.data)
                return True
        return False
    
    def get_summary(self, days: int = 30) -> Dict:
        """获取日记摘要统计"""
        records = self.list(days=days)
        
        if not records:
            return {
                "period_days": days,
                "total_entries": 0,
                "avg_mood_score": 0,
                "mood_distribution": {},
                "tag_cloud": []
            }
        
        # 情绪分布
        mood_dist = {}
        total_score = 0
        all_tags = []
        
        for r in records:
            mood = r["mood"]
            mood_dist[mood] = mood_dist.get(mood, 0) + 1
            total_score += r.get("score", 5)
            all_tags.extend(r.get("tags", []))
        
        # 标签频次
        tag_freq = {}
        for tag in all_tags:
            tag_freq[tag] = tag_freq.get(tag, 0) + 1
        
        tag_cloud = sorted(tag_freq.items(), key=lambda x: x[1], reverse=True)[:10]
        
        return {
            "period_days": days,
            "total_entries": len(records),
            "avg_mood_score": round(total_score / len(records), 1),
            "mood_distribution": mood_dist,
            "tag_cloud": tag_cloud
        }
    
    def calendar_view(self, year: int = None, month: int = None) -> str:
        """生成月历视图"""
        if year is None:
            year = datetime.now().year
        if month is None:
            month = datetime.now().month
        
        # 获取该月的所有日记
        month_str = f"{year}-{month:02d}"
        entries_in_month = [
            e for e in self.data["entries"]
            if e["date"].startswith(month_str)
        ]
        
        # 按日期分组，取每天的情绪
        daily_moods = {}
        for e in entries_in_month:
            day = int(e["date"].split("-")[2])
            if day not in daily_moods:
                daily_moods[day] = []
            daily_moods[day].append(e["mood"])
        
        # 生成日历
        cal = monthcalendar(year, month)
        mood_emojis = {name: data["emoji"] for name, data in self.config["moods"].items()}
        
        lines = [f"\n📅 {year}年{month}月 心情日历\n"]
        lines.append("日  一  二  三  四  五  六")
        lines.append("-" * 26)
        
        for week in cal:
            week_str = ""
            for day in week:
                if day == 0:
                    week_str += "    "
                elif day in daily_moods:
                    # 显示当天的主要情绪
                    main_mood = daily_moods[day][0]
                    emoji = mood_emojis.get(main_mood, "📝")
                    week_str += f"{emoji:2s}  "
                else:
                    week_str += f"{day:2d}  "
            lines.append(week_str)
        
        lines.append("\n图例: " + " ".join([f"{data['emoji']}{name}" for name, data in list(self.config["moods"].items())[:4]]))
        
        return "\n".join(lines)
    
    def get_moods(self) -> Dict:
        """获取情绪类型列表"""
        return self.config.get("moods", {})

    def export_data(self, anonymize: bool = False) -> Dict:
        """
        导出数据（可选匿名化）

        Args:
            anonymize: 是否匿名化（移除原始文本，仅保留情绪统计）
        """
        if anonymize:
            # 隐私友好导出：仅保留统计数据，移除日记原文
            entries = []
            for e in self.data["entries"]:
                entries.append({
                    "id": e["id"],
                    "date": e["date"],
                    "mood": e["mood"],
                    "score": e["score"],
                    "tags": e.get("tags", [])
                })
            return {"entries": entries, "version": self.data.get("version", "1.0")}
        return self.data


def main():
    """命令行入口"""
    tracker = JournalTracker()
    
    if len(sys.argv) < 2:
        print("心情日记 - xinqing-journal")
        print("用法: python journal.py <命令> [参数]\n")
        print("命令:")
        print("  add <内容>        - 添加日记")
        print("  list [天数]       - 列出日记，默认最近7天")
        print("  calendar [年] [月] - 月历视图")
        print("  summary [天数]    - 情绪摘要")
        print("  delete <ID>       - 删除日记")
        print("  moods             - 情绪类型列表")
        print("  update <ID> <内容> - 更新日记")
        print("")
        sys.exit(0)
    
    command = sys.argv[1]
    
    try:
        if command == "add":
            if len(sys.argv) < 3:
                print("❌ 请提供日记内容")
                sys.exit(1)
            
            text = " ".join(sys.argv[2:])
            entry = tracker.add(text)
            mood_config = tracker.get_moods().get(entry["mood"], {})
            emoji = mood_config.get("emoji", "📝")
            
            print(f"✅ 日记已保存 {emoji}")
            print(f"   ID: {entry['id']}")
            print(f"   日期: {entry['date']}")
            print(f"   情绪: {entry['mood']} ({entry['score']}/10)")
            if entry["tags"]:
                print(f"   标签: {', '.join(entry['tags'])}")
            print(f"   内容: {entry['content'][:50]}...")
        
        elif command == "list":
            days = int(sys.argv[2]) if len(sys.argv) > 2 else 7
            mood_filter = sys.argv[3] if len(sys.argv) > 3 else None
            entries = tracker.list(days=days, mood=mood_filter)
            
            if not entries:
                print(f"📭 最近{days}天没有日记")
                return
            
            print(f"📚 最近{days}天日记 ({len(entries)}篇)\n")
            
            current_date = ""
            for e in entries:
                if e["date"] != current_date:
                    current_date = e["date"]
                    print(f"📅 {current_date}")
                
                mood_config = tracker.get_moods().get(e["mood"], {})
                emoji = mood_config.get("emoji", "📝")
                tags_str = f" [{', '.join(e['tags'])}]" if e.get("tags") else ""
                content_preview = e["content"][:30] + "..." if len(e["content"]) > 30 else e["content"]
                
                print(f"  [{e['id']}] {emoji} {e['mood']}({e['score']}){tags_str}")
                print(f"       {content_preview}")
        
        elif command == "calendar":
            year = int(sys.argv[2]) if len(sys.argv) > 2 else None
            month = int(sys.argv[3]) if len(sys.argv) > 3 else None
            print(tracker.calendar_view(year, month))
        
        elif command == "summary":
            days = int(sys.argv[2]) if len(sys.argv) > 2 else 30
            summary = tracker.get_summary(days=days)
            
            print(f"📊 最近{days}天心情摘要\n")
            print(f"📝 日记总数: {summary['total_entries']} 篇")
            print(f"⭐ 平均情绪评分: {summary['avg_mood_score']}/10")
            
            if summary['mood_distribution']:
                print(f"\n🎭 情绪分布:")
                for mood, count in sorted(summary['mood_distribution'].items(), key=lambda x: x[1], reverse=True):
                    mood_config = tracker.get_moods().get(mood, {})
                    emoji = mood_config.get("emoji", "")
                    print(f"   {emoji} {mood}: {count}篇")
            
            if summary['tag_cloud']:
                print(f"\n🏷️ 常用标签:")
                for tag, count in summary['tag_cloud']:
                    print(f"   #{tag}: {count}次")
        
        elif command == "delete":
            if len(sys.argv) < 3:
                print("❌ 请提供日记ID")
                sys.exit(1)
            
            entry_id = sys.argv[2]
            if tracker.delete(entry_id):
                print(f"✅ 已删除日记 #{entry_id}")
            else:
                print(f"❌ 未找到日记 #{entry_id}")
        
        elif command == "moods":
            moods = tracker.get_moods()
            print("🎭 情绪类型列表\n")
            for name, data in moods.items():
                emoji = data.get("emoji", "")
                score_range = data.get("score_range", [1, 10])
                print(f"  {emoji} {name}: 评分范围 {score_range[0]}-{score_range[1]}")
        
        elif command == "update":
            if len(sys.argv) < 4:
                print("❌ 请提供日记ID和新内容")
                sys.exit(1)
            
            entry_id = sys.argv[2]
            new_text = " ".join(sys.argv[3:])
            entry = tracker.update(entry_id, new_text)
            
            if entry:
                print(f"✅ 日记 #{entry_id} 已更新")
                print(f"   情绪: {entry['mood']} ({entry['score']}/10)")
            else:
                print(f"❌ 未找到日记 #{entry_id}")
        
        else:
            print(f"❌ 未知命令: {command}")
            print("可用命令: add, list, calendar, summary, delete, moods, update")
    
    except ValueError as e:
        print(f"❌ 错误: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"❌ 发生错误: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/mood-report.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
心情报告生成器
生成日/周/月情绪报告和趋势分析
"""

import json
import os
import sys
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Tuple

# 数据路径
DATA_DIR = Path(os.path.expanduser("~/.openclaw/workspace/data/journal"))
DATA_FILE = DATA_DIR / "entries.json"

# 加载情绪配置
def load_mood_config() -> Dict:
    """加载情绪配置文件"""
    config_path = Path(__file__).parent.parent / "assets" / "moods.json"
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {
            "moods": {
                "开心": {"score_range": [7, 9], "emoji": "😊", "color": "#FFD93D"},
                "平静": {"score_range": [5, 7], "emoji": "😌", "color": "#6BCB77"},
                "兴奋": {"score_range": [8, 10], "emoji": "🤩", "color": "#FF6B6B"},
                "焦虑": {"score_range": [3, 5], "emoji": "😰", "color": "#9B59B6"},
                "难过": {"score_range": [2, 4], "emoji": "😢", "color": "#3498DB"},
                "愤怒": {"score_range": [1, 3], "emoji": "😠", "color": "#E74C3C"},
                "疲惫": {"score_range": [3, 5], "emoji": "😴", "color": "#95A5A6"}
            }
        }


class MoodReporter:
    """心情报告生成器"""
    
    def __init__(self):
        self.config = load_mood_config()
        self.data = self._load_data()
    
    def _load_data(self) -> Dict:
        """加载日记数据"""
        if DATA_FILE.exists():
            try:
                with open(DATA_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except (json.JSONDecodeError, IOError):
                pass
        return {"entries": []}
    
    def _get_entries_in_range(self, start_date: str, end_date: str) -> List[Dict]:
        """获取日期范围内的日记"""
        entries = []
        for e in self.data.get("entries", []):
            if start_date <= e["date"] <= end_date:
                entries.append(e)
        return entries
    
    def _get_date_range(self, period: str, offset: int = 0) -> Tuple[str, str]:
        """获取日期范围"""
        today = datetime.now()
        
        if period == 'day':
            target = today - timedelta(days=offset)
            start = end = target.strftime("%Y-%m-%d")
        
        elif period == 'week':
            monday = today - timedelta(days=today.weekday(), weeks=offset)
            sunday = monday + timedelta(days=6)
            start = monday.strftime("%Y-%m-%d")
            end = sunday.strftime("%Y-%m-%d")
        
        elif period == 'month':
            year = today.year
            month = today.month - offset
            while month <= 0:
                year -= 1
                month += 12
            
            start = f"{year}-{month:02d}-01"
            if month == 12:
                next_month = datetime(year + 1, 1, 1)
            else:
                next_month = datetime(year, month + 1, 1)
            last_day = (next_month - timedelta(days=1)).day
            end = f"{year}-{month:02d}-{last_day}"
        
        else:
            raise ValueError(f"不支持的周期类型: {period}")
        
        return start, end
    
    def _calculate_mood_stats(self, entries: List[Dict]) -> Dict:
        """计算情绪统计数据"""
        if not entries:
            return {
                "avg_score": 0,
                "mood_distribution": {},
                "dominant_mood": "无数据",
                "score_trend": "stable"
            }
        
        # 平均评分
        total_score = sum(e.get("score", 5) for e in entries)
        avg_score = total_score / len(entries)
        
        # 情绪分布
        mood_dist = defaultdict(int)
        for e in entries:
            mood_dist[e["mood"]] += 1
        
        # 主导情绪
        dominant_mood = max(mood_dist.items(), key=lambda x: x[1])[0] if mood_dist else "无数据"
        
        # 评分趋势（如果有多条记录）
        score_trend = "stable"
        if len(entries) >= 2:
            entries_sorted = sorted(entries, key=lambda x: x["date"])
            first_half = entries_sorted[:len(entries_sorted)//2]
            second_half = entries_sorted[len(entries_sorted)//2:]
            
            if first_half and second_half:
                first_avg = sum(e.get("score", 5) for e in first_half) / len(first_half)
                second_avg = sum(e.get("score", 5) for e in second_half) / len(second_half)
                
                if second_avg > first_avg + 0.5:
                    score_trend = "improving"
                elif second_avg < first_avg - 0.5:
                    score_trend = "declining"
        
        return {
            "avg_score": round(avg_score, 1),
            "mood_distribution": dict(mood_dist),
            "dominant_mood": dominant_mood,
            "score_trend": score_trend
        }
    
    def generate_daily_report(self, date: Optional[str] = None) -> Dict:
        """生成日报"""
        if date is None:
            date = datetime.now().strftime("%Y-%m-%d")
        
        entries = self._get_entries_in_range(date, date)
        stats = self._calculate_mood_stats(entries)
        
        # 获取情绪emoji
        mood_config = self.config["moods"].get(stats["dominant_mood"], {})
        emoji = mood_config.get("emoji", "📝")
        
        return {
            "type": "daily",
            "date": date,
            "emoji": emoji,
            "entry_count": len(entries),
            **stats,
            "entries": entries
        }
    
    def generate_weekly_report(self, offset: int = 0) -> Dict:
        """生成周报"""
        start, end = self._get_date_range('week', offset)
        entries = self._get_entries_in_range(start, end)
        stats = self._calculate_mood_stats(entries)
        
        # 按天统计
        daily_scores = defaultdict(list)
        for e in entries:
            daily_scores[e["date"]].append(e.get("score", 5))
        
        daily_avg = {date: round(sum(scores)/len(scores), 1) for date, scores in daily_scores.items()}
        
        # 情绪emoji
        mood_config = self.config["moods"].get(stats["dominant_mood"], {})
        emoji = mood_config.get("emoji", "📝")
        
        # 生成建议
        suggestion = self._generate_suggestion(stats)
        
        return {
            "type": "weekly",
            "start_date": start,
            "end_date": end,
            "emoji": emoji,
            "entry_count": len(entries),
            "daily_scores": daily_avg,
            **stats,
            "suggestion": suggestion
        }
    
    def generate_monthly_report(self, offset: int = 0) -> Dict:
        """生成月报"""
        start, end = self._get_date_range('month', offset)
        entries = self._get_entries_in_range(start, end)
        stats = self._calculate_mood_stats(entries)
        
        # 按周统计
        weekly_stats = defaultdict(lambda: {"scores": [], "moods": []})
        for e in entries:
            date = datetime.strptime(e["date"], "%Y-%m-%d")
            week_num = date.isocalendar()[1]
            weekly_stats[f"第{week_num}周"]["scores"].append(e.get("score", 5))
            weekly_stats[f"第{week_num}周"]["moods"].append(e["mood"])
        
        weekly_summary = {}
        for week, data in weekly_stats.items():
            weekly_summary[week] = {
                "avg_score": round(sum(data["scores"]) / len(data["scores"]), 1),
                "dominant_mood": max(set(data["moods"]), key=data["moods"].count) if data["moods"] else "无"
            }
        
        # 情绪emoji
        mood_config = self.config["moods"].get(stats["dominant_mood"], {})
        emoji = mood_config.get("emoji", "📝")
        
        # 生成建议
        suggestion = self._generate_suggestion(stats)
        
        return {
            "type": "monthly",
            "start_date": start,
            "end_date": end,
            "emoji": emoji,
            "entry_count": len(entries),
            "weekly_summary": weekly_summary,
            **stats,
            "suggestion": suggestion
        }
    
    def generate_trend_analysis(self, days: int = 30) -> Dict:
        """生成趋势分析报告"""
        end_date = datetime.now()
        start_date = end_date - timedelta(days=days)
        
        entries = self._get_entries_in_range(
            start_date.strftime("%Y-%m-%d"),
            end_date.strftime("%Y-%m-%d")
        )
        
        if not entries:
            return {
                "type": "trend",
                "period_days": days,
                "has_data": False,
                "message": "该时间段内没有日记记录"
            }
        
        # 按周分组统计
        weekly_data = defaultdict(lambda: {"scores": [], "moods": []})
        for e in entries:
            date = datetime.strptime(e["date"], "%Y-%m-%d")
            week_key = date.strftime("%Y-W%W")
            weekly_data[week_key]["scores"].append(e.get("score", 5))
            weekly_data[week_key]["moods"].append(e["mood"])
        
        # 计算每周平均
        weekly_scores = {}
        for week, data in weekly_data.items():
            weekly_scores[week] = round(sum(data["scores"]) / len(data["scores"]), 1)
        
        # 趋势判断
        sorted_weeks = sorted(weekly_scores.keys())
        trend = "stable"
        trend_description = "情绪状态保持稳定"
        
        if len(sorted_weeks) >= 2:
            first_score = weekly_scores[sorted_weeks[0]]
            last_score = weekly_scores[sorted_weeks[-1]]
            
            if last_score > first_score + 1:
                trend = "improving"
                trend_description = "情绪状态呈上升趋势，保持积极！"
            elif last_score < first_score - 1:
                trend = "declining"
                trend_description = "情绪状态呈下降趋势，建议关注心理健康"
        
        # 找出情绪波动较大的时期（标准差大于2）
        volatility_periods = []
        for week, data in weekly_data.items():
            if len(data["scores"]) >= 2:
                mean = sum(data["scores"]) / len(data["scores"])
                variance = sum((x - mean) ** 2 for x in data["scores"]) / len(data["scores"])
                std_dev = variance ** 0.5
                if std_dev > 2:
                    volatility_periods.append({
                        "week": week,
                        "volatility": round(std_dev, 2)
                    })
        
        # 生成建议
        suggestion = self._generate_suggestion({
            "avg_score": sum(e.get("score", 5) for e in entries) / len(entries),
            "dominant_mood": max(set(e["mood"] for e in entries), key=lambda x: sum(1 for e in entries if e["mood"] == x)),
            "score_trend": trend
        })
        
        return {
            "type": "trend",
            "period_days": days,
            "has_data": True,
            "entry_count": len(entries),
            "weekly_scores": weekly_scores,
            "trend": trend,
            "trend_description": trend_description,
            "volatility_periods": volatility_periods,
            "suggestion": suggestion
        }
    
    def _generate_suggestion(self, stats: Dict) -> str:
        """根据统计数据生成建议"""
        avg_score = stats.get("avg_score", 5)
        trend = stats.get("score_trend", "stable")
        dominant_mood = stats.get("dominant_mood", "平静")
        
        suggestions = []
        
        # 基于平均评分
        if avg_score >= 8:
            suggestions.append("你的情绪状态非常好！保持这种积极的生活态度。")
        elif avg_score >= 6:
            suggestions.append("你的情绪状态不错，继续保持良好的生活节奏。")
        elif avg_score >= 4:
            suggestions.append("情绪状态一般，可以尝试增加一些让自己开心的活动。")
        else:
            suggestions.append("最近情绪状态偏低，建议多关注自己的心理健康，必要时寻求专业帮助。")
        
        # 基于趋势
        if trend == "improving":
            suggestions.append("情绪呈上升趋势，很棒！")
        elif trend == "declining":
            suggestions.append("情绪有些下滑，试着找些方式调节一下。")
        
        # 基于主导情绪
        if dominant_mood in ["焦虑", "难过", "愤怒"]:
            suggestions.append(f"最近{dominant_mood}情绪较多，试着找出原因并寻求解决方法。")
        elif dominant_mood in ["开心", "兴奋"]:
            suggestions.append("保持这份好心情！")
        
        return " ".join(suggestions)
    
    def format_report(self, report: Dict, format_type: str = "text") -> str:
        """格式化报告为可读文本"""
        if format_type == "json":
            return json.dumps(report, ensure_ascii=False, indent=2)
        
        lines = []
        
        # 标题
        emoji = report.get("emoji", "📝")
        if report["type"] == "daily":
            lines.append(f"{emoji} 心情日报 ({report['date']})")
        elif report["type"] == "weekly":
            lines.append(f"{emoji} 心情周报 ({report['start_date']} ~ {report['end_date']})")
        elif report["type"] == "monthly":
            lines.append(f"{emoji} 心情月报 ({report['start_date'][:7]})")
        elif report["type"] == "trend":
            lines.append(f"📊 情绪趋势分析 (近{report['period_days']}天)")
        
        lines.append("=" * 50)
        
        # 无数据情况
        if report.get("has_data") is False:
            lines.append(f"\n{report.get('message', '暂无数据')}")
            return "\n".join(lines)
        
        # 基本统计
        if "entry_count" in report:
            lines.append(f"📝 日记数量: {report['entry_count']} 篇")
        
        if "avg_score" in report:
            score_emoji = "😊" if report["avg_score"] >= 7 else "😐" if report["avg_score"] >= 5 else "😔"
            lines.append(f"{score_emoji} 平均情绪评分: {report['avg_score']}/10")
        
        if "dominant_mood" in report:
            lines.append(f"🎭 主导情绪: {report['dominant_mood']}")
        
        # 情绪分布
        if "mood_distribution" in report and report["mood_distribution"]:
            lines.append(f"\n📊 情绪分布:")
            for mood, count in sorted(report["mood_distribution"].items(), key=lambda x: x[1], reverse=True):
                mood_config = self.config["moods"].get(mood, {})
                mood_emoji = mood_config.get("emoji", "")
                lines.append(f"   {mood_emoji} {mood}: {count}篇")
        
        # 趋势信息
        if report["type"] == "trend":
            lines.append(f"\n📈 趋势: {report.get('trend_description', '')}")
            
            if report.get("volatility_periods"):
                lines.append(f"\n⚠️ 情绪波动较大的时期:")
                for period in report["volatility_periods"]:
                    lines.append(f"   {period['week']}: 波动指数 {period['volatility']}")
        
        # 每日/每周详情
        if "daily_scores" in report and report["daily_scores"]:
            lines.append(f"\n📅 每日情绪评分:")
            for date, score in sorted(report["daily_scores"].items()):
                lines.append(f"   {date}: {score}/10")
        
        if "weekly_summary" in report and report["weekly_summary"]:
            lines.append(f"\n📅 每周情绪概况:")
            for week, data in sorted(report["weekly_summary"].items()):
                lines.append(f"   {week}: {data['avg_score']}/10 ({data['dominant_mood']})")
        
        # 建议
        if "suggestion" in report:
            lines.append(f"\n💡 建议:")
            lines.append(f"   {report['suggestion']}")
        
        return "\n".join(lines)


def main():
    """命令行入口"""
    reporter = MoodReporter()
    
    if len(sys.argv) < 2:
        print("心情报告生成器 - xinqing-journal")
        print("用法: python mood-report.py <报告类型> [参数]\n")
        print("报告类型:")
        print("  daily [日期]        - 日报，如: daily 2024-01-15")
        print("  weekly [周偏移]     - 周报，0=本周, 1=上周")
        print("  monthly [月偏移]    - 月报，0=本月, 1=上月")
        print("  trend [天数]        - 趋势分析，默认30天")
        print("")
        sys.exit(0)
    
    command = sys.argv[1]
    
    try:
        if command == "daily":
            date = sys.argv[2] if len(sys.argv) > 2 else None
            report = reporter.generate_daily_report(date)
        
        elif command == "weekly":
            offset = int(sys.argv[2]) if len(sys.argv) > 2 else 0
            report = reporter.generate_weekly_report(offset)
        
        elif command == "monthly":
            offset = int(sys.argv[2]) if len(sys.argv) > 2 else 0
            report = reporter.generate_monthly_report(offset)
        
        elif command == "trend":
            days = int(sys.argv[2]) if len(sys.argv) > 2 else 30
            report = reporter.generate_trend_analysis(days)
        
        else:
            print(f"❌ 未知报告类型: {command}")
            print("可用类型: daily, weekly, monthly, trend")
            sys.exit(1)
        
        # 输出报告
        format_type = sys.argv[-1] if sys.argv[-1] in ["text", "json"] else "text"
        print(reporter.format_report(report, format_type))
    
    except Exception as e:
        print(f"❌ 生成报告失败: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)