sophhub 0.1.0

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 (125) hide show
  1. package/bin/sophhub.js +21 -0
  2. package/package.json +32 -0
  3. package/skills/VERSIONS.md +27 -0
  4. package/skills/builtin/clawhub/SKILL.md +77 -0
  5. package/skills/builtin/flight-booking/SKILL.md +288 -0
  6. package/skills/builtin/flight-booking/scripts/flight_booking.py +1232 -0
  7. package/skills/builtin/inventory-management/SKILL.md +241 -0
  8. package/skills/builtin/inventory-management/scripts/inventory.py +1844 -0
  9. package/skills/builtin/schedule-reminder/SKILL.md +619 -0
  10. package/skills/builtin/schedule-reminder/schedule_template.md +68 -0
  11. package/skills/builtin/schedule-reminder/scripts/append_event.py +204 -0
  12. package/skills/builtin/schedule-reminder/scripts/create_reminders.sh +163 -0
  13. package/skills/builtin/schedule-reminder/scripts/daily_activate.sh +175 -0
  14. package/skills/builtin/schedule-reminder/scripts/parse_schedule.py +704 -0
  15. package/skills/builtin/schedule-reminder/scripts/setup.sh +242 -0
  16. package/skills/builtin/schedule-reminder//347/224/250/346/210/267/346/214/207/345/215/227.md +311 -0
  17. package/skills/builtin/skill-creator/SKILL.md +370 -0
  18. package/skills/builtin/skill-creator/license.txt +202 -0
  19. package/skills/builtin/skill-creator/scripts/init_skill.py +378 -0
  20. package/skills/builtin/skill-creator/scripts/package_skill.py +111 -0
  21. package/skills/builtin/skill-creator/scripts/quick_validate.py +101 -0
  22. package/skills/builtin/sophnet-customer-management/SKILL.md +271 -0
  23. package/skills/builtin/sophnet-customer-management/pyproject.toml +15 -0
  24. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__init__.py +2 -0
  25. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__main__.py +5 -0
  26. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/cli.py +67 -0
  27. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/__init__.py +2 -0
  28. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/customer.py +60 -0
  29. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/export_file.py +18 -0
  30. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/import_file.py +15 -0
  31. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/reminder.py +26 -0
  32. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/schema.py +28 -0
  33. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/config.py +54 -0
  34. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/__init__.py +2 -0
  35. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/exporter.py +85 -0
  36. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/models.py +84 -0
  37. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/normalizer.py +144 -0
  38. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/parser.py +241 -0
  39. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/query.py +109 -0
  40. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/reminder.py +121 -0
  41. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/repository.py +397 -0
  42. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/schema.py +106 -0
  43. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/service.py +565 -0
  44. package/skills/builtin/sophnet-customer-management/uv.lock +48 -0
  45. package/skills/builtin/sophnet-customized-marketing/SKILL.md +144 -0
  46. package/skills/builtin/sophnet-customized-marketing/playbooks/campaign-planning.md +187 -0
  47. package/skills/builtin/sophnet-customized-marketing/playbooks/content-generation.md +124 -0
  48. package/skills/builtin/sophnet-customized-marketing/playbooks/marketing-calendar.md +59 -0
  49. package/skills/builtin/sophnet-customized-marketing/playbooks/multi-channel-bundle.md +94 -0
  50. package/skills/builtin/sophnet-customized-marketing/playbooks/poster-generation.md +182 -0
  51. package/skills/builtin/sophnet-customized-marketing/playbooks/style-profile-workflow.md +103 -0
  52. package/skills/builtin/sophnet-customized-marketing/pyproject.toml +9 -0
  53. package/skills/builtin/sophnet-customized-marketing/references/campaign-mechanics.md +168 -0
  54. package/skills/builtin/sophnet-customized-marketing/references/content-safety.md +26 -0
  55. package/skills/builtin/sophnet-customized-marketing/references/marketing-date-checklist.md +99 -0
  56. package/skills/builtin/sophnet-customized-marketing/references/platform-writing-guidelines.md +88 -0
  57. package/skills/builtin/sophnet-customized-marketing/references/quality-checklist.md +44 -0
  58. package/skills/builtin/sophnet-customized-marketing/scripts/generate_poster.py +585 -0
  59. package/skills/builtin/sophnet-customized-marketing/scripts/style_profile.py +215 -0
  60. package/skills/builtin/sophnet-face-search/SKILL.md +115 -0
  61. package/skills/builtin/sophnet-face-search/pyproject.toml +11 -0
  62. package/skills/builtin/sophnet-face-search/scripts/face_search.py +336 -0
  63. package/skills/builtin/sophnet-face-search/uv.lock +508 -0
  64. package/skills/builtin/sophnet-image-edit/SKILL.md +140 -0
  65. package/skills/builtin/sophnet-image-edit/pyproject.toml +9 -0
  66. package/skills/builtin/sophnet-image-edit/scripts/edit_and_preview.sh +68 -0
  67. package/skills/builtin/sophnet-image-edit/scripts/edit_image.py +279 -0
  68. package/skills/builtin/sophnet-image-edit/uv.lock +234 -0
  69. package/skills/builtin/sophnet-image-generate/SKILL.md +62 -0
  70. package/skills/builtin/sophnet-image-generate/pyproject.toml +9 -0
  71. package/skills/builtin/sophnet-image-generate/scripts/generate_image.py +156 -0
  72. package/skills/builtin/sophnet-image-generate/uv.lock +234 -0
  73. package/skills/builtin/sophnet-image-ocr/SKILL.md +167 -0
  74. package/skills/builtin/sophnet-image-ocr/pyproject.toml +13 -0
  75. package/skills/builtin/sophnet-image-ocr/scripts/ocr.py +226 -0
  76. package/skills/builtin/sophnet-image-ocr/uv.lock +234 -0
  77. package/skills/builtin/sophnet-infinite-talk/SKILL.md +140 -0
  78. package/skills/builtin/sophnet-infinite-talk/pyproject.toml +9 -0
  79. package/skills/builtin/sophnet-infinite-talk/scripts/gen.py +172 -0
  80. package/skills/builtin/sophnet-oss/SKILL.md +109 -0
  81. package/skills/builtin/sophnet-oss/pyproject.toml +8 -0
  82. package/skills/builtin/sophnet-oss/scripts/upload_file.py +43 -0
  83. package/skills/builtin/sophnet-qa-install/SKILL.md +210 -0
  84. package/skills/builtin/sophnet-qa-install/pyproject.toml +6 -0
  85. package/skills/builtin/sophnet-qa-install/scripts/backup_md.py +35 -0
  86. package/skills/builtin/sophnet-qa-install/scripts/check_installed.py +143 -0
  87. package/skills/builtin/sophnet-qa-install/scripts/update_config.py +142 -0
  88. package/skills/builtin/sophnet-qa-install/scripts/update_md.py +73 -0
  89. package/skills/builtin/sophnet-training-install/SKILL.md +211 -0
  90. package/skills/builtin/sophnet-training-install/pyproject.toml +6 -0
  91. package/skills/builtin/sophnet-training-install/scripts/backup_md.py +35 -0
  92. package/skills/builtin/sophnet-training-install/scripts/check_installed.py +144 -0
  93. package/skills/builtin/sophnet-training-install/scripts/update_config.py +142 -0
  94. package/skills/builtin/sophnet-training-install/scripts/update_md.py +73 -0
  95. package/skills/builtin/sophnet-tts/SKILL.md +79 -0
  96. package/skills/builtin/sophnet-tts/pyproject.toml +9 -0
  97. package/skills/builtin/sophnet-tts/scripts/gen_tts.py +130 -0
  98. package/skills/builtin/sophnet-video-generate/SKILL.md +116 -0
  99. package/skills/builtin/sophnet-video-generate/scripts/gen_video.py +304 -0
  100. package/skills/builtin/video-understand/SKILL.md +79 -0
  101. package/skills/builtin/video-understand/scripts/video_understand.py +204 -0
  102. package/skills/builtin/weather/SKILL.md +112 -0
  103. package/skills/builtin/web-scraper/SKILL.md +101 -0
  104. package/skills/builtin/web-scraper/scripts/scrape.py +270 -0
  105. package/skills/builtin/website-builder/SKILL.md +266 -0
  106. package/skills/builtin/website-builder/scripts/deploy_site.sh +46 -0
  107. package/skills/store/didi-ride/SKILL.md +309 -0
  108. package/skills/store/didi-ride/_meta.json +6 -0
  109. package/skills/store/didi-ride/assets/PREFERENCE.md +58 -0
  110. package/skills/store/didi-ride/package.json +15 -0
  111. package/skills/store/didi-ride/references/api_references.md +171 -0
  112. package/skills/store/didi-ride/references/error_handling.md +68 -0
  113. package/skills/store/didi-ride/references/setup.md +73 -0
  114. package/skills/store/didi-ride/references/workflow.md +150 -0
  115. package/skills/store/flyai/SKILL.md +119 -0
  116. package/skills/store/flyai/references/fliggy-fast-search.md +53 -0
  117. package/skills/store/flyai/references/search-flight.md +89 -0
  118. package/skills/store/flyai/references/search-hotels.md +57 -0
  119. package/skills/store/flyai/references/search-poi.md +49 -0
  120. package/src/commands/download.js +103 -0
  121. package/src/commands/list.js +67 -0
  122. package/src/utils/config.js +24 -0
  123. package/src/utils/gitlab.js +67 -0
  124. package/src/utils/paths.js +19 -0
  125. package/src/utils/versions.js +38 -0
@@ -0,0 +1,704 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ parse_schedule.py - 解析 Markdown 日程表,输出今日/近期事项、冲突检测结果
4
+
5
+ 用法:
6
+ python3 parse_schedule.py --file /home/node/.openclaw/workspace/日程.md [--mode today|tomorrow|week|all]
7
+ python3 parse_schedule.py --stdin [--mode today] # 从 stdin 读取 Markdown 内容
8
+
9
+ 输出:JSON,包含 events / upcoming / conflicts / recurring / tasks
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ import re
15
+ import json
16
+ import argparse
17
+ from datetime import datetime, date, timedelta
18
+ from typing import Optional, List, Dict
19
+
20
+
21
+ PRIORITY_ORDER = {"高": 0, "中": 1, "低": 2, "": 1}
22
+
23
+ TASK_KEYWORDS = {"提交", "截止", "deadline", "交付", "汇报", "发布", "上线"}
24
+
25
+
26
+ def parse_args():
27
+ p = argparse.ArgumentParser(description="解析 Markdown 日程表")
28
+ p.add_argument("--file", help="Markdown 日程文件路径")
29
+ p.add_argument("--stdin", action="store_true", help="从 stdin 读取内容")
30
+ p.add_argument(
31
+ "--mode",
32
+ choices=["today", "tomorrow", "week", "all"],
33
+ default="today",
34
+ help="筛选范围(默认 today)",
35
+ )
36
+ p.add_argument("--now", help="覆盖当前时间(格式 YYYY-MM-DD HH:MM,测试用)")
37
+ p.add_argument(
38
+ "--column-map",
39
+ help=(
40
+ "自定义列名映射,格式:'自定义列名=字段名,...'\n"
41
+ "字段名可选:date/start/end/title/location/participants/priority/advance/note\n"
42
+ "示例:--column-map '会期=date,主旨=title,时间段开始=start'"
43
+ ),
44
+ )
45
+ return p.parse_args()
46
+
47
+
48
+ def read_content(args) -> str:
49
+ if args.stdin:
50
+ return sys.stdin.read()
51
+ if args.file:
52
+ path = args.file.replace("~", __import__("os").path.expanduser("~"))
53
+ with open(path, encoding="utf-8") as f:
54
+ return f.read()
55
+ print("错误:请指定 --file 或 --stdin", file=sys.stderr)
56
+ sys.exit(1)
57
+
58
+
59
+ def parse_global_config(content: str) -> dict:
60
+ """从文件头部注释和引用块提取全局配置"""
61
+ cfg = {
62
+ "channel": "dingtalk",
63
+ "default_advance_minutes": 15,
64
+ "timezone": "Asia/Shanghai",
65
+ }
66
+ for line in content.splitlines():
67
+ line = line.strip()
68
+ m = re.match(r">\s*\*?\*?提醒渠道\*?\*?[::]\s*(\S+)", line)
69
+ if m:
70
+ cfg["channel"] = m.group(1).strip()
71
+ m = re.match(r">\s*\*?\*?默认提前提醒\*?\*?[::]\s*(\d+)", line)
72
+ if m:
73
+ cfg["default_advance_minutes"] = int(m.group(1))
74
+ m = re.match(r">\s*\*?\*?时区\*?\*?[::]\s*(\S+)", line)
75
+ if m:
76
+ cfg["timezone"] = m.group(1)
77
+ return cfg
78
+
79
+
80
+ def normalize_date(raw: str) -> Optional[date]:
81
+ raw = raw.strip()
82
+ for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%m-%d", "%m/%d"):
83
+ try:
84
+ d = datetime.strptime(raw, fmt)
85
+ if d.year == 1900:
86
+ d = d.replace(year=date.today().year)
87
+ return d.date()
88
+ except ValueError:
89
+ pass
90
+ return None
91
+
92
+
93
+ def normalize_time(raw: str) -> Optional[int]:
94
+ """返回分钟数(从当天0点起)"""
95
+ raw = raw.strip().replace(":", ":")
96
+ m = re.match(r"(\d{1,2}):(\d{2})", raw)
97
+ if m:
98
+ return int(m.group(1)) * 60 + int(m.group(2))
99
+ return None
100
+
101
+
102
+ # ─────────────────────────────────────────────────────────
103
+ # 列名自动映射(宽松模式,支持不标准的 Excel/CSV 转换结果)
104
+ # ─────────────────────────────────────────────────────────
105
+
106
+ # 各字段的候选列名(支持中英文、简写、常见变体)
107
+ COLUMN_ALIASES = {
108
+ "date": {"日期", "date", "Date", "DATE", "时间", "日", "活动日期", "会议日期", "事件日期",
109
+ "日程日期", "计划日期"},
110
+ "start": {"开始时间", "开始", "start", "Start", "START", "开始时刻", "起始时间", "from", "From",
111
+ "开始时间段", "时间开始", "起始"},
112
+ "end": {"结束时间", "结束", "end", "End", "END", "结束时刻", "to", "To",
113
+ "时间结束", "截止", "终止"},
114
+ "title": {"事项", "标题", "title", "Title", "TITLE", "内容", "会议", "活动", "事件",
115
+ "subject", "Subject", "name", "Name", "任务", "描述", "会议名称", "活动名称",
116
+ "事项名称", "议题", "主题", "项目", "meeting", "Meeting", "event", "Event",
117
+ "topic", "Topic", "item", "Item"},
118
+ "location": {"地点", "location", "Location", "LOCATION", "地址", "会议室", "venue", "Venue",
119
+ "place", "Place", "room", "Room", "场所", "场地"},
120
+ "participants": {"参与者", "人员", "participants", "Participants", "与会者", "成员", "attendees",
121
+ "Attendees", "负责人", "owner", "Owner", "参会人", "参会人员", "出席人员",
122
+ "相关人员", "联系人"},
123
+ "priority": {"优先级", "priority", "Priority", "PRIORITY", "重要程度", "紧急度", "级别",
124
+ "importance", "Importance", "urgency", "Urgency"},
125
+ "advance": {"提前(分钟)", "提前提醒", "提前", "advance", "Advance", "reminder", "Reminder",
126
+ "提前时间", "提醒时间", "提前分钟"},
127
+ "note": {"备注", "note", "Note", "NOTE", "notes", "Notes", "说明", "描述", "remark",
128
+ "Remark", "comments", "Comments", "附注", "补充", "其他"},
129
+ }
130
+
131
+
132
+ def parse_user_column_map(raw: str) -> dict:
133
+ """
134
+ 解析用户自定义列名映射字符串。
135
+ 输入格式:'会期=date,主旨=title,时间段开始=start'
136
+ 返回:{'会期': 'date', '主旨': 'title', '时间段开始': 'start'}
137
+ """
138
+ VALID_FIELDS = {"date", "start", "end", "title", "location",
139
+ "participants", "priority", "advance", "note"}
140
+ result = {}
141
+ for part in raw.split(","):
142
+ part = part.strip()
143
+ if "=" not in part:
144
+ continue
145
+ custom_name, _, field = part.partition("=")
146
+ custom_name = custom_name.strip()
147
+ field = field.strip()
148
+ if field in VALID_FIELDS and custom_name:
149
+ result[custom_name] = field
150
+ return result
151
+
152
+
153
+ def detect_column_mapping(header_cells: List[str],
154
+ user_map: Optional[dict] = None) -> dict:
155
+ """
156
+ 根据表头自动推断列索引映射。
157
+ 优先级:用户自定义映射 > 精确别名匹配 > 子串匹配。
158
+ 返回 {字段名: 列索引}
159
+ """
160
+ mapping: dict = {}
161
+ used_indices: set = set()
162
+
163
+ # 第零轮:用户自定义映射(最高优先级)
164
+ if user_map:
165
+ for i, cell in enumerate(header_cells):
166
+ cell_clean = cell.strip()
167
+ if cell_clean in user_map:
168
+ field = user_map[cell_clean]
169
+ if field not in mapping: # 同一字段只取第一个
170
+ mapping[field] = i
171
+ used_indices.add(i)
172
+
173
+ # 第一轮:精确匹配
174
+ for field, aliases in COLUMN_ALIASES.items():
175
+ for i, cell in enumerate(header_cells):
176
+ if i in used_indices:
177
+ continue
178
+ if cell.strip() in aliases:
179
+ mapping[field] = i
180
+ used_indices.add(i)
181
+ break
182
+
183
+ # 第二轮:子串匹配(仅对未匹配的字段和未使用的列)
184
+ for field, aliases in COLUMN_ALIASES.items():
185
+ if field in mapping:
186
+ continue
187
+ for i, cell in enumerate(header_cells):
188
+ if i in used_indices:
189
+ continue
190
+ cell_clean = cell.strip()
191
+ matched = False
192
+ for alias in aliases:
193
+ if not alias:
194
+ continue
195
+ # 列名包含别名(如"会议名称"包含"名称"→title)
196
+ # 但要避免短词误匹配(别名长度至少2)
197
+ if len(alias) >= 2 and (alias in cell_clean or cell_clean in alias):
198
+ mapping[field] = i
199
+ used_indices.add(i)
200
+ matched = True
201
+ break
202
+ if matched:
203
+ break
204
+
205
+ return mapping
206
+
207
+
208
+ def report_unresolved_columns(header_cells: List[str], mapping: dict) -> List[str]:
209
+ """返回未能映射到任何已知字段的列名列表"""
210
+ mapped_indices = set(mapping.values())
211
+ return [
212
+ header_cells[i].strip()
213
+ for i in range(len(header_cells))
214
+ if i not in mapped_indices and header_cells[i].strip()
215
+ ]
216
+
217
+
218
+ def check_required_fields(mapping: dict) -> List[str]:
219
+ """检查必要字段是否都已映射,返回缺失字段名"""
220
+ required = ["date", "title"]
221
+ return [f for f in required if f not in mapping]
222
+
223
+
224
+ def is_standard_header(cells: List[str]) -> bool:
225
+ """判断是否是标准格式表头(首列精确为日期/Date/date)"""
226
+ SCHEDULE_HEADERS = {"日期", "Date", "date"}
227
+ return bool(cells) and cells[0].strip() in SCHEDULE_HEADERS
228
+
229
+
230
+ def is_any_schedule_header(cells: List[str]) -> bool:
231
+ """宽松判断:表头中是否含有日期相关列"""
232
+ date_aliases = COLUMN_ALIASES["date"]
233
+ title_aliases = COLUMN_ALIASES["title"]
234
+ has_date = any(c.strip() in date_aliases for c in cells)
235
+ has_title = any(c.strip() in title_aliases for c in cells)
236
+ return has_date and has_title
237
+
238
+
239
+ def extract_event_by_mapping(cells: List[str], mapping: dict, default_advance: int,
240
+ section_name: str) -> Optional[dict]:
241
+ """根据列映射从数据行提取事项"""
242
+ def get(field):
243
+ idx = mapping.get(field)
244
+ if idx is not None and idx < len(cells):
245
+ return cells[idx].strip()
246
+ return ""
247
+
248
+ date_val = normalize_date(get("date"))
249
+ if not date_val:
250
+ return None
251
+
252
+ title = get("title")
253
+ if not title:
254
+ return None
255
+
256
+ start_str = get("start")
257
+ end_str = get("end")
258
+ start_min = normalize_time(start_str) if start_str else None
259
+ end_min = normalize_time(end_str) if end_str else None
260
+
261
+ priority_raw = get("priority")
262
+ priority = priority_raw if priority_raw in ("高", "中", "低") else "中"
263
+
264
+ advance_raw = get("advance")
265
+ try:
266
+ advance_min = int(advance_raw) if advance_raw else default_advance
267
+ except ValueError:
268
+ advance_min = default_advance
269
+
270
+ return {
271
+ "date": date_val.isoformat(),
272
+ "start_min": start_min,
273
+ "end_min": end_min,
274
+ "start_str": start_str,
275
+ "end_str": end_str,
276
+ "title": title,
277
+ "location": get("location"),
278
+ "participants": get("participants"),
279
+ "priority": priority,
280
+ "advance_min": advance_min,
281
+ "note": get("note"),
282
+ "section": section_name,
283
+ "is_task": any(k in title + get("note") for k in TASK_KEYWORDS),
284
+ }
285
+
286
+
287
+ def parse_table_section(lines: List[str], section_name: str,
288
+ default_advance: int,
289
+ user_map: Optional[dict] = None) -> dict:
290
+ """
291
+ 解析日程表格。返回 dict:
292
+ events: 解析出的事项列表
293
+ unresolved_columns: 无法映射的列名列表(宽松模式下)
294
+ missing_required: 缺失的必要字段(date/title)
295
+ mode: 'standard' | 'loose' | 'failed'
296
+ """
297
+ in_table = False
298
+ header_found = False
299
+ column_mapping: Optional[dict] = None
300
+ loose_mode = False
301
+ events = []
302
+ unresolved_columns: List[str] = []
303
+ missing_required: List[str] = []
304
+ parse_mode = "failed"
305
+
306
+ TASK_HEADERS = {"截止日期", "DeadLine", "deadline", "Deadline"}
307
+
308
+ for line in lines:
309
+ stripped = line.strip()
310
+ if not stripped.startswith("|"):
311
+ if in_table:
312
+ in_table = False
313
+ header_found = False
314
+ column_mapping = None
315
+ loose_mode = False
316
+ continue
317
+
318
+ cells = [c.strip() for c in stripped.strip("|").split("|")]
319
+ if len(cells) < 2:
320
+ continue
321
+
322
+ # 跳过任务表头,不被主日程解析器误抓
323
+ if cells[0].strip() in TASK_HEADERS:
324
+ continue
325
+
326
+ # 检测表头
327
+ if not in_table:
328
+ if is_standard_header(cells):
329
+ START_HEADERS = {"开始时间", "开始", "start", "Start", "START", "起始时间"}
330
+ second_col = cells[1].strip() if len(cells) > 1 else ""
331
+ if second_col in START_HEADERS:
332
+ # 标准模式
333
+ in_table = True
334
+ header_found = True
335
+ loose_mode = False
336
+ column_mapping = None
337
+ parse_mode = "standard"
338
+ continue
339
+ else:
340
+ # 首列是"日期"但列序不标准,降级为宽松模式
341
+ mapping = detect_column_mapping(cells, user_map)
342
+ missing = check_required_fields(mapping)
343
+ if not missing:
344
+ in_table = True
345
+ header_found = True
346
+ loose_mode = True
347
+ column_mapping = mapping
348
+ parse_mode = "loose"
349
+ unresolved_columns = report_unresolved_columns(cells, mapping)
350
+ missing_required = []
351
+ else:
352
+ missing_required = missing
353
+ parse_mode = "failed"
354
+ continue
355
+ elif is_any_schedule_header(cells) or user_map:
356
+ # 宽松模式:自动映射 + 用户自定义映射
357
+ mapping = detect_column_mapping(cells, user_map)
358
+ missing = check_required_fields(mapping)
359
+ if not missing:
360
+ in_table = True
361
+ header_found = True
362
+ loose_mode = True
363
+ column_mapping = mapping
364
+ parse_mode = "loose"
365
+ unresolved_columns = report_unresolved_columns(cells, mapping)
366
+ missing_required = []
367
+ else:
368
+ missing_required = missing
369
+ parse_mode = "failed"
370
+ continue
371
+ continue
372
+ continue
373
+
374
+ # 跳过分隔行
375
+ if header_found and all(re.match(r"^[-:]+$", c) for c in cells if c):
376
+ continue
377
+
378
+ if not in_table:
379
+ continue
380
+
381
+ if loose_mode and column_mapping:
382
+ # 宽松模式:用列映射提取
383
+ ev = extract_event_by_mapping(cells, column_mapping, default_advance, section_name)
384
+ if ev:
385
+ events.append(ev)
386
+ else:
387
+ # 标准模式:按固定列序提取
388
+ if len(cells) < 4:
389
+ continue
390
+ date_val = normalize_date(cells[0]) if cells[0] else None
391
+ if not date_val:
392
+ continue
393
+ start_min = normalize_time(cells[1]) if len(cells) > 1 else None
394
+ end_min = normalize_time(cells[2]) if len(cells) > 2 else None
395
+ title = cells[3].strip() if len(cells) > 3 else ""
396
+ location = cells[4].strip() if len(cells) > 4 else ""
397
+ participants = cells[5].strip() if len(cells) > 5 else ""
398
+ priority = cells[6].strip() if len(cells) > 6 else "中"
399
+ advance_raw = cells[7].strip() if len(cells) > 7 else ""
400
+ note = cells[8].strip() if len(cells) > 8 else ""
401
+ if not title:
402
+ continue
403
+ try:
404
+ advance_min = int(advance_raw) if advance_raw else default_advance
405
+ except ValueError:
406
+ advance_min = default_advance
407
+ events.append({
408
+ "date": date_val.isoformat(),
409
+ "start_min": start_min,
410
+ "end_min": end_min,
411
+ "start_str": cells[1].strip() if len(cells) > 1 else "",
412
+ "end_str": cells[2].strip() if len(cells) > 2 else "",
413
+ "title": title,
414
+ "location": location,
415
+ "participants": participants,
416
+ "priority": priority if priority in ("高", "中", "低") else "中",
417
+ "advance_min": advance_min,
418
+ "note": note,
419
+ "section": section_name,
420
+ "is_task": any(k in title + note for k in TASK_KEYWORDS),
421
+ })
422
+
423
+ return {
424
+ "events": events,
425
+ "mode": parse_mode,
426
+ "unresolved_columns": unresolved_columns,
427
+ "missing_required": missing_required,
428
+ }
429
+
430
+
431
+ def parse_task_section(lines: List[str], default_advance: int) -> list:
432
+ """解析待提醒任务表(截止日期形式)"""
433
+ in_table = False
434
+ header_found = False
435
+ tasks = []
436
+
437
+ TASK_HEADERS = {"截止日期", "DeadLine", "deadline", "Deadline"}
438
+
439
+ for line in lines:
440
+ stripped = line.strip()
441
+ if not stripped.startswith("|"):
442
+ if in_table:
443
+ in_table = False
444
+ header_found = False
445
+ continue
446
+
447
+ cells = [c.strip() for c in stripped.strip("|").split("|")]
448
+ if not cells:
449
+ continue
450
+
451
+ if cells[0] in TASK_HEADERS:
452
+ in_table = True
453
+ header_found = True
454
+ continue
455
+
456
+ if header_found and all(re.match(r"^[-:]+$", c) for c in cells if c):
457
+ continue
458
+
459
+ if not in_table:
460
+ continue
461
+
462
+ if len(cells) < 2:
463
+ continue
464
+
465
+ date_val = normalize_date(cells[0]) if cells[0] else None
466
+ if not date_val:
467
+ continue
468
+
469
+ title = cells[1].strip() if len(cells) > 1 else ""
470
+ priority = cells[3].strip() if len(cells) > 3 else "中"
471
+ advance_raw = cells[4].strip() if len(cells) > 4 else ""
472
+ note = cells[5].strip() if len(cells) > 5 else ""
473
+
474
+ # 解析"提前1天"、"提前2天"形式
475
+ advance_min = default_advance
476
+ m = re.search(r"提前\s*(\d+)\s*天", advance_raw)
477
+ if m:
478
+ advance_min = int(m.group(1)) * 24 * 60
479
+ else:
480
+ m = re.search(r"(\d+)", advance_raw)
481
+ if m:
482
+ advance_min = int(m.group(1))
483
+
484
+ tasks.append({
485
+ "date": date_val.isoformat(),
486
+ "start_min": 9 * 60, # 默认早上9点触发
487
+ "end_min": 9 * 60 + 30,
488
+ "start_str": "09:00",
489
+ "end_str": "09:30",
490
+ "title": f"📋 {title}",
491
+ "location": "",
492
+ "participants": cells[2].strip() if len(cells) > 2 else "",
493
+ "priority": priority if priority in ("高", "中", "低") else "中",
494
+ "advance_min": advance_min,
495
+ "note": note,
496
+ "section": "tasks",
497
+ "is_task": True,
498
+ })
499
+
500
+ return tasks
501
+
502
+
503
+ def detect_conflicts(events: list[dict]) -> list[dict]:
504
+ """检测同日时间冲突"""
505
+ by_date: dict[str, list[dict]] = {}
506
+ for e in events:
507
+ if e["start_min"] is None or e["end_min"] is None:
508
+ continue
509
+ by_date.setdefault(e["date"], []).append(e)
510
+
511
+ conflicts = []
512
+ for day, day_events in by_date.items():
513
+ sorted_events = sorted(day_events, key=lambda x: x["start_min"])
514
+ for i in range(len(sorted_events)):
515
+ for j in range(i + 1, len(sorted_events)):
516
+ a, b = sorted_events[i], sorted_events[j]
517
+ if a["end_min"] <= b["start_min"]:
518
+ # 不重叠,但检查是否过于紧凑(< 5分钟间隔)
519
+ gap = b["start_min"] - a["end_min"]
520
+ if 0 < gap < 5:
521
+ conflicts.append({
522
+ "type": "tight",
523
+ "date": day,
524
+ "event_a": a["title"],
525
+ "event_b": b["title"],
526
+ "a_time": f"{a['start_str']}–{a['end_str']}",
527
+ "b_time": f"{b['start_str']}–{b['end_str']}",
528
+ "gap_min": gap,
529
+ "message": (
530
+ f"⚠️ 紧凑安排:【{a['title']}】{a['start_str']}–{a['end_str']} "
531
+ f"与【{b['title']}】{b['start_str']} 之间仅 {gap} 分钟,几乎没有缓冲时间。"
532
+ ),
533
+ })
534
+ else:
535
+ # 重叠
536
+ overlap = a["end_min"] - b["start_min"]
537
+ conflicts.append({
538
+ "type": "overlap",
539
+ "date": day,
540
+ "event_a": a["title"],
541
+ "event_b": b["title"],
542
+ "a_time": f"{a['start_str']}–{a['end_str']}",
543
+ "b_time": f"{b['start_str']}–{b['end_str']}",
544
+ "overlap_min": overlap,
545
+ "message": (
546
+ f"⚠️ 时间冲突:【{a['title']}】{a['start_str']}–{a['end_str']} "
547
+ f"与【{b['title']}】{b['start_str']}–{b['end_str']} "
548
+ f"重叠 {overlap} 分钟。"
549
+ ),
550
+ })
551
+ return conflicts
552
+
553
+
554
+ def filter_upcoming(events: list, mode: str, now: datetime) -> list:
555
+ """按 mode 筛选需要提醒的事项"""
556
+ today = now.date()
557
+ tomorrow = today + timedelta(days=1)
558
+ week_end = today + timedelta(days=7)
559
+ now_min = now.hour * 60 + now.minute
560
+
561
+ result = []
562
+ for e in events:
563
+ try:
564
+ ev_date = date.fromisoformat(e["date"])
565
+ except ValueError:
566
+ continue
567
+
568
+ # 已过期跳过
569
+ if ev_date < today:
570
+ continue
571
+ if ev_date == today and e["end_min"] is not None and e["end_min"] < now_min:
572
+ continue
573
+
574
+ # 低优先级且非任务,默认跳过
575
+ if e["priority"] == "低" and not e["is_task"]:
576
+ continue
577
+
578
+ include = False
579
+ if mode == "today" and ev_date == today:
580
+ include = True
581
+ elif mode == "tomorrow" and ev_date == tomorrow:
582
+ include = True
583
+ elif mode == "week" and today <= ev_date <= week_end:
584
+ include = True
585
+ elif mode == "all":
586
+ include = True
587
+
588
+ # today 模式额外:明日高优先级也纳入
589
+ if mode == "today" and ev_date == tomorrow and e["priority"] == "高":
590
+ include = True
591
+
592
+ if include:
593
+ result.append(e)
594
+
595
+ result.sort(key=lambda x: (x["date"], x["start_min"] or 0))
596
+ return result
597
+
598
+
599
+ def build_reminder_time(ev: dict, now: datetime) -> Optional[str]:
600
+ """计算提醒时刻的 cron 表达式(单次)"""
601
+ if ev["start_min"] is None:
602
+ return None
603
+ try:
604
+ ev_date = date.fromisoformat(ev["date"])
605
+ except ValueError:
606
+ return None
607
+
608
+ remind_total_min = ev["start_min"] - ev["advance_min"]
609
+ if remind_total_min < 0:
610
+ remind_total_min = 0
611
+
612
+ r_hour = remind_total_min // 60
613
+ r_min = remind_total_min % 60
614
+
615
+ # 单次 cron:分 时 日 月 *
616
+ return f"{r_min} {r_hour} {ev_date.day} {ev_date.month} *"
617
+
618
+
619
+ def build_reminder_time_str(ev: dict) -> str:
620
+ """人类可读的提醒时刻"""
621
+ if ev["start_min"] is None:
622
+ return ""
623
+ remind_total_min = max(0, ev["start_min"] - ev["advance_min"])
624
+ r_hour = remind_total_min // 60
625
+ r_min = remind_total_min % 60
626
+ return f"{r_hour:02d}:{r_min:02d}"
627
+
628
+
629
+ def main():
630
+ args = parse_args()
631
+ content = read_content(args)
632
+ cfg = parse_global_config(content)
633
+ now = datetime.now()
634
+ if args.now:
635
+ try:
636
+ now = datetime.strptime(args.now, "%Y-%m-%d %H:%M")
637
+ except ValueError:
638
+ pass
639
+
640
+ lines = content.splitlines()
641
+
642
+ # 解析用户自定义列名映射
643
+ user_map = parse_user_column_map(args.column_map) if args.column_map else None
644
+
645
+ # 解析主日程表
646
+ schedule_result = parse_table_section(
647
+ lines, "schedule", cfg["default_advance_minutes"], user_map
648
+ )
649
+ events = schedule_result["events"]
650
+ # 解析任务表
651
+ tasks = parse_task_section(lines, cfg["default_advance_minutes"])
652
+ all_events = events + tasks
653
+
654
+ # 冲突检测(仅对 schedule 类事项)
655
+ conflicts = detect_conflicts(events)
656
+
657
+ # 筛选近期/今日事项
658
+ upcoming = filter_upcoming(all_events, args.mode, now)
659
+
660
+ # 为每个事项计算提醒时刻
661
+ for ev in upcoming:
662
+ ev["remind_cron"] = build_reminder_time(ev, now)
663
+ ev["remind_time_str"] = build_reminder_time_str(ev)
664
+
665
+ output = {
666
+ "meta": {
667
+ "mode": args.mode,
668
+ "now": now.strftime("%Y-%m-%d %H:%M"),
669
+ "config": cfg,
670
+ "parse_mode": schedule_result["mode"],
671
+ },
672
+ "summary": {
673
+ "total_events": len(all_events),
674
+ "upcoming_count": len(upcoming),
675
+ "conflict_count": len(conflicts),
676
+ },
677
+ "upcoming": upcoming,
678
+ "conflicts": conflicts,
679
+ }
680
+
681
+ # 列名问题提示(宽松模式下,让 agent 告知用户)
682
+ if schedule_result["unresolved_columns"]:
683
+ output["warnings"] = {
684
+ "unresolved_columns": schedule_result["unresolved_columns"],
685
+ "message": (
686
+ f"⚠️ 以下列名无法自动识别,其内容已忽略:"
687
+ f"{', '.join(schedule_result['unresolved_columns'])}。"
688
+ f"如需记录这些列,请告诉我它们对应的字段(如:'会期' = 日期,'主旨' = 事项)。"
689
+ ),
690
+ }
691
+ if schedule_result["missing_required"]:
692
+ output["errors"] = {
693
+ "missing_required": schedule_result["missing_required"],
694
+ "message": (
695
+ f"❌ 表格缺少必要字段:{', '.join(schedule_result['missing_required'])},"
696
+ f"无法解析。请确认表格中包含日期列和事项/标题列。"
697
+ ),
698
+ }
699
+
700
+ print(json.dumps(output, ensure_ascii=False, indent=2))
701
+
702
+
703
+ if __name__ == "__main__":
704
+ main()