feat: Add support for importing environment variables into the runtime environment. (#13727)

This commit is contained in:
CityFun authored and GitHub committed 2026-09-07 18:26:35 +08:00
1 parent a47e41a8b7
commit 50a54d0613
15 files changed
+168 -21

No files matched your search

+40 -12
View File
@@ -838,21 +838,49 @@ func restartRuntime(runtime *model.Runtime) (err error) {
}
func getDockerComposeEnvironments(yml []byte) ([]request.Environment, error) {
var (
composeProject docker.ComposeProject
err error
)
err = yaml.Unmarshal(yml, &composeProject)
if err != nil {
var project struct {
Services yaml.Node `yaml:"services"`
}
if err := yaml.Unmarshal(yml, &project); err != nil {
return nil, err
}
services := &project.Services
if services.Kind == yaml.AliasNode {
services = services.Alias
}
if services.Kind != 0 && services.Kind != yaml.MappingNode {
return nil, fmt.Errorf("unsupported services format")
}
var res []request.Environment
for _, service := range composeProject.Services {
for key, value := range service.Environment.Variables {
res = append(res, request.Environment{
Key: key,
Value: value,
})
// Keep the file order for both services and environment entries.
for i := 1; i < len(services.Content); i += 2 {
var service struct {
Environment yaml.Node `yaml:"environment"`
}
if err := services.Content[i].Decode(&service); err != nil {
return nil, err
}
environment := &service.Environment
if environment.Kind == yaml.AliasNode {
environment = environment.Alias
}
switch environment.Kind {
case yaml.MappingNode:
for j := 0; j < len(environment.Content); j += 2 {
res = append(res, request.Environment{Key: environment.Content[j].Value, Value: environment.Content[j+1].Value})
}
case yaml.SequenceNode:
for _, item := range environment.Content {
var entry string
if err := item.Decode(&entry); err != nil {
return nil, err
}
key, value, _ := strings.Cut(entry, "=")
res = append(res, request.Environment{Key: key, Value: value})
}
case 0:
default:
return nil, fmt.Errorf("unsupported environment format")
}
}
return res, nil
+5
View File
@@ -4318,6 +4318,11 @@ const message = {
'The following fields will change: {0}. The rule will be applied and verified immediately. Continue?',
},
runtime: {
importEnv: 'Import Environment Variables',
envImportError: 'Line {0}: {1}',
envInvalidAssignment: 'Expected a valid NAME=value assignment',
envUnclosedQuote: 'Unclosed quote',
envUnexpectedText: 'Unexpected text after the closing quote',
runtime: 'Runtime',
default: 'Default',
workDir: 'Working directory',
+5
View File
@@ -4371,6 +4371,11 @@ const message = {
'Se modificarán los siguientes campos: {0}. La regla se aplicará y verificará inmediatamente. ¿Continuar?',
},
runtime: {
importEnv: 'Importar variables de entorno',
envImportError: 'Línea {0}: {1}',
envInvalidAssignment: 'Se esperaba una asignación NOMBRE=valor válida',
envUnclosedQuote: 'Comillas sin cerrar',
envUnexpectedText: 'Texto inesperado después de las comillas de cierre',
runtime: 'Runtime',
default: 'Predeterminado',
workDir: 'Directorio de trabajo',
+5
View File
@@ -4275,6 +4275,11 @@ const message = {
editRuleConfirm: 'فیلدهای زیر تغییر می‌کنند: {0}. قانون بلافاصله اعمال و بررسی می‌شود. ادامه می‌دهید؟',
},
runtime: {
importEnv: 'وارد کردن متغیرهای محیطی',
envImportError: 'خط {0}: {1}',
envInvalidAssignment: 'یک عبارت معتبر NAME=value لازم است',
envUnclosedQuote: 'علامت نقل قول بسته نشده است',
envUnexpectedText: 'متن اضافی پس از علامت نقل قول پایانی',
runtime: 'محیط اجرا',
default: 'پیش‌فرض',
workDir: 'دایرکتوری کاری',
+5
View File
@@ -4303,6 +4303,11 @@ const message = {
editRuleConfirm: '次の項目を変更します:{0}。ルールは直ちに適用され、再検証されます。続行しますか?',
},
runtime: {
importEnv: '環境変数をインポート',
envImportError: '{0} 行目:{1}',
envInvalidAssignment: '有効な NAME=value 形式で入力してください',
envUnclosedQuote: '引用符が閉じられていません',
envUnexpectedText: '閉じ引用符の後に余分な文字があります',
runtime: 'ランタイム',
workDir: '作業ディレクトリ',
localHelper:
+5
View File
@@ -4223,6 +4223,11 @@ const message = {
editRuleConfirm: '다음 필드가 변경됩니다: {0}. 규칙이 즉시 적용되고 다시 검증됩니다. 계속하시겠습니까?',
},
runtime: {
importEnv: '환경 변수 가져오기',
envImportError: '{0}행: {1}',
envInvalidAssignment: '유효한 NAME=value 형식이 필요합니다',
envUnclosedQuote: '따옴표가 닫히지 않았습니다',
envUnexpectedText: '닫는 따옴표 뒤에 불필요한 텍스트가 있습니다',
runtime: '실행 환경',
workDir: '작업 디렉토리',
localHelper: '로컬 환경 설치 및 오프라인 환경 사용 관련 문제는 다음을 참조하세요 ',
+5
View File
@@ -4186,6 +4186,11 @@ const message = {
editRuleConfirm: 'ຈະປ່ຽນຟິວຕໍ່ໄປນີ້: {0}. ກົດຈະຖືກນຳໃຊ້ແລະກວດສອບທັນທີ. ສືບຕໍ່ບໍ?',
},
runtime: {
importEnv: 'ນຳເຂົ້າຕົວແປສະພາບແວດລ້ອມ',
envImportError: 'ແຖວ {0}: {1}',
envInvalidAssignment: 'ຕ້ອງເປັນຮູບແບບ NAME=value ທີ່ຖືກຕ້ອງ',
envUnclosedQuote: 'ເຄື່ອງໝາຍວົງຢືມຍັງບໍ່ປິດ',
envUnexpectedText: 'ມີຂໍ້ຄວາມເກີນຫຼັງເຄື່ອງໝາຍວົງຢືມປິດ',
runtime: 'ຣັນທາມ (Runtime)',
default: 'ຄ່າເລີ່ມຕົ້ນ',
workDir: 'ໂຟນເດີທີ່ເຮັດວຽກ',
+5
View File
@@ -4383,6 +4383,11 @@ const message = {
editRuleConfirm: 'Medan berikut akan diubah: {0}. Peraturan akan digunakan dan disahkan serta-merta. Teruskan?',
},
runtime: {
importEnv: 'Import Pemboleh Ubah Persekitaran',
envImportError: 'Baris {0}: {1}',
envInvalidAssignment: 'Penetapan NAME=value yang sah diperlukan',
envUnclosedQuote: 'Tanda petikan tidak ditutup',
envUnexpectedText: 'Teks tidak dijangka selepas tanda petikan penutup',
runtime: 'Runtime',
workDir: 'Direktori kerja',
localHelper:
+5
View File
@@ -4409,6 +4409,11 @@ const message = {
'Os seguintes campos serão alterados: {0}. A regra será aplicada e verificada imediatamente. Continuar?',
},
runtime: {
importEnv: 'Importar variáveis de ambiente',
envImportError: 'Linha {0}: {1}',
envInvalidAssignment: 'Era esperada uma atribuição NOME=valor válida',
envUnclosedQuote: 'Aspas não fechadas',
envUnexpectedText: 'Texto inesperado após as aspas de fechamento',
runtime: 'Runtime',
workDir: 'Diretório de trabalho',
localHelper: 'Para problemas de instalação em ambiente local e uso em ambiente offline, consulte ',
+5
View File
@@ -4373,6 +4373,11 @@ const message = {
editRuleConfirm: 'Будут изменены поля: {0}. Правило будет немедленно применено и проверено. Продолжить?',
},
runtime: {
importEnv: 'Импорт переменных окружения',
envImportError: 'Строка {0}: {1}',
envInvalidAssignment: 'Ожидается корректное присваивание NAME=value',
envUnclosedQuote: 'Незакрытая кавычка',
envUnexpectedText: 'Лишний текст после закрывающей кавычки',
runtime: 'Среда выполнения',
workDir: 'Рабочая директория',
localHelper: 'По вопросам установки в локальной среде и использования в автономном режиме обратитесь к ',
+5
View File
@@ -4387,6 +4387,11 @@ const message = {
editRuleConfirm: 'Şu alanlar değiştirilecek: {0}. Kural hemen uygulanıp doğrulanacak. Devam edilsin mi?',
},
runtime: {
importEnv: 'Ortam Değişkenlerini İçe Aktar',
envImportError: 'Satır {0}: {1}',
envInvalidAssignment: 'Geçerli bir NAME=value ataması bekleniyor',
envUnclosedQuote: 'Kapatılmamış tırnak',
envUnexpectedText: 'Kapanış tırnağından sonra beklenmeyen metin',
runtime: 'Çalışma Zamanı',
default: 'Varsayılan',
workDir: 'Çalışma dizini',
+5
View File
@@ -4025,6 +4025,11 @@ const message = {
editRuleConfirm: '將修改以下欄位:{0}。提交後會立即套用並回讀驗證,是否繼續?',
},
runtime: {
importEnv: '匯入環境變數',
envImportError: '第 {0} 行:{1}',
envInvalidAssignment: '應為有效的變數名稱=value',
envUnclosedQuote: '引號未閉合',
envUnexpectedText: '閉合引號後有多餘內容',
runtime: '執行環境',
default: '預設',
workDir: '工作目錄',
+5
View File
@@ -4074,6 +4074,11 @@ const message = {
editRuleConfirm: '将修改以下字段:{0}。提交后会立即应用并回读验证,是否继续?',
},
runtime: {
importEnv: '导入环境变量',
envImportError: '第 {0} 行:{1}',
envInvalidAssignment: '应为有效的变量名=value',
envUnclosedQuote: '引号未闭合',
envUnexpectedText: '闭合引号后存在多余内容',
runtime: '运行环境',
default: '默认',
workDir: '工作目录',
+5 -2
View File
@@ -40,7 +40,7 @@ type LocalTab = 'ollama' | 'vllm' | 'tensorrt' | 'downloader';
const route = useRoute();
const router = useRouter();
const { isFxplay } = useGlobalStore();
const { isFxplay, isProductPro } = useGlobalStore();
const tabLabels: Record<LocalTab, string> = {
ollama: 'Ollama',
@@ -66,7 +66,10 @@ const currentTab = computed<LocalTab>(() => {
if (tab === 'vllm' || tab === 'downloader' || (tab === 'tensorrt' && isFxplay.value)) {
return tab;
}
return 'ollama';
if (tab === 'tensorrt') {
return 'ollama';
}
return isProductPro.value ? 'vllm' : 'ollama';
});
const currentComponent = computed(() => {
@@ -11,7 +11,7 @@
<div class="mt-1">=</div>
</el-col>
<el-col :span="7">
<el-form-item :prop="`environments.${index}.value`" :rules="rules.value">
<el-form-item :prop="`environments.${index}.value`">
<el-input v-model="env.value" :placeholder="$t('runtime.envValue')" />
</el-form-item>
</el-col>
@@ -23,19 +23,59 @@
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="4">
<el-button v-permission @click="addEnv">{{ $t('commons.button.add') }}</el-button>
</el-col>
</el-row>
<div class="flex flex-wrap gap-2">
<el-button v-permission @click="addEnv">{{ $t('commons.button.add') }}</el-button>
<el-button v-permission icon="Upload" @click="openImport">{{ $t('runtime.importEnv') }}</el-button>
</div>
</div>
<el-dialog
v-model="importVisible"
:title="$t('runtime.importEnv')"
width="min(680px, 90vw)"
append-to-body
destroy-on-close
:close-on-click-modal="false"
@closed="importText = ''"
>
<el-input
v-model="importText"
type="textarea"
:rows="8"
:aria-label="$t('runtime.environment')"
placeholder="KEY=value"
spellcheck="false"
/>
<el-alert
v-if="parsed.error"
class="mt-3"
type="error"
:closable="false"
:title="$t('runtime.envImportError', [parsed.error.line, $t('runtime.' + parsed.error.reason)])"
/>
<el-table v-else-if="parsed.entries.length" :data="parsed.entries" max-height="260" class="mt-3">
<el-table-column prop="key" :label="$t('runtime.envKey')" min-width="120" show-overflow-tooltip />
<el-table-column prop="value" :label="$t('runtime.envValue')" min-width="180" show-overflow-tooltip />
</el-table>
<template #footer>
<el-button @click="importVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button
v-permission
type="primary"
:disabled="!!parsed.error || !parsed.entries.length"
@click="importEnv"
>
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import { computed, reactive, ref } from 'vue';
import { FormRules } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { Runtime } from '@/api/interface/runtime';
import { mergeEnvironments, parseEnvironment } from '@/utils/runtime-environment';
const props = defineProps({
environments: {
@@ -50,6 +90,22 @@ const props = defineProps({
const { showHelper } = props;
const importVisible = ref(false);
const importText = ref('');
const parsed = computed(() => parseEnvironment(importText.value));
const openImport = () => {
importText.value = '';
importVisible.value = true;
};
const importEnv = () => {
if (parsed.value.error || !parsed.value.entries.length) return;
mergeEnvironments(props.environments, parsed.value.entries);
importVisible.value = false;
importText.value = '';
};
const rules = reactive<FormRules>({
value: [Rules.requiredInput],
});