first commit
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user