# Idle Detection API

> IdleDetector 报告用户处于活跃还是空闲、屏幕是否锁定。本页说明 idle-detection 权限模型、60 秒最小阈值、规范定义的全部异常，并给出带回退分支的可运行示例。

`IdleDetector` 告诉页面用户是否在你指定的阈值（至少一分钟）内与设备有过交互，以及屏幕是否已锁定；任一答案翻转时触发 `change` 事件。它是受 `idle-detection` 权限保护的 WICG 草案，用途是聊天应用的在线状态指示，以及在无人观看时暂停高开销工作。

支持仅限 Chromium：桌面与 Android 上的 Chrome 94，Edge 94 到 96 以及 Edge 114 起（中间版本移除了该接口），Opera 与 Samsung Internet 在对应的 Chromium 版本中支持。Firefox 与 Safari 的任何版本都没有实现（BCD `api.IdleDetector`）。

## 语法

```js
new IdleDetector()

IdleDetector.requestPermission()

idleDetector.start()
idleDetector.start(options)
```

构造函数不接受参数。`requestPermission()` 是静态方法，仅 `[Exposed=Window]`，返回 `Promise<PermissionState>`，兑现为 `"granted"`、`"denied"` 或 `"prompt"`。`start()` 返回 `Promise<undefined>`，检测器进入 `"started"` 状态后兑现。整个接口带 `[SecureContext]`，并且也暴露在专用 Worker 中：那里可以调用 `start()`，但不存在 `requestPermission()`。

## 参数

`start()` 接受一个可选的 `options` 参数，类型为 `IdleOptions` 字典。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `threshold` | `unsigned long long`（`[EnforceRange]`） | 否 | 无交互多少毫秒后 `userState` 变为 `"idle"`。最小值 60,000，更小的值会拒绝 Promise。负数或非有限值因 `[EnforceRange]` 在绑定层抛出 `TypeError`。 |
| `signal` | `AbortSignal` | 否 | 中止该信号会停止检测器，并以信号的中止原因拒绝尚未完结的 `start()` Promise。 |

`start()` 兑现后，两个只读属性承载状态：`userState` 为 `"active"` 或 `"idle"`，`screenState` 为 `"locked"` 或 `"unlocked"`。第一次 `start()` 完成前两者都是 `null`，所以要在 `change` 处理函数内或 `await idleDetector.start()` 之后读取，不要在 `new IdleDetector()` 之后同步读取。

## 异常

规范定义了以下拒绝情形，顺序即算法的检查顺序。

| 方法 | 异常 | 条件 |
|---|---|---|
| `requestPermission()` | `InvalidStateError` | 文档不是 fully active。 |
| `requestPermission()` | `NotAllowedError` | 调用时没有瞬时用户激活；权限提示只能跟在点击或按键之后。 |
| `start()` | `InvalidStateError` | 文档不是 fully active，或检测器的内部状态不是 `"stopped"`（已在启动中或已启动）。 |
| `start()` | `NotAllowedError` | 文档不允许使用 `idle-detection` 这一受策略控制的特性（默认允许列表为 `'self'`），或启动检测器时 `idle-detection` 权限状态为 `"denied"`。 |
| `start()` | `TypeError` | `threshold` 小于 60,000 ms。 |
| `start()` | 信号的中止原因 | 调用 `start()` 时 `options.signal` 已中止，或 Promise 待定期间被中止。 |

Chromium 有一处偏离规范：Permissions Policy 拦截该特性时，`start()` 抛出 `SecurityError`，而不是以 `NotAllowedError` 拒绝（见实测框）。浏览器端监视器断开时，Chromium 还会以 `NotSupportedError` 和消息 `Idle detection not available.` 拒绝 `start()`，规范没有建模这一情形。

:::observed
在 Chrome 中，用户手势之外调用 `await IdleDetector.requestPermission()` 会以 `NotAllowedError: Must be handling a user gesture to show a permission request.` 拒绝；在手势内调用时，权限气泡显示 `<origin> wants to know when you're actively using this device`（英文界面）。`idleDetector.start({ threshold: 30_000 })` 抛出 `TypeError: Minimum threshold is 1 minute.`，同一实例第二次 `start()` 抛出 `InvalidStateError: Idle detector is already started.`，权限被拒绝时以 `NotAllowedError: Idle detection permission denied` 拒绝。这些字符串来自 Chromium 的 [`idle_detector.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/idle/idle_detector.cc)（chromium.googlesource.com）、[`idle_manager.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/idle/idle_manager.cc)（chromium.googlesource.com）与 [`permissions_strings.grdp`](https://chromium.googlesource.com/chromium/src/+/main/components/permissions_strings.grdp)（chromium.googlesource.com）。
:::

## 示例

每个示例都先检测构造函数是否存在，在 `IdleDetector` 为 undefined 的 Firefox 与 Safari 中回退到页面自身的活动信号（`visibilitychange`、输入事件）。

### 一分钟无操作后显示「离开」在线状态徽标

权限请求在 `click` 处理函数内运行；检测器在 Promise 兑现后启动，仍在同一个任务内，不会丢失激活。API 缺失时，回退逻辑在标签页隐藏时把用户标记为离开，这是那些浏览器能给出的唯一信号。

```js
const badge = document.querySelector('#presence');
const enable = document.querySelector('#enable-presence');

function markAway(isAway) {
  badge.textContent = isAway ? '离开' : '活跃';
}

async function startPresence() {
  if (!('IdleDetector' in window)) {
    document.addEventListener('visibilitychange', () => {
      markAway(document.visibilityState === 'hidden');
    });
    return 'fallback';
  }

  const state = await IdleDetector.requestPermission();
  if (state !== 'granted') return 'denied';

  const detector = new IdleDetector();
  detector.addEventListener('change', () => {
    markAway(detector.userState === 'idle' || detector.screenState === 'locked');
  });
  await detector.start({ threshold: 60_000 });
  markAway(false);
  return 'started';
}

enable.addEventListener('click', async () => {
  try {
    enable.hidden = (await startPresence()) !== 'denied';
  } catch (err) {
    console.error(`${err.name}: ${err.message}`);
  }
});
```

返回 `'denied'` 时按钮保持可见，用户修改站点设置后可以重试；再次调用 `requestPermission()` 会直接兑现已存储的决定，不再弹出提示。

### 用户退出登录时用 AbortSignal 停止检测器

没有 `stop()` 方法。结束监视的唯一方式是传给 `start()` 的 `AbortSignal`，它同时释放浏览器端的监视器。`start()` 兑现之后再中止不会拒绝任何东西；`start()` 待定期间中止则以中止原因拒绝该 Promise，所以下面的 `catch` 要区分主动中止与真正的失败。

```js
const controller = new AbortController();

async function watchUntilSignOut(detector) {
  try {
    await detector.start({ threshold: 120_000, signal: controller.signal });
  } catch (err) {
    if (err.name === 'AbortError') return; // start() 完结前已退出登录
    throw err;
  }
}

document.querySelector('#sign-out').addEventListener('click', () => {
  controller.abort();
});
```

`controller.abort()` 之后检测器回到 `"stopped"` 状态，同一实例可以带着新的信号再次启动；旧的 `userState` 与 `screenState` 值不再更新。

## 另请参阅

- [Screen Wake Lock API](/zh/reference/capabilities/wake-lock/)，与之互补的「阻止屏幕锁定」请求
- [Web Locks API](/zh/reference/capabilities/web-locks/)，用户空闲后协调多个标签页之间的工作
- [Firefox 上的 PWA](/zh/reference/platforms/firefox/)
- [Idle Detection API: start() method](https://wicg.github.io/idle-detection/#api-idledetector-start)（wicg.github.io）
- [Idle Detection API: requestPermission() method](https://wicg.github.io/idle-detection/#api-idledetector-requestpermission)（wicg.github.io）
- [IdleDetector browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/IdleDetector#browser_compatibility)（developer.mozilla.org）