#!/usr/bin/env python3 """ 查询多人共同空闲时段,推荐最佳会议时间 用法: python calendar_free_slot_finder.py \ --users userId1,userId2,userId3 \ --date 2026-03-15 \ --duration 60 python calendar_free_slot_finder.py \ --users userId1,userId2 \ --date 2026-03-15 \ --start-hour 9 --end-hour 18 \ --duration 30 --dry-run """ import sys import json import subprocess import argparse from datetime import datetime, timedelta, timezone from typing import List, Dict, Any, Optional, Tuple TZ = timezone(timedelta(hours=8)) SLOT_STEP_MIN = 30 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 fmt_iso(dt: datetime) -> str: return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00') def parse_busy_intervals( data: Any, ) -> List[Tuple[datetime, datetime]]: intervals = [] if not data: return intervals items = [] if isinstance(data, list): items = data elif isinstance(data, dict): for user_data in data.values(): if isinstance(user_data, list): items.extend(user_data) elif isinstance(user_data, dict): items.extend( user_data.get('busyTimes', []) ) for item in items: start_str = item.get('startTime') or item.get('start', '') end_str = item.get('endTime') or item.get('end', '') if not start_str or not end_str: continue for fmt in ( '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M%z', ): try: s = datetime.strptime(start_str, fmt) e = datetime.strptime(end_str, fmt) if s.tzinfo is None: s = s.replace(tzinfo=TZ) if e.tzinfo is None: e = e.replace(tzinfo=TZ) intervals.append((s, e)) break except ValueError: continue return intervals def find_free_slots( day_start: datetime, day_end: datetime, busy: List[Tuple[datetime, datetime]], duration_min: int, ) -> List[Tuple[datetime, datetime]]: busy_sorted = sorted(busy, key=lambda x: x[0]) merged: List[Tuple[datetime, datetime]] = [] for s, e in busy_sorted: if merged and s <= merged[-1][1]: merged[-1] = (merged[-1][0], max(merged[-1][1], e)) else: merged.append((s, e)) free: List[Tuple[datetime, datetime]] = [] cursor = day_start for bs, be in merged: if cursor < bs: gap = (bs - cursor).total_seconds() / 60 if gap >= duration_min: free.append((cursor, bs)) cursor = max(cursor, be) if cursor < day_end: gap = (day_end - cursor).total_seconds() / 60 if gap >= duration_min: free.append((cursor, day_end)) return free def main(): parser = argparse.ArgumentParser( description='查询多人共同空闲时段' ) parser.add_argument( '--users', required=True, help='用户 ID 列表,逗号分隔' ) parser.add_argument( '--date', required=True, help='查询日期 YYYY-MM-DD' ) parser.add_argument( '--duration', type=int, default=60, help='会议时长(分钟),默认 60', ) parser.add_argument( '--start-hour', type=int, default=9, help='工作日开始小时,默认 9', ) parser.add_argument( '--end-hour', type=int, default=18, help='工作日结束小时,默认 18', ) parser.add_argument( '--dry-run', action='store_true', help='仅显示命令' ) args = parser.parse_args() try: date = datetime.strptime(args.date, '%Y-%m-%d') except ValueError: print('错误:日期格式应为 YYYY-MM-DD') sys.exit(1) day_start = date.replace( hour=args.start_hour, tzinfo=TZ ) day_end = date.replace(hour=args.end_hour, tzinfo=TZ) data = run_dws([ 'calendar', 'busy', 'search', '--users', args.users, '--start', fmt_iso(day_start), '--end', fmt_iso(day_end), '--format', 'json', ], dry_run=args.dry_run) if args.dry_run: return busy = parse_busy_intervals(data) free = find_free_slots(day_start, day_end, busy, args.duration) users_list = args.users.split(',') print(f"\n🕐 空闲时段查询 ({args.date})") print(f" 参与人: {len(users_list)} 人") print(f" 会议时长: {args.duration} 分钟") print(f" 工作时间: {args.start_hour}:00 ~ " f"{args.end_hour}:00") print('=' * 50) if not free: print(' ❌ 该日无共同空闲时段') return print(f"\n✅ 找到 {len(free)} 个可用时段:\n") for i, (s, e) in enumerate(free, 1): gap_min = int((e - s).total_seconds() / 60) label = '⭐ 推荐' if i == 1 else f' 备选{i-1}' print(f" {label} {s.strftime('%H:%M')} ~ " f"{e.strftime('%H:%M')} ({gap_min}分钟)") if __name__ == '__main__': main()