# Slack 运维手册

> 适用范围:Slack 工作区成员同步、坐席申请审批、离职回收
> 最近更新:2026-04-23(首版 — migration 056 上线 Slack 集成 + `slack.seat_sync` 定时任务 + `manual_notify` 半自动流程)

---

## 零、为什么 Slack 跟 Zoom 不一样(必读)

Zoom 给 Helix 的是 Server-to-Server OAuth app,所有操作走官方 REST API,升档/创建/删除用户都能程序化做到 —— 流程可以完全自动。

Slack **不是**。Slack 用户生命周期的 API 是 **SCIM**,而 **SCIM 只在 Slack Business+ 或 Enterprise Grid 档位开放**,Pro/Free 工作区调 SCIM 接口一律返回 `403 plus_teams_only`。PingCAP 的 pingcap.slack.com **当前在 Pro 档**。结果是:

| 操作 | Pro 档(现状) | Business+ / Enterprise Grid |
|------|---------------|-----------------------------|
| 列成员 (`users.list`) | ✅ Bot Token 即可 | ✅ |
| 给成员发 DM (`chat.postMessage`) | ✅ Bot Token 即可 | ✅ |
| **创建/停用成员** | ❌ 无任何 API 可用 | ✅ SCIM v1 |

所以 Helix 的 Slack 集成分两条路:

- **Pro 档**(今天):坐席申请审批通过后,Helix **通过飞书 DM 通知 IT 管理员**,IT 管理员到 Slack 后台手动邀请/停用,完成后回到 Helix `/admin/provisioning` 点「完成」。离职同理。这叫 **manual_notify 执行模式**。
- **Business+ / Enterprise Grid**(未来):把 Slack 集成绑定里的 `execution_mode` 从 `manual_notify` 改成 `auto_api`(UI 一键切换,无需改代码)。Helix 就直接通过 SCIM v1 创建/停用用户,和 Zoom 一样全自动。

同步(`slack.seat_sync`)在两种档位下都能跑,因为它只调 `users.list`,Bot Token 就够。

---

## 一、Slack 集成在 Helix 里的全貌

```
 ┌───────────────────────────────────┐      ┌───────────────────────────────┐
 │ integrations                      │      │ asset_software_catalog        │
 │   slug = 'slack-primary'          │◄─┐   │   name = 'Slack'              │
 │   kind = software_provisioning    │  │   │   offboarding_policy          │
 │   provider = 'slack'              │  │   │     = 'auto_revoke'           │
 │   secrets = {bot_token,           │  │   │   sso_provider = 'google'     │
 │              user_token?}         │  │   └──────────┬────────────────────┘
 └───────────────────────────────────┘  │              │
                                        │              │
                           ┌────────────┴──────────┐   │
                           │ software_connectors   │   │
                           │   binding_config:     │◄──┘
                           │     execution_mode    │
                           │       = manual_notify │    ◄── Pro 档位必选
                           │     action_on_offboard│
                           │       = deactivate    │
                           └────────────┬──────────┘
                                        │
                           ┌────────────▼──────────┐
                           │ software_license_plans│
                           │   name = 'Slack Full Member'
                           │   over_quota_policy   │
                           │     = 'reject'        │    ◄── 用户决定:不做 LRU
                           │   max_seats = NULL    │    ◄── 不做硬约束
                           └──────────┬────────────┘
                                      │
                           ┌──────────▼────────────┐
                           │ software_seat_allocations
                           │ (每个员工一行)        │
                           └───────────────────────┘

        ▲                     ▲
        │                     │
 每天 03:00 UTC        申请通过 / 离职触发 grant|revoke 任务
 scheduled_jobs                     ↓
   slack.seat_sync          execution_mode=manual_notify
   (users.list → 镜像)      → 任务状态 NOTIFIED
                            → 飞书 DM 给 IT 管理员
                            → IT 到 Slack 手动操作
                            → 回 Helix 点「完成」→ 状态 COMPLETED
```

**关键不变量**:
- Slack 坐席台账由 `slack.seat_sync` 每天核对一次现实(谁还在 workspace),出现漂移(有人没走审批就被邀请 / IT 忘记手动操作)会自动纠正状态。
- `offboarding_policy='auto_revoke'`:员工离职时**必须**主动回收 Slack 坐席(不像 Zoom 的 sso_cascade 那样躺平 — Slack 不能光靠 Google SSO 失效)。Pro 档下回收任务也走 `manual_notify`,IT 管理员收到飞书 DM 手动 deactivate。
- `max_seats=NULL` + `over_quota_policy='reject'`:用户明确说**不做 LRU / 不硬卡配额**。Slack 坐席计费是按月、按活跃用户,企业整体用量由 IT/财务管。

---

## 二、首次接入(本地 + 生产一样的步骤)

### 1. 创建并配置 Slack App

1. 以 workspace owner/admin 登录 https://api.slack.com/apps
2. Create New App → From scratch → 名称 `Helix IT Platform`,workspace 选 pingcap。
3. 左侧 **OAuth & Permissions**。
4. **Bot Token Scopes**(必需)—— 加上:
   - `users:read` — 读成员列表
   - `users:read.email` — 读成员邮箱(匹配 identity_persons.work_email)
   - `chat:write` — 发 DM 给 IT 管理员(可选,当前通知走飞书,但将来要是切到 Slack DM 就要用这个)
   - `channels:read` — 读频道列表(可选,用于后续扩展)
5. **User Token Scopes**(只有升 Business+ 才生效,但现在加好是为了之后切 `auto_api` 时不用重新 reinstall):
   - `admin` — 覆盖 SCIM 需要的所有权限
6. 滚到顶部,点 **Install to Workspace**,用 workspace owner 账号授权。
7. 装完后在 OAuth & Permissions 顶部复制两个 token:
   - **Bot User OAuth Token**(`xoxb-...`) → 填 `HELIX_SLACK_BOT_TOKEN`
   - **User OAuth Token**(`xoxp-...`) → 填 `HELIX_SLACK_USER_TOKEN`(可选,Pro 档填了也不报错但用不上)
8. **不要开 Token Rotation**(OAuth & Permissions 页面有个开关,默认关闭)。开了 token 每 12h 过期要走 refresh_token 流程,Helix 目前没实现。

### 2. 把凭据写到 Helix

**方法 A — 跑一次脚本(推荐,幂等、可重复)**:

```bash
# 本地
cd server && uv run python scripts/seed_slack_integration.py --probe --enable

# 生产 (AWS EC2 或 K8s pod)
export HELIX_SLACK_BOT_TOKEN=xoxb-...
export HELIX_SLACK_USER_TOKEN=xoxp-...     # 可选
export HELIX_SLACK_SCIM_BASE_URL=          # 留空用默认,Enterprise Grid 自定义 endpoint 才填
uv run python scripts/seed_slack_integration.py --probe --enable
```

`--probe` 会调 Slack `auth.test`(bot token)+ SCIM 探测(如果给了 user_token)。如果 SCIM 返回 `plus_teams_only`,probe **仍然 OK**,但消息里会告诉你 SCIM 不可用,这是当前 Pro 档位的正常状态。

**方法 B — UI 填**:
`/admin/integrations` → 找到「Slack」 → 点编辑 → 填 `bot_token` / `user_token` → 保存 → 「Enable」。

### 3. 启用定时同步任务

`/admin/scheduled-jobs` → 找到 `slack.seat_sync` → Enable → Run now 做一次回填。

第一次跑:把 pingcap.slack.com 里所有 full member 对应到 identity_persons,建对应 `software_seat_allocations(status='active')`。Guest / Bot / 已停用用户会被过滤掉。

### 4. 验证

```bash
# 本地
docker exec helix-mysql-1 mysql -u helix -phelix helix -e "
  SELECT COUNT(*) AS active_slack_seats
  FROM software_seat_allocations sa
  JOIN software_license_plans p ON p.id = sa.plan_id
  JOIN asset_software_catalog s ON s.id = p.software_id
  WHERE s.name='Slack' AND sa.status='active';
"

# 生产
# 走 AWS SSM → EC2 → docker exec 同样的 SQL,或者直连 TiDB (见 CLAUDE.md 里的 prod access 说明)
```

---

## 三、员工申请坐席的日常流程

1. **员工** 打开 `/me/software-catalog`,找到「Slack」,点「申请坐席」。
2. 选 `Slack Full Member` plan,写申请理由,提交。
3. 如果配了飞书审批(`integration_approval_flows` 有 `software_seat` 条目),Helix 自动把申请推到飞书 审批。否则走 Helix 内的审批(管理员在 `/admin/approvals` 人工点通过)。
4. Lead 审批通过 → 飞书 webhook 回调 Helix → Helix:
   - 把 `software_seat_allocations.status` 从 `pending` 改成 `active`
   - 创建 `provisioning_task(action=grant, target=seat, execution_mode=manual_notify)`
   - 任务状态被 worker 翻成 `NOTIFIED`
   - **飞书 DM 给 IT 管理员**(所有有 `it_admin` 或 `global_admin` 角色的活跃员工):
     > [Helix] 请手工分配 Slack 坐席: 张三
     > 员工 张三 (zhangsan@pingcap.com) 的 Slack - Slack Full Member 坐席需要您手动分配:
     >   · 操作: 在外部系统中分配该用户的账号
     >   · 任务号: <uuid>
     >   · 完成后: 打开 {portal}/admin/provisioning 找到该任务,点「完成」
5. **IT 管理员** 收到 DM →
   1. 到 pingcap.slack.com/admin/invites 邀请 zhangsan@pingcap.com(普通 member)。
   2. 回到 Helix `/admin/provisioning`,找到该任务,点「完成」(可选填操作备注)。任务状态 → `COMPLETED`。
6. 员工当天可以用公司邮箱登录 Slack。

**如果 IT 忘记点「完成」**:没关系。次日凌晨 03:00 UTC 的 `slack.seat_sync` 会发现 Slack 那边确实已经有这个人的 membership,但没有对应 `provisioning_task=completed` — 这是允许的漂移,台账是按 `software_seat_allocations.status=active` 判断的,sync 拉到 Slack 里就补/保留 active 那行。也就是说 Helix 台账始终跟 Slack 现实一致,provisioning_task 的状态只是 IT 工作跟踪。

---

## 四、员工离职时的回收流程

1. HR 在 `/admin/lifecycle` 触发离职,或者 HRIS 同步自动标记。
2. Helix 扫员工所有 `software_seat_allocations(status=active)`:
   - Slack seat (`offboarding_policy='auto_revoke'`):创建 revoke 任务
   - 由于 Slack 绑定 `execution_mode='manual_notify'`,任务进 `NOTIFIED` 状态
   - **飞书 DM 给 IT 管理员**:请到 pingcap.slack.com 停用 xxx@pingcap.com
3. IT 管理员 deactivate 用户(pingcap.slack.com/admin → 找到人 → Deactivate),回 Helix 点「完成」。
4. `slack.seat_sync` 次日核对确认该人已不在 full member 列表,`software_seat_allocations.status` 改为 `revoked`。

**注意**:Google SSO 被 revoke 后用户**仍然能用之前生成的 Slack session 登录**一段时间,直到 Slack 重新要求 SSO 验证(默认 14 天)。所以光靠 SSO 失效**不够**,必须 auto_revoke 主动 deactivate。这是 Slack 和 Zoom 不一样的地方。

---

## 五、什么时候从 manual_notify 切到 auto_api

当 PingCAP 升级到 Slack Business+ 或 Enterprise Grid 后:

1. 确认 SCIM 可用:`/admin/integrations` → Slack → Test Connection,应该看到 `SCIM: available`。
2. `/admin/integrations` → Slack → Bindings → 点编辑 → `execution_mode` 改成 `auto_api` → 保存。
3. 之后所有新的 grant/revoke 任务都会自动走 SCIM:
   - 员工申请通过 → 10 秒内 worker 自动邀请到 Slack
   - 离职 → 10 秒内自动 deactivate
4. **不需要重新跑 seed 脚本,不需要改代码。**

如果切过去后 SCIM 出错(例如 User Token 被 rotate / 权限变更),单任务会标 FAILED,IT 管理员可以重试或 cancel。

---

## 六、常见故障排查

### 6.1 `/admin/integrations` Test Connection 返回 `invalid_auth`

Bot token 过期 / 被 rotate / workspace 账号停用。重新生成 Bot Token(OAuth & Permissions → Reinstall to Workspace),更新 `HELIX_SLACK_BOT_TOKEN` 后跑 `seed_slack_integration.py --enable`。

### 6.2 Test Connection 显示 `plus_teams_only`

正常。Pro 档没 SCIM。绑定保持 `manual_notify` 即可。

### 6.3 IT 管理员没收到飞书 DM

可能原因:
- IT 管理员的 `identity_persons.work_email` 在飞书通讯录里找不到对应人(邮箱不对)。看 `notification_outbox` 表 `failed_channels` 字段确认。
- 飞书 App 没配 / app_secret 错。看 `/admin/integrations` 的「飞书」那条有没有 enabled。

排查 SQL:
```sql
SELECT id, channels, status, delivered_channels, failed_channels, context
FROM notification_outbox
WHERE template_key='provisioning.manual_notify'
ORDER BY created_at DESC LIMIT 10;
```

### 6.4 `slack.seat_sync` 报错 `slack integration not configured or not enabled`

去 `/admin/integrations` 启用 Slack 集成(enabled=1)。或跑 seed 脚本带 `--enable`。

### 6.5 同步把真实员工的坐席误标成 revoked

看 `software_seat_allocations.reason` 里 `slack.seat_sync` 的注脚,再对比 Slack 实际 profile — 多数情况是:
- 员工 Slack profile 里邮箱跟 Helix identity_persons.work_email 不匹配(大小写、域名别名等)
- 员工被设成 single-channel guest,被同步 filter 掉了

同步用 `LOWER(email)` 对比,理论上不应有大小写问题。其他分类差异建议让 IT 在 Slack 把 profile 统一到公司邮箱。

---

## 七、测试入口

```bash
# 单元 (离线,mock HTTP)
cd server && uv run pytest tests/test_integrations/test_slack_driver.py -v

# 同步(跑在真实 DB 上,桩驱动)
cd server && uv run pytest tests/test_core/test_slack_seat_sync.py -v

# 绑定 execution_mode 覆盖的覆盖 (Slack 是首个用例)
cd server && uv run pytest tests/test_core/test_provisioning_coordinator.py -k binding_manual_notify -v
```

线上真实探测脚本:
```bash
cd server && uv run python scripts/seed_slack_integration.py --probe
# 看输出第一行,bot token 能不能 auth;第二行 SCIM 是不是 plus_teams_only(正常)
```

---

## 八、相关代码位置

| 组件 | 路径 |
|------|------|
| 驱动(bot token + 可选 SCIM) | `server/app/integrations/drivers/slack_software.py` |
| 同步处理器 | `server/app/scheduled_jobs/slack_handlers.py` |
| Seat Coordinator | `server/app/core/coordinators/slack_seat_coordinator.py` |
| Migration | `server/alembic/versions/056_seed_slack_integration.py` |
| Seed 脚本 | `server/scripts/seed_slack_integration.py` |
| 通用的 manual_notify 飞书通知 hook | `ProvisioningService._notify_manual_assignees` in `server/app/domains/provisioning/service.py` |
| 绑定 `execution_mode` 覆盖 | `_binding_execution_mode` in `server/app/core/coordinators/provisioning_coordinator.py` |
| 驱动测试 | `server/tests/test_integrations/test_slack_driver.py` |
| 同步测试 | `server/tests/test_core/test_slack_seat_sync.py` |
