24.12-API开放平台

要点

  • API 开放平台允许租户通过 API 集成 AI 能力到自己的产品
  • 使用 API Key 认证,支持多 Key 管理和权限控制
  • 需要严格的限流和配额控制
  • 提供 SDK 和文档,降低集成门槛

内容

1. API Key 管理

1.1 数据模型

-- API Key 表
CREATE TABLE api_keys (
  id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL,
  name TEXT NOT NULL,                    -- Key 名称
  key_hash TEXT NOT NULL,               -- 加密后的 key
  key_prefix TEXT NOT NULL,             -- Key 前缀(用于显示)
  permissions TEXT NOT NULL,            -- JSON: 权限列表
  rate_limit INTEGER DEFAULT 100,       -- 每分钟请求限制
  expires_at INTEGER,                   -- 过期时间
  last_used_at INTEGER,
  is_active INTEGER DEFAULT 1,
  created_at INTEGER NOT NULL,
  created_by TEXT NOT NULL,
  FOREIGN KEY (tenant_id) REFERENCES tenants(id),
  FOREIGN KEY (created_by) REFERENCES users(id)
)
 
-- API 调用日志
CREATE TABLE api_call_logs (
  id TEXT PRIMARY KEY,
  api_key_id TEXT NOT NULL,
  tenant_id TEXT NOT NULL,
  endpoint TEXT NOT NULL,
  method TEXT NOT NULL,
  status_code INTEGER NOT NULL,
  tokens_used INTEGER DEFAULT 0,
  latency_ms INTEGER,
  ip_address TEXT,
  user_agent TEXT,
  created_at INTEGER NOT NULL,
  FOREIGN KEY (api_key_id) REFERENCES api_keys(id)
)
 
-- 索引
CREATE INDEX idx_api_call_logs_key ON api_call_logs(api_key_id, created_at)
CREATE INDEX idx_api_call_logs_tenant ON api_call_logs(tenant_id, created_at)

TypeScript 类型:

// src/types/api-key.ts
export interface ApiKey {
  id: string
  tenantId: string
  name: string
  keyPrefix: string
  permissions: string[]
  rateLimit: number
  expiresAt?: number
  lastUsedAt?: number
  isActive: boolean
  createdAt: number
  createdBy: string
}
 
export interface ApiCallLog {
  id: string
  apiKeyId: string
  tenantId: string
  endpoint: string
  method: string
  statusCode: number
  tokensUsed: number
  latencyMs?: number
  ipAddress?: string
  userAgent?: string
  createdAt: number
}

1.2 创建 API Key

// src/routes/api-keys.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
import { requireRole } from '../middleware/permission'
 
const app = new Hono()
 
app.post('/', requireRole('admin'), zValidator('json', z.object({
  name: z.string().min(1).max(100),
  permissions: z.array(z.string()),
  rateLimit: z.number().min(1).max(10000).optional(),
  expiresAt: z.number().optional(),
})), async (c) => {
  const tenantId = c.get('tenantId')
  const userId = c.get('userId')
  const { name, permissions, rateLimit = 100, expiresAt } = c.req.valid('json')
 
  // 检查 API Key 数量限制
  const plan = c.get('plan')
  if (!plan.features.includes('api-access')) {
    return c.json({ error: '当前套餐不支持 API 访问' }, 403)
  }
 
  const keyCount = await c.env.DB.prepare(`
    SELECT COUNT(*) as count FROM api_keys WHERE tenant_id = ? AND is_active = 1
  `).bind(tenantId).first()
 
  if (keyCount.count >= 10) {
    return c.json({ error: 'API Key 数量已达上限(10个)' }, 403)
  }
 
  // 生成 API Key
  const apiKey = generateApiKey()
  const keyHash = await hashApiKey(apiKey)
  const keyPrefix = apiKey.substring(0, 8)
 
  const id = generateId()
  const now = Date.now()
 
  await c.env.DB.prepare(`
    INSERT INTO api_keys (id, tenant_id, name, key_hash, key_prefix, permissions, rate_limit, expires_at, created_at, created_by)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).bind(
    id,
    tenantId,
    name,
    keyHash,
    keyPrefix,
    JSON.stringify(permissions),
    rateLimit,
    expiresAt || null,
    now,
    userId
  ).run()
 
  // 返回完整的 key(仅此一次)
  return c.json({
    id,
    name,
    apiKey,  // 完整 key 只返回一次
    keyPrefix,
    permissions,
    rateLimit,
    expiresAt,
  }, 201)
})
 
function generateApiKey(): string {
  const prefix = 'sk'
  const random = crypto.randomUUID().replace(/-/g, '')
  return `${prefix}_${random}`
}
 
async function hashApiKey(key: string): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(key)
  const hash = await crypto.subtle.digest('SHA-256', data)
  return Array.from(new Uint8Array(hash))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

1.3 获取 API Key 列表

app.get('/', requireRole('admin'), async (c) => {
  const tenantId = c.get('tenantId')
 
  const keys = await c.env.DB.prepare(`
    SELECT id, name, key_prefix, permissions, rate_limit, expires_at, last_used_at, is_active, created_at
    FROM api_keys
    WHERE tenant_id = ?
    ORDER BY created_at DESC
  `).bind(tenantId).all()
 
  return c.json({
    apiKeys: keys.results.map(k => ({
      id: k.id,
      name: k.name,
      keyPrefix: `${k.key_prefix}...`,
      permissions: JSON.parse(k.permissions),
      rateLimit: k.rate_limit,
      expiresAt: k.expires_at,
      lastUsedAt: k.last_used_at,
      isActive: k.is_active === 1,
      createdAt: k.created_at,
    })),
  })
})

1.4 删除 API Key

app.delete('/:id', requireRole('admin'), async (c) => {
  const keyId = c.req.param('id')
  const tenantId = c.get('tenantId')
 
  await c.env.DB.prepare(`
    UPDATE api_keys SET is_active = 0 WHERE id = ? AND tenant_id = ?
  `).bind(keyId, tenantId).run()
 
  return c.json({ success: true })
})

2. API Key 认证中间件

// src/middleware/api-key-auth.ts
import { Context, Next } from 'hono'
 
export async function apiKeyAuthMiddleware(c: Context, next: Next) {
  const authHeader = c.req.header('Authorization')
 
  if (!authHeader?.startsWith('Bearer ')) {
    return c.json({ error: 'Missing API key' }, 401)
  }
 
  const apiKey = authHeader.slice(7)
  const keyHash = await hashApiKey(apiKey)
 
  // 查找 API Key
  const key = await c.env.DB.prepare(`
    SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1
  `).bind(keyHash).first()
 
  if (!key) {
    return c.json({ error: 'Invalid API key' }, 401)
  }
 
  // 检查过期
  if (key.expires_at && key.expires_at < Date.now()) {
    return c.json({ error: 'API key expired' }, 401)
  }
 
  // 检查租户状态
  const tenant = await c.env.DB.prepare(`
    SELECT status FROM tenants WHERE id = ?
  `).bind(key.tenant_id).first()
 
  if (!tenant || tenant.status !== 'active') {
    return c.json({ error: 'Tenant is not active' }, 403)
  }
 
  // 更新最后使用时间
  c.executionCtx.waitUntil(
    c.env.DB.prepare(`
      UPDATE api_keys SET last_used_at = ? WHERE id = ?
    `).bind(Date.now(), key.id).run()
  )
 
  // 设置上下文
  c.set('tenantId', key.tenant_id)
  c.set('apiKeyId', key.id)
  c.set('permissions', JSON.parse(key.permissions))
 
  await next()
}
 
async function hashApiKey(key: string): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(key)
  const hash = await crypto.subtle.digest('SHA-256', data)
  return Array.from(new Uint8Array(hash))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

3. API 限流

// src/middleware/api-rate-limit.ts
import { Context, Next } from 'hono'
 
export async function apiKeyRateLimitMiddleware(c: Context, next: Next) {
  const apiKeyId = c.get('apiKeyId')
 
  // 获取限流配置
  const key = await c.env.DB.prepare(`
    SELECT rate_limit FROM api_keys WHERE id = ?
  `).bind(apiKeyId).first()
 
  if (!key) {
    return c.json({ error: 'API key not found' }, 401)
  }
 
  const rateLimit = key.rate_limit
  const windowMs = 60 * 1000  // 1 分钟
 
  // 使用 KV 记录请求
  const now = Date.now()
  const windowKey = `rate_limit:${apiKeyId}:${Math.floor(now / windowMs)}`
 
  const count = await c.env.KV.get(windowKey)
  const currentCount = count ? parseInt(count) : 0
 
  if (currentCount >= rateLimit) {
    return c.json({
      error: 'Rate limit exceeded',
      limit: rateLimit,
      window: '1 minute',
      retryAfter: Math.ceil((Math.floor(now / windowMs) + 1) * windowMs - now) / 1000,
    }, 429)
  }
 
  // 更新计数
  await c.env.KV.put(windowKey, String(currentCount + 1), {
    expirationTtl: 120,  // 2 分钟后过期
  })
 
  // 设置限流头
  c.header('X-RateLimit-Limit', String(rateLimit))
  c.header('X-RateLimit-Remaining', String(rateLimit - currentCount - 1))
  c.header('X-RateLimit-Reset', String(Math.floor((Math.floor(now / windowMs) + 1) * windowMs / 1000)))
 
  await next()
}

4. 开放 API 路由

// src/routes/open-api/index.ts
import { Hono } from 'hono'
import { apiKeyAuthMiddleware } from '../../middleware/api-key-auth'
import { apiKeyRateLimitMiddleware } from '../../middleware/api-rate-limit'
import { checkPermission } from '../../lib/permission'
 
const app = new Hono()
 
// 应用中间件
app.use('*', apiKeyAuthMiddleware)
app.use('*', apiKeyRateLimitMiddleware)
 
// Chat API
app.post('/chat', async (c) => {
  const permissions = c.get('permissions')
  if (!checkPermission(permissions, 'chat:use')) {
    return c.json({ error: 'Permission denied' }, 403)
  }
 
  const tenantId = c.get('tenantId')
  const apiKeyId = c.get('apiKeyId')
  const { messages, model, temperature } = await c.req.json()
 
  const startTime = Date.now()
 
  try {
    // 调用 LLM
    const modelRouter = new ModelRouter(c.env)
    const { model: selectedModel, config } = await modelRouter.selectModel({
      tenantId,
      requestedModel: model,
    })
 
    const finalConfig = {
      ...config,
      temperature: temperature ?? config.temperature,
    }
 
    const response = await callLLM(messages, selectedModel, finalConfig, c.env)
 
    // 记录日志
    const latencyMs = Date.now() - startTime
    await logApiCall({
      apiKeyId,
      tenantId,
      endpoint: '/chat',
      method: 'POST',
      statusCode: 200,
      tokensUsed: response.usage.totalTokens,
      latencyMs,
      env: c.env,
    })
 
    // 扣减额度
    const quotaManager = new QuotaManager(c.env.DB, c.env)
    await quotaManager.consume(
      tenantId,
      'api',
      'tokens',
      response.usage.totalTokens,
      selectedModel.modelId
    )
 
    return c.json({
      id: crypto.randomUUID(),
      object: 'chat.completion',
      created: Math.floor(Date.now() / 1000),
      model: selectedModel.modelId,
      choices: [{
        index: 0,
        message: {
          role: 'assistant',
          content: response.content,
        },
        finish_reason: 'stop',
      }],
      usage: {
        prompt_tokens: response.usage.promptTokens,
        completion_tokens: response.usage.completionTokens,
        total_tokens: response.usage.totalTokens,
      },
    })
  } catch (error) {
    // 记录错误日志
    await logApiCall({
      apiKeyId,
      tenantId,
      endpoint: '/chat',
      method: 'POST',
      statusCode: 500,
      env: c.env,
    })
 
    return c.json({ error: error.message }, 500)
  }
})
 
// RAG API
app.post('/rag/search', async (c) => {
  const permissions = c.get('permissions')
  if (!checkPermission(permissions, 'rag:use')) {
    return c.json({ error: 'Permission denied' }, 403)
  }
 
  const tenantId = c.get('tenantId')
  const { knowledgeBaseId, query, topK } = await c.req.json()
 
  // 验证知识库属于该租户
  const kb = await c.env.DB.prepare(`
    SELECT * FROM knowledge_bases WHERE id = ? AND tenant_id = ?
  `).bind(knowledgeBaseId, tenantId).first()
 
  if (!kb) {
    return c.json({ error: 'Knowledge base not found' }, 404)
  }
 
  // 执行搜索
  const queryEmbedding = await generateEmbeddings([query], c.env)
  const results = await searchSimilarChunks(queryEmbedding[0], knowledgeBaseId, topK || 5, c.env)
 
  await logApiCall({
    apiKeyId: c.get('apiKeyId'),
    tenantId,
    endpoint: '/rag/search',
    method: 'POST',
    statusCode: 200,
    env: c.env,
  })
 
  return c.json({
    query,
    results: results.map(r => ({
      content: r.content,
      score: r.score,
      metadata: r.metadata,
    })),
  })
})
 
// 模型列表
app.get('/models', async (c) => {
  const tenantId = c.get('tenantId')
  const plan = await getTenantPlan(tenantId, c.env)
 
  const models = await c.env.DB.prepare(`
    SELECT model_id, name, capabilities, context_length
    FROM model_registry
    WHERE model_id IN (${plan.allowedModels.map(() => '?').join(',')}) AND is_available = 1
  `).bind(...plan.allowedModels).all()
 
  return c.json({
    models: models.results.map(m => ({
      id: m.model_id,
      name: m.name,
      capabilities: JSON.parse(m.capabilities),
      contextLength: m.context_length,
    })),
  })
})
 
// 额度查询
app.get('/quota', async (c) => {
  const tenantId = c.get('tenantId')
  const quotaManager = new QuotaManager(c.env.DB, c.env)
 
  const tokens = await quotaManager.getCurrentUsage(tenantId, 'tokens')
  const limit = await quotaManager.getQuotaLimit(tenantId, 'tokens')
 
  return c.json({
    tokens: {
      used: tokens,
      limit: limit,
      remaining: limit > 0 ? Math.max(0, limit - tokens) : -1,
    },
  })
})

5. API 调用日志

// src/lib/api-logger.ts
export async function logApiCall(params: {
  apiKeyId: string
  tenantId: string
  endpoint: string
  method: string
  statusCode: number
  tokensUsed?: number
  latencyMs?: number
  env: Env
}) {
  const { apiKeyId, tenantId, endpoint, method, statusCode, tokensUsed, latencyMs, env } = params
 
  await env.DB.prepare(`
    INSERT INTO api_call_logs (id, api_key_id, tenant_id, endpoint, method, status_code, tokens_used, latency_ms, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).bind(
    crypto.randomUUID(),
    apiKeyId,
    tenantId,
    endpoint,
    method,
    statusCode,
    tokensUsed || 0,
    latencyMs || null,
    Date.now()
  ).run()
}

6. API 统计

// src/routes/api-keys/:id/stats
app.get('/:id/stats', requireRole('admin'), async (c) => {
  const keyId = c.req.param('id')
  const tenantId = c.get('tenantId')
  const days = parseInt(c.req.query('days') || '30')
 
  const startTime = Date.now() - days * 24 * 60 * 60 * 1000
 
  // 总调用次数
  const totalCalls = await c.env.DB.prepare(`
    SELECT COUNT(*) as count, SUM(tokens_used) as totalTokens
    FROM api_call_logs
    WHERE api_key_id = ? AND created_at > ?
  `).bind(keyId, startTime).first()
 
  // 按端点分布
  const byEndpoint = await c.env.DB.prepare(`
    SELECT endpoint, COUNT(*) as count, SUM(tokens_used) as tokens
    FROM api_call_logs
    WHERE api_key_id = ? AND created_at > ?
    GROUP BY endpoint
    ORDER BY count DESC
  `).bind(keyId, startTime).all()
 
  // 按天趋势
  const trend = await c.env.DB.prepare(`
    SELECT DATE(created_at / 1000, 'unixepoch') as date, COUNT(*) as count
    FROM api_call_logs
    WHERE api_key_id = ? AND created_at > ?
    GROUP BY DATE(created_at / 1000, 'unixepoch')
    ORDER BY date ASC
  `).bind(keyId, startTime).all()
 
  return c.json({
    total: {
      calls: totalCalls.count,
      tokens: totalCalls.totalTokens || 0,
    },
    byEndpoint: byEndpoint.results,
    trend: trend.results,
  })
})

7. SDK 示例

提供 TypeScript SDK 示例:

// packages/sdk/src/index.ts
import { hc } from 'hono/client'
 
export class AIClient {
  private client
 
  constructor(apiKey: string, baseUrl: string = 'https://api.example.com') {
    this.client = hc<any>(baseUrl, {
      headers: {
        'Authorization': `Bearer ${apiKey}`,
      },
    })
  }
 
  async chat(messages: any[], options?: { model?: string; temperature?: number }) {
    const res = await this.client.open.chat.$post({
      json: {
        messages,
        model: options?.model,
        temperature: options?.temperature,
      },
    })
 
    return res.json()
  }
 
  async searchKnowledgeBase(knowledgeBaseId: string, query: string, topK?: number) {
    const res = await this.client.open.rag.search.$post({
      json: {
        knowledgeBaseId,
        query,
        topK,
      },
    })
 
    return res.json()
  }
 
  async getModels() {
    const res = await this.client.open.models.$get()
    return res.json()
  }
 
  async getQuota() {
    const res = await this.client.open.quota.$get()
    return res.json()
  }
}
 
// 使用示例
const client = new AIClient('sk_xxx', 'https://api.example.com')
 
const response = await client.chat([
  { role: 'user', content: '你好' },
], { model: 'gpt-4' })
 
console.log(response.choices[0].message.content)

8. 小结

API 开放平台的关键点:

  1. API Key 管理:生成、列表、删除,支持权限和过期
  2. 认证中间件:验证 Key、检查过期、更新使用时间
  3. 限流控制:使用 KV 记录请求,防止滥用
  4. 日志记录:记录每次调用,支持统计分析
  5. SDK 封装:提供类型安全的客户端

一句话带走:

API 开放平台的核心是「Key 认证 + 限流 + 日志」。API Key 控制权限和有效期,限流防止滥用,日志支持统计和审计。SDK 降低集成门槛,文档保证开发者体验。