| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937 |
- <template>
- <div class="llm-sku-create-form">
- <a-form :form="form.fc" v-bind="formItemLayout">
- <a-form-item :label="$t('aice.project')">
- <domain-project
- v-if="!isEditMode"
- :fc="form.fc"
- :fd="form.fd"
- :form-layout="formItemLayout"
- :decorators="{ project: decorators.project, domain: decorators.domain }" />
- <a-label v-if="isEditMode">{{ projectName }}</a-label>
- </a-form-item>
- <a-form-item :label="$t('common.name')">
- <a-input v-decorator="decorators.name" v-if="!isEditMode" />
- <template v-slot:extra v-if="!isEditMode">
- <name-repeated res="llm_skus" :name="form.fd.name" :default-text="$t('aice.name_repeat_extra')" />
- </template>
- <a-label v-if="isEditMode">{{ modelName }}</a-label>
- </a-form-item>
- <a-form-item :label="$t('aice.llm_type')">
- <a-radio-group
- v-if="!isEditMode"
- class="llm-type-picker"
- button-style="solid"
- v-decorator="decorators.llm_type">
- <a-radio-button v-for="opt in llmTypeOptions" :key="opt.id" :value="opt.id">
- {{ opt.name }}
- </a-radio-button>
- </a-radio-group>
- <a-label v-else>{{ llmTypeName }}</a-label>
- </a-form-item>
- <template v-if="form.fd.llm_type === 'dify'">
- <a-form-item :label="$t('aice.dify_api_image')">
- <base-select
- v-decorator="decorators.dify_api_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'dify_api_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.dify_api_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.dify_plugin_image')">
- <base-select
- v-decorator="decorators.dify_plugin_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'dify_plugin_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.dify_plugin_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.dify_sandbox_image')">
- <base-select
- v-decorator="decorators.dify_sandbox_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'dify_sandbox_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.dify_sandbox_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.dify_ssrf_image')">
- <base-select
- v-decorator="decorators.dify_ssrf_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'dify_ssr_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.dify_ssr_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.dify_weaviate_image')">
- <base-select
- v-decorator="decorators.dify_weaviate_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'dify_weaviate_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.dify_weaviate_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.dify_web_image')">
- <base-select
- v-decorator="decorators.dify_web_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'dify_web_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.dify_web_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.nginx_image')">
- <base-select
- v-decorator="decorators.nginx_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'nginx_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.nginx_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.postgres_image')">
- <base-select
- v-decorator="decorators.postgres_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'postgres_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.postgres_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.redis_image')">
- <base-select
- v-decorator="decorators.redis_image_id"
- resource="llm_images"
- :params="{ ...appImageParams, $t: 'redis_image' }"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.redis_image')]) }" />
- </a-form-item>
- </template>
- <a-form-item v-else :label="$t('aice.llm_image')">
- <base-select
- v-decorator="decorators.llm_image_id"
- resource="llm_images"
- :params="appImageParams"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.llm_image')]) }" />
- </a-form-item>
- <a-form-item :label="$t('aice.cpu')">
- <a-input-number
- v-decorator="decorators.cpu"
- :min="2"
- :step="2"
- :precision="0" /> {{ $t('aice.cpu.unit') }}
- </a-form-item>
- <a-form-item :label="$t('aice.memory')">
- <a-input-number
- v-decorator="decorators.memory"
- :min="2"
- :step="2"
- :precision="0" /> GB
- </a-form-item>
- <a-form-item :label="$t('aice.disk')">
- <a-input-number
- v-decorator="decorators.volume_size"
- :min="10"
- :step="32"
- :precision="0" /> GB
- </a-form-item>
- <a-form-item :label="$t('aice.bandwidth')">
- <a-input-number
- v-decorator="decorators.bandwidth"
- :min="1"
- :max="10000"
- :step="1"
- :precision="0" /> MB
- </a-form-item>
- <template v-for="field in currentTypeFields">
- <a-form-item
- v-if="field.component === 'base-select'"
- :key="field.fieldKey"
- :label="$t(field.label)">
- <base-select
- v-decorator="decorators[field.fieldKey]"
- v-bind="getBaseSelectProps(field)" />
- </a-form-item>
- <a-form-item v-else-if="field.component === 'input-number'" :key="field.fieldKey" :label="$t(field.label)">
- <a-input-number
- v-decorator="decorators[field.fieldKey]"
- v-bind="field.props" />
- <template v-if="field.suffixKey">{{ $t(field.suffixKey) }}</template>
- <template v-else-if="field.suffix">{{ field.suffix }}</template>
- </a-form-item>
- <a-form-item
- v-else-if="field.component === 'input'"
- :key="field.fieldKey"
- :label="$t(field.label)">
- <a-input
- v-decorator="decorators[field.fieldKey]"
- :placeholder="field.props && field.props.placeholderKey ? $t('common.tips.input', [$t(field.props.placeholderKey)]) : ''" />
- </a-form-item>
- <a-form-item
- v-else-if="field.component === 'customized-args'"
- :key="field.fieldKey"
- :label="$t('aice.customized_args')">
- <a-row v-for="item in customizedArgsRows" :key="item.key" :gutter="4">
- <a-col :span="11">
- <a-form-item>
- <a-input
- v-decorator="decorators.customized_args.argKey(item.key)"
- :placeholder="$t('common.tips.input', [$t('aice.customized_arg_key')])" />
- </a-form-item>
- </a-col>
- <a-col :span="11">
- <a-form-item>
- <a-input
- v-decorator="decorators.customized_args.argValue(item.key)"
- :placeholder="$t('common.tips.input', [$t('aice.customized_arg_value')])" />
- </a-form-item>
- </a-col>
- <a-col :span="2">
- <a-button shape="circle" icon="minus" size="small" @click="delCustomizedArg(item)" class="mt-2 ml-2" />
- </a-col>
- </a-row>
- <a-row>
- <a-col>
- <div class="d-flex align-items-center">
- <a-button type="primary" shape="circle" icon="plus" size="small" @click="addCustomizedArg" />
- <a-button type="link" @click="addCustomizedArg">{{ $t('aice.add_customized_arg') }}</a-button>
- </div>
- </a-col>
- </a-row>
- </a-form-item>
- </template>
- <!-- 暂时隐藏 Agent 个性化配置,恢复时将 showAgentPersonalization 改为 true -->
- <a-divider
- v-if="form.fd.llm_type === 'openclaw' && showAgentPersonalization"
- key="section-agent-personalization"
- orientation="left"
- class="openclaw-section-divider">
- {{ $t('aice.openclaw.section.agent_personalization') }}
- </a-divider>
- <a-form-item
- v-if="form.fd.llm_type === 'openclaw' && showAgentPersonalization"
- :label="$t('aice.openclaw.workspace_templates')"
- :extra="$t('aice.openclaw.workspace_templates_tip')">
- <div class="openclaw-template-item">
- <div class="openclaw-template-title">AGENTS.md</div>
- <a-textarea
- v-decorator="decorators.openclaw_agents_md"
- :auto-size="{ minRows: 3, maxRows: 10 }"
- :placeholder="'AGENTS.md'" />
- </div>
- <div class="openclaw-template-item">
- <div class="openclaw-template-title">SOUL.md</div>
- <a-textarea
- v-decorator="decorators.openclaw_soul_md"
- :auto-size="{ minRows: 3, maxRows: 10 }"
- :placeholder="'SOUL.md'" />
- </div>
- <div class="openclaw-template-item mb-0">
- <div class="openclaw-template-title">USER.md</div>
- <a-textarea
- v-decorator="decorators.openclaw_user_md"
- :auto-size="{ minRows: 3, maxRows: 10 }"
- :placeholder="'USER.md'" />
- </div>
- </a-form-item>
- <a-form-item :label="$t('aice.container_port_mapping')" :extra="$t('aice.container_port_mapping_tip')">
- <a-row v-for="item in portMappings" :key="item.key" :gutter="4">
- <a-col :span="11">
- <a-form-item>
- <base-select
- v-decorator="decorators.port_mapppings.protocol(item.key)"
- :options="dict.protocolArr" />
- </a-form-item>
- </a-col>
- <a-col :span="11">
- <a-form-item>
- <a-input
- v-decorator="decorators.port_mapppings.container_port(item.key)"
- :placeholder="$t('common.tips.input', [$t('aice.container_port')])" />
- </a-form-item>
- </a-col>
- <a-col :span="2">
- <a-button shape="circle" icon="minus" size="small" @click="del(item)" class="mt-2 ml-2" />
- </a-col>
- </a-row>
- <a-row>
- <a-col>
- <div class="d-flex align-items-center">
- <a-button type="primary" shape="circle" icon="plus" size="small" @click="add" />
- <a-button type="link" @click="add">{{$t('aice.add_port_mapping')}}</a-button>
- </div>
- </a-col>
- </a-row>
- </a-form-item>
- </a-form>
- </div>
- </template>
- <script>
- import { mapGetters } from 'vuex'
- import WindowsMixin from '@/mixins/windows'
- import DomainProject from '@/sections/DomainProject'
- import NameRepeated from '@/sections/NameRepeated'
- import { isRequired } from '@/utils/validate'
- import { uuid } from '@/utils/utils'
- import { dict } from '../constant'
- import { LLM_TYPE_OPTIONS, LLM_TYPE_FORM_CONFIG, getParamsForType } from '../llmTypeConfig'
- const getInitVal = (list, key, property) => {
- const target = list.filter(item => item.key === key)
- return target.length ? target[0][property] : ''
- }
- export default {
- name: 'LlmSkuCreateForm',
- components: {
- DomainProject,
- NameRepeated,
- },
- mixins: [WindowsMixin],
- props: {
- mode: {
- type: String,
- default: 'create',
- validator: v => ['create', 'edit'].includes(v),
- },
- editData: {
- type: Object,
- default: () => ({}),
- },
- onManager: {
- type: Function,
- default: null,
- },
- },
- data () {
- const data = this.mode === 'edit' && this.editData ? this.editData : {}
- const isApplyType = this.$route.path.includes('app-llm')
- const llmTypeOptions = isApplyType ? LLM_TYPE_OPTIONS.filter(opt => opt.id !== 'vllm' && opt.id !== 'ollama') : LLM_TYPE_OPTIONS.filter(opt => opt.id === 'vllm' || opt.id === 'ollama')
- const {
- domain_id,
- project_domain,
- tenant_id,
- tenant,
- name,
- llm_type: rowLlmType,
- cpu = 2,
- memory = 2048,
- volume = { size: 10240, storage_type: 'local', template_id: undefined },
- image_id,
- envs = [],
- llm_image_id,
- devices,
- mounted_models = [],
- mounted_apps = [],
- llm_spec: llmSpec,
- openclaw: openclawConf = {},
- port_mappings = [],
- preferred_model: rowPreferredModel,
- } = data
- const preferredModelInit = rowPreferredModel != null && rowPreferredModel !== ''
- ? String(rowPreferredModel)
- : (llmSpec?.vllm?.preferred_model != null ? String(llmSpec.vllm.preferred_model) : '')
- const {
- dify_api_image_id,
- dify_plugin_image_id,
- dify_sandbox_image_id,
- dify_ssrf_image_id,
- dify_weaviate_image_id,
- dify_web_image_id,
- nginx_image_id,
- postgres_image_id,
- redis_image_id,
- } = llmSpec?.dify || {}
- // openclaw 优先来自 llm_spec.openclaw,其次兼容旧的顶层 openclaw 字段
- let openclawConfObj = llmSpec?.openclaw ?? openclawConf
- if (typeof openclawConfObj === 'string') {
- try { openclawConfObj = JSON.parse(openclawConfObj) } catch (e) { openclawConfObj = {} }
- }
- if (!openclawConfObj || typeof openclawConfObj !== 'object') openclawConfObj = {}
- let openclawWorkspaceTemplates = openclawConfObj?.workspace_templates
- if (typeof openclawWorkspaceTemplates === 'string') {
- try { openclawWorkspaceTemplates = JSON.parse(openclawWorkspaceTemplates) } catch (e) { openclawWorkspaceTemplates = {} }
- }
- if (!openclawWorkspaceTemplates || typeof openclawWorkspaceTemplates !== 'object') openclawWorkspaceTemplates = {}
- const envVars = (envs || []).map(item => ({ env_key: item.key, env_value: item.value, key: uuid() }))
- const defaultLlmType = (llmTypeOptions[0] && llmTypeOptions[0].id) || (isApplyType ? 'openclaw' : 'ollama')
- const portMappings = port_mappings.map(item => ({ ...item, key: uuid() }))
- let customizedArgsSource = data.customized_args ?? llmSpec?.vllm?.customized_args ?? []
- if (!Array.isArray(customizedArgsSource)) customizedArgsSource = []
- const customizedArgsRows = customizedArgsSource.map((row) => ({
- key: uuid(),
- argKey: row != null && row.key != null ? String(row.key) : '',
- argValue: row != null && row.value != null ? String(row.value) : '',
- }))
- return {
- loading: false,
- // 暂时隐藏 openclaw 创建/编辑时的「Agent 个性化配置」区块,恢复时改为 true
- showAgentPersonalization: false,
- isApplyType,
- llmTypeOptions: llmTypeOptions.map(opt => ({ id: opt.id, name: this.$t(opt.name) })),
- dict,
- portMappings,
- customizedArgsRows,
- form: {
- fc: this.$form.createForm(this, {
- onValuesChange: (props, values) => {
- Object.keys(values).forEach((key) => {
- this.$set(this.form.fd, key, values[key])
- })
- },
- }),
- fd: {
- // 须与 decorators.llm_type.initialValue 一致,否则 currentTypeFields / v-if 与真实选中类型不同步,初始化不展示类型字段
- llm_type: rowLlmType || defaultLlmType,
- },
- },
- decorators: {
- domain: [
- 'domain',
- {
- initialValue: { key: domain_id, label: project_domain },
- rules: [
- { validator: isRequired(), message: this.$t('rules.domain'), trigger: 'change' },
- ],
- },
- ],
- project: [
- 'project',
- {
- initialValue: { key: tenant_id, label: tenant },
- rules: [
- { validator: isRequired(), message: this.$t('rules.project'), trigger: 'change' },
- ],
- },
- ],
- name: [
- 'name',
- {
- initialValue: name,
- rules: [
- { required: true, message: this.$t('common.tips.input', [this.$t('common.name')]) },
- ],
- },
- ],
- llm_image_id: [
- 'llm_image_id',
- {
- initialValue: llm_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.llm_image')]) },
- ],
- },
- ],
- dify_api_image_id: [
- 'dify_api_image_id',
- {
- initialValue: dify_api_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.dify_api_image')]) },
- ],
- },
- ],
- dify_plugin_image_id: [
- 'dify_plugin_image_id',
- {
- initialValue: dify_plugin_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.dify_plugin_image')]) },
- ],
- },
- ],
- dify_sandbox_image_id: [
- 'dify_sandbox_image_id',
- {
- initialValue: dify_sandbox_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.dify_sandbox_image')]) },
- ],
- },
- ],
- dify_ssrf_image_id: [
- 'dify_ssrf_image_id',
- {
- initialValue: dify_ssrf_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.dify_ssr_image')]) },
- ],
- },
- ],
- llm_type: [
- 'llm_type',
- {
- initialValue: rowLlmType || defaultLlmType,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.llm_type')]) },
- ],
- },
- ],
- dify_weaviate_image_id: [
- 'dify_weaviate_image_id',
- {
- initialValue: dify_weaviate_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.dify_weaviate_image')]) },
- ],
- },
- ],
- dify_web_image_id: [
- 'dify_web_image_id',
- {
- initialValue: dify_web_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.dify_web_image')]) },
- ],
- },
- ],
- nginx_image_id: [
- 'nginx_image_id',
- {
- initialValue: nginx_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.nginx_image')]) },
- ],
- },
- ],
- postgres_image_id: [
- 'postgres_image_id',
- {
- initialValue: postgres_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.postgres_image')]) },
- ],
- },
- ],
- redis_image_id: [
- 'redis_image_id',
- {
- initialValue: redis_image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.redis_image')]) },
- ],
- },
- ],
- openclaw_agents_md: [
- 'openclaw_agents_md',
- {
- initialValue: openclawWorkspaceTemplates['AGENTS.md'] || openclawWorkspaceTemplates.agents_md,
- },
- ],
- openclaw_soul_md: [
- 'openclaw_soul_md',
- {
- initialValue: openclawWorkspaceTemplates['SOUL.md'] || openclawWorkspaceTemplates.soul_md,
- },
- ],
- openclaw_user_md: [
- 'openclaw_user_md',
- {
- initialValue: openclawWorkspaceTemplates['USER.md'] || openclawWorkspaceTemplates.user_md,
- },
- ],
- mounted_models: [
- 'mounted_models',
- {
- initialValue: mounted_models.map(v => v.id),
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.model')]) },
- ],
- },
- ],
- preferred_model: [
- 'preferred_model',
- {
- initialValue: preferredModelInit,
- },
- ],
- bandwidth: [
- 'bandwidth',
- {
- initialValue: 100,
- rules: [
- { required: true, message: this.$t('common.tips.input', [this.$t('aice.bandwidth')]) },
- ],
- },
- ],
- cpu: [
- 'cpu',
- {
- initialValue: cpu,
- rules: [
- { required: true, message: this.$t('common.tips.input', ['CPU']) },
- ],
- },
- ],
- memory: [
- 'memory',
- {
- initialValue: memory / 1024,
- rules: [
- { required: true, message: this.$t('common.tips.input', [this.$t('aice.memory')]) },
- ],
- },
- ],
- volume_size: [
- 'volume_size',
- {
- initialValue: volume.size / 1024,
- rules: [
- { required: true, message: this.$t('common.tips.input', [this.$t('aice.disk')]) },
- ],
- },
- ],
- phone_image: [
- 'phone_image',
- {
- initialValue: image_id,
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.image')]) },
- ],
- },
- ],
- request_sync_image: [
- 'request_sync_image',
- {
- initialValue: false,
- },
- ],
- device: [
- 'device',
- {
- initialValue: devices && devices.length > 0 ? devices.map(v => v.model) : [],
- rules: [
- { required: true, message: this.$t('common.tips.select', [this.$t('aice.devices')]) },
- ],
- },
- ],
- mounted_apps: [
- 'mounted_apps',
- {
- initialValue: mounted_apps || [],
- },
- ],
- port_mapppings: {
- protocol: i => [
- `protocol[${i}]`,
- {
- initialValue: getInitVal(portMappings, i, 'protocol'),
- },
- ],
- container_port: i => [
- `container_port[${i}]`,
- {
- initialValue: getInitVal(portMappings, i, 'container_port'),
- rules: [
- { type: 'number', min: 0, max: 65535, message: this.$t('aice.container_port.message'), trigger: 'blur', transform: (v) => parseInt(v) },
- ],
- },
- ],
- },
- customized_args: {
- argKey: rowKey => [
- `customized_arg_key[${rowKey}]`,
- { initialValue: getInitVal(customizedArgsRows, rowKey, 'argKey') },
- ],
- argValue: rowKey => [
- `customized_arg_value[${rowKey}]`,
- { initialValue: getInitVal(customizedArgsRows, rowKey, 'argValue') },
- ],
- },
- },
- formItemLayout: {
- wrapperCol: { span: 20 },
- labelCol: { span: 4 },
- },
- envVars,
- projectName: (project_domain != null && tenant != null) ? `${project_domain}/${tenant}` : '',
- modelName: name || '',
- audioImageParams: { limit: 20, scope: this.$store.getters.scope, $t: 2 },
- streamImageParams: { limit: 20, scope: this.$store.getters.scope, $t: 3 },
- }
- },
- computed: {
- ...mapGetters(['userInfo']),
- isEditMode () {
- return this.mode === 'edit'
- },
- llmTypeName () {
- const cur = this.form?.fd?.llm_type
- const opt = this.llmTypeOptions.find(o => o.id === cur)
- return (opt && opt.name) || cur || '-'
- },
- currentTypeFields () {
- const type = this.form.fd.llm_type || 'ollama'
- const base = LLM_TYPE_FORM_CONFIG[type] || []
- if (type !== 'vllm') return base
- const out = []
- base.forEach((f) => {
- out.push(f)
- if (f.fieldKey === 'preferred_model') {
- out.push({ fieldKey: '__customized_args__', component: 'customized-args' })
- }
- })
- return out
- },
- appImageParams () {
- return {
- limit: 20,
- scope: this.$store.getters.scope,
- $t: 1,
- ...getParamsForType(this.form.fd.llm_type),
- }
- },
- mountedModelParams () {
- return {
- limit: 20,
- scope: this.$store.getters.scope,
- ...getParamsForType(this.form.fd.llm_type),
- }
- },
- credentialParams () {
- const filter = ['type.equals(container_secret)']
- if (this.$store.getters.scope === 'project') {
- const uid = this.$store.getters.userInfo?.id
- if (uid) filter.push(`user_id.equals(${uid})`)
- }
- return {
- scope: this.$store.getters.scope,
- filter,
- }
- },
- specList () {
- const list = Object.values(this.$store.getters.capability?.pci_model_types || {}).filter(item => item.hypervisor === 'pod')
- return list.map(item => ({ key: item.model, label: item.model }))
- },
- },
- watch: {
- 'form.fd.llm_type' (val, oldVal) {
- if (val === oldVal) return
- if (oldVal === 'openclaw' && val !== 'openclaw') {
- this.form.fc.setFieldsValue({
- openclaw_agents_md: undefined,
- openclaw_soul_md: undefined,
- openclaw_user_md: undefined,
- })
- }
- },
- },
- methods: {
- add () {
- this.portMappings.push({ key: uuid() })
- },
- del (item) {
- const idx = this.portMappings.findIndex(v => v.key === item.key)
- this.portMappings.splice(idx, 1)
- },
- addCustomizedArg () {
- this.customizedArgsRows.push({ key: uuid(), argKey: '', argValue: '' })
- },
- delCustomizedArg (item) {
- const idx = this.customizedArgsRows.findIndex(v => v.key === item.key)
- if (idx >= 0) this.customizedArgsRows.splice(idx, 1)
- },
- handleCancel () {
- this.$emit('cancel')
- },
- getBaseSelectProps (field) {
- const { props } = field
- const selectProps = {
- placeholder: this.$t('common.tips.select', [this.$t(props.placeholderKey)]),
- ...(props.selectProps || {}),
- }
- if (props.resource) {
- const paramsKeyMap = {
- appImageParams: 'appImageParams',
- mountedModelParams: 'mountedModelParams',
- credentialParams: 'credentialParams',
- }
- const paramsKey = paramsKeyMap[props.paramsKey] || 'mountedModelParams'
- return {
- resource: props.resource,
- params: this[paramsKey],
- selectProps,
- remote: props.remote || false,
- }
- }
- const options = props.optionsKey ? this[props.optionsKey] : []
- return { options, selectProps }
- },
- async handleConfirm () {
- this.loading = true
- try {
- const manager = new this.$Manager('llm_skus')
- const values = await this.form.fc.validateFields()
- const {
- project,
- name,
- llm_type,
- phone_image,
- request_sync_image,
- llm_image_id,
- dify_api_image_id,
- dify_plugin_image_id,
- dify_sandbox_image_id,
- dify_ssrf_image_id,
- dify_weaviate_image_id,
- dify_web_image_id,
- nginx_image_id,
- postgres_image_id,
- redis_image_id,
- cpu,
- memory,
- volume_size,
- bandwidth,
- protocol,
- container_port,
- customized_arg_key,
- customized_arg_value,
- } = values
- const effectiveLlmType = this.isEditMode ? this.form.fd.llm_type : llm_type
- const typeFields = this.currentTypeFields
- const typeFieldKeys = typeFields.filter(f => f.fieldKey !== '__customized_args__').map(f => f.fieldKey)
- const pickTypeValues = {}
- typeFieldKeys.forEach(key => {
- if (values[key] !== undefined) pickTypeValues[key] = values[key]
- })
- const volumes = [{
- containers: this.mode === 'edit' && this.editData && this.editData.volumes && this.editData.volumes[0] ? this.editData.volumes[0].containers : {
- 1: { mount_path: '/etc/wolf', sub_directory: 'wolf' },
- 2: {
- mount_path: '/home/retro',
- sub_directory: 'home',
- overlay: {
- lower_dir: ['/opt/steam-data/steam', '/opt/steam-data/games'],
- use_disk_image: false,
- },
- },
- },
- size_mb: (volume_size ?? 10) * 1024,
- }]
- const port_mappings = this.portMappings.map(item => {
- return {
- protocol: protocol[item.key],
- container_port: container_port[item.key],
- }
- })
- const data = {
- name,
- llm_image_id,
- image_id: phone_image,
- cpu,
- memory: (memory ?? 2) * 1024,
- bandwidth: bandwidth ?? 100,
- volumes,
- disk_size: volumes[0].size_mb,
- app_type: 'steam',
- }
- if (port_mappings.length > 0) {
- data.port_mappings = port_mappings
- }
- if (!this.isEditMode) {
- data.llm_type = effectiveLlmType
- // 默认填入 llm_sku,空对象即可,如 openclaw => llm_sku: { openclaw: {} }
- data.llm_sku = { [effectiveLlmType]: {} }
- }
- typeFieldKeys.forEach(key => {
- const v = pickTypeValues[key]
- if (v === undefined) return
- if (effectiveLlmType === 'vllm' && key === 'preferred_model') return
- if (key === 'device') {
- data.devices = v.map(k => ({ model: k }))
- } else {
- data[key] = v
- }
- })
- if (effectiveLlmType === 'vllm') {
- const vllm = {}
- const pm = pickTypeValues.preferred_model
- const preferredStr = pm != null ? String(pm).trim() : ''
- if (preferredStr !== '') {
- vllm.preferred_model = preferredStr
- }
- const customized_args = this.customizedArgsRows
- .map((item) => ({
- key: customized_arg_key?.[item.key] != null ? String(customized_arg_key[item.key]).trim() : '',
- value: customized_arg_value?.[item.key] != null ? String(customized_arg_value[item.key]).trim() : '',
- }))
- .filter((row) => row.key !== '' || row.value !== '')
- if (customized_args.length > 0) {
- vllm.customized_args = customized_args
- }
- if (Object.keys(vllm).length > 0) {
- data.llm_spec = { vllm }
- }
- }
- if (effectiveLlmType === 'openclaw') {
- const workspace_templates = {}
- const agents = (values.openclaw_agents_md || '').trim()
- const soul = (values.openclaw_soul_md || '').trim()
- const user = (values.openclaw_user_md || '').trim()
- if (agents) workspace_templates.agents_md = agents
- if (soul) workspace_templates.soul_md = soul
- if (user) workspace_templates.user_md = user
- if (Object.keys(workspace_templates).length > 0) {
- data.llm_spec = { openclaw: { workspace_templates } }
- }
- }
- if (effectiveLlmType === 'dify') {
- data.llm_spec = {
- dify: {
- dify_api_image_id: dify_api_image_id,
- dify_plugin_image_id: dify_plugin_image_id,
- dify_sandbox_image_id: dify_sandbox_image_id,
- dify_ssrf_image_id: dify_ssrf_image_id,
- dify_weaviate_image_id: dify_weaviate_image_id,
- dify_web_image_id: dify_web_image_id,
- nginx_image_id: nginx_image_id,
- postgres_image_id: postgres_image_id,
- redis_image_id: redis_image_id,
- },
- }
- delete data.llm_image_id
- }
- if (this.mode === 'edit' && this.onManager && this.editData) {
- if (request_sync_image) data.request_sync_image = true
- await this.onManager('update', {
- id: this.editData.id,
- managerArgs: { data },
- })
- } else {
- data.generate_name = name
- data.project_id = project?.key || this.userInfo.projectId
- await manager.create({ data })
- }
- this.$message.success(this.$t('common.success'))
- this.$emit('success')
- } catch (error) {
- throw error
- } finally {
- this.loading = false
- }
- },
- },
- }
- </script>
- <style scoped>
- .llm-sku-create-form .form-footer {
- margin-top: 24px;
- padding-top: 16px;
- border-top: 1px solid #e8e8e8;
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- }
- .openclaw-channel-tabs { margin-top: 8px; }
- .llm-type-picker ::v-deep .ant-radio-button-wrapper {
- height: 36px;
- line-height: 34px;
- border-radius: 4px;
- margin-right: 8px;
- margin-bottom: 8px;
- }
- .llm-type-picker ::v-deep .ant-radio-button-wrapper:first-child { border-radius: 4px; }
- .llm-type-picker ::v-deep .ant-radio-button-wrapper:last-child { border-radius: 4px; }
- .openclaw-template-item { margin-bottom: 12px; }
- .openclaw-template-title { font-weight: 500; margin-bottom: 6px; }
- .openclaw-section-divider { margin-top: 20px; }
- .openclaw-credential-intro { color: rgba(0, 0, 0, 0.65); font-size: 13px; }
- .openclaw-credential-mode { margin-bottom: 0; }
- .openclaw-configured-providers { font-weight: 500; }
- .openclaw-new-blob-section-title { font-weight: 600; margin-bottom: 8px; font-size: 13px; }
- .openclaw-new-blob-row ::v-deep .ant-form-item-label { padding-bottom: 4px; }
- .openclaw-filter-empty { padding: 12px 0; font-size: 13px; }
- .openclaw-tab-with-close { display: inline-flex; align-items: center; gap: 6px; }
- .openclaw-tab-close { font-size: 12px; cursor: pointer; opacity: 0.6; }
- .openclaw-tab-close:hover { opacity: 1; }
- </style>
|