Skill: akayj/mwc (version 1.0.1)

--- name: mac-wallpaper-changer description: > 自动更换 Mac 壁纸、壁纸智能推荐与偏好学习。凡涉及壁纸相关操作，都应优先使用此技能：换壁纸、更换桌面背景、 Mac wallpaper、自动换壁纸（cron/定时任务）、按关键词搜索壁纸（如"上海夜景"、"mountain sunset"）、 壁纸评分与喜好统计、壁纸推荐、位置配置。支持 Bing 每日精选 / Unsplash / Picsum 多图源， 支持本地图片、URL 直接设置，兼容 macOS Tahoe（26+）。 --- # Mac 壁纸随心换 自动为 macOS 更换高质量壁纸，通过评分学习偏好并智能推荐。 ## 环境要求 - macOS - uv（用于运行 Python 脚本） ## 脚本一览 | 脚本 | 作用 | | ----------------------- | ----------------------- | | `scripts/change.py` | 换壁纸 | | `scripts/recommend.py` | 推荐壁纸 | | `scripts/preference.py` | 偏好管理（评分 + 统计） | | `scripts/loc.py` | 位置配置 | | `scripts/daily.py` | 每日自动（cron 用） | ## 图源优先级 **Unsplash → Bing → Picsum**（可用 `--source` 强制指定） Unsplash 画质更高且支持主题搜索，始终作为首选；Bing 每日精选作为兜底；Picsum 为最终保底。 ## 核心命令 ```bash # 换壁纸（Unsplash 随机高清图） uv run scripts/change.py # 换壁纸（Unsplash 主题搜索） uv run scripts/change.py --query "Shanghai rain" uv run scripts/change.py --category mountain --file ~/Pictures/x.jpg # 偏好管理（无参数=列出评分） uv run scripts/preference.py uv run scripts/preference.py add # 对当前壁纸评分 uv run scripts/preference.py add path/to.jpg # 对指定文件评分 uv run scripts/preference.py set 3 9 # 第 3 条改为 9 分 uv run scripts/preference.py del 5 # 删除第 5 条 uv run scripts/preference.py stats # 完整统计报告 # 推荐壁纸 uv run scripts/recommend.py uv run scripts/recommend.py -y # 自动应用 uv run scripts/recommend.py -n # 仅显示 # 位置 uv run scripts/loc.py uv run scripts/loc.py --set-location "北京" ``` ## 数据存储 `~/wallpaper-daily/`：`YYYY-MM-DD/` 壁纸、`preferences.parquet` 评分、`logs/` ## Cron ```bash 30 10 * * * cd /path/to/mac-wallpaper-changer && uv run scripts/change.py ``` ## References & Assets | 路径 | 说明 | | --------------------------------------- | ------------------------------------ | | `references/wallpaper-sources.md` | 壁纸源配置（Bing/Unsplash/Picsum） | | `references/embedding-config.md` | Embedding 配置 Schema 与提供商说明 | | `assets/embedding-config.template.json` | Embedding 配置模板（高级智能推荐用） | 高级智能推荐（规划）：基于 embedding 的语义相似度推荐，复制 `assets/embedding-config.template.json` 并填入 `api_key` 等。 ## 故障排查 - **壁纸不生效**：脚本已处理 Tahoe 26+，使用 killall WallpaperAgent - **未刷新**：系统设置 > 墙纸 > 在所有空间中显示 - **日志**：`tail -f ~/wallpaper-daily/logs/change-*.log`

path: assets/embedding-config.template.json
content: {
  "embedding": {
    "provider": "openai",
    "model": "text-embedding-3-small",
    "dimensions": 1536,
    "api_key": "your-api-key-here",
    "endpoint": null,
    "batch_size": 32,
    "metadata_fields": ["title", "tags", "category", "mood", "color_tone"]
  }
}


path: scripts/loc.py
content: #!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12,<3.13"
# dependencies = []
# ///
"""
地理位置管理脚本

逻辑：用户配置 > 默认上海
提示：可通过 --set-location 设置城市
"""

import json
from pathlib import Path
from datetime import datetime

WALLPAPER_DIR = Path.home() / "wallpaper-daily"
LOCATION_CONFIG = WALLPAPER_DIR / "location.json"

DEFAULT_LOCATION = {
    "city": "上海",
    "region": "中国",
    "country": "China",
    "timezone": "Asia/Shanghai"
}


def get_location() -> dict:
    """获取位置：用户配置 > 默认上海"""
    if LOCATION_CONFIG.exists():
        try:
            with open(LOCATION_CONFIG) as f:
                config = json.load(f)
                if config.get("city"):
                    config["source"] = "user_config"
                    return config
        except Exception:
            pass
    
    location = DEFAULT_LOCATION.copy()
    location["source"] = "default"
    return location


def set_location(city: str, region: str = "", country: str = "") -> None:
    """保存用户配置的位置"""
    config = {
        "city": city,
        "region": region or city,
        "country": country or "China",
        "configured_at": datetime.now().isoformat()
    }
    WALLPAPER_DIR.mkdir(exist_ok=True)
    with open(LOCATION_CONFIG, 'w') as f:
        json.dump(config, f, ensure_ascii=False, indent=2)
    print(f"✓ 已设置位置：{city}")


def get_weather_suggestion() -> str:
    """根据时间建议壁纸风格"""
    hour = datetime.now().hour
    return "bright" if 6 <= hour < 18 else "dark"


def main():
    import argparse
    
    parser = argparse.ArgumentParser(
        description="地理位置管理（用于壁纸推荐）",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
提示：默认位置为上海，可通过 --set-location 设置

Examples:
  uv run scripts/loc.py
  uv run scripts/loc.py --set-location "北京"
  uv run scripts/loc.py --json
"""
    )
    parser.add_argument("--json", action="store_true", help="输出 JSON 格式")
    parser.add_argument("--suggest", action="store_true", help="输出壁纸风格建议")
    parser.add_argument("--set-location", type=str, metavar="CITY", 
                        help="设置城市（如：北京、深圳）")
    args = parser.parse_args()
    
    if args.set_location:
        set_location(args.set_location)
        return 0
    
    location = get_location()
    
    if args.json:
        print(json.dumps(location, ensure_ascii=False, indent=2))
    elif args.suggest:
        print(get_weather_suggestion())
    else:
        source_hint = "（默认）" if location.get("source") == "default" else "（已配置）"
        print(f"📍 {location.get('city')}{source_hint}")
        if location.get("source") == "default":
            print(f"   提示：使用 --set-location 设置您的城市")
    
    return 0


if __name__ == "__main__":
    exit(main())


path: scripts/recommend.py
content: #!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12,<3.13"
# dependencies = [
#     "polars>=1.0.0",
#     "duckdb>=1.0.0",
#     "pyobjc-framework-Cocoa>=10.0",
# ]
# ///
"""
智能壁纸推荐脚本
使用 polars + duckdb 分析用户偏好，推荐"猜你喜欢"的壁纸
"""

import polars as pl
import duckdb
import urllib.request
from pathlib import Path
from datetime import datetime
import random

PREFERENCES_FILE = Path.home() / "wallpaper-daily" / "preferences.parquet"
WALLPAPER_DIR = Path.home() / "wallpaper-daily"

# 壁纸源配置
WALLPAPER_SOURCES = {
    "nature": ["https://picsum.photos/3840/2160?random={seed}&nature"],
    "mountain": ["https://picsum.photos/3840/2160?random={seed}&mountain"],
    "forest": ["https://picsum.photos/3840/2160?random={seed}&forest"],
    "ocean": ["https://picsum.photos/3840/2160?random={seed}&ocean"],
    "city": ["https://picsum.photos/3840/2160?random={seed}&city"],
    "space": ["https://picsum.photos/3840/2160?random={seed}&space"],
}


def load_preferences() -> pl.DataFrame | None:
    """加载偏好数据"""
    if not PREFERENCES_FILE.exists():
        print("❌ 偏好数据不足，无法推荐")
        print("提示：先使用换壁纸功能并评分至少 5 次")
        return None
    
    try:
        df = pl.read_parquet(PREFERENCES_FILE)
        if len(df) < 5:
            print(f"❌ 数据量不足（当前：{len(df)}条，需要：≥5 条）")
            return None
        return df
    except Exception as e:
        print(f"❌ 读取数据失败：{e}")
        return None


def recommend_with_duckdb(df: pl.DataFrame, user_tags: list[str] = None) -> list[dict]:
    """使用 duckdb 进行智能推荐"""
    con = duckdb.connect()
    con.register('preferences', df)
    
    # 综合推荐算法
    query = """
        WITH category_stats AS (
            SELECT 
                category,
                AVG(rating) as avg_rating,
                MAX(rating) as max_rating,
                COUNT(*) as rated_count,
                SUM(CASE WHEN rating >= 7 THEN 1 ELSE 0 END) as liked_count
            FROM preferences
            GROUP BY category
        ),
        tone_stats AS (
            SELECT 
                color_tone,
                AVG(rating) as avg_rating
            FROM preferences
            WHERE color_tone != 'unknown'
            GROUP BY color_tone
        ),
        recent_trend AS (
            SELECT 
                category,
                AVG(rating) as recent_avg
            FROM preferences
            WHERE date >= EXTRACT(EPOCH FROM NOW()) - 86400 * 7
            GROUP BY category
        )
        SELECT 
            cs.category,
            cs.avg_rating as base_score,
            cs.max_rating,
            cs.liked_count * 1.0 / cs.rated_count as like_ratio,
            COALESCE(rt.recent_avg, cs.avg_rating) as recent_score,
            ts.avg_rating as tone_bonus
        FROM category_stats cs
        LEFT JOIN recent_trend rt ON cs.category = rt.category
        LEFT JOIN tone_stats ts ON ts.color_tone = (
            SELECT color_tone FROM preferences 
            WHERE category = cs.category AND rating >= 7
            ORDER BY rating DESC LIMIT 1
        )
        ORDER BY 
            (cs.avg_rating * 0.4 + 
             COALESCE(rt.recent_avg, cs.avg_rating) * 0.4 + 
             COALESCE(ts.avg_rating, 5) * 0.2) DESC
        LIMIT 3
    """
    
    result = con.execute(query).fetchall()
    con.close()
    
    if not result:
        return []
    
    recommendations = []
    for row in result:
        category, base_score, max_rating, like_ratio, recent_score, tone_bonus = row
        # 综合得分
        final_score = base_score * 0.4 + recent_score * 0.4 + (tone_bonus or 5) * 0.2
        recommendations.append({
            'category': category,
            'score': final_score,
            'base_score': base_score,
            'recent_score': recent_score,
            'like_ratio': like_ratio,
        })
    
    return recommendations


def download_recommended_wallpaper(category: str) -> Path | None:
    """下载推荐的壁纸"""
    seed = random.randint(1, 10000)
    
    if category in WALLPAPER_SOURCES:
        url_template = random.choice(WALLPAPER_SOURCES[category])
    else:
        url_template = "https://picsum.photos/3840/2160?random={seed}"
    
    url = url_template.format(seed=seed)
    
    today = datetime.now().strftime("%Y-%m-%d")
    today_dir = WALLPAPER_DIR / today
    today_dir.mkdir(exist_ok=True)
    
    filename = f"recommended-{category}-{datetime.now().strftime('%H%M%S')}.jpg"
    filepath = today_dir / filename
    
    try:
        headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'}
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req, timeout=30) as response:
            with open(filepath, 'wb') as f:
                f.write(response.read())
        
        if filepath.stat().st_size > 100 * 1024:
            print(f"✓ 下载成功：{filename}")
            return filepath
        else:
            filepath.unlink()
            return None
    except Exception as e:
        print(f"❌ 下载失败：{e}")
        return None


def set_wallpaper(image_path: Path) -> bool:
    """
    设置壁纸（macOS Tahoe 兼容）
    使用 PyObjC + killall WallpaperAgent
    """
    import subprocess
    
    abs_path = str(image_path.absolute())
    
    # 方法 1: PyObjC + NSWorkspace
    try:
        from AppKit import NSWorkspace, NSScreen
        from Foundation import NSURL
        
        url = NSURL.fileURLWithPath_(abs_path)
        workspace = NSWorkspace.sharedWorkspace()
        
        success = False
        for screen in NSScreen.screens():
            result, _ = workspace.setDesktopImageURL_forScreen_options_error_(
                url, screen, {}, None
            )
            if result:
                success = True
        
        if success:
            # macOS Tahoe: 重启 WallpaperAgent 让壁纸生效
            import time
            time.sleep(0.3)
            subprocess.run(['killall', 'WallpaperAgent'], capture_output=True, timeout=10)
            return True
            
    except ImportError:
        pass
    except Exception:
        pass
    
    # 方法 2: desktoppr 回退
    try:
        result = subprocess.run(['desktoppr', abs_path], capture_output=True, timeout=30)
        if result.returncode == 0:
            subprocess.run(['killall', 'WallpaperAgent'], capture_output=True, timeout=10)
            return True
    except FileNotFoundError:
        pass
    
    return False


def show_recommendations(recommendations: list[dict], df: pl.DataFrame) -> None:
    """显示推荐结果"""
    print("\n" + "=" * 60)
    print("🎯 猜你喜欢 - 智能推荐")
    print("=" * 60)
    
    # 用户偏好总结
    print("\n📊 你的偏好分析:")
    avg_rating = df['rating'].mean()
    top_category = df.group_by('category').agg(
        pl.col('rating').mean().alias('avg')
    ).sort('avg', descending=True).row(0)[0]
    
    print(f"  平均评分：{avg_rating:.1f}/10")
    print(f"  最爱分类：{top_category}")
    print(f"  评分次数：{len(df)}次")
    
    # 推荐列表
    print("\n🏆 推荐分类:")
    for i, rec in enumerate(recommendations, 1):
        stars = "⭐" * int(rec['score'] / 2)
        print(f"\n  {i}. {rec['category'].upper()}")
        print(f"     推荐指数：{stars} ({rec['score']:.2f}分)")
        print(f"     历史平均：{rec['base_score']:.1f}分")
        print(f"     近期趋势：{rec['recent_score']:.1f}分")
        print(f"     喜好比例：{rec['like_ratio']*100:.0f}%")
    
    # 标签建议
    print("\n🔖 热门标签:")
    all_tags = []
    for tags in df.filter(pl.col('rating') >= 7)['tags']:
        all_tags.extend([t.lower() for t in tags])
    
    if all_tags:
        from collections import Counter
        tag_counts = Counter(all_tags).most_common(5)
        for tag, count in tag_counts:
            print(f"    #{tag} ({count}次)")


def main():
    import argparse
    
    parser = argparse.ArgumentParser(
        description="基于评分数据智能推荐壁纸",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  uv run scripts/recommend.py
  uv run scripts/recommend.py -n   # 仅显示
  uv run scripts/recommend.py -y   # 自动应用
"""
    )
    parser.add_argument("--tags", type=str, help="偏好标签（逗号分隔）")
    parser.add_argument("-y", "--yes", action="store_true", help="自动应用，不询问")
    parser.add_argument("-n", "--no-apply", action="store_true", help="仅显示，不下载")
    parser.add_argument("--json", action="store_true", help="JSON 输出")
    args = parser.parse_args()
    
    # 加载数据
    df = load_preferences()
    if df is None:
        return 1
    
    # 计算推荐
    recommendations = recommend_with_duckdb(df, args.tags.split(",") if args.tags else None)
    
    if not recommendations:
        print("❌ 无法生成推荐，数据不足")
        return 1
    
    # JSON 输出模式
    if args.json:
        import json
        output = {
            "recommendations": recommendations,
            "total_ratings": len(df),
            "avg_rating": float(df['rating'].mean())
        }
        print(json.dumps(output, ensure_ascii=False, indent=2))
        return 0
    
    print("🦞 Mac 壁纸智能推荐系统")
    print(f"数据文件：{PREFERENCES_FILE}")
    
    # 显示推荐
    show_recommendations(recommendations, df)
    
    # 应用推荐
    if not args.no_apply:
        top_rec = recommendations[0]
        print(f"\n🎨 正在下载推荐壁纸：{top_rec['category']}...")
        
        filepath = download_recommended_wallpaper(top_rec['category'])
        if filepath:
            if args.yes or input("\n是否设置为壁纸？(y/n): ").lower() == 'y':
                if set_wallpaper(filepath):
                    print("✓ 壁纸已更新")
                    return 0
                else:
                    print("❌ 设置壁纸失败")
            else:
                print("✓ 壁纸已下载，未设置")
        else:
            print("❌ 下载失败")
    
    return 0


if __name__ == "__main__":
    exit(main())


path: references/embedding-config.md
content: # Embedding 配置（高级智能推荐）

配置模板：`assets/embedding-config.template.json`

## 配置位置

- `~/.wallpaper_prefs.json` 的 `embedding` 字段
- 或环境变量 `WALLPAPER_EMBEDDING_CONFIG` 指向的 JSON 路径

## Schema

| 字段              | 类型     | 说明                                                     |
| ----------------- | -------- | -------------------------------------------------------- |
| `provider`        | string   | `openai` / `ollama` / `sentence-transformers` / `custom` |
| `model`           | string   | 模型名称                                                 |
| `api_key`         | string   | API Key（OpenAI/custom 需填）                            |
| `dimensions`      | int      | 向量维度（可选）                                         |
| `endpoint`        | string   | 自定义端点（ollama/custom 需填）                         |
| `metadata_fields` | string[] | 参与 embedding 的偏好字段                                |

## 提供商示例

**OpenAI**：`provider: openai`，`api_key` 必填  
**Ollama**：`provider: ollama`，`endpoint: http://localhost:11434/api/embeddings`  
**Sentence-Transformers**：`provider: sentence-transformers`，无需 api_key  
**Custom**：`provider: custom`，`endpoint` + `api_key` 必填


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