pinme-api

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PinMe Worker API 集成

PinMe Worker API 集成

指导在 PinMe Worker(TypeScript)中调用 PinMe 平台的邮件发送和 LLM API。
指导在 PinMe Worker(TypeScript)中调用 PinMe 平台的邮件发送和 LLM API。

环境变量

环境变量

Worker 创建时自动注入以下环境变量,无需手动配置:
typescript
// backend/src/worker.ts
export interface Env {
  DB: D1Database;
  API_KEY: string;      // 项目 API Key — 用于 send_email 和 chat/completions 认证
  BASE_URL?: string;    // 可选覆盖 PinMe API 基础地址,默认 https://pinme.dev
}
API_KEY
是 Worker 调用 PinMe 平台 API 的唯一凭证。
BASE_URL
未设置时默认使用
https://pinme.dev

Worker 创建时自动注入以下环境变量,无需手动配置:
typescript
// backend/src/worker.ts
export interface Env {
  DB: D1Database;
  API_KEY: string;      // 项目 API Key — 用于 send_email 和 chat/completions 认证
  BASE_URL?: string;    // 可选覆盖 PinMe API 基础地址,默认 https://pinme.dev
}
API_KEY
是 Worker 调用 PinMe 平台 API 的唯一凭证。
BASE_URL
未设置时默认使用
https://pinme.dev

API 1:发送邮件

API 1:发送邮件

端点:
POST {BASE_URL}/api/v4/send_email
认证:
X-API-Key
header(使用
env.API_KEY
发件人: 自动为
{project_name}@pinme.dev
端点:
POST {BASE_URL}/api/v4/send_email
认证:
X-API-Key
header(使用
env.API_KEY
发件人: 自动为
{project_name}@pinme.dev

请求格式

请求格式

json
{
  "to": "user@example.com",
  "subject": "Your verification code",
  "html": "<p>Your code is <strong>123456</strong></p>"
}
字段类型必填说明
to
string收件人邮箱
subject
string邮件主题
html
stringHTML 正文
json
{
  "to": "user@example.com",
  "subject": "Your verification code",
  "html": "<p>Your code is <strong>123456</strong></p>"
}
字段类型必填说明
to
string收件人邮箱
subject
string邮件主题
html
stringHTML 正文

响应格式

响应格式

成功 (200):
json
{ "code": 200, "msg": "ok", "data": { "ok": true } }
错误:
HTTP 状态码含义data.error 示例
401API Key 缺失或无效
"X-API-Key header is required"
/
"Invalid API key"
400参数校验失败
"Invalid email address"
/
"Subject is required"
500邮件服务异常
"Failed to send email"
成功 (200):
json
{ "code": 200, "msg": "ok", "data": { "ok": true } }
错误:
HTTP 状态码含义data.error 示例
401API Key 缺失或无效
"X-API-Key header is required"
/
"Invalid API key"
400参数校验失败
"Invalid email address"
/
"Subject is required"
500邮件服务异常
"Failed to send email"

Worker 示例代码

Worker 示例代码

typescript
async function sendEmail(env: Env, to: string, subject: string, html: string): Promise<{ ok: boolean; error?: string }> {
  const baseUrl = env.BASE_URL ?? 'https://pinme.dev';
  const resp = await fetch(`${baseUrl}/api/v4/send_email`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': env.API_KEY,
    },
    body: JSON.stringify({ to, subject, html }),
  });

  const result = await resp.json() as { code: number; msg: string; data?: { ok?: boolean; error?: string } };

  if (resp.status !== 200 || result.code !== 200) {
    return { ok: false, error: result.data?.error || result.msg || 'Unknown error' };
  }
  return { ok: true };
}

// 在路由中使用
async function handleSendVerification(request: Request, env: Env): Promise<Response> {
  const { email } = await request.json() as { email: string };
  const code = Math.random().toString().slice(2, 8);

  const result = await sendEmail(env, email, 'Verification Code',
    `<p>Your code is <strong>${code}</strong></p>`);

  if (!result.ok) {
    return json({ error: result.error }, 500);
  }
  return json({ ok: true });
}

typescript
async function sendEmail(env: Env, to: string, subject: string, html: string): Promise<{ ok: boolean; error?: string }> {
  const baseUrl = env.BASE_URL ?? 'https://pinme.dev';
  const resp = await fetch(`${baseUrl}/api/v4/send_email`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': env.API_KEY,
    },
    body: JSON.stringify({ to, subject, html }),
  });

  const result = await resp.json() as { code: number; msg: string; data?: { ok?: boolean; error?: string } };

  if (resp.status !== 200 || result.code !== 200) {
    return { ok: false, error: result.data?.error || result.msg || 'Unknown error' };
  }
  return { ok: true };
}

// 在路由中使用
async function handleSendVerification(request: Request, env: Env): Promise<Response> {
  const { email } = await request.json() as { email: string };
  const code = Math.random().toString().slice(2, 8);

  const result = await sendEmail(env, email, 'Verification Code',
    `<p>Your code is <strong>${code}</strong></p>`);

  if (!result.ok) {
    return json({ error: result.error }, 500);
  }
  return json({ ok: true });
}

API 2:LLM Chat Completions

API 2:LLM Chat Completions

端点:
POST {BASE_URL}/api/v1/chat/completions?project_name={project_name}
认证:
X-API-Key
header(使用
env.API_KEY
请求体: OpenAI 兼容格式,原样透传给 LLM 服务 流式: 支持 SSE(
stream: true
端点:
POST {BASE_URL}/api/v1/chat/completions?project_name={project_name}
认证:
X-API-Key
header(使用
env.API_KEY
请求体: OpenAI 兼容格式,原样透传给 LLM 服务 流式: 支持 SSE(
stream: true

请求格式

请求格式

json
{
  "model": "openai/gpt-4o-mini",
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    { "role": "user", "content": "Hello!" }
  ],
  "stream": true
}
project_name
从 Worker 的子域名解析,见下方示例。模型列表参考 PinMe LLM 支持的模型(OpenAI 兼容格式)。
json
{
  "model": "openai/gpt-4o-mini",
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    { "role": "user", "content": "Hello!" }
  ],
  "stream": true
}
project_name
从 Worker 的子域名解析,见下方示例。模型列表参考 PinMe LLM 支持的模型(OpenAI 兼容格式)。

响应格式

响应格式

非流式成功 (200):
json
{
  "id": "chatcmpl-...",
  "choices": [{ "message": { "role": "assistant", "content": "Hello!" }, "finish_reason": "stop" }],
  "usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 }
}
流式成功 (200): SSE 格式
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" there"}}]}
data: [DONE]
错误:
HTTP 状态码含义data.error 示例
401API Key 缺失或无效
"X-API-Key header is required"
/
"Invalid API key or project name"
400project_name 缺失或 LLM 未配置
"project_name is required"
/
"LLM service not configured for this project"
413请求体超过 1MB
"Request body too large (max 1MB)"
502LLM 服务不可用
"LLM service unavailable"
非流式成功 (200):
json
{
  "id": "chatcmpl-...",
  "choices": [{ "message": { "role": "assistant", "content": "Hello!" }, "finish_reason": "stop" }],
  "usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 }
}
流式成功 (200): SSE 格式
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" there"}}]}
data: [DONE]
错误:
HTTP 状态码含义data.error 示例
401API Key 缺失或无效
"X-API-Key header is required"
/
"Invalid API key or project name"
400project_name 缺失或 LLM 未配置
"project_name is required"
/
"LLM service not configured for this project"
413请求体超过 1MB
"Request body too large (max 1MB)"
502LLM 服务不可用
"LLM service unavailable"

Worker 示例代码 — 非流式

Worker 示例代码 — 非流式

typescript
// 获取 project_name:从 Worker 的子域名解析
function getProjectName(request: Request): string {
  const host = new URL(request.url).hostname; // e.g. "my-app-1a2b.pinme.pro"
  return host.split('.')[0];
}

async function callLLM(
  env: Env,
  projectName: string,
  messages: Array<{ role: string; content: string }>,
  model = 'openai/gpt-4o-mini',
): Promise<{ content: string; error?: string }> {
  const baseUrl = env.BASE_URL ?? 'https://pinme.dev';
  const resp = await fetch(
    `${baseUrl}/api/v1/chat/completions?project_name=${projectName}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': env.API_KEY,
      },
      body: JSON.stringify({ model, messages }),
    },
  );

  if (!resp.ok) {
    const err = await resp.json() as { data?: { error?: string } };
    return { content: '', error: err.data?.error || `HTTP ${resp.status}` };
  }

  const data = await resp.json() as { choices: Array<{ message: { content: string } }> };
  return { content: data.choices[0]?.message?.content || '' };
}

// 在路由中使用
async function handleChat(request: Request, env: Env): Promise<Response> {
  const { question } = await request.json() as { question: string };
  const projectName = getProjectName(request);

  const result = await callLLM(env, projectName, [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: question },
  ]);

  if (result.error) {
    return json({ error: result.error }, 502);
  }
  return json({ answer: result.content });
}
typescript
// 获取 project_name:从 Worker 的子域名解析
function getProjectName(request: Request): string {
  const host = new URL(request.url).hostname; // e.g. "my-app-1a2b.pinme.pro"
  return host.split('.')[0];
}

async function callLLM(
  env: Env,
  projectName: string,
  messages: Array<{ role: string; content: string }>,
  model = 'openai/gpt-4o-mini',
): Promise<{ content: string; error?: string }> {
  const baseUrl = env.BASE_URL ?? 'https://pinme.dev';
  const resp = await fetch(
    `${baseUrl}/api/v1/chat/completions?project_name=${projectName}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': env.API_KEY,
      },
      body: JSON.stringify({ model, messages }),
    },
  );

  if (!resp.ok) {
    const err = await resp.json() as { data?: { error?: string } };
    return { content: '', error: err.data?.error || `HTTP ${resp.status}` };
  }

  const data = await resp.json() as { choices: Array<{ message: { content: string } }> };
  return { content: data.choices[0]?.message?.content || '' };
}

// 在路由中使用
async function handleChat(request: Request, env: Env): Promise<Response> {
  const { question } = await request.json() as { question: string };
  const projectName = getProjectName(request);

  const result = await callLLM(env, projectName, [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: question },
  ]);

  if (result.error) {
    return json({ error: result.error }, 502);
  }
  return json({ answer: result.content });
}

Worker 示例代码 — 流式(SSE 透传)

Worker 示例代码 — 流式(SSE 透传)

typescript
async function handleChatStream(request: Request, env: Env): Promise<Response> {
  const body = await request.text();
  const projectName = getProjectName(request);
  const baseUrl = env.BASE_URL ?? 'https://pinme.dev';

  // 确保请求中 stream=true
  let parsed = JSON.parse(body);
  parsed.stream = true;

  const resp = await fetch(
    `${baseUrl}/api/v1/chat/completions?project_name=${projectName}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': env.API_KEY,
      },
      body: JSON.stringify(parsed),
    },
  );

  if (!resp.ok) {
    const err = await resp.json() as { data?: { error?: string } };
    return json({ error: err.data?.error || `HTTP ${resp.status}` }, resp.status);
  }

  // 直接透传 SSE 流
  return new Response(resp.body, {
    status: 200,
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      ...CORS_HEADERS,
    },
  });
}
typescript
async function handleChatStream(request: Request, env: Env): Promise<Response> {
  const body = await request.text();
  const projectName = getProjectName(request);
  const baseUrl = env.BASE_URL ?? 'https://pinme.dev';

  // 确保请求中 stream=true
  let parsed = JSON.parse(body);
  parsed.stream = true;

  const resp = await fetch(
    `${baseUrl}/api/v1/chat/completions?project_name=${projectName}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': env.API_KEY,
      },
      body: JSON.stringify(parsed),
    },
  );

  if (!resp.ok) {
    const err = await resp.json() as { data?: { error?: string } };
    return json({ error: err.data?.error || `HTTP ${resp.status}` }, resp.status);
  }

  // 直接透传 SSE 流
  return new Response(resp.body, {
    status: 200,
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      ...CORS_HEADERS,
    },
  });
}

前端消费 SSE 流示例

前端消费 SSE 流示例

typescript
async function streamChat(question: string, onChunk: (text: string) => void): Promise<void> {
  const resp = await fetch(getApiUrl('/api/chat/stream'), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question }),
  });

  const reader = resp.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop()!; // 保留不完整的行

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const payload = line.slice(6);
      if (payload === '[DONE]') return;

      const chunk = JSON.parse(payload) as { choices: Array<{ delta: { content?: string } }> };
      const content = chunk.choices[0]?.delta?.content;
      if (content) onChunk(content);
    }
  }
}

typescript
async function streamChat(question: string, onChunk: (text: string) => void): Promise<void> {
  const resp = await fetch(getApiUrl('/api/chat/stream'), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question }),
  });

  const reader = resp.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop()!; // 保留不完整的行

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const payload = line.slice(6);
      if (payload === '[DONE]') return;

      const chunk = JSON.parse(payload) as { choices: Array<{ delta: { content?: string } }> };
      const content = chunk.choices[0]?.delta?.content;
      if (content) onChunk(content);
    }
  }
}

错误处理模式

错误处理模式

PinMe 平台 API 统一响应格式:
typescript
interface PinmeResponse<T = unknown> {
  code: number;   // 200=成功,其他=失败
  msg: string;    // "ok" | "error" | "invalid params"
  data?: T;       // 成功时为业务数据,失败时可能含 { error: string }
}
PinMe 平台 API 统一响应格式:
typescript
interface PinmeResponse<T = unknown> {
  code: number;   // 200=成功,其他=失败
  msg: string;    // "ok" | "error" | "invalid params"
  data?: T;       // 成功时为业务数据,失败时可能含 { error: string }
}

推荐的统一错误处理

推荐的统一错误处理

typescript
async function callPinmeAPI<T>(url: string, apiKey: string, body: unknown): Promise<{ data?: T; error?: string }> {
  let resp: Response;
  try {
    resp = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
      body: JSON.stringify(body),
    });
  } catch {
    return { error: 'Network error' };
  }

  if (!resp.ok) {
    try {
      const err = await resp.json() as PinmeResponse;
      return { error: err.data && typeof err.data === 'object' && 'error' in err.data
        ? (err.data as { error: string }).error
        : err.msg || `HTTP ${resp.status}` };
    } catch {
      return { error: `HTTP ${resp.status}` };
    }
  }

  const result = await resp.json() as PinmeResponse<T>;
  if (result.code !== 200) {
    return { error: result.data && typeof result.data === 'object' && 'error' in result.data
      ? (result.data as { error: string }).error
      : result.msg };
  }
  return { data: result.data as T };
}
typescript
async function callPinmeAPI<T>(url: string, apiKey: string, body: unknown): Promise<{ data?: T; error?: string }> {
  let resp: Response;
  try {
    resp = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
      body: JSON.stringify(body),
    });
  } catch {
    return { error: 'Network error' };
  }

  if (!resp.ok) {
    try {
      const err = await resp.json() as PinmeResponse;
      return { error: err.data && typeof err.data === 'object' && 'error' in err.data
        ? (err.data as { error: string }).error
        : err.msg || `HTTP ${resp.status}` };
    } catch {
      return { error: `HTTP ${resp.status}` };
    }
  }

  const result = await resp.json() as PinmeResponse<T>;
  if (result.code !== 200) {
    return { error: result.data && typeof result.data === 'object' && 'error' in result.data
      ? (result.data as { error: string }).error
      : result.msg };
  }
  return { data: result.data as T };
}

使用示例

使用示例

typescript
const baseUrl = env.BASE_URL ?? 'https://pinme.dev';

// 发邮件
const emailResult = await callPinmeAPI<{ ok: boolean }>(
  `${baseUrl}/api/v4/send_email`, env.API_KEY,
  { to: 'user@example.com', subject: 'Hello', html: '<p>Hi</p>' },
);
if (emailResult.error) return json({ error: emailResult.error }, 500);

// 调 LLM(非流式)
const llmResult = await callPinmeAPI<{ choices: Array<{ message: { content: string } }> }>(
  `${baseUrl}/api/v1/chat/completions?project_name=${projectName}`, env.API_KEY,
  { model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },
);
if (llmResult.error) return json({ error: llmResult.error }, 502);
typescript
const baseUrl = env.BASE_URL ?? 'https://pinme.dev';

// 发邮件
const emailResult = await callPinmeAPI<{ ok: boolean }>(
  `${baseUrl}/api/v4/send_email`, env.API_KEY,
  { to: 'user@example.com', subject: 'Hello', html: '<p>Hi</p>' },
);
if (emailResult.error) return json({ error: emailResult.error }, 500);

// 调 LLM(非流式)
const llmResult = await callPinmeAPI<{ choices: Array<{ message: { content: string } }> }>(
  `${baseUrl}/api/v1/chat/completions?project_name=${projectName}`, env.API_KEY,
  { model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },
);
if (llmResult.error) return json({ error: llmResult.error }, 502);