first commit
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
从 JSON 文件批量创建待办(含优先级、截止时间、执行者)
|
||||
|
||||
用法:
|
||||
python todo_batch_create.py todos.json
|
||||
python todo_batch_create.py todos.json --dry-run
|
||||
|
||||
todos.json 格式:
|
||||
[
|
||||
{"title": "修复线上Bug", "executors": "userId1,userId2", "priority": 40},
|
||||
{"title": "写周报", "executors": "userId1", "due": "2026-03-15"},
|
||||
{"title": "代码评审", "executors": "userId1"},
|
||||
{"title": "每日站会", "executors": "userId1", "due": "2026-03-20",
|
||||
"recurrence": "DTSTART:20260320T020000Z\\nRRULE:FREQ=DAILY;INTERVAL=1"}
|
||||
]
|
||||
|
||||
字段说明:
|
||||
- title: 待办标题 (必填)
|
||||
- executors: 执行者 userId,多人逗号分隔 (必填)
|
||||
- priority: 优先级 10=低/20=普通/30=较高/40=紧急 (可选)
|
||||
- due: 截止日期 YYYY-MM-DD 或毫秒时间戳 (可选)
|
||||
- recurrence: 循环待办规则 (可选,需同时有 due);字符串内需含换行,与 dws --recurrence 一致
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
ALLOWED_PRIORITIES = {10, 20, 30, 40}
|
||||
DATE_PATTERN = re.compile(r'^\d{4}-\d{2}-\d{2}$')
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return {'dry_run': True}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ 错误:{result.stderr.strip()}")
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(' ✗ 命令执行超时', file=sys.stderr)
|
||||
return None
|
||||
except (json.JSONDecodeError, FileNotFoundError) as e:
|
||||
print(f" ✗ 错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def parse_due(due_value) -> Optional[str]:
|
||||
if not due_value:
|
||||
return None
|
||||
due_str = str(due_value)
|
||||
if due_str.isdigit() and len(due_str) >= 10:
|
||||
return due_str
|
||||
if DATE_PATTERN.match(due_str):
|
||||
dt = datetime.strptime(due_str, '%Y-%m-%d')
|
||||
dt = dt.replace(hour=23, minute=59, second=59)
|
||||
return str(int(dt.timestamp() * 1000))
|
||||
print(f" ⚠ 无法解析截止时间:{due_value},跳过")
|
||||
return None
|
||||
|
||||
|
||||
def validate_todo(item: Dict[str, Any], idx: int) -> bool:
|
||||
if not isinstance(item, dict):
|
||||
print(f" ✗ #{idx+1} 不是有效对象")
|
||||
return False
|
||||
if not item.get('title', '').strip():
|
||||
print(f" ✗ #{idx+1} 缺少 title")
|
||||
return False
|
||||
if not item.get('executors', '').strip():
|
||||
print(f" ✗ #{idx+1} 缺少 executors")
|
||||
return False
|
||||
priority = item.get('priority')
|
||||
if priority is not None and int(priority) not in ALLOWED_PRIORITIES:
|
||||
print(f" ✗ #{idx+1} 无效优先级:{priority}")
|
||||
return False
|
||||
recurrence = item.get('recurrence')
|
||||
if recurrence and not str(recurrence).strip():
|
||||
print(f" ✗ #{idx+1} recurrence 不能为空字符串")
|
||||
return False
|
||||
if recurrence and not item.get('due'):
|
||||
print(f" ✗ #{idx+1} 设置 recurrence 时必须提供 due")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = '--dry-run' in sys.argv
|
||||
args = [a for a in sys.argv[1:] if a != '--dry-run']
|
||||
if not args:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
file_path = Path(args[0])
|
||||
if not file_path.exists():
|
||||
print(f"错误:文件不存在:{file_path}")
|
||||
sys.exit(1)
|
||||
if file_path.stat().st_size > MAX_FILE_SIZE:
|
||||
print(f"错误:文件过大 (限制 {MAX_FILE_SIZE // 1024}KB)")
|
||||
sys.exit(1)
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
todos = json.load(f)
|
||||
if not isinstance(todos, list) or not todos:
|
||||
print('错误:JSON 文件必须是非空数组')
|
||||
sys.exit(1)
|
||||
|
||||
for i, item in enumerate(todos):
|
||||
if not validate_todo(item, i):
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📋 准备创建 {len(todos)} 条待办\n")
|
||||
success, fail = 0, 0
|
||||
for i, item in enumerate(todos):
|
||||
title = item['title'].strip()
|
||||
cmd_args = [
|
||||
'todo', 'task', 'create',
|
||||
'--title', title,
|
||||
'--executors', item['executors'].strip(),
|
||||
'--format', 'json',
|
||||
]
|
||||
priority = item.get('priority')
|
||||
if priority is not None:
|
||||
cmd_args.extend(['--priority', str(int(priority))])
|
||||
due = parse_due(item.get('due'))
|
||||
if due:
|
||||
cmd_args.extend(['--due', due])
|
||||
recurrence = item.get('recurrence')
|
||||
if recurrence:
|
||||
rr = str(recurrence).replace('\\n', '\n')
|
||||
cmd_args.extend(['--recurrence', rr])
|
||||
|
||||
result = run_dws(cmd_args, dry_run=dry_run)
|
||||
if result:
|
||||
print(f" ✓ [{i+1}/{len(todos)}] {title}")
|
||||
success += 1
|
||||
else:
|
||||
print(f" ✗ [{i+1}/{len(todos)}] {title}")
|
||||
fail += 1
|
||||
|
||||
print(f"\n完成: 成功 {success}, 失败 {fail}")
|
||||
sys.exit(0 if fail == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
查询今天/明天/本周未完成的待办并汇总输出
|
||||
|
||||
用法:
|
||||
python todo_daily_summary.py # 默认查今天
|
||||
python todo_daily_summary.py today # 今天的待办
|
||||
python todo_daily_summary.py tomorrow # 明天的待办
|
||||
python todo_daily_summary.py week # 本周的待办
|
||||
python todo_daily_summary.py --dry-run # 仅显示将执行的命令
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
PRIORITY_MAP = {10: '低', 20: '普通', 30: '较高', 40: '紧急'}
|
||||
PAGE_SIZE = 50
|
||||
MAX_PAGES = 10
|
||||
|
||||
|
||||
def run_dws(args: List[str], dry_run: bool = False) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.TimeoutExpired:
|
||||
print('错误:命令执行超时', file=sys.stderr)
|
||||
return None
|
||||
except (json.JSONDecodeError, FileNotFoundError) as e:
|
||||
print(f"错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def get_date_range(scope: str):
|
||||
now = datetime.now()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if scope == 'today':
|
||||
return today_start, today_start + timedelta(days=1)
|
||||
elif scope == 'tomorrow':
|
||||
tmr = today_start + timedelta(days=1)
|
||||
return tmr, tmr + timedelta(days=1)
|
||||
elif scope == 'week':
|
||||
week_start = today_start - timedelta(days=today_start.weekday())
|
||||
return week_start, week_start + timedelta(days=7)
|
||||
return today_start, today_start + timedelta(days=1)
|
||||
|
||||
|
||||
def fetch_all_todos(
|
||||
dry_run: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
all_todos: List[Dict[str, Any]] = []
|
||||
for page in range(1, MAX_PAGES + 1):
|
||||
data = run_dws([
|
||||
'todo', 'task', 'list',
|
||||
'--page', str(page),
|
||||
'--size', str(PAGE_SIZE),
|
||||
'--status', 'false',
|
||||
'--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run:
|
||||
return []
|
||||
if not data:
|
||||
break
|
||||
if isinstance(data, list):
|
||||
items = data
|
||||
elif isinstance(data, dict):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
items = inner.get('todoCards', [])
|
||||
elif isinstance(inner, list):
|
||||
items = inner
|
||||
else:
|
||||
items = []
|
||||
else:
|
||||
items = []
|
||||
if not items or not isinstance(items, list):
|
||||
break
|
||||
all_todos.extend(items)
|
||||
if len(items) < PAGE_SIZE:
|
||||
break
|
||||
return all_todos
|
||||
|
||||
|
||||
def format_priority(p) -> str:
|
||||
try:
|
||||
return PRIORITY_MAP.get(int(p), str(p))
|
||||
except (ValueError, TypeError):
|
||||
return '普通'
|
||||
|
||||
|
||||
def format_due(due_ms) -> str:
|
||||
if not due_ms:
|
||||
return '无截止时间'
|
||||
try:
|
||||
dt = datetime.fromtimestamp(int(due_ms) / 1000)
|
||||
return dt.strftime('%Y-%m-%d %H:%M')
|
||||
except (ValueError, TypeError, OSError):
|
||||
return str(due_ms)
|
||||
|
||||
|
||||
def filter_by_due(
|
||||
todos: List[Dict[str, Any]], start: datetime, end: datetime,
|
||||
) -> List[Dict[str, Any]]:
|
||||
start_ms = int(start.timestamp() * 1000)
|
||||
end_ms = int(end.timestamp() * 1000)
|
||||
result = []
|
||||
for t in todos:
|
||||
due = t.get('dueTime') or t.get('due')
|
||||
if not due:
|
||||
result.append(t)
|
||||
continue
|
||||
try:
|
||||
due_val = int(due)
|
||||
if start_ms <= due_val < end_ms:
|
||||
result.append(t)
|
||||
except (ValueError, TypeError):
|
||||
result.append(t)
|
||||
return result
|
||||
|
||||
|
||||
def print_summary(
|
||||
todos: List[Dict[str, Any]], scope: str,
|
||||
start: datetime, end: datetime,
|
||||
):
|
||||
scope_label = {
|
||||
'today': '今天', 'tomorrow': '明天', 'week': '本周',
|
||||
}.get(scope, scope)
|
||||
print(f"\n📋 {scope_label}未完成待办 "
|
||||
f"({start.strftime('%m-%d')} ~ {end.strftime('%m-%d')})")
|
||||
print('=' * 50)
|
||||
if not todos:
|
||||
print(' ✅ 暂无待办,轻松一下!')
|
||||
return
|
||||
urgent = [t for t in todos if format_priority(
|
||||
t.get('priority')) == '紧急']
|
||||
if urgent:
|
||||
print(f"\n🔴 紧急 ({len(urgent)} 条)")
|
||||
for t in urgent:
|
||||
title = t.get('subject') or t.get('title', '无标题')
|
||||
print(f" • {title} ⏰ {format_due(t.get('dueTime'))}")
|
||||
normal = [t for t in todos if t not in urgent]
|
||||
if normal:
|
||||
print(f"\n📌 其他 ({len(normal)} 条)")
|
||||
for t in normal:
|
||||
title = t.get('subject') or t.get('title', '无标题')
|
||||
pri = format_priority(t.get('priority'))
|
||||
print(f" • [{pri}] {title} ⏰ {format_due(t.get('dueTime'))}")
|
||||
print(f"\n合计: {len(todos)} 条待办")
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = '--dry-run' in sys.argv
|
||||
args = [a for a in sys.argv[1:] if a != '--dry-run']
|
||||
scope = args[0] if args else 'today'
|
||||
if scope not in ('today', 'tomorrow', 'week'):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
start, end = get_date_range(scope)
|
||||
todos = fetch_all_todos(dry_run=dry_run)
|
||||
if dry_run:
|
||||
return
|
||||
filtered = filter_by_due(todos, start, end)
|
||||
print_summary(filtered, scope, start, end)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
扫描已过截止时间但未完成的待办,输出逾期清单
|
||||
|
||||
用法:
|
||||
python todo_overdue_check.py
|
||||
python todo_overdue_check.py --dry-run
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
PAGE_SIZE = 50
|
||||
MAX_PAGES = 10
|
||||
PRIORITY_MAP = {10: '低', 20: '普通', 30: '较高', 40: '紧急'}
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f"错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def fetch_all_undone(dry_run: bool = False) -> List[Dict[str, Any]]:
|
||||
all_todos: List[Dict[str, Any]] = []
|
||||
for page in range(1, MAX_PAGES + 1):
|
||||
data = run_dws([
|
||||
'todo', 'task', 'list',
|
||||
'--page', str(page), '--size', str(PAGE_SIZE),
|
||||
'--status', 'false', '--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run or not data:
|
||||
break
|
||||
if isinstance(data, list):
|
||||
items = data
|
||||
elif isinstance(data, dict):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
items = inner.get('todoCards', [])
|
||||
elif isinstance(inner, list):
|
||||
items = inner
|
||||
else:
|
||||
items = []
|
||||
else:
|
||||
items = []
|
||||
if not items or not isinstance(items, list):
|
||||
break
|
||||
all_todos.extend(items)
|
||||
if len(items) < PAGE_SIZE:
|
||||
break
|
||||
return all_todos
|
||||
|
||||
|
||||
def find_overdue(todos: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
now_ms = int(datetime.now().timestamp() * 1000)
|
||||
overdue = []
|
||||
for t in todos:
|
||||
due = t.get('dueTime') or t.get('due')
|
||||
if not due:
|
||||
continue
|
||||
try:
|
||||
if int(due) < now_ms:
|
||||
overdue.append(t)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return overdue
|
||||
|
||||
|
||||
def days_overdue(due_ms) -> int:
|
||||
now = datetime.now()
|
||||
try:
|
||||
due_dt = datetime.fromtimestamp(int(due_ms) / 1000)
|
||||
return max(0, (now - due_dt).days)
|
||||
except (ValueError, TypeError, OSError):
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = '--dry-run' in sys.argv
|
||||
todos = fetch_all_undone(dry_run=dry_run)
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
overdue = find_overdue(todos)
|
||||
overdue.sort(
|
||||
key=lambda t: int(t.get('dueTime') or t.get('due', 0))
|
||||
)
|
||||
|
||||
print(f"\n⏰ 逾期待办检查 ({datetime.now().strftime('%Y-%m-%d %H:%M')})")
|
||||
print('=' * 50)
|
||||
|
||||
if not overdue:
|
||||
print(' ✅ 没有逾期待办,继续保持!')
|
||||
return
|
||||
|
||||
for t in overdue:
|
||||
title = t.get('subject') or t.get('title', '无标题')
|
||||
due = t.get('dueTime') or t.get('due')
|
||||
days = days_overdue(due)
|
||||
pri = PRIORITY_MAP.get(
|
||||
int(t.get('priority', 20)), '普通'
|
||||
)
|
||||
due_str = datetime.fromtimestamp(
|
||||
int(due) / 1000
|
||||
).strftime('%Y-%m-%d')
|
||||
print(f" 🔴 [{pri}] {title}")
|
||||
print(f" 截止: {due_str} 逾期: {days} 天")
|
||||
|
||||
print(f"\n合计: {len(overdue)} 条逾期待办")
|
||||
sys.exit(1 if overdue else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user