sophhub 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/package.json +1 -1
  2. package/skills/consensus/skill.json +20 -0
  3. package/skills/consensus/src/SKILL.md +93 -0
  4. package/skills/deepwiki/skill.json +20 -0
  5. package/skills/deepwiki/src/SKILL.md +45 -0
  6. package/skills/deepwiki/src/_meta.json +6 -0
  7. package/skills/deepwiki/src/scripts/deepwiki.js +135 -0
  8. package/skills/feishu-bitable/skill.json +20 -0
  9. package/skills/feishu-bitable/src/CHECKLIST.md +150 -0
  10. package/skills/feishu-bitable/src/README.md +178 -0
  11. package/skills/feishu-bitable/src/SKILL.md +113 -0
  12. package/skills/feishu-bitable/src/_meta.json +6 -0
  13. package/skills/feishu-bitable/src/api.js +381 -0
  14. package/skills/feishu-bitable/src/bin/cli.js +284 -0
  15. package/skills/feishu-bitable/src/description.md +143 -0
  16. package/skills/feishu-bitable/src/examples/create-records.json +52 -0
  17. package/skills/feishu-bitable/src/examples/create-table.json +64 -0
  18. package/skills/feishu-bitable/src/package-lock.json +324 -0
  19. package/skills/feishu-bitable/src/package.json +33 -0
  20. package/skills/feishu-bitable/src/publish-config.json +14 -0
  21. package/skills/feishu-bitable/src/test-simple.js +61 -0
  22. package/skills/feishu-bitable/src/utils.js +261 -0
  23. package/skills/flight-booking/skill.json +9 -2
  24. package/skills/flight-booking/src/scripts/flight_booking.py +2 -1
  25. package/skills/google-maps/skill.json +20 -0
  26. package/skills/google-maps/src/SKILL.md +237 -0
  27. package/skills/google-maps/src/_meta.json +6 -0
  28. package/skills/google-maps/src/lib/map_helper.py +912 -0
  29. package/skills/large-task-router/skill.json +20 -0
  30. package/skills/large-task-router/src/SKILL.md +79 -0
  31. package/skills/large-task-router/src/templates/plan.md +74 -0
  32. package/skills/skillhub/skill.json +11 -4
  33. package/skills/skillhub/src/SKILL.md +11 -1
  34. package/skills/sophnet-dailynews/skill.json +20 -0
  35. package/skills/sophnet-dailynews/src/SKILL.md +179 -0
  36. package/skills/sophnet-dailynews/src/cache.json +151 -0
  37. package/skills/sophnet-dailynews/src/sources.json +230 -0
  38. package/skills/sophnet-schedule/skill.json +20 -0
  39. package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
  40. package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
  41. package/skills/sophnet-schedule/src/SKILL.md +1050 -0
  42. package/skills/sophnet-schedule/src/_meta.json +6 -0
  43. package/skills/sophnet-schedule/src/api/__init__.py +0 -0
  44. package/skills/sophnet-schedule/src/api/models.py +245 -0
  45. package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
  46. package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
  47. package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
  48. package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
  49. package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
  50. package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
  51. package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
  52. package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
  53. package/skills/sophnet-schedule/src/compat.py +66 -0
  54. package/skills/sophnet-schedule/src/config/__init__.py +0 -0
  55. package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
  56. package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
  57. package/skills/sophnet-schedule/src/config/settings.py +133 -0
  58. package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
  59. package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
  60. package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
  61. package/skills/sophnet-schedule/src/gcal/client.py +374 -0
  62. package/skills/sophnet-schedule/src/gcal/models.py +91 -0
  63. package/skills/sophnet-schedule/src/requirements.txt +6 -0
  64. package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
  65. package/skills/sophnet-schedule/src/server.py +669 -0
  66. package/skills/sophnet-schedule/src/services/__init__.py +0 -0
  67. package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
  68. package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
  69. package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
  70. package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
  71. package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
  72. package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
  73. package/skills/sophnet-schedule/src/services/job_store.py +100 -0
  74. package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
  75. package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
  76. package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
  77. package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
  78. package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
  79. package/skills/sophnet-schedule/src/services/time_window.py +72 -0
  80. package/skills/sophnet-stock/skill.json +20 -0
  81. package/skills/sophnet-stock/src/App-Plan.md +442 -0
  82. package/skills/sophnet-stock/src/README.md +214 -0
  83. package/skills/sophnet-stock/src/SKILL.md +236 -0
  84. package/skills/sophnet-stock/src/TODO.md +394 -0
  85. package/skills/sophnet-stock/src/_meta.json +6 -0
  86. package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
  87. package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
  88. package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
  89. package/skills/sophnet-stock/src/docs/README.md +95 -0
  90. package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
  91. package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
  92. package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
  93. package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
  94. package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
  95. package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
  96. package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
  97. package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
  98. package/skills/xiaohongshu/skill.json +20 -0
  99. package/skills/xiaohongshu/src/SKILL.md +91 -0
  100. package/skills/xiaohongshu/src/_meta.json +6 -0
  101. package/skills/xiaohongshu/src/assets/card.html +216 -0
  102. package/skills/xiaohongshu/src/assets/cover.html +82 -0
  103. package/skills/xiaohongshu/src/assets/example.md +84 -0
  104. package/skills/xiaohongshu/src/assets/styles.css +318 -0
  105. package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
  106. package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
  107. package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
  108. package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
  109. package/skills/xiaohongshu/src/workflow.py +185 -0
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ROC(Regular Operation Calendar)定期事件提醒检查。
4
+
5
+ 每天自动运行,检查今日是否有需要提醒的 ROC 事件,输出 JSON 供 Agent 处理。
6
+
7
+ 用法:
8
+ python3 apps/check_roc.py # 检查今天,输出 JSON
9
+ python3 apps/check_roc.py --upcoming 30 # 列出未来 30 天内的提醒
10
+ python3 apps/check_roc.py --list # 列出今年全部事件及提醒日期
11
+ python3 apps/check_roc.py --dry-run # 只计算,不写状态文件
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import re
19
+ import sys
20
+ from datetime import datetime, timedelta
21
+ from pathlib import Path
22
+
23
+ import yaml
24
+
25
+ # ── 路径配置 ──────────────────────────────────────────────────────────
26
+ _SKILL_BASE = Path(__file__).resolve().parent.parent
27
+ _CONFIG_FILE = _SKILL_BASE / "config" / "roc_events.yaml"
28
+ _STATE_FILE = _SKILL_BASE / "data" / "roc_state.json"
29
+
30
+ sys.path.insert(0, str(_SKILL_BASE))
31
+
32
+ from config.settings import TIMEZONE
33
+ from services.calendar_backend import active_provider
34
+ from services.datetime_utils import current_date_str, current_year, now_local
35
+
36
+
37
+ # ── 日期计算 ──────────────────────────────────────────────────────────
38
+
39
+ def _nth_weekday(year: int, month: int, weekday: int, n: int) -> datetime:
40
+ """返回 year/month 中第 n 个 weekday(0=周一…6=周日)。"""
41
+ first = datetime(year, month, 1)
42
+ offset = (weekday - first.weekday()) % 7
43
+ first_match = first + timedelta(days=offset)
44
+ return first_match + timedelta(weeks=n - 1)
45
+
46
+
47
+ _WEEKDAY_MAP = {"周一": 0, "周二": 1, "周三": 2, "周四": 3, "周五": 4, "周六": 5, "周日": 6}
48
+
49
+
50
+ def resolve_event_date(rule: str, year: int, spring: dict) -> datetime | None:
51
+ """将 rule 字符串解析为具体日期。"""
52
+
53
+ # 每年M月D日
54
+ m = re.fullmatch(r"每年(\d+)月(\d+)日", rule.strip())
55
+ if m:
56
+ try:
57
+ return datetime(year, int(m.group(1)), int(m.group(2)))
58
+ except ValueError:
59
+ return None
60
+
61
+ # 每年M月第N个周X
62
+ m = re.search(r"每年(\d+)月第(\d+)个(周[一二三四五六日])", rule)
63
+ if m:
64
+ month = int(m.group(1))
65
+ n = int(m.group(2))
66
+ weekday = _WEEKDAY_MAP.get(m.group(3))
67
+ if weekday is not None:
68
+ return _nth_weekday(year, month, weekday, n)
69
+
70
+ # 春节相关(从 spring_festival 表查)
71
+ sf = spring.get(year, {})
72
+ if "春节前最后工作日" in rule:
73
+ d = sf.get("last_workday")
74
+ return datetime.strptime(d, "%Y-%m-%d") if d else None
75
+ if "春节后第一个工作日" in rule:
76
+ d = sf.get("first_workday")
77
+ return datetime.strptime(d, "%Y-%m-%d") if d else None
78
+
79
+ # 5.1 假期后第一个工作日(固定 5-06,如变动手工在 YAML 配置)
80
+ if "5.1假期后第一个工作日" in rule:
81
+ return datetime(year, 5, 6)
82
+
83
+ return None
84
+
85
+
86
+ # ── 数据加载 ──────────────────────────────────────────────────────────
87
+
88
+ def _load_config() -> dict:
89
+ with open(_CONFIG_FILE, encoding="utf-8") as f:
90
+ return yaml.safe_load(f)
91
+
92
+
93
+ def _load_state() -> dict:
94
+ try:
95
+ return json.loads(_STATE_FILE.read_text(encoding="utf-8"))
96
+ except Exception:
97
+ return {}
98
+
99
+
100
+ def _save_state(state: dict) -> None:
101
+ _STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
102
+ _STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
103
+
104
+
105
+ # ── 核心逻辑 ──────────────────────────────────────────────────────────
106
+
107
+ def build_year_events(year: int, cfg: dict) -> list[dict]:
108
+ """计算指定年份所有 ROC 事件的发生日期和提醒日期。"""
109
+ spring = cfg.get("spring_festival", {})
110
+ result = []
111
+ for ev in cfg.get("events", []):
112
+ event_dt = resolve_event_date(ev["rule"], year, spring)
113
+ if event_dt is None:
114
+ continue
115
+ lead = int(ev.get("lead_days", 0))
116
+ reminder_dt = event_dt - timedelta(days=lead)
117
+ result.append({
118
+ "id": ev["id"],
119
+ "name": ev["name"],
120
+ "rule": ev["rule"],
121
+ "event_date": event_dt.strftime("%Y-%m-%d"),
122
+ "reminder_date": reminder_dt.strftime("%Y-%m-%d"),
123
+ "reminder_time": ev.get("reminder_time", "08:00"),
124
+ "lead_days": lead,
125
+ "description": ev.get("description", ""),
126
+ })
127
+ return result
128
+
129
+
130
+ def check_today(year_events: list[dict], today: str, state: dict) -> list[dict]:
131
+ """返回今天需要提醒且尚未发送过提醒的事件。"""
132
+ hits = []
133
+ for ev in year_events:
134
+ if ev["reminder_date"] != today:
135
+ continue
136
+ # 若同一事件今天已提醒过,跳过
137
+ if state.get(ev["id"]) == today:
138
+ continue
139
+ hits.append(ev)
140
+ return hits
141
+
142
+
143
+ def upcoming_events(year_events: list[dict], today: str, days: int) -> list[dict]:
144
+ """返回未来 days 天内(含今天)提醒日期内的事件,按提醒日期升序。"""
145
+ today_dt = datetime.strptime(today, "%Y-%m-%d")
146
+ cutoff_dt = today_dt + timedelta(days=days)
147
+ result = []
148
+ for ev in year_events:
149
+ rd = datetime.strptime(ev["reminder_date"], "%Y-%m-%d")
150
+ if today_dt <= rd <= cutoff_dt:
151
+ ev = dict(ev)
152
+ ev["days_until_reminder"] = (rd - today_dt).days
153
+ ed = datetime.strptime(ev["event_date"], "%Y-%m-%d")
154
+ ev["days_until_event"] = (ed - today_dt).days
155
+ result.append(ev)
156
+ result.sort(key=lambda x: x["reminder_date"])
157
+ return result
158
+
159
+
160
+ def format_reminder_message(ev: dict) -> str:
161
+ ed = datetime.strptime(ev["event_date"], "%Y-%m-%d")
162
+ today = now_local(TIMEZONE).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
163
+ days = (ed - today).days
164
+ lines = [
165
+ f"📅 **ROC 提醒:{ev['name']}**",
166
+ f"📆 事件日期:{ev['event_date']}({days} 天后)",
167
+ ]
168
+ if ev.get("description"):
169
+ lines.append(f"📝 {ev['description']}")
170
+ return "\n".join(lines)
171
+
172
+
173
+ # ── CLI 入口 ──────────────────────────────────────────────────────────
174
+
175
+ def main() -> None:
176
+ parser = argparse.ArgumentParser(description="ROC 定期事件提醒检查")
177
+ parser.add_argument("--upcoming", type=int, metavar="DAYS",
178
+ help="列出未来 N 天内即将提醒的事件")
179
+ parser.add_argument("--list", action="store_true",
180
+ help="列出今年全部 ROC 事件(事件日期 + 提醒日期)")
181
+ parser.add_argument("--dry-run", action="store_true",
182
+ help="只计算,不写状态文件")
183
+ parser.add_argument("--date", default=None,
184
+ help="模拟日期 YYYY-MM-DD(默认今天,测试用)")
185
+ args = parser.parse_args()
186
+
187
+ today = args.date or current_date_str(TIMEZONE)
188
+ year = int(today[:4])
189
+
190
+ cfg = _load_config()
191
+ year_events = build_year_events(year, cfg)
192
+ state = _load_state()
193
+
194
+ # 当年 ROC 事件若尚未同步到 Google Calendar,自动触发一次
195
+ if active_provider() == "google":
196
+ try:
197
+ from apps.sync_roc_to_gcal import is_year_synced, sync_year
198
+ if not is_year_synced(year):
199
+ print(f"[check_roc] {year} 年 ROC 事件未同步到 GCal,自动同步中…", flush=True)
200
+ result = sync_year(year, verbose=False)
201
+ added = len(result.get("added", []))
202
+ print(f"[check_roc] GCal 同步完成:新增 {added} 个全天事件", flush=True)
203
+ except Exception as e:
204
+ # 同步失败不影响提醒功能
205
+ print(f"[check_roc] GCal 同步跳过({e})", flush=True)
206
+
207
+ # ── --list ────────────────────────────────────────────────────────
208
+ if args.list:
209
+ out = {"year": year, "events": year_events}
210
+ print(json.dumps(out, ensure_ascii=False, indent=2))
211
+ return
212
+
213
+ # ── --upcoming ────────────────────────────────────────────────────
214
+ if args.upcoming is not None:
215
+ evs = upcoming_events(year_events, today, args.upcoming)
216
+ print(json.dumps({"today": today, "upcoming": evs}, ensure_ascii=False, indent=2))
217
+ return
218
+
219
+ # ── 默认:检查今天 ────────────────────────────────────────────────
220
+ hits = check_today(year_events, today, state)
221
+
222
+ output = {
223
+ "ok": True,
224
+ "today": today,
225
+ "reminders": [],
226
+ }
227
+
228
+ for ev in hits:
229
+ output["reminders"].append({
230
+ "id": ev["id"],
231
+ "name": ev["name"],
232
+ "event_date": ev["event_date"],
233
+ "description": ev["description"],
234
+ "message": format_reminder_message(ev),
235
+ })
236
+
237
+ if not args.dry_run and hits:
238
+ for ev in hits:
239
+ state[ev["id"]] = today
240
+ _save_state(state)
241
+
242
+ print(json.dumps(output, ensure_ascii=False, indent=2))
243
+
244
+
245
+ if __name__ == "__main__":
246
+ main()
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 每日计划生成器。
4
+
5
+ 执行时机:每天 07:50(由 OpenClaw Cron 触发)
6
+
7
+ 流程:
8
+ 1. 从 config/task_registry.yaml 读取固定任务,注册到 OpenClaw Cron
9
+ 2. 从 Google Calendar 读取今天(或今天+明天)日程
10
+ 3. 为每个日程生成一次性提醒任务,注册到 OpenClaw Cron
11
+ 4. 生成 daily/cron-YYYYMMDD.md 可读记录
12
+
13
+ 用法:
14
+ python3 apps/generate_daily_plan.py
15
+ python3 apps/generate_daily_plan.py --days 2 # 今天+明天
16
+ python3 apps/generate_daily_plan.py --dry-run # 只生成 md,不注册 cron
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import logging
24
+ import sys
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ try:
28
+ from zoneinfo import ZoneInfo
29
+ except ImportError:
30
+ from backports.zoneinfo import ZoneInfo
31
+
32
+ import yaml
33
+
34
+ # ── sys.path 保障(以脚本方式直接运行时)────────────────────────
35
+ sys.path.insert(0, str(Path(__file__).parent.parent))
36
+
37
+ from config.settings import (
38
+ TIMEZONE,
39
+ DAILY_DIR, TASK_REGISTRY_FILE, REMINDER_RULES_FILE,
40
+ SKILL_BASE,
41
+ )
42
+ from gcal.models import CalendarEvent
43
+ from services.calendar_backend import active_provider, list_event_views
44
+ from services.time_window import get_today_window, get_today_and_tomorrow_window, now_tz
45
+ from services.reminder_planner import plan_reminders, ReminderPlan
46
+ from services.task_builder import (
47
+ build_periodic_silent_task,
48
+ build_reminder_task,
49
+ build_reminder_task_name,
50
+ build_report_task,
51
+ )
52
+ from services.conflict_detector import detect_conflicts, ConflictPair
53
+ from services.job_store import append_job_if_absent, list_job_names
54
+ from services.runtime_utils import expand_command_vars
55
+
56
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
57
+ logger = logging.getLogger(__name__)
58
+
59
+ WEEKDAY_CN = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
60
+
61
+
62
+ # ─────────────────────────── task registry ────────────────────────
63
+
64
+ def load_task_registry() -> list[dict]:
65
+ try:
66
+ with open(TASK_REGISTRY_FILE, encoding="utf-8") as f:
67
+ data = yaml.safe_load(f) or {}
68
+ provider = active_provider()
69
+ return [
70
+ task for task in data.get("tasks", [])
71
+ if task.get("requires_provider", "any") in {"", "any", provider}
72
+ ]
73
+ except Exception as e:
74
+ logger.error(f"加载 task_registry.yaml 失败: {e}")
75
+ return []
76
+
77
+ def parse_fixed_task(task: dict, today: datetime) -> dict | None:
78
+ """
79
+ 将 task_registry 中的一条任务转换为 OpenClaw job payload。
80
+ 返回 None 表示跳过(周期任务不在每日 at 中创建)。
81
+ """
82
+ name = task.get("name", "")
83
+ kind = task.get("type", "report")
84
+ message = expand_command_vars(task.get("message", task.get("command", "")))
85
+ sched = task.get("schedule", {})
86
+ delete = task.get("delete_after_run", True)
87
+
88
+ if sched.get("kind") == "cron":
89
+ # 周期任务:用 build_periodic_silent_task 或 build_report_task
90
+ expr = sched.get("expr", "")
91
+ tz = sched.get("tz", TIMEZONE)
92
+ if kind == "silent":
93
+ return build_periodic_silent_task(name, expr, message, tz)
94
+ else:
95
+ return build_report_task(
96
+ name, message,
97
+ cron_expr=expr, tz=tz,
98
+ )
99
+
100
+ elif sched.get("kind") == "at":
101
+ # 一次性任务:解析 HH:MM,换算 UTC
102
+ time_str = sched.get("time", "")
103
+ try:
104
+ h, m = map(int, time_str.split(":"))
105
+ except ValueError:
106
+ logger.warning(f"无法解析时间 '{time_str}',跳过任务 {name}")
107
+ return None
108
+
109
+ zone = ZoneInfo(TIMEZONE)
110
+ bj_time = today.astimezone(zone).replace(hour=h, minute=m, second=0, microsecond=0)
111
+
112
+ if kind == "silent":
113
+ # 静默一次性:用 agentTurn + delivery:none
114
+ return {
115
+ "name": name,
116
+ "schedule": {"kind": "at", "at": _utc_str(bj_time)},
117
+ "sessionTarget": "isolated",
118
+ "payload": {"kind": "agentTurn", "message": message},
119
+ "delivery": {"mode": "none"},
120
+ "deleteAfterRun": delete,
121
+ }
122
+ else:
123
+ return build_report_task(
124
+ name, message,
125
+ at_utc=bj_time, delete_after_run=delete,
126
+ )
127
+
128
+ return None
129
+
130
+
131
+ # ─────────────────────────── cron 注册 ────────────────────────────
132
+
133
+ def _utc_str(dt: datetime) -> str:
134
+ from datetime import timezone
135
+ return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
136
+
137
+
138
+ def _get_existing_task_names() -> set[str]:
139
+ """从 jobs.json 读取已存在的任务名称"""
140
+ try:
141
+ return list_job_names()
142
+ except Exception as e:
143
+ logger.warning(f"读取现有 cron 任务失败: {e}")
144
+ return set()
145
+
146
+
147
+ def register_cron_task(payload: dict, dry_run: bool, existing: set[str]) -> bool:
148
+ """注册单个任务到 OpenClaw Cron(直接操作 jobs.json)。"""
149
+ name = payload.get("name", "")
150
+ if name in existing:
151
+ logger.info(f"⏭️ 任务已存在,跳过: {name}")
152
+ return False
153
+
154
+ if dry_run:
155
+ logger.info(f"[dry-run] 将注册: {name}")
156
+ existing.add(name)
157
+ return True
158
+
159
+ try:
160
+ added = append_job_if_absent(payload)
161
+ if added:
162
+ existing.add(name)
163
+ logger.info(f"✅ 已注册: {name}")
164
+ return True
165
+ logger.info(f"⏭️ 任务已存在,跳过: {name}")
166
+ return False
167
+ except Exception as e:
168
+ logger.error(f"❌ 注册异常 ({name}): {e}")
169
+ return False
170
+
171
+
172
+ # ─────────────────────────── markdown 生成 ────────────────────────
173
+
174
+ def _load_reminder_rules_text() -> str:
175
+ """从 reminder_rules.yaml 动态生成提醒规则说明文字。"""
176
+ try:
177
+ with open(REMINDER_RULES_FILE, encoding="utf-8") as f:
178
+ data = yaml.safe_load(f) or {}
179
+ lead = data.get("lead_minutes", {})
180
+ type_cn = {"meeting": "会议", "sport": "运动", "meal": "饭局/宴会"}
181
+ lines = []
182
+ for key, cn in type_cn.items():
183
+ mins = lead.get(key)
184
+ if mins is not None:
185
+ lines.append(f"- {cn}:提前 **{mins} 分钟** 提醒")
186
+ default = data.get("default_lead_minutes")
187
+ if default:
188
+ lines.append(f"- 其他:提前 **{default} 分钟** 提醒(默认)")
189
+ return "\n".join(lines) if lines else "- 默认:提前 5 分钟提醒"
190
+ except Exception:
191
+ return "- 会议:提前 **2 分钟** 提醒\n- 运动:提前 **15 分钟** 提醒\n- 饭局/宴会:提前 **15 分钟** 提醒"
192
+
193
+
194
+ def generate_markdown(
195
+ today: datetime,
196
+ fixed_tasks: list[dict],
197
+ events: list[CalendarEvent],
198
+ reminder_plans: list[ReminderPlan],
199
+ conflicts: list[ConflictPair] | None = None,
200
+ ) -> str:
201
+ date_str = today.strftime("%Y-%m-%d")
202
+ weekday = WEEKDAY_CN[today.weekday()]
203
+ lines = [f"# Cron 任务记录 - {date_str} {weekday}", ""]
204
+
205
+ # 提醒规则说明(动态读取)
206
+ lines += ["## 提醒规则", "", _load_reminder_rules_text(), ""]
207
+
208
+ # 冲突告警(有冲突时显示)
209
+ if conflicts:
210
+ lines += ["## ⚠️ 日程冲突", ""]
211
+ for p in conflicts:
212
+ lines.append(f"- {p.format_text()}")
213
+ lines.append("")
214
+
215
+ # 重复性任务
216
+ lines += [
217
+ "## 重复性任务汇总",
218
+ "",
219
+ "| 序号 | 任务 | 执行时间 | 类型 | 任务说明 |",
220
+ "|------|------|----------|------|----------|",
221
+ ]
222
+ for i, t in enumerate(fixed_tasks, 1):
223
+ sched = t.get("schedule", {})
224
+ if sched.get("kind") == "cron":
225
+ time_display = f"cron: {sched.get('expr', '-')}"
226
+ else:
227
+ time_display = sched.get("time", "-")
228
+ lines.append(
229
+ f"| {i} | {t.get('name','-')} | {time_display} | {t.get('type','-')} | {t.get('description','-')} |"
230
+ )
231
+ lines.append("")
232
+
233
+ # 当日一次性提醒
234
+ lines += ["## 当日一次性提醒", ""]
235
+ if reminder_plans:
236
+ lines += [
237
+ "| 序号 | 日程 | 地点 | 开始时间 | 提醒时间 | 类型 | 状态 |",
238
+ "|------|------|------|----------|----------|------|------|",
239
+ ]
240
+ for i, p in enumerate(reminder_plans, 1):
241
+ start_str = p.event.start.strftime("%H:%M")
242
+ remind_str = p.remind_at.strftime("%H:%M")
243
+ status = "✅ 将创建" if p.should_schedule else "⏩ 已过期"
244
+ location = p.event.location or "-"
245
+ lines.append(
246
+ f"| {i} | {p.event.summary} | {location} | {start_str} | {remind_str} | {p.event_type} | {status} |"
247
+ )
248
+ else:
249
+ lines.append("> 今日无日程提醒任务")
250
+ lines.append("")
251
+
252
+ # 今日日程概览
253
+ lines += ["## 今日日程概览", ""]
254
+ if events:
255
+ for e in events:
256
+ t = e.start.strftime("%H:%M") if not e.is_all_day else "全天"
257
+ loc = f" 📍 {e.location}" if e.location else ""
258
+ lines.append(f"- {t} {e.summary}{loc}")
259
+ else:
260
+ lines.append("- 暂无日程")
261
+ lines.append("")
262
+
263
+ lines += [
264
+ "## 变更记录",
265
+ "",
266
+ f"- {today.strftime('%H:%M')} 自动生成今日 Cron 任务文件",
267
+ ]
268
+
269
+ return "\n".join(lines)
270
+
271
+
272
+ # ─────────────────────────── main ─────────────────────────────────
273
+
274
+ def main():
275
+ parser = argparse.ArgumentParser(description="每日计划生成器")
276
+ parser.add_argument("--days", type=int, default=1,
277
+ help="拉取日程天数(1=今天,2=今天+明天)")
278
+ parser.add_argument("--dry-run", action="store_true",
279
+ help="只生成 md 文件,不注册 cron 任务")
280
+ args = parser.parse_args()
281
+
282
+ now = now_tz(TIMEZONE)
283
+ today = now.replace(hour=0, minute=0, second=0, microsecond=0)
284
+
285
+ # ── 1. 拉取日程 ────────────────────────────────────────────────
286
+ if args.days >= 2:
287
+ t_min, t_max = get_today_and_tomorrow_window(TIMEZONE)
288
+ else:
289
+ t_min, t_max = get_today_window(TIMEZONE)
290
+
291
+ try:
292
+ views = list_event_views(t_min, t_max)
293
+ events = [item.event for item in views]
294
+ logger.info(f"拉取到 {len(events)} 个事件")
295
+ except Exception as e:
296
+ logger.error(f"拉取日程失败: {e}")
297
+ events = []
298
+
299
+ # ── 2. 生成提醒计划 ─────────────────────────────────────────────
300
+ plans = plan_reminders(events, now=now, tz=TIMEZONE)
301
+
302
+ # ── 3. 冲突检测 ─────────────────────────────────────────────────
303
+ conflicts = detect_conflicts(events)
304
+ if conflicts:
305
+ logger.warning(f"⚠️ 检测到 {len(conflicts)} 处日程冲突")
306
+ for c in conflicts:
307
+ logger.warning(f" {c.format_text()}")
308
+
309
+ # ── 4. 加载固定任务 ─────────────────────────────────────────────
310
+ task_defs = load_task_registry()
311
+
312
+ # ── 5. 注册 cron 任务 ──────────────────────────────────────────
313
+ existing = set() if args.dry_run else _get_existing_task_names()
314
+
315
+ # 固定任务
316
+ for t in task_defs:
317
+ payload = parse_fixed_task(t, today)
318
+ if payload:
319
+ register_cron_task(payload, args.dry_run, existing)
320
+
321
+ # 日程提醒
322
+ for p in plans:
323
+ if not p.should_schedule:
324
+ logger.info(f"⏩ 提醒时间已过,跳过: {p.event.summary}")
325
+ continue
326
+ payload = build_reminder_task(
327
+ name = build_reminder_task_name(p.event.summary, p.event.start, p.event.id),
328
+ remind_at_utc= p.remind_at_utc,
329
+ message = p.reminder_message(),
330
+ )
331
+ register_cron_task(payload, args.dry_run, existing)
332
+
333
+ # ── 6. 生成 md 文件 ─────────────────────────────────────────────
334
+ DAILY_DIR.mkdir(parents=True, exist_ok=True)
335
+ md_path = DAILY_DIR / f"cron-{today.strftime('%Y%m%d')}.md"
336
+ md_content = generate_markdown(today, task_defs, events, plans, conflicts)
337
+ md_path.write_text(md_content, encoding="utf-8")
338
+ logger.info(f"📄 已生成: {md_path}")
339
+
340
+
341
+ if __name__ == "__main__":
342
+ main()