渠道: 移除主页/favicon 特性, Base URL 可选+完整地址
- 回退渠道 homepage/favicon 字段与代理接口(未采用) - Base URL 改为可选: 留空按供应商默认(openai/anthropic), 兼容兼容型渠道必须填; 填完整地址(含 /v1)时归一化去尾 - 网关 upstreamURL 兜底去 /v1, 避免路径重复 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -49,12 +49,6 @@ func main() {
|
|||||||
fmt.Fprint(w, `{"object":"list","data":[{"id":"gpt-4o-mini","object":"model"},{"id":"gpt-4o","object":"model"},{"id":"claude-sonnet-5","object":"model"}]}`)
|
fmt.Fprint(w, `{"object":"list","data":[{"id":"gpt-4o-mini","object":"model"},{"id":"gpt-4o","object":"model"},{"id":"claude-sonnet-5","object":"model"}]}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// favicon(演示渠道主页图标)
|
|
||||||
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "image/svg+xml")
|
|
||||||
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="4" fill="#34d399"/><path d="M4 4.6h8v1.4H4zM4 7.3h8v1.4H4zM4 10h5v1.4H4z" fill="#09090b"/></svg>`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Anthropic Messages 端点(provider=anthropic 的渠道走这里)
|
// Anthropic Messages 端点(provider=anthropic 的渠道走这里)
|
||||||
http.HandleFunc("/v1/messages", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/v1/messages", func(w http.ResponseWriter, r *http.Request) {
|
||||||
body, _ := io.ReadAll(r.Body)
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (h *Handler) AdminChannels(c *gin.Context) {
|
|||||||
masked = "****"
|
masked = "****"
|
||||||
}
|
}
|
||||||
out = append(out, gin.H{
|
out = append(out, gin.H{
|
||||||
"id": ch.ID, "name": ch.Name, "provider": ch.Provider, "formats": ch.FormatsEffective(), "homepage": ch.Homepage, "base_url": ch.BaseURL,
|
"id": ch.ID, "name": ch.Name, "provider": ch.Provider, "formats": ch.FormatsEffective(), "base_url": ch.BaseURL,
|
||||||
"api_key_masked": masked, "weight": ch.Weight, "priority": ch.Priority,
|
"api_key_masked": masked, "weight": ch.Weight, "priority": ch.Priority,
|
||||||
"timeout_ms": ch.TimeoutMS, "max_concurrency": ch.MaxConcurrency,
|
"timeout_ms": ch.TimeoutMS, "max_concurrency": ch.MaxConcurrency,
|
||||||
"health_status": ch.HealthStatus, "enabled": ch.Enabled,
|
"health_status": ch.HealthStatus, "enabled": ch.Enabled,
|
||||||
@@ -47,8 +47,7 @@ type channelBody struct {
|
|||||||
Name string `json:"name" binding:"required,min=1,max=64"`
|
Name string `json:"name" binding:"required,min=1,max=64"`
|
||||||
Provider string `json:"provider"` // 可选:为空时按 formats 推断(兼容旧数据)
|
Provider string `json:"provider"` // 可选:为空时按 formats 推断(兼容旧数据)
|
||||||
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages(主配置)
|
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages(主配置)
|
||||||
Homepage string `json:"homepage"`
|
BaseURL string `json:"base_url"` // 可选:为空时按供应商默认(openai/anthropic)
|
||||||
BaseURL string `json:"base_url" binding:"required"`
|
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
Weight *int `json:"weight"`
|
Weight *int `json:"weight"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
@@ -57,6 +56,23 @@ type channelBody struct {
|
|||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveBaseURL 渠道 base_url:留空按供应商默认;兼容用户填完整地址(含 /v1)。
|
||||||
|
func resolveBaseURL(provider, raw string) (string, error) {
|
||||||
|
base := strings.TrimRight(raw, "/")
|
||||||
|
if base == "" {
|
||||||
|
switch provider {
|
||||||
|
case store.ChannelProviderOpenAI:
|
||||||
|
base = "https://api.openai.com"
|
||||||
|
case store.ChannelProviderAnthropic:
|
||||||
|
base = "https://api.anthropic.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if base == "" {
|
||||||
|
return "", errors.New("base_url required for compatible channels")
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(base, "/v1"), nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateProvider(p string) bool {
|
func validateProvider(p string) bool {
|
||||||
return p == store.ChannelProviderOpenAI || p == store.ChannelProviderAnthropic || p == store.ChannelProviderCompatible
|
return p == store.ChannelProviderOpenAI || p == store.ChannelProviderAnthropic || p == store.ChannelProviderCompatible
|
||||||
}
|
}
|
||||||
@@ -137,13 +153,18 @@ func (h *Handler) AdminCreateChannel(c *gin.Context) {
|
|||||||
resp.Fail(c, http.StatusBadRequest, err.Error())
|
resp.Fail(c, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
baseURL, err := resolveBaseURL(req.Provider, req.BaseURL)
|
||||||
|
if err != nil {
|
||||||
|
resp.Fail(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
enc, err := h.a.Enc.Encrypt(req.APIKey)
|
enc, err := h.a.Enc.Encrypt(req.APIKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Fail(c, http.StatusInternalServerError, "failed to encrypt api key")
|
resp.Fail(c, http.StatusInternalServerError, "failed to encrypt api key")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ch := store.Channel{
|
ch := store.Channel{
|
||||||
Name: req.Name, Provider: req.Provider, Formats: formats, Homepage: req.Homepage, BaseURL: strings.TrimRight(req.BaseURL, "/"),
|
Name: req.Name, Provider: req.Provider, Formats: formats, BaseURL: baseURL,
|
||||||
APIKeyEnc: enc, Weight: intOr(req.Weight, 1), Priority: intOr(req.Priority, 0),
|
APIKeyEnc: enc, Weight: intOr(req.Weight, 1), Priority: intOr(req.Priority, 0),
|
||||||
TimeoutMS: intOr(req.TimeoutMS, 120000), MaxConcurrency: intOr(req.MaxConcurrency, 16),
|
TimeoutMS: intOr(req.TimeoutMS, 120000), MaxConcurrency: intOr(req.MaxConcurrency, 16),
|
||||||
HealthStatus: store.ChannelHealthHealthy, Enabled: boolOr(req.Enabled, true),
|
HealthStatus: store.ChannelHealthHealthy, Enabled: boolOr(req.Enabled, true),
|
||||||
@@ -163,18 +184,17 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Provider *string `json:"provider"`
|
Provider *string `json:"provider"`
|
||||||
Formats *[]string `json:"formats"`
|
Formats *[]string `json:"formats"`
|
||||||
Homepage *string `json:"homepage"`
|
BaseURL *string `json:"base_url"`
|
||||||
BaseURL *string `json:"base_url"`
|
APIKey *string `json:"api_key"`
|
||||||
APIKey *string `json:"api_key"`
|
Weight *int `json:"weight"`
|
||||||
Weight *int `json:"weight"`
|
Priority *int `json:"priority"`
|
||||||
Priority *int `json:"priority"`
|
TimeoutMS *int `json:"timeout_ms"`
|
||||||
TimeoutMS *int `json:"timeout_ms"`
|
MaxConcurrency *int `json:"max_concurrency"`
|
||||||
MaxConcurrency *int `json:"max_concurrency"`
|
HealthStatus *string `json:"health_status"`
|
||||||
HealthStatus *string `json:"health_status"`
|
Enabled *bool `json:"enabled"`
|
||||||
Enabled *bool `json:"enabled"`
|
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
resp.Fail(c, http.StatusBadRequest, "invalid input")
|
resp.Fail(c, http.StatusBadRequest, "invalid input")
|
||||||
@@ -197,7 +217,16 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
|
|||||||
updates["provider"] = *body.Provider
|
updates["provider"] = *body.Provider
|
||||||
}
|
}
|
||||||
if body.BaseURL != nil {
|
if body.BaseURL != nil {
|
||||||
updates["base_url"] = strings.TrimRight(*body.BaseURL, "/")
|
prov := ch.Provider
|
||||||
|
if body.Provider != nil {
|
||||||
|
prov = *body.Provider
|
||||||
|
}
|
||||||
|
b, berr := resolveBaseURL(prov, *body.BaseURL)
|
||||||
|
if berr != nil {
|
||||||
|
resp.Fail(c, http.StatusBadRequest, berr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updates["base_url"] = b
|
||||||
}
|
}
|
||||||
if body.APIKey != nil && *body.APIKey != "" {
|
if body.APIKey != nil && *body.APIKey != "" {
|
||||||
enc, err := h.a.Enc.Encrypt(*body.APIKey)
|
enc, err := h.a.Enc.Encrypt(*body.APIKey)
|
||||||
@@ -219,9 +248,6 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
|
|||||||
if body.MaxConcurrency != nil {
|
if body.MaxConcurrency != nil {
|
||||||
updates["max_concurrency"] = *body.MaxConcurrency
|
updates["max_concurrency"] = *body.MaxConcurrency
|
||||||
}
|
}
|
||||||
if body.Homepage != nil {
|
|
||||||
updates["homepage"] = *body.Homepage
|
|
||||||
}
|
|
||||||
if body.HealthStatus != nil {
|
if body.HealthStatus != nil {
|
||||||
updates["health_status"] = *body.HealthStatus
|
updates["health_status"] = *body.HealthStatus
|
||||||
}
|
}
|
||||||
@@ -403,53 +429,6 @@ func (h *Handler) AdminImportChannelModels(c *gin.Context) {
|
|||||||
resp.OK(c, gin.H{"imported": imported})
|
resp.OK(c, gin.H{"imported": imported})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminChannelFavicon GET /api/v1/admin/channels/:id/favicon — 代理获取渠道主页 favicon(内存缓存 1h)。
|
|
||||||
func (h *Handler) AdminChannelFavicon(c *gin.Context) {
|
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var ch store.Channel
|
|
||||||
if err := h.a.DB.First(&ch, id).Error; err != nil || ch.Homepage == "" {
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.favMu.Lock()
|
|
||||||
if f, ok := h.favCache[id]; ok && time.Since(f.at) < time.Hour {
|
|
||||||
h.favMu.Unlock()
|
|
||||||
c.Data(http.StatusOK, f.ct, f.data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.favMu.Unlock()
|
|
||||||
|
|
||||||
url := strings.TrimRight(ch.Homepage, "/") + "/favicon.ico"
|
|
||||||
client := &http.Client{Timeout: 5 * time.Second}
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil || resp.StatusCode != http.StatusOK {
|
|
||||||
if resp != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ct := resp.Header.Get("Content-Type")
|
|
||||||
if ct == "" {
|
|
||||||
ct = "image/x-icon"
|
|
||||||
}
|
|
||||||
h.favMu.Lock()
|
|
||||||
h.favCache[id] = favEntry{data: data, ct: ct, at: time.Now()}
|
|
||||||
h.favMu.Unlock()
|
|
||||||
c.Data(http.StatusOK, ct, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = clause.Assignments // 保留 gorm/clause 引用(后续定价批处理用)
|
var _ = clause.Assignments // 保留 gorm/clause 引用(后续定价批处理用)
|
||||||
|
|
||||||
func intOr(p *int, def int) int {
|
func intOr(p *int, def int) int {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -18,20 +17,9 @@ import (
|
|||||||
// Handler 聚合所有管理 API。
|
// Handler 聚合所有管理 API。
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
a *app.App
|
a *app.App
|
||||||
|
|
||||||
favMu sync.Mutex
|
|
||||||
favCache map[uint64]favEntry // 渠道 favicon 内存缓存
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type favEntry struct {
|
func NewHandler(a *app.App) *Handler { return &Handler{a: a} }
|
||||||
data []byte
|
|
||||||
ct string
|
|
||||||
at time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler(a *app.App) *Handler {
|
|
||||||
return &Handler{a: a, favCache: map[uint64]favEntry{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 认证
|
// 认证
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ func NewRouter(a *app.App, gw *proxy.Gateway) *gin.Engine {
|
|||||||
admin.DELETE("/channels/:id", h.AdminDeleteChannel)
|
admin.DELETE("/channels/:id", h.AdminDeleteChannel)
|
||||||
admin.POST("/channels/:id/test", h.AdminTestChannel)
|
admin.POST("/channels/:id/test", h.AdminTestChannel)
|
||||||
admin.POST("/channels/:id/models/import", h.AdminImportChannelModels)
|
admin.POST("/channels/:id/models/import", h.AdminImportChannelModels)
|
||||||
admin.GET("/channels/:id/favicon", h.AdminChannelFavicon)
|
|
||||||
// 模型与定价
|
// 模型与定价
|
||||||
admin.GET("/models", h.AdminModels)
|
admin.GET("/models", h.AdminModels)
|
||||||
admin.POST("/models", h.AdminCreateModel)
|
admin.POST("/models", h.AdminCreateModel)
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ func parseBody(c *gin.Context) (*bodyReq, []byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upstreamURL 组装上游地址:base_url + 路径。
|
// upstreamURL 组装上游地址:base_url + 路径。
|
||||||
|
// 兼容用户填完整 base(含 /v1):去掉尾部 /v1,避免与请求路径重复。
|
||||||
func upstreamURL(ch *store.Channel, path string) string {
|
func upstreamURL(ch *store.Channel, path string) string {
|
||||||
return strings.TrimRight(ch.BaseURL, "/") + path
|
base := strings.TrimRight(ch.BaseURL, "/")
|
||||||
|
return strings.TrimSuffix(base, "/v1") + path
|
||||||
}
|
}
|
||||||
|
|
||||||
// doProxy 通用代理(M5):遍历候选渠道,按需转换;可安全重试的失败自动故障转移。
|
// doProxy 通用代理(M5):遍历候选渠道,按需转换;可安全重试的失败自动故障转移。
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ type Channel struct {
|
|||||||
Name string `gorm:"uniqueIndex;size:64;not null" json:"name"`
|
Name string `gorm:"uniqueIndex;size:64;not null" json:"name"`
|
||||||
Provider string `gorm:"size:16;not null" json:"provider"` // openai|anthropic|compatible(供应商/默认格式)
|
Provider string `gorm:"size:16;not null" json:"provider"` // openai|anthropic|compatible(供应商/默认格式)
|
||||||
Formats []string `gorm:"type:jsonb;serializer:json" json:"formats,omitempty"` // 原生支持的协议格式 chat|responses|messages
|
Formats []string `gorm:"type:jsonb;serializer:json" json:"formats,omitempty"` // 原生支持的协议格式 chat|responses|messages
|
||||||
Homepage string `gorm:"size:255" json:"homepage"` // 渠道主页,用于展示 favicon
|
|
||||||
BaseURL string `gorm:"size:255;not null" json:"base_url"`
|
BaseURL string `gorm:"size:255;not null" json:"base_url"`
|
||||||
APIKeyEnc string `gorm:"size:1024;not null" json:"-"` // AES-GCM 密文
|
APIKeyEnc string `gorm:"size:1024;not null" json:"-"` // AES-GCM 密文
|
||||||
Weight int `gorm:"not null;default:1" json:"weight"`
|
Weight int `gorm:"not null;default:1" json:"weight"`
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export interface Channel {
|
|||||||
name: string
|
name: string
|
||||||
provider: 'openai' | 'anthropic' | 'compatible'
|
provider: 'openai' | 'anthropic' | 'compatible'
|
||||||
formats: string[] // chat | responses | messages
|
formats: string[] // chat | responses | messages
|
||||||
homepage?: string
|
|
||||||
base_url: string
|
base_url: string
|
||||||
api_key_masked: string
|
api_key_masked: string
|
||||||
weight: number
|
weight: number
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const busyId = ref<number | null>(null)
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
formats: ['chat'] as string[],
|
formats: ['chat'] as string[],
|
||||||
homepage: '',
|
|
||||||
base_url: '',
|
base_url: '',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
@@ -29,24 +28,6 @@ const form = reactive({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// favicon 兜底:灰色地球
|
|
||||||
const FAV_FALLBACK =
|
|
||||||
'data:image/svg+xml;utf8,' +
|
|
||||||
encodeURIComponent(
|
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" fill="none" stroke="#a1a1aa" stroke-width="1.4"/><ellipse cx="8" cy="8" rx="3" ry="6.5" fill="none" stroke="#a1a1aa" stroke-width="1.4"/><path d="M1.8 8h12.4" stroke="#a1a1aa" stroke-width="1.4"/></svg>',
|
|
||||||
)
|
|
||||||
function onFavError(e: Event) {
|
|
||||||
;(e.target as HTMLImageElement).src = FAV_FALLBACK
|
|
||||||
}
|
|
||||||
function hostOf(u?: string): string {
|
|
||||||
if (!u) return '-'
|
|
||||||
try {
|
|
||||||
return new URL(u).host
|
|
||||||
} catch {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const { data } = await http.get('/admin/channels')
|
const { data } = await http.get('/admin/channels')
|
||||||
@@ -59,7 +40,7 @@ async function load() {
|
|||||||
function openCreate() {
|
function openCreate() {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: '', formats: ['chat'], homepage: '', base_url: '', api_key: '',
|
name: '', formats: ['chat'], base_url: '', api_key: '',
|
||||||
weight: 1, priority: 0, timeout_ms: 120000, max_concurrency: 16, enabled: true,
|
weight: 1, priority: 0, timeout_ms: 120000, max_concurrency: 16, enabled: true,
|
||||||
})
|
})
|
||||||
editOpen.value = true
|
editOpen.value = true
|
||||||
@@ -69,7 +50,6 @@ function openEdit(ch: Channel) {
|
|||||||
editing.value = ch
|
editing.value = ch
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: ch.name, formats: [...(ch.formats?.length ? ch.formats : ['chat'])],
|
name: ch.name, formats: [...(ch.formats?.length ? ch.formats : ['chat'])],
|
||||||
homepage: ch.homepage || '',
|
|
||||||
base_url: ch.base_url, api_key: '',
|
base_url: ch.base_url, api_key: '',
|
||||||
weight: ch.weight, priority: ch.priority, timeout_ms: ch.timeout_ms,
|
weight: ch.weight, priority: ch.priority, timeout_ms: ch.timeout_ms,
|
||||||
max_concurrency: ch.max_concurrency, enabled: ch.enabled,
|
max_concurrency: ch.max_concurrency, enabled: ch.enabled,
|
||||||
@@ -163,7 +143,6 @@ onMounted(load)
|
|||||||
<tr class="border-b border-edge text-left text-xs text-muted">
|
<tr class="border-b border-edge text-left text-xs text-muted">
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">名称</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">名称</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">API 格式</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">API 格式</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">主页</th>
|
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">Base URL</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">Base URL</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">Key</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">Key</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">健康</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">健康</th>
|
||||||
@@ -183,18 +162,6 @@ onMounted(load)
|
|||||||
>{{ protocolName(f) }}</span>
|
>{{ protocolName(f) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<img
|
|
||||||
:src="`/api/v1/admin/channels/${ch.id}/favicon`"
|
|
||||||
:alt="ch.name"
|
|
||||||
class="size-5 shrink-0 rounded-sm"
|
|
||||||
loading="lazy"
|
|
||||||
@error="onFavError"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-muted">{{ hostOf(ch.homepage) }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.base_url }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.base_url }}</td>
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.api_key_masked || '****' }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.api_key_masked || '****' }}</td>
|
||||||
<td class="px-4 py-2.5">
|
<td class="px-4 py-2.5">
|
||||||
@@ -215,7 +182,7 @@ onMounted(load)
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="channels.length === 0">
|
<tr v-if="channels.length === 0">
|
||||||
<td colspan="8" class="px-4 py-10 text-center text-sm text-muted">还没有渠道,点击「添加渠道」</td>
|
<td colspan="7" class="px-4 py-10 text-center text-sm text-muted">还没有渠道,点击「添加渠道」</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -245,8 +212,12 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-muted">客户端协议不在其中时,网关自动转换为其支持的格式</p>
|
<p class="mt-1.5 text-xs text-muted">客户端协议不在其中时,网关自动转换为其支持的格式</p>
|
||||||
</div>
|
</div>
|
||||||
<Input v-model="form.base_url" label="Base URL" placeholder="https://api.openai.com" />
|
<Input
|
||||||
<Input v-model="form.homepage" label="渠道主页" placeholder="https://openai.com(用于展示 favicon)" />
|
v-model="form.base_url"
|
||||||
|
label="Base URL(可选)"
|
||||||
|
placeholder="https://api.openai.com"
|
||||||
|
hint="留空按供应商默认;可填完整地址,如 https://api.openai.com/v1"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-model="form.api_key"
|
v-model="form.api_key"
|
||||||
label="上游 API Key"
|
label="上游 API Key"
|
||||||
|
|||||||
Reference in New Issue
Block a user