| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- /**
- * 详情页按 llm_type 展示 llm_spec 的共享逻辑
- * 供 LLM 实例详情、LLM 套餐详情复用,避免重复代码
- */
- import { OPENCLAW_CHANNEL_SECTIONS } from '../openclawChannelConfig'
- import { OPENCLAW_PROVIDER_OPTIONS } from '../openclawProviderConfig'
- const CHANNEL_KEY_TO_LABEL = {}
- OPENCLAW_CHANNEL_SECTIONS.forEach(s => {
- CHANNEL_KEY_TO_LABEL[s.sectionKey] = s.sectionLabelKey
- })
- /** provider 短名(create 里 providerShortName)→ providerLabelKey,与渠道同样用 i18n 显示供应商名称 */
- const PROVIDER_SHORT_TO_LABEL = {}
- OPENCLAW_PROVIDER_OPTIONS.forEach(labelKey => {
- const parts = String(labelKey || '').split('.')
- const short = parts[parts.length - 1]
- if (short) PROVIDER_SHORT_TO_LABEL[short] = labelKey
- })
- /**
- * 取供应商展示名(与沟通渠道一致:先显示 provider 名称,再在同一行显示密钥名称)
- * 提交时 providers[].name 为 providerShortName(providerKey),如 moonshot → 映射到 aice.openclaw.provider.moonshot 再 $t
- */
- function getProviderDisplayName (p, vm) {
- if (typeof p === 'string') return p
- if (!p) return '-'
- if (p.name && typeof p.name === 'string') {
- const name = p.name
- if (vm.$te(name)) return vm.$t(name)
- const labelKey = PROVIDER_SHORT_TO_LABEL[name]
- if (labelKey && vm.$te(labelKey)) return vm.$t(labelKey)
- return name
- }
- // 无 name 时尝试用 credential 或 id 占位,避免显示 export_keys 串
- if (p.credential_id) return p.credential_id
- if (p.credential && p.credential.id) return p.credential.id
- return '-'
- }
- /** 取渠道显示名 */
- function getChannelDisplayName (item, vm) {
- const key = typeof item === 'string' ? item : (item && (item.name || item.sectionKey || item.type || item.key)) || ''
- return key ? (vm.$te(CHANNEL_KEY_TO_LABEL[key]) ? vm.$t(CHANNEL_KEY_TO_LABEL[key]) : key) : '-'
- }
- /** 渲染指向容器密钥详情页的链接 */
- function renderCredentialLink (credId, displayText, vm, h) {
- if (!credId) return h('span', { class: 'text-secondary' }, displayText || '-')
- const name = (vm.credentialNamesMap && vm.credentialNamesMap[credId]) || displayText || credId
- return h('side-page-trigger', {
- props: {
- permission: 'credentials_get',
- name: 'ContainerSecretSidePage',
- id: String(credId),
- vm,
- options: { resource: 'credentials', apiVersion: 'v1' },
- },
- }, [name])
- }
- /** 从 openclaw spec 中收集所有容器密钥 id */
- function collectCredentialIds (spec) {
- const ids = new Set()
- if (spec.providers && Array.isArray(spec.providers)) {
- spec.providers.forEach(p => {
- const id = p && (p.credential_id || (p.credential && p.credential.id))
- if (id) ids.add(id)
- })
- }
- if (spec.channels && Array.isArray(spec.channels)) {
- spec.channels.forEach(item => {
- const id = item && item.credential && item.credential.id
- if (id) ids.add(id)
- })
- }
- return Array.from(ids)
- }
- /**
- * 拉取 llm_spec.openclaw 中涉及的容器密钥名称,并写入 vm.credentialNamesMap(需在 Detail 的 data 中初始化 credentialNamesMap: {})
- * 若 vm.skuLlmSpecOpenclaw 存在(实例详情无 AI 供应商时从套餐拉取),会一并收集其 credential id 并拉取名称
- * @param {Object} vm - 详情页 Vue 实例,需有 vm.data、vm.$set,且 data 中有 credentialNamesMap
- */
- export async function fetchLlmSpecCredentialNames (vm) {
- if (!vm || !vm.data) return
- const instanceSpec = vm.data.llm_spec && vm.data.llm_spec.openclaw != null ? vm.data.llm_spec.openclaw : (vm.data.llm_spec || null)
- let ids = instanceSpec ? collectCredentialIds(instanceSpec) : []
- if (vm.skuLlmSpecOpenclaw) {
- const skuIds = collectCredentialIds(vm.skuLlmSpecOpenclaw)
- ids = [...new Set([...ids, ...skuIds])]
- }
- if (ids.length === 0) return
- const map = { ...(vm.credentialNamesMap || {}) }
- const manager = new vm.$Manager('credentials', 'v1')
- await Promise.all(ids.map(async (id) => {
- if (map[id]) return
- try {
- const { data } = await manager.get({ id })
- if (data && data.name) map[id] = data.name
- } catch (e) {
- // 忽略单条失败,保留未拉到的用 id 展示
- }
- }))
- vm.$set(vm, 'credentialNamesMap', map)
- }
- /**
- * 渲染 openclaw llm_spec 的可读内容
- * @param {Object} spec - llm_spec 对象,可能含 providers / channels / workspace_templates
- * @param {Object} vm - Vue 实例,用于 vm.$t
- * @param {Function} h - createElement
- */
- function renderOpenclawSpec (spec, vm, h) {
- const nodes = []
- if (spec.providers && Array.isArray(spec.providers) && spec.providers.length > 0) {
- const sectionLabel = vm.$te('aice.openclaw.section.ai_providers_detail')
- ? vm.$t('aice.openclaw.section.ai_providers_detail')
- : vm.$t('aice.openclaw.section.ai_providers_env')
- const rows = spec.providers.map(p => {
- const providerName = getProviderDisplayName(p, vm)
- const credId = p && (p.credential_id || (p.credential && p.credential.id))
- const credDisplay = (vm.credentialNamesMap && credId && vm.credentialNamesMap[credId]) || (p && p.credential && p.credential.name) || credId || '-'
- return h('div', { class: 'd-flex align-items-center flex-wrap mb-1' }, [
- h('span', { class: 'text-secondary mr-1' }, providerName + ':'),
- renderCredentialLink(credId, credDisplay, vm, h),
- ])
- })
- nodes.push(h('div', { class: 'mb-2' }, [
- h('div', { class: 'detail-item-title text-secondary mb-1' }, sectionLabel),
- h('div', { class: 'detail-item-value' }, rows),
- ]))
- }
- if (spec.channels && Array.isArray(spec.channels) && spec.channels.length > 0) {
- const sectionLabel = vm.$t('aice.openclaw.section.chat_channels')
- const rows = spec.channels.map(item => {
- const channelName = getChannelDisplayName(item, vm)
- const credId = item && item.credential && item.credential.id
- const credDisplay = (vm.credentialNamesMap && credId && vm.credentialNamesMap[credId]) || (item && item.credential && item.credential.name) || credId || '-'
- return h('div', { class: 'd-flex align-items-center flex-wrap mb-1' }, [
- h('span', { class: 'text-secondary mr-1' }, channelName + ':'),
- renderCredentialLink(credId, credDisplay, vm, h),
- ])
- })
- nodes.push(h('div', { class: 'mb-2' }, [
- h('div', { class: 'detail-item-title text-secondary mb-1' }, sectionLabel),
- h('div', { class: 'detail-item-value' }, rows),
- ]))
- }
- if (spec.workspace_templates && typeof spec.workspace_templates === 'object' && Object.keys(spec.workspace_templates).length > 0) {
- const label = vm.$t('aice.openclaw.workspace_templates')
- const keys = Object.keys(spec.workspace_templates)
- nodes.push(h('div', { class: 'mb-2' }, [
- h('div', { class: 'detail-item-title text-secondary mb-1' }, label),
- h('div', { class: 'detail-item-value' }, keys.join('、')),
- ]))
- }
- if (nodes.length === 0) {
- return h('div', { class: 'detail-item-value text-secondary' }, '-')
- }
- return h('div', { class: 'llm-spec-openclaw' }, nodes)
- }
- /** Dify 镜像字段:与创建表单 / 接口返回的 dify_* 一致 */
- const DIFY_SPEC_IMAGE_FIELDS = [
- { keys: ['dify_api_image_id'], labelKey: 'aice.dify_api_image' },
- { keys: ['dify_plugin_image_id'], labelKey: 'aice.dify_plugin_image' },
- { keys: ['dify_sandbox_image_id'], labelKey: 'aice.dify_sandbox_image' },
- { keys: ['dify_ssrf_image_id'], labelKey: 'aice.dify_ssr_image' },
- { keys: ['dify_weaviate_image_id'], labelKey: 'aice.dify_weaviate_image' },
- { keys: ['dify_web_image_id'], labelKey: 'aice.dify_web_image' },
- { keys: ['nginx_image_id'], labelKey: 'aice.nginx_image' },
- { keys: ['postgres_image_id'], labelKey: 'aice.postgres_image' },
- { keys: ['redis_image_id'], labelKey: 'aice.redis_image' },
- ]
- function pickDifyField (dify, keys) {
- if (!dify || typeof dify !== 'object') return null
- for (let i = 0; i < keys.length; i++) {
- const v = dify[keys[i]]
- if (v != null && v !== '') return String(v)
- }
- return null
- }
- function collectDifyImageIds (dify) {
- if (!dify || typeof dify !== 'object') return []
- const ids = new Set()
- DIFY_SPEC_IMAGE_FIELDS.forEach(({ keys }) => {
- const id = pickDifyField(dify, keys)
- if (id) ids.add(id)
- })
- return Array.from(ids)
- }
- /**
- * 根据 llm_spec.dify 中的镜像 id 请求 llm_images,反填名称到 vm.difyImageNamesMap(需在 vm 上初始化 difyImageNamesMap: {})
- */
- export async function fetchLlmSpecDifyImages (vm) {
- if (!vm || !vm.data) return
- const type = (vm.data.llm_type || '').toLowerCase()
- if (type !== 'dify') {
- if (vm.difyImageNamesMap && Object.keys(vm.difyImageNamesMap).length > 0) {
- vm.$set(vm, 'difyImageNamesMap', {})
- }
- return
- }
- const spec = vm.data.llm_spec
- const dify = spec && (spec.dify != null ? spec.dify : spec)
- const ids = collectDifyImageIds(dify)
- if (ids.length === 0) {
- vm.$set(vm, 'difyImageNamesMap', {})
- return
- }
- const map = { ...(vm.difyImageNamesMap || {}) }
- const manager = new vm.$Manager('llm_images')
- await Promise.all(ids.map(async (id) => {
- if (map[id]) return
- try {
- const { data } = await manager.get({ id })
- if (data) {
- map[id] = data.name || data.displayname || id
- }
- } catch (e) {
- // 单条失败仍用 id 展示
- }
- }))
- vm.$set(vm, 'difyImageNamesMap', map)
- }
- function renderDifyImageRow (label, imageId, vm, h) {
- const display = imageId && vm.difyImageNamesMap && vm.difyImageNamesMap[imageId]
- ? vm.difyImageNamesMap[imageId]
- : imageId
- return h('div', {
- class: 'd-flex align-items-center flex-wrap mb-2',
- style: { lineHeight: '1.65' },
- }, [
- h('span', { class: 'text-secondary mr-2', style: { flex: '0 0 200px' } }, label),
- imageId
- ? h('side-page-trigger', {
- props: {
- permission: 'llm_images_get',
- name: 'LlmImageSidePage',
- id: String(imageId),
- vm,
- },
- }, [display || imageId])
- : h('span', { class: 'detail-item-value' }, '-'),
- ])
- }
- function renderDifySpec (dify, vm, h) {
- if (!dify || typeof dify !== 'object') {
- return h('div', { class: 'detail-item-value text-secondary' }, '-')
- }
- const rows = DIFY_SPEC_IMAGE_FIELDS.map(({ keys, labelKey }) => {
- const label = vm.$te(labelKey) ? vm.$t(labelKey) : keys[0]
- const id = pickDifyField(dify, keys)
- return renderDifyImageRow(label, id, vm, h)
- })
- return h('div', { class: 'llm-spec-dify' }, rows)
- }
- /**
- * 仅渲染套餐的 AI 供应商列表(用于实例详情下「对应套餐的 LLM 规格配置」独立 section)
- * @param {Object} vm - 详情页 Vue 实例,需有 vm.skuLlmSpecOpenclaw.providers、vm.credentialNamesMap
- * @param {Function} h - createElement
- */
- function renderSkuProvidersBlock (vm, h) {
- const providers = vm.skuLlmSpecOpenclaw && vm.skuLlmSpecOpenclaw.providers
- if (!providers || !Array.isArray(providers) || providers.length === 0) {
- return h('div', { class: 'detail-item-value text-secondary' }, '-')
- }
- const sectionLabel = vm.$te('aice.openclaw.section.ai_providers_detail')
- ? vm.$t('aice.openclaw.section.ai_providers_detail')
- : vm.$t('aice.openclaw.section.ai_providers_env')
- const rows = providers.map(p => {
- const providerName = getProviderDisplayName(p, vm)
- const credId = p && (p.credential_id || (p.credential && p.credential.id))
- const credDisplay = (vm.credentialNamesMap && credId && vm.credentialNamesMap[credId]) || (p && p.credential && p.credential.name) || credId || '-'
- return h('div', { class: 'd-flex align-items-center flex-wrap mb-1' }, [
- h('span', { class: 'text-secondary mr-1' }, providerName + ':'),
- renderCredentialLink(credId, credDisplay, vm, h),
- ])
- })
- return h('div', { class: 'llm-spec-openclaw' }, [
- h('div', { class: 'mb-2' }, [
- h('div', { class: 'detail-item-title text-secondary mb-1' }, sectionLabel),
- h('div', { class: 'detail-item-value' }, rows),
- ]),
- ])
- }
- /**
- * 按 llm_type 渲染 llm_spec 内容(供 Detail 的 slot 调用)
- * @param {Object} row - 详情 data,含 llm_type、llm_spec
- * @param {Object} vm - Vue 实例
- * @param {Function} h - createElement
- */
- function renderLlmSpecContent (row, vm, h) {
- const spec = row.llm_spec
- if (!spec || (typeof spec === 'object' && Object.keys(spec).length === 0)) {
- return h('div', { class: 'detail-item-value text-secondary' }, '-')
- }
- const type = (row.llm_type || '').toLowerCase()
- if (type === 'openclaw') {
- const openclawData = spec.openclaw != null ? spec.openclaw : spec
- return renderOpenclawSpec(openclawData, vm, h)
- }
- if (type === 'dify') {
- const difyData = spec.dify != null ? spec.dify : spec
- return renderDifySpec(difyData, vm, h)
- }
- // ollama / vllm / comfyui 等:通用 JSON 展示
- try {
- const text = typeof spec === 'string' ? spec : JSON.stringify(spec, null, 2)
- return h('pre', { class: 'detail-item-value mb-0 p-2 bg-secondary rounded', style: { maxHeight: '320px', overflow: 'auto' } }, text)
- } catch (e) {
- return h('div', { class: 'detail-item-value text-secondary' }, '-')
- }
- }
- /**
- * 获取用于 Detail extraInfo 的 llm_spec 区块(无 llm_spec 时返回空数组)
- * @param {Object} vm - 详情页 Vue 实例,需有 vm.data、vm.$t
- * @returns {Array<{ title: string, items: Array }>}
- */
- export function getLlmSpecSections (vm) {
- if (!vm || !vm.data) return []
- if (vm.data.llm_spec == null || (typeof vm.data.llm_spec === 'object' && !Array.isArray(vm.data.llm_spec) && Object.keys(vm.data.llm_spec).length === 0)) {
- return []
- }
- const title = vm.$te('aice.llm_spec_config') ? vm.$t('aice.llm_spec_config') : 'LLM规格配置'
- const sections = [
- {
- title,
- items: [
- {
- field: 'llm_spec',
- slots: {
- default: (scope, h) => renderLlmSpecContent(scope.row || vm.data, vm, h),
- },
- },
- ],
- },
- ]
- if (vm.skuLlmSpecOpenclaw && vm.skuLlmSpecOpenclaw.providers && vm.skuLlmSpecOpenclaw.providers.length > 0) {
- const skuTitle = vm.$te('aice.openclaw.sku_llm_spec_config') ? vm.$t('aice.openclaw.sku_llm_spec_config') : '对应套餐的 LLM 规格配置'
- sections.push({
- title: skuTitle,
- items: [
- {
- field: 'sku_llm_spec',
- slots: {
- default: (scope, h) => renderSkuProvidersBlock(vm, h),
- },
- },
- ],
- })
- }
- return sections
- }
|