463 lines
18 KiB
Go
463 lines
18 KiB
Go
package docs
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
|
||
v1 "github.com/qianfree/team-api/api/docs/v1"
|
||
"github.com/qianfree/team-api/internal/logic/common"
|
||
"github.com/qianfree/team-api/internal/service"
|
||
)
|
||
|
||
func init() {
|
||
service.RegisterDocs(New())
|
||
}
|
||
|
||
type sDocs struct{}
|
||
|
||
func New() *sDocs {
|
||
return &sDocs{}
|
||
}
|
||
|
||
func (s *sDocs) OpenAPISpec(ctx context.Context, _ *v1.OpenAPISpecReq) (json.RawMessage, error) {
|
||
// Try cache first
|
||
cached, err := g.Redis().Do(ctx, "GET", "docs:openapi:spec")
|
||
if err == nil && !cached.IsNil() && !cached.IsEmpty() {
|
||
return json.RawMessage(cached.String()), nil
|
||
}
|
||
|
||
spec := buildOpenAPISpec(ctx)
|
||
raw, err := json.Marshal(spec)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Cache for 1 hour
|
||
_, _ = g.Redis().Do(ctx, "SET", "docs:openapi:spec", string(raw), "EX", 3600)
|
||
|
||
return raw, nil
|
||
}
|
||
|
||
func buildOpenAPISpec(ctx context.Context) map[string]any {
|
||
baseURL := common.Config().GetString(ctx, "api_base_url")
|
||
if baseURL == "" {
|
||
baseURL = "https://api.team-api.example.com"
|
||
}
|
||
return map[string]any{
|
||
"openapi": "3.0.3",
|
||
"info": map[string]any{
|
||
"title": "Team API",
|
||
"description": "多租户 大模型 API 网关,兼容 OpenAI API 格式",
|
||
"version": "1.0.0",
|
||
},
|
||
"servers": []map[string]any{
|
||
{"url": baseURL, "description": "Production"},
|
||
},
|
||
"paths": map[string]any{
|
||
"/v1/chat/completions": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Chat Completions",
|
||
"description": "创建聊天补全,兼容 OpenAI API 格式,支持流式和非流式响应。",
|
||
"operationId": "createChatCompletion",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{"$ref": "#/components/schemas/ChatCompletionRequest"},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{
|
||
"description": "Successful response",
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{"$ref": "#/components/schemas/ChatCompletionResponse"},
|
||
},
|
||
"text/event-stream": map[string]any{
|
||
"schema": map[string]any{"type": "string", "description": "SSE stream"},
|
||
},
|
||
},
|
||
},
|
||
"401": map[string]any{"description": "Authentication failed"},
|
||
"402": map[string]any{"description": "Insufficient quota"},
|
||
"429": map[string]any{"description": "Rate limit exceeded"},
|
||
"500": map[string]any{"description": "Internal error"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/completions": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Text Completions",
|
||
"description": "文本补全,兼容 OpenAI API 格式。",
|
||
"operationId": "createCompletion",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{"$ref": "#/components/schemas/CompletionRequest"},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Successful response"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/embeddings": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Embeddings",
|
||
"description": "创建文本向量嵌入,兼容 OpenAI API 格式。",
|
||
"operationId": "createEmbedding",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{"$ref": "#/components/schemas/EmbeddingRequest"},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Successful response"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/images/generations": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Image Generations",
|
||
"description": "AI 图像生成,兼容 OpenAI API 格式。",
|
||
"operationId": "createImage",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "prompt"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "dall-e-3"},
|
||
"prompt": map[string]any{"type": "string"},
|
||
"n": map[string]any{"type": "integer", "default": 1},
|
||
"size": map[string]any{"type": "string", "enum": []string{"256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"}},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Successful response"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/images/generations/async": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Image Generations Async (Submit)",
|
||
"description": "异步图像生成任务提交。阿里云百炼(DashScope)等异步图像上游无法经同步接口一次性返回,需提交任务拿到 task_id 后轮询 /v1/images/generations/async/{task_id} 取图。",
|
||
"operationId": "createImageAsync",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "prompt"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "wanx2.1-t2i-turbo"},
|
||
"prompt": map[string]any{"type": "string"},
|
||
"n": map[string]any{"type": "integer", "default": 1},
|
||
"size": map[string]any{"type": "string", "example": "1024x1024"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "任务已提交,返回 task_id 与初始状态"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/images/generations/async/{task_id}": map[string]any{
|
||
"get": map[string]any{
|
||
"summary": "Image Generations Async (Fetch)",
|
||
"description": "查询异步图像生成任务结果。轮询直到 status 为 SUCCESS(含图片 url)或 FAILURE(含 error),建议间隔 2~3 秒。",
|
||
"operationId": "getImageAsync",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"parameters": []map[string]any{
|
||
{
|
||
"name": "task_id",
|
||
"in": "path",
|
||
"required": true,
|
||
"schema": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "任务状态与结果"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/videos": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Create Video",
|
||
"description": "创建视频生成任务(OpenAI Videos 协议,兼容 openai-python / openai-node SDK 的 client.videos.create)。请求体支持 multipart/form-data(官方 SDK 形态,input_reference 可为文件 part)与 application/json 两种编码。返回官方 Video 对象(status: queued),轮询 GET /v1/videos/{video_id} 直到 completed,再经 GET /v1/videos/{video_id}/content 获取视频二进制。",
|
||
"operationId": "createVideo",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"multipart/form-data": map[string]any{
|
||
"schema": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "prompt"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "kling-v2-master"},
|
||
"prompt": map[string]any{"type": "string"},
|
||
"seconds": map[string]any{"type": "string", "enum": []string{"4", "8", "12"}, "default": "4"},
|
||
"size": map[string]any{"type": "string", "enum": []string{"720x1280", "1280x720", "1024x1792", "1792x1024"}, "default": "720x1280"},
|
||
"aspect_ratio": map[string]any{"type": "string", "description": "扩展字段:屏幕比例,如 16:9 / 9:16 / 1:1;取值原样透传上游,不传由模型取默认档"},
|
||
"sound": map[string]any{"type": "string", "enum": []string{"on", "off"}, "description": "扩展字段:是否包含声音;仅部分模型支持(可灵 / 豆包),其余模型忽略"},
|
||
"input_reference": map[string]any{"type": "string", "format": "binary", "description": "可选参考图(图生视频),文件上传或 {\"image_url\": \"...\"} 引用对象;file_id 形态不支持"},
|
||
},
|
||
},
|
||
},
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "prompt"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string"},
|
||
"prompt": map[string]any{"type": "string"},
|
||
"seconds": map[string]any{"type": "string"},
|
||
"size": map[string]any{"type": "string"},
|
||
"aspect_ratio": map[string]any{"type": "string", "description": "扩展字段:屏幕比例,如 16:9 / 9:16 / 1:1"},
|
||
"sound": map[string]any{"type": "string", "enum": []string{"on", "off"}, "description": "扩展字段:是否包含声音(也可用布尔形态 generate_audio)"},
|
||
"generate_audio": map[string]any{"type": "boolean", "description": "扩展字段:sound 的等价布尔形态,sound 缺省时生效"},
|
||
"input_reference": map[string]any{"type": "object", "properties": map[string]any{"image_url": map[string]any{"type": "string"}}},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Video 对象(object: video,status: queued)"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/videos/{video_id}": map[string]any{
|
||
"get": map[string]any{
|
||
"summary": "Retrieve Video",
|
||
"description": "查询视频生成任务,返回官方 Video 对象。status: queued / in_progress / completed / failed;非终态响应携带 openai-poll-after-ms 轮询间隔建议头。生成失败以 status=failed + error 对象表达(HTTP 仍为 200)。",
|
||
"operationId": "retrieveVideo",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"parameters": []map[string]any{
|
||
{
|
||
"name": "video_id",
|
||
"in": "path",
|
||
"required": true,
|
||
"schema": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Video 对象(含 progress 百分比、completed_at、失败时的 error)"},
|
||
"404": map[string]any{"description": "视频不存在或已被删除"},
|
||
},
|
||
},
|
||
"delete": map[string]any{
|
||
"summary": "Delete Video",
|
||
"description": "删除已完成或已失败的视频任务(软删除,计费与审计记录保留),删除后查询与下载均返回 404。进行中的任务不可删除。",
|
||
"operationId": "deleteVideo",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"parameters": []map[string]any{
|
||
{
|
||
"name": "video_id",
|
||
"in": "path",
|
||
"required": true,
|
||
"schema": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "删除确认(object: video.deleted,deleted: true)"},
|
||
"400": map[string]any{"description": "任务非终态或计费未结算,不可删除"},
|
||
"404": map[string]any{"description": "视频不存在或已被删除"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/videos/{video_id}/content": map[string]any{
|
||
"get": map[string]any{
|
||
"summary": "Download Video Content",
|
||
"description": "下载已生成完成的视频二进制流(mp4)。仅 status=completed 的任务可下载;variant 仅支持默认 video(thumbnail / spritesheet 暂不支持)。",
|
||
"operationId": "downloadVideoContent",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"parameters": []map[string]any{
|
||
{
|
||
"name": "video_id",
|
||
"in": "path",
|
||
"required": true,
|
||
"schema": map[string]any{"type": "string"},
|
||
},
|
||
{
|
||
"name": "variant",
|
||
"in": "query",
|
||
"required": false,
|
||
"schema": map[string]any{"type": "string", "enum": []string{"video"}, "default": "video"},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "视频二进制流(video/mp4)"},
|
||
"400": map[string]any{"description": "任务未完成或无可用内容"},
|
||
"404": map[string]any{"description": "视频不存在或已被删除"},
|
||
"502": map[string]any{"description": "上游内容拉取失败"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/messages": map[string]any{
|
||
"post": map[string]any{
|
||
"summary": "Claude Messages",
|
||
"description": "Claude Messages API,兼容 Anthropic API 格式。",
|
||
"operationId": "createClaudeMessage",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"requestBody": map[string]any{
|
||
"required": true,
|
||
"content": map[string]any{
|
||
"application/json": map[string]any{
|
||
"schema": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "messages"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "claude-sonnet-4-20250514"},
|
||
"messages": map[string]any{"type": "array"},
|
||
"max_tokens": map[string]any{"type": "integer", "default": 4096},
|
||
"stream": map[string]any{"type": "boolean", "default": false},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Successful response"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/models": map[string]any{
|
||
"get": map[string]any{
|
||
"summary": "List Models",
|
||
"description": "获取可用模型列表。",
|
||
"operationId": "listModels",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Successful response"},
|
||
},
|
||
},
|
||
},
|
||
"/v1/models/{model_id}": map[string]any{
|
||
"get": map[string]any{
|
||
"summary": "Get Model",
|
||
"description": "获取模型详情。",
|
||
"operationId": "getModel",
|
||
"security": []map[string]any{{"BearerAuth": []any{}}},
|
||
"parameters": []map[string]any{
|
||
{"name": "model_id", "in": "path", "required": true, "schema": map[string]any{"type": "string"}},
|
||
},
|
||
"responses": map[string]any{
|
||
"200": map[string]any{"description": "Successful response"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"components": map[string]any{
|
||
"securitySchemes": map[string]any{
|
||
"BearerAuth": map[string]any{
|
||
"type": "http",
|
||
"scheme": "bearer",
|
||
"bearerFormat": "API Key",
|
||
},
|
||
},
|
||
"schemas": map[string]any{
|
||
"ChatCompletionRequest": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "messages"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "gpt-4o", "description": "模型 ID"},
|
||
"messages": map[string]any{
|
||
"type": "array",
|
||
"items": map[string]any{"$ref": "#/components/schemas/ChatMessage"},
|
||
},
|
||
"temperature": map[string]any{"type": "number", "minimum": 0, "maximum": 2, "default": 1},
|
||
"top_p": map[string]any{"type": "number", "minimum": 0, "maximum": 1, "default": 1},
|
||
"max_tokens": map[string]any{"type": "integer", "minimum": 1},
|
||
"stream": map[string]any{"type": "boolean", "default": false},
|
||
"frequency_penalty": map[string]any{"type": "number", "minimum": -2, "maximum": 2},
|
||
"presence_penalty": map[string]any{"type": "number", "minimum": -2, "maximum": 2},
|
||
"stop": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||
},
|
||
},
|
||
"ChatMessage": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"role", "content"},
|
||
"properties": map[string]any{
|
||
"role": map[string]any{"type": "string", "enum": []string{"system", "user", "assistant"}},
|
||
"content": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
"ChatCompletionResponse": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"id": map[string]any{"type": "string"},
|
||
"object": map[string]any{"type": "string"},
|
||
"created": map[string]any{"type": "integer"},
|
||
"model": map[string]any{"type": "string"},
|
||
"choices": map[string]any{
|
||
"type": "array",
|
||
"items": map[string]any{"$ref": "#/components/schemas/ChatChoice"},
|
||
},
|
||
"usage": map[string]any{"$ref": "#/components/schemas/Usage"},
|
||
},
|
||
},
|
||
"ChatChoice": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"index": map[string]any{"type": "integer"},
|
||
"message": map[string]any{"$ref": "#/components/schemas/ChatMessage"},
|
||
"finish_reason": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
"Usage": map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"prompt_tokens": map[string]any{"type": "integer"},
|
||
"completion_tokens": map[string]any{"type": "integer"},
|
||
"total_tokens": map[string]any{"type": "integer"},
|
||
},
|
||
},
|
||
"CompletionRequest": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "prompt"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "gpt-4o"},
|
||
"prompt": map[string]any{"type": "string"},
|
||
"max_tokens": map[string]any{"type": "integer", "default": 16},
|
||
"stream": map[string]any{"type": "boolean", "default": false},
|
||
},
|
||
},
|
||
"EmbeddingRequest": map[string]any{
|
||
"type": "object",
|
||
"required": []string{"model", "input"},
|
||
"properties": map[string]any{
|
||
"model": map[string]any{"type": "string", "example": "text-embedding-3-small"},
|
||
"input": map[string]any{"oneOf": []any{
|
||
map[string]any{"type": "string"},
|
||
map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||
}},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|