feat: improve instance creation environment workflow

This commit is contained in:
Qingshan Chen
2026-04-15 18:20:24 +08:00
parent 4a3081e6f5
commit 366bae194a
10 changed files with 1815 additions and 627 deletions
@@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS instances (
os_version VARCHAR(50) NOT NULL,
image_registry VARCHAR(255),
image_tag VARCHAR(100),
environment_overrides_json LONGTEXT,
storage_class VARCHAR(50) DEFAULT 'standard',
mount_path VARCHAR(255) DEFAULT '/data',
pod_name VARCHAR(255),
@@ -0,0 +1,15 @@
SET @instance_environment_overrides_column_exists = (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'instances'
AND COLUMN_NAME = 'environment_overrides_json'
);
SET @instance_environment_overrides_column_sql = IF(
@instance_environment_overrides_column_exists = 0,
'ALTER TABLE instances ADD COLUMN environment_overrides_json LONGTEXT NULL AFTER image_tag',
'SELECT 1'
);
PREPARE instance_environment_overrides_column_stmt FROM @instance_environment_overrides_column_sql;
EXECUTE instance_environment_overrides_column_stmt;
DEALLOCATE PREPARE instance_environment_overrides_column_stmt;
+31 -29
View File
@@ -62,21 +62,22 @@ type PublishConfigRevisionRequest struct {
// CreateInstanceRequest represents a create instance request
type CreateInstanceRequest struct {
Name string `json:"name" binding:"required,min=3,max=50"`
Description *string `json:"description,omitempty"`
Type string `json:"type" binding:"required,oneof=openclaw ubuntu debian centos custom webtop"`
CPUCores float64 `json:"cpu_cores" binding:"required,min=0.1,max=32"`
MemoryGB int `json:"memory_gb" binding:"required,min=1,max=128"`
DiskGB int `json:"disk_gb" binding:"required,min=10,max=1000"`
GPUEnabled bool `json:"gpu_enabled"`
GPUCount int `json:"gpu_count" binding:"min=0,max=4"`
OSType string `json:"os_type" binding:"required"`
OSVersion string `json:"os_version" binding:"required"`
ImageRegistry *string `json:"image_registry,omitempty"`
ImageTag *string `json:"image_tag,omitempty"`
StorageClass string `json:"storage_class"`
OpenClawConfigPlan *services.OpenClawConfigPlan `json:"openclaw_config_plan,omitempty"`
SkillIDs []int `json:"skill_ids,omitempty"`
Name string `json:"name" binding:"required,min=3,max=50"`
Description *string `json:"description,omitempty"`
Type string `json:"type" binding:"required,oneof=openclaw ubuntu debian centos custom webtop"`
CPUCores float64 `json:"cpu_cores" binding:"required,min=0.1,max=32"`
MemoryGB int `json:"memory_gb" binding:"required,min=1,max=128"`
DiskGB int `json:"disk_gb" binding:"required,min=10,max=1000"`
GPUEnabled bool `json:"gpu_enabled"`
GPUCount int `json:"gpu_count" binding:"min=0,max=4"`
OSType string `json:"os_type" binding:"required"`
OSVersion string `json:"os_version" binding:"required"`
ImageRegistry *string `json:"image_registry,omitempty"`
ImageTag *string `json:"image_tag,omitempty"`
EnvironmentOverrides map[string]string `json:"environment_overrides,omitempty"`
StorageClass string `json:"storage_class"`
OpenClawConfigPlan *services.OpenClawConfigPlan `json:"openclaw_config_plan,omitempty"`
SkillIDs []int `json:"skill_ids,omitempty"`
}
// UpdateInstanceRequest represents an update instance request
@@ -133,20 +134,21 @@ func (h *InstanceHandler) CreateInstance(c *gin.Context) {
}
createReq := services.CreateInstanceRequest{
Name: req.Name,
Description: req.Description,
Type: req.Type,
CPUCores: req.CPUCores,
MemoryGB: req.MemoryGB,
DiskGB: req.DiskGB,
GPUEnabled: req.GPUEnabled,
GPUCount: req.GPUCount,
OSType: req.OSType,
OSVersion: req.OSVersion,
ImageRegistry: req.ImageRegistry,
ImageTag: req.ImageTag,
StorageClass: req.StorageClass,
OpenClawConfigPlan: req.OpenClawConfigPlan,
Name: req.Name,
Description: req.Description,
Type: req.Type,
CPUCores: req.CPUCores,
MemoryGB: req.MemoryGB,
DiskGB: req.DiskGB,
GPUEnabled: req.GPUEnabled,
GPUCount: req.GPUCount,
OSType: req.OSType,
OSVersion: req.OSVersion,
ImageRegistry: req.ImageRegistry,
ImageTag: req.ImageTag,
EnvironmentOverrides: req.EnvironmentOverrides,
StorageClass: req.StorageClass,
OpenClawConfigPlan: req.OpenClawConfigPlan,
}
instance, err := h.instanceService.Create(userID.(int), createReq)
+1
View File
@@ -22,6 +22,7 @@ type Instance struct {
OSVersion string `db:"os_version" json:"os_version"`
ImageRegistry *string `db:"image_registry" json:"image_registry,omitempty"`
ImageTag *string `db:"image_tag" json:"image_tag,omitempty"`
EnvironmentOverridesJSON *string `db:"environment_overrides_json" json:"-"`
StorageClass string `db:"storage_class" json:"storage_class"`
MountPath string `db:"mount_path" json:"mount_path"`
PodName *string `db:"pod_name" json:"pod_name,omitempty"`
+84
View File
@@ -0,0 +1,84 @@
package services
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"clawreef/internal/models"
)
var envNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
func normalizeEnvironmentOverrides(overrides map[string]string) (map[string]string, error) {
if len(overrides) == 0 {
return nil, nil
}
normalized := make(map[string]string, len(overrides))
for rawKey, value := range overrides {
key := strings.TrimSpace(rawKey)
if key == "" {
return nil, fmt.Errorf("environment variable name cannot be empty")
}
if !envNamePattern.MatchString(key) {
return nil, fmt.Errorf("invalid environment variable name: %s", key)
}
if _, exists := normalized[key]; exists {
return nil, fmt.Errorf("duplicate environment variable name: %s", key)
}
normalized[key] = value
}
return normalized, nil
}
func marshalEnvironmentOverrides(overrides map[string]string) (*string, error) {
if len(overrides) == 0 {
return nil, nil
}
raw, err := json.Marshal(overrides)
if err != nil {
return nil, fmt.Errorf("failed to encode environment overrides: %w", err)
}
encoded := string(raw)
return &encoded, nil
}
func parseEnvironmentOverridesJSON(raw *string) (map[string]string, error) {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil, nil
}
var overrides map[string]string
if err := json.Unmarshal([]byte(strings.TrimSpace(*raw)), &overrides); err != nil {
return nil, fmt.Errorf("failed to decode environment overrides: %w", err)
}
normalized, err := normalizeEnvironmentOverrides(overrides)
if err != nil {
return nil, err
}
return normalized, nil
}
func buildInstancePodEnv(instance *models.Instance, runtimeEnv, gatewayEnv, agentEnv map[string]string) (map[string]string, error) {
if instance == nil {
return nil, fmt.Errorf("instance is required")
}
overrides, err := parseEnvironmentOverridesJSON(instance.EnvironmentOverridesJSON)
if err != nil {
return nil, err
}
resolved := mergeEnvMaps(runtimeEnv, mergeEnvMaps(gatewayEnv, agentEnv))
resolved = withInstanceProxyEnv(instance.Type, instance.ID, resolved)
resolved = mergeEnvMaps(resolved, overrides)
return resolved, nil
}
@@ -0,0 +1,70 @@
package services
import (
"testing"
"clawreef/internal/models"
)
func TestNormalizeEnvironmentOverrides(t *testing.T) {
overrides, err := normalizeEnvironmentOverrides(map[string]string{
" FOO ": "bar",
"BAR_2": "",
})
if err != nil {
t.Fatalf("normalizeEnvironmentOverrides returned error: %v", err)
}
if overrides["FOO"] != "bar" {
t.Fatalf("expected trimmed key FOO to be preserved")
}
if value, ok := overrides["BAR_2"]; !ok || value != "" {
t.Fatalf("expected empty override value to be preserved")
}
}
func TestNormalizeEnvironmentOverridesRejectsInvalidNames(t *testing.T) {
if _, err := normalizeEnvironmentOverrides(map[string]string{
"1INVALID": "value",
}); err == nil {
t.Fatalf("expected invalid environment variable name to fail validation")
}
}
func TestBuildInstancePodEnvAppliesOverridesAfterDefaults(t *testing.T) {
t.Setenv("CLAWMANAGER_EGRESS_PROXY_URL", "")
t.Setenv("CLAWMANAGER_SYSTEM_NAMESPACE", "")
t.Setenv("K8S_NAMESPACE", "")
raw, err := marshalEnvironmentOverrides(map[string]string{
"SUBFOLDER": "/custom-proxy",
"CUSTOM": "enabled",
})
if err != nil {
t.Fatalf("marshalEnvironmentOverrides returned error: %v", err)
}
instance := &models.Instance{
ID: 42,
Type: "webtop",
EnvironmentOverridesJSON: raw,
}
env, err := buildInstancePodEnv(instance, map[string]string{
"TITLE": "ClawManager Webtop",
"SUBFOLDER": "/",
}, nil, nil)
if err != nil {
t.Fatalf("buildInstancePodEnv returned error: %v", err)
}
if env["SUBFOLDER"] != "/custom-proxy" {
t.Fatalf("expected SUBFOLDER override to win, got %q", env["SUBFOLDER"])
}
if env["CUSTOM"] != "enabled" {
t.Fatalf("expected custom environment variable to be merged")
}
if env["TITLE"] != "ClawManager Webtop" {
t.Fatalf("expected default environment variable to remain available")
}
}
+54 -35
View File
@@ -33,20 +33,21 @@ type InstanceService interface {
// CreateInstanceRequest holds data for creating an instance
type CreateInstanceRequest struct {
Name string `json:"name" validate:"required,min=3,max=50"`
Description *string `json:"description,omitempty"`
Type string `json:"type" validate:"required,oneof=openclaw ubuntu debian centos custom webtop"`
CPUCores float64 `json:"cpu_cores" validate:"required,min=0.1,max=32"`
MemoryGB int `json:"memory_gb" validate:"required,min=1,max=128"`
DiskGB int `json:"disk_gb" validate:"required,min=10,max=1000"`
GPUEnabled bool `json:"gpu_enabled"`
GPUCount int `json:"gpu_count" validate:"min=0,max=4"`
OSType string `json:"os_type" validate:"required"`
OSVersion string `json:"os_version" validate:"required"`
ImageRegistry *string `json:"image_registry,omitempty"`
ImageTag *string `json:"image_tag,omitempty"`
StorageClass string `json:"storage_class"`
OpenClawConfigPlan *OpenClawConfigPlan `json:"openclaw_config_plan,omitempty"`
Name string `json:"name" validate:"required,min=3,max=50"`
Description *string `json:"description,omitempty"`
Type string `json:"type" validate:"required,oneof=openclaw ubuntu debian centos custom webtop"`
CPUCores float64 `json:"cpu_cores" validate:"required,min=0.1,max=32"`
MemoryGB int `json:"memory_gb" validate:"required,min=1,max=128"`
DiskGB int `json:"disk_gb" validate:"required,min=10,max=1000"`
GPUEnabled bool `json:"gpu_enabled"`
GPUCount int `json:"gpu_count" validate:"min=0,max=4"`
OSType string `json:"os_type" validate:"required"`
OSVersion string `json:"os_version" validate:"required"`
ImageRegistry *string `json:"image_registry,omitempty"`
ImageTag *string `json:"image_tag,omitempty"`
EnvironmentOverrides map[string]string `json:"environment_overrides,omitempty"`
StorageClass string `json:"storage_class"`
OpenClawConfigPlan *OpenClawConfigPlan `json:"openclaw_config_plan,omitempty"`
}
// UpdateInstanceRequest holds data for updating an instance
@@ -97,6 +98,14 @@ func NewInstanceService(instanceRepo repository.InstanceRepository, quotaRepo re
func (s *instanceService) Create(userID int, req CreateInstanceRequest) (*models.Instance, error) {
ctx := context.Background()
req.Name = strings.TrimSpace(req.Name)
environmentOverrides, err := normalizeEnvironmentOverrides(req.EnvironmentOverrides)
if err != nil {
return nil, err
}
environmentOverridesJSON, err := marshalEnvironmentOverrides(environmentOverrides)
if err != nil {
return nil, err
}
// Check user quota
quota, err := s.quotaRepo.GetByUserID(userID)
@@ -184,24 +193,25 @@ func (s *instanceService) Create(userID int, req CreateInstanceRequest) (*models
// Create instance record
now := time.Now()
instance := &models.Instance{
UserID: userID,
Name: req.Name,
Description: req.Description,
Type: req.Type,
Status: "creating",
CPUCores: req.CPUCores,
MemoryGB: req.MemoryGB,
DiskGB: req.DiskGB,
GPUEnabled: req.GPUEnabled,
GPUCount: req.GPUCount,
OSType: req.OSType,
OSVersion: req.OSVersion,
ImageRegistry: req.ImageRegistry,
ImageTag: req.ImageTag,
StorageClass: req.StorageClass,
MountPath: runtimeConfig.MountPath,
CreatedAt: now,
UpdatedAt: now,
UserID: userID,
Name: req.Name,
Description: req.Description,
Type: req.Type,
Status: "creating",
CPUCores: req.CPUCores,
MemoryGB: req.MemoryGB,
DiskGB: req.DiskGB,
GPUEnabled: req.GPUEnabled,
GPUCount: req.GPUCount,
OSType: req.OSType,
OSVersion: req.OSVersion,
ImageRegistry: req.ImageRegistry,
ImageTag: req.ImageTag,
EnvironmentOverridesJSON: environmentOverridesJSON,
StorageClass: req.StorageClass,
MountPath: runtimeConfig.MountPath,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.instanceRepo.Create(instance); err != nil {
@@ -227,6 +237,11 @@ func (s *instanceService) Create(userID int, req CreateInstanceRequest) (*models
s.instanceRepo.Delete(instance.ID)
return nil, fmt.Errorf("failed to build instance agent config: %w", err)
}
extraEnv, err := buildInstancePodEnv(instance, runtimeConfig.Env, gatewayEnv, agentEnv)
if err != nil {
s.instanceRepo.Delete(instance.ID)
return nil, fmt.Errorf("failed to resolve instance environment: %w", err)
}
var bootstrapSnapshot *models.OpenClawInjectionSnapshot
var bootstrapSecretName string
@@ -292,7 +307,7 @@ func (s *instanceService) Create(userID int, req CreateInstanceRequest) (*models
Image: runtimeConfig.Image,
MountPath: runtimeConfig.MountPath,
ContainerPort: runtimeConfig.Port,
ExtraEnv: withInstanceProxyEnv(instance.Type, instance.ID, mergeEnvMaps(runtimeConfig.Env, mergeEnvMaps(gatewayEnv, agentEnv))),
ExtraEnv: extraEnv,
EnvFromSecretNames: []string{bootstrapSecretName},
}
@@ -438,6 +453,11 @@ func (s *instanceService) Start(instanceID int) error {
if err != nil {
return fmt.Errorf("failed to build instance agent config: %w", err)
}
runtimeConfig := buildRuntimeConfig(instance.Type, instance.OSType, instance.OSVersion, instance.ImageRegistry, instance.ImageTag)
extraEnv, err := buildInstancePodEnv(instance, runtimeConfig.Env, gatewayEnv, agentEnv)
if err != nil {
return fmt.Errorf("failed to resolve instance environment: %w", err)
}
bootstrapSecretName := ""
if strings.EqualFold(instance.Type, "openclaw") && s.openClawConfigService != nil && instance.OpenClawConfigSnapshotID != nil && *instance.OpenClawConfigSnapshotID > 0 {
@@ -448,7 +468,6 @@ func (s *instanceService) Start(instanceID int) error {
}
// Create new pod
runtimeConfig := buildRuntimeConfig(instance.Type, instance.OSType, instance.OSVersion, instance.ImageRegistry, instance.ImageTag)
if requiresRestrictedNetwork(instance.Type) {
if err := s.networkPolicyService.EnsureDefaultPolicy(ctx, instance.UserID, instance.ID, instance.Name); err != nil {
return fmt.Errorf("failed to create network policy: %w", err)
@@ -467,7 +486,7 @@ func (s *instanceService) Start(instanceID int) error {
Image: runtimeConfig.Image,
MountPath: instance.MountPath,
ContainerPort: runtimeConfig.Port,
ExtraEnv: withInstanceProxyEnv(instance.Type, instance.ID, mergeEnvMaps(runtimeConfig.Env, mergeEnvMaps(gatewayEnv, agentEnv))),
ExtraEnv: extraEnv,
EnvFromSecretNames: []string{bootstrapSecretName},
}
@@ -1,22 +1,31 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useI18n } from '../contexts/I18nContext';
import { openclawConfigService } from '../services/openclawConfigService';
import React, { useEffect, useMemo, useState } from "react";
import type { AxiosError } from "axios";
import { useI18n } from "../contexts/I18nContext";
import { openclawConfigService } from "../services/openclawConfigService";
import type {
OpenClawConfigBundle,
OpenClawConfigCompilePreview,
OpenClawConfigPlan,
OpenClawConfigResource,
} from '../types/openclawConfig';
} from "../types/openclawConfig";
export type OpenClawInjectionMode = OpenClawConfigPlan['mode'] | 'archive';
export type OpenClawInjectionMode = OpenClawConfigPlan["mode"] | "archive";
interface OpenClawConfigPlanSectionProps {
mode: OpenClawInjectionMode;
bundleId?: number;
resourceIds: number[];
onModeChange: (mode: OpenClawInjectionMode) => void;
onSelectionChange: (payload: { bundleId?: number; resourceIds: number[] }) => void;
onPreviewChange: (preview: OpenClawConfigCompilePreview | null, state: { loading: boolean; error: string | null }) => void;
onSelectionChange: (payload: {
bundleId?: number;
resourceIds: number[];
}) => void;
onPreviewChange: (
preview: OpenClawConfigCompilePreview | null,
state: { loading: boolean; error: string | null },
) => void;
embedded?: boolean;
hideHeader?: boolean;
}
const OpenClawConfigPlanSection: React.FC<OpenClawConfigPlanSectionProps> = ({
@@ -26,6 +35,8 @@ const OpenClawConfigPlanSection: React.FC<OpenClawConfigPlanSectionProps> = ({
onModeChange,
onSelectionChange,
onPreviewChange,
embedded = false,
hideHeader = false,
}) => {
const { t } = useI18n();
const [resources, setResources] = useState<OpenClawConfigResource[]>([]);
@@ -51,7 +62,7 @@ const OpenClawConfigPlanSection: React.FC<OpenClawConfigPlanSectionProps> = ({
}, []);
const channelResources = useMemo(
() => resources.filter((resource) => resource.resource_type === 'channel'),
() => resources.filter((resource) => resource.resource_type === "channel"),
[resources],
);
@@ -59,31 +70,41 @@ const OpenClawConfigPlanSection: React.FC<OpenClawConfigPlanSectionProps> = ({
let cancelled = false;
const compile = async () => {
if (mode === 'none' || mode === 'archive') {
if (mode === "none" || mode === "archive") {
onPreviewChange(null, { loading: false, error: null });
return;
}
if (mode === 'bundle' && !bundleId) {
onPreviewChange(null, { loading: false, error: t('openClawInjectionSection.errors.chooseBundle') });
if (mode === "bundle" && !bundleId) {
onPreviewChange(null, {
loading: false,
error: t("openClawInjectionSection.errors.chooseBundle"),
});
return;
}
if (mode === 'manual' && resourceIds.length === 0) {
onPreviewChange(null, { loading: false, error: t('openClawInjectionSection.errors.chooseResource') });
if (mode === "manual" && resourceIds.length === 0) {
onPreviewChange(null, { loading: false, error: null });
return;
}
try {
onPreviewChange(null, { loading: true, error: null });
const payload: OpenClawConfigPlan = mode === 'bundle'
? { mode: 'bundle', bundle_id: bundleId }
: { mode: 'manual', resource_ids: resourceIds };
const payload: OpenClawConfigPlan =
mode === "bundle"
? { mode: "bundle", bundle_id: bundleId }
: { mode: "manual", resource_ids: resourceIds };
const result = await openclawConfigService.compilePreview(payload);
if (!cancelled) {
onPreviewChange(result, { loading: false, error: null });
}
} catch (err: any) {
} catch (err: unknown) {
const responseError = err as AxiosError<{ error?: string }>;
if (!cancelled) {
onPreviewChange(null, { loading: false, error: err.response?.data?.error || t('openClawInjectionSection.errors.compileFailed') });
onPreviewChange(null, {
loading: false,
error:
responseError.response?.data?.error ||
t("openClawInjectionSection.errors.compileFailed"),
});
}
}
};
@@ -92,32 +113,57 @@ const OpenClawConfigPlanSection: React.FC<OpenClawConfigPlanSectionProps> = ({
return () => {
cancelled = true;
};
}, [bundleId, mode, onPreviewChange, resourceIds]);
}, [bundleId, mode, onPreviewChange, resourceIds, t]);
return (
<div className="app-panel p-6">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-medium text-gray-900">{t('openClawInjectionSection.title')}</h2>
<p className="mt-1 text-sm text-gray-500">
{t('openClawInjectionSection.subtitle')}
</p>
<div className={embedded ? "" : "app-panel p-6"}>
{!hideHeader && (
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-medium text-gray-900">
{t("openClawInjectionSection.title")}
</h2>
<p className="mt-1 text-sm text-gray-500">
{t("openClawInjectionSection.subtitle")}
</p>
</div>
<span className="rounded-full bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-600">
{t("openClawInjectionSection.optional")}
</span>
</div>
<span className="rounded-full bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-600">{t('openClawInjectionSection.optional')}</span>
</div>
)}
<div className="mt-5 grid grid-cols-1 gap-3 md:grid-cols-3">
<div
className={`${hideHeader ? "" : "mt-5"} grid grid-cols-1 gap-3 md:grid-cols-3`}
>
{[
{ value: 'manual', label: t('openClawInjectionSection.modes.manual') },
{ value: 'bundle', label: t('openClawInjectionSection.modes.bundle') },
{ value: 'archive', label: t('openClawInjectionSection.modes.archive') },
{
value: "manual",
label: t("openClawInjectionSection.modes.manual"),
},
{
value: "bundle",
label: t("openClawInjectionSection.modes.bundle"),
},
{
value: "archive",
label: t("openClawInjectionSection.modes.archive"),
},
].map((item) => (
<button
key={item.value}
type="button"
onClick={() => onModeChange(mode === item.value ? 'none' : item.value as OpenClawInjectionMode)}
onClick={() =>
onModeChange(
mode === item.value
? "none"
: (item.value as OpenClawInjectionMode),
)
}
className={`rounded-2xl border px-4 py-3 text-left transition ${
mode === item.value ? 'border-indigo-300 bg-indigo-50 text-indigo-700' : 'border-gray-200 bg-white text-gray-700 hover:border-gray-300'
mode === item.value
? "border-indigo-300 bg-indigo-50 text-indigo-700"
: "border-gray-200 bg-white text-gray-700 hover:border-gray-300"
}`}
>
<div className="font-medium">{item.label}</div>
@@ -125,55 +171,83 @@ const OpenClawConfigPlanSection: React.FC<OpenClawConfigPlanSectionProps> = ({
))}
</div>
{loading && <div className="mt-5 text-sm text-gray-500">{t('openClawInjectionSection.loading')}</div>}
{loading && (
<div className="mt-5 text-sm text-gray-500">
{t("openClawInjectionSection.loading")}
</div>
)}
{!loading && mode === 'bundle' && (
{!loading && mode === "bundle" && (
<div className="mt-5">
<label className="block text-sm font-medium text-gray-700">{t('openClawInjectionSection.chooseBundle')}</label>
<label className="block text-sm font-medium text-gray-700">
{t("openClawInjectionSection.chooseBundle")}
</label>
<select
value={bundleId || ''}
onChange={(e) => onSelectionChange({ bundleId: e.target.value ? Number(e.target.value) : undefined, resourceIds: [] })}
value={bundleId || ""}
onChange={(e) =>
onSelectionChange({
bundleId: e.target.value ? Number(e.target.value) : undefined,
resourceIds: [],
})
}
className="app-input mt-1 w-full"
>
<option value="">{t('openClawInjectionSection.selectBundle')}</option>
<option value="">
{t("openClawInjectionSection.selectBundle")}
</option>
{bundles.map((bundle) => (
<option key={bundle.id} value={bundle.id}>
{bundle.name} ({t('openClawInjectionSection.bundleOptionCount', { count: bundle.items.length })})
{bundle.name} (
{t("openClawInjectionSection.bundleOptionCount", {
count: bundle.items.length,
})}
)
</option>
))}
</select>
</div>
)}
{!loading && mode === 'manual' && (
{!loading && mode === "manual" && (
<div className="mt-5 space-y-5">
<div>
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-[#b46c50]">{t('openClawInjectionSection.channel')}</div>
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-[#b46c50]">
{t("openClawInjectionSection.channel")}
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{channelResources.map((item) => {
const checked = resourceIds.includes(item.id);
return (
<label key={item.id} className={`flex cursor-pointer items-start gap-3 rounded-2xl border px-4 py-3 ${checked ? 'border-indigo-300 bg-indigo-50' : 'border-gray-200 bg-white'}`}>
<label
key={item.id}
className={`flex cursor-pointer items-start gap-3 rounded-2xl border px-4 py-3 ${checked ? "border-indigo-300 bg-indigo-50" : "border-gray-200 bg-white"}`}
>
<input
type="checkbox"
checked={checked}
onChange={(e) => onSelectionChange({
bundleId: undefined,
resourceIds: e.target.checked
? [...resourceIds, item.id]
: resourceIds.filter((value) => value !== item.id),
})}
onChange={(e) =>
onSelectionChange({
bundleId: undefined,
resourceIds: e.target.checked
? [...resourceIds, item.id]
: resourceIds.filter((value) => value !== item.id),
})
}
/>
<span>
<span className="block font-medium text-gray-900">{item.name}</span>
<span className="mt-1 block text-xs text-gray-500">{item.resource_key}</span>
<span className="block font-medium text-gray-900">
{item.name}
</span>
<span className="mt-1 block text-xs text-gray-500">
{item.resource_key}
</span>
</span>
</label>
);
})}
{channelResources.length === 0 && (
<div className="rounded-2xl border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500">
{t('openClawInjectionSection.noChannelResources')}
{t("openClawInjectionSection.noChannelResources")}
</div>
)}
</div>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -119,6 +119,7 @@ export interface CreateInstanceRequest {
os_version: string;
image_registry?: string;
image_tag?: string;
environment_overrides?: Record<string, string>;
storage_class?: string;
openclaw_config_plan?: OpenClawConfigPlan;
skill_ids?: number[];