feat(web): configure AI confidence threshold (#175)

This commit is contained in:
QiuSW
2026-08-31 17:30:04 +08:00
parent 4343ab5c94
commit d00f1591c2
2 changed files with 53 additions and 12 deletions
@@ -10,17 +10,13 @@
<el-tag :type="form.enabled ? 'success' : 'info'" effect="light">{{ form.enabled ? '已启用' : '未启用' }}</el-tag>
</div>
<el-alert
v-if="!isAdmin"
title="当前账号只能查看 AI 规格匹配是否启用。服务地址、模型和 API Key 仅管理员可见和修改。"
type="info"
:closable="false"
show-icon
class="notice"
/>
<template v-if="!isAdmin">
<el-alert title="当前账号只能查看 AI 规格匹配是否启用和建议置信度阈值。服务地址、模型和 API Key 仅管理员可见和修改。" type="info" :closable="false" show-icon class="notice" />
<el-descriptions :column="1" border class="readonly-settings"><el-descriptions-item label="AI 建议置信度阈值">{{ form.autoConfirmMinConfidence.toFixed(2) }}</el-descriptions-item></el-descriptions>
</template>
<template v-else>
<el-alert
title="AI 只会在服务端已有的可选 PDD 颜色和尺码中返回原始标签。没有唯一结果时采购任务会明确失败,不会让 Android 猜测或点击相近规格。"
title="AI 只会返回服务端已有的可选 PDD 规格。达到阈值时,采购期可采用精确 AI 决策,虾皮规格 AI 建议会预填未保存草稿;低于阈值时只显示建议和原因,需人工选择。规格映射保存后仍需人工确认,系统永不自动支付。"
type="warning"
:closable="false"
show-icon
@@ -41,6 +37,7 @@
<el-form-item label="Base URL" prop="baseUrl"><el-input v-model.trim="form.baseUrl" :disabled="!form.enabled || saving" placeholder="http://provider.example.com:8000/v1" autocomplete="url" /></el-form-item>
<el-form-item label="模型" prop="model"><el-input v-model.trim="form.model" :disabled="!form.enabled || saving" placeholder="例如 gpt-4.1-mini" autocomplete="off" /></el-form-item>
<el-form-item label="超时时间(秒)" prop="timeoutSeconds"><el-input-number v-model="form.timeoutSeconds" :min="3" :max="600" :disabled="!form.enabled || saving" controls-position="right" /></el-form-item>
<el-form-item label="AI 建议置信度阈值" prop="autoConfirmMinConfidence"><el-input-number v-model="form.autoConfirmMinConfidence" :min="0" :max="1" :step="0.01" :precision="2" :disabled="!form.enabled || saving" controls-position="right" /><p class="field-help">达到阈值才自动预填建议;范围 0~1。自动预填不等于自动确认或自动采购。</p></el-form-item>
<el-form-item label="API Key" prop="apiKey" class="api-key-field">
<el-input v-model="form.apiKey" type="password" show-password :disabled="!form.enabled || saving" placeholder="请输入 API Key" autocomplete="new-password" />
</el-form-item>
@@ -74,6 +71,7 @@ export default {
} catch (_) { return callback(new Error('请输入有效的 HTTP 或 HTTPS 服务地址')) }
return callback()
}
const validConfidence = (_, value, callback) => { if (Number.isFinite(value) && value >= 0 && value <= 1) callback(); else callback(new Error('置信度阈值必须在 0 与 1 之间')) }
return {
loading: false,
saving: false,
@@ -83,7 +81,8 @@ export default {
rules: {
baseUrl: [{ validator: validHttps, trigger: 'blur' }],
model: [{ validator: (_, value, callback) => { if (!this.form.enabled || value) callback(); else callback(new Error('请输入模型名称')) }, trigger: 'blur' }],
apiKey: [{ validator: (_, value, callback) => { if (!this.form.enabled || value) callback(); else callback(new Error('请输入 API Key')) }, trigger: 'blur' }]
apiKey: [{ validator: (_, value, callback) => { if (!this.form.enabled || value) callback(); else callback(new Error('请输入 API Key')) }, trigger: 'blur' }],
autoConfirmMinConfidence: [{ validator: validConfidence, trigger: 'change' }]
}
}
},
@@ -93,7 +92,7 @@ export default {
},
created() { this.load() },
methods: {
emptyForm() { return { enabled: false, provider: 'openai_compatible', baseUrl: '', model: '', timeoutSeconds: 15, apiKey: '' } },
emptyForm() { return { enabled: false, provider: 'openai_compatible', baseUrl: '', model: '', timeoutSeconds: 15, autoConfirmMinConfidence: 0.9, apiKey: '' } },
async load() {
this.loading = true
try {
@@ -106,7 +105,7 @@ export default {
if (!valid) return
this.saving = true
try {
const response = await saveAiMatchingSettings({ enabled: this.form.enabled, baseUrl: this.form.baseUrl, model: this.form.model, timeoutSeconds: this.form.timeoutSeconds, apiKey: this.form.apiKey })
const response = await saveAiMatchingSettings({ enabled: this.form.enabled, baseUrl: this.form.baseUrl, model: this.form.model, timeoutSeconds: this.form.timeoutSeconds, autoConfirmMinConfidence: this.form.autoConfirmMinConfidence, apiKey: this.form.apiKey })
this.form = { ...this.emptyForm(), ...response.data }
ElMessage.success('AI 规格匹配设置已保存')
} finally { this.saving = false }
@@ -0,0 +1,42 @@
import { expect, test } from '@playwright/test'
const routeFor = (path: string) => [{ path: '/goauto-management', component: 'Layout', menuName: 'GoAutoManagement', title: '采采管理', visible: '0', children: [{ path, component: '/goauto/ai-matching-settings/index', menuName: 'GoAutoAiMatchingSettings', title: 'AI 规格匹配', visible: '0' }] }]
test('管理员可保存 AI 建议置信度阈值', async({ page, context }) => {
await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }])
let saved: { autoConfirmMinConfidence?: number } = {}
await page.route('**/api/**', async route => {
const url = new URL(route.request().url())
if (url.pathname.startsWith('/src/api/')) return route.continue()
if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}})
if (url.pathname.endsWith('/api/v1/menurole')) return route.fulfill({ json: { code: 200, data: routeFor('/ai-matching-settings') }})
if (url.pathname.endsWith('/api/admin/v1/ai-matching-settings') && route.request().method() === 'PUT') { saved = route.request().postDataJSON(); return route.fulfill({ json: { code: 200, data: { enabled: true, provider: 'openai_compatible', baseUrl: 'http://provider/v1', model: 'test', timeoutSeconds: 180, autoConfirmMinConfidence: saved.autoConfirmMinConfidence, apiKey: 'secret' }}}) }
if (url.pathname.endsWith('/api/admin/v1/ai-matching-settings')) return route.fulfill({ json: { code: 200, data: { enabled: true, provider: 'openai_compatible', baseUrl: 'http://provider/v1', model: 'test', timeoutSeconds: 180, autoConfirmMinConfidence: 0.85, apiKey: 'secret' }}})
return route.fulfill({ json: { code: 200, data: [] }})
})
await page.goto('/#/ai-matching-settings')
const threshold = page.getByLabel('AI 建议置信度阈值')
await expect(threshold).toHaveValue('0.85')
const thresholdField = page.locator('.el-form-item').filter({ hasText: 'AI 建议置信度阈值' })
for (let index = 0; index < 10; index += 1) await thresholdField.getByRole('button', { name: '减少数值' }).click()
await expect(threshold).toHaveValue('0.75')
await page.getByRole('button', { name: '保存设置', exact: true }).click()
await expect.poll(() => saved.autoConfirmMinConfidence).toBe(0.75)
await expect(page.getByText('达到阈值才自动预填建议;范围 0~1。自动预填不等于自动确认或自动采购。', { exact: true })).toBeVisible()
})
test('非管理员只读查看 AI 建议置信度阈值', async({ page, context }) => {
await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }])
await page.route('**/api/**', async route => {
const url = new URL(route.request().url())
if (url.pathname.startsWith('/src/api/')) return route.continue()
if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['purchaser'], name: '采购员', avatar: '', introduction: '', permissions: [] }}})
if (url.pathname.endsWith('/api/v1/menurole')) return route.fulfill({ json: { code: 200, data: routeFor('/ai-matching-settings') }})
if (url.pathname.endsWith('/api/admin/v1/ai-matching-settings')) return route.fulfill({ json: { code: 200, data: { enabled: true, autoConfirmMinConfidence: 0.85 }}})
return route.fulfill({ json: { code: 200, data: [] }})
})
await page.goto('/#/ai-matching-settings')
await expect(page.getByText('0.85', { exact: true })).toBeVisible()
await expect(page.getByLabel('AI 建议置信度阈值')).toHaveCount(0)
await expect(page.getByRole('button', { name: '保存设置', exact: true })).toHaveCount(0)
})