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.
- package/bin/sophhub.js +21 -0
- package/package.json +32 -0
- package/skills/VERSIONS.md +27 -0
- package/skills/builtin/clawhub/SKILL.md +77 -0
- package/skills/builtin/flight-booking/SKILL.md +288 -0
- package/skills/builtin/flight-booking/scripts/flight_booking.py +1232 -0
- package/skills/builtin/inventory-management/SKILL.md +241 -0
- package/skills/builtin/inventory-management/scripts/inventory.py +1844 -0
- package/skills/builtin/schedule-reminder/SKILL.md +619 -0
- package/skills/builtin/schedule-reminder/schedule_template.md +68 -0
- package/skills/builtin/schedule-reminder/scripts/append_event.py +204 -0
- package/skills/builtin/schedule-reminder/scripts/create_reminders.sh +163 -0
- package/skills/builtin/schedule-reminder/scripts/daily_activate.sh +175 -0
- package/skills/builtin/schedule-reminder/scripts/parse_schedule.py +704 -0
- package/skills/builtin/schedule-reminder/scripts/setup.sh +242 -0
- package/skills/builtin/schedule-reminder//347/224/250/346/210/267/346/214/207/345/215/227.md +311 -0
- package/skills/builtin/skill-creator/SKILL.md +370 -0
- package/skills/builtin/skill-creator/license.txt +202 -0
- package/skills/builtin/skill-creator/scripts/init_skill.py +378 -0
- package/skills/builtin/skill-creator/scripts/package_skill.py +111 -0
- package/skills/builtin/skill-creator/scripts/quick_validate.py +101 -0
- package/skills/builtin/sophnet-customer-management/SKILL.md +271 -0
- package/skills/builtin/sophnet-customer-management/pyproject.toml +15 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__init__.py +2 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__main__.py +5 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/cli.py +67 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/__init__.py +2 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/customer.py +60 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/export_file.py +18 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/import_file.py +15 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/reminder.py +26 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/schema.py +28 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/config.py +54 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/__init__.py +2 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/exporter.py +85 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/models.py +84 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/normalizer.py +144 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/parser.py +241 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/query.py +109 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/reminder.py +121 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/repository.py +397 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/schema.py +106 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/service.py +565 -0
- package/skills/builtin/sophnet-customer-management/uv.lock +48 -0
- package/skills/builtin/sophnet-customized-marketing/SKILL.md +144 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/campaign-planning.md +187 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/content-generation.md +124 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/marketing-calendar.md +59 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/multi-channel-bundle.md +94 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/poster-generation.md +182 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/style-profile-workflow.md +103 -0
- package/skills/builtin/sophnet-customized-marketing/pyproject.toml +9 -0
- package/skills/builtin/sophnet-customized-marketing/references/campaign-mechanics.md +168 -0
- package/skills/builtin/sophnet-customized-marketing/references/content-safety.md +26 -0
- package/skills/builtin/sophnet-customized-marketing/references/marketing-date-checklist.md +99 -0
- package/skills/builtin/sophnet-customized-marketing/references/platform-writing-guidelines.md +88 -0
- package/skills/builtin/sophnet-customized-marketing/references/quality-checklist.md +44 -0
- package/skills/builtin/sophnet-customized-marketing/scripts/generate_poster.py +585 -0
- package/skills/builtin/sophnet-customized-marketing/scripts/style_profile.py +215 -0
- package/skills/builtin/sophnet-face-search/SKILL.md +115 -0
- package/skills/builtin/sophnet-face-search/pyproject.toml +11 -0
- package/skills/builtin/sophnet-face-search/scripts/face_search.py +336 -0
- package/skills/builtin/sophnet-face-search/uv.lock +508 -0
- package/skills/builtin/sophnet-image-edit/SKILL.md +140 -0
- package/skills/builtin/sophnet-image-edit/pyproject.toml +9 -0
- package/skills/builtin/sophnet-image-edit/scripts/edit_and_preview.sh +68 -0
- package/skills/builtin/sophnet-image-edit/scripts/edit_image.py +279 -0
- package/skills/builtin/sophnet-image-edit/uv.lock +234 -0
- package/skills/builtin/sophnet-image-generate/SKILL.md +62 -0
- package/skills/builtin/sophnet-image-generate/pyproject.toml +9 -0
- package/skills/builtin/sophnet-image-generate/scripts/generate_image.py +156 -0
- package/skills/builtin/sophnet-image-generate/uv.lock +234 -0
- package/skills/builtin/sophnet-image-ocr/SKILL.md +167 -0
- package/skills/builtin/sophnet-image-ocr/pyproject.toml +13 -0
- package/skills/builtin/sophnet-image-ocr/scripts/ocr.py +226 -0
- package/skills/builtin/sophnet-image-ocr/uv.lock +234 -0
- package/skills/builtin/sophnet-infinite-talk/SKILL.md +140 -0
- package/skills/builtin/sophnet-infinite-talk/pyproject.toml +9 -0
- package/skills/builtin/sophnet-infinite-talk/scripts/gen.py +172 -0
- package/skills/builtin/sophnet-oss/SKILL.md +109 -0
- package/skills/builtin/sophnet-oss/pyproject.toml +8 -0
- package/skills/builtin/sophnet-oss/scripts/upload_file.py +43 -0
- package/skills/builtin/sophnet-qa-install/SKILL.md +210 -0
- package/skills/builtin/sophnet-qa-install/pyproject.toml +6 -0
- package/skills/builtin/sophnet-qa-install/scripts/backup_md.py +35 -0
- package/skills/builtin/sophnet-qa-install/scripts/check_installed.py +143 -0
- package/skills/builtin/sophnet-qa-install/scripts/update_config.py +142 -0
- package/skills/builtin/sophnet-qa-install/scripts/update_md.py +73 -0
- package/skills/builtin/sophnet-training-install/SKILL.md +211 -0
- package/skills/builtin/sophnet-training-install/pyproject.toml +6 -0
- package/skills/builtin/sophnet-training-install/scripts/backup_md.py +35 -0
- package/skills/builtin/sophnet-training-install/scripts/check_installed.py +144 -0
- package/skills/builtin/sophnet-training-install/scripts/update_config.py +142 -0
- package/skills/builtin/sophnet-training-install/scripts/update_md.py +73 -0
- package/skills/builtin/sophnet-tts/SKILL.md +79 -0
- package/skills/builtin/sophnet-tts/pyproject.toml +9 -0
- package/skills/builtin/sophnet-tts/scripts/gen_tts.py +130 -0
- package/skills/builtin/sophnet-video-generate/SKILL.md +116 -0
- package/skills/builtin/sophnet-video-generate/scripts/gen_video.py +304 -0
- package/skills/builtin/video-understand/SKILL.md +79 -0
- package/skills/builtin/video-understand/scripts/video_understand.py +204 -0
- package/skills/builtin/weather/SKILL.md +112 -0
- package/skills/builtin/web-scraper/SKILL.md +101 -0
- package/skills/builtin/web-scraper/scripts/scrape.py +270 -0
- package/skills/builtin/website-builder/SKILL.md +266 -0
- package/skills/builtin/website-builder/scripts/deploy_site.sh +46 -0
- package/skills/store/didi-ride/SKILL.md +309 -0
- package/skills/store/didi-ride/_meta.json +6 -0
- package/skills/store/didi-ride/assets/PREFERENCE.md +58 -0
- package/skills/store/didi-ride/package.json +15 -0
- package/skills/store/didi-ride/references/api_references.md +171 -0
- package/skills/store/didi-ride/references/error_handling.md +68 -0
- package/skills/store/didi-ride/references/setup.md +73 -0
- package/skills/store/didi-ride/references/workflow.md +150 -0
- package/skills/store/flyai/SKILL.md +119 -0
- package/skills/store/flyai/references/fliggy-fast-search.md +53 -0
- package/skills/store/flyai/references/search-flight.md +89 -0
- package/skills/store/flyai/references/search-hotels.md +57 -0
- package/skills/store/flyai/references/search-poi.md +49 -0
- package/src/commands/download.js +103 -0
- package/src/commands/list.js +67 -0
- package/src/utils/config.js +24 -0
- package/src/utils/gitlab.js +67 -0
- package/src/utils/paths.js +19 -0
- 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()
|