auto: sync OpenClaw config 2026-08-27 11:17
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
周报数据库同步工具(SQLite 版本)
|
||||
将 Markdown 周报的元数据同步到 SQLite 数据库
|
||||
|
||||
用法:
|
||||
python3 sync_reports_db.py --all # 同步所有周报
|
||||
python3 sync_reports_db.py --file xxx.md # 同步单个文件
|
||||
python3 sync_reports_db.py --week 2026-W35 # 同步指定周
|
||||
python3 sync_reports_db.py --stats # 查看统计信息
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
REPORTS_DIR = Path(__file__).parent
|
||||
DB_PATH = REPORTS_DIR / 'weekly_reports.db'
|
||||
|
||||
|
||||
def init_database():
|
||||
"""初始化 SQLite 数据库"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建周报元数据表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS weekly_reports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
year INTEGER NOT NULL,
|
||||
week_number INTEGER NOT NULL,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
main_task TEXT,
|
||||
file_path TEXT NOT NULL UNIQUE,
|
||||
has_problems INTEGER DEFAULT 0,
|
||||
word_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建每日工作明细表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS weekly_report_daily (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
report_id INTEGER NOT NULL,
|
||||
day_of_week INTEGER NOT NULL,
|
||||
work_date TEXT NOT NULL,
|
||||
work_items TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (report_id) REFERENCES weekly_reports(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建问题记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS weekly_report_problems (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
report_id INTEGER NOT NULL,
|
||||
problem_description TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'open',
|
||||
resolved_at TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (report_id) REFERENCES weekly_reports(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_date ON weekly_reports(start_date, end_date)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_project ON weekly_reports(project)')
|
||||
cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS uk_year_week ON weekly_reports(year, week_number, project)')
|
||||
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
def parse_markdown_report(filepath):
|
||||
"""解析 Markdown 周报文件,提取元数据"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
data = {
|
||||
'file_path': str(filepath),
|
||||
'year': None,
|
||||
'week_number': None,
|
||||
'start_date': None,
|
||||
'end_date': None,
|
||||
'project': None,
|
||||
'main_task': None,
|
||||
'daily_work': {},
|
||||
'problems': [],
|
||||
'word_count': len(content)
|
||||
}
|
||||
|
||||
# 提取年份和周数(从文件名)
|
||||
filename = filepath.name
|
||||
match = re.match(r'(\d{4})-W(\d{2})', filename)
|
||||
if match:
|
||||
data['year'] = int(match.group(1))
|
||||
data['week_number'] = int(match.group(2))
|
||||
|
||||
# 提取日期范围(多种格式兼容)
|
||||
# 格式 1: **日期范围:** 2026-08-25 ~ 2026-08-29
|
||||
date_match = re.search(r'\*\*日期范围:\*\*\s*(\d{4}-\d{2}-\d{2})\s*~\s*(\d{4}-\d{2}-\d{2})', content)
|
||||
if date_match:
|
||||
data['start_date'] = date_match.group(1)
|
||||
data['end_date'] = date_match.group(2)
|
||||
else:
|
||||
# 格式 2: 2026-07-27 ~ 2026-07-31(在文件末尾)
|
||||
date_match2 = re.search(r'(\d{4}-\d{2}-\d{2})\s*~\s*(\d{4}-\d{2}-\d{2})', content)
|
||||
if date_match2:
|
||||
data['start_date'] = date_match2.group(1)
|
||||
data['end_date'] = date_match2.group(2)
|
||||
else:
|
||||
# 格式 3: 从文件名推算(2026-W31)
|
||||
if data['year'] and data['week_number']:
|
||||
# 计算该周的周一和周五
|
||||
jan_first = datetime(data['year'], 1, 1)
|
||||
week_start = jan_first + timedelta(weeks=data['week_number'] - 1)
|
||||
week_start = week_start - timedelta(days=week_start.weekday())
|
||||
week_end = week_start + timedelta(days=4)
|
||||
data['start_date'] = week_start.strftime('%Y-%m-%d')
|
||||
data['end_date'] = week_end.strftime('%Y-%m-%d')
|
||||
|
||||
# 提取项目名称(多种格式兼容)
|
||||
# 格式 1: 项目名称:维云智造 G5
|
||||
project_match = re.search(r'项目名称:\s*(维云智造\s*(G\d+)|G\d+)', content)
|
||||
if project_match:
|
||||
project_text = project_match.group(1)
|
||||
g_match = re.search(r'G(\d+)', project_text)
|
||||
if g_match:
|
||||
data['project'] = f"G{g_match.group(1)}"
|
||||
else:
|
||||
data['project'] = 'G5'
|
||||
else:
|
||||
# 格式 2: **项目:** 维云智造 G6
|
||||
project_match2 = re.search(r'\*\*项目:\*\*\s*(维云智造\s*(G\d+)|G\d+)', content)
|
||||
if project_match2:
|
||||
project_text = project_match2.group(1)
|
||||
g_match = re.search(r'G(\d+)', project_text)
|
||||
if g_match:
|
||||
data['project'] = f"G{g_match.group(1)}"
|
||||
else:
|
||||
data['project'] = 'G5'
|
||||
else:
|
||||
# 默认 G5
|
||||
data['project'] = 'G5'
|
||||
|
||||
# 提取主要任务
|
||||
task_match = re.search(r'主要任务:\s*(.+?)(?:\n|$)', content)
|
||||
if task_match:
|
||||
data['main_task'] = task_match.group(1).strip()
|
||||
|
||||
# 提取每日工作
|
||||
daily_section = re.search(r'## 本周工作内容\s*\n([\s\S]*?)(?:\n##|\Z)', content)
|
||||
if daily_section:
|
||||
daily_text = daily_section.group(1)
|
||||
days = re.findall(r'###?\s*(?:周 [一二三四五六日]|(\d{4}-\d{2}-\d{2}))\s*\n([\s\S]*?)(?=###?|$)', daily_text)
|
||||
for date_str, work_text in days:
|
||||
if date_str:
|
||||
items = re.findall(r'^\s*[-•*]\s*(.+)$', work_text, re.MULTILINE)
|
||||
data['daily_work'][date_str] = items
|
||||
|
||||
# 提取存在问题
|
||||
problem_section = re.search(r'## 存在问题\s*\n([\s\S]*?)(?:\n##|\Z)', content)
|
||||
if problem_section:
|
||||
problem_text = problem_section.group(1).strip()
|
||||
if problem_text and problem_text != '无':
|
||||
data['problems'] = [line.strip() for line in problem_text.split('\n') if line.strip()]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sync_report(conn, report_data):
|
||||
"""同步单个周报到数据库"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 插入或更新周报元数据
|
||||
cursor.execute('''
|
||||
INSERT INTO weekly_reports
|
||||
(year, week_number, start_date, end_date, project, main_task, file_path, has_problems, word_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(year, week_number, project) DO UPDATE SET
|
||||
main_task = excluded.main_task,
|
||||
file_path = excluded.file_path,
|
||||
has_problems = excluded.has_problems,
|
||||
word_count = excluded.word_count,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (
|
||||
report_data['year'],
|
||||
report_data['week_number'],
|
||||
report_data['start_date'],
|
||||
report_data['end_date'],
|
||||
report_data['project'],
|
||||
report_data['main_task'],
|
||||
report_data['file_path'],
|
||||
1 if report_data['problems'] else 0,
|
||||
report_data['word_count']
|
||||
))
|
||||
|
||||
report_id = cursor.lastrowid
|
||||
if report_id is None:
|
||||
# 更新操作,需要获取现有 ID
|
||||
cursor.execute('SELECT id FROM weekly_reports WHERE file_path = ?', (report_data['file_path'],))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
report_id = row[0]
|
||||
|
||||
# 删除旧的每日工作记录
|
||||
cursor.execute('DELETE FROM weekly_report_daily WHERE report_id = ?', (report_id,))
|
||||
|
||||
# 插入新的每日工作记录
|
||||
for work_date, items in report_data['daily_work'].items():
|
||||
day_of_week = datetime.strptime(work_date, '%Y-%m-%d').weekday() + 1
|
||||
items_json = json.dumps(items, ensure_ascii=False)
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO weekly_report_daily
|
||||
(report_id, day_of_week, work_date, work_items)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (report_id, day_of_week, work_date, items_json))
|
||||
|
||||
# 删除旧的问题记录
|
||||
cursor.execute('DELETE FROM weekly_report_problems WHERE report_id = ?', (report_id,))
|
||||
|
||||
# 插入新的问题记录
|
||||
for problem in report_data['problems']:
|
||||
cursor.execute('''
|
||||
INSERT INTO weekly_report_problems
|
||||
(report_id, problem_description)
|
||||
VALUES (?, ?)
|
||||
''', (report_id, problem))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ 数据库错误:{e}")
|
||||
conn.rollback()
|
||||
return False
|
||||
|
||||
|
||||
def show_stats(conn):
|
||||
"""显示统计信息"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\n📊 周报统计信息\n")
|
||||
print("=" * 60)
|
||||
|
||||
# 总览
|
||||
cursor.execute('SELECT COUNT(*) FROM weekly_reports')
|
||||
total = cursor.fetchone()[0]
|
||||
print(f"📁 周报总数:{total} 周")
|
||||
|
||||
# 按项目统计
|
||||
cursor.execute('''
|
||||
SELECT project, COUNT(*) as count,
|
||||
SUM(has_problems) as problem_weeks,
|
||||
AVG(word_count) as avg_words
|
||||
FROM weekly_reports
|
||||
GROUP BY project
|
||||
''')
|
||||
print("\n📋 按项目统计:")
|
||||
for row in cursor.fetchall():
|
||||
print(f" {row[0]}: {row[1]} 周,问题 {row[2]} 周,平均字数 {int(row[3])}")
|
||||
|
||||
# 最近 5 周
|
||||
cursor.execute('''
|
||||
SELECT year, week_number, start_date, end_date, project, main_task, has_problems
|
||||
FROM weekly_reports
|
||||
ORDER BY year DESC, week_number DESC
|
||||
LIMIT 5
|
||||
''')
|
||||
print("\n📅 最近 5 周:")
|
||||
for row in cursor.fetchall():
|
||||
problem_flag = "⚠️" if row[6] else "✅"
|
||||
print(f" {problem_flag} {row[0]}-W{row[1]:02d} ({row[2]}~{row[3]}) {row[4]} - {row[5][:30]}")
|
||||
|
||||
# 存在问题统计
|
||||
cursor.execute('SELECT COUNT(*) FROM weekly_reports WHERE has_problems = 1')
|
||||
problem_count = cursor.fetchone()[0]
|
||||
print(f"\n⚠️ 存在问题周报:{problem_count} 周")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='周报数据库同步工具(SQLite)')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('--all', action='store_true', help='同步所有周报')
|
||||
group.add_argument('--file', type=str, help='同步单个文件')
|
||||
group.add_argument('--week', type=str, help='同步指定周,如 2026-W35')
|
||||
group.add_argument('--stats', action='store_true', help='查看统计信息')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化数据库
|
||||
conn = init_database()
|
||||
print(f"✅ 数据库已初始化:{DB_PATH}")
|
||||
|
||||
if args.stats:
|
||||
show_stats(conn)
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
files_to_sync = []
|
||||
|
||||
if args.file:
|
||||
files_to_sync = [Path(args.file)]
|
||||
elif args.week:
|
||||
match = re.match(r'(\d{4})-W(\d{2})', args.week)
|
||||
if match:
|
||||
year, week = match.groups()
|
||||
pattern = f"{year}-W{week}-*.md"
|
||||
files_to_sync = list(REPORTS_DIR.glob(f"{year}/{pattern}"))
|
||||
elif args.all:
|
||||
for year_dir in REPORTS_DIR.iterdir():
|
||||
if year_dir.is_dir() and year_dir.name.isdigit():
|
||||
files_to_sync.extend(year_dir.glob("*.md"))
|
||||
|
||||
if not files_to_sync:
|
||||
print("⚠️ 未找到需要同步的文件")
|
||||
conn.close()
|
||||
return 1
|
||||
|
||||
print(f"📂 找到 {len(files_to_sync)} 个文件待同步\n")
|
||||
|
||||
success_count = 0
|
||||
for filepath in sorted(files_to_sync):
|
||||
if not filepath.exists():
|
||||
print(f"⚠️ 文件不存在:{filepath}")
|
||||
continue
|
||||
|
||||
print(f"📄 处理:{filepath.name}")
|
||||
report_data = parse_markdown_report(filepath)
|
||||
|
||||
if not report_data['year'] or not report_data['week_number']:
|
||||
print(f" ⚠️ 无法解析年份/周数,跳过")
|
||||
continue
|
||||
|
||||
if sync_report(conn, report_data):
|
||||
print(f" ✅ 同步成功")
|
||||
success_count += 1
|
||||
else:
|
||||
print(f" ❌ 同步失败")
|
||||
|
||||
print(f"\n{'✅' if success_count == len(files_to_sync) else '⚠️'} 完成:{success_count}/{len(files_to_sync)} 个文件")
|
||||
|
||||
conn.close()
|
||||
return 0 if success_count == len(files_to_sync) else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user