---
name: sophnet-schedule
description: >
  Sophnet 日程提醒：Google Calendar 实时同步、本地托底存储、定时提醒、日程变化监控、固定任务管理。
  全自动运行，无需用户每日操作；同时支持用户主动查询日程、检查冲突、管理任务。
  支持双模式：已授权 Google Calendar 时云端同步，未授权时本地存储，功能完全可用。
  触发场景：用户说"今天有什么安排"、"查一下日程冲突"、"帮我加个定时任务"、
  "手动生成今日计划"、"有没有日程变化"等。
metadata:
  {
    "openclaw":
      {
        "emoji": "⏰",
        "requires":
          {
            "bins": ["python3", "uv"],
            "python_packages": ["requests", "pyyaml", "urllib3"],
          },
      },
  }
---

## 加载时说明

当用户启用此 skill 时，向用户发送以下欢迎语（语气友好，使用 Markdown）：

> ⏰ **自动日程调度已就绪！** 每天自动运行：
>
> - **07:50** 生成今日计划 + 注册提醒
> - **08:00** 推送今日日程早报
> - **全天** 每小时监控日程变化（有变化才通知）
> - **会前提醒** 会议提前 2 分钟、运动/饭局提前 15 分钟自动提醒
> - **21:00** 推送今日工作总结
>
> 💡 **支持 Google Calendar + 本地托底**：
> - 已授权 Google Calendar → 日程自动同步到云端
> - 未授权 → 日程保存到本地存储，功能完全可用
>
> 你也可以随时说：
> - 📝 **"明天下午3点牙医预约"** → 我帮你添加日程
> - 📅 **"今天有什么安排"** → 查询今日日程
> - ⚠️ **"有没有日程冲突"** → 检测时间重叠
> - ➕ **"帮我加个定时任务"** → 新增每日提醒

---

# Sophnet 日程提醒

Sophnet 日程提醒系统。基于 Google Calendar API 自动同步日程，配置驱动，无需每日手动操作。

---

## 工作模式

系统支持两种工作模式，自动选择：

| 模式 | 条件 | 行为 |
|------|------|------|
| **Google 模式** | 已完成 Google OAuth 授权（`token.json` 存在且有效） | 日程写入 Google Calendar，数据云端同步 |
| **本地模式** | 未完成 Google OAuth 授权 | 日程写入本地存储（`data/local_events.json`），作为托底方案 |

**模式切换逻辑**：
- 首次使用时，如果没有 Google Calendar 授权，系统自动使用本地模式
- 完成授权后，可以通过"同步本地事件到 Google"将本地已有日程推送到 Google Calendar
- 后续新日程会自动写入 Google Calendar

**Agent 操作要点**：
- 调用 `add_event.py` 后，检查返回 JSON 中的 `provider` 字段
- `provider == "google"` → 告知用户"已写入 Google Calendar"
- `provider == "local"` → 告知用户"已写入本地存储"，并提示可授权 Google Calendar

---

## 一、自动化功能概览

以下任务由 OpenClaw Cron 按时自动执行，**用户无需任何操作**：

| 时间 | 任务 | 说明 |
|------|------|------|
| 07:50 | 每日计划生成 | 拉取 GCal 日程，注册今日提醒，生成 `daily/cron-YYYYMMDD.md` |
| 08:00 | 日程早报推送 | 推送今日全部日程到 Telegram |
| 10:00 | 喝水提醒 | 定时健康提醒 |
| 每小时整点（8:00–20:00） | 日程变化监控 | 对比快照，有变化时通知，无变化不打扰 |
| 21:00 | 今日工作总结 | 整理当天完成事项 |

**会前提醒规则**（从 `config/reminder_rules.yaml` 加载）：

- 会议：提前 **2 分钟**
- 运动：提前 **15 分钟**
- 饭局/宴会：提前 **15 分钟**
- 其他：提前 **5 分钟**（默认）

---

## 二、用户交互指令

### 2.1 自然语言输入日程 → 写入 Google Calendar

**触发词**：用户用自然语言描述一个新日程，如：
- "明天下午3点牙医预约"
- "下周六上午10点亲子游泳课，提前30分钟提醒我"
- "4月5号早上7点跑步，45分钟"
- "帮我记一下后天下午2点到4点的家庭聚餐，小区附近"

**Agent 必须主动进入录入流程，不要等用户说"帮我加到日历"。**

#### Step 1：解析自然语言，展示确认卡片

从用户的话中提取以下字段：

| 字段 | 解析规则 |
|------|----------|
| 事件名称 | 用户描述的事项 |
| 日期 | 今天/明天/后天 → 计算具体日期；周X/下周X → 计算日期；M月D日 → 当年日期 |
| 开始时间 | 上午/下午+时间 → 24小时制；X点 = X:00；X点半 = X:30；X点Y分 = X:Y |
| 结束时间 | 明确说了结束时间则用；说了时长则推算；都没说默认 1 小时 |
| 地点 | 提到地点则填写；未提则留空 |
| 提醒时间 | 用户明确要求提前X分钟 → 使用该值；否则按事件类型默认（会议2分钟，运动/饭局15分钟，其他5分钟） |
| 备注 | 其他补充说明 |

向用户展示确认卡片：

```
📅 我帮你记录了这条日程，请确认：

| 字段     | 内容                    |
|----------|-------------------------|
| 事件     | 牙医预约                |
| 日期     | 2026-04-16（周四）      |
| 时间     | 15:00 – 16:00           |
| 地点     | 龙湖口腔                |
| 提前提醒 | 15 分钟（14:45 提醒）   |
| 备注     |（未填）                 |

✅ 确认写入 Google Calendar  ✏️ 修改某项  ❌ 取消
```

**等待用户确认后再执行 Step 2。**

#### Step 2：调用 add_event.py 写入日程

用户确认后（或说"确认"/"好的"/"写入"等），执行：

```bash
python3 {SKILL_BASE}/apps/add_event.py \
  --summary  "牙医预约" \
  --date     "2026-04-16" \
  --start    "15:00" \
  --end      "16:00" \
  --location "龙湖口腔" \
  --remind   15
```

**参数说明**：
- `--summary`：事件标题（必填）
- `--date`：日期 YYYY-MM-DD（必填）
- `--start`：开始时间 HH:MM（必填）
- `--end`：结束时间 HH:MM（与 `--duration` 二选一）
- `--duration`：持续分钟数（默认 60）
- `--location`：地点（可选）
- `--description`：备注（可选）
- `--remind`：提前提醒分钟数（0 = 不注册提醒）
- `--dry-run`：只打印 payload，不写入（测试用）

**重要**：脚本会根据当前是否有有效的 Google Calendar 授权自动选择：
- 已授权 → 写入 Google Calendar
- 未授权 → 写入本地存储（本地托底）

返回 JSON 中的关键字段：
- `provider`：`"google"` 或 `"local"`，指示写入位置
- `sync_state`：同步状态（本地模式下为 `"local_only"`）
- `event_id`：事件 ID
- `reminder_registered`：提醒是否成功注册（`true`/`false`）
- `reminder_existed`：提醒是否已存在（`true`/`false`）
- `reminder_error`：提醒注册错误信息（如果有）

#### Step 3：读取输出，告知结果

脚本输出 JSON，Agent 读取后根据 `provider` 和提醒注册状态向用户回复：

**成功 - 写入 Google Calendar，提醒已注册**（当 `provider == "google"` 且 `reminder_registered == true`）：
```
✅ 已写入 Google Calendar！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔
⏰ 将在 14:45 提醒你（提前 15 分钟）
```

**成功 - 写入 Google Calendar，提醒注册失败**（当 `provider == "google"` 但 `reminder_registered == false`）：
```
✅ 已写入 Google Calendar！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔

⚠️ 提醒注册失败：{reminder_error 内容}
   日程已保存，但不会收到自动提醒。
```

**成功 - 写入本地存储，提醒已注册**（当 `provider == "local"` 且 `reminder_registered == true`）：
```
✅ 已写入本地存储！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔
⏰ 将在 14:45 提醒你（提前 15 分钟）

💡 提示：当前未连接 Google Calendar，日程已保存到本地。
    如需同步到 Google Calendar，请完成 Google OAuth 授权。
```

**成功 - 写入本地存储，提醒注册失败**（当 `provider == "local"` 但 `reminder_registered == false`）：
```
✅ 已写入本地存储！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔

⚠️ 提醒注册失败：{reminder_error 内容}
   日程已保存到本地，但不会收到自动提醒。
```

**失败**（读取 `"ok": false` 和 `"error"` 字段）：
```
❌ 写入失败：{error 字段内容}

可能原因：
- Google Calendar token 已过期 → 运行 python3 {SKILL_BASE}/scripts/setup_gcal_token.py 重新授权
- 网络问题（需要代理）→ 检查 .env 中的 HTTP_PROXY 配置
```

#### 自然语言解析参考表

| 用户说 | 解析结果 |
|--------|----------|
| `今天` | 当前日期 |
| `明天` | 当前日期 +1 |
| `后天` | 当前日期 +2 |
| `下周五` | 下一个周五的日期 |
| `4月5号` / `4/5` | 2026-04-05 |
| `下午3点` | 15:00 |
| `上午10点半` | 10:30 |
| `晚上8点` | 20:00 |
| `两小时` | duration=120 |
| `45分钟` | duration=45 |
| `到下午5点` | end=17:00 |
| `提前半小时` | remind=30 |
| `提前10分钟` | remind=10 |

#### 示例对话

**用户**：4月16日下午3点牙医预约，龙湖口腔，提前15分钟提醒我

**Bot**：
```
📅 确认日程：

| 字段     | 内容                    |
|----------|-------------------------|
| 事件     | 牙医预约                |
| 日期     | 2026-04-16（周四）      |
| 时间     | 15:00 – 16:00           |
| 地点     | 龙湖口腔                |
| 提前提醒 | 15 分钟（14:45 提醒）   |

确认添加？
```

**用户**：确认

**Bot**（如果已连接 Google Calendar，提醒注册成功）：
```
✅ 已写入 Google Calendar！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔
⏰ 将在 14:45 提醒你（提前 15 分钟）
```

**Bot**（如果已连接 Google Calendar，但提醒注册失败）：
```
✅ 已写入 Google Calendar！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔

⚠️ 提醒注册失败：无法写入任务文件（权限不足）
   日程已保存，但不会收到自动提醒。
   请手动设置提醒或检查 OpenClaw Cron 配置。
```

**Bot**（如果未连接 Google Calendar，使用本地托底）：
```
✅ 已写入本地存储！

📅 **牙医预约**
🗓 2026年4月16日 15:00 – 16:00
📍 龙湖口腔
⏰ 将在 14:45 提醒你（提前 15 分钟）

💡 提示：当前未连接 Google Calendar，日程已保存到本地。
    如需同步到 Google Calendar，请完成 Google OAuth 授权。
```

---

**用户**：4月5号早上7点跑步，45分钟

**Bot**：
```
📅 确认日程：

| 字段     | 内容                    |
|----------|-------------------------|
| 事件     | 跑步                    |
| 日期     | 2026-04-05（周日）      |
| 时间     | 07:00 – 07:45           |
| 地点     |（未填）                 |
| 提前提醒 | 15 分钟（06:45 提醒，运动默认值）|

确认写入？
```

---

### 2.2 发送文件导入日程（Excel / CSV / Word / PDF）

**触发词**：用户发送 Excel、CSV、Word、PDF 文件，或说"帮我导入这个日程表"。

**收到文件后立即主动搜索，不要等用户提供路径。**

#### Step 1：定位文件路径

```bash
# 渠道上传的文件下载到 media/inbound/
ls -t <workspace>/media/inbound/ 2>/dev/null \
  | grep -iE '\.(xlsx|xls|csv|docx|pptx|pdf|html)$' | head -3

# 兜底：用户常用目录
ls -t ~/Downloads/ ~/Desktop/ 2>/dev/null \
  | grep -iE '\.(xlsx|xls|csv|docx|pptx|pdf|html)$' | head -3
```

找到候选文件后，展示给用户确认："找到以下文件，是您刚上传的吗？"，确认后再转换。

#### Step 2：文件转 Markdown

```bash
uvx "markitdown[all]" <file_path> -o /tmp/schedule_converted.md
```

支持格式：Excel（`.xlsx/.xls`）、CSV（`.csv`）、Word（`.docx`）、PowerPoint（`.pptx`）、PDF（`.pdf`）、HTML（`.html`）

> 需要服务器已安装 `uv`

#### Step 3：解析 Markdown 表格，提取事件

```bash
python3 - << 'EOF'
import sys
sys.path.insert(0, "{SKILL_BASE}")
from services.table_parser import parse_events_from_markdown
import json

with open("/tmp/schedule_converted.md", encoding="utf-8") as f:
    content = f.read()

events = parse_events_from_markdown(content)
print(json.dumps(events, ensure_ascii=False, indent=2))
EOF
```

`table_parser` 支持自动识别非标准列名（如"会期"→日期、"主旨"→事项、"起始"→开始时间）。如列名识别不准，使用 `--column-map` 参数手动映射。

#### Step 4：展示识别结果，用户确认

```
从文件中识别到以下日程，确认写入 Google Calendar？

| 事项         | 日期       | 时间          | 地点    |
|--------------|------------|---------------|---------|
| 送小孩 | 2026-04-01 | 09:00–10:00   | 学校 |
| 买菜     | 2026-04-01 | 14:00–15:30   | 菜市场    |

✅ 全部写入  ✏️ 修改某项  ❌ 取消
```

#### Step 5：批量写入日程

用户确认后，将 Step 3 输出的 JSON 传给 `import_events.py`：

```bash
python3 {SKILL_BASE}/apps/import_events.py \
  --json '<Step 3 输出的 JSON 数组>' \
  --remind-default 15
```

读取输出，告知用户结果：

**全部成功，提醒全部注册成功**：
```
✅ 成功写入 2 条日程到 Google Calendar！

- 送小孩 2026-04-01 09:00–10:00  学校
- 买菜      2026-04-01 14:00–15:30  菜市场

⏰ 已同时注册 OpenClaw 提醒任务（提前 15 分钟）
```

**部分提醒注册失败**：
```
✅ 成功写入 2 条日程到 Google Calendar！

- 送小孩 2026-04-01 09:00–10:00  学校
- 买菜      2026-04-01 14:00–15:30  菜市场

⚠️ 部分提醒注册失败：
   - 买菜：提醒注册失败（错误信息）
```

**本地模式**：
```
✅ 成功写入 2 条日程到本地存储！

- 送小孩 2026-04-01 09:00–10:00  学校
- 买菜      2026-04-01 14:00–15:30  菜市场

⏰ 已同时注册 OpenClaw 提醒任务（提前 15 分钟）

💡 提示：当前未连接 Google Calendar，日程已保存到本地。
```

**返回 JSON 字段说明**：
- `added`：成功写入的事件数
- `failed`：失败的事件数
- `results`：每个事件的结果数组
  - `reminder_registered`：提醒是否成功注册
  - `reminder_existed`：提醒是否已存在
  - `reminder_error`：提醒注册错误信息（如果有）

---

### 2.3 发送截图/图片导入日程（OCR）

**触发词**：用户发送日程截图（手机截图、扫描件等），或说"帮我识别这张日程图片"。

**收到图片消息后立即主动搜索路径，不要等用户再次说明。**

#### Step 1：定位图片路径

```bash
# 渠道上传的图片下载到 media/inbound/
ls -t <workspace>/media/inbound/ 2>/dev/null \
  | grep -iE '\.(jpg|jpeg|png|webp|bmp|gif|pdf)$' | head -3

# 兜底：用户常用目录
ls -t ~/Downloads/ ~/Pictures/ ~/Desktop/ 2>/dev/null \
  | grep -iE '\.(jpg|jpeg|png|webp|bmp|gif|pdf)$' | head -3
```

#### Step 2：OCR 识别

```bash
uv run /app/skills/sophnet-image-ocr/scripts/ocr.py <image_path>
```

> 依赖 `sophnet-image-ocr` skill，请确保已安装

#### Step 3：Agent 理解 OCR 输出，提取日程事项

OCR 输出通常是非标准 Markdown 或纯文本，Agent 直接理解表格结构，提取：日期、开始时间、结束时间、事项名称、地点。字段缺失时使用默认值。

若 OCR 结果是标准 Markdown 表格，可用 `table_parser` 辅助：

```bash
python3 - << 'EOF'
import sys, json
sys.path.insert(0, "{SKILL_BASE}")
from services.table_parser import parse_events_from_markdown
ocr_text = """<OCR 输出内容粘贴到这里>"""
events = parse_events_from_markdown(ocr_text)
print(json.dumps(events, ensure_ascii=False, indent=2))
EOF
```

#### Step 4：展示确认卡片，Step 5：批量写入（同 2.2）

---

### 2.4 查询今日日程

**触发词**：用户说"今天有什么安排"、"查一下今天的日程"、"今天有啥"等。

**执行步骤**：

```bash
cd {SKILL_BASE}
python3 apps/generate_daily_plan.py --dry-run
```

同时展示今日日程概览（Markdown 表格，时间升序）：

```
📅 **今日日程（M月D日 周X）**

| 时间 | 事项 | 地点 | 类型 | 提醒时间 |
|------|------|------|------|----------|
| 07:30–08:15 | 晨跑 | 小区公园 | sport | 07:15 |
| 15:00–16:00 | 牙医预约 | 龙湖口腔 | other | 14:45 |
| 18:30–20:00 | 家庭聚餐 | 小区附近餐厅 | meal | 18:15 |
```

若有冲突，紧跟展示：

```
⚠️ **发现日程冲突：**

- ⚠️ 冲突：【亲子游泳课】(09:00–10:30) 与 【去超市买菜】(09:30–10:30) 重叠 60 分钟
  建议：调整其中一项，或分开处理。
```

### 2.5 查询明日/本周日程

**触发词**：用户说"明天有什么安排"、"本周日程"等。

**执行步骤**：

```bash
# 今天 + 明天
python3 {SKILL_BASE}/apps/generate_daily_plan.py --days 2 --dry-run
```

如需查询本周，将 `--days` 设为距本周末的天数（如今天周一，设 `--days 5`），再从结果中筛选并展示。

### 2.6 检查日程冲突

**触发词**：用户说"有没有日程冲突"、"检查冲突"等。

**执行步骤**：

```bash
python3 {SKILL_BASE}/apps/generate_daily_plan.py --dry-run
```

从输出中提取冲突信息，用以下格式展示：

```
⚠️ **日程冲突检测结果**

（若有冲突）
- ⚠️ 冲突：【A】(09:00–10:00) 与 【B】(09:30–10:30) 重叠 30 分钟
- ⚡ 紧接：【B】(10:00–11:00) 结束后仅 3 分钟 【C】(11:03–12:00) 就开始

（若无冲突）
✅ 今日日程无冲突。
```

### 2.7 检查日程变化

**触发词**：用户说"有没有日程变化"、"日历有没有更新"等。

**执行步骤**：

```bash
python3 {SKILL_BASE}/apps/monitor_calendar_changes.py --dry-run
```

- 有变化时：展示新增 / 取消 / 修改的事项
- 无变化时：回复"✅ 今日日程无变化。"

### 2.8 手动触发今日计划生成

**触发词**：用户说"手动生成今日计划"、"重新注册今天的提醒"等。

**执行步骤**：

```bash
python3 {SKILL_BASE}/apps/generate_daily_plan.py
```

成功后告知用户：
- 已注册的新任务数
- 生成的 `daily/cron-YYYYMMDD.md` 文件路径
- 如有冲突，一并展示告警

### 2.9 即时检查 N 分钟内的提醒

**触发词**：用户说"接下来半小时有提醒吗"等。

**执行步骤**：

```bash
python3 {SKILL_BASE}/apps/check_reminders.py --window 30
```

输出 `NO_REPLY` 表示无提醒；否则展示即将触发的提醒。

### 2.10 查看已注册的 Cron 任务

**触发词**：用户说"查看定时任务"、"有哪些 cron"等。

```bash
openclaw cron list
```

以表格形式展示所有任务（名称、下次执行时间、类型）。

> **关键路径说明（调试用）**：
> - OpenClaw Cron 任务存储文件：`~/.openclaw/cron/jobs.json`
> - 单次事件提醒由 `add_event.py` 直接写入此文件，**创建日程时立即生效**，无需等到次日
> - 查看文件确认提醒是否注册：`cat ~/.openclaw/cron/jobs.json | python3 -m json.tool | grep -A3 "提醒"`
> - ⚠️ 不要在 skill 目录下找 `jobs.json`，它不在那里

### 2.11 取消某个提醒

**触发词**：用户说"取消今天的 XX 提醒"等。

列出匹配的任务，确认后执行：

```bash
openclaw cron remove "提醒-{事项名称}"
```

### 2.12 新增固定任务

**触发词**：用户说"帮我加一个定时任务"、"每天 X 点提醒我 XX"等。

**步骤 1：收集信息**，展示确认卡片：

```
📋 确认添加以下定时任务：

| 字段 | 内容 |
|------|------|
| 名称 | 午间休息提醒 |
| 时间 | 每天 12:30 |
| 类型 | report（推送到 Telegram）|
| 内容 | 该午休了，闭眼 20 分钟 |

✅ 确认添加  ✏️ 修改  ❌ 取消
```

**步骤 2：用户确认后，追加到 `config/task_registry.yaml`**，格式：

```yaml
- name: 午间休息提醒
  description: "每天午间休息提醒"
  schedule:
    kind: at
    time: "12:30"
  type: report
  message: "该午休了，闭眼 20 分钟"
  delete_after_run: true
```

**步骤 3：立即同步到 Cron 系统**：

```bash
POST /tasks/sync          # 通过 API（推荐）
# 或
python3 {SKILL_BASE}/apps/generate_daily_plan.py
```

**步骤 4：⚠️ 是否同步写入 Google Calendar？**

固定提醒任务（cron）**不会自动写入 Google Calendar**。如果用户希望在日历里也能看到，需额外询问：

> "要不要也加到 Google Calendar 日历里方便查看？"

- 用户说"要" → 调用 `add_event.py` 创建对应日历事件（全天或指定时间）
- 用户说"不用" → 只保留 cron 提醒，不写 GCal

**类型说明**：
| type | 说明 |
|------|------|
| `silent` | 静默执行，无通知（适合监控类任务） |
| `report` | 执行后 announce 结果到 Telegram |

**周期任务格式**（使用 cron 表达式）：

```yaml
schedule:
  kind: cron
  expr: "0 9,12,15,18 * * *"   # 每天 4 次
  tz: "Asia/Shanghai"
```

### 2.13 修改提醒规则

**触发词**：用户说"把会议提醒改成提前 5 分钟"等。

修改 `config/reminder_rules.yaml` 中对应的 `lead_minutes` 值，无需改代码，下次 `generate_daily_plan.py` 自动读取新规则：

```yaml
lead_minutes:
  meeting: 5    # 改为 5 分钟
  sport:   15
  meal:    15
```

如需新增分类关键词（如让"健走"归入 sport 类），追加到 `keywords.sport` 列表即可。

---

### 2.14 查询 ROC 周期事件

**触发词**：用户说"ROC 有什么事件"、"今年还有哪些周期性提醒"、"ROC 清单"等。

```bash
python3 {SKILL_BASE}/apps/check_roc.py --list
```

以表格展示今年全部 ROC 事件的发生日期和提醒日期：

```
📋 **今年 ROC 定期事件**

| 事件 | 发生日期 | 提醒日期 | 说明 |
|------|----------|----------|------|
| 春节家庭聚会准备 | 02-13 | 02-08 | 提前安排聚餐地点和礼品 |
| 妈妈生日 | 09-08 | 08-25 | 提前订蛋糕和安排家庭聚餐 |
| 暑期家庭出行计划 | 07-01 | 06-01 | 提前预订酒店和机票 |
| 年度家庭体检 | 04-11 | 03-28 | 提前预约体检套餐 |
```

### 2.14.1 ROC 事件同步到 Google Calendar

ROC 事件按**年份独立批量写入** Google Calendar（全天事件），便于在日历视图中提前查看全年重要节点。

**触发机制（自动）：**
- `check_roc.py` 每天 08:05 运行，若当年尚未同步过，自动执行一次全量同步
- 每年 1 月 1 日 08:00，`ROC年度GCal同步` 任务自动将新一年的事件写入 GCal

**手动同步（Agent 操作）：**
```bash
# 同步当年（首次安装后可手动执行一次）
python3 {SKILL_BASE}/apps/sync_roc_to_gcal.py

# 同步指定年份
python3 {SKILL_BASE}/apps/sync_roc_to_gcal.py --year 2027

# 预览（不写入 GCal）
python3 {SKILL_BASE}/apps/sync_roc_to_gcal.py --dry-run

# 强制重建（GCal 事件误删时使用）
python3 {SKILL_BASE}/apps/sync_roc_to_gcal.py --force
```

**去重机制**：同步状态记录在 `data/roc_gcal_sync.json`（`年份 → { roc_id: gcal_event_id }`），已同步的事件不会重复创建。

**用户触发词**：
- "把 ROC 事件同步到日历" → 执行 `sync_roc_to_gcal.py`
- "明年的 ROC 事件写到 Google Calendar" → `--year` 指定下一年
- "ROC 事件在日历里消失了" → `--force` 重新同步

### 2.15 查询近期 ROC 提醒

**触发词**：用户说"最近有没有 ROC 提醒"、"未来一个月有什么周期任务"等。

```bash
python3 {SKILL_BASE}/apps/check_roc.py --upcoming 30
```

### 2.16 新增 ROC 事件

**触发词**：用户说"帮我加一个 ROC 事件"、"新增一个每年提醒"等。

**步骤 1**：收集信息，展示确认卡片：

```
📋 确认添加 ROC 事件：

| 字段 | 内容 |
|------|------|
| ID   | dad_birthday |
| 名称 | 爸爸生日 |
| 规则 | 每年5月20日 |
| 提前提醒 | 14 天 |
| 推送时间 | 09:00 |
| 说明 | 提前安排家庭聚餐 |

✅ 确认添加  ✏️ 修改  ❌ 取消
```

**步骤 2**：用户确认后，追加到 `config/roc_events.yaml` 的 `events` 列表：

```yaml
  - id: dad_birthday
    name: 爸爸生日
    rule: 每年5月20日
    reminder_time: "09:00"
    lead_days: 14
    description: "提前安排家庭聚餐"
```

### 2.17 修改春节日期（每年维护一次）

**触发词**：用户说"更新明年春节日期"等。

直接修改 `config/roc_events.yaml` 中的 `spring_festival` 字段：

```yaml
spring_festival:
  2027:
    last_workday: "2027-02-09"   # 春节前最后一个工作日
    first_workday: "2027-02-19"  # 春节后第一个工作日
```

---

## 三、提醒消息格式

自动提醒触发时，消息格式如下：

```
⏰ **牙医预约** 将在 15 分钟后开始
🕐 15:00–16:00（共 60 分钟）
📍 龙湖口腔
📝 记得带上医保卡
```

---

## 四、日程冲突说明

系统每次生成每日计划时自动检测冲突，规则：

- **重叠**：两个事件时间区间有交集（超过 0 分钟）→ `⚠️ 冲突`
- **紧接**：两个事件间隔 < 5 分钟（无缓冲时间）→ `⚡ 紧接`
- 全天事件不参与冲突检测

---

## 五、配置说明

所有配置集中在 `config/` 目录，**修改 yaml 不需要改代码**。

| 文件 | 用途 |
|------|------|
| `config/settings.py` | 路径、代理、Telegram、时区 |
| `config/reminder_rules.yaml` | 提醒规则和分类关键词 |
| `config/task_registry.yaml` | 所有固定重复任务注册表 |
| `.env` | 敏感配置（token 路径、代理地址等） |

**环境变量覆盖**：

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `SOPHNET_SCHEDULE_BASE` | 自动推导 | skill 根路径（兼容旧名 `CRON_SKILL_BASE`） |
| `OPENCLAW_BASE` | `~/.openclaw` | OpenClaw 工作空间 |
| `GCAL_TOKEN_PATH` | `~/.config/gcalcli/token.json` | Google OAuth token |
| `HTTP_PROXY` | 空 | 访问 Google API 的代理地址 |
| `TELEGRAM_TO` | 空 | Telegram 接收方 ID |
| `GCAL_CALENDAR_ID` | `primary` | 要监控的日历 ID |
| `TIMEZONE` | `Asia/Shanghai` | 默认时区 |

---

## 六、任务投递规则

| 场景 | sessionTarget | payload.kind | delivery |
|------|--------------|--------------|----------|
| 会前/事前提醒 | `main` | `systemEvent` | 不设置 |
| 周期静默监控 | `isolated` | `agentTurn` | `none` |
| 报告/日报推送 | `isolated` | `agentTurn` | `announce` |

**为什么提醒用 `systemEvent + main`**：
`isolated + announce` 不支持 webchat（Web 界面）渠道，会报 `Unsupported channel` 错误。
`systemEvent + main` 直接注入主会话，所有渠道均可用，可靠性最高。

---

## 七、运行测试

```bash
cd {SKILL_BASE}

# 全部单元测试（不需要 Google Calendar 连接）
python3 tests/test_time_window.py
python3 tests/test_event_classifier.py
python3 tests/test_reminder_planner.py
python3 tests/test_event_diff.py
python3 tests/test_task_builder.py
python3 tests/test_conflict_detector.py
python3 tests/test_add_event.py

# 或用 pytest
python3 -m pytest tests/ -v

# 干运行（只生成 md，不注册 cron，需要 GCal token）
python3 apps/generate_daily_plan.py --dry-run
python3 apps/generate_daily_plan.py --days 2 --dry-run
```

---

## 八、配置维护

`config/` 目录下有三个可维护的配置文件，Agent 负责解读用户意图并直接操作对应文件。

---

### 8.1 `task_registry.yaml` — 固定定时任务

**说明**：定义所有每日重复运行的 cron 任务（几点生成日报、几点提醒喝水等）。

**生效方式**：修改后调用 `POST /tasks/sync`（或自然语言"同步任务"），立即写入 `jobs.json`，**不需要等到明天**。

**常见场景**：

> **用户**：帮我加一个每天下午 3 点的午睡提醒
>
> **Agent**：
> 1. 在 `task_registry.yaml` 的 `tasks:` 列表追加：
>    ```yaml
>    - name: 午睡提醒
>      description: "下午休息，恢复精力"
>      schedule:
>        kind: at
>        time: "15:00"
>      type: report
>      message: "该午睡了 😴"
>      delete_after_run: true
>    ```
> 2. 调用 `POST /tasks/sync` 立即注册
> 3. 回复：「已添加"午睡提醒"，每天 15:00 生效」

> **用户**：把喝水提醒改到下午 2 点
>
> **Agent**：找到 `task_registry.yaml` 中 `name: 喝水提醒` 的条目，将 `time: "10:00"` 改为 `time: "14:00"`，再调用 `POST /tasks/sync`。

**验证**：
```bash
# dry-run 预览差异，不写入
curl -s -X POST -H "X-API-Key: $KEY" "$BASE/tasks/sync?dry_run=true" | python3 -m json.tool

# 正式同步
curl -s -X POST -H "X-API-Key: $KEY" "$BASE/tasks/sync" | python3 -m json.tool
```

---

### 8.2 `reminder_rules.yaml` — 提醒提前量与分类关键词

**说明**：控制不同类型事件的提醒提前量（分钟）和匹配关键词。修改后**立即生效**（运行时读取，无需重启）。

**常见场景**：

> **用户**：把体检提醒改成提前 1 小时
>
> **Agent**：将 `reminder_rules.yaml` 中 `medical: 30` 改为 `medical: 60`，回复「已修改，下次提醒计算时生效」。

> **用户**：我想让"攀岩"也算作运动类提醒
>
> **Agent**：在 `keywords.sport` 列表追加 `- 攀岩`，回复「已添加"攀岩"到运动分类」。

> **用户**：加一个"约会"类型，提前 20 分钟提醒
>
> **Agent**：
> 1. 在 `lead_minutes:` 追加 `date: 20`
> 2. 在 `keywords:` 追加：
>    ```yaml
>    date:
>      - 约会
>      - 约好了
>      - 见面
>    ```
> 3. 在 `event_classifier.py` 的 `classify` 方法匹配顺序中加入 `"date"`

**注意**：新增分类类型时，同步更新 `services/event_classifier.py` 中的匹配顺序列表。

---

### 8.3 `roc_events.yaml` — ROC 周期性事件

**说明**：管理每年重复的重要事件（生日、体检、家庭聚会等）。已有完整 REST API 和自然语言交互支持（见 2.14–2.17 节）。

**API 快速参考**：
```bash
GET    /roc/events               # 列出今年所有事件及日期
POST   /roc/events               # 新增事件
PUT    /roc/events/{id}          # 修改事件
DELETE /roc/events/{id}          # 删除事件
GET    /roc/upcoming?days=90     # 未来 N 天内即将到来的事件
```

**特殊场景 — 更新春节日期**（每年 1 月更新一次）：

> **用户**：更新一下明年春节日期
>
> **Agent**：修改 `roc_events.yaml` 顶部 `spring_festival:` 中对应年份的 `cny_date`，保存即可。

---

### 8.4 维护操作速查

| 用户说 | Agent 动作 |
|--------|------------|
| "加个每天 X 点的提醒" | 追加到 `task_registry.yaml` → `POST /tasks/sync` |
| "删掉/改一下某个定时任务" | 修改 `task_registry.yaml` → `POST /tasks/sync` |
| "同步任务/立即生效" | `POST /tasks/sync` |
| "把 XX 提醒改成 N 分钟" | 修改 `reminder_rules.yaml` `lead_minutes` 对应值 |
| "加个关键词到 XX 类型" | 追加到 `reminder_rules.yaml` `keywords.XX` 列表 |
| "加个新的提醒类型" | 同时更新 `reminder_rules.yaml` + `event_classifier.py` |
| "加个 ROC 事件" | `POST /roc/events`（见 2.15 节） |
| "删/改 ROC 事件" | `DELETE`/`PUT /roc/events/{id}` |
| "更新春节日期" | 修改 `roc_events.yaml` `spring_festival.YYYY.cny_date` |

---

## 九、Google Calendar 初始化

首次使用需要完成 OAuth 授权：

```bash
python3 {SKILL_BASE}/scripts/setup_gcal_token.py
```

详细步骤见 `docs/GOOGLE_CALENDAR_SETUP.md`。

---

## 十、常用操作速查

| 用户说 | Agent 动作 |
|--------|------------|
| "明天下午3点牙医预约" | 解析 → 确认卡片 → `add_event.py` → 写入 GCal 或本地存储（根据 provider 字段回复） |
| 发送 Excel/CSV/Word/PDF | 定位文件 → `markitdown` 转换 → `table_parser` 提取 → 确认 → `import_events.py` 写入 |
| 发送截图/图片 | 定位图片 → OCR → Agent 提取事件 → 确认 → `import_events.py` 写入 |
| "今天有什么安排" | `generate_daily_plan.py --dry-run`，展示今日日程 |
| "明天的日程" | `--days 2 --dry-run`，展示明日部分 |
| "有没有日程冲突" | 展示冲突检测结果 |
| "有没有日程变化" | `monitor_calendar_changes.py --dry-run` |
| "手动生成今日计划" | `generate_daily_plan.py`（注册 cron 任务） |
| "接下来有提醒吗" | `check_reminders.py --window 30` |
| "查看定时任务" | `openclaw cron list` |
| "取消今天 XX 提醒" | `openclaw cron remove "提醒-XX"` |
| "帮我加个定时任务" | 收集信息→确认→追加到 task_registry.yaml→`POST /tasks/sync` |
| "把体检提醒改成1小时" | 修改 `reminder_rules.yaml` 的 `medical: 60` |
| "加个关键词到运动分类" | 追加到 `reminder_rules.yaml` `keywords.sport` |
| "ROC 今年有哪些事件" | `check_roc.py --list`，展示全年事件表 |
| "最近有 ROC 提醒吗" | `check_roc.py --upcoming 30` |
| "加个 ROC 事件" | 收集信息→确认→追加到 `roc_events.yaml` |
