sophhub 0.4.33 → 0.4.35
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/intern-daily-report/skill.json +16 -0
- package/skills/intern-daily-report/src/SKILL.md +82 -0
- package/skills/intern-daily-report/src/pyproject.toml +4 -0
- package/skills/intern-daily-report/src/scripts/__init__.py +0 -0
- package/skills/intern-daily-report/src/scripts/intern_daily_report.py +369 -0
- package/skills/intern-daily-report/src/scripts/test_intern_daily_report.py +284 -0
- package/skills/online-bug-report/skill.json +25 -2
- package/skills/online-bug-report/src/SKILL.md +8 -2
- package/skills/online-bug-report/src/scripts/report_bug.py +7 -0
- package/skills/sophnet-docx/skill.json +15 -2
- package/skills/sophnet-docx/src/SKILL.md +32 -400
- package/skills/sophnet-docx/src/package.json +1 -1
- package/skills/sophnet-docx/src/pyproject.toml +4 -0
- package/skills/sophnet-docx/src/references/create-docx.md +94 -0
- package/skills/sophnet-docx/src/references/docx-js-api.md +267 -0
- package/skills/sophnet-docx/src/references/edit-docx.md +70 -0
- package/skills/sophnet-docx/src/references/read-docx.md +28 -0
- package/skills/sophnet-docx/src/scripts/comment.py +115 -2
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "intern-daily-report",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"types": ["private"],
|
|
5
|
+
"displayName": "",
|
|
6
|
+
"description": "",
|
|
7
|
+
"changelog": [
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"date": "2026-06-04",
|
|
11
|
+
"changes": ["初次提交:实习生日报统计,按 PDT 分组输出提交/请假/未提交统计"]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"createdAt": "2026-06-04",
|
|
15
|
+
"updatedAt": "2026-06-04"
|
|
16
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: intern-daily-report
|
|
3
|
+
description: 统计实习生日报提交情况,按 PDT 分组展示每人提交/请假/未提交状态。当用户需要统计日报、查看提交情况、检查谁没交日报时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 实习生日报统计
|
|
7
|
+
|
|
8
|
+
读取实习生名册、日报文件和请假记录,按 PDT 分组统计每日提交情况。
|
|
9
|
+
|
|
10
|
+
## 调用流程
|
|
11
|
+
|
|
12
|
+
1. 从用户输入解析目标日期(如"昨天"→ 昨天日期,"今天"→ 今天日期,"6月2日"→ `2026-06-02`)
|
|
13
|
+
2. 组装命令并执行:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run {baseDir}/scripts/intern_daily_report.py \
|
|
17
|
+
--date <YYYY-MM-DD> \
|
|
18
|
+
--roster-dir memory \
|
|
19
|
+
--daily-dir /home/node/.openclaw/workspace-intern/workspace-intern-qa/records
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
3. 将 stdout 输出返回给用户
|
|
23
|
+
|
|
24
|
+
## 参数说明
|
|
25
|
+
|
|
26
|
+
| 参数 | 必填 | 说明 |
|
|
27
|
+
|------|------|------|
|
|
28
|
+
| `--date` / `-d` | 是 | 查询日期 (YYYY-MM-DD) |
|
|
29
|
+
| `--roster-dir` / `-r` | 否 | 名册目录(默认 `memory/`) |
|
|
30
|
+
| `--daily-dir` | 否 | 日报/请假文件目录(默认生产路径) |
|
|
31
|
+
|
|
32
|
+
## 输出格式
|
|
33
|
+
|
|
34
|
+
按 PDT 分组,每组标题为 `## PDT名称(已提交/总人数人)`,每人一行状态标记:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
# Sophclaw 6月2日实习生日报
|
|
38
|
+
|
|
39
|
+
## 场景PDT(8/9人)
|
|
40
|
+
|
|
41
|
+
- **陈旺:** ✅ 完成 session-identity 多渠道对齐【卡点:否】【下一步:根据意见调整细节】
|
|
42
|
+
- **龙星宇:** 🏖️ 2026-06-02~2026-06-04 — 回学校开会,OA 已提交
|
|
43
|
+
- **刘荧:** ❌ 未提交
|
|
44
|
+
|
|
45
|
+
> 提交:8 | 未提交:0 | 请假:1(不计入未提交)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
状态图标说明:
|
|
51
|
+
|
|
52
|
+
- ✅ 已提交 — 显示工作摘要、卡点、下一步计划
|
|
53
|
+
- 🏖️ 请假 — 显示请假时间及原因
|
|
54
|
+
- ❌ 未提交 — 当天未提交日报且未请假
|
|
55
|
+
- 🆕 今日入职 — 标记当天新入职实习生(提交的追加在 ✅ 后,未提交的追加在 ❌ 前)
|
|
56
|
+
|
|
57
|
+
组内按状态排序(✅ → 🏖️ → ❌),同状态按姓名排。每组末尾一行统计。
|
|
58
|
+
|
|
59
|
+
如有未提交,末尾追加提醒段落,按导师分组 @ 提醒:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
⚠️ **未提交提醒:**
|
|
63
|
+
|
|
64
|
+
- @李超杰:杨祖康 未提交日报
|
|
65
|
+
- @于洋:陈高璋 未提交日报
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 数据源
|
|
69
|
+
|
|
70
|
+
| 数据 | 路径 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| 实习生名册 | `memory/实习生信息表-*.md`(解析「实习生维度」表格) |
|
|
73
|
+
| 日报记录 | `{daily-dir}/YYYY-MM-DD.md` |
|
|
74
|
+
| 请假记录 | `{daily-dir}/leave.md` |
|
|
75
|
+
|
|
76
|
+
## 注意事项
|
|
77
|
+
|
|
78
|
+
- 名册中标记「已离职」「放弃入职」的实习生自动排除
|
|
79
|
+
- 入职日期晚于查询日期或无法解析的实习生不计入统计
|
|
80
|
+
- 请假覆盖当天但提交了日报 → 按已提交处理
|
|
81
|
+
- 同名多次提交 → 取最新一条
|
|
82
|
+
- 无未提交时不输出提醒段落
|
|
File without changes
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""实习生日报统计 — 按 PDT 分组输出提交/请假/未提交统计。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import glob
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from collections import OrderedDict
|
|
12
|
+
from datetime import date, datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_roster_file(roster_dir: str) -> Path:
|
|
18
|
+
pattern = os.path.join(roster_dir, "实习生信息表-*.md")
|
|
19
|
+
matches = sorted(glob.glob(pattern))
|
|
20
|
+
if not matches:
|
|
21
|
+
sys.exit("未找到名册文件: {}".format(pattern))
|
|
22
|
+
return Path(matches[0])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_roster_table(filepath: Path) -> List[Dict[str, Any]]:
|
|
26
|
+
"""解析「实习生维度」表格,返回在职+已入职人员列表。"""
|
|
27
|
+
content = filepath.read_text(encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
m = re.search(r"###\s+1\.\s*实习生维度(.*?)(?=###|\Z)", content, re.DOTALL)
|
|
30
|
+
if not m:
|
|
31
|
+
sys.exit("名册中未找到「1. 实习生维度」章节")
|
|
32
|
+
|
|
33
|
+
section = m.group(1)
|
|
34
|
+
lines = section.strip().split("\n")
|
|
35
|
+
|
|
36
|
+
interns: List[Dict[str, Any]] = []
|
|
37
|
+
in_table = False
|
|
38
|
+
|
|
39
|
+
for line in lines:
|
|
40
|
+
line = line.strip()
|
|
41
|
+
if line.startswith("|") and "姓名" in line and "入职日期" in line:
|
|
42
|
+
in_table = True
|
|
43
|
+
continue
|
|
44
|
+
if in_table and line.startswith("|---"):
|
|
45
|
+
continue
|
|
46
|
+
if in_table and line.startswith("|"):
|
|
47
|
+
cells = [c.strip() for c in line.strip("|").split("|")]
|
|
48
|
+
if len(cells) < 11:
|
|
49
|
+
continue
|
|
50
|
+
name = cells[1].strip()
|
|
51
|
+
if not name:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
start_date_str = cells[2].strip()
|
|
55
|
+
mentor = cells[5].strip()
|
|
56
|
+
pdt = cells[9].strip()
|
|
57
|
+
note = cells[10].strip() if len(cells) > 10 else ""
|
|
58
|
+
|
|
59
|
+
# 排除已离职/放弃入职
|
|
60
|
+
if "已离职" in note or "放弃入职" in note:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# 解析入职日期
|
|
64
|
+
start_date: Optional[date] = None
|
|
65
|
+
try:
|
|
66
|
+
start_date = datetime.strptime(start_date_str, "%Y/%m/%d").date()
|
|
67
|
+
except ValueError:
|
|
68
|
+
pass # "待定"/"已入职" → None,后续按未入职排除
|
|
69
|
+
|
|
70
|
+
interns.append({
|
|
71
|
+
"name": name,
|
|
72
|
+
"mentor": mentor,
|
|
73
|
+
"pdt": pdt or "未分配PDT",
|
|
74
|
+
"start_date": start_date,
|
|
75
|
+
"note": note,
|
|
76
|
+
})
|
|
77
|
+
elif in_table and not line.startswith("|") and line.strip() != "":
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
return interns
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_daily_entries(filepath: Path) -> Dict[str, Dict[str, str]]:
|
|
84
|
+
"""解析 YYYY-MM-DD.md,返回 {姓名: {summary, blocker, next_step, ts}}。"""
|
|
85
|
+
if not filepath.exists():
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
content = filepath.read_text(encoding="utf-8")
|
|
89
|
+
entries = re.split(r"\n---\n", content)
|
|
90
|
+
|
|
91
|
+
result: Dict[str, Dict[str, str]] = {}
|
|
92
|
+
for entry in entries:
|
|
93
|
+
name_m = re.search(r"姓名[::]\s*(.+?)(?:\n|$)", entry)
|
|
94
|
+
if not name_m:
|
|
95
|
+
continue
|
|
96
|
+
name = name_m.group(1).strip().rstrip("。,,.")
|
|
97
|
+
|
|
98
|
+
ts_m = re.search(r"收到时间:(.+?)(?:(|$)", entry)
|
|
99
|
+
ts = ts_m.group(1).strip() if ts_m else ""
|
|
100
|
+
|
|
101
|
+
work_m = re.search(r"今[日天]主要工作[::]\s*(.+?)(?:\n|$)", entry)
|
|
102
|
+
work = work_m.group(1).strip() if work_m else ""
|
|
103
|
+
|
|
104
|
+
blocker_m = re.search(r"(?:是否存在卡点|是否卡点)[::]\s*(.+?)(?:\n|$)", entry)
|
|
105
|
+
blocker = blocker_m.group(1).strip() if blocker_m else ""
|
|
106
|
+
|
|
107
|
+
next_m = re.search(r"下一步计划[::]\s*(.+?)(?:\n|$)", entry)
|
|
108
|
+
next_step = next_m.group(1).strip() if next_m else ""
|
|
109
|
+
|
|
110
|
+
# 同名取第一条(日报文件中条目已按时间倒序排列)
|
|
111
|
+
if name not in result:
|
|
112
|
+
result[name] = {
|
|
113
|
+
"summary": work,
|
|
114
|
+
"blocker": blocker,
|
|
115
|
+
"next_step": next_step,
|
|
116
|
+
"ts": ts,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_leave_table(filepath: Path) -> List[Dict[str, str]]:
|
|
123
|
+
"""解析 leave.md 表格,返回 [{name, leave_time, reason, report_date}]。"""
|
|
124
|
+
if not filepath.exists():
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
content = filepath.read_text(encoding="utf-8")
|
|
128
|
+
lines = content.split("\n")
|
|
129
|
+
|
|
130
|
+
leaves: List[Dict[str, str]] = []
|
|
131
|
+
for line in lines:
|
|
132
|
+
line = line.strip()
|
|
133
|
+
if not line.startswith("|") or "报备日期" in line or line.startswith("|---"):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
cells = [c.strip() for c in line.strip("|").split("|")]
|
|
137
|
+
if len(cells) < 5:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
leaves.append({
|
|
141
|
+
"name": cells[1].strip(),
|
|
142
|
+
"leave_time": cells[3].strip(),
|
|
143
|
+
"reason": cells[4].strip(),
|
|
144
|
+
"report_date": cells[0].strip(),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return leaves
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def is_date_in_leave_range(query_date: date, leave_time_str: str) -> bool:
|
|
151
|
+
"""判断查询日期是否在请假时间范围内。"""
|
|
152
|
+
if not leave_time_str:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
# 提取所有 YYYY-MM-DD
|
|
156
|
+
date_matches = re.findall(r"(\d{4})-(\d{2})-(\d{2})", leave_time_str)
|
|
157
|
+
if not date_matches:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
date_list: List[date] = []
|
|
161
|
+
for y, m, d in date_matches:
|
|
162
|
+
try:
|
|
163
|
+
date_list.append(date(int(y), int(m), int(d)))
|
|
164
|
+
except ValueError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
if not date_list:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
dates: set = set()
|
|
171
|
+
if "~" in leave_time_str and len(date_list) >= 2:
|
|
172
|
+
date_list.sort()
|
|
173
|
+
d = date_list[0]
|
|
174
|
+
end = date_list[-1]
|
|
175
|
+
while d <= end:
|
|
176
|
+
dates.add(d)
|
|
177
|
+
d += timedelta(days=1)
|
|
178
|
+
else:
|
|
179
|
+
dates.update(date_list)
|
|
180
|
+
|
|
181
|
+
return query_date in dates
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def judge_status(
|
|
185
|
+
roster_entry: Dict[str, Any],
|
|
186
|
+
daily_data: Dict[str, Dict[str, str]],
|
|
187
|
+
leave_data: List[Dict[str, str]],
|
|
188
|
+
query_date: date,
|
|
189
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
190
|
+
"""判定单个实习生状态。返回 (status, detail)。
|
|
191
|
+
|
|
192
|
+
status: 'not_onboarded' | 'submitted' | 'on_leave' | 'missing'
|
|
193
|
+
"""
|
|
194
|
+
name = roster_entry["name"]
|
|
195
|
+
start_date = roster_entry.get("start_date")
|
|
196
|
+
|
|
197
|
+
# 未入职(入职日期 > 查询日期 或 无日期)
|
|
198
|
+
if start_date is None or start_date > query_date:
|
|
199
|
+
return ("not_onboarded", {})
|
|
200
|
+
|
|
201
|
+
is_today_onboard = (start_date == query_date)
|
|
202
|
+
|
|
203
|
+
# 提交了日报
|
|
204
|
+
if name in daily_data:
|
|
205
|
+
detail = dict(daily_data[name])
|
|
206
|
+
detail["is_today_onboard"] = is_today_onboard
|
|
207
|
+
return ("submitted", detail)
|
|
208
|
+
|
|
209
|
+
# 未提交 → 查请假
|
|
210
|
+
for lv in leave_data:
|
|
211
|
+
if lv["name"] == name:
|
|
212
|
+
if is_date_in_leave_range(query_date, lv["leave_time"]):
|
|
213
|
+
return ("on_leave", {
|
|
214
|
+
"leave_time": lv["leave_time"],
|
|
215
|
+
"reason": lv["reason"],
|
|
216
|
+
"is_today_onboard": is_today_onboard,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return ("missing", {"is_today_onboard": is_today_onboard})
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def format_report(
|
|
223
|
+
roster: List[Dict[str, Any]],
|
|
224
|
+
daily_data: Dict[str, Dict[str, str]],
|
|
225
|
+
leave_data: List[Dict[str, str]],
|
|
226
|
+
query_date: date,
|
|
227
|
+
) -> str:
|
|
228
|
+
"""按 PDT 分组格式化统计报告。"""
|
|
229
|
+
pdt_groups: Dict[str, List[Dict[str, Any]]] = OrderedDict()
|
|
230
|
+
|
|
231
|
+
for intern in roster:
|
|
232
|
+
status, detail = judge_status(intern, daily_data, leave_data, query_date)
|
|
233
|
+
|
|
234
|
+
if status == "not_onboarded":
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
intern["_status"] = status
|
|
238
|
+
intern["_detail"] = detail
|
|
239
|
+
|
|
240
|
+
pdt = intern.get("pdt", "未分配PDT")
|
|
241
|
+
if pdt not in pdt_groups:
|
|
242
|
+
pdt_groups[pdt] = []
|
|
243
|
+
pdt_groups[pdt].append(intern)
|
|
244
|
+
|
|
245
|
+
# Title
|
|
246
|
+
title = "Sophclaw {}月{}日实习生日报".format(query_date.month, query_date.day)
|
|
247
|
+
lines: List[str] = ["# {}".format(title), ""]
|
|
248
|
+
|
|
249
|
+
# Build output
|
|
250
|
+
status_order = {"submitted": 0, "on_leave": 1, "missing": 2}
|
|
251
|
+
|
|
252
|
+
for pdt, members in pdt_groups.items():
|
|
253
|
+
submitted_count = sum(1 for m in members if m["_status"] == "submitted")
|
|
254
|
+
leave_count = sum(1 for m in members if m["_status"] == "on_leave")
|
|
255
|
+
missing_count = sum(1 for m in members if m["_status"] == "missing")
|
|
256
|
+
|
|
257
|
+
lines.append("## {}({}/{}人)".format(pdt, submitted_count, len(members)))
|
|
258
|
+
lines.append("")
|
|
259
|
+
|
|
260
|
+
members.sort(key=lambda m: (status_order.get(m["_status"], 9), m["name"]))
|
|
261
|
+
|
|
262
|
+
for m in members:
|
|
263
|
+
name = m["name"]
|
|
264
|
+
detail = m.get("_detail", {})
|
|
265
|
+
tag = "🆕 " if detail.get("is_today_onboard") else ""
|
|
266
|
+
|
|
267
|
+
if m["_status"] == "submitted":
|
|
268
|
+
lines.append(
|
|
269
|
+
"- **{}:** {}✅ {}【卡点:{}】【下一步:{}】".format(
|
|
270
|
+
name, tag,
|
|
271
|
+
detail.get("summary", ""),
|
|
272
|
+
detail.get("blocker", "否"),
|
|
273
|
+
detail.get("next_step", ""),
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
elif m["_status"] == "on_leave":
|
|
277
|
+
leave_time = detail.get("leave_time", "")
|
|
278
|
+
reason = detail.get("reason", "")
|
|
279
|
+
lines.append(
|
|
280
|
+
"- **{}:** {}🏖️ {}— {}".format(name, tag, leave_time, reason)
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
lines.append("- **{}:** {}❌ 未提交".format(name, tag))
|
|
284
|
+
|
|
285
|
+
lines.append("")
|
|
286
|
+
lines.append(
|
|
287
|
+
"> 提交:{} | 未提交:{} | 请假:{}(不计入未提交)".format(
|
|
288
|
+
submitted_count, missing_count, leave_count
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
lines.append("")
|
|
292
|
+
lines.append("---")
|
|
293
|
+
lines.append("")
|
|
294
|
+
|
|
295
|
+
# Missing-reminder: group by mentor
|
|
296
|
+
missing_by_mentor: Dict[str, List[str]] = OrderedDict()
|
|
297
|
+
for m in roster:
|
|
298
|
+
if m.get("_status") == "missing":
|
|
299
|
+
mentor = m.get("mentor", "")
|
|
300
|
+
if mentor:
|
|
301
|
+
missing_by_mentor.setdefault(mentor, []).append(m["name"])
|
|
302
|
+
|
|
303
|
+
# Missing reminder
|
|
304
|
+
if missing_by_mentor:
|
|
305
|
+
lines.append("")
|
|
306
|
+
lines.append("---")
|
|
307
|
+
lines.append("")
|
|
308
|
+
lines.append("⚠️ **未提交提醒:**")
|
|
309
|
+
lines.append("")
|
|
310
|
+
for mentor, names in missing_by_mentor.items():
|
|
311
|
+
lines.append(
|
|
312
|
+
"- @{}:{} 未提交日报".format(mentor, "、".join(names))
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return "\n".join(lines)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
DEFAULT_DAILY_DIR = "/home/node/.openclaw/workspace-intern/workspace-intern-qa/records"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> int:
|
|
322
|
+
p = argparse.ArgumentParser(description="实习生日报统计")
|
|
323
|
+
p.add_argument("--date", "-d", required=True, help="查询日期 (YYYY-MM-DD)")
|
|
324
|
+
p.add_argument(
|
|
325
|
+
"--roster-dir", "-r", default="memory", help="名册目录(默认: memory/)"
|
|
326
|
+
)
|
|
327
|
+
p.add_argument(
|
|
328
|
+
"--daily-dir",
|
|
329
|
+
default=DEFAULT_DAILY_DIR,
|
|
330
|
+
help="日报/请假文件目录(默认: {})".format(DEFAULT_DAILY_DIR),
|
|
331
|
+
)
|
|
332
|
+
args = p.parse_args()
|
|
333
|
+
|
|
334
|
+
# 解析日期
|
|
335
|
+
try:
|
|
336
|
+
query_date = datetime.strptime(args.date, "%Y-%m-%d").date()
|
|
337
|
+
except ValueError:
|
|
338
|
+
sys.exit("日期格式错误: {}(需要 YYYY-MM-DD)".format(args.date))
|
|
339
|
+
|
|
340
|
+
# 解析名册
|
|
341
|
+
roster_file = find_roster_file(args.roster_dir)
|
|
342
|
+
roster = parse_roster_table(roster_file)
|
|
343
|
+
|
|
344
|
+
if not roster:
|
|
345
|
+
print("⚠️ 名册中无在职实习生记录")
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
# 解析日报
|
|
349
|
+
daily_dir = Path(args.daily_dir)
|
|
350
|
+
daily_file = daily_dir / "{}.md".format(query_date.isoformat())
|
|
351
|
+
daily_data = parse_daily_entries(daily_file)
|
|
352
|
+
|
|
353
|
+
# 解析请假
|
|
354
|
+
leave_file = daily_dir / "leave.md"
|
|
355
|
+
leave_data = parse_leave_table(leave_file)
|
|
356
|
+
|
|
357
|
+
# 格式化输出
|
|
358
|
+
report = format_report(roster, daily_data, leave_data, query_date)
|
|
359
|
+
print(report)
|
|
360
|
+
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
try:
|
|
366
|
+
raise SystemExit(main())
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
print("⚠️ {}".format(exc), file=sys.stderr)
|
|
369
|
+
raise SystemExit(1)
|