# 背景任務

<Badge type="warning" text="尚未發布" />

背景任務是在 Extension 開啟期間持續執行的函式，例如每 15 分鐘同步一次。它需要 `dozin.capabilities` 裡有 `service`。用 `api.registerService(definition)` 註冊它。

## ServiceDefinition

| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `id` | `string` | 否 | 預設為套件名稱。小寫字母、數字和連字元。每個 Extension 最多 8 個 |
| `name` | `string` | 否 | 顯示在 **設定 › 擴充功能** 裡，1–80 個字元；預設為 `id` |
| `start` | `'auto' \| 'manual'` | 否 | 預設 `manual`。`auto` 會在 Extension 的 `secrets` 填好後隨它一起啟動。以使用者的選擇為準 |
| `restart` | `{ policy?, maxRetries?, backoffSeconds? }` | 否 | `policy`：`never`、`on-failure`（預設）或 `always`；`maxRetries` 0–100（預設 5）；`backoffSeconds` 1–10 個等待時間，每個 1–3600 秒（預設 5、30、120） |
| `stopTimeoutSeconds` | `number` | 否 | 1–30，預設 10：**停止** 等待 `run` 回傳的時間 |
| `run(ctx)` | function | 是 | 見下文 |

任何一個欄位無效，整個任務都會被拒絕（`registerService` 拋出 `TypeError`）；不會替你修正任何內容。

## run(ctx) <Badge type="warning" text="尚未發布" />

任務本身。一直執行到 `ctx.signal` 觸發為止。

| 名稱 | 類型 | 說明 |
|---|---|---|
| `ctx` | `ServiceContext` | 下面的欄位和函式 |

回傳 `Promise<void>`。回傳即結束這次執行。拋出錯誤算作失敗，並依 `restart` 重試。按下 **停止** 後，若 `run` 在 `stopTimeoutSeconds` 內沒有回傳，Extension 的程序會被重新啟動。

## ServiceContext 欄位

| 欄位 | 類型 | 說明 |
|---|---|---|
| `serviceId` | `string` | 任務的鍵：套件名稱，或 `<package>.<id>` |
| `runId` | `string` | 這次執行 |
| `signal` | `AbortSignal` | 在 **停止**、**重新啟動**、關閉 Extension、解除安裝以及結束 Cowork 時觸發。每次請求前先檢查它 |
| `env` | `Readonly<Record<string, string>>` | `dozin.secrets` 的值。缺少值的任務不會啟動，並顯示「缺少設定」 |
| `dataDir` | `string` | Extension 自己的資料資料夾，也是它的工作資料夾 |

## ctx.status(text, level?, progress?) <Badge type="warning" text="尚未發布" />

設定在 **設定 › 擴充功能** 裡任務下方顯示的那一行。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `text` | `string` | 是 | 這一行內容；最多保留 300 個字元 |
| `level` | `'info' \| 'warn' \| 'error'` | 否 | 預設 `info` |
| `progress` | `{ done: number, total: number }` | 否 | 以進度顯示 |

不回傳任何值。`dozin.secrets` 的值會被替換為 `***`。不要放入個人資料：坐在這台電腦前的任何人都看得到這一行。

```js
ctx.status('Synced 52 patients', 'info', { done: 52, total: 52 });
```

## ctx.log(text) <Badge type="warning" text="尚未發布" />

在 Extension 的日誌寫一行。`dozin.secrets` 的值會被替換為 `***`。不回傳任何值。

```js
ctx.log('round started');
```

## ctx.readJson(rel) <Badge type="warning" text="尚未發布" />

讀取 `dataDir` 內的一個 JSON 檔案。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `rel` | `string` | 是 | 相對於 `dataDir` 的路徑 |

回傳 `Promise<unknown>`：解析後的值，檔案不存在時為 `undefined`。路徑為空或在 `dataDir` 之外時以 `PATH_OUTSIDE_DATA` 拒絕；檔案不是有效的 JSON 時以解析錯誤拒絕。

## ctx.writeJson(rel, value) <Badge type="warning" text="尚未發布" />

寫入 `dataDir` 內的一個 JSON 檔案，一步完成替換，所以當機時絕不會留下寫了一半的檔案。

| 名稱 | 類型 | 必填 | 說明 |
|---|---|---|---|
| `rel` | `string` | 是 | 相對於 `dataDir` 的路徑 |
| `value` | `unknown` | 是 | `JSON.stringify` 能接受的任何值 |

回傳 `Promise<void>`。路徑為空或在 `dataDir` 之外時以 `PATH_OUTSIDE_DATA` 拒絕。

一次執行結束後，它發出的 `ctx.site` 和 `ctx.agent` 呼叫會以 `RUN_ENDED` 拒絕。

## 示例

```js
export function activate(api) {
  api.registerService({
    id: 'sync', name: 'Sync', start: 'auto',
    async run(ctx) {
      while (!ctx.signal.aborted) {
        const state = (await ctx.readJson('state.json')) ?? { runs: 0 };
        state.runs += 1;
        await ctx.writeJson('state.json', state);
        ctx.status(`Run ${state.runs} done`);
        await new Promise((resolve) => setTimeout(resolve, 15 * 60 * 1000));
      }
    }
  });
}
```

## 相關

- [站點請求](https://docs.micromeet.ai/zh-tw/extension-sdk/site.md): `ctx.site`
- [服務對話](https://docs.micromeet.ai/zh-tw/extension-sdk/agent.md): `ctx.agent`
- [入口與註冊](https://docs.micromeet.ai/zh-tw/extension-sdk/entry.md): `registerService`
