个人自动化任务集合,通过 macOS launchd 定时调度,自动执行日常任务(日报生成、邮箱整理、桌面整理、科技新闻聚合)并同步到多个平台(Git、Notion、Feishu、Obsidian)
个人自动化任务集合,通过 macOS launchd 定时调度,自动执行日常任务并同步到多个平台。
这个技能包含四个自动化任务,通过 macOS 的 launchd 守护进程定时触发:
1. **日报生成** (00:00) - 从 Git 提交和 Claude Code 会话生成每日工作报告
2. **邮箱整理** (02:00) - 使用 himalaya CLI 自动归档 Gmail 通知类邮件
3. **桌面整理** (04:00) - 按文件类型分类整理桌面和下载目录
4. **科技新闻** (07:00) - 聚合 Hacker News 和 GitHub Trending 生成早报
所有任务输出支持同步到 Obsidian、Feishu、Notion、Git。
设置以下目录结构:
```
zhimeng-agent/
├── tasks/
│ ├── config.py # 统一配置
│ ├── sync_utils.py # 多平台同步工具
│ ├── daily_report/ # 日报生成任务
│ │ ├── __init__.py
│ │ └── main.py
│ ├── email_organizer/ # 邮箱整理任务
│ │ ├── __init__.py
│ │ └── main.py
│ ├── desktop_organizer/ # 桌面整理任务
│ │ ├── __init__.py
│ │ └── main.py
│ ├── tech_news/ # 科技新闻任务
│ │ ├── __init__.py
│ │ └── main.py
│ └── launchd/ # macOS 定时任务配置
│ ├── install.sh
│ ├── com.zhimeng.daily-report.plist
│ ├── com.zhimeng.email-organizer.plist
│ ├── com.zhimeng.desktop-organizer.plist
│ └── com.zhimeng.tech-news.plist
├── logs/ # 任务运行日志
├── pyproject.toml # Poetry 依赖配置
├── .env # 环境变量(不提交到 Git)
└── SKILL.md # 本技能文档
```
创建项目目录并设置 Poetry 环境:
```bash
mkdir -p zhimeng-agent/tasks/{daily_report,email_organizer,desktop_organizer,tech_news,launchd}
mkdir -p zhimeng-agent/logs
cd zhimeng-agent
poetry init
poetry add requests python-dotenv
```
创建 `tasks/config.py` 作为全局配置:
```python
from dataclasses import dataclass
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
@dataclass
class TaskConfig:
# 调度时间
DAILY_REPORT_HOUR: int = 0
EMAIL_ORGANIZE_HOUR: int = 2
DESKTOP_ORGANIZE_HOUR: int = 4
TECH_NEWS_HOUR: int = 7
# 路径配置
IDEAS_ROOT: Path = Path.home() / "workspace/QAL/ideas"
OBSIDIAN_VAULT: Path = Path.home() / "Documents/Obsidian Vault"
DESKTOP: Path = Path.home() / "Desktop"
DOWNLOADS: Path = Path.home() / "Downloads"
# API 密钥
FEISHU_APP_ID: str = os.getenv("FEISHU_APP_ID", "")
FEISHU_APP_SECRET: str = os.getenv("FEISHU_APP_SECRET", "")
FEISHU_RECIPIENT_OPEN_ID: str = os.getenv("FEISHU_RECIPIENT_OPEN_ID", "")
NOTION_TOKEN: str = os.getenv("NOTION_TOKEN", "")
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
config = TaskConfig()
```
创建 `tasks/sync_utils.py` 作为多平台同步工具:
```python
from pathlib import Path
from datetime import datetime
import requests
from tasks.config import config
class PlatformSyncer:
def sync_content(self, title: str, content: str, targets: list[str], **kwargs):
"""同步内容到多个平台"""
results = {}
if "obsidian" in targets:
results["obsidian"] = self._sync_obsidian(title, content, kwargs.get("obsidian_folder", "Journal"))
if "feishu" in targets:
results["feishu"] = self._sync_feishu(title, content)
if "git" in targets:
results["git"] = self._sync_git(title, content, kwargs.get("git_repo"))
return results
def _sync_obsidian(self, title: str, content: str, folder: str):
file_path = config.OBSIDIAN_VAULT / folder / f"{title}.md"
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return {"status": "success", "path": str(file_path)}
def _sync_feishu(self, title: str, content: str):
if not config.FEISHU_APP_ID:
return {"status": "skipped", "reason": "missing config"}
# 实现飞书消息发送逻辑
return {"status": "success"}
def _sync_git(self, title: str, content: str, repo_path: Path):
# 实现 Git 提交逻辑
return {"status": "success"}
def create_syncer() -> PlatformSyncer:
return PlatformSyncer()
```
**日报生成** (`tasks/daily_report/main.py`):
```python
from datetime import datetime
from pathlib import Path
import subprocess
from tasks.config import config
from tasks.sync_utils import create_syncer
class DailyReportRunner:
def run(self, dry_run: bool = False):
date_str = datetime.now().strftime("%Y-%m-%d")
# 收集 Git 提交
commits = self._get_git_commits(date_str)
# 收集 Claude Code 会话
sessions = self._get_claude_sessions(date_str)
# 生成报告
content = self._generate_report(date_str, commits, sessions)
if not dry_run:
syncer = create_syncer()
syncer.sync_content(
title=f"daily-report-{date_str}",
content=content,
targets=["obsidian", "feishu"],
obsidian_folder="Journal"
)
return content
def _get_git_commits(self, date: str) -> list:
# 实现 Git 提交提取逻辑
return []
def _get_claude_sessions(self, date: str) -> list:
# 实现 Claude Code 会话提取逻辑
return []
def _generate_report(self, date: str, commits: list, sessions: list) -> str:
return f"# {date} 工作日报\n\n## 代码提交\n\n## Claude Code 会话\n"
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
runner = DailyReportRunner()
runner.run(dry_run=args.dry_run)
if __name__ == "__main__":
main()
```
**科技新闻** (`tasks/tech_news/main.py`):
```python
import requests
from datetime import datetime
from tasks.config import config
from tasks.sync_utils import create_syncer
class TechNewsRunner:
def run(self, hn_count: int = 15, gh_count: int = 10, dry_run: bool = False):
date_str = datetime.now().strftime("%Y-%m-%d")
# 获取 Hacker News
hn_stories = self._get_hacker_news(hn_count)
# 获取 GitHub Trending
gh_repos = self._get_github_trending(gh_count)
# 生成报告
content = self._generate_report(date_str, hn_stories, gh_repos)
if not dry_run:
syncer = create_syncer()
syncer.sync_content(
title=f"tech-news-{date_str}",
content=content,
targets=["obsidian", "feishu"],
obsidian_folder="News"
)
return content
def _get_hacker_news(self, count: int) -> list:
resp = requests.get("https://hacker-news.firebaseio.com/v0/topstories.json")
story_ids = resp.json()[:count]
stories = []
for story_id in story_ids:
story_resp = requests.get(f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json")
stories.append(story_resp.json())
return stories
def _get_github_trending(self, count: int) -> list:
# 实现 GitHub Trending 抓取逻辑
return []
def _generate_report(self, date: str, hn_stories: list, gh_repos: list) -> str:
report = f"# {date} 科技早报\n\n## Hacker News 热门\n"
for i, story in enumerate(hn_stories, 1):
report += f"{i}. [{story.get('title')}]({story.get('url')})\n"
return report
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--hn-count", type=int, default=15)
parser.add_argument("--gh-count", type=int, default=10)
args = parser.parse_args()
runner = TechNewsRunner()
runner.run(hn_count=args.hn_count, gh_count=args.gh_count, dry_run=args.dry_run)
if __name__ == "__main__":
main()
```
**邮箱整理** 和 **桌面整理** 任务按类似模式实现。
创建 `tasks/launchd/com.zhimeng.tech-news.plist`(其他任务类似):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.zhimeng.tech-news</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/poetry/virtualenvs/bin/python</string>
<string>-m</string>
<string>tasks.tech_news.main</string>
</array>
<key>WorkingDirectory</key>
<string>/path/to/zhimeng-agent</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>7</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/path/to/zhimeng-agent/logs/tech-news.log</string>
<key>StandardErrorPath</key>
<string>/path/to/zhimeng-agent/logs/tech-news.error.log</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
```
创建 `tasks/launchd/install.sh` 安装脚本:
```bash
#!/bin/bash
LAUNCHD_DIR=~/Library/LaunchAgents
TASKS=("daily-report" "email-organizer" "desktop-organizer" "tech-news")
install() {
for task in "${TASKS[@]}"; do
cp com.zhimeng.$task.plist $LAUNCHD_DIR/
launchctl load $LAUNCHD_DIR/com.zhimeng.$task.plist
echo "Installed: com.zhimeng.$task"
done
}
uninstall() {
for task in "${TASKS[@]}"; do
launchctl unload $LAUNCHD_DIR/com.zhimeng.$task.plist
rm $LAUNCHD_DIR/com.zhimeng.$task.plist
echo "Uninstalled: com.zhimeng.$task"
done
}
status() {
launchctl list | grep zhimeng
}
run() {
launchctl start com.zhimeng.$1
}
case "$1" in
install) install ;;
uninstall) uninstall ;;
status) status ;;
run) run "$2" ;;
*) echo "Usage: $0 {install|uninstall|status|run <task>}" ;;
esac
```
```bash
poetry env info --path
chmod +x tasks/launchd/install.sh
cd tasks/launchd
./install.sh install
./install.sh status
./install.sh run tech-news
tail -f ../../logs/tech-news.log
```
在项目根目录创建 `.env` 文件(不要提交到 Git):
```bash
FEISHU_APP_ID=your_app_id
FEISHU_APP_SECRET=your_app_secret
FEISHU_RECIPIENT_OPEN_ID=your_open_id
NOTION_TOKEN=your_token
OPENAI_API_KEY=your_api_key
```
在 `tasks/config.py` 中调整路径和时间配置。
**任务未执行:**
```bash
launchctl list | grep zhimeng
log show --predicate 'subsystem == "com.apple.launchd"' --last 1h | grep zhimeng
launchctl start com.zhimeng.tech-news
```
**Python 环境问题:**
```bash
poetry env info --path
poetry run python -c "from tasks.config import config; print(config)"
```
添加新任务的步骤:
1. 创建任务目录和 `main.py`
2. 实现 `TaskRunner` 类和 `main()` 函数
3. 使用 `sync_utils.create_syncer()` 同步输出
4. 创建对应的 `.plist` 文件
5. 更新 `install.sh` 中的 `TASKS` 数组
6. 运行 `./install.sh install`
**科技新闻报告:**
```markdown
1. [Show HN: I built a personal task automation agent](https://example.com) (320分, 45评论)
2. [The State of AI in 2026](https://example.com) (280分, 32评论)
> Personal task automation framework for macOS
```
**日报:**
```markdown
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/zhimeng-agent-gzqzrf/raw