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.
- 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/flight-booking/skill.json +9 -2
- package/skills/flight-booking/src/scripts/flight_booking.py +2 -1
- 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
|
File without changes
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API 请求 / 响应 Pydantic 模型。
|
|
3
|
+
|
|
4
|
+
所有外部接口的 schema 集中在此定义,与业务逻辑解耦。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── 通用 ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
class OkResponse(BaseModel):
|
|
16
|
+
ok: bool = True
|
|
17
|
+
id: Optional[str] = None
|
|
18
|
+
message: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── 日历事件 ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
class EventOut(BaseModel):
|
|
24
|
+
id: str
|
|
25
|
+
summary: str
|
|
26
|
+
start: str # ISO 8601
|
|
27
|
+
end: str
|
|
28
|
+
is_all_day: bool = False
|
|
29
|
+
location: str = ""
|
|
30
|
+
description: str = ""
|
|
31
|
+
status: str = "confirmed"
|
|
32
|
+
duration_minutes: int = 0
|
|
33
|
+
provider: str = ""
|
|
34
|
+
sync_state: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConflictOut(BaseModel):
|
|
38
|
+
type: str # "conflict" | "tight"
|
|
39
|
+
event_a: str # summary
|
|
40
|
+
event_b: str
|
|
41
|
+
time_a: str # "HH:MM–HH:MM"
|
|
42
|
+
time_b: str
|
|
43
|
+
overlap_minutes: int = 0
|
|
44
|
+
gap_minutes: int = 0
|
|
45
|
+
text: str # 格式化文字描述
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EventsResponse(BaseModel):
|
|
49
|
+
date: str
|
|
50
|
+
events: List[EventOut]
|
|
51
|
+
conflicts: List[ConflictOut]
|
|
52
|
+
provider: str = ""
|
|
53
|
+
active_mode: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EventCreateRequest(BaseModel):
|
|
57
|
+
summary: str = Field(..., description="事件标题")
|
|
58
|
+
date: str = Field(..., description="日期 YYYY-MM-DD")
|
|
59
|
+
start: str = Field(..., description="开始时间 HH:MM")
|
|
60
|
+
end: Optional[str] = Field(None, description="结束时间 HH:MM(与 duration 二选一)")
|
|
61
|
+
duration: int = Field(60, gt=0, description="持续分钟数(默认 60)")
|
|
62
|
+
location: str = Field("", description="地点")
|
|
63
|
+
description: str = Field("", description="备注")
|
|
64
|
+
remind: int = Field(0, ge=0, description="提前提醒分钟数(0 = 不注册)")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class EventCreateResponse(BaseModel):
|
|
68
|
+
ok: bool
|
|
69
|
+
event_id: str = ""
|
|
70
|
+
summary: str = ""
|
|
71
|
+
start: str = ""
|
|
72
|
+
end: str = ""
|
|
73
|
+
reminder_registered: bool = False
|
|
74
|
+
error: str = ""
|
|
75
|
+
provider: str = ""
|
|
76
|
+
sync_state: str = ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EventDeleteResponse(BaseModel):
|
|
80
|
+
ok: bool
|
|
81
|
+
event_id: str
|
|
82
|
+
error: str = ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── ROC 事件 ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
class RocEventOut(BaseModel):
|
|
88
|
+
id: str
|
|
89
|
+
name: str
|
|
90
|
+
rule: str
|
|
91
|
+
reminder_time: str
|
|
92
|
+
lead_days: int
|
|
93
|
+
description: str = ""
|
|
94
|
+
# 计算字段(含年份时附带)
|
|
95
|
+
event_date: Optional[str] = None
|
|
96
|
+
reminder_date: Optional[str] = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RocListResponse(BaseModel):
|
|
100
|
+
year: int
|
|
101
|
+
events: List[RocEventOut]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RocUpcomingItem(BaseModel):
|
|
105
|
+
id: str
|
|
106
|
+
name: str
|
|
107
|
+
event_date: str
|
|
108
|
+
reminder_date: str
|
|
109
|
+
days_until_reminder: int
|
|
110
|
+
days_until_event: int
|
|
111
|
+
description: str = ""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RocUpcomingResponse(BaseModel):
|
|
115
|
+
today: str
|
|
116
|
+
days: int
|
|
117
|
+
upcoming: List[RocUpcomingItem]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RocEventCreateRequest(BaseModel):
|
|
121
|
+
id: str = Field(..., description="唯一 ID(英文小写+下划线)")
|
|
122
|
+
name: str = Field(..., description="事件名称")
|
|
123
|
+
rule: str = Field(..., description="时间规则,如 每年6月15日")
|
|
124
|
+
reminder_time: str = Field("09:00", description="推送时间 HH:MM")
|
|
125
|
+
lead_days: int = Field(7, ge=0, description="提前提醒天数")
|
|
126
|
+
description: str = Field("", description="说明")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RocEventUpdateRequest(BaseModel):
|
|
130
|
+
name: Optional[str] = None
|
|
131
|
+
rule: Optional[str] = None
|
|
132
|
+
reminder_time: Optional[str] = None
|
|
133
|
+
lead_days: Optional[int] = None
|
|
134
|
+
description: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── 提醒 ──────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
class ReminderItem(BaseModel):
|
|
140
|
+
summary: str
|
|
141
|
+
start: str
|
|
142
|
+
end: str
|
|
143
|
+
lead_minutes: int
|
|
144
|
+
trigger_at: str
|
|
145
|
+
message: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class RemindersResponse(BaseModel):
|
|
149
|
+
window_minutes: int
|
|
150
|
+
reminders: List[ReminderItem]
|
|
151
|
+
provider: str = ""
|
|
152
|
+
active_mode: str = ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Cron 任务 ─────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
class TaskItem(BaseModel):
|
|
158
|
+
name: str
|
|
159
|
+
description: str
|
|
160
|
+
schedule_kind: str # "at" | "cron"
|
|
161
|
+
schedule_expr: str # HH:MM 或 cron expr
|
|
162
|
+
type: str # "silent" | "report"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TasksResponse(BaseModel):
|
|
166
|
+
tasks: List[TaskItem]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TaskSyncResponse(BaseModel):
|
|
170
|
+
ok: bool = True
|
|
171
|
+
dry_run: bool = False
|
|
172
|
+
added: List[str] = []
|
|
173
|
+
skipped: List[str] = []
|
|
174
|
+
errors: List[str] = []
|
|
175
|
+
message: str = ""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── 日报 ──────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
class DailyPlanResponse(BaseModel):
|
|
181
|
+
date: str
|
|
182
|
+
events: List[EventOut]
|
|
183
|
+
conflicts: List[ConflictOut]
|
|
184
|
+
reminders_registered: int
|
|
185
|
+
markdown: str # 完整日报 Markdown 文本
|
|
186
|
+
provider: str = ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ── Google 接入 ───────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
class GoogleGuideStep(BaseModel):
|
|
192
|
+
key: str
|
|
193
|
+
title: str
|
|
194
|
+
description: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class GoogleSyncSummary(BaseModel):
|
|
198
|
+
status: str = "idle"
|
|
199
|
+
total: int = 0
|
|
200
|
+
succeeded: int = 0
|
|
201
|
+
failed: int = 0
|
|
202
|
+
skipped: int = 0
|
|
203
|
+
started_at: str = ""
|
|
204
|
+
finished_at: str = ""
|
|
205
|
+
last_error: str = ""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class GoogleStatusResponse(BaseModel):
|
|
209
|
+
active_mode: str
|
|
210
|
+
google_state: str
|
|
211
|
+
has_token: bool
|
|
212
|
+
masked_client_id: str = ""
|
|
213
|
+
needs_initial_sync: bool = False
|
|
214
|
+
current_step: str
|
|
215
|
+
last_error: str = ""
|
|
216
|
+
last_sync_time: str = ""
|
|
217
|
+
last_sync_summary: GoogleSyncSummary
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class GoogleGuideResponse(BaseModel):
|
|
221
|
+
current_step: str
|
|
222
|
+
active_mode: str
|
|
223
|
+
google_state: str
|
|
224
|
+
callback_url: str
|
|
225
|
+
required_fields: List[str]
|
|
226
|
+
steps: List[GoogleGuideStep]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class GoogleStartRequest(BaseModel):
|
|
230
|
+
client_id: str = Field(..., description="Google OAuth Client ID")
|
|
231
|
+
client_secret: str = Field(..., description="Google OAuth Client Secret")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GoogleStartResponse(BaseModel):
|
|
235
|
+
ok: bool = True
|
|
236
|
+
authorize_url: str
|
|
237
|
+
expires_at: str
|
|
238
|
+
google_state: str
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class GoogleSyncResponse(BaseModel):
|
|
242
|
+
ok: bool = True
|
|
243
|
+
active_mode: str
|
|
244
|
+
google_state: str
|
|
245
|
+
summary: GoogleSyncSummary
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
自然语言日程录入工具。
|
|
4
|
+
|
|
5
|
+
Agent 将用户的自然语言解析为结构化字段后,调用此脚本写入 Google Calendar,
|
|
6
|
+
并可选择性地立即在 OpenClaw Cron 中注册提醒任务。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
# 基本:只指定开始时间,持续时间默认 60 分钟
|
|
10
|
+
python3 apps/add_event.py \\
|
|
11
|
+
--summary "客户会议" \\
|
|
12
|
+
--date "2026-03-31" \\
|
|
13
|
+
--start "14:00"
|
|
14
|
+
|
|
15
|
+
# 完整:指定结束时间 + 地点 + 备注
|
|
16
|
+
python3 apps/add_event.py \\
|
|
17
|
+
--summary "产品评审" \\
|
|
18
|
+
--date "2026-04-02" \\
|
|
19
|
+
--start "10:00" \\
|
|
20
|
+
--end "11:30" \\
|
|
21
|
+
--location "线上" \\
|
|
22
|
+
--description "Q2 规划讨论" \\
|
|
23
|
+
--remind 15
|
|
24
|
+
|
|
25
|
+
# 只指定时长(分钟)而非结束时间
|
|
26
|
+
python3 apps/add_event.py \\
|
|
27
|
+
--summary "跑步" \\
|
|
28
|
+
--date "2026-04-01" \\
|
|
29
|
+
--start "07:00" \\
|
|
30
|
+
--duration 45
|
|
31
|
+
|
|
32
|
+
# 干运行(不写 GCal,只打印 payload)
|
|
33
|
+
python3 apps/add_event.py \\
|
|
34
|
+
--summary "测试事件" \\
|
|
35
|
+
--date "2026-04-01" \\
|
|
36
|
+
--start "10:00" \\
|
|
37
|
+
--dry-run
|
|
38
|
+
|
|
39
|
+
输出(JSON,供 Agent 读取):
|
|
40
|
+
成功:{"ok": true, "event_id": "xxx", "summary": "...", "start": "...", "end": "..."}
|
|
41
|
+
失败:{"ok": false, "error": "..."}
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
import sys
|
|
50
|
+
from datetime import datetime, timedelta
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
|
|
53
|
+
# ── sys.path ─────────────────────────────────────────────────────
|
|
54
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
55
|
+
|
|
56
|
+
from config.settings import TIMEZONE
|
|
57
|
+
from gcal.client import CalendarAPIError, CalendarAuthError
|
|
58
|
+
from services.calendar_backend import active_provider, create_event as backend_create_event
|
|
59
|
+
from services.datetime_utils import (
|
|
60
|
+
DateTimeValidationError,
|
|
61
|
+
build_event_range,
|
|
62
|
+
parse_local_date,
|
|
63
|
+
parse_local_time,
|
|
64
|
+
)
|
|
65
|
+
from services.job_store import append_job_if_absent
|
|
66
|
+
from services.task_builder import build_reminder_task, build_reminder_task_name
|
|
67
|
+
|
|
68
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(message)s")
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ─────────────────────────── 时间解析 ────────────────────────────
|
|
73
|
+
|
|
74
|
+
def parse_date(date_str: str, tz: str) -> datetime:
|
|
75
|
+
return parse_local_date(date_str, tz)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_time(time_str: str, base_date: datetime) -> datetime:
|
|
79
|
+
return parse_local_time(time_str, base_date)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ─────────────────────────── 提醒注册 ────────────────────────────
|
|
83
|
+
|
|
84
|
+
def _register_reminder(
|
|
85
|
+
summary: str,
|
|
86
|
+
start: datetime,
|
|
87
|
+
lead_minutes: int,
|
|
88
|
+
dry_run: bool,
|
|
89
|
+
event_id: str = "",
|
|
90
|
+
) -> dict:
|
|
91
|
+
"""
|
|
92
|
+
将提醒任务注册到 OpenClaw Cron(jobs.json)。
|
|
93
|
+
|
|
94
|
+
返回状态字典:
|
|
95
|
+
{
|
|
96
|
+
"registered": bool, # 是否成功注册
|
|
97
|
+
"existed": bool, # 是否已存在
|
|
98
|
+
"error": str or None # 错误信息(如果有)
|
|
99
|
+
}
|
|
100
|
+
"""
|
|
101
|
+
from datetime import timezone
|
|
102
|
+
|
|
103
|
+
remind_at_utc = (start - timedelta(minutes=lead_minutes)).astimezone(timezone.utc)
|
|
104
|
+
name = build_reminder_task_name(summary, start, event_id)
|
|
105
|
+
payload = build_reminder_task(
|
|
106
|
+
name=name,
|
|
107
|
+
remind_at_utc=remind_at_utc,
|
|
108
|
+
message=(
|
|
109
|
+
f"⏰ **{summary}** 将在 {lead_minutes} 分钟后开始\n"
|
|
110
|
+
f"🕐 {start.strftime('%H:%M')}"
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if dry_run:
|
|
115
|
+
print(f"[dry-run] 将注册提醒任务: {name}({remind_at_utc.strftime('%H:%M')} UTC)",
|
|
116
|
+
file=sys.stderr)
|
|
117
|
+
return {"registered": True, "existed": False, "error": None}
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if append_job_if_absent(payload):
|
|
121
|
+
logger.warning(f"✅ 提醒任务已注册: {name}")
|
|
122
|
+
return {"registered": True, "existed": False, "error": None}
|
|
123
|
+
else:
|
|
124
|
+
logger.warning(f"⏭️ 提醒任务已存在,跳过: {name}")
|
|
125
|
+
return {"registered": True, "existed": True, "error": None}
|
|
126
|
+
except Exception as e:
|
|
127
|
+
error_msg = str(e)
|
|
128
|
+
logger.warning(f"注册提醒失败(不影响日历写入): {error_msg}")
|
|
129
|
+
return {"registered": False, "existed": False, "error": error_msg}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ─────────────────────────── main ─────────────────────────────────
|
|
133
|
+
|
|
134
|
+
def main() -> None:
|
|
135
|
+
parser = argparse.ArgumentParser(
|
|
136
|
+
description="将结构化日程信息写入 Google Calendar",
|
|
137
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument("--summary", required=True, help="事件标题")
|
|
140
|
+
parser.add_argument("--date", required=True, help="日期 YYYY-MM-DD")
|
|
141
|
+
parser.add_argument("--start", required=True, help="开始时间 HH:MM")
|
|
142
|
+
parser.add_argument("--end", help="结束时间 HH:MM(与 --duration 二选一)")
|
|
143
|
+
parser.add_argument("--duration", type=int, default=60,
|
|
144
|
+
help="持续时间(分钟,默认 60);--end 优先")
|
|
145
|
+
parser.add_argument("--location", default="", help="地点(可选)")
|
|
146
|
+
parser.add_argument("--description", default="", help="备注(可选)")
|
|
147
|
+
parser.add_argument("--remind", type=int, default=0,
|
|
148
|
+
help="提前提醒分钟数(0 = 不注册提醒,默认 0)")
|
|
149
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
150
|
+
help="只打印 payload,不写入 Google Calendar")
|
|
151
|
+
args = parser.parse_args()
|
|
152
|
+
|
|
153
|
+
def _fail(msg: str) -> None:
|
|
154
|
+
print(json.dumps({"ok": False, "error": msg}, ensure_ascii=False))
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
# ── 1. 解析时间 ───────────────────────────────────────────────
|
|
158
|
+
try:
|
|
159
|
+
start, end = build_event_range(
|
|
160
|
+
args.date,
|
|
161
|
+
args.start,
|
|
162
|
+
args.end,
|
|
163
|
+
args.duration,
|
|
164
|
+
TIMEZONE,
|
|
165
|
+
)
|
|
166
|
+
except DateTimeValidationError as e:
|
|
167
|
+
_fail(str(e))
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# ── 2. dry-run ────────────────────────────────────────────────
|
|
171
|
+
payload_preview = {
|
|
172
|
+
"summary": args.summary,
|
|
173
|
+
"date": args.date,
|
|
174
|
+
"start": start.strftime("%H:%M"),
|
|
175
|
+
"end": end.strftime("%H:%M"),
|
|
176
|
+
"duration_min": int((end - start).total_seconds() / 60),
|
|
177
|
+
"location": args.location,
|
|
178
|
+
"description": args.description,
|
|
179
|
+
"remind_min": args.remind,
|
|
180
|
+
}
|
|
181
|
+
if args.dry_run:
|
|
182
|
+
print(json.dumps(
|
|
183
|
+
{
|
|
184
|
+
"ok": True,
|
|
185
|
+
"dry_run": True,
|
|
186
|
+
"provider": active_provider(),
|
|
187
|
+
"payload": payload_preview,
|
|
188
|
+
},
|
|
189
|
+
ensure_ascii=False, indent=2,
|
|
190
|
+
))
|
|
191
|
+
if args.remind > 0:
|
|
192
|
+
_register_reminder(args.summary, start, args.remind, dry_run=True)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# ── 3. 写入当前事件后端 ───────────────────────────────────────
|
|
196
|
+
try:
|
|
197
|
+
created = backend_create_event(
|
|
198
|
+
summary=args.summary,
|
|
199
|
+
start=start,
|
|
200
|
+
end=end,
|
|
201
|
+
location=args.location,
|
|
202
|
+
description=args.description,
|
|
203
|
+
remind_minutes=args.remind,
|
|
204
|
+
)
|
|
205
|
+
except (CalendarAuthError, CalendarAPIError) as e:
|
|
206
|
+
_fail(str(e))
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# ── 4. 可选:注册提醒 ─────────────────────────────────────────
|
|
210
|
+
reminder_status = {"registered": False, "existed": False, "error": None}
|
|
211
|
+
if args.remind > 0:
|
|
212
|
+
reminder_status = _register_reminder(
|
|
213
|
+
args.summary, start, args.remind, dry_run=False, event_id=created.event_id
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# ── 5. 输出结果 ───────────────────────────────────────────────
|
|
217
|
+
result = {
|
|
218
|
+
"ok": True,
|
|
219
|
+
"provider": created.provider,
|
|
220
|
+
"sync_state": created.sync_state,
|
|
221
|
+
"event_id": created.event_id,
|
|
222
|
+
"summary": args.summary,
|
|
223
|
+
"date": args.date,
|
|
224
|
+
"start": start.strftime("%H:%M"),
|
|
225
|
+
"end": end.strftime("%H:%M"),
|
|
226
|
+
"duration_min": int((end - start).total_seconds() / 60),
|
|
227
|
+
"location": args.location,
|
|
228
|
+
"remind_min": args.remind,
|
|
229
|
+
"reminder_registered": reminder_status["registered"],
|
|
230
|
+
"reminder_existed": reminder_status["existed"],
|
|
231
|
+
"reminder_error": reminder_status["error"],
|
|
232
|
+
}
|
|
233
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
即时提醒检查器。
|
|
4
|
+
|
|
5
|
+
执行时机:由 OpenClaw Cron 高频触发(如每 5 分钟),或手动执行。
|
|
6
|
+
|
|
7
|
+
流程:
|
|
8
|
+
1. 拉取今天剩余的日程
|
|
9
|
+
2. 计算每个事件的提醒时间
|
|
10
|
+
3. 筛选出提醒时间在未来 N 分钟内的事件
|
|
11
|
+
4. 有提醒时输出提醒内容(供 OpenClaw 投递)
|
|
12
|
+
5. 无提醒时只输出 NO_REPLY
|
|
13
|
+
|
|
14
|
+
即时提醒检查器,此处补齐实现。
|
|
15
|
+
|
|
16
|
+
用法:
|
|
17
|
+
python3 apps/check_reminders.py # 默认检查窗口 30 分钟
|
|
18
|
+
python3 apps/check_reminders.py --window 15 # 只检查 15 分钟内
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
from datetime import timedelta
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# ── sys.path ─────────────────────────────────────────────────────
|
|
31
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
32
|
+
|
|
33
|
+
from config.settings import TIMEZONE
|
|
34
|
+
from services.calendar_backend import active_provider, list_events
|
|
35
|
+
from services.time_window import get_remaining_today_window, now_tz
|
|
36
|
+
from services.reminder_planner import plan_reminders
|
|
37
|
+
|
|
38
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(message)s")
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
NO_REPLY = "NO_REPLY"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
parser = argparse.ArgumentParser(description="即时提醒检查")
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--window", type=int, default=30,
|
|
48
|
+
help="检查提醒时间在未来多少分钟内的事件(默认 30)",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--json", action="store_true",
|
|
52
|
+
help="输出结构化 JSON,供 HTTP API 使用",
|
|
53
|
+
)
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
|
|
56
|
+
now = now_tz(TIMEZONE)
|
|
57
|
+
check_until = now + timedelta(minutes=args.window)
|
|
58
|
+
|
|
59
|
+
# ── 1. 拉取今天剩余日程 ───────────────────────────────────────
|
|
60
|
+
t_min, t_max = get_remaining_today_window(TIMEZONE)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
events = list_events(t_min, t_max)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"拉取日程失败: {e}")
|
|
66
|
+
print(str(e), file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
# ── 2. 计算提醒计划 ───────────────────────────────────────────
|
|
70
|
+
plans = plan_reminders(events, now=now, tz=TIMEZONE)
|
|
71
|
+
|
|
72
|
+
# ── 3. 筛选窗口内需要提醒的事件 ─────────────────────────────
|
|
73
|
+
due = [
|
|
74
|
+
p for p in plans
|
|
75
|
+
if p.should_schedule and now <= p.remind_at <= check_until
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if args.json:
|
|
79
|
+
items = [
|
|
80
|
+
{
|
|
81
|
+
"summary": p.event.summary,
|
|
82
|
+
"start": p.event.start.isoformat(),
|
|
83
|
+
"end": p.event.end.isoformat(),
|
|
84
|
+
"lead_minutes": p.lead_minutes,
|
|
85
|
+
"trigger_at": p.remind_at.isoformat(),
|
|
86
|
+
"message": p.reminder_message(),
|
|
87
|
+
}
|
|
88
|
+
for p in due
|
|
89
|
+
]
|
|
90
|
+
print(json.dumps(
|
|
91
|
+
{
|
|
92
|
+
"ok": True,
|
|
93
|
+
"window_minutes": args.window,
|
|
94
|
+
"provider": active_provider(),
|
|
95
|
+
"active_mode": active_provider(),
|
|
96
|
+
"reminders": items,
|
|
97
|
+
},
|
|
98
|
+
ensure_ascii=False,
|
|
99
|
+
))
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if not due:
|
|
103
|
+
print(NO_REPLY)
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
|
|
106
|
+
# ── 4. 输出提醒 ───────────────────────────────────────────────
|
|
107
|
+
for p in due:
|
|
108
|
+
print(p.reminder_message())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|