From 7deb97a4df3a014436f67a1935a5789531f5b35f Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 29 Jan 2026 16:44:42 +0800 Subject: [PATCH 1/3] fix: Installation of the fourth part of form validation error --- ui/src/pages/Install/components/FourthStep/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index 776053c63..a6082b263 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -58,7 +58,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { if (site_name.value && site_name.value.length > 30) { bol = false; - data.site_url = { + data.site_name = { value: site_name.value, isInvalid: true, errorMsg: t('site_name.msg_max_length'), @@ -70,7 +70,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { data.site_url = { value: '', isInvalid: true, - errorMsg: t('site_name.msg.empty'), + errorMsg: t('site_url.msg.empty'), }; } From 6e4b7463e9358b86f51e793986352c780af40d96 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 29 Jan 2026 16:48:35 +0800 Subject: [PATCH 2/3] fix: integrate API key authentication into existing services and routes --- cmd/wire_gen.go | 6 +-- internal/base/middleware/api_key_auth.go | 51 ++++++++++++++++++++++++ internal/base/server/http.go | 5 +++ internal/cli/reset_password.go | 4 +- internal/migrations/init.go | 28 +++++++++++++ internal/migrations/init_data.go | 3 ++ internal/router/answer_api_router.go | 3 ++ internal/router/mcp_router.go | 44 ++++++++++++++++++++ internal/service/auth/auth.go | 26 ++++++++++-- 9 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 internal/base/middleware/api_key_auth.go create mode 100644 internal/router/mcp_router.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 24d32f7eb..9fe134ed6 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -149,7 +149,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) - authService := auth2.NewAuthService(authRepo) + apiKeyRepo := api_key.NewAPIKeyRepo(dataData) + authService := auth2.NewAuthService(authRepo, apiKeyRepo) userRepo := user.NewUserRepo(dataData) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) configRepo := config.NewConfigRepo(dataData) @@ -279,7 +280,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService, siteInfoCommonService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) controller_adminBadgeController := controller_admin.NewBadgeController(badgeService) - apiKeyRepo := api_key.NewAPIKeyRepo(dataData) apiKeyService := apikey.NewAPIKeyService(apiKeyRepo) adminAPIKeyController := controller_admin.NewAdminAPIKeyController(apiKeyService) featureToggleService := feature_toggle.NewFeatureToggleService(siteInfoRepo) @@ -289,7 +289,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, aiController := controller.NewAIController(searchService, siteInfoCommonService, tagCommonService, questionCommon, commentRepo, userCommon, answerRepo, mcpController, aiConversationService, featureToggleService) aiConversationController := controller.NewAIConversationController(aiConversationService, featureToggleService) aiConversationAdminController := controller_admin.NewAIConversationAdminController(aiConversationService, featureToggleService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController, adminAPIKeyController, aiController, aiConversationController, aiConversationAdminController) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController, adminAPIKeyController, aiController, aiConversationController, aiConversationAdminController, mcpController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) diff --git a/internal/base/middleware/api_key_auth.go b/internal/base/middleware/api_key_auth.go new file mode 100644 index 000000000..cc8182d40 --- /dev/null +++ b/internal/base/middleware/api_key_auth.go @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +// AuthAPIKey middleware to authenticate API key +func (am *AuthUserMiddleware) AuthAPIKey() gin.HandlerFunc { + return func(ctx *gin.Context) { + token := ExtractToken(ctx) + if len(token) == 0 { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + pass, err := am.authService.AuthAPIKey(ctx, ctx.Request.Method == "GET", token) + if err != nil { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + if !pass { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + ctx.Next() + } +} diff --git a/internal/base/server/http.go b/internal/base/server/http.go index 050e31923..765cbf6be 100644 --- a/internal/base/server/http.go +++ b/internal/base/server/http.go @@ -113,5 +113,10 @@ func NewHTTPServer(debug bool, agent.RegisterAuthAdminRouter(adminauthV1) return nil }) + + // mcp + mcpAPIGroup := r.Group(uiConf.APIBaseURL + "/answer/api/v1") + mcpAPIGroup.Use(authUserMiddleware.AuthMcpEnable(), authUserMiddleware.AuthAPIKey()) + answerRouter.RegisterMCPRouter(mcpAPIGroup) return r } diff --git a/internal/cli/reset_password.go b/internal/cli/reset_password.go index dbb3422af..00ffe911b 100644 --- a/internal/cli/reset_password.go +++ b/internal/cli/reset_password.go @@ -32,6 +32,7 @@ import ( "github.com/apache/answer/internal/base/conf" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/path" + "github.com/apache/answer/internal/repo/api_key" "github.com/apache/answer/internal/repo/auth" "github.com/apache/answer/internal/repo/user" authService "github.com/apache/answer/internal/service/auth" @@ -95,7 +96,8 @@ func ResetPassword(ctx context.Context, dataDirPath string, opts *ResetPasswordO userRepo := user.NewUserRepo(dataData) authRepo := auth.NewAuthRepo(dataData) - authSvc := authService.NewAuthService(authRepo) + apiKeyRepo := api_key.NewAPIKeyRepo(dataData) + authSvc := authService.NewAuthService(authRepo, apiKeyRepo) email := strings.TrimSpace(opts.Email) if email == "" { diff --git a/internal/migrations/init.go b/internal/migrations/init.go index ae2eeb9a3..9dbe6eb8e 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -87,6 +87,8 @@ func (m *Mentor) InitDB() error { m.do("init site info security", m.initSiteInfoSecurityConfig) m.do("init default content", m.initDefaultContent) m.do("init default badges", m.initDefaultBadges) + m.do("init default ai config", m.initSiteInfoAI) + m.do("init default MCP config", m.initSiteInfoMCP) return m.err } @@ -606,3 +608,29 @@ func (m *Mentor) initDefaultBadges() { } } } + +func (m *Mentor) initSiteInfoAI() { + content := &schema.SiteAIReq{ + PromptConfig: &schema.AIPromptConfig{ + ZhCN: constant.DefaultAIPromptConfigZhCN, + EnUS: constant.DefaultAIPromptConfigEnUS, + }, + } + writeDataBytes, _ := json.Marshal(content) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: constant.SiteTypeAI, + Content: string(writeDataBytes), + Status: 1, + }) +} +func (m *Mentor) initSiteInfoMCP() { + content := &schema.SiteMCPReq{ + Enabled: true, + } + writeDataBytes, _ := json.Marshal(content) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: constant.SiteTypeMCP, + Content: string(writeDataBytes), + Status: 1, + }) +} diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index 356a915a7..daef5bb03 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -76,6 +76,9 @@ var ( &entity.BadgeAward{}, &entity.FileRecord{}, &entity.PluginKVStorage{}, + &entity.APIKey{}, + &entity.AIConversation{}, + &entity.AIConversationRecord{}, } roles = []*entity.Role{ diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index c642b2a13..84b8b4e1c 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -61,6 +61,7 @@ type AnswerAPIRouter struct { aiController *controller.AIController aiConversationController *controller.AIConversationController aiConversationAdminController *controller_admin.AIConversationAdminController + mcpController *controller.MCPController } func NewAnswerAPIRouter( @@ -98,6 +99,7 @@ func NewAnswerAPIRouter( aiController *controller.AIController, aiConversationController *controller.AIConversationController, aiConversationAdminController *controller_admin.AIConversationAdminController, + mcpController *controller.MCPController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ langController: langController, @@ -134,6 +136,7 @@ func NewAnswerAPIRouter( aiController: aiController, aiConversationController: aiConversationController, aiConversationAdminController: aiConversationAdminController, + mcpController: mcpController, } } diff --git a/internal/router/mcp_router.go b/internal/router/mcp_router.go new file mode 100644 index 000000000..54f41971d --- /dev/null +++ b/internal/router/mcp_router.go @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package router + +import ( + "github.com/apache/answer/internal/schema/mcp_tools" + "github.com/gin-gonic/gin" + "github.com/mark3labs/mcp-go/server" +) + +func (a *AnswerAPIRouter) RegisterMCPRouter(r *gin.RouterGroup) { + s := server.NewMCPServer("Answer Enterprise MCP Server", "1.0.0") + + s.AddTool(mcp_tools.NewQuestionsTool(), a.mcpController.MCPQuestionsHandler()) + s.AddTool(mcp_tools.NewAnswersTool(), a.mcpController.MCPAnswersHandler()) + s.AddTool(mcp_tools.NewCommentsTool(), a.mcpController.MCPCommentsHandler()) + s.AddTool(mcp_tools.NewTagsTool(), a.mcpController.MCPTagsHandler()) + s.AddTool(mcp_tools.NewTagDetailTool(), a.mcpController.MCPTagDetailsHandler()) + s.AddTool(mcp_tools.NewUserTool(), a.mcpController.MCPUserDetailsHandler()) + + sseServer := server.NewSSEServer(s, + server.WithSSEEndpoint("/answer/api/v1/mcp/see"), + server.WithMessageEndpoint("/answer/api/v1/mcp/message"), + ) + r.GET("/mcp/sse", gin.WrapH(sseServer.SSEHandler())) + r.POST("/mcp/message", gin.WrapH(sseServer.MessageHandler())) +} diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index b94f7912a..8f539bf11 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -23,8 +23,10 @@ import ( "context" "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/apikey" "github.com/apache/answer/pkg/token" "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/log" ) // AuthRepo auth repository @@ -46,13 +48,15 @@ type AuthRepo interface { // AuthService kit service type AuthService struct { - authRepo AuthRepo + authRepo AuthRepo + apiKeyRepo apikey.APIKeyRepo } // NewAuthService email service -func NewAuthService(authRepo AuthRepo) *AuthService { +func NewAuthService(authRepo AuthRepo, apiKeyRepo apikey.APIKeyRepo) *AuthService { return &AuthService{ - authRepo: authRepo, + authRepo: authRepo, + apiKeyRepo: apiKeyRepo, } } @@ -152,3 +156,19 @@ func (as *AuthService) SetAdminUserCacheInfo(ctx context.Context, accessToken st func (as *AuthService) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) { return as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken) } +func (as *AuthService) AuthAPIKey(ctx context.Context, read bool, apiKey string) (pass bool, err error) { + apiKeyInfo, exist, err := as.apiKeyRepo.GetAPIKey(ctx, apiKey) + if err != nil { + return false, err + } + if !exist { + return false, nil + } + // If the request is not read-only, check if the API key has write permissions + if !read && apiKeyInfo.Scope == "read-only" { + log.Warnf("API key %s does not have write permissions", apiKeyInfo.AccessKey) + return false, nil + } + log.Infof("API key %s is valid, scope: %s", apiKeyInfo.AccessKey, apiKeyInfo.Scope) + return true, nil +} From 08356bfb25879ef0716caf5415c798e55d185522 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 29 Jan 2026 16:48:42 +0800 Subject: [PATCH 3/3] fix: update Indonesian and Russian translations for tag creation and appreciation --- i18n/id_ID.yaml | 2 +- i18n/ru_RU.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/id_ID.yaml b/i18n/id_ID.yaml index 95f235e5b..8ea2a5687 100644 --- a/i18n/id_ID.yaml +++ b/i18n/id_ID.yaml @@ -110,7 +110,7 @@ backend: rank_invite_someone_to_answer_label: other: Undang seseorang untuk menjawab rank_tag_add_label: - other: + other: Buat tag baru rank_tag_edit_label: other: Edit tag description (need to review) rank_question_edit_label: diff --git a/i18n/ru_RU.yaml b/i18n/ru_RU.yaml index 954df14ff..5cf098cd7 100644 --- a/i18n/ru_RU.yaml +++ b/i18n/ru_RU.yaml @@ -701,7 +701,7 @@ backend: other: Активный участник на год, опубликовал по крайней мере один раз. appreciated: name: - other: + other: Appreciated desc: other: Received 1 up vote on 20 posts. respected: