Compare commits

...

38 Commits

Author SHA1 Message Date
Dot.L a22759136e Merge pull request #298 from APIParkLab/feature/1.7-liujian
update docker build script
2025-04-24 16:03:14 +08:00
Liujian b8ebbac2b8 update docker build script 2025-04-24 16:02:32 +08:00
Dot.L 9c4590db07 Merge pull request #297 from APIParkLab/feature/1.7-liujian
Fix: Apikey getting md5 when calling MCP Server at service level
2025-04-22 18:08:51 +08:00
Liujian 7ba8a57793 Fix: Apikey getting md5 when calling MCP Server at service level 2025-04-22 18:08:24 +08:00
Dot.L 2dc16f4bb8 Merge pull request #295 from APIParkLab/feature/1.7-liujian
Fix: Issue where API portal's service list fails to retrieve when Influxdb is not initialized
2025-04-17 16:20:42 +08:00
Liujian b68496a82a Fix: Issue where API portal's service list fails to retrieve when Influxdb is not initialized 2025-04-17 16:20:17 +08:00
ningyv 6892fa34d8 Merge pull request #294 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP-analytics-table-optimize
2025-04-16 15:47:45 +08:00
Liujian c7c3e8033b Merge remote-tracking branch 'github-pro/main' into feature/1.7-liujian 2025-04-16 15:38:08 +08:00
Dot.L cc524810e8 Merge pull request #293 from APIParkLab/feature/1.7-liujian
Fix: Dragging to modify the order of service categories will fail whe…
2025-04-16 15:22:26 +08:00
Liujian b6f593c8d0 Fix: Dragging to modify the order of service categories will fail when there are subcategories in the service category. 2025-04-16 15:20:57 +08:00
ningyv ca54fc581c Merge pull request #292 from APIParkLab/feature/1.7-cxx
Feature/1.7
2025-04-16 14:42:33 +08:00
Dot.L e7372cb7f2 Merge pull request #291 from APIParkLab/feature/1.7-liujian
Feature/1.7 liujian
2025-04-16 14:35:50 +08:00
Liujian feac0428ef Fix: Issue of API duplicate publishing when publishing services 2025-04-16 14:34:18 +08:00
Liujian c8e5b7541d update 2025-04-16 14:12:20 +08:00
Liujian 8365f77c67 update api portal interface 2025-04-16 11:10:45 +08:00
Liujian 96f675ae9c update path 2025-04-16 00:00:58 +08:00
Liujian ae3c189089 MCP Server supports multiple languages. 2025-04-15 22:42:22 +08:00
ningyv 37d421ad8a Merge pull request #290 from APIParkLab/feature/1.7-cxx
Feature/1.7 cxx
2025-04-14 17:46:20 +08:00
ningyv 6c637bf78c Merge pull request #289 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP
2025-04-14 14:10:37 +08:00
ningyv 43690156e3 Merge pull request #288 from APIParkLab/feature/1.7-cxx
Feature/1.7 cxx
2025-04-14 13:38:08 +08:00
Liujian b703ddaae8 Fix: Issue with invalid apikey parameter in service MCP path 2025-04-14 11:57:06 +08:00
ningyv 061ea05935 Merge pull request #287 from APIParkLab/feature/1.7-cxx
Feature/1.7 cxx
2025-04-14 11:34:19 +08:00
Liujian fb31ecc012 Merge remote-tracking branch 'github-pro/main' into feature/1.7-liujian 2025-04-14 11:30:13 +08:00
Liujian 741bdd682c Fix: API forwarding header setting failure issue 2025-04-14 10:19:49 +08:00
ningyv 70a8da7682 Merge pull request #286 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP
2025-04-14 09:54:55 +08:00
ningyv 18615bad33 Merge pull request #285 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP
2025-04-11 18:42:29 +08:00
ningyv 8f82436421 Merge pull request #284 from APIParkLab/feature/1.7-cxx
Feature/1.7 cxx
2025-04-11 18:31:52 +08:00
Liujian 4beb497032 Add the openapiaddress field to the API portal to obtain detailed service information 2025-04-11 14:42:26 +08:00
Dot.L b6115faf7c Merge pull request #283 from APIParkLab/feature/1.7-liujian
Feature/1.7 liujian
2025-04-11 11:53:40 +08:00
Liujian 4fdb677103 service detail add invoke_count 2025-04-11 11:52:27 +08:00
Liujian 34cf8e2b26 update global mcp tool's description 2025-04-11 11:03:20 +08:00
ningyv 93f686af14 Merge pull request #282 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP
2025-04-11 10:06:43 +08:00
Liujian 48ab6d7c7e Merge remote-tracking branch 'github-pro/main' into feature/1.7-liujian 2025-04-10 18:26:12 +08:00
ningyv 08cecd8a0e Merge pull request #281 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP
2025-04-10 17:47:52 +08:00
ningyv 79f29254ca Merge pull request #280 from APIParkLab/feature/1.7-cxx
feature/1.7-MCP
2025-04-10 16:29:34 +08:00
Liujian cc34ca510c Merge remote-tracking branch 'github-pro/main' into feature/1.7-liujian 2025-04-10 16:23:40 +08:00
ningyv a285c54be2 Merge pull request #279 from APIParkLab/feature/1.7-cxx
Feature/1.7 cxx
2025-04-10 16:23:15 +08:00
ningyv 4397784749 Merge pull request #278 from APIParkLab/feature/1.7-cxx
Feature/1.7 cx
2025-04-10 10:48:30 +08:00
39 changed files with 849 additions and 116 deletions
+2 -1
View File
@@ -7,4 +7,5 @@
/.vscode/
.air.toml
/tmp/
/work
/work
/cmd/
+13 -1
View File
@@ -3,6 +3,7 @@ package catalogue
import (
"github.com/APIParkLab/APIPark/module/catalogue"
catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto"
"github.com/APIParkLab/APIPark/module/service"
"github.com/APIParkLab/APIPark/module/tag"
tag_dto "github.com/APIParkLab/APIPark/module/tag/dto"
"github.com/gin-gonic/gin"
@@ -14,6 +15,7 @@ var (
type imlCatalogueController struct {
catalogueModule catalogue.ICatalogueModule `autowired:""`
appModule service.IAppModule `autowired:""`
tagModule tag.ITagModule `autowired:""`
}
@@ -26,7 +28,17 @@ func (i *imlCatalogueController) Subscribe(ctx *gin.Context, subscribeInfo *cata
}
func (i *imlCatalogueController) ServiceDetail(ctx *gin.Context, sid string) (*catalogue_dto.ServiceDetail, error) {
return i.catalogueModule.ServiceDetail(ctx, sid)
detail, err := i.catalogueModule.ServiceDetail(ctx, sid)
if err != nil {
return nil, err
}
_, canSubscribe, err := i.appModule.SearchCanSubscribe(ctx, sid)
if err != nil {
return nil, err
}
detail.CanSubscribe = canSubscribe
return detail, nil
}
func (i *imlCatalogueController) Search(ctx *gin.Context, keyword string) ([]*catalogue_dto.Item, []*tag_dto.Item, error) {
+156 -20
View File
@@ -6,6 +6,8 @@ import (
"strings"
"sync"
application_authorization "github.com/APIParkLab/APIPark/module/application-authorization"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
"github.com/APIParkLab/APIPark/module/mcp"
"github.com/APIParkLab/APIPark/module/system"
@@ -18,11 +20,12 @@ import (
var _ IMcpController = (*imlMcpController)(nil)
type imlMcpController struct {
settingModule system.ISettingModule `autowired:""`
mcpModule mcp.IMcpModule `autowired:""`
sessionKeys sync.Map
server http.Handler
openServer http.Handler
settingModule system.ISettingModule `autowired:""`
authorizationModule application_authorization.IAuthorizationModule `autowired:""`
mcpModule mcp.IMcpModule `autowired:""`
sessionKeys sync.Map
server map[string]http.Handler
openServer http.Handler
}
var mcpDefaultConfig = `{
@@ -42,28 +45,96 @@ func (i *imlMcpController) GlobalMCPConfig(ctx *gin.Context) (string, error) {
return fmt.Sprintf(mcpDefaultConfig, "APIPark-MCP-Server", fmt.Sprintf("%s/openapi/v1/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), mcp_server.GlobalBasePath)), nil
}
func (i *imlMcpController) OnComplete() {
func (i *imlMcpController) generateZhCNMCPServer() *server.MCPServer {
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
s.AddTool(
mcp2.NewTool(
"apipark_service_list",
mcp2.WithDescription("This tool is a standardized interface provided by the Apipark platform under the MCP (Model Context Protocol) framework, designed to retrieve metadata for all registered services in bulk. By invoking this tool, users can efficiently explore the complete list of published services and their core attributes, serving as a prerequisite for subsequent actions such as querying detailed API lists via service IDs, requesting access permissions, or integrating services."),
"service_list",
mcp2.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"),
mcp2.WithString("keyword", mcp2.Description("关键词,用于模糊搜索服务")),
),
i.mcpModule.Services,
)
s.AddTool(
mcp2.NewTool(
"openapi_document",
mcp2.WithDescription("此工具用于获取指定服务的 OpenAPI 接口文档。返回内容支持 OpenAPI v3 与 v2 两种规范格式。通过传入服务 ID,可以查看该服务的所有 API 定义、参数结构、请求方式等详细信息,为后续构造请求做准备。"),
mcp2.WithString("service", mcp2.Description("服务的唯一标识 ID")),
),
i.mcpModule.APIs,
)
s.AddTool(
mcp2.NewTool(
"invoke_api",
mcp2.WithDescription("此工具用于直接调用指定的 API 接口。调用前需根据该接口的 OpenAPI 文档构造必要的请求参数,如请求路径、方法、查询参数、请求头、请求体等。调用过程中无需传递认证信息,例如请求头中的 Authorization 字段不需要提供。"),
mcp2.WithString("path", mcp2.Description("API 请求路径"), mcp2.Required()),
mcp2.WithString("method", mcp2.Description("API 请求方法,例如 GET、POST、PUT"), mcp2.Required()),
mcp2.WithString("content-type", mcp2.Description("请求的 Content-Type 类型。如果方法为 POST、PUT 或 PATCH,则必须指定该字段。")),
mcp2.WithObject("query", mcp2.Description("请求的查询参数,类型为 map[string]string")),
mcp2.WithObject("header", mcp2.Description("请求的头部参数,类型为 map[string]string")),
mcp2.WithString("body", mcp2.Description("请求体内容,通常为 JSON 字符串")),
),
i.mcpModule.Invoke,
)
return s
}
func (i *imlMcpController) generateZhTWMCPServer() *server.MCPServer {
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
s.AddTool(
mcp2.NewTool(
"service_list",
mcp2.WithDescription("此工具用於獲取 APIPark 中已註冊服務的清單。每個服務包含其唯一識別碼(service ID)、名稱、描述以及該服務所包含的 API 列表。支援關鍵字模糊搜尋,可快速縮小查詢範圍。獲取到服務 ID 後,可使用 openapi_document 工具來查詢該服務對應的 OpenAPI 文件,為後續 API 呼叫做準備。"),
mcp2.WithString("keyword", mcp2.Description("關鍵字,用於模糊搜尋服務")),
),
i.mcpModule.Services,
)
s.AddTool(
mcp2.NewTool(
"openapi_document",
mcp2.WithDescription("此工具用於查詢指定服務的 OpenAPI 文件。返回的格式支援 OpenAPI v3 與 v2 標準。透過輸入服務 ID,可查閱該服務所有 API 的定義、參數結構、請求方式等細節,有助於後續構造 API 呼叫請求。"),
mcp2.WithString("service", mcp2.Description("欲查詢的服務唯一識別碼")),
),
i.mcpModule.APIs,
)
s.AddTool(
mcp2.NewTool(
"invoke_api",
mcp2.WithDescription("此工具可直接發送 API 請求。在呼叫此工具之前,需根據該 API 的 OpenAPI 文件構造所需的請求參數,如請求路徑、方法、查詢參數、標頭、主體等。使用此工具時不需傳送任何認證資訊,例如 Authorization 標頭可省略。"),
mcp2.WithString("path", mcp2.Description("API 的請求路徑"), mcp2.Required()),
mcp2.WithString("method", mcp2.Description("API 的請求方法,例如 GET、POST、PUT"), mcp2.Required()),
mcp2.WithString("content-type", mcp2.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")),
mcp2.WithObject("query", mcp2.Description("請求的查詢參數,類型為 map[string]string")),
mcp2.WithObject("header", mcp2.Description("請求的標頭,類型為 map[string]string")),
mcp2.WithString("body", mcp2.Description("請求主體內容,通常為 JSON 字串")),
),
i.mcpModule.Invoke,
)
return s
}
func (i *imlMcpController) generateEnMCPServer() *server.MCPServer {
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
s.AddTool(
mcp2.NewTool(
"service_list",
mcp2.WithDescription("This tool can retrieve a list of registered services on APIPark, including key information such as service ID, name, description, and API list within the service. Support keyword search to quickly narrow down the search scope. After obtaining the service ID, you can use this ID to call the tool openapi_document to obtain the openapi document of the service for the corresponding service, preparing for subsequent API calls."),
mcp2.WithString("keyword", mcp2.Description("Keyword for fuzzy search")),
),
i.mcpModule.Services,
)
s.AddTool(
mcp2.NewTool(
"apipark_service_api_list",
mcp2.WithDescription("This tool is a standardized MCP (Model Context Protocol) interface provided by the Apipark platform, designed to retrieve OpenAPI specification documents for all APIs under a specified service using its service ID. By invoking this tool, users gain precise access to detailed API definitions (including endpoints, parameters, request/response schemas) for debugging, integration, or client SDK generation."),
"openapi_document",
mcp2.WithDescription("This tool returns the openAPI documentation for the corresponding service. The format supports the specifications of OpenAPI v3 and OpenAPI v2."),
mcp2.WithString("service", mcp2.Description("Service ID")),
),
i.mcpModule.APIs,
)
s.AddTool(
mcp2.NewTool(
"apipark_invoke_api",
mcp2.WithDescription("This tool is a core MCP (Model Context Protocol) interface provided by the Apipark platform, enabling users to programmatically invoke APIs using metadata from apipark_service_api_list (API schemas). It acts as a unified gateway for executing API requests with built-in authentication, parameter validation, and error handling, returning structured responses for integration workflows."),
"invoke_api",
mcp2.WithDescription("This tool can directly make API calls. Before calling this tool, it is necessary to construct relevant parameters based on the corresponding API's openAPI documentation, including query, header, body, method, path, and other parameters. By using this tool, no authentication related information needs to be transmitted, that is, no request header Authorization needs to be transmitted."),
mcp2.WithString("path", mcp2.Description("API path"), mcp2.Required()),
mcp2.WithString("method", mcp2.Description("API method"), mcp2.Required()),
mcp2.WithString("content-type", mcp2.Description("API Request Content-Type. If method is POST,PUT,PATCH, it must be set. If not set, it will be ignored.")),
@@ -73,23 +144,72 @@ func (i *imlMcpController) OnComplete() {
),
i.mcpModule.Invoke,
)
i.server = server.NewSSEServer(s, server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.openServer = server.NewSSEServer(s, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
return s
}
func (i *imlMcpController) generateJPMCPServer() *server.MCPServer {
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
s.AddTool(
mcp2.NewTool(
"service_list",
mcp2.WithDescription("このツールは、APIPark に登録されているサービスの一覧を取得するためのものです。各サービスには、サービスID、名称、説明、およびそのサービスに含まれるAPI一覧といった重要な情報が含まれます。キーワードによるあいまい検索が可能で、目的のサービスを素早く絞り込むことができます。取得したサービスIDを使用して openapi_document ツールを呼び出すことで、そのサービスの OpenAPI ドキュメントを取得でき、APIの利用準備が整います。"),
mcp2.WithString("keyword", mcp2.Description("キーワード。サービスをあいまい検索するための文字列")),
),
i.mcpModule.Services,
)
s.AddTool(
mcp2.NewTool(
"openapi_document",
mcp2.WithDescription("指定されたサービスの OpenAPI ドキュメントを取得するためのツールです。OpenAPI v3 および v2 のフォーマットに対応しています。このドキュメントを使用することで、APIのエンドポイント、リクエスト方法、パラメータなどの詳細を確認でき、API呼び出しの準備に役立ちます。"),
mcp2.WithString("service", mcp2.Description("対象のサービスID")),
),
i.mcpModule.APIs,
)
s.AddTool(
mcp2.NewTool(
"invoke_api",
mcp2.WithDescription("このツールは、指定された API を直接呼び出すためのものです。呼び出し前に、OpenAPI ドキュメントに基づいて必要なパラメータ(パス、メソッド、クエリ、ヘッダー、ボディなど)を構築する必要があります。呼び出し時に認証情報(例:Authorization ヘッダー)を送信する必要はありません。"),
mcp2.WithString("path", mcp2.Description("API のリクエストパス"), mcp2.Required()),
mcp2.WithString("method", mcp2.Description("HTTPメソッド(GET、POST、PUTなど)。"), mcp2.Required()),
mcp2.WithString("content-type", mcp2.Description("リクエストの Content-Type。メソッドが POST、PUT、PATCH の場合に必須。")),
mcp2.WithObject("query", mcp2.Description("リクエストのクエリパラメータ。型は map[string]string")),
mcp2.WithObject("header", mcp2.Description("リクエストヘッダー。型は map[string]string")),
mcp2.WithString("body", mcp2.Description("リクエストボディ。通常はJSON文字列")),
),
i.mcpModule.Invoke,
)
return s
}
func (i *imlMcpController) OnComplete() {
i.server = make(map[string]http.Handler)
enSer := i.generateEnMCPServer()
i.server["en-US"] = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.server["zh-CN"] = server.NewSSEServer(i.generateZhCNMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.server["zh-TW"] = server.NewSSEServer(i.generateZhTWMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.server["ja-JP"] = server.NewSSEServer(i.generateJPMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.openServer = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
}
func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
cfg := i.settingModule.Get(ctx)
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
i.server.ServeHTTP(ctx.Writer, req)
locale := utils.I18n(ctx)
if v, ok := i.server[locale]; ok {
v.ServeHTTP(ctx.Writer, req)
return
}
i.server["en-US"].ServeHTTP(ctx.Writer, req)
}
func (i *imlMcpController) GlobalHandleSSE(ctx *gin.Context) {
i.handleSSE(ctx, i.openServer)
apikey := ctx.Request.URL.Query().Get("apikey")
i.handleSSE(ctx, i.openServer, apikey)
}
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler) {
apikey := ctx.Request.URL.Query().Get("apikey")
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apikey string) {
writer := &ResponseWriter{
Writer: ctx.Writer,
sessionId: make(chan string),
@@ -120,7 +240,23 @@ func (i *imlMcpController) MCPHandle(ctx *gin.Context) {
}
func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
i.handleSSE(ctx, mcp_server.DefaultMCPServer())
apikey := ctx.Request.URL.Query().Get("apikey")
serviceId := ctx.Param("serviceId")
if serviceId == "" {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"})
return
}
ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey)
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
return
}
if !ok {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
return
}
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), apikey)
}
func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) {
+3 -2
View File
@@ -542,8 +542,9 @@ type imlAppController struct {
authModule application_authorization.IAuthorizationModule `autowired:""`
}
func (i *imlAppController) SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SimpleAppItem, error) {
return i.module.SearchCanSubscribe(ctx, serviceId)
func (i *imlAppController) SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SubscribeAppItem, error) {
items, _, err := i.module.SearchCanSubscribe(ctx, serviceId)
return items, err
}
func (i *imlAppController) Search(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) {
+1 -1
View File
@@ -44,7 +44,7 @@ type IAppController interface {
// SimpleApps 获取简易项目列表
SimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
MySimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
SearchCanSubscribe(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SubscribeAppItem, error)
GetApp(ctx *gin.Context, appId string) (*service_dto.App, error)
DeleteApp(ctx *gin.Context, appId string) error
}
+1
View File
@@ -16,6 +16,7 @@ type IAPIKeyController interface {
Search(ctx *gin.Context, keyword string) ([]*system_apikey_dto.Item, error)
SimpleList(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error)
MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error)
MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error)
}
func init() {
+4
View File
@@ -12,6 +12,10 @@ type imlAPIKeyController struct {
apikeyModule system_apikey.IAPIKeyModule `autowired:""`
}
func (i *imlAPIKeyController) MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) {
return i.apikeyModule.MyAPIKeysByService(ctx, serviceId)
}
func (i *imlAPIKeyController) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) {
return i.apikeyModule.MyAPIKeys(ctx)
}
+20 -3
View File
@@ -64,8 +64,25 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
body := ""
for k, v := range request.Params.Arguments {
if k == "Body" {
tmp, _ := json.Marshal(v)
body = string(tmp)
switch a := v.(type) {
case string:
body = a
case map[string]interface{}:
switch t.contentType {
case "application/json":
tmp, _ := json.Marshal(a)
body = string(tmp)
case "application/x-www-form-urlencoded":
bodyValue := url.Values{}
for kk, vv := range a {
bodyValue.Set(kk, fmt.Sprintf("%v", vv))
}
body = bodyValue.Encode()
}
default:
tmp, _ := json.Marshal(a)
body = string(tmp)
}
continue
}
tmp, ok := v.(map[string]interface{})
@@ -107,7 +124,7 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
}
apikey := utils.Label(ctx, "apikey")
if apikey != "" {
req.Header.Set("Authorization", utils.Md5(apikey))
req.Header.Set("Authorization", apikey)
}
resp, err := client.Do(req)
+2 -1
View File
@@ -718,7 +718,8 @@ func (i *imlProviderModule) getAiProviders(ctx context.Context) ([]*gateway.Dyna
}
model, has := driver.GetModel(l.DefaultLLM)
if !has {
return nil, fmt.Errorf("model not found: %s", l.DefaultLLM)
continue
//return nil, fmt.Errorf("model not found: %s", l.DefaultLLM)
}
cfg := make(map[string]interface{})
cfg["provider"] = l.Id
@@ -3,7 +3,7 @@ package auth_driver
import (
"encoding/json"
"fmt"
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
)
@@ -83,6 +83,6 @@ func generateStruct[T any](cfg interface{}) (*T, error) {
return nil, err
}
}
return result, nil
}
@@ -3,9 +3,9 @@ package oauth2
import (
"encoding/json"
"strconv"
auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver"
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
)
@@ -33,7 +33,7 @@ func (cfg *Config) ID() string {
}
func (cfg *Config) Valid() ([]byte, error) {
if cfg.HashSecret && !cfg.Hashed {
// 未加密
secret, err := hashSecret([]byte(cfg.ClientSecret), 0, 0, 0)
@@ -48,9 +48,9 @@ func (cfg *Config) Valid() ([]byte, error) {
}
func (cfg *Config) Detail() []application_authorization_dto.DetailItem {
redirectURLs, _ := json.Marshal(cfg.RedirectUrls)
return []application_authorization_dto.DetailItem{
{Key: "客户端ID", Value: cfg.ClientId},
{Key: "客户端密钥", Value: cfg.ClientSecret},
@@ -2,9 +2,10 @@ package application_authorization
import (
"context"
"github.com/APIParkLab/APIPark/module/system"
"reflect"
"github.com/APIParkLab/APIPark/module/system"
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
"github.com/APIParkLab/APIPark/gateway"
@@ -31,6 +32,9 @@ type IAuthorizationModule interface {
Detail(ctx context.Context, appId string, aid string) ([]application_authorization_dto.DetailItem, error)
// Info 获取项目鉴权详情
Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error)
CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error)
//ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error)
}
+74 -1
View File
@@ -7,6 +7,8 @@ import (
"fmt"
"time"
"github.com/APIParkLab/APIPark/service/subscribe"
application_authorization "github.com/APIParkLab/APIPark/service/application-authorization"
"github.com/eolinker/eosc/log"
@@ -36,11 +38,80 @@ var (
type imlAuthorizationModule struct {
serviceService service.IServiceService `autowired:""`
subscribeService subscribe.ISubscribeService `autowired:""`
authorizationService application_authorization.IAuthorizationService `autowired:""`
clusterService cluster.IClusterService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (i *imlAuthorizationModule) CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error) {
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
return false, err
}
if len(list) < 1 {
return false, fmt.Errorf("no application found")
}
appIds := utils.SliceToSlice(list, func(s *subscribe.Subscribe) string {
return s.Application
})
authorizations, err := i.authorizationService.ListByApp(ctx, appIds...)
if err != nil {
return false, err
}
for _, a := range authorizations {
if a.Type != "apikey" {
continue
}
cfg := make(map[string]interface{})
if a.Config != "" {
json.Unmarshal([]byte(a.Config), &cfg)
}
if cfg["apikey"] == apikey {
return true, nil
}
}
return false, nil
}
func (i *imlAuthorizationModule) AuthorizationsByService(ctx context.Context, serviceId string, authorizationType string) ([]*application_authorization_dto.Authorization, error) {
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
return nil, err
}
if len(list) < 1 {
return nil, fmt.Errorf("no application found")
}
appIds := utils.SliceToSlice(list, func(s *subscribe.Subscribe) string {
return s.Application
})
authorizations, err := i.authorizationService.ListByApp(ctx, appIds...)
if err != nil {
return nil, err
}
result := make([]*application_authorization_dto.Authorization, 0, len(authorizations))
for _, a := range authorizations {
if authorizationType != "" && a.Type != authorizationType {
continue
}
cfg := make(map[string]interface{})
if a.Config != "" {
json.Unmarshal([]byte(a.Config), &cfg)
}
result = append(result, &application_authorization_dto.Authorization{
UUID: a.UUID,
Name: a.Name,
Driver: a.Type,
Position: a.Position,
TokenName: a.TokenName,
Config: cfg,
ExpireTime: a.ExpireTime,
HideCredential: a.HideCredential,
})
}
return result, nil
}
func (i *imlAuthorizationModule) ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error) {
list, err := i.authorizationService.List(ctx)
if err != nil {
@@ -129,11 +200,13 @@ func (i *imlAuthorizationModule) initGateway(ctx context.Context, partitionId st
}
func (i *imlAuthorizationModule) online(ctx context.Context, s *service.Service) error {
clusters, err := i.clusterService.List(ctx)
if err != nil {
return err
}
if len(clusters) < 1 {
return nil
}
authorizations, err := i.authorizationService.ListByApp(ctx, s.Id)
if err != nil {
return err
+3
View File
@@ -29,8 +29,10 @@ type ServiceDetail struct {
Document string `json:"document"`
Basic *ServiceBasic `json:"basic"`
APIDoc string `json:"api_doc"`
OpenAPIAddress string `json:"openapi_address"`
MCPServerAddress string `json:"mcp_server_address"`
MCPAccessConfig string `json:"mcp_access_config"`
CanSubscribe bool `json:"can_subscribe"`
}
type ServiceBasic struct {
@@ -46,6 +48,7 @@ type ServiceBasic struct {
ServiceKind string `json:"service_kind"`
InvokeAddress string `json:"invoke_address"`
SitePrefix string `json:"site_prefix"`
InvokeCount int64 `json:"invoke_count"`
EnableMCP bool `json:"enable_mcp"`
}
+20 -3
View File
@@ -181,8 +181,16 @@ func (i *imlCatalogueModule) genCommonWheres(ctx context.Context, clusterIds ...
}
func (i *imlCatalogueModule) ProviderStatistics(ctx context.Context, start, end time.Time, serviceIds ...string) (map[string]int64, error) {
// 判断是否配置influxdb
clusterId := cluster.DefaultClusterID
_, err := i.clusterService.Get(ctx, clusterId)
_, err := i.monitorService.Get(ctx, clusterId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return make(map[string]int64), nil
}
return nil, err
}
_, err = i.clusterService.Get(ctx, clusterId)
if err != nil {
return nil, err
}
@@ -392,8 +400,12 @@ func (i *imlCatalogueModule) ServiceDetail(ctx context.Context, sid string) (*ca
mcpAccessAddress = fmt.Sprintf("%s/openapi/v1/%s/%s/sse?apikey={your_api_key}", strings.TrimSuffix(sitePrefix, "/"), mcp_server.ServiceBasePath, s.Id)
mcpAccessConfig = fmt.Sprintf(mcpDefaultConfig, fmt.Sprintf("APIPark/%s", s.Name), mcpAccessAddress)
}
}
invokeMap, err := i.ProviderStatistics(ctx, time.Now().Add(-24*30*time.Hour), time.Now(), s.Id)
if err != nil {
return nil, err
}
return &catalogue_dto.ServiceDetail{
Name: s.Name,
Description: s.Description,
@@ -412,8 +424,10 @@ func (i *imlCatalogueModule) ServiceDetail(ctx context.Context, sid string) (*ca
InvokeAddress: invokeAddress,
SitePrefix: sitePrefix,
EnableMCP: s.EnableMCP,
InvokeCount: invokeMap[s.Id],
},
APIDoc: apiDoc,
OpenAPIAddress: fmt.Sprintf("%s/api/v1/service/apidoc/%s", strings.TrimSuffix(sitePrefix, "/"), s.Id),
MCPServerAddress: mcpAccessAddress,
MCPAccessConfig: mcpAccessConfig,
}, nil
@@ -509,7 +523,10 @@ func (i *imlCatalogueModule) recurseUpdateSort(ctx context.Context, parent strin
if len(item.Children) < 1 {
continue
}
return i.recurseUpdateSort(ctx, item.Id, item.Children)
err = i.recurseUpdateSort(ctx, item.Id, item.Children)
if err != nil {
return err
}
}
return nil
}
+3
View File
@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -268,6 +269,8 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
for _, value := range v {
queryParam.Add(k, value)
}
case float64:
queryParam.Add(k, strconv.FormatFloat(v, 'f', -1, 64))
default:
return nil, fmt.Errorf("invalid query param type: %T", v)
}
+6
View File
@@ -159,6 +159,7 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
return &gateway.ProxyHeader{
Key: h.Key,
Value: h.Value,
Opt: h.OptType,
}
})
apiInfo.Retry = proxy.Retry
@@ -633,12 +634,17 @@ func (i *imlPublishModule) updateMCPServer(ctx context.Context, sid string, name
if err != nil {
return err
}
//switch a.ContentType {
//case "application/json":
switch tmp.Type {
case "object":
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
//case "application/x-www-form-urlencoded":
// toolOptions = append(toolOptions, mcp.WithString(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...))
}
+4 -7
View File
@@ -78,17 +78,14 @@ func (m *imlReleaseModule) Create(ctx context.Context, serviceId string, input *
return "", fmt.Errorf("cluster not set:%w", err)
}
apis, err := m.apiService.ListInfoForService(ctx, proInfo.Id)
apis, err := m.apiService.ListForService(ctx, proInfo.Id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", errors.New("api not found")
}
return "", err
}
if len(apis) == 0 {
return "", errors.New("api not found")
}
apiUUIDS := utils.SliceToSlice(apis, func(a *api.Info) string {
apiUUIDS := utils.SliceToSlice(apis, func(a *api.API) string {
return a.UUID
})
apiProxy, err := m.apiService.ListLatestCommitProxy(ctx, apiUUIDS...)
@@ -122,10 +119,10 @@ func (m *imlReleaseModule) Create(ctx context.Context, serviceId string, input *
return c.Key, c.UUID
})
})
apiInfos, err := m.apiService.ListInfo(ctx, apiUUIDS...)
var newRelease *release.Release
err = m.transaction.Transaction(ctx, func(ctx context.Context) error {
for _, a := range apis {
for _, a := range apiInfos {
err = m.apiService.SaveRequest(ctx, a.UUID, &api.Request{
Path: a.Path,
Methods: a.Methods,
+4 -3
View File
@@ -80,9 +80,10 @@ func ToServiceProxy(proxy *InputProxy) *api.Proxy {
}
headers := utils.SliceToSlice(proxy.Headers, func(h *Header) *api.Header {
return &api.Header{
Key: h.Key,
Value: h.Value,
Opt: h.Opt,
Key: h.Key,
Value: h.Value,
Opt: h.Opt,
OptType: h.OptType,
}
})
+8 -6
View File
@@ -84,9 +84,10 @@ func FromServiceProxy(proxy *api.Proxy) *Proxy {
Retry: proxy.Retry,
Headers: utils.SliceToSlice(proxy.Headers, func(header *api.Header) *Header {
return &Header{
Key: header.Key,
Value: header.Value,
Opt: header.Opt,
Key: header.Key,
Value: header.Value,
Opt: header.Opt,
OptType: header.OptType,
}
}),
Extends: proxy.Extends,
@@ -104,9 +105,10 @@ type Proxy struct {
}
type Header struct {
Key string `json:"key"`
Value string `json:"value"`
Opt string `json:"opt"`
Key string `json:"key"`
Value string `json:"value"`
Opt string `json:"opt"`
OptType string `json:"optType"`
}
type Export struct {
+8 -1
View File
@@ -351,7 +351,14 @@ func (i *imlRouterModule) Delete(ctx context.Context, serviceId string, apiId st
if err != nil {
return err
}
return i.apiService.Delete(ctx, apiId)
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
err = i.apiService.Delete(ctx, apiId)
if err != nil {
return err
}
return i.apiService.DeleteAPIInfo(ctx, apiId)
})
}
func (i *imlRouterModule) Prefix(ctx context.Context, serviceId string) (string, error) {
+6
View File
@@ -86,6 +86,12 @@ type SimpleAppItem struct {
Description string `json:"description"`
}
type SubscribeAppItem struct {
Id string `json:"id"`
Name string `json:"name"`
IsSubscribed bool `json:"is_subscribed"`
}
type Service struct {
Id string `json:"id"`
Name string `json:"name"`
+50 -37
View File
@@ -742,6 +742,14 @@ func (i *imlServiceModule) Delete(ctx context.Context, id string) error {
if err != nil {
return err
}
err = client.Project().Offline(ctx, &gateway.ProjectRelease{
Id: id,
})
if err != nil {
if err.Error() != "nil" {
return err
}
}
err = client.Subscribe().Offline(ctx, &gateway.SubscribeRelease{
Service: id,
Application: "apipark-global",
@@ -861,65 +869,70 @@ type imlAppModule struct {
transaction store.ITransaction `autowired:""`
}
func (i *imlAppModule) SearchCanSubscribe(ctx context.Context, serviceId string) ([]*service_dto.SimpleAppItem, error) {
func (i *imlAppModule) SearchCanSubscribe(ctx context.Context, serviceId string) ([]*service_dto.SubscribeAppItem, bool, error) {
apps, err := i.searchMyApps(ctx, "", "")
if err != nil {
return nil, err
}
list, err := i.roleService.ListByPermit(ctx, access.SystemWorkspaceApplicationManagerAll)
if err == nil && len(list) > 0 {
return utils.SliceToSlice(apps, func(p *service.Service) *service_dto.SimpleAppItem {
return &service_dto.SimpleAppItem{
Id: p.Id,
Name: p.Name,
Description: p.Description,
Team: auto.UUID(p.Team),
}
}), nil
}
list, err = i.roleService.ListByPermit(ctx, access.TeamConsumerSubscriptionSubscribe)
if err != nil {
return nil, nil
}
roleIds := utils.SliceToSlice(list, func(p *role.RoleByPermit) string {
return p.Id
})
members, err := i.roleMemberService.ListByRoleIds(ctx, utils.UserId(ctx), roleIds...)
if err != nil {
return nil, err
}
if len(members) == 0 {
return nil, nil
return nil, false, err
}
subscribes, err := i.subscribeService.ListByServices(ctx, serviceId)
if err != nil {
return nil, err
return nil, false, err
}
subscribeMap := utils.SliceToMapO(subscribes, func(p *subscribe.Subscribe) (string, struct{}) {
return p.Application, struct{}{}
}, func(s *subscribe.Subscribe) bool {
return s.ApplyStatus == subscribe.ApplyStatusSubscribe
})
canSubscribe := false
list, err := i.roleService.ListByPermit(ctx, access.SystemWorkspaceApplicationManagerAll)
if err == nil && len(list) > 0 {
return utils.SliceToSlice(apps, func(p *service.Service) *service_dto.SubscribeAppItem {
_, isSubscribed := subscribeMap[p.Id]
if !isSubscribed {
canSubscribe = true
}
return &service_dto.SubscribeAppItem{
Id: p.Id,
Name: p.Name,
IsSubscribed: isSubscribed,
}
}), canSubscribe, nil
}
list, err = i.roleService.ListByPermit(ctx, access.TeamConsumerSubscriptionSubscribe)
if err != nil {
return nil, false, nil
}
roleIds := utils.SliceToSlice(list, func(p *role.RoleByPermit) string {
return p.Id
})
members, err := i.roleMemberService.ListByRoleIds(ctx, utils.UserId(ctx), roleIds...)
if err != nil {
return nil, false, err
}
if len(members) == 0 {
return nil, false, nil
}
teamMap := utils.SliceToMapO(members, func(p *role.Member) (string, struct{}) {
return role.TrimTeamTarget(p.Target), struct{}{}
})
result := make([]*service_dto.SimpleAppItem, 0, len(apps))
result := make([]*service_dto.SubscribeAppItem, 0, len(apps))
for _, app := range apps {
if _, ok := teamMap[app.Team]; !ok {
continue
}
if _, ok := subscribeMap[app.Id]; ok {
continue
_, isSubscribed := subscribeMap[app.Id]
if !isSubscribed {
canSubscribe = true
}
result = append(result, &service_dto.SimpleAppItem{
Id: app.Id,
Name: app.Name,
Description: app.Description,
Team: auto.UUID(app.Team),
result = append(result, &service_dto.SubscribeAppItem{
Id: app.Id,
Name: app.Name,
IsSubscribed: isSubscribed,
})
}
return result, nil
return result, canSubscribe, nil
}
func (i *imlAppModule) ExportAll(ctx context.Context) ([]*service_dto.ExportApp, error) {
+1 -1
View File
@@ -51,7 +51,7 @@ type IAppModule interface {
UpdateApp(ctx context.Context, appId string, input *service_dto.UpdateApp) (*service_dto.App, error)
Search(ctx context.Context, teamId string, keyword string) ([]*service_dto.AppItem, error)
SearchMyApps(ctx context.Context, teamId string, keyword string) ([]*service_dto.AppItem, error)
SearchCanSubscribe(ctx context.Context, serviceId string) ([]*service_dto.SimpleAppItem, error)
SearchCanSubscribe(ctx context.Context, serviceId string) ([]*service_dto.SubscribeAppItem, bool, error)
// SimpleApps 获取简易项目列表
SimpleApps(ctx context.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
MySimpleApps(ctx context.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
+6
View File
@@ -24,6 +24,12 @@ type SimpleItem struct {
Expired int64 `json:"expired"`
}
type AuthorizationItem struct {
Id string `json:"id"`
Name string `json:"name"`
Apikeys []SimpleItem `json:"apikeys"`
}
func ToAPIKey(e *system_apikey.APIKey) *APIKey {
return &APIKey{
Item: ToAPIKeyItem(e),
+81
View File
@@ -3,8 +3,11 @@ package system_apikey
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/APIParkLab/APIPark/service/subscribe"
application_authorization "github.com/APIParkLab/APIPark/service/application-authorization"
"github.com/APIParkLab/APIPark/service/service"
@@ -34,9 +37,87 @@ type imlAPIKeyModule struct {
teamMemberService team_member.ITeamMemberService `autowired:""`
serviceService service.IServiceService `autowired:""`
applicationAuthorizationService application_authorization.IAuthorizationService `autowired:""`
subscribeService subscribe.ISubscribeService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (i *imlAPIKeyModule) MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) {
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
return nil, err
}
if len(list) < 1 {
return nil, fmt.Errorf("no subscriber found")
}
appMap := utils.SliceToMapO(list, func(a *subscribe.Subscribe) (string, struct{}) {
return a.Application, struct{}{}
})
members, err := i.teamMemberService.Members(ctx, nil, nil)
if err != nil {
return nil, err
}
if len(members) == 0 {
return nil, nil
}
teamIds := utils.SliceToSlice(members, func(m *team_member.Member) string {
return m.Come
})
apps, err := i.serviceService.AppListByTeam(ctx, teamIds...)
if err != nil {
return nil, err
}
appInfoMap := make(map[string]*service.Service)
appIds := make([]string, 0, len(apps))
for _, a := range apps {
if _, ok := appMap[a.Id]; !ok {
continue
}
appInfoMap[a.Id] = a
appIds = append(appIds, a.Id)
}
if len(appIds) < 1 {
return nil, fmt.Errorf("no app found")
}
auths, err := i.applicationAuthorizationService.ListByApp(ctx, appIds...)
if err != nil {
return nil, err
}
result := make(map[string]*system_apikey_dto.AuthorizationItem)
for _, a := range auths {
if a.Type != "apikey" {
continue
}
appInfo, ok := appInfoMap[a.Application]
if !ok {
continue
}
m := make(map[string]string)
json.Unmarshal([]byte(a.Config), &m)
if m["apikey"] == "" {
continue
}
if _, ok := result[appInfo.Id]; !ok {
result[appInfo.Id] = &system_apikey_dto.AuthorizationItem{
Id: appInfo.Id,
Name: appInfo.Name,
Apikeys: []system_apikey_dto.SimpleItem{},
}
}
result[appInfo.Id].Apikeys = append(result[appInfo.Id].Apikeys, system_apikey_dto.SimpleItem{
Id: a.UUID,
Name: a.Name,
Value: m["apikey"],
Expired: a.ExpireTime,
})
}
return utils.MapToSlice(result, func(k string, t *system_apikey_dto.AuthorizationItem) *system_apikey_dto.AuthorizationItem {
return t
}), nil
}
func (i *imlAPIKeyModule) MyAPIKeys(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error) {
members, err := i.teamMemberService.Members(ctx, nil, nil)
if err != nil {
+1
View File
@@ -18,6 +18,7 @@ type IAPIKeyModule interface {
Search(ctx context.Context, keyword string) ([]*system_apikey_dto.Item, error)
SimpleList(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error)
MyAPIKeys(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error)
MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error)
}
func init() {
+2 -1
View File
@@ -23,7 +23,8 @@ func (p *plugin) systemApikeyApis() []pm3.Api {
pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/system/apikey", []string{"context", "query:apikey", "body"}, nil, p.systemAPIKeyController.Update),
pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/system/apikey", []string{"context", "query:apikey"}, nil, p.systemAPIKeyController.Delete),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/system/apikeys", []string{"context", "query:keyword"}, []string{"apikeys"}, p.systemAPIKeyController.Search),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/apikeys/", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.MyAPIKeys),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.MyAPIKeys),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/app/apikeys", []string{"context", "query:service"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/system/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.SimpleList),
}
}
+5 -1
View File
@@ -14,12 +14,16 @@ import (
func (p *plugin) mcpAPIs() []pm3.Api {
globalMessagePath := fmt.Sprintf("/openapi/v1/%s/message", strings.Trim(mcp_server.GlobalBasePath, "/"))
serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", strings.Trim(mcp_server.ServiceBasePath, "/"))
serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/"))
ignore.IgnorePath("openapi", http.MethodPost, globalMessagePath)
ignore.IgnorePath("openapi", http.MethodGet, serviceSSEPath)
ignore.IgnorePath("openapi", http.MethodPost, serviceMessagePath)
return []pm3.Api{
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/")), p.mcpController.GlobalHandleSSE),
pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalHandleMessage),
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/")), p.mcpController.ServiceHandleSSE),
pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.ServiceHandleSSE),
pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.ServiceHandleMessage),
}
}
+8 -4
View File
@@ -1,10 +1,14 @@
# 名称:apipark通用镜像
# 创建时间:2022-10-25
FROM centos:7.9.2009
MAINTAINER liujian
FROM alpine:latest
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirrors.aliyun.com/alpine|g' /etc/apk/repositories \
&& apk update \
&& apk add --no-cache curl tzdata bind-tools
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
ARG APP
+9 -5
View File
@@ -10,6 +10,11 @@ BuildMode=$3
if [[ "${BuildMode}" == "" ]];then
BuildMode="all"
fi
if [[ "${ARCH}" == "" ]];then
ARCH="amd64"
fi
# 编译可执行文件
./scripts/build.sh "cmd" "" "${BuildMode}" ${ARCH}
@@ -22,12 +27,11 @@ mkdir -p scripts/cmd/ && cp cmd/${APP} scripts/cmd/ && cp cmd/apipark_ai_event_l
VERSION=$(gen_version)
if [[ "${ARCH}" == "" ]];then
ARCH="amd64"
fi
OPTIONS=""
if [[ "${ARCH}" == "arm" ]];then
SYS_ARCH=$(arch)
if [[ (${SYS_ARCH} == "aarch64" || ${SYS_ARCH} == "arm64") && $ARCH == "amd64" ]];then
OPTIONS="--platform=linux/amd64"
elif [[ ${SYS_ARCH} == "amd64" && $ARCH == "arm64" ]];then
OPTIONS="--platform=linux/arm64"
fi
+29 -3
View File
@@ -1,11 +1,10 @@
#!/bin/sh
set -e
source ./init_config.sh
OLD_IFS="$IFS"
IFS=","
arr=(${REDIS_ADDR})
IFS="$OLD_IFS"
@@ -21,10 +20,11 @@ echo -e "redis:" >> config.yml
echo -e " user_name: ${REDIS_USER_NAME}" >> config.yml
echo -e " password: ${REDIS_PWD}" >> config.yml
echo -e " addr: " >> config.yml
for s in ${arr[@]}
for s in $REDIS_ADDR
do
echo -e " - $s" >> config.yml
done
echo -e "nsq:" >> config.yml
echo -e " addr: ${NSQ_ADDR}" >> config.yml
echo -e " topic_prefix: ${NSQ_TOPIC_PREFIX}" >> config.yml
@@ -38,5 +38,31 @@ echo -e " log_period: ${ERROR_PERIOD}" >> config.yml
cat config.yml
nohup ./apipark >> run.log 2>&1 &
wait_for_apipark
nohup ./apipark_ai_event_listen >> run.log 2>&1 &
if [[ ${Init} == "true" ]];then
login_apipark
r=$(is_init)
if [[ $r == "true" ]];then
echo "Already initialized, skipping initialization."
exit 0
fi
wait_for_influxdb
wait_for_apinto
set_cluster
wait_for_influxdb
set_influxdb
set_loki
set_nsq
set_openapi_config
# 重启apipark
kill -9 $(pgrep apipark)
nohup ./apipark >> run.log 2>&1 &
fi
tail -F run.log
+292
View File
@@ -0,0 +1,292 @@
#!/bin/sh
Cookie=""
if [[ "$ApiparkAddress" == "" ]]; then
ApiparkAddress="http://127.0.0.1:8288"
fi
if [[ "${ApintoAddress}" == "" ]]; then
ApintoAddress="http://apipark-apinto:9400"
fi
if [[ "$InfluxdbAddress" == "" ]]; then
InfluxdbAddress="http://apipark-influxdb:8086"
fi
if [[ "$NSQAddress" == "" ]]; then
NSQAddress="apipark-nsq:4150"
fi
if [[ "$LokiAddress" == "" ]]; then
LokiAddress="http://apipark-loki:3100"
fi
echo_fail() {
printf "\e[91m✘ Error:\e[0m $@\n" >&2
}
echo_pass() {
printf "\e[92m✔ Passed:\e[0m $@\n" >&2
}
echo_warn() {
printf "\e[93m⚠ Warning:\e[0m $@\n" >&2
}
echo_pause() {
printf "\e[94m⏸ Pause:\e[0m $1\n" >&2
}
echo_question() {
printf "\e[95m? Question:\e[0m $@\n" >&2
}
echo_info() {
printf "\e[96m Info:\e[0m $1\n" >&2
}
echo_point() {
printf "\e[94m➜ Point:\e[0m $1\n" >&2
}
echo_bullet() {
printf "\e[94m• Step:\e[0m $1\n" >&2
}
echo_wait() {
printf "\e[95m⏳ Waiting:\e[0m $1\n" >&2
}
echo_split() {
echo "" >&2
echo "" >&2
echo -e "\e[94m────────────────────────────────────────────────────────────\e[0m" >&2
}
request_apipark() {
path=$1
body=$2
method=$3
if [[ "$method" == "" ]]; then
method="POST"
fi
if [[ "$Cookie" == "" ]]; then
cmd="curl -X ${method} -s -i -H \"Content-Type: application/json\" -d '$body' \"${ApiparkAddress}${path}\""
echo_info "Executing: $cmd" # 打印命令
response=$(eval "$cmd")
else
cmd="curl -X ${method} -s -i -H \"Content-Type: application/json\" -H \"Cookie: $Cookie\" -d '$body' \"${ApiparkAddress}${path}\""
echo_info "Executing: $cmd" # 打印命令
response=$(eval "$cmd")
fi
echo "$response"
}
request_apinto() {
path="$1"
body="$2"
echo_info "Executing: curl -i -X POST -H \"Content-Type: application/json\" \"${ApintoAddress}${path}\" -d '$body'"
response=$(curl -i -X POST -H "Content-Type: application/json" "${ApintoAddress}${path}" -d "$body")
status_code=$(echo "$response" | grep -E 'HTTP/[0-9.]+ [0-9]+' | awk '{print $2}' || echo "0")
echo_info "$response"
echo "$status_code"
}
login_apipark() {
# 执行登录请求并捕获响应头
body='{"name":"admin","password":"'"${ADMIN_PASSWORD}"'"}'
response=$(request_apipark "/api/v1/account/login/username" "$body")
# 从响应中提取 Set-Cookie 头
cookie=$(echo "$response" | grep -i "Set-Cookie" | sed 's/Set-Cookie: //;s/;.*//')
# 提取 JSON 主体(假设 JSON 在最后一行或响应中可识别)
json_body=$(echo "$response" | grep '^{.*}$')
# 提取 code 值
code=$(echo "$json_body" | sed 's/.*"code":\([0-9]*\).*/\1/')
# 检查 code 是否为 0
if [ "$code" -eq 0 ]; then
Cookie=$cookie
echo_pass "login success"
else
echo_fail "login failed: $json_body"
exit 1
fi
}
set_cluster() {
# 设置集群地址
body='{"manager_address":"'"${ApintoAddress}"'"}'
path="/api/v1/cluster/reset"
response=$(request_apipark "$path" "$body" "PUT")
# 从响应中提取 code
code=$(echo "${response}" | grep '^{.*}$' | sed 's/.*"code":\([0-9]*\).*/\1/')
if [ "$code" -eq 0 ]; then
echo_pass "Set cluster successfully"
else
echo_fail "Set cluster failed: ${response}"
exit 1
fi
}
set_loki() {
# 设置 loki 地址
body='{"config":{"url":"'"${LokiAddress}"'"}}'
path="/api/v1/log/loki"
response=$(request_apipark "$path" "$body")
# 从响应中提取 code
code=$(echo "${response}" | grep '^{.*}$' | sed 's/.*"code":\([0-9]*\).*/\1/')
if [ "$code" -eq 0 ]; then
echo_pass "Set loki successfully"
else
echo_fail "Set loki failed: ${response}"
exit 1
fi
}
set_nsq() {
body="{
\"address\": [
\"${NSQAddress}\"
],
\"description\": \"auto init nsqd config\",
\"driver\": \"nsqd\",
\"formatter\": {
\"ai\": [
\"\$ai_provider\",
\"\$ai_model\",
\"\$ai_model_input_token\",
\"\$ai_model_output_token\",
\"\$ai_model_total_token\",
\"\$ai_model_cost\",
\"\$ai_provider_statuses\"
],
\"fields\": [
\"\$time_iso8601\",
\"\$request_id\",
\"\$api\",
\"\$provider\",
\"@ai\"
]
},
\"scopes\": [
\"access_log\"
],
\"topic\": \"apipark_ai_event\",
\"type\": \"json\"
}"
status_code=$(request_apinto "/api/output/ai_event" "$body")
echo "Status code: $status_code"
if [ "$status_code" -eq 200 ]; then
echo_pass "Update nsq successfully"
else
echo_fail "Update nsq failed: ${status_code}"
exit 1
fi
}
set_influxdb() {
if [ -z "$InfluxdbToken" ]; then
echo_fail "Influxdb token is empty"
exit 1
fi
if [ -z "$InfluxdbOrg" ]; then
InfluxdbOrg="apipark"
fi
body='{"driver":"influxdb-v2","config":{"addr":"'"${InfluxdbAddress}"'","org":"'"${InfluxdbOrg}"'","token":"'${InfluxdbToken}'"}}'
response=$(request_apipark "/api/v1/monitor/config" "$body")
# 从响应中提取 code
code=$(echo "${response}" | grep '^{.*}$' | sed 's/.*"code":\([0-9]*\).*/\1/')
if [ "$code" -eq 0 ]; then
echo_pass "Update influxdb config successfully"
else
echo_fail "Update influxdb config failed: ${response}"
exit 1
fi
}
retry() {
max_wait="$1"
shift
cmd="$@"
sleep_interval=2
curr_wait=0
until $cmd
do
if [ "$curr_wait" -ge "$max_wait" ]
then
echo "Command '$cmd' failed after $curr_wait seconds."
return 1
else
curr_wait=$((curr_wait + sleep_interval))
sleep "$sleep_interval"
fi
done
}
is_init() {
path="/api/v1/system/general"
method="GET"
response=$(request_apipark "$path" "" "$method")
# 从响应中提取 site_prefix
site_prefix=$(echo "${response}" | grep '^{.*}$' | sed 's/.*"site_prefix":"\{0,1\}\([^"]*\)"\{0,1\}.*/\1/')
if [ -z "$site_prefix" ]; then
echo_pass "No apipark openapi address set"
echo false
else
echo_pass "Apipark openapi address found: $site_prefix"
echo true
fi
}
set_openapi_config() {
IP=$(dig +short myip.opendns.com @resolver1.opendns.com | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$')
if [ -z "$IP" ]; then
echo_fail "Failed to resolve IP address"
exit 1
fi
body='{"site_prefix":"http://'"${IP}"':18288"}'
response=$(request_apipark "/api/v1/system/general" "$body")
# 从响应中提取 code
code=$(echo "${response}" | grep '^{.*}$' | sed 's/.*"code":\([0-9]*\).*/\1/')
if [ "$code" -eq 0 ]; then
echo_pass "Update apipark openapi address successfully"
else
echo_fail "Update apipark openapi address failed: ${response}"
exit 1
fi
}
wait_for() {
waitName=$1
cmd=$2
echo ${cmd}
echo_wait "Waiting for ${waitName} to start..."
retry 30 ${cmd}
if [ $? -eq 0 ]; then
echo_pass "${waitName} has been installed successfully"
else
echo_fail "${waitName} installation failed"
exit 1
fi
}
wait_for_apipark() {
wait_for "apipark" "curl -s -o /dev/null ${ApiparkAddress}/api/v1/account/login"
}
wait_for_apinto() {
wait_for "apinto" "curl -s -o /dev/null ${ApintoAddress}/api/router"
}
wait_for_influxdb() {
wait_for "influxdb" "curl -s -o /dev/null ${InfluxdbAddress}/api/v2/health"
}
+6
View File
@@ -41,6 +41,12 @@ type imlAPIService struct {
universally.IServiceDelete
}
func (i *imlAPIService) DeleteAPIInfo(ctx context.Context, aid string) error {
return i.apiInfoStore.SoftDelete(ctx, map[string]interface{}{
"uuid": aid,
})
}
func (i *imlAPIService) DeleteByService(ctx context.Context, serviceId string) error {
return i.store.SoftDelete(ctx, map[string]interface{}{
"service": serviceId,
+4 -3
View File
@@ -144,9 +144,10 @@ type Proxy struct {
}
type Header struct {
Key string `json:"key"`
Value string `json:"value"`
Opt string `json:"opt"`
Key string `json:"key"`
Value string `json:"value"`
Opt string `json:"opt"`
OptType string `json:"opt_type"`
}
type Router struct {
+1
View File
@@ -13,6 +13,7 @@ import (
type IAPIService interface {
universally.IServiceGet[API]
universally.IServiceDelete
DeleteAPIInfo(ctx context.Context, aid string) error
CountByService(ctx context.Context, service string) (int64, error)
CountMapByService(ctx context.Context, service ...string) (map[string]int64, error)
Exist(ctx context.Context, aid string, api *Exist) error
+2 -2
View File
@@ -21,8 +21,8 @@ type ISubscribeService interface {
MySubscribeServices(ctx context.Context, application string, serviceIDs []string) ([]*Subscribe, error)
UpdateSubscribeStatus(ctx context.Context, application string, service string, status int) error
ListBySubscribeStatus(ctx context.Context, projectId string, status int) ([]*Subscribe, error)
SubscribersByProject(ctx context.Context, projectIds ...string) ([]*Subscribe, error)
ListBySubscribeStatus(ctx context.Context, serviceId string, status int) ([]*Subscribe, error)
SubscribersByProject(ctx context.Context, serviceIds ...string) ([]*Subscribe, error)
Subscribers(ctx context.Context, project string, status int) ([]*Subscribe, error)
SubscriptionsByApplication(ctx context.Context, applicationIds ...string) ([]*Subscribe, error)
}
+1
View File
@@ -34,6 +34,7 @@ type Info struct {
Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人
UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"`
Disable bool `gorm:"type:tinyint(1);not null;column:disable;comment:是否禁用 0:否 1:是"`
IsDelete bool `gorm:"type:tinyint(1);not null;column:is_delete;comment:是否删除 0:否 1:是"`
}
func (i *Info) TableName() string {
+1 -1
View File
@@ -77,7 +77,7 @@ func (t *Tag) TableName() string {
type Doc struct {
Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"`
Sid string `gorm:"size:36;not null;column:sid;comment:服务id;uniqueIndex:unique_sid;"`
Doc string `gorm:"type:text;column:content;comment:内容"`
Doc string `gorm:"type:longtext;column:content;comment:内容"`
CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"`
UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"`
Creator string `gorm:"type:varchar(36);not null;column:creator;comment:创建者"`