---
title: OOMOL TypeScript SDK 參考
description: 查閱 Connector、ProjectConnector 與 OpenConnector 共用的設定、型別、錯誤和完整 API。
lang: zh-TW
canonical_url: https://oomol.com/zh-tw/docs/connector-sdk/
markdown_url: https://oomol.com/zh-tw/docs/connector-sdk.md
---

# OOMOL TypeScript SDK 參考

[`@oomol-lab/connector`](https://github.com/oomol-lab/connector-sdk) 包含三個 TypeScript client。請先按產品路徑選擇 client，再查閱本頁的共享設定與 API 參考：

- [`Connector`](/zh-tw/docs/connector-client/) 透過託管閘道呼叫你在 OOMOL 帳號中連接的帳號。
- [`ProjectConnector`](/zh-tw/docs/project-connector/) 為 SaaS 產品的最終使用者連接並呼叫各自的帳號。
- [`OpenConnector`](/zh-tw/docs/openconnector-sdk/) 呼叫由你維運的 OpenConnector runtime。

三個 client 共用傳輸行為、精確 action 型別和錯誤模型，但使用的 key、帳號邊界與可用方法不同。

這個套件輕量、零依賴；SDK 直接建構請求並解析回應。`gmail.search_threads`、`slack.post_message`、`notion.append_block` 等 action 可以直接透過 TypeScript 呼叫。

## 安裝

```sh
npm install @oomol-lab/connector   # 或：bun add / pnpm add / yarn add
```

需要 Node ≥ 18（依賴內建的 `fetch` 和 `AbortController`）。SDK 只發佈 `dist`，零執行時依賴，且 `sideEffects: false`，可以乾淨地做 tree-shaking。請在 Node、Bun、Deno 或邊緣 worker 等可信服務端環境中使用 `Connector` 和 `ProjectConnector`。瀏覽器應用應呼叫你的後端，不要把個人或 Project API key 打包進客戶端程式碼。

## 三個客戶端一覽

| | `Connector` | `ProjectConnector` | `OpenConnector` |
| --- | --- | --- | --- |
| 驗證 | 個人 `api_…` key | Project `oo_proj_…` key | 選用 runtime token `oct_…` |
| 對接 | OOMOL 託管閘道 | OOMOL 託管閘道 | 你執行的 OpenConnector runtime |
| 帳號資源 | 個人或 Team 的 connections | external users 的 connected accounts | runtime 中的 connections |
| 用途 | 呼叫個人或 Team 已連接的帳號 | 為 SaaS 產品使用者連接並呼叫帳號 | 呼叫自部署 runtime |
| 接入指南 | [Connector SDK](/zh-tw/docs/connector-client/) | [ProjectConnector](/zh-tw/docs/project-connector/) | [OpenConnector SDK](/zh-tw/docs/openconnector-sdk/) |

## `Connector`：呼叫個人或 Team connections

### 取得 API key

你需要一個 OOMOL Connector 的**個人 API key**，形如 `api_…`。在 OOMOL Console 建立：

<https://console.oomol.com/api-key>

把它設為環境變數；本指南統一使用 `OOMOL_API_KEY`。每個請求都會由閘道授權，並以 `Authorization: Bearer <apiKey>` 傳送。

> 個人 `api_…` key 在個人或 Team connection 上執行 action。代最終使用者連接帳號時，使用 Project key（`oo_proj_…`）和 `ProjectConnector`；見 [ProjectConnector 接入指南](/zh-tw/docs/project-connector/)。呼叫自部署 runtime 時，使用選用 runtime token（`oct_…`）和 `OpenConnector`；見 [OpenConnector SDK 指南](/zh-tw/docs/openconnector-sdk/)。

### 快速上手

建構客戶端後即可呼叫 action。下面兩種寫法等價，可任選一種。

```ts
import { Connector } from "@oomol-lab/connector";

const oomol = new Connector({ apiKey: process.env.OOMOL_API_KEY! });

// 路徑 1：動態字串。可呼叫任意 action id。
const { threads } = await oomol.execute("gmail.search_threads", { query: "from:boss" });

// 路徑 2：namespace 語法糖。底層仍是同一個呼叫。
const result = await oomol.gmail.search_threads({ query: "is:unread" });
```

`execute` 直接回傳 action 的輸出。當你還想要執行中繼資料時，用 `executeRaw`：

```ts
const raw = await oomol.executeRaw("gmail.search_threads", { query: "from:ceo" });
raw.data;        // 和 execute() 的返回值相同
raw.executionId; // 服務端分配的執行 id（用於工單支援 / 日誌關聯）
raw.actionId;    // 回顯的 action id
raw.message;     // 成功響應信封裡的可讀訊息
```

核心呼叫流程由 `execute` 和 `executeRaw` 組成。

### `Connector` 的概念

SDK 模型由五個核心概念組成，驗證、憑證和實際 provider 呼叫都由閘道處理：

| 術語 | 含義 |
| --- | --- |
| **Gateway（閘道）** | 這個客戶端對接的 OOMOL Connector 託管服務。它持有憑證、真正發起對 provider 的呼叫，並回傳統一的信封。SDK 在本機**不**執行任何整合邏輯。 |
| **Provider / service** | 一個第三方 API（`gmail`、`slack`、`github`、`notion`……）。它就是 action id 的 `<service>` 前綴。 |
| **Action** | provider 上的一個操作，識別為 `"<service>.<action>"`（例如 `gmail.search_threads`）。action 由閘道提供，客戶端負責呼叫。 |
| **Connection（連接）** | 某個 provider 已授權、已儲存的憑證。你從不直接接觸 token；只用 `connectionName` 指明用哪一個連接。OAuth 和憑證生命週期是閘道的職責。 |
| **Team（團隊）** | 選用的租戶作用域，以 `x-oo-team-name` 標頭傳送。 |

同一套詞彙貫穿整個 SDK 介面：action id 始終是 `"<service>.<action>"`，`connectionName` 用來選擇已儲存的連接；在專案客戶端中，`externalUserId` 標識你的某個最終使用者。

### 常見操作

| 想要…… | 用 | 說明 |
| --- | --- | --- |
| 執行一個已建模的 action | `execute` / `executeRaw` | 帶型別的一行呼叫。`executeRaw` 還會回傳 `{ executionId, actionId, message }`。 |
| 呼叫尚未建模為 action 的 endpoint | `proxy` | 透傳到上游 API，由閘道注入該連接的憑證。 |
| 把 action 餵給 LLM / 建構動態表單 | `catalog` | 任意 action 或 provider 的執行時 JSON Schema（2020-12）。 |
| 查看已經連了什麼 | `apps.list` | 唯讀列出你已經建立的連接。 |
| 讓**你的**使用者連接**他們自己的**帳號 | `ProjectConnector` | 一個獨立的、按專案作用域的客戶端，用來代你的最終使用者連接帳號並執行 action。 |

provider 和 action 的覆蓋由閘道提供。目前支援 600+ 個 provider，並持續增加；可透過 `oomol.catalog.providers()` 在執行時發現。

### 精確型別（選用）

動態字串路徑對**任意** `actionId` 都能編譯通過。預設情況下，每個 action 使用寬鬆型別（入參和出參都是 `Record<string, any>`），便於先完成呼叫。需要精確入/出參型別和 JSDoc 時，安裝配套型別套件，並為用到的**每個 provider 新增一行 side-effect import**：

```ts
import { Connector } from "@oomol-lab/connector";
import "@oomol-lab/connector-types/gmail";   // gmail.* 的精確型別 + JSDoc
import "@oomol-lab/connector-types/slack";   // ……以及 slack.*

const oomol = new Connector({ apiKey: process.env.OOMOL_API_KEY! });

await oomol.gmail.search_threads({ query: "from:boss" }); // 入參與出參現在都是精確型別
await oomol.notion.append_block({ pageId, text });        // 沒 import notion → 仍可寬鬆呼叫
```

```sh
npm install -D @oomol-lab/connector-types
```

精確型別由 `@oomol-lab/connector-types` 和 side-effect import 提供，專案中不會生成額外檔案。已註冊的 action 會得到字面量補全和精確的入/出參型別；**未註冊的會退化為 `Record<string, any>`**。即使型別套件落後於後端，新 action 也可以繼續透過寬鬆型別呼叫。核心執行時與型別套件分別發佈。

> 需要把 `moduleResolution` 設為 `bundler`、`node16` 或 `nodenext`，子路徑 import（`@oomol-lab/connector-types/gmail`）才能解析。設定細節見 [`@oomol-lab/connector-types`](https://github.com/oomol-lab/connector-types) 儲存庫。

### 設定

除 `apiKey` 外每個欄位都是選用的：

```ts
new Connector({
  apiKey: process.env.OOMOL_API_KEY!,        // 必填
  baseUrl: "https://connector.oomol.com/v1", // 預設值
  team: "acme",                              // 預設團隊 → x-oo-team-name
  connectionName: "work",                    // 預設連線（更推薦按呼叫 / using() 設定）
  timeoutMs: 30_000,                         // 預設單次請求超時
  maxRetries: 2,                             // 預設；對 429 / 5xx / 網路錯誤用退避 + 抖動重試
  fetch: customFetch,                        // 用於測試 / 代理 / 鏈路追蹤時注入
});
```

| 欄位 | 預設值 | 說明 |
| --- | --- | --- |
| `apiKey` | — | 必填。以 `Authorization: Bearer <apiKey>` 傳送。 |
| `baseUrl` | `https://connector.oomol.com/v1` | 透過 client 設定顯式覆寫。 |
| `team` | — | 呼叫在哪個租戶下執行。 |
| `connectionName` | — | 當某個 provider 有多個連接時，選用哪一個。單連接場景可作為客戶端預設值；多連接場景建議按呼叫或用 `using()` 設定。 |
| `timeoutMs` | `30_000` | 單次請求逾時（毫秒）。 |
| `maxRetries` | `2` | 對 429 / 5xx / 網路錯誤重試，採用指數退避 + 抖動。 |
| `fetch` | 全域 `fetch` | 為測試、代理 agent 或鏈路追蹤注入自訂 `fetch`。 |

對於 `gmail.send_email`、`slack.post_message` 這類有副作用的 action，除非已經確認該 action 提供可用的幂等機制，否則應傳入 `{ retries: 0 }`。網路錯誤可能發生在 provider 已接受請求之後，自動重試可能造成重複操作。

#### 作用域與按呼叫選項

三個層級按此優先順序解析：**按呼叫選項 > `using()` 作用域 > 客戶端預設值。**

`using()` 回傳一個不可變的、合併了給定預設值的子客戶端；原客戶端不受影響：

```ts
const work = oomol.using({ connectionName: "work", team: "acme" });
await work.gmail.search_threads({ query: "label:urgent" }); // 在 "work" / "acme" 下執行
```

按呼叫選項只作用於本次呼叫，並具有最高優先順序：

```ts
await oomol.execute(
  "gmail.search_threads",
  { query: "from:ceo" },
  {
    team: "acme",               // 本次呼叫覆蓋團隊
    connectionName: "alt",      // 本次呼叫選用另一個連線
    timeoutMs: 10_000,          // 本次呼叫用更緊的超時
    retries: 0,                 // 本次呼叫禁用重試
    signal: controller.signal,  // 傳入 AbortSignal
  },
);
```

`connectionName` 按同樣的層級解析。它在傳輸層以 `x-oo-connector-alias` 標頭攜帶（閘道欄位名為 `alias`），SDK 介面統一使用 `connectionName`。

### Proxy：直接呼叫上游 endpoint

`proxy` 可以直接存取上游 API endpoint。閘道會注入所選 connection 的憑證，請求和回應保持上游 API 的原始形態。

```ts
// 帶型別的 GET。請求路徑使用 `endpoint` 欄位。
const repos = await oomol.proxy<Array<{ name: string }>>("github", {
  endpoint: "/user/repos",
  method: "GET",
  query: { per_page: 5, sort: "updated" },
});
repos.status;               // 上游 HTTP 狀態碼
repos.data.map((r) => r.name);

// 帶 body 和上游 header 的 POST；這些 header 會傳送給 provider。
await oomol.proxy("github", {
  endpoint: "/repos/acme/widgets/issues",
  method: "POST",
  headers: { "X-GitHub-Api-Version": "2022-11-28" },
  body: { title: "Tracking issue", labels: ["chore"] },
});
```

`endpoint` 既接受路徑（相對於 provider 的 base URL 解析），也接受完整 URL，適用於帶區域域名的 provider，例如 `https://eu.posthog.com/api/...`。`method` 是 `GET | POST | PUT | PATCH | DELETE` 之一。回應是 `{ status, headers, data }`。proxy 的 body 在後端是 strict 的：未知頂層 key 會以 `invalid_input` 拒絕。

### Catalog：檢視 provider 與 action

catalog 提供唯讀執行時中繼資料，可用於動態表單、驗證和 LLM 工具定義。入/出參 schema 使用 JSON Schema（2020-12），與編譯期型別套件相互獨立。

```ts
// 列出 provider；可選地在服務端收窄。
const all = await oomol.catalog.providers();                       // 全部 provider
const mailish = await oomol.catalog.providers({ q: "mail" });      // 全文搜尋 → ?q=
const some = await oomol.catalog.providers({ service: ["gmail", "slack"] }); // 限定 → ?service=…

// 某個 service 的全部 action。
const actions = await oomol.catalog.actions("gmail");

// 單個 action 的完整後設資料，包含執行時 JSON Schema。
const meta = await oomol.catalog.action("gmail.search_threads");
meta.name;          // 可讀名稱
meta.requiredScopes;// 該 action 需要的 OAuth scope
meta.inputSchema;   // 入參的 JSON Schema（2020-12）
meta.outputSchema;  // 出參的 JSON Schema（2020-12）
```

每個 provider 攜帶 `{ service, displayName, iconUrl, homepageUrl, categories, authTypes }`。

### Apps：列出你已連接的帳號

`apps.list()` 回傳閘道已持有連接的唯讀檢視。連接建立和移除在 Console 中完成。

```ts
const apps = await oomol.apps.list();
for (const app of apps) {
  // { id, service, status, connectionName, … }；未設定時 connectionName 為 null。
  console.log(`${app.service}: id=${app.id} status=${app.status} connectionName=${app.connectionName}`);
}

// 把某個連線的 connectionName 作為按呼叫選擇器傳回，即可指定它。
const work = apps.find((a) => a.connectionName === "work");
if (work) {
  await oomol.execute("gmail.search_threads", { query: "is:unread" }, { connectionName: "work" });
}
```

### 錯誤處理

失敗會拋出帶型別的 `ConnectorError`。呼叫方主動取消（被中止的 `AbortSignal`）會拋出標準 `AbortError`，可與閘道或傳輸錯誤區分。

```ts
import { Connector, ConnectorError, isRetryable } from "@oomol-lab/connector";

try {
  await oomol.slack.post_message({ channel: "#general", text: "shipped" });
} catch (err) {
  if (err instanceof ConnectorError) {
    err.code;        // 可判別聯合，例如 "rate_limited"、"credential_expired"
    err.status;      // HTTP 狀態碼（客戶端 / 網路錯誤時為 0）
    err.requestId;   // 失敗關聯 id
    err.actionId;    // 適用時
    err.executionId; // 適用時
    err.data;        // 上游響應體，例如 provider_error 時
    if (isRetryable(err)) {
      // 429 / 5xx / 網路 / rate_limited / proxy_upstream_timeout / request_in_progress
    }
  } else {
    throw err; // 非 ConnectorError，例如呼叫方取消產生的 AbortError；重新丟擲
  }
}
```

`err.code` 是一個**開放**聯合：已知後端碼有自動補全，新的後端碼也會作為字串透傳。處理時建議保留預設分支。常見錯誤碼按組如下：

| 分組 | 錯誤碼 |
| --- | --- |
| 輸入 / 請求 | `invalid_input`、`invalid_request_payload`、`invalid_request_signature` |
| App / provider | `app_not_found`、`app_not_ready`、`app_auth_type_mismatch`、`provider_not_found`、`provider_not_configured`、`provider_config_not_found`、`provider_error`、`profile_not_found` |
| 憑證 / 驗證 | `credential_expired`、`scope_missing`、`user_oauth_client_required` |
| 連接選擇 | `connection_ambiguous`、`connection_account_conflict`、`connection_alias_conflict`、`connection_request_not_found`、`connected_account_not_found` |
| Proxy | `proxy_not_supported`、`proxy_upstream_error`、`proxy_upstream_timeout`、`proxy_response_too_large` |
| 限流 / 並行 | `rate_limited`、`request_in_progress`、`request_key_conflict`、`request_key_used` |
| 僅客戶端（status 0，請求未發出或傳輸失敗） | `client_invalid_request`、`client_timeout`、`client_network_error`、`client_wait_timeout` |

`isRetryable(err)` 對 `rate_limited`、`proxy_upstream_timeout`、`request_in_progress`、HTTP 429、任意 5xx 以及傳輸失敗（status 0）回傳 `true`。對客戶端驗證錯誤（`client_invalid_request`）和 `waitForConnection` 的等待上限（`client_wait_timeout`）回傳 `false`；這兩類錯誤通常需要修改呼叫或等待邏輯。

#### 取消與逾時

傳入 `AbortSignal` 可取消呼叫；設定 `timeoutMs` 可限定單次呼叫。內建重試層會處理瞬時失敗；需要一次確定性的單次嘗試時，可設定 `retries: 0`。

```ts
const controller = new AbortController();
setTimeout(() => controller.abort(), 50);
try {
  await oomol.execute("gmail.search_threads", { query: "huge" }, { signal: controller.signal });
} catch (err) {
  (err as Error).name; // "AbortError"
}
```

## `ProjectConnector`：為產品使用者連接帳號

`Connector` 在**你自己的**連接上執行 action。`ProjectConnector` 面向 SaaS 產品：**你的**最終使用者透過你的應用連接**他們自己的** Gmail / Slack / GitHub / …… 帳號，然後由你的後端代他們執行 action。這屬於託管驗證（managed auth）模型，使用方式類似 [Composio](https://composio.dev) 和 [Pipedream Connect](https://pipedream.com/docs/connect)。

使用 OAuth 時，使用者在閘道託管頁面上授權，provider token 保留在閘道中，你的程式碼只持有不透明識別碼。`project.connect.apiKey` 和 `project.connect.customCredential` 的邊界不同：最終使用者的密鑰會先由你的可信後端接收，再傳送給閘道。不要讓這些密鑰進入瀏覽器、提示詞、模型上下文或日誌。

### 最終使用者識別碼

**`externalUserId`** 是專案客戶端的使用者隔離鍵，由**你**選定，通常使用你自己資料庫裡的使用者 id。連接帳號、等待連接、執行 action 等專案操作都按它作用域。始終傳入一致的 `externalUserId`，閘道會讓每個使用者的連接彼此隔離。

### 建構專案客戶端

`ProjectConnector` 是一個使用 **Project API key**（`oo_proj_…`）的獨立客戶端。它提供 `connect.*`、`waitForConnection`、`getUserProfile`、`execute`、`executeRaw` 和 `forUser` 等 Project 作用域操作。

```ts
import { ProjectConnector } from "@oomol-lab/connector";

const project = new ProjectConnector({ apiKey: process.env.OOMOL_PROJECT_API_KEY! }); // oo_proj_...
```

> **先在 Console 完成設定。** 在你的後端連接帳號之前，需要由管理員在 OOMOL Console 建立 Project、服務設定（provider config）和 Project API key。一次性設定和對應的後端 REST 流程見 [Connector for SaaS 使用指南](/zh-tw/docs/connector-saas/)。本 SDK 是同一套執行時 API 的帶型別封裝。

### OAuth：建立連結，再等待完成

OAuth 分兩步：建立待處理的連接請求，引導使用者授權，再等待完成。

```ts
// 1. 為你的某個使用者建立一個待處理的連線請求。
const request = await project.connect.oauth("user_42", {
  service: "gmail",
  connectionName: "work",                          // 要賦予的名字；之後用它來指定這個賬號
  returnUri: "https://app.example.com/connected",  // 回撥完成後閘道器把使用者帶回到這裡
});

// 2. 把使用者帶到 provider 的授權頁面。
redirectUserTo(request.authorizationUrl);

// 3. 輪詢直到使用者完成（或失敗 / 過期）。返回最終的連線請求。
const connected = await project.waitForConnection(request);
connected.status;             // "connected" | "failed" | "expired"
connected.connectedAccountId; // 連線成功後儲存的賬號 id
```

`authorizationUrl` 會先開啟 Connector 託管的授權入口頁，展示你的專案和使用者將要授權的 provider，然後再把使用者帶到 provider 的 OAuth 頁面。

![Connector 授權入口頁，顯示 my-saas 正在連線 Gmail 並即將跳轉到 Gmail](/img/docs/connector-sdk/zh-cn/authorization-entry.png)

`waitForConnection` 會輪詢到請求離開 `initiated` 狀態後回傳它。如果使用者未完成授權，請求會自然變為 `expired`。`maxWaitMs`（預設 `600_000` 毫秒，與請求過期時間一致）先耗盡時，會拋出錯誤碼為 `client_wait_timeout` 的 `ConnectorError`；被中止的 `signal` 會拋出標準 `AbortError`。

當你的 `returnUri` 被命中時，閘道會追加一些 query 參數，你可以在回呼頁面讀取：

```text
status=success
service=gmail
providerConfigId=pc-1
externalUserId=user_42
connectedAccountId=ca-1
```

……或者在取消 / provider 出錯時：

```text
status=error
code=<connector-error-code>
message=<human-readable-message>
```

### API key / 自訂憑證：同步回傳

API key 和自訂憑證連接會立即回傳帳號；只有 OAuth 需要 `waitForConnection`。

```ts
// 終端使用者自己的上游 key（例如 OpenAI 的 sk-…），不要填寫 oo_proj_ key。
const account = await project.connect.apiKey("user_42", { service: "openai", apiKey: "sk-..." });
account.available; // 該賬號此刻是否可以執行 action

// provider 特有的憑證欄位，由閘道器按 provider config 校驗。
await project.connect.customCredential("user_42", {
  service: "jira",
  values: { email: "user@acme.com", token: "..." },
});
```

每個 `connect.*` 呼叫用 `service` 或 `providerConfigId` 中**恰好一個**來標識 provider。當一個專案對同一 service 有多個設定時用 `providerConfigId`；簡單情形用 `service`。

### 使用者連接的是哪個第三方帳號

`getUserProfile` 讀取一個已連接帳號背後的第三方帳號主體：你的最終使用者在 provider 上到底是誰。閘道會即時從 provider 拉取，並歸一化成跨 provider 統一的結構。

```ts
const { service, profile, fetchedAt } = await project.getUserProfile(account.connectedAccountId);

profile.id;          // provider 側穩定的使用者 id
profile.kind;        // "user" | "bot" | "service_account" | "unknown"（開放聯合型別）
profile.username;    // 使用者名稱/handle，或 null
profile.displayName; // 展示名，或 null
profile.avatarUrl;   // 頭像 URL，或 null
profile.email;       // 郵箱；已授權的 scope 不包含時為 null
profile.metadata;    // 閘道器暴露的 provider 特有欄位
```

provider 未提供或已授權 scope 未覆蓋的欄位（最常見的是 `email`）會保留在回應中並設為 `null`。`fetchedAt` 是閘道讀取該資料時的 Unix 毫秒時間戳。`connectedAccountId` 可以來自 `connect.apiKey` / `connect.customCredential` 的回傳值，或來自已進入 `connected` 狀態的 `ConnectionRequest`。未知 id 會以 `connected_account_not_found` 拒絕；provider 缺少 `userProfile` 能力時回傳 `profile_not_found`；帳號未就緒或不可用時還可能回傳 `app_not_ready`、`app_auth_type_mismatch` 或 `credential_expired`。限定到單一使用者的子客戶端上也有這個方法：`user.getUserProfile(connectedAccountId)`。

### 代使用者執行 action

```ts
// provider service 從 actionId 字首（"gmail"）推導。
// 未傳 connectionName / connectedAccountId 時，使用該使用者最新的 active 賬號。
const out = await project.execute(
  "user_42",
  "gmail.search_threads",
  { query: "is:unread" },
  { connectionName: "work" },
);
```

帳號選擇優先順序：`connectedAccountId`（指定某個帳號）優先於 `connectionName`（按名字選帳號）；兩者都不傳時，閘道使用該使用者在該 provider 下最新的 active 帳號。`project.executeRaw` 回傳與個人客戶端相同的 `{ data, executionId, actionId, message }` 信封。

### 綁定到單一使用者

`forUser` 可預先綁定 `externalUserId`，後續呼叫不必重複傳 id：

```ts
const user = project.forUser("user_42");
const request = await user.connect.oauth({ service: "slack" });
const slack = await user.waitForConnection(request);
if (slack.status === "connected") {
  await user.execute(
    "slack.post_message",
    { channel: "#general", text: "shipped" },
    { connectedAccountId: slack.connectedAccountId },
  );
}
```

`project.execute` 複用與個人路徑相同的 [`@oomol-lab/connector-types`](https://github.com/oomol-lab/connector-types) 註冊表：已 import 的 provider 得到精確的入/出參，其餘保持寬鬆可呼叫。

### 授權請求與帳號生命週期

| 物件 | 何時拿到 | 關鍵欄位 |
| --- | --- | --- |
| `ConnectionRequest` | 由 `connect.oauth` 回傳，可經 `getConnectionRequest` / `waitForConnection` 重新讀取 | `id`、`status`（`initiated` → `connected` / `failed` / `expired`）、`authorizationUrl`、`connectedAccountId`、`externalUserId`、`connectionName`、`expiresAt` |
| `ConnectedAccount` | 由 `connect.apiKey` / `connect.customCredential` 同步回傳；OAuth 請求完成後也指向它 | `id` / `connectedAccountId`、`status`（`active`、`reauth_required`、`error`、`disconnected`）、`available`、`externalUserId`、`connectionName`、`service` |

只有當執行所需條件都就緒時，`available` 才為 `true`：provider config 是 active、帳號是 active、底層 app 是 active，且憑證存在。兩個 status 欄位都是**開放**聯合；處理時保留預設分支，以相容新的後端狀態。

### 從 Composio / Pipedream 遷移？

| Composio / Pipedream | `@oomol-lab/connector` |
| --- | --- |
| `userId` / `external_user_id` | `externalUserId` |
| `connectedAccounts.initiate` / `createConnectToken`（OAuth） | `project.connect.oauth` |
| `connectedAccounts.initiate` + `AuthScheme.APIKey` | `project.connect.apiKey` |
| `waitForConnection()` | `project.waitForConnection()` |
| `tools.execute(slug, { userId, arguments })` | `project.execute(externalUserId, actionId, input)` |
| `composio.getEntity(userId)` | `project.forUser(externalUserId)` |

## `OpenConnector`：呼叫自部署 runtime

想自己執行開源的 Connector 服務——在 localhost、Docker，或你自己的基礎設施上？**`OpenConnector`** 就是它的個人客戶端。它把 `Connector` 的介面（兩條呼叫路徑、`proxy`、`catalog`、`apps`）指向**你自己**執行的伺服器，所以你已經寫好的程式碼幾乎不用改。搭建伺服器是另一個話題，見 [OpenConnector 自託管指南](/zh-tw/docs/openconnector-self-hosting/)。

`OpenConnector` client 指向一個由你維運的 runtime。帳號組織方式、connection 選擇和存取策略由該 runtime 的設定決定。

```ts
import { OpenConnector } from "@oomol-lab/connector";

const open = new OpenConnector(); // 僅用於本地或私有網路；預設 http://localhost:3000

await open.execute("hackernews.get_top_stories", {});             // 路徑 1 — 動態字串
await open.gmail.search_threads({ query: "from:boss" });          // 路徑 2 — namespace 語法糖，登錄檔型別相同
await open.proxy("github", { endpoint: "/user", method: "GET" }); // 路徑 3 — 透傳到尚未建模的 endpoint
await open.catalog.search("send email", { limit: 5 });            // catalog 擴充套件：search、services
await open.apps.list();                                           // 只讀檢視執行時的連線
```

兩條呼叫路徑和精確型別工作流都與個人 `Connector` 一致；同樣的 `@oomol-lab/connector-types` side-effect import 也適用。OpenConnector 的 `proxy.endpoint` 接受以 `/` 開頭的**相對路徑**；傳入絕對 URL 會回傳 `invalid_input`。缺少 proxy executor 的 provider 會回傳 `proxy_not_supported`。

### 指向你的伺服器

`baseUrl` 接收伺服器 **origin**，客戶端會自動新增 API 路徑前綴。未建立 token 的 runtime 只適合 localhost 或其他私有網路。透過公開 URL 暴露前，應先在 Web Console 的 Access 頁面建立 runtime token（`oct_…`），並要求所有 `/v1` 和 `/mcp` client 攜帶它。

```ts
const open = new OpenConnector({
  baseUrl: "https://connect.internal.example.com",       // 伺服器 origin
  runtimeToken: process.env.OOMOL_CONNECT_RUNTIME_TOKEN!, // oct_…；公開 runtime 必填
  connectionName: "work",                                // 可選的客戶端級預設連線
});
```

每個欄位都是選用的。`timeoutMs`、`maxRetries`、`fetch` 的行為與託管客戶端完全一致。

### 僅執行時才有的介面

`OpenConnector` 提供以下 runtime 專用介面：

```ts
await open.health();                                    // { ok, runtime } — 連通性 / 鑑權探測
await open.catalog.services();                          // 所有有 action 的 service id
await open.catalog.search("top stories", { limit: 3 }); // 按全文相關度對 action 排序
await open.apps.listByService("github");                // 某個 service 的連線
await open.apps.authenticated(["github", "notion"]);    // 哪些已存有**真實**憑證
```

連接選擇分兩層：按呼叫的 `connectionName` 覆寫客戶端預設值；兩層都省略時，runtime 使用 `"default"` connection。

> **管理操作使用 Web Console。** 在主控台建立 connection、設定 OAuth client 並生成 runtime token；`OpenConnector` SDK 呼叫已經設定好的 runtime。設定方法見[自託管指南](/zh-tw/docs/openconnector-self-hosting/)。當 service id 與成員名（`execute` / `executeRaw` / `health` / `proxy` / `catalog` / `apps`）衝突時，可以透過 `execute("<service>.<action>", …)` 呼叫。

完整可執行範例——[`examples/open.ts`](https://github.com/oomol-lab/connector-sdk/blob/main/examples/open.ts)。

## 參考

### `Connector`（個人 `api_…` key）

```ts
new Connector(config: ClientConfig)

oomol.execute(actionId, input, options?)     // → action 輸出
oomol.executeRaw(actionId, input, options?)  // → { data, executionId, actionId, message }
oomol.<service>.<action>(input, options?)    // execute 的 namespace 語法糖
oomol.using(scope)                           // → 不可變的作用域子客戶端
oomol.proxy(service, { endpoint, method, query?, headers?, body? }, options?) // → { status, headers, data }
oomol.catalog.action(actionId, options?)     // → ActionMetadata
oomol.catalog.actions(service, options?)     // → ActionMetadata[]
oomol.catalog.providers(query?, options?)    // → ProviderMetadata[]   query: { service?: string[]; q?: string }
oomol.apps.list(options?)                    // → ConnectedApp[]
```

### `ProjectConnector`（專案 `oo_proj_…` key）

```ts
new ProjectConnector(config: ProjectConnectorConfig)

project.connect.oauth(externalUserId, input, options?)            // → ConnectionRequest（待處理）
project.connect.apiKey(externalUserId, input, options?)           // → ConnectedAccount（同步）
project.connect.customCredential(externalUserId, input, options?) // → ConnectedAccount（同步）
project.getConnectionRequest(connectionRequestId, options?)       // → ConnectionRequest
project.waitForConnection(requestOrId, options?)                  // → ConnectionRequest   options: { pollIntervalMs?, maxWaitMs?, signal?, timeoutMs? }
project.getUserProfile(connectedAccountId, options?)              // → ConnectedAccountProfile
project.execute(externalUserId, actionId, input, options?)        // → action 輸出
project.executeRaw(externalUserId, actionId, input, options?)     // → { data, executionId, actionId, message }
project.forUser(externalUserId)                                   // → ProjectUser（方法相同，id 已繫結）
```

`connect.*` 的入參是 `{ service | providerConfigId } & { connectionName?, … }`（`service` / `providerConfigId` 恰好取一個）。`execute` 的選項額外增加 `{ providerConfigId?, service?, connectedAccountId?, connectionName? }`。

### `OpenConnector`（自託管執行時，選用 `oct_…` token）

```ts
new OpenConnector(config?: OpenConnectorConfig)   // 每個欄位都可選；baseUrl 預設 http://localhost:3000

open.execute(actionId, input, options?)       // → action 輸出
open.executeRaw(actionId, input, options?)    // → { data, executionId, actionId, message }
open.<service>.<action>(input, options?)      // execute 的 namespace 語法糖
open.health(options?)                         // → { ok, runtime }
open.proxy(service, { endpoint, method, query?, headers?, body? }, options?) // → { status, headers, data }（endpoint 必須是相對路徑）
open.catalog.action(actionId, options?)       // → OpenActionMetadata
open.catalog.actions(service, options?)       // → OpenActionMetadata[]
open.catalog.services(options?)               // → string[]（有 action 的 service id）
open.catalog.providers(query?, options?)      // → ProviderMetadata[]
open.catalog.search(q, query?, options?)      // → OpenActionSearchResult[]   query: { service?, limit? }
open.apps.list(options?)                      // → ConnectedApp[]
open.apps.listByService(service, options?)    // → ConnectedApp[]
open.apps.authenticated(services, options?)   // → string[]（存有真實憑證的 service）
```

`options` 是 `{ connectionName?, signal?, timeoutMs?, retries? }`。`config` 支援 `{ baseUrl?, runtimeToken?, connectionName?, timeoutMs?, maxRetries?, fetch? }`。

### 匯出項

```ts
import {
  Connector,
  ProjectConnector,
  OpenConnector,
  ConnectorError,
  isRetryable,
} from "@oomol-lab/connector";
import type {
  ClientConfig, CallOptions, ScopeOptions, RawResult,
  ProxyRequest, ProxyResponse, ProxyMethod,
  CatalogApi, ActionMetadata, ProviderMetadata, ProviderQuery,
  AppsApi, ConnectedApp,
  ConnectorErrorCode,
  ProjectConnectorConfig, ProjectCallOptions, ProjectExecuteOptions,
  ConnectionRequest, ConnectedAccount, ProviderSelector,
  ConnectedAccountProfile, ProviderUserProfile, ProviderUserKind,
  OAuthConnectInput, ApiKeyConnectInput, CustomCredentialConnectInput,
  OpenConnectorConfig, OpenConnectorApi, OpenCallOptions, OpenExecuteOptions,
  OpenCatalogApi, OpenAppsApi, OpenHealth,
  OpenActionMetadata, OpenActionFollowUp, OpenActionAsyncLifecycle,
  OpenActionSearchResult, OpenSearchQuery,
} from "@oomol-lab/connector";
```

可執行且經過型別檢查的範例位於儲存庫的 [`examples/`](https://github.com/oomol-lab/connector-sdk/tree/main/examples) 目錄。

## 授權條款

MIT，見 [connector-sdk](https://github.com/oomol-lab/connector-sdk) 儲存庫。
