index.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. <template>
  2. <div>
  3. <page-header :title="$t('aice.llm_image.import_community')" />
  4. <page-body>
  5. <a-alert type="info" class="mb-3">
  6. <span slot="message">
  7. {{ $t('aice.llm_image.community_registry') }}({{ $t('aice.llm_image.community_registry_platform') }})
  8. </span>
  9. </a-alert>
  10. <page-card-list
  11. :list="cardList"
  12. :card-fields="cardFields"
  13. :showPageer="false"
  14. :isRefreshed="false"
  15. :singleActions="singleActions" />
  16. </page-body>
  17. </div>
  18. </template>
  19. <script>
  20. const DEFAULT_ICON = (require('@/assets/images/invalidImg.svg') || {}).default || require('@/assets/images/invalidImg.svg')
  21. const QIANWEN_SVG = '<svg t="1768898043404" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6328" width="32" height="32"><path d="M952.6 606.7L841.3 411.8l47.8-91.2c6.7-8.5 8.5-21.5 5.8-31.7l-61.2-110.4c-4.9-7.2-13-11.6-21.5-12.1H584.7L533.3 77c-3.6-7.2-10.3-11.6-18.3-12.5H394.8c-8.9 0-15.2 6.7-19.7 14.3l-1.8 3.1-113.5 194.9H150.7c-8.9 0-17.4 4.5-22.3 12.1L65.8 399.7c-4 8-4 17.5 0 25.5l113.1 196.2-51 88.9c-4 8-4 17.5 0 25.5l58.1 102c4.9 7.6 13 12.5 22.3 12.5h227.5l55 95.2c4 7.2 11.6 12.1 19.7 13h128.7c8.9 0 17-4.9 21.5-12.5l112.2-196.2H873c8.9-0.9 17-5.8 21.5-13.4L952.6 634c5.3-9 5.3-19.3 0-27.3z m-140.8 12.5l-58.1-107.3L515 932.1l-65.3-107.3H211.5l57.2-103.7h121.6L151.6 302.3h124.7L394.8 90.9 454.2 195 393 302.3h477.4l-59.9 106.4 120.2 210.5H811.8z m0 0" fill="#605BEC" p-id="6329"></path><path d="M509.6 659.4l148.8-238.2H359.9l149.7 238.2z m0 0" fill="#605BEC" p-id="6330"></path></svg>'
  22. const QIANWEN_ICON_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(QIANWEN_SVG)}`
  23. export default {
  24. name: 'LlmImageImportCommunity',
  25. data () {
  26. return {
  27. loadingList: false,
  28. keyword: '',
  29. rawList: [],
  30. cardFields: {
  31. url: 'icon',
  32. title: 'title',
  33. description: 'description',
  34. desc: 'desc',
  35. },
  36. }
  37. },
  38. computed: {
  39. filteredList () {
  40. const kw = (this.keyword || '').trim().toLowerCase()
  41. if (!kw) return this.rawList
  42. return this.rawList.filter(item => {
  43. return [
  44. item.full_name,
  45. item.model_name,
  46. item.tag_name,
  47. item.description,
  48. (item.capabilities || []).join(' '),
  49. ].some(v => (v || '').toString().toLowerCase().includes(kw))
  50. })
  51. },
  52. cardList () {
  53. return {
  54. data: this.filteredList.map(item => {
  55. const caps = Array.isArray(item.capabilities) ? item.capabilities : []
  56. const context = item.context_length || '-'
  57. const size = item.model_size || '-'
  58. return {
  59. data: {
  60. // page-card-list 固定用 data.url 做 copy,因此保留 url 为可复制的模型标识
  61. // url: item.full_name,
  62. icon: this.getModelIcon(item),
  63. title: item.full_name,
  64. description: item.description || '-',
  65. desc: `${caps.join(', ') || '-'} | ${context} | ${size}`,
  66. _raw: item,
  67. },
  68. }
  69. }),
  70. loading: this.loadingList,
  71. }
  72. },
  73. singleActions () {
  74. return [
  75. {
  76. label: this.$t('aice.import'),
  77. action: async (data) => {
  78. const raw = data?._raw || {}
  79. const fullName = raw.full_name || data?.url
  80. const createData = {
  81. generate_name: this.toSafeName(fullName),
  82. llm_type: 'ollama',
  83. model_name: raw.model_name,
  84. model_tag: raw.tag_name,
  85. }
  86. await new this.$Manager('llm_instant_models').create({ data: createData })
  87. this.$message.success(this.$t('common.success'))
  88. this.$router.push({ name: 'LlmInstantmodelList' })
  89. },
  90. meta: () => {
  91. return {
  92. buttonType: 'primary',
  93. }
  94. },
  95. },
  96. ]
  97. },
  98. },
  99. created () {
  100. this.fetchRegistry()
  101. },
  102. methods: {
  103. noop () {},
  104. goBack () {
  105. this.$router.push({ name: 'LlmImageList' })
  106. },
  107. getModelIcon (item) {
  108. const name = `${item?.model_name || ''} ${item?.full_name || ''}`
  109. // Qwen / 千问系列(qwen3, qwen2.5-coder, qwen3-vl 等)
  110. if (/(\b|_|-)(qwen|qianwen)(\b|_|-)/i.test(name) || /^qwen/i.test(item?.model_name || '')) {
  111. return QIANWEN_ICON_URL
  112. }
  113. return DEFAULT_ICON
  114. },
  115. buildRawListFromDoc (doc) {
  116. const models = Array.isArray(doc?.ollama) ? doc.ollama : []
  117. const ret = []
  118. models.forEach(model => {
  119. const modelName = model?.name
  120. const description = model?.description || ''
  121. const tags = Array.isArray(model?.tags) ? model.tags : []
  122. tags.forEach(tag => {
  123. const tagName = tag?.name
  124. if (!modelName || !tagName) return
  125. ret.push({
  126. full_name: `${modelName}:${tagName}`,
  127. model_name: modelName,
  128. tag_name: tagName,
  129. description,
  130. capabilities: tag?.capabilities || [],
  131. context_length: tag?.context_length,
  132. model_size: tag?.model_size,
  133. is_latest: !!tag?.is_latest,
  134. })
  135. })
  136. })
  137. return ret
  138. },
  139. async fetchRegistry () {
  140. this.loadingList = true
  141. try {
  142. const manager = new this.$Manager('llm_instant_models')
  143. const res = await manager.get({ id: 'community-registry' })
  144. const doc = res?.data || {}
  145. this.rawList = this.buildRawListFromDoc(doc)
  146. } catch (e) {
  147. this.$message.error(this.$t('aice.llm_image.community_registry_fetch_failed'))
  148. throw e
  149. } finally {
  150. this.loadingList = false
  151. }
  152. },
  153. toSafeName (fullName) {
  154. return (fullName || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
  155. },
  156. },
  157. }
  158. </script>
  159. <style scoped>
  160. .mb-3 {
  161. margin-bottom: 12px;
  162. }
  163. .toolbar {
  164. display: flex;
  165. gap: 8px;
  166. align-items: center;
  167. margin-bottom: 8px;
  168. }
  169. .toolbar-right {
  170. margin-left: auto;
  171. display: flex;
  172. align-items: center;
  173. }
  174. .mr-2 {
  175. margin-right: 8px;
  176. }
  177. /* 隐藏复制链接功能 */
  178. ::v-deep .link_copy {
  179. display: none !important;
  180. }
  181. </style>