create.vue 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388
  1. <template>
  2. <div>
  3. <page-header :title="isApplyType ? $t('aice.app_llm_create') : $t('aice.llm_create')" />
  4. <page-body needMarginBottom>
  5. <a-form :form="form.fc" hideRequiredMark v-bind="formItemLayout">
  6. <a-form-item :label="$t('common.name')">
  7. <a-input v-decorator="decorators.name" :placeholder="$t('common.tips.input', [$t('common.name')])" />
  8. <template v-slot:extra>
  9. <name-repeated res="llms" :name="form.fd.name" />
  10. </template>
  11. </a-form-item>
  12. <a-form-item :label="isApplyType ? $t('aice.llm_type.app') : $t('aice.llm_type.llm')">
  13. <a-radio-group
  14. class="llm-type-picker"
  15. button-style="solid"
  16. v-decorator="decorators.llm_type">
  17. <a-radio-button v-for="opt in llmTypeOptions" :key="opt.id" :value="opt.id">
  18. {{ opt.name }}
  19. </a-radio-button>
  20. </a-radio-group>
  21. </a-form-item>
  22. <a-form-item :label="isApplyType ? $t('aice.app_llm_sku') : $t('aice.llm_sku')">
  23. <base-select
  24. v-decorator="decorators.llm_sku_id"
  25. resource="llm_skus"
  26. :select-props="{
  27. placeholder: $t('common.tips.select', [isApplyType ? $t('aice.app_llm_sku') : $t('aice.llm_sku')]),
  28. }"
  29. :params="llmSkuParams" />
  30. </a-form-item>
  31. <!-- <a-form-item :label="$t('aice.bandwidth_mb')">
  32. <a-input-number
  33. v-decorator="decorators.bandwidth_mb"
  34. :min="1"
  35. :max="10000"
  36. :step="1"
  37. :precision="0" />
  38. </a-form-item> -->
  39. <a-form-item :label="$t('compute.text_104')" class="mb-0">
  40. <server-network
  41. :form="form"
  42. :decorator="decorators.network"
  43. :network-list-params="networkParams"
  44. :schedtag-params="resourcesParams.schedtag"
  45. :network-resource-mapper="networkResourceMapper"
  46. :hiddenNetworkOptions="['schedtag']"
  47. defaultNetworkType="default"
  48. :hiddenAdd="true"
  49. :isDialog="true" />
  50. </a-form-item>
  51. <a-collapse :bordered="false" v-model="collapseActive">
  52. <a-collapse-panel :header="$t('compute.text_309')" key="1">
  53. <a-form-item :label="$t('dictionary.host')">
  54. <base-select
  55. v-decorator="decorators.prefer_host"
  56. resource="hosts"
  57. :select-props="{
  58. placeholder: $t('common.tips.select', [$t('dictionary.host')]),
  59. allowClear: true,
  60. }"
  61. :params="hostParams" />
  62. </a-form-item>
  63. </a-collapse-panel>
  64. </a-collapse>
  65. <template v-if="form.fd.llm_type === 'openclaw'">
  66. <a-divider orientation="left" class="openclaw-section-divider">{{ $t('aice.openclaw.section.ai_providers') }}</a-divider>
  67. <a-form-item :label="$t('aice.openclaw.provider_filter')" :extra="$t('aice.openclaw.provider_select_tip')">
  68. <a-select
  69. v-model="openclawSelectedProviders"
  70. mode="multiple"
  71. :placeholder="$t('aice.openclaw.provider_filter_placeholder')"
  72. allow-clear
  73. show-search
  74. :filter-option="filterProviderOption"
  75. style="width: 100%; max-width: 400px;">
  76. <a-select-option v-for="opt in providerOptionsForSelect" :key="opt.value" :value="opt.value">
  77. {{ opt.label }}
  78. </a-select-option>
  79. </a-select>
  80. </a-form-item>
  81. <a-tabs
  82. v-if="providerTabList.length > 0"
  83. :activeKey="openclawProviderActiveKey"
  84. type="card"
  85. class="openclaw-provider-tabs"
  86. :animated="false"
  87. @change="openclawProviderActiveTab = $event">
  88. <a-tab-pane
  89. v-for="item in providerTabList"
  90. :key="item.key"
  91. :forceRender="true">
  92. <span slot="tab" class="openclaw-tab-with-close">
  93. {{ $t(item.labelKey) }}{{ item.required ? ' *' : '' }}
  94. <a-icon type="close" class="openclaw-tab-close" @click.prevent.stop="closeProviderTab(item.key)" />
  95. </span>
  96. <a-form-item :label="$t('aice.openclaw.credential_mode.label')">
  97. <a-radio-group
  98. :value="openclawProviderCredentialMode[item.key] || 'new'"
  99. @change="e => $set(openclawProviderCredentialMode, item.key, e.target.value)">
  100. <a-radio value="new">{{ $t('aice.openclaw.credential_mode.new') }}</a-radio>
  101. <a-radio value="existing">{{ $t('aice.openclaw.credential_mode.existing') }}</a-radio>
  102. </a-radio-group>
  103. </a-form-item>
  104. <template v-if="(openclawProviderCredentialMode[item.key] || 'new') === 'existing'">
  105. <a-form-item :label="$t('aice.container_secret')">
  106. <base-select
  107. v-model="openclawProviderCredentialId[item.key]"
  108. resource="credentials"
  109. :params="credentialParamsForProvider(item.key)"
  110. :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.container_secret')]) }"
  111. @change="val => onProviderCredentialChange(item.key, val)" />
  112. </a-form-item>
  113. <a-form-item :label="$t('aice.container_secret.export_keys')" :extra="$t('aice.container_secret.export_keys_tip')">
  114. <a-checkbox-group
  115. :value="openclawProviderExportKeys[item.key] || []"
  116. @change="val => $set(openclawProviderExportKeys, item.key, val)">
  117. <a-checkbox v-for="k in (openclawProviderBlobKeys[item.key] || [])" :key="k" :value="k">{{ k }}</a-checkbox>
  118. </a-checkbox-group>
  119. </a-form-item>
  120. </template>
  121. <template v-else>
  122. <div class="openclaw-auto-credential-name text-color-secondary mb-2">
  123. {{ $t('aice.openclaw.new_credential_name') }}:{{ genCredentialName({ llmName: form.fd.name, usage: 'provider', key: providerShortName(item.key) }) }}
  124. </div>
  125. <div class="openclaw-new-blob-section">
  126. <div v-for="v in item.vars" :key="v.envKey" class="openclaw-new-blob-row mb-2">
  127. <a-form-item
  128. :label="v.envKey"
  129. :required="item.required || v.required"
  130. :extra="((item.required || v.required) ? $t('aice.openclaw.required_hint') + ' ' : '') + ($te(v.descriptionKey) ? $t(v.descriptionKey) : '')">
  131. <a-input-password
  132. v-if="isSecretEnvKey(v.envKey)"
  133. :value="(openclawProviderBlob[item.key] || {})[v.envKey]"
  134. :placeholder="v.envKey"
  135. allow-clear
  136. @change="e => $set(openclawProviderBlob[item.key], v.envKey, e.target.value)" />
  137. <div
  138. v-else-if="v.component === 'a-select'"
  139. class="d-flex align-items-center openclaw-primary-model-select-row">
  140. <a-select
  141. class="flex-grow-1"
  142. style="min-width: 0"
  143. :value="(openclawProviderBlob[item.key] || {})[v.envKey] || undefined"
  144. :placeholder="openclawProviderAselectPlaceholder(v)"
  145. allow-clear
  146. show-search
  147. :filter-option="false"
  148. :loading="openclawPrimaryModelLoading"
  149. @dropdownVisibleChange="open => onOpenclawPrimaryModelDropdown(open, v, item.key)"
  150. @search="q => onOpenclawPrimaryModelSearch(q, v, item.key)"
  151. @change="val => $set(openclawProviderBlob[item.key], v.envKey, val)">
  152. <a-select-option
  153. v-for="opt in openclawPrimaryModelOptions"
  154. :key="String(opt.value)"
  155. :value="opt.value">
  156. {{ opt.label }}
  157. </a-select-option>
  158. </a-select>
  159. <a-icon
  160. type="sync"
  161. class="ml-2 primary-color flex-shrink-0"
  162. :spin="openclawPrimaryModelLoading"
  163. @click="refreshOpenclawPrimaryModel(v, item.key)" />
  164. </div>
  165. <a-input
  166. v-else
  167. :value="(openclawProviderBlob[item.key] || {})[v.envKey]"
  168. :placeholder="v.envKey"
  169. allow-clear
  170. @change="e => $set(openclawProviderBlob[item.key], v.envKey, e.target.value)" />
  171. </a-form-item>
  172. <a-form-item
  173. v-if="v.overrideUrlKey"
  174. :label="v.overrideUrlKey"
  175. :extra="($te('aice.openclaw.env.' + v.overrideUrlKey) ? $t('aice.openclaw.env.' + v.overrideUrlKey) : '') + ' ' + $t('aice.openclaw.override_url_optional')"
  176. class="openclaw-override-url mt-1">
  177. <a-input
  178. :value="(openclawProviderBlob[item.key] || {})[v.overrideUrlKey]"
  179. :placeholder="overrideUrlPlaceholder(v.overrideUrlKey)"
  180. allow-clear
  181. @change="e => $set(openclawProviderBlob[item.key], v.overrideUrlKey, e.target.value)" />
  182. </a-form-item>
  183. </div>
  184. </div>
  185. </template>
  186. </a-tab-pane>
  187. </a-tabs>
  188. <div v-else class="openclaw-filter-empty text-color-secondary">
  189. {{ openclawSelectedProviders.length === 0 ? $t('aice.openclaw.provider_select_first') : $t('aice.openclaw.provider_filter_empty') }}
  190. </div>
  191. <a-divider orientation="left" class="openclaw-section-divider">{{ $t('aice.openclaw.section.chat_channels') }}</a-divider>
  192. <a-form-item :label="$t('aice.openclaw.channels')" :extra="$t('aice.openclaw.channels_extra')">
  193. <a-select
  194. v-decorator="decorators.openclaw_channels"
  195. mode="multiple"
  196. :placeholder="$t('aice.openclaw.channel_select_placeholder')"
  197. allow-clear
  198. show-search
  199. :filter-option="filterChannelOption"
  200. style="width: 100%; max-width: 400px;">
  201. <a-select-option v-for="opt in channelOptionsForSelect" :key="opt.value" :value="opt.value">
  202. {{ opt.label }}
  203. </a-select-option>
  204. </a-select>
  205. </a-form-item>
  206. <template v-if="filteredChannelSections.length > 0">
  207. <div class="openclaw-channel-config-hint text-color-secondary mb-2">
  208. {{ $t('aice.openclaw.channel_config_hint') }}
  209. </div>
  210. <a-tabs
  211. :activeKey="openclawChannelActiveTab || (filteredChannelSections[0] && filteredChannelSections[0].sectionKey)"
  212. class="openclaw-channel-tabs"
  213. :animated="false"
  214. @change="openclawChannelActiveTab = $event">
  215. <a-tab-pane
  216. v-for="section in filteredChannelSections"
  217. :key="section.sectionKey"
  218. :forceRender="true">
  219. <span slot="tab" class="openclaw-tab-with-close">
  220. {{ $t(section.sectionLabelKey) }}
  221. <a-icon type="close" class="openclaw-tab-close" @click.prevent.stop="closeChannelTab(section.sectionKey)" />
  222. </span>
  223. <a-form-item :label="$t('aice.openclaw.credential_mode.label')">
  224. <a-radio-group
  225. :value="openclawChannelCredentialMode[section.sectionKey] || 'new'"
  226. @change="e => $set(openclawChannelCredentialMode, section.sectionKey, e.target.value)">
  227. <a-radio value="new">{{ $t('aice.openclaw.credential_mode.new') }}</a-radio>
  228. <a-radio value="existing">{{ $t('aice.openclaw.credential_mode.existing') }}</a-radio>
  229. </a-radio-group>
  230. </a-form-item>
  231. <template v-if="(openclawChannelCredentialMode[section.sectionKey] || 'new') === 'existing'">
  232. <a-form-item :label="$t('aice.container_secret')">
  233. <base-select
  234. v-model="openclawChannelCredentialId[section.sectionKey]"
  235. resource="credentials"
  236. :params="credentialParamsForChannel(section.sectionKey)"
  237. :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.container_secret')]) }"
  238. @change="val => onChannelCredentialChange(section.sectionKey, val)" />
  239. </a-form-item>
  240. <a-form-item :label="$t('aice.container_secret.export_keys')" :extra="$t('aice.container_secret.export_keys_tip')">
  241. <a-checkbox-group
  242. :value="openclawChannelExportKeys[section.sectionKey] || []"
  243. @change="val => $set(openclawChannelExportKeys, section.sectionKey, val)">
  244. <a-checkbox
  245. v-for="k in sectionBasicEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || [])"
  246. :key="k"
  247. :value="k">{{ k }}</a-checkbox>
  248. <a-collapse :bordered="false" v-if="sectionAdvancedEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || []).length">
  249. <a-collapse-panel :header="$t('common.adv_config')" key="advanced">
  250. <a-checkbox
  251. v-for="k in sectionAdvancedEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || [])"
  252. :key="k"
  253. :value="k">{{ k }}</a-checkbox>
  254. </a-collapse-panel>
  255. </a-collapse>
  256. </a-checkbox-group>
  257. </a-form-item>
  258. </template>
  259. <template v-else>
  260. <div class="openclaw-auto-credential-name text-color-secondary mb-2">
  261. {{ $t('aice.openclaw.new_credential_name') }}:{{ genCredentialName({ llmName: form.fd.name, usage: 'channel', key: section.sectionKey }) }}
  262. </div>
  263. <div class="openclaw-new-blob-section">
  264. <div v-for="v in channelBasicVars(section.vars)" :key="v.envKey" class="openclaw-new-blob-row mb-2">
  265. <a-form-item
  266. :label="v.envKey"
  267. :required="v.required"
  268. :extra="($te(v.descriptionKey) ? $t(v.descriptionKey) : '') + ' ' + (v.required ? $t('aice.openclaw.required_hint') : $t('aice.openclaw.channel_var_optional'))">
  269. <template v-if="v.envKey === 'FEISHU_DOMAIN'">
  270. <a-radio-group
  271. :value="((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue"
  272. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)">
  273. <a-radio value="feishu">feishu</a-radio>
  274. <a-radio value="lark">lark</a-radio>
  275. </a-radio-group>
  276. </template>
  277. <template v-else-if="['FEISHU_DM_POLICY', 'DISCORD_DM_POLICY', 'TELEGRAM_DM_POLICY'].includes(v.envKey)">
  278. <a-checkbox
  279. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'open'"
  280. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : v.defaultValue)">
  281. open
  282. </a-checkbox>
  283. <a-checkbox
  284. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'pairing'"
  285. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'pairing' : v.defaultValue)">
  286. pairing
  287. </a-checkbox>
  288. <a-checkbox
  289. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'allowlist'"
  290. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : v.defaultValue)">
  291. allowlist
  292. </a-checkbox>
  293. <a-checkbox
  294. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'disabled'"
  295. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : v.defaultValue)">
  296. disabled
  297. </a-checkbox>
  298. </template>
  299. <template v-else-if="['FEISHU_GROUP_POLICY', 'DISCORD_GROUP_POLICY', 'TELEGRAM_GROUP_POLICY'].includes(v.envKey)">
  300. <a-checkbox
  301. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'open'"
  302. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : '')">
  303. open
  304. </a-checkbox>
  305. <a-checkbox
  306. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'allowlist'"
  307. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : '')">
  308. allowlist
  309. </a-checkbox>
  310. <a-checkbox
  311. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'disabled'"
  312. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : '')">
  313. disabled
  314. </a-checkbox>
  315. </template>
  316. <template v-else-if="v.envKey === 'FEISHU_TYPING_INDICATOR'">
  317. <a-checkbox
  318. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  319. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  320. </template>
  321. <template v-else-if="v.envKey === 'FEISHU_RESOLVE_SENDER_NAMES'">
  322. <a-checkbox
  323. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  324. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  325. </template>
  326. <template v-else>
  327. <a-input-password
  328. v-if="isSecretEnvKey(v.envKey)"
  329. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  330. :placeholder="v.defaultValue || v.envKey"
  331. allow-clear
  332. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  333. <a-input
  334. v-else
  335. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  336. :placeholder="v.defaultValue || v.envKey"
  337. allow-clear
  338. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  339. </template>
  340. </a-form-item>
  341. </div>
  342. <a-collapse :bordered="false" v-if="channelAdvancedVars(section.vars).length">
  343. <a-collapse-panel :header="$t('common.adv_config')" key="advanced">
  344. <div v-for="v in channelAdvancedVars(section.vars)" :key="v.envKey" class="openclaw-new-blob-row mb-2">
  345. <a-form-item
  346. :label="v.envKey"
  347. :required="v.required"
  348. :extra="($te(v.descriptionKey) ? $t(v.descriptionKey) : '') + ' ' + (v.required ? $t('aice.openclaw.required_hint') : $t('aice.openclaw.channel_var_optional'))">
  349. <template v-if="v.envKey === 'FEISHU_DOMAIN'">
  350. <a-radio-group
  351. :value="((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue"
  352. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)">
  353. <a-radio value="feishu">feishu</a-radio>
  354. <a-radio value="lark">lark</a-radio>
  355. </a-radio-group>
  356. </template>
  357. <template v-else-if="['FEISHU_DM_POLICY', 'DISCORD_DM_POLICY', 'TELEGRAM_DM_POLICY'].includes(v.envKey)">
  358. <a-checkbox
  359. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'open'"
  360. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : v.defaultValue)">
  361. open
  362. </a-checkbox>
  363. <a-checkbox
  364. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'pairing'"
  365. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'pairing' : v.defaultValue)">
  366. pairing
  367. </a-checkbox>
  368. <a-checkbox
  369. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'allowlist'"
  370. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : v.defaultValue)">
  371. allowlist
  372. </a-checkbox>
  373. <a-checkbox
  374. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'disabled'"
  375. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : v.defaultValue)">
  376. disabled
  377. </a-checkbox>
  378. </template>
  379. <template v-else-if="['FEISHU_GROUP_POLICY', 'DISCORD_GROUP_POLICY', 'TELEGRAM_GROUP_POLICY'].includes(v.envKey)">
  380. <a-checkbox
  381. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'open'"
  382. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : '')">
  383. open
  384. </a-checkbox>
  385. <a-checkbox
  386. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'allowlist'"
  387. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : '')">
  388. allowlist
  389. </a-checkbox>
  390. <a-checkbox
  391. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'disabled'"
  392. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : '')">
  393. disabled
  394. </a-checkbox>
  395. </template>
  396. <template v-else-if="v.envKey === 'FEISHU_TYPING_INDICATOR'">
  397. <a-checkbox
  398. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  399. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  400. </template>
  401. <template v-else-if="v.envKey === 'FEISHU_RESOLVE_SENDER_NAMES'">
  402. <a-checkbox
  403. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  404. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  405. </template>
  406. <template v-else>
  407. <a-input-password
  408. v-if="isSecretEnvKey(v.envKey)"
  409. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  410. :placeholder="v.defaultValue || v.envKey"
  411. allow-clear
  412. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  413. <a-input
  414. v-else
  415. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  416. :placeholder="v.defaultValue || v.envKey"
  417. allow-clear
  418. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  419. </template>
  420. </a-form-item>
  421. </div>
  422. </a-collapse-panel>
  423. </a-collapse>
  424. </div>
  425. </template>
  426. </a-tab-pane>
  427. </a-tabs>
  428. </template>
  429. </template>
  430. <a-form-item :label="$t('compute.text_494')" :extra="$t('compute.text_495')">
  431. <a-switch v-decorator="decorators.auto_start" :checkedChildren="$t('compute.text_115')" :unCheckedChildren="$t('compute.text_116')" />
  432. </a-form-item>
  433. </a-form>
  434. </page-body>
  435. <page-footer>
  436. <template v-slot:right>
  437. <a-button type="primary" @click="handleConfirm">{{ $t('common.create') }}</a-button>
  438. <a-button class="ml-2" @click="handleCancel">{{ $t('common.cancel') }}</a-button>
  439. </template>
  440. </page-footer>
  441. </div>
  442. </template>
  443. <script>
  444. import * as R from 'ramda'
  445. import WindowsMixin from '@/mixins/windows'
  446. import validateForm from '@/utils/validate'
  447. import NameRepeated from '@/sections/NameRepeated'
  448. import { NETWORK_OPTIONS_MAP } from '@Compute/constants'
  449. import ServerNetwork from '@Compute/sections/ServerNetwork'
  450. import { LLM_TYPE_OPTIONS, getParamsForType } from '../llm-sku/llmTypeConfig'
  451. import { OPENCLAW_CHANNEL_SECTIONS, OPENCLAW_CHANNEL_OPTIONS } from '../llm-sku/openclawChannelConfig'
  452. import { OPENCLAW_PROVIDER_SECTIONS, OPENCLAW_PROVIDER_OPTIONS } from '../llm-sku/openclawProviderConfig'
  453. export default {
  454. name: 'LLMCreate',
  455. provide () {
  456. return {
  457. form: this.form,
  458. }
  459. },
  460. components: {
  461. NameRepeated,
  462. ServerNetwork,
  463. },
  464. mixins: [WindowsMixin],
  465. data () {
  466. const isApplyType = this.$route.path.includes('app-llm')
  467. 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')
  468. const defaultLlmType = (llmTypeOptions[0] && llmTypeOptions[0].id) || (isApplyType ? 'openclaw' : 'ollama')
  469. return {
  470. isApplyType,
  471. llmTypeOptions: llmTypeOptions.map(opt => ({ id: opt.id, name: this.$t(opt.name) })),
  472. collapseActive: [],
  473. loading: false,
  474. form: {
  475. fc: this.$form.createForm(this, {
  476. onValuesChange: (props, values) => {
  477. Object.keys(values).forEach((key) => {
  478. this.$set(this.form.fd, key, values[key])
  479. })
  480. },
  481. }),
  482. fd: {
  483. llm_type: defaultLlmType,
  484. },
  485. fi: {
  486. networkList: [],
  487. capability: {
  488. max_nic_count: 8,
  489. },
  490. },
  491. },
  492. decorators: {
  493. name: [
  494. 'name',
  495. {
  496. rules: [
  497. { required: true, validator: this.$validate('resourceName') },
  498. ],
  499. },
  500. ],
  501. llm_type: [
  502. 'llm_type',
  503. {
  504. initialValue: defaultLlmType,
  505. rules: [
  506. { required: true, message: this.$t('common.tips.select', [isApplyType ? this.$t('aice.llm_type.app') : this.$t('aice.llm_type.llm')]) },
  507. ],
  508. },
  509. ],
  510. llm_sku_id: [
  511. 'llm_sku_id',
  512. {
  513. rules: [
  514. { required: true, message: this.$t('common.tips.select', [this.$t('aice.llm_sku')]) },
  515. ],
  516. },
  517. ],
  518. // bandwidth_mb: [
  519. // 'bandwidth_mb',
  520. // {
  521. // initialValue: 30,
  522. // rules: [
  523. // { required: true, message: this.$t('common.tips.input', [this.$t('aice.bandwidth_mb')]) },
  524. // ],
  525. // },
  526. // ],
  527. network: {
  528. networkType: [
  529. 'networkType',
  530. {
  531. initialValue: NETWORK_OPTIONS_MAP.default.key,
  532. },
  533. ],
  534. networkConfig: {
  535. vpcs: i => [
  536. `vpcs[${i}]`,
  537. {
  538. validateTrigger: ['change', 'blur'],
  539. rules: [{
  540. required: true,
  541. message: this.$t('compute.text_194'),
  542. }],
  543. },
  544. ],
  545. networks: i => [
  546. `networks[${i}]`,
  547. {
  548. validateTrigger: ['change', 'blur'],
  549. rules: [{
  550. required: true,
  551. message: this.$t('compute.text_217'),
  552. }],
  553. },
  554. ],
  555. ips: (i, networkData) => [
  556. `networkIps[${i}]`,
  557. {
  558. validateFirst: true,
  559. validateTrigger: ['blur', 'change'],
  560. rules: [
  561. {
  562. validator: validateForm('IPv4'),
  563. },
  564. ],
  565. },
  566. ],
  567. macs: (i, networkData) => [
  568. `networkMacs[${i}]`,
  569. {
  570. validateFirst: true,
  571. validateTrigger: ['blur', 'change'],
  572. rules: [
  573. {
  574. validator: validateForm('mac'),
  575. },
  576. ],
  577. },
  578. ],
  579. },
  580. networkSchedtag: {
  581. schedtags: i => [
  582. `networkSchedtags[${i}]`,
  583. {
  584. validateTrigger: ['change', 'blur'],
  585. rules: [{
  586. required: true,
  587. message: this.$t('compute.text_123'),
  588. }],
  589. },
  590. ],
  591. policys: (i, networkData) => [
  592. `networkPolicys[${i}]`,
  593. {
  594. validateTrigger: ['blur', 'change'],
  595. rules: [{
  596. required: true,
  597. message: this.$t('common_256'),
  598. }],
  599. },
  600. ],
  601. },
  602. },
  603. prefer_host: [
  604. 'prefer_host',
  605. {
  606. rules: [
  607. { required: false, message: this.$t('common.tips.select', [this.$t('dictionary.host')]) },
  608. ],
  609. },
  610. ],
  611. auto_start: [
  612. 'auto_start',
  613. {
  614. valuePropName: 'checked',
  615. initialValue: true,
  616. },
  617. ],
  618. openclaw_channels: [
  619. 'openclaw_channels',
  620. { initialValue: [], rules: [] },
  621. ],
  622. },
  623. formItemLayout: {
  624. wrapperCol: {
  625. md: { span: 17 },
  626. xl: { span: 18 },
  627. xxl: { span: 20 },
  628. },
  629. labelCol: {
  630. md: { span: 7 },
  631. xl: { span: 6 },
  632. xxl: { span: 4 },
  633. },
  634. },
  635. openclawChannelCredentialMode: {},
  636. openclawChannelCredentialId: {},
  637. openclawChannelBlobKeys: {},
  638. openclawChannelExportKeys: {},
  639. openclawChannelBlob: {},
  640. openclawChannelActiveTab: '',
  641. OPENCLAW_PROVIDER_SECTIONS,
  642. openclawSelectedProviders: [],
  643. openclawProviderCredentialMode: {},
  644. openclawProviderCredentialId: {},
  645. openclawProviderBlobKeys: {},
  646. openclawProviderExportKeys: {},
  647. openclawProviderBlob: {},
  648. openclawProviderActiveTab: '',
  649. openclawPrimaryModelOptions: [],
  650. openclawPrimaryModelLoading: false,
  651. }
  652. },
  653. computed: {
  654. networkParams () {
  655. const ret = {
  656. scope: this.$store.getters.scope,
  657. limit: 20,
  658. usable: true,
  659. host_type: 'container',
  660. vpc: this.form.fd.vpc,
  661. }
  662. return ret
  663. },
  664. resourcesParams () {
  665. const schedtag = {
  666. limit: 1024,
  667. filter: ['resource_type.equals(networks)'],
  668. }
  669. return {
  670. schedtag,
  671. }
  672. },
  673. llmSkuParams () {
  674. return {
  675. limit: 20,
  676. scope: this.$store.getters.scope,
  677. ...getParamsForType(this.form.fd.llm_type),
  678. }
  679. },
  680. hostParams () {
  681. return {
  682. limit: 20,
  683. scope: this.$store.getters.scope,
  684. host_status: 'online',
  685. host_type: 'container',
  686. }
  687. },
  688. channelOptionsForSelect () {
  689. return OPENCLAW_CHANNEL_OPTIONS.map(opt => ({
  690. value: opt.value,
  691. label: this.$te(opt.label) ? this.$t(opt.label) : opt.label,
  692. }))
  693. },
  694. filteredChannelSections () {
  695. const channels = this.form.fd.openclaw_channels || []
  696. if (channels.length === 0) return []
  697. const set = new Set(channels)
  698. return OPENCLAW_CHANNEL_SECTIONS.filter(s => set.has(s.sectionKey))
  699. },
  700. providerOptionsForSelect () {
  701. return OPENCLAW_PROVIDER_OPTIONS.map(key => ({
  702. value: key,
  703. label: this.$te(key) ? this.$t(key) : key,
  704. }))
  705. },
  706. providerTabList () {
  707. const selected = this.openclawSelectedProviders || []
  708. if (selected.length === 0) return []
  709. const list = []
  710. selected.forEach(providerLabelKey => {
  711. const vars = []
  712. let required = false
  713. this.OPENCLAW_PROVIDER_SECTIONS.forEach(section => {
  714. section.vars.forEach(v => {
  715. if (v.providerLabelKey === providerLabelKey) {
  716. vars.push(v)
  717. if (section.required) required = true
  718. }
  719. })
  720. })
  721. if (vars.length) list.push({ key: providerLabelKey, labelKey: providerLabelKey, vars, required })
  722. })
  723. return list
  724. },
  725. openclawProviderActiveKey () {
  726. const list = this.providerTabList
  727. if (!list.length) return ''
  728. const keys = list.map(t => t.key)
  729. return keys.includes(this.openclawProviderActiveTab) ? this.openclawProviderActiveTab : (keys[0] || '')
  730. },
  731. /** 供 watch:OLLAMA_BASE_URL 变更时重新拉取主模型列表 */
  732. openclawOllamaBaseUrlForPrimaryModel () {
  733. const v = this.findOpenclawPrimaryModelVar()
  734. if (!v) return ''
  735. const pk = v.providerLabelKey
  736. return String((this.openclawProviderBlob[pk] || {}).OLLAMA_BASE_URL ?? '')
  737. },
  738. },
  739. watch: {
  740. openclawOllamaBaseUrlForPrimaryModel (newVal, oldVal) {
  741. if (oldVal === undefined) return
  742. if (newVal === oldVal) return
  743. const v = this.findOpenclawPrimaryModelVar()
  744. if (!v) return
  745. const pk = v.providerLabelKey
  746. const blob = this.openclawProviderBlob[pk] || {}
  747. const cur = blob.OPENCLAW_PRIMARY_MODEL
  748. const hasModel = cur !== undefined && cur !== null && String(cur).trim() !== ''
  749. if (!hasModel) {
  750. this.openclawPrimaryModelOptions = []
  751. return
  752. }
  753. if (newVal) {
  754. this.loadOpenclawPrimaryModelOptions(v, pk, '')
  755. } else {
  756. this.openclawPrimaryModelOptions = []
  757. }
  758. },
  759. 'form.fd.llm_type' (val, oldVal) {
  760. if (val === oldVal) return
  761. this.form.fc.setFieldsValue({ llm_sku_id: undefined })
  762. if (oldVal === 'openclaw' && val !== 'openclaw') {
  763. this.form.fc.setFieldsValue({ openclaw_channels: [] })
  764. this.$set(this.form.fd, 'openclaw_channels', [])
  765. this.openclawSelectedProviders = []
  766. this.openclawProviderActiveTab = ''
  767. this.openclawProviderCredentialMode = {}
  768. this.openclawProviderCredentialId = {}
  769. this.openclawProviderBlobKeys = {}
  770. this.openclawProviderExportKeys = {}
  771. }
  772. },
  773. filteredChannelSections (sections) {
  774. ;(sections || []).forEach(s => this.ensureChannelState(s.sectionKey))
  775. },
  776. providerTabList (list) {
  777. const keys = (list || []).map(t => t.key)
  778. if (keys.length && !keys.includes(this.openclawProviderActiveTab)) {
  779. this.openclawProviderActiveTab = keys[0] || ''
  780. }
  781. },
  782. openclawSelectedProviders: {
  783. handler (list) {
  784. ;(list || []).forEach((providerKey) => {
  785. this.ensureProviderState(providerKey)
  786. })
  787. },
  788. deep: true,
  789. },
  790. },
  791. created () {
  792. // 初始化 providers 的 blob 输入结构(按 key 分组)
  793. this.OPENCLAW_PROVIDER_SECTIONS.forEach(section => {
  794. section.vars.forEach(({ providerLabelKey, envKey, overrideUrlKey }) => {
  795. if (!this.openclawProviderBlob[providerLabelKey]) this.$set(this.openclawProviderBlob, providerLabelKey, {})
  796. if (this.openclawProviderBlob[providerLabelKey][envKey] === undefined) this.$set(this.openclawProviderBlob[providerLabelKey], envKey, '')
  797. if (overrideUrlKey) {
  798. if (this.openclawProviderBlob[providerLabelKey][overrideUrlKey] === undefined) this.$set(this.openclawProviderBlob[providerLabelKey], overrideUrlKey, '')
  799. }
  800. })
  801. })
  802. },
  803. methods: {
  804. genCredentialName ({ llmName, usage, key }) {
  805. const base = String(llmName || '').trim() || 'llm'
  806. const u = String(usage || '').trim() || 'unknown'
  807. const k = String(key || '').trim() || 'default'
  808. // 简单清洗:空白转为 -,并去掉多余的 -
  809. const normalize = (s) => String(s || '').trim()
  810. .replace(/\s+/g, '-')
  811. .replace(/-+/g, '-')
  812. .replace(/^-|-$/g, '')
  813. return normalize(`${base}-${u}-${k}`) || 'llm-credential'
  814. },
  815. credentialParamsForChannel (channelKey) {
  816. const filter = ['type.equals(container_secret)']
  817. if (this.$store.getters.scope === 'project') {
  818. const uid = this.$store.getters.userInfo?.id
  819. if (uid) filter.push(`user_id.equals(${uid})`)
  820. }
  821. const base = { scope: this.$store.getters.scope, filter }
  822. return {
  823. ...base,
  824. 'tags.0.key': 'user:openclaw_usage',
  825. 'tags.0.value': 'channel',
  826. 'tags.1.key': 'user:openclaw_name',
  827. 'tags.1.value': channelKey,
  828. }
  829. },
  830. ensureChannelState (channelKey) {
  831. if (!this.openclawChannelCredentialMode[channelKey]) this.$set(this.openclawChannelCredentialMode, channelKey, 'new')
  832. if (!this.openclawChannelCredentialId[channelKey]) this.$set(this.openclawChannelCredentialId, channelKey, undefined)
  833. if (!this.openclawChannelBlobKeys[channelKey]) this.$set(this.openclawChannelBlobKeys, channelKey, [])
  834. if (!this.openclawChannelExportKeys[channelKey]) this.$set(this.openclawChannelExportKeys, channelKey, [])
  835. if (!this.openclawChannelBlob[channelKey]) this.$set(this.openclawChannelBlob, channelKey, {})
  836. },
  837. async fetchCredentialBlobKeys (credentialId) {
  838. if (!credentialId) return []
  839. const manager = new this.$Manager('credentials', 'v1')
  840. const { data } = await manager.get({ id: credentialId })
  841. const blob = data?.blob
  842. let obj = blob
  843. if (typeof blob === 'string') {
  844. try { obj = JSON.parse(blob) } catch (e) { obj = {} }
  845. }
  846. if (obj && typeof obj === 'object') {
  847. return Object.keys(obj)
  848. }
  849. return []
  850. },
  851. async onChannelCredentialChange (channelKey, credentialId) {
  852. this.ensureChannelState(channelKey)
  853. this.$set(this.openclawChannelCredentialId, channelKey, credentialId)
  854. this.$set(this.openclawChannelExportKeys, channelKey, [])
  855. try {
  856. const keys = await this.fetchCredentialBlobKeys(credentialId)
  857. this.$set(this.openclawChannelBlobKeys, channelKey, keys)
  858. this.$set(this.openclawChannelExportKeys, channelKey, [...keys])
  859. } catch (e) {
  860. this.$set(this.openclawChannelBlobKeys, channelKey, [])
  861. }
  862. },
  863. credentialParamsForProvider (providerKey) {
  864. const filter = ['type.equals(container_secret)']
  865. if (this.$store.getters.scope === 'project') {
  866. const uid = this.$store.getters.userInfo?.id
  867. if (uid) filter.push(`user_id.equals(${uid})`)
  868. }
  869. const base = { scope: this.$store.getters.scope, filter }
  870. const shortName = this.providerShortName(providerKey)
  871. return {
  872. ...base,
  873. 'tags.0.key': 'user:openclaw_usage',
  874. 'tags.0.value': 'provider',
  875. 'tags.1.key': 'user:openclaw_name',
  876. 'tags.1.value': shortName,
  877. }
  878. },
  879. providerShortName (providerKey) {
  880. const s = String(providerKey || '')
  881. const parts = s.split('.')
  882. return parts[parts.length - 1] || s
  883. },
  884. ensureProviderState (providerKey) {
  885. if (!this.openclawProviderCredentialMode[providerKey]) this.$set(this.openclawProviderCredentialMode, providerKey, 'new')
  886. if (!this.openclawProviderCredentialId[providerKey]) this.$set(this.openclawProviderCredentialId, providerKey, undefined)
  887. if (!this.openclawProviderBlobKeys[providerKey]) this.$set(this.openclawProviderBlobKeys, providerKey, [])
  888. if (!this.openclawProviderExportKeys[providerKey]) this.$set(this.openclawProviderExportKeys, providerKey, [])
  889. if (!this.openclawProviderBlob[providerKey]) this.$set(this.openclawProviderBlob, providerKey, {})
  890. },
  891. async onProviderCredentialChange (providerKey, credentialId) {
  892. this.ensureProviderState(providerKey)
  893. this.$set(this.openclawProviderCredentialId, providerKey, credentialId)
  894. this.$set(this.openclawProviderExportKeys, providerKey, [])
  895. try {
  896. const keys = await this.fetchCredentialBlobKeys(credentialId)
  897. this.$set(this.openclawProviderBlobKeys, providerKey, keys)
  898. this.$set(this.openclawProviderExportKeys, providerKey, [...keys])
  899. } catch (e) {
  900. this.$set(this.openclawProviderBlobKeys, providerKey, [])
  901. }
  902. },
  903. closeProviderTab (providerKey) {
  904. const next = this.openclawSelectedProviders.filter(k => k !== providerKey)
  905. this.openclawSelectedProviders = next
  906. if (this.openclawProviderActiveTab === providerKey && next.length > 0) {
  907. this.openclawProviderActiveTab = next[0]
  908. } else if (next.length === 0) {
  909. this.openclawProviderActiveTab = ''
  910. }
  911. this.$delete(this.openclawProviderCredentialMode, providerKey)
  912. this.$delete(this.openclawProviderCredentialId, providerKey)
  913. this.$delete(this.openclawProviderBlobKeys, providerKey)
  914. this.$delete(this.openclawProviderExportKeys, providerKey)
  915. },
  916. filterProviderOption (input, option) {
  917. const value = option.componentOptions && option.componentOptions.propsData && option.componentOptions.propsData.value
  918. if (value == null) return true
  919. const label = this.$te(value) ? this.$t(value) : String(value)
  920. return label.toLowerCase().indexOf((input || '').toLowerCase()) >= 0
  921. },
  922. closeChannelTab (sectionKey) {
  923. const channels = (this.form.fd.openclaw_channels || []).filter(k => k !== sectionKey)
  924. this.form.fc.setFieldsValue({ openclaw_channels: channels })
  925. this.$set(this.form.fd, 'openclaw_channels', channels)
  926. if (this.openclawChannelActiveTab === sectionKey && channels.length > 0) {
  927. this.openclawChannelActiveTab = channels[0]
  928. } else if (channels.length === 0) {
  929. this.openclawChannelActiveTab = ''
  930. }
  931. this.$delete(this.openclawChannelCredentialMode, sectionKey)
  932. this.$delete(this.openclawChannelCredentialId, sectionKey)
  933. this.$delete(this.openclawChannelBlobKeys, sectionKey)
  934. this.$delete(this.openclawChannelExportKeys, sectionKey)
  935. },
  936. filterChannelOption (input, option) {
  937. const value = option.componentOptions && option.componentOptions.propsData && option.componentOptions.propsData.value
  938. if (value == null) return true
  939. const opt = this.channelOptionsForSelect.find(o => o.value === value)
  940. const label = opt ? opt.label : (this.$te(value) ? this.$t(value) : String(value))
  941. return String(label).toLowerCase().indexOf((input || '').toLowerCase()) >= 0
  942. },
  943. channelBasicVars (vars) {
  944. return (vars || []).filter(v => !v.advanced)
  945. },
  946. channelAdvancedVars (vars) {
  947. return (vars || []).filter(v => !!v.advanced)
  948. },
  949. sectionBasicEnvKeys (section, envKeys) {
  950. if (!section) return envKeys || []
  951. const basicKeys = new Set(this.channelBasicVars(section.vars).map(v => v.envKey))
  952. return (envKeys || []).filter(k => basicKeys.has(k))
  953. },
  954. sectionAdvancedEnvKeys (section, envKeys) {
  955. if (!section) return []
  956. const basicKeys = new Set(this.channelBasicVars(section.vars).map(v => v.envKey))
  957. return (envKeys || []).filter(k => !basicKeys.has(k))
  958. },
  959. isSecretEnvKey (envKey) {
  960. const lower = (envKey || '').toLowerCase()
  961. return lower.includes('key') || lower.includes('secret') || lower.includes('token') || lower.includes('password')
  962. },
  963. findOpenclawPrimaryModelVar () {
  964. const sections = this.OPENCLAW_PROVIDER_SECTIONS || []
  965. for (let i = 0; i < sections.length; i++) {
  966. const vars = sections[i].vars || []
  967. for (let j = 0; j < vars.length; j++) {
  968. const x = vars[j]
  969. if (x.envKey === 'OPENCLAW_PRIMARY_MODEL' && x.component === 'a-select') return x
  970. }
  971. }
  972. return null
  973. },
  974. /** a-select 等可能为数字,不能用 (raw[k] || '').trim(),否则主模型等字段不会写入 credential blob */
  975. pickTrimmedOpenclawBlob (raw) {
  976. const blob = {}
  977. Object.keys(raw || {}).forEach(k => {
  978. const val = raw[k]
  979. if (val === undefined || val === null) return
  980. const s = typeof val === 'string' ? val.trim() : String(val).trim()
  981. if (s !== '') blob[k] = k === 'OPENCLAW_PRIMARY_MODEL' ? `ollama/${s}` : s
  982. })
  983. return blob
  984. },
  985. validateOpenclawProviderRequiredEnv (providerKey) {
  986. const raw = this.openclawProviderBlob[providerKey] || {}
  987. const sections = this.OPENCLAW_PROVIDER_SECTIONS || []
  988. for (let i = 0; i < sections.length; i++) {
  989. const vars = sections[i].vars || []
  990. for (let j = 0; j < vars.length; j++) {
  991. const v = vars[j]
  992. if (v.providerLabelKey !== providerKey || !v.required) continue
  993. const val = raw[v.envKey]
  994. const empty = val === undefined || val === null || val === '' ||
  995. (typeof val === 'string' && !String(val).trim())
  996. if (empty) {
  997. const label = v.placeholderKey && this.$te(v.placeholderKey)
  998. ? this.$t(v.placeholderKey)
  999. : (this.$te(v.descriptionKey) ? this.$t(v.descriptionKey) : v.envKey)
  1000. const tip = v.component === 'a-select'
  1001. ? this.$t('common.tips.select', [label])
  1002. : this.$t('common.tips.input', [label])
  1003. this.$message.warning(tip)
  1004. return false
  1005. }
  1006. }
  1007. }
  1008. return true
  1009. },
  1010. /** 全量拉取 models 后:已选值若不在列表中则清空(搜索分页结果不调用) */
  1011. syncOpenclawPrimaryModelIfNotInOptions (providerKey) {
  1012. const blob = (this.openclawProviderBlob || {})[providerKey]
  1013. if (!blob) return
  1014. const cur = blob.OPENCLAW_PRIMARY_MODEL
  1015. const hasModel = cur !== undefined && cur !== null && String(cur).trim() !== ''
  1016. if (!hasModel) return
  1017. const opts = this.openclawPrimaryModelOptions || []
  1018. const inList = opts.some(opt => String(opt.value) === String(cur))
  1019. if (!inList) {
  1020. this.$set(this.openclawProviderBlob[providerKey], 'OPENCLAW_PRIMARY_MODEL', '')
  1021. }
  1022. },
  1023. openclawProviderAselectPlaceholder (v) {
  1024. const labelKey = v.placeholderKey || 'aice.model'
  1025. const label = this.$te(labelKey) ? this.$t(labelKey) : labelKey
  1026. return this.$t('common.tips.select', [label])
  1027. },
  1028. /** 与 BaseSelect 的 refresh 一致:重新走 $Manager(resource).create 拉列表 */
  1029. refreshOpenclawPrimaryModel (v, providerKey) {
  1030. this.loadOpenclawPrimaryModelOptions(v, providerKey, '')
  1031. },
  1032. async onOpenclawPrimaryModelDropdown (open, v, providerKey) {
  1033. if (!open) return
  1034. await this.loadOpenclawPrimaryModelOptions(v, providerKey, '')
  1035. },
  1036. onOpenclawPrimaryModelSearch (q, v, providerKey) {
  1037. if (this._openclawPrimaryModelSearchTimer) clearTimeout(this._openclawPrimaryModelSearchTimer)
  1038. this._openclawPrimaryModelSearchTimer = setTimeout(() => {
  1039. this.loadOpenclawPrimaryModelOptions(v, providerKey, q || '')
  1040. }, 300)
  1041. },
  1042. async loadOpenclawPrimaryModelOptions (v, providerKey, search) {
  1043. const resource = (v && v.resource) || 'llms/provider-models'
  1044. const blob = (this.openclawProviderBlob || {})[providerKey] || {}
  1045. this.openclawPrimaryModelLoading = true
  1046. try {
  1047. const manager = new this.$Manager(resource, 'v2')
  1048. const body = {
  1049. scope: this.$store.getters.scope,
  1050. limit: 20,
  1051. url: blob.OLLAMA_BASE_URL,
  1052. provider_type: 'ollama',
  1053. }
  1054. if (search) body.filter = [`name.contains(${search})`]
  1055. const { data } = await manager.create({
  1056. data: body,
  1057. params: { ignoreErrorStatusCode: [400, 502] },
  1058. })
  1059. const rows = this.unwrapOpenclawPrimaryModelResponse(data)
  1060. this.openclawPrimaryModelOptions = rows.map(item => ({
  1061. value: item,
  1062. label: item,
  1063. })).filter(opt => opt.value !== undefined && opt.value !== null && opt.value !== '')
  1064. if (!search) {
  1065. this.syncOpenclawPrimaryModelIfNotInOptions(providerKey)
  1066. }
  1067. } catch (e) {
  1068. this.openclawPrimaryModelOptions = []
  1069. } finally {
  1070. this.openclawPrimaryModelLoading = false
  1071. }
  1072. },
  1073. unwrapOpenclawPrimaryModelResponse (data) {
  1074. console.log('unwrapOpenclawPrimaryModelResponse', data)
  1075. return data?.models || []
  1076. },
  1077. overrideUrlPlaceholder (overrideUrlKey) {
  1078. const defaults = {
  1079. MOONSHOT_BASE_URL: 'https://api.moonshot.cn/v1',
  1080. KIMI_BASE_URL: 'https://api.moonshot.ai/anthropic',
  1081. }
  1082. return defaults[overrideUrlKey] || 'https://...'
  1083. },
  1084. networkResourceMapper (list) {
  1085. return (list || []).map(val => {
  1086. const isHostSubnet = val.server_type === 'host'
  1087. if (!isHostSubnet) return val
  1088. return {
  1089. ...val,
  1090. name: `${val.name}(Host IP 子网)`,
  1091. }
  1092. })
  1093. },
  1094. async genNetworks (values) {
  1095. let ret = [{ exit: false }]
  1096. // 指定 IP 子网
  1097. if (this.form.fd.networkType === NETWORK_OPTIONS_MAP.manual.key) {
  1098. ret = []
  1099. R.forEachObjIndexed((value, key) => {
  1100. const obj = {
  1101. network: value,
  1102. }
  1103. if (this.form.fd.networkIps) {
  1104. const address = this.form.fd.networkIps[key]
  1105. if (address) {
  1106. obj.address = address
  1107. }
  1108. }
  1109. if (this.form.fd.networkMacs) {
  1110. const mac = this.form.fd.networkMacs[key]
  1111. if (mac) {
  1112. obj.mac = mac
  1113. }
  1114. }
  1115. ret.push(obj)
  1116. }, values.networks)
  1117. }
  1118. // 指定 调度标签
  1119. if (this.form.fd.networkType === NETWORK_OPTIONS_MAP.schedtag.key) {
  1120. ret = []
  1121. R.forEachObjIndexed((value, key) => {
  1122. const obj = {
  1123. id: value,
  1124. }
  1125. const strategy = this.form.fd.networkPolicys[key]
  1126. if (strategy) {
  1127. obj.strategy = strategy
  1128. }
  1129. ret.push({
  1130. schedtags: [obj],
  1131. })
  1132. }, values.networkSchedtags)
  1133. }
  1134. return ret
  1135. },
  1136. async handleConfirm () {
  1137. this.loading = true
  1138. try {
  1139. const values = await this.form.fc.validateFields()
  1140. const networks = await this.genNetworks(values)
  1141. const data = {
  1142. generate_name: values.name,
  1143. llm_sku_id: values.llm_sku_id,
  1144. // bandwidth_mb: values.bandwidth_mb,
  1145. auto_start: values.auto_start,
  1146. nets: networks,
  1147. }
  1148. if (this.collapseActive.includes('1') && values.prefer_host) {
  1149. data.prefer_host = values.prefer_host
  1150. }
  1151. if (this.form.fd.llm_type === 'openclaw') {
  1152. const openclaw = {}
  1153. const channelsSelected = values.openclaw_channels || []
  1154. const channels = []
  1155. const credManager = new this.$Manager('credentials', 'v1')
  1156. for (let i = 0; i < channelsSelected.length; i++) {
  1157. const channelKey = channelsSelected[i]
  1158. this.ensureChannelState(channelKey)
  1159. const mode = this.openclawChannelCredentialMode[channelKey] || 'new'
  1160. let credentialId
  1161. let exportKeys
  1162. if (mode === 'existing') {
  1163. credentialId = this.openclawChannelCredentialId[channelKey]
  1164. if (!credentialId) {
  1165. this.$message.warning(this.$t('common.tips.select', [this.$t('aice.container_secret')]))
  1166. this.loading = false
  1167. return
  1168. }
  1169. exportKeys = this.openclawChannelExportKeys[channelKey] || []
  1170. if (channelKey === 'qqbot') {
  1171. const requiredKeys = ['QQBOT_APP_ID', 'QQBOT_CLIENT_SECRET']
  1172. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1173. if (missing.length) {
  1174. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1175. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1176. this.loading = false
  1177. return
  1178. }
  1179. }
  1180. if (channelKey === 'feishu') {
  1181. const requiredKeys = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET']
  1182. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1183. if (missing.length) {
  1184. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1185. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1186. this.loading = false
  1187. return
  1188. }
  1189. }
  1190. if (channelKey === 'discord') {
  1191. const requiredKeys = ['DISCORD_BOT_TOKEN']
  1192. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1193. if (missing.length) {
  1194. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1195. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1196. this.loading = false
  1197. return
  1198. }
  1199. }
  1200. if (channelKey === 'telegram') {
  1201. const requiredKeys = ['TELEGRAM_BOT_TOKEN']
  1202. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1203. if (missing.length) {
  1204. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1205. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1206. this.loading = false
  1207. return
  1208. }
  1209. }
  1210. } else {
  1211. const credName = this.genCredentialName({ llmName: values.name, usage: 'channel', key: channelKey })
  1212. const raw = this.openclawChannelBlob[channelKey] || {}
  1213. const blob = this.pickTrimmedOpenclawBlob(raw)
  1214. if (channelKey === 'qqbot') {
  1215. const missing = ['QQBOT_APP_ID', 'QQBOT_CLIENT_SECRET'].filter(k => !blob[k])
  1216. if (missing.length) {
  1217. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1218. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1219. this.loading = false
  1220. return
  1221. }
  1222. }
  1223. if (channelKey === 'feishu') {
  1224. const missing = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'].filter(k => !blob[k])
  1225. if (missing.length) {
  1226. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1227. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1228. this.loading = false
  1229. return
  1230. }
  1231. // 单选/勾选控件可能只显示 defaultValue,但不一定触发 @change 写入 blob
  1232. // 这里兜底补齐默认值,确保这些字段会写入 credential blob 与 export_keys
  1233. if (!blob.FEISHU_DOMAIN) blob.FEISHU_DOMAIN = 'feishu'
  1234. if (!blob.FEISHU_DM_POLICY) blob.FEISHU_DM_POLICY = 'open'
  1235. if (!blob.FEISHU_TYPING_INDICATOR) blob.FEISHU_TYPING_INDICATOR = 'true'
  1236. if (!blob.FEISHU_RESOLVE_SENDER_NAMES) blob.FEISHU_RESOLVE_SENDER_NAMES = 'true'
  1237. }
  1238. if (channelKey === 'discord') {
  1239. const missing = ['DISCORD_BOT_TOKEN'].filter(k => !blob[k])
  1240. if (missing.length) {
  1241. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1242. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1243. this.loading = false
  1244. return
  1245. }
  1246. if (!blob.DISCORD_DM_POLICY) blob.DISCORD_DM_POLICY = 'open'
  1247. }
  1248. if (channelKey === 'telegram') {
  1249. const missing = ['TELEGRAM_BOT_TOKEN'].filter(k => !blob[k])
  1250. if (missing.length) {
  1251. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1252. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1253. this.loading = false
  1254. return
  1255. }
  1256. if (!blob.TELEGRAM_DM_POLICY) blob.TELEGRAM_DM_POLICY = 'open'
  1257. }
  1258. if (Object.keys(blob).length === 0) {
  1259. this.$message.warning(this.$t('aice.openclaw.provider_filter_empty'))
  1260. this.loading = false
  1261. return
  1262. }
  1263. const { data: credData } = await credManager.create({
  1264. data: {
  1265. type: 'container_secret',
  1266. name: credName,
  1267. blob,
  1268. __meta__: {
  1269. 'user:openclaw_usage': 'channel',
  1270. 'user:openclaw_name': channelKey,
  1271. },
  1272. },
  1273. })
  1274. credentialId = credData.id
  1275. exportKeys = Object.keys(blob)
  1276. }
  1277. channels.push({
  1278. name: channelKey,
  1279. credential: { id: credentialId, export_keys: exportKeys },
  1280. })
  1281. }
  1282. if (channels.length) openclaw.channels = channels
  1283. const providersSelected = this.openclawSelectedProviders || []
  1284. if (!providersSelected.length) {
  1285. this.$message.warning(this.$t('aice.openclaw.provider_select_first'))
  1286. this.loading = false
  1287. return
  1288. }
  1289. const providers = []
  1290. for (let i = 0; i < providersSelected.length; i++) {
  1291. const providerKey = providersSelected[i]
  1292. this.ensureProviderState(providerKey)
  1293. if (!this.validateOpenclawProviderRequiredEnv(providerKey)) {
  1294. this.loading = false
  1295. return
  1296. }
  1297. const mode = this.openclawProviderCredentialMode[providerKey] || 'new'
  1298. let credentialId
  1299. let exportKeys
  1300. if (mode === 'existing') {
  1301. credentialId = this.openclawProviderCredentialId[providerKey]
  1302. if (!credentialId) {
  1303. this.$message.warning(this.$t('common.tips.select', [this.$t('aice.container_secret')]))
  1304. this.loading = false
  1305. return
  1306. }
  1307. exportKeys = this.openclawProviderExportKeys[providerKey] || []
  1308. } else {
  1309. const credName = this.genCredentialName({ llmName: values.name, usage: 'provider', key: this.providerShortName(providerKey) })
  1310. const raw = this.openclawProviderBlob[providerKey] || {}
  1311. const blob = this.pickTrimmedOpenclawBlob(raw)
  1312. if (Object.keys(blob).length === 0) {
  1313. this.$message.warning(this.$t('aice.openclaw.ai_providers.at_least_one'))
  1314. this.loading = false
  1315. return
  1316. }
  1317. const { data: credData } = await credManager.create({
  1318. data: {
  1319. type: 'container_secret',
  1320. name: credName,
  1321. blob,
  1322. __meta__: {
  1323. 'user:openclaw_usage': 'provider',
  1324. 'user:openclaw_name': this.providerShortName(providerKey),
  1325. },
  1326. },
  1327. })
  1328. credentialId = credData.id
  1329. exportKeys = Object.keys(blob)
  1330. }
  1331. providers.push({
  1332. name: this.providerShortName(providerKey),
  1333. credential: { id: credentialId, export_keys: exportKeys },
  1334. })
  1335. }
  1336. if (providers.length) openclaw.providers = providers
  1337. if (Object.keys(openclaw).length) data.llm_spec = { openclaw }
  1338. }
  1339. await new this.$Manager('llms', 'v1').create({
  1340. data,
  1341. })
  1342. this.$message.success(this.$t('common.success'))
  1343. this.$router.push(this.isApplyType ? '/app-llm' : '/llm')
  1344. } catch (error) {
  1345. throw error
  1346. } finally {
  1347. this.loading = false
  1348. }
  1349. },
  1350. handleCancel () {
  1351. this.$router.push(this.isApplyType ? '/app-llm' : '/llm')
  1352. },
  1353. },
  1354. }
  1355. </script>
  1356. <style scoped>
  1357. .llm-type-picker ::v-deep .ant-radio-button-wrapper {
  1358. height: 36px;
  1359. line-height: 34px;
  1360. border-radius: 4px;
  1361. margin-right: 8px;
  1362. margin-bottom: 8px;
  1363. }
  1364. .llm-type-picker ::v-deep .ant-radio-button-wrapper:first-child {
  1365. border-radius: 4px;
  1366. }
  1367. .llm-type-picker ::v-deep .ant-radio-button-wrapper:last-child {
  1368. border-radius: 4px;
  1369. }
  1370. .openclaw-channel-tabs { margin-top: 8px; }
  1371. .openclaw-provider-tabs { margin-top: 8px; }
  1372. .openclaw-section-divider { margin-top: 20px; }
  1373. .openclaw-tab-with-close { display: inline-flex; align-items: center; gap: 6px; }
  1374. .openclaw-tab-close { font-size: 12px; cursor: pointer; opacity: 0.6; }
  1375. .openclaw-tab-close:hover { opacity: 1; }
  1376. .openclaw-new-blob-section { margin-top: 8px; }
  1377. .openclaw-new-blob-row ::v-deep .ant-form-item-label { padding-bottom: 4px; }
  1378. .openclaw-filter-empty { padding: 12px 0; font-size: 13px; }
  1379. </style>