#!/usr/bin/env python3 """ 周报发送脚本(支持 G5/G6) 功能: 1. 构造多段邮件(纯文本 + HTML),存入钉邮草稿箱 2. 自动归档到 weekly-reports/YYYY/YYYY-Www.md 用法: python3 send_weekly_report.py --project G5 --plain "..." --html "..." --date-range "2026-07-27 ~ 2026-07-31" --archive """ import email from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header import imaplib import os import time import sys import re import argparse from datetime import datetime from pathlib import Path def load_env(path): """读取 .env 文件,返回键值字典(标准库实现,无额外依赖)。""" env = {} try: with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#') or '=' not in line: continue key, _, value = line.partition('=') env[key.strip()] = value.strip().strip('"').strip("'") except FileNotFoundError: pass return env _ENV = load_env(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env')) IMAP_HOST = 'imap.mxhichina.com' IMAP_PORT = 993 IMAP_USER = _ENV.get('WITSOFT_IMAP_USER', 'yangxuan@witsoft.cn') IMAP_PASSWORD = _ENV.get('WITSOFT_IMAP_PASSWORD', '') # MySQL 同步配置(简历 resume 库,用于月度/年度总结) MYSQL_HOST = '127.0.0.1' MYSQL_PORT = 3306 MYSQL_USER = _ENV.get('MYSQL_USER', 'root') MYSQL_PASSWORD = _ENV.get('MYSQL_PWD', '') MYSQL_DB = 'resume' MYSQL_ENABLED = bool(MYSQL_PASSWORD) def archive_report(plain_body: str, date_range: str, project: str = "G5"): """将周报归档到 weekly-reports/YYYY/YYYY-Www.md Args: plain_body: 纯文本正文 date_range: 日期范围字符串,如 "2026-07-27 ~ 2026-07-31" project: 项目名称 Returns: 归档文件路径 """ # 解析日期范围 match = re.match(r'(\d{4}-\d{2}-\d{2})\s*~\s*(\d{4}-\d{2}-\d{2})', date_range) if not match: print(f"⚠️ 日期格式错误,跳过归档:{date_range}") return None start_date = datetime.strptime(match.group(1), '%Y-%m-%d') end_date = datetime.strptime(match.group(2), '%Y-%m-%d') # 计算周数 week_number = start_date.isocalendar()[1] year = start_date.year # 创建年份目录 reports_dir = Path(__file__).parent / 'weekly-reports' / str(year) reports_dir.mkdir(parents=True, exist_ok=True) # 生成文件名 filename = f"{year}-W{week_number:02d}-周报.md" filepath = reports_dir / filename # 生成归档内容 archive_content = f"""# {project}开发周报 ({year}-W{week_number:02d}) **日期范围:** {date_range} ## 基本信息 {plain_body.split('本周工作内容')[0].strip()} ## 本周工作内容 {plain_body.split('本周工作内容')[1].split('存在问题')[0].strip()} ## 存在问题 {plain_body.split('存在问题')[1].strip() if '存在问题' in plain_body else '无'} --- *归档时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}* *原始邮件主题:{project}开发周报 ({date_range})* """ # 写入文件 with open(filepath, 'w', encoding='utf-8') as f: f.write(archive_content) print(f"✅ 周报已归档:{filepath}") return str(filepath) def _parse_plain_for_db(plain_body: str): """从纯文本正文解析出项目、主要任务、每日明细、问题,供 MySQL 写入。 返回 dict:{project, main_task, days:[(date, weekday, items)], problems:[]} """ project = "G5" main_task = "" days = [] problems = [] cur = None in_prob = False weekday_map = {"周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7} for raw in plain_body.splitlines(): line = raw.strip() if not line: continue if line.startswith("项目名称"): pm = re.sub(r"^[\U0001F4C5]?\s*项目名称[::]?\s*", "", line).strip() if "G6" in pm: project = "G6" elif "G5" in pm: project = "G5" continue if line.startswith("主要任务"): main_task = re.sub(r"^[\U0001F4C5]?\s*主要任务[::]?\s*", "", line).strip() continue if "存在问题" in line: in_prob = True cur = None continue if re.match(r"^(下周计划|下周计划:|备注[::]|说明[::])", line): in_prob = False cur = None continue low = line.lower() if any(s in low for s in ("best regards", "杨轩", "地址", "手机", "邮箱", "yangxuan@")): break dm = re.match(r"^[\U0001F4C5\U0001F539\u2022]?\s*(周一|周二|周三|周四|周五|周六|周日)[((]\s*(\d{4})-(\d{2})-(\d{2})\s*[))]", line) if not dm: dm = re.match(r"^[\U0001F4C5\U0001F539\u2022]?\s*(周一|周二|周三|周四|周五|周六|周日)[((]\s*(\d{2})-(\d{2})\s*[))]", line) if dm: wd = dm.group(1) if len(dm.groups()) == 4: y, mm, dd = dm.group(2), dm.group(3), dm.group(4) else: y, mm, dd = "2026", dm.group(2), dm.group(3) cur = {"date": f"{y}-{mm}-{dd}", "weekday": weekday_map.get(wd, 0), "items": []} days.append(cur) in_prob = False continue if in_prob: if line not in ("无", "暂无", "None", ""): problems.append(re.sub(r"^[-*\u00B7\u25CF\u25C6]\s*", "", line).strip()) elif cur: item = re.sub(r"^[-*\u00B7\u25CF\u25C6]\s*", "", line).strip() if item: cur["items"].append(item) return {"project": project, "main_task": main_task, "days": days, "problems": problems} def sync_to_mysql(plain_body: str, date_range: str, project: str = "G5"): """将周报数据同步到 MySQL resume 库(用于月度/年度总结、OKR 分析)。 无密码或连接失败时静默跳过,不影响发信与本地归档。 """ if not MYSQL_ENABLED: print("⚠️ MYSQL_PWD 未配置,跳过 MySQL 同步") return try: import pymysql except ImportError: print("⚠️ 未安装 pymysql,跳过 MySQL 同步(pip install pymysql)") return data = _parse_plain_for_db(plain_body) real_proj = data["project"] or project m = re.match(r"(\d{4}-\d{2}-\d{2})\s*~\s*(\d{4}-\d{2}-\d{2})", date_range) if not m: print("⚠️ 日期格式错误,跳过 MySQL 同步") return start_s, end_s = m.group(1), m.group(2) start_d = datetime.strptime(start_s, "%Y-%m-%d") week_number = start_d.isocalendar()[1] year = start_d.year try: conn = pymysql.connect(host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASSWORD, database=MYSQL_DB, charset="utf8mb4") cur = conn.cursor() # 清同键旧数据(含级联) cur.execute("SELECT id FROM weekly_reports WHERE year=%s AND week_number=%s AND project=%s", (year, week_number, real_proj)) rows = cur.fetchall() for (rid,) in rows: cur.execute("DELETE FROM weekly_report_daily WHERE report_id=%s", (rid,)) cur.execute("DELETE FROM weekly_report_problems WHERE report_id=%s", (rid,)) cur.execute("DELETE FROM weekly_reports WHERE id=%s", (rid,)) # 插入主表 cur.execute( "INSERT INTO weekly_reports (year, week_number, start_date, end_date, project, main_task, has_problems) " "VALUES (%s,%s,%s,%s,%s,%s,%s)", (year, week_number, start_s, end_s, real_proj, data["main_task"], 1 if data["problems"] else 0)) new_id = cur.lastrowid for d in data["days"]: cur.execute( "INSERT INTO weekly_report_daily (report_id, day_of_week, work_date, work_items) VALUES (%s,%s,%s,%s)", (new_id, d["weekday"], d["date"], "\n".join(d["items"]))) for p in data["problems"]: cur.execute( "INSERT INTO weekly_report_problems (report_id, problem_description) VALUES (%s,%s)", (new_id, p)) conn.commit() cur.close() conn.close() print(f"✅ 周报已同步 MySQL resume 库:{real_proj} W{week_number:02d}({data['main_task'][:20] or '无任务'})") except Exception as e: print(f"⚠️ MySQL 同步失败(不影响发信):{e}") def send_to_drafts(plain_body: str, html_body: str, date_range: str, project: str = "G5", archive: bool = True, sync_mysql: bool = True): """构造多段邮件并存入钉邮草稿箱,同时可选归档到本地 Args: plain_body: 纯文本正文 html_body: HTML 正文 date_range: 日期范围字符串 project: 项目名称,默认"G5" archive: 是否同时归档到本地,默认 True sync_mysql: 是否同步到 MySQL resume 库,默认 True Returns: IMAP append 结果 """ # 归档到本地 if archive: archive_report(plain_body, date_range, project) # 同步 MySQL(月度/年度总结数据源) if sync_mysql: sync_to_mysql(plain_body, date_range, project) msg = MIMEMultipart('related') msg['From'] = Header('杨轩', 'utf-8').encode() + ' ' msg['To'] = Header('刘强', 'utf-8').encode() + ' ' msg['Cc'] = ( Header('陈明', 'utf-8').encode() + ' , ' + Header('曾莉', 'utf-8').encode() + ' , ' + Header('袁晴', 'utf-8').encode() + ' ' ) msg['Subject'] = Header(f'{project}开发周报 ({date_range})', 'utf-8').encode() alt = MIMEMultipart('alternative') alt.attach(MIMEText(plain_body, 'plain', 'utf-8')) alt.attach(MIMEText(html_body, 'html', 'utf-8')) msg.attach(alt) raw_bytes = msg.as_bytes() conn = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) conn.login(IMAP_USER, IMAP_PASSWORD) drafts = '"&g0l6Pw-"' result = conn.append(drafts, None, imaplib.Time2Internaldate(time.time()), raw_bytes) conn.logout() return result def main(): parser = argparse.ArgumentParser(description='周报发送脚本(支持 G5/G6)') parser.add_argument('--project', type=str, default='G5', help='项目名称,默认 G5') parser.add_argument('--plain', type=str, required=True, help='纯文本正文') parser.add_argument('--html', type=str, required=True, help='HTML 正文') parser.add_argument('--date-range', type=str, required=True, help='日期范围,如 "2026-07-27 ~ 2026-07-31"') parser.add_argument('--no-archive', action='store_true', help='不归档到本地') parser.add_argument('--no-mysql', action='store_true', help='不同步到 MySQL resume 库') args = parser.parse_args() result = send_to_drafts( plain_body=args.plain, html_body=args.html, date_range=args.date_range, project=args.project, archive=not args.no_archive, sync_mysql=not args.no_mysql ) print(f"邮件存入草稿箱结果:{result}") if __name__ == '__main__': # 命令行模式 if len(sys.argv) > 1: main() else: # 测试模式 plain = """项目名称: 维云智造 G5 主要任务: 测试 本周工作内容 周一(2026-07-27) - 测试内容 1 - 测试内容 2 存在问题 无""" html = "

test

" result = send_to_drafts(plain, html, "2026-07-27 ~ 2026-07-31", "G5") print(result)