# Dấu trang

> Nhập một lần tệp dấu trang xuất từ trình duyệt rồi đưa cùng bộ dấu trang vào nhiều hồ sơ — gộp hoặc thay thế, trong một thư mục hay toàn bộ — do chính trình duyệt của từng hồ sơ áp dụng.

Một bộ dấu trang là danh sách liên kết đọc từ tệp xuất dấu trang của trình duyệt. Bạn nhập tệp một lần vào thư viện của workspace, rồi xếp lượt cho bao nhiêu hồ sơ cũng được. Trình duyệt của mỗi hồ sơ áp dụng qua chính trình quản lý dấu trang của nó ở **lần mở kế tiếp**, trước khi phiên được báo sẵn sàng, và báo lại đúng những gì đã làm.

## Nhập tệp

Xuất dấu trang từ bất kỳ trình duyệt nào (Chrome, Edge, Brave, Firefox…: *Trình quản lý dấu trang → Xuất dấu trang*). Kết quả là tệp `bookmarks.html` theo định dạng Netscape. Máy chủ Browlix đọc tệp bằng trình phân tích HTML thật và chỉ giữ phần an toàn:

- chỉ liên kết `http://` và `https://` — bookmarklet (`javascript:`), `data:`, `file:`, `chrome:`, `place:` và mọi giao thức khác đều **bị loại kèm lý do**;
- địa chỉ chứa tên đăng nhập hoặc mật khẩu (`https://user:pass@…`) bị từ chối và không bao giờ được lưu;
- tiêu đề là chữ thuần; biểu tượng và mô tả trong tệp bị bỏ qua;
- thanh dấu trang vẫn là thanh dấu trang, phần còn lại thành *dấu trang khác*, thư mục được giữ (tối đa 10 cấp);
- tối đa 10 MB và 5.000 liên kết mỗi lần nhập; cùng một liên kết hai lần trong cùng thư mục chỉ giữ một.

```ts tab=TypeScript
import { readFile } from "node:fs/promises";

const imported = await bx.call("bookmarks.import", {
  body: { name: "Team links October", html: await readFile("./bookmarks.html", "utf8"), file_name: "bookmarks.html" },
});
const set = imported.data.set;
console.log(set.links, set.rejected_count); // what was kept, how many were refused
```

```json tab=MCP
{
  "name": "bookmarks_import",
  "arguments": { "name": "Team links October", "html": "<!DOCTYPE NETSCAPE-Bookmark-file-1>…" }
}
```

Nhập lại đúng danh sách đó sẽ trả về bộ đã có (`created: false`). `bookmarks.get` trả mọi liên kết (`root` bar/other, `path` thư mục, `title`, `url`) và tối đa 50 ví dụ liên kết bị loại kèm lý do: `unsafe_scheme`, `invalid_url`, `credentials`, `too_long`, `too_deep` hoặc `over_limit`.

## Đưa vào hồ sơ

Việc đưa vào là **so sánh-và-ghi theo từng hồ sơ**: đọc revision dấu trang của từng hồ sơ trước, rồi gửi lại làm `expected_rev`. Hồ sơ nào đổi dấu trang trong lúc đó (có lượt nhập khác được xếp hoặc đã áp dụng) được báo `conflict` và không được xếp lượt — đọc lại rồi quyết định.

```ts tab=TypeScript
const set = (await bx.call("bookmarks.list", {})).data.find((s) => s.name === "Team links October");
const profileIds = ["…", "…"];
const preview = await bx.call("bookmarks.preview", { body: { profile_ids: profileIds } });

if (set) {
  const queued = await bx.call("bookmarks.apply", {
    path: { id: set.id },
    body: {
      mode: "merge",
      scope: "folder",
      folder: "Team links",
      targets: preview.data.map((p) => ({ profile_id: p.profile_id, expected_rev: p.rev })),
    },
  });
  console.log(queued.data.queued, queued.data.conflicts, queued.data.not_found);
}
```

| Lựa chọn | Tác động trong mỗi hồ sơ |
|---|---|
| `mode: "merge"` | thêm liên kết chưa có trong thư mục đích; dấu trang hiện có giữ nguyên |
| `mode: "replace"` | xoá dấu trang trong vùng đích trước, rồi nhập danh sách |
| `scope: "folder"` | mọi liên kết vào một thư mục trên thanh dấu trang (`folder`, tạo mới nếu chưa có), giữ thư mục con của tệp |
| `scope: "all"` | thanh dấu trang và dấu trang khác đúng như trong tệp |

`replace` với `scope: "all"` sẽ xoá **mọi** dấu trang hiện có của các hồ sơ đó trước — ứng dụng web yêu cầu xác nhận rõ ràng trước khi gửi.

Tối đa 500 hồ sơ mỗi lần gọi; hồ sơ ngoài nhóm của bạn là `not_found`. Lượt đã xếp có thể rút bằng `bookmarks.cancel` cho tới khi một trình duyệt nhận nó.

## Máy chạy làm gì

Máy cần worker **0.16.0** trở lên. Worker cũ hơn không bao giờ nhận lượt nhập: chúng vẫn chờ (phiên hiển thị là đang chờ) cho tới khi hồ sơ mở trên máy đã cập nhật.

Khi hồ sơ mở, lệnh mở chỉ ghi từng lượt nhập đang chờ và số liên kết của nó; chỉ máy đang giữ đúng lệnh mở đó tải được danh sách liên kết. Trình duyệt mở trình quản lý dấu trang của chính nó ở nền, áp dụng các lượt theo thứ tự đã xếp (tối đa 8 lượt mỗi lần mở), đóng lại, rồi mới báo phiên sẵn sàng. `profiles.bookmarks` và `sessions.get` hiển thị số liệu do chính trình duyệt báo:

| Trạng thái | Ý nghĩa |
|---|---|
| `pending` | chờ lần mở kế tiếp của hồ sơ |
| `applying` | một trình duyệt đã nhận và đang áp dụng |
| `applied` | xong: `added`, `duplicates` (đã có sẵn) và `removed` (do thay thế) |
| `failed` | trình duyệt không áp dụng được; lý do nói vì sao (thay đổi đã làm vẫn được đếm) |
| `cancelled` | đã rút trước khi trình duyệt nhận |

Lượt nhập mà máy đã nhận nhưng không xác nhận — lệnh mở lỗi, hoặc máy dừng trước khi báo — quay lại `pending` và chạy ở lần mở sau. Gộp là idempotent: chạy lại không thêm gì đã có.

## Lỗi bạn sẽ gặp

| Mã | Khi nào |
|---|---|
| `FORBIDDEN` | nhập, đưa vào, rút hoặc xoá khi thiếu `bookmark.import` |
| `VALIDATION_FAILED` | không phải tệp xuất dấu trang, quá 10 MB, không còn liên kết http(s) nào, thiếu tên thư mục với `scope: "folder"`, hoặc quá 500 hồ sơ |
| `CONFLICT` | rút lượt mà trình duyệt đang áp dụng (hoặc đã kết thúc), hoặc xoá bộ mà hồ sơ vẫn đang chờ |
| `NOT_FOUND` | bộ dấu trang không có trong workspace này |

Mọi quyết định đều có trong nhật ký kiểm toán: `bookmark.import`, `bookmark.apply` (theo từng hồ sơ, kèm cách nhập, vùng đích và revision), `bookmark.cancel`, `bookmark.delete`.
