# calendar schedules free list — 查询参与人共同空闲

查询同企业成员在指定时间窗口内的共同空闲时段。直接返回可推荐的时段列表。

## 输出前必检（CRITICAL）

**任何 free list 触发的回复，最终对外文本都必须满足**：
- 不出现 `wo` 前缀字符串（userid 仅用于工具调用，对用户只显示姓名 / 别名）
- 不出现 `mt_` / `td_` / `wo_` / `doc_` / `room_` 等内部 ID 前缀

多人查询时尤其容易在"对齐姓名↔userid"中无意泄露——展示阶段如果你写到 `wo` 字符，
**立即停下重写**，只保留姓名（来自 `available_users[].name`）。

## 命令示例

```bash
# 查询 woxxx 和 woyyy 在 2026-04-07 09:00:00 和 2026-04-07 18:00:00 之间的空闲时段，并且必须是60分钟整块的 
wecom-cli calendar schedules free list --json '{
  "userids": [{"userid": "woxxx"}, {"userid": "woyyy"}],
  "begin_time": "2026-04-07 09:00:00",
  "end_time": "2026-04-07 18:00:00",
  "min_duration_minutes": 60,
  "limit": 5
}'
```

## 参数

| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|:----:|------|------|
| `userids` | object[] | 是 | — | >=1 个成员，对象数组格式 `[{"userid": "woxxx"}]`（`wo` 前缀）。允许单人调用，等价于"某人什么时候有空"查询 |
| `begin_time` | string | 是 | — | 查询窗口起，格式 `YYYY-MM-DD HH:mm:ss`。早于服务端当前时刻的部分会被自动截断 |
| `end_time` | string | 是 | — | 查询窗口止，必须晚于 `begin_time`，且与 `begin_time` 的间隔 ≤ 24 小时 |
| `min_duration_minutes` | int | 否 | `30` | 过滤掉短于该值的空闲段，避免推荐过碎的时间窗 |
| `strategy` | string | 否 | `max_attendees` | 推荐策略，详见下表 |
| `limit` | int | 否 | `10` | 返回时段数量上限 |

### `strategy` 取值

| 值 | 行为 | 状态 |
|----|------|------|
| `max_attendees` | 按最多可参与人数筛选，只返回最高一档人数的所有时段，同档内按时间升序。有共同空闲时即全员到场窗口；无共同空闲时自然降级为次大可达人数。 | 当前唯一实现，默认值 |

## 返回结构

```json
{
  "total_count": 2,
  "strategy": "max_attendees",
  "extra_info": "没有找到所有人都空闲的时段，下面是符合 strategy 规则的时间段",
  "slots": [
    {
      "begin_time": "2026-04-07 13:00:00",
      "end_time":   "2026-04-07 14:00:00",
      "available_users": [
        {"userid": "woxxx", "name": "张三"},
        {"userid": "woyyy", "name": "李四"}
      ],
      "available_count": 2,
      "busy_users": []
    }
  ]
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `total_count` | int | 本次查询的有效人数 |
| `strategy` | string | 服务端实际采用的策略 |
| `extra_info` | string | 服务端提示文案。**降级场景**会说明"没有全员都空闲，下面是按 strategy 筛选的最佳时段"等内容，可作为措辞参考 |
| `slots[]` | array | 推荐的空闲时段，已按策略筛选、已过滤过去时段、已应用 `min_duration_minutes` |
| `slots[].begin_time` | string | 时段起始时间，格式 `YYYY-MM-DD HH:mm:ss`，与请求参数同格式，可直接展示 |
| `slots[].end_time` | string | 时段结束时间，格式 `YYYY-MM-DD HH:mm:ss` |
| `slots[].available_users` | array | 该时段内空闲的人（`userid` + `name`）。**展示时只用 `name`，禁止暴露 userid** |
| `slots[].available_count` | int | 该时段内空闲人数 |
| `slots[].busy_users` | array | 该时段内忙碌的人（`userid` + `name`）。`max_attendees` 全员命中时为空，降级时列出冲突人 |

> 展示时段前心算一次 `end_time - begin_time` 的分钟数，确认 ≥ 请求传入的
> `min_duration_minutes`（默认 30），避免把短时段的时长说宽。


### 指定重要优先人物优先的查询

如果用户希望查询一批人的空闲时间，但是优先其中某个子集（重要人物）必须空闲（不重要的人可以不空闲导致缺席），可以先单独查询重要人物的空闲时间，再查询全员的空闲时间。再推荐一个合适的时间。

### 分支判断（一次请求覆盖三种情况）

拿到响应后，比较 `slots[0].available_count` 与 `total_count`：

| 情况 | 含义 | Agent 行为 |
|------|------|-----------|
| `slots[0].available_count == total_count` | 存在全员共同空闲 | 展示所有可用时段让用户选择 |
| `0 < slots[0].available_count < total_count` | 无全员共同空闲，服务端已降级到"最多人能到"的窗口 | **先告知用户哪些人冲突、几人能参加**，再展示时段，让用户决定继续还是换时间 |
| `slots == []` | 查询窗口内没有任何符合最小粒度的可用时段 | 不要硬推荐，引导用户**扩大时间范围或减少参与人** |

> 一次请求已覆盖正常 / 降级 / 全忙三种语义，**不要发起第二次"降级查询"**。

> **查询"某时段有没有空"时，忙碌也要如实响应**：当用户问的是特定时间段的忙闲（如"张三下午 3 点有空吗""明天上午大家都在吗"），若该时段没有空闲（`slots` 为空）、或被问的人不在该时段的 `available_users` 里，必须明确回复"该时段忙 / 已有安排"，并尽量点明是谁忙（取 `busy_users[].name`）、忙在哪一段；不要只报空闲时段，也不要用"无共同空闲"一笔带过而不点明忙碌状态。

### 切片与展示

- **推荐时段按 1 小时维度切分**：`slots` 返回的可用空闲段，若长度超过 1 小时，须在 Agent 侧按 1 小时粒度切成多个候选时段分别推荐（如空闲段 `15:00-18:00` 切为 `15:00-16:00`、`16:00-17:00`、`17:00-18:00`），每个候选统一按整 1 小时呈现；不足 1 小时的空闲段按其实际长度原样展示。查询时建议传 `min_duration_minutes=60`，避免推荐出不足 1 小时的碎片段。
- **候选起点不得越界（起点 ≤ 段终点 − 日程时长）[REQUIRED]**：候选切片的长度只是展示粒度，用户选中后实际占用的是「起点 + 完整日程时长」。因此当日程时长 D 超过 1 小时时，必须剔除那些「起点 + D」会超出本空闲段终点的候选起点——即候选起点必须满足 `起点 ≤ 段终点 − D`，否则实际区间会落到未经忙闲验证的时段、可能与他人冲突。例如 **2 小时**会议、空闲段 `15:00-18:00`：合法起点上限为 `18:00 − 2h = 16:00`，故只保留 `15:00`、`16:00` 两个起点（对应实际区间 `15:00-17:00`、`16:00-18:00`），必须剔除 `17:00`（其实际区间 `17:00-19:00` 已越过 18:00）。当空闲段长度本身小于 D 时，该段不产生任何候选。
- **推荐时段的长度只表示"这段时间可用"，不代表日程/会议时长**：切出的 1 小时候选仅用于给用户挑选开始时段，用户选定后，日程/会议的实际时长仍以用户明确指定的为准；用户未明确时长时一律默认 1 小时（见 [calendar-create](wecomcli-calendar-create.md) 与 wecomcli-meeting.md 创建文档），禁止把推荐时段的长度直接当作时长。

### 输出格式

**情况 1：全员共同空闲**
```
推荐时间：
  方案 1: 04-07 15:00-16:00 — 张三、李四都有空
  方案 2: 04-07 16:00-17:00 — 张三、李四都有空
  方案 3: 04-07 13:00-14:00 — 张三、李四都有空

选哪个方案？或者说"换一批"看其他时间。
```

**情况 2：降级（部分人能参加）**
```
当前时间范围内没有所有人都空闲的时段，最多 2 人能到。

方案 1: 04-07 15:00-16:00 — 张三、李四能参加（王五此时有日程）
方案 2: 04-07 17:00-18:00 — 张三、李四能参加（王五此时有日程）

要按这些时段安排吗？或者换个时间窗口让王五也能参加？
```

**情况 3：全员无空**
```
04-07 13:00-18:00 内，张三、李四、王五 没有任何能凑齐的空闲时段（最小粒度 30 分钟）。

建议：
1. 扩大时间窗口（如延长到傍晚或换一天）
2. 减少参与人
```

> 展示参与人时只用姓名。`available_users[].userid` 仅用于回传到 `schedules create` 的 `attendees`，禁止出现在面向用户的文案里。

## 典型场景

### 1. 有共同空闲时段

```
用户：帮我约张三和李四明天下午聊一下
→ 通过 wecomcli-contact.md 批量搜索「张三」「李四」，解析为 userid
→ 调用 free list（明天 13:00-18:00；`userids` = 自己 + 张三 + 李四——新建日程的共同空闲须把自己也纳入，避免排到自己已占用的时段）
→ slots[0].available_count == total_count == 3，存在全员共同空闲
→ 展示前 3 个时段让用户选择
→ 用户选择 → 调用 create
```

### 2. 部分降级 / 全员无空

```
用户：帮我约王五和赵六、孙七明天上午碰一下
→ 通过 wecomcli-contact.md 批量搜索，解析为 userid
→ 调用 free list（明天 09:00-12:00）
→ 情况 A: slots 为空 → 引导扩大窗口或减少参与人
→ 情况 B: slots[0].available_count = 2 < 3 → 告知冲突的人和"最多 2 人能参加"的时段
  → 用户选"换个时间" → 重新追问范围 → 再次调用
  → 用户选"按 2 人安排" → 调用 create（只把 available_users 中的人作为参与人）
```

### 3. 单人空闲查询

```
用户：李四明天什么时候有空
→ 通过 wecomcli-contact.md 搜索「李四」，解析为 userid
→ 调用 free list（userids 单元素，begin_time/end_time 覆盖明天工作时段）
→ slots 即李四的空闲段
→ 用人话展示时段起止时间
```

### 加人 / 改时间到已有日程时的查询对象（避免自冲突误报）[CRITICAL]

为"已存在的日程"加人或改时间而做忙闲检查时，查询对象**必须排除正被该日程占用、因而必然显示忙碌的人和时间段**，否则会误报冲突。

**核心原则**：对【已在本日程中的人】（日程创建者 / 自己 + 已有参与人）只查"与本日程**当前时段不重叠**"的时间——本日程已占着原时段，对这些人在原时段查到的"忙"是它自己造成的自冲突误报；【新增参与人】才查完整目标时段。据此分三种情况：

- **① 只加人、不改时间** → `userids` 只放**新增参与人**，针对**日程原时段**查询。**不要**把当前用户（创建者 / 自己）和已有参与人放进 `userids`——他们正因这条日程而"忙"，纳入后会误判为冲突，而用户本意恰恰是让别人加入自己这个已定时间的日程。
- **② 改时间，且新时段与原时段【不重叠】**（平移 / 改期，如 15:00 改到 17:00）→ `userids` 放"改后仍需参加的人 + 新增参与人"，针对**新时段**查询。新旧时段无交集，现有参与人查新时段不会撞上本日程，可正常纳入。
- **③ 改时间，且新时段与原时段【有重叠】**（延长 / 提前等，新时段含部分原时段）→ 不能整段查现有参与人，否则重叠部分会被本日程自己误报为忙：
  - **新增参与人**：查**完整新时段**。
  - **现有参与人及自己**：只查**新时段去掉与原时段重叠后剩下的增量段**（如 15:00-16:00 延到 15:00-17:00，只查 16:00-17:00；如 15:00-16:00 提前到 14:00-16:00，只查 14:00-15:00）。增量段为空（如仅缩短时间）则现有参与人无需查。

> 该约束同样适用于 [calendar-update](wecomcli-calendar-update.md) 的"参与人变更工作流"：先按上述规则圈定查询对象和查询时段，再调用 `free list`。

## 查询范围约束

- **必须传未来时间**：`begin_time` 早于服务端当前时刻的部分会被自动截断；传纯历史窗口会得到空 `slots`。
  **调用前先检查**：若用户问"昨天 / 上周 / 上个月某人什么时候有空"等纯过去时间，直接告知用户"过去时段无法查询忙闲"并引导改成未来时间，不要先调 `free list` 拿到空结果再解释。
- **单次窗口 ≤ 24 小时**：`end_time` 必须晚于 `begin_time` 且间隔不超过 24h。跨天 / 多天需求必须拆成多段分别调用，再在 Agent 侧按顺序拼接 slots。
- **未给时间窗口的默认值**：用户只问"X 什么时候有空"没给日期范围时，默认只查当天剩余工作时段 + 明天工作时段（共两个 24h 窗口），不要主动展开 3 天以上——若不够再询问用户。
- **周期日程限制**：仅覆盖最近两个月有修改的周期日程，更早的可能不在结果中。
- **隐私保留**：返回中不包含日程主题、描述、其他参与人；只暴露忙 / 闲的归属人。

## 异常处理

| 异常场景 | 处理方式 |
|---------|---------|
| 接口调用失败 | 告知"忙闲查询暂时不可用"，建议用户直接确认时间后创建日程 |
| `slots == []` 且窗口合理 | 引导用户扩大时间窗口或减少参与人，不要重复传同一窗口试错 |

## 参考

- [wecomcli-calendar.md](wecomcli-calendar.md) — 日程主文档
- [calendar-create](wecomcli-calendar-create.md) — 创建日程
