# 后台任务

<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` 在 `secrets` 都填好后随 Extension 一起启动。以用户的选择为准 |
| `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` reject；文件不是有效 JSON 时以解析错误 reject。

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

写入 `dataDir` 内的一个 JSON 文件，一步完成替换，所以崩溃时不会留下写了一半的文件。

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `rel` | `string` | 是 | 相对于 `dataDir` 的路径 |
| `value` | `unknown` | 是 | 任何 `JSON.stringify` 能接受的值 |

返回 `Promise<void>`。路径为空或在 `dataDir` 之外时以 `PATH_OUTSIDE_DATA` reject。

一次运行结束后，它发起的 `ctx.site` 和 `ctx.agent` 调用会以 `RUN_ENDED` reject。

## 示例

```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/extension-sdk/site.md): `ctx.site`
- [服务对话](https://docs.micromeet.ai/zh/extension-sdk/agent.md): `ctx.agent`
- [入口与注册](https://docs.micromeet.ai/zh/extension-sdk/entry.md): `registerService`
