forked from casdoor/casdoor
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:
committed by
GitHub
parent
d08bd1e486
commit
2b5f5d26ab
2
go.mod
2
go.mod
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")}:
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user