| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212 |
- <template>
- <base-dialog :width="900" @cancel="cancelDialog">
- <div slot="header">{{ $t('aice.llm_spec_update') }}</div>
- <div slot="body">
- <a-form :form="form.fc" hideRequiredMark v-bind="formItemLayout">
- <template v-if="isOpenclaw">
- <a-divider orientation="left" class="openclaw-section-divider">{{ $t('aice.openclaw.section.ai_providers') }}</a-divider>
- <a-form-item :label="$t('aice.openclaw.provider_filter')" :extra="$t('aice.openclaw.provider_select_tip')">
- <a-select
- v-model="openclawSelectedProviders"
- mode="multiple"
- :placeholder="$t('aice.openclaw.provider_filter_placeholder')"
- allow-clear
- show-search
- :filter-option="filterProviderOption"
- style="width: 100%; max-width: 400px;">
- <a-select-option v-for="opt in providerOptionsForSelect" :key="opt.value" :value="opt.value">
- {{ opt.label }}
- </a-select-option>
- </a-select>
- </a-form-item>
- <a-tabs
- v-if="providerTabList.length > 0"
- :activeKey="openclawProviderActiveKey"
- type="card"
- class="openclaw-provider-tabs"
- :animated="false"
- @change="openclawProviderActiveTab = $event">
- <a-tab-pane
- v-for="item in providerTabList"
- :key="item.key"
- :forceRender="true">
- <span slot="tab" class="openclaw-tab-with-close">
- {{ $t(item.labelKey) }}{{ item.required ? ' *' : '' }}
- <a-icon type="close" class="openclaw-tab-close" @click.prevent.stop="closeProviderTab(item.key)" />
- </span>
- <a-form-item :label="$t('aice.openclaw.credential_mode.label')">
- <a-radio-group
- :value="openclawProviderCredentialMode[item.key] || 'new'"
- @change="e => $set(openclawProviderCredentialMode, item.key, e.target.value)">
- <a-radio value="new">{{ $t('aice.openclaw.credential_mode.new') }}</a-radio>
- <a-radio value="existing">{{ $t('aice.openclaw.credential_mode.existing') }}</a-radio>
- </a-radio-group>
- </a-form-item>
- <template v-if="(openclawProviderCredentialMode[item.key] || 'new') === 'existing'">
- <a-form-item :label="$t('aice.container_secret')">
- <base-select
- ref="credentialSelects"
- :ref-in-for="true"
- v-model="openclawProviderCredentialId[item.key]"
- resource="credentials"
- :params="credentialParamsForProvider(item.key)"
- :label-format="credentialLabelFormat"
- :extra-opts="credentialExtraOpts(openclawProviderCredentialId[item.key])"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.container_secret')]), optionLabelProp: 'label' }"
- @change="val => onProviderCredentialChange(item.key, val)" />
- </a-form-item>
- <a-form-item :label="$t('aice.container_secret.export_keys')" :extra="$t('aice.container_secret.export_keys_tip')">
- <a-checkbox-group
- :value="openclawProviderExportKeys[item.key] || []"
- @change="val => $set(openclawProviderExportKeys, item.key, val)">
- <a-checkbox v-for="k in (openclawProviderBlobKeys[item.key] || [])" :key="k" :value="k">{{ k }}</a-checkbox>
- </a-checkbox-group>
- </a-form-item>
- </template>
- <template v-else>
- <div class="openclaw-auto-credential-name text-color-secondary mb-2">
- {{ $t('aice.openclaw.new_credential_name') }}:{{ genCredentialName({ llmName: instanceName, usage: 'provider', key: providerShortName(item.key) }) }}
- </div>
- <div class="openclaw-new-blob-section">
- <div v-for="v in item.vars" :key="v.envKey" class="openclaw-new-blob-row mb-2">
- <a-form-item
- :label="v.envKey"
- :required="item.required || v.required"
- :extra="((item.required || v.required) ? $t('aice.openclaw.required_hint') + ' ' : '') + ($te(v.descriptionKey) ? $t(v.descriptionKey) : '')">
- <a-input-password
- v-if="isSecretEnvKey(v.envKey)"
- :value="(openclawProviderBlob[item.key] || {})[v.envKey]"
- :placeholder="v.envKey"
- allow-clear
- @change="e => $set(openclawProviderBlob[item.key], v.envKey, e.target.value)" />
- <div
- v-else-if="v.component === 'a-select'"
- class="d-flex align-items-center openclaw-primary-model-select-row">
- <a-select
- class="flex-grow-1"
- style="min-width: 0"
- :value="(openclawProviderBlob[item.key] || {})[v.envKey] || undefined"
- :placeholder="openclawProviderAselectPlaceholder(v)"
- allow-clear
- show-search
- :filter-option="false"
- :loading="openclawPrimaryModelLoading"
- @dropdownVisibleChange="open => onOpenclawPrimaryModelDropdown(open, v, item.key)"
- @search="q => onOpenclawPrimaryModelSearch(q, v, item.key)"
- @change="val => $set(openclawProviderBlob[item.key], v.envKey, val)">
- <a-select-option
- v-for="opt in openclawPrimaryModelOptions"
- :key="String(opt.value)"
- :value="opt.value">
- {{ opt.label }}
- </a-select-option>
- </a-select>
- <a-icon
- type="sync"
- class="ml-2 primary-color flex-shrink-0"
- :spin="openclawPrimaryModelLoading"
- @click="refreshOpenclawPrimaryModel(v, item.key)" />
- </div>
- <a-input
- v-else
- :value="(openclawProviderBlob[item.key] || {})[v.envKey]"
- :placeholder="v.envKey"
- allow-clear
- @change="e => $set(openclawProviderBlob[item.key], v.envKey, e.target.value)" />
- </a-form-item>
- <a-form-item
- v-if="v.overrideUrlKey"
- :label="v.overrideUrlKey"
- :extra="($te('aice.openclaw.env.' + v.overrideUrlKey) ? $t('aice.openclaw.env.' + v.overrideUrlKey) : '') + ' ' + $t('aice.openclaw.override_url_optional')"
- class="openclaw-override-url mt-1">
- <a-input
- :value="(openclawProviderBlob[item.key] || {})[v.overrideUrlKey]"
- :placeholder="overrideUrlPlaceholder(v.overrideUrlKey)"
- allow-clear
- @change="e => $set(openclawProviderBlob[item.key], v.overrideUrlKey, e.target.value)" />
- </a-form-item>
- </div>
- </div>
- </template>
- </a-tab-pane>
- </a-tabs>
- <div v-else class="openclaw-filter-empty text-color-secondary">
- {{ openclawSelectedProviders.length === 0 ? $t('aice.openclaw.provider_select_first') : $t('aice.openclaw.provider_filter_empty') }}
- </div>
- <a-divider orientation="left" class="openclaw-section-divider">{{ $t('aice.openclaw.section.chat_channels') }}</a-divider>
- <a-form-item :label="$t('aice.openclaw.channels')" :extra="$t('aice.openclaw.channels_extra')">
- <a-select
- v-decorator="decorators.openclaw_channels"
- mode="multiple"
- :placeholder="$t('aice.openclaw.channel_select_placeholder')"
- allow-clear
- show-search
- :filter-option="filterChannelOption"
- style="width: 100%; max-width: 400px;">
- <a-select-option v-for="opt in channelOptionsForSelect" :key="opt.value" :value="opt.value">
- {{ opt.label }}
- </a-select-option>
- </a-select>
- </a-form-item>
- <template v-if="filteredChannelSections.length > 0">
- <div class="openclaw-channel-config-hint text-color-secondary mb-2">
- {{ $t('aice.openclaw.channel_config_hint') }}
- </div>
- <a-tabs
- :activeKey="openclawChannelActiveTab || (filteredChannelSections[0] && filteredChannelSections[0].sectionKey)"
- class="openclaw-channel-tabs"
- :animated="false"
- @change="openclawChannelActiveTab = $event">
- <a-tab-pane
- v-for="section in filteredChannelSections"
- :key="section.sectionKey"
- :forceRender="true">
- <span slot="tab" class="openclaw-tab-with-close">
- {{ $t(section.sectionLabelKey) }}
- <a-icon type="close" class="openclaw-tab-close" @click.prevent.stop="closeChannelTab(section.sectionKey)" />
- </span>
- <a-form-item :label="$t('aice.openclaw.credential_mode.label')">
- <a-radio-group
- :value="openclawChannelCredentialMode[section.sectionKey] || 'new'"
- @change="e => $set(openclawChannelCredentialMode, section.sectionKey, e.target.value)">
- <a-radio value="new">{{ $t('aice.openclaw.credential_mode.new') }}</a-radio>
- <a-radio value="existing">{{ $t('aice.openclaw.credential_mode.existing') }}</a-radio>
- </a-radio-group>
- </a-form-item>
- <template v-if="(openclawChannelCredentialMode[section.sectionKey] || 'new') === 'existing'">
- <a-form-item :label="$t('aice.container_secret')">
- <base-select
- ref="credentialSelects"
- :ref-in-for="true"
- v-model="openclawChannelCredentialId[section.sectionKey]"
- resource="credentials"
- :params="credentialParamsForChannel(section.sectionKey)"
- :label-format="credentialLabelFormat"
- :extra-opts="credentialExtraOpts(openclawChannelCredentialId[section.sectionKey])"
- :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.container_secret')]), optionLabelProp: 'label' }"
- @change="val => onChannelCredentialChange(section.sectionKey, val)" />
- </a-form-item>
- <a-form-item :label="$t('aice.container_secret.export_keys')" :extra="$t('aice.container_secret.export_keys_tip')">
- <a-checkbox-group
- :value="openclawChannelExportKeys[section.sectionKey] || []"
- @change="val => $set(openclawChannelExportKeys, section.sectionKey, val)">
- <a-checkbox
- v-for="k in sectionBasicEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || [])"
- :key="k"
- :value="k">{{ k }}</a-checkbox>
- <a-collapse :bordered="false" v-if="sectionAdvancedEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || []).length">
- <a-collapse-panel :header="$t('common.adv_config')" key="advanced">
- <a-checkbox
- v-for="k in sectionAdvancedEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || [])"
- :key="k"
- :value="k">{{ k }}</a-checkbox>
- </a-collapse-panel>
- </a-collapse>
- </a-checkbox-group>
- </a-form-item>
- </template>
- <template v-else>
- <div class="openclaw-auto-credential-name text-color-secondary mb-2">
- {{ $t('aice.openclaw.new_credential_name') }}:{{ genCredentialName({ llmName: instanceName, usage: 'channel', key: section.sectionKey }) }}
- </div>
- <div class="openclaw-new-blob-section">
- <div v-for="v in channelBasicVars(section.vars)" :key="v.envKey" class="openclaw-new-blob-row mb-2">
- <a-form-item
- :label="v.envKey"
- :required="v.required"
- :extra="($te(v.descriptionKey) ? $t(v.descriptionKey) : '') + ' ' + (v.required ? $t('aice.openclaw.required_hint') : $t('aice.openclaw.channel_var_optional'))">
- <template v-if="v.envKey === 'FEISHU_DOMAIN'">
- <a-radio-group
- :value="((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)">
- <a-radio value="feishu">feishu</a-radio>
- <a-radio value="lark">lark</a-radio>
- </a-radio-group>
- </template>
- <template v-else-if="['FEISHU_DM_POLICY', 'DISCORD_DM_POLICY', 'TELEGRAM_DM_POLICY'].includes(v.envKey)">
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'open'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : v.defaultValue)">
- open
- </a-checkbox>
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'pairing'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'pairing' : v.defaultValue)">
- pairing
- </a-checkbox>
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'allowlist'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : v.defaultValue)">
- allowlist
- </a-checkbox>
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'disabled'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : v.defaultValue)">
- disabled
- </a-checkbox>
- </template>
- <template v-else-if="['FEISHU_GROUP_POLICY', 'DISCORD_GROUP_POLICY', 'TELEGRAM_GROUP_POLICY'].includes(v.envKey)">
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'open'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : '')">
- open
- </a-checkbox>
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'allowlist'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : '')">
- allowlist
- </a-checkbox>
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'disabled'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : '')">
- disabled
- </a-checkbox>
- </template>
- <template v-else-if="v.envKey === 'FEISHU_TYPING_INDICATOR'">
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
- </template>
- <template v-else-if="v.envKey === 'FEISHU_RESOLVE_SENDER_NAMES'">
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
- </template>
- <template v-else>
- <a-input-password
- v-if="isSecretEnvKey(v.envKey)"
- :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
- :placeholder="v.defaultValue || v.envKey"
- allow-clear
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
- <a-input
- v-else
- :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
- :placeholder="v.defaultValue || v.envKey"
- allow-clear
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
- </template>
- </a-form-item>
- </div>
- <a-collapse :bordered="false" v-if="channelAdvancedVars(section.vars).length">
- <a-collapse-panel :header="$t('common.adv_config')" key="advanced">
- <div v-for="v in channelAdvancedVars(section.vars)" :key="v.envKey" class="openclaw-new-blob-row mb-2">
- <a-form-item
- :label="v.envKey"
- :required="v.required"
- :extra="($te(v.descriptionKey) ? $t(v.descriptionKey) : '') + ' ' + (v.required ? $t('aice.openclaw.required_hint') : $t('aice.openclaw.channel_var_optional'))">
- <template v-if="v.envKey === 'FEISHU_DOMAIN'">
- <a-radio-group
- :value="((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)">
- <a-radio value="feishu">feishu</a-radio>
- <a-radio value="lark">lark</a-radio>
- </a-radio-group>
- </template>
- <template v-else-if="['FEISHU_DM_POLICY', 'DISCORD_DM_POLICY', 'TELEGRAM_DM_POLICY'].includes(v.envKey)">
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'open'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : v.defaultValue)">
- open
- </a-checkbox>
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'pairing'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'pairing' : v.defaultValue)">
- pairing
- </a-checkbox>
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'allowlist'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : v.defaultValue)">
- allowlist
- </a-checkbox>
- <a-checkbox
- :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'disabled'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : v.defaultValue)">
- disabled
- </a-checkbox>
- </template>
- <template v-else-if="['FEISHU_GROUP_POLICY', 'DISCORD_GROUP_POLICY', 'TELEGRAM_GROUP_POLICY'].includes(v.envKey)">
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'open'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : '')">
- open
- </a-checkbox>
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'allowlist'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : '')">
- allowlist
- </a-checkbox>
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'disabled'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : '')">
- disabled
- </a-checkbox>
- </template>
- <template v-else-if="v.envKey === 'FEISHU_TYPING_INDICATOR'">
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
- </template>
- <template v-else-if="v.envKey === 'FEISHU_RESOLVE_SENDER_NAMES'">
- <a-checkbox
- :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
- </template>
- <template v-else>
- <a-input-password
- v-if="isSecretEnvKey(v.envKey)"
- :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
- :placeholder="v.defaultValue || v.envKey"
- allow-clear
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
- <a-input
- v-else
- :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
- :placeholder="v.defaultValue || v.envKey"
- allow-clear
- @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
- </template>
- </a-form-item>
- </div>
- </a-collapse-panel>
- </a-collapse>
- </div>
- </template>
- </a-tab-pane>
- </a-tabs>
- </template>
- </template>
- <template v-else>
- <a-alert type="warning" :message="$t('aice.llm_spec_update')" :description="$t('aice.llm_spec')" show-icon />
- </template>
- </a-form>
- </div>
- <div slot="footer">
- <a-button type="primary" @click="handleConfirm" :loading="loading">{{ $t('dialog.ok') }}</a-button>
- <a-button @click="cancelDialog">{{ $t('dialog.cancel') }}</a-button>
- </div>
- </base-dialog>
- </template>
- <script>
- import DialogMixin from '@/mixins/dialog'
- import WindowsMixin from '@/mixins/windows'
- import { OPENCLAW_CHANNEL_SECTIONS, OPENCLAW_CHANNEL_OPTIONS } from '../../llm-sku/openclawChannelConfig'
- import { OPENCLAW_PROVIDER_SECTIONS, OPENCLAW_PROVIDER_OPTIONS } from '../../llm-sku/openclawProviderConfig'
- import { getParamsForType } from '../../llm-sku/llmTypeConfig'
- export default {
- name: 'LlmUpdateSpecDialog',
- mixins: [DialogMixin, WindowsMixin],
- data () {
- const row = (this.params && this.params.data && this.params.data[0]) || {}
- return {
- loading: false,
- instanceName: row.name || '',
- row,
- credentialNameMap: {},
- form: {
- fc: this.$form.createForm(this, {
- onValuesChange: (props, values) => {
- Object.keys(values).forEach((key) => {
- this.$set(this.form.fd, key, values[key])
- })
- },
- }),
- fd: {
- openclaw_channels: [],
- },
- },
- decorators: {
- openclaw_channels: [
- 'openclaw_channels',
- { initialValue: (row?.llm_spec?.openclaw?.channels || []).map(item => item.name), rules: [] },
- ],
- },
- formItemLayout: {
- wrapperCol: {
- span: 19,
- },
- labelCol: {
- span: 5,
- },
- },
- OPENCLAW_PROVIDER_SECTIONS,
- openclawSelectedProviders: [],
- openclawProviderCredentialMode: {},
- openclawProviderCredentialId: {},
- openclawProviderBlobKeys: {},
- openclawProviderExportKeys: {},
- openclawProviderBlob: {},
- openclawProviderActiveTab: '',
- openclawChannelCredentialMode: {},
- openclawChannelCredentialId: {},
- openclawChannelBlobKeys: {},
- openclawChannelExportKeys: {},
- openclawChannelBlob: {},
- openclawChannelActiveTab: '',
- openclawPrimaryModelOptions: [],
- openclawPrimaryModelLoading: false,
- }
- },
- computed: {
- isOpenclaw () {
- return (this.row.llm_type || '').toLowerCase() === 'openclaw'
- },
- channelOptionsForSelect () {
- return OPENCLAW_CHANNEL_OPTIONS.map(opt => ({
- value: opt.value,
- label: this.$te(opt.label) ? this.$t(opt.label) : opt.label,
- }))
- },
- filteredChannelSections () {
- const channels = this.form.fd.openclaw_channels || []
- if (channels.length === 0) return []
- const set = new Set(channels)
- return OPENCLAW_CHANNEL_SECTIONS.filter(s => set.has(s.sectionKey))
- },
- providerOptionsForSelect () {
- return OPENCLAW_PROVIDER_OPTIONS.map(key => ({
- value: key,
- label: this.$te(key) ? this.$t(key) : key,
- }))
- },
- providerTabList () {
- const selected = this.openclawSelectedProviders || []
- if (selected.length === 0) return []
- const list = []
- selected.forEach(providerLabelKey => {
- const vars = []
- let required = false
- this.OPENCLAW_PROVIDER_SECTIONS.forEach(section => {
- section.vars.forEach(v => {
- if (v.providerLabelKey === providerLabelKey) {
- vars.push(v)
- if (section.required) required = true
- }
- })
- })
- if (vars.length) list.push({ key: providerLabelKey, labelKey: providerLabelKey, vars, required })
- })
- return list
- },
- openclawProviderActiveKey () {
- const list = this.providerTabList
- if (!list.length) return ''
- const keys = list.map(t => t.key)
- return keys.includes(this.openclawProviderActiveTab) ? this.openclawProviderActiveTab : (keys[0] || '')
- },
- openclawOllamaBaseUrlForPrimaryModel () {
- const v = this.findOpenclawPrimaryModelVar()
- if (!v) return ''
- const pk = v.providerLabelKey
- return String((this.openclawProviderBlob[pk] || {}).OLLAMA_BASE_URL ?? '')
- },
- },
- watch: {
- openclawOllamaBaseUrlForPrimaryModel (newVal, oldVal) {
- if (oldVal === undefined) return
- if (newVal === oldVal) return
- const v = this.findOpenclawPrimaryModelVar()
- if (!v) return
- const pk = v.providerLabelKey
- const blob = this.openclawProviderBlob[pk] || {}
- const cur = blob.OPENCLAW_PRIMARY_MODEL
- const hasModel = cur !== undefined && cur !== null && String(cur).trim() !== ''
- if (!hasModel) {
- this.openclawPrimaryModelOptions = []
- return
- }
- if (newVal) {
- this.loadOpenclawPrimaryModelOptions(v, pk, '')
- } else {
- this.openclawPrimaryModelOptions = []
- }
- },
- filteredChannelSections (sections) {
- ;(sections || []).forEach(s => this.ensureChannelState(s.sectionKey))
- },
- providerTabList (list) {
- const keys = (list || []).map(t => t.key)
- if (keys.length && !keys.includes(this.openclawProviderActiveTab)) {
- this.openclawProviderActiveTab = keys[0] || ''
- }
- },
- openclawSelectedProviders: {
- handler (list) {
- ;(list || []).forEach((providerKey) => {
- this.ensureProviderState(providerKey)
- })
- },
- deep: true,
- },
- },
- created () {
- // 初始化 providers 的 blob 输入结构
- this.OPENCLAW_PROVIDER_SECTIONS.forEach(section => {
- section.vars.forEach(({ providerLabelKey, envKey, overrideUrlKey }) => {
- if (!this.openclawProviderBlob[providerLabelKey]) this.$set(this.openclawProviderBlob, providerLabelKey, {})
- if (this.openclawProviderBlob[providerLabelKey][envKey] === undefined) this.$set(this.openclawProviderBlob[providerLabelKey], envKey, '')
- if (overrideUrlKey) {
- if (this.openclawProviderBlob[providerLabelKey][overrideUrlKey] === undefined) this.$set(this.openclawProviderBlob[providerLabelKey], overrideUrlKey, '')
- }
- })
- })
- this.initFromSpec()
- },
- methods: {
- credentialLabelFormat (item) {
- if (!item) return ''
- const id = String(item.id || item.key || '')
- const byMap = id && this.credentialNameMap && this.credentialNameMap[id]
- if (byMap) return byMap
- return item.name || id
- },
- credentialExtraOpts (credentialId) {
- if (!credentialId) return []
- const id = String(credentialId)
- const name = this.credentialNameMap && this.credentialNameMap[id]
- if (!name) return []
- return [{ id, name }]
- },
- initFromSpec () {
- const spec = this.row.llm_spec && this.row.llm_spec.openclaw != null ? this.row.llm_spec.openclaw : (this.row.llm_spec || {})
- if (!spec || typeof spec !== 'object') return
- const allowedChannelKeys = new Set(OPENCLAW_CHANNEL_SECTIONS.map(s => s.sectionKey))
- // providers
- if (Array.isArray(spec.providers) && spec.providers.length > 0) {
- const providerKeys = []
- spec.providers.forEach(p => {
- const name = p && p.name
- if (!name) return
- // name 为 providerShortName,例如 moonshot,需要映射回 providerLabelKey
- // 这里尽量做兼容:
- // 1) 如果历史数据直接存了 providerLabelKey,则直接匹配;
- // 2) 否则用 providerShortName 做大小写不敏感匹配;
- // 这样可以避免更新规格初始化时找不到对应 provider(导致 tab/输入项不出现)。
- const normalizedName = String(name)
- const labelKey =
- OPENCLAW_PROVIDER_OPTIONS.find(k => k === normalizedName) ||
- OPENCLAW_PROVIDER_OPTIONS.find(k => String(this.providerShortName(k) || '').toLowerCase() === normalizedName.toLowerCase()) ||
- null
- if (!labelKey) return
- providerKeys.push(labelKey)
- this.ensureProviderState(labelKey)
- const cred = p.credential || {}
- if (cred.id) {
- if (cred.name) {
- this.$set(this.credentialNameMap, String(cred.id), cred.name)
- }
- this.$set(this.openclawProviderCredentialMode, labelKey, 'existing')
- this.$set(this.openclawProviderCredentialId, labelKey, cred.id)
- const exportKeys = cred.export_keys || []
- this.$set(this.openclawProviderExportKeys, labelKey, exportKeys)
- // 拉取 blob keys 以填充可选项
- this.fetchCredentialBlobKeys(cred.id).then(keys => {
- this.$set(this.openclawProviderBlobKeys, labelKey, keys)
- }).catch(() => {
- this.$set(this.openclawProviderBlobKeys, labelKey, [])
- })
- } else {
- this.$set(this.openclawProviderCredentialMode, labelKey, 'new')
- }
- })
- this.openclawSelectedProviders = providerKeys
- }
- // channels
- const rawChannels = spec.channels
- if (Array.isArray(rawChannels) && rawChannels.length > 0) {
- const channelKeys = []
- rawChannels.forEach(item => {
- const key = item && (item.name || item.sectionKey || item.type || item.key)
- if (!key) return
- if (!allowedChannelKeys.has(key)) return
- channelKeys.push(key)
- this.ensureChannelState(key)
- const cred = item.credential || {}
- if (cred.id) {
- if (cred.name) {
- this.$set(this.credentialNameMap, String(cred.id), cred.name)
- }
- this.$set(this.openclawChannelCredentialMode, key, 'existing')
- this.$set(this.openclawChannelCredentialId, key, cred.id)
- const exportKeys = cred.export_keys || []
- this.$set(this.openclawChannelExportKeys, key, exportKeys)
- this.fetchCredentialBlobKeys(cred.id).then(keys => {
- this.$set(this.openclawChannelBlobKeys, key, keys)
- }).catch(() => {
- this.$set(this.openclawChannelBlobKeys, key, [])
- })
- } else {
- this.$set(this.openclawChannelCredentialMode, key, 'new')
- }
- })
- this.form.fc.setFieldsValue({ openclaw_channels: channelKeys })
- this.$set(this.form.fd, 'openclaw_channels', channelKeys)
- if (!this.openclawChannelActiveTab && channelKeys.length > 0) {
- this.openclawChannelActiveTab = channelKeys[0]
- }
- } else if (rawChannels && typeof rawChannels === 'object') {
- const channelKeys = []
- Object.keys(rawChannels).forEach((k) => {
- const key = String(k)
- if (!allowedChannelKeys.has(key)) return
- const item = rawChannels[key] || {}
- channelKeys.push(key)
- this.ensureChannelState(key)
- const cred = item.credential || item || {}
- const id = cred.id
- if (id) {
- if (cred.name) {
- this.$set(this.credentialNameMap, String(id), cred.name)
- }
- this.$set(this.openclawChannelCredentialMode, key, 'existing')
- this.$set(this.openclawChannelCredentialId, key, id)
- const exportKeys = cred.export_keys || []
- this.$set(this.openclawChannelExportKeys, key, exportKeys)
- this.fetchCredentialBlobKeys(id).then(keys => {
- this.$set(this.openclawChannelBlobKeys, key, keys)
- }).catch(() => {
- this.$set(this.openclawChannelBlobKeys, key, [])
- })
- } else {
- this.$set(this.openclawChannelCredentialMode, key, 'new')
- }
- })
- this.form.fc.setFieldsValue({ openclaw_channels: channelKeys })
- this.$set(this.form.fd, 'openclaw_channels', channelKeys)
- if (!this.openclawChannelActiveTab && channelKeys.length > 0) {
- this.openclawChannelActiveTab = channelKeys[0]
- }
- }
- },
- genCredentialName ({ llmName, usage, key }) {
- const base = String(llmName || '').trim() || 'llm'
- const u = String(usage || '').trim() || 'unknown'
- const k = String(key || '').trim() || 'default'
- const normalize = (s) => String(s || '').trim()
- .replace(/\s+/g, '-')
- .replace(/-+/g, '-')
- .replace(/^-|-$/g, '')
- return normalize(`${base}-${u}-${k}`) || 'llm-credential'
- },
- credentialParamsForChannel (channelKey) {
- 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 {
- $t: 2,
- scope: this.$store.getters.scope,
- filter,
- 'tags.0.key': 'user:openclaw_usage',
- 'tags.0.value': 'channel',
- 'tags.1.key': 'user:openclaw_name',
- 'tags.1.value': channelKey,
- }
- },
- ensureChannelState (channelKey) {
- if (!this.openclawChannelCredentialMode[channelKey]) this.$set(this.openclawChannelCredentialMode, channelKey, 'new')
- if (!this.openclawChannelCredentialId[channelKey]) this.$set(this.openclawChannelCredentialId, channelKey, undefined)
- if (!this.openclawChannelBlobKeys[channelKey]) this.$set(this.openclawChannelBlobKeys, channelKey, [])
- if (!this.openclawChannelExportKeys[channelKey]) this.$set(this.openclawChannelExportKeys, channelKey, [])
- if (!this.openclawChannelBlob[channelKey]) this.$set(this.openclawChannelBlob, channelKey, {})
- },
- async fetchCredentialBlobKeys (credentialId) {
- if (!credentialId) return []
- const manager = new this.$Manager('credentials', 'v1')
- const { data } = await manager.get({ id: credentialId })
- const idStr = String(credentialId)
- const credName = data && data.name
- if (credName) {
- this.$set(this.credentialNameMap, idStr, credName)
- }
- const blob = data?.blob
- let obj = blob
- if (typeof blob === 'string') {
- try { obj = JSON.parse(blob) } catch (e) { obj = {} }
- }
- if (obj && typeof obj === 'object') {
- return Object.keys(obj)
- }
- return []
- },
- async onChannelCredentialChange (channelKey, credentialId) {
- this.ensureChannelState(channelKey)
- this.$set(this.openclawChannelCredentialId, channelKey, credentialId)
- this.$set(this.openclawChannelExportKeys, channelKey, [])
- try {
- const keys = await this.fetchCredentialBlobKeys(credentialId)
- this.$set(this.openclawChannelBlobKeys, channelKey, keys)
- this.$set(this.openclawChannelExportKeys, channelKey, [...keys])
- } catch (e) {
- this.$set(this.openclawChannelBlobKeys, channelKey, [])
- }
- },
- credentialParamsForProvider (providerKey) {
- 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})`)
- }
- const shortName = this.providerShortName(providerKey)
- return {
- $t: 1,
- scope: this.$store.getters.scope,
- filter,
- 'tags.0.key': 'user:openclaw_usage',
- 'tags.0.value': 'provider',
- 'tags.1.key': 'user:openclaw_name',
- 'tags.1.value': shortName,
- }
- },
- providerShortName (providerKey) {
- const s = String(providerKey || '')
- const parts = s.split('.')
- return parts[parts.length - 1] || s
- },
- ensureProviderState (providerKey) {
- if (!this.openclawProviderCredentialMode[providerKey]) this.$set(this.openclawProviderCredentialMode, providerKey, 'new')
- if (!this.openclawProviderCredentialId[providerKey]) this.$set(this.openclawProviderCredentialId, providerKey, undefined)
- if (!this.openclawProviderBlobKeys[providerKey]) this.$set(this.openclawProviderBlobKeys, providerKey, [])
- if (!this.openclawProviderExportKeys[providerKey]) this.$set(this.openclawProviderExportKeys, providerKey, [])
- if (!this.openclawProviderBlob[providerKey]) this.$set(this.openclawProviderBlob, providerKey, {})
- },
- async onProviderCredentialChange (providerKey, credentialId) {
- this.ensureProviderState(providerKey)
- this.$set(this.openclawProviderCredentialId, providerKey, credentialId)
- this.$set(this.openclawProviderExportKeys, providerKey, [])
- try {
- const keys = await this.fetchCredentialBlobKeys(credentialId)
- this.$set(this.openclawProviderBlobKeys, providerKey, keys)
- this.$set(this.openclawProviderExportKeys, providerKey, [...keys])
- } catch (e) {
- this.$set(this.openclawProviderBlobKeys, providerKey, [])
- }
- },
- closeProviderTab (providerKey) {
- const next = this.openclawSelectedProviders.filter(k => k !== providerKey)
- this.openclawSelectedProviders = next
- if (this.openclawProviderActiveTab === providerKey && next.length > 0) {
- this.openclawProviderActiveTab = next[0]
- } else if (next.length === 0) {
- this.openclawProviderActiveTab = ''
- }
- this.$delete(this.openclawProviderCredentialMode, providerKey)
- this.$delete(this.openclawProviderCredentialId, providerKey)
- this.$delete(this.openclawProviderBlobKeys, providerKey)
- this.$delete(this.openclawProviderExportKeys, providerKey)
- },
- filterProviderOption (input, option) {
- const value = option.componentOptions && option.componentOptions.propsData && option.componentOptions.propsData.value
- if (value == null) return true
- const label = this.$te(value) ? this.$t(value) : String(value)
- return label.toLowerCase().indexOf((input || '').toLowerCase()) >= 0
- },
- closeChannelTab (sectionKey) {
- const channels = (this.form.fd.openclaw_channels || []).filter(k => k !== sectionKey)
- this.form.fc.setFieldsValue({ openclaw_channels: channels })
- this.$set(this.form.fd, 'openclaw_channels', channels)
- if (this.openclawChannelActiveTab === sectionKey && channels.length > 0) {
- this.openclawChannelActiveTab = channels[0]
- } else if (channels.length === 0) {
- this.openclawChannelActiveTab = ''
- }
- this.$delete(this.openclawChannelCredentialMode, sectionKey)
- this.$delete(this.openclawChannelCredentialId, sectionKey)
- this.$delete(this.openclawChannelBlobKeys, sectionKey)
- this.$delete(this.openclawChannelExportKeys, sectionKey)
- },
- filterChannelOption (input, option) {
- const value = option.componentOptions && option.componentOptions.propsData && option.componentOptions.propsData.value
- if (value == null) return true
- const opt = this.channelOptionsForSelect.find(o => o.value === value)
- const label = opt ? opt.label : (this.$te(value) ? this.$t(value) : String(value))
- return String(label).toLowerCase().indexOf((input || '').toLowerCase()) >= 0
- },
- channelBasicVars (vars) {
- return (vars || []).filter(v => !v.advanced)
- },
- channelAdvancedVars (vars) {
- return (vars || []).filter(v => !!v.advanced)
- },
- sectionBasicEnvKeys (section, envKeys) {
- if (!section) return envKeys || []
- const basicKeys = new Set(this.channelBasicVars(section.vars).map(v => v.envKey))
- return (envKeys || []).filter(k => basicKeys.has(k))
- },
- sectionAdvancedEnvKeys (section, envKeys) {
- if (!section) return []
- const basicKeys = new Set(this.channelBasicVars(section.vars).map(v => v.envKey))
- return (envKeys || []).filter(k => !basicKeys.has(k))
- },
- isSecretEnvKey (envKey) {
- const lower = (envKey || '').toLowerCase()
- return lower.includes('key') || lower.includes('secret') || lower.includes('token') || lower.includes('password')
- },
- findOpenclawPrimaryModelVar () {
- const sections = this.OPENCLAW_PROVIDER_SECTIONS || []
- for (let i = 0; i < sections.length; i++) {
- const vars = sections[i].vars || []
- for (let j = 0; j < vars.length; j++) {
- const x = vars[j]
- if (x.envKey === 'OPENCLAW_PRIMARY_MODEL' && x.component === 'a-select') return x
- }
- }
- return null
- },
- pickTrimmedOpenclawBlob (raw) {
- const blob = {}
- Object.keys(raw || {}).forEach(k => {
- const val = raw[k]
- if (val === undefined || val === null) return
- const s = typeof val === 'string' ? val.trim() : String(val).trim()
- if (s !== '') blob[k] = s
- })
- return blob
- },
- validateOpenclawProviderRequiredEnv (providerKey) {
- const raw = this.openclawProviderBlob[providerKey] || {}
- const sections = this.OPENCLAW_PROVIDER_SECTIONS || []
- for (let i = 0; i < sections.length; i++) {
- const vars = sections[i].vars || []
- for (let j = 0; j < vars.length; j++) {
- const v = vars[j]
- if (v.providerLabelKey !== providerKey || !v.required) continue
- const val = raw[v.envKey]
- const empty = val === undefined || val === null || val === '' ||
- (typeof val === 'string' && !String(val).trim())
- if (empty) {
- const label = v.placeholderKey && this.$te(v.placeholderKey)
- ? this.$t(v.placeholderKey)
- : (this.$te(v.descriptionKey) ? this.$t(v.descriptionKey) : v.envKey)
- const tip = v.component === 'a-select'
- ? this.$t('common.tips.select', [label])
- : this.$t('common.tips.input', [label])
- this.$message.warning(tip)
- return false
- }
- }
- }
- return true
- },
- syncOpenclawPrimaryModelIfNotInOptions (providerKey) {
- const blob = (this.openclawProviderBlob || {})[providerKey]
- if (!blob) return
- const cur = blob.OPENCLAW_PRIMARY_MODEL
- const hasModel = cur !== undefined && cur !== null && String(cur).trim() !== ''
- if (!hasModel) return
- const opts = this.openclawPrimaryModelOptions || []
- const inList = opts.some(opt => String(opt.value) === String(cur))
- if (!inList) {
- this.$set(this.openclawProviderBlob[providerKey], 'OPENCLAW_PRIMARY_MODEL', '')
- }
- },
- openclawProviderAselectPlaceholder (v) {
- const labelKey = v.placeholderKey || 'aice.model'
- const label = this.$te(labelKey) ? this.$t(labelKey) : labelKey
- return this.$t('common.tips.select', [label])
- },
- refreshOpenclawPrimaryModel (v, providerKey) {
- this.loadOpenclawPrimaryModelOptions(v, providerKey, '')
- },
- async onOpenclawPrimaryModelDropdown (open, v, providerKey) {
- if (!open) return
- await this.loadOpenclawPrimaryModelOptions(v, providerKey, '')
- },
- onOpenclawPrimaryModelSearch (q, v, providerKey) {
- if (this._openclawPrimaryModelSearchTimer) clearTimeout(this._openclawPrimaryModelSearchTimer)
- this._openclawPrimaryModelSearchTimer = setTimeout(() => {
- this.loadOpenclawPrimaryModelOptions(v, providerKey, q || '')
- }, 300)
- },
- async loadOpenclawPrimaryModelOptions (v, providerKey, search) {
- const resource = (v && v.resource) || 'llms/provider-models'
- const blob = (this.openclawProviderBlob || {})[providerKey] || {}
- this.openclawPrimaryModelLoading = true
- try {
- const manager = new this.$Manager(resource, 'v2')
- const body = {
- scope: this.$store.getters.scope,
- limit: 20,
- ...getParamsForType('openclaw'),
- url: blob.OLLAMA_BASE_URL,
- provider_type: 'ollama',
- }
- if (search) body.filter = [`name.contains(${search})`]
- const { data } = await manager.create({ data: body })
- const rows = this.unwrapOpenclawPrimaryModelResponse(data)
- this.openclawPrimaryModelOptions = rows.map(item => ({
- value: item.id || item.model_id,
- label: item.fullname || item.name || item.model_id || String(item.id || ''),
- })).filter(opt => opt.value !== undefined && opt.value !== null && opt.value !== '')
- if (!search) {
- this.syncOpenclawPrimaryModelIfNotInOptions(providerKey)
- }
- } catch (e) {
- this.openclawPrimaryModelOptions = []
- } finally {
- this.openclawPrimaryModelLoading = false
- }
- },
- unwrapOpenclawPrimaryModelResponse (data) {
- if (!data) return []
- if (Array.isArray(data)) return data
- if (Array.isArray(data.data)) return data.data
- if (data.data && Array.isArray(data.data.data)) return data.data.data
- return []
- },
- overrideUrlPlaceholder (overrideUrlKey) {
- const defaults = {
- MOONSHOT_BASE_URL: 'https://api.moonshot.cn/v1',
- KIMI_BASE_URL: 'https://api.moonshot.ai/anthropic',
- }
- return defaults[overrideUrlKey] || 'https://...'
- },
- async handleConfirm () {
- if (!this.isOpenclaw) {
- this.cancelDialog()
- return
- }
- this.loading = true
- try {
- const row = this.row
- const channelsSelected = this.form.fd.openclaw_channels || []
- const openclaw = {}
- const credManager = new this.$Manager('credentials', 'v1')
- const channels = []
- for (let i = 0; i < channelsSelected.length; i++) {
- const channelKey = channelsSelected[i]
- this.ensureChannelState(channelKey)
- const mode = this.openclawChannelCredentialMode[channelKey] || 'new'
- let credentialId
- let exportKeys
- if (mode === 'existing') {
- credentialId = this.openclawChannelCredentialId[channelKey]
- if (!credentialId) {
- this.$message.warning(this.$t('common.tips.select', [this.$t('aice.container_secret')]))
- this.loading = false
- return
- }
- exportKeys = this.openclawChannelExportKeys[channelKey] || []
- if (channelKey === 'qqbot') {
- const requiredKeys = ['QQBOT_APP_ID', 'QQBOT_CLIENT_SECRET']
- const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- }
- if (channelKey === 'feishu') {
- const requiredKeys = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET']
- const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- }
- if (channelKey === 'discord') {
- const requiredKeys = ['DISCORD_BOT_TOKEN']
- const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- }
- if (channelKey === 'telegram') {
- const requiredKeys = ['TELEGRAM_BOT_TOKEN']
- const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- }
- } else {
- const credName = this.genCredentialName({ llmName: this.instanceName, usage: 'channel', key: channelKey })
- const raw = this.openclawChannelBlob[channelKey] || {}
- const blob = this.pickTrimmedOpenclawBlob(raw)
- if (channelKey === 'qqbot') {
- const missing = ['QQBOT_APP_ID', 'QQBOT_CLIENT_SECRET'].filter(k => !blob[k])
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- }
- if (channelKey === 'feishu') {
- const missing = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'].filter(k => !blob[k])
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- // 单选/勾选控件可能只显示 defaultValue,但不一定触发 @change 写入 blob
- // 这里兜底补齐默认值,确保这些字段会写入 credential blob 与 export_keys
- if (!blob.FEISHU_DOMAIN) blob.FEISHU_DOMAIN = 'feishu'
- if (!blob.FEISHU_DM_POLICY) blob.FEISHU_DM_POLICY = 'open'
- if (!blob.FEISHU_TYPING_INDICATOR) blob.FEISHU_TYPING_INDICATOR = 'true'
- if (!blob.FEISHU_RESOLVE_SENDER_NAMES) blob.FEISHU_RESOLVE_SENDER_NAMES = 'true'
- }
- if (channelKey === 'discord') {
- const missing = ['DISCORD_BOT_TOKEN'].filter(k => !blob[k])
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- if (!blob.DISCORD_DM_POLICY) blob.DISCORD_DM_POLICY = 'open'
- }
- if (channelKey === 'telegram') {
- const missing = ['TELEGRAM_BOT_TOKEN'].filter(k => !blob[k])
- if (missing.length) {
- const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
- this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
- this.loading = false
- return
- }
- if (!blob.TELEGRAM_DM_POLICY) blob.TELEGRAM_DM_POLICY = 'open'
- }
- if (Object.keys(blob).length === 0) {
- this.$message.warning(this.$t('aice.openclaw.provider_filter_empty'))
- this.loading = false
- return
- }
- const { data: credData } = await credManager.create({
- data: {
- type: 'container_secret',
- name: credName,
- blob,
- __meta__: {
- 'user:openclaw_usage': 'channel',
- 'user:openclaw_name': channelKey,
- },
- },
- })
- credentialId = credData.id
- exportKeys = Object.keys(blob)
- }
- channels.push({
- name: channelKey,
- credential: { id: credentialId, export_keys: exportKeys },
- })
- }
- if (channels.length) openclaw.channels = channels
- const providersSelected = this.openclawSelectedProviders || []
- if (!providersSelected.length) {
- this.$message.warning(this.$t('aice.openclaw.provider_select_first'))
- this.loading = false
- return
- }
- const providers = []
- for (let i = 0; i < providersSelected.length; i++) {
- const providerKey = providersSelected[i]
- this.ensureProviderState(providerKey)
- if (!this.validateOpenclawProviderRequiredEnv(providerKey)) {
- this.loading = false
- return
- }
- const mode = this.openclawProviderCredentialMode[providerKey] || 'new'
- let credentialId
- let exportKeys
- if (mode === 'existing') {
- credentialId = this.openclawProviderCredentialId[providerKey]
- if (!credentialId) {
- this.$message.warning(this.$t('common.tips.select', [this.$t('aice.container_secret')]))
- this.loading = false
- return
- }
- exportKeys = this.openclawProviderExportKeys[providerKey] || []
- } else {
- const credName = this.genCredentialName({ llmName: this.instanceName, usage: 'provider', key: this.providerShortName(providerKey) })
- const raw = this.openclawProviderBlob[providerKey] || {}
- const blob = this.pickTrimmedOpenclawBlob(raw)
- if (Object.keys(blob).length === 0) {
- this.$message.warning(this.$t('aice.openclaw.ai_providers.at_least_one'))
- this.loading = false
- return
- }
- const { data: credData } = await credManager.create({
- data: {
- type: 'container_secret',
- name: credName,
- blob,
- __meta__: {
- 'user:openclaw_usage': 'provider',
- 'user:openclaw_name': this.providerShortName(providerKey),
- },
- },
- })
- credentialId = credData.id
- exportKeys = Object.keys(blob)
- }
- providers.push({
- name: this.providerShortName(providerKey),
- credential: { id: credentialId, export_keys: exportKeys },
- })
- }
- if (providers.length) openclaw.providers = providers
- if (!Object.keys(openclaw).length) {
- this.$message.warning(this.$t('aice.openclaw.provider_filter_empty'))
- this.loading = false
- return
- }
- await this.params.onManager('update', {
- id: row.id,
- managerArgs: {
- data: {
- llm_spec: { openclaw },
- },
- },
- })
- this.$message.success(this.$t('common.success'))
- this.cancelDialog()
- this.params.refresh && this.params.refresh()
- } catch (error) {
- throw error
- } finally {
- this.loading = false
- }
- },
- },
- }
- </script>
- <style scoped>
- .openclaw-channel-tabs { margin-top: 8px; }
- .openclaw-provider-tabs { margin-top: 8px; }
- .openclaw-section-divider { margin-top: 20px; }
- .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; }
- .openclaw-new-blob-section { margin-top: 8px; }
- .openclaw-new-blob-row ::v-deep .ant-form-item-label {
- padding-bottom: 4px;
- overflow: visible;
- }
- .openclaw-new-blob-row ::v-deep .ant-form-item-label label {
- overflow: visible;
- white-space: nowrap;
- }
- .openclaw-filter-empty { padding: 12px 0; font-size: 13px; }
- .openclaw-channel-config-hint { font-size: 13px; }
- </style>
|