chore: import upstream snapshot with attribution
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:32 +08:00
commit 39dbe3a57d
1131 changed files with 272709 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
/*
* 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 server
// HTTP http config
type HTTP struct {
Addr string `json:"addr" mapstructure:"addr"`
}
// UI ui config
type UI struct {
BaseURL string `json:"base_url" mapstructure:"base_url" yaml:"base_url"`
APIBaseURL string `json:"api_base_url" mapstructure:"api_base_url" yaml:"api_base_url"`
}
+126
View File
@@ -0,0 +1,126 @@
/*
* 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 server
import (
"html/template"
"io/fs"
"os"
"strings"
brotli "github.com/anargu/gin-brotli"
"github.com/apache/answer/internal/base/middleware"
"github.com/apache/answer/internal/router"
"github.com/apache/answer/plugin"
"github.com/apache/answer/ui"
"github.com/gin-gonic/gin"
)
// NewHTTPServer new http server.
func NewHTTPServer(debug bool,
staticRouter *router.StaticRouter,
answerRouter *router.AnswerAPIRouter,
swaggerRouter *router.SwaggerRouter,
viewRouter *router.UIRouter,
authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware,
shortIDMiddleware *middleware.ShortIDMiddleware,
templateRouter *router.TemplateRouter,
pluginAPIRouter *router.PluginAPIRouter,
uiConf *UI,
) *gin.Engine {
if debug {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(middleware.Recovery(
uiConf.APIBaseURL+"/answer/api/v1",
uiConf.APIBaseURL+"/answer/admin/api",
))
r.Use(func(ctx *gin.Context) {
if strings.Contains(ctx.Request.URL.Path, "/chat/completions") {
return
}
brotli.Brotli(brotli.DefaultCompression)(ctx)
}, middleware.ExtractAndSetAcceptLanguage, shortIDMiddleware.SetShortIDFlag())
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
templatePath := os.Getenv("ANSWER_TEMPLATE_PATH")
if templatePath != "" {
r.LoadHTMLGlob(templatePath)
} else {
html, _ := fs.Sub(ui.Template, "template")
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
r.SetHTMLTemplate(htmlTemplate)
}
r.Use(middleware.HeadersByRequestURI())
viewRouter.Register(r, uiConf.BaseURL)
rootGroup := r.Group("")
swaggerRouter.Register(rootGroup)
static := r.Group(uiConf.APIBaseURL)
static.Use(avatarMiddleware.AvatarThumb(), authUserMiddleware.VisitAuth())
staticRouter.RegisterStaticRouter(static)
// The route must be available without logging in
mustUnAuthV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1")
answerRouter.RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware, mustUnAuthV1)
// register api that no need to login
unAuthV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1")
unAuthV1.Use(authUserMiddleware.Auth(), authUserMiddleware.EjectUserBySiteInfo())
answerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1)
// register api that must be authenticated but no need to check account status
authWithoutStatusV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1")
authWithoutStatusV1.Use(authUserMiddleware.MustAuthWithoutAccountAvailable())
answerRouter.RegisterAuthUserWithAnyStatusAnswerAPIRouter(authWithoutStatusV1)
// register api that must be authenticated
authV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1")
authV1.Use(authUserMiddleware.MustAuthAndAccountAvailable())
answerRouter.RegisterAnswerAPIRouter(authV1)
adminauthV1 := r.Group(uiConf.APIBaseURL + "/answer/admin/api")
adminauthV1.Use(authUserMiddleware.AdminAuth())
answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)
templateRouter.RegisterTemplateRouter(rootGroup, uiConf.BaseURL)
// plugin routes
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1)
pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)
_ = plugin.CallAgent(func(agent plugin.Agent) error {
agent.RegisterUnAuthRouter(mustUnAuthV1)
agent.RegisterAuthUserRouter(authV1)
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
}
+154
View File
@@ -0,0 +1,154 @@
/*
* 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 server
import (
"html/template"
"regexp"
"strconv"
"strings"
"time"
"github.com/apache/answer/internal/base/translator"
"github.com/apache/answer/internal/controller"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/pkg/converter"
"github.com/apache/answer/pkg/day"
"github.com/apache/answer/pkg/htmltext"
"github.com/segmentfault/pacman/i18n"
)
var funcMap = template.FuncMap{
"replaceHTMLTag": func(src string, tags ...string) string {
p := `(?U)<(\d+)>.+</(\d+)>`
re := regexp.MustCompile(p)
ms := re.FindAllStringSubmatch(src, -1)
for _, mi := range ms {
if mi[1] == mi[2] {
i, err := strconv.Atoi(mi[1])
if err != nil || len(tags) < i {
break
}
src = strings.ReplaceAll(src, mi[0], tags[i-1])
}
}
return src
},
"join": func(sep string, elems ...string) string {
return strings.Join(elems, sep)
},
"templateHTML": func(data string) template.HTML {
return template.HTML(data)
},
"formatLinkNofollow": func(data string) template.HTML {
return template.HTML(FormatLinkNofollow(data))
},
"translator": func(la i18n.Language, data string, params ...any) string {
trans := translator.GlobalTrans.Tr(la, data)
if len(params) > 0 && len(params)%2 == 0 {
for i := 0; i < len(params); i += 2 {
k := converter.InterfaceToString(params[i])
v := converter.InterfaceToString(params[i+1])
trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
trans = strings.ReplaceAll(trans, "{{"+k+"}}", v)
}
}
return trans
},
"timeFormatISO": func(tz string, timestamp int64) string {
_, _ = time.LoadLocation(tz)
return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z")
},
"translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string {
trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time")
return day.Format(timestamp, trans, tz)
},
"translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string {
var (
now = time.Now().Unix()
between int64 = 0
trans string
)
_, _ = time.LoadLocation(tz)
if now > timestamp {
between = now - timestamp
}
if between <= 1 {
return translator.GlobalTrans.Tr(la, "ui.dates.now")
}
if between > 1 && between < 60 {
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago")
return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between))
}
if between >= 60 && between < 3600 {
min := between / 60
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago")
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatInt(min, 10))
}
if between >= 3600 && between < 3600*24 {
h := between / 3600
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago")
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatInt(h, 10))
}
if between >= 3600*24 &&
between < 3600*24*366 &&
time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") {
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date")
return day.Format(timestamp, trans, tz)
}
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year")
return day.Format(timestamp, trans, tz)
},
"wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]any {
return map[string]any{
"comments": comments,
"language": la,
"timezone": tz,
}
},
"urlTitle": htmltext.UrlTitle,
}
func FormatLinkNofollow(html string) string {
var hrefRegexp = regexp.MustCompile("(?m)<a.*?[^<]>.*?</a>")
match := hrefRegexp.FindAllString(html, -1)
for _, v := range match {
hasNofollow := strings.Contains(v, "rel=\"nofollow\"")
hasSiteUrl := strings.Contains(v, controller.SiteUrl)
if !hasSiteUrl {
if !hasNofollow {
nofollowUrl := strings.Replace(v, "<a", "<a rel=\"nofollow\"", 1)
html = strings.Replace(html, v, nofollowUrl, 1)
}
}
}
return html
}
+25
View File
@@ -0,0 +1,25 @@
/*
* 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 server
import "github.com/google/wire"
// ProviderSetServer is providers.
var ProviderSetServer = wire.NewSet(NewHTTPServer)