feat: Add AI gateway support to Site model

Agent-Logs-Url: https://github.com/casdoor/casdoor/sessions/3f955867-5207-4bfa-b6d3-52287ab6afa3

Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-03 04:10:14 +00:00
committed by GitHub
parent d08bd1e486
commit 2b5f5d26ab
6 changed files with 94 additions and 3 deletions

2
go.mod
View File

@@ -87,6 +87,7 @@ require (
golang.org/x/text v0.33.0
golang.org/x/time v0.8.0
google.golang.org/api v0.215.0
google.golang.org/protobuf v1.36.11
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
maunium.net/go/mautrix v0.22.1
modernc.org/sqlite v1.18.2
@@ -300,7 +301,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

View File

@@ -41,6 +41,7 @@ type Site struct {
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Type string `xorm:"varchar(100)" json:"type"`
Tag string `xorm:"varchar(100)" json:"tag"`
Domain string `xorm:"varchar(100)" json:"domain"`
OtherDomains []string `xorm:"varchar(500)" json:"otherDomains"`
@@ -63,6 +64,9 @@ type Site struct {
Status string `xorm:"varchar(100)" json:"status"`
Nodes []*NodeItem `xorm:"mediumtext" json:"nodes"`
AiProvider string `xorm:"varchar(100)" json:"aiProvider"`
AiProviderObj *Provider `xorm:"-" json:"aiProviderObj"`
CasdoorApplication string `xorm:"varchar(100)" json:"casdoorApplication"`
ApplicationObj *Application `xorm:"-" json:"applicationObj"`
}

View File

@@ -71,12 +71,32 @@ func getCasdoorApplicationMap() (map[string]*Application, error) {
return res, nil
}
func getAiProviderMap() (map[string]*Provider, error) {
providers, err := GetGlobalProviders()
if err != nil {
return nil, fmt.Errorf("GetGlobalProviders() error: %s", err.Error())
}
res := map[string]*Provider{}
for _, provider := range providers {
if provider.Category == "AI" {
res[provider.Name] = provider
}
}
return res, nil
}
func refreshSiteMap() error {
applicationMap, err := getCasdoorApplicationMap()
if err != nil {
fmt.Println(err)
}
aiProviderMap, err := getAiProviderMap()
if err != nil {
fmt.Printf("Failed to get AI provider map: %v\n", err)
}
newSiteMap := map[string]*Site{}
newHealthCheckNeededDomains := make([]string, 0)
sites, err := GetGlobalSites()
@@ -98,6 +118,14 @@ func refreshSiteMap() error {
}
}
if aiProviderMap != nil {
if site.AiProvider != "" && site.AiProviderObj == nil {
if v, ok2 := aiProviderMap[site.AiProvider]; ok2 {
site.AiProviderObj = v
}
}
}
if site.Domain != "" && site.PublicIp == "" {
go func(site *Site) {
site.PublicIp = util.ResolveDomainToIp(site.Domain)

View File

@@ -31,7 +31,7 @@ import (
"github.com/casdoor/casdoor/util"
)
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) {
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request, aiApiKey ...string) {
target, err := url.Parse(targetUrl)
if nil != err {
@@ -53,6 +53,12 @@ func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.
r.Header.Set("X-Real-Ip", clientIP)
}
}
if len(aiApiKey) > 0 && aiApiKey[0] != "" {
// Sanitize the API key to prevent header injection (remove newlines/carriage returns)
key := strings.NewReplacer("\r", "", "\n", "").Replace(aiApiKey[0])
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
}
}
proxy.ModifyResponse = func(resp *http.Response) error {
@@ -299,7 +305,11 @@ func nextHandle(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path)
} else {
targetUrl := joinPath(site.GetHost(), r.RequestURI)
forwardHandler(targetUrl, w, r)
if site.Type == "AI" && site.AiProviderObj != nil {
forwardHandler(targetUrl, w, r, site.AiProviderObj.ClientSecret)
} else {
forwardHandler(targetUrl, w, r)
}
}
}

View File

@@ -36,6 +36,7 @@ class SiteEditPage extends React.Component {
siteName: props.match.params.siteName,
rules: [],
providers: [],
aiProviders: [],
site: null,
certs: null,
applications: null,
@@ -50,6 +51,7 @@ class SiteEditPage extends React.Component {
this.getRules();
this.getApplications();
this.getAlertProviders();
this.getAiProviders();
}
getOrganizations() {
@@ -135,6 +137,20 @@ class SiteEditPage extends React.Component {
});
}
getAiProviders() {
ProviderBackend.getProviders()
.then((res) => {
if (res.status === "ok") {
const data = res.data.filter(provider => provider.category === "AI");
this.setState({
aiProviders: data,
});
} else {
Setting.showMessage("error", `Failed to get providers: ${res.msg}`);
}
});
}
parseSiteField(key, value) {
if (["score"].includes(key)) {
value = Setting.myParseInt(value);
@@ -205,6 +221,37 @@ class SiteEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("site:Type")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.site.type} onChange={(value => {this.updateSiteField("type", value);})}>
{
[
{id: "HTTP", name: "HTTP"},
{id: "AI", name: "AI"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.site.type === "AI" && (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("site:AI provider")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.site.aiProvider} onChange={(value => {this.updateSiteField("aiProvider", value);})}>
{
this.state.aiProviders.map((provider, index) => <Option key={index} value={provider.name}>{provider.displayName || provider.name}</Option>)
}
</Select>
</Col>
</Row>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("site:Domain")}:

View File

@@ -45,6 +45,7 @@ class SiteListPage extends BaseListPage {
name: `site_${randomName}`,
createdTime: moment().format(),
displayName: `New Site - ${randomName}`,
type: "HTTP",
domain: "door.casdoor.com",
otherDomains: [],
needRedirect: false,
@@ -64,6 +65,7 @@ class SiteListPage extends BaseListPage {
node: "",
isSelf: false,
nodes: [],
aiProvider: "",
casdoorApplication: "",
organizations: [],
};