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,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
将 ROC 事件按年份批量同步到 Google Calendar。
|
|
4
|
+
|
|
5
|
+
设计原则:
|
|
6
|
+
- 每个年份独立管理,同一年只同步一次(除非 --force)。
|
|
7
|
+
- 同步记录保存在 data/roc_gcal_sync.json:
|
|
8
|
+
{ "2026": {"roc-001": "gcal_event_id_xxx", ...}, "2027": {...} }
|
|
9
|
+
- 事件以"全天事件"写入 Google Calendar,标题带 📅 前缀,备注含提醒日期。
|
|
10
|
+
- 跨年自动处理:每年 1 月 1 日触发当年同步;check_roc.py 首次运行也会触发。
|
|
11
|
+
|
|
12
|
+
用法:
|
|
13
|
+
python3 apps/sync_roc_to_gcal.py # 同步当年
|
|
14
|
+
python3 apps/sync_roc_to_gcal.py --year 2027 # 同步指定年份
|
|
15
|
+
python3 apps/sync_roc_to_gcal.py --dry-run # 只打印,不写入 GCal
|
|
16
|
+
python3 apps/sync_roc_to_gcal.py --force # 强制重新同步(已有记录也重建)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
_SKILL_BASE = Path(__file__).resolve().parent.parent
|
|
30
|
+
sys.path.insert(0, str(_SKILL_BASE))
|
|
31
|
+
|
|
32
|
+
from config.settings import (
|
|
33
|
+
GCAL_TOKEN_PATH, PROXIES, GCAL_TIMEOUT, GCAL_MAX_RETRIES, GCAL_CALENDAR_ID,
|
|
34
|
+
GCAL_SSL_VERIFY, TIMEZONE,
|
|
35
|
+
)
|
|
36
|
+
from gcal.client import CalendarClient, CalendarAuthError, CalendarAPIError
|
|
37
|
+
from services.datetime_utils import current_year
|
|
38
|
+
|
|
39
|
+
_SYNC_STATE_FILE = _SKILL_BASE / "data" / "roc_gcal_sync.json"
|
|
40
|
+
_CONFIG_FILE = _SKILL_BASE / "config" / "roc_events.yaml"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── 状态文件 ──────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def _load_sync_state() -> dict:
|
|
46
|
+
try:
|
|
47
|
+
return json.loads(_SYNC_STATE_FILE.read_text(encoding="utf-8"))
|
|
48
|
+
except Exception:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _save_sync_state(state: dict) -> None:
|
|
53
|
+
_SYNC_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
_SYNC_STATE_FILE.write_text(
|
|
55
|
+
json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── 核心同步逻辑 ─────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def sync_year(
|
|
62
|
+
year: int,
|
|
63
|
+
dry_run: bool = False,
|
|
64
|
+
force: bool = False,
|
|
65
|
+
verbose: bool = True,
|
|
66
|
+
) -> dict:
|
|
67
|
+
"""
|
|
68
|
+
同步指定年份的全部 ROC 事件到 Google Calendar。
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
{"added": [...], "skipped": [...], "errors": [...]}
|
|
72
|
+
"""
|
|
73
|
+
# 复用 check_roc 的日期解析逻辑,避免重复代码
|
|
74
|
+
from apps.check_roc import build_year_events
|
|
75
|
+
|
|
76
|
+
with open(_CONFIG_FILE, encoding="utf-8") as f:
|
|
77
|
+
cfg = yaml.safe_load(f)
|
|
78
|
+
|
|
79
|
+
year_events = build_year_events(year, cfg)
|
|
80
|
+
sync_state = _load_sync_state()
|
|
81
|
+
year_key = str(year)
|
|
82
|
+
year_state = dict(sync_state.get(year_key, {}))
|
|
83
|
+
|
|
84
|
+
client = None
|
|
85
|
+
if not dry_run:
|
|
86
|
+
client = CalendarClient(
|
|
87
|
+
GCAL_TOKEN_PATH, PROXIES, GCAL_CALENDAR_ID, GCAL_TIMEOUT, GCAL_MAX_RETRIES,
|
|
88
|
+
verify=GCAL_SSL_VERIFY,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
added, skipped, errors = [], [], []
|
|
92
|
+
|
|
93
|
+
for ev in year_events:
|
|
94
|
+
ev_id = ev["id"]
|
|
95
|
+
|
|
96
|
+
# 已同步且不强制重建 → 跳过
|
|
97
|
+
if ev_id in year_state and not force:
|
|
98
|
+
skipped.append(ev["name"])
|
|
99
|
+
if verbose:
|
|
100
|
+
print(f" ⏭ 跳过(已同步): {ev['name']} {ev['event_date']}")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
summary = f"📅 {ev['name']}"
|
|
104
|
+
date_str = ev["event_date"]
|
|
105
|
+
desc_lines = []
|
|
106
|
+
if ev.get("description"):
|
|
107
|
+
desc_lines.append(ev["description"])
|
|
108
|
+
desc_lines.append(f"ROC 周期事件 | 提前提醒日期: {ev['reminder_date']}")
|
|
109
|
+
description = "\n".join(desc_lines)
|
|
110
|
+
|
|
111
|
+
if dry_run:
|
|
112
|
+
print(f" [dry-run] 将创建: {summary} {date_str}")
|
|
113
|
+
added.append(ev["name"])
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
assert client is not None
|
|
118
|
+
gcal_id = client.create_allday_event(
|
|
119
|
+
summary, date_str, description=description
|
|
120
|
+
)
|
|
121
|
+
year_state[ev_id] = gcal_id
|
|
122
|
+
added.append(ev["name"])
|
|
123
|
+
if verbose:
|
|
124
|
+
print(f" ✅ 已创建: {summary} {date_str} (gcal_id={gcal_id})")
|
|
125
|
+
except (CalendarAuthError, CalendarAPIError, Exception) as e:
|
|
126
|
+
errors.append(f"{ev['name']}: {e}")
|
|
127
|
+
if verbose:
|
|
128
|
+
print(f" ❌ 失败: {ev['name']}: {e}")
|
|
129
|
+
|
|
130
|
+
# 写回状态(干跑时不写)
|
|
131
|
+
if not dry_run:
|
|
132
|
+
sync_state[year_key] = year_state
|
|
133
|
+
_save_sync_state(sync_state)
|
|
134
|
+
|
|
135
|
+
return {"added": added, "skipped": skipped, "errors": errors}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_year_synced(year: int) -> bool:
|
|
139
|
+
"""检查指定年份的 ROC 事件是否已同步(有任意记录即视为已同步)。"""
|
|
140
|
+
state = _load_sync_state()
|
|
141
|
+
return bool(state.get(str(year)))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── CLI ──────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def main() -> None:
|
|
147
|
+
parser = argparse.ArgumentParser(description="ROC 事件 → Google Calendar 按年同步")
|
|
148
|
+
parser.add_argument("--year", type=int, default=current_year(TIMEZONE),
|
|
149
|
+
help="目标年份(默认当年)")
|
|
150
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
151
|
+
help="只打印计划,不写入 GCal")
|
|
152
|
+
parser.add_argument("--force", action="store_true",
|
|
153
|
+
help="强制重新同步(已有记录也重建)")
|
|
154
|
+
args = parser.parse_args()
|
|
155
|
+
|
|
156
|
+
label = f"{args.year} 年 ROC 事件"
|
|
157
|
+
suffix = "(dry-run,不写入)" if args.dry_run else ""
|
|
158
|
+
print(f"📅 同步 {label} 到 Google Calendar{suffix} …")
|
|
159
|
+
|
|
160
|
+
result = sync_year(args.year, dry_run=args.dry_run, force=args.force)
|
|
161
|
+
|
|
162
|
+
print(
|
|
163
|
+
f"\n完成:新增 {len(result['added'])} 个,"
|
|
164
|
+
f"跳过 {len(result['skipped'])} 个,"
|
|
165
|
+
f"失败 {len(result['errors'])} 个"
|
|
166
|
+
)
|
|
167
|
+
if result["errors"]:
|
|
168
|
+
for err in result["errors"]:
|
|
169
|
+
print(f" ✗ {err}")
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python 3.8 / 3.9+ 时区兼容层。
|
|
3
|
+
|
|
4
|
+
Python 3.9+ 内置 zoneinfo;Python 3.8 回退到 pytz。
|
|
5
|
+
统一暴露 ZoneInfo 和 make_aware() 两个接口,
|
|
6
|
+
其余模块只从此处导入,不直接写 from zoneinfo import ZoneInfo。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from zoneinfo import ZoneInfo # Python 3.9+
|
|
15
|
+
|
|
16
|
+
def make_aware(dt: datetime, key: str) -> datetime:
|
|
17
|
+
"""将朴素 datetime 本地化为指定时区的 aware datetime。"""
|
|
18
|
+
if dt.tzinfo is not None:
|
|
19
|
+
return dt.astimezone(ZoneInfo(key))
|
|
20
|
+
return dt.replace(tzinfo=ZoneInfo(key))
|
|
21
|
+
|
|
22
|
+
except ImportError:
|
|
23
|
+
import pytz # Python 3.8 fallback
|
|
24
|
+
from datetime import tzinfo as _tzinfo
|
|
25
|
+
|
|
26
|
+
class ZoneInfo(_tzinfo): # type: ignore[no-redef]
|
|
27
|
+
"""pytz 驱动的 ZoneInfo 最小兼容实现,继承 tzinfo 供 datetime.now(tz) 使用。"""
|
|
28
|
+
|
|
29
|
+
_cache: dict = {}
|
|
30
|
+
|
|
31
|
+
def __new__(cls, key: str):
|
|
32
|
+
if key not in cls._cache:
|
|
33
|
+
obj = _tzinfo.__new__(cls)
|
|
34
|
+
obj._tz = pytz.timezone(key)
|
|
35
|
+
obj._key = key
|
|
36
|
+
cls._cache[key] = obj
|
|
37
|
+
return cls._cache[key]
|
|
38
|
+
|
|
39
|
+
def __init__(self, key: str) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def key(self) -> str:
|
|
44
|
+
return self._key
|
|
45
|
+
|
|
46
|
+
def utcoffset(self, dt):
|
|
47
|
+
naive = dt.replace(tzinfo=None) if dt is not None else datetime.utcnow()
|
|
48
|
+
return self._tz.utcoffset(naive)
|
|
49
|
+
|
|
50
|
+
def tzname(self, dt):
|
|
51
|
+
return self._tz.zone
|
|
52
|
+
|
|
53
|
+
def dst(self, dt):
|
|
54
|
+
naive = dt.replace(tzinfo=None) if dt is not None else datetime.utcnow()
|
|
55
|
+
return self._tz.dst(naive)
|
|
56
|
+
|
|
57
|
+
def fromutc(self, dt: datetime) -> datetime:
|
|
58
|
+
# pytz.fromutc 要求 dt.tzinfo 是其自身,先替换回 pytz tz
|
|
59
|
+
return self._tz.fromutc(dt.replace(tzinfo=self._tz))
|
|
60
|
+
|
|
61
|
+
def make_aware(dt: datetime, key: str) -> datetime: # type: ignore[misc]
|
|
62
|
+
"""使用 pytz.localize() 正确处理 DST,避免朴素 datetime 赋值偏移问题。"""
|
|
63
|
+
tz = pytz.timezone(key)
|
|
64
|
+
if dt.tzinfo is not None:
|
|
65
|
+
return dt.astimezone(tz)
|
|
66
|
+
return tz.localize(dt)
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 提醒规则配置
|
|
2
|
+
# 修改此文件即可调整所有提醒规则,无需改代码。
|
|
3
|
+
|
|
4
|
+
# 各类型提醒提前量(分钟)
|
|
5
|
+
lead_minutes:
|
|
6
|
+
sport: 15 # 运动/健身:需要换装热身
|
|
7
|
+
meal: 15 # 聚餐/饭局:需要出行时间
|
|
8
|
+
family: 15 # 家庭/亲子:需要准备、集合
|
|
9
|
+
medical: 30 # 就医/体检:需要带证件、提前到场
|
|
10
|
+
appointment: 10 # 通用预约/约定(默认)
|
|
11
|
+
|
|
12
|
+
# 无法匹配任何类型时的默认行为
|
|
13
|
+
default_type: appointment
|
|
14
|
+
default_lead_minutes: 10
|
|
15
|
+
|
|
16
|
+
# 事件分类关键词(summary 中包含以下词即归入对应类型)
|
|
17
|
+
keywords:
|
|
18
|
+
sport:
|
|
19
|
+
- 跑步
|
|
20
|
+
- 晨跑
|
|
21
|
+
- 夜跑
|
|
22
|
+
- 健身
|
|
23
|
+
- 游泳
|
|
24
|
+
- 打球
|
|
25
|
+
- 羽毛球
|
|
26
|
+
- 乒乓球
|
|
27
|
+
- 网球
|
|
28
|
+
- 篮球
|
|
29
|
+
- 足球
|
|
30
|
+
- 瑜伽
|
|
31
|
+
- 骑行
|
|
32
|
+
- 爬山
|
|
33
|
+
- 徒步
|
|
34
|
+
|
|
35
|
+
meal:
|
|
36
|
+
- 聚餐
|
|
37
|
+
- 家庭聚餐
|
|
38
|
+
- 生日聚餐
|
|
39
|
+
- 朋友聚餐
|
|
40
|
+
- 同学聚餐
|
|
41
|
+
- 团建
|
|
42
|
+
- 饭局
|
|
43
|
+
- 下午茶
|
|
44
|
+
- 火锅
|
|
45
|
+
- 烧烤
|
|
46
|
+
- 晚餐
|
|
47
|
+
- 午餐
|
|
48
|
+
- 早午餐
|
|
49
|
+
|
|
50
|
+
family:
|
|
51
|
+
- 亲子
|
|
52
|
+
- 接送
|
|
53
|
+
- 接孩子
|
|
54
|
+
- 送孩子
|
|
55
|
+
- 陪孩子
|
|
56
|
+
- 家长会
|
|
57
|
+
- 辅导
|
|
58
|
+
- 老人
|
|
59
|
+
- 探望
|
|
60
|
+
- 探亲
|
|
61
|
+
- 回家
|
|
62
|
+
|
|
63
|
+
medical:
|
|
64
|
+
- 体检
|
|
65
|
+
- 看病
|
|
66
|
+
- 就医
|
|
67
|
+
- 门诊
|
|
68
|
+
- 复诊
|
|
69
|
+
- 取药
|
|
70
|
+
- 打疫苗
|
|
71
|
+
- 牙医
|
|
72
|
+
- 口腔
|
|
73
|
+
- 眼科
|
|
74
|
+
- 体检
|
|
75
|
+
|
|
76
|
+
appointment:
|
|
77
|
+
- 预约
|
|
78
|
+
- 约定
|
|
79
|
+
- 美发
|
|
80
|
+
- 理发
|
|
81
|
+
- 美甲
|
|
82
|
+
- 保养
|
|
83
|
+
- 维修
|
|
84
|
+
- 快递
|
|
85
|
+
- 等待
|
|
86
|
+
- 面谈
|
|
87
|
+
- 会谈
|
|
88
|
+
- 活动
|
|
89
|
+
- 讲座
|
|
90
|
+
- 展览
|
|
91
|
+
- 观影
|
|
92
|
+
- 演出
|
|
93
|
+
- 旅行
|
|
94
|
+
- 出行
|
|
95
|
+
- 培训
|
|
96
|
+
- 课程
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
spring_festival:
|
|
2
|
+
2026:
|
|
3
|
+
last_workday: '2026-02-13'
|
|
4
|
+
first_workday: '2026-02-23'
|
|
5
|
+
2027:
|
|
6
|
+
last_workday: '2027-02-09'
|
|
7
|
+
first_workday: '2027-02-19'
|
|
8
|
+
events:
|
|
9
|
+
- id: spring_family_reunion
|
|
10
|
+
name: 春节家庭聚会准备
|
|
11
|
+
rule: 春节前最后工作日
|
|
12
|
+
reminder_time: 09:00
|
|
13
|
+
lead_days: 5
|
|
14
|
+
description: 提前安排聚餐地点和礼品
|
|
15
|
+
- id: mom_birthday
|
|
16
|
+
name: 妈妈生日
|
|
17
|
+
rule: 每年9月8日
|
|
18
|
+
reminder_time: 09:00
|
|
19
|
+
lead_days: 14
|
|
20
|
+
description: 提前订蛋糕和安排家庭聚餐
|
|
21
|
+
- id: after_cny_gathering
|
|
22
|
+
name: 春节后家庭聚餐
|
|
23
|
+
rule: 春节后第一个工作日
|
|
24
|
+
reminder_time: 09:00
|
|
25
|
+
lead_days: 7
|
|
26
|
+
description: ''
|
|
27
|
+
- id: summer_family_trip
|
|
28
|
+
name: 暑期家庭出行计划
|
|
29
|
+
rule: 每年7月1日
|
|
30
|
+
reminder_time: 09:00
|
|
31
|
+
lead_days: 30
|
|
32
|
+
description: 提前预订酒店和机票
|
|
33
|
+
- id: car_annual_check
|
|
34
|
+
name: 汽车年检
|
|
35
|
+
rule: 每年11月第1个周三
|
|
36
|
+
reminder_time: 09:00
|
|
37
|
+
lead_days: 21
|
|
38
|
+
description: 提前准备行驶证和保险凭证
|
|
39
|
+
- id: health_checkup
|
|
40
|
+
name: 年度家庭体检
|
|
41
|
+
rule: 每年4月第2个周五
|
|
42
|
+
reminder_time: 09:00
|
|
43
|
+
lead_days: 14
|
|
44
|
+
description: 提前预约体检套餐,注意检查前一天饮食
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
全局配置入口。
|
|
3
|
+
|
|
4
|
+
所有第三方服务连接信息、路径、时区统一在此管理。
|
|
5
|
+
环境变量优先级高于默认值,方便不同环境(本地开发 / 测试 / 生产)切换。
|
|
6
|
+
|
|
7
|
+
第三方集成一览:
|
|
8
|
+
- Google Calendar API v3 (token 文件:gcalcli 兼容格式)
|
|
9
|
+
- HTTP 代理 (内网代理,访问 Google API 用)
|
|
10
|
+
- Telegram (OpenClaw 消息投递通道)
|
|
11
|
+
- OpenClaw Cron (任务调度运行时)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_env(env_file: Path) -> None:
|
|
20
|
+
"""加载 .env 文件,已有的环境变量不覆盖(系统/进程注入的优先级更高)。"""
|
|
21
|
+
if not env_file.is_file():
|
|
22
|
+
return
|
|
23
|
+
with open(env_file, encoding="utf-8") as f:
|
|
24
|
+
for line in f:
|
|
25
|
+
line = line.strip()
|
|
26
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
27
|
+
continue
|
|
28
|
+
key, _, val = line.partition("=")
|
|
29
|
+
key = key.strip()
|
|
30
|
+
val = val.strip()
|
|
31
|
+
if key and key not in os.environ:
|
|
32
|
+
os.environ[key] = val
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# 自动加载 sophnet-schedule(本 skill)根目录下的 .env(config/ 的上级目录)
|
|
36
|
+
_load_env(Path(__file__).resolve().parent.parent / ".env")
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────
|
|
39
|
+
# 基础路径
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
# 本 skill 根目录(从环境变量覆盖,或从本文件推导)
|
|
43
|
+
_default_skill_base = Path(__file__).resolve().parent.parent # config/ → skill 根(src/)
|
|
44
|
+
SKILL_BASE: Path = Path(
|
|
45
|
+
os.environ.get("SOPHNET_SCHEDULE_BASE")
|
|
46
|
+
or os.environ.get("CRON_SKILL_BASE")
|
|
47
|
+
or _default_skill_base
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# OpenClaw 工作空间根(部署时通常是 /home/node/.openclaw)
|
|
51
|
+
OPENCLAW_BASE: Path = Path(
|
|
52
|
+
os.environ.get("OPENCLAW_BASE", str(Path.home() / ".openclaw"))
|
|
53
|
+
)
|
|
54
|
+
WORKSPACE_BASE: Path = OPENCLAW_BASE / "workspace"
|
|
55
|
+
SKILLS_CRON_BASE: Path = WORKSPACE_BASE / "skills" / "cron"
|
|
56
|
+
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────
|
|
58
|
+
# 数据 / 输出目录
|
|
59
|
+
# ─────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
DAILY_DIR: Path = SKILL_BASE / "daily"
|
|
62
|
+
DATA_DIR: Path = SKILL_BASE / "data"
|
|
63
|
+
LAST_EVENTS_FILE: Path = DATA_DIR / "last_events.json"
|
|
64
|
+
LOCAL_EVENTS_FILE: Path = DATA_DIR / "local_events.json"
|
|
65
|
+
GOOGLE_STATE_FILE: Path = DATA_DIR / "google_state.json"
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────────────────────────────
|
|
68
|
+
# 第三方:Google Calendar API
|
|
69
|
+
# ─────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
# gcalcli 兼容 token 文件(含 client_id / client_secret / refresh_token)
|
|
72
|
+
GCAL_TOKEN_PATH: Path = Path(
|
|
73
|
+
os.environ.get(
|
|
74
|
+
"GCAL_TOKEN_PATH",
|
|
75
|
+
Path.home() / ".config" / "gcalcli" / "token.json",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# HTTP 代理(内网访问 Google API 需要)
|
|
80
|
+
_PROXY = os.environ.get("HTTP_PROXY", "")
|
|
81
|
+
PROXIES: "dict[str, str]" = {"http": _PROXY, "https": _PROXY} if _PROXY else {}
|
|
82
|
+
|
|
83
|
+
GCAL_TIMEOUT: int = int(os.environ.get("GCAL_TIMEOUT", "10"))
|
|
84
|
+
GCAL_MAX_RETRIES: int = int(os.environ.get("GCAL_MAX_RETRIES", "3"))
|
|
85
|
+
GCAL_CALENDAR_ID: str = os.environ.get("GCAL_CALENDAR_ID", "primary")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _parse_ssl_verify(value: str) -> bool | str:
|
|
89
|
+
raw = value.strip()
|
|
90
|
+
if not raw:
|
|
91
|
+
return True
|
|
92
|
+
lowered = raw.lower()
|
|
93
|
+
if lowered in {"0", "false", "no", "off"}:
|
|
94
|
+
return False
|
|
95
|
+
if lowered in {"1", "true", "yes", "on"}:
|
|
96
|
+
return True
|
|
97
|
+
return raw
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
GCAL_SSL_VERIFY: bool | str = _parse_ssl_verify(
|
|
101
|
+
os.environ.get("GCAL_SSL_VERIFY", "true")
|
|
102
|
+
)
|
|
103
|
+
GOOGLE_OAUTH_SESSION_TTL_MINUTES: int = int(
|
|
104
|
+
os.environ.get("GOOGLE_OAUTH_SESSION_TTL_MINUTES", "15")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ─────────────────────────────────────────────────────────────────
|
|
108
|
+
# 时区
|
|
109
|
+
# ─────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
TIMEZONE: str = os.environ.get("TIMEZONE", "Asia/Shanghai")
|
|
112
|
+
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────
|
|
114
|
+
# 第三方:Telegram(通过 OpenClaw 消息通道投递)
|
|
115
|
+
# ─────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
TELEGRAM_CHANNEL: str = os.environ.get("TELEGRAM_CHANNEL", "telegram")
|
|
118
|
+
TELEGRAM_TO: str = os.environ.get("TELEGRAM_TO", "")
|
|
119
|
+
|
|
120
|
+
# ─────────────────────────────────────────────────────────────────
|
|
121
|
+
# OpenClaw CLI
|
|
122
|
+
# ─────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
OPENCLAW_BIN: str = os.environ.get("OPENCLAW_BIN", "openclaw")
|
|
125
|
+
PYTHON_BIN: str = os.environ.get("PYTHON_BIN", sys.executable)
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────────────────────────────
|
|
128
|
+
# 配置文件路径
|
|
129
|
+
# ─────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
CONFIG_DIR: Path = SKILL_BASE / "config"
|
|
132
|
+
REMINDER_RULES_FILE: Path = CONFIG_DIR / "reminder_rules.yaml"
|
|
133
|
+
TASK_REGISTRY_FILE: Path = CONFIG_DIR / "task_registry.yaml"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# 固定任务注册表
|
|
2
|
+
#
|
|
3
|
+
# 此文件是所有重复性任务的唯一注册入口。
|
|
4
|
+
# 修改此文件即可增减任务,无需改代码。
|
|
5
|
+
# 按需增删任务条目
|
|
6
|
+
#
|
|
7
|
+
# 字段说明:
|
|
8
|
+
# name 任务唯一名称
|
|
9
|
+
# schedule 调度时间
|
|
10
|
+
# kind: at 一次性(当天执行),time 格式 HH:MM(北京时间)
|
|
11
|
+
# kind: cron 周期性,expr 为 cron 表达式,tz 为时区
|
|
12
|
+
# type 任务投递类型
|
|
13
|
+
# silent 静默监控:不发送通知(delivery: none)
|
|
14
|
+
# report 报告推送:执行后 announce 到 Telegram
|
|
15
|
+
# reminder 提醒注入:由 apps/generate_daily_plan.py 动态生成,不在此注册
|
|
16
|
+
# message 发给 agent 的指令或脚本命令
|
|
17
|
+
# description 任务说明(仅供文档展示)
|
|
18
|
+
# delete_after_run 一次性任务执行后自动删除(默认 true)
|
|
19
|
+
# requires_provider 需要的日历后端:any | google(默认 any)
|
|
20
|
+
#
|
|
21
|
+
# 占位变量(运行时由 settings.py 替换):
|
|
22
|
+
# {OPENCLAW_BASE} OpenClaw 工作空间根路径
|
|
23
|
+
# {SKILL_BASE} 本 skill 根路径
|
|
24
|
+
|
|
25
|
+
tasks:
|
|
26
|
+
|
|
27
|
+
# ── 早晨 ──────────────────────────────────────────────────────
|
|
28
|
+
- name: 每日计划生成
|
|
29
|
+
description: "生成今日 Cron 任务文件,注册日程提醒"
|
|
30
|
+
schedule:
|
|
31
|
+
kind: at
|
|
32
|
+
time: "07:50"
|
|
33
|
+
type: report
|
|
34
|
+
message: "{PYTHON_BIN} {SKILL_BASE}/apps/generate_daily_plan.py"
|
|
35
|
+
delete_after_run: true
|
|
36
|
+
|
|
37
|
+
- name: 每日日程推送
|
|
38
|
+
description: "查询今日日程,推送早报"
|
|
39
|
+
schedule:
|
|
40
|
+
kind: at
|
|
41
|
+
time: "08:00"
|
|
42
|
+
type: report
|
|
43
|
+
message: "查询今天日程,格式:📅 今日日程\n• 时间 事项"
|
|
44
|
+
delete_after_run: true
|
|
45
|
+
|
|
46
|
+
- name: 喝水提醒
|
|
47
|
+
description: "提醒补水,保持健康"
|
|
48
|
+
schedule:
|
|
49
|
+
kind: at
|
|
50
|
+
time: "10:00"
|
|
51
|
+
type: report
|
|
52
|
+
message: "该喝水了 💧"
|
|
53
|
+
delete_after_run: true
|
|
54
|
+
|
|
55
|
+
# ── 白天(周期) ───────────────────────────────────────────────
|
|
56
|
+
- name: 日程变化监控
|
|
57
|
+
description: "对比今天+明天日程快照,有变化时通知,无变化不打扰"
|
|
58
|
+
schedule:
|
|
59
|
+
kind: cron
|
|
60
|
+
expr: "0 8-20 * * *" # 每小时整点(北京时间 8:00–20:00)
|
|
61
|
+
tz: "Asia/Shanghai"
|
|
62
|
+
type: silent
|
|
63
|
+
message: "{PYTHON_BIN} {SKILL_BASE}/apps/monitor_calendar_changes.py"
|
|
64
|
+
|
|
65
|
+
# ── 晚间 ──────────────────────────────────────────────────────
|
|
66
|
+
- name: ROC年度GCal同步
|
|
67
|
+
description: "每年1月1日将本年度全部ROC事件批量写入Google Calendar全天事件"
|
|
68
|
+
requires_provider: google
|
|
69
|
+
schedule:
|
|
70
|
+
kind: cron
|
|
71
|
+
expr: "0 8 1 1 *" # 每年 1 月 1 日 08:00(北京时间)
|
|
72
|
+
tz: "Asia/Shanghai"
|
|
73
|
+
type: report
|
|
74
|
+
message: "{PYTHON_BIN} {SKILL_BASE}/apps/sync_roc_to_gcal.py"
|
|
75
|
+
|
|
76
|
+
- name: ROC事件检查
|
|
77
|
+
description: "检查今天是否有 ROC 周期性事件需要提醒(生日、体检、家庭聚会等)"
|
|
78
|
+
schedule:
|
|
79
|
+
kind: at
|
|
80
|
+
time: "08:05"
|
|
81
|
+
type: report
|
|
82
|
+
message: "{PYTHON_BIN} {SKILL_BASE}/apps/check_roc.py"
|
|
83
|
+
delete_after_run: true
|
|
84
|
+
|
|
85
|
+
- name: 每日总结
|
|
86
|
+
description: "生成今日工作总结,列出完成事项"
|
|
87
|
+
schedule:
|
|
88
|
+
kind: at
|
|
89
|
+
time: "21:00"
|
|
90
|
+
type: report
|
|
91
|
+
message: "整理今天的工作记录,生成简短日总结"
|
|
92
|
+
delete_after_run: true
|