---
url: https://docs.ogmabox.com/th/plugins/README.md
description: >-
  สร้าง จัดแพ็กเกจ ติดตั้ง เปิดใช้งาน และแจกจ่ายปลั๊กอิน Ogma
  พร้อมลอจิกแบ็กเอนด์ แผงฟรอนต์เอนด์ คำสั่ง สิทธิ์ และเมทาดาทาของมาร์เก็ตเพลซ
---

# ระบบปลั๊กอิน Ogma {#ogma-plugin-system}

ปลั๊กอิน Ogma ขยายความสามารถของเครื่องมือด้วยลอจิกแบ็กเอนด์ที่กำหนดเอง แผง UI ฟรอนต์เอนด์ และขั้นตอนเวิร์กโฟลว์ ปลั๊กอินติดตั้งภายในเครื่องจากไดเรกทอรีบนดิสก์ เปิดใช้งานแยกตามโครงการ และทำงานในสภาพแวดล้อมแซนด์บ็อกซ์

เอกสารนี้เป็นข้อมูลอ้างอิงหลักสำหรับผู้พัฒนาปลั๊กอิน

หากต้องการเริ่มต้นให้เร็วที่สุด ให้ใช้ [คู่มือเริ่มต้นใช้งานปลั๊กอินอย่างรวดเร็ว](/th/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; ใช้คำสั่ง import ได้เมื่อใช้เครื่องมือรวมบันเดิล):

```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` | ใช่ | สตริง | ใช้ได้เฉพาะตัวอักษรละตินพิมพ์เล็ก ตัวเลข และยัติภังค์ สูงสุด 64 อักขระ ต้องไม่ซ้ำกับปลั๊กอินอื่นที่ติดตั้งไว้ |
| `version` | ใช่ | สตริง | การกำหนดเวอร์ชันเชิงความหมาย: `MAJOR.MINOR.PATCH` |
| `name` | ไม่ | สตริง | ชื่อที่แสดงใน UI ค่าเริ่มต้นคือ `id` |
| `description` | ไม่ | สตริง | สรุปในหนึ่งบรรทัด |
| `author` | ไม่ | ออบเจ็กต์ | `{ "name": "...", "email": "...", "url": "..." }` |
| `homepage` | ไม่ | สตริง | URL ของที่เก็บซอร์สโค้ดหรือเอกสาร |
| `plugins` | ใช่ | อาร์เรย์ | รายการองค์ประกอบปลั๊กอินหนึ่งรายการขึ้นไป (ดูด้านล่าง) |
| `permissions` | ไม่ | อาร์เรย์ | รายการชื่อสิทธิ์ที่จำเป็น (ดู [สิทธิ์](#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` | ไม่ | ไฟล์ CSS ที่โหลดใน iframe ของปลั๊กอิน |
| `assets` | ไม่ | ไดเรกทอรีของแอสเซ็ตแบบคงที่ที่ให้บริการภายใต้ `/plugins/{id}/assets/` |
| `backend.id` | ไม่ | เชื่อมองค์ประกอบฟรอนต์เอนด์กับองค์ประกอบแบ็กเอนด์ของตนเพื่อเรียก RPC ผ่าน `sdk.backend.*` |
| `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 เหล่านี้ทำงานแบบซิงโครนัสในรันไทม์ปลั๊กอิน ไม่ใช่โมดูล `fs` เต็มรูปแบบของ Node.js การเข้าถึงไฟล์ของปลั๊กอินต้องมีสิทธิ์ `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}

ลงทะเบียนฟังก์ชัน RPC แบ็กเอนด์ที่ฟรอนต์เอนด์เรียกผ่าน `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" });
```

ตัวจัดการรับอาร์กิวเมนต์ที่ส่งมาจากฟรอนต์เอนด์ (ไม่มีการแทรกอาร์กิวเมนต์ `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}

โค้ดปลั๊กอินฟรอนต์เอนด์ทำงานใน iframe ที่อยู่ในแซนด์บ็อกซ์และโหลดจาก `/plugins/{id}/ui` iframe ใช้ `postMessage` เพื่อสื่อสารกับโฮสต์ Ogma ซึ่งทำหน้าที่ส่งต่อการเรียกไปยังแบ็กเอนด์

SDK พร้อมใช้งานผ่าน `window.ogmaSDK` เรียก `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)` (หรือกำหนดให้เป็นฟังก์ชันโกลบอล)
* ชุดย่อยของ ES2020 ที่ QuickJS รองรับ: `async/await`, `Promise`, `Map`, `Set`, `Symbol`, `Proxy`, `Date`, `RegExp`, `JSON` ไม่มี `fetch` และไม่มี `Buffer`
* import แบบคงที่ที่รองรับจะถูกแก้ไขการอ้างอิงระหว่างการประมวลผลปลั๊กอินล่วงหน้า: `@ogma/sdk`, `crypto`, `fs`, `path`
* ขนาดไฟล์สูงสุด: 256 KB

### ข้อกำหนดสคริปต์ฟรอนต์เอนด์ {#frontend-script-requirements}

* ทำงานภายใน iframe ที่อยู่ในแซนด์บ็อกซ์ อนุญาต `connect-src: 'self'` เพื่อให้ปลั๊กอินส่ง POST ไปยัง `/plugins/{id}/api/*` และโพล `/plugins/{id}/events/poll` ได้
* CSP: `default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'`
* ไม่มี `allow-same-origin` ในแซนด์บ็อกซ์ iframe ปลั๊กอินจึงเข้าถึง DOM ของหน้าหลักของ Ogma หรือคุกกี้ไม่ได้
* ใช้ `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 หรือไม่ ตรวจสอบให้แน่ใจว่าสคริปต์ฟรอนต์เอนด์เรียก `ogmaSDK.ready(cb)` ก่อนเข้าถึงเมธอด SDK ใด ๆ

**`sdk.requests.send` แจ้งข้อผิดพลาด `Permission denied`:** ต้องประกาศสิทธิ์ `send_requests` ในมานิเฟสต์ และผู้ใช้ต้องอนุมัติจากแท็บสิทธิ์ด้วย

**เรียกฟังก์ชัน `sdk.api.register` จากฟรอนต์เอนด์ไม่ได้:** ต้องเปิดใช้งานแบ็กเอนด์ (ไม่ใช่เพียงติดตั้งไว้) ชื่อฟังก์ชันต้องตรงกับชื่อที่ฟรอนต์เอนด์ส่งให้ `sdk.backend.call` ทุกตัวอักษร (แยกแยะตัวพิมพ์ใหญ่และเล็ก)

**คำเตือนความเข้ากันได้แสดงเป็นข้อผิดพลาด:** คำเตือนเหล่านี้ไม่ขัดขวางการทำงาน แต่บ่งชี้ส่วนที่ API ยังรองรับไม่ครบ ดูตารางการแมป SDK ข้างต้นสำหรับรายละเอียดความครอบคลุมของ API
