sophhub 0.2.3 → 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.
- package/package.json +1 -1
- package/skills/consensus/skill.json +20 -0
- package/skills/consensus/src/SKILL.md +93 -0
- package/skills/deepwiki/skill.json +20 -0
- package/skills/deepwiki/src/SKILL.md +45 -0
- package/skills/deepwiki/src/_meta.json +6 -0
- package/skills/deepwiki/src/scripts/deepwiki.js +135 -0
- package/skills/feishu-bitable/skill.json +20 -0
- package/skills/feishu-bitable/src/CHECKLIST.md +150 -0
- package/skills/feishu-bitable/src/README.md +178 -0
- package/skills/feishu-bitable/src/SKILL.md +113 -0
- package/skills/feishu-bitable/src/_meta.json +6 -0
- package/skills/feishu-bitable/src/api.js +381 -0
- package/skills/feishu-bitable/src/bin/cli.js +284 -0
- package/skills/feishu-bitable/src/description.md +143 -0
- package/skills/feishu-bitable/src/examples/create-records.json +52 -0
- package/skills/feishu-bitable/src/examples/create-table.json +64 -0
- package/skills/feishu-bitable/src/package-lock.json +324 -0
- package/skills/feishu-bitable/src/package.json +33 -0
- package/skills/feishu-bitable/src/publish-config.json +14 -0
- package/skills/feishu-bitable/src/test-simple.js +61 -0
- package/skills/feishu-bitable/src/utils.js +261 -0
- package/skills/google-maps/skill.json +20 -0
- package/skills/google-maps/src/SKILL.md +237 -0
- package/skills/google-maps/src/_meta.json +6 -0
- package/skills/google-maps/src/lib/map_helper.py +912 -0
- package/skills/large-task-router/skill.json +20 -0
- package/skills/large-task-router/src/SKILL.md +79 -0
- package/skills/large-task-router/src/templates/plan.md +74 -0
- package/skills/skillhub/skill.json +11 -4
- package/skills/skillhub/src/SKILL.md +11 -1
- package/skills/sophnet-dailynews/skill.json +20 -0
- package/skills/sophnet-dailynews/src/SKILL.md +179 -0
- package/skills/sophnet-dailynews/src/cache.json +151 -0
- package/skills/sophnet-dailynews/src/sources.json +230 -0
- package/skills/sophnet-schedule/skill.json +20 -0
- package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
- package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
- package/skills/sophnet-schedule/src/SKILL.md +1050 -0
- package/skills/sophnet-schedule/src/_meta.json +6 -0
- package/skills/sophnet-schedule/src/api/__init__.py +0 -0
- package/skills/sophnet-schedule/src/api/models.py +245 -0
- package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
- package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
- package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
- package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
- package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
- package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
- package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
- package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
- package/skills/sophnet-schedule/src/compat.py +66 -0
- package/skills/sophnet-schedule/src/config/__init__.py +0 -0
- package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
- package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
- package/skills/sophnet-schedule/src/config/settings.py +133 -0
- package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
- package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
- package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
- package/skills/sophnet-schedule/src/gcal/client.py +374 -0
- package/skills/sophnet-schedule/src/gcal/models.py +91 -0
- package/skills/sophnet-schedule/src/requirements.txt +6 -0
- package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
- package/skills/sophnet-schedule/src/server.py +669 -0
- package/skills/sophnet-schedule/src/services/__init__.py +0 -0
- package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
- package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
- package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
- package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
- package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
- package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
- package/skills/sophnet-schedule/src/services/job_store.py +100 -0
- package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
- package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
- package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
- package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
- package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
- package/skills/sophnet-schedule/src/services/time_window.py +72 -0
- package/skills/sophnet-stock/skill.json +20 -0
- package/skills/sophnet-stock/src/App-Plan.md +442 -0
- package/skills/sophnet-stock/src/README.md +214 -0
- package/skills/sophnet-stock/src/SKILL.md +236 -0
- package/skills/sophnet-stock/src/TODO.md +394 -0
- package/skills/sophnet-stock/src/_meta.json +6 -0
- package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
- package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
- package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
- package/skills/sophnet-stock/src/docs/README.md +95 -0
- package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
- package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
- package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
- package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
- package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
- package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
- package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
- package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
- package/skills/xiaohongshu/skill.json +20 -0
- package/skills/xiaohongshu/src/SKILL.md +91 -0
- package/skills/xiaohongshu/src/_meta.json +6 -0
- package/skills/xiaohongshu/src/assets/card.html +216 -0
- package/skills/xiaohongshu/src/assets/cover.html +82 -0
- package/skills/xiaohongshu/src/assets/example.md +84 -0
- package/skills/xiaohongshu/src/assets/styles.css +318 -0
- package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
- package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
- package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
- package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
- package/skills/xiaohongshu/src/workflow.py +185 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
批量日程导入工具。
|
|
4
|
+
|
|
5
|
+
接收 JSON 格式的事件列表,批量写入 Google Calendar,并可选注册 OpenClaw 提醒任务。
|
|
6
|
+
配合 table_parser.py 使用:先解析文件/OCR 内容得到 JSON,再由本脚本批量写入 GCal。
|
|
7
|
+
|
|
8
|
+
输入格式(每条事件):
|
|
9
|
+
{
|
|
10
|
+
"summary": "产品评审", # 必填
|
|
11
|
+
"date": "2026-04-02", # 必填 YYYY-MM-DD
|
|
12
|
+
"start": "14:00", # 必填 HH:MM
|
|
13
|
+
"end": "15:30", # 可选(省略则 start + 60 分钟)
|
|
14
|
+
"location": "线上", # 可选
|
|
15
|
+
"description": "Q2 规划", # 可选
|
|
16
|
+
"remind": 15 # 可选,提前提醒分钟数(0=不提醒)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
# 从 JSON 字符串
|
|
21
|
+
python3 apps/import_events.py --json '[{"summary":"...","date":"...","start":"..."}]'
|
|
22
|
+
|
|
23
|
+
# 从 JSON 文件
|
|
24
|
+
python3 apps/import_events.py --file /tmp/events.json
|
|
25
|
+
|
|
26
|
+
# 通过 stdin(配合 table_parser 管道)
|
|
27
|
+
python3 apps/import_events.py --stdin
|
|
28
|
+
|
|
29
|
+
# 干运行(不写 GCal)
|
|
30
|
+
python3 apps/import_events.py --file /tmp/events.json --dry-run
|
|
31
|
+
|
|
32
|
+
# 覆盖所有事件的提醒时间
|
|
33
|
+
python3 apps/import_events.py --file /tmp/events.json --remind-default 15
|
|
34
|
+
|
|
35
|
+
输出(JSON):
|
|
36
|
+
{"ok": true, "added": 3, "failed": 0, "skipped": 0, "results": [...]}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import argparse
|
|
42
|
+
import json
|
|
43
|
+
import sys
|
|
44
|
+
from datetime import timedelta
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
48
|
+
|
|
49
|
+
from config.settings import TIMEZONE
|
|
50
|
+
from gcal.client import CalendarAPIError, CalendarAuthError
|
|
51
|
+
from services.calendar_backend import active_provider, create_event as backend_create_event
|
|
52
|
+
from services.datetime_utils import DateTimeValidationError, build_event_range
|
|
53
|
+
from services.job_store import append_job_if_absent
|
|
54
|
+
from services.task_builder import build_reminder_task, build_reminder_task_name
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ─────────────────────────── 事件写入 ────────────────────────────
|
|
58
|
+
|
|
59
|
+
def _import_one(
|
|
60
|
+
event: dict,
|
|
61
|
+
dry_run: bool,
|
|
62
|
+
remind_override: int | None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""
|
|
65
|
+
导入单个事件,返回结果 dict。
|
|
66
|
+
result: {"summary": ..., "ok": bool, "event_id": ..., "error": ...}
|
|
67
|
+
"""
|
|
68
|
+
summary = event.get("summary", "").strip()
|
|
69
|
+
date_str = event.get("date", "").strip()
|
|
70
|
+
start_str = event.get("start", "").strip()
|
|
71
|
+
|
|
72
|
+
if not summary or not date_str or not start_str:
|
|
73
|
+
return {"summary": summary or "(无标题)", "ok": False,
|
|
74
|
+
"error": "缺少必填字段 summary/date/start"}
|
|
75
|
+
|
|
76
|
+
end_str = event.get("end", "")
|
|
77
|
+
try:
|
|
78
|
+
start, end = build_event_range(
|
|
79
|
+
date_str,
|
|
80
|
+
start_str,
|
|
81
|
+
end_str or None,
|
|
82
|
+
60,
|
|
83
|
+
TIMEZONE,
|
|
84
|
+
)
|
|
85
|
+
except DateTimeValidationError as e:
|
|
86
|
+
return {"summary": summary, "ok": False, "error": str(e)}
|
|
87
|
+
|
|
88
|
+
location = event.get("location", "")
|
|
89
|
+
description = event.get("description", "")
|
|
90
|
+
remind = remind_override if remind_override is not None else int(event.get("remind", 0))
|
|
91
|
+
|
|
92
|
+
if dry_run:
|
|
93
|
+
return {
|
|
94
|
+
"summary": summary,
|
|
95
|
+
"ok": True,
|
|
96
|
+
"dry_run": True,
|
|
97
|
+
"provider": active_provider(),
|
|
98
|
+
"date": date_str,
|
|
99
|
+
"start": start.strftime("%H:%M"),
|
|
100
|
+
"end": end.strftime("%H:%M"),
|
|
101
|
+
"location": location,
|
|
102
|
+
"remind": remind,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
created = backend_create_event(
|
|
107
|
+
summary=summary, start=start, end=end,
|
|
108
|
+
location=location, description=description,
|
|
109
|
+
remind_minutes=remind,
|
|
110
|
+
)
|
|
111
|
+
except (CalendarAuthError, CalendarAPIError) as e:
|
|
112
|
+
return {"summary": summary, "ok": False, "error": str(e)}
|
|
113
|
+
|
|
114
|
+
# 可选:注册 OpenClaw 提醒
|
|
115
|
+
reminder_status = {"registered": False, "existed": False, "error": None}
|
|
116
|
+
if remind > 0:
|
|
117
|
+
try:
|
|
118
|
+
from datetime import timezone as _tz
|
|
119
|
+
remind_at_utc = (start - timedelta(minutes=remind)).astimezone(_tz.utc)
|
|
120
|
+
payload = build_reminder_task(
|
|
121
|
+
name=build_reminder_task_name(summary, start, created.event_id),
|
|
122
|
+
remind_at_utc=remind_at_utc,
|
|
123
|
+
message=(
|
|
124
|
+
f"⏰ **{summary}** 将在 {remind} 分钟后开始\n"
|
|
125
|
+
f"🕐 {start.strftime('%H:%M')}"
|
|
126
|
+
+ (f"\n📍 {location}" if location else "")
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
if append_job_if_absent(payload):
|
|
130
|
+
reminder_status["registered"] = True
|
|
131
|
+
reminder_status["existed"] = False
|
|
132
|
+
else:
|
|
133
|
+
reminder_status["registered"] = True
|
|
134
|
+
reminder_status["existed"] = True
|
|
135
|
+
except Exception as e:
|
|
136
|
+
reminder_status["error"] = str(e)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"summary": summary,
|
|
140
|
+
"ok": True,
|
|
141
|
+
"provider": created.provider,
|
|
142
|
+
"sync_state": created.sync_state,
|
|
143
|
+
"event_id": created.event_id,
|
|
144
|
+
"date": date_str,
|
|
145
|
+
"start": start.strftime("%H:%M"),
|
|
146
|
+
"end": end.strftime("%H:%M"),
|
|
147
|
+
"location": location,
|
|
148
|
+
"remind": remind,
|
|
149
|
+
"reminder_registered": reminder_status["registered"],
|
|
150
|
+
"reminder_existed": reminder_status["existed"],
|
|
151
|
+
"reminder_error": reminder_status["error"],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ─────────────────────────── main ────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def main() -> None:
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
description="批量导入日程事件到 Google Calendar",
|
|
160
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
161
|
+
)
|
|
162
|
+
src = parser.add_mutually_exclusive_group(required=True)
|
|
163
|
+
src.add_argument("--json", help="事件 JSON 字符串(数组)")
|
|
164
|
+
src.add_argument("--file", help="事件 JSON 文件路径")
|
|
165
|
+
src.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON")
|
|
166
|
+
parser.add_argument("--remind-default", type=int, default=None,
|
|
167
|
+
help="覆盖所有事件的提前提醒分钟数(0=不提醒)")
|
|
168
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
169
|
+
help="只打印 payload,不写入 Google Calendar")
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
|
|
172
|
+
# ── 读取事件列表 ──────────────────────────────────────────────
|
|
173
|
+
try:
|
|
174
|
+
if args.json:
|
|
175
|
+
events = json.loads(args.json)
|
|
176
|
+
elif args.file:
|
|
177
|
+
with open(args.file, encoding="utf-8") as f:
|
|
178
|
+
events = json.load(f)
|
|
179
|
+
else:
|
|
180
|
+
events = json.load(sys.stdin)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(json.dumps({"ok": False, "error": f"读取事件失败: {e}"}, ensure_ascii=False))
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
if not isinstance(events, list):
|
|
186
|
+
events = [events]
|
|
187
|
+
|
|
188
|
+
if not events:
|
|
189
|
+
print(json.dumps({"ok": True, "added": 0, "failed": 0, "skipped": 0, "results": []},
|
|
190
|
+
ensure_ascii=False))
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# ── 逐条写入 ─────────────────────────────────────────────────
|
|
194
|
+
results = []
|
|
195
|
+
added = failed = skipped = 0
|
|
196
|
+
|
|
197
|
+
for ev in events:
|
|
198
|
+
r = _import_one(ev, args.dry_run, args.remind_default)
|
|
199
|
+
results.append(r)
|
|
200
|
+
if r["ok"]:
|
|
201
|
+
added += 1
|
|
202
|
+
else:
|
|
203
|
+
failed += 1
|
|
204
|
+
|
|
205
|
+
summary = {
|
|
206
|
+
"ok": failed == 0,
|
|
207
|
+
"added": added,
|
|
208
|
+
"failed": failed,
|
|
209
|
+
"skipped": skipped,
|
|
210
|
+
"results": results,
|
|
211
|
+
}
|
|
212
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
日程变化监控器。
|
|
4
|
+
|
|
5
|
+
执行时机:每小时整点(北京时间 8:00–20:00),由 OpenClaw Cron 触发。
|
|
6
|
+
|
|
7
|
+
流程:
|
|
8
|
+
1. 加载上次快照(data/last_events.json)
|
|
9
|
+
2. 拉取 Google Calendar 今天+明天的事件
|
|
10
|
+
3. 对比变化:新增、修改、取消
|
|
11
|
+
4. 保存新快照
|
|
12
|
+
5. 有变化时打印通知(OpenClaw 捕获 stdout 并投递)
|
|
13
|
+
无变化时只打印日志,不输出通知
|
|
14
|
+
|
|
15
|
+
核心规则:
|
|
16
|
+
- 无变化 → 不通知
|
|
17
|
+
- 已结束事件消失 → 不通知
|
|
18
|
+
- 被取消事件消失 → 通知
|
|
19
|
+
|
|
20
|
+
用法:
|
|
21
|
+
python3 apps/monitor_calendar_changes.py
|
|
22
|
+
python3 apps/monitor_calendar_changes.py --dry-run # 不写快照,只打印结果
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import sys
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
# ── sys.path ─────────────────────────────────────────────────────
|
|
35
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
36
|
+
|
|
37
|
+
from config.settings import (
|
|
38
|
+
GCAL_TOKEN_PATH, PROXIES, GCAL_TIMEOUT, GCAL_MAX_RETRIES,
|
|
39
|
+
GCAL_CALENDAR_ID, GCAL_SSL_VERIFY, TIMEZONE, DATA_DIR, LAST_EVENTS_FILE,
|
|
40
|
+
)
|
|
41
|
+
from gcal.client import CalendarClient
|
|
42
|
+
from gcal.models import CalendarEvent
|
|
43
|
+
from services.calendar_backend import active_provider
|
|
44
|
+
from services.time_window import get_today_and_tomorrow_window
|
|
45
|
+
from services.event_diff import diff_events
|
|
46
|
+
|
|
47
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ─────────────────────────── 快照读写 ────────────────────────────
|
|
52
|
+
|
|
53
|
+
def load_snapshot() -> list[CalendarEvent]:
|
|
54
|
+
if not LAST_EVENTS_FILE.exists():
|
|
55
|
+
logger.info("📝 无历史快照,首次运行")
|
|
56
|
+
return []
|
|
57
|
+
try:
|
|
58
|
+
with open(LAST_EVENTS_FILE, encoding="utf-8") as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
events = [CalendarEvent.from_dict(e) for e in data.get("events", [])]
|
|
61
|
+
logger.info(f"加载上次快照:{len(events)} 个事件,时间 {data.get('last_updated', '未知')}")
|
|
62
|
+
return events
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"读取快照失败: {e}")
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def save_snapshot(events: list[CalendarEvent]) -> None:
|
|
69
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
now_utc = datetime.now(timezone.utc).isoformat()
|
|
71
|
+
# 只保存未来的事件(已结束的不再追踪)
|
|
72
|
+
future = [e for e in events if not e.is_past()]
|
|
73
|
+
data = {
|
|
74
|
+
"events": [e.to_dict() for e in future],
|
|
75
|
+
"last_updated": now_utc,
|
|
76
|
+
"total": len(future),
|
|
77
|
+
}
|
|
78
|
+
with open(LAST_EVENTS_FILE, "w", encoding="utf-8") as f:
|
|
79
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
80
|
+
logger.info(f"✅ 快照已更新:{len(future)} 个未来事件")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ─────────────────────────── main ────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
parser = argparse.ArgumentParser(description="日程变化监控")
|
|
87
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
88
|
+
help="不写快照,仅打印变化结果")
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
|
|
91
|
+
logger.info(f"=== 日程变化监控 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===")
|
|
92
|
+
|
|
93
|
+
if active_provider() != "google":
|
|
94
|
+
logger.info("当前为本地模式,跳过 Google 日程变化轮询")
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
# ── 1. 拉取当前日程 ───────────────────────────────────────────
|
|
98
|
+
client = CalendarClient(
|
|
99
|
+
GCAL_TOKEN_PATH, PROXIES,
|
|
100
|
+
calendar_id=GCAL_CALENDAR_ID,
|
|
101
|
+
timeout=GCAL_TIMEOUT,
|
|
102
|
+
max_retries=GCAL_MAX_RETRIES,
|
|
103
|
+
verify=GCAL_SSL_VERIFY,
|
|
104
|
+
)
|
|
105
|
+
t_min, t_max = get_today_and_tomorrow_window(TIMEZONE)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
current_events = client.list_events(t_min, t_max)
|
|
109
|
+
logger.info(f"当前事件数:{len(current_events)}")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"❌ 拉取日程失败: {e}")
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
# ── 2. 加载上次快照 ──────────────────────────────────────────
|
|
115
|
+
last_events = load_snapshot()
|
|
116
|
+
|
|
117
|
+
# ── 3. diff ──────────────────────────────────────────────────
|
|
118
|
+
result = diff_events(
|
|
119
|
+
current=current_events,
|
|
120
|
+
last=last_events,
|
|
121
|
+
get_remote_status=client.get_event_status,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# ── 4. 保存快照 ──────────────────────────────────────────────
|
|
125
|
+
if not args.dry_run:
|
|
126
|
+
save_snapshot(current_events)
|
|
127
|
+
|
|
128
|
+
# ── 5. 输出结果 ──────────────────────────────────────────────
|
|
129
|
+
if not result.has_changes:
|
|
130
|
+
logger.info("✅ 无变化,不发送通知")
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
logger.info(f"检测到变化:{result.summary_text}")
|
|
134
|
+
# OpenClaw cron 会捕获 stdout 并投递给 用户
|
|
135
|
+
# 监控任务配置为 delivery:none,由此脚本自行控制是否通知
|
|
136
|
+
print(result.format_notification())
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
使用 OpenClaw API 注册 Cron 任务(不依赖 openclaw 命令)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
# Handle zoneinfo import for Python 3.8 compatibility
|
|
13
|
+
try:
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
15
|
+
except ImportError:
|
|
16
|
+
from backports.zoneinfo import ZoneInfo
|
|
17
|
+
|
|
18
|
+
# 添加父目录到路径
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
20
|
+
|
|
21
|
+
from config.settings import (
|
|
22
|
+
TIMEZONE,
|
|
23
|
+
TASK_REGISTRY_FILE,
|
|
24
|
+
)
|
|
25
|
+
from services.task_builder import (
|
|
26
|
+
build_reminder_task,
|
|
27
|
+
build_periodic_silent_task,
|
|
28
|
+
build_report_task,
|
|
29
|
+
)
|
|
30
|
+
from services.job_store import append_job_if_absent, list_job_names
|
|
31
|
+
from services.runtime_utils import expand_command_vars
|
|
32
|
+
from services.datetime_utils import now_local
|
|
33
|
+
from services.calendar_backend import active_provider
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _utc_str(dt: datetime) -> str:
|
|
37
|
+
"""转换为 UTC 时间字符串"""
|
|
38
|
+
if dt.tzinfo is None:
|
|
39
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
40
|
+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_task_enabled(task: Dict[str, Any], provider: str | None = None) -> bool:
|
|
44
|
+
required = task.get("requires_provider", "any")
|
|
45
|
+
current = provider or active_provider()
|
|
46
|
+
return required in {"", "any", current}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_fixed_tasks(provider: str | None = None) -> List[Dict[str, Any]]:
|
|
50
|
+
"""加载固定任务配置"""
|
|
51
|
+
import yaml
|
|
52
|
+
with open(TASK_REGISTRY_FILE) as f:
|
|
53
|
+
data = yaml.safe_load(f)
|
|
54
|
+
tasks = data.get("tasks", [])
|
|
55
|
+
return [task for task in tasks if is_task_enabled(task, provider=provider)]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_cron_job_from_task(task: Dict[str, Any], today: datetime) -> Optional[Dict[str, Any]]:
|
|
59
|
+
"""根据任务配置构建 Cron Job"""
|
|
60
|
+
name = task.get("name", "")
|
|
61
|
+
task_type = task.get("type", "")
|
|
62
|
+
message = expand_command_vars(task.get("message", task.get("command", "")))
|
|
63
|
+
schedule = task.get("schedule", {})
|
|
64
|
+
delete = task.get("delete_after_run", True)
|
|
65
|
+
|
|
66
|
+
if not name or not schedule:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
kind = schedule.get("kind")
|
|
70
|
+
zone = ZoneInfo(TIMEZONE)
|
|
71
|
+
|
|
72
|
+
if kind == "at":
|
|
73
|
+
time_str = schedule.get("time", "")
|
|
74
|
+
try:
|
|
75
|
+
h, m = map(int, time_str.split(":"))
|
|
76
|
+
bj_time = today.astimezone(zone).replace(hour=h, minute=m, second=0, microsecond=0)
|
|
77
|
+
except (ValueError, IndexError):
|
|
78
|
+
print("⚠️ 无法解析时间 '{}', 跳过任务 {}".format(time_str, name))
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
if task_type == "silent":
|
|
82
|
+
# 静默一次性任务
|
|
83
|
+
return {
|
|
84
|
+
"name": name,
|
|
85
|
+
"schedule": {"kind": "at", "at": _utc_str(bj_time)},
|
|
86
|
+
"sessionTarget": "isolated",
|
|
87
|
+
"payload": {"kind": "agentTurn", "message": message},
|
|
88
|
+
"delivery": {"mode": "none"},
|
|
89
|
+
"deleteAfterRun": delete,
|
|
90
|
+
"enabled": True,
|
|
91
|
+
}
|
|
92
|
+
else:
|
|
93
|
+
# 报告任务
|
|
94
|
+
return build_report_task(
|
|
95
|
+
name, message,
|
|
96
|
+
at_utc=bj_time, delete_after_run=delete,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
elif kind == "cron":
|
|
100
|
+
cron_expr = schedule.get("expr", "")
|
|
101
|
+
tz = schedule.get("tz", TIMEZONE)
|
|
102
|
+
if task_type == "silent":
|
|
103
|
+
return build_periodic_silent_task(name, cron_expr, message, tz)
|
|
104
|
+
else:
|
|
105
|
+
return build_report_task(
|
|
106
|
+
name, message,
|
|
107
|
+
cron_expr=cron_expr, tz=tz, delete_after_run=delete,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
import argparse
|
|
115
|
+
parser = argparse.ArgumentParser(description="使用 OpenClaw API 注册 Cron 任务")
|
|
116
|
+
parser.add_argument("--dry-run", action="store_true", help="只生成,不注册")
|
|
117
|
+
parser.add_argument("--days", type=int, default=1, help="生成 N 天的任务")
|
|
118
|
+
args = parser.parse_args()
|
|
119
|
+
|
|
120
|
+
# 加载任务配置
|
|
121
|
+
tasks = load_fixed_tasks()
|
|
122
|
+
print("📋 加载了 {} 个固定任务配置\n".format(len(tasks)))
|
|
123
|
+
|
|
124
|
+
# 生成任务
|
|
125
|
+
today = now_local(TIMEZONE)
|
|
126
|
+
jobs = []
|
|
127
|
+
|
|
128
|
+
for task in tasks:
|
|
129
|
+
job = build_cron_job_from_task(task, today)
|
|
130
|
+
if job:
|
|
131
|
+
jobs.append(job)
|
|
132
|
+
if args.dry_run:
|
|
133
|
+
print("[dry-run] 将注册: {}".format(job['name']))
|
|
134
|
+
else:
|
|
135
|
+
print("✅ 准备注册: {}".format(job['name']))
|
|
136
|
+
|
|
137
|
+
if args.dry_run:
|
|
138
|
+
print("\n📊 共 {} 个任务(dry-run 模式,未注册)".format(len(jobs)))
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
existing_names = list_job_names()
|
|
143
|
+
print("\n📄 已有 {} 个任务".format(len(existing_names)))
|
|
144
|
+
|
|
145
|
+
new_jobs = []
|
|
146
|
+
for job in jobs:
|
|
147
|
+
name = job.get("name", "")
|
|
148
|
+
if name not in existing_names:
|
|
149
|
+
if append_job_if_absent(job):
|
|
150
|
+
new_jobs.append(job)
|
|
151
|
+
existing_names.add(name)
|
|
152
|
+
print("✅ 已注册: {}".format(name))
|
|
153
|
+
else:
|
|
154
|
+
print("⏭️ 任务已存在,跳过: {}".format(name))
|
|
155
|
+
else:
|
|
156
|
+
print("⏭️ 任务已存在,跳过: {}".format(name))
|
|
157
|
+
|
|
158
|
+
print("\n🎉 成功注册 {} 个新任务!".format(len(new_jobs)))
|
|
159
|
+
print("📁 任务已保存到: {}".format(Path.home() / ".openclaw" / "cron" / "jobs.json"))
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print("\n❌ 注册失败: {}".format(e))
|
|
163
|
+
import traceback
|
|
164
|
+
traceback.print_exc()
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|