Detail.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. <template>
  2. <detail
  3. :onManager="onManager"
  4. :data="data"
  5. :base-info="baseInfo"
  6. :extra-info="extraInfo"
  7. status-module="server"
  8. resource="llms" />
  9. </template>
  10. <script>
  11. import {
  12. getUserTagColumn,
  13. } from '@/utils/common/detailColumn'
  14. import { sizestr } from '@/utils/utils'
  15. import WindowsMixin from '@/mixins/windows'
  16. import { getStatusTableColumn } from '@/utils/common/tableColumn'
  17. import {
  18. getLlmIpColumn,
  19. // getStreamEndpointColumn,
  20. // getPortsColumn,
  21. getLlmSkuColumn,
  22. getLlmImageColumn,
  23. getCpuTableColumn,
  24. getMemoryTableColumn,
  25. getBandwidthTableColumn,
  26. getNetworkTypeTableColumn,
  27. getNetworkTableColumn,
  28. } from '../utils/columns'
  29. import { getLlmSpecSections, fetchLlmSpecCredentialNames, fetchLlmSpecDifyImages } from '../../llm-sku/utils/llmSpecDetail'
  30. export default {
  31. name: 'PhoneDetail',
  32. mixins: [WindowsMixin],
  33. props: {
  34. data: {
  35. type: Object,
  36. required: true,
  37. },
  38. onManager: {
  39. type: Function,
  40. required: true,
  41. },
  42. },
  43. data () {
  44. return {
  45. isApplyType: this.$route.path.includes('app-llm'),
  46. credentialNamesMap: {},
  47. difyImageNamesMap: {},
  48. skuLlmSpecOpenclaw: null,
  49. baseInfo: [
  50. getUserTagColumn({
  51. onManager: this.onManager,
  52. resource: 'llms',
  53. columns: () => this.columns,
  54. tipName: this.$t('aice.instance'),
  55. }),
  56. getStatusTableColumn({ field: 'llm_status', statusModule: 'container', title: this.$t('aice.container_status') }),
  57. ],
  58. streamEndpoint: null,
  59. ports: [],
  60. loginInfo: null,
  61. loginInfoSection: [],
  62. loginPasswordVisible: false,
  63. }
  64. },
  65. computed: {
  66. extraInfo () {
  67. return [
  68. {
  69. title: this.$t('aice.config_info'),
  70. items: [
  71. getLlmIpColumn(),
  72. getLlmSkuColumn({ vm: this, isApplyType: this.isApplyType }),
  73. getLlmImageColumn({ vm: this }),
  74. getCpuTableColumn(),
  75. getMemoryTableColumn(),
  76. getBandwidthTableColumn(),
  77. getNetworkTypeTableColumn(),
  78. getNetworkTableColumn(),
  79. {
  80. field: 'volume',
  81. title: this.$t('aice.disk'),
  82. slots: {
  83. default: ({ row }) => {
  84. if (!row.volume || !row.volume.size_mb) return '-'
  85. const { id, size_mb, storage_type } = row.volume
  86. const storageType = this.$te('common.storage.' + storage_type) ? this.$t('common.storage.' + storage_type) : storage_type
  87. const volumeText = `${id} (${sizestr(size_mb, 'M', 1024)} ${storageType})`
  88. return [
  89. <list-body-cell-wrap copy hideField={true} field='volume' row={row} message={id}>
  90. <side-page-trigger permission='disks_get' name='DiskSidePage' id={id} vm={this}>{volumeText}</side-page-trigger>
  91. </list-body-cell-wrap>,
  92. ]
  93. },
  94. },
  95. },
  96. {
  97. field: 'cmp_id',
  98. title: this.$t('dictionary.server_container'),
  99. slots: {
  100. default: ({ row }) => {
  101. if (!row.cmp_id) return '-'
  102. const serverText = `${row.server} (${row.cmp_id})`
  103. return [
  104. <list-body-cell-wrap copy hideField={true} field='server' row={row} message={row.cmp_id}>
  105. <side-page-trigger permission='servers_get' name='VmContainerInstanceSidePage' id={row.cmp_id} vm={this}>{serverText}</side-page-trigger>
  106. </list-body-cell-wrap>,
  107. ]
  108. },
  109. },
  110. },
  111. {
  112. field: 'host',
  113. title: this.$t('compute.text_111'),
  114. sortable: true,
  115. showOverflow: 'ellipsis',
  116. minWidth: 100,
  117. slots: {
  118. default: ({ row }) => {
  119. const text = row.host || '-'
  120. return [
  121. <list-body-cell-wrap copy hideField={true} field='host' row={row} message={text}>
  122. <side-page-trigger permission='hosts_get' name='HostSidePage' id={row.host_id} vm={this}>{row.host}</side-page-trigger>
  123. </list-body-cell-wrap>,
  124. ]
  125. },
  126. },
  127. },
  128. {
  129. field: 'mounted_models',
  130. title: this.isApplyType ? this.$t('aice.app_llm_instantapp') : this.$t('aice.llm_instantapp'),
  131. slots: {
  132. default: ({ row }) => {
  133. const mounted_apps = row.mounted_models
  134. if (mounted_apps?.length) {
  135. return mounted_apps.map((item, idx) => {
  136. return <list-body-cell-wrap copy hideField={true} field='mounted_models' row={item} message={item.fullname}>
  137. <side-page-trigger permission='llm_instant_models_get' name='LlmInstantModelSidePage' id={item.id} vm={this}>{item.fullname}</side-page-trigger>
  138. </list-body-cell-wrap>
  139. })
  140. }
  141. return '-'
  142. },
  143. },
  144. },
  145. ],
  146. },
  147. ...this.loginInfoSection,
  148. ...getLlmSpecSections(this),
  149. ]
  150. },
  151. },
  152. watch: {
  153. 'data.llm_spec': {
  154. handler () {
  155. this.fetchSkuOpenclawIfNeeded()
  156. fetchLlmSpecCredentialNames(this)
  157. fetchLlmSpecDifyImages(this)
  158. },
  159. deep: true,
  160. },
  161. 'data.llm_sku_id': {
  162. handler () {
  163. this.fetchSkuOpenclawIfNeeded()
  164. },
  165. },
  166. },
  167. created () {
  168. this.getAccessInfo()
  169. this.fetchSkuOpenclawIfNeeded()
  170. fetchLlmSpecCredentialNames(this)
  171. fetchLlmSpecDifyImages(this)
  172. },
  173. methods: {
  174. handleUpdateSpec () {
  175. if ((this.data.llm_type || '').toLowerCase() !== 'openclaw') return
  176. this.createDialog('LlmUpdateSpecDialog', {
  177. data: [this.data],
  178. onManager: this.onManager,
  179. refresh: () => {
  180. if (this.$emit) this.$emit('refresh')
  181. },
  182. })
  183. },
  184. async fetchSkuOpenclawIfNeeded () {
  185. if (!this.data || !this.data.llm_sku_id) {
  186. this.skuLlmSpecOpenclaw = null
  187. return
  188. }
  189. const hasInstanceProviders = this.data.llm_spec?.openclaw?.providers?.length > 0
  190. if (hasInstanceProviders) {
  191. this.skuLlmSpecOpenclaw = null
  192. return
  193. }
  194. try {
  195. const res = await new this.$Manager('llm_skus').get({ id: this.data.llm_sku_id })
  196. const openclaw = res.data && res.data.llm_spec && res.data.llm_spec.openclaw != null ? res.data.llm_spec.openclaw : null
  197. this.skuLlmSpecOpenclaw = openclaw && (openclaw.providers?.length > 0) ? openclaw : null
  198. if (this.skuLlmSpecOpenclaw) {
  199. fetchLlmSpecCredentialNames(this)
  200. }
  201. } catch (e) {
  202. this.skuLlmSpecOpenclaw = null
  203. }
  204. },
  205. async getAccessInfo () {
  206. const loginSectionItems = []
  207. this.loginPasswordVisible = false
  208. try {
  209. // GET /llms/<llm_id>/login-info
  210. const loginRes = await new this.$Manager('llms', 'v2').get({
  211. id: `${this.data.id}/login-info`,
  212. })
  213. const info = loginRes.data || {}
  214. this.loginInfo = info
  215. const loginUrl = info.login_url != null ? info.login_url : ''
  216. const username = info.username != null ? info.username : ''
  217. const password = info.password != null ? info.password : ''
  218. const extra = info.extra && typeof info.extra === 'object' ? info.extra : {}
  219. loginSectionItems.push({
  220. field: 'login_url',
  221. title: this.$t('aice.login_url'),
  222. slots: {
  223. default: () => {
  224. const urlCtrl = (loginUrl) => {
  225. return <list-body-cell-wrap copy hideField={true} field='login_url' row={{ login_url: loginUrl }} message={loginUrl}>
  226. {loginUrl || '-'}
  227. <a-icon type="link" class="ml-1" onClick={() => this.openLoginUrl(loginUrl)} />
  228. </list-body-cell-wrap>
  229. }
  230. const urls = [
  231. urlCtrl(loginUrl),
  232. ]
  233. if (this.loginInfo.internal_url && this.loginInfo.internal_url !== loginUrl) {
  234. urls.push(urlCtrl(this.loginInfo.internal_url))
  235. }
  236. if (this.loginInfo.public_url && this.loginInfo.public_url !== loginUrl) {
  237. urls.push(urlCtrl(this.loginInfo.public_url))
  238. }
  239. return urls
  240. },
  241. },
  242. })
  243. loginSectionItems.push({
  244. field: 'login_username',
  245. title: this.$t('aice.login_username'),
  246. slots: {
  247. default: () => [
  248. <list-body-cell-wrap copy hideField={true} field='username' row={{ username }} message={username}>
  249. {username || '-'}
  250. </list-body-cell-wrap>,
  251. ],
  252. },
  253. })
  254. loginSectionItems.push({
  255. field: 'login_password',
  256. title: this.$t('aice.login_password'),
  257. slots: {
  258. default: () => {
  259. const displayValue = this.loginPasswordVisible ? (password || '-') : password ? '••••••' : '-'
  260. return [
  261. <div class="login-password-row">
  262. <span class="login-password-value">{displayValue}</span>
  263. {password &&
  264. [
  265. <a-icon
  266. class="login-password-eye ml-1"
  267. type={this.loginPasswordVisible ? 'eye-invisible' : 'eye'}
  268. theme="twoTone"
  269. twoToneColor="#1890ff"
  270. on-click={() => { this.loginPasswordVisible = !this.loginPasswordVisible }}
  271. />,
  272. <a-icon
  273. class="login-password-copy ml-1"
  274. type="copy"
  275. theme="twoTone"
  276. twoToneColor="#1890ff"
  277. on-click={() => this.copyLoginPassword(password)}
  278. />,
  279. ]}
  280. </div>,
  281. ]
  282. },
  283. },
  284. })
  285. const extraKeys = Object.keys(extra)
  286. if (extraKeys.length > 0) {
  287. const extraLines = extraKeys.map(k => {
  288. const v = extra[k]
  289. return `${k}: ${v != null ? String(v) : ''}`
  290. }).join('\n')
  291. const extraFullText = extraLines
  292. loginSectionItems.push({
  293. field: 'login_extra',
  294. title: this.$t('aice.login_extra'),
  295. slots: {
  296. default: () => [
  297. <list-body-cell-wrap copy hideField={true} field='extra' row={{ extra: extraFullText }} message={extraFullText}>
  298. <div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
  299. {extraLines}
  300. </div>
  301. </list-body-cell-wrap>,
  302. ],
  303. },
  304. })
  305. }
  306. } catch (loginErr) {
  307. try {
  308. const urlRes = await new this.$Manager('llms', 'v2').get({
  309. id: `${this.data.id}/url`,
  310. })
  311. const access_url = (urlRes.data && urlRes.data.access_url) != null ? urlRes.data.access_url : ''
  312. loginSectionItems.push({
  313. field: 'access_url',
  314. title: this.$t('aice.access_url'),
  315. slots: {
  316. default: () => [
  317. <list-body-cell-wrap copy hideField={true} field='access_url' row={{ access_url }} message={access_url}>
  318. {access_url || '-'}
  319. </list-body-cell-wrap>,
  320. ],
  321. },
  322. })
  323. } catch (urlErr) {
  324. console.error(urlErr)
  325. }
  326. }
  327. if (loginSectionItems.length > 0) {
  328. this.loginInfoSection = [{
  329. title: this.$t('aice.login_info'),
  330. items: loginSectionItems,
  331. }]
  332. }
  333. },
  334. async copyLoginPassword (value) {
  335. if (value == null || value === '') return
  336. try {
  337. await this.$copyText(String(value))
  338. this.$message.success(this.$t('common.copy'))
  339. } catch (e) {
  340. this.$message.error(this.$t('common.copyError'))
  341. }
  342. },
  343. openLoginUrl (url) {
  344. if (url == null || url === '') return
  345. window.open(url, '_blank')
  346. },
  347. },
  348. }
  349. </script>
  350. <style scoped>
  351. .login-password-row {
  352. display: flex;
  353. align-items: center;
  354. flex-wrap: wrap;
  355. }
  356. .login-password-value {
  357. color: #333;
  358. }
  359. .login-password-eye {
  360. cursor: pointer;
  361. vertical-align: middle;
  362. }
  363. .login-password-copy {
  364. cursor: pointer;
  365. vertical-align: middle;
  366. }
  367. </style>