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,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