@@ -112,12 +112,16 @@
< div slot = "header" class = "clearfix" >
< span class = "card-title" >
< i class = "el-icon-connection" > < / i >
大模型接入配置
大模型接入配置 ( 多套 · 激活一套生效 )
< / span >
< div style = "float: right;" >
< el-button type = "primary" size = "small" icon = "el-icon-plus" @click ="openLlmDialog(false)" > 新增配置 < / el -button >
< el-button size = "small" icon = "el-icon-refresh" @click ="loadLlmList" > 刷新 < / el -button >
< / div >
< / div >
< el-alert
v-if = "llmConfig && llmConfig .redisAvailable === false"
title = "当前未连接 Redis, 无法保存接入配置 ; Jarvis 将始终使用其 application.yml 中的默认 Ollama。"
v-if = "llmSummary && llmSummary .redisAvailable === false"
title = "当前未连接 Redis, 无法保存; Jarvis 将始终使用 application.yml 中的默认 Ollama。"
type = "warning"
:closable = "false"
show -icon
@@ -128,30 +132,88 @@
show -icon
style = "margin-bottom: 16px;" >
< div slot = "title" >
< p style = "margin: 0 0 8px 0;" > < strong > 说明 : < / strong > 与上方提示词相同 , 配置写入 Redis , 由 Jarvis 读取 。 可在 「 本地 Ollama 」 与 「 OpenAI 兼容 HTTP 」 之间切换 。 < / p >
< p style = "margin: 0 0 8px 0;" > < strong > 说明 : < / strong > 可保存多套 ( 如本地 Ollama 、 远程 API ) , 在表格中点击 < strong > 激活 < / strong > 后 , Jarvis 社媒 AI 仅使用 < strong > 当前激活 < / strong > 的一套 。 未激活或清空全部时走 Jarvis 默认 Ollama 。 < / p >
< ul style = "margin: 0; padding-left: 18px;" >
< li > < strong > Ollama < / strong > : 填写 根地址( 如 < code > http : //127.0.0.1:11434</code>),可不填则使用 Jarvis 默认; 模型名 可空则使用 yml 默认。</li>
< li > < strong > OpenAI 兼容 < / strong > : 填写 完整 Chat Completions URL ( 如远程 < code > https : //api.xxx.com/v1/chat/completions</code> 或本地 <code>http://127.0.0.1:11434/v1/chat/completions</code>),并填写模型名;密钥无则留空(部分本地服务不需要)。</li>
< li > < strong > Ollama < / strong > : 根地址可空 ( 用 Jarvis 默认 ) , 模型可空。 < / li >
< li > < strong > OpenAI 兼容 < / strong > : 须 填完整 Chat Completions URL 与模型名 ; 密钥可空 。 < / li >
< / ul >
< / div >
< / el-alert >
< p v-if = "llmSummary && llmSummary.redisAvailable !== false" class="llm-active-hint" >
< template v-if = "llmSummary.activeId" >
当前 Jarvis 使用 :
< el -tag type = "success" size = "small" > { { llmActiveName || llmSummary . activeId } } < / el-tag >
< / template >
< template v-else >
< el -tag type = "info" size = "small" > 未激活 < / el-tag >
< span style = "margin-left: 8px; color: #909399;" > 将使用 Jarvis yml 默认 Ollama < / span >
< / template >
< / p >
< el-table
v-loading = "llmListLoading"
:data = "llmProfileRows"
border
stripe
empty -text = " 暂无配置 , 请点击 「 新增配置 」 "
style = "width: 100%;" >
< el-table-column prop = "name" label = "名称" min -width = " 120 " show -overflow -tooltip / >
< el-table-column label = "方式" width = "108" >
< template slot -scope = " scope " >
{ { scope . row . mode === 'openai' ? 'OpenAI兼容' : 'Ollama' } }
< / template >
< / el-table-column >
< el-table-column prop = "baseUrl" label = "地址" min -width = " 200 " show -overflow -tooltip / >
< el-table-column prop = "model" label = "模型" min -width = " 120 " show -overflow -tooltip / >
< el-table-column label = "密钥" width = "100" >
< template slot -scope = " scope " >
{ { scope . row . hasApiKey ? ( scope . row . apiKeyMasked || '已设' ) : '—' } }
< / template >
< / el-table-column >
< el-table-column label = "当前" width = "88" align = "center" >
< template slot -scope = " scope " >
< el-tag v-if = "scope.row.active" type="success" size="mini" > 使用中 < / el -tag >
< span v-else > — < / span >
< / template >
< / el -table -column >
< el-table-column label = "操作" width = "200" fixed = "right" >
< template slot -scope = " scope " >
< el-button type = "text" size = "small" :disabled = "scope.row.active" @click ="handleActivateLlm(scope.row)" > 激活 < / el -button >
< el-button type = "text" size = "small" @click ="openLlmDialog(true, scope.row)" > 编辑 < / el -button >
< el-button type = "text" size = "small" class = "llm-del-btn" @click ="handleDeleteLlm(scope.row)" > 删除 < / el -button >
< / template >
< / el-table-column >
< / el-table >
< div class = "llm-toolbar-bottom" >
< el-button size = "small" @click ="handleClearActiveLlm" > 取消激活 < / el -button >
< el-button type = "danger" plain size = "small" @click ="handleResetAllLlm" > 清空全部配置 < / el -button >
< / div >
< el-dialog
: title = "llmDialogEditId ? '编辑大模型配置' : '新增大模型配置'"
:visible.sync = "llmDialogVisible"
width = "580px"
append -to -body
@close ="resetLlmDialogForm" >
< el -form :model = "llmForm" label -width = " 140px " class = "llm-form" >
< el-form-item label = "配置名称" required >
< el-input v-model = "llmForm.name" placeholder="如:本机 Ollama、DeepSeek 官方" maxlength="80" show -word -limit / >
< / el-form-item >
< el-form-item label = "接入方式" >
< el-radio-group v-model = "llmForm.mode" >
< el -radio label = "ollama" > 本地 Ollama < / el-radio >
< el-radio label = "openai" > OpenAI 兼容接口 < / el-radio >
< el -radio label = "ollama" > Ollama < / el-radio >
< el-radio label = "openai" > OpenAI 兼容 < / el-radio >
< / el-radio-group >
< / el-form-item >
< el-form-item : label = "llmForm.mode === 'openai' ? 'API 完整地址' : 'Ollama 根地址'" >
< el-input
v-model = "llmForm.baseUrl"
: placeholder = "llmForm.mode === 'openai' ? 'https://api.openai.com/v1/chat/completions' : 'http://127.0.0.1:11434( 可留空使用 Jarvis 默认 ) '"
: placeholder = "llmForm.mode === 'openai' ? 'https://api.openai.com/v1/chat/completions' : 'http://127.0.0.1:11434( 可空 ) '"
clearable / >
< / el-form-item >
< el-form-item label = "模型名称" >
< el-input
v-model = "llmForm.model"
: placeholder = "llmForm.mode === 'openai' ? '必填,如 gpt-4o-mini、deepseek-chat' : '可选,不填则用 Jarvis 默认模型 '"
: placeholder = "llmForm.mode === 'openai' ? '必填' : '可空 '"
clearable / >
< / el-form-item >
< el-form-item label = "API 密钥" >
@@ -160,24 +222,37 @@
type = "password"
show -password
autocomplete = "new-password"
placeholder = "修改时 填写新密钥;留空表示保持原密钥 " / >
< span v-if = "llmHasApiKey" class="llm-key-hint" > 当前 已保存密钥 : {{ llmApiKeyMasked | | ' * * * * ' }} < / span >
: placeholder = "llmDialogEditId ? '留空不 修改; 填写则覆盖' : '可选' " / >
< span v-if = "llmDialogEditId && llmHasApiKey" class="llm-key-hint" > 已保存 : {{ llmApiKeyMasked | | ' * * * * ' }} < / span >
< / el -form -item >
< el-form-item v-if = "llmHasApiKey" >
< el -checkbox v-model = "llmClearApiKey" > 清除已保存的 API 密钥 < / el -checkbox >
< / el-form-item >
< el-form-item >
< el-button type = "primary" icon = "el-icon-check" :loading = "llmSaving" @click ="handleSaveLlm" > 保存接入配置 < / el -button >
< el-button icon = "el-icon-refresh-left" @click ="handleLoadLlm" > 重新加载 < / el -button >
< el-button type = "danger" plain icon = "el-icon-delete" @click ="handleResetLlm" > 恢复默认 ( 清除 Redis 中的接入配置 ) < / el -button >
< el-form-item v-if = "llmDialogEditId && llmHasApiKey" >
< el -checkbox v-model = "llmClearApiKey" > 清除已保存的密钥 < / el -checkbox >
< / el-form-item >
< / el-form >
< div slot = "footer" class = "dialog-footer" >
< el-button @click ="llmDialogVisible = false" > 取 消 < / el -button >
< el-button type = "primary" :loading = "llmSaving" @click ="submitLlmDialog" > 保 存 < / el -button >
< / div >
< / el-dialog >
< / el-card >
< / div >
< / template >
< script >
import { listPromptTemplates , getPromptTemplate , savePromptTemplate , deletePromptTemplate , getLlmConfig , saveLlmConfig , resetLlmConfig } from '@/api/jarvis/socialMediaPrompt'
import {
listPromptTemplates ,
getPromptTemplate ,
savePromptTemplate ,
deletePromptTemplate ,
listLlmProfiles ,
getLlmProfile ,
createLlmProfile ,
updateLlmProfile ,
deleteLlmProfile ,
setActiveLlmProfile ,
clearActiveLlmProfile ,
resetAllLlmConfig
} from '@/api/jarvis/socialMediaPrompt'
export default {
name : 'SocialMediaPromptConfig' ,
@@ -187,8 +262,13 @@ export default {
activeTab : 'keywords' ,
templates : { } ,
saving : { } ,
llmConfig : null ,
llmSummary : null ,
llmProfileRows : [ ] ,
llmListLoading : false ,
llmDialogVisible : false ,
llmDialogEditId : null ,
llmForm : {
name : '' ,
mode : 'ollama' ,
baseUrl : '' ,
model : '' ,
@@ -200,9 +280,15 @@ export default {
llmSaving : false
}
} ,
computed : {
llmActiveName ( ) {
const r = this . llmProfileRows . find ( p => p . active )
return r ? r . name : ''
}
} ,
mounted ( ) {
this . loadTemplates ( )
this . loadLlmConfig ( )
this . loadLlmList ( )
} ,
methods : {
/** 加载所有模板 */
@@ -309,54 +395,110 @@ export default {
return labels [ key ] || key
} ,
async loadLlmConfig ( ) {
async loadLlmList ( ) {
this . llmListLoading = true
try {
const res = await ge tLlmConfig ( )
const res = await lis tLlmProfiles ( )
if ( res . code === 200 && res . data ) {
this . llmConfig = res . data
this . llmForm . mode = res . data . mode === 'openai' ? 'openai' : 'ollama'
this . llmForm . baseUrl = res . data . baseUrl || ''
this . llmForm . model = res . data . model || ''
this . llmForm . apiKeyInput = ''
this . llmHasApiKey = ! ! res . data . hasApiKey
this . llmApiKeyMasked = res . data . apiKeyMasked || null
this . llmClearApiKey = false
this . llmSummary = res . data
this . llmProfileRows = Array . isArray ( res . data . profiles ) ? res . data . profiles : [ ]
} else {
this . $message . error ( res . msg || '加载大模型配置失败' )
}
} catch ( e ) {
console . error ( e )
this . $message . error ( '加载大模型配置失败:' + ( e . message || '未知错误' ) )
} finally {
this . llmListLoading = false
}
} ,
async handleSaveLl m( ) {
resetLlmDialogFor m( ) {
this . llmDialogEditId = null
this . llmForm = {
name : '' ,
mode : 'ollama' ,
baseUrl : '' ,
model : '' ,
apiKeyInput : ''
}
this . llmHasApiKey = false
this . llmApiKeyMasked = null
this . llmClearApiKey = false
} ,
async openLlmDialog ( edit , row ) {
if ( this . llmSummary && this . llmSummary . redisAvailable === false ) {
this . $message . warning ( 'Redis 未配置,无法保存' )
return
}
this . resetLlmDialogForm ( )
if ( edit && row && row . id ) {
this . llmDialogEditId = row . id
try {
const res = await getLlmProfile ( row . id )
if ( res . code !== 200 || ! res . data ) {
this . $message . error ( res . msg || '加载失败' )
return
}
const d = res . data
this . llmForm . name = d . name || ''
this . llmForm . mode = d . mode === 'openai' ? 'openai' : 'ollama'
this . llmForm . baseUrl = d . baseUrl || ''
this . llmForm . model = d . model || ''
this . llmHasApiKey = ! ! d . hasApiKey
this . llmApiKeyMasked = d . apiKeyMasked || null
} catch ( e ) {
console . error ( e )
this . $message . error ( '加载失败' )
return
}
}
this . llmDialogVisible = true
} ,
async submitLlmDialog ( ) {
if ( ! this . llmForm . name || ! this . llmForm . name . trim ( ) ) {
this . $message . warning ( '请填写配置名称' )
return
}
if ( this . llmForm . mode === 'openai' ) {
if ( ! this . llmForm . baseUrl || ! this . llmForm . baseUrl . trim ( ) ) {
this . $message . warning ( 'OpenAI 兼容模式 须填写完整 API 地址' )
this . $message . warning ( 'OpenAI 兼容须填写完整 API 地址' )
return
}
if ( ! this . llmForm . model || ! this . llmForm . model . trim ( ) ) {
this . $message . warning ( 'OpenAI 兼容模式 须填写模型名称' )
this . $message . warning ( 'OpenAI 兼容须填写模型名称' )
return
}
}
const payload = {
name : this . llmForm . name . trim ( ) ,
mode : this . llmForm . mode ,
baseUrl : ( this . llmForm . baseUrl || '' ) . trim ( ) ,
model : ( this . llmForm . model || '' ) . trim ( )
}
if ( this . llmDialogEditId ) {
if ( this . llmClearApiKey ) {
payload . clearApiKey = true
} else if ( this . llmForm . apiKeyInput && this . llmForm . apiKeyInput . trim ( ) ) {
payload . apiKey = this . llmForm . apiKeyInput . trim ( )
}
} else if ( this . llmForm . apiKeyInput && this . llmForm . apiKeyInput . trim ( ) ) {
payload . apiKey = this . llmForm . apiKeyInput . trim ( )
}
this . llmSaving = true
try {
cons t res = await saveLlmConfig ( payload )
le t res
if ( this . llmDialogEditId ) {
res = await updateLlmProfile ( this . llmDialogEditId , payload )
} else {
res = await createLlmProfile ( payload )
}
if ( res . code === 200 ) {
this . $message . success ( '大模型接入配置已 保存' )
await this . loadLlmConfig ( )
this . $message . success ( '保存成功 ' )
this . llmDialogVisible = false
await this . loadLlmList ( )
} else {
this . $message . error ( res . msg || '保存失败' )
}
@@ -368,18 +510,61 @@ export default {
}
} ,
async handleLoad Llm ( ) {
await this . loadLlmConfig ( )
this . $message . success ( '已重新加载' )
async handleActivate Llm ( row ) {
try {
const res = await setActiveLlmProfile ( row . id )
if ( res . code === 200 ) {
this . $message . success ( res . msg || '已激活' )
await this . loadLlmList ( )
} else {
this . $message . error ( res . msg || '操作失败' )
}
} catch ( e ) {
console . error ( e )
this . $message . error ( '操作失败:' + ( e . message || '未知错误' ) )
}
} ,
async handleRes etLlm ( ) {
async handleDel ete Llm ( row ) {
try {
await this . $confirm ( '将删除 Redis 中的大模型接入配置, Jarvis 恢复为 application.yml 默认 Ollama。是否继续? ' , '提示' , { type : 'warning' } )
const res = await res etLlmConfig ( )
await this . $confirm ( ` 确定删除配置「 ${ row . name || row . id } 」? ` , '提示' , { type : 'warning' } )
const res = await del ete LlmProfile ( row . id )
if ( res . code === 200 ) {
this . $message . success ( res . msg || '已恢复默认 ')
await this . loadLlmConfig ( )
this . $message . success ( '已删除 ')
await this . loadLlmList ( )
} else {
this . $message . error ( res . msg || '删除失败' )
}
} catch ( e ) {
if ( e !== 'cancel' ) {
console . error ( e )
this . $message . error ( '删除失败:' + ( e . message || '未知错误' ) )
}
}
} ,
async handleClearActiveLlm ( ) {
try {
const res = await clearActiveLlmProfile ( )
if ( res . code === 200 ) {
this . $message . success ( res . msg || '已取消激活' )
await this . loadLlmList ( )
} else {
this . $message . error ( res . msg || '操作失败' )
}
} catch ( e ) {
console . error ( e )
this . $message . error ( '操作失败:' + ( e . message || '未知错误' ) )
}
} ,
async handleResetAllLlm ( ) {
try {
await this . $confirm ( '将删除所有大模型接入配置及旧版单键, Jarvis 恢复为默认 Ollama。是否继续? ' , '提示' , { type : 'warning' } )
const res = await resetAllLlmConfig ( )
if ( res . code === 200 ) {
this . $message . success ( res . msg || '已清空' )
await this . loadLlmList ( )
} else {
this . $message . error ( res . msg || '操作失败' )
}
@@ -463,5 +648,22 @@ export default {
font - size : 12 px ;
color : # 909399 ;
}
. llm - active - hint {
margin - bottom : 12 px ;
font - size : 14 px ;
}
. llm - toolbar - bottom {
margin - top : 14 px ;
}
. llm - toolbar - bottom . el - button {
margin - right : 10 px ;
}
. llm - del - btn {
color : # f56c6c ;
}
< / style >