mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3217ad2bba | |||
| 1485b31226 | |||
| 8968e1f961 | |||
| 4b8fa43c36 | |||
| c21d783c52 | |||
| e928cd84d7 | |||
| 412cb75bf0 | |||
| a13b2a8afe | |||
| c0472c8539 | |||
| 5f40d5092c | |||
| b949939816 | |||
| 55f09f7542 | |||
| ef1ccb81a9 | |||
| 7aad2174aa | |||
| 8fba1911ad | |||
| 2a951c2854 | |||
| e91a9e7726 | |||
| e0d97186b1 | |||
| dff6e722c0 | |||
| b8c92961c1 | |||
| 025bd4c6cc | |||
| 3d2ec67fc1 | |||
| b2a8c8d901 | |||
| 90226ac6af | |||
| 0e1efc9656 | |||
| 061027aa36 | |||
| 96e2e6ad51 | |||
| a083f994f4 | |||
| 5898337481 | |||
| 0f1496137d | |||
| aff2d1ce01 | |||
| 78d10318ad | |||
| 0525c51a8f | |||
| 47950d9a2b | |||
| 4caa19586e | |||
| a574ed8dae | |||
| 8eba1ea898 | |||
| 17bd63b27e | |||
| 88d890107e | |||
| 6b90770a92 | |||
| c1c5eaa84e | |||
| 388cd7660d | |||
| 3df303abc3 | |||
| 217e31787c | |||
| 96d92e2323 | |||
| 6ced12af75 | |||
| 9e7feff093 | |||
| ba6342d207 | |||
| 5b38b2f78a | |||
| 707d98ba34 | |||
| d5f2578a3f | |||
| a8d39b021c | |||
| 78dc382e82 | |||
| 4b58534cb6 | |||
| 63f5a552c8 | |||
| a03e310418 | |||
| 28ad364352 | |||
| e485ae2511 | |||
| 853c04337d | |||
| 526dddb579 | |||
| 902c64903c | |||
| 2f314e0e64 | |||
| 9879506c30 | |||
| 6dabd5d31b | |||
| 16e5a37087 | |||
| 12c5ac85d3 | |||
| 997e9292a2 | |||
| 76ae5287eb | |||
| c9c88116a8 | |||
| 393271d72a | |||
| 1bddef2bda | |||
| 58d02bcf08 | |||
| b1b9f49b06 | |||
| 8f2857cf55 | |||
| f149fede71 | |||
| 972f072346 | |||
| 3cab3c1828 | |||
| 88bf7d0244 | |||
| a7523c7b54 | |||
| 590f328e07 |
@@ -75,6 +75,11 @@ jobs:
|
||||
with:
|
||||
name: frontend-package
|
||||
path: frontend/dist
|
||||
# 设置 QEMU 以支持多架构构建
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login Docker #登录docker
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
|
||||
@@ -156,6 +156,10 @@ curl -sSO https://download.apipark.com/install/quick-start.sh ; bash quick-sta
|
||||
|
||||
<br>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
|
||||
# 🚀 Use Cases
|
||||
## Simplify AI Integration Costs
|
||||
- Connect to 100+ major models from all mainstream AI vendors, with standardized API calls requiring no additional adaptation work.
|
||||
@@ -199,11 +203,20 @@ To achieve this goal, we plan to add new features to APIPark, including:
|
||||
# 📕 Documentation
|
||||
Visit [APIPark Documentation](https://docs.apipark.com/docs/deploy) for detailed installation guides, API references, and usage instructions.
|
||||
|
||||
|
||||
# 🧑🤝🧑Friendly Links
|
||||
<a href="https://xroute.ai/">
|
||||
<img width="1248" height="158" alt="新建 PPTX 演示文稿 (2)_03" src="https://github.com/user-attachments/assets/0ebd694c-410a-4e3f-a793-90f1140d15df" />
|
||||
|
||||
</a>
|
||||
<br>
|
||||
|
||||
<br>
|
||||
|
||||
# 🧾 License
|
||||
APIPark uses the Apache 2.0 License. For more details, please refer to the LICENSE file.
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
# 💌 Contact Us
|
||||
@@ -215,3 +228,7 @@ For enterprise-level features and professional technical support, contact our pr
|
||||
<br>
|
||||
|
||||
🙏 A big thanks to everyone who helped shape APIPark. We are thrilled to hear the community’s thoughts! Let’s make the world of APIs and AI stronger and more fun together. 🎉
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+62748
-60558
File diff suppressed because it is too large
Load Diff
+124
-155
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/APIParkLab/APIPark/module/service"
|
||||
|
||||
application_authorization "github.com/APIParkLab/APIPark/module/application-authorization"
|
||||
|
||||
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
|
||||
@@ -13,7 +15,6 @@ import (
|
||||
"github.com/APIParkLab/APIPark/module/system"
|
||||
"github.com/eolinker/go-common/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
mcp2 "github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
@@ -22,12 +23,80 @@ var _ IMcpController = (*imlMcpController)(nil)
|
||||
type imlMcpController struct {
|
||||
settingModule system.ISettingModule `autowired:""`
|
||||
authorizationModule application_authorization.IAuthorizationModule `autowired:""`
|
||||
appModule service.IAppModule `autowired:""`
|
||||
mcpModule mcp.IMcpModule `autowired:""`
|
||||
sessionKeys sync.Map
|
||||
server map[string]http.Handler
|
||||
openServer http.Handler
|
||||
}
|
||||
|
||||
func (i *imlMcpController) AppMCPHandle(ctx *gin.Context) {
|
||||
appId := ctx.Param("app")
|
||||
if appId == "" {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"})
|
||||
return
|
||||
}
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
|
||||
req = req.WithContext(utils.SetLabel(req.Context(), "app", appId))
|
||||
paths := strings.Split(req.URL.Path, "/")
|
||||
req.URL.Path = fmt.Sprintf("/api/v1/%s/%s", mcp_server.GlobalBasePath, paths[len(paths)-1])
|
||||
locale := utils.I18n(ctx)
|
||||
if v, ok := i.server[locale]; ok {
|
||||
v.ServeHTTP(ctx.Writer, req)
|
||||
return
|
||||
}
|
||||
|
||||
i.server[languageEnUs].ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) AppHandleSSE(ctx *gin.Context) {
|
||||
apikey := ctx.Request.URL.Query().Get("apikey")
|
||||
appId := ctx.Param("app")
|
||||
if appId == "" {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"})
|
||||
return
|
||||
}
|
||||
ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByApp(ctx, appId, 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
|
||||
}
|
||||
|
||||
ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/sse", mcp_server.GlobalBasePath)
|
||||
i.handleSSE(ctx, i.openServer, SessionInfo{
|
||||
Apikey: apikey,
|
||||
App: appId,
|
||||
})
|
||||
}
|
||||
|
||||
func (i *imlMcpController) AppHandleMessage(ctx *gin.Context) {
|
||||
appId := ctx.Param("app")
|
||||
if appId == "" {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"})
|
||||
return
|
||||
}
|
||||
ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/message", mcp_server.GlobalBasePath)
|
||||
ctx.Request = ctx.Request.WithContext(utils.SetLabel(ctx.Request.Context(), "app", appId))
|
||||
i.handleMessage(ctx, i.openServer)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) AppMCPConfig(ctx *gin.Context, appId string) (string, error) {
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
if cfg.SitePrefix == "" {
|
||||
return "", fmt.Errorf("site prefix is empty")
|
||||
}
|
||||
appInfo, err := i.appModule.GetApp(ctx, appId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get app info error: %v", err)
|
||||
}
|
||||
return fmt.Sprintf(mcpDefaultConfig, appInfo.Name, fmt.Sprintf("%s/openapi/v1/mcp/app/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), appId)), nil
|
||||
}
|
||||
|
||||
var mcpDefaultConfig = `{
|
||||
"mcpServers": {
|
||||
"%s": {
|
||||
@@ -45,151 +114,18 @@ 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) generateZhCNMCPServer() *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 定义、参数结构、请求方式等详细信息,为后续构造请求做准备。"),
|
||||
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(
|
||||
"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(
|
||||
"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.")),
|
||||
mcp2.WithObject("query", mcp2.Description("API Request query,param type is map[string]string")),
|
||||
mcp2.WithObject("header", mcp2.Description("API Request header,param type is map[string]string")),
|
||||
mcp2.WithString("body", mcp2.Description("API Request body")),
|
||||
),
|
||||
i.mcpModule.Invoke,
|
||||
)
|
||||
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, "/"))))
|
||||
for language, tools := range mcpToolsByLanguage {
|
||||
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
|
||||
s.AddTool(tools[ToolServiceList], i.mcpModule.Services)
|
||||
s.AddTool(tools[ToolOpenAPIDocument], i.mcpModule.APIs)
|
||||
s.AddTool(tools[ToolInvokeAPI], i.mcpModule.Invoke)
|
||||
i.server[language] = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
|
||||
if language == languageEnUs {
|
||||
i.openServer = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
|
||||
@@ -200,15 +136,17 @@ func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
|
||||
v.ServeHTTP(ctx.Writer, req)
|
||||
return
|
||||
}
|
||||
i.server["en-US"].ServeHTTP(ctx.Writer, req)
|
||||
i.server[languageEnUs].ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) GlobalHandleSSE(ctx *gin.Context) {
|
||||
apikey := ctx.Request.URL.Query().Get("apikey")
|
||||
i.handleSSE(ctx, i.openServer, apikey)
|
||||
i.handleSSE(ctx, i.openServer, SessionInfo{
|
||||
Apikey: apikey,
|
||||
})
|
||||
}
|
||||
|
||||
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apikey string) {
|
||||
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, sIn SessionInfo) {
|
||||
|
||||
writer := &ResponseWriter{
|
||||
Writer: ctx.Writer,
|
||||
@@ -222,7 +160,7 @@ func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apik
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
i.sessionKeys.Store(sessionId, apikey)
|
||||
i.sessionKeys.Store(sessionId, sIn)
|
||||
}()
|
||||
server.ServeHTTP(writer, ctx.Request)
|
||||
i.sessionKeys.Delete(sessionId)
|
||||
@@ -246,7 +184,7 @@ func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"})
|
||||
return
|
||||
}
|
||||
ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey)
|
||||
ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
|
||||
return
|
||||
@@ -256,22 +194,53 @@ func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), apikey)
|
||||
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), SessionInfo{
|
||||
Apikey: apikey,
|
||||
})
|
||||
}
|
||||
|
||||
func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) {
|
||||
i.handleMessage(ctx, mcp_server.DefaultMCPServer())
|
||||
}
|
||||
|
||||
func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) {
|
||||
sessionId := ctx.Request.URL.Query().Get("sessionId")
|
||||
apikey, ok := i.sessionKeys.Load(sessionId)
|
||||
func (i *imlMcpController) ServiceHandleStreamHTTP(ctx *gin.Context) {
|
||||
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.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.String(403, "sessionId not found")
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
|
||||
return
|
||||
}
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
|
||||
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", apikey.(string)))
|
||||
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", apikey))
|
||||
mcp_server.DefaultMCPServer().ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) {
|
||||
sessionId := ctx.Request.URL.Query().Get("sessionId")
|
||||
params, ok := i.sessionKeys.Load(sessionId)
|
||||
if !ok {
|
||||
ctx.String(403, "sessionId not found")
|
||||
return
|
||||
}
|
||||
ps, ok := params.(SessionInfo)
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
|
||||
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", ps.Apikey))
|
||||
req = req.WithContext(utils.SetLabel(req.Context(), "app", ps.App))
|
||||
server.ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
|
||||
type SessionInfo struct {
|
||||
Apikey string
|
||||
App string
|
||||
}
|
||||
|
||||
+10
-1
@@ -9,12 +9,21 @@ import (
|
||||
|
||||
type IMcpController interface {
|
||||
MCPHandle(ctx *gin.Context)
|
||||
|
||||
GlobalMCPHandle(ctx *gin.Context)
|
||||
GlobalHandleSSE(ctx *gin.Context)
|
||||
GlobalHandleMessage(ctx *gin.Context)
|
||||
GlobalMCPConfig(ctx *gin.Context) (string, error)
|
||||
|
||||
AppMCPHandle(ctx *gin.Context)
|
||||
AppHandleSSE(ctx *gin.Context)
|
||||
AppHandleMessage(ctx *gin.Context)
|
||||
AppMCPConfig(ctx *gin.Context, appId string) (string, error)
|
||||
|
||||
ServiceHandleSSE(ctx *gin.Context)
|
||||
ServiceHandleMessage(ctx *gin.Context)
|
||||
GlobalMCPConfig(ctx *gin.Context) (string, error)
|
||||
|
||||
ServiceHandleStreamHTTP(ctx *gin.Context)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package mcp
|
||||
|
||||
import "github.com/mark3labs/mcp-go/mcp"
|
||||
|
||||
const (
|
||||
ToolServiceList = "service_list"
|
||||
ToolOpenAPIDocument = "openapi_document"
|
||||
ToolInvokeAPI = "invoke_api"
|
||||
|
||||
languageZhCN = "zh-CN"
|
||||
languageZhTW = "zh-TW"
|
||||
languageEnUs = "en-US"
|
||||
languageJaJp = "ja-JP"
|
||||
)
|
||||
|
||||
var mcpToolsByLanguage = map[string]map[string]mcp.Tool{
|
||||
languageZhCN: toolsZhCN,
|
||||
languageZhTW: toolsZhTW,
|
||||
languageEnUs: toolsEnUs,
|
||||
languageJaJp: toolsJaJp,
|
||||
}
|
||||
|
||||
var toolsZhTW = map[string]mcp.Tool{
|
||||
ToolServiceList: mcp.NewTool(
|
||||
ToolServiceList,
|
||||
mcp.WithDescription("此工具用於獲取 APIPark 中已註冊服務的列表。每個服務包含其唯一標識(service ID)、名稱、描述及包含的 API 列表等關鍵信息。支持通過關鍵詞進行模糊搜索,以便快速縮小查找範圍。在獲得某個服務的 ID 後,可以調用 openapi_document 工具來獲取該服務的 OpenAPI 文檔,以便後續調用其提供的 API 接口。"),
|
||||
mcp.WithString("keyword", mcp.Description("關鍵詞,用於模糊搜索服務"))),
|
||||
ToolOpenAPIDocument: mcp.NewTool(
|
||||
ToolOpenAPIDocument,
|
||||
mcp.WithDescription("此工具用於獲取 APIPark 中服務的 OpenAPI 文檔。"),
|
||||
mcp.WithString("service_id", mcp.Description("服務 ID"))),
|
||||
ToolInvokeAPI: mcp.NewTool(
|
||||
ToolInvokeAPI,
|
||||
mcp.WithDescription("此工具可直接發送 API 請求。在調用此工具之前,需要根據該 API 的 OpenAPI 文檔構造所需的請求參數,如請求路徑、方法、查詢參數、頭部信息、主體內容等。使用此工具時不需要傳遞任何認證信息,例如 Authorization 頭部可以省略。"),
|
||||
mcp.WithString("path", mcp.Description("API 的請求路徑"), mcp.Required()),
|
||||
mcp.WithString("method", mcp.Description("API 的請求方法,例如 GET、POST、PUT"), mcp.Required()),
|
||||
mcp.WithString("content-type", mcp.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")),
|
||||
mcp.WithObject("query", mcp.Description("請求的查詢參數,類型為 map[string]string")),
|
||||
mcp.WithObject("header", mcp.Description("請求的頭部,類型為 map[string]string")),
|
||||
mcp.WithString("body", mcp.Description("請求主體內容,通常為 JSON 字符串")),
|
||||
),
|
||||
}
|
||||
|
||||
var toolsZhCN = map[string]mcp.Tool{
|
||||
ToolServiceList: mcp.NewTool(
|
||||
ToolServiceList,
|
||||
mcp.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"),
|
||||
mcp.WithString("keyword", mcp.Description("关键词,用于模糊搜索服务"))),
|
||||
ToolOpenAPIDocument: mcp.NewTool(
|
||||
ToolOpenAPIDocument, mcp.WithDescription("此工具用于查询指定服务的 OpenAPI 文档。返回的格式支持 OpenAPI v3 和 v2 标准。通过输入服务 ID,可以查看该服务所有 API 的定义、参数结构、请求方式等详细信息,为后续构造 API 调用请求做准备。"),
|
||||
mcp.WithString("service", mcp.Description("欲查询的服务唯一标识")),
|
||||
),
|
||||
ToolInvokeAPI: mcp.NewTool(ToolInvokeAPI,
|
||||
mcp.WithDescription("此工具可直接发送 API 请求。在调用此工具之前,需要根据该 API 的 OpenAPI 文档构造所需的请求参数,如请求路径、方法、查询参数、头部信息、主体内容等。使用此工具时不需要传递任何认证信息,例如 Authorization 头部可以省略。"),
|
||||
mcp.WithString("path", mcp.Description("API 的请求路径"), mcp.Required()),
|
||||
mcp.WithString("method", mcp.Description("API 的请求方法,例如 GET、POST、PUT"), mcp.Required()),
|
||||
mcp.WithString("content-type", mcp.Description("请求的 Content-Type。若方法为 POST、PUT 或 PATCH,则必须指定")),
|
||||
mcp.WithObject("query", mcp.Description("请求的查询参数,类型为 map[string]string")),
|
||||
mcp.WithObject("header", mcp.Description("请求的头部,类型为 map[string]string")),
|
||||
mcp.WithString("body", mcp.Description("请求主体内容,通常为 JSON 字符串")),
|
||||
),
|
||||
}
|
||||
|
||||
var toolsEnUs = map[string]mcp.Tool{
|
||||
ToolServiceList: mcp.NewTool(
|
||||
ToolServiceList,
|
||||
mcp.WithDescription("This tool is used to retrieve a list of registered services in APIPark. Each service includes its unique identifier (service ID), name, description, and a list of APIs it contains. It supports fuzzy searching by keyword for quick narrowing down of results. After obtaining a service ID, you can use the openapi_document tool to get the OpenAPI documentation for that service, which is necessary for invoking its APIs."),
|
||||
mcp.WithString("keyword", mcp.Description("Keyword for fuzzy search of services"))),
|
||||
ToolOpenAPIDocument: mcp.NewTool(
|
||||
ToolOpenAPIDocument,
|
||||
mcp.WithDescription("This tool is used to query the OpenAPI documentation of a specified service. The returned format supports both OpenAPI v3 and v2 standards. By entering the service ID, you can view detailed information about all APIs of that service, including definitions, parameter structures, request methods, etc., which prepares you for subsequent API calls."),
|
||||
mcp.WithString("service", mcp.Description("Unique identifier of the service to query"))),
|
||||
ToolInvokeAPI: mcp.NewTool(
|
||||
ToolInvokeAPI,
|
||||
mcp.WithDescription("This tool can directly send API requests. Before using this tool, you need to construct the required request parameters based on the OpenAPI documentation of the API, such as request path, method, query parameters, header information, body content, etc. No authentication information like Authorization header is required when using this tool."),
|
||||
mcp.WithString("path", mcp.Description("API request path"), mcp.Required()),
|
||||
mcp.WithString("method", mcp.Description("API request method, e.g., GET, POST, PUT"), mcp.Required()),
|
||||
mcp.WithString("content-type", mcp.Description("Content-Type of the request. Must be specified if method is POST, PUT, or PATCH")),
|
||||
mcp.WithObject("query", mcp.Description("Query parameters of the request, type map[string]string")),
|
||||
mcp.WithObject("header", mcp.Description("Header information of the request, type map[string]string")),
|
||||
mcp.WithString("body", mcp.Description("Body content of the request, usually in JSON string")),
|
||||
),
|
||||
}
|
||||
|
||||
var toolsJaJp = map[string]mcp.Tool{
|
||||
ToolServiceList: mcp.NewTool(
|
||||
ToolServiceList,
|
||||
mcp.WithDescription("このツールは、APIParkに登録されているサービスのリストを取得するために使用されます。各サービスには、ユニークな識別子(サービスID)、名前、説明、および含まれるAPIのリストが含まれています。キーワードによるあいまい検索をサポートしており、結果を迅速に絞り込むことができます。サービスIDを取得した後は、openapi_documentツールを使用してそのサービスのOpenAPIドキュメントを取得し、そのAPIを呼び出す準備をします。"),
|
||||
mcp.WithString("keyword", mcp.Description("サービスをあいまい検索するためのキーワード"))),
|
||||
ToolOpenAPIDocument: mcp.NewTool(
|
||||
ToolOpenAPIDocument,
|
||||
mcp.WithDescription("このツールは、指定されたサービスのOpenAPIドキュメントを照会するために使用されます。返される形式は、OpenAPI v3およびv2標準の両方をサポートしています。サービスIDを入力することで、そのサービスのすべてのAPIに関する詳細情報(定義、パラメータ構造、リクエスト方法など)を表示し、後続のAPI呼び出しの準備をします。"),
|
||||
mcp.WithString("service", mcp.Description("照会するサービスのユニークな識別子"))),
|
||||
ToolInvokeAPI: mcp.NewTool(
|
||||
ToolInvokeAPI,
|
||||
mcp.WithDescription("このツールは、APIリクエストを直接送信できます。このツールを使用する前に、APIのOpenAPIドキュメントに基づいて、必要なリクエストパラメータ(リクエストパス、メソッド、クエリパラメータ、ヘッダー情報、ボディコンテンツなど)を構築する必要があります。このツールを使用する際には、Authorizationヘッダーなどの認証情報は必要ありません。"),
|
||||
mcp.WithString("path", mcp.Description("APIのリクエストパス"), mcp.Required()),
|
||||
mcp.WithString("method", mcp.Description("APIのリクエストメソッド(例:GET、POST、PUT)"), mcp.Required()),
|
||||
mcp.WithString("content-type", mcp.Description("リクエストのContent-Type。メソッドがPOST、PUT、またはPATCHの場合は必須です")),
|
||||
mcp.WithObject("query", mcp.Description("リクエストのクエリパラメータ、タイプはmap[string]string")),
|
||||
mcp.WithObject("header", mcp.Description("リクエストのヘッダー、タイプはmap[string]string")),
|
||||
mcp.WithString("body", mcp.Description("リクエストのボディコンテンツ、通常はJSON文字列")),
|
||||
),
|
||||
}
|
||||
@@ -16,7 +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)
|
||||
MyAPIKeysByService(ctx *gin.Context, serviceId string, appId string) ([]*system_apikey_dto.AuthorizationItem, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -12,8 +12,14 @@ 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) MyAPIKeysByService(ctx *gin.Context, serviceId string, appId string) ([]*system_apikey_dto.AuthorizationItem, error) {
|
||||
if serviceId != "" {
|
||||
return i.apikeyModule.MyAPIKeysByService(ctx, serviceId)
|
||||
}
|
||||
if appId != "" {
|
||||
return i.apikeyModule.MyAPIKeysByApp(ctx, appId)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=core --stream --verbose ",
|
||||
"build": "set NODE_OPTIONS=--max-old-space-size=8192 && lerna run build --scope=core --stream --verbose ",
|
||||
"serve": "lerna run preview --parallel",
|
||||
"serve:remotes": "lerna run serve --scope=remote --parallel",
|
||||
"dev": "lerna run dev --scope=core --stream",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -137,7 +137,7 @@ function BasicLayout({ project = 'core' }: { project: string }) {
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
() =>
|
||||
[
|
||||
userInfo?.type !== 'guest' && {
|
||||
!['guest', 'third-user'].includes(userInfo?.type as string) && {
|
||||
key: '2',
|
||||
label: (
|
||||
<Button
|
||||
|
||||
@@ -119,6 +119,11 @@ export const PERMISSION_DEFINITION = [
|
||||
anyOf: [{ backend: ['system.organization.role.manager_team_role'] }]
|
||||
}
|
||||
},
|
||||
'system.organization.auth.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.login.manager', 'system.settings.login.view'] }]
|
||||
}
|
||||
},
|
||||
'system.api_market.service_classification.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.general.view'] }]
|
||||
@@ -634,6 +639,15 @@ export const PERMISSION_DEFINITION = [
|
||||
]
|
||||
}
|
||||
},
|
||||
'team.consumer.mcp.view': {
|
||||
granted: {
|
||||
anyOf: [
|
||||
{
|
||||
backend: ['team.consumer.mcp.manager', 'team.consumer.mcp.view']
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
'team.application.authorization.add': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.workspace.application.manager_all', 'team.consumer.authorization.manager'] }]
|
||||
|
||||
@@ -207,8 +207,15 @@ const mockData = [
|
||||
name: '角色',
|
||||
key: 'role',
|
||||
path: '/role',
|
||||
icon: 'ic:baseline-verified-user',
|
||||
icon: 'ph:user-circle-gear-fill',
|
||||
access: 'system.organization.role.view'
|
||||
},
|
||||
{
|
||||
name: '鉴权',
|
||||
key: 'auth',
|
||||
path: '/auth',
|
||||
icon: 'ic:baseline-verified-user',
|
||||
access: 'system.organization.auth.view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -60,6 +60,16 @@ const mockData = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'auth',
|
||||
router: [
|
||||
{
|
||||
path: 'auth',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'cluster',
|
||||
|
||||
@@ -461,6 +461,17 @@
|
||||
"待审核": "K35612f29",
|
||||
"已审核": "K47eaafde",
|
||||
"发布申请": "K56b4254f",
|
||||
"鉴权": "Kb35e6a18",
|
||||
"系统用户账号登录授权配置": "K679bd7e4",
|
||||
"授权类型": "K9e7bb257",
|
||||
"请选择授权类型": "Kc499fc1d",
|
||||
"APP ID": "Kee9f8f26",
|
||||
"请输入APP ID": "K9e4c19bb",
|
||||
"APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上": "K45c99f97",
|
||||
"APP Secret": "K90f7c3b4",
|
||||
"请输入APP Secret": "Kdc53d96f",
|
||||
"APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上": "K56e77c4",
|
||||
"启用授权": "K50693bd8",
|
||||
"API 调用地址": "Kea2f9279",
|
||||
"API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。": "K7fc496a1",
|
||||
"OpenAPI & MCP 调用地址": "Ka7ca8fde",
|
||||
@@ -553,6 +564,7 @@
|
||||
"请输入密码": "K25c895d5",
|
||||
"密码": "K551b0348",
|
||||
"登录": "Kd2c1a316",
|
||||
"飞书授权登录": "K682b11cb",
|
||||
"访客模式": "K192b3e38",
|
||||
"您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。": "K91aa4801",
|
||||
"Version (0)-(1)": "K480045ce",
|
||||
@@ -564,6 +576,8 @@
|
||||
"AI 代理集成": "Ke6908f16",
|
||||
"请先订阅该服务": "K71ed51fa",
|
||||
"申请": "K4aa9ed2c",
|
||||
"未配置 API Key": "Kf7b54a1",
|
||||
"配置": "K2a1422d2",
|
||||
"选择 API Key": "K1bec8cbe",
|
||||
"新增 API Key": "Kb0e0aeda",
|
||||
"API 密钥可用于调用系统级 Open API 和 MCP。": "K9d81999c",
|
||||
|
||||
@@ -972,5 +972,19 @@
|
||||
"K6c267c7b": "Avg Requests per Subscriber",
|
||||
"K133d4291": "Avg Traffic per Subscriber",
|
||||
"K37c5f1d0": "Token",
|
||||
"Kb98264d4": "Avg Token per Subscriber"
|
||||
"Kb98264d4": "Avg Token per Subscriber",
|
||||
"Kb35e6a18": "Auth",
|
||||
"K679bd7e4": "System User Account Login Authorization Settings",
|
||||
"K9e7bb257": "Authorization Type",
|
||||
"Kc499fc1d": "Please select an authorization type",
|
||||
"Kee9f8f26": "APP ID",
|
||||
"K9e4c19bb": "Please enter APP ID",
|
||||
"K90f7c3b4": "APP Secret",
|
||||
"Kdc53d96f": "Please enter APP Secret",
|
||||
"K50693bd8": "Enable Authorization",
|
||||
"K682b11cb": "Feishu Authorization Login",
|
||||
"K45c99f97": "The APP ID parameter can be found on the App Credentials and Basic Information page in the Feishu Developer Console",
|
||||
"K56e77c4": "The APP Secret parameter can be found on the App Credentials and Basic Information page in the Feishu Developer Console",
|
||||
"Kf7b54a1": "API Key not configured",
|
||||
"K2a1422d2": "Configure"
|
||||
}
|
||||
|
||||
@@ -994,5 +994,19 @@
|
||||
"K6c267c7b": "消費者あたりの平均リクエスト数",
|
||||
"K133d4291": "消費者あたりの平均ネットワークトラフィック",
|
||||
"K37c5f1d0": "トークン消費量",
|
||||
"Kb98264d4": "消費者あたりの平均トークン消費量"
|
||||
"Kb98264d4": "消費者あたりの平均トークン消費量",
|
||||
"Kb35e6a18": "認証",
|
||||
"K679bd7e4": "システムユーザーアカウントログイン認可設定",
|
||||
"K9e7bb257": "認可タイプ",
|
||||
"Kc499fc1d": "認可タイプを選択してください",
|
||||
"Kee9f8f26": "APP ID",
|
||||
"K9e4c19bb": "APP ID を入力してください",
|
||||
"K90f7c3b4": "APP Secret",
|
||||
"Kdc53d96f": "APP Secret を入力してください",
|
||||
"K50693bd8": "認可を有効化",
|
||||
"K682b11cb": "Feishu 認証ログイン",
|
||||
"K45c99f97": "APP ID パラメータは Feishu 開発者コンソールのアプリ認証情報と基本情報ページにあります",
|
||||
"K56e77c4": "APP Secret パラメータは Feishu 開発者コンソールのアプリ認証情報と基本情報ページにあります",
|
||||
"Kf7b54a1": "API Key が設定されていません",
|
||||
"K2a1422d2": "設定"
|
||||
}
|
||||
|
||||
@@ -923,5 +923,19 @@
|
||||
"K6c267c7b": "平均每消费者的请求次数",
|
||||
"K133d4291": "平均每消费者的网络流量",
|
||||
"K37c5f1d0": "Token 消耗",
|
||||
"Kb98264d4": "平均每消费者的 Token 消耗"
|
||||
"Kb98264d4": "平均每消费者的 Token 消耗",
|
||||
"Kb35e6a18": "鉴权",
|
||||
"K679bd7e4": "系统用户账号登录授权配置",
|
||||
"K9e7bb257": "授权类型",
|
||||
"Kc499fc1d": "请选择授权类型",
|
||||
"Kee9f8f26": "APP ID",
|
||||
"K9e4c19bb": "请输入APP ID",
|
||||
"K90f7c3b4": "APP Secret",
|
||||
"Kdc53d96f": "请输入APP Secret",
|
||||
"K50693bd8": "启用授权",
|
||||
"K682b11cb": "飞书授权登录",
|
||||
"K45c99f97": "APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上",
|
||||
"K56e77c4": "APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上",
|
||||
"Kf7b54a1": "未配置 API Key",
|
||||
"K2a1422d2": "配置"
|
||||
}
|
||||
|
||||
@@ -994,5 +994,19 @@
|
||||
"K6c267c7b": "平均每位使用者的請求次數",
|
||||
"K133d4291": "平均每位使用者的網路流量",
|
||||
"K37c5f1d0": "Token 消耗",
|
||||
"Kb98264d4": "平均每位使用者的 Token 消耗"
|
||||
"Kb98264d4": "平均每位使用者的 Token 消耗",
|
||||
"Kb35e6a18": "鑑權",
|
||||
"K679bd7e4": "系統用戶帳號登入授權配置",
|
||||
"K9e7bb257": "授權類型",
|
||||
"Kc499fc1d": "請選擇授權類型",
|
||||
"Kee9f8f26": "APP ID",
|
||||
"K9e4c19bb": "請輸入 APP ID",
|
||||
"K90f7c3b4": "APP Secret",
|
||||
"Kdc53d96f": "請輸入 APP Secret",
|
||||
"K50693bd8": "啟用授權",
|
||||
"K682b11cb": "飛書授權登入",
|
||||
"K45c99f97": "APP ID 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面",
|
||||
"K56e77c4": "APP Secret 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面",
|
||||
"Kf7b54a1": "未配置 API Key",
|
||||
"K2a1422d2": "配置"
|
||||
}
|
||||
|
||||
@@ -618,6 +618,16 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx'
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'mcp',
|
||||
key: 'consumerMcp',
|
||||
lazy: lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/mcpContent.tsx'
|
||||
)
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -693,6 +703,14 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'auth',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/auth/Auth.tsx')),
|
||||
key: 'auth'
|
||||
}
|
||||
],
|
||||
[
|
||||
'analytics',
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ export type MemberTableListItem = {
|
||||
enable:boolean
|
||||
departmentId:string
|
||||
roles:EntityItem[]
|
||||
from: string
|
||||
};
|
||||
|
||||
export type AddToDepartmentProps = {
|
||||
@@ -40,7 +41,7 @@ export type MemberDropdownModalFieldType = {
|
||||
|
||||
export type MemberDropdownModalProps = {
|
||||
type:'addDep'|'addChild'|'addMember'|'editMember'|'rename'
|
||||
entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string})
|
||||
entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string,from?:string})
|
||||
selectedMemberGroupId?:string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,221 +1,405 @@
|
||||
import {FC, useCallback, useEffect, useRef, useState} from "react";
|
||||
import {App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip} from "antd";
|
||||
import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import {BasicResponse, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip } from 'antd'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { BasicResponse, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
// import {useCrypto} from "../hooks/crypto.ts";
|
||||
import Logo from '@common/assets/layout-logo.png'
|
||||
import { $t } from "@common/locales";
|
||||
import { Icon } from "@iconify/react/dist/iconify.js";
|
||||
import LanguageSetting from "@common/components/aoplatform/LanguageSetting";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import FeishuLogo from '@common/assets/feishu.png'
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
|
||||
const Login:FC = ()=> {
|
||||
const {state, dispatch} = useGlobalContext()
|
||||
const {fetchData} = useFetch()
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate();
|
||||
const formRef = useRef<FormInstance>(null);
|
||||
const [loading,setLoading] = useState<boolean>()
|
||||
const [allowGuest, setAllowGuest] = useState<boolean>(false)
|
||||
const [spinning,setSpinning] = useState<boolean>(false)
|
||||
const Login: FC = () => {
|
||||
const { state, dispatch } = useGlobalContext()
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const formRef = useRef<FormInstance>(null)
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const [allowGuest, setAllowGuest] = useState<boolean>(false)
|
||||
const [spinning, setSpinning] = useState<boolean>(false)
|
||||
// 是否允许飞书登录
|
||||
const [allowFeishuLogin, setAllowFeishuLogin] = useState<boolean>(false)
|
||||
// 飞书登录app_id
|
||||
const [feishuAppId, setFeishuAppId] = useState<string>()
|
||||
// 获取 url 参数
|
||||
const query = new URLSearchParams(useLocation().search)
|
||||
// 是否是飞书登录
|
||||
const [isFeishuLogin, setIsFeishuLogin] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isFeishuLogin) {
|
||||
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
navigate(callbackUrl)
|
||||
} else {
|
||||
navigate(state.mainPage)
|
||||
}
|
||||
setIsFeishuLogin(false)
|
||||
}
|
||||
}, [isFeishuLogin])
|
||||
/**
|
||||
* 飞书登录
|
||||
* @param feishuCode 飞书 code
|
||||
*/
|
||||
const feishuLogin = async (feishuCode: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const feishuCallbackUrl = localStorage.getItem('feishuCallbackUrl')
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/feishu', {
|
||||
method: 'POST',
|
||||
eoBody: {
|
||||
code: feishuCode,
|
||||
redirect_uri: feishuCallbackUrl
|
||||
}
|
||||
})
|
||||
|
||||
const check = useCallback(()=>{
|
||||
state.isAuthenticated &&setSpinning(true)
|
||||
fetchData<BasicResponse<{channel:Array<{name:string}>, status:string}>>('account/login',{method:'GET'}).then(response=>{
|
||||
const {code,data} = response
|
||||
if(code === STATUS_CODE.SUCCESS && data.status !== 'anonymous'){
|
||||
dispatch({type:'LOGIN'})
|
||||
navigate(state.mainPage,{replace:true})
|
||||
}else{
|
||||
dispatch({type:'LOGOUT'})
|
||||
setAllowGuest(data.channel.filter(x=>x.name === 'guest_access').length > 0)
|
||||
setSpinning(false)
|
||||
}
|
||||
})
|
||||
},[])
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
setIsFeishuLogin(true)
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
setIsFeishuLogin(false)
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const check = useCallback(() => {
|
||||
state.isAuthenticated && setSpinning(true)
|
||||
fetchData<BasicResponse<{ channel: Array<{ name: string; config: { [key: string]: any } }>; status: string }>>(
|
||||
'account/login',
|
||||
{ method: 'GET' }
|
||||
).then((response) => {
|
||||
const { code, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS && data.status !== 'anonymous') {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
navigate(state.mainPage, { replace: true })
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
setAllowGuest(data.channel.filter((x: any) => x.name === 'guest_access').length > 0)
|
||||
const feishu = data.channel.find((x: any) => x.name === 'feishu')
|
||||
if (feishu) {
|
||||
setFeishuAppId(feishu.config.client_id)
|
||||
setAllowFeishuLogin(true)
|
||||
}
|
||||
const code = query.get('code')
|
||||
if (code) {
|
||||
feishuLogin(code)
|
||||
setSpinning(false)
|
||||
return
|
||||
}
|
||||
if (isInFeishuClient() && feishu) {
|
||||
openFeishuLogin(feishu.config.client_id)
|
||||
}
|
||||
setSpinning(false)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getSystemInfo = useCallback(() => {
|
||||
fetchData<BasicResponse<{ version: string; buildTime: string }>>('common/version', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['build_time']
|
||||
}).then((response) => {
|
||||
const { code, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'UPDATE_VERSION', version: data.version })
|
||||
dispatch({ type: 'UPDATE_DATE', updateDate: data.buildTime })
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const fetchLogin = async (values: any) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { username, password } = values
|
||||
// const encryptedPassword = encryptByEnAES(username, password);
|
||||
|
||||
const body = {
|
||||
name: username,
|
||||
password: password
|
||||
// client: 1,
|
||||
// type: 1,
|
||||
// app_type: 4,
|
||||
}
|
||||
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/username', {
|
||||
method: 'POST',
|
||||
eoBody: body
|
||||
})
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
// message.success($t(RESPONSE_TIPS.loginSuccess));
|
||||
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
navigate(callbackUrl)
|
||||
} else {
|
||||
navigate(state.mainPage)
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const login = async () => {
|
||||
if (formRef.current) {
|
||||
const values = await formRef.current.validateFields()
|
||||
fetchLogin(values)
|
||||
}
|
||||
}
|
||||
|
||||
const loginAsGuest = () => {
|
||||
fetchLogin({ username: 'guest', password: '12345678' })
|
||||
}
|
||||
|
||||
const isInFeishuClient = () => {
|
||||
// 方法1:检查User-Agent
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isLark = ua.includes('lark') || ua.includes('feishu');
|
||||
|
||||
const getSystemInfo = useCallback(()=>{
|
||||
fetchData<BasicResponse<{version:string, buildTime:string}>>('common/version',{method:'GET', eoTransformKeys:['build_time']}).then(response=>{
|
||||
const {code,data} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
dispatch({type:'UPDATE_VERSION',version:data.version})
|
||||
dispatch({type:'UPDATE_DATE',updateDate:data.buildTime})
|
||||
}
|
||||
})
|
||||
},[])
|
||||
// 方法2:检查全局对象
|
||||
const hasSDK = typeof window.h5sdk !== 'undefined' || typeof window.tt !== 'undefined';
|
||||
|
||||
// 方法3:检查URL参数
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hasFeishuParams = params.has('from') || params.has('required_launch_ability');
|
||||
|
||||
return isLark || hasSDK || hasFeishuParams;
|
||||
}
|
||||
|
||||
// 打开飞书授权页面
|
||||
const openFeishuLogin = (id?: string) => {
|
||||
const href = window.location.origin + window.location.pathname
|
||||
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${id || feishuAppId}&redirect_uri=${href}`
|
||||
localStorage.setItem('feishuCallbackUrl', href)
|
||||
window.location.href = authUrl
|
||||
}
|
||||
|
||||
const fetchLogin = async (values:any)=>{
|
||||
try {
|
||||
setLoading(true);
|
||||
const { username, password } = values;
|
||||
// const encryptedPassword = encryptByEnAES(username, password);
|
||||
useEffect(() => {
|
||||
check()
|
||||
getSystemInfo()
|
||||
}, [])
|
||||
|
||||
const body = {
|
||||
name:username,
|
||||
password: password
|
||||
// client: 1,
|
||||
// type: 1,
|
||||
// app_type: 4,
|
||||
};
|
||||
return spinning ? (
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={spinning}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
></Spin>
|
||||
) : (
|
||||
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
|
||||
<div id="glow-background" className="background-container">
|
||||
<svg className="background-pattern" aria-hidden="true">
|
||||
<defs>
|
||||
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
|
||||
<path d="M.5 200V.5H200" fill="none"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
|
||||
</svg>
|
||||
|
||||
const {code,msg } = await fetchData<BasicResponse<null>>('account/login/username',{method:'POST',eoBody:(body)})
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:svgjs="http://svgjs.dev/svgjs"
|
||||
viewBox="0 0 800 450"
|
||||
opacity="1"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
id="bbblurry-filter"
|
||||
x="-100%"
|
||||
y="-100%"
|
||||
width="400%"
|
||||
height="400%"
|
||||
filterUnits="objectBoundingBox"
|
||||
primitiveUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feGaussianBlur
|
||||
stdDeviation="99"
|
||||
x="0%"
|
||||
y="0%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
in="SourceGraphic"
|
||||
edgeMode="none"
|
||||
result="blur"
|
||||
></feGaussianBlur>
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter="url(#bbblurry-filter)">
|
||||
<ellipse
|
||||
rx="80.5"
|
||||
ry="66.5"
|
||||
cx="623.0285107902043"
|
||||
cy="25.708028895006635"
|
||||
fill="hsla(187, 67%, 50%, 1.00)"
|
||||
>
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)"
|
||||
dur="6s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({type:'LOGIN'})
|
||||
// message.success($t(RESPONSE_TIPS.loginSuccess));
|
||||
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl');
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
navigate(callbackUrl);
|
||||
} else {
|
||||
navigate(state.mainPage);
|
||||
}
|
||||
}else{
|
||||
dispatch({type:'LOGOUT'})
|
||||
message.error(msg)
|
||||
}
|
||||
<ellipse
|
||||
rx="80.5"
|
||||
ry="66.5"
|
||||
cx="446.471435546875"
|
||||
cy="-11.694503784179688"
|
||||
fill="hsla(234, 78%, 61%, 1.00)"
|
||||
>
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)"
|
||||
dur="8s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const login = async () => {
|
||||
if (formRef.current) {
|
||||
const values = await formRef.current.validateFields();
|
||||
fetchLogin(values);
|
||||
}
|
||||
};
|
||||
<ellipse
|
||||
rx="80.5"
|
||||
ry="66.5"
|
||||
cx="200.54574247724838"
|
||||
cy="-19.02454901710908"
|
||||
fill="hsla(167, 87%, 56%, 1.00)"
|
||||
>
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
|
||||
const loginAsGuest = ()=>{
|
||||
fetchLogin({username:'guest',password:'12345678'})
|
||||
}
|
||||
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)"
|
||||
dur="8s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
|
||||
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]">
|
||||
<div className="mx-auto">
|
||||
<span className="flex items-center justify-center">
|
||||
<img className="h-[40px] mr-[8px]" src={Logo} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
useEffect(() => {
|
||||
check()
|
||||
getSystemInfo()
|
||||
}, []);
|
||||
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
|
||||
<div className="h-full">
|
||||
<div className="">
|
||||
<Form onFinish={login} className="w-[350px]" ref={formRef}>
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none"
|
||||
name="username"
|
||||
rules={[{ required: true, message: $t('请输入账号'), whitespace: true }]}
|
||||
>
|
||||
<Input
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t('账号')}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
return (
|
||||
spinning?
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={spinning} className='w-full h-full flex items-center justify-center'></Spin> :
|
||||
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
|
||||
<div id="glow-background" className="background-container">
|
||||
<svg className="background-pattern" aria-hidden="true">
|
||||
<defs>
|
||||
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
|
||||
<path d="M.5 200V.5H200" fill="none"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
|
||||
</svg>
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none "
|
||||
name="password"
|
||||
rules={[{ required: true, message: $t('请输入密码') }]}
|
||||
>
|
||||
<Input.Password
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t('密码')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 800 450" opacity="1">
|
||||
<defs>
|
||||
<filter id="bbblurry-filter" x="-100%" y="-100%" width="400%" height="400%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="99" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"></feGaussianBlur>
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter="url(#bbblurry-filter)">
|
||||
<ellipse rx="80.5" ry="66.5" cx="623.0285107902043" cy="25.708028895006635" fill="hsla(187, 67%, 50%, 1.00)">
|
||||
<animate attributeName="fill" values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)" dur="6s" repeatCount="indefinite"></animate>
|
||||
</ellipse>
|
||||
|
||||
<ellipse rx="80.5" ry="66.5" cx="446.471435546875" cy="-11.694503784179688" fill="hsla(234, 78%, 61%, 1.00)">
|
||||
<animate attributeName="fill" values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)" dur="8s" repeatCount="indefinite"></animate>
|
||||
</ellipse>
|
||||
|
||||
<ellipse rx="80.5" ry="66.5" cx="200.54574247724838" cy="-19.02454901710908" fill="hsla(167, 87%, 56%, 1.00)">
|
||||
<animate attributeName="fill" values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)" dur="10s" repeatCount="indefinite"></animate>
|
||||
</ellipse>
|
||||
|
||||
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
|
||||
<animate attributeName="fill" values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)" dur="8s" repeatCount="indefinite"></animate>
|
||||
</ellipse>
|
||||
</g>
|
||||
</svg>
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none ">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] mt-mbase w-full inline-flex justify-center items-center"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
{$t('登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
{allowFeishuLogin && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] w-full inline-flex justify-center items-center"
|
||||
type="default"
|
||||
onClick={() => openFeishuLogin(feishuAppId)}
|
||||
>
|
||||
<img className="h-[30px]" src={FeishuLogo} />
|
||||
{$t('飞书授权登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{allowGuest && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] w-full inline-flex justify-center items-center"
|
||||
type="default"
|
||||
onClick={loginAsGuest}
|
||||
>
|
||||
{$t('访客模式')}{' '}
|
||||
<Tooltip
|
||||
title={$t(
|
||||
'您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。'
|
||||
)}
|
||||
>
|
||||
<Icon icon="ic:baseline-help" height={18} width={18} />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
|
||||
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]" >
|
||||
<div className="mx-auto">
|
||||
<span className="flex items-center justify-center">
|
||||
<img
|
||||
className="h-[40px] mr-[8px]"
|
||||
src={Logo}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
|
||||
<div className="h-full">
|
||||
<div className="">
|
||||
<Form onFinish={login} className="w-[350px]"
|
||||
ref={formRef}>
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none"
|
||||
name="username"
|
||||
rules={[{ required: true, message: $t('请输入账号') ,whitespace:true }]}
|
||||
>
|
||||
<Input
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t("账号")}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none "
|
||||
name="password"
|
||||
rules={[{ required: true, message: $t('请输入密码') }]}
|
||||
>
|
||||
<Input.Password
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t("密码")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none "
|
||||
>
|
||||
<Button loading={loading} className="h-[40px] mt-mbase w-full inline-flex justify-center items-center" type="primary" htmlType="submit">
|
||||
{$t('登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
{
|
||||
allowGuest && <>
|
||||
<Divider />
|
||||
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none mb-0"
|
||||
>
|
||||
<Button loading={loading} className="h-[40px] w-full inline-flex justify-center items-center" type="default" onClick={loginAsGuest}>
|
||||
{$t('访客模式')} <Tooltip title={$t('您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。')}><Icon icon="ic:baseline-help" height={18} width={18} /></Tooltip>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
|
||||
<p className="leading-[28px]">
|
||||
{$t('Version (0)-(1)',[state?.version,state?.updateDate])}, {$t(state?.powered || '-')}
|
||||
</p>
|
||||
<LanguageSetting mode="light"/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
|
||||
<p className="leading-[28px]">
|
||||
{$t('Version (0)-(1)', [state?.version, state?.updateDate])}, {$t(state?.powered || '-')}
|
||||
</p>
|
||||
<LanguageSetting mode="light" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Login;
|
||||
export default Login
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { App, Button, Form, Input, Row, Select, Switch } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
type AuthSetting = {
|
||||
config: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
type AuthFieldType = {
|
||||
authType: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const Auth = () => {
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const [thirdPartyDrivers, setThirdPartyDrivers] = useState<{ label: string; value: string }[]>([])
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
const onFinish = () => {
|
||||
form.validateFields().then((value) => {
|
||||
return fetchData<BasicResponse<null>>(`account/third/${value.authType}`, {
|
||||
method: 'POST',
|
||||
eoBody: {
|
||||
enable: value.enabled,
|
||||
config: {
|
||||
client_id: value.clientId,
|
||||
client_secret: value.clientSecret
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
return Promise.reject(errorInfo)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第三方授权列表
|
||||
*/
|
||||
const getThirdPartyAuthList = () => {
|
||||
fetchData<
|
||||
BasicResponse<{
|
||||
drivers: {
|
||||
name: string
|
||||
value: string
|
||||
}[]
|
||||
}>
|
||||
>('account/third', {
|
||||
method: 'GET',
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setThirdPartyDrivers(data.drivers.map((item: any) => ({ label: item.name, value: item.value })))
|
||||
if (data.drivers.length) {
|
||||
form.setFieldValue('authType', data.drivers[0].value)
|
||||
getThirdPartyAuthSetting()
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第三方授权配置
|
||||
*/
|
||||
const getThirdPartyAuthSetting = () => {
|
||||
fetchData<BasicResponse<{ info: AuthSetting }>>(`account/third/${form.getFieldValue('authType')}`, {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['client_id', 'client_secret']
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
form.setFieldsValue({
|
||||
clientId: data.driver?.config?.clientId || '',
|
||||
clientSecret: data.driver?.config?.clientSecret || '',
|
||||
enabled: data.driver?.enable || false
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getThirdPartyAuthList()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<InsidePage pageTitle={$t('鉴权')} showBorder={false} contentClassName="pr-PAGE_INSIDE_X" scrollPage={false} description={$t("系统用户账号登录授权配置")}>
|
||||
<WithPermission access="">
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className={`mx-auto`}
|
||||
name="authConfig"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<AuthFieldType>
|
||||
label={$t('授权类型')}
|
||||
name="authType"
|
||||
rules={[{ required: true, message: $t('请选择授权类型') }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t('请选择授权类型')} onChange={getThirdPartyAuthSetting} options={thirdPartyDrivers} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AuthFieldType>
|
||||
label={$t('APP ID')}
|
||||
name="clientId"
|
||||
rules={[{ required: true, whitespace: true, message: $t('请输入APP ID') }]}
|
||||
extra={$t('APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} autoComplete="off" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AuthFieldType>
|
||||
label={$t('APP Secret')}
|
||||
name="clientSecret"
|
||||
rules={[{ required: true, whitespace: true, message: $t('请输入APP Secret') }]}
|
||||
extra={$t('APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')}
|
||||
>
|
||||
<Input.Password className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AuthFieldType> label={$t('启用授权')} name="enabled" valuePropName="checked">
|
||||
<Switch checkedChildren={$t('启用')} unCheckedChildren={$t('停用')} />
|
||||
</Form.Item>
|
||||
|
||||
<Row className="mb-[10px]">
|
||||
<WithPermission access="system.devops.system_setting.edit">
|
||||
<Button type="primary" htmlType="submit">
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</Row>
|
||||
</Form>
|
||||
</WithPermission>
|
||||
</InsidePage>
|
||||
)
|
||||
}
|
||||
|
||||
export default Auth
|
||||
@@ -50,17 +50,23 @@ type ServiceApiKeyList = {
|
||||
expired: number
|
||||
}>
|
||||
}
|
||||
|
||||
type ConsumerParamsType = {
|
||||
consumerId: string
|
||||
teamId: string
|
||||
}
|
||||
export interface IntegrationAIContainerRef {
|
||||
getServiceKeysList: () => void;
|
||||
}
|
||||
export interface IntegrationAIContainerProps {
|
||||
type: 'global' | 'service'
|
||||
type: 'global' | 'service' | 'consumer'
|
||||
handleToolsChange: (value: Tool[]) => void
|
||||
customClassName?: string
|
||||
service?: ServiceDetailType
|
||||
serviceId?: string
|
||||
currentTab?: string
|
||||
openModal?: (type: 'apply') => void
|
||||
consumerParams?: ConsumerParamsType
|
||||
}
|
||||
export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, IntegrationAIContainerProps>(
|
||||
({
|
||||
@@ -69,8 +75,9 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
customClassName,
|
||||
service,
|
||||
serviceId,
|
||||
currentTab,
|
||||
openModal
|
||||
currentTab,
|
||||
openModal,
|
||||
consumerParams
|
||||
}: IntegrationAIContainerProps, ref) => {
|
||||
/** 当前激活的标签 */
|
||||
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
|
||||
@@ -180,7 +187,35 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消费者 MCP 配置
|
||||
* @returns
|
||||
*/
|
||||
const getConsumerMcpConfig = () => {
|
||||
fetchData<BasicResponse<null>>('app/mcp/config', {
|
||||
method: 'GET',
|
||||
eoParams: { app: consumerParams?.consumerId, team: consumerParams?.teamId }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTabContent((prevTabContent) => ({
|
||||
...prevTabContent,
|
||||
mcp: {
|
||||
...prevTabContent.mcp,
|
||||
configContent: data.config || ''
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -191,6 +226,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
navigator('/mcpKey')
|
||||
}
|
||||
|
||||
const dropAuthPage = () => {
|
||||
navigator(`/consumer/${consumerParams?.teamId}/inside/${consumerParams?.consumerId}/authorization`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 API Key 列表
|
||||
*/
|
||||
@@ -217,7 +256,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -229,12 +268,12 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
}))
|
||||
|
||||
/**
|
||||
* 获取服务 API Key 列表
|
||||
* 获取 API Key 列表
|
||||
*/
|
||||
const getServiceKeysList = () => {
|
||||
const getServiceKeysList = (consumerId?: string) => {
|
||||
fetchData<BasicResponse<null>>(`my/app/apikeys`, {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId }
|
||||
eoParams: consumerId ? { app: consumerId } : { service: serviceId }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
@@ -258,7 +297,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -345,6 +384,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
getGlobalMcpConfig()
|
||||
setMcpServerUrl('mcp/global/sse')
|
||||
getGlobalKeysList()
|
||||
} else if (type === 'consumer'){
|
||||
getConsumerMcpConfig()
|
||||
setMcpServerUrl(`mcp/app/${consumerParams?.consumerId}/sse`)
|
||||
getServiceKeysList(consumerParams?.consumerId)
|
||||
} else {
|
||||
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
|
||||
getServiceKeysList()
|
||||
@@ -362,6 +405,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
useEffect(() => {
|
||||
initTabsData()
|
||||
type === 'global' && getGlobalMcpConfig()
|
||||
type === 'consumer' && getConsumerMcpConfig()
|
||||
}, [state.language])
|
||||
/**
|
||||
* 切换标签
|
||||
@@ -408,7 +452,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
}
|
||||
connectMcpServer()
|
||||
}
|
||||
}, [mcpServerUrl, ...(type === 'global' ? [state.language] : [])])
|
||||
}, [mcpServerUrl, ...(type === 'global' || type === 'consumer' ? [state.language] : [])])
|
||||
/**
|
||||
* 获取 MCP tools
|
||||
*/
|
||||
@@ -452,7 +496,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{type === 'service' && !apiKeyList.length ? (
|
||||
{(type === 'service' || type === 'consumer') && !apiKeyList.length ? (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: '10px' }}
|
||||
@@ -461,12 +505,23 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center py-3">
|
||||
<span className="text-[14px] mb-5">{$t('请先订阅该服务')}</span>
|
||||
<Button type="primary" onClick={() => openModal?.('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
type === 'service' ? (
|
||||
<div className="flex flex-col items-center justify-center py-3">
|
||||
<span className="text-[14px] mb-5">{$t('请先订阅该服务')}</span>
|
||||
<Button type="primary" onClick={() => openModal?.('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-3">
|
||||
<span className="text-[14px] mb-5">{$t('未配置 API Key')}</span>
|
||||
<Button type="primary" onClick={() => dropAuthPage()}>
|
||||
{$t('配置')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -14,7 +14,7 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
|
||||
const {fetchData} = useFetch()
|
||||
const [departmentList, setDepartmentList] = useState<DepartmentListItem[]>([])
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const [disableEditMemberData] = useState<boolean>(entity?.from === 'feishu')
|
||||
const save:()=>Promise<boolean | string> = ()=>{
|
||||
let url:string
|
||||
let method:string
|
||||
@@ -182,27 +182,28 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
|
||||
name="name"
|
||||
rules={[{required: true,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" disabled={type ==='editMember'} placeholder={$t(PLACEHOLDER.input)}/>
|
||||
<Input className="w-INPUT_NORMAL" disabled={disableEditMemberData || type ==='editMember'} placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item<MemberDropdownModalFieldType>
|
||||
label={$t("邮箱")}
|
||||
name="email"
|
||||
rules={[{required: true,whitespace:true },{type:"email",message: $t(VALIDATE_MESSAGE.email)}]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
<Input className="w-INPUT_NORMAL" disabled={disableEditMemberData} placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item<MemberDropdownModalFieldType>
|
||||
label={$t("密码")}
|
||||
name="password"
|
||||
rules={[{required: type === 'addMember',whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
<Input disabled={disableEditMemberData} className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item<MemberDropdownModalFieldType>
|
||||
label={$t("部门")}
|
||||
name="departmentIds"
|
||||
>
|
||||
<TreeSelect
|
||||
disabled={disableEditMemberData}
|
||||
className="w-INPUT_NORMAL"
|
||||
fieldNames={{label:'name',value:'id',children:'children'}}
|
||||
showSearch
|
||||
|
||||
@@ -95,14 +95,15 @@ const AddToDepartment = forwardRef<AddToDepartmentHandle, AddToDepartmentProps>(
|
||||
treeData?.map((x: DataNode) => ({
|
||||
...x,
|
||||
name: $t((x as unknown as { name: string }).name),
|
||||
checkable: false,
|
||||
children: x.children?.map(y => ({ ...y, checkable: false }))
|
||||
checkable: false, // 根节点不可选中
|
||||
children: x.children?.map(y => ({ ...y, checkable: true })) // 子节点可以选中
|
||||
})),
|
||||
[state.language, treeData]
|
||||
)
|
||||
|
||||
const onCheck: TreeProps['onCheck'] = (checkedKeys: string[]) => {
|
||||
setSelectedKeys(checkedKeys.checked)
|
||||
const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
|
||||
const selectedIds = Array.isArray(checkedKeys) ? checkedKeys : checkedKeys.checked || []
|
||||
setSelectedKeys(selectedIds)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -153,11 +154,12 @@ const MemberList = () => {
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true)
|
||||
const [tableListDataSource, setTableListDataSource] = useState<MemberTableListItem[]>([])
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { topGroupId, selectedDepartmentIds, refreshGroup } = useOutletContext<{
|
||||
const { topGroupId, selectedDepartmentIds, refreshGroup, refreshTableCount } = useOutletContext<{
|
||||
topGroupId: string
|
||||
departmentList: DepartmentListItem[]
|
||||
selectedDepartmentIds: string[]
|
||||
refreshGroup: () => void
|
||||
refreshTableCount: number
|
||||
}>()
|
||||
const AddMemberRef = useRef<MemberDropdownModalHandle>(null)
|
||||
const EditMemberRef = useRef<MemberDropdownModalHandle>(null)
|
||||
@@ -396,7 +398,7 @@ const MemberList = () => {
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
okButtonProps: {
|
||||
disabled: isActionAllowed(type)
|
||||
disabled: isActionAllowed(type) || (type === 'editMember' && entity?.from === 'feishu')
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
@@ -415,6 +417,13 @@ const MemberList = () => {
|
||||
getDepartmentList()
|
||||
}, [])
|
||||
|
||||
// 监听外部刷新触发器
|
||||
useEffect(() => {
|
||||
if (refreshTableCount > 0) {
|
||||
manualReloadTable()
|
||||
}
|
||||
}, [refreshTableCount])
|
||||
|
||||
const getDepartmentList = async () => {
|
||||
setDepartmentValueEnum([])
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ department: DepartmentListItem }>>(
|
||||
|
||||
@@ -36,6 +36,11 @@ const MemberPage = ()=>{
|
||||
const [selectedDepartmentId, setSelectedDepartmentId] = useState<string>('-1')
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const [refreshMemberCount, setRefreshMemberCount] = useState<number>(0)
|
||||
const [refreshTableCount, setRefreshTableCount] = useState<number>(0)
|
||||
|
||||
const refreshMemberTable = () => {
|
||||
setRefreshTableCount(prev => prev + 1)
|
||||
}
|
||||
const onSearchWordChange = (e:string)=>{
|
||||
setSearchWord(e || '')
|
||||
}
|
||||
@@ -90,7 +95,7 @@ const MemberPage = ()=>{
|
||||
case 'addChild':
|
||||
return AddChildRef.current?.save().then((res)=>{if(res === true)getDepartmentList()})
|
||||
case 'addMember':
|
||||
return AddMemberRef.current?.save().then((res)=>{if(res === true){getDepartmentList();setRefreshMemberCount(pre=>pre+1)}})
|
||||
return AddMemberRef.current?.save().then((res)=>{if(res === true){getDepartmentList();setRefreshMemberCount(pre=>pre+1);refreshMemberTable()}})
|
||||
case 'rename':
|
||||
return RenameRef.current?.save().then((res)=>{if(res === true)getDepartmentList()})
|
||||
case 'delete':
|
||||
@@ -262,7 +267,7 @@ const MemberPage = ()=>{
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 p-btnbase pr-PAGE_INSIDE_X overflow-x-hidden">
|
||||
<Outlet context={{refreshMemberCount, selectedDepartmentIds,refreshGroup:()=>getDepartmentList()}}/>
|
||||
<Outlet context={{refreshMemberCount, selectedDepartmentIds,refreshGroup:()=>getDepartmentList(), refreshTableCount}}/>
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>);
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function ManagementInsidePage() {
|
||||
const { message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const [activeMenu, setActiveMenu] = useState<string>('service')
|
||||
const [activeMenu, setActiveMenu] = useState<string>('authorization')
|
||||
const { appId, teamId } = useParams<RouterParams>()
|
||||
const navigateTo = useNavigate()
|
||||
const currentUrl = useLocation().pathname
|
||||
@@ -31,8 +31,9 @@ export default function ManagementInsidePage() {
|
||||
|
||||
const TENANT_MANAGEMENT_APP_MENU: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
|
||||
getItem($t('访问授权'), 'authorization', undefined, undefined, undefined, 'team.consumer.authorization.view'),
|
||||
getItem($t('MCP 服务'), 'mcp', undefined, undefined, undefined, 'team.consumer.mcp.view'),
|
||||
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
|
||||
getItem($t('消费者管理'), 'setting', undefined, undefined, undefined, 'team.application.application.view')
|
||||
],
|
||||
[state.language]
|
||||
@@ -55,7 +56,7 @@ export default function ManagementInsidePage() {
|
||||
}, [accessData, accessInit, TENANT_MANAGEMENT_APP_MENU])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(currentUrl.split('/').pop() || 'service')
|
||||
setActiveMenu(currentUrl.split('/').pop() || 'authorization')
|
||||
}, [currentUrl])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = (node) => {
|
||||
|
||||
@@ -336,7 +336,7 @@ export default function ServiceHubManagement() {
|
||||
setTableSearchWord={setTableSearchWord}
|
||||
editApp={(row: ServiceHubAppListItem) => {
|
||||
setAppName(row.name)
|
||||
navigateTo(`/consumer/${row.team.id}/inside/${row.id}/service`)
|
||||
navigateTo(`/consumer/${row.team.id}/inside/${row.id}/authorization`)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { $t } from '@common/locales'
|
||||
import { IntegrationAIContainer } from '@core/pages/mcpService/IntegrationAIContainer'
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { RouterParams } from '@common/const/type'
|
||||
|
||||
const mcpContent = () => {
|
||||
const [tools, setTools] = useState<Tool[]>([])
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { teamId, appId } = useParams<RouterParams>()
|
||||
const { state } = useGlobalContext()
|
||||
const handleToolsChange = (value: Tool[]) => {
|
||||
setTools(value)
|
||||
}
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
return (
|
||||
<div className=" h-full pt-[32px]">
|
||||
<div className="flex items-center justify-between w-full ml-[10px] text-[18px] leading-[25px] pb-[16px]">
|
||||
<span className="font-bold">{$t('MCP 服务')}</span>
|
||||
</div>
|
||||
<div className="h-[calc(100%-41px)] flex flex-col ">
|
||||
<div className="flex mt-[10px] pr-[40px]">
|
||||
<McpToolsContainer tools={tools} />
|
||||
<IntegrationAIContainer
|
||||
consumerParams={{ consumerId: appId!, teamId: teamId! }}
|
||||
type={'consumer'}
|
||||
handleToolsChange={handleToolsChange}
|
||||
></IntegrationAIContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default mcpContent
|
||||
@@ -5,17 +5,17 @@ go 1.23.4
|
||||
toolchain go1.23.6
|
||||
|
||||
require (
|
||||
github.com/eolinker/ap-account v1.0.15
|
||||
github.com/eolinker/ap-account v1.0.18
|
||||
github.com/eolinker/eosc v0.18.3
|
||||
github.com/eolinker/go-common v1.1.7
|
||||
github.com/gabriel-vasile/mimetype v1.4.4
|
||||
github.com/getkin/kin-openapi v0.127.0
|
||||
github.com/getkin/kin-openapi v0.132.0
|
||||
github.com/gin-contrib/gzip v1.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||
github.com/mark3labs/mcp-go v0.17.0
|
||||
github.com/mark3labs/mcp-go v0.33.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nsqio/go-nsq v1.1.0
|
||||
github.com/ollama/ollama v0.5.8
|
||||
@@ -46,7 +46,6 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@@ -59,10 +58,13 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oapi-codegen/runtime v1.0.0 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/redis/go-redis/v9 v9.5.3 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
|
||||
@@ -28,16 +28,18 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eolinker/ap-account v1.0.15 h1:n6DJeL6RHZ8eLlZUcY2U3H4d/GPaA5oelAx3R0E6yL8=
|
||||
github.com/eolinker/ap-account v1.0.15/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g=
|
||||
github.com/eolinker/ap-account v1.0.18 h1:YgDHoUmAdofPmaGhOQx+vj3+uhsv486kD3KLU/JKmBs=
|
||||
github.com/eolinker/ap-account v1.0.18/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g=
|
||||
github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A=
|
||||
github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk=
|
||||
github.com/eolinker/go-common v1.1.7 h1:bi7wDmlCYQGjS3k8Bz/o+Mo9aMJAzmPsBLXWurxPfwk=
|
||||
github.com/eolinker/go-common v1.1.7/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
|
||||
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
|
||||
@@ -78,8 +80,6 @@ github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjw
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
@@ -101,8 +101,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930=
|
||||
github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
|
||||
github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc=
|
||||
github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -118,6 +118,10 @@ github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
|
||||
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
|
||||
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
|
||||
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/ollama/ollama v0.5.8 h1:b2S6YdZ18/ntCsWzoy/HmB3BHGW4GX0Qp7RARrJtJXU=
|
||||
github.com/ollama/ollama v0.5.8/go.mod h1:ibdmDvb/TjKY1OArBWIazL3pd1DHTk8eG2MMjEkWhiI=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
@@ -134,6 +138,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
import (
|
||||
_ "github.com/APIParkLab/APIPark/frontend"
|
||||
_ "github.com/APIParkLab/APIPark/gateway/apinto"
|
||||
_ "github.com/APIParkLab/APIPark/login_driver/feishu"
|
||||
_ "github.com/APIParkLab/APIPark/plugins/core"
|
||||
_ "github.com/APIParkLab/APIPark/plugins/openapi"
|
||||
_ "github.com/APIParkLab/APIPark/plugins/permit"
|
||||
|
||||
@@ -33,12 +33,12 @@ func TestLoki(t *testing.T) {
|
||||
// headers["Content-Type"] = "application/json"
|
||||
// headers["X-Scope-OrgID"] = "tenant1"
|
||||
// queries := url.Values{}
|
||||
// queries.Set("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`")
|
||||
// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`")
|
||||
// now := time.Now()
|
||||
// start := now.Add(-time.Hour * 24 * 30)
|
||||
// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
|
||||
// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10))
|
||||
// queries.Set("limit", "100")
|
||||
// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10))
|
||||
// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10))
|
||||
// queries.SetMCPServer("limit", "100")
|
||||
// a := time.Now()
|
||||
// result, err := send[LogInfo](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "")
|
||||
// if err != nil {
|
||||
@@ -57,8 +57,8 @@ func TestLoki(t *testing.T) {
|
||||
// headers["Content-Type"] = "application/json"
|
||||
// headers["X-Scope-OrgID"] = "tenant1"
|
||||
// queries := url.Values{}
|
||||
// //queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)")
|
||||
// queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))")
|
||||
// //queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)")
|
||||
// queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))")
|
||||
// result, err := send[LogCount](http.MethodGet, "http://localhost:3100/loki/api/v1/query", headers, queries, "")
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to send request: %v", err)
|
||||
@@ -75,12 +75,12 @@ func TestLoki(t *testing.T) {
|
||||
// headers["Content-Type"] = "application/json"
|
||||
// headers["X-Scope-OrgID"] = "tenant1"
|
||||
// queries := url.Values{}
|
||||
// queries.Set("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"")
|
||||
// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"")
|
||||
// now := time.Now()
|
||||
// start := now.Add(-time.Hour * 24 * 30)
|
||||
// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
|
||||
// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10))
|
||||
// queries.Set("limit", "1")
|
||||
// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10))
|
||||
// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10))
|
||||
// queries.SetMCPServer("limit", "1")
|
||||
// now = time.Now()
|
||||
// result, err := send[map[string]interface{}](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "")
|
||||
// t.LogItem(time.Now().Sub(now))
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package feishu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
client = http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func SendRequest[T any](uri string, method string, header http.Header, query url.Values, body []byte) (*T, error) {
|
||||
if uri == "" {
|
||||
return nil, fmt.Errorf("invalid URL")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, uri, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if query != nil {
|
||||
req.URL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
if header != nil {
|
||||
req.Header = header
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := new(T)
|
||||
err = json.Unmarshal(respBody, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("status code error: %d, response: %s", resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package feishu
|
||||
|
||||
type UserTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
type UserInfoResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data UserInfo `json:"data"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Name string `json:"name"`
|
||||
OpenID string `json:"open_id"`
|
||||
UnionId string `json:"union_id"`
|
||||
Email string `json:"email"`
|
||||
Mobile string `json:"mobile"`
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package feishu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/eolinker/go-common/autowire"
|
||||
|
||||
"github.com/eolinker/ap-account/service/role"
|
||||
"github.com/eolinker/ap-account/service/user"
|
||||
|
||||
"github.com/eolinker/go-common/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/eolinker/ap-account/service/account"
|
||||
|
||||
"github.com/eolinker/ap-account/auth_driver"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "feishu"
|
||||
title = "飞书"
|
||||
getTokenUri = "https://open.feishu.cn/open-apis/authen/v2/oauth/token"
|
||||
getUserInfoUri = "https://open.feishu.cn/open-apis/authen/v1/user_info"
|
||||
)
|
||||
|
||||
var _ auth_driver.IDriver = (*Driver)(nil)
|
||||
|
||||
func init() {
|
||||
d := &Driver{}
|
||||
|
||||
auth_driver.Register(name, d)
|
||||
}
|
||||
|
||||
type Driver struct {
|
||||
isInit bool
|
||||
accountService account.IAccountService `autowired:""`
|
||||
userService user.IUserService `autowired:""`
|
||||
roleService role.IRoleService `autowired:""`
|
||||
roleMemberService role.IRoleMemberService `autowired:""`
|
||||
}
|
||||
|
||||
func (d *Driver) Init() {
|
||||
if d.isInit {
|
||||
return
|
||||
}
|
||||
autowire.Autowired(&d.accountService)
|
||||
autowire.Autowired(&d.userService)
|
||||
autowire.Autowired(&d.roleService)
|
||||
autowire.Autowired(&d.roleMemberService)
|
||||
d.isInit = true
|
||||
}
|
||||
|
||||
func (d *Driver) FilterConfig(config map[string]string) {
|
||||
delete(config, "client_secret")
|
||||
}
|
||||
|
||||
func (d *Driver) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (d *Driver) Title() string {
|
||||
return title
|
||||
}
|
||||
|
||||
func (d *Driver) ThirdLogin(ctx context.Context, args map[string]string) (string, error) {
|
||||
code, ok := args["code"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing code parameter")
|
||||
}
|
||||
clientId, ok := args["client_id"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing client_id parameter")
|
||||
}
|
||||
clientSecret, ok := args["client_secret"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing client_secret parameter")
|
||||
}
|
||||
redirectUri, ok := args["redirect_uri"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing redirect_uri parameter")
|
||||
}
|
||||
u, err := url.Parse(redirectUri)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid redirect_uri parameter")
|
||||
}
|
||||
query := u.Query()
|
||||
query.Del("code")
|
||||
redirectUri = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
|
||||
if len(query) > 0 {
|
||||
redirectUri = fmt.Sprintf("%s?%s", redirectUri, query.Encode())
|
||||
}
|
||||
tokenResp, err := getUserToken(code, redirectUri, clientId, clientSecret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
userInfoResp, err := getUserInfo(tokenResp.TokenType, tokenResp.AccessToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
userId := userInfoResp.Data.UnionId
|
||||
username := userInfoResp.Data.Name
|
||||
email := userInfoResp.Data.Email
|
||||
mobile := userInfoResp.Data.Mobile
|
||||
info, err := d.accountService.GetIdentifier(ctx, name, userId)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", err
|
||||
}
|
||||
uId := uuid.NewString()
|
||||
|
||||
err = d.accountService.Save(ctx, name, uId, userId, utils.Md5(fmt.Sprintf("%s%s", uId, userId)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = d.userService.Create(ctx, uId, username, email, mobile, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := d.roleService.GetDefaultRole(ctx, role.SystemTarget())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = d.roleMemberService.Add(ctx, &role.AddMember{
|
||||
Role: r.Id,
|
||||
User: uId,
|
||||
Target: role.SystemTarget(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return uId, nil
|
||||
}
|
||||
_, err = d.userService.Update(ctx, info.Uid, &username, &email, &mobile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return info.Uid, nil
|
||||
}
|
||||
|
||||
func getUserToken(code string, redirectUri, clientId string, clientSecret string) (*UserTokenResponse, error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("Content-Type", "application/json")
|
||||
//body := url.Values{}
|
||||
//body.Set("grant_type", "authorization_code")
|
||||
//body.Set("code", code)
|
||||
//body.Set("client_id", clientId)
|
||||
//body.Set("client_secret", clientSecret)
|
||||
//body.Set("redirect_uri", redirectUri)
|
||||
body := map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": clientId,
|
||||
"client_secret": clientSecret,
|
||||
"redirect_uri": redirectUri,
|
||||
}
|
||||
bodyByte, _ := json.Marshal(body)
|
||||
resp, err := SendRequest[UserTokenResponse](getTokenUri, http.MethodPost, headers, nil, bodyByte)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user token: %w", err)
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return nil, fmt.Errorf("failed to get user token: %s", resp.ErrorDescription)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func getUserInfo(tokenType string, token string) (*UserInfoResponse, error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("Content-Type", "application/json")
|
||||
switch tokenType {
|
||||
case "Bearer":
|
||||
headers.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
resp, err := SendRequest[UserInfoResponse](getUserInfoUri, http.MethodGet, headers, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return nil, fmt.Errorf("failed to get user info: %s", resp.Msg)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (d *Driver) Delete(ctx context.Context, ids ...string) error {
|
||||
return d.accountService.OnRemoveUsers(ctx, ids...)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var client = http.Client{}
|
||||
|
||||
type Position string
|
||||
|
||||
const (
|
||||
PositionHeader Position = "header"
|
||||
PositionBody Position = "body"
|
||||
PositionQuery Position = "query"
|
||||
PositionPath Position = "path"
|
||||
)
|
||||
|
||||
type ContentType string
|
||||
|
||||
const (
|
||||
ContentTypeJSON ContentType = "application/json"
|
||||
ContentTypeXML ContentType = "application/xml"
|
||||
ContentTypeHTML ContentType = "text/html"
|
||||
ContentTypeText ContentType = "text/plain"
|
||||
ContentTypeForm ContentType = "application/x-www-form-urlencoded"
|
||||
ContentTypeFile ContentType = "multipart/form-data"
|
||||
)
|
||||
|
||||
func NewParam(position Position, required bool, description string) *Param {
|
||||
return &Param{position: position, required: required, description: description}
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
position Position
|
||||
required bool
|
||||
description string
|
||||
}
|
||||
|
||||
func (p *Param) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
func (p *Param) Required() bool {
|
||||
return p.required
|
||||
}
|
||||
|
||||
type BodyParam struct {
|
||||
contentType ContentType
|
||||
params map[string]interface{}
|
||||
}
|
||||
|
||||
func NewBodyParam(contentType string) *BodyParam {
|
||||
t := ContentType(contentType)
|
||||
if t == "" {
|
||||
t = ContentTypeJSON
|
||||
}
|
||||
return &BodyParam{contentType: t}
|
||||
}
|
||||
|
||||
func (p *BodyParam) Set(k string, v interface{}) {
|
||||
if p.params == nil {
|
||||
p.params = make(map[string]interface{})
|
||||
}
|
||||
p.params[k] = v
|
||||
}
|
||||
|
||||
func (p *BodyParam) Encode() (string, error) {
|
||||
switch p.contentType {
|
||||
case ContentTypeJSON:
|
||||
data, err := json.Marshal(p.params)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("body param encode error: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
case ContentTypeForm, ContentTypeFile:
|
||||
data := url.Values{}
|
||||
for k, v := range p.params {
|
||||
data.Set(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
return data.Encode(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported content type: %s", p.contentType)
|
||||
}
|
||||
}
|
||||
+214
-34
@@ -4,69 +4,137 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/eolinker/eosc"
|
||||
)
|
||||
|
||||
var (
|
||||
mcpServer = NewServer()
|
||||
ServiceBasePath = "mcp/service"
|
||||
GlobalBasePath = "mcp/global"
|
||||
AppBasePath = "mcp/app"
|
||||
)
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
sseServers: eosc.BuildUntyped[string, *server.SSEServer](),
|
||||
servers: make(map[string]*Handler),
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
sseServers eosc.Untyped[string, *server.SSEServer]
|
||||
servers map[string]*Handler
|
||||
locker sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *Server) Set(path string, sseServer *server.SSEServer) {
|
||||
s.sseServers.Set(path, sseServer)
|
||||
type Handler struct {
|
||||
*server.MCPServer
|
||||
handlers map[string]http.Handler
|
||||
}
|
||||
|
||||
func (s *Server) Del(path string) {
|
||||
s.sseServers.Del(path)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
sseServer, has := s.sseServers.Get(trimPath(r.URL.Path))
|
||||
if has {
|
||||
sseServer.ServeHTTP(w, r)
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
|
||||
if strings.HasSuffix(r.URL.Path, "/mcp") {
|
||||
h.handlers["openapi-stream"].ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/api") {
|
||||
h.handlers["api-sse"].ServeHTTP(w, r)
|
||||
return
|
||||
} else if strings.HasPrefix(r.URL.Path, "/openapi") {
|
||||
h.handlers["openapi-sse"].ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func trimPath(path string) string {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/message")
|
||||
path = strings.TrimSuffix(path, "/sse")
|
||||
return path
|
||||
}
|
||||
|
||||
func SetSSEServer(sid string, name string, version string, tools ...ITool) {
|
||||
s := server.NewMCPServer(name, version)
|
||||
for _, tool := range tools {
|
||||
tool.RegisterMCP(s)
|
||||
func (s *Server) Set(id string, ser *server.MCPServer) {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
tmp := &Handler{
|
||||
MCPServer: ser,
|
||||
handlers: make(map[string]http.Handler),
|
||||
}
|
||||
apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid)
|
||||
openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid)
|
||||
mcpServer.Set(apiPath, server.NewSSEServer(s, server.WithBasePath(apiPath)))
|
||||
mcpServer.Set(openAPIPath, server.NewSSEServer(s, server.WithBasePath(openAPIPath)))
|
||||
tmp.handlers["api-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, id)))
|
||||
tmp.handlers["openapi-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, id)))
|
||||
tmp.handlers["openapi-stream"] = server.NewStreamableHTTPServer(ser, server.WithEndpointPath(fmt.Sprintf("/openapi/v1/%s/%s/mcp", ServiceBasePath, id)))
|
||||
s.servers[id] = tmp
|
||||
|
||||
}
|
||||
|
||||
func DelSSEServer(sid string) {
|
||||
apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid)
|
||||
openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid)
|
||||
mcpServer.Del(apiPath)
|
||||
mcpServer.Del(openAPIPath)
|
||||
func (s *Server) Del(id string) {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
delete(s.servers, id)
|
||||
}
|
||||
|
||||
func (s *Server) Get(id string) (*Handler, bool) {
|
||||
s.locker.RLock()
|
||||
defer s.locker.RUnlock()
|
||||
ser, has := s.servers[id]
|
||||
if !has {
|
||||
return nil, false
|
||||
}
|
||||
m := &Handler{
|
||||
MCPServer: ser.MCPServer,
|
||||
handlers: make(map[string]http.Handler),
|
||||
}
|
||||
for k, v := range ser.handlers {
|
||||
m.handlers[k] = v
|
||||
}
|
||||
|
||||
return m, true
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
sid, err := genPath(r.URL.Path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
ser, has := s.Get(sid)
|
||||
if has {
|
||||
ser.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func genPath(path string) (sid string, err error) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
ps := strings.Split(path, "/")
|
||||
if len(ps) < 2 {
|
||||
err = fmt.Errorf("invalid path: %s", path)
|
||||
return
|
||||
}
|
||||
sid = ps[len(ps)-2]
|
||||
return
|
||||
}
|
||||
|
||||
func SetServer(sid string, name string, version string, tools ...ITool) {
|
||||
ser, has := mcpServer.Get(sid)
|
||||
if !has {
|
||||
mcpServer.Set(sid, server.NewMCPServer(name, version, server.WithToolCapabilities(true)))
|
||||
ser, has = mcpServer.Get(sid)
|
||||
if !has {
|
||||
return
|
||||
}
|
||||
}
|
||||
ts := make([]server.ServerTool, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
ts = append(ts, tool.Tool())
|
||||
}
|
||||
ser.SetTools(ts...)
|
||||
}
|
||||
|
||||
func DelServer(sid string) {
|
||||
mcpServer.Del(sid)
|
||||
}
|
||||
|
||||
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -76,3 +144,115 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func DefaultMCPServer() *Server {
|
||||
return mcpServer
|
||||
}
|
||||
|
||||
func SetServerByOpenapi(sid, name, version, content string) error {
|
||||
mcpInfo, err := ConvertMCPFromOpenAPI3Data([]byte(content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
|
||||
}
|
||||
tools := make([]ITool, 0, len(mcpInfo.Apis))
|
||||
for _, a := range mcpInfo.Apis {
|
||||
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
|
||||
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
|
||||
params := make(map[string]*Param)
|
||||
for _, v := range a.Params {
|
||||
params[v.Name] = NewParam(Position(v.In), v.Required, v.Description)
|
||||
options := make([]mcp.PropertyOption, 0, 2)
|
||||
if v.Required {
|
||||
options = append(options, mcp.Required())
|
||||
}
|
||||
options = append(options, mcp.Description(v.Description))
|
||||
toolOptions = append(toolOptions, mcp.WithString(v.Name, options...))
|
||||
}
|
||||
if a.Body != nil {
|
||||
type Schema struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Properties map[string]interface{} `mapstructure:"properties"`
|
||||
Items interface{} `mapstructure:"items"`
|
||||
Required interface{} `mapstructure:"required"`
|
||||
}
|
||||
var tmp Schema
|
||||
err = mapstructure.Decode(a.Body, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
required := map[string]struct{}{}
|
||||
switch t := tmp.Required.(type) {
|
||||
case []interface{}:
|
||||
for _, v := range t {
|
||||
i, ok := v.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
required[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
for k, v := range tmp.Properties {
|
||||
description := ""
|
||||
typ := "string"
|
||||
isRequired := false
|
||||
if _, ok := required[k]; ok {
|
||||
isRequired = true
|
||||
}
|
||||
var props map[string]interface{}
|
||||
var items interface{}
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
if m, ok := t["type"]; ok {
|
||||
n, ok := m.(string)
|
||||
if ok {
|
||||
typ = n
|
||||
}
|
||||
}
|
||||
if m, ok := t["description"]; ok {
|
||||
n, ok := m.(string)
|
||||
if ok {
|
||||
description = n
|
||||
}
|
||||
}
|
||||
switch typ {
|
||||
case "array":
|
||||
if m, ok := t["items"]; ok {
|
||||
items = m
|
||||
}
|
||||
case "object":
|
||||
if m, ok := t["properties"]; ok {
|
||||
n, ok := m.(map[string]interface{})
|
||||
if ok {
|
||||
props = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params[k] = NewParam(PositionBody, isRequired, description)
|
||||
options := make([]mcp.PropertyOption, 0, 3)
|
||||
options = append(options, mcp.Description(description))
|
||||
if props != nil {
|
||||
options = append(options, mcp.Properties(props))
|
||||
}
|
||||
if items != nil {
|
||||
options = append(options, mcp.Items(items))
|
||||
}
|
||||
switch typ {
|
||||
case "string":
|
||||
toolOptions = append(toolOptions, mcp.WithString(k, options...))
|
||||
case "integer", "number", "float":
|
||||
toolOptions = append(toolOptions, mcp.WithNumber(k, options...))
|
||||
case "boolean":
|
||||
toolOptions = append(toolOptions, mcp.WithBoolean(k, options...))
|
||||
case "array":
|
||||
toolOptions = append(toolOptions, mcp.WithArray(k, options...))
|
||||
case "object":
|
||||
toolOptions = append(toolOptions, mcp.WithObject(k, options...))
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tools = append(tools, NewTool(a.Summary, a.Path, a.Method, a.ContentType, params, toolOptions...))
|
||||
}
|
||||
SetServer(sid, name, version, tools...)
|
||||
return nil
|
||||
}
|
||||
|
||||
+44
-60
@@ -2,7 +2,6 @@ package mcp_server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -16,36 +15,38 @@ import (
|
||||
)
|
||||
|
||||
type ITool interface {
|
||||
RegisterMCP(s *server.MCPServer)
|
||||
Tool() server.ServerTool
|
||||
}
|
||||
|
||||
const (
|
||||
MCPBody = "Body"
|
||||
MCPHeader = "Header"
|
||||
MCPQuery = "Query"
|
||||
MCPPath = "Path"
|
||||
)
|
||||
|
||||
type Tool struct {
|
||||
name string
|
||||
url string
|
||||
method string
|
||||
contentType string
|
||||
params map[string]*Param
|
||||
opts []mcp.ToolOption
|
||||
}
|
||||
|
||||
func NewTool(name string, uri string, method string, contentType string, opts ...mcp.ToolOption) ITool {
|
||||
func (t *Tool) Tool() server.ServerTool {
|
||||
return server.ServerTool{
|
||||
Tool: mcp.NewTool(t.name, t.opts...),
|
||||
Handler: generateInvokeTool(t.url, t.method, t.contentType, t.params),
|
||||
}
|
||||
}
|
||||
|
||||
func NewTool(name string, uri string, method string, contentType string, params map[string]*Param, opts ...mcp.ToolOption) ITool {
|
||||
return &Tool{
|
||||
name: name,
|
||||
url: uri,
|
||||
method: method,
|
||||
contentType: contentType,
|
||||
params: params,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tool) RegisterMCP(s *server.MCPServer) {
|
||||
s.AddTool(mcp.NewTool(t.name, t.opts...), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
func generateInvokeTool(path string, method string, contentType string, params map[string]*Param) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
invokeAddress := utils.GatewayInvoke(ctx)
|
||||
if invokeAddress == "" {
|
||||
return nil, fmt.Errorf("invoke address is empty")
|
||||
@@ -58,69 +59,54 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
path := t.url
|
||||
queries := url.Values{}
|
||||
headers := make(map[string]string)
|
||||
body := ""
|
||||
for k, v := range request.Params.Arguments {
|
||||
if k == "Body" {
|
||||
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()
|
||||
bodyParam := NewBodyParam(contentType)
|
||||
for k, p := range params {
|
||||
vv, ok := request.GetArguments()[k]
|
||||
if !ok && p.required {
|
||||
return nil, fmt.Errorf("param %s is required", k)
|
||||
}
|
||||
if p.position == PositionHeader || p.position == PositionQuery || p.position == PositionPath {
|
||||
v, ok := vv.(string)
|
||||
if !ok || v == "<nil>" {
|
||||
if p.required {
|
||||
return nil, fmt.Errorf("param %s is required", k)
|
||||
}
|
||||
default:
|
||||
tmp, _ := json.Marshal(a)
|
||||
body = string(tmp)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
tmp, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case MCPHeader:
|
||||
for kk, vv := range tmp {
|
||||
headers[kk] = fmt.Sprintf("%v", vv)
|
||||
}
|
||||
|
||||
case MCPQuery:
|
||||
for kk, vv := range tmp {
|
||||
queries.Set(kk, fmt.Sprintf("%v", vv))
|
||||
}
|
||||
case MCPPath:
|
||||
for kk, vv := range tmp {
|
||||
p, ok := vv.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid path %s", v)
|
||||
}
|
||||
path = strings.Replace(path, fmt.Sprintf("{%s}", kk), p, -1)
|
||||
switch p.position {
|
||||
case PositionPath:
|
||||
path = strings.ReplaceAll(path, "{"+k+"}", fmt.Sprintf("%v", vv))
|
||||
case PositionQuery:
|
||||
queries.Set(k, fmt.Sprintf("%v", vv))
|
||||
case PositionHeader:
|
||||
headers[k] = fmt.Sprintf("%v", vv)
|
||||
case PositionBody:
|
||||
if vv == nil {
|
||||
continue
|
||||
}
|
||||
bodyParam.Set(k, vv)
|
||||
}
|
||||
}
|
||||
bodyData, err := bodyParam.Encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Path = path
|
||||
u.RawQuery = queries.Encode()
|
||||
|
||||
req, err := http.NewRequest(t.method, u.String(), strings.NewReader(body))
|
||||
req, err := http.NewRequest(method, u.String(), strings.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
if t.contentType != "" {
|
||||
req.Header.Set("Content-Type", t.contentType)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
apikey := utils.Label(ctx, "apikey")
|
||||
if apikey != "" {
|
||||
@@ -141,7 +127,5 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(d)), nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var client = http.Client{}
|
||||
|
||||
+32
-12
@@ -8,6 +8,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
|
||||
|
||||
ai_provider_local "github.com/APIParkLab/APIPark/ai-provider/local"
|
||||
|
||||
model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime"
|
||||
@@ -35,12 +37,13 @@ var (
|
||||
)
|
||||
|
||||
type imlAPIModule struct {
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
aiAPIService ai_api.IAPIService `autowired:""`
|
||||
aiModelService ai_model.IProviderModelService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
serviceOverviewService service_overview.IOverviewService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
aiAPIService ai_api.IAPIService `autowired:""`
|
||||
aiModelService ai_model.IProviderModelService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAPIModule) getAPIDoc(ctx context.Context, serviceId string) (*openapi3.T, error) {
|
||||
@@ -77,9 +80,17 @@ func (i *imlAPIModule) updateAPIDoc(ctx context.Context, serviceId, serviceName,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
|
||||
ID: uuid.New().String(),
|
||||
Content: string(result),
|
||||
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
|
||||
count, err := i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
|
||||
ID: uuid.New().String(),
|
||||
Content: string(result),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update api doc error:%v", err)
|
||||
}
|
||||
return i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
|
||||
ApiCount: &count,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,10 +104,19 @@ func (i *imlAPIModule) deleteAPIDoc(ctx context.Context, serviceId string, path
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
|
||||
ID: uuid.New().String(),
|
||||
Content: string(result),
|
||||
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
|
||||
count, err := i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
|
||||
ID: uuid.New().String(),
|
||||
Content: string(result),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update api doc error:%v", err)
|
||||
}
|
||||
return i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
|
||||
ApiCount: &count,
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (i *imlAPIModule) Create(ctx context.Context, serviceId string, input *ai_api_dto.CreateAPI) error {
|
||||
|
||||
+24
-15
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func genOpenAPI3Template(title string, description string) *openapi3.T {
|
||||
result := new(openapi3.T)
|
||||
result.OpenAPI = "3.1.0"
|
||||
result.OpenAPI = "3.0.1"
|
||||
result.Info = &openapi3.Info{
|
||||
Title: title,
|
||||
Description: description,
|
||||
@@ -37,6 +37,8 @@ func genOperation(summary string, description string, variables []*ai_api_dto.Ai
|
||||
|
||||
func genRequestBody(variables []*ai_api_dto.AiPromptVariable) *openapi3.RequestBodyRef {
|
||||
requestBody := openapi3.NewRequestBody()
|
||||
requestBody.Description = "Request body"
|
||||
requestBody.Required = true
|
||||
requestBody.Content = openapi3.NewContentWithSchema(genRequestBodySchema(variables), []string{"application/json"})
|
||||
return &openapi3.RequestBodyRef{
|
||||
Value: requestBody,
|
||||
@@ -55,10 +57,14 @@ func genResponse() *openapi3.ResponseRef {
|
||||
|
||||
func genRequestBodySchema(variables []*ai_api_dto.AiPromptVariable) *openapi3.Schema {
|
||||
result := openapi3.NewObjectSchema()
|
||||
required := make([]string, 0, 2)
|
||||
required = append(required, "messages")
|
||||
if len(variables) > 0 {
|
||||
result.WithProperty("variables", genVariableSchema(variables))
|
||||
result.WithRequired([]string{"variables", "messages"})
|
||||
required = append(required, "variables")
|
||||
}
|
||||
|
||||
result.WithRequired(required)
|
||||
streamSchema := openapi3.NewBoolSchema()
|
||||
streamSchema.Title = "stream"
|
||||
streamSchema.Description = "Whether to stream the response"
|
||||
@@ -129,6 +135,8 @@ func genMessageSchema() *openapi3.Schema {
|
||||
"role": roleSchema,
|
||||
"content": contentSchema,
|
||||
})
|
||||
|
||||
result.WithRequired([]string{"role", "content"})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -137,20 +145,21 @@ func genMessagesSchema() *openapi3.Schema {
|
||||
result.Title = "Messages"
|
||||
result.Description = "Chat Messages"
|
||||
result.Items = openapi3.NewSchemaRef("#/components/schemas/Message", messageSchema)
|
||||
result.Required = []string{"content", "role"}
|
||||
return result
|
||||
}
|
||||
|
||||
func genResponseSchema() *openapi3.Schema {
|
||||
result := openapi3.NewObjectSchema()
|
||||
result.Description = "Response from the server"
|
||||
|
||||
|
||||
// 创建 choices 数组
|
||||
choicesSchema := openapi3.NewArraySchema()
|
||||
choiceItemSchema := openapi3.NewObjectSchema()
|
||||
|
||||
|
||||
// choice 中的 message 字段
|
||||
choiceItemSchema.WithPropertyRef("message", messageSchemaRef)
|
||||
|
||||
|
||||
// finish_reason 字段
|
||||
finishReasonSchema := openapi3.NewStringSchema().WithEnum(
|
||||
"stop",
|
||||
@@ -160,41 +169,41 @@ func genResponseSchema() *openapi3.Schema {
|
||||
"null",
|
||||
)
|
||||
choiceItemSchema.WithProperty("finish_reason", finishReasonSchema)
|
||||
|
||||
|
||||
// index 字段
|
||||
choiceItemSchema.WithProperty("index", openapi3.NewIntegerSchema())
|
||||
|
||||
|
||||
// logprobs 字段,可以为 null
|
||||
choiceItemSchema.WithProperty("logprobs", openapi3.NewSchema().WithNullable())
|
||||
|
||||
|
||||
choicesSchema.Items = &openapi3.SchemaRef{Value: choiceItemSchema}
|
||||
result.WithProperty("choices", choicesSchema)
|
||||
|
||||
|
||||
// object 字段
|
||||
result.WithProperty("object", openapi3.NewStringSchema().WithEnum("chat.completion"))
|
||||
|
||||
|
||||
// usage 字段
|
||||
usageSchema := openapi3.NewObjectSchema()
|
||||
usageSchema.WithProperty("prompt_tokens", openapi3.NewIntegerSchema())
|
||||
usageSchema.WithProperty("completion_tokens", openapi3.NewIntegerSchema())
|
||||
usageSchema.WithProperty("total_tokens", openapi3.NewIntegerSchema())
|
||||
|
||||
|
||||
// prompt_tokens_details 字段
|
||||
promptTokensDetailsSchema := openapi3.NewObjectSchema()
|
||||
promptTokensDetailsSchema.WithProperty("cached_tokens", openapi3.NewIntegerSchema())
|
||||
usageSchema.WithProperty("prompt_tokens_details", promptTokensDetailsSchema)
|
||||
|
||||
|
||||
result.WithProperty("usage", usageSchema)
|
||||
|
||||
|
||||
// 其他字段
|
||||
result.WithProperty("created", openapi3.NewIntegerSchema())
|
||||
result.WithProperty("system_fingerprint", openapi3.NewStringSchema().WithNullable())
|
||||
result.WithProperty("model", openapi3.NewStringSchema())
|
||||
result.WithProperty("id", openapi3.NewStringSchema())
|
||||
|
||||
|
||||
// 保留原有的错误字段
|
||||
result.WithProperty("code", openapi3.NewIntegerSchema())
|
||||
result.WithProperty("error", openapi3.NewStringSchema())
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
+15
-11
@@ -128,17 +128,19 @@ func (i *imlProviderModelModule) DeleteProviderModel(ctx *gin.Context, provider
|
||||
if err := i.providerModelService.Delete(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
err = i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
|
||||
{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: fmt.Sprintf("%s#%s", provider, modelInfo.Name),
|
||||
Resource: "ai-model",
|
||||
if p.GetModelConfig().AccessConfigurationStatus {
|
||||
err = i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
|
||||
{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: fmt.Sprintf("%s$%s", provider, modelInfo.Name),
|
||||
Resource: "ai-model",
|
||||
},
|
||||
Attr: nil,
|
||||
},
|
||||
Attr: nil,
|
||||
},
|
||||
}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
p.RemoveModel(id)
|
||||
@@ -200,7 +202,9 @@ func (i *imlProviderModelModule) AddProviderModel(ctx *gin.Context, provider str
|
||||
}
|
||||
|
||||
func newModel(provider string, model string, config string) *gateway.DynamicRelease {
|
||||
|
||||
if config == "" {
|
||||
config = "{}"
|
||||
}
|
||||
return &gateway.DynamicRelease{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: fmt.Sprintf("%s$%s", provider, model),
|
||||
|
||||
@@ -3,6 +3,11 @@ package api_doc
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/eolinker/go-common/store"
|
||||
|
||||
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
|
||||
|
||||
api_doc_dto "github.com/APIParkLab/APIPark/module/api-doc/dto"
|
||||
api_doc "github.com/APIParkLab/APIPark/service/api-doc"
|
||||
@@ -16,8 +21,10 @@ import (
|
||||
var _ IAPIDocModule = (*imlAPIDocModule)(nil)
|
||||
|
||||
type imlAPIDocModule struct {
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
serviceOverviewService service_overview.IOverviewService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAPIDocModule) UpdateDoc(ctx context.Context, serviceId string, input *api_doc_dto.UpdateDoc) (*api_doc_dto.ApiDocDetail, error) {
|
||||
@@ -29,11 +36,18 @@ func (i *imlAPIDocModule) UpdateDoc(ctx context.Context, serviceId string, input
|
||||
input.Id = uuid.New().String()
|
||||
}
|
||||
// 每个API加上前缀
|
||||
|
||||
err = i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
|
||||
ID: input.Id,
|
||||
Content: input.Content,
|
||||
Prefix: info.Prefix,
|
||||
err = i.transaction.Transaction(ctx, func(ctx context.Context) error {
|
||||
count, err := i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
|
||||
ID: input.Id,
|
||||
Content: input.Content,
|
||||
Prefix: info.Prefix,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update api doc error:%v", err)
|
||||
}
|
||||
return i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
|
||||
ApiCount: &count,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -33,7 +33,8 @@ type IAuthorizationModule interface {
|
||||
// Info 获取项目鉴权详情
|
||||
Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error)
|
||||
|
||||
CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error)
|
||||
CheckAPIKeyAuthorizationByService(ctx context.Context, serviceId string, apikey string) (bool, error)
|
||||
CheckAPIKeyAuthorizationByApp(ctx context.Context, appId string, apikey string) (bool, error)
|
||||
|
||||
//ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,27 @@ type imlAuthorizationModule struct {
|
||||
transaction store.ITransaction `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAuthorizationModule) CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error) {
|
||||
func (i *imlAuthorizationModule) CheckAPIKeyAuthorizationByApp(ctx context.Context, appId string, apikey string) (bool, error) {
|
||||
authorizations, err := i.authorizationService.ListByApp(ctx, appId)
|
||||
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) CheckAPIKeyAuthorizationByService(ctx context.Context, serviceId string, apikey string) (bool, error) {
|
||||
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
+35
-32
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
|
||||
|
||||
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
|
||||
|
||||
"github.com/APIParkLab/APIPark/module/monitor/driver"
|
||||
@@ -58,21 +60,22 @@ var (
|
||||
)
|
||||
|
||||
type imlCatalogueModule struct {
|
||||
catalogueService catalogue.ICatalogueService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
serviceTagService service_tag.ITagService `autowired:""`
|
||||
serviceDocService service_doc.IDocService `autowired:""`
|
||||
tagService tag.ITagService `autowired:""`
|
||||
releaseService release.IReleaseService `autowired:""`
|
||||
subscribeService subscribe.ISubscribeService `autowired:""`
|
||||
subscribeApplyService subscribe.ISubscribeApplyService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
settingService setting.ISettingService `autowired:""`
|
||||
monitorService monitor.IMonitorService `autowired:""`
|
||||
root *Root
|
||||
catalogueService catalogue.ICatalogueService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
serviceOverviewService service_overview.IOverviewService `autowired:""`
|
||||
serviceTagService service_tag.ITagService `autowired:""`
|
||||
serviceDocService service_doc.IDocService `autowired:""`
|
||||
tagService tag.ITagService `autowired:""`
|
||||
releaseService release.IReleaseService `autowired:""`
|
||||
subscribeService subscribe.ISubscribeService `autowired:""`
|
||||
subscribeApplyService subscribe.ISubscribeApplyService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
settingService setting.ISettingService `autowired:""`
|
||||
monitorService monitor.IMonitorService `autowired:""`
|
||||
root *Root
|
||||
}
|
||||
|
||||
func (i *imlCatalogueModule) DefaultCatalogue(ctx context.Context) (*catalogue_dto.Catalogue, error) {
|
||||
@@ -447,27 +450,26 @@ func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*c
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceIds := utils.SliceToSlice(items, func(i *service.Service) string {
|
||||
return i.Id
|
||||
}, func(s *service.Service) bool {
|
||||
// 未发布的不给展示
|
||||
_, err = i.releaseService.GetRunning(ctx, s.Id)
|
||||
return err == nil
|
||||
})
|
||||
overviewMap, err := i.serviceOverviewService.Map(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceIds = utils.SliceToSlice(serviceIds, func(s string) string {
|
||||
return s
|
||||
}, func(s string) bool {
|
||||
// 只展示已发布的服务
|
||||
if info, ok := overviewMap[s]; ok && info.IsReleased {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if len(serviceIds) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commits, err := i.releaseService.GetRunningApiDocCommits(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiCountMap, err := i.apiDocService.LatestAPICountByCommits(ctx, commits...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscriberCountMap, err := i.subscribeService.CountMapByService(ctx, subscribe.ApplyStatusSubscribe, serviceIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -479,8 +481,9 @@ func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*c
|
||||
|
||||
result := make([]*catalogue_dto.ServiceItem, 0, len(items))
|
||||
for _, v := range items {
|
||||
apiNum, ok := apiCountMap[v.Id]
|
||||
if !ok || apiNum < 1 {
|
||||
|
||||
ov, ok := overviewMap[v.Id]
|
||||
if !ok || ov.ReleaseApiCount < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -489,8 +492,8 @@ func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*c
|
||||
Name: v.Name,
|
||||
Tags: auto.List(serviceTagMap[v.Id]),
|
||||
Catalogue: auto.UUID(v.Catalogue),
|
||||
ApiNum: apiNum,
|
||||
SubscriberNum: subscriberCountMap[v.Id],
|
||||
ApiNum: ov.ReleaseApiCount,
|
||||
Description: v.Description,
|
||||
Logo: v.Logo,
|
||||
EnableMCP: v.EnableMCP,
|
||||
|
||||
+85
-114
@@ -10,7 +10,6 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/APIParkLab/APIPark/service/subscribe"
|
||||
|
||||
@@ -47,18 +46,43 @@ type imlMcpModule struct {
|
||||
releaseService release.IReleaseService `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlMcpModule) subscribeServiceIds(ctx context.Context, appId string) ([]string, error) {
|
||||
subscribes, err := i.subscriberService.SubscriptionsByApplication(ctx, appId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get subscriber error: %w,app id is %s", err, appId)
|
||||
}
|
||||
serviceIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string {
|
||||
return s.Service
|
||||
}, func(s *subscribe.Subscribe) bool {
|
||||
return s.ApplyStatus == subscribe.ApplyStatusSubscribe
|
||||
})
|
||||
if len(serviceIds) == 0 {
|
||||
return nil, fmt.Errorf("no subscriber found,app id is %s", appId)
|
||||
}
|
||||
return serviceIds, nil
|
||||
}
|
||||
|
||||
func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
keyword, _ := req.Params.Arguments["keyword"].(string)
|
||||
list, err := i.serviceService.Search(ctx, keyword, map[string]interface{}{
|
||||
|
||||
keyword, _ := req.GetArguments()["keyword"].(string)
|
||||
appId := utils.Label(ctx, "app")
|
||||
condition := map[string]interface{}{
|
||||
"as_server": true,
|
||||
}, "update_at desc")
|
||||
}
|
||||
if appId != "" {
|
||||
serviceIds, err := i.subscribeServiceIds(ctx, appId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get subscriber service ids error: %w,app id is %s", err, appId)
|
||||
}
|
||||
condition["uuid"] = serviceIds
|
||||
}
|
||||
|
||||
list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search service error: %w", err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
list, err = i.serviceService.Search(ctx, "", map[string]interface{}{
|
||||
"as_server": true,
|
||||
}, "update_at desc")
|
||||
list, err = i.serviceService.Search(ctx, "", condition, "update_at desc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search service error: %w", err)
|
||||
}
|
||||
@@ -116,116 +140,57 @@ func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*
|
||||
|
||||
}
|
||||
|
||||
func (i *imlMcpModule) Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
keyword := req.Params.Arguments["keyword"].(string)
|
||||
condition := make(map[string]interface{})
|
||||
condition["as_app"] = true
|
||||
list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search service error: %w", err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
list, err = i.serviceService.Search(ctx, "", condition, "update_at desc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search service error: %w", err)
|
||||
}
|
||||
}
|
||||
data, _ := json.Marshal(utils.SliceToSlice(list, func(s *service.Service) *mcp_dto.App {
|
||||
return &mcp_dto.App{
|
||||
Id: s.Id,
|
||||
Name: s.Name,
|
||||
Description: s.Name,
|
||||
CreateTime: s.CreateTime,
|
||||
UpdateTime: s.UpdateTime,
|
||||
}
|
||||
}))
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
|
||||
func (i *imlMcpModule) APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
serviceId, _ := req.Params.Arguments["service"].(string)
|
||||
serviceIds := make([]string, 0, 1)
|
||||
serviceId, _ := req.GetArguments()["service"].(string)
|
||||
if serviceId == "" {
|
||||
serviceIds = append(serviceIds, serviceId)
|
||||
return nil, fmt.Errorf("service id is empty")
|
||||
}
|
||||
serviceList, err := i.serviceService.ServiceList(ctx)
|
||||
s, err := i.serviceService.Get(ctx, serviceId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get service list error: %w", err)
|
||||
return nil, fmt.Errorf("get service error: %w,service id is %s", err, serviceId)
|
||||
}
|
||||
appId := utils.Label(ctx, "app")
|
||||
if appId != "" {
|
||||
subscribers, err := i.subscriberService.ListByApplication(ctx, serviceId, appId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get subscriber error: %w,app id is %s", err, appId)
|
||||
}
|
||||
if len(subscribers) < 1 || subscribers[0].ApplyStatus != subscribe.ApplyStatusSubscribe {
|
||||
return nil, fmt.Errorf("no subscriber found,app id is %s", appId)
|
||||
}
|
||||
}
|
||||
result := make([]*mcp_dto.ServiceAPI, 0, len(serviceList))
|
||||
|
||||
for _, s := range serviceList {
|
||||
serviceRelease, err := i.releaseService.GetRunning(ctx, s.Id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("get service release error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
continue
|
||||
serviceRelease, err := i.releaseService.GetRunning(ctx, serviceId)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("get service release error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
_, _, apiDocRelease, _, _, err := i.releaseService.GetReleaseInfos(ctx, serviceRelease.UUID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("get service release info error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
continue
|
||||
}
|
||||
commit, err := i.apiDocService.GetDocCommit(ctx, apiDocRelease.Commit)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("get api doc release error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
continue
|
||||
}
|
||||
T, err := openapi3Loader.LoadFromData([]byte(commit.Data.Content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load openapi3 error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
result = append(result, &mcp_dto.ServiceAPI{
|
||||
ServiceID: s.Id,
|
||||
ServiceName: s.Name,
|
||||
APIDoc: T,
|
||||
})
|
||||
return nil, fmt.Errorf("no service found,service id is %s", serviceId)
|
||||
}
|
||||
_, _, apiDocRelease, _, _, err := i.releaseService.GetReleaseInfos(ctx, serviceRelease.UUID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("get service release info error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
return nil, fmt.Errorf("no service found,service id is %s", serviceId)
|
||||
}
|
||||
commit, err := i.apiDocService.GetDocCommit(ctx, apiDocRelease.Commit)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("get api doc release error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
return nil, fmt.Errorf("no service found,service id is %s", serviceId)
|
||||
}
|
||||
T, err := openapi3Loader.LoadFromData([]byte(commit.Data.Content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load openapi3 error: %w,service id is %s", err, s.Id)
|
||||
}
|
||||
data, _ := json.Marshal(result)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
|
||||
func (i *imlMcpModule) SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
serviceId, ok := req.Params.Arguments["service"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("service id is required")
|
||||
result := &mcp_dto.ServiceAPI{
|
||||
ServiceID: serviceId,
|
||||
ServiceName: s.Name,
|
||||
APIDoc: T,
|
||||
}
|
||||
subscribes, err := i.subscriberService.Subscribers(ctx, serviceId, subscribe.ApplyStatusSubscribe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get subscriber error: %w,service id is %s", err, serviceId)
|
||||
}
|
||||
appIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string {
|
||||
return s.Application
|
||||
})
|
||||
if len(appIds) == 0 {
|
||||
return nil, fmt.Errorf("no subscriber found,service id is %s", serviceId)
|
||||
}
|
||||
list, err := i.appAuthorizationService.ListByApp(ctx, appIds...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get app authorization error: %w,app ids is %s", err, appIds)
|
||||
}
|
||||
result := utils.SliceToSlice(list, func(a *application_authorization.Authorization) *mcp_dto.AppAuthorization {
|
||||
return &mcp_dto.AppAuthorization{
|
||||
Id: a.UUID,
|
||||
Name: a.Name,
|
||||
Position: a.Position,
|
||||
TokenName: a.TokenName,
|
||||
Config: a.Config,
|
||||
}
|
||||
}, func(a *application_authorization.Authorization) bool {
|
||||
if a.Type != "apikey" {
|
||||
return false
|
||||
}
|
||||
if a.ExpireTime != 0 && a.ExpireTime < time.Now().Unix() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
data, _ := json.Marshal(result)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
@@ -248,18 +213,18 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
path, ok := req.Params.Arguments["path"].(string)
|
||||
path, ok := req.GetArguments()["path"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid path")
|
||||
}
|
||||
u.Path = fmt.Sprintf("%s/%s", strings.TrimSuffix(u.Path, "/"), strings.TrimPrefix(path, "/"))
|
||||
|
||||
method, ok := req.Params.Arguments["method"].(string)
|
||||
method, ok := req.GetArguments()["method"].(string)
|
||||
if !ok {
|
||||
method = "GET"
|
||||
}
|
||||
queryParam := url.Values{}
|
||||
query, ok := req.Params.Arguments["query"].(map[string]interface{})
|
||||
query, ok := req.GetArguments()["query"].(map[string]interface{})
|
||||
if ok {
|
||||
for k, v := range query {
|
||||
switch v := v.(type) {
|
||||
@@ -278,7 +243,7 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
|
||||
}
|
||||
u.RawQuery = queryParam.Encode()
|
||||
headerParam := http.Header{}
|
||||
header, ok := req.Params.Arguments["header"].(map[string]interface{})
|
||||
header, ok := req.GetArguments()["header"].(map[string]interface{})
|
||||
if ok {
|
||||
for k, v := range header {
|
||||
switch v := v.(type) {
|
||||
@@ -294,12 +259,12 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
|
||||
}
|
||||
}
|
||||
|
||||
body, ok := req.Params.Arguments["body"].(string)
|
||||
body, ok := req.GetArguments()["body"].(string)
|
||||
if !ok {
|
||||
body = ""
|
||||
}
|
||||
|
||||
contentType, ok := req.Params.Arguments["content-type"].(string)
|
||||
contentType, ok := req.GetArguments()["content-type"].(string)
|
||||
if !ok {
|
||||
contentType = "application/json"
|
||||
}
|
||||
@@ -310,8 +275,14 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
|
||||
request.Header = headerParam
|
||||
request.Header.Set("Content-Type", contentType)
|
||||
apikey := utils.Label(ctx, "apikey")
|
||||
|
||||
if apikey != "" {
|
||||
request.Header.Set("Authorization", utils.Md5(apikey))
|
||||
appId := utils.Label(ctx, "app")
|
||||
if appId == "" {
|
||||
request.Header.Set("Authorization", utils.Md5(apikey))
|
||||
} else {
|
||||
request.Header.Set("Authorization", apikey)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
|
||||
@@ -12,13 +12,7 @@ import (
|
||||
type IMcpModule interface {
|
||||
// Services 获取服务列表
|
||||
Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
// Apps 获取应用列表
|
||||
Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
// APIs 获取API列表
|
||||
APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
|
||||
// SubscriberAuthorizations 获取订阅者授权
|
||||
SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
Invoke(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
}
|
||||
|
||||
|
||||
+188
-176
@@ -7,10 +7,14 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/eolinker/go-common/server"
|
||||
|
||||
"github.com/eolinker/go-common/register"
|
||||
|
||||
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
|
||||
|
||||
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
|
||||
api_doc "github.com/APIParkLab/APIPark/service/api-doc"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
strategy_driver "github.com/APIParkLab/APIPark/module/strategy/driver"
|
||||
strategy_dto "github.com/APIParkLab/APIPark/module/strategy/dto"
|
||||
@@ -51,22 +55,70 @@ var (
|
||||
)
|
||||
|
||||
type imlPublishModule struct {
|
||||
projectDiffModule serviceDiff.IServiceDiffModule `autowired:""`
|
||||
releaseModule releaseModule.IReleaseModule `autowired:""`
|
||||
publishService publish.IPublishService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
upstreamService upstream.IUpstreamService `autowired:""`
|
||||
strategyService strategy.IStrategyService `autowired:""`
|
||||
releaseService release.IReleaseService `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
projectDiffModule serviceDiff.IServiceDiffModule `autowired:""`
|
||||
releaseModule releaseModule.IReleaseModule `autowired:""`
|
||||
publishService publish.IPublishService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
upstreamService upstream.IUpstreamService `autowired:""`
|
||||
strategyService strategy.IStrategyService `autowired:""`
|
||||
releaseService release.IReleaseService `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
serviceOverviewService service_overview.IOverviewService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error {
|
||||
func (i *imlPublishModule) OnInit() {
|
||||
register.Handle(func(v server.Server) {
|
||||
ctx := context.Background()
|
||||
list, err := i.releaseService.GetRunningList(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("onInit: get running list failed:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if len(list) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
serviceMap := make(map[string]*release.Release)
|
||||
serviceIds := make([]string, 0, len(list))
|
||||
for _, v := range list {
|
||||
if _, ok := serviceMap[v.Service]; !ok {
|
||||
serviceMap[v.Service] = v
|
||||
serviceIds = append(serviceIds, v.Service)
|
||||
}
|
||||
}
|
||||
overviewList, err := i.serviceOverviewService.List(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
log.Errorf("onInit: get running list failed:%s", err.Error())
|
||||
return
|
||||
}
|
||||
for _, v := range overviewList {
|
||||
if v.IsReleased {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
listCommits, err := i.apiDocService.ListLatestDocCommit(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
log.Errorf("onInit: get running api doc commits failed:%s", err.Error())
|
||||
return
|
||||
}
|
||||
isReleased := true
|
||||
for _, v := range listCommits {
|
||||
i.serviceOverviewService.Update(ctx, v.Target, &service_overview.Update{
|
||||
ApiCount: nil,
|
||||
ReleaseApiCount: &v.Data.APICount,
|
||||
IsReleased: &isReleased,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (i *imlPublishModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error {
|
||||
return nil
|
||||
//projects, err := m.serviceService.List(ctx)
|
||||
//projects, err := i.serviceService.List(ctx)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
@@ -74,7 +126,7 @@ func (m *imlPublishModule) initGateway(ctx context.Context, partitionId string,
|
||||
// return p.Id
|
||||
//})
|
||||
//for _, projectId := range projectIds {
|
||||
// releaseInfo, err := m.GetProjectRelease(ctx, projectId, partitionId)
|
||||
// releaseInfo, err := i.GetProjectRelease(ctx, projectId, partitionId)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
@@ -90,8 +142,8 @@ func (m *imlPublishModule) initGateway(ctx context.Context, partitionId string,
|
||||
//return nil
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID string, commitId string) (*gateway.ProjectRelease, error) {
|
||||
commits, err := m.releaseService.GetCommits(ctx, commitId)
|
||||
func (i *imlPublishModule) getProjectRelease(ctx context.Context, projectID string, commitId string) (*gateway.ProjectRelease, error) {
|
||||
commits, err := i.releaseService.GetCommits(ctx, commitId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -110,17 +162,17 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
|
||||
strategyCommitIds = append(strategyCommitIds, c.Commit)
|
||||
}
|
||||
}
|
||||
serviceInfo, err := m.serviceService.Get(ctx, projectID)
|
||||
serviceInfo, err := i.serviceService.Get(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiInfos, err := m.apiService.ListInfo(ctx, apiIds...)
|
||||
apiInfos, err := i.apiService.ListInfo(ctx, apiIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxyCommits, err := m.apiService.ListProxyCommit(ctx, apiProxyCommitIds...)
|
||||
proxyCommits, err := i.apiService.ListProxyCommit(ctx, apiProxyCommitIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,6 +185,41 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
|
||||
Id: projectID,
|
||||
Version: version,
|
||||
}
|
||||
upstreamProxyHeaders := make([]*gateway.ProxyHeader, 0)
|
||||
var upstreamRelease *gateway.UpstreamRelease
|
||||
if len(upstreamCommitIds) > 0 {
|
||||
upstreamCommits, err := i.upstreamService.ListCommit(ctx, upstreamCommitIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range upstreamCommits {
|
||||
upstreamRelease = &gateway.UpstreamRelease{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: c.Target,
|
||||
Version: version,
|
||||
MatchLabels: map[string]string{
|
||||
"serviceId": projectID,
|
||||
},
|
||||
},
|
||||
PassHost: c.Data.PassHost,
|
||||
Scheme: c.Data.Scheme,
|
||||
Balance: c.Data.Balance,
|
||||
Timeout: c.Data.Timeout,
|
||||
Nodes: utils.SliceToSlice(c.Data.Nodes, func(n *upstream.NodeConfig) string {
|
||||
return fmt.Sprintf("%s weight=%d", n.Address, n.Weight)
|
||||
}),
|
||||
}
|
||||
|
||||
upstreamProxyHeaders = utils.SliceToSlice(c.Data.ProxyHeaders, func(n *upstream.ProxyHeader) *gateway.ProxyHeader {
|
||||
return &gateway.ProxyHeader{
|
||||
Key: n.Key,
|
||||
Value: n.Value,
|
||||
Opt: n.OptType,
|
||||
}
|
||||
})
|
||||
}
|
||||
r.Upstream = upstreamRelease
|
||||
}
|
||||
apis := make([]*gateway.ApiRelease, 0, len(apiInfos))
|
||||
hasUpstream := len(upstreamCommitIds) > 0
|
||||
for _, a := range apiInfos {
|
||||
@@ -169,40 +256,17 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
|
||||
Opt: h.OptType,
|
||||
}
|
||||
})
|
||||
apiInfo.ProxyHeaders = append(apiInfo.ProxyHeaders, upstreamProxyHeaders...)
|
||||
|
||||
apiInfo.Retry = proxy.Retry
|
||||
apiInfo.Timeout = proxy.Timeout
|
||||
}
|
||||
apis = append(apis, apiInfo)
|
||||
}
|
||||
r.Apis = apis
|
||||
var upstreamRelease *gateway.UpstreamRelease
|
||||
if len(upstreamCommitIds) > 0 {
|
||||
upstreamCommits, err := m.upstreamService.ListCommit(ctx, upstreamCommitIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range upstreamCommits {
|
||||
upstreamRelease = &gateway.UpstreamRelease{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: c.Target,
|
||||
Version: version,
|
||||
MatchLabels: map[string]string{
|
||||
"serviceId": projectID,
|
||||
},
|
||||
},
|
||||
PassHost: c.Data.PassHost,
|
||||
Scheme: c.Data.Scheme,
|
||||
Balance: c.Data.Balance,
|
||||
Timeout: c.Data.Timeout,
|
||||
Nodes: utils.SliceToSlice(c.Data.Nodes, func(n *upstream.NodeConfig) string {
|
||||
return fmt.Sprintf("%s weight=%d", n.Address, n.Weight)
|
||||
}),
|
||||
}
|
||||
}
|
||||
r.Upstream = upstreamRelease
|
||||
}
|
||||
|
||||
if len(strategyCommitIds) > 0 {
|
||||
strategyCommits, err := m.strategyService.ListStrategyCommit(ctx, strategyCommitIds...)
|
||||
strategyCommits, err := i.strategyService.ListStrategyCommit(ctx, strategyCommitIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -234,9 +298,9 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) GetProjectRelease(ctx context.Context, projectID string, partitionId string) (*gateway.ProjectRelease, error) {
|
||||
func (i *imlPublishModule) GetProjectRelease(ctx context.Context, projectID string, partitionId string) (*gateway.ProjectRelease, error) {
|
||||
|
||||
releaseInfo, err := m.releaseService.GetRunning(ctx, projectID)
|
||||
releaseInfo, err := i.releaseService.GetRunning(ctx, projectID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
@@ -244,11 +308,11 @@ func (m *imlPublishModule) GetProjectRelease(ctx context.Context, projectID stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return m.getProjectRelease(ctx, projectID, releaseInfo.UUID)
|
||||
return i.getProjectRelease(ctx, projectID, releaseInfo.UUID)
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releaseId, version string, clusterIds []string) (map[string]*gateway.ProjectRelease, error) {
|
||||
projectRelease, err := m.getProjectRelease(ctx, projectID, releaseId)
|
||||
func (i *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releaseId, version string, clusterIds []string) (map[string]*gateway.ProjectRelease, error) {
|
||||
projectRelease, err := i.getProjectRelease(ctx, projectID, releaseId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -266,19 +330,19 @@ func (m *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releas
|
||||
return projectReleaseMap, nil
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string, id string) ([]*dto.PublishStatus, error) {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string, id string) ([]*dto.PublishStatus, error) {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flow, err := m.publishService.Get(ctx, id)
|
||||
flow, err := i.publishService.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flow.Service != serviceId {
|
||||
return nil, errors.New("服务不一致")
|
||||
}
|
||||
list, err := m.publishService.GetPublishStatus(ctx, id)
|
||||
list, err := i.publishService.GetPublishStatus(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -302,18 +366,18 @@ func (m *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string
|
||||
//
|
||||
// ctx context.Context, serviceId string, input *dto.ApplyInput
|
||||
// *dto.Publish, error
|
||||
func (m *imlPublishModule) Apply(ctx context.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) Apply(ctx context.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.checkPublish(ctx, serviceId, input.Release)
|
||||
err = i.checkPublish(ctx, serviceId, input.Release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
previous := ""
|
||||
running, err := m.releaseService.GetRunning(ctx, serviceId)
|
||||
running, err := i.releaseService.GetRunning(ctx, serviceId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
return nil, err
|
||||
@@ -322,42 +386,42 @@ func (m *imlPublishModule) Apply(ctx context.Context, serviceId string, input *d
|
||||
previous = running.UUID
|
||||
}
|
||||
|
||||
releaseToPublish, err := m.releaseService.GetRelease(ctx, input.Release)
|
||||
releaseToPublish, err := i.releaseService.GetRelease(ctx, input.Release)
|
||||
if err != nil {
|
||||
// 目标版本不存在
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newPublishId := uuid.NewString()
|
||||
diff, ok, err := m.projectDiffModule.DiffForLatest(ctx, serviceId, previous)
|
||||
diff, ok, err := i.projectDiffModule.DiffForLatest(ctx, serviceId, previous)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("latest completeness check failed")
|
||||
}
|
||||
err = m.publishService.Create(ctx, newPublishId, serviceId, releaseToPublish.UUID, previous, releaseToPublish.Version, input.Remark, diff)
|
||||
err = i.publishService.Create(ctx, newPublishId, serviceId, releaseToPublish.UUID, previous, releaseToPublish.Version, input.Remark, diff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
np, err := m.publishService.Get(ctx, newPublishId)
|
||||
np, err := i.publishService.Get(ctx, newPublishId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.FromModel(np, releaseToPublish.Remark), nil
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, releaseId string) (*dto.DiffOut, error) {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, releaseId string) (*dto.DiffOut, error) {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.checkPublish(ctx, serviceId, releaseId)
|
||||
err = i.checkPublish(ctx, serviceId, releaseId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
running, err := m.releaseService.GetRunning(ctx, serviceId)
|
||||
running, err := i.releaseService.GetRunning(ctx, serviceId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -367,30 +431,30 @@ func (m *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, r
|
||||
}
|
||||
if releaseId == "" {
|
||||
// 发布latest 版本
|
||||
diff, _, err := m.projectDiffModule.DiffForLatest(ctx, serviceId, runningReleaseId)
|
||||
diff, _, err := i.projectDiffModule.DiffForLatest(ctx, serviceId, runningReleaseId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.projectDiffModule.Out(ctx, diff)
|
||||
return i.projectDiffModule.Out(ctx, diff)
|
||||
} else {
|
||||
// 发布 releaseId 版本, 返回 与当前版本的差异
|
||||
diff, err := m.projectDiffModule.Diff(ctx, serviceId, runningReleaseId, releaseId)
|
||||
diff, err := i.projectDiffModule.Diff(ctx, serviceId, runningReleaseId, releaseId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.projectDiffModule.Out(ctx, diff)
|
||||
return i.projectDiffModule.Out(ctx, diff)
|
||||
}
|
||||
|
||||
}
|
||||
func (m *imlPublishModule) checkPublish(ctx context.Context, serviceId string, releaseId string) error {
|
||||
flows, err := m.publishService.ListForStatus(ctx, serviceId, publish.StatusApply, publish.StatusAccept)
|
||||
func (i *imlPublishModule) checkPublish(ctx context.Context, serviceId string, releaseId string) error {
|
||||
flows, err := i.publishService.ListForStatus(ctx, serviceId, publish.StatusApply, publish.StatusAccept)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if len(flows) > 0 {
|
||||
return errors.New("正在发布中")
|
||||
}
|
||||
running, err := m.releaseService.GetRunning(ctx, serviceId)
|
||||
running, err := i.releaseService.GetRunning(ctx, serviceId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
@@ -403,8 +467,8 @@ func (m *imlPublishModule) checkPublish(ctx context.Context, serviceId string, r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *imlPublishModule) Close(ctx context.Context, serviceId, id string) error {
|
||||
err := m.publishService.SetStatus(ctx, serviceId, id, publish.StatusClose)
|
||||
func (i *imlPublishModule) Close(ctx context.Context, serviceId, id string) error {
|
||||
err := i.publishService.SetStatus(ctx, serviceId, id, publish.StatusClose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -412,12 +476,12 @@ func (m *imlPublishModule) Close(ctx context.Context, serviceId, id string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) Stop(ctx context.Context, serviceId string, id string) error {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) Stop(ctx context.Context, serviceId string, id string) error {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flow, err := m.publishService.Get(ctx, id)
|
||||
flow, err := i.publishService.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -432,44 +496,44 @@ func (m *imlPublishModule) Stop(ctx context.Context, serviceId string, id string
|
||||
if flow.Status == publish.StatusApply {
|
||||
status = publish.StatusClose
|
||||
}
|
||||
return m.publishService.SetStatus(ctx, serviceId, id, status)
|
||||
return i.publishService.SetStatus(ctx, serviceId, id, status)
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) Refuse(ctx context.Context, serviceId string, id string, commits string) error {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) Refuse(ctx context.Context, serviceId string, id string, commits string) error {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.publishService.Refuse(ctx, serviceId, id, commits)
|
||||
return i.publishService.Refuse(ctx, serviceId, id, commits)
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) Accept(ctx context.Context, serviceId string, id string, commits string) error {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) Accept(ctx context.Context, serviceId string, id string, commits string) error {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.publishService.Accept(ctx, serviceId, id, commits)
|
||||
return i.publishService.Accept(ctx, serviceId, id, commits)
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) publish(ctx context.Context, id string, clusterId string, projectRelease *gateway.ProjectRelease) error {
|
||||
func (i *imlPublishModule) publish(ctx context.Context, id string, clusterId string, projectRelease *gateway.ProjectRelease) error {
|
||||
|
||||
publishStatus := &publish.Status{
|
||||
Publish: id,
|
||||
Status: publish.StatusPublishing,
|
||||
UpdateAt: time.Now(),
|
||||
}
|
||||
err := m.publishService.SetPublishStatus(ctx, publishStatus)
|
||||
err := i.publishService.SetPublishStatus(ctx, publishStatus)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set publishing publishStatus error: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
err := m.publishService.SetPublishStatus(ctx, publishStatus)
|
||||
err := i.publishService.SetPublishStatus(ctx, publishStatus)
|
||||
if err != nil {
|
||||
log.Errorf("set publishing publishStatus error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
client, err := m.clusterService.GatewayClient(ctx, clusterId)
|
||||
client, err := i.clusterService.GatewayClient(ctx, clusterId)
|
||||
if err != nil {
|
||||
publishStatus.Status = publish.StatusPublishError
|
||||
publishStatus.Error = err.Error()
|
||||
@@ -501,12 +565,12 @@ func (m *imlPublishModule) publish(ctx context.Context, id string, clusterId str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id string) error {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) Publish(ctx context.Context, serviceId string, id string) error {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flow, err := m.publishService.Get(ctx, id)
|
||||
flow, err := i.publishService.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -516,7 +580,7 @@ func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id str
|
||||
if flow.Status != publish.StatusAccept && flow.Status != publish.StatusDone {
|
||||
return errors.New("只有通过状态才能发布")
|
||||
}
|
||||
clusters, err := m.clusterService.List(ctx)
|
||||
clusters, err := i.clusterService.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -524,21 +588,21 @@ func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id str
|
||||
return i.Uuid
|
||||
})
|
||||
|
||||
projectReleaseMap, err := m.getReleaseInfo(ctx, serviceId, flow.Release, flow.Release, clusterIds)
|
||||
projectReleaseMap, err := i.getReleaseInfo(ctx, serviceId, flow.Release, flow.Release, clusterIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasError := false
|
||||
return m.transaction.Transaction(ctx, func(ctx context.Context) error {
|
||||
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
|
||||
for _, c := range clusters {
|
||||
err = m.publish(ctx, flow.Id, c.Uuid, projectReleaseMap[c.Uuid])
|
||||
err = i.publish(ctx, flow.Id, c.Uuid, projectReleaseMap[c.Uuid])
|
||||
if err != nil {
|
||||
hasError = true
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
err = m.releaseService.SetRunning(ctx, serviceId, flow.Release)
|
||||
err = i.releaseService.SetRunning(ctx, serviceId, flow.Release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -547,29 +611,38 @@ func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id str
|
||||
status = publish.StatusPublishError
|
||||
}
|
||||
if status == publish.StatusDone {
|
||||
info, err := m.serviceService.Get(ctx, serviceId)
|
||||
info, err := i.serviceService.Get(ctx, serviceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiDocCommit, err := i.apiDocService.LatestDocCommit(ctx, serviceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isReleased := true
|
||||
i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
|
||||
ReleaseApiCount: &apiDocCommit.Data.APICount,
|
||||
IsReleased: &isReleased,
|
||||
})
|
||||
if info.EnableMCP {
|
||||
err = m.updateMCPServer(ctx, serviceId, info.Name, flow.Version)
|
||||
err = mcp_server.SetServerByOpenapi(serviceId, info.Name, flow.Version, apiDocCommit.Data.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.publishService.SetStatus(ctx, serviceId, id, status)
|
||||
return i.publishService.SetStatus(ctx, serviceId, id, status)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) List(ctx context.Context, serviceId string, page, pageSize int) ([]*dto.Publish, int64, error) {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) List(ctx context.Context, serviceId string, page, pageSize int) ([]*dto.Publish, int64, error) {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
list, total, err := m.publishService.ListProjectPage(ctx, serviceId, page, pageSize)
|
||||
list, total, err := i.publishService.ListProjectPage(ctx, serviceId, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -595,95 +668,34 @@ func (i *imlPublishModule) updateMCPServer(ctx context.Context, sid string, name
|
||||
if err != nil {
|
||||
return fmt.Errorf("get api doc commit error: %w", err)
|
||||
}
|
||||
mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
|
||||
}
|
||||
tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis))
|
||||
for _, a := range mcpInfo.Apis {
|
||||
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
|
||||
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
|
||||
headers := make(map[string]interface{})
|
||||
queries := make(map[string]interface{})
|
||||
path := make(map[string]interface{})
|
||||
for _, v := range a.Params {
|
||||
p := map[string]interface{}{
|
||||
"type": "string",
|
||||
"required": v.Required,
|
||||
"description": v.Description,
|
||||
}
|
||||
switch v.In {
|
||||
case "header":
|
||||
headers[v.Name] = p
|
||||
case "query":
|
||||
queries[v.Name] = p
|
||||
case "path":
|
||||
path[v.Name] = p
|
||||
}
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers.")))
|
||||
}
|
||||
if len(queries) > 0 {
|
||||
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries.")))
|
||||
}
|
||||
if len(path) > 0 {
|
||||
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params.")))
|
||||
}
|
||||
if a.Body != nil {
|
||||
type Schema struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Properties map[string]interface{} `mapstructure:"properties"`
|
||||
Items interface{} `mapstructure:"items"`
|
||||
}
|
||||
var tmp Schema
|
||||
err = mapstructure.Decode(a.Body, &tmp)
|
||||
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...))
|
||||
}
|
||||
mcp_server.SetSSEServer(sid, name, version, tools...)
|
||||
return nil
|
||||
return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content)
|
||||
}
|
||||
|
||||
func (m *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) {
|
||||
_, err := m.serviceService.Check(ctx, serviceId, asServer)
|
||||
func (i *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) {
|
||||
_, err := i.serviceService.Check(ctx, serviceId, asServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flow, err := m.publishService.Get(ctx, id)
|
||||
flow, err := i.publishService.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flow.Service != serviceId {
|
||||
return nil, errors.New("项目不一致")
|
||||
}
|
||||
diff, err := m.publishService.GetDiff(ctx, id)
|
||||
diff, err := i.publishService.GetDiff(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := m.projectDiffModule.Out(ctx, diff)
|
||||
out, err := i.projectDiffModule.Out(ctx, diff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publishStatuses, err := m.PublishStatuses(ctx, serviceId, id)
|
||||
publishStatuses, err := i.PublishStatuses(ctx, serviceId, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
releaseInfo, err := m.releaseService.GetRelease(ctx, flow.Release)
|
||||
releaseInfo, err := i.releaseService.GetRelease(ctx, flow.Release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ func (m *imlReleaseModule) latestStrategyCommits(ctx context.Context, serviceId
|
||||
}
|
||||
|
||||
func (m *imlReleaseModule) Create(ctx context.Context, serviceId string, input *dto.CreateInput) (string, error) {
|
||||
|
||||
proInfo, err := m.projectService.Check(ctx, serviceId, projectRuleMustServer)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
@@ -30,7 +30,7 @@ type Create struct {
|
||||
MatchRules []Match `json:"match"`
|
||||
Upstream string `json:"upstream"`
|
||||
Proxy *InputProxy `json:"proxy"`
|
||||
Disable bool `json:"disabled"`
|
||||
Disable bool `json:"disable"`
|
||||
}
|
||||
|
||||
type InputProxy struct {
|
||||
@@ -70,7 +70,7 @@ type Edit struct {
|
||||
Methods *[]string `json:"methods"`
|
||||
Protocols *[]string `json:"protocols"`
|
||||
MatchRules *[]Match `json:"match"`
|
||||
Disable *bool `json:"disabled"`
|
||||
Disable *bool `json:"disable"`
|
||||
Upstream *string `json:"upstream"`
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ type Detail struct {
|
||||
SimpleDetail
|
||||
Proxy *Proxy `json:"proxy"`
|
||||
Protocols []string `json:"protocols"`
|
||||
Disable bool `json:"disabled"`
|
||||
Disable bool `json:"disable"`
|
||||
//Doc map[string]interface{} `json:"doc"`
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ type EditService struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
ServiceType *string `json:"service_type"`
|
||||
Prefix *string `json:"prefix"`
|
||||
Catalogue *string `json:"catalogue"`
|
||||
Logo *string `json:"logo"`
|
||||
Tags *[]string `json:"tags"`
|
||||
|
||||
+54
-79
@@ -10,14 +10,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
|
||||
|
||||
"github.com/APIParkLab/APIPark/common"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/eolinker/go-common/register"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
|
||||
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
|
||||
|
||||
"github.com/APIParkLab/APIPark/service/release"
|
||||
@@ -83,11 +81,12 @@ type imlServiceModule struct {
|
||||
tagService tag.ITagService `autowired:""`
|
||||
localModelService ai_local.ILocalModelService `autowired:""`
|
||||
|
||||
serviceTagService service_tag.ITagService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
subscribeServer subscribe.ISubscribeService `autowired:""`
|
||||
serviceOverviewService service_overview.IOverviewService `autowired:""`
|
||||
serviceTagService service_tag.ITagService `autowired:""`
|
||||
apiService api.IAPIService `autowired:""`
|
||||
apiDocService api_doc.IAPIDocService `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
subscribeServer subscribe.ISubscribeService `autowired:""`
|
||||
|
||||
releaseService release.IReleaseService `autowired:""`
|
||||
serviceModelMappingService service_model_mapping.IServiceModelMappingService `autowired:""`
|
||||
@@ -304,6 +303,7 @@ func (i *imlServiceModule) OnInit() {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range services {
|
||||
err = i.updateMCPServer(ctx, s.Id, s.Name, "1.0")
|
||||
if err != nil {
|
||||
@@ -311,6 +311,28 @@ func (i *imlServiceModule) OnInit() {
|
||||
return
|
||||
}
|
||||
}
|
||||
overviews, err := i.serviceOverviewService.List(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if len(overviews) > 0 {
|
||||
return
|
||||
}
|
||||
countMap, err := i.apiDocService.APICountByServices(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
for k, v := range countMap {
|
||||
err = i.serviceOverviewService.Update(ctx, k, &service_overview.Update{
|
||||
ApiCount: &v,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -369,67 +391,11 @@ func (i *imlServiceModule) updateMCPServer(ctx context.Context, sid string, name
|
||||
if err != nil {
|
||||
return fmt.Errorf("get api doc commit error: %w", err)
|
||||
}
|
||||
mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
|
||||
}
|
||||
tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis))
|
||||
for _, a := range mcpInfo.Apis {
|
||||
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
|
||||
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
|
||||
headers := make(map[string]interface{})
|
||||
queries := make(map[string]interface{})
|
||||
path := make(map[string]interface{})
|
||||
for _, v := range a.Params {
|
||||
p := map[string]interface{}{
|
||||
"type": "string",
|
||||
"required": v.Required,
|
||||
"description": v.Description,
|
||||
}
|
||||
switch v.In {
|
||||
case "header":
|
||||
headers[v.Name] = p
|
||||
case "query":
|
||||
queries[v.Name] = p
|
||||
case "path":
|
||||
path[v.Name] = p
|
||||
}
|
||||
}
|
||||
if len(headers) > 0 {
|
||||
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers.")))
|
||||
}
|
||||
if len(queries) > 0 {
|
||||
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries.")))
|
||||
}
|
||||
if len(path) > 0 {
|
||||
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params.")))
|
||||
}
|
||||
if a.Body != nil {
|
||||
type Schema struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Properties map[string]interface{} `mapstructure:"properties"`
|
||||
Items interface{} `mapstructure:"items"`
|
||||
}
|
||||
var tmp Schema
|
||||
err = mapstructure.Decode(a.Body, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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.")))
|
||||
}
|
||||
}
|
||||
tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...))
|
||||
}
|
||||
mcp_server.SetSSEServer(sid, name, version, tools...)
|
||||
return nil
|
||||
return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content)
|
||||
}
|
||||
|
||||
func (i *imlServiceModule) deleteMCPServer(ctx context.Context, sid string) {
|
||||
mcp_server.DelSSEServer(sid)
|
||||
mcp_server.DelServer(sid)
|
||||
}
|
||||
|
||||
func (i *imlServiceModule) ExportAll(ctx context.Context) ([]*service_dto.ExportService, error) {
|
||||
@@ -510,7 +476,14 @@ func (i *imlServiceModule) SearchMyServices(ctx context.Context, teamId string,
|
||||
serviceIds := utils.SliceToSlice(services, func(p *service.Service) string {
|
||||
return p.Id
|
||||
})
|
||||
apiCountMap, err := i.apiDocService.APICountByServices(ctx, serviceIds...)
|
||||
//apiCountMap, err := i.apiDocService.APICountByServices(ctx, serviceIds...)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//serviceIds := utils.SliceToSlice(services, func(s *service.Service) string {
|
||||
// return s.Id
|
||||
//})
|
||||
overviewMap, err := i.serviceOverviewService.Map(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -520,10 +493,12 @@ func (i *imlServiceModule) SearchMyServices(ctx context.Context, teamId string,
|
||||
if teamId != "" && model.Team != teamId {
|
||||
continue
|
||||
}
|
||||
apiCount := apiCountMap[model.Id]
|
||||
item := toServiceItem(model)
|
||||
item.ApiNum = apiCount
|
||||
item.CanDelete = apiCount == 0
|
||||
if ov, ok := overviewMap[model.Id]; ok {
|
||||
item.ApiNum = ov.ApiCount
|
||||
item.CanDelete = ov.ApiCount == 0
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
|
||||
}
|
||||
@@ -629,22 +604,21 @@ func (i *imlServiceModule) Search(ctx context.Context, teamID string, keyword st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceIds := utils.SliceToSlice(list, func(s *service.Service) string {
|
||||
return s.Id
|
||||
})
|
||||
|
||||
apiCountMap, err := i.apiDocService.APICountByServices(ctx, serviceIds...)
|
||||
overviewMap, err := i.serviceOverviewService.Map(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]*service_dto.ServiceItem, 0, len(list))
|
||||
for _, model := range list {
|
||||
apiCount := apiCountMap[model.Id]
|
||||
item := toServiceItem(model)
|
||||
item.ApiNum = apiCount
|
||||
item.CanDelete = apiCount == 0
|
||||
if v, ok := overviewMap[model.Id]; ok {
|
||||
item.ApiNum = v.ApiCount
|
||||
item.CanDelete = v.ApiCount == 0
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
@@ -734,7 +708,7 @@ func (i *imlServiceModule) Create(ctx context.Context, teamID string, input *ser
|
||||
mo.AsServer = *input.AsServer
|
||||
}
|
||||
|
||||
input.Prefix = strings.Trim(strings.Trim(input.Prefix, " "), "/")
|
||||
//input.Prefix = strings.Trim(strings.Trim(input.Prefix, " "), "/")
|
||||
err := i.transaction.Transaction(ctx, func(ctx context.Context) error {
|
||||
if input.Tags != nil {
|
||||
tags, err := i.getTagUuids(ctx, input.Tags)
|
||||
@@ -839,6 +813,7 @@ func (i *imlServiceModule) Edit(ctx context.Context, id string, input *service_d
|
||||
ServiceType: serviceType,
|
||||
Catalogue: input.Catalogue,
|
||||
AdditionalConfig: &info.AdditionalConfig,
|
||||
Prefix: input.Prefix,
|
||||
ApprovalType: &approvalType,
|
||||
EnableMCP: input.EnableMCP,
|
||||
}
|
||||
|
||||
@@ -41,6 +41,46 @@ type imlAPIKeyModule struct {
|
||||
transaction store.ITransaction `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyModule) MyAPIKeysByApp(ctx context.Context, appId string) ([]*system_apikey_dto.AuthorizationItem, error) {
|
||||
appInfo, err := i.serviceService.Get(ctx, appId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auths, err := i.applicationAuthorizationService.ListByApp(ctx, appId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]*system_apikey_dto.AuthorizationItem)
|
||||
for _, a := range auths {
|
||||
if a.Type != "apikey" {
|
||||
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) MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) {
|
||||
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ type IAPIKeyModule interface {
|
||||
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)
|
||||
MyAPIKeysByApp(ctx context.Context, appId string) ([]*system_apikey_dto.AuthorizationItem, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -15,15 +15,22 @@ func (p *plugin) mcpAPIs() []pm3.Api {
|
||||
serviceMessagePath := fmt.Sprintf("/api/v1/%s/:serviceId/message", mcp_server.ServiceBasePath)
|
||||
globalSSEPath := fmt.Sprintf("/api/v1/%s/sse", mcp_server.GlobalBasePath)
|
||||
globalMessagePath := fmt.Sprintf("/api/v1/%s/message", mcp_server.GlobalBasePath)
|
||||
appSSEPath := fmt.Sprintf("/api/v1/%s/sse", mcp_server.AppBasePath)
|
||||
appMessagePath := fmt.Sprintf("/api/v1/%s/message", mcp_server.AppBasePath)
|
||||
ignore.IgnorePath("login", http.MethodGet, serviceSSEPath)
|
||||
ignore.IgnorePath("login", http.MethodPost, serviceMessagePath)
|
||||
ignore.IgnorePath("login", http.MethodGet, globalSSEPath)
|
||||
ignore.IgnorePath("login", http.MethodPost, globalMessagePath)
|
||||
ignore.IgnorePath("login", http.MethodGet, appSSEPath)
|
||||
ignore.IgnorePath("login", http.MethodPost, appMessagePath)
|
||||
return []pm3.Api{
|
||||
pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.MCPHandle),
|
||||
pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.MCPHandle),
|
||||
pm3.CreateApiSimple(http.MethodGet, globalSSEPath, p.mcpController.GlobalMCPHandle),
|
||||
pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalMCPHandle),
|
||||
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/global/mcp/config", []string{"context"}, []string{"config"}, p.mcpController.GlobalMCPConfig),
|
||||
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/mcp/config", []string{"context", "query:app"}, []string{"config"}, p.mcpController.AppMCPConfig),
|
||||
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/api/v1/%s/:app/sse", mcp_server.AppBasePath), p.mcpController.AppMCPHandle),
|
||||
pm3.CreateApiSimple(http.MethodPost, fmt.Sprintf("/api/v1/%s/:app/message", mcp_server.AppBasePath), p.mcpController.AppMCPHandle),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (p *plugin) systemApikeyApis() []pm3.Api {
|
||||
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/app/apikeys", []string{"context", "query:service"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService),
|
||||
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/app/apikeys", []string{"context", "query:service", "query:app"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService),
|
||||
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/system/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.SimpleList),
|
||||
}
|
||||
}
|
||||
|
||||
+26
-6
@@ -5,25 +5,45 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/eolinker/go-common/ignore"
|
||||
|
||||
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
|
||||
"github.com/eolinker/go-common/ignore"
|
||||
"github.com/eolinker/go-common/pm3"
|
||||
)
|
||||
|
||||
func (p *plugin) mcpAPIs() []pm3.Api {
|
||||
globalSSEPath := fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/"))
|
||||
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)
|
||||
|
||||
appSSEPath := fmt.Sprintf("/openapi/v1/%s/:app/sse", strings.Trim(mcp_server.AppBasePath, "/"))
|
||||
appMessagePath := fmt.Sprintf("/openapi/v1/%s/:app/message", strings.Trim(mcp_server.AppBasePath, "/"))
|
||||
|
||||
serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/"))
|
||||
serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", strings.Trim(mcp_server.ServiceBasePath, "/"))
|
||||
serviceStreamablePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/mcp", strings.Trim(mcp_server.ServiceBasePath, "/"))
|
||||
|
||||
ignore.IgnorePath("openapi", http.MethodPost, globalMessagePath)
|
||||
ignore.IgnorePath("openapi", http.MethodGet, globalSSEPath)
|
||||
ignore.IgnorePath("openapi", http.MethodPost, appMessagePath)
|
||||
ignore.IgnorePath("openapi", http.MethodGet, appSSEPath)
|
||||
ignore.IgnorePath("openapi", http.MethodGet, serviceSSEPath)
|
||||
ignore.IgnorePath("openapi", http.MethodPost, serviceMessagePath)
|
||||
ignore.IgnorePath("openapi", http.MethodGet, serviceStreamablePath)
|
||||
ignore.IgnorePath("openapi", http.MethodPost, serviceStreamablePath)
|
||||
ignore.IgnorePath("openapi", http.MethodDelete, serviceStreamablePath)
|
||||
|
||||
return []pm3.Api{
|
||||
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/")), p.mcpController.GlobalHandleSSE),
|
||||
pm3.CreateApiSimple(http.MethodGet, globalSSEPath, p.mcpController.GlobalHandleSSE),
|
||||
pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalHandleMessage),
|
||||
|
||||
pm3.CreateApiSimple(http.MethodGet, appSSEPath, p.mcpController.AppHandleSSE),
|
||||
pm3.CreateApiSimple(http.MethodPost, appMessagePath, p.mcpController.AppHandleMessage),
|
||||
|
||||
pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.ServiceHandleSSE),
|
||||
pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.ServiceHandleMessage),
|
||||
|
||||
pm3.CreateApiSimple(http.MethodPost, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
|
||||
pm3.CreateApiSimple(http.MethodDelete, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
|
||||
pm3.CreateApiSimple(http.MethodGet, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -202,6 +202,16 @@ curl -sSO https://download.apipark.com/install/quick-start.sh; bash quick-start.
|
||||
|
||||
# 📕文档
|
||||
访问 [APIPark文档](https://docs.apipark.com/docs/deploy) 获取详细的安装指南、API 参考和使用说明。
|
||||
<br>
|
||||
|
||||
|
||||
|
||||
友情链接
|
||||
<a href="https://xroute.ai/">
|
||||
|
||||
<img width="1248" height="158" alt="新建 PPTX 演示文稿 (2)_02(1)" src="https://github.com/user-attachments/assets/3e1cd0c1-c4c3-4f0c-8649-810d76f3166b" />
|
||||
|
||||
</a>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -131,6 +131,16 @@ system:
|
||||
value: 'manager'
|
||||
dependents:
|
||||
- system.settings.ai_api.view
|
||||
- name: login
|
||||
value: 'login'
|
||||
children:
|
||||
- name: view
|
||||
value: 'view'
|
||||
guest_allow: true
|
||||
- name: manager
|
||||
value: 'manager'
|
||||
dependents:
|
||||
- system.settings.login.view
|
||||
- name: ai log
|
||||
value: 'ai_log'
|
||||
children:
|
||||
@@ -294,6 +304,17 @@ team:
|
||||
value: 'manager_subscribed_services'
|
||||
dependents:
|
||||
- team.consumer.subscription.manager_subscribed_services
|
||||
- name: mcp
|
||||
cname: MCP
|
||||
value: 'mcp'
|
||||
children:
|
||||
- name: view
|
||||
value: 'view'
|
||||
guest_allow: true
|
||||
- name: manager
|
||||
value: 'manager'
|
||||
dependents:
|
||||
- team.consumer.mcp.view
|
||||
- name: authorization
|
||||
value: 'authorization'
|
||||
children:
|
||||
|
||||
@@ -26,6 +26,8 @@ system:
|
||||
- system.settings.general.view
|
||||
- system.settings.log_configuration.manager
|
||||
- system.settings.log_configuration.view
|
||||
- system.settings.login.manager
|
||||
- system.settings.login.view
|
||||
- system.settings.mcp.view
|
||||
- system.settings.mcp.manager
|
||||
- system.settings.role.view
|
||||
@@ -69,6 +71,8 @@ system:
|
||||
- system.settings.general.view
|
||||
- system.settings.log_configuration.manager
|
||||
- system.settings.log_configuration.view
|
||||
- system.settings.login.manager
|
||||
- system.settings.login.view
|
||||
- system.settings.ssl_certificate.manager
|
||||
- system.settings.ssl_certificate.view
|
||||
- system.settings.strategy.view
|
||||
@@ -88,6 +92,8 @@ team:
|
||||
permits:
|
||||
- team.consumer.authorization.manager
|
||||
- team.consumer.authorization.view
|
||||
- team.consumer.mcp.manager
|
||||
- team.consumer.mcp.view
|
||||
- team.consumer.subscription.manager_subscribed_services
|
||||
- team.consumer.subscription.subscribe
|
||||
- team.consumer.subscription.view_subscribed_service
|
||||
@@ -161,6 +167,8 @@ team:
|
||||
permits:
|
||||
- team.consumer.authorization.manager
|
||||
- team.consumer.authorization.view
|
||||
- team.consumer.mcp.manager
|
||||
- team.consumer.mcp.view
|
||||
- team.consumer.subscription.manager_subscribed_services
|
||||
- team.consumer.subscription.subscribe
|
||||
- team.consumer.subscription.view_subscribed_service
|
||||
@@ -172,9 +180,10 @@ team:
|
||||
- name: consumer developer
|
||||
value: consumer_developer
|
||||
permits:
|
||||
- team.consumer.application.manager
|
||||
- team.consumer.authorization.manager
|
||||
- team.consumer.authorization.view
|
||||
- team.consumer.mcp.manager
|
||||
- team.consumer.mcp.view
|
||||
- team.consumer.subscription.subscribe
|
||||
- team.consumer.subscription.view_subscribed_service
|
||||
- team.team.consumer.view
|
||||
|
||||
@@ -29,9 +29,11 @@ VERSION=$(gen_version)
|
||||
|
||||
|
||||
SYS_ARCH=$(arch)
|
||||
echo "SYS_ARCH: ${SYS_ARCH}"
|
||||
echo "ARCH: ${ARCH}"
|
||||
if [[ (${SYS_ARCH} == "aarch64" || ${SYS_ARCH} == "arm64") && $ARCH == "amd64" ]];then
|
||||
OPTIONS="--platform=linux/amd64"
|
||||
elif [[ ${SYS_ARCH} == "amd64" && $ARCH == "arm64" ]];then
|
||||
elif [[ (${SYS_ARCH} == "amd64" || ${SYS_ARCH} == "x86_64") && $ARCH == "arm64" ]];then
|
||||
OPTIONS="--platform=linux/arm64"
|
||||
fi
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ echo "login qiniu..."
|
||||
qshell account ${AccessKey} ${SecretKey} ${QINIU_NAME}
|
||||
|
||||
echo "qshell rput ${QINIU_BUCKET} \"${APP}/images/${Tar}\" ${Tar}"
|
||||
qshell rput ${QINIU_BUCKET} "${APP}/images/${Tar}" ${Tar}
|
||||
qshell rput --overwrite ${QINIU_BUCKET} "${APP}/images/${Tar}" ${Tar}
|
||||
|
||||
rm -f ${Tar}
|
||||
docker rmi -f ${ImageName}:${Version}
|
||||
+14
-10
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/eolinker/eosc/log"
|
||||
|
||||
"github.com/APIParkLab/APIPark/service/universally/commit"
|
||||
"github.com/APIParkLab/APIPark/stores/api"
|
||||
"github.com/eolinker/go-common/utils"
|
||||
@@ -59,7 +61,9 @@ func (i *imlAPIDocService) APICountByServices(ctx context.Context, serviceIds ..
|
||||
if len(serviceIds) > 0 {
|
||||
w["service"] = serviceIds
|
||||
}
|
||||
now := time.Now()
|
||||
list, err := i.store.List(ctx, w)
|
||||
log.Infof("search api doc count by services, serviceIds: %v, cost: %v", serviceIds, time.Since(now))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,35 +72,35 @@ func (i *imlAPIDocService) APICountByServices(ctx context.Context, serviceIds ..
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (i *imlAPIDocService) UpdateDoc(ctx context.Context, serviceId string, input *UpdateDoc) error {
|
||||
func (i *imlAPIDocService) UpdateDoc(ctx context.Context, serviceId string, input *UpdateDoc) (int64, error) {
|
||||
doc, err := NewDocLoader(input.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if err := doc.Valid(); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if input.Prefix != "" {
|
||||
err = doc.AddPrefixInAll(input.Prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
data, err := doc.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
input.Content = string(data)
|
||||
|
||||
operator := utils.UserId(ctx)
|
||||
info, err := i.store.First(ctx, map[string]interface{}{
|
||||
"service": serviceId,
|
||||
})
|
||||
operator := utils.UserId(ctx)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
return i.store.Insert(ctx, &api.Doc{
|
||||
return doc.APICount(), i.store.Insert(ctx, &api.Doc{
|
||||
UUID: input.ID,
|
||||
Service: serviceId,
|
||||
Content: input.Content,
|
||||
@@ -109,7 +113,7 @@ func (i *imlAPIDocService) UpdateDoc(ctx context.Context, serviceId string, inpu
|
||||
info.Updater = operator
|
||||
info.UpdateAt = time.Now()
|
||||
info.APICount = doc.APICount()
|
||||
return i.store.Save(ctx, info)
|
||||
return doc.APICount(), i.store.Save(ctx, info)
|
||||
}
|
||||
|
||||
func (i *imlAPIDocService) GetDoc(ctx context.Context, serviceId string) (*Doc, error) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type IAPIDocService interface {
|
||||
// UpdateDoc 更新文档
|
||||
UpdateDoc(ctx context.Context, serviceId string, input *UpdateDoc) error
|
||||
UpdateDoc(ctx context.Context, serviceId string, input *UpdateDoc) (int64, error)
|
||||
// GetDoc 获取文档
|
||||
GetDoc(ctx context.Context, serviceId string) (*Doc, error)
|
||||
|
||||
|
||||
@@ -28,6 +28,43 @@ type imlReleaseService struct {
|
||||
releaseRuntime release.IReleaseRuntime `autowired:""`
|
||||
}
|
||||
|
||||
func (s *imlReleaseService) UpdateRelease(ctx context.Context, id string, update *Update) error {
|
||||
info, err := s.releaseStore.GetByUUID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if update.Version != nil {
|
||||
info.Name = *update.Version
|
||||
}
|
||||
if update.Remark != nil {
|
||||
info.Remark = *update.Remark
|
||||
}
|
||||
|
||||
_, err = s.releaseStore.Update(ctx, info)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *imlReleaseService) GetRunningList(ctx context.Context, serviceId ...string) ([]*Release, error) {
|
||||
w := make(map[string]interface{})
|
||||
if len(serviceId) > 0 {
|
||||
w["service"] = serviceId
|
||||
}
|
||||
list, err := s.releaseRuntime.List(ctx, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
commitIds := utils.SliceToSlice(list, func(o *release.Runtime) string {
|
||||
return o.Release
|
||||
})
|
||||
commits, err := s.releaseStore.List(ctx, map[string]interface{}{
|
||||
"uuid": commitIds,
|
||||
})
|
||||
return utils.SliceToSlice(commits, FromEntity), err
|
||||
}
|
||||
|
||||
func (s *imlReleaseService) GetRunningApiDocCommits(ctx context.Context, serviceIds ...string) ([]string, error) {
|
||||
w := make(map[string]interface{})
|
||||
if len(serviceIds) > 0 {
|
||||
|
||||
@@ -26,6 +26,12 @@ func FromEntity(e *release.Release) *Release {
|
||||
}
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Version *string
|
||||
Remark *string
|
||||
APICount *int64 // API数量
|
||||
}
|
||||
|
||||
type APICommit struct {
|
||||
Release string
|
||||
API string
|
||||
|
||||
@@ -15,12 +15,15 @@ type IReleaseService interface {
|
||||
GetRelease(ctx context.Context, id string) (*Release, error)
|
||||
// CreateRelease 创建发布
|
||||
CreateRelease(ctx context.Context, service, version, remark string, apiRequestCommit, apisProxyCommits map[string]string, apiDocCommits, serviceDocCommits string, upstreams map[string]map[string]string, strategies map[string]string) (*Release, error)
|
||||
UpdateRelease(ctx context.Context, id string, update *Update) error
|
||||
// DeleteRelease 删除发布
|
||||
DeleteRelease(ctx context.Context, id string) error
|
||||
|
||||
GetRunningApiDocCommits(ctx context.Context, serviceIds ...string) ([]string, error)
|
||||
List(ctx context.Context, service string) ([]*Release, error)
|
||||
GetReleaseInfos(ctx context.Context, id string) ([]*APICommit, []*APICommit, *APICommit, []*UpstreamCommit, *ServiceCommit, error)
|
||||
GetCommits(ctx context.Context, id string) ([]*ProjectCommits, error)
|
||||
GetRunningApiDocCommits(ctx context.Context, serviceIds ...string) ([]string, error)
|
||||
//GetRunningApiDocCommits(ctx context.Context, serviceIds ...string) ([]string, error)
|
||||
GetRunningApiProxyCommit(ctx context.Context, service string, apiUUID string) (string, error)
|
||||
Completeness(partitions []string, apis []string, requestCommits []*commit.Commit[api.Request], proxyCommits []*commit.Commit[api.Proxy], upstreamCommits []*commit.Commit[upstream.Config]) bool
|
||||
|
||||
@@ -30,6 +33,7 @@ type IReleaseService interface {
|
||||
// service: the service name
|
||||
// Return type(s): *Release, error
|
||||
GetRunning(ctx context.Context, service string) (*Release, error)
|
||||
GetRunningList(ctx context.Context, serviceId ...string) ([]*Release, error)
|
||||
|
||||
SetRunning(ctx context.Context, service string, id string) error
|
||||
CheckNewVersion(ctx context.Context, service string, version string) (bool, error)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package service_overview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/eolinker/go-common/utils"
|
||||
|
||||
"github.com/APIParkLab/APIPark/stores/service"
|
||||
)
|
||||
|
||||
var _ IOverviewService = (*imlOverviewService)(nil)
|
||||
|
||||
type imlOverviewService struct {
|
||||
store service.IOverviewStore `autowired:""`
|
||||
}
|
||||
|
||||
func genUpdateFields(info *service.Overview, update *Update) {
|
||||
if update.ApiCount != nil {
|
||||
info.ApiCount = *update.ApiCount
|
||||
}
|
||||
if update.ReleaseApiCount != nil {
|
||||
info.ReleaseApiCount = *update.ReleaseApiCount
|
||||
}
|
||||
if update.IsReleased != nil {
|
||||
info.IsReleased = *update.IsReleased
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i imlOverviewService) Update(ctx context.Context, serviceId string, update *Update) error {
|
||||
if update == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := i.store.First(ctx, map[string]interface{}{
|
||||
"service": serviceId,
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
info = &service.Overview{
|
||||
Service: serviceId,
|
||||
}
|
||||
genUpdateFields(info, update)
|
||||
return i.store.Insert(ctx, info)
|
||||
}
|
||||
genUpdateFields(info, update)
|
||||
_, err = i.store.Update(ctx, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i imlOverviewService) List(ctx context.Context, serviceIds ...string) ([]*Overview, error) {
|
||||
w := make(map[string]interface{})
|
||||
if len(serviceIds) > 0 {
|
||||
w = map[string]interface{}{
|
||||
"service": serviceIds,
|
||||
}
|
||||
}
|
||||
list, err := i.store.List(ctx, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceToSlice(list, FromEntity), nil
|
||||
}
|
||||
|
||||
func (i imlOverviewService) Map(ctx context.Context, serviceIds ...string) (map[string]*Overview, error) {
|
||||
list, err := i.List(ctx, serviceIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceToMap(list, func(i *Overview) string {
|
||||
return i.Service
|
||||
}), nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package service_overview
|
||||
|
||||
import (
|
||||
"github.com/APIParkLab/APIPark/stores/service"
|
||||
)
|
||||
|
||||
type Overview struct {
|
||||
Service string
|
||||
ApiCount int64
|
||||
ReleaseApiCount int64
|
||||
IsReleased bool
|
||||
}
|
||||
|
||||
func FromEntity(e *service.Overview) *Overview {
|
||||
return &Overview{
|
||||
Service: e.Service,
|
||||
ApiCount: e.ApiCount,
|
||||
ReleaseApiCount: e.ReleaseApiCount,
|
||||
IsReleased: e.IsReleased,
|
||||
}
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
ApiCount *int64
|
||||
ReleaseApiCount *int64
|
||||
IsReleased *bool
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package service_overview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/eolinker/go-common/autowire"
|
||||
)
|
||||
|
||||
type IOverviewService interface {
|
||||
Update(ctx context.Context, serviceId string, update *Update) error
|
||||
List(ctx context.Context, serviceIds ...string) ([]*Overview, error)
|
||||
Map(ctx context.Context, serviceIds ...string) (map[string]*Overview, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
autowire.Auto[IOverviewService](func() reflect.Value {
|
||||
return reflect.ValueOf(new(imlOverviewService))
|
||||
})
|
||||
}
|
||||
@@ -229,5 +229,8 @@ func updateHandler(e *service.Service, i *Edit) {
|
||||
if i.EnableMCP != nil {
|
||||
e.EnableMCP = *i.EnableMCP
|
||||
}
|
||||
//if i.Prefix != nil {
|
||||
// e.Prefix = *i.Prefix
|
||||
//}
|
||||
e.UpdateAt = time.Now()
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ type Edit struct {
|
||||
AdditionalConfig *map[string]string
|
||||
State *int
|
||||
ApprovalType *ApprovalType
|
||||
Prefix *string
|
||||
EnableMCP *bool
|
||||
}
|
||||
|
||||
|
||||
@@ -58,11 +58,12 @@ func (s *imlServiceGetSoftDelete[T, E]) List(ctx context.Context, uuids ...strin
|
||||
return utils.SliceToSlice(list, s.toModelHandler), nil
|
||||
}
|
||||
func (s *imlServiceGetSoftDelete[T, E]) Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) {
|
||||
if condition == nil {
|
||||
condition = make(map[string]interface{})
|
||||
w := make(map[string]interface{})
|
||||
for k, v := range condition {
|
||||
w[k] = v
|
||||
}
|
||||
condition[SoftDeleteField] = false
|
||||
ps, err := s.store.Search(ctx, keyword, condition, sortRule...)
|
||||
w[SoftDeleteField] = false
|
||||
ps, err := s.store.Search(ctx, keyword, w, sortRule...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,11 +71,12 @@ func (s *imlServiceGetSoftDelete[T, E]) Search(ctx context.Context, keyword stri
|
||||
}
|
||||
|
||||
func (s *imlServiceGetSoftDelete[T, E]) SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) {
|
||||
if condition == nil {
|
||||
condition = make(map[string]interface{})
|
||||
w := make(map[string]interface{})
|
||||
for k, v := range condition {
|
||||
w[k] = v
|
||||
}
|
||||
condition[SoftDeleteField] = false
|
||||
ps, total, err := s.store.SearchByPage(ctx, keyword, condition, page, pageSize, sortRule...)
|
||||
w[SoftDeleteField] = false
|
||||
ps, total, err := s.store.SearchByPage(ctx, keyword, w, page, pageSize, sortRule...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -71,7 +71,11 @@ func (s *imlServiceGet[T, E]) List(ctx context.Context, uuids ...string) ([]*T,
|
||||
return utils.SliceToSlice(list, s.toModelHandler), nil
|
||||
}
|
||||
func (s *imlServiceGet[T, E]) Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) {
|
||||
ps, err := s.store.Search(ctx, keyword, condition, sortRule...)
|
||||
w := make(map[string]interface{})
|
||||
for k, v := range condition {
|
||||
w[k] = v
|
||||
}
|
||||
ps, err := s.store.Search(ctx, keyword, w, sortRule...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -79,7 +83,11 @@ func (s *imlServiceGet[T, E]) Search(ctx context.Context, keyword string, condit
|
||||
}
|
||||
|
||||
func (s *imlServiceGet[T, E]) SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) {
|
||||
ps, total, err := s.store.SearchByPage(ctx, keyword, condition, page, pageSize, sortRule...)
|
||||
w := make(map[string]interface{})
|
||||
for k, v := range condition {
|
||||
w[k] = v
|
||||
}
|
||||
ps, total, err := s.store.SearchByPage(ctx, keyword, w, page, pageSize, sortRule...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ type Doc struct {
|
||||
Content string `gorm:"type:longtext;null;column:content;comment:文档内容"`
|
||||
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:更新时间"`
|
||||
APICount int64 `gorm:"type:int(11);not null;column:api_count;comment:接口数量"`
|
||||
APICount int64 `gorm:"type:int(11);not null;column:api_count;default:0;comment:接口数量"`
|
||||
}
|
||||
|
||||
func (i *Doc) TableName() string {
|
||||
|
||||
@@ -35,6 +35,22 @@ func (p *Service) TableName() string {
|
||||
return "service"
|
||||
}
|
||||
|
||||
type Overview struct {
|
||||
Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"`
|
||||
Service string `gorm:"size:255;not null;column:service;comment:服务ID"`
|
||||
ApiCount int64 `gorm:"type:BIGINT(20);not null;column:api_count;comment:接口数量"`
|
||||
ReleaseApiCount int64 `gorm:"type:BIGINT(20);not null;column:release_api_count;comment:已发布接口数量"`
|
||||
IsReleased bool `gorm:"type:tinyint(1);not null;column:is_released;comment:是否已发布"`
|
||||
}
|
||||
|
||||
func (o *Overview) IdValue() int64 {
|
||||
return o.Id
|
||||
}
|
||||
|
||||
func (o *Overview) TableName() string {
|
||||
return "service_overview"
|
||||
}
|
||||
|
||||
type Authorization struct {
|
||||
Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"`
|
||||
UUID string `gorm:"size:36;not null;column:uuid;uniqueIndex:uuid;comment:UUID;"`
|
||||
|
||||
@@ -13,6 +13,12 @@ type IServiceStore interface {
|
||||
type imlServiceStore struct {
|
||||
store.SearchStore[Service]
|
||||
}
|
||||
type IOverviewStore interface {
|
||||
store.IBaseStore[Overview]
|
||||
}
|
||||
type imlOverviewStore struct {
|
||||
store.Store[Overview]
|
||||
}
|
||||
|
||||
type IServiceTagStore interface {
|
||||
store.IBaseStore[Tag]
|
||||
@@ -61,6 +67,9 @@ func init() {
|
||||
return reflect.ValueOf(new(imlServiceDocStore))
|
||||
})
|
||||
|
||||
autowire.Auto[IOverviewStore](func() reflect.Value {
|
||||
return reflect.ValueOf(new(imlOverviewStore))
|
||||
})
|
||||
autowire.Auto[IServiceModelMappingStore](func() reflect.Value {
|
||||
return reflect.ValueOf(new(imlServiceModelMappingStore))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user