> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tapapi.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js / TypeScript

> Node.js 后端、脚本和 Next.js 服务端接入

Node.js / TypeScript 适合 Web 后端、Server Actions、队列任务和自动化工具。不要在浏览器端直接暴露 Tapapi API Key。

<Warning>以下示例默认运行在服务端、脚本、队列 worker、Next.js Route Handler 或 Server Action 中。不要把 Tapapi API Key 放进浏览器前端、移动端包、公开仓库或客户端配置。</Warning>

## 安装

```bash theme={null}
npm install openai
```

## 环境变量

```bash theme={null}
TAPAPI_API_KEY=sk-xxx
TAPAPI_BASE_URL=https://tapapi.ai/v1
```

## 初始化客户端

```typescript theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.TAPAPI_API_KEY,
  baseURL: process.env.TAPAPI_BASE_URL || "https://tapapi.ai/v1",
  timeout: 120_000,
});
```

OpenAI Node SDK 的 `timeout` 单位是毫秒。SDK 默认会对部分网络错误、429 和 5xx 做有限重试；生产系统仍然需要自己的业务 `task_id`、幂等记录和并发控制。

## 文本请求

```typescript theme={null}
const resp = await client.chat.completions.create({
  model: "gpt-5.4",
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user", content: "用一句话解释 Tapapi 是什么" },
  ],
});

console.log(resp.choices[0]?.message?.content);
```

## 流式输出

```typescript theme={null}
const stream = await client.chat.completions.create({
  model: "gpt-5.4",
  stream: true,
  stream_options: { include_usage: true },
  messages: [{ role: "user", content: "写一段 100 字的产品介绍" }],
});

for await (const chunk of stream) {
  const text = chunk.choices[0]?.delta?.content;
  if (text) process.stdout.write(text);
}
```

流式响应通常读取 `choices[0].delta.content`。生产解析时要跳过空内容、保活事件和没有普通文本 delta 的 chunk；最终 usage 可能受上游或网络影响缺失，计费以控制台账单记录为准。

## Responses 请求

`/v1/chat/completions` 是 Node.js 接入的默认主线。需要 Responses API 时，先确认你的 `openai` SDK 版本、模型详情和返回字段：

```typescript theme={null}
const resp = await client.responses.create({
  model: "gpt-5.4",
  input: "用一句话解释 Tapapi 是什么",
});

console.log(resp.output_text);
```

Responses 的返回结构和 Chat Completions 不完全一样，不要共用同一套解析代码。

## 图片生成

```typescript theme={null}
const image = await client.images.generate({
  model: "nano-banana-pro",
  prompt: "a clean product photo of a white ceramic mug",
  n: 1,
  size: "1024x1024",
  response_format: "url",
});

if (!image.data?.length) {
  throw new Error("Tapapi returned empty image data");
}

const item = image.data[0];
const output = item.url || item.b64_json;

if (!output) {
  throw new Error("Tapapi returned image data without url or b64_json");
}

console.log(output);
```

图片返回可能是 `url` 或 `b64_json`。生产环境建议把图片转存到自己的对象存储，不要长期依赖临时 URL。批量生成时还要记录请求 `n`、实际返回数量和成功转存数量；如果响应包含 `metadata.tapapi_partial`，按实际返回张数处理。

## 保存图片结果

```typescript theme={null}
import { writeFile } from "node:fs/promises";

async function saveImageItem(
  item: { url?: string | null; b64_json?: string | null },
  path: string
) {
  if (item.url) {
    const response = await fetch(item.url);

    if (!response.ok) {
      throw new Error(`Image download failed: ${response.status}`);
    }

    const bytes = Buffer.from(await response.arrayBuffer());
    await writeFile(path, bytes);
    return;
  }

  if (item.b64_json) {
    await writeFile(path, Buffer.from(item.b64_json, "base64"));
    return;
  }

  throw new Error("image item has no url or b64_json");
}

await saveImageItem(item, "output.png");
```

如果 URL 下载失败，优先重试下载和转存，不要立刻重新生成图片。

## 原生 fetch

```typescript theme={null}
async function safeJson(response: Response) {
  try {
    return await response.json();
  } catch {
    return {};
  }
}

function parseTapapiError(response: Response, payload: any) {
  const error = payload?.error || {};

  return {
    status: response.status,
    request_id: response.headers.get("X-Oneapi-Request-Id"),
    type: error.type,
    code: error.code || payload?.code,
    message: error.message || payload?.message || response.statusText,
  };
}

const response = await fetch("https://tapapi.ai/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TAPAPI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "gpt-5.4",
    messages: [{ role: "user", content: "Hello" }],
  }),
});

const data = await safeJson(response);

if (!response.ok) {
  throw new Error(JSON.stringify(parseTapapiError(response, data)));
}

console.log(data.choices[0].message.content);
```

## SDK 错误处理

```typescript theme={null}
try {
  const resp = await client.chat.completions.create({
    model: "gpt-5.4",
    messages: [{ role: "user", content: "Hello" }],
  });

  console.log(resp.choices[0]?.message?.content);
} catch (err) {
  if (err instanceof OpenAI.APIError) {
    console.error({
      status: err.status,
      request_id: err.request_id,
      code: err.code,
      message: err.message,
    });
  }

  throw err;
}
```

OpenAI SDK 的 `request_id` 通常来自上游标准 `x-request-id`。Tapapi 排障时还要优先保存响应头里的 `X-Oneapi-Request-Id`；如果你需要完整请求标识，建议在关键路径使用原生 `fetch` 或 SDK 的 raw response 能力读取 headers。

## 超时控制

```typescript theme={null}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 120_000);

try {
  const response = await fetch("https://tapapi.ai/v1/images/generations", {
    method: "POST",
    signal: controller.signal,
    headers: {
      Authorization: `Bearer ${process.env.TAPAPI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "nano-banana-pro",
      prompt: "a product photo of a white mug",
      size: "1024x1024",
    }),
  });

  const data = await safeJson(response);

  if (!response.ok) {
    throw new Error(JSON.stringify(parseTapapiError(response, data)));
  }

  if (!data.data?.length) {
    throw new Error("Tapapi returned empty image data");
  }

  const item = data.data[0];
  console.log(item.url || item.b64_json);
} finally {
  clearTimeout(timer);
}
```

客户端超时只代表你的进程不再等待响应，不一定代表上游任务没有执行。遇到 `AbortError`、504 或 524 时，先查自己的业务任务状态、Tapapi 账单和日志，再决定是否补跑。

## 生产建议

| 场景      | 建议                                                                     |
| ------- | ---------------------------------------------------------------------- |
| Next.js | 用 Route Handler 或 Server Action 做服务端代理，不要在 Client Component 里调用 Tapapi |
| API Key | 只放服务端环境变量，不放 URL query、前端代码或公开配置                                       |
| 批量任务    | 加队列和并发限制，不要一次性并发打满                                                     |
| 重试      | 只对 429、临时 5xx、网络中断做有限重试，并加随机抖动                                         |
| 幂等      | 每个业务任务保存自己的 `task_id`，避免重复点击或 worker 重启重复请求                            |
| 日志      | 保存接口、模型、HTTP 状态码、`request_id`、错误字段和最终用量                                |
| 图片保存    | 同时兼容 `url` 和 `b64_json`，成功后尽快转存                                        |
| 视频 API  | 受控开放，先看 [视频 API](/video-generation/overview)，不要默认写进批量任务                |

## 下一步

| 场景              | 文档                                           |
| --------------- | -------------------------------------------- |
| OpenAI SDK 迁移总览 | [OpenAI SDK 迁移](/integrations/openai-sdk)    |
| 不使用 SDK         | [HTTP REST](/integrations/http-rest)         |
| Python 完整示例     | [Python](/integrations/python)               |
| PHP / Laravel   | [PHP / Laravel](/integrations/php-laravel)   |
| Next.js 代理      | [Next.js 代理接入](/integrations/nextjs-proxy)   |
| 生产上线检查          | [上线 Checklist](/production/launch-checklist) |
