diff --git a/README.md b/README.md index 1ab3f57..2d0e208 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,9 @@ skills/my-skill/ ## 技能列表 -| 技能名 | 描述 | -|--------|------| -| — | — | +| 技能名 | 调用方式 | 描述 | +|--------|----------|------| +| [weekly-report](./skills/weekly-report/) | `/weekly-report` | 统计过去7天的 commit 和 PR,生成协作者个人周报与项目整体报告,通过 SMTP 发送邮件 | ## 更新技能 diff --git a/skills/.gitkeep b/skills/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/skills/weekly-report/SKILL.md b/skills/weekly-report/SKILL.md new file mode 100644 index 0000000..9f3d2e2 --- /dev/null +++ b/skills/weekly-report/SKILL.md @@ -0,0 +1,131 @@ +--- +name: weekly-report +description: 生成项目过去7天的周报,统计每位协作者的 commit 和 PR,生成个人工作日志与项目整体报告,通过 SMTP 发送到配置邮箱 +argument-hint: "" +--- + +生成当前项目过去 7 天的周报并发送邮件。按以下步骤执行,每步完成后告知用户进度。 + +## 步骤 1:读取配置 + +读取当前项目根目录的 `report-config.json`,格式如下: + +```json +{ + "recipients": ["boss@company.com", "team@company.com"], + "project_name": "项目名称" +} +``` + +如果文件不存在,**停止执行**,提示用户在项目根目录创建该文件后重试。 + +## 步骤 2:计算日期范围 + +用 Python 计算日期,避免平台差异: + +```bash +python -c "from datetime import date, timedelta; d=date.today()-timedelta(days=7); print(d)" +``` + +## 步骤 3:拉取最新代码 + +```bash +git pull origin main +``` + +如果当前不在 git 仓库内,停止并提示用户。 + +## 步骤 4:获取过去 7 天的提交记录 + +```bash +git log origin/main --since="7 days ago" --format="%an|||%ae|||%ad|||%s" --date=short --no-merges +``` + +将输出按 `|||` 分割,整理为结构化数据(作者姓名、邮箱、日期、提交说明)。 + +## 步骤 5:获取过去 7 天合并的 PR + +```bash +gh pr list --state merged --base main --limit 100 --json number,title,author,mergedAt,additions,deletions +``` + +过滤 `mergedAt` 在过去 7 天内的 PR,按作者分组。 + +如果 `gh` 命令不可用,跳过此步并在报告中注明。 + +## 步骤 6:生成报告内容 + +### 个人周报(每位协作者一份) + +格式要求: +- 姓名与提交统计(commit 数、PR 数、增删行数) +- 合并 PR 列表(编号、标题) +- 关键 commit 摘要(按日期列出,去除 merge commit) +- 工作总结(2-3 句话,用中文概括本周主要工作) + +### 项目整体进度报告 + +格式要求: +- 统计摘要:参与人数、总 commit 数、合并 PR 总数 +- 主要完成内容(按功能或模块归类,中文) +- 整体进度评估(根据提交内容客观描述) +- 如有未合并的 open PR,列出数量作为下周关注点 + +### 拼装 HTML 报告 + +将个人周报和整体报告拼装成一份 HTML 邮件,写入临时文件: + +```bash +python -c " +from datetime import date +filename = f'/tmp/weekly_report_{date.today().strftime(\"%Y%m%d\")}.html' +print(filename) +" +``` + +HTML 结构建议: +1. 标题:`【周报】 - <日期范围>` +2. 项目整体报告(放在最前) +3. 分割线 +4. 各协作者个人报告(每人一节) + +## 步骤 7:检查 SMTP 环境变量 + +在发送前,检查以下环境变量是否全部存在: + +```bash +python -c " +import os, sys +required = ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASSWORD'] +missing = [k for k in required if not os.environ.get(k)] +if missing: + print('缺少环境变量:' + ', '.join(missing)) + sys.exit(1) +else: + print('环境变量检查通过') +" +``` + +如果有缺失,**停止执行**,提示用户配置对应环境变量: + +| 变量 | 说明 | 示例 | +|------|------|------| +| `SMTP_HOST` | SMTP 服务器地址 | `smtp.qq.com` | +| `SMTP_PORT` | 端口(SSL用465,STARTTLS用587) | `465` | +| `SMTP_USER` | 发件人邮箱 | `noreply@company.com` | +| `SMTP_PASSWORD` | SMTP 密码或授权码 | `your-auth-code` | + +**绝对不要将这些值硬编码在任何文件中。** + +## 步骤 8:发送邮件 + +使用 Glob 工具找到与本 SKILL.md 同目录的 `send_email.py` 的绝对路径,然后执行: + +```bash +python \ + --config report-config.json \ + --subject "【周报】 " \ + --body-file <步骤6生成的HTML文件路径> +``` + +发送成功后,告知用户已发送至哪些邮箱,并清理临时文件。 diff --git a/skills/weekly-report/send_email.py b/skills/weekly-report/send_email.py new file mode 100644 index 0000000..8f35c62 --- /dev/null +++ b/skills/weekly-report/send_email.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +周报邮件发送脚本 +SMTP 凭证从环境变量读取,不接受命令行参数传入凭证。 + +必须设置的环境变量: + SMTP_HOST - SMTP 服务器地址 + SMTP_PORT - SMTP 端口(SSL用465,STARTTLS用587) + SMTP_USER - 发件人邮箱 + SMTP_PASSWORD - SMTP 密码或授权码 +""" + +import argparse +import json +import os +import smtplib +import sys +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + + +def load_config(config_path: str) -> dict: + try: + with open(config_path, "r", encoding="utf-8") as f: + return json.load(f) + except FileNotFoundError: + print(f"错误:找不到配置文件 {config_path}", file=sys.stderr) + print("请在项目根目录创建 report-config.json,格式:", file=sys.stderr) + print('{"recipients": ["email@example.com"], "project_name": "项目名"}', file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError as e: + print(f"错误:report-config.json 格式错误 - {e}", file=sys.stderr) + sys.exit(1) + + +def get_smtp_config() -> tuple: + host = os.environ.get("SMTP_HOST") + port = os.environ.get("SMTP_PORT", "465") + user = os.environ.get("SMTP_USER") + password = os.environ.get("SMTP_PASSWORD") + + missing = [k for k, v in { + "SMTP_HOST": host, + "SMTP_USER": user, + "SMTP_PASSWORD": password, + }.items() if not v] + + if missing: + print(f"错误:缺少以下环境变量:{', '.join(missing)}", file=sys.stderr) + print("请在 shell 中设置(不要写入任何文件):", file=sys.stderr) + for k in missing: + print(f" export {k}=your_value", file=sys.stderr) + sys.exit(1) + + try: + port = int(port) + except ValueError: + print(f"错误:SMTP_PORT 必须是数字,当前值为 '{port}'", file=sys.stderr) + sys.exit(1) + + return host, port, user, password + + +def send_email(recipients: list, subject: str, body_html: str, sender: str): + host, port, user, password = get_smtp_config() + + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = sender or user + msg["To"] = ", ".join(recipients) + msg.attach(MIMEText(body_html, "html", "utf-8")) + + try: + if port == 465: + with smtplib.SMTP_SSL(host, port, timeout=30) as smtp: + smtp.login(user, password) + smtp.sendmail(user, recipients, msg.as_string()) + else: + with smtplib.SMTP(host, port, timeout=30) as smtp: + smtp.ehlo() + smtp.starttls() + smtp.login(user, password) + smtp.sendmail(user, recipients, msg.as_string()) + except smtplib.SMTPAuthenticationError: + print("错误:SMTP 认证失败,请检查 SMTP_USER 和 SMTP_PASSWORD", file=sys.stderr) + sys.exit(1) + except smtplib.SMTPConnectError: + print(f"错误:无法连接到 {host}:{port},请检查 SMTP_HOST 和 SMTP_PORT", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"错误:发送失败 - {e}", file=sys.stderr) + sys.exit(1) + + print(f"邮件已发送至:{', '.join(recipients)}") + + +def main(): + parser = argparse.ArgumentParser(description="发送周报邮件") + parser.add_argument("--config", required=True, help="report-config.json 路径") + parser.add_argument("--subject", required=True, help="邮件主题") + parser.add_argument("--body-file", required=True, help="HTML 正文文件路径") + args = parser.parse_args() + + config = load_config(args.config) + recipients = config.get("recipients", []) + if not recipients: + print("错误:report-config.json 中 recipients 为空", file=sys.stderr) + sys.exit(1) + + try: + with open(args.body_file, "r", encoding="utf-8") as f: + body_html = f.read() + except FileNotFoundError: + print(f"错误:找不到邮件正文文件 {args.body_file}", file=sys.stderr) + sys.exit(1) + + send_email(recipients, args.subject, body_html, sender=config.get("sender", "")) + + +if __name__ == "__main__": + main()