refactor(llm): extract shared desktop webtop config (#24701)

Move common desktop/webtop container envs, base spec, rootfs, and login-info parsing
into llm_container helpers and reuse in openclaw and hermes-agent drivers.
This commit is contained in:
Zexi Li
2026-04-21 15:04:54 +08:00
committed by GitHub
parent 45a1498578
commit b61af890d1
4 changed files with 127 additions and 201 deletions
+5 -5
View File
@@ -2,10 +2,6 @@ package llm
const (
LLM_OPENCLAW_GATEWAY_TOKEN = LLMEnvKey("OPENCLAW_GATEWAY_TOKEN")
LLM_OPENCLAW_AUTH_USERNAME = LLMEnvKey("AUTH_USERNAME")
LLM_OPENCLAW_CUSTOM_USER = LLMEnvKey("CUSTOM_USER")
LLM_OPENCLAW_AUTH_PASSWORD = LLMEnvKey("AUTH_PASSWORD")
LLM_OPENCLAW_PASSWORD = LLMEnvKey("PASSWORD")
LLM_OPENCLAW_CUSTOM_CONFIG = LLMEnvKey("OPENCLAW_CUSTOM_CONFIG")
LLM_OPENCLAW_CUSTOM_CONFIG_FILE = "/opt/openclaw_base_config.json"
@@ -16,7 +12,11 @@ const (
)
const (
LLM_OPENCLAW_DEFAULT_PORT = 3001
LLM_DESKTOP_DEFAULT_PORT = 3001
LLM_DESKTOP_AUTH_USERNAME = LLMEnvKey("AUTH_USERNAME")
LLM_DESKTOP_CUSTOM_USER = LLMEnvKey("CUSTOM_USER")
LLM_DESKTOP_AUTH_PASSWORD = LLMEnvKey("AUTH_PASSWORD")
LLM_DESKTOP_PASSWORD = LLMEnvKey("PASSWORD")
)
type OpenClawConfig struct {
@@ -0,0 +1,96 @@
package llm_container
import (
"context"
"database/sql"
"strings"
"yunion.io/x/pkg/errors"
commonapi "yunion.io/x/onecloud/pkg/apis"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/llm/models"
)
// desktopWebtopImageBaseContainerSpec 返回 OpenClaw / Hermes 等桌面栈共用的镜像与运行时字段(不含 Envs)。
func desktopWebtopImageBaseContainerSpec(image *models.SLLMImage) commonapi.ContainerSpec {
return commonapi.ContainerSpec{
Image: image.ToContainerImage(),
ImageCredentialId: image.CredentialId,
EnableLxcfs: true,
AlwaysRestart: true,
ShmSizeMB: 2048,
DisableNoNewPrivs: true,
}
}
// desktopWebtopCommonEnvs 返回 webtop / Selkies 等桌面容器共用的环境变量(时区、locale、admin 登录、Homebrew、侧边栏等)。
func desktopWebtopCommonEnvs(llmId string) []*commonapi.ContainerKeyValue {
httpAuthUsername := "admin"
httpAuthPassword := openclawFixed9DigitPassword(llmId)
return []*commonapi.ContainerKeyValue{
{Key: "TZ", Value: "Asia/Shanghai"},
{Key: "PUID", Value: "1000"},
{Key: "PGID", Value: "1000"},
{Key: "LC_ALL", Value: "zh_CN.UTF-8"},
{Key: string(api.LLM_DESKTOP_AUTH_USERNAME), Value: httpAuthUsername},
{Key: string(api.LLM_DESKTOP_CUSTOM_USER), Value: httpAuthUsername},
{Key: string(api.LLM_DESKTOP_AUTH_PASSWORD), Value: httpAuthPassword},
{Key: string(api.LLM_DESKTOP_PASSWORD), Value: httpAuthPassword},
{Key: "HOMEBREW_PREFIX", Value: "/home/linuxbrew/.linuxbrew"},
{Key: "HOMEBREW_CELLAR", Value: "/home/linuxbrew/.linuxbrew/Cellar"},
{Key: "HOMEBREW_REPOSITORY", Value: "/home/linuxbrew/.linuxbrew/Homebrew"},
{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"},
}
}
func desktopContainerRootFs(diskIndex *int) *commonapi.ContainerRootfs {
return &commonapi.ContainerRootfs{
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
Disk: &commonapi.ContainerVolumeMountDisk{
Index: diskIndex,
SubDirectory: "rootfs",
},
Persistent: false,
}
}
// getDesktopWebUILoginInfo 从已部署容器 Spec 中解析桌面 Web UI 登录信息与 gateway token(若存在)。
func getDesktopWebUILoginInfo(ctx context.Context, 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_DESKTOP_AUTH_USERNAME) {
username = env.Value
}
if env.Key == string(api.LLM_DESKTOP_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
}
+11 -100
View File
@@ -2,11 +2,7 @@ package llm_container
import (
"context"
"database/sql"
"fmt"
"strings"
"yunion.io/x/pkg/errors"
commonapi "yunion.io/x/onecloud/pkg/apis"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
@@ -68,68 +64,16 @@ func (h *hermesAgent) GetContainerSpec(ctx context.Context, llm *models.SLLM, im
MountPath: hermesAgentDataDir,
},
}
httpAuthUsername := "admin"
httpAuthPassword := openclawFixed9DigitPassword(llm.GetId())
hermesInner := desktopWebtopImageBaseContainerSpec(image)
hermesInner.Envs = append(desktopWebtopCommonEnvs(llm.GetId()),
models.NewEnv("HERMES_HOME", hermesAgentDataDir),
&commonapi.ContainerKeyValue{Key: "HERMES_WEB_DIST", Value: "/opt/hermes/hermes_cli/web_dist"},
)
hermesSpec := 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},
// 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"},
// Hermes env
models.NewEnv("HERMES_HOME", hermesAgentDataDir),
{Key: "HERMES_WEB_DIST", Value: "/opt/hermes/hermes_cli/web_dist"},
},
},
VolumeMounts: hermesVols,
RootFs: &commonapi.ContainerRootfs{
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
Disk: &commonapi.ContainerVolumeMountDisk{
Index: &diskIndex,
SubDirectory: "rootfs",
},
Persistent: false,
},
ContainerSpec: hermesInner,
VolumeMounts: hermesVols,
RootFs: desktopContainerRootFs(&diskIndex),
}
// inject credential envs
// spec := c.GetEffectiveSpec(llm, sku)
if llm.LLMSpec == nil || llm.LLMSpec.HermesAgent == nil {
return &computeapi.PodContainerCreateInput{
Name: fmt.Sprintf("%s-%d", llm.GetName(), 0),
ContainerSpec: hermesSpec,
}
}
return &computeapi.PodContainerCreateInput{
Name: fmt.Sprintf("%s-%d", llm.GetName(), 0),
ContainerSpec: hermesSpec,
@@ -143,43 +87,10 @@ func (h *hermesAgent) GetContainerSpecs(ctx context.Context, llm *models.SLLM, i
}
func (h *hermesAgent) GetLLMAccessUrlInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *models.LLMAccessInfoInput) (*api.LLMAccessUrlInfo, error) {
//TODO implement me
return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "https", api.LLM_OPENCLAW_DEFAULT_PORT)
return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "https", api.LLM_DESKTOP_DEFAULT_PORT)
}
// GetLoginInfo returns OpenClaw web UI login credentials (same defaults as container env).
// GetLoginInfo returns desktop web UI login credentials (same defaults as container env).
func (h *hermesAgent) 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
return getDesktopWebUILoginInfo(ctx, llm)
}
+15 -96
View File
@@ -2,14 +2,11 @@ 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"
@@ -312,66 +309,20 @@ func (c *openclaw) GetContainerSpecs(ctx context.Context, llm *models.SLLM, imag
MountPath: api.LLM_OPENCLAW_CUSTOM_CONFIG_FILE,
},
}
httpAuthUsername := "admin"
httpAuthPassword := openclawFixed9DigitPassword(llm.GetId())
openclawInner := desktopWebtopImageBaseContainerSpec(image)
openclawInner.Envs = desktopWebtopCommonEnvs(llm.GetId())
openclawInner.Envs = append(openclawInner.Envs,
&commonapi.ContainerKeyValue{Key: string(api.LLM_OPENCLAW_CUSTOM_CONFIG), Value: api.LLM_OPENCLAW_CUSTOM_CONFIG_FILE},
&commonapi.ContainerKeyValue{Key: "OPENCLAW_GATEWAY_TOKEN", Value: llm.GetId()},
&commonapi.ContainerKeyValue{Key: "OPENCLAW_GATEWAY_PORT", Value: "18789"},
&commonapi.ContainerKeyValue{Key: "OPENCLAW_GATEWAY_BIND", Value: "loopback"},
&commonapi.ContainerKeyValue{Key: "OPENCLAW_STATE_DIR", Value: "/config/.openclaw"},
&commonapi.ContainerKeyValue{Key: "OPENCLAW_WORKSPACE_DIR", Value: "/config/.openclaw/workspace"},
)
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,
},
ContainerSpec: openclawInner,
VolumeMounts: openclawVols,
RootFs: desktopContainerRootFs(&diskIndex),
}
// inject credential envs
// spec := c.GetEffectiveSpec(llm, sku)
@@ -425,44 +376,12 @@ func (c *openclaw) GetContainerSpecs(ctx context.Context, llm *models.SLLM, imag
}
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)
return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "https", api.LLM_DESKTOP_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
return getDesktopWebUILoginInfo(ctx, llm)
}
func (c *openclaw) GetProbedInstantModelsExt(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, mdlIds ...string) (map[string]api.LLMInternalInstantMdlInfo, error) {