Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions internal/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ func (a *AppConfig) ProfileName() string {

// MultiAppConfig is the multi-app config file format.
type MultiAppConfig struct {
StrictMode StrictMode `json:"strictMode,omitempty"`
CurrentApp string `json:"currentApp,omitempty"`
PreviousApp string `json:"previousApp,omitempty"`
Apps []AppConfig `json:"apps"`
StrictMode StrictMode `json:"strictMode,omitempty"`
CurrentApp string `json:"currentApp,omitempty"`
PreviousApp string `json:"previousApp,omitempty"`
ExtraBrands map[string]*Endpoints `json:"extraBrands,omitempty"`
Apps []AppConfig `json:"apps"`
}

// CurrentAppConfig returns the currently active app config.
Expand Down Expand Up @@ -199,6 +200,11 @@ func LoadMultiAppConfig() (*MultiAppConfig, error) {
if len(multi.Apps) == 0 {
return nil, fmt.Errorf("invalid config format: no apps")
}
for name, ep := range multi.ExtraBrands {
if ep != nil {
RegisterBrand(name, *ep)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return &multi, nil
}

Expand Down
78 changes: 59 additions & 19 deletions internal/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,70 @@ func ParseBrand(value string) LarkBrand {

// Endpoints holds resolved endpoint URLs for different Lark services.
type Endpoints struct {
Open string // e.g. "https://open.feishu.cn"
Accounts string // e.g. "https://accounts.feishu.cn"
MCP string // e.g. "https://mcp.feishu.cn"
AppLink string // e.g. "https://applink.feishu.cn"
Open string `json:"open"` // e.g. "https://open.feishu.cn"
Accounts string `json:"accounts"` // e.g. "https://accounts.feishu.cn"
MCP string `json:"mcp"` // e.g. "https://mcp.feishu.cn"
AppLink string `json:"applink"` // e.g. "https://applink.feishu.cn"
}

// brandRegistry maps brand names to their endpoint defaults.
// Built-in brands (feishu, lark) are pre-registered via init.
// Custom brands can be added via RegisterBrand.
var brandRegistry = map[string]Endpoints{}

func init() {
brandRegistry[string(BrandFeishu)] = Endpoints{
Open: "https://open.feishu.cn",
Accounts: "https://accounts.feishu.cn",
MCP: "https://mcp.feishu.cn",
AppLink: "https://applink.feishu.cn",
}
brandRegistry[string(BrandLark)] = Endpoints{
Open: "https://open.larksuite.com",
Accounts: "https://accounts.larksuite.com",
MCP: "https://mcp.larksuite.com",
AppLink: "https://applink.larksuite.com",
}
}

// RegisterBrand adds or updates a custom brand's endpoint defaults.
// Partial overrides are merged on top of the "feishu" defaults.
// Built-in brands (feishu, lark) and empty names are silently ignored.
func RegisterBrand(name string, ep Endpoints) {
if name == "" || name == string(BrandFeishu) || name == string(BrandLark) {
return
}
base := brandRegistry[string(BrandFeishu)]
brandRegistry[name] = MergeEndpointOverrides(base, &ep)
}

// MergeEndpointOverrides returns a copy of base with non-empty overrides applied.
func MergeEndpointOverrides(base Endpoints, overrides *Endpoints) Endpoints {
if overrides == nil {
return base
}
result := base
if overrides.Open != "" {
result.Open = overrides.Open
}
if overrides.Accounts != "" {
result.Accounts = overrides.Accounts
}
if overrides.MCP != "" {
result.MCP = overrides.MCP
}
if overrides.AppLink != "" {
result.AppLink = overrides.AppLink
}
return result
}

// ResolveEndpoints resolves endpoint URLs based on brand.
func ResolveEndpoints(brand LarkBrand) Endpoints {
switch brand {
case BrandLark:
return Endpoints{
Open: "https://open.larksuite.com",
Accounts: "https://accounts.larksuite.com",
MCP: "https://mcp.larksuite.com",
AppLink: "https://applink.larksuite.com",
}
default:
return Endpoints{
Open: "https://open.feishu.cn",
Accounts: "https://accounts.feishu.cn",
MCP: "https://mcp.feishu.cn",
AppLink: "https://applink.feishu.cn",
}
if ep, ok := brandRegistry[string(brand)]; ok {
return ep
}
return brandRegistry[string(BrandFeishu)]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// ResolveOpenBaseURL returns the Open API base URL for the given brand.
Expand Down
80 changes: 80 additions & 0 deletions internal/core/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,83 @@ func TestResolveOpenBaseURL(t *testing.T) {
t.Errorf("ResolveOpenBaseURL(lark) = %q", got)
}
}

func TestMergeEndpointOverrides(t *testing.T) {
base := Endpoints{
Open: "https://open.feishu.cn",
Accounts: "https://accounts.feishu.cn",
MCP: "https://mcp.feishu.cn",
AppLink: "https://applink.feishu.cn",
}

t.Run("nil overrides returns base", func(t *testing.T) {
got := MergeEndpointOverrides(base, nil)
if got != base {
t.Errorf("got %v, want %v", got, base)
}
})

t.Run("partial override", func(t *testing.T) {
got := MergeEndpointOverrides(base, &Endpoints{Open: "https://proxy.example.com"})
if got.Open != "https://proxy.example.com" {
t.Errorf("Open = %q", got.Open)
}
if got.Accounts != "https://accounts.feishu.cn" {
t.Errorf("Accounts = %q, want unchanged", got.Accounts)
}
})

t.Run("full override", func(t *testing.T) {
overrides := Endpoints{
Open: "https://a.example.com", Accounts: "https://b.example.com",
MCP: "https://c.example.com", AppLink: "https://d.example.com",
}
got := MergeEndpointOverrides(base, &overrides)
if got != overrides {
t.Errorf("got %v, want %v", got, overrides)
}
})
}

func TestRegisterBrand(t *testing.T) {
RegisterBrand("staging", Endpoints{Open: "https://open-staging.feishu.cn"})
defer delete(brandRegistry, "staging")

ep := ResolveEndpoints("staging")
if ep.Open != "https://open-staging.feishu.cn" {
t.Errorf("Open = %q, want staging URL", ep.Open)
}
if ep.Accounts != "https://accounts.feishu.cn" {
t.Errorf("Accounts = %q, want feishu default", ep.Accounts)
}
}

func TestRegisterBrand_Full(t *testing.T) {
RegisterBrand("proxy", Endpoints{
Open: "https://api-proxy.example.com", Accounts: "https://acct-proxy.example.com",
MCP: "https://mcp-proxy.example.com", AppLink: "https://applink-proxy.example.com",
})
defer delete(brandRegistry, "proxy")

ep := ResolveEndpoints("proxy")
if ep.Open != "https://api-proxy.example.com" {
t.Errorf("Open = %q", ep.Open)
}
if ep.Accounts != "https://acct-proxy.example.com" {
t.Errorf("Accounts = %q", ep.Accounts)
}
}

func TestRegisterBrand_IgnoresBuiltIn(t *testing.T) {
original := brandRegistry[string(BrandFeishu)]
RegisterBrand("feishu", Endpoints{Open: "https://malicious.example.com"})
RegisterBrand("lark", Endpoints{Open: "https://malicious.example.com"})
RegisterBrand("", Endpoints{Open: "https://malicious.example.com"})

if brandRegistry[string(BrandFeishu)] != original {
t.Error("RegisterBrand should not overwrite built-in feishu brand")
}
if brandRegistry[string(BrandLark)].Open == "https://malicious.example.com" {
t.Error("RegisterBrand should not overwrite built-in lark brand")
}
}
Loading