| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- package llm_container
- import (
- "context"
- "database/sql"
- "encoding/base64"
- "fmt"
- "hash/fnv"
- "strings"
- "yunion.io/x/jsonutils"
- "yunion.io/x/pkg/errors"
- commonapi "yunion.io/x/onecloud/pkg/apis"
- computeapi "yunion.io/x/onecloud/pkg/apis/compute"
- api "yunion.io/x/onecloud/pkg/apis/llm"
- "yunion.io/x/onecloud/pkg/llm/models"
- "yunion.io/x/onecloud/pkg/mcclient"
- )
- // coollabsio/openclaw docker-compose: openclaw (main) + browser (CDP sidecar for /browser/)
- // See: https://github.com/coollabsio/openclaw/blob/main/docker-compose.yml
- const (
- // openclawContainerName = "openclaw"
- // browserContainerName = "browser"
- // openclawBrowserImage = "registry.cn-beijing.aliyuncs.com/cloudpods/openclaw-browser:latest"
- openclawDataDir = "/data"
- desktopConfigDir = "/config"
- homeDir = "/home/"
- // openclawBrowserCDPPort = "9222"
- )
- func openclawFixed9DigitPassword(llmId string) string {
- h := fnv.New64a()
- _, _ = h.Write([]byte(llmId))
- x := h.Sum64()
- // 固定 9 位,字母数字混合,并保证至少包含 1 个字母
- const alpha = "abcdefghijklmnopqrstuvwxyz"
- const base36 = "0123456789abcdefghijklmnopqrstuvwxyz"
- out := make([]byte, 9)
- out[0] = alpha[x%26]
- for i := 1; i < len(out); i++ {
- // xorshift64* 生成稳定伪随机序列
- x ^= x >> 12
- x ^= x << 25
- x ^= x >> 27
- x *= 2685821657736338717
- out[i] = base36[x%36]
- }
- return string(out)
- }
- func appendCredentialEnvs(envs []*commonapi.ContainerKeyValue, cred *api.LLMSpecCredential) []*commonapi.ContainerKeyValue {
- if cred == nil {
- return envs
- }
- for _, key := range cred.ExportKeys {
- envs = append(envs, &commonapi.ContainerKeyValue{
- Key: key,
- ValueFrom: &commonapi.ContainerValueSource{
- Credential: &commonapi.ContainerValueSourceCredential{
- Id: cred.Id,
- Key: key,
- },
- },
- })
- }
- return envs
- }
- func init() {
- models.RegisterLLMContainerDriver(newOpenClaw())
- }
- type openclaw struct {
- baseDriver
- }
- func newOpenClaw() models.ILLMContainerDriver {
- return &openclaw{baseDriver: newBaseDriver(api.LLM_CONTAINER_OPENCLAW)}
- }
- func (c *openclaw) GetSpec(sku *models.SLLMSku) interface{} {
- if sku == nil || sku.LLMSpec == nil {
- return nil
- }
- return sku.LLMSpec.OpenClaw
- }
- // mergeOpenClaw merges llm and sku OpenClaw specs; llm takes priority, use sku when llm field is unset (nil or empty).
- func mergeOpenClaw(llm, sku *api.LLMSpecOpenClaw) *api.LLMSpecOpenClaw {
- if llm == nil {
- if sku == nil {
- return nil
- }
- return copyOpenClaw(sku)
- }
- if sku == nil {
- return copyOpenClaw(llm)
- }
- out := &api.LLMSpecOpenClaw{}
- if len(llm.Providers) > 0 {
- out.Providers = make([]*api.LLMSpecOpenClawProvider, len(llm.Providers))
- copy(out.Providers, llm.Providers)
- } else if len(sku.Providers) > 0 {
- out.Providers = make([]*api.LLMSpecOpenClawProvider, len(sku.Providers))
- copy(out.Providers, sku.Providers)
- }
- if len(llm.Channels) > 0 {
- out.Channels = make([]*api.LLMSpecOpenClawChannel, len(llm.Channels))
- copy(out.Channels, llm.Channels)
- } else if len(sku.Channels) > 0 {
- out.Channels = make([]*api.LLMSpecOpenClawChannel, len(sku.Channels))
- copy(out.Channels, sku.Channels)
- }
- if llm.WorkspaceTemplates != nil && (llm.WorkspaceTemplates.AgentsMD != "" || llm.WorkspaceTemplates.SoulMD != "" || llm.WorkspaceTemplates.UserMD != "") {
- out.WorkspaceTemplates = &api.LLMSpecOpenClawWorkspaceTemplates{
- AgentsMD: llm.WorkspaceTemplates.AgentsMD,
- SoulMD: llm.WorkspaceTemplates.SoulMD,
- UserMD: llm.WorkspaceTemplates.UserMD,
- }
- } else if sku.WorkspaceTemplates != nil {
- out.WorkspaceTemplates = &api.LLMSpecOpenClawWorkspaceTemplates{
- AgentsMD: sku.WorkspaceTemplates.AgentsMD,
- SoulMD: sku.WorkspaceTemplates.SoulMD,
- UserMD: sku.WorkspaceTemplates.UserMD,
- }
- }
- return out
- }
- func copyOpenClaw(s *api.LLMSpecOpenClaw) *api.LLMSpecOpenClaw {
- if s == nil {
- return nil
- }
- out := &api.LLMSpecOpenClaw{}
- if len(s.Providers) > 0 {
- out.Providers = make([]*api.LLMSpecOpenClawProvider, len(s.Providers))
- copy(out.Providers, s.Providers)
- }
- if len(s.Channels) > 0 {
- out.Channels = make([]*api.LLMSpecOpenClawChannel, len(s.Channels))
- copy(out.Channels, s.Channels)
- }
- if s.WorkspaceTemplates != nil {
- out.WorkspaceTemplates = &api.LLMSpecOpenClawWorkspaceTemplates{
- AgentsMD: s.WorkspaceTemplates.AgentsMD,
- SoulMD: s.WorkspaceTemplates.SoulMD,
- UserMD: s.WorkspaceTemplates.UserMD,
- }
- }
- return out
- }
- func (c *openclaw) GetEffectiveSpec(llm *models.SLLM, sku *models.SLLMSku) interface{} {
- if sku == nil || sku.LLMSpec == nil {
- return llm.LLMSpec.OpenClaw
- }
- var llmOC *api.LLMSpecOpenClaw
- if llm != nil && llm.LLMSpec != nil {
- llmOC = llm.LLMSpec.OpenClaw
- }
- return mergeOpenClaw(llmOC, sku.LLMSpec.OpenClaw)
- }
- func (c *openclaw) StartLLM(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM) error {
- // lc, err := llm.GetLLMContainer()
- // if err != nil {
- // return errors.Wrap(err, "get llm container")
- // }
- // // 启动 openclaw gateway
- // cmd := fmt.Sprintf("/app/scripts/entrypoint-gui.sh")
- // _, err = exec(ctx, lc.CmpId, cmd, 30)
- // if err != nil {
- // return errors.Wrap(err, "exec start openclaw gateway")
- // }
- return nil
- }
- func (c *openclaw) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) *computeapi.PodContainerCreateInput {
- // Multi-container: use GetContainerSpecs
- return nil
- }
- // func (c *openclaw) GetContainerSpecs(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput {
- // diskIndex := 0
- // // 1. Browser sidecar: CDP on 9222, persistent /config, shm 2g
- // browserVols := []*commonapi.ContainerVolumeMount{
- // {
- // Disk: &commonapi.ContainerVolumeMountDisk{
- // Index: &diskIndex,
- // SubDirectory: browserStorageDir,
- // },
- // Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
- // MountPath: browserConfigDir,
- // },
- // }
- // browserSpec := computeapi.ContainerSpec{
- // ContainerSpec: commonapi.ContainerSpec{
- // Image: openclawBrowserImage,
- // EnableLxcfs: true,
- // AlwaysRestart: true,
- // ShmSizeMB: 2048, // 2g for Chrome
- // Envs: []*commonapi.ContainerKeyValue{
- // {Key: "PUID", Value: "1000"},
- // {Key: "PGID", Value: "1000"},
- // {Key: "TZ", Value: "Etc/UTC"},
- // {Key: "CHROME_CLI", Value: "--remote-debugging-port=" + openclawBrowserCDPPort},
- // },
- // },
- // VolumeMounts: browserVols,
- // }
- // // 2. OpenClaw main: nginx :8080 -> gateway :18789, /data, depends on browser
- // openclawVols := []*commonapi.ContainerVolumeMount{
- // {
- // Disk: &commonapi.ContainerVolumeMountDisk{
- // Index: &diskIndex,
- // SubDirectory: "data",
- // },
- // Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
- // MountPath: openclawDataDir,
- // },
- // }
- // openclawSpec := computeapi.ContainerSpec{
- // ContainerSpec: commonapi.ContainerSpec{
- // Image: image.ToContainerImage(),
- // ImageCredentialId: image.CredentialId,
- // EnableLxcfs: true,
- // AlwaysRestart: true,
- // DependsOn: []string{fmt.Sprintf("%s-%s", llm.GetName(), browserContainerName)},
- // Envs: []*commonapi.ContainerKeyValue{
- // // Provider
- // {Key: "MOONSHOT_API_KEY", Value: "sk-9taa32DcGGQliadQTEcZfpMUL9LCAnZVfyE6hKWPUMWEofJ8"},
- // {Key: "OPENCLAW_PRIMARY_MODEL", Value: "moonshot/kimi-k2.5"},
- // // Auth
- // {Key: "AUTH_USERNAME", Value: "admin"},
- // {Key: "AUTH_PASSWORD", Value: "admin@123"},
- // {Key: "OPENCLAW_GATEWAY_TOKEN", Value: "90d42cfc7a925201a27b61ce9b6403693629d2a18094a596"},
- // // Browser sidecar
- // {Key: "BROWSER_CDP_URL", Value: "http://localhost" + ":" + openclawBrowserCDPPort},
- // {Key: "BROWSER_DEFAULT_PROFILE", Value: "openclaw"},
- // {Key: "BROWSER_EVALUATE_ENABLED", Value: "true"},
- // },
- // },
- // VolumeMounts: openclawVols,
- // }
- // return []*computeapi.PodContainerCreateInput{
- // {Name: fmt.Sprintf("%s-%s", llm.GetName(), browserContainerName), ContainerSpec: browserSpec},
- // {Name: fmt.Sprintf("%s-%s", llm.GetName(), openclawContainerName), ContainerSpec: openclawSpec},
- // }
- // }
- func (c *openclaw) getOpenClawBaseConfig(llm *models.SLLM) *api.OpenClawConfig {
- return &api.OpenClawConfig{
- Browser: &api.OpenClawConfigBrowser{
- Enabled: true,
- DefaultProfile: "openclaw",
- SSRFPolicy: map[string]interface{}{
- "dangerouslyAllowPrivateNetwork": true,
- },
- Headless: false,
- NoSandbox: false,
- },
- Agents: &api.OpenClawConfigAgents{
- "defaults": &api.OpenClawConfigAgent{
- // TODO: 支持从 llm spec 里面自动选择
- ImageModel: &api.OpenClawConfigAgentModel{
- Primary: "moonshot/kimi-k2.5",
- },
- },
- },
- }
- }
- func (c *openclaw) GetContainerSpecs(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput {
- diskIndex := 0
- openclawVols := []*commonapi.ContainerVolumeMount{
- {
- Disk: &commonapi.ContainerVolumeMountDisk{
- Index: &diskIndex,
- SubDirectory: "config",
- },
- Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
- MountPath: desktopConfigDir,
- },
- {
- Disk: &commonapi.ContainerVolumeMountDisk{
- Index: &diskIndex,
- SubDirectory: "home",
- },
- Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
- MountPath: homeDir,
- },
- {
- Disk: &commonapi.ContainerVolumeMountDisk{
- Index: &diskIndex,
- SubDirectory: "data",
- },
- Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
- MountPath: openclawDataDir,
- },
- {
- Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_TEXT,
- Text: &commonapi.ContainerVolumeMountText{
- Content: jsonutils.Marshal(c.getOpenClawBaseConfig(llm)).PrettyString(),
- },
- MountPath: api.LLM_OPENCLAW_CUSTOM_CONFIG_FILE,
- },
- }
- httpAuthUsername := "admin"
- httpAuthPassword := openclawFixed9DigitPassword(llm.GetId())
- openclawSpec := computeapi.ContainerSpec{
- ContainerSpec: commonapi.ContainerSpec{
- Image: image.ToContainerImage(),
- ImageCredentialId: image.CredentialId,
- EnableLxcfs: true,
- AlwaysRestart: true,
- ShmSizeMB: 2048,
- DisableNoNewPrivs: true,
- Envs: []*commonapi.ContainerKeyValue{
- // Desktop env
- // {Key: "TZ", Value: "Etc/UTC"},
- {Key: "TZ", Value: "Asia/Shanghai"},
- {Key: "PUID", Value: "1000"},
- {Key: "PGID", Value: "1000"},
- {Key: "LC_ALL", Value: "zh_CN.UTF-8"},
- // webtop envs: https://github.com/linuxserver/docker-webtop?tab=readme-ov-file#advanced-configuration
- // {Key: "DISABLE_SUDO", Value: "true"},
- // Provider
- // {Key: "MOONSHOT_API_KEY", Value: "abc"},
- // {Key: "OPENCLAW_PRIMARY_MODEL", Value: "moonshot/kimi-k2.5"},
- // Auth
- {Key: string(api.LLM_OPENCLAW_AUTH_USERNAME), Value: httpAuthUsername},
- {Key: string(api.LLM_OPENCLAW_CUSTOM_USER), Value: httpAuthUsername},
- {Key: string(api.LLM_OPENCLAW_AUTH_PASSWORD), Value: httpAuthPassword},
- {Key: string(api.LLM_OPENCLAW_PASSWORD), Value: httpAuthPassword},
- {Key: string(api.LLM_OPENCLAW_CUSTOM_CONFIG), Value: api.LLM_OPENCLAW_CUSTOM_CONFIG_FILE},
- // // Browser sidecar
- // {Key: "BROWSER_CDP_URL", Value: "http://localhost" + ":" + openclawBrowserCDPPort},
- // {Key: "BROWSER_DEFAULT_PROFILE", Value: "openclaw"},
- // {Key: "BROWSER_EVALUATE_ENABLED", Value: "true"},
- // OpenClaw env
- {Key: "OPENCLAW_GATEWAY_TOKEN", Value: llm.GetId()},
- {Key: "OPENCLAW_GATEWAY_PORT", Value: "18789"},
- {Key: "OPENCLAW_GATEWAY_BIND", Value: "loopback"},
- {Key: "OPENCLAW_STATE_DIR", Value: "/config/.openclaw"},
- {Key: "OPENCLAW_WORKSPACE_DIR", Value: "/config/.openclaw/workspace"},
- // Brew env
- {Key: "HOMEBREW_PREFIX", Value: "/home/linuxbrew/.linuxbrew"},
- {Key: "HOMEBREW_CELLAR", Value: "/home/linuxbrew/.linuxbrew/Cellar"},
- {Key: "HOMEBREW_REPOSITORY", Value: "/home/linuxbrew/.linuxbrew/Homebrew"},
- // Selkies env
- {Key: "SELKIES_UI_TITLE", Value: "Cloudpods Desktop"},
- {Key: "SELKIES_UI_SHOW_LOGO", Value: "False"},
- {Key: "SELKIES_UI_SIDEBAR_SHOW_APPS", Value: "False"},
- {Key: "SELKIES_UI_SIDEBAR_SHOW_GAMEPADS", Value: "False"},
- },
- },
- VolumeMounts: openclawVols,
- RootFs: &commonapi.ContainerRootfs{
- Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
- Disk: &commonapi.ContainerVolumeMountDisk{
- Index: &diskIndex,
- SubDirectory: "rootfs",
- },
- Persistent: false,
- },
- }
- // inject credential envs
- // spec := c.GetEffectiveSpec(llm, sku)
- if llm.LLMSpec == nil || llm.LLMSpec.OpenClaw == nil {
- return []*computeapi.PodContainerCreateInput{
- {
- Name: fmt.Sprintf("%s-%d", llm.GetName(), 0),
- ContainerSpec: openclawSpec,
- },
- }
- }
- spec := llm.LLMSpec.OpenClaw
- for _, provider := range spec.Providers {
- openclawSpec.Envs = appendCredentialEnvs(openclawSpec.Envs, provider.Credential)
- }
- for _, channel := range spec.Channels {
- openclawSpec.Envs = appendCredentialEnvs(openclawSpec.Envs, channel.Credential)
- }
- if sku.LLMSpec != nil && sku.LLMSpec.OpenClaw != nil {
- skuSpec := sku.LLMSpec.OpenClaw
- // inject workspace templates
- if skuSpec.WorkspaceTemplates != nil {
- if skuSpec.WorkspaceTemplates.AgentsMD != "" {
- openclawSpec.Envs = append(openclawSpec.Envs, &commonapi.ContainerKeyValue{
- Key: string(api.LLM_OPENCLAW_TEMPLATE_AGENTS_MD_B64),
- Value: base64.StdEncoding.EncodeToString([]byte(skuSpec.WorkspaceTemplates.AgentsMD)),
- })
- }
- if skuSpec.WorkspaceTemplates.SoulMD != "" {
- openclawSpec.Envs = append(openclawSpec.Envs, &commonapi.ContainerKeyValue{
- Key: string(api.LLM_OPENCLAW_TEMPLATE_SOUL_MD_B64),
- Value: base64.StdEncoding.EncodeToString([]byte(skuSpec.WorkspaceTemplates.SoulMD)),
- })
- }
- if skuSpec.WorkspaceTemplates.UserMD != "" {
- openclawSpec.Envs = append(openclawSpec.Envs, &commonapi.ContainerKeyValue{
- Key: string(api.LLM_OPENCLAW_TEMPLATE_USER_MD_B64),
- Value: base64.StdEncoding.EncodeToString([]byte(skuSpec.WorkspaceTemplates.UserMD)),
- })
- }
- }
- }
- return []*computeapi.PodContainerCreateInput{
- {
- Name: fmt.Sprintf("%s-%d", llm.GetName(), 0),
- ContainerSpec: openclawSpec,
- },
- }
- }
- func (c *openclaw) GetLLMAccessUrlInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *models.LLMAccessInfoInput) (*api.LLMAccessUrlInfo, error) {
- return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "https", api.LLM_OPENCLAW_DEFAULT_PORT)
- }
- // GetLoginInfo returns OpenClaw web UI login credentials (same defaults as container env).
- func (c *openclaw) GetLoginInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM) (*api.LLMAccessInfo, error) {
- ctr, err := llm.GetLLMSContainer(ctx)
- if err != nil {
- if errors.Cause(err) == sql.ErrNoRows || strings.Contains(strings.ToLower(err.Error()), "not found") {
- return nil, nil
- }
- return nil, errors.Wrap(err, "get llm cloud container")
- }
- if ctr.Spec == nil {
- return nil, errors.Wrap(errors.ErrEmpty, "no Spec")
- }
- var (
- username string
- password string
- gatewayToken string
- )
- for _, env := range ctr.Spec.Envs {
- if env.Key == string(api.LLM_OPENCLAW_AUTH_USERNAME) {
- username = env.Value
- }
- if env.Key == string(api.LLM_OPENCLAW_AUTH_PASSWORD) {
- password = env.Value
- }
- if env.Key == string(api.LLM_OPENCLAW_GATEWAY_TOKEN) {
- gatewayToken = env.Value
- }
- }
- return &api.LLMAccessInfo{
- Username: username,
- Password: password,
- Extra: map[string]string{
- string(api.LLM_OPENCLAW_GATEWAY_TOKEN): gatewayToken,
- },
- }, nil
- }
- func (c *openclaw) GetProbedInstantModelsExt(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, mdlIds ...string) (map[string]api.LLMInternalInstantMdlInfo, error) {
- return nil, nil
- }
- func (c *openclaw) DetectModelPaths(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, pkgInfo api.LLMInternalInstantMdlInfo) ([]string, error) {
- return nil, nil
- }
- func (c *openclaw) GetImageInternalPathMounts(sApp *models.SInstantModel) map[string]string {
- return nil
- }
- func (c *openclaw) GetSaveDirectories(sApp *models.SInstantModel) (string, []string, error) {
- return "", nil, nil
- }
- func (c *openclaw) ValidateMounts(mounts []string, mdlName string, mdlTag string) ([]string, error) {
- return nil, nil
- }
- func (c *openclaw) CheckDuplicateMounts(errStr string, dupIndex int) string {
- return "Duplicate mounts detected"
- }
- func (c *openclaw) GetInstantModelIdByPostOverlay(postOverlay *commonapi.ContainerVolumeMountDiskPostOverlay, mdlNameToId map[string]string) string {
- return ""
- }
- func (c *openclaw) GetDirPostOverlay(dir api.LLMMountDirInfo) *commonapi.ContainerVolumeMountDiskPostOverlay {
- return nil
- }
- func (c *openclaw) PreInstallModel(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, instMdl *models.SLLMInstantModel) error {
- return nil
- }
- func (c *openclaw) InstallModel(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, dirs []string, mdlIds []string) error {
- return nil
- }
- func (c *openclaw) UninstallModel(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, instMdl *models.SLLMInstantModel) error {
- return nil
- }
- func (c *openclaw) DownloadModel(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, tmpDir string, modelName string, modelTag string) (string, []string, error) {
- return "", nil, nil
- }
|