feat: support openapi docs

This commit is contained in:
tt
2025-08-02 10:52:59 +08:00
parent ee2d8493ce
commit ede3bde13f
5 changed files with 2516 additions and 29 deletions

127
docs/README.md Normal file
View File

@@ -0,0 +1,127 @@
# WuKongIM API Documentation
This directory contains the API documentation for WuKongIM.
## Files
- `openapi.json` - OpenAPI 3.0 specification for the WuKongIM REST API
- `README.md` - This documentation file
## Accessing the API Documentation
### Swagger UI Interface
When the WuKongIM server is running, you can access the interactive API documentation at:
```
http://localhost:5001/docs
```
This provides a user-friendly Swagger UI interface where you can:
- Browse all API endpoints
- View detailed request/response schemas
- Test API endpoints directly from the browser
- Download the OpenAPI specification
### Direct OpenAPI Specification
You can also access the raw OpenAPI specification at:
```
http://localhost:5001/docs/openapi.json
```
## API Overview
The WuKongIM API provides the following functionality:
### System & Health
- Health checks and system status
- Migration status monitoring
### User Management
- User authentication and token management
- Online status tracking
- System user management
### Connection Management
- Connection removal and management
- Connection monitoring and statistics
### Channel Management
- Channel creation, update, and deletion
- Subscriber management
- Blacklist and whitelist management
### Message Management
- Message sending (single and batch)
- Message searching and retrieval
- Stream message handling
### Conversation Management
- Conversation synchronization
- Unread message management
### Monitoring & Metrics
- Connection statistics (`/connz`)
- System variables and metrics (`/varz`)
### Administrative
- Manager authentication
- System configuration
## Using the API
### Authentication
Most API endpoints require proper authentication. Refer to the specific endpoint documentation in the Swagger UI for authentication requirements.
### Base URL
The default base URL for the API is:
```
http://localhost:5001
```
### Content Type
Most endpoints expect and return JSON data:
```
Content-Type: application/json
```
### Error Handling
The API uses standard HTTP status codes and returns error information in JSON format:
```json
{
"error": "Error description"
}
```
## Development
### Updating the Documentation
The OpenAPI specification is automatically generated based on the API route definitions in the codebase. To update the documentation:
1. Modify the API endpoints in the `internal/api/` directory
2. Update the `docs/openapi.json` file accordingly
3. Restart the WuKongIM server to see the changes
### Local Development
For local development, ensure the `docs/openapi.json` file is present in the project root or docs directory. The documentation endpoint will automatically locate and serve the specification file.
## Support
For questions about the API or this documentation, please refer to:
- [WuKongIM GitHub Repository](https://github.com/WuKongIM/WuKongIM)
- [WuKongIM Documentation](https://githubim.com)
## Version
This documentation is for WuKongIM API version 2.0.0.

1970
docs/openapi.json Normal file

File diff suppressed because it is too large Load Diff

253
internal/api/docs.go Normal file
View File

@@ -0,0 +1,253 @@
package api
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"github.com/WuKongIM/WuKongIM/internal/options"
"github.com/WuKongIM/WuKongIM/pkg/wkhttp"
"github.com/WuKongIM/WuKongIM/pkg/wklog"
"go.uber.org/zap"
)
type docs struct {
s *Server
wklog.Log
}
func newDocs(s *Server) *docs {
return &docs{
s: s,
Log: wklog.NewWKLog("docs"),
}
}
// route 配置文档路由
func (d *docs) route(r *wkhttp.WKHttp) {
// 在生产模式下禁用文档端点以提高安全性和性能
if options.G.Mode == options.ReleaseMode {
d.Info("Documentation endpoints disabled in release mode for security")
r.GET("/docs", d.disabledInRelease) // 显示禁用消息
r.GET("/docs/", d.disabledInRelease) // 显示禁用消息
r.GET("/docs/openapi.json", d.disabledInRelease) // 显示禁用消息
r.GET("/docs/health", d.releaseHealthCheck) // 简化的健康检查
return
}
// 开发模式下启用完整的文档功能
d.Info("Documentation endpoints enabled in development mode")
r.GET("/docs", d.swaggerUI) // Swagger UI 主页面
r.GET("/docs/", d.redirectToDocs) // 重定向 /docs/ 到 /docs
r.GET("/docs/openapi.json", d.openAPI) // OpenAPI 规范文件
r.GET("/docs/health", d.docsHealth) // 文档服务健康检查
}
// redirectToDocs 重定向 /docs/ 到 /docs
func (d *docs) redirectToDocs(c *wkhttp.Context) {
c.Redirect(http.StatusMovedPermanently, "/docs")
}
// docsHealth 文档服务健康检查
func (d *docs) docsHealth(c *wkhttp.Context) {
c.JSON(http.StatusOK, map[string]interface{}{
"status": "ok",
"service": "docs",
"description": "WuKongIM API Documentation Service",
"endpoints": map[string]string{
"swagger_ui": "/docs",
"openapi_spec": "/docs/openapi.json",
"health_check": "/docs/health",
},
})
}
// disabledInRelease 在生产模式下显示禁用消息
func (d *docs) disabledInRelease(c *wkhttp.Context) {
c.JSON(http.StatusForbidden, map[string]interface{}{
"error": "Documentation endpoints are disabled in release mode",
"message": "API documentation is only available in development mode for security reasons",
"mode": string(options.G.Mode),
"suggestion": "To access documentation, run the server in debug mode or check the static documentation files",
})
}
// releaseHealthCheck 生产模式下的简化健康检查
func (d *docs) releaseHealthCheck(c *wkhttp.Context) {
c.JSON(http.StatusOK, map[string]interface{}{
"status": "disabled",
"service": "docs",
"mode": string(options.G.Mode),
"description": "Documentation service is disabled in release mode",
})
}
// swaggerUI 提供 Swagger UI 界面
func (d *docs) swaggerUI(c *wkhttp.Context) {
// 生成 Swagger UI HTML
html := d.generateSwaggerHTML()
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, html)
}
// openAPI 提供 OpenAPI 规范文件
func (d *docs) openAPI(c *wkhttp.Context) {
// 尝试多个可能的 OpenAPI 文件路径
possiblePaths := []string{
filepath.Join("docs", "openapi.json"), // 相对于工作目录
filepath.Join(options.G.DataDir, "..", "docs", "openapi.json"), // 相对于数据目录
"./docs/openapi.json", // 当前目录
}
var data []byte
var err error
var usedPath string
for _, path := range possiblePaths {
data, err = os.ReadFile(path)
if err == nil {
usedPath = path
break
}
}
if err != nil {
d.Error("Failed to read openapi.json from any location", zap.Error(err), zap.Strings("tried_paths", possiblePaths))
c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to load API specification. Please ensure docs/openapi.json exists.",
})
return
}
d.Debug("Successfully loaded OpenAPI spec", zap.String("path", usedPath))
// 验证 JSON 格式
var spec map[string]interface{}
if err := json.Unmarshal(data, &spec); err != nil {
d.Error("Invalid JSON in openapi.json", zap.Error(err), zap.String("path", usedPath))
c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Invalid API specification format",
})
return
}
c.Header("Content-Type", "application/json")
c.Header("Access-Control-Allow-Origin", "*") // Allow CORS for Swagger UI
c.Data(http.StatusOK, "application/json", data)
}
// generateSwaggerHTML 生成 Swagger UI HTML 页面
func (d *docs) generateSwaggerHTML() string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WuKongIM API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.15.5/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.15.5/favicon-16x16.png" sizes="16x16" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
.swagger-ui .topbar {
background-color: #1b1b1b;
padding: 10px 0;
}
.swagger-ui .topbar .download-url-wrapper {
display: none;
}
.swagger-ui .topbar .link {
content: "WuKongIM API Documentation";
}
.swagger-ui .info .title {
color: #3b4151;
}
.custom-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
.custom-header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.custom-header p {
margin: 10px 0 0 0;
font-size: 1.1em;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="custom-header">
<h1>🐒 WuKongIM API</h1>
<p>High-Performance Instant Messaging System - REST API Documentation</p>
</div>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js" charset="UTF-8"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: '/docs/openapi.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
validatorUrl: null,
docExpansion: "list",
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 1,
displayRequestDuration: true,
tryItOutEnabled: true,
filter: true,
showExtensions: true,
showCommonExtensions: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'],
onComplete: function() {
console.log('WuKongIM API Documentation loaded successfully');
// Add custom styling after load
const style = document.createElement('style');
style.textContent = '.swagger-ui .topbar-wrapper .link:after { content: "WuKongIM API v2.0"; }';
document.head.appendChild(style);
},
onFailure: function(data) {
console.error('Failed to load API specification:', data);
// Show user-friendly error message
document.getElementById('swagger-ui').innerHTML =
'<div style="padding: 40px; text-align: center; color: #721c24; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; margin: 20px;">' +
'<h3>⚠️ Failed to Load API Documentation</h3>' +
'<p>Could not load the OpenAPI specification. Please ensure the server is running properly.</p>' +
'<p><strong>Error:</strong> ' + (data.message || 'Unknown error') + '</p>' +
'</div>';
}
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>`
}

View File

@@ -141,6 +141,10 @@ func (s *apiServer) setRoutes() {
tag := newTag(s.s)
tag.route(s.r)
// docs - API documentation with Swagger UI
docs := newDocs(s.s)
docs.route(s.r)
// 分布式api
clusterServer, ok := service.Cluster.(*cluster.Server)
if ok {

View File

@@ -35,6 +35,7 @@ import (
"github.com/WuKongIM/WuKongIM/pkg/wknet"
"github.com/WuKongIM/WuKongIM/pkg/wkserver/proto"
"github.com/WuKongIM/WuKongIM/version"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
"github.com/judwhite/go-svc"
"go.uber.org/zap"
@@ -243,37 +244,15 @@ func (s *Server) Init(env svc.Environment) error {
}
func (s *Server) Start() error {
// 显示增强的启动横幅
s.printEnhancedBanner()
fmt.Println(`
__ __ ____ __. .___ _____
/ \ / \__ __| |/ _|____ ____ ____ | | / \
\ \/\/ / | \ < / _ \ / \ / ___\| |/ \ / \
\ /| | / | ( <_> ) | \/ /_/ > / Y \
\__/\ / |____/|____|__ \____/|___| /\___ /|___\____|__ /
\/ \/ \//_____/ \/
`)
s.Info("WuKongIM is Starting...")
s.Info(fmt.Sprintf(" Using config file: %s", s.opts.ConfigFileUsed()))
s.Info(fmt.Sprintf(" Mode: %s", s.opts.Mode))
s.Info(fmt.Sprintf(" Version: %s", version.Version))
s.Info(fmt.Sprintf(" Git: %s", fmt.Sprintf("%s-%s", version.CommitDate, version.Commit)))
s.Info(fmt.Sprintf(" Go build: %s", runtime.Version()))
s.Info(fmt.Sprintf(" DataDir: %s", s.opts.DataDir))
startTime := time.Now()
s.Info(fmt.Sprintf("Listening for TCP client on %s", s.opts.Addr))
s.Info(fmt.Sprintf("Listening for WS client on %s", s.opts.WSAddr))
if s.opts.WSSAddr != "" {
s.Info(fmt.Sprintf("Listening for WSS client on %s", s.opts.WSSAddr))
}
s.Info(fmt.Sprintf("Listening for Manager http api on %s", fmt.Sprintf("http://%s", s.opts.HTTPAddr)))
if s.opts.Manager.On {
s.Info(fmt.Sprintf("Listening for Manager on %s", s.opts.Manager.Addr))
}
defer s.Info("Server is ready")
defer func() {
duration := time.Since(startTime)
s.Info(fmt.Sprintf("🚀 Server is ready! (startup time: %v)", duration))
}()
var err error
@@ -432,6 +411,160 @@ func (s *Server) getSlotId(v string) uint32 {
return service.Cluster.GetSlotId(v)
}
// printEnhancedBanner 打印增强的启动横幅
func (s *Server) printEnhancedBanner() {
// 定义颜色
cyan := color.New(color.FgCyan, color.Bold)
yellow := color.New(color.FgYellow, color.Bold)
green := color.New(color.FgGreen, color.Bold)
blue := color.New(color.FgBlue, color.Bold)
magenta := color.New(color.FgMagenta, color.Bold)
white := color.New(color.FgWhite, color.Bold)
// 打印空行
fmt.Println()
// 打印 ASCII 艺术字
cyan.Println(" ╔══════════════════════════════════════════════════════════════════╗")
cyan.Println(" ║ ║")
cyan.Print(" ║ ")
yellow.Print("🐒 ")
magenta.Print("██╗ ██╗██╗ ██╗██╗ ██╗ ██████╗ ███╗ ██╗ ██████╗ ██╗███╗ ███╗")
cyan.Println(" ║")
cyan.Print(" ║ ")
magenta.Print("██║ ██║██║ ██║██║ ██╔╝██╔═══██╗████╗ ██║██╔════╝ ██║████╗ ████║")
cyan.Println(" ║")
cyan.Print(" ║ ")
magenta.Print("██║ █╗ ██║██║ ██║█████╔╝ ██║ ██║██╔██╗ ██║██║ ███╗██║██╔████╔██║")
cyan.Println(" ║")
cyan.Print(" ║ ")
magenta.Print("██║███╗██║██║ ██║██╔═██╗ ██║ ██║██║╚██╗██║██║ ██║██║██║╚██╔╝██║")
cyan.Println(" ║")
cyan.Print(" ║ ")
magenta.Print("╚███╔███╔╝╚██████╔╝██║ ██╗╚██████╔╝██║ ╚████║╚██████╔╝██║██║ ╚═╝ ██║")
cyan.Println(" ║")
cyan.Print(" ║ ")
magenta.Print("╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝╚═╝ ╚═╝")
cyan.Println(" ║")
cyan.Println(" ║ ║")
cyan.Print(" ║ ")
white.Print("High-Performance Instant Messaging System")
cyan.Println(" ║")
cyan.Println(" ║ ║")
cyan.Println(" ╚══════════════════════════════════════════════════════════════════╝")
fmt.Println()
// 系统信息
green.Print("🚀 Starting WuKongIM Server...")
fmt.Println()
fmt.Println()
// 配置信息
blue.Print("📋 Configuration:")
fmt.Println()
fmt.Printf(" ├─ Config File: %s\n", s.opts.ConfigFileUsed())
fmt.Printf(" ├─ Mode: %s\n", s.getModeWithIcon())
fmt.Printf(" ├─ Version: %s\n", version.Version)
fmt.Printf(" ├─ Git: %s-%s\n", version.CommitDate, version.Commit)
fmt.Printf(" ├─ Go Build: %s\n", runtime.Version())
fmt.Printf(" └─ Data Directory: %s\n", s.opts.DataDir)
fmt.Println()
// 网络监听信息
yellow.Print("🌐 Network Endpoints:")
fmt.Println()
fmt.Printf(" ├─ TCP Client: %s\n", s.opts.Addr)
fmt.Printf(" ├─ WebSocket: %s\n", s.opts.WSAddr)
if s.opts.WSSAddr != "" {
fmt.Printf(" ├─ WebSocket Secure: %s\n", s.opts.WSSAddr)
}
fmt.Printf(" ├─ HTTP API: http://%s\n", s.opts.HTTPAddr)
// 文档端点信息(根据模式显示)
if s.opts.Mode != options.ReleaseMode {
green.Printf(" ├─ 📚 API Documentation: http://%s/docs\n", s.opts.HTTPAddr)
}
if s.opts.Manager.On {
fmt.Printf(" ├─ Manager: %s\n", s.opts.Manager.Addr)
}
if s.opts.Demo.On {
fmt.Printf(" └─ 🎮 Demo: http://%s\n", s.opts.Demo.Addr)
} else {
fmt.Printf(" └─ Demo: disabled\n")
}
fmt.Println()
// 功能状态
magenta.Print("⚙️ Features:")
fmt.Println()
fmt.Printf(" ├─ Cluster Mode: %s\n", s.getClusterStatus())
fmt.Printf(" ├─ Conversation: %s\n", s.getBoolStatus(s.opts.Conversation.On))
fmt.Printf(" ├─ Token Auth: %s\n", s.getBoolStatus(s.opts.TokenAuthOn))
fmt.Printf(" ├─ Encryption: %s\n", s.getBoolStatus(!s.opts.DisableEncryption))
fmt.Printf(" └─ Documentation: %s\n", s.getDocsStatus())
fmt.Println()
// 启动提示
white.Print("💡 Quick Links:")
fmt.Println()
fmt.Printf(" ├─ Health Check: http://%s/health\n", s.opts.HTTPAddr)
if s.opts.Mode != options.ReleaseMode {
fmt.Printf(" ├─ API Docs: http://%s/docs\n", s.opts.HTTPAddr)
}
if s.opts.Demo.On {
fmt.Printf(" ├─ Chat Demo: http://%s\n", s.opts.Demo.Addr)
}
fmt.Printf(" └─ System Info: http://%s/varz\n", s.opts.HTTPAddr)
fmt.Println()
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Println()
}
// getModeWithIcon 获取带图标的模式显示
func (s *Server) getModeWithIcon() string {
switch s.opts.Mode {
case options.ReleaseMode:
return "🚀 release (production)"
case options.DebugMode:
return "🐛 debug (development)"
case options.BenchMode:
return "⚡ bench (performance testing)"
default:
return fmt.Sprintf("❓ %s", s.opts.Mode)
}
}
// getBoolStatus 获取布尔状态的显示
func (s *Server) getBoolStatus(enabled bool) string {
if enabled {
return "✅ enabled"
}
return "❌ disabled"
}
// getClusterStatus 获取集群状态
func (s *Server) getClusterStatus() string {
if len(s.opts.Cluster.InitNodes) > 0 {
return fmt.Sprintf("✅ enabled (%d nodes)", len(s.opts.Cluster.InitNodes))
}
return "❌ standalone mode"
}
// getDocsStatus 获取文档服务状态
func (s *Server) getDocsStatus() string {
if s.opts.Mode == options.ReleaseMode {
return "🔒 disabled (release mode)"
}
return "📚 enabled (development mode)"
}
func (s *Server) onConnect(conn wknet.Conn) error {
conn.SetMaxIdle(time.Second * 2) // 在认证之前连接最多空闲2秒
s.trace.Metrics.App().ConnCountAdd(1)