first commit
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
#!/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):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, list):
|
||||
items = inner
|
||||
elif isinstance(inner, dict):
|
||||
for user_data in inner.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()
|
||||
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
一键创建日程(可选:带参与者 + 预定空闲会议室)
|
||||
|
||||
流程:
|
||||
1. 若需预定会议室 (--book-room),先搜索空闲会议室;无可用则提前报错退出
|
||||
2. 使用 event create 一次性完成日程创建 + 添加参与者 + 预定会议室
|
||||
|
||||
用法:
|
||||
python calendar_schedule_meeting.py \
|
||||
--title "Q1 复盘会" \
|
||||
--start "2026-03-15T14:00" \
|
||||
--end "2026-03-15T15:00" \
|
||||
--users userId1,userId2 \
|
||||
--book-room
|
||||
|
||||
python calendar_schedule_meeting.py --dry-run \
|
||||
--title "测试" --start "2026-03-15T14:00" --end "2026-03-15T15:00"
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Any, Optional, Tuple
|
||||
|
||||
TZ = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
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 {'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, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f" ✗ 错误:{e}")
|
||||
return None
|
||||
|
||||
|
||||
def normalize_time(time_str: str) -> str:
|
||||
for fmt in ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M',
|
||||
'%Y-%m-%dT%H:%M:%S'):
|
||||
try:
|
||||
dt = datetime.strptime(time_str, fmt)
|
||||
dt = dt.replace(tzinfo=TZ)
|
||||
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
||||
except ValueError:
|
||||
continue
|
||||
if '+' in time_str or time_str.endswith('Z'):
|
||||
return time_str
|
||||
raise ValueError(f"无法解析时间:{time_str}")
|
||||
|
||||
|
||||
def parse_group_ids(raw: str) -> List[str]:
|
||||
if raw is None:
|
||||
return []
|
||||
return [part.strip() for part in raw.split(',') if part.strip()]
|
||||
|
||||
|
||||
def extract_room_candidates(payload: Any) -> Tuple[List[dict], str]:
|
||||
candidates: Any = []
|
||||
if isinstance(payload, list):
|
||||
candidates = payload
|
||||
elif isinstance(payload, dict):
|
||||
if isinstance(payload.get('rooms'), list):
|
||||
candidates = payload.get('rooms', [])
|
||||
elif isinstance(payload.get('result'), dict):
|
||||
nested = payload.get('result', {})
|
||||
if isinstance(nested.get('rooms'), list):
|
||||
candidates = nested.get('rooms', [])
|
||||
elif isinstance(nested.get('result'), list):
|
||||
candidates = nested.get('result', [])
|
||||
elif isinstance(payload.get('result'), list):
|
||||
candidates = payload.get('result', [])
|
||||
|
||||
if not isinstance(candidates, list):
|
||||
return [], '返回结构中未找到会议室列表'
|
||||
|
||||
valid_rooms: List[dict] = []
|
||||
placeholder_count = 0
|
||||
for item in candidates:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get('roomId') or item.get('id'):
|
||||
valid_rooms.append(item)
|
||||
continue
|
||||
if item.get('labels') is None and len(item) == 1:
|
||||
placeholder_count += 1
|
||||
|
||||
if valid_rooms:
|
||||
return valid_rooms, f'返回 {len(valid_rooms)} 个有效会议室'
|
||||
if placeholder_count:
|
||||
return [], '仅返回占位结果(如 labels:null),无有效 roomId'
|
||||
if candidates:
|
||||
return [], '返回了对象列表,但均不含有效 roomId'
|
||||
return [], '未返回任何会议室'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='一键创建日程(可选:带参与者 + 预定会议室)'
|
||||
)
|
||||
parser.add_argument('--title', required=True, help='日程标题')
|
||||
parser.add_argument('--start', required=True, help='开始时间')
|
||||
parser.add_argument('--end', required=True, help='结束时间')
|
||||
parser.add_argument('--desc', default='', help='日程描述')
|
||||
parser.add_argument('--users', default='', help='参与者 userId,逗号分隔')
|
||||
parser.add_argument(
|
||||
'--book-room', action='store_true', help='自动搜索并预定空闲会议室'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--room-group-id', default='',
|
||||
help='允许搜索的 groupId;同一地点请只传最相关 group,多个仅用于用户明确允许的多个地点'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run', action='store_true', help='仅显示命令'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
start_iso = normalize_time(args.start)
|
||||
end_iso = normalize_time(args.end)
|
||||
except ValueError as e:
|
||||
print(f"错误:{e}")
|
||||
sys.exit(1)
|
||||
|
||||
# ── Step 1: 若需预定会议室,先搜索空闲会议室 ──────────────────
|
||||
room_id: Optional[str] = None
|
||||
room_name: Optional[str] = None
|
||||
|
||||
if args.book_room:
|
||||
print('🏢 搜索空闲会议室...')
|
||||
group_ids = parse_group_ids(args.room_group_id)
|
||||
search_scopes = group_ids or [None]
|
||||
selected_room = None
|
||||
failure_reasons: List[str] = []
|
||||
|
||||
for group_id in search_scopes:
|
||||
scope_label = f'group {group_id}' if group_id else '根目录'
|
||||
print(f' - 查询范围: {scope_label}')
|
||||
search_args = [
|
||||
'calendar', 'room', 'search',
|
||||
'--start', start_iso,
|
||||
'--end', end_iso,
|
||||
'--available',
|
||||
'--format', 'json',
|
||||
]
|
||||
if group_id:
|
||||
search_args.extend(['--group-id', group_id])
|
||||
rooms_data = run_dws(search_args, dry_run=args.dry_run)
|
||||
|
||||
if args.dry_run:
|
||||
continue
|
||||
if not rooms_data:
|
||||
failure_reasons.append(f'{scope_label}: room search 执行失败')
|
||||
continue
|
||||
|
||||
rooms, detail = extract_room_candidates(rooms_data)
|
||||
if rooms:
|
||||
selected_room = rooms[0]
|
||||
break
|
||||
failure_reasons.append(f'{scope_label}: {detail}')
|
||||
|
||||
if not args.dry_run:
|
||||
if selected_room:
|
||||
room_id = selected_room.get('roomId') or selected_room.get('id')
|
||||
room_name = selected_room.get('roomName') or selected_room.get('name')
|
||||
print(f' ✓ 找到空闲会议室: {room_name} ({room_id})')
|
||||
else:
|
||||
print(f' ✗ {start_iso} ~ {end_iso} 时段内无可用会议室')
|
||||
for reason in failure_reasons:
|
||||
print(f' - {reason}')
|
||||
print(' 请向用户汇报失败,或询问是否放宽范围/改时间。')
|
||||
sys.exit(2)
|
||||
|
||||
# ── Step 2: 一次性创建日程(含参与者 + 会议室) ────────────────
|
||||
print('\n📅 创建日程...')
|
||||
create_args = [
|
||||
'calendar', 'event', 'create',
|
||||
'--title', args.title,
|
||||
'--start', start_iso,
|
||||
'--end', end_iso,
|
||||
'--format', 'json',
|
||||
]
|
||||
if args.desc:
|
||||
create_args.extend(['--desc', args.desc])
|
||||
if args.users:
|
||||
create_args.extend(['--attendees', args.users])
|
||||
if room_id:
|
||||
create_args.extend(['--rooms', str(room_id)])
|
||||
|
||||
result = run_dws(create_args, dry_run=args.dry_run)
|
||||
if not result:
|
||||
sys.exit(1)
|
||||
|
||||
# 解析响应
|
||||
event_id = None
|
||||
if not args.dry_run and isinstance(result, dict):
|
||||
# MCP 响应通常嵌套在 result 字段内: {"result": {"id": "..."}}
|
||||
inner = result.get('result', result)
|
||||
if isinstance(inner, dict):
|
||||
event_id = inner.get('eventId') or inner.get('id')
|
||||
else:
|
||||
event_id = result.get('eventId') or result.get('id')
|
||||
|
||||
# 输出结果摘要
|
||||
parts = []
|
||||
if event_id:
|
||||
parts.append(f'eventId: {event_id}')
|
||||
if args.users:
|
||||
parts.append(f'参与者: {args.users}')
|
||||
if room_name:
|
||||
parts.append(f'会议室: {room_name}')
|
||||
detail_str = f" ({', '.join(parts)})" if parts else ''
|
||||
print(f' ✓ 日程已创建{detail_str}')
|
||||
|
||||
print('\n✅ 完成!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
查看今天/明天/本周的日程安排
|
||||
|
||||
用法:
|
||||
python calendar_today_agenda.py # 今天
|
||||
python calendar_today_agenda.py today # 今天
|
||||
python calendar_today_agenda.py tomorrow # 明天
|
||||
python calendar_today_agenda.py week # 本周
|
||||
python calendar_today_agenda.py --dry-run # 仅显示命令
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
TZ = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
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 get_range(scope: str):
|
||||
now = datetime.now(TZ)
|
||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if scope == 'today':
|
||||
return today, today + timedelta(days=1)
|
||||
elif scope == 'tomorrow':
|
||||
t = today + timedelta(days=1)
|
||||
return t, t + timedelta(days=1)
|
||||
elif scope == 'week':
|
||||
ws = today - timedelta(days=today.weekday())
|
||||
return ws, ws + timedelta(days=7)
|
||||
return today, today + timedelta(days=1)
|
||||
|
||||
|
||||
def fmt_iso(dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
||||
|
||||
|
||||
def fmt_time(iso_str: str) -> str:
|
||||
if not iso_str:
|
||||
return '??:??'
|
||||
try:
|
||||
for fmt in ('%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S'):
|
||||
try:
|
||||
dt = datetime.strptime(iso_str, fmt)
|
||||
return dt.strftime('%H:%M')
|
||||
except ValueError:
|
||||
continue
|
||||
return iso_str[:16]
|
||||
except Exception:
|
||||
return iso_str[:16]
|
||||
|
||||
|
||||
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_range(scope)
|
||||
data = run_dws([
|
||||
'calendar', 'event', 'list',
|
||||
'--start', fmt_iso(start),
|
||||
'--end', fmt_iso(end),
|
||||
'--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
events = []
|
||||
if isinstance(data, list):
|
||||
events = data
|
||||
elif isinstance(data, dict):
|
||||
inner = data.get('result', data)
|
||||
if isinstance(inner, dict):
|
||||
events = inner.get('events', [])
|
||||
elif isinstance(inner, list):
|
||||
events = inner
|
||||
|
||||
label = {'today': '今天', 'tomorrow': '明天', 'week': '本周'
|
||||
}.get(scope, scope)
|
||||
print(f"\n📅 {label}日程 ({start.strftime('%m-%d')} ~ "
|
||||
f"{end.strftime('%m-%d')})")
|
||||
print('=' * 50)
|
||||
|
||||
if not events:
|
||||
print(' ✅ 暂无日程,自由安排!')
|
||||
return
|
||||
|
||||
for e in events:
|
||||
if not isinstance(e, dict):
|
||||
print(f" 🕐 {e}")
|
||||
continue
|
||||
title = e.get('summary') or e.get('title', '无标题')
|
||||
s = e.get('start', {})
|
||||
ed = e.get('end', {})
|
||||
start_t = fmt_time(
|
||||
s.get('dateTime', '') if isinstance(s, dict) else str(s)
|
||||
)
|
||||
end_t = fmt_time(
|
||||
ed.get('dateTime', '') if isinstance(ed, dict) else str(ed)
|
||||
)
|
||||
loc = e.get('location', {})
|
||||
loc_str = (loc.get('displayName', '')
|
||||
if isinstance(loc, dict) else str(loc or ''))
|
||||
line = f" 🕐 {start_t}-{end_t} {title}"
|
||||
if loc_str:
|
||||
line += f" 📍{loc_str}"
|
||||
print(line)
|
||||
|
||||
print(f"\n合计: {len(events)} 场日程")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user