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.
Files changed (107) 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/google-maps/skill.json +20 -0
  24. package/skills/google-maps/src/SKILL.md +237 -0
  25. package/skills/google-maps/src/_meta.json +6 -0
  26. package/skills/google-maps/src/lib/map_helper.py +912 -0
  27. package/skills/large-task-router/skill.json +20 -0
  28. package/skills/large-task-router/src/SKILL.md +79 -0
  29. package/skills/large-task-router/src/templates/plan.md +74 -0
  30. package/skills/skillhub/skill.json +11 -4
  31. package/skills/skillhub/src/SKILL.md +11 -1
  32. package/skills/sophnet-dailynews/skill.json +20 -0
  33. package/skills/sophnet-dailynews/src/SKILL.md +179 -0
  34. package/skills/sophnet-dailynews/src/cache.json +151 -0
  35. package/skills/sophnet-dailynews/src/sources.json +230 -0
  36. package/skills/sophnet-schedule/skill.json +20 -0
  37. package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
  38. package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
  39. package/skills/sophnet-schedule/src/SKILL.md +1050 -0
  40. package/skills/sophnet-schedule/src/_meta.json +6 -0
  41. package/skills/sophnet-schedule/src/api/__init__.py +0 -0
  42. package/skills/sophnet-schedule/src/api/models.py +245 -0
  43. package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
  44. package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
  45. package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
  46. package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
  47. package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
  48. package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
  49. package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
  50. package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
  51. package/skills/sophnet-schedule/src/compat.py +66 -0
  52. package/skills/sophnet-schedule/src/config/__init__.py +0 -0
  53. package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
  54. package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
  55. package/skills/sophnet-schedule/src/config/settings.py +133 -0
  56. package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
  57. package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
  58. package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
  59. package/skills/sophnet-schedule/src/gcal/client.py +374 -0
  60. package/skills/sophnet-schedule/src/gcal/models.py +91 -0
  61. package/skills/sophnet-schedule/src/requirements.txt +6 -0
  62. package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
  63. package/skills/sophnet-schedule/src/server.py +669 -0
  64. package/skills/sophnet-schedule/src/services/__init__.py +0 -0
  65. package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
  66. package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
  67. package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
  68. package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
  69. package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
  70. package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
  71. package/skills/sophnet-schedule/src/services/job_store.py +100 -0
  72. package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
  73. package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
  74. package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
  75. package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
  76. package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
  77. package/skills/sophnet-schedule/src/services/time_window.py +72 -0
  78. package/skills/sophnet-stock/skill.json +20 -0
  79. package/skills/sophnet-stock/src/App-Plan.md +442 -0
  80. package/skills/sophnet-stock/src/README.md +214 -0
  81. package/skills/sophnet-stock/src/SKILL.md +236 -0
  82. package/skills/sophnet-stock/src/TODO.md +394 -0
  83. package/skills/sophnet-stock/src/_meta.json +6 -0
  84. package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
  85. package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
  86. package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
  87. package/skills/sophnet-stock/src/docs/README.md +95 -0
  88. package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
  89. package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
  90. package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
  91. package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
  92. package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
  93. package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
  94. package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
  95. package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
  96. package/skills/xiaohongshu/skill.json +20 -0
  97. package/skills/xiaohongshu/src/SKILL.md +91 -0
  98. package/skills/xiaohongshu/src/_meta.json +6 -0
  99. package/skills/xiaohongshu/src/assets/card.html +216 -0
  100. package/skills/xiaohongshu/src/assets/cover.html +82 -0
  101. package/skills/xiaohongshu/src/assets/example.md +84 -0
  102. package/skills/xiaohongshu/src/assets/styles.css +318 -0
  103. package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
  104. package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
  105. package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
  106. package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
  107. package/skills/xiaohongshu/src/workflow.py +185 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "slug": "sophnet-schedule",
3
+ "version": "1.0.0",
4
+ "description": "Sophnet 日程提醒:Google Calendar 自动同步、定时提醒、日程变化监控、ROC 周期事件管理",
5
+ "publishedAt": 1743292800000
6
+ }
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()