fix(settings): 补齐CUDA运行组件下载与安装入口
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { apiClient } from '@/services/apiClient'
|
||||
import LocalModelSettings from './LocalModelSettings.vue'
|
||||
|
||||
vi.mock('@/services/apiClient', () => ({apiClient:{get:vi.fn(),post:vi.fn()}}))
|
||||
it('shows an optional CUDA installer and live installation stage', async () => {
|
||||
vi.mocked(apiClient.get).mockImplementation(async (url) => url.includes('runtime-components')
|
||||
? {status:'not_installed', stage:'尚未安装', supported:true, cuda_available:null,custom_interpreter:false}
|
||||
: {items:[],config:null,runtime_installed:true,last_inference:null})
|
||||
vi.mocked(apiClient.post).mockResolvedValue({status:'installing',stage:'下载并安装 PyTorch CUDA(约 3 GB)',supported:true})
|
||||
const wrapper = mount(LocalModelSettings)
|
||||
try {
|
||||
await flushPromises()
|
||||
const button = wrapper.findAll('button').find(b => b.text() === '下载并安装 CUDA 组件')!
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(apiClient.post).not.toHaveBeenCalled()
|
||||
await button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/local-models/runtime-components/cuda')
|
||||
expect(wrapper.text()).toContain('下载并安装 PyTorch CUDA')
|
||||
expect(wrapper.get('progress').attributes('value')).toBeUndefined()
|
||||
expect(button.attributes('disabled')).toBeDefined()
|
||||
} finally {wrapper.unmount()}
|
||||
})
|
||||
@@ -6,6 +6,16 @@ interface Model {key: string; name: string; capability: string; revision: string
|
||||
const items = ref<Model[]>([])
|
||||
const config = ref<Config | null>(null)
|
||||
const installed = ref(false)
|
||||
interface CudaComponent {status:string;stage:string;cuda_available:boolean|null;supported:boolean;custom_interpreter:boolean;error?:string;torch?:string}
|
||||
const cuda = ref<CudaComponent|null>(null)
|
||||
const cudaError = ref('')
|
||||
async function loadCuda() {
|
||||
try { cuda.value = await apiClient.get<CudaComponent>('/api/local-models/runtime-components/cuda'); cudaError.value = '' }
|
||||
catch(e) { cudaError.value = (e as Error).message }
|
||||
}
|
||||
async function installCuda() {
|
||||
await act(async () => { cuda.value = await apiClient.post<CudaComponent>('/api/local-models/runtime-components/cuda') })
|
||||
}
|
||||
const lastInference = ref<{actual_device:string;requested_device:string;inference_seconds?:number;elapsed_seconds?:number;status?:string;error_code?:string}|null>(null)
|
||||
const error = ref('')
|
||||
const dirty = ref(false)
|
||||
@@ -15,6 +25,7 @@ let stopped = false
|
||||
const size = (bytes: number | null) => bytes === null ? '未知' : `${(bytes / 1024 / 1024).toFixed(1)} MiB`
|
||||
const labels: Record<string,string> = {not_installed:'未下载',downloading:'下载中',installed:'已下载并校验',failed:'下载失败',interrupted:'已中断,可续传'}
|
||||
async function load() {
|
||||
await loadCuda()
|
||||
try {
|
||||
const data = await apiClient.get<{items:Model[];config:Config;runtime_installed:boolean;last_inference:typeof lastInference.value}>('/api/local-models')
|
||||
items.value = data.items; installed.value = data.runtime_installed
|
||||
@@ -45,6 +56,20 @@ onUnmounted(() => { stopped = true; clearTimeout(timer) })
|
||||
<p v-if="error" class="error-banner" role="alert">{{ error }}</p>
|
||||
<p v-if="lastInference" class="subtle">最近实际运行:{{ lastInference.actual_device || '未开始推理' }} · 请求设备 {{ lastInference.requested_device }} · 推理 {{ (lastInference.inference_seconds ?? lastInference.elapsed_seconds ?? 0).toFixed(2) }} 秒 · {{ lastInference.status }} {{ lastInference.error_code || '' }}</p>
|
||||
<p v-if="!installed" class="subtle">尚未安装模型运行环境。在项目根目录执行 <code>./backend/scripts/install-model-runtime.ps1</code>;CUDA 选装追加 <code>-Device cuda</code>。</p>
|
||||
<article class="item-card cuda-components" aria-label="CUDA 运行组件">
|
||||
<h4>CUDA 运行组件(可选)</h4>
|
||||
<p class="subtle">默认使用 CPU。需要 NVIDIA GPU 加速时下载此组件,约 3 GB,安装时还需要额外磁盘空间;不包含显卡驱动和模型权重。</p>
|
||||
<p v-if="cudaError" class="error-text" role="alert">{{ cudaError }} <button class="button-secondary" @click="loadCuda">重新检查</button></p>
|
||||
<template v-if="cuda">
|
||||
<p role="status">{{ cuda.stage }} {{ cuda.torch || '' }}</p>
|
||||
<progress v-if="['checking','installing'].includes(cuda.status)" aria-label="CUDA 组件安装进度" />
|
||||
<p v-if="cuda.error" class="error-text">{{ cuda.error }}</p>
|
||||
<p v-if="!cuda.supported" class="subtle">当前平台暂不支持页面安装,请使用对应平台的模型运行环境。</p>
|
||||
<button v-else-if="cuda.status !== 'installed'" class="button-primary" :disabled="busy || ['checking','installing'].includes(cuda.status)" @click="installCuda">{{ cuda.status === 'installing' ? '正在下载并安装…' : ['failed','interrupted'].includes(cuda.status) ? '重试安装 CUDA 组件' : '下载并安装 CUDA 组件' }}</button>
|
||||
<p v-if="cuda.status === 'installed'" class="subtle">{{ cuda.cuda_available ? '组件已就绪。在下方选择 CUDA 并保存即可启用。' : '组件已安装,但当前未检测到可用 CUDA 设备,将回退 CPU。' }}</p>
|
||||
<p v-if="cuda.custom_interpreter" class="subtle">当前后端设置了 APP_MODEL_PYTHON,优先使用指定环境;要使用页面安装的组件,请移除该覆盖并重启后端。</p>
|
||||
</template>
|
||||
</article>
|
||||
<form v-if="config" @submit.prevent="save" @input="dirty = true" @change="dirty = true">
|
||||
<div class="runtime-grid"><label>请求设备<select v-model="config.device" class="select"><option value="cpu">CPU(默认)</option><option value="cuda">CUDA(不可用则 CPU)</option></select></label>
|
||||
<label>Embedding<select v-model="config.embedding_model" class="select"><option value="bekko">Bekko A8M</option><option value="granite">Granite 97M 多语言</option></select></label>
|
||||
|
||||
Reference in New Issue
Block a user