auto: sync OpenClaw config 2026-08-27 11:17
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
G6 周报发送脚本
|
||||
用法:构造多段邮件(纯文本 + HTML),存入钉邮草稿箱
|
||||
周报发送脚本(支持 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
|
||||
@@ -11,6 +16,10 @@ import imaplib
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_env(path):
|
||||
@@ -36,15 +45,214 @@ IMAP_PORT = 993
|
||||
IMAP_USER = _ENV.get('WITSOFT_IMAP_USER', 'yangxuan@witsoft.cn')
|
||||
IMAP_PASSWORD = _ENV.get('WITSOFT_IMAP_PASSWORD', '')
|
||||
|
||||
def send_to_drafts(plain_body: str, html_body: str, date_range: str, project: str = "G6"):
|
||||
"""构造多段邮件并存入钉邮草稿箱
|
||||
# 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: 项目名称,默认"G6",用于邮件主题
|
||||
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>'
|
||||
@@ -70,17 +278,44 @@ def send_to_drafts(plain_body: str, html_body: str, date_range: str, project: st
|
||||
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__':
|
||||
# 测试
|
||||
plain = """项目名称: 维云智造 G6
|
||||
# 命令行模式
|
||||
if len(sys.argv) > 1:
|
||||
main()
|
||||
else:
|
||||
# 测试模式
|
||||
plain = """项目名称: 维云智造 G5
|
||||
主要任务: 测试
|
||||
|
||||
本周工作内容
|
||||
周一(2026-07-13)
|
||||
- 测试
|
||||
周一(2026-07-27)
|
||||
- 测试内容 1
|
||||
- 测试内容 2
|
||||
|
||||
存在问题
|
||||
无"""
|
||||
html = "<p>test</p>"
|
||||
result = send_to_drafts(plain, html, "2026-07-13 ~ 2026-07-17", "G6")
|
||||
print(result)
|
||||
html = "<p>test</p>"
|
||||
result = send_to_drafts(plain, html, "2026-07-27 ~ 2026-07-31", "G5")
|
||||
print(result)
|
||||
|
||||
Reference in New Issue
Block a user