---
url: https://docs.ogmabox.com/zh-Hant/plugins/README.md
description: 建立、封裝、安裝、啟用及散布 Ogma 外掛，涵蓋後端邏輯、前端面板、命令、權限與市集中繼資料。
---

# Ogma 外掛系統 {#ogma-plugin-system}

Ogma 外掛可透過自訂後端邏輯、前端 UI 面板和工作流程步驟，擴充工具的功能。外掛從磁碟上的目錄安裝至本機，依專案啟用，並在沙箱環境中執行。

本文是外掛開發者的主要參考文件。

若要以最快的方式開始，請參閱[外掛快速入門](/zh-Hant/plugins/quickstart)。

***

## 快速入門 {#quick-start}

### 最小後端外掛 {#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；使用封裝工具時可使用匯入語句）：

```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());
    }
  });
}
```

以上內容已足以建立可運作的純後端外掛。將它維持為 `backend/script.js` 中的單一檔案，並讓 `manifest.json` 直接指向該檔案。

### 一分鐘建置流程（TypeScript 原始碼） {#one-minute-build-flow-typescript-source}

若以 TypeScript 開發，請使用以下結構：

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

建置：

```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
```

安裝：開啟**外掛 > 安裝**，選取 `my-plugin/` 目錄，再啟用外掛。

***

## 資訊清單參考 {#manifest-reference}

`manifest.json` 位於套件根目錄。所有欄位都區分大小寫。

### 頂層欄位 {#top-level-fields}

| 欄位 | 必填 | 型別 | 說明 |
|-------|----------|------|-------|
| `id` | 是 | string | 僅限小寫字母、數字及連字號。最多 64 個字元，且必須在已安裝外掛中保持唯一。 |
| `version` | 是 | string | 語意化版本：`MAJOR.MINOR.PATCH` |
| `name` | 否 | string | UI 中顯示的名稱，預設為 `id`。 |
| `description` | 否 | string | 單行摘要。 |
| `author` | 否 | object | `{ "name": "...", "email": "...", "url": "..." }` |
| `homepage` | 否 | string | 原始碼儲存庫或文件的 URL。 |
| `plugins` | 是 | array | 一個或多個外掛元件項目（見下文）。 |
| `permissions` | 否 | array | 所需權限名稱的清單（參閱[權限](#permissions)）。 |

### 外掛元件項目 {#plugin-component-entry}

`plugins` 陣列中的每個物件都描述一個元件。

**後端元件：**

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

**前端元件：**

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

| 欄位 | 必填 | 說明 |
|-------|----------|-------|
| `kind` | 是 | `"backend"` 或 `"frontend"` |
| `id` | 是 | 在資訊清單內必須唯一。使用小寫字母及連字號。 |
| `entrypoint` | 是 | JS 進入點檔案的相對路徑。 |
| `style` | 否 | 在外掛 iframe 中載入的 CSS 檔案。 |
| `assets` | 否 | 透過 `/plugins/{id}/assets/` 提供的靜態資源目錄。 |
| `backend.id` | 否 | 將前端元件連結至其後端元件，以使用 `sdk.backend.*` RPC。 |
| `runtime` | 否（僅後端） | `"javascript"`（預設值，也是唯一支援的值）。 |

***

## 後端外掛 API（sdk） {#backend-plugin-api-sdk}

後端的 `sdk` 物件會傳入你的 `init(sdk)` 函式。除非標示為 `async`，否則所有方法均為同步方法。

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

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

將訊息寫入外掛的記錄緩衝區（可在記錄分頁中查看）。最多保留 500 筆，每則訊息會在 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()` 指向外掛可寫入的私有資料目錄，例如 `~/.local/share/ogma/plugins/my-plugin/data`。此目錄會自動建立，可用於保存外掛資料，使其在重新啟動後仍然保留。

此外也提供 `sdk.storage`、`sdk.path` 和 `sdk.fs`，用於狀態管理及檔案輔助操作。

### `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` 的作用範圍限定於外掛 ID，其資料會在外掛重新啟動後保留。

### `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` 僅能存取 `sdk.meta.path()` 下的檔案。

`read` 和 `write` 仍分別是 `readFile` 和 `writeFile` 的相容別名。建立不希望覆寫的檔案前，請使用 `exists` 或 `existsSync` 檢查。這些 API 在外掛執行環境中同步運作，並非完整的 Node.js `fs` 模組。外掛檔案存取需要 `plugin_storage` 權限，且僅限於外掛的私有資料目錄。工作流程 JavaScript 使用不同的檔案系統情境；請參閱[工作流程檔案存取](../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}

為 Ogma 事件註冊回呼。所有回呼都會在 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` 時，必須在資訊清單中宣告 `send_requests` 權限，並由使用者授予該權限。參閱[權限](#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");
```

`sdk.findings.create` 的頻率限制：每分鐘 10 次、每個外掛工作階段 500 次、每次事件回呼 3 次。

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

註冊可由前端透過 `sdk.backend.*` 呼叫的後端 RPC 函式：

```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" });
```

處理函式會接收前端傳入的引數（不會額外注入 `sdk` 引數）。傳回值會序列化為 JSON，再送回呼叫端。

前端透過 `sdk.backend.getScans(scanId)` 呼叫這些函式；參閱[前端外掛 API](#frontend-plugin-api-sdk)。

`sdk.api.send` 會將事件推送至各外掛獨立的佇列（最多 200 筆）。前端透過 `sdk.backend.onEvent` 輪詢此佇列。

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

這些都是唯讀查詢命名空間。完整方法簽章請參閱[後端 SDK 參考](./backend-sdk.md)。

### 請求類別 {#request-classes}

**`RequestSpecRaw`**：代表遭攔截的請求。你會在 `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`**：唯讀的已擷取請求（來自 `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`**：唯讀的已擷取回應。

```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（sdk） {#frontend-plugin-api-sdk}

前端外掛程式碼在從 `/plugins/{id}/ui` 載入的沙箱 iframe 中執行。iframe 透過 `postMessage` 與 Ogma 主應用程式通訊，再由主應用程式將呼叫轉送至後端。

可透過 `window.ogmaSDK` 使用 SDK。呼叫 `ogmaSDK.ready(cb)`，即可在主應用程式橋接建立後收到可用的 SDK：

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

所有 SDK 方法都會傳回 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" });
```

需要 `read_http_history` 權限（自動授予，不需使用者核准）。

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

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

需要 `read_findings` 權限（自動授予）。

### `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 {#sdk-backend-backend-rpc}

呼叫透過後端 `sdk.api.register` 註冊的函式：

```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` 在內部以 2 秒的間隔輪詢。呼叫其傳回的取消訂閱函式，即可停止監聽：

```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" });
```

註冊導覽頁面。目前主應用程式會確認此操作；完整路由整合仍在開發中。

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

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

註冊側邊欄項目。目前僅限於外掛 UI 面板；全域側邊欄插槽的串接仍在開發中。

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

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

主應用程式會確認此操作。命令選擇區整合仍在開發中。

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

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

主應用程式會確認此操作。快顯選單注入仍在開發中。

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

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

在外掛面板中顯示短暫通知。類型：`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
```

***

## 權限 {#permissions}

在 `manifest.json` 中宣告權限：

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

### 自動授予的權限（不需使用者核准） {#auto-granted-permissions-no-user-approval-needed}

任何已安裝的外掛都會取得以下權限：

| 權限 | 允許的操作 |
|------------|----------------|
| `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` |

### 受保護的權限（需要使用者核准） {#protected-permissions-require-user-approval}

必須在資訊清單中宣告這些權限，並由使用者在權限分頁明確授予：

| 權限 | 允許的操作 |
|------------|----------------|
| `send_requests` | `sdk.requests.send`：發出對外 HTTP 請求 |
| `write_findings` | `sdk.findings.create`, `sdk.findings.update` |

使用者啟用宣告了受保護權限的外掛時，會看到提示。他們也可隨時在權限分頁授予或撤銷權限。

***

## 外掛套件結構 {#plugin-package-structure}

外掛套件可從本機目錄安裝；透過瀏覽器操作時，也可從匯出的 `.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.)
```

### 後端指令碼要求 {#backend-script-requirements}

* 必須是單一自包含的 JS 檔案。
* 不支援 `require()` 或動態 `import()`。
* 必須匯出 `init(sdk)` 函式（或將它定義為全域函式）。
* 支援 QuickJS 所實作的 ES2020 子集：`async/await`、`Promise`、`Map`、`Set`、`Symbol`、`Proxy`、`Date`、`RegExp`、`JSON`。不提供 `fetch`，也不提供 `Buffer`。
* 支援的靜態匯入會由外掛前置處理解析：`@ogma/sdk`、`crypto`、`fs`、`path`。
* 檔案大小上限：256 KB。

### 前端指令碼要求 {#frontend-script-requirements}

* 在沙箱 iframe 內執行。允許 `connect-src: 'self'`，因此外掛可向 `/plugins/{id}/api/*` 發出 POST 請求，並輪詢 `/plugins/{id}/events/poll`。
* CSP：`default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'`。
* iframe 沙箱不包含 `allow-same-origin`，因此外掛無法存取 Ogma 的父層 DOM 或 Cookie。
* 透過 `ogmaSDK.ready(cb)` 存取 SDK；回呼觸發前，不要呼叫 SDK 方法。

***

## 為 Ogma 建置外掛 {#building-a-plugin-for-ogma}

由於後端必須是單一封裝後的 JS 檔案，因此安裝前必須先封裝 TypeScript/ES 模組原始碼。

建議的工具鏈：

```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
```

若使用 Caido 開發工具鏈（`@caido-community/dev`），請執行 `caido-dev build`，再將輸出複製成相容於 Ogma 的配置，並將 `manifest.json` 放在根目錄。

***

## 安裝外掛 {#installing-a-plugin}

1. 開啟左側邊欄的**外掛**。
2. 點選**安裝**（位於已安裝分頁頂端）。
3. 在桌面應用程式中，點選**瀏覽**以開啟原生資料夾選擇器。在瀏覽器中，輸入伺服器端外掛目錄的完整路徑。
4. 點選**驗證**，檢查資訊清單及檔案清單。
5. 驗證通過後，點選**安裝**。
6. 在清單中選取外掛，並點選**啟用**。
7. 若外掛宣告了受保護權限，請在啟用前，於**權限**分頁審查並授予這些權限。

***

## 疑難排解 {#troubleshooting}

**外掛初始化失敗但沒有提示：** 檢查**記錄**分頁。最常見的原因包括：

* 呼叫了 `sdk.meta.path()`，但無法建立資料目錄。
* `init()` 中有未處理的例外狀況。
* `sdk.*` 呼叫遺漏或拼字錯誤。

**前端顯示空白：** 檢查瀏覽器主控台是否有 CSP 違規。確認前端指令碼在存取任何 SDK 方法前，已呼叫 `ogmaSDK.ready(cb)`。

**`sdk.requests.send` 擲回 `Permission denied`：** 必須在資訊清單中宣告 `send_requests` 權限，並由使用者在權限分頁授予該權限，兩者缺一不可。

**前端無法呼叫 `sdk.api.register` 註冊的函式：** 後端必須已啟用（而不只是安裝）。函式名稱必須與前端傳入 `sdk.backend.call` 的名稱完全相符（區分大小寫）。

**相容性警告顯示為錯誤：** 這些訊息不會阻止運作，但代表 API 功能仍有缺口。API 支援範圍請參閱上方的 SDK 對照表。
