---
url: https://docs.ogmabox.com/vi/plugins/README.md
description: >-
  Phát triển, đóng gói, cài đặt, bật và phân phối plugin Ogma với logic backend,
  bảng frontend, lệnh, quyền và siêu dữ liệu chợ plugin.
---

# Hệ thống plugin Ogma {#ogma-plugin-system}

Plugin Ogma mở rộng công cụ bằng logic backend tùy chỉnh, bảng giao diện frontend và các bước quy trình. Plugin được cài đặt cục bộ từ một thư mục trên đĩa, bật theo từng dự án và chạy trong môi trường cách ly bằng sandbox.

Tài liệu này là nguồn tham khảo chính cho người viết plugin.

Để bắt đầu nhanh nhất có thể, xem [Bắt đầu nhanh với plugin](/vi/plugins/quickstart).

***

## Bắt đầu nhanh {#quick-start}

### Plugin backend tối giản {#minimal-backend-plugin}

```
my-plugin/
  manifest.json
  backend/script.js
```

`manifest.json`:

```json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "plugins": [
    {
      "kind": "backend",
      "id": "my-plugin-backend",
      "entrypoint": "backend/script.js"
    }
  ]
}
```

`backend/script.js` (ES2020; có thể dùng import khi dùng công cụ tạo bundle):

```js
async function init(sdk) {
  sdk.console.log("my-plugin started");

  sdk.events.onInterceptResponse(function(req, res) {
    if (res.getCode() === 403) {
      sdk.console.warn("403 on " + req.getUrl());
    }
  });
}
```

Như vậy là đủ để có một plugin chỉ có backend hoạt động. Giữ plugin trong một tệp duy nhất tại `backend/script.js` và cho `manifest.json` trỏ trực tiếp đến tệp đó.

### Quy trình biên dịch trong một phút (mã nguồn TypeScript) {#one-minute-build-flow-typescript-source}

Nếu bạn viết TypeScript, dùng cấu trúc sau:

```text
my-plugin/
  manifest.json
  backend/
    src/index.ts
```

Biên dịch:

```bash
pnpm add -D @ogmabox/ogma-sdk esbuild typescript
pnpm exec esbuild backend/src/index.ts --bundle --format=iife --platform=neutral --external:@ogma/sdk --external:@ogmabox/ogma-sdk --outfile=backend/script.js
```

Cài đặt: **Plugin > Cài đặt**, chọn thư mục `my-plugin/`. Sau đó bật plugin.

***

## Tài liệu tham khảo manifest {#manifest-reference}

`manifest.json` nằm trong thư mục gốc của gói. Tất cả các trường đều phân biệt chữ hoa và chữ thường.

### Các trường cấp cao nhất {#top-level-fields}

| Trường | Bắt buộc | Kiểu | Ghi chú |
|-------|----------|------|-------|
| `id` | có | string | Chỉ gồm chữ thường, chữ số và dấu gạch nối. Tối đa 64 ký tự. Duy nhất trong số các plugin đã cài đặt. |
| `version` | có | string | Semver: `MAJOR.MINOR.PATCH` |
| `name` | không | string | Tên hiển thị trong giao diện. Mặc định là `id`. |
| `description` | không | string | Tóm tắt một dòng. |
| `author` | không | object | `{ "name": "...", "email": "...", "url": "..." }` |
| `homepage` | không | string | URL đến kho mã nguồn hoặc tài liệu. |
| `plugins` | có | array | Một hoặc nhiều mục thành phần plugin (xem bên dưới). |
| `permissions` | không | array | Danh sách tên các quyền cần thiết (xem [Quyền](#permissions)). |

### Mục thành phần plugin {#plugin-component-entry}

Mỗi đối tượng trong mảng `plugins` mô tả một thành phần.

**Thành phần backend:**

```json
{
  "kind": "backend",
  "id": "my-plugin-backend",
  "entrypoint": "backend/script.js",
  "runtime": "javascript",
  "assets": "backend/assets"
}
```

**Thành phần frontend:**

```json
{
  "kind": "frontend",
  "id": "my-plugin-frontend",
  "entrypoint": "frontend/script.js",
  "style": "frontend/style.css",
  "assets": "frontend/assets",
  "backend": { "id": "my-plugin-backend" }
}
```

| Trường | Bắt buộc | Ghi chú |
|-------|----------|-------|
| `kind` | có | `"backend"` hoặc `"frontend"` |
| `id` | có | Duy nhất trong manifest. Chữ thường và dấu gạch nối. |
| `entrypoint` | có | Đường dẫn tương đối đến tệp JS tại điểm vào. |
| `style` | không | Tệp CSS được tải trong iframe của plugin. |
| `assets` | không | Thư mục tài nguyên tĩnh được phục vụ dưới `/plugins/{id}/assets/`. |
| `backend.id` | không | Liên kết thành phần frontend với thành phần backend của nó để gọi RPC `sdk.backend.*`. |
| `runtime` | không (chỉ backend) | `"javascript"` (giá trị mặc định và duy nhất được hỗ trợ). |

***

## API plugin backend (sdk) {#backend-plugin-api-sdk}

Đối tượng `sdk` backend được truyền vào hàm `init(sdk)` của bạn. Tất cả phương thức đều đồng bộ, trừ khi được đánh dấu `async`.

### `sdk.console` {#sdk-console}

```js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")
```

Ghi vào bộ đệm nhật ký của plugin (hiển thị trong thẻ Nhật ký). Giữ lại tối đa 500 mục. Mỗi thông điệp bị cắt ngắn ở 1 KB.

### `sdk.meta` {#sdk-meta}

```js
sdk.meta.id()          // > string: plugin id (e.g. "my-plugin")
sdk.meta.packageId()   // > string: same as id
sdk.meta.version()     // > string: semver (e.g. "1.0.0")
sdk.meta.path()        // > string: writable data directory for this plugin
```

`sdk.meta.path()` trỏ đến thư mục dữ liệu riêng có thể ghi của plugin, ví dụ `~/.local/share/ogma/plugins/my-plugin/data`. Thư mục được tạo tự động và có thể dùng để lưu bền vững dữ liệu plugin qua các lần khởi động lại.

`sdk.storage`, `sdk.path` và `sdk.fs` cũng có sẵn để hỗ trợ xử lý trạng thái và tệp.

### `sdk.storage` {#sdk-storage}

```js
sdk.storage.get("key")      // > string | null
sdk.storage.set("key", "value")
sdk.storage.delete("key")
sdk.storage.clear()
sdk.storage.keys()          // > string[]
```

`sdk.storage` có phạm vi theo ID plugin và được lưu bền vững qua các lần khởi động lại plugin.

### `sdk.fs` {#sdk-fs}

```js
sdk.fs.readFile("relative/file.txt")      // > string
sdk.fs.writeFile("relative/file.txt", "text")
sdk.fs.appendFile("relative/file.txt", "more")
sdk.fs.exists("relative/file.txt")        // > boolean
sdk.fs.existsSync("relative/file.txt")    // > boolean
sdk.fs.list("relative/dir")              // > string[]
sdk.fs.mkdir("relative/dir")
```

`sdk.fs` chỉ được thao tác với tệp nằm dưới `sdk.meta.path()`.

`read` và `write` vẫn là bí danh tương thích của `readFile` và `writeFile`. Dùng `exists` hoặc `existsSync` trước khi tạo tệp mà bạn không muốn ghi đè. Các API này hoạt động đồng bộ trong môi trường chạy plugin; chúng không phải mô-đun `fs` đầy đủ của Node.js. Truy cập tệp của plugin cần quyền `plugin_storage` và chỉ nằm trong thư mục dữ liệu riêng của plugin. JavaScript của quy trình có ngữ cảnh hệ thống tệp khác; xem [Truy cập tệp trong quy trình](../app/workflows.md#javascript-and-files).

### `sdk.path` {#sdk-path}

```js
sdk.path.join("a", "b", "c")
sdk.path.basename("/tmp/file.txt")
sdk.path.dirname("/tmp/file.txt")
sdk.path.extname("file.txt")
sdk.path.resolve("/a", "b")
sdk.path.isAbsolute("/tmp/file.txt")
sdk.path.sep
```

### `sdk.events` {#sdk-events}

Đăng ký callback cho các sự kiện Ogma. Tất cả callback được gọi đồng bộ trong sandbox QuickJS.

```js
sdk.events.onInterceptRequest(function(req) {
  // req: RequestSpecRaw
  // Return a modified RequestSpecRaw to mutate the request.
  // Return null/undefined to pass through unchanged.
});

sdk.events.onInterceptResponse(function(req, res) {
  // req: Request (read-only), res: Response (read-only)
  // Return value is ignored.
});

sdk.events.onProjectChange(function() {
  // no callback args
});

sdk.events.onFindingCreated(function(finding) {
  // finding: { id, title, reporter }
});
```

### `sdk.requests` {#sdk-requests}

```js
// Get a single HTTP entry by id
var entry = sdk.requests.get("entry-id");
// entry: { id, method, host, path, query, tls, ... } or null

// Search HTTP history
var results = sdk.requests.search({ limit: 20, offset: 0 });
// results: { entries: [...], total: N }

// Send an HTTP request (requires send_requests permission)
var response = await sdk.requests.send(spec);
// spec: RequestSpecRaw (see below)
// response: Response
```

`sdk.requests.send` yêu cầu quyền `send_requests` được khai báo trong manifest và được người dùng cấp. Xem [Quyền](#permissions).

### `sdk.findings` {#sdk-findings}

```js
// Create a finding (requires write_findings permission)
sdk.findings.create({
  title: "SSRF via redirect",
  reporter: "my-plugin",
  dedupeKey: "ssrf-" + request.getId(),
  request: { id: request.getId() }
});

// Check if a finding already exists (dedup check)
var exists = sdk.findings.exists({ dedupeKey: "ssrf-abc" });

// List findings
var page = sdk.findings.list({ limit: 20, offset: 0 });

// Get a single finding
var finding = sdk.findings.get("finding-id");
```

Giới hạn tần suất của `sdk.findings.create`: 10 mỗi phút, 500 mỗi phiên plugin và 3 mỗi callback sự kiện.

### `sdk.api` {#sdk-api}

Đăng ký các hàm RPC backend mà frontend có thể gọi qua `sdk.backend.*`:

```js
// In backend init:
sdk.api.register("getScans", function(scanId) {
  return { scans: [] };
});

// Emit an event to connected frontends:
sdk.api.send("scan:complete", { scanId: 1, status: "ok" });
```

Hàm xử lý nhận các đối số được truyền từ frontend (không chèn thêm đối số `sdk`). Giá trị trả về được tuần tự hóa thành JSON và gửi lại cho bên gọi.

Frontend gọi các hàm này qua `sdk.backend.getScans(scanId)`; xem [API plugin frontend](#frontend-plugin-api-sdk).

`sdk.api.send` đưa sự kiện vào hàng đợi riêng cho mỗi plugin (tối đa 200 mục). Frontend truy vấn định kỳ hàng đợi này qua `sdk.backend.onEvent`.

### `sdk.replay`, `sdk.projects`, `sdk.scope`, `sdk.workflows`, `sdk.matchReplace` {#sdk-replay-sdk-projects-sdk-scope-sdk-workflows-sdk-matchreplace}

Đây là các namespace truy vấn chỉ đọc. Xem [tài liệu tham khảo SDK backend](./backend-sdk.md) để biết chữ ký phương thức đầy đủ.

### Lớp yêu cầu {#request-classes}

**`RequestSpecRaw`** đại diện cho yêu cầu đã bị chặn bắt. Bạn nhận đối tượng này trong `onInterceptRequest`.

```js
spec.getMethod()          // > string
spec.setMethod("POST")
spec.getHost()            // > string
spec.setHost("example.com")
spec.getPort()            // > number
spec.getPath()            // > string
spec.setPath("/new/path")
spec.getQuery()           // > string
spec.getTls()             // > boolean
spec.getHeaders()         // > Record<string, string[]>
spec.setHeader("X-Foo", "bar")
spec.getBody()            // > Body | null
spec.setBody("new body")
spec.getRaw()             // > Uint8Array (raw bytes) or []
spec.setRaw(bytes)        // set raw bytes

// Create a new spec from a URL string:
var spec = new RequestSpecRaw("https://example.com/path?q=1");
```

**`Request`** là yêu cầu đã thu thập, chỉ đọc (từ `sdk.requests.get`).

```js
req.getId()
req.getMethod()
req.getHost()
req.getPort()
req.getTls()
req.getPath()
req.getQuery()
req.getUrl()           // > full URL string
req.getHeaders()       // > Record<string, string>
req.getHeader("name")
req.getBody()          // > Body | null
req.getCreatedAt()     // > Date
req.toSpec()           // > RequestSpec (mutable copy)
```

**`Response`** là phản hồi đã thu thập, chỉ đọc.

```js
res.getCode()            // > number (HTTP status)
res.getHeaders()         // > Record<string, string>
res.getHeader("name")
res.getBody()            // > Body | null
res.getRoundtripTime()   // > number (ms)
res.getCreatedAt()       // > Date
```

**`Body`**:

```js
body.toText()    // > string
body.toJson()    // > parsed object or null
body.toRaw()     // > Uint8Array
body.length      // > number (original size, may differ from toText() if truncated)
```

***

## API plugin frontend (sdk) {#frontend-plugin-api-sdk}

Mã frontend của plugin chạy trong iframe được cách ly bằng sandbox, được tải từ `/plugins/{id}/ui`. Iframe dùng `postMessage` để giao tiếp với ứng dụng chủ Ogma, ứng dụng này làm trung gian cho các lệnh gọi backend.

SDK có sẵn qua `window.ogmaSDK`. Gọi `ogmaSDK.ready(cb)` để nhận SDK đang hoạt động sau khi cầu nối với ứng dụng chủ được thiết lập:

```js
ogmaSDK.ready(function(sdk) {
  // sdk is the live SDK - safe to call any method here
  sdk.log.info("frontend ready");
});
```

Tất cả phương thức SDK trả về Promise.

### `sdk.log` {#sdk-log}

```js
sdk.log.info("message")
sdk.log.warn("message")
sdk.log.error("message")
```

### `sdk.meta` {#sdk-meta-1}

```js
var meta = await sdk.meta.get();
// { pluginId, packageId, name, version, ogmaVersion }
```

### `sdk.requests` {#sdk-requests-1}

```js
var entry = await sdk.requests.get({ id: "entry-id" });
var result = await sdk.requests.search({ limit: 20, offset: 0, query: "host:example.com" });
```

Yêu cầu quyền `read_http_history` (được cấp tự động; không cần người dùng phê duyệt).

### `sdk.findings` {#sdk-findings-1}

```js
var page = await sdk.findings.list({ limit: 20, offset: 0 });
```

Yêu cầu quyền `read_findings` (được cấp tự động).

### `sdk.scope` {#sdk-scope}

```js
var scope = await sdk.scope.getActive();
```

### `sdk.projects` {#sdk-projects}

```js
var project = await sdk.projects.getCurrent();
```

### `sdk.backend` - RPC backend {#sdk-backend-backend-rpc}

Gọi các hàm đã đăng ký bằng `sdk.api.register` ở backend:

```js
// Call a named backend function
var result = await sdk.backend.call("getScans", [scanId]);

// Poll for backend-emitted events (sdk.api.send on the backend side)
var { events, next_since } = await sdk.backend.poll(since);
// events: [{ event: "scan:complete", args: [...] }]

// Register an event listener (uses polling internally)
sdk.backend.onEvent("scan:complete", function(data) {
  console.log("scan done", data);
});
```

`sdk.backend.onEvent` dùng vòng lặp truy vấn định kỳ mỗi 2 giây ở bên trong. Dừng lắng nghe bằng cách gọi hàm hủy đăng ký được trả về:

```js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();
```

### `sdk.navigation` {#sdk-navigation}

```js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });
```

Đăng ký trang điều hướng. Hiện ứng dụng chủ xác nhận đã nhận. Việc tích hợp đầy đủ với bộ định tuyến đang được thực hiện.

### `sdk.sidebar` {#sdk-sidebar}

```js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });
```

Đăng ký mục trên thanh bên. Hiện chỉ có trong bảng giao diện của plugin; việc kết nối với các slot của thanh bên toàn cục đang được thực hiện.

### `sdk.commands` {#sdk-commands}

```js
await sdk.commands.register("my-plugin:scan", {
  name: "Scan with My Plugin",
  handler: function(context) { /* ... */ }
});
```

Ứng dụng chủ xác nhận đã nhận. Việc tích hợp với bảng lệnh đang được thực hiện.

### `sdk.menu` {#sdk-menu}

```js
await sdk.menu.registerItem({
  type: "Request",
  commandId: "my-plugin:scan",
  leadingIcon: "shield"
});
```

Ứng dụng chủ xác nhận đã nhận. Việc chèn vào menu ngữ cảnh đang được thực hiện.

### `sdk.window` {#sdk-window}

```js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });
```

Hiển thị thông báo tạm thời trong bảng plugin. Các kiểu: `info`, `success`, `warning`, `error`.

### `sdk.ui` {#sdk-ui}

```js
sdk.ui.resize(600);                              // request iframe height change
sdk.ui.sidebar.registerItem("name", "/path");    // alias for sdk.sidebar.registerItem
```

***

## Quyền {#permissions}

Khai báo quyền trong `manifest.json`:

```json
{
  "permissions": ["send_requests", "write_findings"]
}
```

### Quyền được cấp tự động (không cần người dùng phê duyệt) {#auto-granted-permissions-no-user-approval-needed}

Các quyền này luôn được cấp cho mọi plugin đã cài đặt:

| Quyền | Cho phép |
|------------|----------------|
| `read_http_history` | `sdk.requests.get`, `sdk.requests.search` |
| `read_findings` | `sdk.findings.get`, `sdk.findings.list` |
| `read_scope` | `sdk.scope.getActive` |
| `read_projects` | `sdk.projects.getCurrent`, `sdk.projects.list` |
| `plugin_storage` | `sdk.storage`, `sdk.fs`, `sdk.path` |

### Quyền được bảo vệ (cần người dùng phê duyệt) {#protected-permissions-require-user-approval}

Các quyền này phải được khai báo trong manifest và được người dùng cấp rõ ràng từ thẻ Quyền:

| Quyền | Cho phép |
|------------|----------------|
| `send_requests` | `sdk.requests.send` - gửi yêu cầu HTTP ra ngoài |
| `write_findings` | `sdk.findings.create`, `sdk.findings.update` |

Người dùng thấy lời nhắc khi bật plugin có khai báo quyền được bảo vệ. Họ cũng có thể cấp hoặc thu hồi quyền bất cứ lúc nào từ thẻ Quyền.

***

## Cấu trúc gói plugin {#plugin-package-structure}

Có thể cài đặt gói plugin từ thư mục cục bộ và, trong quy trình trên trình duyệt, cả từ bản xuất `.zip`.

```
my-plugin/
  manifest.json           - required
  backend/
    script.js             - bundled backend JS (ES2020)
  frontend/
    script.js             - bundled frontend JS
    style.css             - optional CSS
    assets/               - static assets (images, fonts, etc.)
```

### Yêu cầu đối với tập lệnh backend {#backend-script-requirements}

* Phải là một tệp JS duy nhất, tự chứa đầy đủ mã cần thiết.
* Không hỗ trợ `require()` và `import()` động.
* Phải xuất hàm `init(sdk)` (hoặc định nghĩa hàm đó ở phạm vi toàn cục).
* Tập con ES2020 được QuickJS hỗ trợ: `async/await`, `Promise`, `Map`, `Set`, `Symbol`, `Proxy`, `Date`, `RegExp`, `JSON`. Không có `fetch` và không có `Buffer`.
* Các import tĩnh được hỗ trợ được xử lý trong bước tiền xử lý plugin: `@ogma/sdk`, `crypto`, `fs`, `path`.
* Kích thước tệp tối đa: 256 KB.

### Yêu cầu đối với tập lệnh frontend {#frontend-script-requirements}

* Chạy trong iframe được cách ly bằng sandbox. Cho phép `connect-src: 'self'` để plugin có thể gửi POST đến `/plugins/{id}/api/*` và truy vấn định kỳ `/plugins/{id}/events/poll`.
* CSP: `default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'`.
* Không có `allow-same-origin` trong sandbox của iframe; plugin không thể truy cập DOM của trang cha Ogma hoặc cookie của Ogma.
* Dùng `ogmaSDK.ready(cb)` để truy cập SDK; không gọi phương thức SDK trước khi callback được chạy.

***

## Biên dịch plugin cho Ogma {#building-a-plugin-for-ogma}

Vì backend phải là một tệp JS bundle duy nhất, bạn phải tạo bundle từ mã nguồn TypeScript hoặc mô-đun ES trước khi cài đặt.

Bộ công cụ được khuyến nghị:

```bash
# Install dependencies
pnpm install

# Bundle backend (outputs a single CJS/IIFE file):
esbuild packages/backend/src/index.ts \
  --bundle \
  --platform=neutral \
  --format=iife \
  --global-name=_plugin \
  --outfile=dist/backend/script.js \
  --external:@ogma/sdk --external:@ogmabox/ogma-sdk

# Bundle frontend:
vite build packages/frontend --outDir ../../dist/frontend
```

Nếu bạn dùng bộ công cụ phát triển Caido (`@caido-community/dev`), chạy `caido-dev build`, sau đó sao chép đầu ra vào cấu trúc tương thích Ogma với `manifest.json` ở thư mục gốc.

***

## Cài đặt plugin {#installing-a-plugin}

1. Mở **Plugin** trên thanh bên trái.
2. Nhấp **Cài đặt** (ở đầu thẻ Đã cài đặt).
3. Trong ứng dụng máy tính, nhấp **Duyệt** để mở bộ chọn thư mục của hệ điều hành. Trong trình duyệt, nhập đường dẫn đầy đủ phía máy chủ đến thư mục plugin.
4. Nhấp **Kiểm tra tính hợp lệ** để kiểm tra manifest và danh sách tệp.
5. Nhấp **Cài đặt** nếu kiểm tra tính hợp lệ thành công.
6. Chọn plugin trong danh sách và nhấp **Bật**.
7. Nếu plugin khai báo quyền được bảo vệ, xem xét và cấp quyền từ thẻ **Quyền** trước khi bật.

***

## Khắc phục sự cố {#troubleshooting}

**Khởi tạo plugin thất bại mà không báo lỗi:** Kiểm tra thẻ **Nhật ký**. Các nguyên nhân thường gặp nhất:

* Đã gọi `sdk.meta.path()` nhưng không tạo được thư mục dữ liệu.
* Ngoại lệ chưa được xử lý trong `init()`.
* Lệnh gọi `sdk.*` bị thiếu hoặc viết sai.

**Frontend hiển thị trống:** Kiểm tra console trình duyệt để tìm vi phạm CSP. Đảm bảo tập lệnh frontend của bạn gọi `ogmaSDK.ready(cb)` trước khi truy cập bất kỳ phương thức SDK nào.

**`sdk.requests.send` phát sinh `Permission denied`:** Quyền `send_requests` phải được khai báo trong manifest VÀ được người dùng cấp trong thẻ Quyền.

**Không gọi được các hàm `sdk.api.register` từ frontend:** Backend phải được bật (không chỉ được cài đặt). Tên hàm phải khớp chính xác, có phân biệt chữ hoa và chữ thường, với tên frontend truyền vào `sdk.backend.call`.

**Cảnh báo tương thích hiển thị như lỗi:** Chúng không ngăn hoạt động nhưng cho biết các phần API còn thiếu. Xem các bảng ánh xạ SDK ở trên để biết mức độ bao phủ API.
