UpdateSpec.vue 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. <template>
  2. <base-dialog :width="900" @cancel="cancelDialog">
  3. <div slot="header">{{ $t('aice.llm_spec_update') }}</div>
  4. <div slot="body">
  5. <a-form :form="form.fc" hideRequiredMark v-bind="formItemLayout">
  6. <template v-if="isOpenclaw">
  7. <a-divider orientation="left" class="openclaw-section-divider">{{ $t('aice.openclaw.section.ai_providers') }}</a-divider>
  8. <a-form-item :label="$t('aice.openclaw.provider_filter')" :extra="$t('aice.openclaw.provider_select_tip')">
  9. <a-select
  10. v-model="openclawSelectedProviders"
  11. mode="multiple"
  12. :placeholder="$t('aice.openclaw.provider_filter_placeholder')"
  13. allow-clear
  14. show-search
  15. :filter-option="filterProviderOption"
  16. style="width: 100%; max-width: 400px;">
  17. <a-select-option v-for="opt in providerOptionsForSelect" :key="opt.value" :value="opt.value">
  18. {{ opt.label }}
  19. </a-select-option>
  20. </a-select>
  21. </a-form-item>
  22. <a-tabs
  23. v-if="providerTabList.length > 0"
  24. :activeKey="openclawProviderActiveKey"
  25. type="card"
  26. class="openclaw-provider-tabs"
  27. :animated="false"
  28. @change="openclawProviderActiveTab = $event">
  29. <a-tab-pane
  30. v-for="item in providerTabList"
  31. :key="item.key"
  32. :forceRender="true">
  33. <span slot="tab" class="openclaw-tab-with-close">
  34. {{ $t(item.labelKey) }}{{ item.required ? ' *' : '' }}
  35. <a-icon type="close" class="openclaw-tab-close" @click.prevent.stop="closeProviderTab(item.key)" />
  36. </span>
  37. <a-form-item :label="$t('aice.openclaw.credential_mode.label')">
  38. <a-radio-group
  39. :value="openclawProviderCredentialMode[item.key] || 'new'"
  40. @change="e => $set(openclawProviderCredentialMode, item.key, e.target.value)">
  41. <a-radio value="new">{{ $t('aice.openclaw.credential_mode.new') }}</a-radio>
  42. <a-radio value="existing">{{ $t('aice.openclaw.credential_mode.existing') }}</a-radio>
  43. </a-radio-group>
  44. </a-form-item>
  45. <template v-if="(openclawProviderCredentialMode[item.key] || 'new') === 'existing'">
  46. <a-form-item :label="$t('aice.container_secret')">
  47. <base-select
  48. ref="credentialSelects"
  49. :ref-in-for="true"
  50. v-model="openclawProviderCredentialId[item.key]"
  51. resource="credentials"
  52. :params="credentialParamsForProvider(item.key)"
  53. :label-format="credentialLabelFormat"
  54. :extra-opts="credentialExtraOpts(openclawProviderCredentialId[item.key])"
  55. :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.container_secret')]), optionLabelProp: 'label' }"
  56. @change="val => onProviderCredentialChange(item.key, val)" />
  57. </a-form-item>
  58. <a-form-item :label="$t('aice.container_secret.export_keys')" :extra="$t('aice.container_secret.export_keys_tip')">
  59. <a-checkbox-group
  60. :value="openclawProviderExportKeys[item.key] || []"
  61. @change="val => $set(openclawProviderExportKeys, item.key, val)">
  62. <a-checkbox v-for="k in (openclawProviderBlobKeys[item.key] || [])" :key="k" :value="k">{{ k }}</a-checkbox>
  63. </a-checkbox-group>
  64. </a-form-item>
  65. </template>
  66. <template v-else>
  67. <div class="openclaw-auto-credential-name text-color-secondary mb-2">
  68. {{ $t('aice.openclaw.new_credential_name') }}:{{ genCredentialName({ llmName: instanceName, usage: 'provider', key: providerShortName(item.key) }) }}
  69. </div>
  70. <div class="openclaw-new-blob-section">
  71. <div v-for="v in item.vars" :key="v.envKey" class="openclaw-new-blob-row mb-2">
  72. <a-form-item
  73. :label="v.envKey"
  74. :required="item.required || v.required"
  75. :extra="((item.required || v.required) ? $t('aice.openclaw.required_hint') + ' ' : '') + ($te(v.descriptionKey) ? $t(v.descriptionKey) : '')">
  76. <a-input-password
  77. v-if="isSecretEnvKey(v.envKey)"
  78. :value="(openclawProviderBlob[item.key] || {})[v.envKey]"
  79. :placeholder="v.envKey"
  80. allow-clear
  81. @change="e => $set(openclawProviderBlob[item.key], v.envKey, e.target.value)" />
  82. <div
  83. v-else-if="v.component === 'a-select'"
  84. class="d-flex align-items-center openclaw-primary-model-select-row">
  85. <a-select
  86. class="flex-grow-1"
  87. style="min-width: 0"
  88. :value="(openclawProviderBlob[item.key] || {})[v.envKey] || undefined"
  89. :placeholder="openclawProviderAselectPlaceholder(v)"
  90. allow-clear
  91. show-search
  92. :filter-option="false"
  93. :loading="openclawPrimaryModelLoading"
  94. @dropdownVisibleChange="open => onOpenclawPrimaryModelDropdown(open, v, item.key)"
  95. @search="q => onOpenclawPrimaryModelSearch(q, v, item.key)"
  96. @change="val => $set(openclawProviderBlob[item.key], v.envKey, val)">
  97. <a-select-option
  98. v-for="opt in openclawPrimaryModelOptions"
  99. :key="String(opt.value)"
  100. :value="opt.value">
  101. {{ opt.label }}
  102. </a-select-option>
  103. </a-select>
  104. <a-icon
  105. type="sync"
  106. class="ml-2 primary-color flex-shrink-0"
  107. :spin="openclawPrimaryModelLoading"
  108. @click="refreshOpenclawPrimaryModel(v, item.key)" />
  109. </div>
  110. <a-input
  111. v-else
  112. :value="(openclawProviderBlob[item.key] || {})[v.envKey]"
  113. :placeholder="v.envKey"
  114. allow-clear
  115. @change="e => $set(openclawProviderBlob[item.key], v.envKey, e.target.value)" />
  116. </a-form-item>
  117. <a-form-item
  118. v-if="v.overrideUrlKey"
  119. :label="v.overrideUrlKey"
  120. :extra="($te('aice.openclaw.env.' + v.overrideUrlKey) ? $t('aice.openclaw.env.' + v.overrideUrlKey) : '') + ' ' + $t('aice.openclaw.override_url_optional')"
  121. class="openclaw-override-url mt-1">
  122. <a-input
  123. :value="(openclawProviderBlob[item.key] || {})[v.overrideUrlKey]"
  124. :placeholder="overrideUrlPlaceholder(v.overrideUrlKey)"
  125. allow-clear
  126. @change="e => $set(openclawProviderBlob[item.key], v.overrideUrlKey, e.target.value)" />
  127. </a-form-item>
  128. </div>
  129. </div>
  130. </template>
  131. </a-tab-pane>
  132. </a-tabs>
  133. <div v-else class="openclaw-filter-empty text-color-secondary">
  134. {{ openclawSelectedProviders.length === 0 ? $t('aice.openclaw.provider_select_first') : $t('aice.openclaw.provider_filter_empty') }}
  135. </div>
  136. <a-divider orientation="left" class="openclaw-section-divider">{{ $t('aice.openclaw.section.chat_channels') }}</a-divider>
  137. <a-form-item :label="$t('aice.openclaw.channels')" :extra="$t('aice.openclaw.channels_extra')">
  138. <a-select
  139. v-decorator="decorators.openclaw_channels"
  140. mode="multiple"
  141. :placeholder="$t('aice.openclaw.channel_select_placeholder')"
  142. allow-clear
  143. show-search
  144. :filter-option="filterChannelOption"
  145. style="width: 100%; max-width: 400px;">
  146. <a-select-option v-for="opt in channelOptionsForSelect" :key="opt.value" :value="opt.value">
  147. {{ opt.label }}
  148. </a-select-option>
  149. </a-select>
  150. </a-form-item>
  151. <template v-if="filteredChannelSections.length > 0">
  152. <div class="openclaw-channel-config-hint text-color-secondary mb-2">
  153. {{ $t('aice.openclaw.channel_config_hint') }}
  154. </div>
  155. <a-tabs
  156. :activeKey="openclawChannelActiveTab || (filteredChannelSections[0] && filteredChannelSections[0].sectionKey)"
  157. class="openclaw-channel-tabs"
  158. :animated="false"
  159. @change="openclawChannelActiveTab = $event">
  160. <a-tab-pane
  161. v-for="section in filteredChannelSections"
  162. :key="section.sectionKey"
  163. :forceRender="true">
  164. <span slot="tab" class="openclaw-tab-with-close">
  165. {{ $t(section.sectionLabelKey) }}
  166. <a-icon type="close" class="openclaw-tab-close" @click.prevent.stop="closeChannelTab(section.sectionKey)" />
  167. </span>
  168. <a-form-item :label="$t('aice.openclaw.credential_mode.label')">
  169. <a-radio-group
  170. :value="openclawChannelCredentialMode[section.sectionKey] || 'new'"
  171. @change="e => $set(openclawChannelCredentialMode, section.sectionKey, e.target.value)">
  172. <a-radio value="new">{{ $t('aice.openclaw.credential_mode.new') }}</a-radio>
  173. <a-radio value="existing">{{ $t('aice.openclaw.credential_mode.existing') }}</a-radio>
  174. </a-radio-group>
  175. </a-form-item>
  176. <template v-if="(openclawChannelCredentialMode[section.sectionKey] || 'new') === 'existing'">
  177. <a-form-item :label="$t('aice.container_secret')">
  178. <base-select
  179. ref="credentialSelects"
  180. :ref-in-for="true"
  181. v-model="openclawChannelCredentialId[section.sectionKey]"
  182. resource="credentials"
  183. :params="credentialParamsForChannel(section.sectionKey)"
  184. :label-format="credentialLabelFormat"
  185. :extra-opts="credentialExtraOpts(openclawChannelCredentialId[section.sectionKey])"
  186. :selectProps="{ placeholder: $t('common.tips.select', [$t('aice.container_secret')]), optionLabelProp: 'label' }"
  187. @change="val => onChannelCredentialChange(section.sectionKey, val)" />
  188. </a-form-item>
  189. <a-form-item :label="$t('aice.container_secret.export_keys')" :extra="$t('aice.container_secret.export_keys_tip')">
  190. <a-checkbox-group
  191. :value="openclawChannelExportKeys[section.sectionKey] || []"
  192. @change="val => $set(openclawChannelExportKeys, section.sectionKey, val)">
  193. <a-checkbox
  194. v-for="k in sectionBasicEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || [])"
  195. :key="k"
  196. :value="k">{{ k }}</a-checkbox>
  197. <a-collapse :bordered="false" v-if="sectionAdvancedEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || []).length">
  198. <a-collapse-panel :header="$t('common.adv_config')" key="advanced">
  199. <a-checkbox
  200. v-for="k in sectionAdvancedEnvKeys(section, openclawChannelBlobKeys[section.sectionKey] || [])"
  201. :key="k"
  202. :value="k">{{ k }}</a-checkbox>
  203. </a-collapse-panel>
  204. </a-collapse>
  205. </a-checkbox-group>
  206. </a-form-item>
  207. </template>
  208. <template v-else>
  209. <div class="openclaw-auto-credential-name text-color-secondary mb-2">
  210. {{ $t('aice.openclaw.new_credential_name') }}:{{ genCredentialName({ llmName: instanceName, usage: 'channel', key: section.sectionKey }) }}
  211. </div>
  212. <div class="openclaw-new-blob-section">
  213. <div v-for="v in channelBasicVars(section.vars)" :key="v.envKey" class="openclaw-new-blob-row mb-2">
  214. <a-form-item
  215. :label="v.envKey"
  216. :required="v.required"
  217. :extra="($te(v.descriptionKey) ? $t(v.descriptionKey) : '') + ' ' + (v.required ? $t('aice.openclaw.required_hint') : $t('aice.openclaw.channel_var_optional'))">
  218. <template v-if="v.envKey === 'FEISHU_DOMAIN'">
  219. <a-radio-group
  220. :value="((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue"
  221. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)">
  222. <a-radio value="feishu">feishu</a-radio>
  223. <a-radio value="lark">lark</a-radio>
  224. </a-radio-group>
  225. </template>
  226. <template v-else-if="['FEISHU_DM_POLICY', 'DISCORD_DM_POLICY', 'TELEGRAM_DM_POLICY'].includes(v.envKey)">
  227. <a-checkbox
  228. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'open'"
  229. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : v.defaultValue)">
  230. open
  231. </a-checkbox>
  232. <a-checkbox
  233. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'pairing'"
  234. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'pairing' : v.defaultValue)">
  235. pairing
  236. </a-checkbox>
  237. <a-checkbox
  238. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'allowlist'"
  239. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : v.defaultValue)">
  240. allowlist
  241. </a-checkbox>
  242. <a-checkbox
  243. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'disabled'"
  244. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : v.defaultValue)">
  245. disabled
  246. </a-checkbox>
  247. </template>
  248. <template v-else-if="['FEISHU_GROUP_POLICY', 'DISCORD_GROUP_POLICY', 'TELEGRAM_GROUP_POLICY'].includes(v.envKey)">
  249. <a-checkbox
  250. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'open'"
  251. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : '')">
  252. open
  253. </a-checkbox>
  254. <a-checkbox
  255. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'allowlist'"
  256. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : '')">
  257. allowlist
  258. </a-checkbox>
  259. <a-checkbox
  260. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'disabled'"
  261. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : '')">
  262. disabled
  263. </a-checkbox>
  264. </template>
  265. <template v-else-if="v.envKey === 'FEISHU_TYPING_INDICATOR'">
  266. <a-checkbox
  267. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  268. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  269. </template>
  270. <template v-else-if="v.envKey === 'FEISHU_RESOLVE_SENDER_NAMES'">
  271. <a-checkbox
  272. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  273. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  274. </template>
  275. <template v-else>
  276. <a-input-password
  277. v-if="isSecretEnvKey(v.envKey)"
  278. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  279. :placeholder="v.defaultValue || v.envKey"
  280. allow-clear
  281. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  282. <a-input
  283. v-else
  284. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  285. :placeholder="v.defaultValue || v.envKey"
  286. allow-clear
  287. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  288. </template>
  289. </a-form-item>
  290. </div>
  291. <a-collapse :bordered="false" v-if="channelAdvancedVars(section.vars).length">
  292. <a-collapse-panel :header="$t('common.adv_config')" key="advanced">
  293. <div v-for="v in channelAdvancedVars(section.vars)" :key="v.envKey" class="openclaw-new-blob-row mb-2">
  294. <a-form-item
  295. :label="v.envKey"
  296. :required="v.required"
  297. :extra="($te(v.descriptionKey) ? $t(v.descriptionKey) : '') + ' ' + (v.required ? $t('aice.openclaw.required_hint') : $t('aice.openclaw.channel_var_optional'))">
  298. <template v-if="v.envKey === 'FEISHU_DOMAIN'">
  299. <a-radio-group
  300. :value="((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue"
  301. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)">
  302. <a-radio value="feishu">feishu</a-radio>
  303. <a-radio value="lark">lark</a-radio>
  304. </a-radio-group>
  305. </template>
  306. <template v-else-if="['FEISHU_DM_POLICY', 'DISCORD_DM_POLICY', 'TELEGRAM_DM_POLICY'].includes(v.envKey)">
  307. <a-checkbox
  308. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'open'"
  309. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : v.defaultValue)">
  310. open
  311. </a-checkbox>
  312. <a-checkbox
  313. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'pairing'"
  314. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'pairing' : v.defaultValue)">
  315. pairing
  316. </a-checkbox>
  317. <a-checkbox
  318. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'allowlist'"
  319. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : v.defaultValue)">
  320. allowlist
  321. </a-checkbox>
  322. <a-checkbox
  323. :checked="(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'disabled'"
  324. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : v.defaultValue)">
  325. disabled
  326. </a-checkbox>
  327. </template>
  328. <template v-else-if="['FEISHU_GROUP_POLICY', 'DISCORD_GROUP_POLICY', 'TELEGRAM_GROUP_POLICY'].includes(v.envKey)">
  329. <a-checkbox
  330. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'open'"
  331. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'open' : '')">
  332. open
  333. </a-checkbox>
  334. <a-checkbox
  335. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'allowlist'"
  336. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'allowlist' : '')">
  337. allowlist
  338. </a-checkbox>
  339. <a-checkbox
  340. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || '') === 'disabled'"
  341. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'disabled' : '')">
  342. disabled
  343. </a-checkbox>
  344. </template>
  345. <template v-else-if="v.envKey === 'FEISHU_TYPING_INDICATOR'">
  346. <a-checkbox
  347. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  348. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  349. </template>
  350. <template v-else-if="v.envKey === 'FEISHU_RESOLVE_SENDER_NAMES'">
  351. <a-checkbox
  352. :checked="String(((openclawChannelBlob[section.sectionKey] || {})[v.envKey]) || v.defaultValue) === 'true'"
  353. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.checked ? 'true' : 'false')" />
  354. </template>
  355. <template v-else>
  356. <a-input-password
  357. v-if="isSecretEnvKey(v.envKey)"
  358. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  359. :placeholder="v.defaultValue || v.envKey"
  360. allow-clear
  361. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  362. <a-input
  363. v-else
  364. :value="(openclawChannelBlob[section.sectionKey] || {})[v.envKey]"
  365. :placeholder="v.defaultValue || v.envKey"
  366. allow-clear
  367. @change="e => $set(openclawChannelBlob[section.sectionKey], v.envKey, e.target.value)" />
  368. </template>
  369. </a-form-item>
  370. </div>
  371. </a-collapse-panel>
  372. </a-collapse>
  373. </div>
  374. </template>
  375. </a-tab-pane>
  376. </a-tabs>
  377. </template>
  378. </template>
  379. <template v-else>
  380. <a-alert type="warning" :message="$t('aice.llm_spec_update')" :description="$t('aice.llm_spec')" show-icon />
  381. </template>
  382. </a-form>
  383. </div>
  384. <div slot="footer">
  385. <a-button type="primary" @click="handleConfirm" :loading="loading">{{ $t('dialog.ok') }}</a-button>
  386. <a-button @click="cancelDialog">{{ $t('dialog.cancel') }}</a-button>
  387. </div>
  388. </base-dialog>
  389. </template>
  390. <script>
  391. import DialogMixin from '@/mixins/dialog'
  392. import WindowsMixin from '@/mixins/windows'
  393. import { OPENCLAW_CHANNEL_SECTIONS, OPENCLAW_CHANNEL_OPTIONS } from '../../llm-sku/openclawChannelConfig'
  394. import { OPENCLAW_PROVIDER_SECTIONS, OPENCLAW_PROVIDER_OPTIONS } from '../../llm-sku/openclawProviderConfig'
  395. import { getParamsForType } from '../../llm-sku/llmTypeConfig'
  396. export default {
  397. name: 'LlmUpdateSpecDialog',
  398. mixins: [DialogMixin, WindowsMixin],
  399. data () {
  400. const row = (this.params && this.params.data && this.params.data[0]) || {}
  401. return {
  402. loading: false,
  403. instanceName: row.name || '',
  404. row,
  405. credentialNameMap: {},
  406. form: {
  407. fc: this.$form.createForm(this, {
  408. onValuesChange: (props, values) => {
  409. Object.keys(values).forEach((key) => {
  410. this.$set(this.form.fd, key, values[key])
  411. })
  412. },
  413. }),
  414. fd: {
  415. openclaw_channels: [],
  416. },
  417. },
  418. decorators: {
  419. openclaw_channels: [
  420. 'openclaw_channels',
  421. { initialValue: (row?.llm_spec?.openclaw?.channels || []).map(item => item.name), rules: [] },
  422. ],
  423. },
  424. formItemLayout: {
  425. wrapperCol: {
  426. span: 19,
  427. },
  428. labelCol: {
  429. span: 5,
  430. },
  431. },
  432. OPENCLAW_PROVIDER_SECTIONS,
  433. openclawSelectedProviders: [],
  434. openclawProviderCredentialMode: {},
  435. openclawProviderCredentialId: {},
  436. openclawProviderBlobKeys: {},
  437. openclawProviderExportKeys: {},
  438. openclawProviderBlob: {},
  439. openclawProviderActiveTab: '',
  440. openclawChannelCredentialMode: {},
  441. openclawChannelCredentialId: {},
  442. openclawChannelBlobKeys: {},
  443. openclawChannelExportKeys: {},
  444. openclawChannelBlob: {},
  445. openclawChannelActiveTab: '',
  446. openclawPrimaryModelOptions: [],
  447. openclawPrimaryModelLoading: false,
  448. }
  449. },
  450. computed: {
  451. isOpenclaw () {
  452. return (this.row.llm_type || '').toLowerCase() === 'openclaw'
  453. },
  454. channelOptionsForSelect () {
  455. return OPENCLAW_CHANNEL_OPTIONS.map(opt => ({
  456. value: opt.value,
  457. label: this.$te(opt.label) ? this.$t(opt.label) : opt.label,
  458. }))
  459. },
  460. filteredChannelSections () {
  461. const channels = this.form.fd.openclaw_channels || []
  462. if (channels.length === 0) return []
  463. const set = new Set(channels)
  464. return OPENCLAW_CHANNEL_SECTIONS.filter(s => set.has(s.sectionKey))
  465. },
  466. providerOptionsForSelect () {
  467. return OPENCLAW_PROVIDER_OPTIONS.map(key => ({
  468. value: key,
  469. label: this.$te(key) ? this.$t(key) : key,
  470. }))
  471. },
  472. providerTabList () {
  473. const selected = this.openclawSelectedProviders || []
  474. if (selected.length === 0) return []
  475. const list = []
  476. selected.forEach(providerLabelKey => {
  477. const vars = []
  478. let required = false
  479. this.OPENCLAW_PROVIDER_SECTIONS.forEach(section => {
  480. section.vars.forEach(v => {
  481. if (v.providerLabelKey === providerLabelKey) {
  482. vars.push(v)
  483. if (section.required) required = true
  484. }
  485. })
  486. })
  487. if (vars.length) list.push({ key: providerLabelKey, labelKey: providerLabelKey, vars, required })
  488. })
  489. return list
  490. },
  491. openclawProviderActiveKey () {
  492. const list = this.providerTabList
  493. if (!list.length) return ''
  494. const keys = list.map(t => t.key)
  495. return keys.includes(this.openclawProviderActiveTab) ? this.openclawProviderActiveTab : (keys[0] || '')
  496. },
  497. openclawOllamaBaseUrlForPrimaryModel () {
  498. const v = this.findOpenclawPrimaryModelVar()
  499. if (!v) return ''
  500. const pk = v.providerLabelKey
  501. return String((this.openclawProviderBlob[pk] || {}).OLLAMA_BASE_URL ?? '')
  502. },
  503. },
  504. watch: {
  505. openclawOllamaBaseUrlForPrimaryModel (newVal, oldVal) {
  506. if (oldVal === undefined) return
  507. if (newVal === oldVal) return
  508. const v = this.findOpenclawPrimaryModelVar()
  509. if (!v) return
  510. const pk = v.providerLabelKey
  511. const blob = this.openclawProviderBlob[pk] || {}
  512. const cur = blob.OPENCLAW_PRIMARY_MODEL
  513. const hasModel = cur !== undefined && cur !== null && String(cur).trim() !== ''
  514. if (!hasModel) {
  515. this.openclawPrimaryModelOptions = []
  516. return
  517. }
  518. if (newVal) {
  519. this.loadOpenclawPrimaryModelOptions(v, pk, '')
  520. } else {
  521. this.openclawPrimaryModelOptions = []
  522. }
  523. },
  524. filteredChannelSections (sections) {
  525. ;(sections || []).forEach(s => this.ensureChannelState(s.sectionKey))
  526. },
  527. providerTabList (list) {
  528. const keys = (list || []).map(t => t.key)
  529. if (keys.length && !keys.includes(this.openclawProviderActiveTab)) {
  530. this.openclawProviderActiveTab = keys[0] || ''
  531. }
  532. },
  533. openclawSelectedProviders: {
  534. handler (list) {
  535. ;(list || []).forEach((providerKey) => {
  536. this.ensureProviderState(providerKey)
  537. })
  538. },
  539. deep: true,
  540. },
  541. },
  542. created () {
  543. // 初始化 providers 的 blob 输入结构
  544. this.OPENCLAW_PROVIDER_SECTIONS.forEach(section => {
  545. section.vars.forEach(({ providerLabelKey, envKey, overrideUrlKey }) => {
  546. if (!this.openclawProviderBlob[providerLabelKey]) this.$set(this.openclawProviderBlob, providerLabelKey, {})
  547. if (this.openclawProviderBlob[providerLabelKey][envKey] === undefined) this.$set(this.openclawProviderBlob[providerLabelKey], envKey, '')
  548. if (overrideUrlKey) {
  549. if (this.openclawProviderBlob[providerLabelKey][overrideUrlKey] === undefined) this.$set(this.openclawProviderBlob[providerLabelKey], overrideUrlKey, '')
  550. }
  551. })
  552. })
  553. this.initFromSpec()
  554. },
  555. methods: {
  556. credentialLabelFormat (item) {
  557. if (!item) return ''
  558. const id = String(item.id || item.key || '')
  559. const byMap = id && this.credentialNameMap && this.credentialNameMap[id]
  560. if (byMap) return byMap
  561. return item.name || id
  562. },
  563. credentialExtraOpts (credentialId) {
  564. if (!credentialId) return []
  565. const id = String(credentialId)
  566. const name = this.credentialNameMap && this.credentialNameMap[id]
  567. if (!name) return []
  568. return [{ id, name }]
  569. },
  570. initFromSpec () {
  571. const spec = this.row.llm_spec && this.row.llm_spec.openclaw != null ? this.row.llm_spec.openclaw : (this.row.llm_spec || {})
  572. if (!spec || typeof spec !== 'object') return
  573. const allowedChannelKeys = new Set(OPENCLAW_CHANNEL_SECTIONS.map(s => s.sectionKey))
  574. // providers
  575. if (Array.isArray(spec.providers) && spec.providers.length > 0) {
  576. const providerKeys = []
  577. spec.providers.forEach(p => {
  578. const name = p && p.name
  579. if (!name) return
  580. // name 为 providerShortName,例如 moonshot,需要映射回 providerLabelKey
  581. // 这里尽量做兼容:
  582. // 1) 如果历史数据直接存了 providerLabelKey,则直接匹配;
  583. // 2) 否则用 providerShortName 做大小写不敏感匹配;
  584. // 这样可以避免更新规格初始化时找不到对应 provider(导致 tab/输入项不出现)。
  585. const normalizedName = String(name)
  586. const labelKey =
  587. OPENCLAW_PROVIDER_OPTIONS.find(k => k === normalizedName) ||
  588. OPENCLAW_PROVIDER_OPTIONS.find(k => String(this.providerShortName(k) || '').toLowerCase() === normalizedName.toLowerCase()) ||
  589. null
  590. if (!labelKey) return
  591. providerKeys.push(labelKey)
  592. this.ensureProviderState(labelKey)
  593. const cred = p.credential || {}
  594. if (cred.id) {
  595. if (cred.name) {
  596. this.$set(this.credentialNameMap, String(cred.id), cred.name)
  597. }
  598. this.$set(this.openclawProviderCredentialMode, labelKey, 'existing')
  599. this.$set(this.openclawProviderCredentialId, labelKey, cred.id)
  600. const exportKeys = cred.export_keys || []
  601. this.$set(this.openclawProviderExportKeys, labelKey, exportKeys)
  602. // 拉取 blob keys 以填充可选项
  603. this.fetchCredentialBlobKeys(cred.id).then(keys => {
  604. this.$set(this.openclawProviderBlobKeys, labelKey, keys)
  605. }).catch(() => {
  606. this.$set(this.openclawProviderBlobKeys, labelKey, [])
  607. })
  608. } else {
  609. this.$set(this.openclawProviderCredentialMode, labelKey, 'new')
  610. }
  611. })
  612. this.openclawSelectedProviders = providerKeys
  613. }
  614. // channels
  615. const rawChannels = spec.channels
  616. if (Array.isArray(rawChannels) && rawChannels.length > 0) {
  617. const channelKeys = []
  618. rawChannels.forEach(item => {
  619. const key = item && (item.name || item.sectionKey || item.type || item.key)
  620. if (!key) return
  621. if (!allowedChannelKeys.has(key)) return
  622. channelKeys.push(key)
  623. this.ensureChannelState(key)
  624. const cred = item.credential || {}
  625. if (cred.id) {
  626. if (cred.name) {
  627. this.$set(this.credentialNameMap, String(cred.id), cred.name)
  628. }
  629. this.$set(this.openclawChannelCredentialMode, key, 'existing')
  630. this.$set(this.openclawChannelCredentialId, key, cred.id)
  631. const exportKeys = cred.export_keys || []
  632. this.$set(this.openclawChannelExportKeys, key, exportKeys)
  633. this.fetchCredentialBlobKeys(cred.id).then(keys => {
  634. this.$set(this.openclawChannelBlobKeys, key, keys)
  635. }).catch(() => {
  636. this.$set(this.openclawChannelBlobKeys, key, [])
  637. })
  638. } else {
  639. this.$set(this.openclawChannelCredentialMode, key, 'new')
  640. }
  641. })
  642. this.form.fc.setFieldsValue({ openclaw_channels: channelKeys })
  643. this.$set(this.form.fd, 'openclaw_channels', channelKeys)
  644. if (!this.openclawChannelActiveTab && channelKeys.length > 0) {
  645. this.openclawChannelActiveTab = channelKeys[0]
  646. }
  647. } else if (rawChannels && typeof rawChannels === 'object') {
  648. const channelKeys = []
  649. Object.keys(rawChannels).forEach((k) => {
  650. const key = String(k)
  651. if (!allowedChannelKeys.has(key)) return
  652. const item = rawChannels[key] || {}
  653. channelKeys.push(key)
  654. this.ensureChannelState(key)
  655. const cred = item.credential || item || {}
  656. const id = cred.id
  657. if (id) {
  658. if (cred.name) {
  659. this.$set(this.credentialNameMap, String(id), cred.name)
  660. }
  661. this.$set(this.openclawChannelCredentialMode, key, 'existing')
  662. this.$set(this.openclawChannelCredentialId, key, id)
  663. const exportKeys = cred.export_keys || []
  664. this.$set(this.openclawChannelExportKeys, key, exportKeys)
  665. this.fetchCredentialBlobKeys(id).then(keys => {
  666. this.$set(this.openclawChannelBlobKeys, key, keys)
  667. }).catch(() => {
  668. this.$set(this.openclawChannelBlobKeys, key, [])
  669. })
  670. } else {
  671. this.$set(this.openclawChannelCredentialMode, key, 'new')
  672. }
  673. })
  674. this.form.fc.setFieldsValue({ openclaw_channels: channelKeys })
  675. this.$set(this.form.fd, 'openclaw_channels', channelKeys)
  676. if (!this.openclawChannelActiveTab && channelKeys.length > 0) {
  677. this.openclawChannelActiveTab = channelKeys[0]
  678. }
  679. }
  680. },
  681. genCredentialName ({ llmName, usage, key }) {
  682. const base = String(llmName || '').trim() || 'llm'
  683. const u = String(usage || '').trim() || 'unknown'
  684. const k = String(key || '').trim() || 'default'
  685. const normalize = (s) => String(s || '').trim()
  686. .replace(/\s+/g, '-')
  687. .replace(/-+/g, '-')
  688. .replace(/^-|-$/g, '')
  689. return normalize(`${base}-${u}-${k}`) || 'llm-credential'
  690. },
  691. credentialParamsForChannel (channelKey) {
  692. const filter = ['type.equals(container_secret)']
  693. if (this.$store.getters.scope === 'project') {
  694. const uid = this.$store.getters.userInfo?.id
  695. if (uid) filter.push(`user_id.equals(${uid})`)
  696. }
  697. return {
  698. $t: 2,
  699. scope: this.$store.getters.scope,
  700. filter,
  701. 'tags.0.key': 'user:openclaw_usage',
  702. 'tags.0.value': 'channel',
  703. 'tags.1.key': 'user:openclaw_name',
  704. 'tags.1.value': channelKey,
  705. }
  706. },
  707. ensureChannelState (channelKey) {
  708. if (!this.openclawChannelCredentialMode[channelKey]) this.$set(this.openclawChannelCredentialMode, channelKey, 'new')
  709. if (!this.openclawChannelCredentialId[channelKey]) this.$set(this.openclawChannelCredentialId, channelKey, undefined)
  710. if (!this.openclawChannelBlobKeys[channelKey]) this.$set(this.openclawChannelBlobKeys, channelKey, [])
  711. if (!this.openclawChannelExportKeys[channelKey]) this.$set(this.openclawChannelExportKeys, channelKey, [])
  712. if (!this.openclawChannelBlob[channelKey]) this.$set(this.openclawChannelBlob, channelKey, {})
  713. },
  714. async fetchCredentialBlobKeys (credentialId) {
  715. if (!credentialId) return []
  716. const manager = new this.$Manager('credentials', 'v1')
  717. const { data } = await manager.get({ id: credentialId })
  718. const idStr = String(credentialId)
  719. const credName = data && data.name
  720. if (credName) {
  721. this.$set(this.credentialNameMap, idStr, credName)
  722. }
  723. const blob = data?.blob
  724. let obj = blob
  725. if (typeof blob === 'string') {
  726. try { obj = JSON.parse(blob) } catch (e) { obj = {} }
  727. }
  728. if (obj && typeof obj === 'object') {
  729. return Object.keys(obj)
  730. }
  731. return []
  732. },
  733. async onChannelCredentialChange (channelKey, credentialId) {
  734. this.ensureChannelState(channelKey)
  735. this.$set(this.openclawChannelCredentialId, channelKey, credentialId)
  736. this.$set(this.openclawChannelExportKeys, channelKey, [])
  737. try {
  738. const keys = await this.fetchCredentialBlobKeys(credentialId)
  739. this.$set(this.openclawChannelBlobKeys, channelKey, keys)
  740. this.$set(this.openclawChannelExportKeys, channelKey, [...keys])
  741. } catch (e) {
  742. this.$set(this.openclawChannelBlobKeys, channelKey, [])
  743. }
  744. },
  745. credentialParamsForProvider (providerKey) {
  746. const filter = ['type.equals(container_secret)']
  747. if (this.$store.getters.scope === 'project') {
  748. const uid = this.$store.getters.userInfo?.id
  749. if (uid) filter.push(`user_id.equals(${uid})`)
  750. }
  751. const shortName = this.providerShortName(providerKey)
  752. return {
  753. $t: 1,
  754. scope: this.$store.getters.scope,
  755. filter,
  756. 'tags.0.key': 'user:openclaw_usage',
  757. 'tags.0.value': 'provider',
  758. 'tags.1.key': 'user:openclaw_name',
  759. 'tags.1.value': shortName,
  760. }
  761. },
  762. providerShortName (providerKey) {
  763. const s = String(providerKey || '')
  764. const parts = s.split('.')
  765. return parts[parts.length - 1] || s
  766. },
  767. ensureProviderState (providerKey) {
  768. if (!this.openclawProviderCredentialMode[providerKey]) this.$set(this.openclawProviderCredentialMode, providerKey, 'new')
  769. if (!this.openclawProviderCredentialId[providerKey]) this.$set(this.openclawProviderCredentialId, providerKey, undefined)
  770. if (!this.openclawProviderBlobKeys[providerKey]) this.$set(this.openclawProviderBlobKeys, providerKey, [])
  771. if (!this.openclawProviderExportKeys[providerKey]) this.$set(this.openclawProviderExportKeys, providerKey, [])
  772. if (!this.openclawProviderBlob[providerKey]) this.$set(this.openclawProviderBlob, providerKey, {})
  773. },
  774. async onProviderCredentialChange (providerKey, credentialId) {
  775. this.ensureProviderState(providerKey)
  776. this.$set(this.openclawProviderCredentialId, providerKey, credentialId)
  777. this.$set(this.openclawProviderExportKeys, providerKey, [])
  778. try {
  779. const keys = await this.fetchCredentialBlobKeys(credentialId)
  780. this.$set(this.openclawProviderBlobKeys, providerKey, keys)
  781. this.$set(this.openclawProviderExportKeys, providerKey, [...keys])
  782. } catch (e) {
  783. this.$set(this.openclawProviderBlobKeys, providerKey, [])
  784. }
  785. },
  786. closeProviderTab (providerKey) {
  787. const next = this.openclawSelectedProviders.filter(k => k !== providerKey)
  788. this.openclawSelectedProviders = next
  789. if (this.openclawProviderActiveTab === providerKey && next.length > 0) {
  790. this.openclawProviderActiveTab = next[0]
  791. } else if (next.length === 0) {
  792. this.openclawProviderActiveTab = ''
  793. }
  794. this.$delete(this.openclawProviderCredentialMode, providerKey)
  795. this.$delete(this.openclawProviderCredentialId, providerKey)
  796. this.$delete(this.openclawProviderBlobKeys, providerKey)
  797. this.$delete(this.openclawProviderExportKeys, providerKey)
  798. },
  799. filterProviderOption (input, option) {
  800. const value = option.componentOptions && option.componentOptions.propsData && option.componentOptions.propsData.value
  801. if (value == null) return true
  802. const label = this.$te(value) ? this.$t(value) : String(value)
  803. return label.toLowerCase().indexOf((input || '').toLowerCase()) >= 0
  804. },
  805. closeChannelTab (sectionKey) {
  806. const channels = (this.form.fd.openclaw_channels || []).filter(k => k !== sectionKey)
  807. this.form.fc.setFieldsValue({ openclaw_channels: channels })
  808. this.$set(this.form.fd, 'openclaw_channels', channels)
  809. if (this.openclawChannelActiveTab === sectionKey && channels.length > 0) {
  810. this.openclawChannelActiveTab = channels[0]
  811. } else if (channels.length === 0) {
  812. this.openclawChannelActiveTab = ''
  813. }
  814. this.$delete(this.openclawChannelCredentialMode, sectionKey)
  815. this.$delete(this.openclawChannelCredentialId, sectionKey)
  816. this.$delete(this.openclawChannelBlobKeys, sectionKey)
  817. this.$delete(this.openclawChannelExportKeys, sectionKey)
  818. },
  819. filterChannelOption (input, option) {
  820. const value = option.componentOptions && option.componentOptions.propsData && option.componentOptions.propsData.value
  821. if (value == null) return true
  822. const opt = this.channelOptionsForSelect.find(o => o.value === value)
  823. const label = opt ? opt.label : (this.$te(value) ? this.$t(value) : String(value))
  824. return String(label).toLowerCase().indexOf((input || '').toLowerCase()) >= 0
  825. },
  826. channelBasicVars (vars) {
  827. return (vars || []).filter(v => !v.advanced)
  828. },
  829. channelAdvancedVars (vars) {
  830. return (vars || []).filter(v => !!v.advanced)
  831. },
  832. sectionBasicEnvKeys (section, envKeys) {
  833. if (!section) return envKeys || []
  834. const basicKeys = new Set(this.channelBasicVars(section.vars).map(v => v.envKey))
  835. return (envKeys || []).filter(k => basicKeys.has(k))
  836. },
  837. sectionAdvancedEnvKeys (section, envKeys) {
  838. if (!section) return []
  839. const basicKeys = new Set(this.channelBasicVars(section.vars).map(v => v.envKey))
  840. return (envKeys || []).filter(k => !basicKeys.has(k))
  841. },
  842. isSecretEnvKey (envKey) {
  843. const lower = (envKey || '').toLowerCase()
  844. return lower.includes('key') || lower.includes('secret') || lower.includes('token') || lower.includes('password')
  845. },
  846. findOpenclawPrimaryModelVar () {
  847. const sections = this.OPENCLAW_PROVIDER_SECTIONS || []
  848. for (let i = 0; i < sections.length; i++) {
  849. const vars = sections[i].vars || []
  850. for (let j = 0; j < vars.length; j++) {
  851. const x = vars[j]
  852. if (x.envKey === 'OPENCLAW_PRIMARY_MODEL' && x.component === 'a-select') return x
  853. }
  854. }
  855. return null
  856. },
  857. pickTrimmedOpenclawBlob (raw) {
  858. const blob = {}
  859. Object.keys(raw || {}).forEach(k => {
  860. const val = raw[k]
  861. if (val === undefined || val === null) return
  862. const s = typeof val === 'string' ? val.trim() : String(val).trim()
  863. if (s !== '') blob[k] = s
  864. })
  865. return blob
  866. },
  867. validateOpenclawProviderRequiredEnv (providerKey) {
  868. const raw = this.openclawProviderBlob[providerKey] || {}
  869. const sections = this.OPENCLAW_PROVIDER_SECTIONS || []
  870. for (let i = 0; i < sections.length; i++) {
  871. const vars = sections[i].vars || []
  872. for (let j = 0; j < vars.length; j++) {
  873. const v = vars[j]
  874. if (v.providerLabelKey !== providerKey || !v.required) continue
  875. const val = raw[v.envKey]
  876. const empty = val === undefined || val === null || val === '' ||
  877. (typeof val === 'string' && !String(val).trim())
  878. if (empty) {
  879. const label = v.placeholderKey && this.$te(v.placeholderKey)
  880. ? this.$t(v.placeholderKey)
  881. : (this.$te(v.descriptionKey) ? this.$t(v.descriptionKey) : v.envKey)
  882. const tip = v.component === 'a-select'
  883. ? this.$t('common.tips.select', [label])
  884. : this.$t('common.tips.input', [label])
  885. this.$message.warning(tip)
  886. return false
  887. }
  888. }
  889. }
  890. return true
  891. },
  892. syncOpenclawPrimaryModelIfNotInOptions (providerKey) {
  893. const blob = (this.openclawProviderBlob || {})[providerKey]
  894. if (!blob) return
  895. const cur = blob.OPENCLAW_PRIMARY_MODEL
  896. const hasModel = cur !== undefined && cur !== null && String(cur).trim() !== ''
  897. if (!hasModel) return
  898. const opts = this.openclawPrimaryModelOptions || []
  899. const inList = opts.some(opt => String(opt.value) === String(cur))
  900. if (!inList) {
  901. this.$set(this.openclawProviderBlob[providerKey], 'OPENCLAW_PRIMARY_MODEL', '')
  902. }
  903. },
  904. openclawProviderAselectPlaceholder (v) {
  905. const labelKey = v.placeholderKey || 'aice.model'
  906. const label = this.$te(labelKey) ? this.$t(labelKey) : labelKey
  907. return this.$t('common.tips.select', [label])
  908. },
  909. refreshOpenclawPrimaryModel (v, providerKey) {
  910. this.loadOpenclawPrimaryModelOptions(v, providerKey, '')
  911. },
  912. async onOpenclawPrimaryModelDropdown (open, v, providerKey) {
  913. if (!open) return
  914. await this.loadOpenclawPrimaryModelOptions(v, providerKey, '')
  915. },
  916. onOpenclawPrimaryModelSearch (q, v, providerKey) {
  917. if (this._openclawPrimaryModelSearchTimer) clearTimeout(this._openclawPrimaryModelSearchTimer)
  918. this._openclawPrimaryModelSearchTimer = setTimeout(() => {
  919. this.loadOpenclawPrimaryModelOptions(v, providerKey, q || '')
  920. }, 300)
  921. },
  922. async loadOpenclawPrimaryModelOptions (v, providerKey, search) {
  923. const resource = (v && v.resource) || 'llms/provider-models'
  924. const blob = (this.openclawProviderBlob || {})[providerKey] || {}
  925. this.openclawPrimaryModelLoading = true
  926. try {
  927. const manager = new this.$Manager(resource, 'v2')
  928. const body = {
  929. scope: this.$store.getters.scope,
  930. limit: 20,
  931. ...getParamsForType('openclaw'),
  932. url: blob.OLLAMA_BASE_URL,
  933. provider_type: 'ollama',
  934. }
  935. if (search) body.filter = [`name.contains(${search})`]
  936. const { data } = await manager.create({ data: body })
  937. const rows = this.unwrapOpenclawPrimaryModelResponse(data)
  938. this.openclawPrimaryModelOptions = rows.map(item => ({
  939. value: item.id || item.model_id,
  940. label: item.fullname || item.name || item.model_id || String(item.id || ''),
  941. })).filter(opt => opt.value !== undefined && opt.value !== null && opt.value !== '')
  942. if (!search) {
  943. this.syncOpenclawPrimaryModelIfNotInOptions(providerKey)
  944. }
  945. } catch (e) {
  946. this.openclawPrimaryModelOptions = []
  947. } finally {
  948. this.openclawPrimaryModelLoading = false
  949. }
  950. },
  951. unwrapOpenclawPrimaryModelResponse (data) {
  952. if (!data) return []
  953. if (Array.isArray(data)) return data
  954. if (Array.isArray(data.data)) return data.data
  955. if (data.data && Array.isArray(data.data.data)) return data.data.data
  956. return []
  957. },
  958. overrideUrlPlaceholder (overrideUrlKey) {
  959. const defaults = {
  960. MOONSHOT_BASE_URL: 'https://api.moonshot.cn/v1',
  961. KIMI_BASE_URL: 'https://api.moonshot.ai/anthropic',
  962. }
  963. return defaults[overrideUrlKey] || 'https://...'
  964. },
  965. async handleConfirm () {
  966. if (!this.isOpenclaw) {
  967. this.cancelDialog()
  968. return
  969. }
  970. this.loading = true
  971. try {
  972. const row = this.row
  973. const channelsSelected = this.form.fd.openclaw_channels || []
  974. const openclaw = {}
  975. const credManager = new this.$Manager('credentials', 'v1')
  976. const channels = []
  977. for (let i = 0; i < channelsSelected.length; i++) {
  978. const channelKey = channelsSelected[i]
  979. this.ensureChannelState(channelKey)
  980. const mode = this.openclawChannelCredentialMode[channelKey] || 'new'
  981. let credentialId
  982. let exportKeys
  983. if (mode === 'existing') {
  984. credentialId = this.openclawChannelCredentialId[channelKey]
  985. if (!credentialId) {
  986. this.$message.warning(this.$t('common.tips.select', [this.$t('aice.container_secret')]))
  987. this.loading = false
  988. return
  989. }
  990. exportKeys = this.openclawChannelExportKeys[channelKey] || []
  991. if (channelKey === 'qqbot') {
  992. const requiredKeys = ['QQBOT_APP_ID', 'QQBOT_CLIENT_SECRET']
  993. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  994. if (missing.length) {
  995. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  996. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  997. this.loading = false
  998. return
  999. }
  1000. }
  1001. if (channelKey === 'feishu') {
  1002. const requiredKeys = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET']
  1003. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1004. if (missing.length) {
  1005. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1006. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1007. this.loading = false
  1008. return
  1009. }
  1010. }
  1011. if (channelKey === 'discord') {
  1012. const requiredKeys = ['DISCORD_BOT_TOKEN']
  1013. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1014. if (missing.length) {
  1015. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1016. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1017. this.loading = false
  1018. return
  1019. }
  1020. }
  1021. if (channelKey === 'telegram') {
  1022. const requiredKeys = ['TELEGRAM_BOT_TOKEN']
  1023. const missing = requiredKeys.filter(k => !(exportKeys || []).includes(k))
  1024. if (missing.length) {
  1025. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1026. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1027. this.loading = false
  1028. return
  1029. }
  1030. }
  1031. } else {
  1032. const credName = this.genCredentialName({ llmName: this.instanceName, usage: 'channel', key: channelKey })
  1033. const raw = this.openclawChannelBlob[channelKey] || {}
  1034. const blob = this.pickTrimmedOpenclawBlob(raw)
  1035. if (channelKey === 'qqbot') {
  1036. const missing = ['QQBOT_APP_ID', 'QQBOT_CLIENT_SECRET'].filter(k => !blob[k])
  1037. if (missing.length) {
  1038. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1039. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1040. this.loading = false
  1041. return
  1042. }
  1043. }
  1044. if (channelKey === 'feishu') {
  1045. const missing = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'].filter(k => !blob[k])
  1046. if (missing.length) {
  1047. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1048. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1049. this.loading = false
  1050. return
  1051. }
  1052. // 单选/勾选控件可能只显示 defaultValue,但不一定触发 @change 写入 blob
  1053. // 这里兜底补齐默认值,确保这些字段会写入 credential blob 与 export_keys
  1054. if (!blob.FEISHU_DOMAIN) blob.FEISHU_DOMAIN = 'feishu'
  1055. if (!blob.FEISHU_DM_POLICY) blob.FEISHU_DM_POLICY = 'open'
  1056. if (!blob.FEISHU_TYPING_INDICATOR) blob.FEISHU_TYPING_INDICATOR = 'true'
  1057. if (!blob.FEISHU_RESOLVE_SENDER_NAMES) blob.FEISHU_RESOLVE_SENDER_NAMES = 'true'
  1058. }
  1059. if (channelKey === 'discord') {
  1060. const missing = ['DISCORD_BOT_TOKEN'].filter(k => !blob[k])
  1061. if (missing.length) {
  1062. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1063. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1064. this.loading = false
  1065. return
  1066. }
  1067. if (!blob.DISCORD_DM_POLICY) blob.DISCORD_DM_POLICY = 'open'
  1068. }
  1069. if (channelKey === 'telegram') {
  1070. const missing = ['TELEGRAM_BOT_TOKEN'].filter(k => !blob[k])
  1071. if (missing.length) {
  1072. const missingLabels = missing.map(k => this.$t(`aice.openclaw.channel.env.${k}`)).join(', ')
  1073. this.$message.warning(this.$t('aice.openclaw.required_hint') + missingLabels)
  1074. this.loading = false
  1075. return
  1076. }
  1077. if (!blob.TELEGRAM_DM_POLICY) blob.TELEGRAM_DM_POLICY = 'open'
  1078. }
  1079. if (Object.keys(blob).length === 0) {
  1080. this.$message.warning(this.$t('aice.openclaw.provider_filter_empty'))
  1081. this.loading = false
  1082. return
  1083. }
  1084. const { data: credData } = await credManager.create({
  1085. data: {
  1086. type: 'container_secret',
  1087. name: credName,
  1088. blob,
  1089. __meta__: {
  1090. 'user:openclaw_usage': 'channel',
  1091. 'user:openclaw_name': channelKey,
  1092. },
  1093. },
  1094. })
  1095. credentialId = credData.id
  1096. exportKeys = Object.keys(blob)
  1097. }
  1098. channels.push({
  1099. name: channelKey,
  1100. credential: { id: credentialId, export_keys: exportKeys },
  1101. })
  1102. }
  1103. if (channels.length) openclaw.channels = channels
  1104. const providersSelected = this.openclawSelectedProviders || []
  1105. if (!providersSelected.length) {
  1106. this.$message.warning(this.$t('aice.openclaw.provider_select_first'))
  1107. this.loading = false
  1108. return
  1109. }
  1110. const providers = []
  1111. for (let i = 0; i < providersSelected.length; i++) {
  1112. const providerKey = providersSelected[i]
  1113. this.ensureProviderState(providerKey)
  1114. if (!this.validateOpenclawProviderRequiredEnv(providerKey)) {
  1115. this.loading = false
  1116. return
  1117. }
  1118. const mode = this.openclawProviderCredentialMode[providerKey] || 'new'
  1119. let credentialId
  1120. let exportKeys
  1121. if (mode === 'existing') {
  1122. credentialId = this.openclawProviderCredentialId[providerKey]
  1123. if (!credentialId) {
  1124. this.$message.warning(this.$t('common.tips.select', [this.$t('aice.container_secret')]))
  1125. this.loading = false
  1126. return
  1127. }
  1128. exportKeys = this.openclawProviderExportKeys[providerKey] || []
  1129. } else {
  1130. const credName = this.genCredentialName({ llmName: this.instanceName, usage: 'provider', key: this.providerShortName(providerKey) })
  1131. const raw = this.openclawProviderBlob[providerKey] || {}
  1132. const blob = this.pickTrimmedOpenclawBlob(raw)
  1133. if (Object.keys(blob).length === 0) {
  1134. this.$message.warning(this.$t('aice.openclaw.ai_providers.at_least_one'))
  1135. this.loading = false
  1136. return
  1137. }
  1138. const { data: credData } = await credManager.create({
  1139. data: {
  1140. type: 'container_secret',
  1141. name: credName,
  1142. blob,
  1143. __meta__: {
  1144. 'user:openclaw_usage': 'provider',
  1145. 'user:openclaw_name': this.providerShortName(providerKey),
  1146. },
  1147. },
  1148. })
  1149. credentialId = credData.id
  1150. exportKeys = Object.keys(blob)
  1151. }
  1152. providers.push({
  1153. name: this.providerShortName(providerKey),
  1154. credential: { id: credentialId, export_keys: exportKeys },
  1155. })
  1156. }
  1157. if (providers.length) openclaw.providers = providers
  1158. if (!Object.keys(openclaw).length) {
  1159. this.$message.warning(this.$t('aice.openclaw.provider_filter_empty'))
  1160. this.loading = false
  1161. return
  1162. }
  1163. await this.params.onManager('update', {
  1164. id: row.id,
  1165. managerArgs: {
  1166. data: {
  1167. llm_spec: { openclaw },
  1168. },
  1169. },
  1170. })
  1171. this.$message.success(this.$t('common.success'))
  1172. this.cancelDialog()
  1173. this.params.refresh && this.params.refresh()
  1174. } catch (error) {
  1175. throw error
  1176. } finally {
  1177. this.loading = false
  1178. }
  1179. },
  1180. },
  1181. }
  1182. </script>
  1183. <style scoped>
  1184. .openclaw-channel-tabs { margin-top: 8px; }
  1185. .openclaw-provider-tabs { margin-top: 8px; }
  1186. .openclaw-section-divider { margin-top: 20px; }
  1187. .openclaw-tab-with-close { display: inline-flex; align-items: center; gap: 6px; }
  1188. .openclaw-tab-close { font-size: 12px; cursor: pointer; opacity: 0.6; }
  1189. .openclaw-tab-close:hover { opacity: 1; }
  1190. .openclaw-new-blob-section { margin-top: 8px; }
  1191. .openclaw-new-blob-row ::v-deep .ant-form-item-label {
  1192. padding-bottom: 4px;
  1193. overflow: visible;
  1194. }
  1195. .openclaw-new-blob-row ::v-deep .ant-form-item-label label {
  1196. overflow: visible;
  1197. white-space: nowrap;
  1198. }
  1199. .openclaw-filter-empty { padding: 12px 0; font-size: 13px; }
  1200. .openclaw-channel-config-hint { font-size: 13px; }
  1201. </style>