# 考勤排班操作参考 (attendance-schedule)

> 本文档由 `attendance.md` 路由调用。覆盖两类排班操作：
> 1. **排班导入**（写操作）：当用户提到"排班"、"导入排班"、"安排班次"、"设置排班"、"调班"、"换班"、"排休"时
> 2. **排班查询导出**（只读操作）：当用户提到"查看排班"、"排班表"、"导出排班"、"排班记录"时
>
> 不适用于：班次定义查询（用 `attendance class search`）、考勤组配置（用 `attendance group get`）。

## 强制门禁（必须先读完本文档才能执行）

**任何排班操作都必须经过本文档定义的工作流，严禁绕过本文档直接调用 `dws attendance schedule import` 命令。**

违反将出现以下任一问题：
1. 未按"阶段 1"确认考勤组 → 把固定班制考勤组当排班制操作，接口报错
2. 未按"阶段 3"校验班次 → 传入不属于该考勤组的班次 ID，导致排班数据错乱
3. 未按"阶段 4"回显确认 → 用户未看到排班内容就直接执行，排错了无法回退
4. 未按"阶段 2"解析人员 → 传入错误的 userId，导致排班到错误的人
5. 未经用户确认就执行排班 → 排班是写操作，一旦执行就会覆盖原有排班

**执行前自检（必须能在心中回答）**：
- [ ] 考勤组是排班制（TURN）吗？
- [ ] 员工都属于该考勤组吗？
- [ ] 班次都属于该考勤组可用的班次吗？
- [ ] 用户已经确认了排班内容吗？

如果上述任何一项答不出，**回到本文档对应章节重新阅读**，禁止凭记忆/想象组装命令。

**前提**：当前用户必须是钉钉考勤管理员，否则排班接口返回权限错误。

## 业务约束（必须深刻理解）

> **这两条约束是排班的根基，贯穿整个工作流的每一步。**

1. **用户只能属于一个考勤组**：每个员工有且只有一个考勤组，不存在"选择考勤组"的场景。直接通过 `dws attendance rules` 查询即可唯一确定。
2. **排班只能排考勤组关联的班次**：考勤组绑定了固定的班次列表（`shiftVOList`），排班时只能从这些班次中选择，不能使用企业其他考勤组的班次，更不能编造班次。

**由此推导出的执行顺序**：必须先查清考勤组和它关联的班次，再去收集日期、人员等其他参数。

## 核心原则

Agent 解析用户意图（考勤组、员工、日期范围、班次安排），完成校验后，**调用 Python 脚本执行排班**。
- **先查后排**：任何排班操作的第一步都是查询考勤组及其关联班次，拿到真实数据后再进行后续参数收集
- **脚本自包含**：考勤组校验、班次校验、员工校验、回显确认、调用排班 API 全部由脚本内部完成
- **Agent 职责**：先查考勤组和关联班次，再解析用户意图、获取必要的 ID（员工 userId）、组装脚本参数
- **脚本职责**：二次校验、回显排班表格、等待用户确认、执行排班、输出结果摘要
- 排班是**写操作**，必须经过回显确认后才能执行

## 严格禁止 (NEVER DO)

- **禁止直接调用 `dws attendance schedule import`**，必须通过脚本执行
- 禁止凭历史记忆复用任何 ID（考勤组 ID、班次 ID、userId），必须从当次命令返回值中提取
- 禁止在未确认考勤组类型为排班制（TURN）的情况下执行排班
- 禁止在班次未经校验的情况下执行排班
- 禁止跳过用户确认直接执行排班
- **禁止在未向用户展示完整排班明细表格（含每天的班次名称）的情况下弹出确认卡片**。用户必须先看到"谁、哪天、上什么班"才能做出确认决策
- 禁止编造任何字段值或用户姓名
- 禁止直接输出裸 userId，脚本已内置 userId → 姓名转换
- **禁止直接输出裸 classId 数字**，必须展示班次名称（如"早班"），用户不理解 classId 是什么

## 严格要求 (MUST DO)

- 所有 `dws` 命令必须携带 `--format json`
- 必须先确认考勤组类型为 TURN（排班制），否则拒绝执行
- 必须通过考勤组详情获取绑定班次列表，校验班次 ID 属于该考勤组
- 必须在执行排班前向用户回显排班内容并获得确认
- 任何接口失败必须向用户清晰报错，禁止静默吞掉

## 涉及工具

| 工具 | 用途 | 安全等级 |
|------|------|---------|
| `dws attendance group search` | 搜索考勤组（按名称/类型） | 只读 |
| `dws attendance group get` | 查询考勤组全量信息（含绑定班次列表） | 只读 |
| `dws attendance group filtered-get` | 查询考勤组详情（成员列表） | 只读 |
| `dws attendance class search` | 查询班次列表（ID→名称映射） | 只读 |
| `dws attendance class get` | 查询班次详情 | 只读 |
| `dws attendance schedule import` | 导入排班记录（**仅由脚本内部调用**） | 写操作（危险） |
| `dws attendance schedule get` | 查询现有排班记录 | 只读 |
| `dws aisearch person` | 按姓名搜索用户获取 userId | 只读 |
| `dws contact user get` | 批量查询 userId → 用户信息 | 只读 |
| `dws contact dept search` | 搜索部门获取 deptId | 只读 |
| `dws contact dept list-members` | 获取部门成员 userId 列表 | 只读 |

## 意图判断

### 排班操作类型

| 用户说 | 操作类型 | 处理方式 |
|--------|---------|---------|
| "帮我给研发组排下周的班" / "排班" / "安排班次" | 批量排班 | 走本文档工作流 |
| "帮我把张三下周一改成早班" / "调班" / "换班" | 单人调班 | 走本文档工作流（单人模式） |
| "帮我把李四下周三排休" | 排休 | 走本文档工作流（isRest=Y） |

### 易混淆场景

| 用户说 | 应路由到 |
|--------|---------|
| "查看下周的排班" / "排班表" / "导出排班" / "导出排班表" / "XX考勤组的排班" | **本文档「排班查询导出工作流」**（走脚本） |
| "有哪些班次" / "班次列表" | `dws attendance class search`（查询班次定义） |
| "我属于哪个考勤组" | `dws attendance rules`（查询考勤规则） |
| "导出考勤报表" / "导出考勤" / "考勤明细" / "出勤汇总" （**不含"排班"二字**） | `attendance-report.md`（报表 skill） |

> **关键区分**："导出排班表" ≠ "导出考勤报表"。判断标准：句中含"排班"→ 本文档；不含"排班"且说的是"考勤报表/考勤数据/出勤统计" → `attendance-report.md`。

## 工作流

### 阶段 0: 先查考勤组和关联班次，再收集缺失参数

> **核心逻辑：先查后问。** 用户只属于一个考勤组，排班只能排该考勤组关联的班次。所以第一步永远是查清考勤组和它的班次，拿到真实数据后再向用户收集其他信息。

**步骤 0a — 查询考勤组（必须最先执行）**：

```bash
# 自动获取当前用户的考勤组（用户只属于一个考勤组，无需选择）
dws attendance rules --date <今天日期> --format json
# → 从返回中提取 groupId
```

如果是给指定员工排班，先查该员工的 userId，再查其考勤组。

**步骤 0b — 查询考勤组详情和关联班次（必须在 ask_question 之前完成）**：

```bash
# 获取考勤组详情（含绑定的班次列表）
dws attendance group get --group-id <groupId> --format json
# → 校验 groupVO.type 必须为 TURN（排班制）
# → 从 groupVO.shiftVOList 提取关联的班次
```

**提取结果**：
- 考勤组名称：`groupVO.name`
- 考勤组类型：`groupVO.type`（必须为 TURN）
- 关联班次列表：`groupVO.shiftVOList[].shiftSetting.{shiftId, shiftName}`

**步骤 0c — 收集缺失参数（ask_question 卡片交互）**：

拿到考勤组和关联班次后，再向用户收集缺失的参数。排班所需的四个参数：
- **考勤组**：已在 0a 自动获取，无需询问
- **班次**：已在 0b 获取关联班次列表，展示给用户选择
- **员工范围**（必填）：指定员工姓名 / 部门 / 考勤组全员 / "给我排班"
- **日期范围**（必填）：具体日期 / 日期范围（如"下周"、"5月19日到5月23日"）

**只收集真正缺失的参数**，用户已经提供的不要重复询问。将缺失参数**合并到一次 `ask_question` 调用中**。

示例：用户说"帮我排班"，Agent 应先自动查询考勤组和关联班次（步骤 0a + 0b），然后只询问日期范围和班次：

   ```
   // ===== 步骤 0a + 0b 已完成，此时你已经拿到了以下真实数据 =====
   // groupId = 实际的考勤组ID
   // groupName = 实际的考勤组名称
   // shiftVOList = 考勤组关联的班次列表（来自 dws attendance group get 的返回）

   // 从 shiftVOList 构建班次选项（伪代码）：
   shiftOptions = []
   for each shift in groupVO.shiftVOList:
       shiftOptions.append({ id: String(shift.shiftSetting.shiftId), label: shift.shiftSetting.shiftName })
   shiftOptions.append({ id: "rest", label: "排休" })

   // 如果考勤组只关联了一个班次，直接使用，不需要询问用户
   if shiftVOList.length == 1:
       selectedShift = shiftVOList[0]  // 自动选定，跳过班次选择

   // 构建日期选项（必须填入实际计算的日期）：
   todayStr = 当天日期（YYYY-MM-DD）
   thisWeekEnd = 本周日日期
   nextWeekStart = 下周一日期
   nextWeekEnd = 下周日日期

   ask_question({
     title: "排班参数确认",
     questions: [
       {
         id: "date_range",
         prompt: "请选择排班日期范围",
         options: [
           { id: "this_week", label: "本周剩余时间（" + todayStr + "-" + thisWeekEnd + "）" },
           { id: "next_week", label: "下周（" + nextWeekStart + "-" + nextWeekEnd + "）" },
           { id: "custom", label: "自定义日期范围" }
         ]
       },
       {
         id: "shift",
         prompt: "请选择班次（以下为您考勤组「" + groupName + "」关联的班次）",
         options: shiftOptions   // 直接使用上面从 shiftVOList 动态构建的选项，严禁替换为任何硬编码值
       }
     ]
   })
   ```

   **⚠️ 班次选项严禁硬编码**：上述伪代码中的 `shiftOptions` 必须在运行时从 `shiftVOList` 动态生成。禁止在 `ask_question` 中写入任何固定的班次名称（如"早班"、"晚班"、"正常班"、"全部排XX班"等）。如果你发现自己在 options 里手写班次名，说明你做错了。

   **动态选项原则（严格执行）**：
   - 班次选项**必须且只能**来自考勤组的 `shiftVOList`，**严禁编造任何班次名称**（如"正常班"、"早班"、"晚班"、"全部排XX班"等都是编造）
   - 每个班次选项的 `id` 必须是 `shiftVOList` 中的真实 `shiftId`，`label` 必须是真实的 `shiftName`
   - 如果 `shiftVOList` 为空，降级从 `groupVO.classIds` + `class search` 按 ID 精确查询（不是全局搜索）
   - **只收集缺失的参数**：用户已经提供的参数不要重复询问
   - **如果考勤组只有一个班次，直接使用该班次，不需要询问用户选择**
   - **禁止用 `class search` 全局搜索来给用户展示班次选项**——全局班次列表包含不属于该考勤组的班次，用户选了也会被校验拒绝

### 阶段 1: 确认考勤组（必须为排班制）

> **业务事实：用户只属于一个考勤组。** 不存在"选考勤组"的场景，直接通过 `dws attendance rules` 自动获取即可。只有在用户明确指定了一个考勤组名称时，才用 `group search` 按名称确认。

**方式 A — 自动获取（默认方式，适用于绝大多数场景）**：
```bash
# 用户只属于一个考勤组，直接查询即可确定
dws attendance rules --date <今天日期> --format json
# → 返回中提取 groupId，然后用 group get 获取详情
dws attendance group get --group-id <groupId> --format json
```

**方式 B — 用户明确指定考勤组名称时**：
```bash
dws attendance group search --query "<考勤组名称>" --type TURN --format json
```

**校验规则**：
1. 考勤组类型必须为 **TURN（排班制）**，如果是 FIXED（固定班制）或 NONE（自由工时），拒绝并提示"该考勤组不是排班制，无法进行排班操作"
2. 确认考勤组后，**必须立即获取其关联班次**（`groupVO.shiftVOList`），后续所有班次选项都从这里取

**提取信息**：考勤组 ID（`groupId`）、考勤组名称（`name`）、**关联班次列表（`shiftVOList`）**

### 阶段 2: 获取员工列表

**场景 A — 指定员工姓名**：
```bash
dws aisearch person --keyword "<员工姓名>" --dimension name --format json
```

**场景 B — 按部门查询**：
```bash
dws contact dept search --query "<部门名>" --format json
dws contact dept list-members --ids <deptId> --format json
```

**场景 C — 考勤组全员**：
```bash
dws attendance group filtered-get --group-id <groupId> --member --format json
```

**场景 D — 用户已给 userId 列表**：直接跳过本步。

### 阶段 3: 校验班次（必须是考勤组关联的班次）

> **业务约束：排班只能排考勤组关联的班次。** 阶段 0/1 已经通过 `dws attendance group get` 拿到了 `shiftVOList`，本阶段直接使用该数据校验，**不需要也不应该再调用 `class search` 全局搜索**。

**班次数据来源**（已在阶段 0 或阶段 1 获取）：
- `groupVO.shiftVOList[].shiftSetting.shiftId` — 班次 ID
- `groupVO.shiftVOList[].shiftSetting.shiftName` — 班次名称

**禁止调用 `dws attendance class search` 全局搜索班次**——全局搜索会返回不属于该考勤组的班次，即使用户指定了班次名称，也必须在 `shiftVOList` 中匹配，而不是全局搜索。

**校验规则**：
1. 用户在 `ask_question` 卡片中选择的班次，其 `shiftId` 必须存在于 `shiftVOList` 中（卡片选项本身就是从 `shiftVOList` 构建的，所以天然满足）
2. 如果用户通过自然语言指定了班次名称（如"排早班"），必须在 `shiftVOList` 中**按名称模糊匹配**，找到对应的 `shiftId`
3. 如果用户指定的班次不在 `shiftVOList` 中，**必须拒绝**，并列出该考勤组关联的全部班次让用户重新选择
4. 仅当 `shiftVOList` 为空时，才降级从 `groupVO.classIds` + `dws attendance class get` 按 ID 精确查询（仍然不是全局搜索）

**提取信息**：班次 ID（`classId` / `shiftId`）、班次名称（`shiftName`）

### 阶段 4: 回显排班内容并使用 ask_question 卡片确认

> **[硬性门禁]** 必须先用普通文本向用户展示**完整的排班明细表格**（包含每个人、每天、具体班次名称），用户看到排班明细后，才能弹出 `ask_question` 确认卡片。
> **禁止在用户还不知道"谁、哪天、上什么班"的情况下就弹确认卡片**——这等于让用户盲签，体验极差。

**步骤 4a — 展示排班明细（必须在确认卡片之前）**：

在调用 `ask_question` 之前，**必须先**用普通文本向用户展示排班内容。表格中**必须包含班次名称**（如"早班 09:00-18:00"），不能只展示 classId 数字：

```
排班预览

考勤组: <考勤组名称>（ID: <groupId>）
排班日期: <startDate> ~ <endDate>

| 员工姓名 | 日期 | 星期 | 班次 | 是否排休 |
|---------|------|------|------|---------|
| 张三 | 2026-05-19 | 周一 | 早班 09:00-18:00 | 否 |
| 张三 | 2026-05-20 | 周二 | 早班 09:00-18:00 | 否 |
| 张三 | 2026-05-21 | 周三 | 排休 | 是 |
| 李四 | 2026-05-19 | 周一 | 晚班 18:00-02:00 | 否 |
| ... | ... | ... | ... | ... |

共 <N> 条排班记录
```

**自查清单（展示表格前必须确认）**：
- [ ] 表格中有员工姓名（不是 userId）
- [ ] 表格中有具体日期和星期几
- [ ] 表格中有班次名称（不是 classId 数字）
- [ ] 排休的记录标注了"排休"
- [ ] 表格涵盖了所有待排班的员工和日期

**步骤 4b — 使用 `ask_question` 卡片确认（必须在展示表格之后）**：

```
ask_question({
  title: "排班执行确认",
  questions: [
    {
      id: "confirm_execute",
      prompt: "以上排班将覆盖所选日期的现有排班记录，确认执行吗？",
      options: [
        { id: "yes", label: "确认执行" },
        { id: "no", label: "取消" }
      ]
    }
  ]
})
```

**处理用户选择**：
- 用户选择 **"确认执行"** → 进入阶段 5
- 用户选择 **"取消"** → 终止流程，提示"已取消排班操作"

### 阶段 5: 调用脚本执行排班

```bash
python scripts/attendance_schedule_import.py \
  --group-id <groupId> \
  --schedules '<JSON数组>' \
  --confirm
```

参数说明：
- `--group-id`（必填）：考勤组 ID
- `--schedules`（必填）：排班记录 JSON 数组，每条记录包含 `userId`、`workDate`、`classId`、`isRest`
- `--confirm`（必填）：表示用户已确认，脚本收到此标志才会执行排班

脚本内部自动处理：
1. 二次校验考勤组类型（必须为 TURN）
2. 从考勤组详情提取绑定班次，二次校验班次 ID 属于该考勤组
3. 格式化 workDate 为 `yyyy-MM-dd HH:mm:ss`
4. 调用 `dws attendance schedule import` 执行排班
5. 输出执行结果摘要（含全部排班明细）

### 阶段 6: 返回结果给用户

- 将脚本 stdout 输出的摘要信息原样转告用户
- 如果脚本输出 warning，原样转告用户
- 如果执行失败，将 stderr 错误信息转告用户

## 排班记录 JSON 格式

每条排班记录的字段说明：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `userId` | string | 是 | 员工的 userId |
| `workDate` | string | 是 | 排班日期，格式 YYYY-MM-DD |
| `classId` | int | 是 | 班次 ID（从 `class search` 获取） |
| `isRest` | string | 是 | 是否排休，`Y`=排休 / `N`=正常上班 |

排休时 `classId` 传 0，`isRest` 传 `Y`。

## API 返回结构注意事项（Agent 必读）

> 以下是实际执行中多次踩坑的关键数据结构说明。**禁止凭直觉假设字段在顶层**，必须按本节描述的嵌套路径提取。

### `dws attendance group get` 返回结构

```
run_dws 解包后的结构（unwrap_result 去掉 success/result 包装后）：
{
  "groupVO": {           ← 关键！type/name/classIds 等字段在这一层
    "type": "TURN",      ← 考勤组类型
    "name": "研发组",
    "classIds": [1290384739, ...],  ← 绑定的班次 ID 列表
    "shiftVOList": [     ← 排班制特有，班次详情
      {
        "shiftSetting": {
          "shiftId": 1290384739,  ← 班次 ID（与 classIds 对应）
          "shiftName": "早班 09:00-18:00"
        }
      }
    ],
    "selectedClass": [...],  ← 部分环境使用此字段
    ...
  },
  ...其他顶层字段...
}
```

**提取规则**：
- 考勤组类型：`result["groupVO"]["type"]`
- 考勤组名称：`result["groupVO"]["name"]`
- 绑定班次 ID 列表：`result["groupVO"]["classIds"]`
- 班次名称：`result["groupVO"]["shiftVOList"][N]["shiftSetting"]["shiftName"]`
- **禁止从 result 顶层直接取 type/name/classIds，那里没有这些字段**

### `dws attendance class search` 返回结构

```
run_dws 解包后可能为以下之一：
1. 直接 list[dict]: [{id, name, ...}, ...]
2. {"data": [...]} 或 {"items": [...]} 或 {"classList": [...]}
```

**注意**：如果 `class search` 返回 0 条记录，不一定是错误——可能是当前账号没有班次管理权限。此时从 `group get` 的 `shiftVOList` 中也可获取班次名称。

### `dws aisearch person` 搜索同名问题

同一个姓名可能返回**多个不同 userId**（如主管理员账号 vs 子管理员账号）。必须通过以下方式确认正确的 userId：
1. 检查目标考勤组的成员列表：`dws attendance group filtered-get --group-id <id> --member`
2. 取成员列表中存在的那个 userId

**禁止直接取搜索结果的第一条 userId，必须与考勤组成员列表交叉验证。**

### `dws contact user get` 可能的权限错误

`resolve_user_names`（userId→姓名转换）可能遇到 `SECURITY_CHECK_INVOKE_FAILED` 错误。这只影响**展示层**，不影响排班数据的正确性：
- 脚本已内置降级处理：权限失败时直接使用 userId 替代姓名
- **不要因为姓名获取失败就中止排班流程**

### Agent 常见错误模式（严禁）

| 错误做法 | 正确做法 |
|------------|------------|
| `result.get("type")` 从顶层取类型 | `result["groupVO"]["type"]` |
| `result.get("classIds")` 从顶层取班次 | `result["groupVO"]["classIds"]` 或 `result["groupVO"]["shiftVOList"]` |
| 用 `python3 -c "..."` inline 脚本解析 JSON | 调用已有的 Python 脚本（`attendance_schedule_import.py`） |
| 人名搜到多个结果直接取第一个 | 与考勤组成员列表交叉验证 |
| 姓名获取失败就中止流程 | 降级用 userId 展示，继续执行排班 |
| 直接调用 `dws attendance schedule import` | 必须通过 `attendance_schedule_import.py` 脚本 |

## 错误处理

| 错误 | 原因 | 处理方式 |
|------|------|---------|
| 权限错误（403） | 当前账号非管理员 | 提示需要管理员权限，不要重试 |
| 考勤组不是排班制 | 考勤组类型为 FIXED 或 NONE | 提示"该考勤组不是排班制，无法排班" |
| 班次不在可用列表中 | classId 无效 | 列出可用班次让用户重新选择 |
| userId 无效 | 用户 ID 错误或已离职 | 提示具体哪个用户无效 |
| 脚本执行失败 | 接口异常/配置问题 | 将 stderr 错误信息转告用户 |
| SECURITY_CHECK_INVOKE_FAILED | userId→姓名转换权限不足 | 仅影响展示，降级用 userId，不中止流程 |
| class search 返回空列表 | 账号无班次管理权限 | 从 `group get` 的 `shiftVOList` 提取班次名称 |

## 使用示例

### 示例 1: 给指定员工排班
**用户说**: "帮我给张三下周一到周五排早班，考勤组是研发组"

```bash
# 1. 确认考勤组并获取关联班次（先查后排）
dws attendance group search --query "研发组" --type TURN --format json
# → 拿到 groupId
dws attendance group get --group-id <groupId> --format json
# → 从 groupVO.shiftVOList 获取关联班次列表
# → 在 shiftVOList 中匹配"早班"，拿到对应的 shiftId 作为 classId
# ⚠️ 禁止用 class search 全局搜索班次

# 2. 获取员工 userId
dws aisearch person --keyword "张三" --dimension name --format json

# 3. 回显确认（Agent 向用户展示排班表格）
# ... 用户确认 ...

# 4. 调用脚本执行
python scripts/attendance_schedule_import.py \
  --group-id 123456 \
  --schedules '[{"userId":"user001","workDate":"2026-05-19","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-20","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-21","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-22","classId":789,"isRest":"N"},{"userId":"user001","workDate":"2026-05-23","classId":789,"isRest":"N"}]' \
  --confirm
```

### 示例 2: 给员工排休
**用户说**: "帮我把李四下周三排休"

```bash
# 1. 先查考勤组和关联班次（即使排休也需要确认考勤组）
dws attendance rules --date 2026-05-15 --format json
# → 拿到 groupId
dws attendance group get --group-id <groupId> --format json
# → 确认是排班制（TURN）

# 2. 获取员工 userId
dws aisearch person --keyword "李四" --dimension name --format json

# 3. 回显确认 → 用户确认 → 执行
python scripts/attendance_schedule_import.py \
  --group-id 123456 \
  --schedules '[{"userId":"user002","workDate":"2026-05-21","classId":0,"isRest":"Y"}]' \
  --confirm
```

### 示例 3: 部门批量排班
**用户说**: "帮我给研发部全员下周排早班"

```bash
# 1. 先查考勤组并获取关联班次（先查后排）
dws attendance rules --date 2026-05-15 --format json
# → 拿到 groupId
dws attendance group get --group-id <groupId> --format json
# → 从 groupVO.shiftVOList 中匹配"早班"，拿到 shiftId
# ⚠️ 禁止用 class search 全局搜索班次

# 2. 获取部门成员
dws contact dept search --query "研发部" --format json
dws contact dept list-members --ids <deptId> --format json

# 3. 回显确认 → 用户确认 → 执行
python scripts/attendance_schedule_import.py \
  --group-id 123456 \
  --schedules '[...]' \
  --confirm
```

## 配套脚本

| 脚本 | 用途 | CLI 参数 |
|------|------|---------|
| [attendance_schedule_import.py](../../scripts/attendance_schedule_import.py) | 排班导入（含校验、回显、执行） | `--group-id --schedules --confirm` |
| [attendance_schedule_export.py](../../scripts/attendance_schedule_export.py) | 排班查询导出（分批查询、排班表 Excel） | `--users --start --end [--output]` |

---

## 排班查询导出工作流

> 当用户提到"查看排班"、"排班表"、"导出排班"、"排班记录"时，走此工作流。
> **禁止直接调用 `dws attendance schedule get`**，必须通过脚本执行，脚本自动处理分批、姓名转换、班次名称转换、排班表格式输出。

### 查询导出 — 阶段 1: 参数收集

1. **员工范围**（必填）：需获取 userId 列表
   - 指定员工姓名 → `dws aisearch person` 获取 userId
   - 指定部门 → `dws contact dept search` + `dws contact dept list-members`
   - 指定考勤组全员 → `dws attendance group filtered-get --member`
   - 用户已给 userId 列表 → 直接使用
2. **日期范围**（必填）：开始日期 ~ 结束日期（YYYY-MM-DD）
   - 用户说"下周" → 计算下周一到周日
   - 用户说"本月" → 计算本月 1 日到月末
   - 任何缺失信息必须追问

### 查询导出 — 阶段 2: 调用脚本

```bash
python scripts/attendance_schedule_export.py \
  --users <userId1,userId2,...> \
  --start <YYYY-MM-DD> \
  --end <YYYY-MM-DD> \
  [--output <output_path.xlsx>]
```

参数说明：
- `--users`（必填）：userId 列表，逗号分隔
- `--start`（必填）：开始日期，格式 YYYY-MM-DD
- `--end`（必填）：结束日期，格式 YYYY-MM-DD
- `--output`（可选）：输出文件路径，默认 `attendance_schedule_<start>_<end>.xlsx`

脚本内部自动处理：
1. **分批查询**：超过 20 人自动分批调用 `dws attendance schedule get`
2. **班次名称转换**：classId → className（优先从记录中提取，缺失时回退 class search）
3. **姓名转换**：userId → 员工姓名
4. **排班表格式**：日历表（行=员工，列=日期，单元格=班次名称）
5. **Excel 输出**：钉钉风格美化排版

### 查询导出 — 阶段 3: 返回结果给用户

- 将脚本 stdout 输出的摘要信息（人数、日期、记录数、预览表格）原样转告用户
- 提醒用户完整排班表已导出到 Excel 文件
- 如果执行失败，将 stderr 错误信息转告用户

### 查询导出示例

**用户说**: "帮我导出研发组下周的排班表"

```bash
# 1. 获取考勤组成员
dws attendance group search --query "研发组" --format json
dws attendance group filtered-get --group-id <groupId> --member --format json

# 2. 调用脚本导出
python scripts/attendance_schedule_export.py \
  --users user001,user002,user003 \
  --start 2026-05-18 \
  --end 2026-05-24
```

**用户说**: "帮我查看张三和李四本月的排班"

```bash
# 1. 获取 userId
dws aisearch person --keyword "张三" --dimension name --format json
dws aisearch person --keyword "李四" --dimension name --format json

# 2. 调用脚本导出
python scripts/attendance_schedule_export.py \
  --users user001,user002 \
  --start 2026-05-01 \
  --end 2026-05-31
```
