feat: add model configuration for PPT and poster generation
- PptSettingsAdmin.vue: full settings page with provider/model/API key config for PPT extraction, poster copy, and poster image generation - Backend: GET /insurance/admin/ppt/available-models to fetch models from Dify API (falls back to builtin list when unavailable) - LLM client: read model config from SystemSetting DB with 60s cache, priority: database > environment variables > defaults - ppt-admin-api.ts: add getAvailableModels() method Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6c3343a222
commit
c8e0c0bb1e
@ -227,6 +227,12 @@ def export_history():
|
|||||||
|
|
||||||
# ---- 系统配置 ----
|
# ---- 系统配置 ----
|
||||||
|
|
||||||
|
@ppt_admin_bp.route("/available-models", methods=["GET"])
|
||||||
|
@jwt_required
|
||||||
|
def get_available_models():
|
||||||
|
return jsonify(ppt_admin_service.get_available_models())
|
||||||
|
|
||||||
|
|
||||||
@ppt_admin_bp.route("/settings", methods=["GET"])
|
@ppt_admin_bp.route("/settings", methods=["GET"])
|
||||||
@jwt_required
|
@jwt_required
|
||||||
def get_settings():
|
def get_settings():
|
||||||
|
|||||||
@ -476,6 +476,59 @@ class PptAdminService:
|
|||||||
|
|
||||||
# ---- 系统配置 ----
|
# ---- 系统配置 ----
|
||||||
|
|
||||||
|
# 内置常用模型列表(Dify 不可用时的降级方案)
|
||||||
|
_BUILTIN_MODELS = [
|
||||||
|
{"provider": "deepseek", "model": "deepseek-chat", "label": "DeepSeek Chat"},
|
||||||
|
{"provider": "deepseek", "model": "deepseek-reasoner", "label": "DeepSeek Reasoner"},
|
||||||
|
{"provider": "minimax", "model": "MiniMax-2.7-Flash", "label": "MiniMax 2.7 Flash"},
|
||||||
|
{"provider": "gemini", "model": "gemini-2.5-flash", "label": "Gemini 2.5 Flash"},
|
||||||
|
{"provider": "gemini", "model": "gemini-2.5-pro", "label": "Gemini 2.5 Pro"},
|
||||||
|
{"provider": "openai", "model": "gpt-4o", "label": "GPT-4o"},
|
||||||
|
{"provider": "openai", "model": "gpt-4o-mini", "label": "GPT-4o Mini"},
|
||||||
|
{"provider": "openai", "model": "gpt-image-1", "label": "GPT Image 1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_available_models(self) -> dict:
|
||||||
|
"""获取可用 LLM 模型列表,优先从 Dify 获取,失败时返回内置列表。"""
|
||||||
|
# 尝试从 Dify 获取
|
||||||
|
dify_models = self._fetch_dify_models()
|
||||||
|
if dify_models is not None:
|
||||||
|
return {"code": 0, "data": {"models": dify_models, "source": "dify"}}
|
||||||
|
return {"code": 0, "data": {"models": self._BUILTIN_MODELS, "source": "builtin"}}
|
||||||
|
|
||||||
|
def _fetch_dify_models(self) -> list | None:
|
||||||
|
"""调用 Dify API 获取可用 LLM 模型列表,失败返回 None。"""
|
||||||
|
import requests
|
||||||
|
try:
|
||||||
|
# 从 SystemSetting 读取 Dify Workspace API Key
|
||||||
|
key_setting = SystemSetting.query.filter_by(key="dify_workspace_api_key").first()
|
||||||
|
api_key = key_setting.value if key_setting and key_setting.value else ""
|
||||||
|
if not api_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
base_url = os.getenv("DIFY_BASE_URL", "http://localhost:5001")
|
||||||
|
resp = requests.get(
|
||||||
|
f"{base_url}/v1/workspaces/current/models/model-types/llm",
|
||||||
|
headers={"Authorization": f"Bearer {api_key}"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
logger.warning(f"Dify 模型查询失败: HTTP {resp.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
models = []
|
||||||
|
for item in data.get("data", []):
|
||||||
|
provider = item.get("provider", "")
|
||||||
|
for model in item.get("models", []):
|
||||||
|
model_name = model.get("model", "")
|
||||||
|
label = model.get("label", {}).get("zh_Hans", "") or model.get("label", {}).get("en_US", "") or model_name
|
||||||
|
models.append({"provider": provider, "model": model_name, "label": label})
|
||||||
|
return models if models else None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"从 Dify 获取模型列表失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_settings(self) -> dict:
|
def get_settings(self) -> dict:
|
||||||
settings = SystemSetting.query.all()
|
settings = SystemSetting.query.all()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -204,8 +204,13 @@ class LLMClient:
|
|||||||
self._configs: list[tuple[LLMProviderConfig, str]] = []
|
self._configs: list[tuple[LLMProviderConfig, str]] = []
|
||||||
self._limiters: dict[str, RateLimiter] = {}
|
self._limiters: dict[str, RateLimiter] = {}
|
||||||
self._active_idx = 0
|
self._active_idx = 0
|
||||||
|
self._db_config_time: float = 0 # 上次从数据库加载配置的时间戳
|
||||||
|
self._db_config_ttl: float = 60 # 配置缓存有效期(秒)
|
||||||
|
|
||||||
# 从环境变量加载
|
self._load_env_config()
|
||||||
|
|
||||||
|
def _load_env_config(self):
|
||||||
|
"""从环境变量加载默认配置。"""
|
||||||
deepseek_key = os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY", "")
|
deepseek_key = os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY", "")
|
||||||
minimax_key = os.environ.get("MINIMAX_API_KEY", "")
|
minimax_key = os.environ.get("MINIMAX_API_KEY", "")
|
||||||
gemini_key = os.environ.get("GEMINI_API_KEY", "")
|
gemini_key = os.environ.get("GEMINI_API_KEY", "")
|
||||||
@ -223,6 +228,54 @@ class LLMClient:
|
|||||||
if not self._configs:
|
if not self._configs:
|
||||||
logger.warning("[LLMClient] 未配置任何 API Key,使用 mock 模式")
|
logger.warning("[LLMClient] 未配置任何 API Key,使用 mock 模式")
|
||||||
|
|
||||||
|
def _try_load_db_config(self):
|
||||||
|
"""尝试从数据库加载模型配置(带缓存,不阻塞)。"""
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - self._db_config_time < self._db_config_ttl:
|
||||||
|
return
|
||||||
|
self._db_config_time = now
|
||||||
|
try:
|
||||||
|
from insurance.models.system_setting import SystemSetting
|
||||||
|
settings = {s.key: s.value for s in SystemSetting.query.filter(
|
||||||
|
SystemSetting.key.in_([
|
||||||
|
"ppt_llm_provider", "ppt_llm_model", "ppt_llm_api_key", "ppt_llm_base_url",
|
||||||
|
])
|
||||||
|
).all()}
|
||||||
|
provider = settings.get("ppt_llm_provider", "").strip()
|
||||||
|
api_key = settings.get("ppt_llm_api_key", "").strip()
|
||||||
|
if not provider or not api_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
model = settings.get("ppt_llm_model", "").strip()
|
||||||
|
base_url = settings.get("ppt_llm_base_url", "").strip()
|
||||||
|
|
||||||
|
# 内置供应商:替换对应配置
|
||||||
|
if provider in PROVIDERS and not base_url:
|
||||||
|
cfg = PROVIDERS[provider]
|
||||||
|
if model:
|
||||||
|
cfg = LLMProviderConfig(
|
||||||
|
name=cfg.name, base_url=cfg.base_url, model=model,
|
||||||
|
max_retries=cfg.max_retries, rate_limit=cfg.rate_limit,
|
||||||
|
)
|
||||||
|
self._configs = [(cfg, api_key)]
|
||||||
|
self._limiters = {provider: RateLimiter(cfg.rate_limit)}
|
||||||
|
self._active_idx = 0
|
||||||
|
logger.info(f"[LLMClient] 使用数据库配置: {provider}/{cfg.model}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 自定义供应商
|
||||||
|
if not base_url:
|
||||||
|
return
|
||||||
|
cfg = LLMProviderConfig(
|
||||||
|
name=provider, base_url=base_url, model=model or "gpt-4o-mini",
|
||||||
|
)
|
||||||
|
self._configs = [(cfg, api_key)]
|
||||||
|
self._limiters = {provider: RateLimiter(0)}
|
||||||
|
self._active_idx = 0
|
||||||
|
logger.info(f"[LLMClient] 使用自定义模型: {provider}/{cfg.model}")
|
||||||
|
except Exception:
|
||||||
|
pass # 无 Flask 上下文或数据库不可用,使用环境变量配置
|
||||||
|
|
||||||
async def chat(self, prompt: str, system_prompt: str = "") -> LLMResponse:
|
async def chat(self, prompt: str, system_prompt: str = "") -> LLMResponse:
|
||||||
"""简单聊天。"""
|
"""简单聊天。"""
|
||||||
messages = []
|
messages = []
|
||||||
@ -271,6 +324,7 @@ class LLMClient:
|
|||||||
|
|
||||||
async def _call(self, messages: list[dict], attempt: int = 0) -> LLMResponse:
|
async def _call(self, messages: list[dict], attempt: int = 0) -> LLMResponse:
|
||||||
"""多供应商自动切换调用。"""
|
"""多供应商自动切换调用。"""
|
||||||
|
self._try_load_db_config()
|
||||||
if not self._configs:
|
if not self._configs:
|
||||||
return LLMResponse(content="{}", provider="mock", latency_ms=0)
|
return LLMResponse(content="{}", provider="mock", latency_ms=0)
|
||||||
|
|
||||||
|
|||||||
@ -1,68 +1,233 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>系统设置</h2>
|
<h2>PPT/海报模型设置</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card v-loading="loading" style="max-width: 500px">
|
<el-form v-loading="loading" :model="form" label-width="140px" style="max-width: 680px">
|
||||||
<el-form label-width="180px">
|
<!-- PPT 解析模型 -->
|
||||||
<el-form-item label="PPT历史保留天数">
|
<el-divider content-position="left">PPT 解析模型</el-divider>
|
||||||
<el-input-number v-model="settings.ppt_history_retention_days" :min="0" :max="3650" />
|
|
||||||
<span class="form-hint">0 = 永久保留</span>
|
<el-form-item label="供应商">
|
||||||
</el-form-item>
|
<el-select v-model="form.ppt_llm_provider" placeholder="选择供应商" style="width: 100%" @change="onPptProviderChange">
|
||||||
<el-form-item label="海报历史保留天数">
|
<el-option label="DeepSeek" value="deepseek" />
|
||||||
<el-input-number v-model="settings.poster_history_retention_days" :min="0" :max="3650" />
|
<el-option label="MiniMax" value="minimax" />
|
||||||
<span class="form-hint">0 = 永久保留</span>
|
<el-option label="Gemini" value="gemini" />
|
||||||
</el-form-item>
|
<el-option label="OpenAI" value="openai" />
|
||||||
<el-form-item>
|
<el-option label="自定义" value="custom" />
|
||||||
<el-button type="primary" @click="onSave" :loading="saving">保存</el-button>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
|
||||||
</el-card>
|
<el-form-item label="模型名称">
|
||||||
|
<div style="display: flex; gap: 8px; width: 100%">
|
||||||
|
<el-input v-model="form.ppt_llm_model" placeholder="如 deepseek-chat" />
|
||||||
|
<el-dropdown v-if="difyModels.length" trigger="click" @command="(cmd: string) => applyDifyModel('ppt', cmd)">
|
||||||
|
<el-button>从 Dify 获取 <el-icon><ArrowDown /></el-icon></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-for="m in difyModels" :key="m.provider + '/' + m.model"
|
||||||
|
:command="m.provider + '|' + m.model">
|
||||||
|
{{ m.label }} ({{ m.provider }}/{{ m.model }})
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<el-button v-else @click="fetchDifyModels" :loading="fetchingModels">从 Dify 获取</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="API Key">
|
||||||
|
<el-input v-model="form.ppt_llm_api_key" type="password" show-password placeholder="留空则使用环境变量配置" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.ppt_llm_provider === 'custom'" label="Base URL">
|
||||||
|
<el-input v-model="form.ppt_llm_base_url" placeholder="如 https://api.openai.com/v1" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 海报文案模型 -->
|
||||||
|
<el-divider content-position="left">海报文案模型</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="供应商">
|
||||||
|
<el-select v-model="form.poster_llm_provider" placeholder="选择供应商" style="width: 100%" @change="onPosterProviderChange">
|
||||||
|
<el-option label="DeepSeek" value="deepseek" />
|
||||||
|
<el-option label="MiniMax" value="minimax" />
|
||||||
|
<el-option label="Gemini" value="gemini" />
|
||||||
|
<el-option label="OpenAI" value="openai" />
|
||||||
|
<el-option label="自定义" value="custom" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="模型名称">
|
||||||
|
<div style="display: flex; gap: 8px; width: 100%">
|
||||||
|
<el-input v-model="form.poster_llm_model" placeholder="如 deepseek-chat" />
|
||||||
|
<el-dropdown v-if="difyModels.length" trigger="click" @command="(cmd: string) => applyDifyModel('poster', cmd)">
|
||||||
|
<el-button>从 Dify 获取 <el-icon><ArrowDown /></el-icon></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-for="m in difyModels" :key="m.provider + '/' + m.model"
|
||||||
|
:command="m.provider + '|' + m.model">
|
||||||
|
{{ m.label }} ({{ m.provider }}/{{ m.model }})
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="API Key">
|
||||||
|
<el-input v-model="form.poster_llm_api_key" type="password" show-password placeholder="留空则使用环境变量配置" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.poster_llm_provider === 'custom'" label="Base URL">
|
||||||
|
<el-input v-model="form.poster_llm_base_url" placeholder="如 https://api.openai.com/v1" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 海报图片模型 -->
|
||||||
|
<el-divider content-position="left">海报图片模型</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="模型名称">
|
||||||
|
<el-input v-model="form.poster_image_model" placeholder="默认 gpt-image-1" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="API Key">
|
||||||
|
<el-input v-model="form.poster_image_api_key" type="password" show-password placeholder="留空则使用环境变量配置" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- Dify 集成 -->
|
||||||
|
<el-divider content-position="left">Dify 集成</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="Workspace API Key">
|
||||||
|
<el-input v-model="form.dify_workspace_api_key" type="password" show-password placeholder="用于从 Dify 获取可用模型列表" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="onSave" :loading="saving">保存设置</el-button>
|
||||||
|
<el-button @click="loadSettings">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import { pptAdminApi } from '@/utils/ppt-admin-api'
|
import { pptAdminApi } from '@/utils/ppt-admin-api'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const settings = ref<Record<string, string>>({
|
const fetchingModels = ref(false)
|
||||||
ppt_history_retention_days: '365',
|
const difyModels = ref<Array<{ provider: string; model: string; label: string }>>([])
|
||||||
poster_history_retention_days: '365',
|
|
||||||
|
const form = reactive({
|
||||||
|
ppt_llm_provider: 'deepseek',
|
||||||
|
ppt_llm_model: '',
|
||||||
|
ppt_llm_api_key: '',
|
||||||
|
ppt_llm_base_url: '',
|
||||||
|
poster_llm_provider: 'deepseek',
|
||||||
|
poster_llm_model: '',
|
||||||
|
poster_llm_api_key: '',
|
||||||
|
poster_llm_base_url: '',
|
||||||
|
poster_image_model: 'gpt-image-1',
|
||||||
|
poster_image_api_key: '',
|
||||||
|
dify_workspace_api_key: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
const DEFAULT_MODELS: Record<string, string> = {
|
||||||
|
deepseek: 'deepseek-chat',
|
||||||
|
minimax: 'MiniMax-2.7-Flash',
|
||||||
|
gemini: 'gemini-2.5-flash',
|
||||||
|
openai: 'gpt-4o',
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPptProviderChange(val: string) {
|
||||||
|
if (val !== 'custom' && !form.ppt_llm_model) {
|
||||||
|
form.ppt_llm_model = DEFAULT_MODELS[val] || ''
|
||||||
|
}
|
||||||
|
if (val !== 'custom') {
|
||||||
|
form.ppt_llm_base_url = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPosterProviderChange(val: string) {
|
||||||
|
if (val !== 'custom' && !form.poster_llm_model) {
|
||||||
|
form.poster_llm_model = DEFAULT_MODELS[val] || ''
|
||||||
|
}
|
||||||
|
if (val !== 'custom') {
|
||||||
|
form.poster_llm_base_url = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDifyModel(target: 'ppt' | 'poster', command: string) {
|
||||||
|
const [provider, model] = command.split('|')
|
||||||
|
if (target === 'ppt') {
|
||||||
|
form.ppt_llm_provider = provider
|
||||||
|
form.ppt_llm_model = model
|
||||||
|
} else {
|
||||||
|
form.poster_llm_provider = provider
|
||||||
|
form.poster_llm_model = model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDifyModels() {
|
||||||
|
fetchingModels.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await pptAdminApi.getAvailableModels()
|
||||||
|
difyModels.value = res?.data?.data?.models || []
|
||||||
|
if (!difyModels.value.length) {
|
||||||
|
ElMessage.warning('未获取到可用模型,请检查 Dify Workspace API Key 配置')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('获取模型列表失败')
|
||||||
|
} finally {
|
||||||
|
fetchingModels.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await pptAdminApi.getSettings()
|
const res: any = await pptAdminApi.getSettings()
|
||||||
const data = res?.data?.data ?? res?.data ?? {}
|
const data = res?.data?.data || {}
|
||||||
if (data.ppt_history_retention_days) settings.value.ppt_history_retention_days = data.ppt_history_retention_days
|
for (const key of Object.keys(form)) {
|
||||||
if (data.poster_history_retention_days) settings.value.poster_history_retention_days = data.poster_history_retention_days
|
if (data[key]) {
|
||||||
|
;(form as any)[key] = data[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('加载配置失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await pptAdminApi.updateSettings(settings.value)
|
await pptAdminApi.updateSettings({ ...form })
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
} catch (e: any) {
|
} catch {
|
||||||
ElMessage.error(e?.response?.data?.message || '保存失败')
|
ElMessage.error('保存失败')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(loadSettings)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.form-hint {
|
.page-container {
|
||||||
margin-left: 12px;
|
padding: 24px;
|
||||||
color: #909399;
|
}
|
||||||
font-size: 13px;
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -102,4 +102,7 @@ export const pptAdminApi = {
|
|||||||
updateSettings(data: Record<string, string>) {
|
updateSettings(data: Record<string, string>) {
|
||||||
return api.put('/admin/ppt/settings', data)
|
return api.put('/admin/ppt/settings', data)
|
||||||
},
|
},
|
||||||
|
getAvailableModels() {
|
||||||
|
return api.get('/admin/ppt/available-models')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user