Files
openclaw-config/workspace-resume/send_weekly_report.py
T

322 lines
12 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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() + ' <yangxuan@witsoft.cn>'
msg['To'] = Header('刘强', 'utf-8').encode() + ' <liuqiang@witsoft.cn>'
msg['Cc'] = (
Header('陈明', 'utf-8').encode() + ' <chenm@witsoft.cn>, '
+ Header('曾莉', 'utf-8').encode() + ' <zengli@witsoft.cn>, '
+ Header('袁晴', 'utf-8').encode() + ' <yuanq@witsoft.cn>'
)
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 = "<p>test</p>"
result = send_to_drafts(plain, html, "2026-07-27 ~ 2026-07-31", "G5")
print(result)