first commit

This commit is contained in:
2026-09-02 11:44:52 +08:00
commit 0c8fa2653e
309 changed files with 57278 additions and 0 deletions
@@ -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()