mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
faaf13a0e9
* feat: add Supermarket integration (MCP & Skill marketplace) Backend: - Add [supermarket] config section with base_url (default: supermarket.memoh.ai) - Add SupermarketHandler with proxy endpoints for MCPs, Skills, and Tags - Add install endpoints: POST /bots/:id/supermarket/install-mcp (creates MCP connection with env vars) and install-skill (downloads tar.gz, extracts to container via gRPC) - Register handler in FX wiring, generate Swagger docs and TypeScript SDK Frontend: - Add /settings/supermarket route with Store icon in sidebar - Create supermarket page with search, tag filtering, MCP and Skill sections - Add MCP/Skill card components with tag badges and install buttons - Add install dialogs: MCP (bot selector + env var form), Skill (bot selector) - Add i18n entries for en.json and zh.json * fix: improve supermarket install UX - Create BotSelect component with avatar + name using UI Select - Replace NativeSelect in install dialogs and usage page with BotSelect - Change MCP install flow: navigate to bot detail MCP tab with pre-filled draft instead of direct install, letting users review before saving - Move Supermarket sidebar entry between Browser and Usage * web: remove supermarket page top tag selector bar Drop the horizontal tag chips and getSupermarketTags fetch; keep search and tag filter via card tag clicks with clearable badge. * web: add homepage link to supermarket MCP and Skill cards Show an external-link icon next to the card title when homepage is available, opening in a new tab on click.
269 lines
6.7 KiB
Go
269 lines
6.7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
const (
|
|
DefaultConfigPath = "config.toml"
|
|
DefaultHTTPAddr = ":8080"
|
|
DefaultNamespace = "default"
|
|
DefaultSocketPath = "/run/containerd/containerd.sock"
|
|
DefaultDataRoot = "data"
|
|
DefaultDataMount = "/data"
|
|
DefaultCNIBinaryDir = "/opt/cni/bin"
|
|
DefaultCNIConfigDir = "/etc/cni/net.d"
|
|
DefaultJWTExpiresIn = "24h"
|
|
DefaultPGHost = "127.0.0.1"
|
|
DefaultPGPort = 5432
|
|
DefaultPGUser = "postgres"
|
|
DefaultPGDatabase = "memoh"
|
|
DefaultPGSSLMode = "disable"
|
|
DefaultQdrantURL = "http://127.0.0.1:6334"
|
|
DefaultQdrantCollection = "memory"
|
|
DefaultRuntimeDir = "/opt/memoh/runtime"
|
|
DefaultBaseImage = "debian:bookworm-slim"
|
|
DefaultTimezone = "UTC"
|
|
)
|
|
|
|
type Config struct {
|
|
Log LogConfig `toml:"log"`
|
|
Server ServerConfig `toml:"server"`
|
|
Admin AdminConfig `toml:"admin"`
|
|
Auth AuthConfig `toml:"auth"`
|
|
Timezone string `toml:"timezone"`
|
|
Containerd ContainerdConfig `toml:"containerd"`
|
|
Workspace WorkspaceConfig `toml:"workspace"`
|
|
Postgres PostgresConfig `toml:"postgres"`
|
|
Qdrant QdrantConfig `toml:"qdrant"`
|
|
Sparse SparseConfig `toml:"sparse"`
|
|
BrowserGateway BrowserGatewayConfig `toml:"browser_gateway"`
|
|
Registry RegistryConfig `toml:"registry"`
|
|
Supermarket SupermarketConfig `toml:"supermarket"`
|
|
}
|
|
|
|
type LogConfig struct {
|
|
Level string `toml:"level"`
|
|
Format string `toml:"format"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Addr string `toml:"addr"`
|
|
}
|
|
|
|
type AdminConfig struct {
|
|
Username string `toml:"username"`
|
|
Password string `toml:"password" json:"-"`
|
|
Email string `toml:"email"`
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
JWTSecret string `toml:"jwt_secret" json:"-"`
|
|
JWTExpiresIn string `toml:"jwt_expires_in"`
|
|
}
|
|
|
|
type ContainerdConfig struct {
|
|
SocketPath string `toml:"socket_path"`
|
|
Namespace string `toml:"namespace"`
|
|
Socktainer SocktainerConfig `toml:"socktainer"`
|
|
}
|
|
|
|
type SocktainerConfig struct {
|
|
SocketPath string `toml:"socket_path"`
|
|
BinaryPath string `toml:"binary_path"`
|
|
}
|
|
|
|
type WorkspaceConfig struct {
|
|
Registry string `toml:"registry"`
|
|
DefaultImage string `toml:"default_image"`
|
|
Snapshotter string `toml:"snapshotter"`
|
|
DataRoot string `toml:"data_root"`
|
|
CNIBinaryDir string `toml:"cni_bin_dir"`
|
|
CNIConfigDir string `toml:"cni_conf_dir"`
|
|
RuntimeDir string `toml:"runtime_dir"`
|
|
}
|
|
|
|
// ImageRef returns the fully qualified image reference for the base image,
|
|
// prepending the registry mirror when configured and normalizing for containerd
|
|
// compatibility.
|
|
func (c WorkspaceConfig) ImageRef() string {
|
|
img := c.DefaultImage
|
|
if img == "" {
|
|
img = DefaultBaseImage
|
|
}
|
|
if c.Registry != "" {
|
|
return c.Registry + "/" + img
|
|
}
|
|
return NormalizeImageRef(img)
|
|
}
|
|
|
|
// RuntimePath returns the path to the workspace runtime directory.
|
|
func (c WorkspaceConfig) RuntimePath() string {
|
|
if c.RuntimeDir != "" {
|
|
return c.RuntimeDir
|
|
}
|
|
return DefaultRuntimeDir
|
|
}
|
|
|
|
// NormalizeImageRef ensures an image reference is fully qualified for containerd.
|
|
func NormalizeImageRef(ref string) string {
|
|
firstSlash := strings.Index(ref, "/")
|
|
if firstSlash == -1 {
|
|
return "docker.io/library/" + ref
|
|
}
|
|
firstSegment := ref[:firstSlash]
|
|
if strings.Contains(firstSegment, ".") || strings.Contains(firstSegment, ":") || firstSegment == "localhost" {
|
|
return ref
|
|
}
|
|
return "docker.io/" + ref
|
|
}
|
|
|
|
type PostgresConfig struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
User string `toml:"user"`
|
|
Password string `toml:"password" json:"-"`
|
|
Database string `toml:"database"`
|
|
SSLMode string `toml:"sslmode"`
|
|
}
|
|
|
|
type QdrantConfig struct {
|
|
BaseURL string `toml:"base_url"`
|
|
APIKey string `toml:"api_key" json:"-"`
|
|
TimeoutSeconds int `toml:"timeout_seconds"`
|
|
}
|
|
|
|
type SparseConfig struct {
|
|
BaseURL string `toml:"base_url"`
|
|
}
|
|
|
|
const DefaultProvidersDir = "conf/providers"
|
|
|
|
type RegistryConfig struct {
|
|
ProvidersDir string `toml:"providers_dir"`
|
|
}
|
|
|
|
// ProvidersPath returns the configured providers directory or the default.
|
|
func (c RegistryConfig) ProvidersPath() string {
|
|
if c.ProvidersDir != "" {
|
|
return c.ProvidersDir
|
|
}
|
|
return DefaultProvidersDir
|
|
}
|
|
|
|
type BrowserGatewayConfig struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
}
|
|
|
|
func (c BrowserGatewayConfig) BaseURL() string {
|
|
host := c.Host
|
|
if host == "" {
|
|
host = "127.0.0.1"
|
|
}
|
|
port := c.Port
|
|
if port == 0 {
|
|
port = 8083
|
|
}
|
|
return "http://" + host + ":" + strconv.Itoa(port)
|
|
}
|
|
|
|
const DefaultSupermarketBaseURL = "https://supermarket.memoh.ai"
|
|
|
|
type SupermarketConfig struct {
|
|
BaseURL string `toml:"base_url"`
|
|
}
|
|
|
|
func (c SupermarketConfig) GetBaseURL() string {
|
|
if c.BaseURL != "" {
|
|
return c.BaseURL
|
|
}
|
|
return DefaultSupermarketBaseURL
|
|
}
|
|
|
|
func Load(path string) (Config, error) {
|
|
cfg := Config{
|
|
Log: LogConfig{
|
|
Level: "info",
|
|
Format: "text",
|
|
},
|
|
Server: ServerConfig{
|
|
Addr: DefaultHTTPAddr,
|
|
},
|
|
Admin: AdminConfig{
|
|
Username: "admin",
|
|
Password: "change-your-password-here",
|
|
Email: "you@example.com",
|
|
},
|
|
Auth: AuthConfig{
|
|
JWTExpiresIn: DefaultJWTExpiresIn,
|
|
},
|
|
Timezone: DefaultTimezone,
|
|
Containerd: ContainerdConfig{
|
|
SocketPath: DefaultSocketPath,
|
|
Namespace: DefaultNamespace,
|
|
},
|
|
Workspace: WorkspaceConfig{
|
|
DefaultImage: DefaultBaseImage,
|
|
DataRoot: DefaultDataRoot,
|
|
CNIBinaryDir: DefaultCNIBinaryDir,
|
|
CNIConfigDir: DefaultCNIConfigDir,
|
|
},
|
|
Postgres: PostgresConfig{
|
|
Host: DefaultPGHost,
|
|
Port: DefaultPGPort,
|
|
User: DefaultPGUser,
|
|
Database: DefaultPGDatabase,
|
|
SSLMode: DefaultPGSSLMode,
|
|
},
|
|
BrowserGateway: BrowserGatewayConfig{
|
|
Host: "127.0.0.1",
|
|
Port: 8083,
|
|
},
|
|
}
|
|
|
|
if path == "" {
|
|
path = DefaultConfigPath
|
|
}
|
|
path = filepath.Clean(path)
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return cfg, nil
|
|
}
|
|
return cfg, err
|
|
}
|
|
|
|
//nolint:gosec // config path is intentionally user-configurable
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
|
|
var raw struct {
|
|
Workspace map[string]any `toml:"workspace"`
|
|
MCP map[string]any `toml:"mcp"`
|
|
}
|
|
if _, err := toml.Decode(string(data), &raw); err != nil {
|
|
return cfg, err
|
|
}
|
|
if raw.MCP != nil {
|
|
if raw.Workspace != nil {
|
|
return cfg, errors.New("config uses both [mcp] and [workspace]; remove [mcp] and keep only [workspace]")
|
|
}
|
|
return cfg, errors.New("config section [mcp] has been renamed to [workspace]; update your config.toml and restart")
|
|
}
|
|
|
|
if _, err := toml.Decode(string(data), &cfg); err != nil {
|
|
return cfg, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|