This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit bf5572fa203e5d258d493dda92719836f90a52c1 Author: Yang Luo <[email protected]> AuthorDate: Fri Sep 4 22:51:32 2026 +0800 feat(providers): import a New API channel by pasting its connection info --- object/import_link.go | 9 +++++ object/provider_link.go | 55 +++++++++++++++++++++++++++++ web/src/components/ProviderSourcePicker.tsx | 10 +++--- web/src/locales/en/data.json | 6 ++-- web/src/locales/zh/data.json | 6 ++-- 5 files changed, 75 insertions(+), 11 deletions(-) diff --git a/object/import_link.go b/object/import_link.go index bd9f3a0..e909ccc 100644 --- a/object/import_link.go +++ b/object/import_link.go @@ -110,6 +110,15 @@ type SkillImport struct { // balance with, and the flags that would activate what was imported — Gateway // asks the reader that instead. func ParseImportLink(owner string, raw string) (*ImportLink, error) { + // New API sites hand out connection info as JSON rather than a link; it is + // pasted into the same box, so it is read here. + if provider, isChannel, err := providerFromNewApiChannel(owner, raw); isChannel { + if err != nil { + return nil, err + } + return &ImportLink{Resource: ImportResourceProvider, Provider: provider}, nil + } + query, err := importLinkQuery(raw) if err != nil { return nil, err diff --git a/object/provider_link.go b/object/provider_link.go index e35093b..2b5a36c 100644 --- a/object/provider_link.go +++ b/object/provider_link.go @@ -15,6 +15,7 @@ package object import ( + "encoding/json" "fmt" "net/url" "strings" @@ -108,3 +109,57 @@ func linkModels(query url.Values) []string { } return models } + +// NewApiChannelType marks the connection info a New API site copies out of its +// channel page. It is JSON rather than a link, and carries only these two +// fields, so it is read where a link is pasted instead of getting a box of its +// own. +const NewApiChannelType = "newapi_channel_conn" + +type newApiChannel struct { + Type string `json:"_type"` + Key string `json:"key"` + Url string `json:"url"` +} + +// providerFromNewApiChannel reads that connection info. The second value says +// whether the text was this format at all, so text that is not falls through to +// being read as a link. +func providerFromNewApiChannel(owner string, raw string) (*Provider, bool, error) { + value := strings.TrimSpace(raw) + if !strings.HasPrefix(value, "{") { + return nil, false, nil + } + + var channel newApiChannel + if err := json.Unmarshal([]byte(value), &channel); err != nil { + return nil, true, fmt.Errorf("this is neither a link nor JSON: %w", err) + } + if channel.Type != NewApiChannelType { + return nil, true, fmt.Errorf("this JSON is not New API connection info: %q", channel.Type) + } + + site := strings.TrimRight(strings.TrimSpace(channel.Url), "/") + parsed, err := url.Parse(site) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { + return nil, true, fmt.Errorf("this connection info does not name a site: %q", channel.Url) + } + key := strings.TrimSpace(channel.Key) + if key == "" { + return nil, true, fmt.Errorf("this connection info carries no API key") + } + + return &Provider{ + Owner: owner, + DisplayName: strings.TrimPrefix(parsed.Host, "www."), + // A New API site resells every vendor behind one OpenAI-compatible + // endpoint, so the models are whatever that one site sells. + Type: "custom", + Status: "enabled", + AuthMode: ProviderAuthProvider, + BaseUrl: site, + ApiKey: key, + Models: []string{}, + Icon: site, + }, true, nil +} diff --git a/web/src/components/ProviderSourcePicker.tsx b/web/src/components/ProviderSourcePicker.tsx index 7d55124..a7bbf77 100644 --- a/web/src/components/ProviderSourcePicker.tsx +++ b/web/src/components/ProviderSourcePicker.tsx @@ -99,7 +99,7 @@ export function ProviderSourcePicker({ onLink, }: { onPick: (source: ProviderSource) => void; - /** A vendor's own "add this provider" link, pasted rather than typed out. */ + /** A vendor's "add this provider" link, or New API connection info. */ onLink?: (link: string) => Promise<void>; }) { const [query, setQuery] = React.useState(""); @@ -148,7 +148,7 @@ export function ProviderSourcePicker({ <div className="grid gap-1.5 rounded-lg border border-dashed p-3"> <span className="flex items-center gap-2 text-sm font-medium"> <Link2 className="size-4" /> - {i18next.t("provider:Paste a provider link")} + {i18next.t("provider:Paste a link or connection info")} </span> <div className="flex flex-wrap gap-2"> <Input @@ -160,15 +160,15 @@ export function ProviderSourcePicker({ importLink(); } }} - placeholder="ccswitch://v1/import?resource=provider&..." + placeholder={"ccswitch://v1/import?... or {\"_type\":\"newapi_channel_conn\",...}"} className="min-w-0 flex-1" /> <Button type="button" variant="outline" loading={importing} onClick={importLink}> - {i18next.t("provider:Read the link")} + {i18next.t("provider:Read it")} </Button> </div> <span className="text-muted-foreground text-xs"> - {i18next.t("provider:Paste a provider link hint")} + {i18next.t("provider:Paste a link or connection info hint")} </span> </div> )} diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index d16564b..18666e2 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -1105,8 +1105,8 @@ "Notes hint": "For whoever reads this later: which account the key belongs to, when it expires, who pays for it.", "Nothing to test": "There is nothing to test yet", "Page description": "One endpoint in front of your model vendors. Gateway holds the API key, so the agents never have it — or forwards the agent's own sign-in and holds nothing.", - "Paste a provider link": "Paste a provider link", - "Paste a provider link hint": "Vendors publish these as \"add to CC Switch\" buttons. Gateway reads the same link and fills the form in below, and stores nothing until you confirm.", + "Paste a link or connection info": "Paste a link or connection info", + "Paste a link or connection info hint": "Vendors publish these as \"add to CC Switch\" buttons, and a New API site copies its connection info out as JSON. Gateway reads either one and fills the form in below, and stores nothing until you confirm.", "Path hint": "A dotted path into the answer, where a number indexes an array: balance_infos.0.total_balance.", "Plan usage": "Plan usage", "Preset": "Preset", @@ -1119,7 +1119,7 @@ "Provider not found": "Provider not found", "Provider saved": "Provider saved", "Providers": "Providers", - "Read the link": "Read the link", + "Read it": "Read it", "Recovering": "Recovering", "Refresh balances": "Refresh balances", "Remaining": "Remaining", diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index e4cd473..40621d0 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -1177,8 +1177,8 @@ "Notes hint": "写给以后的自己:这个密钥属于哪个账号、什么时候过期、由谁付费。", "Nothing to test": "暂时没有可探测的内容", "Page description": "挡在模型厂商前面的统一入口。API Key 由 Gateway 保管,Agent 拿不到;也可以转发 Agent 自己的登录,什么都不保管。", - "Paste a provider link": "粘贴供应商链接", - "Paste a provider link hint": "中转站通常把它做成“添加到 CC Switch”的按钮。Gateway 读的是同一种链接,读完只会把下面的表单填好,你确认之前不会保存任何东西。", + "Paste a link or connection info": "粘贴链接或连接信息", + "Paste a link or connection info hint": "中转站通常把它做成“添加到 CC Switch”的按钮,New API 站点则是把连接信息复制成一段 JSON。两种 Gateway 都认,读完只会把下面的表单填好,你确认之前不会保存任何东西。", "Path hint": "返回内容里的点分路径,数字表示数组下标:balance_infos.0.total_balance。", "Plan usage": "套餐用量", "Preset": "预设", @@ -1191,7 +1191,7 @@ "Provider not found": "供应商不存在", "Provider saved": "供应商已保存", "Providers": "供应商", - "Read the link": "读取链接", + "Read it": "读取", "Recovering": "恢复中", "Refresh balances": "刷新余额", "Remaining": "余额", --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
