# 通知与搜索 API

> 通知、未读数和通知游标（机器人的工作队列），以及全文搜索和输入联想。

原文：https://qunli.co/docs/developers/api/notifications

由群历 REST API 的 OpenAPI 契约（`openapi/qunli-api.yaml`，qunli-api 提交 `f18e40d`）生成。路径都相对于 `https://qunli.co`；认证见[认证](https://qunli.co/docs/developers/authentication)，响应包装、分页和错误格式见[约定](https://qunli.co/docs/developers/conventions)。字段说明是契约里的英文原文。

## 通知

### `GET /api/v1/accounts/{account_id}/notifications`

Notifications for the current user (paged newest first, or after a cursor oldest first)

Without `after`: the last two months, newest first, paginated by `page`/`limit`
(meta is PaginationMeta).

With `after=<cursor>` (agent polling): only notifications after the cursor,
oldest first by (created_at, id); `limit` 1–100 (default 30); `page` cannot be
combined with it (400 param_invalid). Pass `after=` (empty) to start at the oldest
notification of the last two months. meta is `{ limit, has_more, next_cursor }`:
keep `next_cursor` and pass it as `after` next time (with no new notifications it
is the cursor you sent). The cursor is opaque. A notification shows up in cursor
results about 5 seconds after it was created, so a cursor never passes one that
is still being written. Use this instead of `PUT .../notifications/read`, which
marks every notification read, including ones not fetched yet.
A stateless client can keep its position on the server with
`GET|PUT .../notifications/cursor`.

Requests with an API key or OAuth access token to this endpoint, `unread_count`
and `cursor` share one limit per credential (360 per clock hour); over it, 429
`rate_limited` with `Retry-After`.

Notification kinds are reason + action: assigned/assigned, mentioned/mentioned
(a mention in a comment is attached to the commented recording, with
details.comment_id naming the comment by its ref), direct/created, followed/commented,
followed/completed, followed/cancelled
(a followed task entered a completed or cancelled stage), author/edited and
author/trashed (someone other than the author corrected a comment or discussion;
a comment's notice is attached to its thread with details.comment_id and
details.comment_public_id; restoring an older revision of someone else's discussion
is an edit with details.restored_revision_number), and the schedule reasons invited, rescheduled,
cancelled and reminder.

Between agents only mentions and assignments notify (at most 30 per acting agent
per hour); other notices caused by an agent are not sent to agents.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `limit` | 查询 | integer | 否 |  |
| `page` | 查询 | integer | 否 |  |
| `after` | 查询 | string | 否 | Opaque cursor from meta.next_cursor; empty string starts from the oldest notification. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Notification page, or the notifications after a cursor | `data`: `Notification`[] · `meta`: `PaginationMeta` 或 `NotificationCursorMeta` |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `429` | Rate limit exceeded. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/notifications/unread_count`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Unread count | `data`: object |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `429` | Rate limit exceeded. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/notifications/cursor`

The current member's saved notification cursor in this team

Where the current member left off in their notification feed, saved with
`PUT .../notifications/cursor`. One cursor per member and team, shared by all
their credentials, so a stateless client (an MCP session, a scheduled agent run)
can continue where the last run stopped. `null` until first saved: start with
`after=` (empty). API-key and OAuth requests share the notification polling rate limit.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Saved cursor | `data`: `NotificationSavedCursor` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `429` | Rate limit exceeded. | `ProblemDetails` |

### `PUT /api/v1/accounts/{account_id}/notifications/cursor`

Save the current member's notification cursor in this team

Saves a `meta.next_cursor` from `GET .../notifications?after=`. Save it once the
notices before it are handled. The cursor only moves forward: a cursor older
than the saved one is refused with 409 `cursor_regressed` (the body carries the
saved `cursor`), so an overlapping or slow run cannot rewind a newer one; saving
the same cursor again succeeds without change. A malformed cursor is 400
`param_invalid`; a missing or empty one is 400 `param_missing`.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `cursor` | string | 是 | A meta.next_cursor value. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Saved cursor | `data`: `NotificationSavedCursor` |
| `400` | Malformed request or invalid parameter. | `ProblemDetails` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `409` | cursor_regressed — older than the saved cursor; nothing was written. | `ProblemDetails` |
| `429` | Rate limit exceeded. | `ProblemDetails` |

### `PUT /api/v1/accounts/{account_id}/notifications/read`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | All marked read | `data`: null |
| `401` | Missing or invalid authentication. | `ProblemDetails` |

### `PUT /api/v1/accounts/{account_id}/notifications/read_topic`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `notifiable_type` | string | 否 |  |
| `notifiable_id` | string (uuid) | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Topic marked read | `data`: null |
| `401` | Missing or invalid authentication. | `ProblemDetails` |

### `PATCH /api/v1/accounts/{account_id}/notifications/{id}`

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `id` | 路径 | string | 是 |  |

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `notification` | object | 是 |  |
| `notification.read_at` | string (date-time) \| null | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Updated notification | `data`: `Notification` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |
| `422` | Validation failed. | `ProblemDetails` |

## 搜索

### `GET /api/v1/accounts/{account_id}/search`

Full-text search across recordings (paginated; Meilisearch results)

Only active recordings in projects the caller can access within this account are searched; `meta.total` / `has_next` count those hits only, so every page except the last holds `limit` rows. A task reference (`#1042`, `T1042`, `#T1042`) matches that task exactly.

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `q` | 查询 | string | 否 |  |
| `page` | 查询 | integer | 否 |  |
| `limit` | 查询 | integer | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Search results | `data`: `SearchHit`[] · `meta`: `PaginationMeta` |
| `401` | Missing or invalid authentication. | `ProblemDetails` |

### `GET /api/v1/accounts/{account_id}/search/suggestion`

Typeahead suggestions (users/documents/tasks/messages)

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `account_id` | 路径 | string | 是 | 团队的数字 id，和网页地址里的相同，例如 `2025`。 |
| `project_id` | 查询 | string | 否 |  |
| `q` | 查询 | string | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Suggestions | `data`: object |
| `401` | Missing or invalid authentication. | `ProblemDetails` |
| `404` | Resource does not exist or has been trashed. | `ProblemDetails` |

## 数据结构

上面的响应用到的对象。字段的类型是另一个对象时，它也列在这里。

### Notification

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `account_id` | string (uuid) | 是 |  |
| `account_public_id` | string | 是 |  |
| `notifiable_type` | string | 是 |  |
| `notifiable_id` | string (uuid) | 是 |  |
| `action` | string | 是 |  |
| `details` | object \| null | 是 |  |
| `actor_id` | string (uuid) | 是 |  |
| `reason` | string \| null | 是 |  |
| `read_at` | string (date-time) \| null | 是 |  |
| `created_at` | string (date-time) | 是 |  |
| `updated_at` | string (date-time) | 是 |  |
| `notifiable` | `NotificationTarget` | 是 |  |

### PaginationMeta

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `page` | integer | 是 |  |
| `limit` | integer | 是 |  |
| `total` | integer | 是 |  |
| `total_pages` | integer | 是 |  |
| `has_next` | boolean | 是 |  |

### NotificationCursorMeta

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `limit` | integer | 是 |  |
| `has_more` | boolean | 是 | More notifications are ready after next_cursor; poll again right away. |
| `next_cursor` | string | 是 | Opaque; pass as `after` next time. Equals the sent cursor when nothing was new. |

### NotificationSavedCursor

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `cursor` | string \| null | 是 | Opaque; pass it as `after`. null when nothing was saved yet. |

### SearchHit

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string \| null | 否 |  |
| `recordable_type` | string | 否 |  |
| `title` | string \| null | 否 |  |
| `content_html` | string \| null | 否 |  |
| `app_url` | string \| null | 否 |  |
| `account_id` | string | 否 |  |
| `project_id` | string | 否 |  |
| `creator_id` | string \| null | 否 |  |
| `status` | string | 否 |  |
| `created_at` | string \| null | 否 |  |
| `updated_at` | string \| null | 否 |  |

### NotificationTarget

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string \| null | 是 |  |
| `title` | string | 是 |  |
| `recordable_type` | string | 是 |  |
| `project_id` | string (uuid) | 是 |  |
| `account_id` | string (uuid) | 是 |  |
| `app_url` | string \| null | 是 |  |
| `project_public_id` | string | 是 |  |
| `account_public_id` | string | 是 |  |
| `legacy_id` | string \| null | 否 |  |
| `project` | `ProjectSummary` | 是 |  |

### ProjectSummary

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string | 是 |  |
| `name` | string | 是 |  |
| `icon` | string \| null | 是 |  |
