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]

Reply via email to