feat: 支持在 WebUI 自定义 OpenAI API extra_body 参数 (#2719)
* feat: 支持OPENAI系 模型的自定义标头,以解决qwen模型无法使用的问题 * fix: 修复AI说的问题 * fix: 布尔开关向右对齐
This commit is contained in:
@@ -599,6 +599,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
"hint": "也兼容所有与 OpenAI API 兼容的服务。",
|
||||
},
|
||||
@@ -613,6 +614,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"xAI": {
|
||||
@@ -625,6 +627,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.x.ai/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Anthropic": {
|
||||
@@ -654,6 +657,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
||||
"api_base": "http://localhost:11434/v1",
|
||||
"model_config": {"model": "llama3.1-8b", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"LM Studio": {
|
||||
@@ -667,6 +671,7 @@ CONFIG_METADATA_2 = {
|
||||
"model_config": {
|
||||
"model": "llama-3.1-8b",
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Gemini(OpenAI兼容)": {
|
||||
@@ -682,6 +687,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gemini-1.5-flash",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Gemini": {
|
||||
@@ -722,6 +728,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.deepseek.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"302.AI": {
|
||||
@@ -734,6 +741,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.302.ai/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"硅基流动": {
|
||||
@@ -749,6 +757,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"PPIO派欧云": {
|
||||
@@ -764,6 +773,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek/deepseek-r1",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
},
|
||||
"优云智算": {
|
||||
"id": "compshare",
|
||||
@@ -777,6 +787,7 @@ CONFIG_METADATA_2 = {
|
||||
"model_config": {
|
||||
"model": "moonshotai/Kimi-K2-Instruct",
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Kimi": {
|
||||
@@ -789,6 +800,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"api_base": "https://api.moonshot.cn/v1",
|
||||
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"智谱 AI": {
|
||||
@@ -847,6 +859,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"api_base": "https://api-inference.modelscope.cn/v1",
|
||||
"model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"FastGPT": {
|
||||
@@ -858,6 +871,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "https://api.fastgpt.in/api/v1",
|
||||
"timeout": 60,
|
||||
"custom_extra_body": {},
|
||||
},
|
||||
"Whisper(API)": {
|
||||
"id": "whisper",
|
||||
@@ -1102,6 +1116,12 @@ CONFIG_METADATA_2 = {
|
||||
"render_type": "checkbox",
|
||||
"hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。",
|
||||
},
|
||||
"custom_extra_body": {
|
||||
"description": "自定义请求体参数",
|
||||
"type": "dict",
|
||||
"items": {},
|
||||
"hint": "此处添加的键值对将被合并到发送给 API 的 extra_body 中。值可以是字符串、数字或布尔值。",
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"invisible": True,
|
||||
|
||||
@@ -99,12 +99,13 @@ class ProviderOpenAIOfficial(Provider):
|
||||
for key in to_del:
|
||||
del payloads[key]
|
||||
|
||||
model = payloads.get("model", "")
|
||||
# 针对 qwen3 非 thinking 模型的特殊处理:非流式调用必须设置 enable_thinking=false
|
||||
if "qwen3" in model.lower() and "thinking" not in model.lower():
|
||||
extra_body["enable_thinking"] = False
|
||||
# 读取并合并 custom_extra_body 配置
|
||||
custom_extra_body = self.provider_config.get("custom_extra_body", {})
|
||||
if isinstance(custom_extra_body, dict):
|
||||
extra_body.update(custom_extra_body)
|
||||
|
||||
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
|
||||
elif model == "deepseek-reasoner" and "tools" in payloads:
|
||||
if model == "deepseek-reasoner" and "tools" in payloads:
|
||||
del payloads["tools"]
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
@@ -137,6 +138,12 @@ class ProviderOpenAIOfficial(Provider):
|
||||
|
||||
# 不在默认参数中的参数放在 extra_body 中
|
||||
extra_body = {}
|
||||
|
||||
# 读取并合并 custom_extra_body 配置
|
||||
custom_extra_body = self.provider_config.get("custom_extra_body", {})
|
||||
if isinstance(custom_extra_body, dict):
|
||||
extra_body.update(custom_extra_body)
|
||||
|
||||
to_del = []
|
||||
for key in payloads.keys():
|
||||
if key not in self.default_params:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ListConfigItem from './ListConfigItem.vue'
|
||||
import ObjectEditor from './ObjectEditor.vue'
|
||||
import ProviderSelector from './ProviderSelector.vue'
|
||||
import PersonaSelector from './PersonaSelector.vue'
|
||||
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
|
||||
@@ -80,7 +81,7 @@ function shouldShowItem(itemMeta, itemKey) {
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
|
||||
// 检查当前索引之后是否还有可见的配置项
|
||||
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
|
||||
const [itemKey, itemValue] = itemEntries[i]
|
||||
@@ -89,7 +90,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
@@ -130,7 +131,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Regular Property -->
|
||||
<template v-else>
|
||||
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
|
||||
@@ -145,7 +146,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey].items[key]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -153,10 +154,10 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey].items[key]?.type || 'string' }}
|
||||
</v-chip>
|
||||
@@ -166,35 +167,35 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<div v-if="metadata[metadataKey].items[key]" class="w-100">
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<div v-if="metadata[metadataKey].items[key]?._special === 'select_provider'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="iterable[key]"
|
||||
:provider-type="'chat_completion'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_provider_stt'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="iterable[key]"
|
||||
:provider-type="'speech_to_text'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_provider_tts'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="iterable[key]"
|
||||
:provider-type="'text_to_speech'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_persona'">
|
||||
<PersonaSelector
|
||||
<PersonaSelector
|
||||
v-model="iterable[key]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_knowledgebase'">
|
||||
<KnowledgeBaseSelector
|
||||
<KnowledgeBaseSelector
|
||||
v-model="iterable[key]"
|
||||
/>
|
||||
</div>
|
||||
<!-- List item with options-->
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.type === 'list' && metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible && metadata[metadataKey].items[key]?.render_type === 'checkbox'"
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.type === 'list' && metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible && metadata[metadataKey].items[key]?.render_type === 'checkbox'"
|
||||
class="d-flex flex-wrap gap-20">
|
||||
<v-checkbox
|
||||
v-for="(option, index) in metadata[metadataKey].items[key]?.options"
|
||||
@@ -233,10 +234,10 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
<!-- Code Editor with Full Screen Option -->
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.editor_mode && !metadata[metadataKey].items[key]?.invisible" class="editor-container">
|
||||
<VueMonacoEditor
|
||||
:theme="metadata[metadataKey].items[key]?.editor_theme || 'vs-light'"
|
||||
<VueMonacoEditor
|
||||
:theme="metadata[metadataKey].items[key]?.editor_theme || 'vs-light'"
|
||||
:language="metadata[metadataKey].items[key]?.editor_language || 'json'"
|
||||
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
|
||||
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
|
||||
v-model:value="iterable[key]"
|
||||
>
|
||||
</VueMonacoEditor>
|
||||
@@ -252,7 +253,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-icon>mdi-fullscreen</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- String input -->
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
|
||||
@@ -262,7 +263,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
@@ -273,7 +274,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible"
|
||||
@@ -283,7 +284,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
|
||||
|
||||
<!-- Boolean switch -->
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'bool' && !metadata[metadataKey].items[key]?.invisible"
|
||||
@@ -293,20 +294,27 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Dict item (key-value editor) -->
|
||||
<ObjectEditor
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'dict' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Fallback for unknown metadata -->
|
||||
<div v-else class="w-100">
|
||||
<v-text-field
|
||||
v-model="iterable[key]"
|
||||
:label="key"
|
||||
<v-text-field
|
||||
v-model="iterable[key]"
|
||||
:label="key"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
@@ -316,14 +324,14 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider
|
||||
<v-divider
|
||||
v-if="hasVisibleItemsAfter(filteredIterable, index) && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
|
||||
class="config-divider"
|
||||
></v-divider>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Simple Value Configuration -->
|
||||
<div v-else class="simple-config">
|
||||
<v-row class="config-row">
|
||||
@@ -342,9 +350,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey]?.type }}
|
||||
@@ -364,7 +372,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
|
||||
<!-- String input -->
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
|
||||
@@ -374,7 +382,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
@@ -385,7 +393,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible"
|
||||
@@ -396,7 +404,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
|
||||
|
||||
<!-- Boolean switch -->
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible"
|
||||
@@ -406,9 +414,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
class="config-field"
|
||||
@@ -435,9 +443,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<VueMonacoEditor
|
||||
<VueMonacoEditor
|
||||
:theme="currentEditingTheme"
|
||||
:language="currentEditingLanguage"
|
||||
:language="currentEditingLanguage"
|
||||
style="height: calc(100vh - 64px);"
|
||||
v-model:value="currentEditingKeyIterable[currentEditingKey]"
|
||||
>
|
||||
@@ -567,11 +575,11 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
.nested-object {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
.config-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
|
||||
.property-info, .type-indicator, .config-input {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ListConfigItem from './ListConfigItem.vue'
|
||||
import ObjectEditor from './ObjectEditor.vue'
|
||||
import ProviderSelector from './ProviderSelector.vue'
|
||||
import PersonaSelector from './PersonaSelector.vue'
|
||||
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
|
||||
@@ -102,7 +103,7 @@ function shouldShowItem(itemMeta, itemKey) {
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
|
||||
// 检查当前索引之后是否还有可见的配置项
|
||||
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
|
||||
const [itemKey, itemMeta] = itemEntries[i]
|
||||
@@ -110,7 +111,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
@@ -188,13 +189,20 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
color="primary" inset density="compact" hide-details style="display: flex; justify-content: end;"></v-switch>
|
||||
|
||||
<!-- List item for JSON selector -->
|
||||
<ListConfigItem
|
||||
<ListConfigItem
|
||||
v-else-if="itemMeta?.type === 'list'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="修改"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Object editor for JSON selector -->
|
||||
<ObjectEditor
|
||||
v-else-if="itemMeta?.type === 'dict'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Fallback for JSON selector -->
|
||||
<v-text-field v-else v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined"
|
||||
class="config-field" hide-details></v-text-field>
|
||||
@@ -202,48 +210,48 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<div v-else-if="itemMeta?._special === 'select_provider'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_stt'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'speech_to_text'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_tts'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'text_to_speech'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'provider_pool'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
button-text="选择提供商池..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_persona'">
|
||||
<PersonaSelector
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'persona_pool'">
|
||||
<PersonaSelector
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="选择人格池..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_knowledgebase'">
|
||||
<KnowledgeBaseSelector
|
||||
<KnowledgeBaseSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_plugin_set'">
|
||||
<PluginSetSelector
|
||||
<PluginSetSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
@@ -261,12 +269,12 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<small class="text-grey">已选择的插件:</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap ga-2 mt-2">
|
||||
<v-chip
|
||||
v-for="plugin in (createSelectorModel(itemKey).value || [])"
|
||||
:key="plugin"
|
||||
size="small"
|
||||
label
|
||||
color="primary"
|
||||
<v-chip
|
||||
v-for="plugin in (createSelectorModel(itemKey).value || [])"
|
||||
:key="plugin"
|
||||
size="small"
|
||||
label
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ plugin === '*' ? '所有插件' : plugin }}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<span v-if="!modelValue || Object.keys(modelValue).length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
暂无项目
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="key in displayKeys" :key="key" size="x-small" label color="primary">
|
||||
{{ key.length > 20 ? key.slice(0, 20) + '...' : key }}
|
||||
</v-chip>
|
||||
<v-chip v-if="Object.keys(modelValue).length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
|
||||
+{{ Object.keys(modelValue).length - maxDisplayItems }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ buttonText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Key-Value Management Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
{{ dialogTitle }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
|
||||
<div v-if="localKeyValuePairs.length > 0">
|
||||
<div v-for="(pair, index) in localKeyValuePairs" :key="index" class="key-value-pair">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="pair.key"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="键名"
|
||||
@blur="updateKey(index, pair.key)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
|
||||
<v-text-field
|
||||
v-if="pair.type === 'string'"
|
||||
v-model="pair.value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="字符串值"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-else-if="pair.type === 'number'"
|
||||
v-model.number="pair.value"
|
||||
type="number"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="数值"
|
||||
></v-text-field>
|
||||
<v-switch
|
||||
v-else-if="pair.type === 'boolean'"
|
||||
v-model="pair.value"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="1" class="pl-2">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="removeKeyValuePair(index)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
|
||||
<p class="text-grey mt-4">暂无参数</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add new key-value pair section -->
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-text-field
|
||||
v-model="newKey"
|
||||
label="新键名"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-model="newValueType"
|
||||
:items="['string', 'number', 'boolean']"
|
||||
label="值类型"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 120px;"
|
||||
></v-select>
|
||||
<v-btn @click="addKeyValuePair" variant="tonal" color="primary">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
添加
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelDialog">取消</v-btn>
|
||||
<v-btn color="primary" @click="confirmDialog">确认</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '修改'
|
||||
},
|
||||
dialogTitle: {
|
||||
type: String,
|
||||
default: '修改键值对'
|
||||
},
|
||||
maxDisplayItems: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialog = ref(false)
|
||||
const localKeyValuePairs = ref([])
|
||||
const originalKeyValuePairs = ref([])
|
||||
const newKey = ref('')
|
||||
const newValueType = ref('string')
|
||||
|
||||
// 计算要显示的键名
|
||||
const displayKeys = computed(() => {
|
||||
return Object.keys(props.modelValue).slice(0, props.maxDisplayItems)
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化,主要用于初始化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
// This watch is primarily for initialization or external changes
|
||||
// The dialog-based editing handles internal updates
|
||||
}, { immediate: true })
|
||||
|
||||
function initializeLocalKeyValuePairs() {
|
||||
localKeyValuePairs.value = []
|
||||
for (const [key, value] of Object.entries(props.modelValue)) {
|
||||
localKeyValuePairs.value.push({
|
||||
key: key,
|
||||
value: value,
|
||||
type: typeof value // Store the original type
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
initializeLocalKeyValuePairs()
|
||||
originalKeyValuePairs.value = JSON.parse(JSON.stringify(localKeyValuePairs.value)) // Deep copy
|
||||
newKey.value = ''
|
||||
newValueType.value = 'string'
|
||||
dialog.value = true
|
||||
}
|
||||
|
||||
function addKeyValuePair() {
|
||||
const key = newKey.value.trim()
|
||||
if (key !== '') {
|
||||
const isKeyExists = localKeyValuePairs.value.some(pair => pair.key === key)
|
||||
if (isKeyExists) {
|
||||
alert('键名已存在')
|
||||
return
|
||||
}
|
||||
|
||||
let defaultValue
|
||||
switch (newValueType.value) {
|
||||
case 'number':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
default: // string
|
||||
defaultValue = ""
|
||||
break
|
||||
}
|
||||
|
||||
localKeyValuePairs.value.push({
|
||||
key: key,
|
||||
value: defaultValue,
|
||||
type: newValueType.value
|
||||
})
|
||||
newKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function removeKeyValuePair(index) {
|
||||
localKeyValuePairs.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function updateKey(index, newKey) {
|
||||
const originalKey = localKeyValuePairs.value[index].key
|
||||
// 如果键名没有改变,则不执行任何操作
|
||||
if (originalKey === newKey) return
|
||||
|
||||
// 检查新键名是否已存在
|
||||
const isKeyExists = localKeyValuePairs.value.some((pair, i) => i !== index && pair.key === newKey)
|
||||
if (isKeyExists) {
|
||||
// 如果键名已存在,提示用户并恢复原值
|
||||
alert('键名已存在')
|
||||
// 将键名恢复为修改前的原始值
|
||||
localKeyValuePairs.value[index].key = originalKey
|
||||
return
|
||||
}
|
||||
|
||||
// 更新本地副本
|
||||
localKeyValuePairs.value[index].key = newKey
|
||||
}
|
||||
|
||||
function confirmDialog() {
|
||||
const updatedValue = {}
|
||||
for (const pair of localKeyValuePairs.value) {
|
||||
let convertedValue = pair.value
|
||||
// 根据声明的类型进行转换
|
||||
switch (pair.type) {
|
||||
case 'number':
|
||||
// 尝试转换为数字,如果失败则保持原值(或设为默认值0)
|
||||
convertedValue = Number(pair.value)
|
||||
// 可选:检查是否为有效数字,无效则设为0或报错
|
||||
// if (isNaN(convertedValue)) convertedValue = 0;
|
||||
break
|
||||
case 'boolean':
|
||||
// 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换
|
||||
// 注意:在 JavaScript 中,只有严格的 false, 0, "", null, undefined, NaN 会被转换为 false
|
||||
// 这里直接赋值 pair.value 应该是安全的,因为 v-model 绑定的就是布尔值
|
||||
// convertedValue = Boolean(pair.value)
|
||||
break
|
||||
case 'string':
|
||||
default:
|
||||
// 默认转换为字符串
|
||||
convertedValue = String(pair.value)
|
||||
break
|
||||
}
|
||||
updatedValue[pair.key] = convertedValue
|
||||
}
|
||||
emit('update:modelValue', updatedValue)
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function cancelDialog() {
|
||||
// Reset to original state
|
||||
localKeyValuePairs.value = JSON.parse(JSON.stringify(originalKeyValuePairs.value))
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.key-value-pair {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user