Compare commits

..

34 Commits

Author SHA1 Message Date
lcx 1942344373 feat: 三端合一 2026-05-06 13:53:14 +08:00
lcx 96ad58fe28 feat: 三端合一 2026-04-29 15:39:28 +08:00
lcx abb4963bac fix: issue#395 2025-10-21 18:29:31 +08:00
Dot.L a4642ec6e6 Merge pull request #389 from APIParkLab/feature/liujian-1.9
Fix the issue of ineffective authentication for JWT, Oauth2, AK/SK
2025-08-29 18:28:15 +08:00
Dot.L 9ba746ba7f add azure openai
Feature/liujian 1.9
2025-08-26 17:28:36 +08:00
Dot.L e010f00a19 Merge pull request #383 from APIParkLab/feature/liujian-1.9
fix bug: fail to get ai logs
2025-08-15 16:27:51 +08:00
JackLiu 3e4f4e1cff Update README.md 2025-08-15 09:33:53 +08:00
ningyv 3217ad2bba Merge pull request #380 from APIParkLab/feature/1.9-OAuth
fix: Fix department selection issue
2025-08-12 10:44:59 +08:00
lcx 1485b31226 fix: Fix department selection issue 2025-08-12 10:43:17 +08:00
ningyv 8968e1f961 Merge pull request #379 from APIParkLab/feature/1.9-OAuth
fix: Fix department selection issue.
2025-08-12 10:23:42 +08:00
lcx 4b8fa43c36 fix: Fix department selection issue. 2025-08-12 10:23:14 +08:00
ningyv c21d783c52 Merge pull request #378 from APIParkLab/feature/1.9-OAuth
fix: issue with adding user permissions
2025-08-11 19:00:31 +08:00
lcx e928cd84d7 fix: issue with adding user permissions 2025-08-11 18:59:39 +08:00
FreyLoong 412cb75bf0 Update readme-zh-cn.md 2025-08-08 18:42:46 +08:00
FreyLoong a13b2a8afe Update README.md 2025-08-08 18:40:14 +08:00
Dot.L c0472c8539 Merge pull request #375 from APIParkLab/feature/liujian-1.9
Feature/liujian 1.9
2025-08-08 12:06:44 +08:00
Dot.L 55f09f7542 Merge pull request #374 from APIParkLab/feature/liujian-1.9
fix: fail to delete model
2025-08-08 11:31:25 +08:00
Dot.L 7aad2174aa Merge pull request #368 from APIParkLab/feature/liujian-1.9
update service publish config
2025-07-23 20:06:32 +08:00
ningyv 2a951c2854 Merge pull request #366 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-23 15:40:50 +08:00
lcx e91a9e7726 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-23 15:40:13 +08:00
ningyv e0d97186b1 Merge pull request #363 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 10:45:53 +08:00
lcx dff6e722c0 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 10:45:16 +08:00
ningyv b8c92961c1 Merge pull request #362 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 10:30:18 +08:00
lcx 025bd4c6cc feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 10:28:59 +08:00
ningyv 3d2ec67fc1 Merge pull request #361 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 09:55:00 +08:00
lcx b2a8c8d901 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 09:53:41 +08:00
ningyv 90226ac6af Merge pull request #360 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 09:41:09 +08:00
lcx 0e1efc9656 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 09:40:30 +08:00
Dot.L 061027aa36 Merge pull request #359 from APIParkLab/feature/liujian-1.9
update ap-account version
2025-07-21 18:51:07 +08:00
ningyv a083f994f4 Merge pull request #358 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-21 18:31:44 +08:00
lcx 5898337481 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 18:30:50 +08:00
ningyv 0f1496137d Merge pull request #357 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-21 18:14:47 +08:00
lcx aff2d1ce01 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 18:14:12 +08:00
Dot.L 78d10318ad Merge pull request #356 from APIParkLab/feature/liujian-1.9
Feature/liujian 1.9
2025-07-21 16:44:55 +08:00
576 changed files with 5688 additions and 4076 deletions
+54 -1
View File
@@ -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
@@ -212,6 +225,46 @@ For enterprise-level features and professional technical support, contact our pr
- Website: https://apipark.com
- Email: contact@apipark.com
🙏 A big thanks to everyone who helped shape APIPark. We are thrilled to hear the communitys thoughts! Lets make the world of APIs and AI stronger and more fun together. 🎉
<br>
🙏 A big thanks to everyone who helped shape APIPark. We are thrilled to hear the communitys thoughts! Lets make the world of APIs and AI stronger and more fun together. 🎉
# 🤝 Partner
- [Cursor](https://www.cursor.com/): Cursor is an AI-powered code editor that integrates artificial intelligence directly into the coding workflow, offering features like intelligent next edit suggestions, deep codebase understanding for relevant answers, and natural language editing to streamline development tasks and boost developer productivity.
- [Dify](https://dify.ai/): Dify is a leading Agentic AI Development Platform that provides a comprehensive suite of tools for building and extending AI applications, offering everything needed for agentic workflows, RAG pipelines, integrations, and observability, while allowing users to amplify their applications with various global Large Language Models (LLMs) and versatile plugins.
- [Trae](https://www.trae.ai/): Trae is an AI-native Integrated Development Environment (IDE) product that aims to embody the concept of “The Real AI Engineer” through intelligent productivity, seamlessly integrating into the development process to enhance quality and efficiency, featuring a chat-based interaction interface and supporting code generation and assistance.
- [Windsurf](https://windsurf.com/): Windsurf is an AI code editor designed to provide a seamless and limitless flow for developers, introducing a new purpose-built IDE that leverages AI to enhance coding with features like "Cascade" for deep codebase understanding, "Windsurf Tab" for intelligent autocompletion, and "Memories" for remembering important aspects of the codebase.
- [Coze](https://www.coze.com/): Coze is a next-generation AI application and chatbot development platform by ByteDance, empowering users to easily create and deploy powerful AI chatbots across various platforms with a no-code bot builder, integrated workflow logic, access to proprietary data, and simplified creation through pre-built plugins, knowledge bases, and workflows.
- [Claude Code](https://www.anthropic.com/claude-code): Claude Code is a command-line AI tool by Anthropic that embeds the Claude Opus 4 model directly into the users terminal, providing deep codebase awareness, the ability to edit files and execute commands, and making coordinated changes across multiple files, all while integrating seamlessly with popular IDEs and leveraging existing test suites.
- [Flowith](https://flowith.io/): Flowith is an AI creation workspace designed to revolutionize productivity and deep work by transforming knowledge and streamlining tasks through a multi-thread interface powered by advanced AI agents, offering an intuitive canvas-based user experience unlike traditional chat-based AI tools, and including a 24/7 operational version for complex tasks.
- [OpenManus](https://github.com/FoundationAgents/OpenManus): OpenManus is an open-source framework dedicated to building general AI agents, aiming to provide a platform where users can create and deploy their own agents without an invite code, supporting multi-agent capabilities, and requiring configuration for Large Language Model (LLM) APIs while integrating with browser automation tools.
- [Fellou](https://fellou.ai/): Fellou is an innovative Agentic Browser designed to transcend traditional web browsing by actively performing actions on behalf of the user, automating the entire process of information gathering and insight delivery, and excelling in in-depth research with seamless integrations with popular tools like Notion and LinkedIn.
- [Genspark](https://www.genspark.ai/): Genspark is an ultimate all-in-one AI companion offering a comprehensive suite of tools like AI Slides, AI Sheets, and AI Chat, designed to enhance various aspects of productivity and content creation, with personalized tools and AI Pods for generating content from diverse sources.
- [TEN](https://github.com/TEN-framework/ten-framework): TEN (The Embodied Narrator) is an open-source framework for building real-time, multimodal conversational voice AI agents, including components like TEN Framework, TEN Turn Detection, TEN Agent, TMAN Designer, and TEN Portal, offering features like Real-time Avatar, seamless MCP integration, real-time hardware communication, and vision/screenshare detection.
- [ChatGPT](https://chatgpt.com/): ChatGPT is an AI chatbot developed by OpenAI, built upon large language models like GPT-3.5 and GPT-4, designed to generate human-like conversational dialogue, understand context, answer follow-up questions, and integrate with various platforms for enhanced productivity through advanced language understanding, generation, and multilingual capabilities.
- [LangChain](https://www.langchain.com/): LangChain is a robust platform engineered for the development of reliable agents and Large Language Model (LLM) applications, offering a comprehensive product suite that seamlessly integrates various tools across the entire application development lifecycle, including LangGraph, LangSmith, and the LangGraph Platform, with functionalities for code generation, automation, and AI Search.
- [LEMON AI](https://lemonai.cc/): Lemon AI is the first Full-stack, Open-source, Agentic AI framework, offering a fully local alternative to platforms like Manus & Genspark AI. It features an integrated Code Interpreter VM sandbox for safe execution.
- [LobeChat](https://lobehub.com/): LobeHub offers LobeChat, a personal LLM productivity tool designed to elevate the user experience beyond traditional chatbots by empowering individuals to build personal AI agents and professional teams, supporting a wide array of LLMs, offering a simple chat interface, visual recognition, voice interaction, a rich plugin ecosystem, and knowledge base functionalities.
- [VS Code](https://code.visualstudio.com/): Visual Studio Code (VS Code) is a widely popular, free, and open-source code editor by Microsoft, renowned for its extensibility and customization, supporting vast programming languages, and integrating AI capabilities like intelligent next edit suggestions and an advanced “agent mode” for complex tasks, with broad compatibility with various AI models.
- [XRoute](https://xroute.ai): The Unified Interface For LLMs, provides better prices, better throughput, and no subscription.
- [XPack MCP Marketplace](https://github.com/xpack-ai/XPack-MCP-Marketplace): The world's first open-source MCP monetization platform, transform any OpenAPI into a monetizable MCP server and build your own API marketplace in just 10 minutes. Everything is open-source and ready for commercial use.
- [MemU](https://github.com/NevaMind-AI/memU): MemU is an open-source memory framework for AI companions
+1 -5
View File
@@ -113,13 +113,9 @@ func (i *imlAPIController) Edit(ctx *gin.Context, serviceId string, apiId string
if input.AiModel.Type != "local" {
provider = input.AiModel.Provider
}
modelName := input.AiModel.Name
if modelName == "" {
modelName = input.AiModel.Id
}
proxy.Plugins["ai_formatter"] = api.PluginSetting{
Config: plugin_model.ConfigType{
"model": modelName,
"model": input.AiModel.Name,
"provider": provider,
"config": input.AiModel.Config,
},
+32 -72
View File
@@ -21,14 +21,13 @@ import (
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
sseServers map[string]http.Handler
openSseServer http.Handler
openStreamableServer http.Handler
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) {
@@ -43,12 +42,12 @@ func (i *imlMcpController) AppMCPHandle(ctx *gin.Context) {
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.sseServers[locale]; ok {
if v, ok := i.server[locale]; ok {
v.ServeHTTP(ctx.Writer, req)
return
}
i.sseServers[languageEnUs].ServeHTTP(ctx.Writer, req)
i.server[languageEnUs].ServeHTTP(ctx.Writer, req)
}
func (i *imlMcpController) AppHandleSSE(ctx *gin.Context) {
@@ -69,7 +68,7 @@ func (i *imlMcpController) AppHandleSSE(ctx *gin.Context) {
}
ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/sse", mcp_server.GlobalBasePath)
i.handleSSE(ctx, i.openSseServer, SessionInfo{
i.handleSSE(ctx, i.openServer, SessionInfo{
Apikey: apikey,
App: appId,
})
@@ -82,29 +81,8 @@ func (i *imlMcpController) AppHandleMessage(ctx *gin.Context) {
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.openSseServer)
}
func (i *imlMcpController) AppHandleStreamHTTP(ctx *gin.Context) {
apikey := ctx.Request.Header.Get("Authorization")
apikey = strings.TrimPrefix(apikey, "Bearer ")
if apikey == "" {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
return
}
appId := ctx.Request.Header.Get("X-Application-Id")
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(), "apikey", apikey))
req = req.WithContext(utils.SetLabel(req.Context(), "app", appId))
req.URL.Path = mcp_server.OpenGlobalMCPPath
i.openStreamableServer.ServeHTTP(ctx.Writer, req)
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) {
@@ -116,44 +94,36 @@ func (i *imlMcpController) AppMCPConfig(ctx *gin.Context, appId string) (string,
if err != nil {
return "", fmt.Errorf("get app info error: %v", err)
}
return mcp_server.NewMCPConfig(
mcp_server.TransportTypeStreamableHTTP,
fmt.Sprintf("%s%s", strings.TrimSuffix(cfg.SitePrefix, "/"), mcp_server.OpenAppMCPPath),
map[string]string{
"Authorization": "Bearer {your_api_key}",
"X-Application-Id": appId,
},
nil,
).ToString(appInfo.Name), nil
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": {
"url": "%s"
}
}
}
`
func (i *imlMcpController) GlobalMCPConfig(ctx *gin.Context) (string, error) {
cfg := i.settingModule.Get(ctx)
if cfg.SitePrefix == "" {
return "", fmt.Errorf("site prefix is empty")
}
return mcp_server.NewMCPConfig(
mcp_server.TransportTypeStreamableHTTP,
fmt.Sprintf("%s%s", strings.TrimSuffix(cfg.SitePrefix, "/"), mcp_server.OpenGlobalMCPPath),
map[string]string{
"Authorization": "Bearer {your_api_key}",
},
nil,
).ToString("APIPark-MCP-Server"), nil
return fmt.Sprintf(mcpDefaultConfig, "APIPark-MCP-Server", fmt.Sprintf("%s/openapi/v1/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), mcp_server.GlobalBasePath)), nil
}
func (i *imlMcpController) OnComplete() {
i.sseServers = make(map[string]http.Handler)
i.server = make(map[string]http.Handler)
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.sseServers[language] = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.server[language] = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
if language == languageEnUs {
i.openSseServer = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
i.openStreamableServer = server.NewStreamableHTTPServer(s, server.WithEndpointPath(mcp_server.OpenGlobalMCPPath))
i.openServer = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
}
}
}
@@ -162,16 +132,16 @@ func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
cfg := i.settingModule.Get(ctx)
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
locale := utils.I18n(ctx)
if v, ok := i.sseServers[locale]; ok {
if v, ok := i.server[locale]; ok {
v.ServeHTTP(ctx.Writer, req)
return
}
i.sseServers[languageEnUs].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.openSseServer, SessionInfo{
i.handleSSE(ctx, i.openServer, SessionInfo{
Apikey: apikey,
})
}
@@ -197,16 +167,7 @@ func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, sIn
}
func (i *imlMcpController) GlobalHandleMessage(ctx *gin.Context) {
i.handleMessage(ctx, i.openSseServer)
}
func (i *imlMcpController) GlobalHandleStreamHTTP(ctx *gin.Context) {
apikey := ctx.Request.Header.Get("Authorization")
apikey = strings.TrimPrefix(apikey, "Bearer ")
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))
i.openStreamableServer.ServeHTTP(ctx.Writer, req)
i.handleMessage(ctx, i.openServer)
}
func (i *imlMcpController) MCPHandle(ctx *gin.Context) {
@@ -243,13 +204,12 @@ func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) {
}
func (i *imlMcpController) ServiceHandleStreamHTTP(ctx *gin.Context) {
apikey := ctx.Request.Header.Get("Authorization")
serviceId := ctx.Request.Header.Get("X-Service-Id")
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
}
apikey = strings.TrimPrefix(apikey, "Bearer ")
ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey)
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
-2
View File
@@ -13,13 +13,11 @@ type IMcpController interface {
GlobalMCPHandle(ctx *gin.Context)
GlobalHandleSSE(ctx *gin.Context)
GlobalHandleMessage(ctx *gin.Context)
GlobalHandleStreamHTTP(ctx *gin.Context)
GlobalMCPConfig(ctx *gin.Context) (string, error)
AppMCPHandle(ctx *gin.Context)
AppHandleSSE(ctx *gin.Context)
AppHandleMessage(ctx *gin.Context)
AppHandleStreamHTTP(ctx *gin.Context)
AppMCPConfig(ctx *gin.Context, appId string) (string, error)
ServiceHandleSSE(ctx *gin.Context)
+10 -16
View File
@@ -520,21 +520,16 @@ func (i *imlServiceController) createAIService(ctx *gin.Context, teamID string,
modelId := ""
modelCfg := ""
modelType := "online"
if input.Model != nil {
modelId = *input.Model
}
if *input.Provider == ai_provider_local.ProviderLocal {
modelType = "local"
if modelId == "" {
list, err := i.aiLocalModel.SimpleList(ctx)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, fmt.Errorf("no local model")
}
modelId = list[0].Id
list, err := i.aiLocalModel.SimpleList(ctx)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, fmt.Errorf("no local model")
}
modelId = list[0].Id
modelCfg = ai_provider_local.LocalConfig
} else {
pv, err := i.providerModule.Provider(ctx, *input.Provider)
@@ -545,15 +540,14 @@ func (i *imlServiceController) createAIService(ctx *gin.Context, teamID string,
if !has {
return nil, fmt.Errorf("provider not found")
}
if modelId == "" {
modelId = pv.DefaultLLM
}
m, has := p.GetModel(modelId)
m, has := p.GetModel(pv.DefaultLLM)
if !has {
return nil, fmt.Errorf("model %s not found", pv.DefaultLLM)
}
//modelId = m.ID()
modelId = m.Name()
modelCfg = m.DefaultConfig()
}
var info *service_dto.Service
+1 -1
View File
@@ -21,7 +21,7 @@
"plugins": ["react", "@typescript-eslint", "prettier", "unused-imports"],
"rules": {
"react/react-in-jsx-scope": "off",
"prettier/prettier": "error",
"prettier/prettier": "off",
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
+1 -1
View File
@@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.next/
node_modules
dist
market_dist
+42
View File
@@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
const srcDir = path.join(__dirname, 'src');
const filesToDelete = [
'package.json',
'tsconfig.json',
'tsconfig.node.json',
'vite.config.ts',
'postcss.config.js',
'tailwind.config.js',
'.eslintrc.cjs',
'index.html',
'start-vite.js'
];
function walkDirAndClean(dir) {
if (!fs.existsSync(dir)) return;
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
try {
let stat = fs.statSync(dirPath);
if (stat.isDirectory() && f !== 'node_modules') {
// If it's a top-level module directory like src/core, src/common
if (dir === srcDir) {
filesToDelete.forEach(file => {
const fileToDelete = path.join(dirPath, file);
if (fs.existsSync(fileToDelete)) {
fs.unlinkSync(fileToDelete);
console.log(`Deleted: ${fileToDelete}`);
}
});
}
}
} catch(e) {}
});
}
walkDirAndClean(srcDir);
console.log('Cleanup completed!');
+50
View File
@@ -0,0 +1,50 @@
const fs = require('fs');
const path = require('path');
const srcDir = path.join(__dirname, 'src');
function walkDir(dir, callback) {
if (dir.includes('node_modules')) return;
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
try {
let isDirectory = fs.statSync(dirPath).isDirectory();
isDirectory ? walkDir(dirPath, callback) : callback(path.join(dir, f));
} catch(e) {}
});
}
const filesToRename = [];
const filesToUpdate = [];
walkDir(srcDir, (filePath) => {
if (filePath.endsWith('.module.css')) {
filesToRename.push(filePath);
} else if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
filesToUpdate.push(filePath);
}
});
filesToRename.forEach(oldPath => {
const newPath = oldPath.replace(/\.module\.css$/, '.css');
// Read and remove :global wrappers entirely
let content = fs.readFileSync(oldPath, 'utf8');
content = content.replace(/:global\(([^)]+)\)/g, '$1'); // replace :global(.foo) with .foo
content = content.replace(/:global\s+/g, ''); // replace :global .foo with .foo
fs.writeFileSync(oldPath, content);
fs.renameSync(oldPath, newPath);
console.log(`Renamed and cleaned: ${path.basename(oldPath)} -> ${path.basename(newPath)}`);
});
filesToUpdate.forEach(filePath => {
let content = fs.readFileSync(filePath, 'utf8');
if (content.includes('.module.css')) {
content = content.replace(/\.module\.css/g, '.css');
fs.writeFileSync(filePath, content);
console.log(`Updated imports in: ${path.basename(filePath)}`);
}
});
console.log('Done!');
-7
View File
@@ -1,7 +0,0 @@
{
"packages": [
"packages/*"
],
"version": "independent"
}
+144
View File
@@ -0,0 +1,144 @@
const fs = require('fs');
const path = require('path');
const rootDir = __dirname;
const packagesDir = path.join(rootDir, 'packages');
const srcDir = path.join(rootDir, 'src');
const appDir = path.join(srcDir, 'app');
console.log('🚀 开始拆除 Lerna 并迁移至 Next.js...');
// 1. 创建基础目录
if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir);
if (!fs.existsSync(appDir)) fs.mkdirSync(appDir);
// 2. 读取并合并 package.json
const rootPkgPath = path.join(rootDir, 'package.json');
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf8'));
const mergedDeps = { ...rootPkg.dependencies };
const mergedDevDeps = { ...rootPkg.devDependencies };
if (fs.existsSync(packagesDir)) {
const packages = fs.readdirSync(packagesDir);
for (const pkg of packages) {
const pkgPath = path.join(packagesDir, pkg);
if (fs.statSync(pkgPath).isDirectory()) {
// 合并依赖
const childPkgPath = path.join(pkgPath, 'package.json');
if (fs.existsSync(childPkgPath)) {
const childPkg = JSON.parse(fs.readFileSync(childPkgPath, 'utf8'));
Object.assign(mergedDeps, childPkg.dependencies || {});
Object.assign(mergedDevDeps, childPkg.devDependencies || {});
}
// 移动目录到 src 下
const destPath = path.join(srcDir, pkg);
if (!fs.existsSync(destPath)) {
fs.renameSync(pkgPath, destPath);
console.log(`📦 已迁移模块: packages/${pkg} -> src/${pkg}`);
}
}
}
// 删除空的 packages 文件夹
try { fs.rmdirSync(packagesDir); } catch (e) { console.error('Failed to remove packages dir, skipping', e) }
}
// 3. 清理并更新根 package.json
delete rootPkg.workspaces; // 移除 lerna workspaces
rootPkg.scripts = {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
};
// 移除 Vite 和 Lerna 相关依赖
const removeDeps = ['lerna', 'vite', '@originjs/vite-plugin-federation', '@vitejs/plugin-react', 'vite-tsconfig-paths'];
removeDeps.forEach(dep => {
delete mergedDeps[dep];
delete mergedDevDeps[dep];
});
// 添加 Next.js 和 React 最新核心依赖 (与 xroute-ui 对齐)
mergedDeps['next'] = "15.4.5";
mergedDeps['react'] = "19.1.0";
mergedDeps['react-dom'] = "19.1.0";
mergedDevDeps['@types/react'] = "^19";
mergedDevDeps['@types/react-dom'] = "^19";
rootPkg.dependencies = mergedDeps;
rootPkg.devDependencies = mergedDevDeps;
fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
console.log('✅ package.json 依赖已合并并重写');
// 4. 生成 tsconfig.json (配置路径别名)
const tsconfigPath = path.join(rootDir, 'tsconfig.json');
const tsconfig = {
compilerOptions: {
target: "es5",
lib: ["dom", "dom.iterable", "esnext"],
allowJs: true,
skipLibCheck: true,
strict: false,
noEmit: true,
esModuleInterop: true,
module: "esnext",
moduleResolution: "bundler",
resolveJsonModule: true,
isolatedModules: true,
jsx: "preserve",
incremental: true,
plugins: [{ name: "next" }],
baseUrl: ".",
paths: {
"@/*": ["src/*"],
// 欺骗原有代码,使其能找到拍平后的新路径
"@apipark/common/*": ["src/common/src/*"],
"@apipark/core/*": ["src/core/src/*"],
"@apipark/dashboard/*": ["src/dashboard/src/*"],
"@apipark/market/*": ["src/market/src/*"],
"@apipark/openApi/*": ["src/openApi/src/*"],
"@apipark/systemRunning/*": ["src/systemRunning/src/*"]
}
},
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
exclude: ["node_modules"]
};
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
console.log('✅ tsconfig.json 别名映射已配置');
// 5. 创建 Next.js App Router 挂载点
const layoutCode = `export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}`;
fs.writeFileSync(path.join(appDir, 'layout.tsx'), layoutCode);
const slugDir = path.join(appDir, '[[...slug]]');
if (!fs.existsSync(slugDir)) fs.mkdirSync(slugDir, { recursive: true });
const pageCode = `"use client";
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
// 动态导入原有的 Vite SPA 根组件,禁用 SSR 避免 window 报错
const ApiParkApp = dynamic(() => import('@/core/src/App'), {
ssr: false,
loading: () => <div style={{ padding: 50 }}>Loading APIPark...</div>
});
export default function Page() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <ApiParkApp />;
}`;
fs.writeFileSync(path.join(slugDir, 'page.tsx'), pageCode);
console.log('✅ Next.js 路由挂载点创建完毕!');
console.log('🎉 迁移完成!请执行 pnpm install 重新安装依赖。');
+4
View File
@@ -0,0 +1,4 @@
/// <reference types="next" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+42
View File
@@ -0,0 +1,42 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
disableStaticImages: true,
},
experimental: {
optimizePackageImports: ["@heroui/react"],
},
transpilePackages: ['@heroui/react', '@heroui/theme', '@ant-design', 'antd', 'rc-util', 'rc-pagination', 'rc-picker', 'rc-tree', 'rc-table'],
async rewrites() {
return [
{
source: '/api/v1/:path*',
destination: 'http://172.18.166.219:8288/api/v1/:path*', // Proxy to backend
},
{
source: '/api2/v1/:path*',
destination: 'http://172.18.166.219:8288/api2/v1/:path*', // Proxy to backend 2
}
];
},
webpack: (config) => {
config.module.rules.push({
test: /\.(svg|png|jpe?g|gif|webp)$/i,
type: 'asset/resource',
generator: {
filename: 'static/media/[name].[hash][ext]'
}
});
// 解决一些 Node.js polyfill 在浏览器端缺失的问题
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
os: false,
};
return config;
},
};
module.exports = nextConfig;
+59 -30
View File
@@ -2,20 +2,12 @@
"name": "frontend",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"description": "",
"scripts": {
"test": "jest",
"build": "set NODE_OPTIONS=--max-old-space-size=4096 && 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",
"stop": "kill-port --port 5000",
"scan": "i18next-scanner --config i18next-scanner.config.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix && prettier --write ."
"dev": "next dev -p 5000",
"build": "next build",
"start": "next start -p 5000",
"lint": "next lint"
},
"keywords": [],
"author": "",
@@ -23,51 +15,88 @@
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@ant-design/pro-components": "2.7.19",
"@originjs/vite-plugin-federation": "^1.3.3",
"@emotion/react": "^11.14.0",
"@floating-ui/react": "^0.26.24",
"@formkit/auto-animate": "^0.8.1",
"@heroui/react": "^3.0.3",
"@heroui/styles": "^3.0.3",
"@heroui/theme": "^2.4.20",
"@lexical/code": "^0.17.1",
"@lexical/react": "^0.17.1",
"@lexical/selection": "^0.17.1",
"@lexical/text": "^0.17.1",
"@lexical/utils": "^0.17.1",
"@modelcontextprotocol/sdk": "^1.9.0",
"@mui/icons-material": "^5.15.6",
"@mui/lab": "5.0.0-alpha.150",
"@mui/material": "5.14.14",
"@mui/x-data-grid-pro": "6.18.1",
"@rollup/plugin-dynamic-import-vars": "^2.1.2",
"@tinymce/tinymce-react": "^4.3.2",
"@types/dompurify": "^3.0.5",
"@types/lodash-es": "^4.17.12",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0",
"@xyflow/react": "^12.3.6",
"ahooks": "^3.8.1",
"allotment": "^1.20.0",
"autoprefixer": "^10.4.16",
"copy-to-clipboard": "^3.3.3",
"crc": "^4.3.2",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
"echarts": "^5.5.0",
"echarts-for-react": "^3.0.2",
"framer-motion": "^10.16.4",
"fs-extra": "^11.2.0",
"highlight.js": "^11.9.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"js-base64": "^3.7.5",
"lexical": "^0.17.1",
"mockjs": "^1.1.0",
"next": "15.4.5",
"postcss": "^8.4.31",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.5",
"react": "^18.2.0",
"rc-picker": "^4.1.1",
"react": "19.1.0",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.49.3",
"react-i18next": "^15.0.1",
"react-joyride": "^2.8.2",
"react-router-dom": "6.20.0",
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^3.3.5",
"uuid": "^9.0.1",
"vite-tsconfig-paths": "^4.3.2",
"react-json-view": "^1.21.3",
"zod": "^3.23.8",
"@modelcontextprotocol/sdk": "^1.9.0",
"echarts-for-react": "^3.0.2"
"react-router-dom": "6.20.0",
"react-virtuoso": "^4.7.11",
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^4.2.1",
"tinymce": "^6.8.1",
"use-context-selector": "^2.0.0",
"uuid": "^9.0.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@ant-design/cssinjs": "^1.18.2",
"@antv/g6": "^4.8.24",
"@formily/antd-v5": "^1.2.1",
"@formily/core": "^2.2.13",
"@formily/react": "^2.2.13",
"@formily/reactive": "^2.2.13",
"@iconify/react": "^5.0.2",
"@monaco-editor/react": "^4.6.0",
"@tailwindcss/postcss": "^4.2.1",
"lightningcss": "^1.32.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@testing-library/react-hooks": "^8.0.1",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/react": "^19",
"@types/react-dom": "^19",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"antd": "^5.19.4",
"babel-jest": "^29.7.0",
"eslint": "^8.53.0",
@@ -77,22 +106,22 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-unused-imports": "^4.1.4",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"i18next-scanner": "^4.5.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jsdom": "^24.0.0",
"lerna": "^8.1.3",
"less": "^4.2.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.45.0",
"postcss-nested": "^6.0.1",
"prettier": "^3.1.1",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.1.2",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vite-jest": "^0.1.4"
}
}
}
-41
View File
@@ -1,41 +0,0 @@
{
"name": "common",
"version": "1.0.0",
"description": "Common library for AO Platform",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "node ./__tests__/common.test.js"
},
"dependencies": {
"@floating-ui/react": "^0.26.24",
"@formkit/auto-animate": "^0.8.1",
"@lexical/code": "^0.17.1",
"@lexical/react": "^0.17.1",
"@lexical/selection": "^0.17.1",
"@lexical/text": "^0.17.1",
"@lexical/utils": "^0.17.1",
"@mui/icons-material": "^5.15.6",
"@mui/lab": "5.0.0-alpha.150",
"@mui/material": "5.14.14",
"@mui/x-data-grid-pro": "6.18.1",
"ahooks": "^3.8.1",
"allotment": "^1.20.0",
"echarts": "^5.5.0",
"lexical": "^0.17.1",
"mockjs": "^1.1.0",
"rc-picker": "^4.1.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.49.3",
"use-context-selector": "^2.0.0"
},
"devDependencies": {
"@formily/antd-v5": "^1.2.1",
"@formily/core": "^2.2.13",
"@formily/react": "^2.2.13",
"@formily/reactive": "^2.2.13",
"@monaco-editor/react": "^4.6.0",
"exceljs": "^4.4.0",
"monaco-editor": "^0.45.0"
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
-4
View File
@@ -1,4 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -1,26 +0,0 @@
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker()
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker()
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker()
}
return new editorWorker()
}
}
export { monaco }
@@ -1,98 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
important:true,
content: [
`../*/src/**/*.{js,ts,jsx,tsx}`,
]
,
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
backgroundImage:{
LAYOUT_BG:'linear-gradient(107.97deg, rgba(32,41,117,1) 4.41%,rgba(16,13,27,1) 86.11%)',
LAYOUT_BG_DARK:'#fff',
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);',
'navbar-height': 'var(--layout-header-height)',
TAG_LEFT:'10px',
PAGE_INSIDE_X:'40px',
PAGE_INSIDE_T:'30px',
PAGE_INSIDE_B:'20px',
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [
function({ addUtilities }) {
addUtilities({
'.h-calc-100vh-minus-navbar': {
height: 'calc(100vh - var(--layout-header-height))',
},
'.w-calc-100vw-minus-padding-r': {
width: 'calc(100% - 40px)',
},
}, ['responsive', 'hover']);
}
],
corePlugins: {
preflight: false,
},
}
-28
View File
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["./src/*"],
"@core/*": ["../core/src/*"],
"@market/*": ["../market/src/*"]
},
},
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-49
View File
@@ -1,49 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'
export default defineConfig({
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true
}
},
modules: {
localsConvention: 'camelCase',
generateScopedName: '[local]_[hash:base64:2]'
}
},
plugins: [
react(),
dynamicImportVars({
include: ['src'],
exclude: [],
warnOnError: false
})
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@common', replacement: path.resolve(__dirname, './src') },
{ find: '@market', replacement: path.resolve(__dirname, '/./market/src') },
{ find: '@core', replacement: path.resolve(__dirname, '../core/src') }
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true
}
}
},
logLevel: 'info'
})
-1
View File
@@ -1 +0,0 @@
VITE_APP_MODE=openSource
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs','public','code-snippet','ace-editor'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
-36
View File
@@ -1,36 +0,0 @@
<!doctype html>
<html lang="en">
<head id="head">
<meta charset="UTF-8" />
<link id="favicon" rel="icon" type="image/svg+xml" href="/frontend/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body id="eo-body">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const eoBody = document.getElementById('eo-body');
const favicon = document.getElementById('favicon');
const createScript = (id, src) => {
const script = document.createElement('script');
script.id = id;
script.async = true;
script.src = src;
return script;
};
const iconparkApintoSrc = window.location.hostname === 'localhost' ? '/iconpark_apinto.js' : '/frontend/iconpark_apinto.js';
const iconparkEolinkSrc = window.location.hostname === 'localhost' ? '/iconpark_eolink.js' : '/frontend/iconpark_eolink.js';
const faviconSrc = window.location.hostname === 'localhost' ? '/favicon.ico' : '/frontend/favicon.ico';
favicon.href = faviconSrc;
eoBody.appendChild(createScript('iconpark_apinto', iconparkApintoSrc));
eoBody.appendChild(createScript('iconpark_eolink', iconparkEolinkSrc));
});
</script>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
{
"name": "core",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": " vite --mode open --port 5000 --strictPort",
"dev:pro": " vite --config ./vite.pro.config.ts --mode pro --port 5000 --strictPort ",
"build": "vite build --mode open",
"build:pro": "vite --config ./vite.pro.config.ts build --mode pro",
"postinstall": "node scripts/moveTinymce.js",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview --port 5000 --strictPort",
"serve": "vite preview --port 5000 --strictPort"
},
"dependencies": {
"@tinymce/tinymce-react": "^4.3.2",
"@xyflow/react": "^12.3.6",
"fs-extra": "^11.2.0",
"highlight.js": "^11.9.0",
"tinymce": "^6.8.1"
}
}
-10
View File
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
-309
View File
@@ -1,309 +0,0 @@
@tailwind base;
@tailwind components;
@layer components {
.button-bottom-default {
@apply border-[0px] border-b-[1px] border-solid border-BORDER;
}
}
@tailwind utilities;
#root {
width: 100vw;
height:100vh;
}
:global.ant-tree-node-content-wrapper{
overflow: hidden;
}
.tree-title-hover{
display: flex;
justify-content: space-between;
align-items:center;
.tree-title-span{
text-overflow: ellipsis;
}
.tree-title-more{
display: none;
}
&:hover .tree-title-more{
display: flex;
height:22px;
width:22px;
}
}
.ant-layout-content.apipark-layout-layout-content{
border-radius:10px 0 0 0 ;
overflow:hidden;
background-color:'transparent'
}
.apipark-layout-global-header-collapsed-button{
color:hsl(0, 0%, 100%);
}
.apipark-layout-top-nav-header-main{
display: flex;
align-items: center;
}
.apipark-layout-top-nav-header-menu {
height:50px;
line-height:50px;
.ant-menu-item.apipark-layout-base-menu-horizontal-menu-item.ant-menu-item-selected::after{
border-bottom:2px solid #fff !important;
}
.ant-menu-item.apipark-layout-base-menu-horizontal-menu-item.ant-menu-item-active:not(.ant-menu-item-selected)::after{
border-bottom:2px solid transparent !important;
}
}
.apipark-layout-base-menu-inline-group .ant-menu-item-group-title{
color:rgb(255 255 255 / 70%) !important;
}
.avatar-dom > div{
display: flex;
flex-direction: row-reverse;
align-items: center;
gap:8px;
}
.apipark-layout-layout{
.apipark-layout-layout-bg-list{
background-image: radial-gradient(circle farthest-corner at 450px 350px, #050eb7, #17163e 500px);
}
.ant-layout-header.apipark-layout-layout-header{
backdrop-filter: unset !important;
height:var(--layout-header-height);
line-height: var(--layout-header-height);
background-color: transparent;
li.apipark-layout-base-menu-horizontal-menu-item{
color:rgb(255 255 255 / 70%) !important;
&.ant-menu-item-selected{
color:#fff !important;
}
&.ant-menu-item-active{
color:#fff !important;
}
}
li.ant-menu-submenu-horizontal.ant-menu-overflow-item-rest .ant-menu-submenu-title{
color:#fff !important;
}
}
.ant-layout-sider.apipark-layout-sider{
height:calc(100vh - var(--layout-header-height)) !important;
inset-block-start: var(--layout-header-height);
.ant-menu {
.ant-menu-item-group-title{
font-size:12px;
padding:12px 16px;
}
.ant-menu-item{
margin-block:0 !important;
}
.ant-menu-light:not(.ant-menu-horizontal) .ant-menu-item:not(.ant-menu-item-selected):active{
background-color: unset;
}
}
.apipark-layout-sider-collapsed-button{
display: none;
}
ul.ant-menu.ant-menu-root.ant-menu-inline,
ul.ant-menu.ant-menu-root.ant-menu-vertical{
> li {
color:rgb(255 255 255 / 70%) !important;
/* border-radius: 10px;
background-color: rgba(255,255,255,0.1) !important;
border: 1px solid rgba(255,255,255,0.15); */
}
> li.ant-menu-item-active {
color:#fff !important;
}
> li.ant-menu-item-selected {
background-color: #fff !important;
border: 1px solid #fff !important;
color:#333 !important;
}
}
ul.apipark-layout-sider-menu .ant-menu-item-group-list{
> li {
color:rgb(255 255 255 / 70%) !important;
}
> li:active{
background-color: transparent;
}
> li.ant-menu-item-active {
color:#fff !important;
}
> li.ant-menu-item-selected {
background-color: #fff !important;
border: 1px solid #fff !important;
color:#333 !important;
}
}
.ant-menu-item {
height:40px;
margin-block:10px;
}
}
.apipark-layout-drawer-sider{
background:#17163E;
padding-top:20px;
.ant-layout-sider.apipark-layout-sider{
height: 100% !important;
inset-block: 20px;
}
}
.apipark-layout-layout-container{
>.ant-layout-header{
height:var(--layout-header-height) !important;
line-height:var(--layout-header-height) !important;
}
>.apipark-layout-layout-content.apipark-layout-layout-has-header{
padding-block:0px;
padding-inline:0px;
background-color: #fff !important;
}
}
.ant-pro-global-header-header-actions-avatar > div{
color:#fff !important;
}
.ant-menu-item-divider.apipark-layout-base-menu-inline-divider{
border-color: rgb(255 255 255 / 15%) !important;
}
}
.tox-tinymce{
border:none !important;
}
a{
transition:none !important;
}
.ant-result ant-result-error{
background-color: #fff !important;
}
.ant-tabs-tab-btn{
display: flex;
align-items:center;
.ant-tabs-tab-icon{
display: inline-flex;
align-items:center;
}
}
.eo_page_list .ant-pro-table{
overflow: hidden;
border-radius: 10px;
border:1px solid var(--table-border-color) !important;
}
.swagger-ui{
width: 100%;
.model-box-control:focus,.models-control:focus, .opblock-summary-control:focus{
outline:unset !important;
}
.information-container{
.info{
display: none;
}
}
}
.ant-pro-table .ant-popover .ant-popover-inner-content{
.ant-form-item{
background-color: transparent;
border:none;
}
}
.ant-menu .ant-menu-title-content{
display:unset !important;
}
.ai-setting-svg-container svg{
width: 100%;
height:100%;
display:block;
}
.ai-service-api-preview .swagger-ui h3.opblock-tag{
display: none;
}
/* 整个背景容器设置 */
.background-container {
background: radial-gradient(ellipse 80% 900px at top, rgb(255 255 255 / 10%) 0%, rgb(4 0 71) 30%, rgb(13 17 23) 100%);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
overflow: hidden;
z-index: 1;
isolate: isolate;
}
/* SVG背景图案 */
.background-pattern {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
stroke: rgba(255, 255, 255, 0.1);
mask-image: radial-gradient(100% 100% at top right, white, transparent);
}
.login-block{
background: rgba(255, 255, 255, 0.1) !important;
.login-input{
color:#fff !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
&:hover, &:focus, &.ant-input-status-error, &.ant-input-status-error:hover, &.ant-input-status-error:focus-within{
background: rgba(255, 255, 255, 0.2) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
}
}
input:-webkit-autofill,
input:-webkit-autofill:focus {
transition: background-color 0s 600000s, color 0s 600000s !important;
}
}
.ant-select-selection-overflow-item:first-child {
max-width: calc(100% - 60px);
margin-right: 4px;
}
a[disabled]:hover {
color: #BBB;
cursor: not-allowed;
}
.ant-input-group-addon{
height:32px !important;
.ant-btn.ant-btn-default{
height:32px !important;
}
}
-385
View File
@@ -1,385 +0,0 @@
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 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 [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
}
})
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)
}
})
}, [])
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 openFeishuLogin = () => {
const href = window.location.origin + window.location.pathname
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${feishuAppId}&redirect_uri=${href}`
localStorage.setItem('feishuCallbackUrl', href)
window.location.href = authUrl
}
useEffect(() => {
check()
getSystemInfo()
}, [])
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>
<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>
</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>
<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>
{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}
>
<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>
</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>
)
}
export default Login
@@ -1,656 +0,0 @@
import { App, Button, Card, CascaderProps, Empty, Select } from 'antd'
import { $t } from '@common/locales/index.ts'
import { Icon } from '@iconify/react/dist/iconify.js'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import ReactJson from 'react-json-view'
import { IconButton } from '@common/components/postcat/api/IconButton'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { useConnection } from './hook/useConnection'
import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { useNavigate } from 'react-router-dom'
import { ServiceDetailType } from '@market/const/serviceHub/type'
import useCopyToClipboard from '@common/hooks/copy'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { Cascader } from 'antd/lib'
type ConfigList = {
openApi?: {
title: string
configContent: string
apiKeys: string[]
}
mcp: {
title: string
configContent: string
apiKeys: string[]
}
}
type ApiKeyItem = {
expired: number
id: string
name: string
value: string
}
interface Option {
value: string
label: string
children?: Option[]
}
type ServiceApiKeyList = {
id: string
name: string
apikeys: Array<{
id: string
name: string
value: string
expired: number
}>
}
type ConsumerParamsType = {
consumerId: string
teamId: string
}
export interface IntegrationAIContainerRef {
getServiceKeysList: () => void;
}
export interface IntegrationAIContainerProps {
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>(
({
type,
handleToolsChange,
customClassName,
service,
serviceId,
currentTab,
openModal,
consumerParams
}: IntegrationAIContainerProps, ref) => {
/** 当前激活的标签 */
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
/** 弹窗组件 */
const { message } = App.useApp()
/** 配置内容 */
const [configContent, setConfigContent] = useState<string>('')
/** 当前选中 API Key */
const [apiKey, setApiKey] = useState<string>('')
/** API Key 列表 */
const [apiKeyList, setApiKeyList] = useState<any[]>([])
/** Cascader Key 列表 */
const [cascaderKeyList, setCascaderKeyList] = useState<string[]>([])
/** MCP 服务器地址 */
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
/** 全局状态 */
const { state } = useGlobalContext()
const navigator = useNavigate()
/** 复制组件 */
const { copyToClipboard } = useCopyToClipboard()
/** 错误提示 */
const [errors, setErrors] = useState<Record<string, string | null>>({
resources: null,
prompts: null,
tools: null
})
/** 标签内容 */
const [tabContent, setTabContent] = useState<ConfigList>({
mcp: {
title: $t('MCP 配置'),
configContent: '',
apiKeys: []
}
})
/** HTTP 请求 */
const { fetchData } = useFetch()
/**
* 初始化标签数据
*/
const initTabsData = () => {
const params: ConfigList = {
mcp: {
title: $t('MCP 配置'),
configContent: service?.mcpAccessConfig || '',
apiKeys: []
}
}
if (type === 'service') {
params.openApi = {
title: $t('Open API 文档'),
configContent: service?.openapiAddress || '',
apiKeys: []
}
}
setTabContent(params)
}
/**
* 复制
* @param value
* @returns
*/
const handleCopy = async (value: string): Promise<void> => {
if (value) {
copyToClipboard(value)
message.success($t(RESPONSE_TIPS.copySuccess))
}
}
/**
* 选择 API Key
* @param value
*/
const handleSelectChange = (value: string) => {
setApiKey(value)
}
/**
* Cascader 选择
* @param value
*/
const handleCascaderChange: CascaderProps<Option>['onChange'] = (value) => {
setApiKey(value.at(-1) || '')
setCascaderKeyList(value)
}
/**
* 获取全局 MCP 配置
* @returns
*/
const getGlobalMcpConfig = () => {
fetchData<BasicResponse<null>>('global/mcp/config', {
method: 'GET'
})
.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))
})
}
/**
* 获取消费者 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))
})
}
/**
* 全局 MCP 跳转
*/
const addKey = () => {
navigator('/mcpKey')
}
const dropAuthPage = () => {
navigator(`/consumer/${consumerParams?.teamId}/inside/${consumerParams?.consumerId}/authorization`)
}
/**
* 获取全局 API Key 列表
*/
const getGlobalKeysList = () => {
fetchData<BasicResponse<null>>('simple/system/apikeys', {
method: 'GET'
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
if (data.apikeys && data.apikeys.length > 0) {
setApiKeyList(
data.apikeys.map((item: ApiKeyItem) => {
return {
label: item.name,
value: item.value
}
})
)
setApiKey(data.apikeys[0].value)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 抛出获取服务 API Key 列表
*/
useImperativeHandle(ref, () => ({
getServiceKeysList
}))
/**
* 获取 API Key 列表
*/
const getServiceKeysList = (consumerId?: string) => {
fetchData<BasicResponse<null>>(`my/app/apikeys`, {
method: 'GET',
eoParams: consumerId ? { app: consumerId } : { service: serviceId }
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
if (data.apps && data.apps.length > 0) {
// 转换数据结构为 Cascader 所需格式
const transformedData = data.apps.map((app: ServiceApiKeyList) => ({
value: app.id,
label: app.name,
children: app.apikeys.map((key) => ({
...key,
label: key.name
}))
}))
setApiKeyList(transformedData)
if (data.apps[0].apikeys?.length) {
setApiKey(data.apps[0].apikeys[0].value)
setCascaderKeyList([data.apps[0].id, data.apps[0].apikeys[0].value])
}
}
}
})
.catch((errorInfo) => {
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 清除错误提示
*/
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }))
}
/**
* 发送请求
*/
const makeRequest = async <T extends z.ZodType>(request: ClientRequest, schema: T, tabKey?: keyof typeof errors) => {
try {
const response = await makeConnectionRequest(request, schema)
if (tabKey !== undefined) {
clearError(tabKey)
}
return response
} catch (e) {
const errorString = (e as Error).message ?? String(e)
if (tabKey !== undefined) {
setErrors((prev) => ({
...prev,
[tabKey]: errorString
}))
}
throw e
}
}
/**
* 获取 MCP 的 tools
*/
const listTools = async () => {
const response = await makeRequest(
{
method: 'tools/list' as const,
params: {}
},
ListToolsResultSchema,
'tools'
)
handleToolsChange(response.tools)
}
/**
* 初始化连接 mcp
*/
const {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
handleCompletion,
completionsSupported,
connect: connectMcpServer,
disconnect: disconnectMcpServer
} = useConnection({
transportType: 'sse',
sseUrl: '',
proxyServerUrl: mcpServerUrl,
requestTimeout: 1000
})
// 使用 useRef 保存最新的连接状态和断开函数
const connectionStatusRef = useRef(connectionStatus)
const disconnectFnRef = useRef(disconnectMcpServer)
// 当连接状态或断开函数变化时更新 ref
useEffect(() => {
connectionStatusRef.current = connectionStatus
disconnectFnRef.current = disconnectMcpServer
}, [connectionStatus, disconnectMcpServer])
/**
* 初始化数据
*/
const setupComponent = () => {
initTabsData()
if (type === 'global') {
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()
}
}
/**
* 初始化数据
*/
useEffect(() => {
setupComponent()
}, [service])
/**
* 初始化标签数据
*/
useEffect(() => {
initTabsData()
type === 'global' && getGlobalMcpConfig()
type === 'consumer' && getConsumerMcpConfig()
}, [state.language])
/**
* 切换标签
*/
useEffect(() => {
if (type === 'service') {
currentTab === 'MCP' ? setActiveTab('mcp') : setActiveTab('openApi')
}
}, [currentTab])
/**
* 仅在组件加载时执行初始化逻辑
*/
useEffect(() => {
// 返回清理函数,只会在组件卸载时执行
return () => {
try {
// 使用 ref 中保存的最新函数强制断开连接
const disconnectFn = disconnectFnRef.current
if (disconnectFn) {
disconnectFn()
}
} catch (err) {
console.error('断开连接时出错:', err)
}
}
}, [type])
/**
* 切换标签时更新配置内容
*/
useEffect(() => {
if (activeTab === 'openApi' && tabContent?.openApi?.configContent) {
setConfigContent(tabContent?.openApi?.configContent)
} else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) {
setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
}
}, [service, apiKey, activeTab, tabContent])
/**
* 连接 MCP 服务器
*/
useEffect(() => {
if (mcpServerUrl) {
if (connectionStatus === 'connected') {
disconnectMcpServer()
}
connectMcpServer()
}
}, [mcpServerUrl, ...(type === 'global' || type === 'consumer' ? [state.language] : [])])
/**
* 获取 MCP tools
*/
useEffect(() => {
if (connectionStatus === 'connected') {
listTools()
}
}, [connectionStatus])
return (
<>
<Card
style={{ borderRadius: '10px' }}
className={`w-[400px] h-fit ${customClassName}`}
classNames={{
body: 'p-[10px]'
}}
>
<p>
<Icon
icon="icon-park-solid:connection-point-two"
className="align-text-bottom mr-[5px]"
width="16"
height="16"
/>
{$t('AI 代理集成')}
</p>
{type === 'service' && service?.basic.enableMcp && (
<div className="mt-3 tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
onClick={() => setActiveTab('openApi')}
>
Open API
</div>
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'mcp' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
onClick={() => setActiveTab('mcp')}
>
MCP
</div>
</div>
)}
{(type === 'service' || type === 'consumer') && !apiKeyList.length ? (
<>
<Card
style={{ borderRadius: '10px' }}
className={`w-full mt-3`}
classNames={{
body: 'p-[10px]'
}}
>
{
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>
</>
) : (
<>
<div className="tab-container mt-3">
<div className="tab-content font-semibold mt-[10px]">
{activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title}
</div>
{/* 标签页内容区域 */}
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
{activeTab === 'mcp' ? (
<ReactJson
src={
configContent
? typeof configContent === 'string'
? (() => {
try {
return JSON.parse(configContent)
} catch (e) {
return {}
}
})()
: configContent
: {}
}
theme="monokai"
indentWidth={2}
displayDataTypes={false}
displayObjectSize={false}
name={false}
collapsed={false}
enableClipboard={false}
style={{
backgroundColor: 'transparent',
wordBreak: 'break-word',
whiteSpace: 'normal'
}}
/>
) : (
<>
<pre className="whitespace-pre-wrap break-words">{configContent || ''}</pre>
</>
)}
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
sx={{
position: 'absolute',
top: '5px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</div>
{activeTab === 'mcp' && (
<>
<div className="tab-content font-semibold my-[10px]">API Key</div>
{apiKeyList.length ? (
<>
{type === 'global' ? (
<>
<Select
showSearch
optionFilterProp="label"
value={apiKey}
className="w-full"
onChange={handleSelectChange}
options={apiKeyList}
/>
<Card
style={{ borderRadius: '5px' }}
className="w-full mt-[5px] "
classNames={{
body: 'p-[5px]'
}}
>
<div className="relative h-[25px]">
{apiKey}
<IconButton
name="copy"
onClick={() => handleCopy(apiKey)}
sx={{
position: 'absolute',
top: '0px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</Card>
</>
) : (
<>
<Cascader
className='w-full'
allowClear={false}
options={apiKeyList}
value={cascaderKeyList}
onChange={handleCascaderChange}
placeholder={$t('选择 API Key')}
/>
</>
)}
</>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={''}>
<Button onClick={addKey} type="primary">
{$t('新增 API Key')}
</Button>
</Empty>
)}
</>
)}
</>
)}
</Card>
</>
)
})
@@ -1,199 +0,0 @@
import PageList from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Modal} from "antd";
import {BasicResponse, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import { SimpleMemberItem } from "@common/const/type.ts";
import {useFetch} from "@common/hooks/http.ts";
import { TEAM_TABLE_COLUMNS } from "../../const/team/const.tsx";
import { TeamConfigFieldType, TeamConfigHandle, TeamTableListItem } from "../../const/team/type.ts";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { checkAccess } from "@common/utils/permission.ts";
import TeamConfig from "./TeamConfig.tsx";
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
import { $t } from "@common/locales/index.ts";
const TeamList:FC = ()=>{
const [searchWord, setSearchWord] = useState<string>('')
const navigate = useNavigate();
const location = useLocation()
const currentUrl = location.pathname
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const pageListRef = useRef<ActionType>(null);
const {fetchData} = useFetch()
const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({})
const teamConfigRef = useRef<TeamConfigHandle>(null)
const {accessData,checkPermission,accessInit, getGlobalAccessData,state} = useGlobalContext()
const [curTeam, setCurTeam] = useState<TeamConfigFieldType>({} as TeamConfigFieldType)
const [modalVisible, setModalVisible] = useState<boolean>(false)
const [modalType, setModalType] = useState<'add'|'edit'>('add')
const getTeamList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then?.(()=>{getTeamList()})
return
}
return fetchData<BasicResponse<{teams:TeamTableListItem}>>(!checkPermission('system.workspace.team.view_all') ? 'teams':'manager/teams',{method:'GET',eoParams:{keyword:searchWord},eoTransformKeys:['create_time','service_num','can_delete']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
return {data:data.teams, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const deleteTeam = (entity:TeamTableListItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>(`manager/team`,{method:'DELETE',eoParams:{id:entity.id}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
})
}
const getMemberList = async ()=>{
setMemberValueEnum({})
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
const tmpValueEnum:{[k:string]:{text:string}} = {}
data.members?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
setMemberValueEnum(tmpValueEnum)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
const manualReloadTable = () => {
pageListRef.current?.reload()
};
const openModal = async (type:'add'|'edit'|'delete',entity?:TeamTableListItem)=>{
let title:string = ''
let content:string | React.ReactNode= ''
switch (type){
case 'add':{
setModalType('add')
setModalVisible(true)
return;}
case 'edit':{
message.loading($t(RESPONSE_TIPS.loading))
const {code,data,msg} = await fetchData<BasicResponse<{team:TeamConfigFieldType}>>(`manager/team`,{method:'GET',eoParams:{id:entity!.id}})
message.destroy()
if(code === STATUS_CODE.SUCCESS){
setCurTeam({...data.team,master:data.team.master.id})
setModalVisible(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return
}
setModalType('edit')
return;}
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=>{
switch (type){
case 'delete':
return deleteTeam(entity!).then((res)=>{if(res === true) manualReloadTable()})
}
},
width:600,
okText:$t('确认'),
okButtonProps:{
disabled : !checkAccess( `system.organization.team.${type}`, accessData)
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
useEffect(() => {
setBreadcrumb([
{title: $t('团队')}
])
manualReloadTable()
}, [currentUrl]);
useEffect(()=>{
getMemberList()
},[])
const columns = useMemo(()=>{
return TEAM_TABLE_COLUMNS.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('master') !== -1 ) ){x.valueEnum = memberValueEnum} return {...x, title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
},[memberValueEnum,state.language])
return (
<InsidePage
pageTitle={$t('团队')}
description={$t("设置团队和成员,然后你可以在团队内创建服务和消费者、订阅API,成员只能看到所属团队内的服务和消费者。")}
showBorder={false}
contentClassName=" pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
>
<PageList
id="global_team"
className="pl-btnbase"
ref={pageListRef}
columns = {[...columns]}
request = {()=>getTeamList()}
showPagination={false}
addNewBtnTitle={$t('添加团队')}
addNewBtnAccess = "system.organization.team.add"
searchPlaceholder={$t("输入名称、ID、负责人查找团队")}
onAddNewBtnClick={()=>{openModal('add')}}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onRowClick={(row:TeamTableListItem)=>(navigate(`../inside/${row.id}/setting`))}
/>
<Modal
title={modalType === 'add' ? $t("添加团队") : $t("配置团队")}
open={modalVisible}
width={600}
destroyOnClose={true}
maskClosable={false}
afterOpenChange={(open:boolean)=>{
if(!open){
setModalVisible(false)
setCurTeam({} as unknown as TeamConfigFieldType)
}
}}
onCancel={() => {setModalVisible(false)}}
okText={$t("确认")}
okButtonProps={{disabled : !checkAccess( `system.organization.team.edit`, accessData)}}
cancelText={$t('取消')}
closable={true}
onOk={()=>teamConfigRef.current?.save().then((res)=>{
if(res){
setModalVisible(false)
manualReloadTable()
}
return res})}
>
<TeamConfig ref={teamConfigRef} entity={modalType === 'add' ? undefined : curTeam} />
</Modal>
</InsidePage>
)
}
export default TeamList
-17
View File
@@ -1,17 +0,0 @@
// start-vite.js// start-vite.js
import { exec } from 'child_process';
const viteProcess = exec('pnpm run build');
viteProcess.stdout.on('data', (data) => {
console.log(data.toString());
});
viteProcess.stderr.on('data', (data) => {
console.error(data.toString());
});
viteProcess.on('close', (code) => {
console.log(`Vite process exited with code ${code}`);
});
-82
View File
@@ -1,82 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);',
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
-31
View File
@@ -1,31 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"typeRoots": ["./node_modules/@types", "../common/src/types"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@core/*": ["./src/*"],
"@common/*": ["../common/src/*"],
"@market/*": ["../market/src/*"],
"@dashboard/*": ["../dashboard/src/*"],
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../common/src/components/aoplatform/ScrollableSection.tsx", "../common/src/utils/postcat.tsx", "../common/src/utils/curl.ts", "../common/src/components/aoplatform/ResetPsw.tsx", "../common/src/components/aoplatform/SubscribeApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePageForHub.tsx", "src/components/aoplatform/RenderRoutes.tsx", "../common/src/components/aoplatform/PublishApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePage.tsx", "../common/src/const/type.ts", "../common/src/components/aoplatform/intelligent-plugin", "../common/src/const/domain"],
"references": [{ "path": "./tsconfig.node.json" }]
}
-10
View File
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-84
View File
@@ -1,84 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
cacheDir: './node_modules/.vite',
build:{
target: 'esnext',
outDir:'../../dist',
sourcemap: false,
chunkSizeWarningLimit: 50,
cacheDir: './node_modules/.vite',
rollupOptions: {
output: {
chunkFileNames: 'assets/eo-[name]-[hash].js',
},
},
},
css: {
postcss: {
plugins: [
tailwindcss(path.resolve(__dirname, '../common/tailwind.config.js')),
autoprefixer
],
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
},
},
modules:{
localsConvention:"camelCase",
generateScopedName:"[local]_[hash:base64:2]"
}
},
plugins: [react(),
dynamicImportVars({
include:["src"],
exclude:[],
warnOnError:false
}),
federation({
name:"container",
remotes:{
remoteApp: 'http://localhost:5001/assets/remoteEntry.js' // 远程项目的URL
},
shared:[
"react",
"react-dom",
]
})
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@common', replacement: path.resolve(__dirname, '../common/src') },
{ find: '@market', replacement: path.resolve(__dirname, '../market/src') },
{ find: '@core', replacement: path.resolve(__dirname, './src') },
{ find: '@dashboard', replacement: path.resolve(__dirname, '../dashboard/src') },
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true,
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true,
}
},
open: true
},
logLevel:'info'
})
-11
View File
@@ -1,11 +0,0 @@
{
"name": "dashboard",
"version": "0.0.0",
"description": "dashboard for AO Platform",
"author": "maggieyyy ",
"homepage": "",
"license": "ISC",
"dependencies": {
"echarts-for-react": "^3.0.2"
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
@@ -1,81 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);'
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
"@dashboard/*": ["./src/*"]
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-16
View File
@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/frontend/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>APIPark - 企业API数据开放平台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/frontend/iconpark_eolink.js"></script>
<script src="/frontend/iconpark_apinto.js"></script>
</body>
</html>
-17
View File
@@ -1,17 +0,0 @@
{
"name": "market",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "service market project",
"scripts": {
"dev": " vite --port 5000 --strictPort",
"build": "vite build",
"test": "node ./__tests__/market.test.js"
},
"dependencies": {
"@types/dompurify": "^3.0.5",
"dompurify": "^3.1.6",
"react-virtuoso": "^4.7.11"
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
-30
View File
@@ -1,30 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
#root {
width: 100vw;
height:100vh;
}
:global.ant-tree-node-content-wrapper{
overflow: hidden;
}
.tree-title-hover{
display: flex;
justify-content: space-between;
align-items:center;
.tree-title-span{
text-overflow: ellipsis;
}
.tree-title-more{
display: none;
}
&:hover .tree-title-more{
display: flex;
height:22px;
width:22px;
}
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
"@market/*": ["./src/*"]
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-52
View File
@@ -1,52 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'
export default defineConfig({
build: {
outDir: '../../tenant_dist',
sourcemap: false,
chunkSizeWarningLimit: 50000,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
}
}
}
},
plugins: [
react(),
dynamicImportVars({
include: ['src'],
exclude: [],
warnOnError: false
})
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@market', replacement: path.resolve(__dirname, './src') },
{ find: '@common', replacement: path.resolve(__dirname, '../common/src') },
{ find: '@core', replacement: path.resolve(__dirname, '../core/src') }
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8488/',
changeOrigin: true
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8488/',
changeOrigin: true
}
}
},
logLevel: 'info'
})
-15
View File
@@ -1,15 +0,0 @@
{
"name": "open-api",
"version": "0.0.0",
"description": "openApi module",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "node ./__tests__/common.test.js"
},
"dependencies": {
"copy-to-clipboard": "^3.3.3"
},
"devDependencies": {
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
@@ -1,81 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);'
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
"@openApi/*": ["./src/*"]
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
@@ -1,8 +0,0 @@
{
"name": "systemrunning",
"version": "1.0.0",
"description": "> TODO: description",
"author": "maggieyyy <61950669+maggieyyy@users.noreply.github.com>",
"homepage": "",
"license": "ISC"
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
@@ -1,81 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);'
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1,546 @@
'use client'
import { BasicResponse, 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 localAIPic from '@common/assets/localAI.svg'
import onlineAIPic from '@common/assets/onlineAI.svg'
import restAPIPic from '@common/assets/restAPI.svg'
import { Icon } from '@iconify/react/dist/iconify.js'
import { checkAccess } from '@common/utils/permission'
import AiSettingModalContent, { AiSettingModalContentHandle } from '@core/pages/aiSetting/AiSettingModal'
import LocalAiDeploy, { LocalAiDeployHandle } from '@core/pages/guide/LocalAiDeploy'
import RestAIDeploy, { RestAIDeployHandle } from '@core/pages/guide/RestAIDeploy'
import useDeployLocalModel from '@core/pages/guide/deployModelUtil'
import { App as AppAntd, Button, Card, Collapse } from 'antd'
import { usePathname, useRouter } from 'next/navigation'
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
function AIModelGuide() {
const { message, modal } = AppAntd.useApp()
const entityData = useRef<any>(null)
const router = useRouter()
const { accessData } = useGlobalContext()
const modalRef = useRef<AiSettingModalContentHandle>()
const localAiDeployRef = useRef<LocalAiDeployHandle>()
const restAiDeployRef = useRef<RestAIDeployHandle>()
const { deployLocalModel } = useDeployLocalModel()
const { fetchData } = useFetch()
const [ollamaAddress, setOllamaAddress] = useState<string>('')
const dumpServerPage = () => {
router.push('/service/list')
}
const restCardClick = async () => {
const permission = checkAccess('system.workspace.service.edit', accessData)
if (!permission) {
return message.warning($t('暂无权限'))
}
modal.confirm({
title: $t('添加 Rest 服务'),
content: <RestAIDeploy ref={restAiDeployRef}></RestAIDeploy>,
onOk: () => {
return restAiDeployRef.current?.deployRestAIServer().then((res) => {
if (res === true) {
dumpServerPage()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const aiCardClick = () => {
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
if (!permission) {
return message.warning($t('暂无权限'))
}
const updateEntityData = (data: any) => {
entityData.current = data
modalInstance.update({})
}
const modalInstance = modal.confirm({
title: $t('模型配置'),
content: (
<AiSettingModalContent
ref={modalRef}
modelMode="manual"
updateEntityData={updateEntityData}
source="guide"
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
),
onOk: () => {
return modalRef.current?.deployAIServer().then((res) => {
if (res === true) {
dumpServerPage()
}
})
},
width: 600,
okText: $t('确认'),
footer: (_, { OkBtn, CancelBtn }) => (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href={entityData.current?.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [entityData.current?.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
<CancelBtn />
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
</div>
</div>
),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
useEffect(() => {
fetchData<BasicResponse<{ data: any[] }>>('model/local/source/ollama', {
method: 'GET'
}).then((response) => {
if (response.code === STATUS_CODE.SUCCESS) {
setOllamaAddress(response.data?.config?.address || '')
} else {
message.error(response.msg || $t(RESPONSE_TIPS.error))
}
})
}, [])
const localModelCardClick = async () => {
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
if (!permission) {
return message.warning($t('暂无权限'))
}
if (!ollamaAddress) {
router.push('/aisetting?status=unconfigure')
return
}
const modalInstance = modal.confirm({
title: $t('部署本地模型'),
content: (
<LocalAiDeploy
ref={localAiDeployRef}
onClose={() => {
modalInstance.destroy()
dumpServerPage()
}}
></LocalAiDeploy>
),
onOk: () => {
return localAiDeployRef.current?.deployLocalAIServer().then((res) => {
if (res === true) {
dumpServerPage()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const deployDeepSeek = async (e: any) => {
e.stopPropagation()
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
if (!permission) {
return message.warning($t('暂无权限'))
}
if (!ollamaAddress) {
router.push('/aisetting?status=unconfigure')
return
}
await deployLocalModel({ modelID: 'deepseek-r1' })
dumpServerPage()
}
const cardList = [
{
imgSrc: restAPIPic,
title: $t('添加 Rest 服务'),
description: $t('导入OpenAPI文档,将现有系统的API发布到APIPark。'),
click: restCardClick
},
{
imgSrc: onlineAIPic,
title: $t('添加在线 AI API'),
description: $t('添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。'),
click: aiCardClick
},
{
imgSrc: localAIPic,
title: $t('本地部署 AI 并生成 API'),
description: $t('快速在本地部署开源模型并自动生成 API。'),
click: localModelCardClick,
bottomRender: (
<span className="text-[#2196f3] text-[13px] hover:text-[#1976d2]" onClick={deployDeepSeek}>
<Icon className="align-sub mr-[5px]" icon="lsicon:lightning-filled" width="15" height="15" />
{$t('部署')} Deepseek-R1
</span>
)
}
]
return (
<>
<p>{$t('⚡您可快速通过以下方式开放API供大家使用:')}</p>
<div className="mb-[30px] pt-[25px] flex justify-between space-x-4">
{cardList.map((item, itemIndex) => (
<Card
key={itemIndex}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] overflow-visible cursor-pointer flex-1 transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
body: 'p-[20px] pt-[50px] pb-[50px] text-[12px] text-[#666] text-center'
}}
onClick={item.click}
>
<img src={item.imgSrc} alt="" width={60} height={60} />
<p className="text-[13px] font-bold text-black mt-[10px] mb-[10px]">{item.title}</p>
<p className="break-words mb-[10px]">{item.description}</p>
{item.bottomRender ? item.bottomRender : null}
</Card>
))}
</div>
</>
)
}
function QuickGuideContent({
changeGuideShow,
guideSections
}: {
changeGuideShow: Dispatch<SetStateAction<boolean>>
guideSections: {
title: string
items: {
title: string
description: string
link: string
}[]
}[]
}) {
return (
<div className="">
{guideSections.map((section, index) => (
<div key={index}>
<p className="flex gap-[8px] items-center text-[14px] font-bold">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
{section.title}
</p>
<div className="ml-[9px] border-[0px] border-l-[1px] my-[10px] border-dashed border-BORDER">
<div
className="grid gap-[20px] px-[20px] py-[10px] justify-start content-start"
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 0fr))',
gridAutoRows: '1fr'
}}
>
{section.items.map((item, itemIndex) => (
<Card
key={itemIndex}
title={item.title}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer w-[300px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
body: 'p-[20px] pt-0 text-[12px] text-[#666]'
}}
onClick={() => window.open(item.link, '_blank')}
>
<span>{item.description}</span>
</Card>
))}
</div>
</div>
</div>
))}
<div className="flex gap-[8px] items-center">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
<div className="flex items-center w-full gap-4">
<Button
type="link"
icon={<Icon icon="ic:baseline-open-in-new" width="18" height="18" />}
iconPosition="end"
classNames={{ icon: 'h-[22px] flex items-center' }}
href="https://docs.apipark.com"
target="_blank"
className="text-[14px] font-bold px-0"
>
{$t('了解更多功能')}
</Button>
<Button
type="text"
icon={<Icon icon="ic:baseline-visibility-off" width="18" height="18" />}
onClick={() => changeGuideShow((prev) => !prev)}
classNames={{ icon: 'h-[22px] flex items-center' }}
className="text-[14px] font-bold"
>
{$t('隐藏该教程')}
</Button>
</div>
</div>
</div>
)
}
export default function GuidePage() {
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false')
const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false')
const [, forceUpdate] = useState<unknown>(null)
const { state } = useGlobalContext()
const pathname = usePathname()
const router = useRouter()
useEffect(() => {
setShowGuide(window.localStorage.getItem('showGuide') !== 'false')
setShowAdvancedGuide(window.localStorage.getItem('showAdvancedGuide') !== 'false')
}, [])
const guideSections = useMemo(
() => [
{
title: $t('快速接入 AI'),
items: [
{
title: $t('配置你的 AI 模型'),
description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'),
link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers'
},
{
title: $t('创建 AI 服务和 API'),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/ai_services'
},
{
title: $t('创建调用 Token'),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t('调用'),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
]
},
{
title: $t('快速接入 REST API'),
items: [
{
title: $t('创建 REST 服务和 API'),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/rest_services'
},
{
title: $t('创建调用 Token'),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t('调用'),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
]
},
{
title: $t('仪表盘'),
items: [
{
title: $t('统计 API 调用情况'),
description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'),
link: 'https://docs.apipark.com/docs/analysis'
}
]
}
],
[state.language]
)
const advanceGuideSections = useMemo(
() => [
{
title: $t('核心功能'),
items: [
{
title: $t('账号与角色'),
description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'),
link: 'https://docs.apipark.com/docs/system_setting/account_role'
},
{
title: $t('团队'),
description: $t(
'团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'
),
link: 'https://docs.apipark.com/docs/teams'
},
{
title: $t('服务'),
description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'),
link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1'
}
]
},
{
title: $t('权限管理'),
items: [
{
title: $t('订阅服务'),
description: $t(
'如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'
),
link: 'https://docs.apipark.com/docs/developer_portal'
},
{
title: $t('审核订阅申请'),
description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/services/review_consumers'
}
]
},
{
title: $t('集成'),
items: [
{
title: $t('日志'),
description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'),
link: 'https://docs.apipark.com/docs/system_setting/log/'
}
]
}
],
[state.language]
)
useEffect(() => {
window.localStorage.setItem('showGuide', showGuide.toString())
}, [showGuide])
useEffect(() => {
window.localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString())
}, [showAdvancedGuide])
useEffect(() => {
if (pathname === '/guide') {
router.replace('/guide/page')
}
}, [pathname, router])
useEffect(() => {
forceUpdate({})
}, [state.language])
return (
<div className="flex flex-col flex-1 h-full overflow-auto">
<div className="border-[0px] mr-PAGE_INSIDE_X pt-[30px] pl-[40px]">
<div className="mb-[30px]">
<div className="flex justify-between mb-[20px] items-center">
<div className="flex items-center gap-[8px] text-theme text-[26px]">
<span>👋</span>
<span>{$t('Hello!欢迎使用 APIPark')}</span>
</div>
</div>
<div className="flex flex-col gap-[8px]">
<p>
<span className="font-bold">🦄 APIPark </span>
{$t(
'是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。'
)}
</p>
<p>
{$t('✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。')}
<span className="font-bold">
{$t('点击这里')}
<span className="align-middle leading-[16px]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank">
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" alt="" />
</a>
<span className="align-middle leading-[16px]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
{$t('点击')}
&nbsp;
<span className="align-middle leading-[16px]">
<Icon icon="emojione:star" width="16" height="16" />
</span>
Star
</span>
</p>
</div>
</div>
</div>
<div className="w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B pl-[40px]">
<AIModelGuide />
<div className="flex flex-col gap-[15px] pb-PAGE_INSIDE_B">
{showGuide && (
<Collapse
size="large"
expandIconPosition="end"
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent"
items={[
{
key: '1',
label: (
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🚀</span>
<span>{`${$t('快速入门')}`}</span>{' '}
</p>
<p className="text-[12px]">{$t('我们提供了一些任务来帮你快速了解 APIPark')}</p>
</div>
),
children: <QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} />
}
]}
/>
)}
{showAdvancedGuide && (
<Collapse
size="large"
expandIconPosition="end"
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent"
items={[
{
key: '1',
label: (
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🏍</span>
<span>{`${$t('进阶教程')}`}</span>{' '}
</p>
<p className="text-[12px]">{$t('了解 APIPark 如何更好地管理 API 和 AI')}</p>
</div>
),
children: <QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} />
}
]}
/>
)}
</div>
</div>
</div>
)
}
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
+319
View File
@@ -0,0 +1,319 @@
'use client'
import { StyleProvider } from '@ant-design/cssinjs'
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components'
import { LoadingOutlined } from '@ant-design/icons'
import AvatarPic from '@common/assets/default-avatar.png'
import Logo from '@common/assets/layout-logo.png'
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const'
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { UserInfoType } from '@common/const/type'
import { GlobalProvider, useGlobalContext } from '@common/contexts/GlobalStateContext'
import { LocaleProvider, useLocaleContext } from '@common/contexts/LocaleContext'
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'
import { PluginSlotHubProvider, usePluginSlotHub } from '@common/contexts/PluginSlotHubContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { transformMenuData } from '@common/utils/navigation'
import { Icon } from '@iconify/react/dist/iconify.js'
import { App as AppAntd, Button, ConfigProvider, Dropdown, MenuProps, Spin } from 'antd'
import { usePathname, useRouter } from 'next/navigation'
import { ReactNode, useEffect, useMemo, useState } from 'react'
const themeToken = {
bgLayout: '#17163E;',
header: {
heightLayoutHeader: 72
},
pageContainer: {
paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0
}
}
function AdminShell({ children, project = 'core' }: { children: ReactNode; project?: string }) {
const router = useRouter()
const pathname = usePathname()
const { state, accessData, checkPermission, accessInit, dispatch, resetAccess, getGlobalAccessData, menuList } =
useGlobalContext()
const [currentPath, setCurrentPath] = useState(pathname)
const mainPage = state.mainPage || (project === 'core' ? '/guide/page' : '/portal/list')
const [menuItems, setMenuItems] = useState<MenuProps['items']>()
const pluginSlotHub = usePluginSlotHub()
const { message } = AppAntd.useApp()
const [userInfo, setUserInfo] = useState<UserInfoType>()
const { fetchData } = useFetch()
useEffect(() => {
setMenuItems(transformMenuData(menuList))
}, [menuList, state.language, accessInit])
useEffect(() => {
if (pathname === '/') {
router.push(mainPage)
}
}, [pathname, mainPage, router])
useEffect(() => {
setCurrentPath(pathname)
}, [pathname])
const headerMenuData = useMemo(() => {
const hasAccess = (access: unknown) => checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
return [...menu]
.filter((x) => x)
.map((item: any) => {
if (item.routes && item.routes.length > 0) {
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes)
if (filteredRoutes.length === 0) {
return false
}
return { ...item, routes: filteredRoutes, name: $t(item.name) }
}
if (item.access) {
return item.access === 'all' || hasAccess(item.access) ? { ...item, name: $t(item.name) } : null
}
return { ...item, name: $t(item.name) }
})
.filter((x) => x)
}
const res = [...(menuItems || [])]
.filter((x) => x)
.map((x: any) =>
x.routes ? { ...x, name: $t(x.name), routes: filterMenu(x.routes) } : { ...x, name: $t(x.name) }
)
return {
path: '/',
routes: res
.map((x) => ({ ...x, routes: x.routes?.filter((routeItem: any) => routeItem.access || routeItem.routes?.length > 0) }))
.filter((x) => x.access || x.routes?.length > 0)
}
}, [accessData, state.language, menuItems, checkPermission])
useEffect(() => {
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
getGlobalAccessData()
}, [])
const logOut = () => {
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGOUT' })
resetAccess()
router.push('/admin/login')
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const items: MenuProps['items'] = useMemo(
() =>
[
!['guest', 'third-user'].includes(userInfo?.type as string) && {
key: '2',
label: (
<Button
key="changePsw"
type="text"
className="flex items-center p-0 bg-transparent border-none"
onClick={() => router.push('/userProfile/changepsw')}
>
{$t('账号设置')}
</Button>
)
},
{
key: '3',
label: (
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none" onClick={logOut}>
{$t('退出登录')}
</Button>
)
}
].filter(Boolean),
[userInfo, router]
)
const actionRender = useMemo(() => {
return [
<LanguageSetting key="lang" />,
<Button
key="docs"
className="text-[#ffffffb3] hover:text-[#fff] border-none"
type="default"
ghost
onClick={() => window.open('https://docs.apipark.com', '_blank')}
>
<span className="flex items-center gap-[8px]">
<Icon icon="ic:baseline-help" width="14" height="14" />
{$t('文档')}
</span>
</Button>,
...(((pluginSlotHub.getSlot('basicLayoutAfterBtns') as ReactNode[]) || []) as ReactNode[])
]
}, [state.language, pluginSlotHub])
const logoSrc = typeof Logo === 'string' ? Logo : (Logo as any)?.src
const avatarSrc = userInfo?.avatar || (typeof AvatarPic === 'string' ? AvatarPic : (AvatarPic as any)?.src)
return (
<div
id="test-pro-layout"
style={{
height: '100vh',
overflow: 'auto'
}}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('test-pro-layout') || document.body
}}
>
<ProLayout
prefixCls="apipark-layout"
location={{ pathname: currentPath }}
siderWidth={220}
breakpoint={'lg'}
route={headerMenuData as any}
token={themeToken}
siderMenuType="group"
menu={{ type: 'group', collapsedShowGroupTitle: true }}
disableMobile={true}
avatarProps={{
src: avatarSrc,
size: 'small',
title: userInfo?.username || 'unknown',
render: (props, dom) => (
<Dropdown menu={{ items }}>
<div className="avatar-dom">{dom}</div>
</Dropdown>
)
}}
actionsRender={(props) => {
if (props.isMobile) return []
return actionRender
}}
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
<img className="h-[20px] cursor-pointer" src={logoSrc} onClick={() => router.push(mainPage)} alt="logo" />
<a
className="align-text-top ml-[5px] h-[25px] relative"
href="https://github.com/APIParkLab/APIPark"
target="_blank"
rel="noreferrer"
>
<img
src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social"
className="absolute top-[6px]"
width={75}
alt=""
/>
</a>
</div>
)}
logo={logoSrc}
pageTitleRender={() => $t('APIPark')}
menuFooterRender={(props) => {
if (props?.collapsed) return undefined
}}
menuItemRender={(item, dom) => (
<div
onClick={() => {
if (
item.key &&
routerKeyMap.get(item.key as string) &&
(routerKeyMap.get(item.key as string) as string[])?.length > 0 &&
(routerKeyMap.get(item.key as string) as string[])?.indexOf(currentPath.split('/')[1]) !== -1
) {
return
}
if (item.key === currentPath.split('/')[1]) {
return
}
if (item.path) {
router.push(item.path)
}
setCurrentPath(item.path || '')
}}
>
{dom}
</div>
)}
fixSiderbar={true}
layout="mix"
splitMenus={true}
collapsed={false}
collapsedButtonRender={false}
>
<div
className={`w-full h-calc-100vh-minus-navbar ${currentPath.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
} ${currentPath.startsWith('/guide/page') ? '' : 'pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T'}`}
>
{children}
</div>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
}
function AdminProviders({ children }: { children: ReactNode }) {
const { locale } = useLocaleContext()
return (
<StyleProvider hashPriority="high">
<ConfigProvider locale={locale} wave={{ disabled: true }}>
<PluginEventHubProvider>
<GlobalProvider>
<AppAntd className="h-full" message={{ maxCount: 1 }}>
<PluginSlotHubProvider>
<AdminShell project="core">{children}</AdminShell>
</PluginSlotHubProvider>
</AppAntd>
</GlobalProvider>
</PluginEventHubProvider>
</ConfigProvider>
</StyleProvider>
)
}
export default function AdminLayout({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<Spin
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={true}
className="w-full h-full flex items-center justify-center"
/>
)
}
return (
<LocaleProvider>
<AdminProviders>{children}</AdminProviders>
</LocaleProvider>
)
}
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1 @@
export { default } from "@/app/_legacy/LegacyAppPage";
@@ -0,0 +1,15 @@
import { redirect } from 'next/navigation'
export default async function ServiceLegacyFallbackPage({
params
}: {
params: Promise<{ slug: string[] }>
}) {
const { slug } = await params
if (slug.length >= 4 && (slug[1] === 'inside' || slug[1] === 'aiInside')) {
redirect(`/service/${slug[0]}/${slug[1]}/${slug[2]}/overview`)
}
redirect('/service/list')
}
@@ -0,0 +1,5 @@
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
export default function AiServiceApprovalRoutePage() {
return <ServiceDetailLegacyTabs side="aiInside" type="approval" />
}
@@ -0,0 +1,46 @@
'use client'
import { usePathname } from 'next/navigation'
import { ReactNode } from 'react'
import { ServiceDetailLayout } from '../../../_components/ServicePages'
const serviceKeys = [
'overview',
'route',
'api',
'document',
'servicepolicy',
'publish',
'approval',
'subscriber',
'setting',
'logs'
] as const
function getActiveKey(pathname: string) {
const segments = pathname.split('/').filter(Boolean)
const active = segments[4]
return (serviceKeys.find((key) => key === active) || 'overview') as (typeof serviceKeys)[number]
}
export default function AiServiceDetailLayout({
children,
params
}: {
children: ReactNode
params: { teamId: string; serviceId: string }
}) {
const pathname = usePathname()
const { teamId, serviceId } = params
return (
<ServiceDetailLayout
teamId={teamId}
serviceId={serviceId}
side="aiInside"
activeKey={getActiveKey(pathname)}
>
{children}
</ServiceDetailLayout>
)
}
@@ -0,0 +1,10 @@
import { ServiceOverviewPage } from '../../../../_components/ServicePages'
export default async function AiServiceOverviewRoutePage({
params
}: {
params: Promise<{ teamId: string; serviceId: string }>
}) {
const { teamId, serviceId } = await params
return <ServiceOverviewPage serviceType="aiService" teamId={teamId} serviceId={serviceId} />
}
@@ -0,0 +1,10 @@
import { redirect } from 'next/navigation'
export default async function AiServiceEntryPage({
params
}: {
params: Promise<{ teamId: string; serviceId: string }>
}) {
const { teamId, serviceId } = await params
redirect(`/service/${teamId}/aiInside/${serviceId}/overview`)
}
@@ -0,0 +1,5 @@
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
export default function AiServicePublishRoutePage() {
return <ServiceDetailLegacyTabs side="aiInside" type="publish" />
}
@@ -0,0 +1,10 @@
import { ServiceRouteListPage } from '../../../../_components/ServicePages'
export default async function AiServiceRouteListRoutePage({
params
}: {
params: Promise<{ teamId: string; serviceId: string }>
}) {
const { teamId, serviceId } = await params
return <ServiceRouteListPage teamId={teamId} serviceId={serviceId} side="aiInside" />
}
@@ -0,0 +1,5 @@
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
export default function RestServiceApprovalRoutePage() {
return <ServiceDetailLegacyTabs side="inside" type="approval" />
}
@@ -0,0 +1,47 @@
'use client'
import { usePathname } from 'next/navigation'
import { ReactNode } from 'react'
import { ServiceDetailLayout } from '../../../_components/ServicePages'
const serviceKeys = [
'overview',
'route',
'api',
'upstream',
'document',
'servicepolicy',
'publish',
'approval',
'subscriber',
'setting',
'logs'
] as const
function getActiveKey(pathname: string) {
const segments = pathname.split('/').filter(Boolean)
const active = segments[4]
return (serviceKeys.find((key) => key === active) || 'overview') as (typeof serviceKeys)[number]
}
export default function RestServiceDetailLayout({
children,
params
}: {
children: ReactNode
params: { teamId: string; serviceId: string }
}) {
const pathname = usePathname()
const { teamId, serviceId } = params
return (
<ServiceDetailLayout
teamId={teamId}
serviceId={serviceId}
side="inside"
activeKey={getActiveKey(pathname)}
>
{children}
</ServiceDetailLayout>
)
}
@@ -0,0 +1,10 @@
import { ServiceOverviewPage } from '../../../../_components/ServicePages'
export default async function RestServiceOverviewPage({
params
}: {
params: Promise<{ teamId: string; serviceId: string }>
}) {
const { teamId, serviceId } = await params
return <ServiceOverviewPage serviceType="restService" teamId={teamId} serviceId={serviceId} />
}
@@ -0,0 +1,10 @@
import { redirect } from 'next/navigation'
export default async function RestServiceEntryPage({
params
}: {
params: Promise<{ teamId: string; serviceId: string }>
}) {
const { teamId, serviceId } = await params
redirect(`/service/${teamId}/inside/${serviceId}/overview`)
}
@@ -0,0 +1,5 @@
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
export default function RestServicePublishRoutePage() {
return <ServiceDetailLegacyTabs side="inside" type="publish" />
}
@@ -0,0 +1,10 @@
import { ServiceRouteListPage } from '../../../../_components/ServicePages'
export default async function RestServiceRouteListRoutePage({
params
}: {
params: Promise<{ teamId: string; serviceId: string }>
}) {
const { teamId, serviceId } = await params
return <ServiceRouteListPage teamId={teamId} serviceId={serviceId} side="inside" />
}
@@ -0,0 +1,99 @@
'use client'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { $t } from '@common/locales'
import { SYSTEM_INSIDE_APPROVAL_TAB_ITEMS, SYSTEM_PUBLISH_TAB_ITEMS } from '@core/const/system/const'
import AiServiceInsideApprovalList from '@core/pages/aiService/approval/AiServiceInsideApprovalList'
import AiServiceInsidePublishList from '@core/pages/aiService/publish/AiServiceInsidePublishList'
import SystemInsideApprovalList from '@core/pages/system/approval/SystemInsideApprovalList'
import SystemInsidePublishList from '@core/pages/system/publish/SystemInsidePublishList'
import { Tabs } from 'antd'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { ReactElement, useMemo } from 'react'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
type ServiceDetailLegacyTabsProps = {
side: 'inside' | 'aiInside'
type: 'approval' | 'publish'
}
function buildTabHref(pathname: string, searchParams: URLSearchParams, key: string) {
const nextSearchParams = new URLSearchParams(searchParams.toString())
if (key === '0') {
nextSearchParams.delete('status')
} else {
nextSearchParams.set('status', key)
}
const nextQuery = nextSearchParams.toString()
return nextQuery ? `${pathname}?${nextQuery}` : pathname
}
function LegacyRouteBridge({
pathname,
search,
routeType,
element
}: {
pathname: string
search: string
routeType: 'approval' | 'publish'
element: ReactElement
}) {
const entry = `${pathname}${search ? `?${search}` : ''}`
const routePath = `/service/:teamId/:side/:serviceId/${routeType}`
return (
<MemoryRouter initialEntries={[entry]} key={entry}>
<Routes>
<Route path={routePath} element={element} />
<Route path={`${routePath}/*`} element={element} />
</Routes>
</MemoryRouter>
)
}
export function ServiceDetailLegacyTabs({ side, type }: ServiceDetailLegacyTabsProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const { state } = useGlobalContext()
const status = searchParams.get('status') || '0'
const search = searchParams.toString()
const tabItems = useMemo(
() =>
(type === 'approval' ? SYSTEM_INSIDE_APPROVAL_TAB_ITEMS : SYSTEM_PUBLISH_TAB_ITEMS)?.map((item) => ({
...item,
label: typeof item?.label === 'string' ? $t(item.label) : item?.label
})),
[type, state.language]
)
const content = useMemo(() => {
if (type === 'approval') {
return side === 'aiInside' ? <AiServiceInsideApprovalList /> : <SystemInsideApprovalList />
}
return side === 'aiInside' ? <AiServiceInsidePublishList /> : <SystemInsidePublishList />
}, [side, type])
return (
<>
<Tabs
activeKey={status}
size="small"
className="h-auto bg-MAIN_BG"
tabBarStyle={{ paddingLeft: '10px' }}
tabBarGutter={20}
items={tabItems}
destroyInactiveTabPane={true}
onChange={(key) => {
router.push(buildTabHref(pathname, new URLSearchParams(search), key))
}}
/>
<LegacyRouteBridge pathname={pathname} search={search} routeType={type} element={content} />
</>
)
}
@@ -0,0 +1,968 @@
'use client'
import PageList from '@common/components/aoplatform/PageList'
import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard'
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
import { TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { SimpleMemberItem, SimpleTeamItem } from '@common/const/type'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { ActionType } from '@ant-design/pro-components'
import { LoadingOutlined } from '@ant-design/icons'
import { App as AppAntd, Card, Menu, MenuProps, Spin, Tag } from 'antd'
import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const'
import { AiServiceRouterTableListItem } from '@core/const/ai-service/type'
import { SERVICE_KIND_OPTIONS, SYSTEM_API_TABLE_COLUMNS, SYSTEM_TABLE_COLUMNS } from '@core/const/system/const'
import { SystemApiTableListItem, SystemTableListItem } from '@core/const/system/type'
import RankingList from '@core/pages/serviceOverview/rankingList/RankingList'
import ServiceAreaChart from '@core/pages/serviceOverview/charts/ServiceAreaChart'
import ServiceBarChar, { BarChartInfo } from '@core/pages/serviceOverview/charts/ServiceBarChar'
import DateSelectFilter, { TimeOption } from '@core/pages/serviceOverview/filter/DateSelectFilter'
import { setBarChartInfoData } from '@core/pages/serviceOverview/utils'
import { LogsFooter } from '@core/pages/system/serviceDeployment/ServiceDeployMentFooter'
import { ServiceDeployment } from '@core/pages/system/serviceDeployment/ServiceDeployment'
import {
abbreviateFloat,
formatBytes,
formatDuration,
formatNumberWithUnit,
getTime
} from '@dashboard/utils/dashboard'
import { useRouter } from 'next/navigation'
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
export type ServiceSide = 'inside' | 'aiInside'
export type ServiceMenuKey =
| 'overview'
| 'route'
| 'api'
| 'upstream'
| 'document'
| 'servicepolicy'
| 'publish'
| 'approval'
| 'subscriber'
| 'setting'
| 'logs'
function ServiceOverviewIndicator({
indicatorInfo,
onNavigate
}: {
indicatorInfo: any
onNavigate: (path: string) => void
}) {
const side = indicatorInfo?.serviceKind === 'ai' ? 'aiInside' : 'inside'
const items = [
{
title: indicatorInfo?.enableMcp ? 'APIs / Tools' : 'APIs',
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/route`,
content: indicatorInfo?.apiNum ?? 0
},
{
title: $t('订阅数量'),
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/subscriber`,
content: indicatorInfo?.subscriberNum ?? 0
},
{
title: 'MCP',
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/setting`,
content: indicatorInfo?.enableMcp ? $t('已开启') : $t('开启 MCP')
}
]
return (
<div className="flex">
{items.map((item, index) => (
<Card
key={item.title}
className={`flex-1 rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{ body: 'py-[20px] px-[18px]' }}
onClick={() => {
if (item.link) {
onNavigate(item.link)
}
}}
>
<div className="text-[14px] text-[#999999] mb-[10px]" style={{ fontFamily: 'Microsoft YaHei' }}>
{item.title}
</div>
<div
className={`${index < 2 ? 'text-[32px] font-medium text-[#101010]' : 'text-[14px]'}`}
style={{ fontFamily: 'Microsoft YaHei' }}
>
{item.content}
</div>
</Card>
))}
</div>
)
}
export function ServiceOverviewPage({
serviceType,
teamId,
serviceId
}: {
serviceType: 'aiService' | 'restService'
teamId: string
serviceId: string
}) {
const { fetchData } = useFetch()
const { message } = AppAntd.useApp()
const { state } = useGlobalContext()
const router = useRouter()
const [dashboardLoading, setDashboardLoading] = useState(true)
const [defaultTime] = useState<TimeOption>('day')
const [timeRange, setTimeRange] = useState<TimeRange | undefined>()
const [barChartInfo, setBarChartInfo] = useState<any>()
const [perBarChartInfo, setPerBarChartInfo] = useState<any>()
const [indicatorInfo, setIndicatorInfo] = useState<any>([])
const [topRankingList, setTopRankingList] = useState<any>([])
const [aiServiceOverview, setAiServiceOverview] = useState<any>()
const [restServiceOverview, setRestServiceOverview] = useState<any>()
const selectCallback = (date: TimeRange) => {
setTimeRange(date)
}
const setRestChartInfo = (serviceOverview: any) => {
setIndicatorInfo({
apiNum: serviceOverview.apiNum,
subscriberNum: serviceOverview.subscriberNum,
teamId,
enableMcp: serviceOverview.enableMcp,
serviceKind: serviceOverview.serviceKind,
serviceId
})
setBarChartInfo([
{
...setBarChartInfoData({
title: $t('请求次数'),
data: serviceOverview.requestOverview,
value: formatNumberWithUnit(serviceOverview.requestTotal),
date: serviceOverview.date
}),
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
},
{
...setBarChartInfoData({
title: $t('网络流量'),
data: serviceOverview.trafficOverview,
value: formatBytes(serviceOverview.trafficTotal),
date: serviceOverview.date
}),
traffic2xxTotal: formatBytes(serviceOverview.traffic2xxTotal),
traffic4xxTotal: formatBytes(serviceOverview.traffic4xxTotal),
traffic5xxTotal: formatBytes(serviceOverview.traffic5xxTotal)
}
])
setPerBarChartInfo([
{
title: $t('平均响应时间'),
data: serviceOverview.avgResponseTimeOverview,
value: formatDuration(serviceOverview.avgResponseTime),
originValue: serviceOverview.avgResponseTime,
date: serviceOverview.date,
max: formatDuration(serviceOverview.maxResponseTime),
min: formatDuration(serviceOverview.minResponseTime),
type: 'area',
showXAxis: false
},
{
...setBarChartInfoData({
title: $t('平均每消费者的请求次数'),
data: serviceOverview.avgRequestPerSubscriberOverview,
date: serviceOverview.date,
showXAxis: false
}),
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
},
{
...setBarChartInfoData({
title: $t('平均每消费者的网络流量'),
data: serviceOverview.avgTrafficPerSubscriberOverview,
date: serviceOverview.date,
showXAxis: false
}),
max: formatBytes(serviceOverview.maxTrafficPerSubscriber),
min: formatBytes(serviceOverview.minTrafficPerSubscriber)
}
])
}
const setAiChartInfo = (serviceOverview: any) => {
setIndicatorInfo({
apiNum: serviceOverview.apiNum,
subscriberNum: serviceOverview.subscriberNum,
teamId,
enableMcp: serviceOverview.enableMcp,
serviceKind: serviceOverview.serviceKind,
serviceId
})
setBarChartInfo([
{
...setBarChartInfoData({
title: $t('请求次数'),
data: serviceOverview.requestOverview,
value: formatNumberWithUnit(serviceOverview.requestTotal),
date: serviceOverview.date
}),
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
},
{
...setBarChartInfoData({
title: $t('Token 消耗'),
data: serviceOverview.tokenOverview.map((item: { inputToken: number; outputToken: number }) => ({
inputToken: item.inputToken,
outputToken: item.outputToken
})),
value: formatNumberWithUnit(serviceOverview.tokenTotal),
date: serviceOverview.date
}),
inputTokenTotal: formatNumberWithUnit(serviceOverview.inputTokenTotal),
outputTokenTotal: formatNumberWithUnit(serviceOverview.outputTokenTotal)
}
])
setPerBarChartInfo([
{
title: $t('平均 Token 消耗'),
data: serviceOverview.avgTokenOverview,
value: `${formatNumberWithUnit(serviceOverview.avgToken)} Token/s`,
originValue: serviceOverview.avgToken,
date: serviceOverview.date,
min: `${formatNumberWithUnit(serviceOverview.minToken)} Token/s`,
max: `${formatNumberWithUnit(serviceOverview.maxToken)} Token/s`,
type: 'area'
},
{
...setBarChartInfoData({
title: $t('平均每消费者的请求次数'),
data: serviceOverview.avgRequestPerSubscriberOverview,
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
},
{
...setBarChartInfoData({
title: $t('平均每消费者的 Token 消耗'),
data: serviceOverview.avgTokenPerSubscriberOverview.map((item: { inputToken: number; outputToken: number }) => ({
inputToken: item.inputToken,
outputToken: item.outputToken
})),
date: serviceOverview.date
}),
max: abbreviateFloat(serviceOverview.maxTokenPerSubscriber),
min: abbreviateFloat(serviceOverview.minTokenPerSubscriber)
}
])
}
const getAIServiceOverview = () => {
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/ai', {
method: 'GET',
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
eoTransformKeys: [
'enable_mcp',
'subscriber_num',
'api_num',
'service_kind',
'avaliable_monitor',
'request_overview',
'token_overview',
'avg_token_overview',
'avg_request_per_subscriber_overview',
'avg_token_per_subscriber_overview',
'request_total',
'token_total',
'avg_token',
'max_token',
'min_token',
'avg_request_per_subscriber',
'avg_token_per_subscriber',
'input_token',
'output_token',
'total_token',
'request_2xx_total',
'request_4xx_total',
'request_5xx_total',
'input_token_total',
'output_token_total',
'max_token_per_subscriber',
'min_token_per_subscriber',
'max_request_per_subscriber',
'min_request_per_subscriber'
]
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setAiServiceOverview(data.overview)
setAiChartInfo(data.overview)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
setDashboardLoading(false)
})
}
const getRestServiceOverview = () => {
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/rest', {
method: 'GET',
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
eoTransformKeys: [
'enable_mcp',
'subscriber_num',
'api_num',
'service_kind',
'avaliable_monitor',
'request_overview',
'traffic_overview',
'avg_request_per_subscriber_overview',
'avg_response_time_overview',
'avg_traffic_per_subscriber_overview',
'request_total',
'traffic_total',
'max_response_time',
'min_response_time',
'avg_response_time',
'avg_request_per_subscriber',
'avg_traffic_per_subscriber',
'request_2xx_total',
'request_4xx_total',
'request_5xx_total',
'traffic_2xx_total',
'traffic_4xx_total',
'traffic_5xx_total',
'max_request_per_subscriber',
'min_request_per_subscriber',
'max_traffic_per_subscriber',
'min_traffic_per_subscriber'
]
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setRestServiceOverview(data.overview)
setRestChartInfo(data.overview)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
setDashboardLoading(false)
})
}
const getTopRankingList = () => {
fetchData<BasicResponse<any>>('service/monitor/top10', {
method: 'GET',
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end }
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setTopRankingList({
'TOP API': data.apis,
'TOP Consumer': data.consumers
})
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
setDashboardLoading(false)
})
}
useEffect(() => {
const { startTime, endTime } = getTime(defaultTime, [])
setTimeRange({ start: startTime, end: endTime })
}, [defaultTime])
useEffect(() => {
if (timeRange) {
setDashboardLoading(true)
if (serviceType === 'aiService') {
getAIServiceOverview()
} else {
getRestServiceOverview()
}
getTopRankingList()
}
}, [timeRange])
useEffect(() => {
if (serviceType === 'aiService') {
if (aiServiceOverview) {
setAiChartInfo(aiServiceOverview)
}
} else if (restServiceOverview) {
setRestChartInfo(restServiceOverview)
}
}, [state.language])
return (
<Spin
className="h-full pb-[20px]"
wrapperClassName="h-full min-h-[150px]"
indicator={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ transform: 'scale(1.5)' }}>
<LoadingOutlined style={{ fontSize: 30 }} spin />
</div>
</div>
}
spinning={dashboardLoading}
>
<div className="mr-[30px]">
<ServiceOverviewIndicator indicatorInfo={indicatorInfo} onNavigate={(path) => router.push(path)} />
<div className="mt-[20px]">
<DateSelectFilter selectCallback={selectCallback} defaultTime={defaultTime} />
</div>
<div className="mt-[20px] flex mb-[10px]">
{barChartInfo?.map((item: BarChartInfo, index: number) => (
<Card
key={index}
className={`flex-1 min-w-[430px] rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{ body: 'py-[15px] px-[0px]' }}
>
<ServiceBarChar showLegendIndicator={true} height={400} dataInfo={item} customClassNames="flex-1" />
</Card>
))}
</div>
<div className="flex mb-[10px]">
{perBarChartInfo?.map((item: any, index: number) => (
<Card
key={index}
className={`flex-1 rounded-[10px] min-w-[284px] ${index > 0 ? 'ml-[10px]' : ''}`}
classNames={{ body: 'py-[15px] px-[0px]' }}
>
{item.type === 'area' ? (
<ServiceAreaChart
height={270}
dataInfo={item}
showAvgLine={true}
customClassNames="flex-1 relative"
/>
) : (
<ServiceBarChar height={270} dataInfo={item} hideIndicatorValue={true} customClassNames="flex-1" />
)}
</Card>
))}
</div>
<RankingList topRankingList={topRankingList} serviceType={serviceType} />
</div>
</Spin>
)
}
export function ServiceRouteListPage({
teamId,
serviceId,
side
}: {
teamId: string
serviceId: string
side: ServiceSide
}) {
const router = useRouter()
const { fetchData } = useFetch()
const { modal, message } = AppAntd.useApp()
const pageListRef = useRef<ActionType>(null)
const { state } = useGlobalContext()
const [searchWord, setSearchWord] = useState('')
const [tableHttpReload, setTableHttpReload] = useState(true)
const [tableListDataSource, setTableListDataSource] = useState<Array<SystemApiTableListItem | AiServiceRouterTableListItem>>([])
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const isAiService = side === 'aiInside'
const manualReloadTable = () => {
setTableHttpReload(true)
pageListRef.current?.reload()
}
const getMemberList = async () => {
setMemberValueEnum([])
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
method: 'GET'
})
if (code === STATUS_CODE.SUCCESS) {
setMemberValueEnum(data.members)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
useEffect(() => {
getMemberList()
manualReloadTable()
}, [serviceId, side])
const getRoutesList = (): Promise<{ data: Array<SystemApiTableListItem | AiServiceRouterTableListItem>; success: boolean }> => {
if (!tableHttpReload) {
setTableHttpReload(true)
return Promise.resolve({ data: tableListDataSource, success: true })
}
return fetchData<BasicResponse<any>>(isAiService ? 'service/ai-routers' : 'service/routers', {
method: 'GET',
eoParams: { service: serviceId, team: teamId, keyword: searchWord },
eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const items = isAiService ? data.apis : data.routers
setTableListDataSource(items)
setTableHttpReload(false)
return { data: items, success: true }
}
message.error(msg || $t(RESPONSE_TIPS.error))
return { data: [], success: false }
})
.catch(() => ({ data: [], success: false }))
}
const deleteRoute = (entity: SystemApiTableListItem | AiServiceRouterTableListItem) => {
return new Promise((resolve, reject) => {
fetchData<BasicResponse<null>>(isAiService ? 'service/ai-router' : 'service/router', {
method: 'DELETE',
eoParams: { service: serviceId, team: teamId, router: entity.id }
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
}
const openDeleteModal = (entity: SystemApiTableListItem | AiServiceRouterTableListItem) => {
modal.confirm({
title: $t('删除'),
content: $t('确认删除该数据?'),
onOk: () =>
deleteRoute(entity).then((res) => {
if (res === true) {
manualReloadTable()
}
}),
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const routeColumns = useMemo(() => {
const baseColumns = (isAiService ? AI_SERVICE_ROUTER_TABLE_COLUMNS : SYSTEM_API_TABLE_COLUMNS).map((column) => {
const nextColumn = { ...column }
const dataIndex = nextColumn.dataIndex as string[] | string | undefined
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('creator')) {
const valueEnum: Record<string, { text: string }> = {}
memberValueEnum.forEach((item) => {
valueEnum[item.name] = { text: item.name }
})
nextColumn.valueEnum = valueEnum
}
if (nextColumn.filters && Array.isArray(dataIndex) && (dataIndex.includes('disable') || dataIndex.includes('disabled'))) {
nextColumn.valueEnum = {
true: { text: <span className="text-red-500">{$t('拦截')}</span> },
false: { text: <span className="text-green-500">{$t('放行')}</span> }
}
}
return {
...nextColumn,
title: typeof nextColumn.title === 'string' ? $t(nextColumn.title) : nextColumn.title
}
})
return [
...baseColumns,
{
title: '操作',
key: 'option',
btnNums: 2,
fixed: 'right' as const,
valueType: 'option' as const,
render: (_: ReactNode, entity: SystemApiTableListItem | AiServiceRouterTableListItem) => [
<TableBtnWithPermission
access="team.service.router.edit"
key="edit"
btnType="edit"
onClick={() => {
router.push(`/service/${teamId}/${side}/${serviceId}/route/${entity.id}`)
}}
btnTitle="编辑"
/>,
<TableBtnWithPermission
access="team.service.router.delete"
key="delete"
btnType="delete"
onClick={() => {
openDeleteModal(entity)
}}
btnTitle="删除"
/>
]
}
]
}, [isAiService, memberValueEnum, state.language, router, teamId, side, serviceId])
return (
<PageList
id={`service_route_${side}`}
ref={pageListRef}
columns={routeColumns as any}
request={() => getRoutesList()}
dataSource={tableListDataSource}
addNewBtnTitle={$t('添加路由')}
searchPlaceholder={$t('输入 URL 查找路由')}
onAddNewBtnClick={() => {
router.push(`/service/${teamId}/${side}/${serviceId}/route/create`)
}}
addNewBtnAccess="team.service.router.add"
tableClickAccess="team.service.router.view"
manualReloadTable={manualReloadTable}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
}}
onChange={() => {
setTableHttpReload(false)
}}
onRowClick={(row: SystemApiTableListItem | AiServiceRouterTableListItem) =>
router.push(`/service/${teamId}/${side}/${serviceId}/route/${row.id}`)
}
tableClass="mr-PAGE_INSIDE_X"
/>
)
}
export function ServiceListPage() {
const router = useRouter()
const { message, modal } = AppAntd.useApp()
const { fetchData } = useFetch()
const pageListRef = useRef<ActionType>(null)
const { checkPermission, accessInit, getGlobalAccessData, state } = useGlobalContext()
const [tableSearchWord, setTableSearchWord] = useState('')
const [teamList, setTeamList] = useState<{ [k: string]: { text: string } }>()
const [tableListDataSource, setTableListDataSource] = useState<SystemTableListItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true)
const [memberValueEnum, setMemberValueEnum] = useState<{ [k: string]: { text: string } }>({})
const [stateColumnMap] = useState<{ [k: string]: { text: string; className?: string } }>({
normal: { text: '正常' },
deploying: { text: '部署中', className: 'text-[#2196f3]' },
error: { text: '异常', className: 'text-[#ff4d4f]' },
public: { text: '公共服务' },
private: { text: '私有服务' }
})
const getSystemList = () => {
if (!accessInit) {
getGlobalAccessData()?.then?.(() => {
getSystemList()
})
return Promise.resolve({ data: [], success: false })
}
if (!tableHttpReload) {
setTableHttpReload(true)
return Promise.resolve({ data: tableListDataSource, success: true })
}
return fetchData<BasicResponse<{ services: SystemTableListItem[] }>>(
!checkPermission('system.workspace.service.view_all') ? 'my_services' : 'services',
{
method: 'GET',
eoParams: { keyword: tableSearchWord },
eoTransformKeys: ['api_num', 'service_num', 'create_time']
}
)
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setTableListDataSource(data.services)
setTableHttpReload(false)
return { data: data.services, success: true }
}
message.error(msg || $t(RESPONSE_TIPS.error))
return { data: [], success: false }
})
.catch(() => ({ data: [], success: false }))
}
const getTeamsList = () => {
if (!accessInit) {
getGlobalAccessData()?.then?.(() => {
getTeamsList()
})
return
}
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
{ method: 'GET', eoTransformKeys: [] }
).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const valueEnum: Record<string, { text: string }> = {}
data.teams?.forEach((x: SimpleMemberItem) => {
valueEnum[x.name] = { text: x.name }
})
setTeamList(valueEnum)
return
}
message.error(msg || $t(RESPONSE_TIPS.error))
})
}
const getMemberList = async () => {
setMemberValueEnum({})
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
method: 'GET'
})
if (code === STATUS_CODE.SUCCESS) {
const valueEnum: Record<string, { text: string }> = {}
data.members?.forEach((x: SimpleMemberItem) => {
valueEnum[x.name] = { text: x.name }
})
setMemberValueEnum(valueEnum)
return
}
message.error(msg || $t(RESPONSE_TIPS.error))
}
const manualReloadTable = () => {
setTableHttpReload(true)
pageListRef.current?.reload()
}
const openLogsModal = (record: SystemTableListItem) => {
const closeModal = (reload = true) => {
modalInstance.destroy()
if (reload) {
manualReloadTable()
}
}
const updateFooter = () => {
record.state = 'error'
modalInstance.update({})
}
let cancelCb: () => void = () => {}
const cancel = (cb: () => void) => {
cancelCb = cb
}
const modalInstance = modal.confirm({
title: $t('部署过程'),
content: <ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />,
footer: () => <LogsFooter record={record} closeModal={closeModal} />,
afterClose: () => {
cancelCb()
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
useEffect(() => {
getTeamsList()
getMemberList()
}, [])
const columns = useMemo(() => {
return SYSTEM_TABLE_COLUMNS.map((column) => {
const nextColumn = { ...column }
const dataIndex = nextColumn.dataIndex as string | string[] | undefined
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('master')) {
nextColumn.valueEnum = memberValueEnum
}
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('team')) {
nextColumn.valueEnum = teamList
}
if (nextColumn.dataIndex === 'service_kind') {
nextColumn.render = (_dom: ReactNode, record: SystemTableListItem & { enable_mcp?: boolean }) => (
<span className="text-[13px]">
<Tag
color={`#${record.service_kind === 'ai' ? 'EADEFF' : 'DEFFE7'}`}
className="text-[#000] font-normal border-0 mr-[10px] max-w-[150px] truncate"
bordered={false}
title={record.service_kind || '-'}
>
{SERVICE_KIND_OPTIONS.find((item) => item.value === record.service_kind)?.label || '-'}
</Tag>
{record.enable_mcp && (
<Tag
color="#FFF0C1"
className="text-[#000] font-normal border-0 mr-[12px] max-w-[150px] truncate"
bordered={false}
title="MCP"
>
MCP
</Tag>
)}
</span>
)
}
if (nextColumn.dataIndex === 'state') {
nextColumn.render = (_dom: ReactNode, record: SystemTableListItem) => (
<span
className={`text-[13px] ${stateColumnMap[record.state]?.className || ''}`}
onClick={(event) => {
if (['deploying', 'error'].includes(record.state)) {
event.stopPropagation()
openLogsModal(record)
}
}}
>
{$t(stateColumnMap[record.state]?.text || '-')}
</span>
)
}
return {
...nextColumn,
title: typeof nextColumn.title === 'string' ? $t(nextColumn.title) : nextColumn.title
}
})
}, [memberValueEnum, teamList, state.language])
return (
<div className="flex flex-col flex-1 h-full overflow-hidden">
<div className="border-[0px] mr-PAGE_INSIDE_X mb-[30px]">
<div className="flex justify-between mb-[20px] items-center">
<div className="flex items-center gap-TAG_LEFT">
<div className="text-theme text-[26px]">{$t('服务')}</div>
</div>
</div>
<div>
{$t(
'服务提供了高性能 API 网关,并且可以无缝接入多种大型 AI 模型,并将这些 AI 能力打包成 API 进行调用,从而大幅简化了 AI 模型的使用门槛。同时,我们的平台提供了完善的 API 管理功能,支持 API 的创建、监控、访问控制等,保障开发者可以高效、安全地开发和管理 API 服务。'
)}
</div>
</div>
<div className="h-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B overflow-hidden">
<PageList
id="global_system"
ref={pageListRef}
columns={columns}
request={() => getSystemList()}
searchPlaceholder={$t('输入名称、ID、所属团队、负责人查找服务')}
manualReloadTable={manualReloadTable}
onChange={() => {
setTableHttpReload(false)
}}
onSearchWordChange={(event) => {
setTableSearchWord(event.target.value)
}}
onRowClick={(row: SystemTableListItem) => {
router.push(`/service/${row.team.id}/${row.service_kind === 'ai' ? 'aiInside' : 'inside'}/${row.id}/overview`)
}}
/>
</div>
</div>
)
}
export function ServiceDetailLayout({
teamId,
serviceId,
side,
activeKey,
children
}: {
teamId: string
serviceId: string
side: ServiceSide
activeKey: ServiceMenuKey
children: ReactNode
}) {
const router = useRouter()
const { state, checkPermission } = useGlobalContext()
const menuItems = useMemo<MenuProps['items']>(() => {
const items: Array<{ key: ServiceMenuKey; label: string; access?: string }> = side === 'aiInside'
? [
{ key: 'overview', label: $t('总览') },
{ key: 'route', label: $t('API 路由'), access: 'team.service.router.view' },
{ key: 'api', label: $t('API 文档'), access: 'team.service.api_doc.view' },
{ key: 'document', label: $t('使用说明'), access: 'team.service.service_intro.view' },
{ key: 'servicepolicy', label: $t('服务策略'), access: 'team.service.policy.view' },
{ key: 'publish', label: $t('发布'), access: 'team.service.release.view' },
{ key: 'approval', label: $t('订阅审核'), access: 'team.service.subscription.view' },
{ key: 'subscriber', label: $t('订阅方管理'), access: 'team.service.subscription.view' },
{ key: 'setting', label: $t('设置') },
{ key: 'logs', label: $t('日志') }
]
: [
{ key: 'overview', label: $t('总览') },
{ key: 'route', label: $t('API 路由'), access: 'team.service.router.view' },
{ key: 'api', label: $t('API 文档'), access: 'team.service.api_doc.view' },
{ key: 'upstream', label: $t('上游'), access: 'team.service.upstream.view' },
{ key: 'document', label: $t('使用说明'), access: 'team.service.service_intro.view' },
{ key: 'servicepolicy', label: $t('服务策略'), access: 'team.service.policy.view' },
{ key: 'publish', label: $t('发布'), access: 'team.service.release.view' },
{ key: 'approval', label: $t('订阅审核'), access: 'team.service.subscription.view' },
{ key: 'subscriber', label: $t('订阅方管理'), access: 'team.service.subscription.view' },
{ key: 'setting', label: $t('设置') },
{ key: 'logs', label: $t('日志') }
]
return items
.filter((item) => (item.access ? checkPermission(item.access as any) : true))
.map((item) => ({
key: item.key,
label: item.label
}))
}, [side, state.language, checkPermission])
return (
<div className="flex flex-col flex-1 h-full overflow-hidden">
<div className="mr-PAGE_INSIDE_X mb-[20px]">
<ServiceInfoCard serviceId={serviceId} teamId={teamId} />
</div>
<div className="flex flex-1 h-full overflow-hidden">
<Menu
className="overflow-y-auto h-full"
style={{ width: 220 }}
selectedKeys={[activeKey]}
mode="inline"
items={menuItems}
onClick={({ key }) => {
router.push(`/service/${teamId}/${side}/${serviceId}/${key}`)
}}
/>
<div className="w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B">
{children}
</div>
</div>
</div>
)
}
@@ -0,0 +1,5 @@
import { ServiceListPage } from '../../_components/ServicePages'
export default function TeamServiceListRoutePage() {
return <ServiceListPage />
}
@@ -0,0 +1,5 @@
import { ServiceListPage } from '../_components/ServicePages'
export default function ServiceListRoutePage() {
return <ServiceListPage />
}
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'
export default function ServiceRootPage() {
redirect('/service/list')
}

Some files were not shown because too many files have changed in this diff Show More