# App giữ trình duyệt

> Gắn tài khoản trong app của bạn với hồ sơ, giữ trình duyệt của hồ sơ trong lúc code làm việc, gia hạn, nhả mà không đóng trình duyệt và theo dõi sự kiện phiên.

Một app làm việc với nhiều tài khoản — đăng bài, kiểm tra hộp thư, thu thập dữ liệu — cần ba điều từ Browlix: hồ sơ nào thuộc tài khoản nào, một trình duyệt dùng được mà không ai giành mất giữa chừng, và cách nhả ra mà không bỏ phí trình duyệt đang mở cho việc kế tiếp. Liên kết (binding) và lượt giữ (hold) làm đúng việc đó, qua cùng API, công cụ MCP, SDK và quyền như mọi thứ khác.

Một chương trình hoàn chỉnh làm tất cả việc này — và phục hồi sau mất kết nối, khởi động lại — là [app mẫu](https://browlix.com/docs/sample-app).

## Gắn tài khoản với hồ sơ

Một **liên kết** nói rằng "tài khoản `acc-42` của app `fb-poster` dùng hồ sơ này". Liên kết không bao giờ chứa mật khẩu, cookie hay token — trạng thái đăng nhập đã nằm trong hồ sơ.

```ts tab=TypeScript
const b = await bx.call("bindings.create", {
  body: { app_id: "fb-poster", external_account_id: "acc-42", profile_id: profileId, label: "Shop page" },
});
```

```bash tab=curl
curl -X POST https://browlix.com/api/public/v1/bindings \
  -H "Authorization: Bearer $BROWLIX_API_KEY" -H "Content-Type: application/json" \
  -d '{"command_id":"'$(uuidgen)'","app_id":"fb-poster","external_account_id":"acc-42","profile_id":"'$PROFILE_ID'"}'
```

- Mỗi cặp (`app_id`, `external_account_id`) chỉ có một liên kết. Tạo lần hai trả `CONFLICT` kèm `binding_id` đang có; muốn đổi hồ sơ thì dùng `bindings.update`.
- `bindings.update` và `bindings.delete` cần `revision` bạn đọc gần nhất, để hai công cụ không ghi đè nhau.
- `metadata` tối đa 4 KiB dữ liệu của riêng bạn. Khoá trông giống thông tin đăng nhập (`password`, `token`, `cookie`, `secret`, `otp`, `auth`, `api_key`) bị từ chối.
- `bindings.list` trả từng liên kết kèm phiên đang chạy của hồ sơ và ai đang giữ.

Tạo, sửa, xoá liên kết cần quyền `profile.edit`; đọc cần `profile.read`. Phạm vi nhóm vẫn áp dụng: khoá của thành viên chỉ thấy vài nhóm thì chỉ thấy liên kết của các nhóm đó.

## Giữ trình duyệt

`sessions.acquire` mở hồ sơ trên một máy phù hợp — hoặc dùng lại trình duyệt nếu nó còn mở và rảnh — rồi trao cho app của bạn một **lượt giữ**.

```ts tab=TypeScript
const hold = await bx.acquireSession({
  appId: "fb-poster",
  externalAccountId: "acc-42",
  holderRef: "poster-worker-3",   // tên cố định của worker này
  ttlSeconds: 120,                // phải gia hạn trong khoảng này
  keepWarmSeconds: 300,           // giữ mở chừng này sau khi nhả
  wait: true,                     // trả về khi trình duyệt sẵn sàng
  onLost: () => abortCurrentJob(), // đã có lượt giữ mới hơn, hoặc phiên đã kết thúc
});
const a = await bx.attach(hold.sessionId);
// … điều khiển trang bằng Playwright (xem Gắn Playwright)
await hold.detach();
```

```bash tab=curl
curl -X POST https://browlix.com/api/public/v1/sessions/acquire \
  -H "Authorization: Bearer $BROWLIX_API_KEY" -H "Content-Type: application/json" \
  -d '{"command_id":"'$(uuidgen)'","app_id":"fb-poster","external_account_id":"acc-42","holder_ref":"poster-worker-3","ttl_seconds":120,"keep_warm_seconds":300}'
```

Gửi `profile_id` thay cho `app_id` + `external_account_id` nếu bạn không dùng liên kết. Thêm `device_id` để chọn máy; nếu không, Browlix chọn máy online ít việc nhất có cùng hệ điều hành với hồ sơ.

Kết quả là phiên kèm `lease` (`generation`, `holder_ref`, `expires_at`, …) và `acquired`:

| `acquired` | HTTP | Ý nghĩa |
|---|---|---|
| `opened` | 201 | Đã mở trình duyệt mới cho bạn |
| `reused` | 200 | Trình duyệt còn mở từ lượt giữ trước; bạn nhận nó với số thế hệ mới |
| `renewed` | 200 | Bạn đang giữ rồi (cùng khoá và `holder_ref`); lượt giữ được gia hạn |

### Khi người khác đang giữ

Mỗi hồ sơ có một trình duyệt và một người giữ tại một thời điểm. Nếu đang bận, acquire trả `BUSY` kèm `details.holder_kind`:

| `holder_kind` | Ai | `retryable` |
|---|---|---|
| `person` | Người đang dùng trên web (xem từ xa có quyền điều khiển) hoặc phiên mở không qua lượt giữ | không — hỏi họ |
| `automation` | Một luồng tự động hoá đang chạy | có, khi lượt chạy xong |
| `app` | Lượt giữ hiện tại của app khác | có, khi họ nhả hoặc hết hạn |
| `stopping` | Trình duyệt đang đóng | có, chờ chút |
| `other_machine` | Đang mở trên máy khác với `device_id` bạn yêu cầu | không |

Browlix không bao giờ cho biết người giữ kia là ai. `RECOVERY_REQUIRED` nghĩa là phiên trước kết thúc không chắc chắn — [phục hồi](https://browlix.com/docs/sessions#phuc-hoi) trước.

## Chặn bản cũ

Mỗi lượt giữ có một `generation` (số thế hệ). Mỗi lần người giữ khác nhận trình duyệt, số này tăng lên, nên một worker bị treo, mất mạng hay chạy quá giờ không thể thao tác thay cho trình duyệt nữa:

- `sessions.renew` và `sessions.detach` phải gửi đúng `generation` và `holder_ref` mà acquire trả về, bằng cùng khoá API.
- Số thế hệ cũ, lượt giữ đã hết hạn, đã nhả hoặc phiên đã kết thúc đều trả `FENCED` kèm `details.reason` (`superseded`, `expired`, `detached` hoặc `session_ended`).
- Khi nhận `FENCED`, ngừng đụng vào trình duyệt đó và acquire lại. Hàm `acquireSession` của SDK tự ngừng gia hạn và gọi `onLost`.

Gia hạn trước khi hết `ttl_seconds` (30–600, mặc định 120) — SDK gia hạn sau mỗi một phần ba khoảng đó. Lượt giữ không được gia hạn sẽ **hết hạn**: trình duyệt vẫn mở thêm `keep_warm_seconds`, sau đó tắt nếu không ai giữ lại.

## Nhả mà không đóng

`sessions.detach` kết thúc lượt giữ nhưng không bao giờ tự tắt trình duyệt. Nó vẫn mở thêm `keep_warm_seconds` (0–1800, mặc định 120, hoặc giá trị gửi kèm detach) để việc kế tiếp của cùng tài khoản chạy ngay. Hết khoảng đó trình duyệt tắt — trừ khi trong lúc đó có người giành quyền điều khiển hoặc một lượt tự động hoá nhận nó; khi đó trình duyệt tiếp tục chạy cho họ. Muốn đóng ngay, gửi `keep_warm_seconds: 0` hoặc gọi `sessions.stop`.

Browlix kiểm các lượt giữ hết hạn mỗi phút một lần, nên trình duyệt có thể mở quá thời gian giữ ấm tối đa khoảng một phút.

## Theo dõi sự kiện phiên

Webhook có thể bị lỡ khi máy nhận của bạn tắt. `sessions.events` là nhật ký đứng sau chúng: mọi thay đổi trạng thái phiên và mọi lượt giữ được nhận, nhả, hết hạn hay kết thúc, theo thứ tự id.

```ts tab=TypeScript
let after = loadCursor(); // id cuối cùng bạn đã xử lý, lần đầu là 0
const r = await bx.call("sessions.events", { query: { after, limit: 200 } });
for (const e of r.data as { id: number; kind: string; session_id: string }[]) {
  handle(e);           // id chỉ tăng; bỏ qua id đã gặp
  after = e.id;
}
saveCursor(after);
```

- Sự kiện được giữ 7 ngày. `gap: true` nghĩa là các sự kiện sau con trỏ của bạn đã bị xoá — đồng bộ lại bằng `sessions.list` và `bindings.list`.
- Lọc theo `session_id` hoặc `profile_id`. Đọc cần quyền `session.read`.

## Mỗi máy hỗ trợ gì

`devices.capabilities` liệt kê việc mỗi máy làm được lúc này — gắn CDP (luôn trên cùng máy), chạy ẩn, đồng bộ hồ sơ, khoá lưu trữ, xem từ xa, tự động hoá, điều khiển đồng loạt, AI agent và API tương thích AdsPower — kèm phiên bản worker cần có, sức chứa và số trình duyệt đang chạy. Hãy đọc nó thay vì đoán từ số phiên bản.

## Quyền

| Lệnh | Quyền |
|---|---|
| `bindings.list` | `profile.read` |
| `bindings.create`, `bindings.update`, `bindings.delete` | `profile.edit` |
| `sessions.acquire`, `sessions.renew`, `sessions.detach` | `session.start` |
| `sessions.events` | `session.read` |
| `devices.capabilities` | `device.read` |

Mọi lệnh ghi đều có nhật ký và nhận `command_id`, nên gửi lại sau khi mất phản hồi sẽ nhận đúng kết quả lần đầu ([lệnh và biên nhận](https://browlix.com/docs/commands-receipts)). Qua [MCP](https://browlix.com/docs/mcp), các lệnh này là công cụ `bindings_create`, `sessions_acquire`, `sessions_renew`, `sessions_detach`, `sessions_events`, v.v.
