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 开放平台的关键点:
- API Key 管理:生成、列表、删除,支持权限和过期
- 认证中间件:验证 Key、检查过期、更新使用时间
- 限流控制:使用 KV 记录请求,防止滥用
- 日志记录:记录每次调用,支持统计分析
- SDK 封装:提供类型安全的客户端
一句话带走:
API 开放平台的核心是「Key 认证 + 限流 + 日志」。API Key 控制权限和有效期,限流防止滥用,日志支持统计和审计。SDK 降低集成门槛,文档保证开发者体验。